Skip to content

Commit 3549e86

Browse files
authored
feat!: add server discovery and negotiation (SEP-2575) (modelcontextprotocol#973)
* feat!: add server discovery and negotiation * feat!: make protocol unions extensible * ci: add discovery conformance coverage
1 parent b93220d commit 3549e86

14 files changed

Lines changed: 1325 additions & 98 deletions

.github/workflows/conformance.yml

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: Test conformance server
3838
run: cargo test -p mcp-conformance --bin conformance-server
3939

40-
- name: Start conformance server
40+
- name: Start 2025-11-25 server
4141
run: |
4242
PORT=8001 ./target/debug/conformance-server &
4343
echo $! > server.pid
@@ -50,7 +50,7 @@ jobs:
5050
echo "conformance server did not become ready" >&2
5151
exit 1
5252
53-
- name: Run server conformance suite
53+
- name: Run 2025-11-25 server suite
5454
run: |
5555
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
5656
--url http://127.0.0.1:8001/mcp \
@@ -59,7 +59,7 @@ jobs:
5959
6060
# These pass today but are excluded from the default "active" suite;
6161
# run them explicitly so regressions are still caught.
62-
- name: Run pending scenarios
62+
- name: Run 2025-11-25 pending scenarios
6363
run: |
6464
for scenario in json-schema-2020-12 server-sse-polling; do
6565
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
@@ -68,7 +68,7 @@ jobs:
6868
-o conformance-results
6969
done
7070
71-
- name: Start draft conformance server
71+
- name: Start draft server
7272
run: |
7373
STATELESS=1 PORT=8002 ./target/debug/conformance-server &
7474
echo $! > draft-server.pid
@@ -81,27 +81,85 @@ jobs:
8181
echo "draft conformance server did not become ready" >&2
8282
exit 1
8383
84-
- name: Run draft SEP scenarios
84+
# Run discovery separately until #985 enables the full draft suite.
85+
- name: Run SEP-2575 discovery contract
86+
run: |
87+
endpoint=http://127.0.0.1:8002/mcp
88+
common_headers=(
89+
-H "Content-Type: application/json"
90+
-H "Accept: application/json, text/event-stream"
91+
-H "Mcp-Method: server/discover"
92+
)
93+
94+
discover_response="$(
95+
curl --fail-with-body --silent --show-error \
96+
"${common_headers[@]}" \
97+
-H "MCP-Protocol-Version: 2026-07-28" \
98+
--data '{
99+
"jsonrpc": "2.0",
100+
"id": "discover",
101+
"method": "server/discover",
102+
"params": {
103+
"_meta": {
104+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
105+
"io.modelcontextprotocol/clientInfo": {
106+
"name": "conformance-workflow",
107+
"version": "1.0.0"
108+
},
109+
"io.modelcontextprotocol/clientCapabilities": {}
110+
}
111+
}
112+
}' \
113+
"$endpoint"
114+
)"
115+
jq -e '
116+
.result.resultType == "complete" and
117+
(.result.supportedVersions | index("2026-07-28") != null) and
118+
(.result.capabilities | type == "object") and
119+
(.result.serverInfo.name | type == "string") and
120+
.result.ttlMs == 0 and
121+
.result.cacheScope == "private"
122+
' <<<"$discover_response"
123+
124+
status="$(
125+
curl --silent --show-error \
126+
--output /tmp/unsupported-version.json \
127+
--write-out "%{http_code}" \
128+
"${common_headers[@]}" \
129+
-H "MCP-Protocol-Version: 2099-01-01" \
130+
--data '{
131+
"jsonrpc": "2.0",
132+
"id": "unsupported",
133+
"method": "server/discover",
134+
"params": {
135+
"_meta": {
136+
"io.modelcontextprotocol/protocolVersion": "2099-01-01",
137+
"io.modelcontextprotocol/clientInfo": {
138+
"name": "conformance-workflow",
139+
"version": "1.0.0"
140+
},
141+
"io.modelcontextprotocol/clientCapabilities": {}
142+
}
143+
}
144+
}' \
145+
"$endpoint"
146+
)"
147+
test "$status" = "400"
148+
jq -e '
149+
.id == "unsupported" and
150+
.error.code == -32022 and
151+
.error.data.requested == "2099-01-01" and
152+
(.error.data.supported | index("2026-07-28") != null)
153+
' /tmp/unsupported-version.json
154+
155+
# Keep this explicit list until the full draft suite is enabled by #985.
156+
- name: Run supported draft server scenarios
85157
run: |
86158
for scenario in \
87159
sep-2164-resource-not-found \
88160
caching \
89161
http-header-validation \
90162
http-custom-header-server-validation \
91-
; do
92-
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
93-
--url http://127.0.0.1:8002/mcp \
94-
--scenario "$scenario" \
95-
--spec-version draft \
96-
-o conformance-results
97-
done
98-
99-
# SEP-2322 MRTR scenarios (spec 2026-07-28). They speak the stateless
100-
# lifecycle (bare JSON-RPC POSTs, no initialize handshake), so they run
101-
# against the stateless draft server.
102-
- name: Run SEP-2322 MRTR scenarios
103-
run: |
104-
for scenario in \
105163
input-required-result-basic-elicitation \
106164
input-required-result-basic-sampling \
107165
input-required-result-basic-list-roots \
@@ -120,17 +178,15 @@ jobs:
120178
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
121179
--url http://127.0.0.1:8002/mcp \
122180
--scenario "$scenario" \
181+
--spec-version draft \
123182
-o conformance-results
124183
done
125184
126-
- name: Stop draft conformance server
185+
- name: Stop conformance servers
127186
if: always()
128-
run: kill "$(cat draft-server.pid)" 2>/dev/null || true
129-
130-
131-
- name: Stop conformance server
132-
if: always()
133-
run: kill "$(cat server.pid)" 2>/dev/null || true
187+
run: |
188+
kill "$(cat draft-server.pid)" 2>/dev/null || true
189+
kill "$(cat server.pid)" 2>/dev/null || true
134190
135191
- name: Upload results
136192
if: always()
@@ -154,7 +210,7 @@ jobs:
154210
- name: Build conformance binaries
155211
run: cargo build -p mcp-conformance
156212

