Skip to content

Python: Add origin-scoped headers for MCP connect authentication - #7892

Closed
Shivani . (Shivani767) wants to merge 2 commits into
microsoft:mainfrom
Shivani767:fix/7841-mcp-connect-headers
Closed

Python: Add origin-scoped headers for MCP connect authentication#7892
Shivani . (Shivani767) wants to merge 2 commits into
microsoft:mainfrom
Shivani767:fix/7841-mcp-connect-headers

Conversation

@Shivani767

@Shivani767 Shivani . (Shivani767) commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

MCPStreamableHTTPTool.connect() can still reach servers that authenticate the initialize handshake without credentials when auth comes only from a kwargs-dependent header_provider. Per-call kwargs are not available until call_tool(), so those servers get a 401 (or hang) before any tool call runs.

#7305 already delivers header_provider headers across transport tasks and authenticates ambient requests for static providers. This PR covers the remaining connect-time gap for servers that require handshake auth.

Description & Review Guide

  • What are the major changes?

    • Add an origin-scoped headers= constructor argument on MCPStreamableHTTPTool.
    • Apply those static headers to initialize, discovery, pings, reconnects, and tool calls.
    • Keep header_provider as a per-call overlay on top of headers=.
    • Strip previously injected header keys on cross-origin redirects (covers secrets like X-API-Key that HTTPX would otherwise copy).
    • Route SecureMCPToolProxy URL mode through headers= instead of baking tokens into AsyncClient.
    • Document the connect vs per-call split in AGENTS.md and the API-key sample.
    • Add regression tests for ambient injection, call overlay, cross-origin strip, and end-to-end initialize auth.
  • What is the impact of these changes?

    • Callers can authenticate MCP connect() without baking tokens into a custom httpx.AsyncClient.
    • Kwargs-only header_provider setups can still authenticate the handshake via headers= while using dynamic tokens for tool calls.
    • Existing header_provider / custom-client behavior is unchanged for callers that already worked.
  • What do you want reviewers to focus on?

    • Whether headers= is the right connect-time contract vs changing ambient header_provider({}) behavior.
    • Origin-scoped injection and cross-origin stripping for non-Authorization secrets.

Related Issue

Fixes #7841

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI balanced review requested due to automatic review settings August 26, 2026 19:06
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Aug 26, 2026

Copilot AI 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.

Pull request overview

Adds origin-scoped static headers to authenticate MCP initialization while retaining per-call header overlays.

Changes:

  • Adds headers= support and header resolution.
  • Adds ambient, overlay, redirect, and initialize tests.
  • Documents connect-time versus per-call authentication.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
python/packages/core/agent_framework/_mcp.py Implements static header injection.
python/packages/core/tests/core/test_mcp.py Adds authentication regression tests.
python/packages/core/agent_framework/security.py Updates proxy guidance.
python/packages/core/AGENTS.md Documents header behavior.
python/samples/02-agents/mcp/mcp_api_key_auth.py Explains handshake authentication.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/packages/core/tests/core/test_mcp.py Outdated
Comment thread python/packages/core/agent_framework/security.py Outdated
Comment thread python/packages/core/agent_framework/_mcp.py
Allow MCPStreamableHTTPTool to authenticate initialize/handshake with static headers while keeping header_provider for per-call overlays, so kwargs-only providers no longer leave connect unauthenticated.
Track headers injected by the MCP request hook and remove them on redirected cross-origin requests, route SecureMCPToolProxy through headers=, and harden the redirect regression tests with X-API-Key.
Comment on lines 3178 to +3183
if _url_origin(request.url) != target_origin:
# Strip secrets this hook previously injected. HTTPX removes
# Authorization on cross-origin redirects, but other credentials
# (e.g. X-API-Key) can remain on the redirected request.
for key in self._injected_header_keys:
request.headers.pop(key, None)

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.

Following up on the redirect thread: could we track these keys on the request rather than on the tool? _injected_header_keys is the last same-origin request's global set, so an independent request through a shared http_client to another origin loses its own same-named header; concurrent ambient and call traffic can also replace the set before a redirect, leaving the earlier secret attached. A marker in request.extensions would follow the HTTPX redirect chain without affecting unrelated requests.

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.

Should the client created here be owned and closed by the tool? Passing this AsyncClient into streamable_http_client makes the SDK treat it as caller-owned, so _exit_stack never closes it; every MCPStreamableHTTPTool(headers=...) or URL-mode SecureMCPToolProxy lifecycle leaves its connection pool open. Could the tool register http_client.aclose on _exit_stack only when it creates the client?

@moonbox3

Copy link
Copy Markdown
Contributor

Shivani . (@Shivani767) please fix the file conflicts as well, thanks.

@moonbox3

Copy link
Copy Markdown
Contributor

Re-open when wanting to take this forward.

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

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: MCPStreamableHTTPTool.connect() never sends a header_provider's headers, so any headers_env/header_provider-authenticated server 401s on connect

3 participants