Skip to content
Merged
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
112 changes: 84 additions & 28 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Test conformance server
run: cargo test -p mcp-conformance --bin conformance-server

- name: Start conformance server
- name: Start 2025-11-25 server
run: |
PORT=8001 ./target/debug/conformance-server &
echo $! > server.pid
Expand All @@ -50,7 +50,7 @@ jobs:
echo "conformance server did not become ready" >&2
exit 1

- name: Run server conformance suite
- name: Run 2025-11-25 server suite
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
--url http://127.0.0.1:8001/mcp \
Expand All @@ -59,7 +59,7 @@ jobs:

# These pass today but are excluded from the default "active" suite;
# run them explicitly so regressions are still caught.
- name: Run pending scenarios
- name: Run 2025-11-25 pending scenarios
run: |
for scenario in json-schema-2020-12 server-sse-polling; do
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
Expand All @@ -68,7 +68,7 @@ jobs:
-o conformance-results
done

- name: Start draft conformance server
- name: Start draft server
run: |
STATELESS=1 PORT=8002 ./target/debug/conformance-server &
echo $! > draft-server.pid
Expand All @@ -81,27 +81,85 @@ jobs:
echo "draft conformance server did not become ready" >&2
exit 1

- name: Run draft SEP scenarios
# Run discovery separately until #985 enables the full draft suite.
- name: Run SEP-2575 discovery contract
run: |
endpoint=http://127.0.0.1:8002/mcp
common_headers=(
-H "Content-Type: application/json"
-H "Accept: application/json, text/event-stream"
-H "Mcp-Method: server/discover"
)

discover_response="$(
curl --fail-with-body --silent --show-error \
"${common_headers[@]}" \
-H "MCP-Protocol-Version: 2026-07-28" \
--data '{
"jsonrpc": "2.0",
"id": "discover",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "conformance-workflow",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}' \
"$endpoint"
)"
jq -e '
.result.resultType == "complete" and
(.result.supportedVersions | index("2026-07-28") != null) and
(.result.capabilities | type == "object") and
(.result.serverInfo.name | type == "string") and
.result.ttlMs == 0 and
.result.cacheScope == "private"
' <<<"$discover_response"

status="$(
curl --silent --show-error \
--output /tmp/unsupported-version.json \
--write-out "%{http_code}" \
"${common_headers[@]}" \
-H "MCP-Protocol-Version: 2099-01-01" \
--data '{
"jsonrpc": "2.0",
"id": "unsupported",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2099-01-01",
"io.modelcontextprotocol/clientInfo": {
"name": "conformance-workflow",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}' \
"$endpoint"
)"
test "$status" = "400"
jq -e '
.id == "unsupported" and
.error.code == -32022 and
.error.data.requested == "2099-01-01" and
(.error.data.supported | index("2026-07-28") != null)
' /tmp/unsupported-version.json

# Keep this explicit list until the full draft suite is enabled by #985.
- name: Run supported draft server scenarios
run: |
for scenario in \
sep-2164-resource-not-found \
caching \
http-header-validation \
http-custom-header-server-validation \
; do
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
--url http://127.0.0.1:8002/mcp \
--scenario "$scenario" \
--spec-version draft \
-o conformance-results
done

# SEP-2322 MRTR scenarios (spec 2026-07-28). They speak the stateless
# lifecycle (bare JSON-RPC POSTs, no initialize handshake), so they run
# against the stateless draft server.
- name: Run SEP-2322 MRTR scenarios
run: |
for scenario in \
input-required-result-basic-elicitation \
input-required-result-basic-sampling \
input-required-result-basic-list-roots \
Expand All @@ -120,17 +178,15 @@ jobs:
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
--url http://127.0.0.1:8002/mcp \
--scenario "$scenario" \
--spec-version draft \
-o conformance-results
done

- name: Stop draft conformance server
- name: Stop conformance servers
if: always()
run: kill "$(cat draft-server.pid)" 2>/dev/null || true


- name: Stop conformance server
if: always()
run: kill "$(cat server.pid)" 2>/dev/null || true
run: |
kill "$(cat draft-server.pid)" 2>/dev/null || true
kill "$(cat server.pid)" 2>/dev/null || true

- name: Upload results
if: always()
Expand All @@ -154,7 +210,7 @@ jobs:
- name: Build conformance binaries
run: cargo build -p mcp-conformance

- name: Run full client conformance suite
- name: Run 2025-11-25 client suite
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
--command "$(pwd)/target/debug/conformance-client" \
Expand All @@ -163,7 +219,7 @@ jobs:
-o conformance-client-results/full

# SEP-2322 MRTR client scenario (spec 2026-07-28).
- name: Run SEP-2322 MRTR client scenario
- name: Run draft SEP-2322 client scenario
run: |
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" client \
--command "$(pwd)/target/debug/conformance-client" \
Expand Down
62 changes: 61 additions & 1 deletion crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
use std::sync::Arc;
use std::{borrow::Cow, sync::Arc};

use crate::{
error::ErrorData as McpError,
Expand Down Expand Up @@ -30,11 +30,46 @@ impl<H: ServerHandler> Service<RoleServer> for H {
let mrtr_supported = protocol_version
.as_ref()
.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(_));
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) {
return Err(McpError::unsupported_protocol_version(
requested_version.clone(),
&supported_versions,
));
}
}
if matches!(&request, ClientRequest::DiscoverRequest(_)) {
if requested_version.is_none() {
return Err(McpError::invalid_params(
"server/discover requires protocolVersion in request _meta",
None,
));
}
if context.meta.client_info().is_none() {
return Err(McpError::invalid_params(
"server/discover requires clientInfo in request _meta",
None,
));
}
if context.meta.client_capabilities().is_none() {
return Err(McpError::invalid_params(
"server/discover requires clientCapabilities in request _meta",
None,
));
}
}
let result = match request {
ClientRequest::InitializeRequest(request) => self
.initialize(request.params, context)
.await
.map(ServerResult::InitializeResult),
ClientRequest::DiscoverRequest(_request) => self
.discover(context)
.await
.map(ServerResult::DiscoverResult),
ClientRequest::PingRequest(_request) => {
self.ping(context).await.map(ServerResult::empty)
}
Expand Down Expand Up @@ -225,6 +260,20 @@ macro_rules! server_handler_methods {
);
std::future::ready(Ok(info))
}
/// Return the protocol versions supported by this server.
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
Cow::Borrowed(ProtocolVersion::KNOWN_VERSIONS)
}
/// Return this server's discovery information.
fn discover(
&self,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Ok(DiscoverResult::from_server_info(
self.supported_protocol_versions().into_owned(),
self.get_info(),
)))
}
fn complete(
&self,
request: CompleteRequestParams,
Expand Down Expand Up @@ -479,6 +528,17 @@ macro_rules! impl_server_handler_for_wrapper {
(**self).initialize(request, context)
}

fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
(**self).supported_protocol_versions()
}

fn discover(
&self,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
(**self).discover(context)
}

fn complete(
&self,
request: CompleteRequestParams,
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub use handler::server::wrapper::Json;
#[cfg(any(feature = "client", feature = "server"))]
pub use service::{Peer, Service, ServiceError, ServiceExt};
#[cfg(feature = "client")]
pub use service::{RoleClient, serve_client};
pub use service::{RoleClient, select_protocol_version, serve_client};
#[cfg(feature = "server")]
pub use service::{RoleServer, serve_server};

Expand Down
Loading