Skip to content

Commit 99f9c28

Browse files
Add HeaderName constants for MCP HTTP headers
Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
1 parent 4a738b9 commit 99f9c28

5 files changed

Lines changed: 91 additions & 34 deletions

File tree

crates/rmcp/src/transport/common/http_header.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ pub const JSON_MIME_TYPE: &str = "application/json";
88
pub const HEADER_MCP_METHOD: &str = "Mcp-Method";
99
pub const HEADER_MCP_NAME: &str = "Mcp-Name";
1010
pub const HEADER_MCP_PARAM_PREFIX: &str = "Mcp-Param-";
11+
pub const HEADER_MCP_PARAM_PREFIX_LOWER: &str = "mcp-param-";
12+
13+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
14+
pub const HEADER_NAME_SESSION_ID: http::HeaderName =
15+
http::HeaderName::from_static("mcp-session-id");
16+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
17+
pub const HEADER_NAME_LAST_EVENT_ID: http::HeaderName =
18+
http::HeaderName::from_static("last-event-id");
19+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
20+
pub const HEADER_NAME_MCP_PROTOCOL_VERSION: http::HeaderName =
21+
http::HeaderName::from_static("mcp-protocol-version");
22+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
23+
pub const HEADER_NAME_MCP_METHOD: http::HeaderName = http::HeaderName::from_static("mcp-method");
24+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
25+
pub const HEADER_NAME_MCP_NAME: http::HeaderName = http::HeaderName::from_static("mcp-name");
1126

