fix: stamp server info on graceful subscription results (#1078)
* fix: stamp server info on graceful subscription results * fix: expose subscription server info metadata --------- Co-authored-by: mrcs64 <9069178+mrcs64@users.noreply.github.com>
This commit is contained in:
parent
4e361b715f
commit
cb50ae7890
7 changed files with 94 additions and 26 deletions
|
|
@ -154,7 +154,8 @@ impl<H: ServerHandler> Service<RoleServer> for H {
|
||||||
McpError::method_not_found::<SubscriptionsListenRequestMethod>(),
|
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 handler_accepted = requested.intersection(&candidate);
|
||||||
let accepted = handler_accepted.intersection(&advertised);
|
let accepted = handler_accepted.intersection(&advertised);
|
||||||
if accepted != handler_accepted {
|
if accepted != handler_accepted {
|
||||||
|
|
@ -168,15 +169,16 @@ impl<H: ServerHandler> Service<RoleServer> for H {
|
||||||
"subscription filter reduced to advertised server capabilities"
|
"subscription filter reduced to advertised server capabilities"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let server_implementation = server_info.server_info;
|
||||||
let subscription_id = context.id.clone();
|
let subscription_id = context.id.clone();
|
||||||
let subscription =
|
let subscription =
|
||||||
SubscriptionContext::establish(context, requested, accepted).await?;
|
SubscriptionContext::establish(context, requested, accepted).await?;
|
||||||
// The 2026-07-28 schema defines a final result for graceful
|
// The 2026-07-28 schema defines a final result for graceful
|
||||||
// server teardown; explicit stdio cancellation remains a notification.
|
// server teardown; explicit stdio cancellation remains a notification.
|
||||||
self.listen(subscription).await.map(|()| {
|
self.listen(subscription).await.map(|()| {
|
||||||
ServerResult::SubscriptionsListenResult(
|
let mut result = SubscriptionsListenResult::complete(subscription_id);
|
||||||
SubscriptionsListenResult::complete(subscription_id),
|
result.meta.set_server_info(server_implementation);
|
||||||
)
|
ServerResult::SubscriptionsListenResult(result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1199,9 +1199,20 @@ pub struct DiscoverResult {
|
||||||
pub meta: Option<MetaObject>,
|
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.
|
/// Create a non-cacheable private discovery result.
|
||||||
pub fn new(supported_versions: Vec<ProtocolVersion>, capabilities: ServerCapabilities) -> Self {
|
pub fn new(supported_versions: Vec<ProtocolVersion>, capabilities: ServerCapabilities) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -1217,21 +1228,12 @@ impl DiscoverResult {
|
||||||
|
|
||||||
/// Return the server implementation information stored in result metadata.
|
/// Return the server implementation information stored in result metadata.
|
||||||
pub fn server_info(&self) -> Option<Implementation> {
|
pub fn server_info(&self) -> Option<Implementation> {
|
||||||
self.meta
|
server_info_from_meta(self.meta.as_ref()?)
|
||||||
.as_ref()?
|
|
||||||
.0
|
|
||||||
.get(Self::SERVER_INFO_META_KEY)
|
|
||||||
.and_then(|value| serde_json::from_value(value.clone()).ok())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store server implementation information in result metadata.
|
/// Store server implementation information in result metadata.
|
||||||
pub fn set_server_info(&mut self, server_info: Implementation) {
|
pub fn set_server_info(&mut self, server_info: Implementation) {
|
||||||
let server_info =
|
set_server_info_on_meta(self.meta.get_or_insert_default(), 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store server implementation information in result metadata.
|
/// Store server implementation information in result metadata.
|
||||||
|
|
@ -2174,6 +2176,16 @@ impl SubscriptionsListenResultMeta {
|
||||||
subscription_id.into_json_value(),
|
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 {
|
impl<'de> Deserialize<'de> for SubscriptionsListenResultMeta {
|
||||||
|
|
@ -2212,9 +2224,14 @@ impl schemars::JsonSchema for SubscriptionsListenResultMeta {
|
||||||
|
|
||||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
let subscription_id = generator.subschema_for::<RequestId>();
|
let subscription_id = generator.subschema_for::<RequestId>();
|
||||||
|
let server_info = generator.subschema_for::<Implementation>();
|
||||||
schemars::json_schema!({
|
schemars::json_schema!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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,
|
"io.modelcontextprotocol/subscriptionId": subscription_id,
|
||||||
},
|
},
|
||||||
"required": ["io.modelcontextprotocol/subscriptionId"],
|
"required": ["io.modelcontextprotocol/subscriptionId"],
|
||||||
|
|
|
||||||
|
|
@ -3450,6 +3450,14 @@
|
||||||
"SubscriptionsListenResultMeta": {
|
"SubscriptionsListenResultMeta": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"io.modelcontextprotocol/subscriptionId": {
|
||||||
"$ref": "#/definitions/NumberOrString"
|
"$ref": "#/definitions/NumberOrString"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3450,6 +3450,14 @@
|
||||||
"SubscriptionsListenResultMeta": {
|
"SubscriptionsListenResultMeta": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"io.modelcontextprotocol/subscriptionId": {
|
||||||
"$ref": "#/definitions/NumberOrString"
|
"$ref": "#/definitions/NumberOrString"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ impl ServerHandler for ToolsOnlyServer {
|
||||||
.enable_tool_list_changed()
|
.enable_tool_list_changed()
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
.with_server_info(Implementation::new("tools-only-server", "1.0.0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accepted_subscription_filter(
|
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(),
|
result.meta.subscription_id().as_ref(),
|
||||||
Some(subscription.id())
|
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?;
|
client.cancel().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use rmcp::model::{
|
use rmcp::model::{
|
||||||
ClientJsonRpcMessage, ClientRequest, GetMeta, JsonRpcNotification, JsonRpcRequest,
|
ClientJsonRpcMessage, ClientRequest, GetMeta, Implementation, JsonRpcNotification,
|
||||||
NotificationMetaObject, RequestId, RequestMetaObject, ServerJsonRpcMessage, ServerNotification,
|
JsonRpcRequest, NotificationMetaObject, RequestId, RequestMetaObject, ServerJsonRpcMessage,
|
||||||
SubscriptionFilter, SubscriptionsAcknowledgedNotification,
|
ServerNotification, SubscriptionFilter, SubscriptionsAcknowledgedNotification,
|
||||||
SubscriptionsAcknowledgedNotificationParams, SubscriptionsListenRequest,
|
SubscriptionsAcknowledgedNotificationParams, SubscriptionsListenRequest,
|
||||||
SubscriptionsListenRequestParams, SubscriptionsListenResult, SubscriptionsListenResultMeta,
|
SubscriptionsListenRequestParams, SubscriptionsListenResult, SubscriptionsListenResultMeta,
|
||||||
};
|
};
|
||||||
|
|
@ -155,6 +155,7 @@ fn acknowledged_notification_round_trips_numeric_subscription_id_and_metadata()
|
||||||
#[test]
|
#[test]
|
||||||
fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata() {
|
fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata() {
|
||||||
let mut meta = SubscriptionsListenResultMeta::new(RequestId::String("subscription-2".into()));
|
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" }));
|
meta.insert("com.example/result".into(), json!({ "reason": "shutdown" }));
|
||||||
let result = SubscriptionsListenResult::new(meta);
|
let result = SubscriptionsListenResult::new(meta);
|
||||||
|
|
||||||
|
|
@ -165,6 +166,10 @@ fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata
|
||||||
"resultType": "complete",
|
"resultType": "complete",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"io.modelcontextprotocol/subscriptionId": "subscription-2",
|
"io.modelcontextprotocol/subscriptionId": "subscription-2",
|
||||||
|
"io.modelcontextprotocol/serverInfo": {
|
||||||
|
"name": "test-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
},
|
||||||
"com.example/result": {
|
"com.example/result": {
|
||||||
"reason": "shutdown",
|
"reason": "shutdown",
|
||||||
},
|
},
|
||||||
|
|
@ -178,6 +183,10 @@ fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata
|
||||||
round_trip.meta.subscription_id(),
|
round_trip.meta.subscription_id(),
|
||||||
Some(RequestId::String("subscription-2".into()))
|
Some(RequestId::String("subscription-2".into()))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
round_trip.meta.server_info(),
|
||||||
|
Some(Implementation::new("test-server", "1.0.0"))
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
round_trip.meta.get("com.example/result"),
|
round_trip.meta.get("com.example/result"),
|
||||||
Some(&json!({ "reason": "shutdown" }))
|
Some(&json!({ "reason": "shutdown" }))
|
||||||
|
|
@ -232,5 +241,14 @@ fn subscription_schemas_mark_only_draft_required_fields_as_required() {
|
||||||
acknowledgment_schema["properties"]["_meta"]["$ref"],
|
acknowledgment_schema["properties"]["_meta"]["$ref"],
|
||||||
"#/$defs/NotificationMetaObject"
|
"#/$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"]));
|
assert_eq!(result_schema["required"], json!(["resultType", "_meta"]));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ use std::{
|
||||||
use rmcp::{
|
use rmcp::{
|
||||||
ClientLifecycleMode, ClientServiceExt, ServerHandler,
|
ClientLifecycleMode, ClientServiceExt, ServerHandler,
|
||||||
model::{
|
model::{
|
||||||
ClientInfo, ClientRequest, ListToolsRequest, ProtocolVersion, RequestMetaObject,
|
ClientInfo, ClientRequest, Implementation, ListToolsRequest, ProtocolVersion,
|
||||||
ServerCapabilities, ServerInfo, ServerNotification, SubscriptionFilter,
|
RequestMetaObject, ServerCapabilities, ServerInfo, ServerNotification, SubscriptionFilter,
|
||||||
},
|
},
|
||||||
service::{PeerRequestOptions, SubscriptionContext, SubscriptionEnd},
|
service::{PeerRequestOptions, SubscriptionContext, SubscriptionEnd},
|
||||||
transport::{
|
transport::{
|
||||||
|
|
@ -59,6 +59,7 @@ impl ServerHandler for HttpSubscriptionServer {
|
||||||
.enable_tool_list_changed()
|
.enable_tool_list_changed()
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
.with_server_info(Implementation::new("http-subscription-server", "1.0.0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accepted_subscription_filter(
|
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_some());
|
||||||
assert!(subscription.next().await?.is_none());
|
assert!(subscription.next().await?.is_none());
|
||||||
assert!(matches!(
|
let Some(SubscriptionEnd::Graceful(result)) = subscription.end() else {
|
||||||
subscription.end(),
|
panic!("expected graceful final result");
|
||||||
Some(SubscriptionEnd::Graceful(_))
|
};
|
||||||
));
|
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?;
|
client.cancel().await?;
|
||||||
server_ct.cancel();
|
server_ct.cancel();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue