diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index e57fa972..a5c3a763 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -34,6 +34,9 @@ jobs: - name: Build conformance binaries run: cargo build -p mcp-conformance + - name: Test conformance server + run: cargo test -p mcp-conformance --bin conformance-server + - name: Start conformance server run: | PORT=8001 ./target/debug/conformance-server & @@ -80,7 +83,12 @@ jobs: - name: Run draft SEP scenarios run: | - for scenario in sep-2164-resource-not-found caching http-header-validation; do + 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" \ diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index 09bdd5e9..4df4812d 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -28,6 +28,20 @@ fn json_object(v: Value) -> JsonObject { } } +fn custom_header_tool() -> Tool { + Tool::new( + "test_custom_header", + "Validates SEP-2243 custom parameter headers", + json_object(json!({ + "type": "object", + "properties": { + "value": { "type": "string", "x-mcp-header": "Value" } + }, + "required": ["value"] + })), + ) +} + /// Signing key for SEP-2322 `requestState` sealing. A fixed key is fine for a /// conformance harness; real servers must load a secret out of clients' reach. const REQUEST_STATE_KEY: &[u8] = b"rust-sdk-conformance-request-state-key!!"; @@ -361,6 +375,10 @@ impl ConformanceServer { } impl ServerHandler for ConformanceServer { + fn get_tool(&self, name: &str) -> Option { + (name == "test_custom_header").then(custom_header_tool) + } + async fn initialize( &self, request: InitializeRequestParams, @@ -521,6 +539,7 @@ impl ServerHandler for ConformanceServer { "properties": {} })), ), + custom_header_tool(), ]; // SEP-2322 MRTR test tools; all take no arguments. let mrtr_tools = [ @@ -668,6 +687,14 @@ impl ServerHandler for ConformanceServer { )])) } + "test_custom_header" => { + let value = args + .get("value") + .and_then(Value::as_str) + .ok_or_else(|| ErrorData::invalid_params("value must be a string", None))?; + Ok(CallToolResult::success(vec![ContentBlock::text(value)])) + } + "test_sampling" => { let prompt = args .get("prompt") @@ -1218,3 +1245,26 @@ async fn main() -> anyhow::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn server_exposes_custom_header_tool_for_transport_validation() { + let tool = ConformanceServer::new() + .get_tool("test_custom_header") + .expect("custom-header conformance tool"); + let value = Value::Object((*tool.input_schema).clone()); + + assert_eq!( + value.pointer("/properties/value/type"), + Some(&json!("string")) + ); + assert_eq!( + value.pointer("/properties/value/x-mcp-header"), + Some(&json!("Value")) + ); + assert_eq!(value.pointer("/required/0"), Some(&json!("value"))); + } +} diff --git a/crates/rmcp/tests/test_streamable_http_standard_headers.rs b/crates/rmcp/tests/test_streamable_http_standard_headers.rs index 5316725c..c2c51a7c 100644 --- a/crates/rmcp/tests/test_streamable_http_standard_headers.rs +++ b/crates/rmcp/tests/test_streamable_http_standard_headers.rs @@ -267,6 +267,29 @@ async fn rejects_param_mismatch_with_32020() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn rejects_decoded_base64_param_mismatch_with_32020() -> anyhow::Result<()> { + let (client, url, ct) = spawn_server().await; + + let response = post_tool_call( + &client, + &url, + SEP_VERSION, + "deploy", + serde_json::json!({ "region": "us-west1" }), + Some("tools/call"), + Some("deploy"), + Some("=?base64?ZXUtY2VudHJhbDE=?="), + ) + .await; + assert_eq!(response.status(), 400); + let body: serde_json::Value = response.json().await?; + assert_eq!(body["error"]["code"], -32020); + + ct.cancel(); + Ok(()) +} + #[tokio::test] async fn rejects_missing_param_header_with_32020() -> anyhow::Result<()> { let (client, url, ct) = spawn_server().await;