1227
/// Sentinel wrapping a Base64-encoded SEP-2243 header value (`=?base64?<b64>?=`).
1328
pub const BASE64_HEADER_PREFIX: &str = "=?base64?";
@@ -29,21 +44,23 @@ pub(crate) const RESERVED_HEADERS: &[&str] = &[
2944
/// `MCP-Protocol-Version` is reserved but allowed through (the worker injects it post-init).
3045
#[cfg(feature = "client-side-sse")]
3146
pub(crate) fn validate_custom_header(name: &http::HeaderName) -> Result<(), String> {
32-
if RESERVED_HEADERS
33-
.iter()
34-
.any(|&r| name.as_str().eq_ignore_ascii_case(r))
35-
{
36-
if name
37-
.as_str()
38-
.eq_ignore_ascii_case(HEADER_MCP_PROTOCOL_VERSION)
39-
{
47+
if is_reserved_header_name(name) {
48+
if name == HEADER_NAME_MCP_PROTOCOL_VERSION {
4049
return Ok(());
4150
}
4251
return Err(name.to_string());
4352
}
4453
Ok(())
4554
}
4655

56+
#[cfg(feature = "client-side-sse")]
57+
fn is_reserved_header_name(name: &http::HeaderName) -> bool {
58+
name == http::header::ACCEPT
59+
|| name == HEADER_NAME_SESSION_ID
60+
|| name == HEADER_NAME_MCP_PROTOCOL_VERSION
61+
|| name == HEADER_NAME_LAST_EVENT_ID
62+
}
63+
4764
/// Extracts the `scope=` parameter from a `WWW-Authenticate` header value.
4865
/// Handles both quoted (`scope="files:read files:write"`) and unquoted (`scope=read:data`) forms.
4966
#[cfg(feature = "client-side-sse")]
@@ -74,6 +91,9 @@ pub(crate) fn extract_scope_from_header(header: &str) -> Option<String> {
7491

7592
#[cfg(test)]
7693
mod tests {
94+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
95+
use http::{HeaderMap, HeaderName, HeaderValue};
96+
7797
#[cfg(feature = "client-side-sse")]
7898
use super::*;
7999

@@ -137,4 +157,37 @@ mod tests {
137157
let name = http::HeaderName::from_static("x-custom");
138158
assert!(validate_custom_header(&name).is_ok());
139159
}
160+
161+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
162+
#[test]
163+
fn header_name_constants_match_case_insensitively() {
164+
let cases = [
165+
(HEADER_NAME_SESSION_ID, "McP-SeSsIoN-Id"),
166+
(HEADER_NAME_LAST_EVENT_ID, "LaSt-EvEnT-Id"),
167+
(HEADER_NAME_MCP_PROTOCOL_VERSION, "McP-PrOtOcOl-VeRsIoN"),
168+
(HEADER_NAME_MCP_METHOD, "McP-MeThOd"),
169+
(HEADER_NAME_MCP_NAME, "McP-NaMe"),
170+
];
171+
172+
for (constant, mixed_case) in cases {
173+
let mut headers = HeaderMap::new();
174+
headers.insert(
175+
HeaderName::from_bytes(mixed_case.as_bytes()).expect("valid header name"),
176+
HeaderValue::from_static("value"),
177+
);
178+
179+
assert_eq!(
180+
headers.get(constant),
181+
Some(&HeaderValue::from_static("value"))
182+
);
183+
}
184+
}
185+
186+
#[cfg(any(feature = "client-side-sse", feature = "server-side-http"))]
187+
#[test]
188+
fn mcp_param_lower_prefix_matches_header_names() {
189+
let name = HeaderName::from_static("mcp-param-user");
190+
191+
assert!(name.as_str().starts_with(HEADER_MCP_PARAM_PREFIX_LOWER));
192+
}
140193
}

crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ use crate::{
1111
common::{
1212
client_side_sse::{DEFAULT_MAX_SSE_EVENT_SIZE, bounded_sse_stream},
1313
http_header::{
14-
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_SESSION_ID, JSON_MIME_TYPE,
15-
extract_scope_from_header, validate_custom_header,
14+
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_NAME_SESSION_ID,
15+
HEADER_SESSION_ID, JSON_MIME_TYPE, extract_scope_from_header,
16+
validate_custom_header,
1617
},
1718
},
1819
streamable_http_client::*,
@@ -257,7 +258,7 @@ impl StreamableHttpClient for reqwest::Client {
257258
let content_length = response.content_length();
258259
let session_id = response
259260
.headers()
260-
.get(HEADER_SESSION_ID)
261+
.get(HEADER_NAME_SESSION_ID)
261262
.and_then(|v| v.to_str().ok())
262263
.map(|s| s.to_string());
263264
// Spec requires 202 Accepted for these, but some servers return an empty 200.

crates/rmcp/src/transport/common/unix_socket.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ use crate::{
1515
common::{
1616
client_side_sse::{DEFAULT_MAX_SSE_EVENT_SIZE, bounded_sse_stream},
1717
http_header::{
18-
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_SESSION_ID, JSON_MIME_TYPE,
19-
extract_scope_from_header, validate_custom_header,
18+
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_NAME_SESSION_ID,
19+
HEADER_SESSION_ID, JSON_MIME_TYPE, extract_scope_from_header,
20+
validate_custom_header,
2021
},
2122
},
2223
streamable_http_client::*,
@@ -287,7 +288,7 @@ impl StreamableHttpClient for UnixSocketHttpClient {
287288
.and_then(|v| v.parse::<u64>().ok());
288289
let session_id = response
289290
.headers()
290-
.get(HEADER_SESSION_ID)
291+
.get(HEADER_NAME_SESSION_ID)
291292
.and_then(|v| v.to_str().ok())
292293
.map(|s| s.to_string());
293294

crates/rmcp/src/transport/streamable_http_client.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ use crate::{
2525
},
2626
service::InboundStreamOrigin,
2727
transport::{
28-
common::{client_side_sse::SseAutoReconnectStream, mcp_headers},
28+
common::{
29+
client_side_sse::SseAutoReconnectStream, http_header::HEADER_NAME_MCP_PROTOCOL_VERSION,
30+
mcp_headers,
31+
},
2932
worker::{Worker, WorkerQuitReason, WorkerSendRequest, WorkerTransport},
3033
},
3134
};
@@ -78,7 +81,7 @@ fn request_version_headers(
7881
};
7982
let mut headers = build_request_headers(base, message, tool_cache, &version);
8083
if let Ok(value) = HeaderValue::from_str(version.as_str()) {
81-
headers.insert(HeaderName::from_static("mcp-protocol-version"), value);
84+
headers.insert(HEADER_NAME_MCP_PROTOCOL_VERSION, value);
8285
}
8386
(version, headers)
8487
}
@@ -117,9 +120,8 @@ fn negotiate_version_headers(
117120
&& let ServerResult::InitializeResult(init_result) = &response.result
118121
{
119122
version = init_result.protocol_version.clone();
120-
// HeaderName::from_static requires lowercase
121123
if let Ok(hv) = HeaderValue::from_str(init_result.protocol_version.as_str()) {
122-
headers.insert(HeaderName::from_static("mcp-protocol-version"), hv);
124+
headers.insert(HEADER_NAME_MCP_PROTOCOL_VERSION, hv);
123125
}
124126
}
125127
(version, headers)
@@ -1137,8 +1139,7 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
11371139
if inline_version.is_some() {
11381140
negotiated_version = request_version.clone();
11391141
if let Ok(value) = HeaderValue::from_str(request_version.as_str()) {
1140-
protocol_headers
1141-
.insert(HeaderName::from_static("mcp-protocol-version"), value);
1142+
protocol_headers.insert(HEADER_NAME_MCP_PROTOCOL_VERSION, value);
11421143
}
11431144
if let Some(cleanup) = &mut session_cleanup_info {
11441145
cleanup.protocol_headers = protocol_headers.clone();

crates/rmcp/src/transport/streamable_http_server/tower.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ use crate::{
3939
OneshotTransport, TransportAdapterIdentity,
4040
common::{
4141
http_header::{
42-
EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_MCP_PROTOCOL_VERSION,
43-
HEADER_SESSION_ID, JSON_MIME_TYPE,
42+
EVENT_STREAM_MIME_TYPE, HEADER_NAME_LAST_EVENT_ID,
43+
HEADER_NAME_MCP_PROTOCOL_VERSION, HEADER_NAME_SESSION_ID, HEADER_SESSION_ID,
44+
JSON_MIME_TYPE,
4445
},
4546
mcp_headers,
4647
server_side_http::{
@@ -260,7 +261,7 @@ fn validate_protocol_version_header(
260261
headers: &http::HeaderMap,
261262
allow_unknown: bool,
262263
) -> Result<(), BoxResponse> {
263-
if let Some(value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) {
264+
if let Some(value) = headers.get(HEADER_NAME_MCP_PROTOCOL_VERSION) {
264265
let version_str = value.to_str().map_err(|_| {
265266
Response::builder()
266267
.status(http::StatusCode::BAD_REQUEST)
@@ -396,7 +397,7 @@ fn is_legacy_request(
396397
let version = from_body
397398
.or_else(|| {
398399
headers
399-
.get(HEADER_MCP_PROTOCOL_VERSION)
400+
.get(HEADER_NAME_MCP_PROTOCOL_VERSION)
400401
.and_then(|value| value.to_str().ok())
401402
.and_then(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
402403
})
@@ -466,7 +467,7 @@ fn validate_header_matches_init_body(
466467
body_version: &str,
467468
request_id: Option<RequestId>,
468469
) -> Result<(), BoxResponse> {
469-
let Some(header_value) = headers.get(HEADER_MCP_PROTOCOL_VERSION) else {
470+
let Some(header_value) = headers.get(HEADER_NAME_MCP_PROTOCOL_VERSION) else {
470471
return Ok(());
471472
};
472473
let header_str = header_value.to_str().map_err(|_| {
@@ -508,7 +509,7 @@ fn validate_request_protocol_version_meta(
508509
let is_discover = matches!(&request.request, ClientRequest::DiscoverRequest(_));
509510
let meta = request.request.get_meta();
510511
let header_version = headers
511-
.get(HEADER_MCP_PROTOCOL_VERSION)
512+
.get(HEADER_NAME_MCP_PROTOCOL_VERSION)
512513
.and_then(|value| value.to_str().ok());
513514
let Some(meta_version) = meta.protocol_version() else {
514515
let requires_request_metadata = is_discover
@@ -569,7 +570,7 @@ fn validate_required_protocol_header(
569570
// Initialize keeps its own header-matching rule.
570571
return Ok(());
571572
}
572-
if headers.contains_key(HEADER_MCP_PROTOCOL_VERSION) {
573+
if headers.contains_key(HEADER_NAME_MCP_PROTOCOL_VERSION) {
573574
return Ok(());
574575
}
575576
Err(header_mismatch_jsonrpc_response(
@@ -676,7 +677,7 @@ fn validate_standard_headers(
676677
tool_schema: impl Fn(&str) -> Option<Arc<JsonObject>>,
677678
) -> Result<(), BoxResponse> {
678679
let version_requires_headers = headers
679-
.get(HEADER_MCP_PROTOCOL_VERSION)
680+
.get(HEADER_NAME_MCP_PROTOCOL_VERSION)
680681
.and_then(|value| value.to_str().ok())
681682
.is_some_and(|version| version >= ProtocolVersion::STANDARD_HEADERS.as_str());
682683
if !version_requires_headers {
@@ -1562,7 +1563,7 @@ where
15621563
if !legacy_request {
15631564
let Some(last_event_id) = request
15641565
.headers()
1565-
.get(HEADER_LAST_EVENT_ID)
1566+
.get(HEADER_NAME_LAST_EVENT_ID)
15661567
.and_then(|value| value.to_str().ok())
15671568
else {
15681569
return Ok(method_not_allowed_response());
@@ -1586,7 +1587,7 @@ where
15861587
// check session id
15871588
let session_id = request
15881589
.headers()
1589-
.get(HEADER_SESSION_ID)
1590+
.get(HEADER_NAME_SESSION_ID)
15901591
.and_then(|v| v.to_str().ok())
15911592
.map(|s| s.to_owned().into());
15921593
let Some(session_id) = session_id else {
@@ -1622,7 +1623,7 @@ where
16221623
// check if last event id is provided
16231624
let last_event_id = parts
16241625
.headers
1625-
.get(HEADER_LAST_EVENT_ID)
1626+
.get(HEADER_NAME_LAST_EVENT_ID)
16261627
.and_then(|v| v.to_str().ok())
16271628
.map(|s| s.to_owned());
16281629
if let Some(last_event_id) = last_event_id {
@@ -1730,7 +1731,7 @@ where
17301731
// do we have a session id?
17311732
let session_id = part
17321733
.headers
1733-
.get(HEADER_SESSION_ID)
1734+
.get(HEADER_NAME_SESSION_ID)
17341735
.and_then(|v| v.to_str().ok());
17351736
if let Some(session_id) = session_id {
17361737
let session_id = session_id.to_owned().into();
@@ -2048,7 +2049,7 @@ where
20482049
// check session id
20492050
let session_id = request
20502051
.headers()
2051-
.get(HEADER_SESSION_ID)
2052+
.get(HEADER_NAME_SESSION_ID)
20522053
.and_then(|v| v.to_str().ok())
20532054
.map(|s| s.to_owned().into());
20542055
let Some(session_id) = session_id else {
@@ -2092,7 +2093,7 @@ where
20922093
init.params.protocol_version.clone()
20932094
} else {
20942095
headers
2095-
.get(HEADER_MCP_PROTOCOL_VERSION)
2096+
.get(HEADER_NAME_MCP_PROTOCOL_VERSION)
20962097
.and_then(|v| v.to_str().ok())
20972098
.and_then(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
20982099
.unwrap_or(ProtocolVersion::V_2025_03_26)

0 commit comments

Comments
 (0)