From 1a89b55a41d7c841d31dfc3fff5b3e110aa904f4 Mon Sep 17 00:00:00 2001 From: mrcs64 <9069178+mrcs64@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:03:08 +0200 Subject: [PATCH 1/2] fix: stamp server info on graceful subscription results --- crates/rmcp/src/handler/server.rs | 14 ++++++++++---- crates/rmcp/src/model.rs | 8 ++++---- crates/rmcp/tests/test_subscriptions.rs | 8 ++++++++ .../test_subscriptions_streamable_http.rs | 19 +++++++++++++------ 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index 415c1a3ba..62449fb98 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -154,7 +154,8 @@ impl Service for H { McpError::method_not_found::(), ); }; - 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 { @@ -168,15 +169,20 @@ impl Service 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.insert( + SERVER_INFO_META_KEY.to_owned(), + serde_json::to_value(server_implementation) + .expect("Implementation serialization cannot fail"), + ); + ServerResult::SubscriptionsListenResult(result) }) } } diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 14aa31d2a..4f4e9d189 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -1199,9 +1199,9 @@ pub struct DiscoverResult { pub meta: Option, } -impl DiscoverResult { - const SERVER_INFO_META_KEY: &str = "io.modelcontextprotocol/serverInfo"; +pub(crate) const SERVER_INFO_META_KEY: &str = "io.modelcontextprotocol/serverInfo"; +impl DiscoverResult { /// Create a non-cacheable private discovery result. pub fn new(supported_versions: Vec, capabilities: ServerCapabilities) -> Self { Self { @@ -1220,7 +1220,7 @@ impl DiscoverResult { self.meta .as_ref()? .0 - .get(Self::SERVER_INFO_META_KEY) + .get(SERVER_INFO_META_KEY) .and_then(|value| serde_json::from_value(value.clone()).ok()) } @@ -1231,7 +1231,7 @@ impl DiscoverResult { self.meta .get_or_insert_default() .0 - .insert(Self::SERVER_INFO_META_KEY.to_owned(), server_info); + .insert(SERVER_INFO_META_KEY.to_owned(), server_info); } /// Store server implementation information in result metadata. diff --git a/crates/rmcp/tests/test_subscriptions.rs b/crates/rmcp/tests/test_subscriptions.rs index a268a4b0e..1c8cdfbb8 100644 --- a/crates/rmcp/tests/test_subscriptions.rs +++ b/crates/rmcp/tests/test_subscriptions.rs @@ -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( @@ -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 + .get("io.modelcontextprotocol/serverInfo") + .and_then(|value| serde_json::from_value::(value.clone()).ok()), + Some(Implementation::new("tools-only-server", "1.0.0")) + ); client.cancel().await?; Ok(()) diff --git a/crates/rmcp/tests/test_subscriptions_streamable_http.rs b/crates/rmcp/tests/test_subscriptions_streamable_http.rs index 8acfe2873..75c48eb3f 100644 --- a/crates/rmcp/tests/test_subscriptions_streamable_http.rs +++ b/crates/rmcp/tests/test_subscriptions_streamable_http.rs @@ -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::{ @@ -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( @@ -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 + .get("io.modelcontextprotocol/serverInfo") + .and_then(|value| serde_json::from_value::(value.clone()).ok()), + Some(Implementation::new("http-subscription-server", "1.0.0")) + ); client.cancel().await?; server_ct.cancel(); From 4dfb28863afcc95e47881e022cf73636335d6481 Mon Sep 17 00:00:00 2001 From: mrcs64 <9069178+mrcs64@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:57:17 +0200 Subject: [PATCH 2/2] fix: expose subscription server info metadata --- crates/rmcp/src/handler/server.rs | 6 +-- crates/rmcp/src/model.rs | 41 +++++++++++++------ .../server_json_rpc_message_schema.json | 8 ++++ ...erver_json_rpc_message_schema_current.json | 8 ++++ crates/rmcp/tests/test_subscriptions.rs | 6 +-- crates/rmcp/tests/test_subscriptions_model.rs | 24 +++++++++-- .../test_subscriptions_streamable_http.rs | 6 +-- 7 files changed, 73 insertions(+), 26 deletions(-) diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index 62449fb98..61f414963 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -177,11 +177,7 @@ impl Service for H { // server teardown; explicit stdio cancellation remains a notification. self.listen(subscription).await.map(|()| { let mut result = SubscriptionsListenResult::complete(subscription_id); - result.meta.insert( - SERVER_INFO_META_KEY.to_owned(), - serde_json::to_value(server_implementation) - .expect("Implementation serialization cannot fail"), - ); + result.meta.set_server_info(server_implementation); ServerResult::SubscriptionsListenResult(result) }) } diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 4f4e9d189..06ed855e1 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -1199,7 +1199,18 @@ pub struct DiscoverResult { pub meta: Option, } -pub(crate) 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 { + 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. @@ -1217,21 +1228,12 @@ impl DiscoverResult { /// Return the server implementation information stored in result metadata. pub fn server_info(&self) -> Option { - self.meta - .as_ref()? - .0 - .get(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(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. @@ -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 { + 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 { @@ -2212,9 +2224,14 @@ impl schemars::JsonSchema for SubscriptionsListenResultMeta { fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { let subscription_id = generator.subschema_for::(); + let server_info = generator.subschema_for::(); 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"], diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index fb6876ead..ca0ef5611 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -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" } diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index fb6876ead..ca0ef5611 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -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" } diff --git a/crates/rmcp/tests/test_subscriptions.rs b/crates/rmcp/tests/test_subscriptions.rs index 1c8cdfbb8..8ceef93cd 100644 --- a/crates/rmcp/tests/test_subscriptions.rs +++ b/crates/rmcp/tests/test_subscriptions.rs @@ -444,9 +444,9 @@ async fn listen_exposes_acknowledged_filter_and_graceful_result() -> anyhow::Res assert_eq!( result .meta - .get("io.modelcontextprotocol/serverInfo") - .and_then(|value| serde_json::from_value::(value.clone()).ok()), - Some(Implementation::new("tools-only-server", "1.0.0")) + .server_info() + .expect("graceful result should contain valid server info"), + Implementation::new("tools-only-server", "1.0.0") ); client.cancel().await?; diff --git a/crates/rmcp/tests/test_subscriptions_model.rs b/crates/rmcp/tests/test_subscriptions_model.rs index fe9bbdf42..8d79fb119 100644 --- a/crates/rmcp/tests/test_subscriptions_model.rs +++ b/crates/rmcp/tests/test_subscriptions_model.rs @@ -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, }; @@ -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); @@ -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", }, @@ -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" })) @@ -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"])); } diff --git a/crates/rmcp/tests/test_subscriptions_streamable_http.rs b/crates/rmcp/tests/test_subscriptions_streamable_http.rs index 75c48eb3f..bfb931371 100644 --- a/crates/rmcp/tests/test_subscriptions_streamable_http.rs +++ b/crates/rmcp/tests/test_subscriptions_streamable_http.rs @@ -224,9 +224,9 @@ async fn modern_http_graceful_close_returns_final_listen_result() -> anyhow::Res assert_eq!( result .meta - .get("io.modelcontextprotocol/serverInfo") - .and_then(|value| serde_json::from_value::(value.clone()).ok()), - Some(Implementation::new("http-subscription-server", "1.0.0")) + .server_info() + .expect("graceful result should contain valid server info"), + Implementation::new("http-subscription-server", "1.0.0") ); client.cancel().await?;