Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 121 additions & 78 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ pub mod wrapper;
/// the request's `_meta` per-request capabilities or, for session-mode
/// peers, at `initialize` time), the spec requires `-32021` Missing
/// Required Client Capability with the required capability in `data`.
fn validate_tasks_capability<M: ConstString, H: ServerHandler>(
fn validate_tasks_capability<H: ServerHandler>(
handler: &H,
method: &str,
context: &RequestContext<RoleServer>,
) -> Result<(), McpError> {
if !handler.get_info().capabilities.supports_tasks() {
return Err(McpError::method_not_found::<M>());
return Err(McpError::new(
ErrorCode::METHOD_NOT_FOUND,
method.to_owned(),
None,
));
}
let client_declared = context
.client_capabilities()
Expand All @@ -47,6 +52,56 @@ fn validate_tasks_capability<M: ConstString, H: ServerHandler>(
}
}

/// Decides, in one place and before dispatch, whether `method` is served on
/// this lifecycle (SEP-2575) and, for the task methods, whether both sides
/// declared the tasks capability.
///
/// The 2026-07-28 lifecycle has no handshake, so `initialize`, `ping`,
/// `logging/setLevel`, `resources/subscribe` and `resources/unsubscribe` are
/// not found there; the legacy lifecycle has no `subscriptions/listen`. These
/// answer `-32601` whatever the params carried, so a removed method with
/// malformed params is still "not found" rather than "invalid params".
///
/// `initialize` is judged by `inline_session` rather than `legacy_request`:
/// its own params supply the fallback protocol version while the handshake
/// is still open, so only a session already on the inline lifecycle, or a
/// request that declares 2026-07-28 in its `_meta`, has no `initialize`.
fn validate_method_served<H: ServerHandler>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up for the rebase: main now gates skills/get / skills/list and resources/directory/read inside their match arms with validate_skills_capability. Those arms run after this pre-check. On a rebased branch, with a server that doesn't declare the skills extension (or directoryRead):

  • skills/get with malformed params → -32602 (expected -32601)
  • resources/directory/read with malformed params → -32602 (expected -32601)

Moving the skills and directoryRead checks in here, next to the tasks check, gives -32601 for both, and regression tests for both pass with it. skills/list isn't affected: a bad cursor object still gets -32601.

Every capability-gated method added later will need the same treatment. A table-driven test would catch the next one: for each gated method, turn its capability off, send malformed params, and assert -32601.

handler: &H,
method: &str,
legacy_request: bool,
inline_session: bool,
context: &RequestContext<RoleServer>,
) -> Result<(), McpError> {
let not_served: &[&str] = if legacy_request {
&[SubscriptionsListenRequestMethod::VALUE]
} else {
&[
PingRequestMethod::VALUE,
SetLevelRequestMethod::VALUE,
SubscribeRequestMethod::VALUE,
UnsubscribeRequestMethod::VALUE,
]
};
if not_served.contains(&method) || (inline_session && method == InitializeResultMethod::VALUE) {
return Err(McpError::new(
ErrorCode::METHOD_NOT_FOUND,
method.to_owned(),
None,
));
}
if [
GetTaskMethod::VALUE,
UpdateTaskMethod::VALUE,
CancelTaskMethod::VALUE,
]
.contains(&method)
{
validate_tasks_capability(handler, method, context)?;
}
Ok(())
}

