Skip to content

Commit 13e4798

Browse files
Julien CôtéCopilot
andcommitted
fix: reject discovery for legacy-only servers
Servers that advertise only initialize-based protocol versions cannot establish the discovery lifecycle. Return MethodNotFound before dispatching `server/discover` so Auto clients fall back to `initialize` without invoking the discovery handler. Update discovery HTTP fixtures to advertise a discovery-capable version and cover the legacy-only fallback path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 79437f2 commit 13e4798

3 files changed

Lines changed: 83 additions & 14 deletions

File tree

‎crates/rmcp/src/handler/server.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ impl<H: ServerHandler> Service<RoleServer> for H {
6161
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
6262
let requested_version = context.meta.protocol_version();
6363
let uses_inline_negotiation = !matches!(&request, ClientRequest::InitializeRequest(_));
64+
// Legacy-only servers do not implement discovery. MethodNotFound tells
65+
// dual-lifecycle clients to fall back to initialize.
66+
if matches!(&request, ClientRequest::DiscoverRequest(_))
67+
&& self
68+
.supported_protocol_versions()
69+
.iter()
70+
.all(ProtocolVersion::has_initialize)
71+
{
72+
return Err(McpError::method_not_found::<DiscoverRequestMethod>());
73+
}
6474
if uses_inline_negotiation && let Some(requested_version) = requested_version.as_ref() {
6575
let supported_versions = self.supported_protocol_versions();
6676
if !supported_versions.contains(requested_version) {

‎crates/rmcp/tests/test_server_discover_http.rs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl ServerHandler for DiscoveryServer {
2727
}
2828

2929
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
30-
Cow::Borrowed(&[ProtocolVersion::V_2025_11_25])
30+
Cow::Borrowed(&[ProtocolVersion::V_2025_11_25, ProtocolVersion::V_2026_07_28])
3131
}
3232
}
3333

@@ -120,7 +120,7 @@ async fn discover_returns_server_metadata_without_session() {
120120
body["result"],
121121
json!({
122122
"resultType": "complete",
123-
"supportedVersions": ["2025-11-25"],
123+
"supportedVersions": ["2025-11-25", "2026-07-28"],
124124
"capabilities": { "tools": {} },
125125
"_meta": {
126126
"io.modelcontextprotocol/serverInfo": {
@@ -153,16 +153,16 @@ async fn discover_does_not_require_initialization_in_legacy_session_mode() {
153153
async fn discover_rejects_unsupported_version_with_http_400() {
154154
let (client, url, cancellation_token) = spawn_server(true).await;
155155

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

158158
assert_eq!(response.status(), 400);
159159
let body: serde_json::Value = response.json().await.expect("response should be JSON");
160160
assert_eq!(body["error"]["code"], -32022);
161161
assert_eq!(
162162
body["error"]["data"],
163163
json!({
164-
"requested": "2026-07-28",
165-
"supported": ["2025-11-25"]
164+
"requested": "2099-01-01",
165+
"supported": ["2025-11-25", "2026-07-28"]
166166
})
167167
);
168168

@@ -191,7 +191,7 @@ async fn regular_request_rejects_server_unsupported_meta_version() {
191191
"method": "tools/list",
192192
"params": {
193193
"_meta": {
194-
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
194+
"io.modelcontextprotocol/protocolVersion": "2099-01-01"
195195
}
196196
}
197197
});
@@ -200,7 +200,7 @@ async fn regular_request_rejects_server_unsupported_meta_version() {
200200
.post(&url)
201201
.header("Content-Type", "application/json")
202202
.header("Accept", "application/json, text/event-stream")
203-
.header("MCP-Protocol-Version", "2026-07-28")
203+
.header("MCP-Protocol-Version", "2099-01-01")
204204
.header("Mcp-Method", "tools/list")
205205
.json(&body)
206206
.send()
@@ -365,7 +365,7 @@ async fn discover_accepts_missing_optional_client_info() {
365365
async fn discover_error_uses_http_400_when_sse_is_configured() {
366366
let (client, url, cancellation_token) = spawn_server(false).await;
367367

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

370370
assert_eq!(response.status(), 400);
371371
assert_eq!(

‎crates/rmcp/tests/test_stateless_server_requests.rs‎

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
#![cfg(all(feature = "server", not(feature = "local")))]
22

33
use std::{
4-
sync::{Arc, Mutex},
4+
borrow::Cow,
5+
sync::{
6+
Arc, Mutex,
7+
atomic::{AtomicBool, Ordering},
8+
},
59
time::Duration,
610
};
711

812
use rmcp::{
9-
ServerHandler, ServiceExt,
13+
ClientLifecycleMode, ClientServiceExt, ServerHandler, ServiceExt,
1014
model::{
1115
ClientCapabilities, ClientJsonRpcMessage, ClientRequest, DiscoverRequest,
12-
DiscoverRequestParams, ErrorCode, ErrorData, Implementation, ListToolsRequest,
13-
ListToolsResult, NumberOrString, PaginatedRequestParams, ProgressNotificationParam,
14-
ProgressToken, ProtocolVersion, RequestId, RequestMetaObject, ServerJsonRpcMessage,
15-
ServerNotification,
16+
DiscoverRequestMethod, DiscoverRequestParams, DiscoverResult, ErrorCode, ErrorData,
17+
Implementation, ListToolsRequest, ListToolsResult, NumberOrString, PaginatedRequestParams,
18+
ProgressNotificationParam, ProgressToken, ProtocolVersion, RequestId, RequestMetaObject,
19+
ServerJsonRpcMessage, ServerNotification,
1620
},
1721
service::{MaybeSendFuture, RequestContext, RoleServer},
1822
transport::{IntoTransport, Transport},
@@ -23,6 +27,25 @@ struct StatelessServer;
2327

2428
impl ServerHandler for StatelessServer {}
2529

30+
#[derive(Clone)]
31+
struct LegacyServer {
32+
discover_called: Arc<AtomicBool>,
33+
}
34+
35+
impl ServerHandler for LegacyServer {
36+
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
37+
Cow::Borrowed(ProtocolVersion::known_up_to(&ProtocolVersion::V_2025_11_25))
38+
}
39+
40+
async fn discover(
41+
&self,
42+
_context: RequestContext<RoleServer>,
43+
) -> Result<DiscoverResult, ErrorData> {
44+
self.discover_called.store(true, Ordering::Relaxed);
45+
Err(ErrorData::method_not_found::<DiscoverRequestMethod>())
46+
}
47+
}
48+
2649
fn complete_meta() -> RequestMetaObject {
2750
complete_meta_for("stateless-client")
2851
}
@@ -123,6 +146,42 @@ async fn stateless_server_rejects_missing_metadata_on_every_request() {
123146
running.cancel().await.expect("cancel server");
124147
}
125148

149+
#[tokio::test]
150+
async fn legacy_only_server_rejects_discovery_before_dispatch() {
151+
let discover_called = Arc::new(AtomicBool::new(false));
152+
let server = LegacyServer {
153+
discover_called: Arc::clone(&discover_called),
154+
};
155+
let (server_transport, client_transport) = tokio::io::duplex(4096);
156+
let server_task = tokio::spawn(async move {
157+
server
158+
.serve(server_transport)
159+
.await
160+
.expect("server should start")
161+
});
162+
163+
let client = ()
164+
.serve_with_lifecycle(
165+
client_transport,
166+
ClientLifecycleMode::Auto {
167+
preferred_versions: vec![ProtocolVersion::V_2026_07_28],
168+
legacy_version: Some(ProtocolVersion::V_2025_11_25),
169+
},
170+
)
171+
.await
172+
.expect("client should fall back to initialize");
173+
174+
assert!(!discover_called.load(Ordering::Relaxed));
175+
176+
client.cancel().await.expect("cancel client");
177+
server_task
178+
.await
179+
.expect("server task")
180+
.cancel()
181+
.await
182+
.expect("cancel server");
183+
}
184+
126185
#[derive(Clone)]
127186
struct ContextServer {
128187
seen_clients: Arc<Mutex<Vec<String>>>,

0 commit comments

Comments
 (0)