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
2 changes: 1 addition & 1 deletion conformance/src/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ impl ServerHandler for ConformanceServer {
} else {
Err(ErrorData::resource_not_found(
format!("Resource not found: {}", uri),
None,
Some(json!({ "uri": uri })),
))
}
}
Expand Down
17 changes: 15 additions & 2 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ impl<H: ServerHandler> Service<RoleServer> for H {
request: <RoleServer as ServiceRole>::PeerReq,
context: RequestContext<RoleServer>,
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
match request {
// `context` is moved into the dispatch below, so read the negotiated version first.
let protocol_version = context.protocol_version();
let result = match request {
ClientRequest::InitializeRequest(request) => self
.initialize(request.params, context)
.await
Expand Down Expand Up @@ -127,7 +129,18 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.cancel_task(request.params, context)
.await
.map(ServerResult::CancelTaskResult),
}
};
// SEP-2164: peers negotiating 2026-07-28+ get the standard INVALID_PARAMS code for
// resource-not-found; older peers keep RESOURCE_NOT_FOUND. ISO `YYYY-MM-DD` versions
// compare lexically the same as chronologically.
let use_invalid_params =
protocol_version.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
result.map_err(|mut error| {
if use_invalid_params && error.code == ErrorCode::RESOURCE_NOT_FOUND {
error.code = ErrorCode::INVALID_PARAMS;
}
error
})
}

async fn handle_notification(
Expand Down
6 changes: 6 additions & 0 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ impl std::fmt::Display for ProtocolVersion {
}

impl ProtocolVersion {
pub const V_2026_07_28: Self = Self(Cow::Borrowed("2026-07-28"));
pub const V_2025_11_25: Self = Self(Cow::Borrowed("2025-11-25"));
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
Expand All @@ -164,6 +165,7 @@ impl ProtocolVersion {
Self::V_2025_03_26,
Self::V_2025_06_18,
Self::V_2025_11_25,
Self::V_2026_07_28,
];

/// Returns the string representation of this protocol version.
Expand Down Expand Up @@ -193,6 +195,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
"2025-03-26" => return Ok(ProtocolVersion::V_2025_03_26),
"2025-06-18" => return Ok(ProtocolVersion::V_2025_06_18),
"2025-11-25" => return Ok(ProtocolVersion::V_2025_11_25),
"2026-07-28" => return Ok(ProtocolVersion::V_2026_07_28),
_ => {}
}
Ok(ProtocolVersion(Cow::Owned(s)))
Expand Down Expand Up @@ -541,9 +544,12 @@ impl ErrorData {
data,
}
}
/// Resource-not-found error (`-32002`). The server upgrades this to `INVALID_PARAMS`
/// (`-32602`) for peers negotiating protocol `2026-07-28` or newer (SEP-2164).
pub fn resource_not_found(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::RESOURCE_NOT_FOUND, message, data)
}

pub fn parse_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::PARSE_ERROR, message, data)
}
Expand Down
10 changes: 10 additions & 0 deletions crates/rmcp/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,16 @@ impl<R: ServiceRole> RequestContext<R> {
}
}

#[cfg(feature = "server")]
impl RequestContext<RoleServer> {
/// The protocol version the client negotiated, or `None` before peer info is recorded.
pub fn protocol_version(&self) -> Option<crate::model::ProtocolVersion> {
self.peer
.peer_info()
.map(|info| info.protocol_version.clone())
}
}

/// Request execution context
#[derive(Debug, Clone)]
#[non_exhaustive]
Expand Down
4 changes: 3 additions & 1 deletion crates/rmcp/tests/test_custom_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,16 +866,18 @@ async fn test_server_rejects_unsupported_protocol_version() {
fn test_protocol_version_utilities() {
use rmcp::model::ProtocolVersion;

assert_eq!(ProtocolVersion::V_2026_07_28.as_str(), "2026-07-28");
assert_eq!(ProtocolVersion::V_2025_11_25.as_str(), "2025-11-25");
assert_eq!(ProtocolVersion::V_2025_06_18.as_str(), "2025-06-18");
assert_eq!(ProtocolVersion::V_2025_03_26.as_str(), "2025-03-26");
assert_eq!(ProtocolVersion::V_2024_11_05.as_str(), "2024-11-05");

assert_eq!(ProtocolVersion::KNOWN_VERSIONS.len(), 4);
assert_eq!(ProtocolVersion::KNOWN_VERSIONS.len(), 5);
assert!(ProtocolVersion::KNOWN_VERSIONS.contains(&ProtocolVersion::V_2024_11_05));
assert!(ProtocolVersion::KNOWN_VERSIONS.contains(&ProtocolVersion::V_2025_03_26));
assert!(ProtocolVersion::KNOWN_VERSIONS.contains(&ProtocolVersion::V_2025_06_18));
assert!(ProtocolVersion::KNOWN_VERSIONS.contains(&ProtocolVersion::V_2025_11_25));
assert!(ProtocolVersion::KNOWN_VERSIONS.contains(&ProtocolVersion::V_2026_07_28));
}

/// Integration test: Verify server validates only the Host header for DNS rebinding protection
Expand Down
91 changes: 91 additions & 0 deletions crates/rmcp/tests/test_resource_not_found_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! SEP-2164: the resource-not-found error code follows the negotiated protocol version.
//!
//! `2026-07-28` and newer get the standard `INVALID_PARAMS` (-32602); older versions
//! keep the legacy `RESOURCE_NOT_FOUND` (-32002).
#![cfg(not(feature = "local"))]
#![cfg(feature = "client")]

use rmcp::{
ClientHandler, RoleServer, ServerHandler, ServiceError, ServiceExt,
model::{
ClientInfo, ErrorCode, ErrorData, ProtocolVersion, ReadResourceRequestParams,
ReadResourceResult,
},
service::RequestContext,
};

#[derive(Debug, Clone, Default)]
struct ResourceServer;

impl ServerHandler for ResourceServer {
async fn read_resource(
&self,
_request: ReadResourceRequestParams,
_context: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, ErrorData> {
Err(ErrorData::resource_not_found("resource not found", None))
}
}

#[derive(Debug, Clone)]
struct VersionedClient {
protocol_version: ProtocolVersion,
}

impl ClientHandler for VersionedClient {
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
info.protocol_version = self.protocol_version.clone();
info
}
}

async fn not_found_code(client_version: ProtocolVersion) -> ErrorCode {
let (server_transport, client_transport) = tokio::io::duplex(4096);

let server_handle = tokio::spawn(async move {
ResourceServer
.serve(server_transport)
.await?
.waiting()
.await?;
anyhow::Ok(())
});

let client = VersionedClient {
protocol_version: client_version,
}
.serve(client_transport)
.await
.expect("client should connect");

let error = client
.read_resource(ReadResourceRequestParams::new("missing://resource"))
.await
.expect_err("missing resource should error");

let code = match error {
ServiceError::McpError(data) => data.code,
other => panic!("expected McpError, got: {other:?}"),
};

client.cancel().await.expect("client should cancel");
server_handle.await.expect("server task").expect("server");
code
}

#[tokio::test]
async fn legacy_version_gets_resource_not_found_code() {
assert_eq!(
not_found_code(ProtocolVersion::V_2025_11_25).await,
ErrorCode::RESOURCE_NOT_FOUND,
);
}

#[tokio::test]
async fn sep_2164_version_gets_invalid_params_code() {
assert_eq!(
not_found_code(ProtocolVersion::V_2026_07_28).await,
ErrorCode::INVALID_PARAMS,
);
}