fix!: remove server_info from DiscoverResult (#1065)
Fixes https://github.com/modelcontextprotocol/rust-sdk/issues/1064 Replaces https://github.com/modelcontextprotocol/rust-sdk/pull/1044 This removes the `server_info` field which is not part of the spec: https://modelcontextprotocol.io/specification/draft/schema#discoverresult.
This commit is contained in:
parent
e54c2220af
commit
9a41811a66
13 changed files with 373 additions and 155 deletions
|
|
@ -51,6 +51,14 @@ async fn client_initialization_accepts_stringified_numeric_response_id() {
|
|||
.serve(client_transport)
|
||||
.await
|
||||
.expect("client should accept stringified initialize response ID");
|
||||
assert!(
|
||||
client
|
||||
.peer_info()
|
||||
.expect("peer info should be retained")
|
||||
.server_info
|
||||
.is_some(),
|
||||
"initialize always provides a server implementation identity"
|
||||
);
|
||||
client.cancel().await.expect("cancel client");
|
||||
server_task.await.expect("server task");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,11 +36,13 @@ async fn discover_startup_accepts_stringified_numeric_response_id() {
|
|||
};
|
||||
server
|
||||
.send(ServerJsonRpcMessage::response(
|
||||
ServerResult::DiscoverResult(DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
Implementation::new("discover-server", "1.0.0"),
|
||||
)),
|
||||
ServerResult::DiscoverResult(
|
||||
DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
)
|
||||
.with_server_info(Implementation::new("discover-server", "1.0.0")),
|
||||
),
|
||||
RequestId::String(response_id.to_string().into()),
|
||||
))
|
||||
.await
|
||||
|
|
@ -60,6 +62,110 @@ async fn discover_startup_accepts_stringified_numeric_response_id() {
|
|||
server_task.await.expect("server task");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[allow(deprecated)]
|
||||
async fn discover_startup_accepts_missing_optional_server_info() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let mut server = IntoTransport::<rmcp::RoleServer, _, _>::into_transport(server_transport);
|
||||
let (rejection_observed_tx, rejection_observed_rx) = tokio::sync::oneshot::channel();
|
||||
let server_task = tokio::spawn(async move {
|
||||
let ClientJsonRpcMessage::Request(discover_request) =
|
||||
server.receive().await.expect("expected discover request")
|
||||
else {
|
||||
panic!("expected discover request");
|
||||
};
|
||||
let mut result = DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::builder().enable_tools().build(),
|
||||
);
|
||||
result.instructions = Some("discovery instructions".into());
|
||||
result.meta = Some(rmcp::model::MetaObject::new());
|
||||
result
|
||||
.meta
|
||||
.as_mut()
|
||||
.expect("metadata")
|
||||
.0
|
||||
.insert("example.test/key".into(), serde_json::json!(7));
|
||||
server
|
||||
.send(ServerJsonRpcMessage::response(
|
||||
ServerResult::DiscoverResult(result),
|
||||
discover_request.id,
|
||||
))
|
||||
.await
|
||||
.expect("send discover response");
|
||||
|
||||
server
|
||||
.send(ServerJsonRpcMessage::request(
|
||||
rmcp::model::ServerRequest::CreateMessageRequest(
|
||||
rmcp::model::CreateMessageRequest::new(
|
||||
rmcp::model::CreateMessageRequestParams::new(
|
||||
vec![rmcp::model::SamplingMessage::user_text("unsolicited")],
|
||||
16,
|
||||
),
|
||||
),
|
||||
),
|
||||
RequestId::Number(99),
|
||||
))
|
||||
.await
|
||||
.expect("send unsolicited server request");
|
||||
let Some(ClientJsonRpcMessage::Error(error)) = server.receive().await else {
|
||||
panic!("expected unsolicited server request to be rejected");
|
||||
};
|
||||
assert_eq!(error.error.code, ErrorCode::INVALID_PARAMS);
|
||||
rejection_observed_tx
|
||||
.send(())
|
||||
.expect("signal observed rejection");
|
||||
|
||||
let ClientJsonRpcMessage::Request(request) =
|
||||
server.receive().await.expect("expected normal request")
|
||||
else {
|
||||
panic!("expected normal request");
|
||||
};
|
||||
assert_eq!(
|
||||
request.request.get_meta().protocol_version(),
|
||||
Some(ProtocolVersion::V_2026_07_28)
|
||||
);
|
||||
server
|
||||
.send(ServerJsonRpcMessage::response(
|
||||
ServerResult::ListToolsResult(Default::default()),
|
||||
request.id,
|
||||
))
|
||||
.await
|
||||
.expect("send list tools response");
|
||||
});
|
||||
|
||||
let client = DiscoverClient
|
||||
.serve_with_lifecycle(
|
||||
client_transport,
|
||||
ClientLifecycleMode::Discover {
|
||||
preferred_versions: vec![ProtocolVersion::V_2026_07_28],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("missing optional server info should not fail discovery");
|
||||
let peer_info = client.peer_info().expect("peer info should be retained");
|
||||
assert_eq!(peer_info.protocol_version, ProtocolVersion::V_2026_07_28);
|
||||
assert!(peer_info.capabilities.tools.is_some());
|
||||
assert_eq!(peer_info.server_info, None);
|
||||
assert_eq!(
|
||||
peer_info.instructions.as_deref(),
|
||||
Some("discovery instructions")
|
||||
);
|
||||
assert_eq!(
|
||||
peer_info
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.0.get("example.test/key")),
|
||||
Some(&serde_json::json!(7))
|
||||
);
|
||||
rejection_observed_rx
|
||||
.await
|
||||
.expect("server should observe association rejection");
|
||||
client.list_tools(None).await.expect("list tools");
|
||||
client.cancel().await.expect("cancel client");
|
||||
server_task.await.expect("server task");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn high_level_server_accepts_discover_startup_without_initialize() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
|
@ -103,11 +209,13 @@ async fn discover_startup_omits_initialize() {
|
|||
|
||||
server
|
||||
.send(ServerJsonRpcMessage::response(
|
||||
ServerResult::DiscoverResult(DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
Implementation::new("discover-server", "1.0.0"),
|
||||
)),
|
||||
ServerResult::DiscoverResult(
|
||||
DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
)
|
||||
.with_server_info(Implementation::new("discover-server", "1.0.0")),
|
||||
),
|
||||
request.id,
|
||||
))
|
||||
.await
|
||||
|
|
@ -267,11 +375,13 @@ async fn discover_startup_retries_a_mutually_supported_version() {
|
|||
);
|
||||
server
|
||||
.send(ServerJsonRpcMessage::response(
|
||||
ServerResult::DiscoverResult(DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
Implementation::new("discover-server", "1.0.0"),
|
||||
)),
|
||||
ServerResult::DiscoverResult(
|
||||
DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
)
|
||||
.with_server_info(Implementation::new("discover-server", "1.0.0")),
|
||||
),
|
||||
second.id,
|
||||
))
|
||||
.await
|
||||
|
|
@ -326,11 +436,13 @@ async fn discover_startup_retries_current_version_once_when_server_reports_it_su
|
|||
);
|
||||
server
|
||||
.send(ServerJsonRpcMessage::response(
|
||||
ServerResult::DiscoverResult(DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
Implementation::new("discover-server", "1.0.0"),
|
||||
)),
|
||||
ServerResult::DiscoverResult(
|
||||
DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::default(),
|
||||
)
|
||||
.with_server_info(Implementation::new("discover-server", "1.0.0")),
|
||||
),
|
||||
second.id,
|
||||
))
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -121,6 +121,13 @@ mod tests {
|
|||
let schema = settings
|
||||
.into_generator()
|
||||
.into_root_schema_for::<ServerJsonRpcMessage>();
|
||||
let schema_value =
|
||||
serde_json::to_value(&schema).expect("Failed to serialize server schema");
|
||||
let discover_result = &schema_value["definitions"]["DiscoverResult"];
|
||||
assert!(
|
||||
discover_result["properties"].get("serverInfo").is_none(),
|
||||
"DiscoverResult serverInfo belongs in namespaced _meta"
|
||||
);
|
||||
let schema_str = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema");
|
||||
|
||||
compare_schemas(
|
||||
|
|
|
|||
|
|
@ -2255,7 +2255,6 @@
|
|||
"additionalProperties": true,
|
||||
"required": [
|
||||
"io.modelcontextprotocol/protocolVersion",
|
||||
"io.modelcontextprotocol/clientInfo",
|
||||
"io.modelcontextprotocol/clientCapabilities"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2255,7 +2255,6 @@
|
|||
"additionalProperties": true,
|
||||
"required": [
|
||||
"io.modelcontextprotocol/protocolVersion",
|
||||
"io.modelcontextprotocol/clientInfo",
|
||||
"io.modelcontextprotocol/clientCapabilities"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -759,14 +759,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"serverInfo": {
|
||||
"description": "Information about the server implementation.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Implementation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"supportedVersions": {
|
||||
"description": "Protocol versions implemented by this server.",
|
||||
"type": "array",
|
||||
|
|
@ -785,7 +777,6 @@
|
|||
"resultType",
|
||||
"supportedVersions",
|
||||
"capabilities",
|
||||
"serverInfo",
|
||||
"ttlMs",
|
||||
"cacheScope"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -759,14 +759,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"serverInfo": {
|
||||
"description": "Information about the server implementation.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Implementation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"supportedVersions": {
|
||||
"description": "Protocol versions implemented by this server.",
|
||||
"type": "array",
|
||||
|
|
@ -785,7 +777,6 @@
|
|||
"resultType",
|
||||
"supportedVersions",
|
||||
"capabilities",
|
||||
"serverInfo",
|
||||
"ttlMs",
|
||||
"cacheScope"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ where
|
|||
let client = serve_directly::<RoleClient, _, _, _, _>(
|
||||
MrtrClient,
|
||||
client_transport,
|
||||
Some(client_peer_info),
|
||||
Some(client_peer_info.into()),
|
||||
);
|
||||
|
||||
let result = body(client).await;
|
||||
|
|
@ -580,7 +580,7 @@ async fn request_state_codec_seals_and_verifies_through_the_loop() -> anyhow::Re
|
|||
let client = serve_directly::<RoleClient, _, _, _, _>(
|
||||
MrtrClient,
|
||||
client_transport,
|
||||
Some(server_info(ProtocolVersion::V_2026_07_28)),
|
||||
Some(server_info(ProtocolVersion::V_2026_07_28).into()),
|
||||
);
|
||||
|
||||
let result = client
|
||||
|
|
|
|||
|
|
@ -103,8 +103,9 @@ fn discover_result_accepts_server_info_in_namespaced_metadata() {
|
|||
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 server_info = result.server_info().expect("server info should be present");
|
||||
assert_eq!(server_info.name, "conformance-mock-server");
|
||||
assert_eq!(server_info.version, "1.0.0");
|
||||
|
||||
let metadata = result.meta.expect("discovery metadata should be preserved");
|
||||
assert_eq!(
|
||||
|
|
@ -121,25 +122,42 @@ fn discover_result_accepts_server_info_in_namespaced_metadata() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn discover_result_serializes_top_level_server_info() {
|
||||
let result = DiscoverResult::new(
|
||||
fn discover_result_serializes_server_info_in_namespaced_metadata() {
|
||||
let mut result = DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
rmcp::model::ServerCapabilities::default(),
|
||||
rmcp::model::Implementation::new("test-server", "1.0.0"),
|
||||
);
|
||||
result.meta = Some(rmcp::model::MetaObject(
|
||||
json!({
|
||||
"io.modelcontextprotocol/serverInfo": {
|
||||
"name": "stale-server",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"unrelated": { "preserved": true }
|
||||
})
|
||||
.as_object()
|
||||
.expect("metadata is an object")
|
||||
.clone(),
|
||||
));
|
||||
result.set_server_info(rmcp::model::Implementation::new("test-server", "1.0.0"));
|
||||
|
||||
let serialized = serde_json::to_value(result).expect("serialize discovery result");
|
||||
assert!(serialized.get("serverInfo").is_none());
|
||||
assert_eq!(
|
||||
serialized["serverInfo"],
|
||||
serialized["_meta"]["io.modelcontextprotocol/serverInfo"],
|
||||
json!({
|
||||
"name": "test-server",
|
||||
"version": "1.0.0"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serialized["_meta"]["unrelated"],
|
||||
json!({ "preserved": true })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_result_prefers_top_level_server_info_over_namespaced_metadata() {
|
||||
fn discover_result_ignores_legacy_top_level_server_info() {
|
||||
let result: DiscoverResult = serde_json::from_value(json!({
|
||||
"resultType": "complete",
|
||||
"supportedVersions": ["2026-07-28"],
|
||||
|
|
@ -160,8 +178,11 @@ fn discover_result_prefers_top_level_server_info_over_namespaced_metadata() {
|
|||
}))
|
||||
.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");
|
||||
let server_info = result
|
||||
.server_info()
|
||||
.expect("namespaced server info is present");
|
||||
assert_eq!(server_info.name, "metadata-server");
|
||||
assert_eq!(server_info.version, "1.0.0");
|
||||
assert_eq!(
|
||||
result
|
||||
.meta
|
||||
|
|
@ -172,7 +193,7 @@ fn discover_result_prefers_top_level_server_info_over_namespaced_metadata() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn discover_result_requires_valid_top_level_or_namespaced_server_info() {
|
||||
fn discover_result_allows_missing_or_malformed_optional_server_info() {
|
||||
let result = json!({
|
||||
"resultType": "complete",
|
||||
"supportedVersions": ["2026-07-28"],
|
||||
|
|
@ -182,7 +203,9 @@ fn discover_result_requires_valid_top_level_or_namespaced_server_info() {
|
|||
"_meta": { "unrelated": true }
|
||||
});
|
||||
|
||||
assert!(serde_json::from_value::<DiscoverResult>(result).is_err());
|
||||
let result =
|
||||
serde_json::from_value::<DiscoverResult>(result).expect("server info metadata is optional");
|
||||
assert_eq!(result.server_info(), None);
|
||||
|
||||
let malformed_server_info = json!({
|
||||
"resultType": "complete",
|
||||
|
|
@ -193,7 +216,9 @@ fn discover_result_requires_valid_top_level_or_namespaced_server_info() {
|
|||
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "missing-version" } }
|
||||
});
|
||||
|
||||
assert!(serde_json::from_value::<DiscoverResult>(malformed_server_info).is_err());
|
||||
let result = serde_json::from_value::<DiscoverResult>(malformed_server_info)
|
||||
.expect("opaque metadata should not prevent deserialization");
|
||||
assert_eq!(result.server_info(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ async fn client_discover_helper_returns_typed_result() {
|
|||
.expect("discover should succeed");
|
||||
|
||||
assert_eq!(
|
||||
result.server_info,
|
||||
Implementation::new("discovery-server", "1.0.0")
|
||||
result.server_info(),
|
||||
Some(Implementation::new("discovery-server", "1.0.0"))
|
||||
);
|
||||
client.cancel().await.expect("client should cancel");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,9 +122,11 @@ async fn discover_returns_server_metadata_without_session() {
|
|||
"resultType": "complete",
|
||||
"supportedVersions": ["2025-11-25"],
|
||||
"capabilities": { "tools": {} },
|
||||
"serverInfo": {
|
||||
"name": "discovery-server",
|
||||
"version": "1.0.0"
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/serverInfo": {
|
||||
"name": "discovery-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
"instructions": "Use the tools carefully",
|
||||
"ttlMs": 0,
|
||||
|
|
@ -327,6 +329,38 @@ async fn discover_rejects_missing_client_capabilities() {
|
|||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_accepts_missing_optional_client_info() {
|
||||
let (client, url, cancellation_token) = spawn_server(true).await;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "server/discover",
|
||||
"params": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2025-11-25",
|
||||
"io.modelcontextprotocol/clientCapabilities": {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", "2025-11-25")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should send");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
let body: serde_json::Value = response.json().await.expect("response should be JSON");
|
||||
assert!(body.get("result").is_some());
|
||||
|
||||
cancellation_token.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_error_uses_http_400_when_sse_is_configured() {
|
||||
let (client, url, cancellation_token) = spawn_server(false).await;
|
||||
|
|
|
|||
|
|
@ -262,8 +262,8 @@ impl rmcp::service::Service<RoleServer> for MalformedAcknowledgmentServer {
|
|||
context: RequestContext<RoleServer>,
|
||||
) -> Result<ServerResult, rmcp::ErrorData> {
|
||||
match request {
|
||||
ClientRequest::DiscoverRequest(_) => {
|
||||
Ok(ServerResult::DiscoverResult(DiscoverResult::new(
|
||||
ClientRequest::DiscoverRequest(_) => Ok(ServerResult::DiscoverResult(
|
||||
DiscoverResult::new(
|
||||
vec![ProtocolVersion::V_2026_07_28],
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
|
|
@ -271,9 +271,9 @@ impl rmcp::service::Service<RoleServer> for MalformedAcknowledgmentServer {
|
|||
.enable_prompts()
|
||||
.enable_prompts_list_changed()
|
||||
.build(),
|
||||
Implementation::new("malformed-ack-server", "1.0.0"),
|
||||
)))
|
||||
}
|
||||
)
|
||||
.with_server_info(Implementation::new("malformed-ack-server", "1.0.0")),
|
||||
)),
|
||||
ClientRequest::SubscriptionsListenRequest(_) => {
|
||||
let mut acknowledgment = SubscriptionsAcknowledgedNotification::new(
|
||||
SubscriptionsAcknowledgedNotificationParams::new(
|
||||
|
|
|
|||
|
|
@ -215,7 +215,6 @@ fn subscription_schemas_mark_only_draft_required_fields_as_required() {
|
|||
request_schema["properties"]["_meta"]["required"],
|
||||
json!([
|
||||
"io.modelcontextprotocol/protocolVersion",
|
||||
"io.modelcontextprotocol/clientInfo",
|
||||
"io.modelcontextprotocol/clientCapabilities"
|
||||
])
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue