Skip to content

Commit d4801bd

Browse files
its-mashalexhancock
authored andcommitted
fix: use correct HTTP status codes for session errors per MCP spec
MCP spec (2025-11-25) section "Session Management" requires: - Missing session ID header → 400 Bad Request (not 401) - Unknown/terminated session → 404 Not Found (not 401) Using 401 Unauthorized caused MCP clients (e.g. VS Code) to trigger full OAuth re-authentication on server restart, instead of simply re-initializing the session. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent a56742a commit d4801bd

2 files changed

Lines changed: 23 additions & 23 deletions

File tree

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,10 @@ where
299299
.and_then(|v| v.to_str().ok())
300300
.map(|s| s.to_owned().into());
301301
let Some(session_id) = session_id else {
302-
// unauthorized
302+
// MCP spec: servers that require a session ID SHOULD respond with 400 Bad Request
303303
return Ok(Response::builder()
304-
.status(http::StatusCode::UNAUTHORIZED)
305-
.body(Full::new(Bytes::from("Unauthorized: Session ID is required")).boxed())
304+
.status(http::StatusCode::BAD_REQUEST)
305+
.body(Full::new(Bytes::from("Bad Request: Session ID is required")).boxed())
306306
.expect("valid response"));
307307
};
308308
// check if session exists
@@ -312,10 +312,10 @@ where
312312
.await
313313
.map_err(internal_error_response("check session"))?;
314314
if !has_session {
315-
// unauthorized
315+
// MCP spec: server MUST respond with 404 Not Found for terminated/unknown sessions
316316
return Ok(Response::builder()
317-
.status(http::StatusCode::UNAUTHORIZED)
318-
.body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed())
317+
.status(http::StatusCode::NOT_FOUND)
318+
.body(Full::new(Bytes::from("Not Found: Session not found")).boxed())
319319
.expect("valid response"));
320320
}
321321
// Validate MCP-Protocol-Version header (per 2025-06-18 spec)
@@ -444,10 +444,10 @@ where
444444
.await
445445
.map_err(internal_error_response("check session"))?;
446446
if !has_session {
447-
// unauthorized
447+
// MCP spec: server MUST respond with 404 Not Found for terminated/unknown sessions
448448
return Ok(Response::builder()
449-
.status(http::StatusCode::UNAUTHORIZED)
450-
.body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed())
449+
.status(http::StatusCode::NOT_FOUND)
450+
.body(Full::new(Bytes::from("Not Found: Session not found")).boxed())
451451
.expect("valid response"));
452452
}
453453

@@ -647,10 +647,10 @@ where
647647
.and_then(|v| v.to_str().ok())
648648
.map(|s| s.to_owned().into());
649649
let Some(session_id) = session_id else {
650-
// unauthorized
650+
// MCP spec: servers that require a session ID SHOULD respond with 400 Bad Request
651651
return Ok(Response::builder()
652-
.status(http::StatusCode::UNAUTHORIZED)
653-
.body(Full::new(Bytes::from("Unauthorized: Session ID is required")).boxed())
652+
.status(http::StatusCode::BAD_REQUEST)
653+
.body(Full::new(Bytes::from("Bad Request: Session ID is required")).boxed())
654654
.expect("valid response"));
655655
};
656656
// Validate MCP-Protocol-Version header (per 2025-06-18 spec)

crates/rmcp/tests/test_sse_channel_replacement_bug.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -577,9 +577,10 @@ async fn reconnect_after_stream_timeout() {
577577

578578
// ─── Tests: Edge cases ──────────────────────────────────────────────────────
579579

580-
/// GET without a valid session ID should return 401 Unauthorized.
580+
/// GET with an unknown session ID should return 404 Not Found per MCP spec.
581+
/// This signals the client to re-initialize (not re-authenticate).
581582
#[tokio::test]
582-
async fn get_without_valid_session_returns_401() {
583+
async fn get_without_valid_session_returns_404() {
583584
let ct = CancellationToken::new();
584585
let trigger = Arc::new(Notify::new());
585586
let url = start_test_server(ct.clone(), trigger).await;
@@ -595,16 +596,16 @@ async fn get_without_valid_session_returns_401() {
595596

596597
assert_eq!(
597598
resp.status().as_u16(),
598-
401,
599-
"GET with invalid session ID should return 401"
599+
404,
600+
"GET with unknown session ID should return 404 Not Found per MCP spec"
600601
);
601602

602603
ct.cancel();
603604
}
604605

605-
/// GET without session ID header should return an error (400 or similar).
606+
/// GET without session ID header should return 400 Bad Request per MCP spec.
606607
#[tokio::test]
607-
async fn get_without_session_id_header_returns_error() {
608+
async fn get_without_session_id_header_returns_400() {
608609
let ct = CancellationToken::new();
609610
let trigger = Arc::new(Notify::new());
610611
let url = start_test_server(ct.clone(), trigger).await;
@@ -617,11 +618,10 @@ async fn get_without_session_id_header_returns_error() {
617618
.await
618619
.expect("GET without session ID");
619620

620-
// Should fail (400 Bad Request or similar)
621-
assert!(
622-
!resp.status().is_success(),
623-
"GET without session ID should fail, got {}",
624-
resp.status()
621+
assert_eq!(
622+
resp.status().as_u16(),
623+
400,
624+
"GET without session ID should return 400 Bad Request per MCP spec"
625625
);
626626

627627
ct.cancel();

0 commit comments

Comments
 (0)