fix: require metadata for modern HTTP requests (#1089)
This commit is contained in:
parent
d272389fd4
commit
58b136f44f
2 changed files with 179 additions and 8 deletions
|
|
@ -467,19 +467,27 @@ fn validate_request_protocol_version_meta(
|
|||
return Ok(());
|
||||
}
|
||||
let is_discover = matches!(&request.request, ClientRequest::DiscoverRequest(_));
|
||||
let Some(meta_version) = request.request.get_meta().protocol_version() else {
|
||||
if is_discover {
|
||||
let meta = request.request.get_meta();
|
||||
let header_version = headers
|
||||
.get(HEADER_MCP_PROTOCOL_VERSION)
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let Some(meta_version) = meta.protocol_version() else {
|
||||
let requires_request_metadata = is_discover
|
||||
|| header_version
|
||||
.is_some_and(|version| version >= ProtocolVersion::V_2026_07_28.as_str());
|
||||
if requires_request_metadata {
|
||||
let missing = meta.missing_required_keys(&ProtocolVersion::V_2026_07_28);
|
||||
return Err(invalid_params_jsonrpc_response(
|
||||
Some(request.id.clone()),
|
||||
"Invalid params: server/discover requires protocolVersion in request _meta",
|
||||
format!(
|
||||
"Invalid params: request _meta is missing or has malformed required fields: {}",
|
||||
missing.join(", ")
|
||||
),
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
let Some(header_version) = headers
|
||||
.get(HEADER_MCP_PROTOCOL_VERSION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
else {
|
||||
let Some(header_version) = header_version else {
|
||||
return Err(header_mismatch_jsonrpc_response(
|
||||
Some(request.id.clone()),
|
||||
"request _meta protocolVersion requires MCP-Protocol-Version header",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#![cfg(not(feature = "local"))]
|
||||
//! Regression tests for the `MCP-Protocol-Version` header / initialize body consistency check.
|
||||
//! Streamable HTTP protocol-version and request-metadata validation tests.
|
||||
use std::sync::Arc;
|
||||
|
||||
use rmcp::transport::streamable_http_server::{
|
||||
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
mod common;
|
||||
|
|
@ -91,6 +92,31 @@ async fn post_non_initialize(client: &reqwest::Client, url: &str) -> reqwest::Re
|
|||
.expect("send non-initialize request")
|
||||
}
|
||||
|
||||
async fn post_modern_request(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
method: &str,
|
||||
name: Option<&str>,
|
||||
params: Value,
|
||||
) -> reqwest::Response {
|
||||
let mut request = client
|
||||
.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json, text/event-stream")
|
||||
.header("MCP-Protocol-Version", "2026-07-28")
|
||||
.header("Mcp-Method", method)
|
||||
.json(&json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}));
|
||||
if let Some(name) = name {
|
||||
request = request.header("Mcp-Name", name);
|
||||
}
|
||||
request.send().await.expect("send modern request")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_init_rejects_when_header_older_than_body() -> anyhow::Result<()> {
|
||||
let (client, url, ct) = spawn_server(stateless_json_config()).await;
|
||||
|
|
@ -217,3 +243,140 @@ async fn stateless_missing_protocol_header_returns_header_mismatch() -> anyhow::
|
|||
ct.cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_tools_list_rejects_missing_request_meta() {
|
||||
let (client, url, ct) = spawn_server(stateless_json_config()).await;
|
||||
|
||||
let response = post_modern_request(&client, &url, "tools/list", None, json!({})).await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32602);
|
||||
assert!(
|
||||
body["error"]["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| message.contains("io.modelcontextprotocol/protocolVersion")),
|
||||
"expected error message to mention protocolVersion, got: {body}"
|
||||
);
|
||||
ct.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_tools_call_rejects_missing_request_meta() {
|
||||
let (client, url, ct) = spawn_server(stateless_json_config()).await;
|
||||
|
||||
let response = post_modern_request(
|
||||
&client,
|
||||
&url,
|
||||
"tools/call",
|
||||
Some("sum"),
|
||||
json!({
|
||||
"name": "sum",
|
||||
"arguments": {
|
||||
"a": 1,
|
||||
"b": 2
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32602);
|
||||
assert!(
|
||||
body["error"]["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| message.contains("io.modelcontextprotocol/protocolVersion")),
|
||||
"expected error message to mention protocolVersion, got: {body}"
|
||||
);
|
||||
ct.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_request_rejects_missing_meta_protocol_version() {
|
||||
let (client, url, ct) = spawn_server(stateless_json_config()).await;
|
||||
|
||||
let response = post_modern_request(
|
||||
&client,
|
||||
&url,
|
||||
"tools/list",
|
||||
None,
|
||||
json!({
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/clientInfo": {
|
||||
"name": "test",
|
||||
"version": "1.0"
|
||||
},
|
||||
"io.modelcontextprotocol/clientCapabilities": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32602);
|
||||
assert!(
|
||||
body["error"]["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| message.contains("io.modelcontextprotocol/protocolVersion")),
|
||||
"expected error message to mention protocolVersion, got: {body}"
|
||||
);
|
||||
ct.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_request_rejects_missing_meta_client_capabilities() {
|
||||
let (client, url, ct) = spawn_server(stateless_json_config()).await;
|
||||
|
||||
let response = post_modern_request(
|
||||
&client,
|
||||
&url,
|
||||
"tools/list",
|
||||
None,
|
||||
json!({
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientInfo": {
|
||||
"name": "test",
|
||||
"version": "1.0"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
let body: Value = response.json().await.expect("response should be JSON");
|
||||
assert_eq!(body["error"]["code"], -32602);
|
||||
assert!(
|
||||
body["error"]["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| message.contains("io.modelcontextprotocol/clientCapabilities")),
|
||||
"expected error message to mention clientCapabilities, got: {body}"
|
||||
);
|
||||
ct.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_request_accepts_missing_optional_meta_client_info() {
|
||||
let (client, url, ct) = spawn_server(stateless_json_config()).await;
|
||||
|
||||
let response = post_modern_request(
|
||||
&client,
|
||||
&url,
|
||||
"tools/list",
|
||||
None,
|
||||
json!({
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientCapabilities": {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
ct.cancel();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue