feat: standardize resource-not-found error code (SEP-2164) (#899)
* feat: implement SEP-2164 resource not found errors * test: update protocol version utility expectations * feat: gate not-found code at server boundary --------- Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
parent
8f5310b424
commit
95a8e961e0
2 changed files with 126 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
91
crates/rmcp/tests/test_resource_not_found_version.rs
Normal file
91
crates/rmcp/tests/test_resource_not_found_version.rs
Normal file
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue