Skip to content

fix(client): add extra_headers escape hatch for multi-tenant servers#585

Merged
bokelley merged 1 commit intomainfrom
bokelley/mcp-tenant-header
May 5, 2026
Merged

fix(client): add extra_headers escape hatch for multi-tenant servers#585
bokelley merged 1 commit intomainfrom
bokelley/mcp-tenant-header

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 5, 2026

Summary

AgentConfig only exposed a single auth header, so SDK callers couldn't attach the additional routing headers some servers require (e.g. x-adcp-tenant on multi-tenant MCP servers like salesagent). Adds a transport-layer extra_headers dict on AgentConfig, plumbed through both MCP and A2A adapters, with a matching CLI -H/--header flag and ~/.adcp/config.json field.

Changes

  • AgentConfig.extra_headers: dict[str, str] (validated against CR/LF/NUL injection; rejects collisions with the configured auth_header and standard Authorization)
  • MCP adapter: merges extra_headers into the headers dict for both streamable_http and sse transports
  • A2A adapter: merges into the shared httpx.AsyncClient.headers (covers both agent-card discovery and RPC sends)
  • CLI: -H KEY=VALUE (repeatable). With --save-auth it persists into the saved config; at runtime it merges over saved-config headers (CLI wins on conflict). --list-agents shows persisted header keys (not values).
  • Docstring frames the field as a transport escape hatch, not an AdCP protocol extension point — protocol-level fields belong in RequestContext.metadata or the request envelope.

Reviewer feedback applied

Spawned code-reviewer, ad-tech-protocol-expert, and dx-expert before opening. Convergent fixes incorporated:

  • CR/LF/NUL rejection in keys and values (defense-in-depth against header injection)
  • Error message includes the actual auth_header value on collision
  • Plaintext-storage warning in the docstring
  • A2A pass-through test added
  • CLI runtime-vs-saved merge precedence test added (merge_headers helper extracted for testability)

Reviewer divergence (deliberately not adopted):

  • Reserving the entire x-adcp-* prefix would block the actual user flow (x-adcp-tenant is what salesagent reads); the right fix is for salesagent to rename to a vendor-namespaced prefix, not for the SDK to refuse the request. Documented the namespace concern in the docstring.
  • Accepting : as a separator (curl-style) deferred — keep one canonical form for now.
  • Surfacing upstream response body on connect failure — out of scope, separate PR.

Test plan

  • ruff check clean
  • mypy src/adcp/ clean
  • Full suite: 3722 passed, 17 skipped, 1 xfailed (matches baseline)
  • New tests: 14 covering AgentConfig validation (collision, empty key, CRLF in key, CRLF in value), CLI parse_header_args parsing, merge_headers precedence, MCP streamable_http + sse pass-through, A2A httpx-client pass-through, and saved-config persistence

Usage

# one-shot
adcp https://server.example.com/mcp get_products --auth $TOKEN -H x-adcp-tenant=acme

# persisted
adcp --save-auth tenant1 https://server.example.com/mcp mcp -H x-adcp-tenant=tenant1
adcp tenant1 get_products --auth $TOKEN
from adcp import ADCPClient
from adcp.types import AgentConfig, Protocol

config = AgentConfig(
    id="tenant1",
    agent_uri="https://server.example.com/mcp",
    protocol=Protocol.MCP,
    auth_token=token,
    extra_headers={"x-adcp-tenant": "tenant1"},
)

🤖 Generated with Claude Code

AgentConfig only exposed a single auth header, so SDK callers couldn't
attach the additional routing headers some servers require (e.g.
x-adcp-tenant on multi-tenant MCP). Adds a transport-layer extra_headers
dict on AgentConfig, plumbed through both MCP and A2A adapters, with a
matching CLI -H/--header flag and ~/.adcp/config.json field.

Validates against CR/LF/NUL injection and rejects collisions with the
configured auth_header / standard Authorization. Documents the field as
a transport escape hatch — protocol-level fields belong in
RequestContext.metadata, not arbitrary headers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 66b7456 into main May 5, 2026
15 checks passed
@bokelley bokelley deleted the bokelley/mcp-tenant-header branch May 5, 2026 12:56
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