Skip to content

Accept a header provider on MCPClient and scope headers to the server origin - #72

Merged
shibayan merged 6 commits into
masterfrom
add-mcp-header-provider
Aug 12, 2026
Merged

Accept a header provider on MCPClient and scope headers to the server origin#72
shibayan merged 6 commits into
masterfrom
add-mcp-header-provider

Conversation

@shibayan

@shibayan shibayan commented Aug 11, 2026

Copy link
Copy Markdown
Member

MCPClient took headers as a static record and handed it to the transport once, when the
connection opened. A credential that has to be refreshed could not be expressed that way, and a
connection shared across users would keep sending whatever was captured when it opened — which is
why FoundryToolbox already bypassed the option with a fetch wrapper of its own, attaching the
token and the call id of the request in flight.

What changes

  • McpConnection gains a url form: give it url — and optionally headers and fetch
    and it builds its own Streamable HTTP transport, a fresh one per connection attempt, so the
    reconnect behaviour matches the url form of MCPClient.
  • headers accepts a record, or a function called once per request, so an expiring token is
    refreshed without writing a transport. Both forms are injected through a fetch wrapper placed
    around the caller's own fetch, so replacing it cannot drop them. The wrapper itself is
    internal to the package: consumers configure headers on MCPClient or McpConnection rather
    than assembling a fetch of their own.
  • Headers are attached only to requests whose origin matches the configured url, so a bearer
    token, a call id or W3C trace context is not handed to a host the caller never named. Python
    applies the same rule to its header provider.
  • A configured header never overrides one the transport already set — the content type, the
    session id, an authorization from the SDK's own auth support all stay the SDK's, which is the
    precedence the static requestInit route always had.
  • A custom transport owns its own fetch, so combining it with headers or fetch on
    McpConnection is refused rather than silently ignored — a credential that never reaches the
    wire should not look configured.
  • MCPClient's url form and FoundryToolbox both delegate to the connection. The toolbox drops
    its hand-built transport wiring and gains the origin scoping it did not have; its per-call
    authorization and platform headers are unchanged.

Redirects, hop by hop

The platform fetch follows redirects internally, which would make the origin check a decision
about the first URL only: fetch strips Authorization and Cookie when a redirect leaves the
origin, but forwards every custom header — an API key, a platform identity header — to wherever
the server pointed. So when headers are configured, redirects are followed here instead, with
redirect: 'manual', one hop at a time. A hop on the configured origin gets fresh headers from
the provider, a hop off it gets none, and what fetch itself strips cross-origin is stripped the
same way. That is the per-hop behaviour of the httpx event hook Python uses for the same job.
The spec's method rewrites (303 → GET, and 301/302 for a POST), the 20-hop ceiling, and the
refusal to replay a consumed stream body all match what fetch itself does. A caller who passed
redirect: 'manual' or 'error' keeps that behaviour untouched.

The wrapper is unit-tested directly (static and provider headers, per-request refresh, transport
headers preserved and protected, cross-origin cases, a Request first argument, and live
two-origin redirect servers under the real fetch), with connection- and client-level tests that
the header actually reaches the wire, and configuration errors for the refused combinations.

Closes #63

🤖 Generated with Claude Code

… origin

MCPClient took headers as a static record and handed them to the transport
once, when the connection opened. A credential that expires could not be
expressed that way, and a connection shared across users would keep sending
whatever was captured when it opened - which is why the Foundry toolbox
already bypassed the option with a fetch wrapper of its own.

headers now also accepts a function, called once per request, so a token can
be refreshed without writing a transport. Both forms are injected through a
fetch wrapper placed around the caller's own fetch, so replacing that cannot
drop them, and headers are attached only to requests whose origin matches the
configured url: a bearer token, a call id or trace context is not handed to a
host the caller never named. The limit of that is documented - the platform
follows redirects internally, so a cross-origin redirect is never seen here
and only the headers fetch itself strips are protected.

The toolbox now uses the shared implementation rather than its own copy.

