mirror of
https://github.com/openai/codex.git
synced 2026-11-12 22:42:36 +00:00
## What changed - Update the Rust MCP SDK from `3.0.0-beta.3` to `3.0.0` and adapt to its renamed metadata and server discovery types. - Accept discovery responses without server identity metadata, using the configured server name as a fallback, and rely on the SDK's native support for namespaced server identity metadata. - Preserve typed OAuth HTTP errors so transport failures, cross-origin redirects, and transient HTTP responses are reported instead of being treated as anonymous access. ## Testing - Cover modern discovery with namespaced or missing server identity over HTTP, SSE, and stdio. - Cover OAuth discovery error propagation for transport failures, redirects, and transient status codes. GitOrigin-RevId: 12c1e45136cca89ce4fb15986c2b5df14608682a
152 lines
4.9 KiB
Rust
152 lines
4.9 KiB
Rust
use pretty_assertions::assert_eq;
|
|
use rmcp::model::JsonRpcMessage;
|
|
use rmcp::model::ServerResult;
|
|
use serde_json::json;
|
|
|
|
use super::deserialize_incoming_jsonrpc_message;
|
|
use super::normalize_sse_jsonrpc_message;
|
|
|
|
#[test]
|
|
fn discovery_accepts_metadata_namespaced_server_identity() {
|
|
let message = json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"resultType": "complete",
|
|
"supportedVersions": ["2026-07-28"],
|
|
"capabilities": {"tools": {}},
|
|
"ttlMs": 0,
|
|
"cacheScope": "private",
|
|
"_meta": {
|
|
"io.modelcontextprotocol/serverInfo": {
|
|
"name": "modern-server",
|
|
"version": "1.0.0",
|
|
},
|
|
"retained": "metadata",
|
|
},
|
|
},
|
|
});
|
|
|
|
let decoded = deserialize_incoming_jsonrpc_message(&serde_json::to_vec(&message).unwrap())
|
|
.expect("metadata-only identity must decode as discovery");
|
|
let JsonRpcMessage::Response(response) = decoded else {
|
|
panic!("expected a discovery response");
|
|
};
|
|
let ServerResult::DiscoverResult(result) = response.result else {
|
|
panic!("metadata-only identity must not become a completed tool result");
|
|
};
|
|
assert_eq!(
|
|
result.server_info().map(|server_info| server_info.name),
|
|
Some("modern-server".to_owned())
|
|
);
|
|
assert_eq!(
|
|
result.meta.and_then(|meta| meta.get("retained").cloned()),
|
|
Some(json!("metadata"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extension_result_with_discovery_like_fields_remains_a_custom_result() {
|
|
let result = json!({
|
|
"supportedVersions": ["custom-extension-v1"],
|
|
"capabilities": {"custom": true},
|
|
});
|
|
let message = json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 2,
|
|
"result": result,
|
|
});
|
|
|
|
let decoded = deserialize_incoming_jsonrpc_message(&serde_json::to_vec(&message).unwrap())
|
|
.expect("legacy extension result must deserialize normally");
|
|
let JsonRpcMessage::Response(response) = decoded else {
|
|
panic!("expected a legacy extension response");
|
|
};
|
|
let ServerResult::CustomResult(custom) = response.result else {
|
|
panic!("legacy extension response must remain a custom result");
|
|
};
|
|
assert_eq!(custom.0, result);
|
|
}
|
|
|
|
#[test]
|
|
fn input_required_discriminator_wins_over_tool_result_metadata() {
|
|
let message = json!({
|
|
"jsonrpc": "2.0",
|
|
"id": "tool-call",
|
|
"result": {
|
|
"resultType": "input_required",
|
|
"requestState": "opaque-server-state",
|
|
"_meta": {"retained": "response-metadata"},
|
|
},
|
|
});
|
|
|
|
let decoded = deserialize_incoming_jsonrpc_message(&serde_json::to_vec(&message).unwrap())
|
|
.expect("input-required result must decode");
|
|
let JsonRpcMessage::Response(response) = decoded else {
|
|
panic!("expected an input-required response");
|
|
};
|
|
let ServerResult::InputRequiredResult(result) = response.result else {
|
|
panic!("input_required must not become an empty completed tool result");
|
|
};
|
|
assert_eq!(result.request_state.as_deref(), Some("opaque-server-state"));
|
|
assert_eq!(
|
|
result.meta.and_then(|meta| meta.get("retained").cloned()),
|
|
Some(json!("response-metadata"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sse_discovery_preserves_metadata_namespaced_identity_without_normalization() {
|
|
let message = json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"resultType": "complete",
|
|
"supportedVersions": ["2026-07-28"],
|
|
"capabilities": {},
|
|
"ttlMs": 0,
|
|
"cacheScope": "private",
|
|
"_meta": {
|
|
"io.modelcontextprotocol/serverInfo": {
|
|
"name": "sse-server",
|
|
"version": "1.0.0",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert_eq!(
|
|
normalize_sse_jsonrpc_message(&message.to_string(), /*modern_session*/ true),
|
|
None
|
|
);
|
|
|
|
let decoded = deserialize_incoming_jsonrpc_message(&serde_json::to_vec(&message).unwrap())
|
|
.expect("rmcp must natively decode metadata-only SSE discovery");
|
|
let JsonRpcMessage::Response(response) = decoded else {
|
|
panic!("expected a discovery response");
|
|
};
|
|
let ServerResult::DiscoverResult(result) = response.result else {
|
|
panic!("metadata-only identity must decode as discovery without normalization");
|
|
};
|
|
assert_eq!(
|
|
result.server_info().map(|server_info| server_info.name),
|
|
Some("sse-server".to_owned())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_sse_payload_preserves_intermediate_response_metadata() {
|
|
let message = json!({
|
|
"jsonrpc": "2.0",
|
|
"id": "legacy-extension",
|
|
"result": {
|
|
"resultType": "input_required",
|
|
"_meta": {"preserved": "legacy-extension-metadata"},
|
|
},
|
|
});
|
|
|
|
assert_eq!(
|
|
normalize_sse_jsonrpc_message(&message.to_string(), /*modern_session*/ false),
|
|
None
|
|
);
|
|
}
|