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:
John Howard 2026-07-28 14:55:40 -05:00 • committed by GitHub
commit 9a41811a66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 373 additions and 155 deletions

View file

@ -1085,6 +1085,66 @@ impl InitializeResult {
pub type ServerInfo = InitializeResult;
pub type ClientInfo = InitializeRequestParams;
/// Information negotiated about a server peer.
///
/// Unlike [`InitializeResult`], the server implementation identity is optional
/// because discovery responses are not required to provide it.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ServerPeerInfo {
/// The negotiated MCP protocol version.
pub protocol_version: ProtocolVersion,
/// The capabilities this server provides.
pub capabilities: ServerCapabilities,
/// Information about the server implementation, when provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub server_info: Option<Implementation>,
/// Optional human-readable instructions about using this server.
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
/// Protocol-level response metadata.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<MetaObject>,
}
impl ServerPeerInfo {
/// Create peer information without a server implementation identity.
pub fn new(protocol_version: ProtocolVersion, capabilities: ServerCapabilities) -> Self {
Self {
protocol_version,
capabilities,
server_info: None,
instructions: None,
meta: None,
}
}
/// Set the server implementation identity.
pub fn with_server_info(mut self, server_info: Implementation) -> Self {
self.server_info = Some(server_info);
self
}
/// Set instructions supplied by the server.
pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
}
impl From<InitializeResult> for ServerPeerInfo {
fn from(result: InitializeResult) -> Self {
Self {
protocol_version: result.protocol_version,
capabilities: result.capabilities,
server_info: Some(result.server_info),
instructions: result.instructions,
meta: result.meta,
}
}
}
const_string!(DiscoverRequestMethod = "server/discover");
/// Parameters for [`DiscoverRequest`].
@ -1116,9 +1176,9 @@ impl schemars::JsonSchema for DiscoverRequestParams {
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
/// The server's response to a [`DiscoverRequest`].
#[derive(Debug, Serialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct DiscoverResult {
/// Identifies how the result should be parsed.
@ -1127,8 +1187,6 @@ pub struct DiscoverResult {
pub supported_versions: Vec<ProtocolVersion>,
/// Capabilities provided by this server.
pub capabilities: ServerCapabilities,
/// Information about the server implementation.
pub server_info: Implementation,
/// Optional guidance for using the server.
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
@ -1141,65 +1199,15 @@ pub struct DiscoverResult {
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 {
const SERVER_INFO_META_KEY: &str = "io.modelcontextprotocol/serverInfo";
/// Create a non-cacheable private discovery result.
pub fn new(
supported_versions: Vec<ProtocolVersion>,
capabilities: ServerCapabilities,
server_info: Implementation,
) -> Self {
pub fn new(supported_versions: Vec<ProtocolVersion>, capabilities: ServerCapabilities) -> Self {
Self {
result_type: ResultType::COMPLETE,
supported_versions,
capabilities,
server_info,
instructions: None,
ttl_ms: 0,
cache_scope: CacheScope::Private,
@ -1207,6 +1215,31 @@ impl DiscoverResult {
}
}
/// Return the server implementation information stored in result metadata.
pub fn server_info(&self) -> Option<Implementation> {
self.meta
.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.
pub fn set_server_info(&mut self, server_info: Implementation) {
let 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.
pub fn with_server_info(mut self, server_info: Implementation) -> Self {
self.set_server_info(server_info);
self
}
/// Create a discovery result from the server's initialization information.
pub fn from_server_info(
supported_versions: Vec<ProtocolVersion>,
@ -1219,9 +1252,16 @@ impl DiscoverResult {
meta,
..
} = server_info;
let mut result = Self::new(supported_versions, capabilities, server_info);
result.instructions = instructions;
result.meta = meta;
let mut result = Self {
result_type: ResultType::COMPLETE,
supported_versions,
capabilities,
instructions,
ttl_ms: 0,
cache_scope: CacheScope::Private,
meta,
};
result.set_server_info(server_info);
result
}
@ -1238,6 +1278,20 @@ impl DiscoverResult {
}
}
impl ServerPeerInfo {
/// Create peer information from a discovery result and the selected version.
pub fn from_discover_result(protocol_version: ProtocolVersion, result: DiscoverResult) -> Self {
let server_info = result.server_info();
Self {
protocol_version,
capabilities: result.capabilities,
server_info,
instructions: result.instructions,
meta: result.meta,
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for ServerInfo {
fn default() -> Self {

View file

@ -374,9 +374,10 @@ impl schemars::JsonSchema for MetaObject {
/// - `io.modelcontextprotocol/clientCapabilities` (SEP-2575)
/// - `io.modelcontextprotocol/logLevel` (SEP-2575)
///
/// The 2026-07-28 schema defines required per-request metadata; earlier
/// protocol versions do not know these keys. All keys therefore stay optional
/// at runtime and in the generated (version-shared) JSON schema — use
/// The 2026-07-28 draft schema requires the protocol-version and
/// client-capabilities keys; client-info is optional. Earlier protocol versions
/// do not know them. All keys therefore stay optional at runtime and in the
/// generated (version-shared) JSON schema — use
/// [`RequestMetaObject::missing_required_keys`] to validate a request against
/// the negotiated protocol version.
///
@ -395,10 +396,9 @@ impl RequestMetaObject {
const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
/// Request `_meta` keys validated for the 2026-07-28 protocol.
pub const DRAFT_REQUIRED_KEYS: [&str; 3] = [
/// Request `_meta` keys the 2026-07-28 draft schema marks as required.
pub const DRAFT_REQUIRED_KEYS: [&str; 2] = [
Self::META_KEY_PROTOCOL_VERSION,
Self::META_KEY_CLIENT_INFO,
Self::META_KEY_CLIENT_CAPABILITIES,
];
@ -523,9 +523,6 @@ impl RequestMetaObject {
if self.protocol_version().is_none() {
missing.push(Self::META_KEY_PROTOCOL_VERSION);
}
if self.client_info().is_none() {
missing.push(Self::META_KEY_CLIENT_INFO);
}
if self.client_capabilities().is_none() {
missing.push(Self::META_KEY_CLIENT_CAPABILITIES);
}
@ -838,7 +835,6 @@ mod tests {
fn treats_malformed_values_as_missing() {
let meta: RequestMetaObject = serde_json::from_value(serde_json::json!({
"io.modelcontextprotocol/protocolVersion": 123,
"io.modelcontextprotocol/clientInfo": "not an implementation",
"io.modelcontextprotocol/clientCapabilities": null,
}))
.unwrap();

View file

@ -25,7 +25,7 @@ use crate::{
NumberOrString, PaginatedRequestParams, ProgressNotification, ProgressNotificationParam,
ProtocolVersion, ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse,
ReadResourceResult, Reference, RequestId, RequestMetaObject, RootsListChangedNotification,
ServerInfo, ServerJsonRpcMessage, ServerNotification, ServerRequest, ServerResult,
ServerJsonRpcMessage, ServerNotification, ServerPeerInfo, ServerRequest, ServerResult,
SetLevelRequest, SetLevelRequestParams, SubscribeRequest, SubscribeRequestParams,
SubscriptionFilter, SubscriptionsListenRequest, SubscriptionsListenRequestParams,
SubscriptionsListenResult, UnsubscribeRequest, UnsubscribeRequestParams, UpdateTaskParams,
@ -213,7 +213,7 @@ impl ServiceRole for RoleClient {
type PeerResp = ServerResult;
type PeerNot = ServerNotification;
type Info = ClientInfo;
type PeerInfo = ServerInfo;
type PeerInfo = ServerPeerInfo;
type InitializeError = ClientInitializeError;
const IS_CLIENT: bool = true;
@ -776,7 +776,7 @@ where
let ServerResult::InitializeResult(initialize_result) = response else {
return Err(ClientInitializeError::ExpectedInitResult(Some(response)));
};
peer.set_peer_info(initialize_result);
peer.set_peer_info(initialize_result.into());
// send notification
let notification = ClientJsonRpcMessage::notification(
@ -846,13 +846,10 @@ where
server_supported: result.supported_versions,
});
};
peer.set_peer_info(ServerInfo {
protocol_version: selected.clone(),
capabilities: result.capabilities,
server_info: result.server_info,
instructions: result.instructions,
meta: result.meta,
});
peer.set_peer_info(ServerPeerInfo::from_discover_result(
selected.clone(),
result,
));
peer.set_client_request_metadata(ClientRequestMetadata {
protocol_version: selected,
client_info: client_info.client_info.clone(),
@ -2197,13 +2194,10 @@ mod tests {
let peer = disconnected_peer();
let meta = RequestMetaObject::default();
let key = discover_cache_key();
let expected = DiscoverResult::new(
vec![ProtocolVersion::default()],
Default::default(),
crate::model::Implementation::from_build_env(),
)
.with_ttl_ms(5_000)
.with_cache_scope(CacheScope::Public);
let expected = DiscoverResult::new(vec![ProtocolVersion::default()], Default::default())
.with_server_info(crate::model::Implementation::from_build_env())
.with_ttl_ms(5_000)
.with_cache_scope(CacheScope::Public);
peer.cache_response(
key,
ServerResult::DiscoverResult(expected.clone()),