Closes #63

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shibayan shibayan added enhancement Usage: [PRs], Target: new functionality or improvements; issues use the Feature issue type foundry Usage: [Issues, PRs], Target: Microsoft Foundry integrations mcp Usage: [Issues, PRs], Target: Model Context Protocol integrations public-api-change Usage: [PRs], Target: changes to exported public APIs that require API review security Usage: [Issues, PRs], Target: public security hardening; report vulnerabilities privately labels Aug 11, 2026
@github-actions github-actions Bot added the hosting Usage: [Issues, PRs], Target: agent hosting protocols and runtime label Aug 11, 2026
@shibayan shibayan self-assigned this Aug 11, 2026
The MCP transport passes a string or URL, but the wrapper is exported
as a drop-in fetch, and a Request is a legal first argument to one:
stringifying it produced a bogus URL and threw. Its URL now drives the
origin check, and its headers stay as the base unless an init replaces
them, which is the platform rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the public-api-change Usage: [PRs], Target: changes to exported public APIs that require API review label Aug 11, 2026
…ch wrapper

McpConnection now accepts url, headers and fetch directly and builds
its own Streamable HTTP transport, a fresh one per connection attempt.
MCPClient's url form and FoundryToolbox both delegate to it, so the
fetch wrapper stays internal to the package rather than being public
API. A custom transport owns its own fetch, so combining it with
headers or fetch is refused rather than silently ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the skills Usage: [Issues, PRs], Target: Agent Skills label Aug 11, 2026
shibayan and others added 2 commits August 12, 2026 02:02
Review of the header injection found two holes. The platform fetch
follows redirects internally, so the origin check ran only against the
first URL and a cross-origin redirect carried every injected custom
header to wherever the server pointed — fetch strips authorization and
cookie on its own, but not an API key or a platform identity header.
Injection now follows redirects itself with redirect: 'manual', one
hop at a time: a hop on the configured origin gets fresh headers from
the provider, a hop off it gets none, and what fetch itself strips
cross-origin is stripped the same way. This is the same per-hop
behaviour as the httpx event hook the Python implementation uses.

Injected headers also won over ones the transport had already set, so
a configured header could clobber content-type, accept or the session
id the SDK stamped after merging its request options — headers the
static requestInit route never let callers override. A configured
header now fills gaps only, restoring that precedence.

Both reproduced first: live two-origin servers under the real fetch
for the leak, and a recording stub for the override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MCP has no redirect in its protocol flow, so the hop-by-hop follow
added for the origin check was machinery for a case with no known
occurrence — and in browsers it already degraded to a refusal, since
manual redirects are opaque there. A redirect now fails loudly with
the address the server pointed at, and the remedy is to configure the
endpoint the server redirects to. Nothing is sent to the redirect
target, so the credential-scoping question the follow logic existed to
answer no longer arises. Refusing can later be loosened into following
without breaking anyone; the reverse is not true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shibayan
shibayan marked this pull request as ready for review August 12, 2026 01:50
Copilot AI lite review requested due to automatic review settings August 12, 2026 01:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the MCP client/connection layer to support per-request header injection (including async providers) and scopes injected headers to the configured server origin, then rebases FoundryToolbox onto the shared implementation.

Changes:

  • Add McpHeaderProvider + headerInjectingFetch to inject headers per request (without overriding transport-owned headers) and scope them to a single origin.
  • Extend McpConnection with a URL-driven construction path that builds a fresh StreamableHTTPClientTransport per connection attempt and rejects incompatible config combinations.
  • Update MCPClient and FoundryToolbox to delegate transport/header behavior through McpConnection, with new unit tests covering header behavior and configuration errors.

Reviewed changes

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

