fix: accept namespaced discovery server information (#1044)
* fix: accept namespaced discovery server information (#1039) * fix: preserve discovery server-info compatibility
This commit is contained in:
parent
9528801f6e
commit
2047ae42ec
2 changed files with 170 additions and 1 deletions
|
|
@ -1120,7 +1120,7 @@ impl schemars::JsonSchema for DiscoverRequestParams {
|
||||||
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
|
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
|
||||||
|
|
||||||
/// The server's response to a [`DiscoverRequest`].
|
/// The server's response to a [`DiscoverRequest`].
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Clone, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
|
|
@ -1145,6 +1145,53 @@ pub struct DiscoverResult {
|
||||||
pub meta: Option<MetaObject>,
|
pub meta: Option<MetaObject>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for DiscoverResult {
|
||||||
|
fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
|
||||||
|
where
|
||||||
|
__D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Helper {
|
||||||
|
result_type: ResultType,
|
||||||
|
supported_versions: Vec<ProtocolVersion>,
|
||||||
|
capabilities: ServerCapabilities,
|
||||||
|
server_info: Option<Implementation>,
|
||||||
|
instructions: Option<String>,
|
||||||
|
ttl_ms: u64,
|
||||||
|
cache_scope: CacheScope,
|
||||||
|
#[serde(rename = "_meta")]
|
||||||
|
meta: Option<MetaObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let helper = Helper::deserialize(deserializer)?;
|
||||||
|
let server_info = match helper.server_info {
|
||||||
|
Some(server_info) => server_info,
|
||||||
|
None => {
|
||||||
|
let metadata_server_info = helper
|
||||||
|
.meta
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|metadata| metadata.0.get("io.modelcontextprotocol/serverInfo"))
|
||||||
|
.ok_or_else(|| serde::de::Error::missing_field("serverInfo"))?;
|
||||||
|
|
||||||
|
serde_json::from_value(metadata_server_info.clone())
|
||||||
|
.map_err(serde::de::Error::custom)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
result_type: helper.result_type,
|
||||||
|
supported_versions: helper.supported_versions,
|
||||||
|
capabilities: helper.capabilities,
|
||||||
|
server_info,
|
||||||
|
instructions: helper.instructions,
|
||||||
|
ttl_ms: helper.ttl_ms,
|
||||||
|
cache_scope: helper.cache_scope,
|
||||||
|
meta: helper.meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DiscoverResult {
|
impl DiscoverResult {
|
||||||
/// Create a non-cacheable private discovery result.
|
/// Create a non-cacheable private discovery result.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,128 @@ fn discover_result_deserializes_to_typed_variant() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_result_accepts_server_info_in_namespaced_metadata() {
|
||||||
|
let message: ServerJsonRpcMessage = serde_json::from_value(json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"resultType": "complete",
|
||||||
|
"supportedVersions": ["2026-07-28"],
|
||||||
|
"capabilities": {},
|
||||||
|
"ttlMs": 0,
|
||||||
|
"cacheScope": "private",
|
||||||
|
"_meta": {
|
||||||
|
"io.modelcontextprotocol/serverInfo": {
|
||||||
|
"name": "conformance-mock-server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"unrelated": { "preserved": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("discovery response with namespaced server info should deserialize");
|
||||||
|
|
||||||
|
let ServerJsonRpcMessage::Response(JsonRpcResponse { result, .. }) = message else {
|
||||||
|
panic!("expected response");
|
||||||
|
};
|
||||||
|
let ServerResult::DiscoverResult(result) = result else {
|
||||||
|
panic!("expected discovery response, not a tool-call result");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(result.server_info.name, "conformance-mock-server");
|
||||||
|
assert_eq!(result.server_info.version, "1.0.0");
|
||||||
|
|
||||||
|
let metadata = result.meta.expect("discovery metadata should be preserved");
|
||||||
|
assert_eq!(
|
||||||
|
metadata.0.get("io.modelcontextprotocol/serverInfo"),
|
||||||
|
Some(&json!({
|
||||||
|
"name": "conformance-mock-server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
metadata.0.get("unrelated"),
|
||||||
|
Some(&json!({ "preserved": true }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_result_serializes_top_level_server_info() {
|
||||||
|
let result = DiscoverResult::new(
|
||||||
|
vec![ProtocolVersion::V_2026_07_28],
|
||||||
|
rmcp::model::ServerCapabilities::default(),
|
||||||
|
rmcp::model::Implementation::new("test-server", "1.0.0"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let serialized = serde_json::to_value(result).expect("serialize discovery result");
|
||||||
|
assert_eq!(
|
||||||
|
serialized["serverInfo"],
|
||||||
|
json!({
|
||||||
|
"name": "test-server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_result_prefers_top_level_server_info_over_namespaced_metadata() {
|
||||||
|
let result: DiscoverResult = serde_json::from_value(json!({
|
||||||
|
"resultType": "complete",
|
||||||
|
"supportedVersions": ["2026-07-28"],
|
||||||
|
"capabilities": {},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "top-level-server",
|
||||||
|
"version": "2.0.0"
|
||||||
|
},
|
||||||
|
"ttlMs": 0,
|
||||||
|
"cacheScope": "private",
|
||||||
|
"_meta": {
|
||||||
|
"io.modelcontextprotocol/serverInfo": {
|
||||||
|
"name": "metadata-server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"unrelated": true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("top-level server info should remain supported");
|
||||||
|
|
||||||
|
assert_eq!(result.server_info.name, "top-level-server");
|
||||||
|
assert_eq!(result.server_info.version, "2.0.0");
|
||||||
|
assert_eq!(
|
||||||
|
result
|
||||||
|
.meta
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|metadata| metadata.0.get("unrelated")),
|
||||||
|
Some(&json!(true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_result_requires_valid_top_level_or_namespaced_server_info() {
|
||||||
|
let result = json!({
|
||||||
|
"resultType": "complete",
|
||||||
|
"supportedVersions": ["2026-07-28"],
|
||||||
|
"capabilities": {},
|
||||||
|
"ttlMs": 0,
|
||||||
|
"cacheScope": "private",
|
||||||
|
"_meta": { "unrelated": true }
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(serde_json::from_value::<DiscoverResult>(result).is_err());
|
||||||
|
|
||||||
|
let malformed_server_info = json!({
|
||||||
|
"resultType": "complete",
|
||||||
|
"supportedVersions": ["2026-07-28"],
|
||||||
|
"capabilities": {},
|
||||||
|
"ttlMs": 0,
|
||||||
|
"cacheScope": "private",
|
||||||
|
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "missing-version" } }
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(serde_json::from_value::<DiscoverResult>(malformed_server_info).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_protocol_version_error_matches_draft_schema() {
|
fn unsupported_protocol_version_error_matches_draft_schema() {
|
||||||
let error = ErrorData::unsupported_protocol_version(
|
let error = ErrorData::unsupported_protocol_version(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue