Skip to content
Open
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ let client = ClientInfo::default()
)
.await?;

// Or probe the discover lifecycle and fall back when a legacy server reports
// that server/discover is not implemented.
// Or probe the discover lifecycle and fall back when the response does not
// positively identify a modern server.
let client = ClientInfo::default()
.serve_with_lifecycle(
transport,
Expand All @@ -127,7 +127,9 @@ let client = ClientInfo::default()
`ClientLifecycleMode::Initialize` is equivalent to the existing `serve()` behavior.
Discover startup does not send `notifications/initialized`; discovery completes
startup, and each subsequent request carries its protocol version, client
information, and capabilities in `_meta`.
information, and capabilities in `_meta`. Auto mode preserves authentication,
transport, and recognized modern-protocol errors instead of treating them as
legacy-server signals.

### Build a Server

Expand Down
13 changes: 12 additions & 1 deletion crates/rmcp/src/service/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ where
match discover_result {
Ok(()) => {}
Err(ClientInitializeError::JsonRpcError(error))
if error.code == crate::model::ErrorCode::METHOD_NOT_FOUND =>
if !is_modern_server_json_rpc_error(&error) =>
{
let mut legacy_info = client_info;
if let Some(version) = legacy_version {
Expand All @@ -756,6 +756,17 @@ where
Ok(serve_inner(service, transport, peer, peer_rx, ct))
}

// Only specification-defined modern errors prove that the peer understands
// per-request metadata; other JSON-RPC errors are legacy fallback signals.
fn is_modern_server_json_rpc_error(error: &ErrorData) -> bool {
matches!(
error.code,
crate::model::ErrorCode::UNSUPPORTED_PROTOCOL_VERSION
| crate::model::ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY
| crate::model::ErrorCode::HEADER_MISMATCH
)
}

async fn legacy_startup<S, T>(
service: &S,
transport: &mut T,
Expand Down
118 changes: 85 additions & 33 deletions crates/rmcp/tests/test_client_lifecycle_modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,7 @@ async fn discover_startup_omits_initialize() {
server_task.await.expect("server task");
}

#[tokio::test]
async fn auto_startup_falls_back_after_discover_method_not_found() {
async fn run_auto_discover_error_scenario(error: ErrorData, expect_fallback: bool) {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let mut server = IntoTransport::<rmcp::RoleServer, _, _>::into_transport(server_transport);
let server_task = tokio::spawn(async move {
Expand All @@ -292,51 +291,104 @@ async fn auto_startup_falls_back_after_discover_method_not_found() {
ClientRequest::DiscoverRequest(_)
));
server
.send(ServerJsonRpcMessage::error(
ErrorData::new(ErrorCode::METHOD_NOT_FOUND, "Method not found", None),
Some(discover.id),
))
.send(ServerJsonRpcMessage::error(error, Some(discover.id)))
.await
.expect("send method-not-found");
.expect("send discover error");

let ClientJsonRpcMessage::Request(initialize) =
server.receive().await.expect("expected initialize request")
else {
panic!("expected request");
};
assert!(matches!(
initialize.request,
ClientRequest::InitializeRequest(_)
));
server
.send(ServerJsonRpcMessage::response(
ServerResult::InitializeResult(
InitializeResult::new(ServerCapabilities::default()),
),
initialize.id,
))
.await
.expect("send initialize response");
assert!(matches!(
server.receive().await,
Some(ClientJsonRpcMessage::Notification(_))
));
if expect_fallback {
let ClientJsonRpcMessage::Request(initialize) =
server.receive().await.expect("expected initialize request")
else {
panic!("expected request");
};
assert!(matches!(
initialize.request,
ClientRequest::InitializeRequest(_)
));
server
.send(ServerJsonRpcMessage::response(
ServerResult::InitializeResult(InitializeResult::new(
ServerCapabilities::default(),
)),
initialize.id,
))
.await
.expect("send initialize response");
assert!(matches!(
server.receive().await,
Some(ClientJsonRpcMessage::Notification(_))
));
} else if let Some(message) = server.receive().await {
panic!("modern discover error should close the client, got {message:?}");
}
});

let client = DiscoverClient
let client_result = DiscoverClient
.serve_with_lifecycle(
client_transport,
ClientLifecycleMode::Auto {
preferred_versions: vec![ProtocolVersion::V_2026_07_28],
legacy_version: Some(ProtocolVersion::V_2025_11_25),
},
)
.await
.expect("auto client should fall back");
client.cancel().await.expect("cancel client");
.await;

if expect_fallback {
let client = client_result.expect("auto client should fall back");
client.cancel().await.expect("cancel client");
} else {
assert!(
client_result.is_err(),
"modern discover error should be surfaced"
);
}
server_task.await.expect("server task");
}

#[tokio::test]
async fn auto_startup_falls_back_after_discover_method_not_found() {
run_auto_discover_error_scenario(
ErrorData::new(ErrorCode::METHOD_NOT_FOUND, "Method not found", None),
true,
)
.await;
}

#[tokio::test]
async fn auto_startup_falls_back_after_discover_implementation_defined_error() {
run_auto_discover_error_scenario(
ErrorData::new(
ErrorCode(-32000),
"Bad Request: The MCP-Protocol-Version header value '2026-07-28' is not supported.",
None,
),
true,
)
.await;
}

#[tokio::test]
async fn auto_startup_does_not_fall_back_after_missing_required_capability() {
run_auto_discover_error_scenario(
ErrorData::new(
ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY,
"Missing required client capability",
None,
),
false,
)
.await;
}

#[tokio::test]
async fn auto_startup_does_not_fall_back_after_header_mismatch() {
run_auto_discover_error_scenario(
ErrorData::new(ErrorCode::HEADER_MISMATCH, "Header mismatch", None),
false,
)
.await;
}

#[tokio::test]
async fn discover_startup_retries_a_mutually_supported_version() {
let unsupported: ProtocolVersion =
Expand Down