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
10 changes: 10 additions & 0 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
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(ProtocolVersion::has_initialize)
{
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
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
71 changes: 65 additions & 6 deletions crates/rmcp/tests/test_stateless_server_requests.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
#![cfg(all(feature = "server", not(feature = "local")))]

use std::{
sync::{Arc, Mutex},
borrow::Cow,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};

use rmcp::{
ServerHandler, ServiceExt,
ClientLifecycleMode, ClientServiceExt, ServerHandler, ServiceExt,
model::{
ClientCapabilities, ClientJsonRpcMessage, ClientRequest, DiscoverRequest,
DiscoverRequestParams, ErrorCode, ErrorData, Implementation, ListToolsRequest,
ListToolsResult, NumberOrString, PaginatedRequestParams, ProgressNotificationParam,
ProgressToken, ProtocolVersion, RequestId, RequestMetaObject, ServerJsonRpcMessage,
ServerNotification,
DiscoverRequestMethod, DiscoverRequestParams, DiscoverResult, ErrorCode, ErrorData,
Implementation, ListToolsRequest, ListToolsResult, NumberOrString, PaginatedRequestParams,
ProgressNotificationParam, ProgressToken, ProtocolVersion, RequestId, RequestMetaObject,
ServerJsonRpcMessage, ServerNotification,
},
service::{MaybeSendFuture, RequestContext, RoleServer},
transport::{IntoTransport, Transport},
Expand All @@ -23,6 +27,25 @@ struct StatelessServer;

impl ServerHandler for StatelessServer {}

#[derive(Clone)]
struct LegacyServer {
discover_called: Arc<AtomicBool>,
}

impl ServerHandler for LegacyServer {
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
Cow::Borrowed(ProtocolVersion::known_up_to(&ProtocolVersion::V_2025_11_25))
}

async fn discover(
&self,
_context: RequestContext<RoleServer>,
) -> Result<DiscoverResult, ErrorData> {
self.discover_called.store(true, Ordering::Relaxed);
Err(ErrorData::method_not_found::<DiscoverRequestMethod>())
}
}

fn complete_meta() -> RequestMetaObject {
complete_meta_for("stateless-client")
}
Expand Down Expand Up @@ -123,6 +146,42 @@ async fn stateless_server_rejects_missing_metadata_on_every_request() {
running.cancel().await.expect("cancel server");
}

#[tokio::test]
async fn legacy_only_server_rejects_discovery_before_dispatch() {
let discover_called = Arc::new(AtomicBool::new(false));
let server = LegacyServer {
discover_called: Arc::clone(&discover_called),
};
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_task = tokio::spawn(async move {
server
.serve(server_transport)
.await
.expect("server should start")
});

let client = ()
.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("client should fall back to initialize");

assert!(!discover_called.load(Ordering::Relaxed));

client.cancel().await.expect("cancel client");
server_task
.await
.expect("server task")
.cancel()
.await
.expect("cancel server");
}

#[derive(Clone)]
struct ContextServer {
seen_clients: Arc<Mutex<Vec<String>>>,
Expand Down
Loading