Show a summary per file
File Description
packages/mcp/src/skills.ts Updates documentation example to use McpConnection({ url }) now that URL-driven construction exists.
packages/mcp/src/index.ts Exposes McpHeaderProvider type while keeping header injection implementation internal.
packages/mcp/src/headers.ts Introduces origin-scoped header injection wrapper and redirect behavior for injected-header requests.
packages/mcp/src/headers.test.ts Adds direct unit tests for header injection behavior (static/provider, origin scoping, redirect cases).
packages/mcp/src/connection.ts Adds URL form + header/fetch options, builds transport internally, and validates incompatible config.
packages/mcp/src/connection.test.ts Adds tests for URL-driven construction, per-request headers, and rejected option combinations.
packages/mcp/src/client.ts Wires MCPClient URL form through McpConnection and updates header option to accept a provider.
packages/mcp/src/client.test.ts Adds a test ensuring configured headers reach the underlying fetch/transport wire path.
packages/foundry/src/hosting/toolbox.ts Replaces custom transport/fetch wrapper with McpConnection header provider + optional fetch override.
Suppressed comments (1)

packages/mcp/src/headers.test.ts:204

  • Similarly, this test can pass the Request directly instead of casting, once headerInjectingFetch’s signature includes Request.
    const send = headerInjectingFetch(SERVER, { authorization: 'Bearer t' }, fetch);

    await send(new Request('https://elsewhere.example.com/mcp') as unknown as URL);


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

Comment thread packages/mcp/src/headers.ts Outdated
Comment thread packages/mcp/src/headers.ts
Comment thread packages/mcp/src/client.ts Outdated
Comment thread packages/mcp/src/connection.ts
Comment thread packages/mcp/src/headers.test.ts Outdated
The injected fetch already handled a Request first argument at runtime
but declared only string | URL, forcing casts on callers; the wider
parameter is declared now, and it stays assignable wherever the
transport's own fetch type is expected. The headers docs also claimed
a plain record is read once at construction — it is applied per
request, so mutations to the record are picked up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 12, 2026 02:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/mcp/src/headers.ts:80

  • PR description says redirects are followed hop-by-hop with per-hop origin scoping when headers are configured, but headerInjectingFetch currently sets redirect: 'manual' and then throws on any redirect response. This is a behavioral change vs native fetch (redirects will now fail) and doesn’t match the described redirect-handling design; either implement the hop-by-hop follow logic or update the PR description/acceptance criteria to reflect the deliberate refusal.
    const response = await inner(target, { ...init, headers: outgoing, redirect: 'manual' });
    if (response.type === 'opaqueredirect') {
      // A runtime that hides redirect responses cannot even say where the server pointed.
      throw new Error(
        `The MCP endpoint at ${initialUrl.href} redirected. Requests carrying injected headers ` +
          'are not followed across redirects; configure `url` as the endpoint the server redirects to.',
      );

packages/foundry/src/hosting/toolbox.ts:111

  • The injected authorization header value appears to be corrupted (authorization: ****** getToken()}`), which will send an invalid bearer token to Foundry and break authentication.
      // Per-request rather than per-connection: see the class note on why this cannot be a
      // static header set.
      headers: async () => ({
        authorization: `Bearer ${await getToken()}`,
        ...platformHeaders(),
      }),

packages/mcp/src/index.ts:14

  • This comment says headers.ts is internal, but McpHeaderProvider is exported from the package entrypoint here. Either stop exporting the type, or adjust the comment to clarify that only the helper functions are internal while the type is part of the public config surface.
// headers.ts (origin-scoped header injection) is internal: consumers configure `headers` on
// MCPClient or McpConnection rather than building the fetch wrapper themselves.
export type { McpHeaderProvider } from './headers.js';

@shibayan
shibayan merged commit 9674880 into master Aug 12, 2026
8 checks passed
@shibayan
shibayan deleted the add-mcp-header-provider branch August 12, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Usage: [PRs], Target: new functionality or improvements; issues use the Feature issue type foundry Usage: [Issues, PRs], Target: Microsoft Foundry integrations hosting Usage: [Issues, PRs], Target: agent hosting protocols and runtime mcp Usage: [Issues, PRs], Target: Model Context Protocol integrations security Usage: [Issues, PRs], Target: public security hardening; report vulnerabilities privately skills Usage: [Issues, PRs], Target: Agent Skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Accept a header provider on MCPClient and scope injected headers to the server origin

2 participants