fix: resolve clippy warnings across workspace (#1195)
* fix: resolve clippy warnings across workspace * ci: also run clippy on rmcp with all features except local --all-features enables the local feature, which cfg-gates out tower.rs and most of the test suite, so the existing clippy step never actually lints them.
This commit is contained in:
parent
f9a68bfa5c
commit
6fa40536de
9 changed files with 277 additions and 289 deletions
|
|
@ -55,6 +55,24 @@ use crate::{
|
|||
pub(crate) const DEFAULT_MAX_REQUEST_BODY_BYTES: usize = 4 * 1024 * 1024;
|
||||
const STATELESS_STREAM_CHANNEL_CAPACITY: usize = 16;
|
||||
|
||||
struct ErrorResponse(Box<BoxResponse>);
|
||||
|
||||
impl ErrorResponse {
|
||||
fn into_response(self) -> BoxResponse {
|
||||
*self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BoxResponse> for ErrorResponse {
|
||||
fn from(response: BoxResponse) -> Self {
|
||||
Self(Box::new(response))
|
||||
}
|
||||
}
|
||||
|
||||
type HttpResult<T> = Result<T, ErrorResponse>;
|
||||
type RestoreResultSender = tokio::sync::watch::Sender<Option<bool>>;
|
||||
type PendingRestores = Arc<tokio::sync::RwLock<HashMap<SessionId, RestoreResultSender>>>;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StreamableHttpServerConfig {
|
||||
|
|
@ -247,10 +265,6 @@ impl StreamableHttpServerConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
/// Validates the `MCP-Protocol-Version` header on incoming HTTP requests.
|
||||
///
|
||||
/// Per the MCP 2025-06-18 spec:
|
||||
|
|
@ -259,7 +273,7 @@ impl StreamableHttpServerConfig {
|
|||
fn validate_protocol_version_header(
|
||||
headers: &http::HeaderMap,
|
||||
allow_unknown: bool,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
if let Some(value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) {
|
||||
let version_str = value.to_str().map_err(|_| {
|
||||
Response::builder()
|
||||
|
|
@ -284,7 +298,8 @@ fn validate_protocol_version_header(
|
|||
)))
|
||||
.boxed(),
|
||||
)
|
||||
.expect("valid response"));
|
||||
.expect("valid response")
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -349,28 +364,24 @@ impl<S: Service<RoleServer>> Service<RoleServer> for NegotiatingStatelessHttpSer
|
|||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
// SEP-2567: sessions are removed from the discover lifecycle. Validate
|
||||
// protocol-version consistency, then classify the request with the shared
|
||||
// lifecycle helper.
|
||||
fn is_legacy_request(
|
||||
message: Option<&ClientJsonRpcMessage>,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<bool, BoxResponse> {
|
||||
) -> HttpResult<bool> {
|
||||
let has_per_request_version = message.is_some_and(message_has_per_request_protocol_version);
|
||||
validate_protocol_version_header(headers, has_per_request_version)?;
|
||||
if let Some(message) = message {
|
||||
if let ClientJsonRpcMessage::Request(req) = message {
|
||||
if let ClientRequest::InitializeRequest(init) = &req.request {
|
||||
validate_header_matches_init_body(
|
||||
headers,
|
||||
init.params.protocol_version.as_str(),
|
||||
Some(req.id.clone()),
|
||||
)?;
|
||||
}
|
||||
if let ClientJsonRpcMessage::Request(req) = message
|
||||
&& let ClientRequest::InitializeRequest(init) = &req.request
|
||||
{
|
||||
validate_header_matches_init_body(
|
||||
headers,
|
||||
init.params.protocol_version.as_str(),
|
||||
Some(req.id.clone()),
|
||||
)?;
|
||||
}
|
||||
validate_request_protocol_version_meta(headers, message)?;
|
||||
}
|
||||
|
|
@ -430,10 +441,10 @@ async fn persist_and_forward_event(
|
|||
output: &mut Option<tokio::sync::mpsc::Sender<ServerSseMessage>>,
|
||||
) -> Result<(), EventStoreError> {
|
||||
event.event_id = Some(event_store.store_event(stream_id, &event).await?);
|
||||
if let Some(sender) = output {
|
||||
if sender.send(event).await.is_err() {
|
||||
*output = None;
|
||||
}
|
||||
if let Some(sender) = output
|
||||
&& sender.send(event).await.is_err()
|
||||
{
|
||||
*output = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -464,16 +475,12 @@ fn invalid_params_jsonrpc_response(
|
|||
.expect("valid response")
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
/// Absent header is allowed; the first initialize round-trip may legitimately omit it.
|
||||
fn validate_header_matches_init_body(
|
||||
headers: &http::HeaderMap,
|
||||
body_version: &str,
|
||||
request_id: Option<RequestId>,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
let Some(header_value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -494,19 +501,16 @@ fn validate_header_matches_init_body(
|
|||
format!(
|
||||
"Invalid Request: MCP-Protocol-Version header ({header_str}) does not match initialize params.protocolVersion ({body_version})"
|
||||
),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
fn validate_request_protocol_version_meta(
|
||||
headers: &HeaderMap,
|
||||
message: &ClientJsonRpcMessage,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
let ClientJsonRpcMessage::Request(request) = message else {
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -530,7 +534,8 @@ fn validate_request_protocol_version_meta(
|
|||
"Invalid params: request _meta is missing or has malformed required fields: {}",
|
||||
missing.join(", ")
|
||||
),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -538,7 +543,8 @@ fn validate_request_protocol_version_meta(
|
|||
return Err(header_mismatch_jsonrpc_response(
|
||||
Some(request.id.clone()),
|
||||
"request _meta protocolVersion requires MCP-Protocol-Version header",
|
||||
));
|
||||
)
|
||||
.into());
|
||||
};
|
||||
if header_version != meta_version.as_str() {
|
||||
return Err(header_mismatch_jsonrpc_response(
|
||||
|
|
@ -546,7 +552,8 @@ fn validate_request_protocol_version_meta(
|
|||
format!(
|
||||
"MCP-Protocol-Version header ({header_version}) does not match request _meta protocolVersion ({meta_version})"
|
||||
),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -557,15 +564,11 @@ fn validate_request_protocol_version_meta(
|
|||
/// HTTP 400 / JSON-RPC `-32020` before handler dispatch. `server/discover`
|
||||
/// is included so the seam aligns with the per-POST header contract; its
|
||||
/// body-metadata rule is preserved unchanged.
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
fn validate_required_protocol_header(
|
||||
config: &StreamableHttpServerConfig,
|
||||
headers: &HeaderMap,
|
||||
message: &ClientJsonRpcMessage,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
if !config.stateless_protocol_metadata_required {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -583,7 +586,8 @@ fn validate_required_protocol_header(
|
|||
Err(header_mismatch_jsonrpc_response(
|
||||
Some(request.id.clone()),
|
||||
"Missing MCP-Protocol-Version header for request requiring per-request protocol metadata",
|
||||
))
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
/// When `stateless_protocol_metadata_required` is enabled in stateless mode,
|
||||
|
|
@ -593,14 +597,10 @@ fn validate_required_protocol_header(
|
|||
/// `server/discover` (whose body-metadata rule is already enforced by
|
||||
/// `validate_request_protocol_version_meta`), notifications, and other message
|
||||
/// kinds are exempt.
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
fn validate_required_protocol_meta(
|
||||
config: &StreamableHttpServerConfig,
|
||||
message: &ClientJsonRpcMessage,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
if !config.stateless_protocol_metadata_required {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -619,7 +619,8 @@ fn validate_required_protocol_meta(
|
|||
Err(invalid_params_jsonrpc_response(
|
||||
Some(request.id.clone()),
|
||||
"Invalid params: request requires protocolVersion in request _meta",
|
||||
))
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
fn jsonrpc_http_status(message: &ServerJsonRpcMessage) -> http::StatusCode {
|
||||
|
|
@ -640,7 +641,7 @@ fn jsonrpc_http_status(message: &ServerJsonRpcMessage) -> http::StatusCode {
|
|||
fn jsonrpc_message_response(
|
||||
message: ServerJsonRpcMessage,
|
||||
map_protocol_status: bool,
|
||||
) -> Result<BoxResponse, BoxResponse> {
|
||||
) -> HttpResult<BoxResponse> {
|
||||
let status = if map_protocol_status {
|
||||
jsonrpc_http_status(&message)
|
||||
} else {
|
||||
|
|
@ -674,15 +675,11 @@ fn header_mismatch_jsonrpc_response(
|
|||
/// The `initialize` handshake is exempt: clients emit these headers only after the
|
||||
/// version has been negotiated. `tool_schema` supplies the called tool's input schema
|
||||
/// so annotated `Mcp-Param-*` headers can be checked (no schema => those are skipped).
|
||||
#[expect(
|
||||
clippy::result_large_err,
|
||||
reason = "BoxResponse is intentionally large; matches other handlers in this file"
|
||||
)]
|
||||
fn validate_standard_headers(
|
||||
headers: &HeaderMap,
|
||||
message: &ClientJsonRpcMessage,
|
||||
tool_schema: impl Fn(&str) -> Option<Arc<JsonObject>>,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
let version_requires_headers = headers
|
||||
.get(HEADER_MCP_PROTOCOL_VERSION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
|
|
@ -715,7 +712,7 @@ fn validate_standard_headers(
|
|||
.and_then(|name| name.as_str())
|
||||
.and_then(tool_schema);
|
||||
if let Err(reason) = mcp_headers::validate_request_headers(headers, &value, schema.as_deref()) {
|
||||
return Err(header_mismatch_jsonrpc_response(request_id, reason));
|
||||
return Err(header_mismatch_jsonrpc_response(request_id, reason).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -839,10 +836,7 @@ fn bad_request_response(message: &str) -> BoxResponse {
|
|||
.expect("failed to build bad request response")
|
||||
}
|
||||
|
||||
fn parse_host_header(
|
||||
uri: &http::Uri,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<NormalizedAuthority, BoxResponse> {
|
||||
fn parse_host_header(uri: &http::Uri, headers: &HeaderMap) -> HttpResult<NormalizedAuthority> {
|
||||
if let Some(host) = headers.get(http::header::HOST) {
|
||||
let host_str = host
|
||||
.to_str()
|
||||
|
|
@ -873,23 +867,20 @@ fn validate_dns_rebinding_headers(
|
|||
uri: &http::Uri,
|
||||
headers: &HeaderMap,
|
||||
config: &StreamableHttpServerConfig,
|
||||
) -> Result<(), BoxResponse> {
|
||||
) -> HttpResult<()> {
|
||||
let host = parse_host_header(uri, headers)?;
|
||||
if !host_is_allowed(&host, &config.allowed_hosts) {
|
||||
tracing::warn!(
|
||||
host = ?host,
|
||||
"rejected request with disallowed Host header (possible DNS rebinding attempt)",
|
||||
);
|
||||
return Err(forbidden_response("Forbidden: Host header is not allowed"));
|
||||
return Err(forbidden_response("Forbidden: Host header is not allowed").into());
|
||||
}
|
||||
validate_origin_header(headers, &config.allowed_origins)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_origin_header(
|
||||
headers: &HeaderMap,
|
||||
allowed_origins: &[String],
|
||||
) -> Result<(), BoxResponse> {
|
||||
fn validate_origin_header(headers: &HeaderMap, allowed_origins: &[String]) -> HttpResult<()> {
|
||||
if allowed_origins.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -914,9 +905,7 @@ fn validate_origin_header(
|
|||
origin = ?origin,
|
||||
"rejected request with disallowed Origin header (possible cross-origin attack)",
|
||||
);
|
||||
return Err(forbidden_response(
|
||||
"Forbidden: Origin header is not allowed",
|
||||
));
|
||||
return Err(forbidden_response("Forbidden: Origin header is not allowed").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1012,9 +1001,7 @@ pub struct StreamableHttpService<S, M> {
|
|||
/// same unknown session ID wait for the first restore to complete rather
|
||||
/// than racing to replay the initialize handshake. `None` when no external
|
||||
/// session store is configured (avoids allocating the map).
|
||||
pending_restores: Option<
|
||||
Arc<tokio::sync::RwLock<HashMap<SessionId, tokio::sync::watch::Sender<Option<bool>>>>>,
|
||||
>,
|
||||
pending_restores: Option<PendingRestores>,
|
||||
/// Caches tool input schemas by name for SEP-2243 `Mcp-Param-*` validation.
|
||||
/// Populated lazily via `get_tool` so the service factory runs at most once
|
||||
/// per tool name. `None` value means the tool exposes no schema.
|
||||
|
|
@ -1067,10 +1054,9 @@ where
|
|||
/// `result` defaults to `false` (failure / cancellation). Only the success path
|
||||
/// needs to set it to `true` before returning.
|
||||
struct PendingRestoreGuard {
|
||||
pending_restores:
|
||||
Arc<tokio::sync::RwLock<HashMap<SessionId, tokio::sync::watch::Sender<Option<bool>>>>>,
|
||||
pending_restores: PendingRestores,
|
||||
session_id: SessionId,
|
||||
watch_tx: tokio::sync::watch::Sender<Option<bool>>,
|
||||
watch_tx: RestoreResultSender,
|
||||
/// The value that will be broadcast to waiting tasks on drop.
|
||||
result: bool,
|
||||
}
|
||||
|
|
@ -1098,12 +1084,10 @@ where
|
|||
session_manager: Arc<M>,
|
||||
config: StreamableHttpServerConfig,
|
||||
) -> Self {
|
||||
let pending_restores = config.session_store.is_some().then(|| {
|
||||
Arc::new(tokio::sync::RwLock::new(HashMap::<
|
||||
SessionId,
|
||||
tokio::sync::watch::Sender<Option<bool>>,
|
||||
>::new()))
|
||||
});
|
||||
let pending_restores = config
|
||||
.session_store
|
||||
.is_some()
|
||||
.then(|| Arc::new(tokio::sync::RwLock::new(HashMap::new())));
|
||||
Self {
|
||||
config,
|
||||
session_manager,
|
||||
|
|
@ -1130,19 +1114,18 @@ where
|
|||
|
||||
tokio::spawn(async move {
|
||||
let mut sender = Some(sender);
|
||||
if let Some(retry) = retry {
|
||||
if let Err(error) = persist_and_forward_event(
|
||||
if let Some(retry) = retry
|
||||
&& let Err(error) = persist_and_forward_event(
|
||||
event_store.as_ref(),
|
||||
&stream_id,
|
||||
ServerSseMessage::retry(retry),
|
||||
&mut sender,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%stream_id, %error, "failed to persist SSE priming event");
|
||||
request_ct.cancel();
|
||||
return;
|
||||
}
|
||||
{
|
||||
tracing::error!(%stream_id, %error, "failed to persist SSE priming event");
|
||||
request_ct.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut first = first;
|
||||
|
|
@ -1214,7 +1197,7 @@ where
|
|||
service: S,
|
||||
mut request: crate::model::JsonRpcRequest<ClientRequest>,
|
||||
parts: http::request::Parts,
|
||||
) -> Result<BoxResponse, BoxResponse> {
|
||||
) -> HttpResult<BoxResponse> {
|
||||
let peer_info = Self::peer_info_for_stateless_request(&request, &parts.headers);
|
||||
request.request.extensions_mut().insert(parts);
|
||||
let (transport, mut receiver) =
|
||||
|
|
@ -1279,10 +1262,10 @@ where
|
|||
/// per name to read its `ServerHandler::get_tool` definition. Used to
|
||||
/// validate SEP-2243 `Mcp-Param-*` headers against the request body.
|
||||
fn tool_schema(&self, name: &str) -> Option<Arc<JsonObject>> {
|
||||
if let Ok(cache) = self.tool_schemas.read() {
|
||||
if let Some(schema) = cache.get(name) {
|
||||
return schema.clone();
|
||||
}
|
||||
if let Ok(cache) = self.tool_schemas.read()
|
||||
&& let Some(schema) = cache.get(name)
|
||||
{
|
||||
return schema.clone();
|
||||
}
|
||||
let schema = self
|
||||
.get_service()
|
||||
|
|
@ -1472,23 +1455,15 @@ where
|
|||
Some(init_done_tx),
|
||||
);
|
||||
|
||||
if let Err(e) = self
|
||||
.session_manager
|
||||
self.session_manager
|
||||
.initialize_session(session_id, restore_init)
|
||||
.await
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
|
||||
if let Err(e) = self
|
||||
.session_manager
|
||||
self.session_manager
|
||||
.accept_message(session_id, restore_initialized)
|
||||
.await
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
|
||||
if init_done_rx.await.is_err() {
|
||||
return Err(std::io::Error::other(
|
||||
|
|
@ -1513,7 +1488,7 @@ where
|
|||
if let Err(response) =
|
||||
validate_dns_rebinding_headers(request.uri(), request.headers(), &self.config)
|
||||
{
|
||||
return response;
|
||||
return response.into_response();
|
||||
}
|
||||
let method = request.method().clone();
|
||||
let supports_stateless_replay = self.session_manager.event_store().is_some();
|
||||
|
|
@ -1540,10 +1515,10 @@ where
|
|||
};
|
||||
match result {
|
||||
Ok(response) => response,
|
||||
Err(response) => response,
|
||||
Err(response) => response.into_response(),
|
||||
}
|
||||
}
|
||||
async fn handle_get<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>
|
||||
async fn handle_get<B>(&self, request: Request<B>) -> HttpResult<BoxResponse>
|
||||
where
|
||||
B: Body + Send + 'static,
|
||||
B::Error: Display,
|
||||
|
|
@ -1686,7 +1661,7 @@ where
|
|||
))
|
||||
}
|
||||
|
||||
async fn handle_post<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>
|
||||
async fn handle_post<B>(&self, request: Request<B>) -> HttpResult<BoxResponse>
|
||||
where
|
||||
B: Body + Send + 'static,
|
||||
B::Error: Display,
|
||||
|
|
@ -1838,7 +1813,7 @@ where
|
|||
let stored_init_params = match &mut message {
|
||||
ClientJsonRpcMessage::Request(req) => {
|
||||
let ClientRequest::InitializeRequest(init_req) = &req.request else {
|
||||
return Err(unexpected_message_response("initialize request"));
|
||||
return Err(unexpected_message_response("initialize request").into());
|
||||
};
|
||||
// Reject mismatched MCP-Protocol-Version header before binding the session to anything.
|
||||
validate_header_matches_init_body(
|
||||
|
|
@ -1856,7 +1831,7 @@ where
|
|||
stored_init_params
|
||||
}
|
||||
_ => {
|
||||
return Err(unexpected_message_response("initialize request"));
|
||||
return Err(unexpected_message_response("initialize request").into());
|
||||
}
|
||||
};
|
||||
let service = self
|
||||
|
|
@ -2013,7 +1988,8 @@ where
|
|||
std::io::ErrorKind::UnexpectedEof,
|
||||
"no response message received from handler",
|
||||
),
|
||||
));
|
||||
)
|
||||
.into());
|
||||
};
|
||||
tracing::trace!(?message);
|
||||
if matches!(
|
||||
|
|
@ -2045,7 +2021,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
async fn handle_delete<B>(&self, request: Request<B>) -> Result<BoxResponse, BoxResponse>
|
||||
async fn handle_delete<B>(&self, request: Request<B>) -> HttpResult<BoxResponse>
|
||||
where
|
||||
B: Body + Send + 'static,
|
||||
B::Error: Display,
|
||||
|
|
|
|||
|
|
@ -380,10 +380,10 @@ async fn test_mcp_custom_headers_sent_to_server() -> anyhow::Result<()> {
|
|||
let mut headers_map = HashMap::new();
|
||||
for (name, value) in headers.iter() {
|
||||
let name_str = name.as_str();
|
||||
if name_str.starts_with("x-") {
|
||||
if let Ok(v) = value.to_str() {
|
||||
headers_map.insert(name_str.to_string(), v.to_string());
|
||||
}
|
||||
if name_str.starts_with("x-")
|
||||
&& let Ok(v) = value.to_str()
|
||||
{
|
||||
headers_map.insert(name_str.to_string(), v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -392,48 +392,48 @@ async fn test_mcp_custom_headers_sent_to_server() -> anyhow::Result<()> {
|
|||
stored.extend(headers_map);
|
||||
|
||||
// Parse the MCP request
|
||||
if let Ok(json_body) = serde_json::from_slice::<serde_json::Value>(&body) {
|
||||
if let Some(method) = json_body.get("method").and_then(|m| m.as_str()) {
|
||||
if method == "initialize" {
|
||||
state.initialize_called.notify_one();
|
||||
// Return a valid MCP initialize response with session header
|
||||
let response = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": json_body.get("id"),
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"serverInfo": {
|
||||
"name": "test-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
if let Ok(json_body) = serde_json::from_slice::<serde_json::Value>(&body)
|
||||
&& let Some(method) = json_body.get("method").and_then(|m| m.as_str())
|
||||
{
|
||||
if method == "initialize" {
|
||||
state.initialize_called.notify_one();
|
||||
// Return a valid MCP initialize response with session header
|
||||
let response = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": json_body.get("id"),
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"serverInfo": {
|
||||
"name": "test-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
});
|
||||
return (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"test-session-123",
|
||||
),
|
||||
],
|
||||
response.to_string(),
|
||||
);
|
||||
} else if method == "notifications/initialized" {
|
||||
// For initialized notification, return 202 Accepted
|
||||
return (
|
||||
StatusCode::ACCEPTED,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"test-session-123",
|
||||
),
|
||||
],
|
||||
String::new(),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"test-session-123",
|
||||
),
|
||||
],
|
||||
response.to_string(),
|
||||
);
|
||||
} else if method == "notifications/initialized" {
|
||||
// For initialized notification, return 202 Accepted
|
||||
return (
|
||||
StatusCode::ACCEPTED,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"test-session-123",
|
||||
),
|
||||
],
|
||||
String::new(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use rmcp::{
|
||||
ClientHandler, RoleServer, ServerHandler, ServiceExt,
|
||||
ClientHandler, ServerHandler, ServiceExt,
|
||||
handler::server::{router::prompt::PromptRouter, wrapper::Parameters},
|
||||
model::{
|
||||
ClientInfo, ContentBlock, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
|
||||
PaginatedRequestParams, PromptMessage, Role,
|
||||
ClientInfo, ContentBlock, GetPromptRequestParams, GetPromptResult, PromptMessage, Role,
|
||||
},
|
||||
prompt, prompt_handler, prompt_router,
|
||||
service::RequestContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
|
|||
|
|
@ -20,12 +20,6 @@ struct Request {
|
|||
fields: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||
struct Sum {
|
||||
a: i32,
|
||||
b: i32,
|
||||
}
|
||||
|
||||
#[rmcp::prompt_router(router = "test_router")]
|
||||
impl<T> TestHandler<T> {
|
||||
#[rmcp::prompt]
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ fn test_tool_result_content_requires_content() {
|
|||
#[case::array(serde_json::json!([{ "city": "SF", "temp": 72 }, { "city": "NY", "temp": 65 }]))]
|
||||
#[case::string(serde_json::json!("sunny"))]
|
||||
#[case::integer(serde_json::json!(42))]
|
||||
#[case::float(serde_json::json!(3.14))]
|
||||
#[case::float(serde_json::json!(3.5))]
|
||||
#[case::boolean(serde_json::json!(true))]
|
||||
fn tool_result_content_round_trips_non_object_structured_content(
|
||||
#[case] structured: serde_json::Value,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#![cfg(all(feature = "server", feature = "client", not(feature = "local")))]
|
||||
#![allow(deprecated)]
|
||||
#![expect(
|
||||
deprecated,
|
||||
reason = "This test verifies request association for the deprecated sampling API"
|
||||
)]
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
|
@ -18,9 +21,11 @@ use tokio::{
|
|||
sync::oneshot,
|
||||
};
|
||||
|
||||
type RequestResultSender = oneshot::Sender<Result<(), ServiceError>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SamplingServer {
|
||||
outside: Arc<Mutex<Option<oneshot::Sender<Result<(), ServiceError>>>>>,
|
||||
outside: Arc<Mutex<Option<RequestResultSender>>>,
|
||||
}
|
||||
|
||||
impl ServerHandler for SamplingServer {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ use rmcp::{
|
|||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, rmcp::schemars::JsonSchema)]
|
||||
pub struct SumArgs {
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
struct SumArgs {
|
||||
a: i32,
|
||||
b: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
|||
|
|
@ -21,12 +21,6 @@ struct Request {
|
|||
fields: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||
struct Sum {
|
||||
a: i32,
|
||||
b: i32,
|
||||
}
|
||||
|
||||
#[rmcp::tool_router(router = test_router_1)]
|
||||
impl<T> TestHandler<T> {
|
||||
#[rmcp::tool]
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ async fn mcp_handler(
|
|||
let mut headers_map = HashMap::new();
|
||||
for (name, value) in headers.iter() {
|
||||
let name_str = name.as_str();
|
||||
if name_str.starts_with("x-") || name_str == "host" {
|
||||
if let Ok(v) = value.to_str() {
|
||||
headers_map.insert(name_str.to_string(), v.to_string());
|
||||
}
|
||||
if (name_str.starts_with("x-") || name_str == "host")
|
||||
&& let Ok(v) = value.to_str()
|
||||
{
|
||||
headers_map.insert(name_str.to_string(), v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,46 +46,46 @@ async fn mcp_handler(
|
|||
stored.extend(headers_map);
|
||||
drop(stored);
|
||||
|
||||
if let Ok(json_body) = serde_json::from_slice::<serde_json::Value>(&body) {
|
||||
if let Some(method) = json_body.get("method").and_then(|m| m.as_str()) {
|
||||
if method == "initialize" {
|
||||
state.initialize_called.notify_one();
|
||||
let response = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": json_body.get("id"),
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"serverInfo": {
|
||||
"name": "test-unix-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
if let Ok(json_body) = serde_json::from_slice::<serde_json::Value>(&body)
|
||||
&& let Some(method) = json_body.get("method").and_then(|m| m.as_str())
|
||||
{
|
||||
if method == "initialize" {
|
||||
state.initialize_called.notify_one();
|
||||
let response = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": json_body.get("id"),
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"serverInfo": {
|
||||
"name": "test-unix-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
});
|
||||
return (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"unix-test-session",
|
||||
),
|
||||
],
|
||||
response.to_string(),
|
||||
);
|
||||
} else if method == "notifications/initialized" {
|
||||
return (
|
||||
StatusCode::ACCEPTED,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"unix-test-session",
|
||||
),
|
||||
],
|
||||
String::new(),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"unix-test-session",
|
||||
),
|
||||
],
|
||||
response.to_string(),
|
||||
);
|
||||
} else if method == "notifications/initialized" {
|
||||
return (
|
||||
StatusCode::ACCEPTED,
|
||||
[
|
||||
(http::header::CONTENT_TYPE, "application/json"),
|
||||
(
|
||||
http::HeaderName::from_static("mcp-session-id"),
|
||||
"unix-test-session",
|
||||
),
|
||||
],
|
||||
String::new(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue