fix: require metadata for modern HTTP requests (#1089)

This commit is contained in:
Dale Seo 2026-07-30 15:45:01 -04:00 • committed by GitHub
commit 58b136f44f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 179 additions and 8 deletions

View file

@ -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",

View file

@ -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();
}