feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: transparent session re-init on HTTP 404 for streamable HTTP - #743

Merged
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit
Mar 11, 2026
Merged

feat: transparent session re-init on HTTP 404 for streamable HTTP#743
DaleSeo merged 1 commit into
mainfrom
feat/streamable-http-session-reinit

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Mar 10, 2026

Copy link
Copy Markdown
Member

Fixes#733

Motivation and Context

The MCP spec on Session Management states that when a client gets an HTTP 404 response to a request with Mcp-Session-Id, it must start a new session by sending a fresh InitializeRequest without a session ID. Before, the streamable HTTP client would show a generic UnexpectedServerResponse error and get stuck with the old session, which goes against the spec.

This PR allows the client to detect 404 responses on session-aware requests and show them as a new SessionExpired error. It will then automatically re-initialize by replaying the original initialize handshake, setting up a new SSE stream, and retrying the original message, all without the caller noticing. It will attempt a single retry, and if the re-initialization fails, the error will propagate normally to avoid infinite loops.

How Has This Been Tested?

Added new integration tests to verify both the low-level error detection and the end-to-end transparent recovery.

Breaking Changes

StreamableHttpError gains a new SessionExpired variant. Because the enum is #[non_exhaustive], downstream code that matches on it will not break.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@github-actionsgithub-actionsBot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Mar 10, 2026
@DaleSeoDaleSeo changed the title feat: transparent session re-init on HTTP 404feat: transparent session re-init on HTTP 404 for streamable HTTPMar 10, 2026
@DaleSeoDaleSeo self-assigned this Mar 10, 2026
.expect_initialized::<C::Error>()
.await?;

let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
new_session_id_str
to a log file.

Copilot Autofix

AI 6 months ago

In general, to fix cleartext logging of sensitive information, you either (a) remove the sensitive data from the log output entirely, or (b) mask/replace it with a redacted or hashed version that cannot be used to compromise security. You only log high‑level status (e.g., “session reinitialized”) rather than the exact session identifier or token.

For this specific code, the data in question is new_session_id_str / new_session_id, which likely contains a session identifier. The safe approach is to ensure that, when we log reinitialization, we do not ever include the actual session id. Since the only logging machinery visible in this file is tracing::debug, and CodeQL says that the mapping operation leads to a logging sink, the least‑intrusive fix is to add a dedicated debug message that explicitly avoids printing the session id, and not log new_session_id_str directly at all. To make the intent clear and to satisfy the analyzer, we can log only whether a session id was obtained (e.g., Some vs None), without including its content. This preserves existing functionality (session handling logic is unchanged) while removing any potential for accidentally logging the raw session identifier.

Concretely, within perform_reinitialization in crates/rmcp/src/transport/streamable_http_client.rs, right after we convert new_session_id_str into new_session_id, we’ll add a debug! call that reports only the presence/absence of the ID, not the ID value. We do not need any new imports (the file already imports tracing::debug). We avoid any change to how new_session_id is used elsewhere.

Suggested changeset 1
crates/rmcp/src/transport/streamable_http_client.rs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs
--- a/crates/rmcp/src/transport/streamable_http_client.rs
+++ b/crates/rmcp/src/transport/streamable_http_client.rs
@@ -343,6 +343,10 @@
.await?;
let new_session_id: Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
+ debug!(
+ "Reinitialization completed; new session id obtained: {}",
+ if new_session_id.is_some() { "yes" } else { "no" }
+ );
// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
EOF
@@ -343,6 +343,10 @@
.await?;

let new_session_id:Option<Arc<str>> = new_session_id_str.map(|s| Arc::from(s.as_str()));
debug!(
"Reinitialization completed; new session id obtained: {}",
if new_session_id.is_some(){"yes"}else{"no"}
);

// Start from custom_headers, then inject the negotiated MCP-Protocol-Version
// so all subsequent requests carry the right version (MCP 2025-06-18 spec).
Copilot is powered by AI and may make mistakes. Always verify output.

@DaleSeoDaleSeoMar 11, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a false positive. We're not even logging the session ID in the first place. new_session_id_str is only mapped into an Arc<str> and returned.

@DaleSeo
DaleSeo marked this pull request as ready for review March 11, 2026 13:09
@DaleSeo
DaleSeo requested a review from a team as a code ownerMarch 11, 2026 13:09

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@DaleSeo
DaleSeo merged commit 27b0096 into mainMar 11, 2026
15 of 16 checks passed
@DaleSeo
DaleSeo deleted the feat/streamable-http-session-reinit branch March 11, 2026 14:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP client does not re-initialize session on HTTP 404 (MCP spec violation)

3 participants

@DaleSeo@alexhancock@github-advanced-security