Skip to content

fix(mcp): fix auth header handling and bump rmcp to 1.1 for stateless HTTP support#330

Open
ciaranashton wants to merge 12 commits intospacedriveapp:mainfrom
ciaranashton:fix/mcp-http-auth-header
Open

fix(mcp): fix auth header handling and bump rmcp to 1.1 for stateless HTTP support#330
ciaranashton wants to merge 12 commits intospacedriveapp:mainfrom
ciaranashton:fix/mcp-http-auth-header

Conversation

@ciaranashton
Copy link
Contributor

@ciaranashton ciaranashton commented Mar 5, 2026

Summary

Fixes two issues with MCP HTTP transport connections:

  1. Auth header not sent on all requests — The Authorization header was passed via custom_headers(), which rmcp only includes on POST requests. The initial GET/SSE request went out unauthenticated, causing 401 errors. Now extracted and passed via .auth_header() which is included on all request types.

  2. Stateless HTTP servers fail to connect — rmcp 0.17 expected persistent sessions, so stateless MCP servers (those with sessionIdGenerator: undefined) failed with "unexpected server response: expect initialized, accepted". Bumping to rmcp 1.1.0 adds proper stateless Streamable HTTP support.

Changes

  • src/mcp.rs: Extract Authorization from resolved headers and pass via .auth_header() instead of .custom_headers()
  • src/mcp.rs: Validate Authorization header value before use
  • src/mcp.rs: Strip Bearer prefix before passing to .auth_header() (rmcp adds it automatically via reqwest's bearer_auth())
  • src/mcp.rs: Migrate to builder APIs for ClientInfo, Implementation, CallToolRequestParams (now #[non_exhaustive] in rmcp 1.1)
  • Cargo.toml: Bump rmcp from 0.17 to 1.1
  • Dockerfile: Add system Chromium for cross-arch browser support

Test plan

  • Tested locally against a stateless Streamable HTTP MCP server with API key auth
  • Verify stdio MCP transport still works
  • Verify HTTP MCP servers without auth headers still connect

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This pull request introduces builder-style construction patterns for MCP protocol models, adds per-server Authorization header handling for HTTP transport, exposes available MCP tool names through a new get_tool_names method on McpManager, and integrates those tool names into worker capability prompts across the agent layer.

Changes

Cohort / File(s) Summary
MCP Core Implementation
src/mcp.rs, Cargo.toml
Refactored ClientInfo and CallToolRequestParams to use builder patterns; added Authorization header parsing and validation with contextual error reporting; introduced get_tool_names() async method on McpManager to collect namespaced tool descriptors from connected servers; extended HTTP transport setup for per-server Authorization header handling.
Prompt Template
prompts/en/fragments/worker_capabilities.md.j2
Added conditional section to render "MCP tools" header and bullet-list of available tools when mcp_tool_names is provided.
Agent Layer
src/agent/channel.rs, src/agent/cortex_chat.rs
Integrated asynchronous retrieval of MCP tool names via get_tool_names().await and passed results to render_worker_capabilities for both standard and batched message handling paths.
Prompt Engine
src/prompts/engine.rs
Extended render_worker_capabilities signature to accept mcp_tool_names: &[String] parameter and wired it into template context for MCP tools rendering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: fixing MCP auth header handling and bumping rmcp to support stateless HTTP.
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining specific fixes for MCP HTTP transport connection issues and detailing changes across src/mcp.rs, Cargo.toml, and Dockerfile.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ciaranashton ciaranashton changed the title fix(mcp): pass auth header via dedicated rmcp method for HTTP transport fix(mcp): fix auth header handling and bump rmcp to 1.1 for stateless HTTP support Mar 5, 2026
ciaranashton and others added 3 commits March 5, 2026 18:18
…header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Upgrade rmcp from 0.17 to 1.1.0 for stateless Streamable HTTP server
  compatibility (servers with sessionIdGenerator: undefined)
- Migrate to builder APIs for non_exhaustive structs (ClientInfo,
  Implementation, CallToolRequestParams)
- Strip "Bearer " prefix before passing to auth_header() since rmcp
  adds it automatically via reqwest's bearer_auth()
- Add system Chromium to Dockerfile for cross-arch browser support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ciaranashton ciaranashton force-pushed the fix/mcp-http-auth-header branch from 0e7ecc7 to e598a9a Compare March 5, 2026 18:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/mcp.rs (1)

366-375: Consider case-insensitive prefix stripping for robustness.

The current logic only strips "Bearer " and "bearer ", but HTTP authentication schemes are case-insensitive. A user providing "BEARER token" or "BeArEr token" would result in a malformed header like "Bearer BEARER token".

♻️ Suggested fix for case-insensitive handling
 if let Some(auth_value) = auth_header_value {
     // rmcp uses reqwest's .bearer_auth() which adds
     // "Bearer " automatically, so strip the prefix if
     // the user included it in their config.
-    let token = auth_value
-        .strip_prefix("Bearer ")
-        .or_else(|| auth_value.strip_prefix("bearer "))
-        .unwrap_or(&auth_value);
+    let token = if auth_value.len() > 7
+        && auth_value[..7].eq_ignore_ascii_case("bearer ")
+    {
+        &auth_value[7..]
+    } else {
+        &auth_value
+    };
     transport_config = transport_config.auth_header(token);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp.rs` around lines 366 - 375, The auth prefix stripping is currently
only handling "Bearer " and "bearer " and should be made case-insensitive;
update the logic around auth_header_value where you call strip_prefix so that
you detect and remove any case variation of the "Bearer " prefix (e.g. by
checking the first 7 chars of auth_header_value, comparing them in a
case-insensitive way such as to_ascii_lowercase() == "bearer ", and then slicing
off that prefix) before calling transport_config.auth_header(token) so tokens
like "BEARER token" or "BeArEr token" are normalized correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/mcp.rs`:
- Around line 366-375: The auth prefix stripping is currently only handling
"Bearer " and "bearer " and should be made case-insensitive; update the logic
around auth_header_value where you call strip_prefix so that you detect and
remove any case variation of the "Bearer " prefix (e.g. by checking the first 7
chars of auth_header_value, comparing them in a case-insensitive way such as
to_ascii_lowercase() == "bearer ", and then slicing off that prefix) before
calling transport_config.auth_header(token) so tokens like "BEARER token" or
"BeArEr token" are normalized correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf0174ce-8875-465c-8366-e0cfd2b5b3e9

📥 Commits

Reviewing files that changed from the base of the PR and between dcd35a4 and 0e7ecc7.

⛔ Files ignored due to path filters (1)
  • Cargo.toml is excluded by !**/*.toml
📒 Files selected for processing (2)
  • Dockerfile
  • src/mcp.rs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Dockerfile (1)

52-53: ⚠️ Potential issue | 🟡 Minor

Update stale runtime stage comment.

Lines 52-53 still reference "Chrome runtime libraries for fetcher-downloaded Chromium" and imply Chrome is "downloaded on first browser tool use." With the system chromium package now installed, Chromium is pre-installed and the fetcher should not be triggered.

📝 Suggested comment update
 # ---- Runtime stage ----
-# Minimal runtime with Chrome runtime libraries for fetcher-downloaded Chromium.
-# Chrome itself is downloaded on first browser tool use and cached on the volume.
+# Minimal runtime with system Chromium for browser tools.
+# System package works on amd64/arm64 and avoids the x86_64-only BrowserFetcher.
 FROM debian:bookworm-slim
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 52 - 53, The runtime-stage comment that currently
reads "Minimal runtime with Chrome runtime libraries for fetcher-downloaded
Chromium" is stale; update that comment (the Dockerfile comment that references
the Chrome runtime and the fetcher behavior) to state that the system "chromium"
package is now installed and Chromium is pre-installed on the image, and remove
any text implying Chromium is downloaded on first browser tool use or that a
fetcher will be triggered; keep the comment concise and accurate about
pre-installed Chromium and cached browser state.
🧹 Nitpick comments (1)
src/mcp.rs (1)

261-263: Use a non-abbreviated local name instead of args.

Please rename args to a full name to match repo Rust naming conventions.

♻️ Suggested rename
-        if let Some(args) = arguments {
-            params = params.with_arguments(args);
+        if let Some(arguments_map) = arguments {
+            params = params.with_arguments(arguments_map);
         }

As per coding guidelines "Use non-abbreviated variable names in Rust code: queue not q, message not msg, channel not ch; common abbreviations like config are acceptable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp.rs` around lines 261 - 263, The local pattern binding `args` should
be renamed to a non-abbreviated name to match Rust conventions; change the `if
let Some(args) = arguments { ... }` to e.g. `if let Some(arguments_value) =
arguments { params = params.with_arguments(arguments_value); }`, updating the
binding reference used in the call to `params.with_arguments` (symbols to
locate: `arguments`, `params`, `with_arguments`).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp.rs`:
- Around line 366-375: The current logic around auth_header_value can pass
non-Bearer schemes into transport_config.auth_header(), producing malformed
headers; update the block handling auth_header_value to validate the scheme: if
auth_header_value contains no space treat it as a raw token and pass to
transport_config.auth_header(token); if it contains a space, split into scheme
and token and only accept scheme equals "Bearer" (case-insensitive) then pass
token; otherwise return/fail fast with a clear error (propagate a Result/Error)
indicating unsupported auth scheme. Ensure you reference and update the existing
auth_header_value handling and calls to transport_config.auth_header to
implement this validation and proper early error return.

---

Outside diff comments:
In `@Dockerfile`:
- Around line 52-53: The runtime-stage comment that currently reads "Minimal
runtime with Chrome runtime libraries for fetcher-downloaded Chromium" is stale;
update that comment (the Dockerfile comment that references the Chrome runtime
and the fetcher behavior) to state that the system "chromium" package is now
installed and Chromium is pre-installed on the image, and remove any text
implying Chromium is downloaded on first browser tool use or that a fetcher will
be triggered; keep the comment concise and accurate about pre-installed Chromium
and cached browser state.

---

Nitpick comments:
In `@src/mcp.rs`:
- Around line 261-263: The local pattern binding `args` should be renamed to a
non-abbreviated name to match Rust conventions; change the `if let Some(args) =
arguments { ... }` to e.g. `if let Some(arguments_value) = arguments { params =
params.with_arguments(arguments_value); }`, updating the binding reference used
in the call to `params.with_arguments` (symbols to locate: `arguments`,
`params`, `with_arguments`).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5af2878-a90b-4d56-97d9-8bf23ae031d3

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7ecc7 and e598a9a.

⛔ Files ignored due to path filters (1)
  • Cargo.toml is excluded by !**/*.toml
📒 Files selected for processing (2)
  • Dockerfile
  • src/mcp.rs

ciaranashton and others added 2 commits March 5, 2026 19:09
Add get_tool_names() to McpManager and pass connected MCP tool names
into the worker_capabilities prompt template so the channel agent
knows which external tools are available to workers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/mcp.rs (1)

366-375: ⚠️ Potential issue | 🟠 Major

Validate and normalize auth scheme before calling .auth_header().

This still accepts values like Basic ... (or malformed bearer values), which then become Authorization: Bearer Basic ... downstream. Reject unsupported schemes and empty bearer tokens early.

🔧 Suggested fix
                 if let Some(auth_value) = auth_header_value {
-                    // rmcp uses reqwest's .bearer_auth() which adds
-                    // "Bearer " automatically, so strip the prefix if
-                    // the user included it in their config.
-                    let token = auth_value
-                        .strip_prefix("Bearer ")
-                        .or_else(|| auth_value.strip_prefix("bearer "))
-                        .unwrap_or(&auth_value);
-                    transport_config = transport_config.auth_header(token);
+                    // rmcp/reqwest add the "Bearer " scheme automatically.
+                    // Accept either a raw token or "Bearer <token>".
+                    let token = match auth_value.split_once(char::is_whitespace) {
+                        Some((scheme, value)) => {
+                            if !scheme.eq_ignore_ascii_case("bearer") {
+                                return Err(anyhow!(
+                                    "unsupported authorization scheme '{}' for server '{}'; expected Bearer token",
+                                    scheme,
+                                    self.name
+                                ));
+                            }
+                            let value = value.trim();
+                            if value.is_empty() {
+                                return Err(anyhow!(
+                                    "missing bearer token in authorization header for server '{}'",
+                                    self.name
+                                ));
+                            }
+                            value
+                        }
+                        None => {
+                            if auth_value.is_empty() {
+                                return Err(anyhow!(
+                                    "missing bearer token in authorization header for server '{}'",
+                                    self.name
+                                ));
+                            }
+                            auth_value.as_str()
+                        }
+                    };
+                    transport_config = transport_config.auth_header(token);
                 }
In rmcp 1.1.0, what format does StreamableHttpClientTransportConfig::auth_header() expect, and does the reqwest transport prepend "Bearer " automatically?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp.rs` around lines 366 - 375, The code currently accepts any
auth_header_value and naively strips "Bearer " prefixes, causing values like
"Basic ..." to become "Authorization: Bearer Basic ..."; update the block that
handles auth_header_value to validate and normalize the scheme: parse
auth_header_value to split scheme and token (case-insensitive), accept only
"bearer" and ensure token is non-empty, then call
transport_config.auth_header(token); for unsupported schemes or empty tokens do
not set auth_header (or return an error) so transport_config.auth_header() is
only called with a raw bearer token; refer to auth_header_value,
transport_config, and StreamableHttpClientTransportConfig::auth_header in the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp.rs`:
- Around line 465-505: get_tool_names currently injects raw tool.description
into prompt-visible names and can hide the server namespace; update
get_tool_names to always include a stable server namespace (use
connection.name()/server_name combined with tool.name, e.g.
"{server_name}:{tool.name}") and stop inserting raw descriptions directly. If
you must include a description, sanitize it by
stripping/control-character/newline removal, limit to a short max length (e.g.
100 chars) and escape or remove special characters before appending in
parentheses; perform this sanitization where tool.description is read (in
get_tool_names before formatting) and use the sanitized string in the names.push
call so list_tools/tool.description cannot steer prompts or obscure server
identity.

---

Duplicate comments:
In `@src/mcp.rs`:
- Around line 366-375: The code currently accepts any auth_header_value and
naively strips "Bearer " prefixes, causing values like "Basic ..." to become
"Authorization: Bearer Basic ..."; update the block that handles
auth_header_value to validate and normalize the scheme: parse auth_header_value
to split scheme and token (case-insensitive), accept only "bearer" and ensure
token is non-empty, then call transport_config.auth_header(token); for
unsupported schemes or empty tokens do not set auth_header (or return an error)
so transport_config.auth_header() is only called with a raw bearer token; refer
to auth_header_value, transport_config, and
StreamableHttpClientTransportConfig::auth_header in the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1dde19a-c504-4014-82cc-03eb6b068770

📥 Commits

Reviewing files that changed from the base of the PR and between e598a9a and beae9e8.

📒 Files selected for processing (5)
  • prompts/en/fragments/worker_capabilities.md.j2
  • src/agent/channel.rs
  • src/agent/cortex_chat.rs
  • src/mcp.rs
  • src/prompts/engine.rs

Comment on lines +465 to +505
/// Return namespaced tool names for all connected MCP servers.
///
/// Used to inform the channel prompt about available MCP tools so the
/// agent knows it can delegate work that uses them.
pub async fn get_tool_names(&self) -> Vec<String> {
let connections = self
.connections
.read()
.await
.values()
.cloned()
.collect::<Vec<_>>();

let mut names = Vec::new();
for connection in connections {
if !connection.is_connected().await {
continue;
}

let server_name = connection.name().to_string();
let tools = connection.list_tools().await;
for tool in tools {
let description = tool
.description
.as_ref()
.map(|d| d.as_ref().to_string())
.unwrap_or_default();
names.push(format!(
"{} — {}",
tool.name,
if description.is_empty() {
format!("from {}", server_name)
} else {
description
}
));
}
}

names
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid injecting raw external tool descriptions into prompt-visible “tool names.”

get_tool_names() currently includes unsanitized tool.description text from external MCP servers and may omit stable server namespacing when descriptions are present. This can pollute/steer prompt behavior and makes same-named tools ambiguous across servers.

🔧 Suggested hardening
         let mut names = Vec::new();
         for connection in connections {
@@
             let server_name = connection.name().to_string();
             let tools = connection.list_tools().await;
             for tool in tools {
-                let description = tool
+                let namespaced_name = format!("{}::{}", server_name, tool.name);
+                let description = tool
                     .description
                     .as_ref()
-                    .map(|d| d.as_ref().to_string())
+                    .map(|d| d.as_ref().replace(['\r', '\n'], " ").trim().to_string())
                     .unwrap_or_default();
-                names.push(format!(
-                    "{} — {}",
-                    tool.name,
-                    if description.is_empty() {
-                        format!("from {}", server_name)
-                    } else {
-                        description
-                    }
-                ));
+                if description.is_empty() {
+                    names.push(namespaced_name);
+                } else {
+                    names.push(format!("{namespaced_name} — {description}"));
+                }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp.rs` around lines 465 - 505, get_tool_names currently injects raw
tool.description into prompt-visible names and can hide the server namespace;
update get_tool_names to always include a stable server namespace (use
connection.name()/server_name combined with tool.name, e.g.
"{server_name}:{tool.name}") and stop inserting raw descriptions directly. If
you must include a description, sanitize it by
stripping/control-character/newline removal, limit to a short max length (e.g.
100 chars) and escape or remove special characters before appending in
parentheses; perform this sanitization where tool.description is read (in
get_tool_names before formatting) and use the sanitized string in the names.push
call so list_tools/tool.description cannot steer prompts or obscure server
identity.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp.rs`:
- Around line 366-388: The Authorization parsing in the block handling
auth_header_value should be made case-insensitive for the scheme and must reject
empty or missing bearer tokens; update the logic around auth_header_value /
has_scheme / token so you: 1) split the header at the first space (or treat the
whole header as token if no space) and compare the scheme case-insensitively
(e.g., lowercase) rather than only matching "Bearer " or "bearer "; 2) if a
scheme is present and it is not "bearer" bail with an error including self.name;
3) if the scheme is "bearer" ensure the following token is non-empty (bail if
empty or only whitespace); and 4) pass the validated non-empty token to
transport_config.auth_header(token).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52637cce-9327-432d-84c3-bcf6e6dda7ba

📥 Commits

Reviewing files that changed from the base of the PR and between beae9e8 and 05d85a0.

📒 Files selected for processing (1)
  • src/mcp.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant