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
12 changes: 11 additions & 1 deletion crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
model::*,
service::{
MaybeSendFuture, NotificationContext, RequestContext, RoleServer, Service, ServiceRole,
SubscriptionContext, negotiate_protocol_version, uses_legacy_lifecycle,
SubscriptionContext, is_legacy_version, negotiate_protocol_version, uses_legacy_lifecycle,
},
};

Expand Down Expand Up @@ -61,6 +61,16 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
let requested_version = context.meta.protocol_version();
let uses_inline_negotiation = !matches!(&request, ClientRequest::InitializeRequest(_));
// Legacy-only servers do not implement discovery. MethodNotFound tells
// dual-lifecycle clients to fall back to initialize.
if matches!(&request, ClientRequest::DiscoverRequest(_))
&& self
.supported_protocol_versions()
.iter()
.all(is_legacy_version)
{
return Err(McpError::method_not_found::<DiscoverRequestMethod>());
}
if uses_inline_negotiation && let Some(requested_version) = requested_version.as_ref() {
let supported_versions = self.supported_protocol_versions();
if !supported_versions.contains(requested_version) {
Expand Down
139 changes: 88 additions & 51 deletions crates/rmcp/src/service/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,70 +564,107 @@ where

// Get initialize request; the MCP spec permits ping before initialize.
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
let (request, id) = loop {
let (initialize_request, id) = loop {
let msg = expect_next_message(&mut transport, "initialize request").await?;
match msg {
ClientJsonRpcMessage::Request(req)
if matches!(req.request, ClientRequest::PingRequest(_)) =>
{
transport
.send(ServerJsonRpcMessage::response(
ServerResult::EmptyResult(EmptyResult {}),
req.id,
))
.await
.map_err(|error| {
ServerInitializeError::transport::<T>(
error,
"sending pre-init ping response",
)
})?;
}
ClientJsonRpcMessage::Request(req) => break (req.request, req.id),
let request = match msg {
ClientJsonRpcMessage::Request(request) => request,
other => {
return Err(ServerInitializeError::ExpectedInitializeRequest(Some(
other,
)));
}
};

if matches!(&request.request, ClientRequest::PingRequest(_)) {
transport
.send(ServerJsonRpcMessage::response(
ServerResult::EmptyResult(EmptyResult {}),
request.id,
))
.await
.map_err(|error| {
ServerInitializeError::transport::<T>(error, "sending pre-init ping response")
})?;
continue;
}
};

let initialize_request = match request {
ClientRequest::InitializeRequest(request) => request,
request => {
let missing_metadata = request
.get_meta()
.missing_required_keys(&ProtocolVersion::V_2026_07_28);
if !missing_metadata.is_empty() {
transport
.send(ServerJsonRpcMessage::error(
missing_request_metadata_error(&missing_metadata),
Some(id.clone()),
))
.await
.map_err(|error| {
let id = request.id;
match request.request {
ClientRequest::InitializeRequest(request) => break (request, id),
mut request => {
let missing_metadata = request
.get_meta()
.missing_required_keys(&ProtocolVersion::V_2026_07_28);
if !missing_metadata.is_empty() {
transport
.send(ServerJsonRpcMessage::error(
missing_request_metadata_error(&missing_metadata),
Some(id.clone()),
))
.await
.map_err(|error| {
ServerInitializeError::transport::<T>(
error,
"sending pre-init metadata error response",
)
})?;
return Err(ServerInitializeError::ExpectedInitializeRequest(Some(
ClientJsonRpcMessage::request(request, id),
)));
}

let (peer, peer_rx) = Peer::new(id_provider.clone(), None);
if matches!(&request, ClientRequest::DiscoverRequest(_)) {
let context = RequestContext {
ct: ct.child_token(),
id: id.clone(),
meta: std::mem::take(request.get_meta_mut()),
extensions: std::mem::take(request.extensions_mut()),
peer: peer.clone(),
};
let response = match service.handle_request(request, context).await {
Ok(result) => ServerJsonRpcMessage::response(result, id),
Err(error) => {
transport
.send(ServerJsonRpcMessage::error(error, Some(id)))
.await
.map_err(|error| {
ServerInitializeError::transport::<T>(
error,
"sending rejected discover response",
)
})?;
continue;
}
};

peer.require_request_metadata();
transport.send(response).await.map_err(|error| {
ServerInitializeError::transport::<T>(
error,
"sending pre-init metadata error response",
"sending negotiated request response",
)
})?;
return Err(ServerInitializeError::ExpectedInitializeRequest(Some(
ClientJsonRpcMessage::request(request, id),
)));
return Ok(serve_inner(
service,
transport,
peer,
peer_rx,
VecDeque::new(),
ct,
));
}

peer.require_request_metadata();
return Ok(serve_inner(
service,
transport,
peer,
peer_rx,
VecDeque::from([ClientJsonRpcMessage::request(request, id)]),
ct,
));
}
let (peer, peer_rx) = Peer::new(id_provider, None);
peer.require_request_metadata();
// Dispatch the request from inside the service loop rather than
// inline: its handler may send notifications through `peer`, which
// only complete once the loop drains `peer_rx`.
return Ok(serve_inner(
service,
transport,
peer,
peer_rx,
VecDeque::from([ClientJsonRpcMessage::request(request, id)]),
ct,
));
}
};
let requested_protocol_version = initialize_request.params.protocol_version.clone();
Expand Down
4 changes: 2 additions & 2 deletions crates/rmcp/tests/test_cancelled_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use rmcp::{
model::{
CancelledNotification, CancelledNotificationParam, ClientJsonRpcMessage, ClientRequest,
ClientResult, ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction,
ElicitationSchema, PingRequest, RequestId, ServerJsonRpcMessage, ServerNotification,
ServerRequest, ServerResult,
ElicitationSchema, InitializeResult, PingRequest, RequestId, ServerJsonRpcMessage,
ServerNotification, ServerRequest, ServerResult,
},
service::{PeerRequestOptions, QuitReason, serve_directly},
transport::{IntoTransport, Transport},
Expand Down
16 changes: 8 additions & 8 deletions crates/rmcp/tests/test_server_discover_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl ServerHandler for DiscoveryServer {
}

fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
Cow::Borrowed(&[ProtocolVersion::V_2025_11_25])
Cow::Borrowed(&[ProtocolVersion::V_2025_11_25, ProtocolVersion::V_2026_07_28])
}
}

Expand Down Expand Up @@ -120,7 +120,7 @@ async fn discover_returns_server_metadata_without_session() {
body["result"],
json!({
"resultType": "complete",
"supportedVersions": ["2025-11-25"],
"supportedVersions": ["2025-11-25", "2026-07-28"],
"capabilities": { "tools": {} },
"_meta": {
"io.modelcontextprotocol/serverInfo": {
Expand Down Expand Up @@ -153,16 +153,16 @@ async fn discover_does_not_require_initialization_in_legacy_session_mode() {
async fn discover_rejects_unsupported_version_with_http_400() {
let (client, url, cancellation_token) = spawn_server(true).await;

let response = post_discover(&client, &url, "2026-07-28", Some("2026-07-28")).await;
let response = post_discover(&client, &url, "2099-01-01", Some("2099-01-01")).await;

assert_eq!(response.status(), 400);
let body: serde_json::Value = response.json().await.expect("response should be JSON");
assert_eq!(body["error"]["code"], -32022);
assert_eq!(
body["error"]["data"],
json!({
"requested": "2026-07-28",
"supported": ["2025-11-25"]
"requested": "2099-01-01",
"supported": ["2025-11-25", "2026-07-28"]
})
);

Expand Down Expand Up @@ -191,7 +191,7 @@ async fn regular_request_rejects_server_unsupported_meta_version() {
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
"io.modelcontextprotocol/protocolVersion": "2099-01-01"
}
}
});
Expand All @@ -200,7 +200,7 @@ async fn regular_request_rejects_server_unsupported_meta_version() {
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream")
.header("MCP-Protocol-Version", "2026-07-28")
.header("MCP-Protocol-Version", "2099-01-01")
.header("Mcp-Method", "tools/list")
.json(&body)
.send()
Expand Down Expand Up @@ -365,7 +365,7 @@ async fn discover_accepts_missing_optional_client_info() {
async fn discover_error_uses_http_400_when_sse_is_configured() {
let (client, url, cancellation_token) = spawn_server(false).await;

let response = post_discover(&client, &url, "2026-07-28", Some("2026-07-28")).await;
let response = post_discover(&client, &url, "2099-01-01", Some("2099-01-01")).await;

assert_eq!(response.status(), 400);
assert_eq!(
Expand Down
Loading