impl<H: ServerHandler> Service<RoleServer> for H {
async fn handle_request(
&self,
Expand Down Expand Up @@ -100,6 +155,17 @@ impl<H: ServerHandler> Service<RoleServer> for H {
}
let legacy_request =
uses_legacy_lifecycle(protocol_version.as_ref(), requires_request_metadata);
let inline_session = context.peer.request_metadata_required()
|| requested_version
.as_ref()
.is_some_and(|version| version.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
validate_method_served(
self,
request.method(),
legacy_request,
inline_session,
&context,
)?;
let result = match request {
ClientRequest::InitializeRequest(request) => self
.initialize(request.params, context)
Expand All @@ -110,11 +176,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.await
.map(ServerResult::DiscoverResult),
ClientRequest::PingRequest(_request) => {
if !legacy_request {
Err(McpError::method_not_found::<PingRequestMethod>())
} else {
self.ping(context).await.map(ServerResult::empty)
}
self.ping(context).await.map(ServerResult::empty)
}
ClientRequest::CompleteRequest(request) => self
.complete(request.params, context)
Expand Down Expand Up @@ -145,61 +207,45 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.await
.map(ServerResult::from),
ClientRequest::SubscriptionsListenRequest(request) => {
if legacy_request {
Err(McpError::method_not_found::<SubscriptionsListenRequestMethod>())
} else {
let requested = request.params.notifications;
let Some(candidate) = self.accepted_subscription_filter(&requested) else {
return Err(
McpError::method_not_found::<SubscriptionsListenRequestMethod>(),
);
};
let server_info = self.get_info();
let advertised = requested.supported_by(&server_info.capabilities);
let handler_accepted = requested.intersection(&candidate);
let accepted = handler_accepted.intersection(&advertised);
if accepted != handler_accepted {
tracing::debug!(
requested_resource_count = requested
.resource_subscriptions
.as_ref()
.map_or(0, Vec::len),
accepted_resource_count =
accepted.resource_subscriptions.as_ref().map_or(0, Vec::len),
"subscription filter reduced to advertised server capabilities"
);
}
let server_implementation = server_info.server_info;
let subscription_id = context.id.clone();
let subscription =
SubscriptionContext::establish(context, requested, accepted).await?;
// The 2026-07-28 schema defines a final result for graceful
// server teardown; explicit stdio cancellation remains a notification.
self.listen(subscription).await.map(|()| {
let mut result = SubscriptionsListenResult::complete(subscription_id);
result.meta.set_server_info(server_implementation);
ServerResult::SubscriptionsListenResult(result)
})
let requested = request.params.notifications;
let Some(candidate) = self.accepted_subscription_filter(&requested) else {
return Err(McpError::method_not_found::<SubscriptionsListenRequestMethod>());
};
let server_info = self.get_info();
let advertised = requested.supported_by(&server_info.capabilities);
let handler_accepted = requested.intersection(&candidate);
let accepted = handler_accepted.intersection(&advertised);
if accepted != handler_accepted {
tracing::debug!(
requested_resource_count = requested
.resource_subscriptions
.as_ref()
.map_or(0, Vec::len),
accepted_resource_count =
accepted.resource_subscriptions.as_ref().map_or(0, Vec::len),
"subscription filter reduced to advertised server capabilities"
);
}
}
ClientRequest::SubscribeRequest(request) => {
if !legacy_request {
Err(McpError::method_not_found::<SubscribeRequestMethod>())
} else {
self.subscribe(request.params, context)
.await
.map(ServerResult::empty)
}
}
ClientRequest::UnsubscribeRequest(request) => {
if !legacy_request {
Err(McpError::method_not_found::<UnsubscribeRequestMethod>())
} else {
self.unsubscribe(request.params, context)
.await
.map(ServerResult::empty)
}
}
let server_implementation = server_info.server_info;
let subscription_id = context.id.clone();
let subscription =
SubscriptionContext::establish(context, requested, accepted).await?;
// The 2026-07-28 schema defines a final result for graceful
// server teardown; explicit stdio cancellation remains a notification.
self.listen(subscription).await.map(|()| {
let mut result = SubscriptionsListenResult::complete(subscription_id);
result.meta.set_server_info(server_implementation);
ServerResult::SubscriptionsListenResult(result)
})
}
ClientRequest::SubscribeRequest(request) => self
.subscribe(request.params, context)
.await
.map(ServerResult::empty),
ClientRequest::UnsubscribeRequest(request) => self
.unsubscribe(request.params, context)
.await
.map(ServerResult::empty),
ClientRequest::CallToolRequest(request) => {
let client_declared_tasks = context
.client_capabilities()
Expand All @@ -224,24 +270,21 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.on_custom_request(request, context)
.await
.map(ServerResult::CustomResult),
ClientRequest::GetTaskRequest(request) => {
validate_tasks_capability::<GetTaskMethod, _>(self, &context)?;
self.get_task(request.params, context)
.await
.map(ServerResult::GetTaskResult)
}
ClientRequest::UpdateTaskRequest(request) => {
validate_tasks_capability::<UpdateTaskMethod, _>(self, &context)?;
self.update_task(request.params, context)
.await
.map(ServerResult::task_ack)
}
ClientRequest::CancelTaskRequest(request) => {
validate_tasks_capability::<CancelTaskMethod, _>(self, &context)?;
self.cancel_task(request.params, context)
.await
.map(ServerResult::task_ack)
ClientRequest::InvalidParams(request) => {
Err(McpError::invalid_params(request.to_string(), None))
}
ClientRequest::GetTaskRequest(request) => self
.get_task(request.params, context)
.await
.map(ServerResult::GetTaskResult),
ClientRequest::UpdateTaskRequest(request) => self
.update_task(request.params, context)
.await
.map(ServerResult::task_ack),
ClientRequest::CancelTaskRequest(request) => self
.cancel_task(request.params, context)
.await
.map(ServerResult::task_ack),
};
let result = result.and_then(|mut result| {
if matches!(result, ServerResult::InputRequiredResult(_)) && !is_2026_07_28_or_later {
Expand Down
Loading
Loading