157-
- name: Run full client conformance suite
213+
- name: Run 2025-11-25 client suite
158214
run: |
159215
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
160216
--command "$(pwd)/target/debug/conformance-client" \
@@ -163,7 +219,7 @@ jobs:
163219
-o conformance-client-results/full
164220
165221
# SEP-2322 MRTR client scenario (spec 2026-07-28).
166-
- name: Run SEP-2322 MRTR client scenario
222+
- name: Run draft SEP-2322 client scenario
167223
run: |
168224
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" client \
169225
--command "$(pwd)/target/debug/conformance-client" \

crates/rmcp/src/handler/server.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
22
#![expect(deprecated)]
3-
use std::sync::Arc;
3+
use std::{borrow::Cow, sync::Arc};
44

55
use crate::{
66
error::ErrorData as McpError,
@@ -30,11 +30,46 @@ impl<H: ServerHandler> Service<RoleServer> for H {
3030
let mrtr_supported = protocol_version
3131
.as_ref()
3232
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
33+
let requested_version = context.meta.protocol_version();
34+
let uses_inline_negotiation = !matches!(&request, ClientRequest::InitializeRequest(_));
35+
if uses_inline_negotiation && let Some(requested_version) = requested_version.as_ref() {
36+
let supported_versions = self.supported_protocol_versions();
37+
if !supported_versions.contains(requested_version) {
38+
return Err(McpError::unsupported_protocol_version(
39+
requested_version.clone(),
40+
&supported_versions,
41+
));
42+
}
43+
}
44+
if matches!(&request, ClientRequest::DiscoverRequest(_)) {
45+
if requested_version.is_none() {
46+
return Err(McpError::invalid_params(
47+
"server/discover requires protocolVersion in request _meta",
48+
None,
49+
));
50+
}
51+
if context.meta.client_info().is_none() {
52+
return Err(McpError::invalid_params(
53+
"server/discover requires clientInfo in request _meta",
54+
None,
55+
));
56+
}
57+
if context.meta.client_capabilities().is_none() {
58+
return Err(McpError::invalid_params(
59+
"server/discover requires clientCapabilities in request _meta",
60+
None,
61+
));
62+
}
63+
}
3364
let result = match request {
3465
ClientRequest::InitializeRequest(request) => self
3566
.initialize(request.params, context)
3667
.await
3768
.map(ServerResult::InitializeResult),
69+
ClientRequest::DiscoverRequest(_request) => self
70+
.discover(context)
71+
.await
72+
.map(ServerResult::DiscoverResult),
3873
ClientRequest::PingRequest(_request) => {
3974
self.ping(context).await.map(ServerResult::empty)
4075
}
@@ -225,6 +260,20 @@ macro_rules! server_handler_methods {
225260
);
226261
std::future::ready(Ok(info))
227262
}
263+
/// Return the protocol versions supported by this server.
264+
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
265+
Cow::Borrowed(ProtocolVersion::KNOWN_VERSIONS)
266+
}
267+
/// Return this server's discovery information.
268+
fn discover(
269+
&self,
270+
context: RequestContext<RoleServer>,
271+
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
272+
std::future::ready(Ok(DiscoverResult::from_server_info(
273+
self.supported_protocol_versions().into_owned(),
274+
self.get_info(),
275+
)))
276+
}
228277
fn complete(
229278
&self,
230279
request: CompleteRequestParams,
@@ -479,6 +528,17 @@ macro_rules! impl_server_handler_for_wrapper {
479528
(**self).initialize(request, context)
480529
}
481530

531+
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
532+
(**self).supported_protocol_versions()
533+
}
534+
535+
fn discover(
536+
&self,
537+
context: RequestContext<RoleServer>,
538+
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
539+
(**self).discover(context)
540+
}
541+
482542
fn complete(
483543
&self,
484544
request: CompleteRequestParams,

crates/rmcp/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub use handler::server::wrapper::Json;
1919
#[cfg(any(feature = "client", feature = "server"))]
2020
pub use service::{Peer, Service, ServiceError, ServiceExt};
2121
#[cfg(feature = "client")]
22-
pub use service::{RoleClient, serve_client};
22+
pub use service::{RoleClient, select_protocol_version, serve_client};
2323
#[cfg(feature = "server")]
2424
pub use service::{RoleServer, serve_server};
2525

0 commit comments

Comments
 (0)