Skip to content

Commit 2047ae4

Browse files
fix: accept namespaced discovery server information (#1044)
* fix: accept namespaced discovery server information (#1039) * fix: preserve discovery server-info compatibility
1 parent 9528801 commit 2047ae4

2 files changed

Lines changed: 170 additions & 1 deletion

File tree

crates/rmcp/src/model.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,7 @@ impl schemars::JsonSchema for DiscoverRequestParams {
11201120
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
11211121

11221122
/// The server's response to a [`DiscoverRequest`].
1123-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1123+
#[derive(Debug, Serialize, Clone, PartialEq)]
11241124
#[serde(rename_all = "camelCase")]
11251125
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11261126
#[non_exhaustive]
@@ -1145,6 +1145,53 @@ pub struct DiscoverResult {
11451145
pub meta: Option<MetaObject>,
11461146
}
11471147

1148+
impl<'de> Deserialize<'de> for DiscoverResult {
1149+
fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
1150+
where
1151+
__D: serde::Deserializer<'de>,
1152+
{
1153+
#[derive(Deserialize)]
1154+
#[serde(rename_all = "camelCase")]
1155+
struct Helper {
1156+
result_type: ResultType,
1157+
supported_versions: Vec<ProtocolVersion>,
1158+
capabilities: ServerCapabilities,
1159+
server_info: Option<Implementation>,
1160+
instructions: Option<String>,
1161+
ttl_ms: u64,
1162+
cache_scope: CacheScope,
1163+
#[serde(rename = "_meta")]
1164+
meta: Option<MetaObject>,
1165+
}
1166+
1167+
let helper = Helper::deserialize(deserializer)?;
1168+
let server_info = match helper.server_info {
1169+
Some(server_info) => server_info,
1170+
None => {
1171+
let metadata_server_info = helper
1172+
.meta
1173+
.as_ref()
1174+
.and_then(|metadata| metadata.0.get("io.modelcontextprotocol/serverInfo"))
1175+
.ok_or_else(|| serde::de::Error::missing_field("serverInfo"))?;
1176+
1177+
serde_json::from_value(metadata_server_info.clone())
1178+
.map_err(serde::de::Error::custom)?
1179+
}
1180+
};
1181+
1182+
Ok(Self {
1183+
result_type: helper.result_type,
1184+
supported_versions: helper.supported_versions,
1185+
capabilities: helper.capabilities,
1186+
server_info,
1187+
instructions: helper.instructions,
1188+
ttl_ms: helper.ttl_ms,
1189+
cache_scope: helper.cache_scope,
1190+
meta: helper.meta,
1191+
})
1192+
}
1193+
}
1194+
11481195
impl DiscoverResult {
11491196
/// Create a non-cacheable private discovery result.
11501197
pub fn new(

crates/rmcp/tests/test_server_discover.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,128 @@ fn discover_result_deserializes_to_typed_variant() {
7474
);
7575
}
7676

77+
#[test]
78+
fn discover_result_accepts_server_info_in_namespaced_metadata() {
79+
let message: ServerJsonRpcMessage = serde_json::from_value(json!({
80+
"jsonrpc": "2.0",
81+
"id": 1,
82+
"result": {
83+
"resultType": "complete",
84+
"supportedVersions": ["2026-07-28"],
85+
"capabilities": {},
86+
"ttlMs": 0,
87+
"cacheScope": "private",
88+
"_meta": {
89+
"io.modelcontextprotocol/serverInfo": {
90+
"name": "conformance-mock-server",
91+
"version": "1.0.0"
92+
},
93+
"unrelated": { "preserved": true }
94+
}
95+
}
96+
}))
97+
.expect("discovery response with namespaced server info should deserialize");
98+
99+
let ServerJsonRpcMessage::Response(JsonRpcResponse { result, .. }) = message else {
100+
panic!("expected response");
101+
};
102+
let ServerResult::DiscoverResult(result) = result else {
103+
panic!("expected discovery response, not a tool-call result");
104+
};
105+
106+
assert_eq!(result.server_info.name, "conformance-mock-server");
107+
assert_eq!(result.server_info.version, "1.0.0");
108+
109+
let metadata = result.meta.expect("discovery metadata should be preserved");
110+
assert_eq!(
111+
metadata.0.get("io.modelcontextprotocol/serverInfo"),
112+
Some(&json!({
113+
"name": "conformance-mock-server",
114+
"version": "1.0.0"
115+
}))
116+
);
117+
assert_eq!(
118+
metadata.0.get("unrelated"),
119+
Some(&json!({ "preserved": true }))
120+
);
121+
}
122+
123+
#[test]
124+
fn discover_result_serializes_top_level_server_info() {
125+
let result = DiscoverResult::new(
126+
vec![ProtocolVersion::V_2026_07_28],
127+
rmcp::model::ServerCapabilities::default(),
128+
rmcp::model::Implementation::new("test-server", "1.0.0"),
129+
);
130+
131+
let serialized = serde_json::to_value(result).expect("serialize discovery result");
132+
assert_eq!(
133+
serialized["serverInfo"],
134+
json!({
135+
"name": "test-server",
136+
"version": "1.0.0"
137+
})
138+
);
139+
}
140+
141+
#[test]
142+
fn discover_result_prefers_top_level_server_info_over_namespaced_metadata() {
143+
let result: DiscoverResult = serde_json::from_value(json!({
144+
"resultType": "complete",
145+
"supportedVersions": ["2026-07-28"],
146+
"capabilities": {},
147+
"serverInfo": {
148+
"name": "top-level-server",
149+
"version": "2.0.0"
150+
},
151+
"ttlMs": 0,
152+
"cacheScope": "private",
153+
"_meta": {
154+
"io.modelcontextprotocol/serverInfo": {
155+
"name": "metadata-server",
156+
"version": "1.0.0"
157+
},
158+
"unrelated": true
159+
}
160+
}))
161+
.expect("top-level server info should remain supported");
162+
163+
assert_eq!(result.server_info.name, "top-level-server");
164+
assert_eq!(result.server_info.version, "2.0.0");
165+
assert_eq!(
166+
result
167+
.meta
168+
.as_ref()
169+
.and_then(|metadata| metadata.0.get("unrelated")),
170+
Some(&json!(true))
171+
);
172+
}
173+
174+
#[test]
175+
fn discover_result_requires_valid_top_level_or_namespaced_server_info() {
176+
let result = json!({
177+
"resultType": "complete",
178+
"supportedVersions": ["2026-07-28"],
179+
"capabilities": {},
180+
"ttlMs": 0,
181+
"cacheScope": "private",
182+
"_meta": { "unrelated": true }
183+
});
184+
185+
assert!(serde_json::from_value::<DiscoverResult>(result).is_err());
186+
187+
let malformed_server_info = json!({
188+
"resultType": "complete",
189+
"supportedVersions": ["2026-07-28"],
190+
"capabilities": {},
191+
"ttlMs": 0,
192+
"cacheScope": "private",
193+
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "missing-version" } }
194+
});
195+
196+
assert!(serde_json::from_value::<DiscoverResult>(malformed_server_info).is_err());
197+
}
198+
77199
#[test]
78200
fn unsupported_protocol_version_error_matches_draft_schema() {
79201
let error = ErrorData::unsupported_protocol_version(

0 commit comments

Comments
 (0)