Skip to content

Commit a08dd9a

Browse files
fix: accept namespaced discovery server information (#1039)
1 parent 14298b7 commit a08dd9a

8 files changed

Lines changed: 350 additions & 21 deletions

File tree

crates/rmcp/src/model.rs

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,42 @@ impl InitializeResult {
10891089
pub type ServerInfo = InitializeResult;
10901090
pub type ClientInfo = InitializeRequestParams;
10911091

1092+
/// Information learned about a server by a client.
1093+
///
1094+
/// Legacy initialization requires [`server_info`](Self::server_info), while
1095+
/// the modern discovery lifecycle carries it as optional, self-reported result
1096+
/// metadata. The remaining fields are available in both lifecycle modes.
1097+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1098+
#[serde(rename_all = "camelCase")]
1099+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1100+
#[non_exhaustive]
1101+
pub struct ServerPeerInfo {
1102+
/// The negotiated protocol version.
1103+
pub protocol_version: ProtocolVersion,
1104+
/// The capabilities advertised by the server.
1105+
pub capabilities: ServerCapabilities,
1106+
/// Optional, self-reported server implementation identity.
1107+
pub server_info: Option<Implementation>,
1108+
/// Optional human-readable instructions about using the server.
1109+
#[serde(skip_serializing_if = "Option::is_none")]
1110+
pub instructions: Option<String>,
1111+
/// Protocol-level response metadata.
1112+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1113+
pub meta: Option<MetaObject>,
1114+
}
1115+
1116+
impl From<ServerInfo> for ServerPeerInfo {
1117+
fn from(info: ServerInfo) -> Self {
1118+
Self {
1119+
protocol_version: info.protocol_version,
1120+
capabilities: info.capabilities,
1121+
server_info: Some(info.server_info),
1122+
instructions: info.instructions,
1123+
meta: info.meta,
1124+
}
1125+
}
1126+
}
1127+
10921128
const_string!(DiscoverRequestMethod = "server/discover");
10931129

10941130
/// Parameters for [`DiscoverRequest`].
@@ -1120,7 +1156,7 @@ impl schemars::JsonSchema for DiscoverRequestParams {
11201156
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
11211157

11221158
/// The server's response to a [`DiscoverRequest`].
1123-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1159+
#[derive(Debug, Serialize, Clone, PartialEq)]
11241160
#[serde(rename_all = "camelCase")]
11251161
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11261162
#[non_exhaustive]
@@ -1131,8 +1167,6 @@ pub struct DiscoverResult {
11311167
pub supported_versions: Vec<ProtocolVersion>,
11321168
/// Capabilities provided by this server.
11331169
pub capabilities: ServerCapabilities,
1134-
/// Information about the server implementation.
1135-
pub server_info: Implementation,
11361170
/// Optional guidance for using the server.
11371171
#[serde(skip_serializing_if = "Option::is_none")]
11381172
pub instructions: Option<String>,
@@ -1145,18 +1179,77 @@ pub struct DiscoverResult {
11451179
pub meta: Option<MetaObject>,
11461180
}
11471181

1182+
impl<'de> Deserialize<'de> for DiscoverResult {
1183+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1184+
where
1185+
D: serde::Deserializer<'de>,
1186+
{
1187+
#[derive(Deserialize)]
1188+
#[serde(rename_all = "camelCase")]
1189+
struct Helper {
1190+
result_type: ResultType,
1191+
supported_versions: Vec<ProtocolVersion>,
1192+
capabilities: ServerCapabilities,
1193+
server_info: Option<serde_json::Value>,
1194+
instructions: Option<String>,
1195+
ttl_ms: u64,
1196+
cache_scope: CacheScope,
1197+
#[serde(rename = "_meta")]
1198+
meta: Option<MetaObject>,
1199+
}
1200+
1201+
let mut helper = Helper::deserialize(deserializer)?;
1202+
let has_canonical_server_info = helper
1203+
.meta
1204+
.as_ref()
1205+
.is_some_and(|metadata| metadata.0.contains_key(MetaObject::META_KEY_SERVER_INFO));
1206+
if !has_canonical_server_info
1207+
&& let Some(server_info) = helper
1208+
.server_info
1209+
.and_then(|value| serde_json::from_value::<Implementation>(value).ok())
1210+
{
1211+
helper
1212+
.meta
1213+
.get_or_insert_with(MetaObject::new)
1214+
.set_server_info(server_info);
1215+
}
1216+
1217+
Ok(Self {
1218+
result_type: helper.result_type,
1219+
supported_versions: helper.supported_versions,
1220+
capabilities: helper.capabilities,
1221+
instructions: helper.instructions,
1222+
ttl_ms: helper.ttl_ms,
1223+
cache_scope: helper.cache_scope,
1224+
meta: helper.meta,
1225+
})
1226+
}
1227+
}
1228+
11481229
impl DiscoverResult {
11491230
/// Create a non-cacheable private discovery result.
11501231
pub fn new(
11511232
supported_versions: Vec<ProtocolVersion>,
11521233
capabilities: ServerCapabilities,
11531234
server_info: Implementation,
1235+
) -> Self {
1236+
Self::new_without_server_info(supported_versions, capabilities)
1237+
.with_server_info(server_info)
1238+
}
1239+
1240+
/// Create a non-cacheable private discovery result without a server identity.
1241+
///
1242+
/// Server identity is optional display-only metadata. Servers should normally
1243+
/// use [`DiscoverResult::new`], but this constructor supports peers that do
1244+
/// not advertise an implementation name and version.
1245+
pub fn new_without_server_info(
1246+
supported_versions: Vec<ProtocolVersion>,
1247+
capabilities: ServerCapabilities,
11541248
) -> Self {
11551249
Self {
11561250
result_type: ResultType::COMPLETE,
11571251
supported_versions,
11581252
capabilities,
1159-
server_info,
11601253
instructions: None,
11611254
ttl_ms: 0,
11621255
cache_scope: CacheScope::Private,
@@ -1178,10 +1271,30 @@ impl DiscoverResult {
11781271
} = server_info;
11791272
let mut result = Self::new(supported_versions, capabilities, server_info);
11801273
result.instructions = instructions;
1181-
result.meta = meta;
1274+
if let Some(meta) = meta {
1275+
result.meta.get_or_insert_with(MetaObject::new).extend(meta);
1276+
}
11821277
result
11831278
}
11841279

1280+
/// Return the optional self-reported server identity from result metadata.
1281+
pub fn server_info(&self) -> Option<Implementation> {
1282+
self.meta.as_ref().and_then(MetaObject::server_info)
1283+
}
1284+
1285+
/// Set the self-reported server identity in canonical result metadata.
1286+
pub fn set_server_info(&mut self, server_info: Implementation) {
1287+
self.meta
1288+
.get_or_insert_with(MetaObject::new)
1289+
.set_server_info(server_info);
1290+
}
1291+
1292+
/// Set the self-reported server identity in canonical result metadata.
1293+
pub fn with_server_info(mut self, server_info: Implementation) -> Self {
1294+
self.set_server_info(server_info);
1295+
self
1296+
}
1297+
11851298
/// Set the cache lifetime hint in milliseconds.
11861299
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
11871300
self.ttl_ms = ttl_ms;

crates/rmcp/src/model/meta.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ pub struct MetaObject(pub JsonObject);
253253
pub use self::MetaObject as Meta;
254254

255255
impl MetaObject {
256+
/// Reserved result metadata key for the server implementation identity.
257+
pub const META_KEY_SERVER_INFO: &'static str = "io.modelcontextprotocol/serverInfo";
256258
/// Reserved `_meta` key for the W3C Trace Context `traceparent` value (SEP-414).
257259
const TRACEPARENT_FIELD: &str = "traceparent";
258260
/// Reserved `_meta` key for the W3C Trace Context `tracestate` value (SEP-414).
@@ -322,6 +324,19 @@ impl MetaObject {
322324
self.0.extend(other.0);
323325
}
324326

327+
/// Get the self-reported server implementation identity, if present and valid.
328+
///
329+
/// This value is intended for display, logging, and debugging. Callers must
330+
/// not use it for behavioral or security decisions.
331+
pub fn server_info(&self) -> Option<Implementation> {
332+
self.decode_value(Self::META_KEY_SERVER_INFO)
333+
}
334+
335+
/// Set the self-reported server implementation identity.
336+
pub fn set_server_info(&mut self, server_info: Implementation) {
337+
self.insert_serialized(Self::META_KEY_SERVER_INFO, server_info);
338+
}
339+
325340
fn decode_value<T>(&self, key: &str) -> Option<T>
326341
where
327342
T: for<'de> Deserialize<'de>,

crates/rmcp/src/service/client.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::{
2525
NumberOrString, PaginatedRequestParams, ProgressNotification, ProgressNotificationParam,
2626
ProtocolVersion, ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse,
2727
ReadResourceResult, Reference, RequestId, RequestMetaObject, RootsListChangedNotification,
28-
ServerInfo, ServerJsonRpcMessage, ServerNotification, ServerRequest, ServerResult,
28+
ServerJsonRpcMessage, ServerNotification, ServerPeerInfo, ServerRequest, ServerResult,
2929
SetLevelRequest, SetLevelRequestParams, SubscribeRequest, SubscribeRequestParams,
3030
SubscriptionFilter, SubscriptionsListenRequest, SubscriptionsListenRequestParams,
3131
SubscriptionsListenResult, UnsubscribeRequest, UnsubscribeRequestParams, UpdateTaskParams,
@@ -188,7 +188,7 @@ impl ServiceRole for RoleClient {
188188
type PeerResp = ServerResult;
189189
type PeerNot = ServerNotification;
190190
type Info = ClientInfo;
191-
type PeerInfo = ServerInfo;
191+
type PeerInfo = ServerPeerInfo;
192192
type InitializeError = ClientInitializeError;
193193
const IS_CLIENT: bool = true;
194194

@@ -751,7 +751,7 @@ where
751751
let ServerResult::InitializeResult(initialize_result) = response else {
752752
return Err(ClientInitializeError::ExpectedInitResult(Some(response)));
753753
};
754-
peer.set_peer_info(initialize_result);
754+
peer.set_peer_info(initialize_result.into());
755755

756756
// send notification
757757
let notification = ClientJsonRpcMessage::notification(
@@ -821,10 +821,11 @@ where
821821
server_supported: result.supported_versions,
822822
});
823823
};
824-
peer.set_peer_info(ServerInfo {
824+
let server_info = result.server_info();
825+
peer.set_peer_info(ServerPeerInfo {
825826
protocol_version: selected.clone(),
826827
capabilities: result.capabilities,
827-
server_info: result.server_info,
828+
server_info,
828829
instructions: result.instructions,
829830
meta: result.meta,
830831
});

crates/rmcp/tests/test_client_lifecycle_modes.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,51 @@ async fn discover_startup_accepts_stringified_numeric_response_id() {
6060
server_task.await.expect("server task");
6161
}
6262

63+
#[tokio::test]
64+
async fn discover_startup_accepts_anonymous_server() {
65+
let (server_transport, client_transport) = tokio::io::duplex(4096);
66+
let mut server = IntoTransport::<rmcp::RoleServer, _, _>::into_transport(server_transport);
67+
let server_task = tokio::spawn(async move {
68+
let ClientJsonRpcMessage::Request(request) =
69+
server.receive().await.expect("expected discover request")
70+
else {
71+
panic!("expected discover request");
72+
};
73+
let result: DiscoverResult = serde_json::from_value(serde_json::json!({
74+
"resultType": "complete",
75+
"supportedVersions": ["2026-07-28"],
76+
"capabilities": {},
77+
"ttlMs": 0,
78+
"cacheScope": "private"
79+
}))
80+
.expect("anonymous discovery result");
81+
server
82+
.send(ServerJsonRpcMessage::response(
83+
ServerResult::DiscoverResult(result),
84+
request.id,
85+
))
86+
.await
87+
.expect("send discover response");
88+
});
89+
90+
let client = DiscoverClient
91+
.serve_with_lifecycle(
92+
client_transport,
93+
ClientLifecycleMode::Discover {
94+
preferred_versions: vec![ProtocolVersion::V_2026_07_28],
95+
},
96+
)
97+
.await
98+
.expect("anonymous server should remain discoverable");
99+
let peer = client
100+
.peer_info()
101+
.expect("discovery should store peer state");
102+
assert_eq!(peer.protocol_version, ProtocolVersion::V_2026_07_28);
103+
assert_eq!(peer.server_info, None);
104+
client.cancel().await.expect("cancel client");
105+
server_task.await.expect("server task");
106+
}
107+
63108
#[tokio::test]
64109
async fn high_level_server_accepts_discover_startup_without_initialize() {
65110
let (server_transport, client_transport) = tokio::io::duplex(4096);

crates/rmcp/tests/test_mrtr_behavior.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ where
316316
let client = serve_directly::<RoleClient, _, _, _, _>(
317317
MrtrClient,
318318
client_transport,
319-
Some(client_peer_info),
319+
Some(client_peer_info.into()),
320320
);
321321

322322
let result = body(client).await;
@@ -580,7 +580,7 @@ async fn request_state_codec_seals_and_verifies_through_the_loop() -> anyhow::Re
580580
let client = serve_directly::<RoleClient, _, _, _, _>(
581581
MrtrClient,
582582
client_transport,
583-
Some(server_info(ProtocolVersion::V_2026_07_28)),
583+
Some(server_info(ProtocolVersion::V_2026_07_28).into()),
584584
);
585585

586586
let result = client

0 commit comments

Comments
 (0)