Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true

env:
CONFORMANCE_VERSION: "0.2.0-alpha.9"
CONFORMANCE_VERSION: "0.2.0-alpha.10"

jobs:
server:
Expand Down
3 changes: 3 additions & 0 deletions conformance/expected-failures-extensions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ server: []
client:
# Informational OAuth extension scenarios.
- auth/enterprise-managed-authorization
- auth/dpop
- auth/dpop-nonce
- auth/wif-jwt-bearer
174 changes: 114 additions & 60 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down Expand Up @@ -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.
Expand All @@ -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>,
Expand All @@ -1141,72 +1199,47 @@ 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,
meta: None,
}
}

/// 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>,
Expand All @@ -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
}

Expand All @@ -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 {
Expand Down
16 changes: 6 additions & 10 deletions crates/rmcp/src/model/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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,
];

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down
28 changes: 11 additions & 17 deletions crates/rmcp/src/service/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()),
Expand Down
8 changes: 8 additions & 0 deletions crates/rmcp/tests/test_client_initialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Loading