Skip to content
Merged
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
10 changes: 6 additions & 4 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ impl<H: ServerHandler> Service<RoleServer> for H {
McpError::method_not_found::<SubscriptionsListenRequestMethod>(),
);
};
let advertised = requested.supported_by(&self.get_info().capabilities);
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 {
Expand All @@ -168,15 +169,16 @@ impl<H: ServerHandler> Service<RoleServer> for H {
"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(|()| {
ServerResult::SubscriptionsListenResult(
SubscriptionsListenResult::complete(subscription_id),
)
let mut result = SubscriptionsListenResult::complete(subscription_id);
result.meta.set_server_info(server_implementation);
ServerResult::SubscriptionsListenResult(result)
})
}
}
Expand Down
43 changes: 30 additions & 13 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1199,9 +1199,20 @@ pub struct DiscoverResult {
pub meta: Option<MetaObject>,
}

impl DiscoverResult {
const SERVER_INFO_META_KEY: &str = "io.modelcontextprotocol/serverInfo";
const SERVER_INFO_META_KEY: &str = "io.modelcontextprotocol/serverInfo";

fn server_info_from_meta(meta: &MetaObject) -> Option<Implementation> {
meta.get(SERVER_INFO_META_KEY)
.and_then(|value| serde_json::from_value(value.clone()).ok())
}

fn set_server_info_on_meta(meta: &mut MetaObject, server_info: Implementation) {
let server_info =
serde_json::to_value(server_info).expect("Implementation serialization cannot fail");
meta.insert(SERVER_INFO_META_KEY.to_owned(), server_info);
}

impl DiscoverResult {
/// Create a non-cacheable private discovery result.
pub fn new(supported_versions: Vec<ProtocolVersion>, capabilities: ServerCapabilities) -> Self {
Self {
Expand All @@ -1217,21 +1228,12 @@ impl DiscoverResult {

/// Return the server implementation information stored in result metadata.
pub fn server_info(&self) -> Option<Implementation> {
self.meta
.as_ref()?
.0
.get(Self::SERVER_INFO_META_KEY)
.and_then(|value| serde_json::from_value(value.clone()).ok())
server_info_from_meta(self.meta.as_ref()?)
}

/// Store server implementation information in result metadata.
pub fn set_server_info(&mut self, server_info: Implementation) {
let server_info =
serde_json::to_value(server_info).expect("Implementation serialization cannot fail");
self.meta
.get_or_insert_default()
.0
.insert(Self::SERVER_INFO_META_KEY.to_owned(), server_info);
set_server_info_on_meta(self.meta.get_or_insert_default(), server_info);
}

/// Store server implementation information in result metadata.
Expand Down Expand Up @@ -2174,6 +2176,16 @@ impl SubscriptionsListenResultMeta {
subscription_id.into_json_value(),
);
}

/// Return the server implementation information stored in result metadata.
pub fn server_info(&self) -> Option<Implementation> {
server_info_from_meta(&self.0)
}

/// Store server implementation information in result metadata.
pub fn set_server_info(&mut self, server_info: Implementation) {
set_server_info_on_meta(&mut self.0, server_info);
}
}

impl<'de> Deserialize<'de> for SubscriptionsListenResultMeta {
Expand Down Expand Up @@ -2212,9 +2224,14 @@ impl schemars::JsonSchema for SubscriptionsListenResultMeta {

fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let subscription_id = generator.subschema_for::<RequestId>();
let server_info = generator.subschema_for::<Implementation>();
schemars::json_schema!({
"type": "object",
"properties": {
"io.modelcontextprotocol/serverInfo": {
"description": "Identifies the server software producing the response. Servers SHOULD include this field on every response unless specifically configured not to do so.",
"allOf": [server_info],
},
"io.modelcontextprotocol/subscriptionId": subscription_id,
},
"required": ["io.modelcontextprotocol/subscriptionId"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3450,6 +3450,14 @@
"SubscriptionsListenResultMeta": {
"type": "object",
"properties": {
"io.modelcontextprotocol/serverInfo": {
"description": "Identifies the server software producing the response. Servers SHOULD include this field on every response unless specifically configured not to do so.",
"allOf": [
{
"$ref": "#/definitions/Implementation"
}
]
},
"io.modelcontextprotocol/subscriptionId": {
"$ref": "#/definitions/NumberOrString"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3450,6 +3450,14 @@
"SubscriptionsListenResultMeta": {
"type": "object",
"properties": {
"io.modelcontextprotocol/serverInfo": {
"description": "Identifies the server software producing the response. Servers SHOULD include this field on every response unless specifically configured not to do so.",
"allOf": [
{
"$ref": "#/definitions/Implementation"
}
]
},
"io.modelcontextprotocol/subscriptionId": {
"$ref": "#/definitions/NumberOrString"
}
Expand Down
8 changes: 8 additions & 0 deletions crates/rmcp/tests/test_subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl ServerHandler for ToolsOnlyServer {
.enable_tool_list_changed()
.build(),
)
.with_server_info(Implementation::new("tools-only-server", "1.0.0"))
}

fn accepted_subscription_filter(
Expand Down Expand Up @@ -440,6 +441,13 @@ async fn listen_exposes_acknowledged_filter_and_graceful_result() -> anyhow::Res
result.meta.subscription_id().as_ref(),
Some(subscription.id())
);
assert_eq!(
result
.meta
.server_info()
.expect("graceful result should contain valid server info"),
Implementation::new("tools-only-server", "1.0.0")
);

client.cancel().await?;
Ok(())
Expand Down
24 changes: 21 additions & 3 deletions crates/rmcp/tests/test_subscriptions_model.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rmcp::model::{
ClientJsonRpcMessage, ClientRequest, GetMeta, JsonRpcNotification, JsonRpcRequest,
NotificationMetaObject, RequestId, RequestMetaObject, ServerJsonRpcMessage, ServerNotification,
SubscriptionFilter, SubscriptionsAcknowledgedNotification,
ClientJsonRpcMessage, ClientRequest, GetMeta, Implementation, JsonRpcNotification,
JsonRpcRequest, NotificationMetaObject, RequestId, RequestMetaObject, ServerJsonRpcMessage,
ServerNotification, SubscriptionFilter, SubscriptionsAcknowledgedNotification,
SubscriptionsAcknowledgedNotificationParams, SubscriptionsListenRequest,
SubscriptionsListenRequestParams, SubscriptionsListenResult, SubscriptionsListenResultMeta,
};
Expand Down Expand Up @@ -155,6 +155,7 @@ fn acknowledged_notification_round_trips_numeric_subscription_id_and_metadata()
#[test]
fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata() {
let mut meta = SubscriptionsListenResultMeta::new(RequestId::String("subscription-2".into()));
meta.set_server_info(Implementation::new("test-server", "1.0.0"));
meta.insert("com.example/result".into(), json!({ "reason": "shutdown" }));
let result = SubscriptionsListenResult::new(meta);

Expand All @@ -165,6 +166,10 @@ fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/subscriptionId": "subscription-2",
"io.modelcontextprotocol/serverInfo": {
"name": "test-server",
"version": "1.0.0",
},
"com.example/result": {
"reason": "shutdown",
},
Expand All @@ -178,6 +183,10 @@ fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata
round_trip.meta.subscription_id(),
Some(RequestId::String("subscription-2".into()))
);
assert_eq!(
round_trip.meta.server_info(),
Some(Implementation::new("test-server", "1.0.0"))
);
assert_eq!(
round_trip.meta.get("com.example/result"),
Some(&json!({ "reason": "shutdown" }))
Expand Down Expand Up @@ -232,5 +241,14 @@ fn subscription_schemas_mark_only_draft_required_fields_as_required() {
acknowledgment_schema["properties"]["_meta"]["$ref"],
"#/$defs/NotificationMetaObject"
);
let result_meta_schema = &result_schema["$defs"]["SubscriptionsListenResultMeta"];
assert_eq!(
result_meta_schema["properties"]["io.modelcontextprotocol/serverInfo"]["allOf"][0]["$ref"],
"#/$defs/Implementation"
);
assert_eq!(
result_meta_schema["required"],
json!(["io.modelcontextprotocol/subscriptionId"])
);
assert_eq!(result_schema["required"], json!(["resultType", "_meta"]));
}
19 changes: 13 additions & 6 deletions crates/rmcp/tests/test_subscriptions_streamable_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use std::{
use rmcp::{
ClientLifecycleMode, ClientServiceExt, ServerHandler,
model::{
ClientInfo, ClientRequest, ListToolsRequest, ProtocolVersion, RequestMetaObject,
ServerCapabilities, ServerInfo, ServerNotification, SubscriptionFilter,
ClientInfo, ClientRequest, Implementation, ListToolsRequest, ProtocolVersion,
RequestMetaObject, ServerCapabilities, ServerInfo, ServerNotification, SubscriptionFilter,
},
service::{PeerRequestOptions, SubscriptionContext, SubscriptionEnd},
transport::{
Expand Down Expand Up @@ -59,6 +59,7 @@ impl ServerHandler for HttpSubscriptionServer {
.enable_tool_list_changed()
.build(),
)
.with_server_info(Implementation::new("http-subscription-server", "1.0.0"))
}

fn accepted_subscription_filter(
Expand Down Expand Up @@ -217,10 +218,16 @@ async fn modern_http_graceful_close_returns_final_listen_result() -> anyhow::Res

assert!(subscription.next().await?.is_some());
assert!(subscription.next().await?.is_none());
assert!(matches!(
subscription.end(),
Some(SubscriptionEnd::Graceful(_))
));
let Some(SubscriptionEnd::Graceful(result)) = subscription.end() else {
panic!("expected graceful final result");
};
assert_eq!(
result
.meta
.server_info()
.expect("graceful result should contain valid server info"),
Implementation::new("http-subscription-server", "1.0.0")
);

client.cancel().await?;
server_ct.cancel();
Expand Down