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
1 change: 1 addition & 0 deletions crates/rmcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ rustdoc-args = ["--cfg", "docsrs"]
async-trait = { version = "0.1.89", optional = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde_json_canonicalizer = "0.3.2"
thiserror = "2"
tokio = { version = "1", features = ["sync", "macros", "rt", "time"] }
futures = "0.3"
Expand Down
38 changes: 32 additions & 6 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,17 @@ impl<H: ServerHandler> Service<RoleServer> for H {
Err(McpError::method_not_found::<SubscriptionsListenRequestMethod>())
} else {
let requested = request.params.notifications;
let Some(candidate) = self.accepted_subscription_filter(&requested) else {
let Some(candidate) = self
.accept_subscription_filter(requested.clone(), context.clone())
.await?
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);
let accepted = handler_accepted.supported_by(&server_info.capabilities);
if accepted != handler_accepted {
tracing::debug!(
requested_resource_count = requested
Expand Down Expand Up @@ -405,15 +407,31 @@ macro_rules! server_handler_methods {
/// Return the subset of a requested notification filter this server accepts.
///
/// Returning `None` leaves `subscriptions/listen` unimplemented. The SDK
/// intersects the returned filter with both `requested` and the notification
/// capabilities advertised by [`Self::get_info`] before acknowledging it.
/// Categories that were not requested or advertised are always removed.
/// intersects the returned filter with `requested`, then filters the core
/// notification categories against capabilities advertised by [`Self::get_info`].
/// Extension-owned fields returned by this handler are preserved when the
/// client requested the same field because the SDK does not own their schema.
fn accepted_subscription_filter(
&self,
requested: &SubscriptionFilter,
) -> Option<SubscriptionFilter> {
None
}
/// Asynchronously accept a requested notification filter with full request context.
///
/// Gateways and other policy-aware servers can override this hook when subscription
/// authorization depends on request-scoped client capabilities or other contextual
/// metadata. The default preserves source compatibility by delegating to
/// [`Self::accepted_subscription_filter`]. Returning an error rejects the request
/// before the acknowledgement notification is emitted.
fn accept_subscription_filter(
&self,
requested: SubscriptionFilter,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<Option<SubscriptionFilter>, McpError>> + MaybeSendFuture + '_ {
let accepted = self.accepted_subscription_filter(&requested);
std::future::ready(Ok(accepted))
}
/// Run one established subscription until it is cancelled or closed gracefully.
///
/// The SDK sends the acknowledgment before invoking this method. Returning
Expand Down Expand Up @@ -693,6 +711,14 @@ macro_rules! impl_server_handler_for_wrapper {
(**self).accepted_subscription_filter(requested)
}

fn accept_subscription_filter(
&self,
requested: SubscriptionFilter,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<Option<SubscriptionFilter>, McpError>> + MaybeSendFuture + '_ {
(**self).accept_subscription_filter(requested, context)
}

fn listen(
&self,
context: SubscriptionContext,
Expand Down
21 changes: 20 additions & 1 deletion crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,13 @@ pub struct SubscriptionFilter {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(with = "Vec<String>"))]
pub resource_subscriptions: Option<Vec<String>>,
/// Extension-owned subscription filter entries not modeled by the core SDK.
///
/// The server handler remains responsible for validating and accepting
/// these values. Core capability filtering preserves handler-accepted
/// extension fields instead of interpreting their schemas.
#[serde(flatten)]
pub additional_fields: JsonObject,
Comment on lines 1927 to +1936
}

impl SubscriptionFilter {
Expand Down Expand Up @@ -1955,6 +1962,12 @@ impl SubscriptionFilter {
})
})
.filter(|uris: &Vec<String>| !uris.is_empty());
let additional_fields = other
.additional_fields
.iter()
.filter(|(key, _)| self.additional_fields.contains_key(*key))
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
Self {
tools_list_changed: (self.tools_list_changed == Some(true)
&& other.tools_list_changed == Some(true))
Expand All @@ -1966,6 +1979,7 @@ impl SubscriptionFilter {
&& other.resources_list_changed == Some(true))
.then_some(true),
resource_subscriptions,
additional_fields,
}
}

Expand All @@ -1986,7 +2000,11 @@ impl SubscriptionFilter {
.is_some_and(|requested| requested.contains(uri))
})
});
booleans_are_subset && resources_are_subset
let additional_fields_are_subset = self
.additional_fields
.keys()
.all(|key| other.additional_fields.contains_key(key));
booleans_are_subset && resources_are_subset && additional_fields_are_subset
}

/// Return the requested notification types advertised by server capabilities.
Expand Down Expand Up @@ -2016,6 +2034,7 @@ impl SubscriptionFilter {
.is_some_and(|resources| resources.subscribe == Some(true))
.then(|| self.resource_subscriptions.clone())
.flatten(),
additional_fields: self.additional_fields.clone(),
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions crates/rmcp/src/model/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ pub struct ClientCapabilities {
/// Capability to handle elicitation requests from servers for interactive user input
#[serde(skip_serializing_if = "Option::is_none")]
pub elicitation: Option<ElicitationCapability>,
/// Additional capability entries not yet modeled by this SDK.
///
/// MCP capability objects are open sets. Retaining these values allows
/// intermediaries to preserve additive or extension-defined capabilities
/// without requiring a new SDK release for every field.
#[serde(flatten)]
pub additional_capabilities: JsonObject,
}

impl ClientCapabilities {
Expand Down Expand Up @@ -240,6 +247,13 @@ pub struct ServerCapabilities {
pub resources: Option<ResourcesCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapability>,
/// Additional capability entries not yet modeled by this SDK.
///
/// MCP capability objects are open sets. Retaining these values allows
/// intermediaries to preserve additive or extension-defined capabilities
/// without requiring a new SDK release for every field.
#[serde(flatten)]
pub additional_capabilities: JsonObject,
}

impl ServerCapabilities {
Expand Down Expand Up @@ -277,6 +291,7 @@ macro_rules! builder {
pub fn build(self) -> $Target {
$Target {
$( $f: self.$f, )*
..Default::default()
}
}
}
Expand Down Expand Up @@ -677,4 +692,41 @@ mod test {
serde_json::json!({})
);
}

#[test]
fn client_capabilities_preserve_unknown_top_level_fields() {
let input = serde_json::json!({
"sampling": {},
"com.example/futureCoreCapability": {
"mode": "future",
"nested": [1, {"enabled": true}]
}
});

let capabilities: ClientCapabilities = serde_json::from_value(input.clone()).unwrap();
let output = serde_json::to_value(capabilities).unwrap();

assert_eq!(
output["com.example/futureCoreCapability"],
input["com.example/futureCoreCapability"]
);
}

#[test]
fn server_capabilities_preserve_unknown_top_level_fields() {
let input = serde_json::json!({
"tools": {"listChanged": true},
"com.example/futureCoreCapability": {
"arbitrary": ["data", 42]
}
});

let capabilities: ServerCapabilities = serde_json::from_value(input.clone()).unwrap();
let output = serde_json::to_value(capabilities).unwrap();

assert_eq!(
output["com.example/futureCoreCapability"],
input["com.example/futureCoreCapability"]
);
}
}
Loading
Loading