Skip to content

Support per-run headers for MCP server calls #1969

Closed as not planned
Closed as not planned

Description

@p-arndt

Summary

Allow specifying HTTP headers for the MCP server for each run invocation, not only at initialization time. In multi-user or multi-tenant environments it’s often necessary to attach user-specific or session-specific headers (e.g., auth token, user-id, tenant-id) for each agent run.

Problem

Currently, the SDK supports configuring the MCP server client (URL, transport, static headers) at initialization. But if you need different headers per run (for example, different user tokens or metadata when the same agent is shared among users), there’s no direct hook to override or supply headers with each Runner.run() or Runner.run_streamed() invocation.

Proposed behaviour

  • Extend the Runner run() and run_streamed() methods to accept a new parameter (for example mcp_headers: dict[str, str] | None) that allows specifying HTTP headers for the MCP server just for that run.

  • Example usage:

    result = await Runner.run(
        starting_agent=agent,
        input="Do something",
        run_config=run_config,
        mcp_headers={"Authorization": f"Bearer {user_token}", "X-User-Id": user_id}
    )
  • The SDK should merge default headers (set at MCP server/client init) and the per-run headers, with per-run headers taking precedence.

  • For streaming:

    result = await Runner.run_streamed(
        starting_agent=agent,
        input="Another task",
        mcp_headers={"X-Tenant-Id": tenant_id}
    )

Benefits

  • Supports multi-user / multi-tenant scenarios where many users share an agent or MCP connection but need per-request credentials or context headers.
  • Avoids needing to recreate the MCP client for each user or each run just to change headers.
  • Simplifies agent orchestration when per-run metadata (e.g., user-id, request id) must be sent to the MCP server for authorization, analytics, or routing.

Backward compatibility & notes

  • If mcp_headers is not provided, behaviour remains unchanged (use init‐level headers).
  • The new parameter is optional, so existing code continues working without change.
  • Performance impact is minimal — just header injection per request.

Suggested API addition

class Runner:
    @classmethod
    async def run(
        # existing parameters...
        mcp_headers: dict[str, str] | None = None,
    ) -> RunResult:
        ...

And similarly for run_streamed(...).

Activity

  1. seratch commented on Oct 21, 2025

    @seratch
    Member

    Thanks for the idea, but this design does not work when a developer would like to pass the custom header only to MCP server A while having multiple MCP servers (A, B, C ...) for the agent. Attaching custom headers to an MCP server instance when initializing an Agent may work but I haven't verified if this approach works well:

        # this is pseudo code; it does not yet work
        agent = Agent(
            name="Assistant",
            mcp_servers=[mcp_server.with_headers({
                "X-User-ID": "123",
            })],
        )
  2. seratch commented on Oct 21, 2025

    @seratch
    Member

    I've thought more about this. Remote MCP servers often need authorization headers and similar credentials when establishing a connection. Therefore, passing these headers during the initialization of MCP server clients is the correct way to include them. Changing the headers later would require disconnecting the current session and reconnecting with a new set of headers. So, I believe the most practical solution is to create new MCP servers each time your app receives a user request.

  3. p-arndt commented on Oct 21, 2025

    @p-arndt
    Author

    @seratch I see your point. However, initializing a new MCP server for each user request introduces noticeable latency due to connection setup, which increases the time to first token. Maintaining a persistent connection and updating request headers dynamically could reduce this overhead.

    What are the common practices here — do people typically cache server instances to avoid this, or just accept the added latency? I’m open to better approaches or suggestions. Thanks!

  4. deleted a comment from ProCityHub on Oct 21, 2025
  5. deleted a comment from ProCityHub on Oct 21, 2025
  6. seratch commented on Oct 22, 2025

    @seratch
    Member

    I've checked the underlying official MCP SDK's code too. Your app has to pass required custom headers when establishing a connection to a remote MCP server, so it's not feasible to override some of the headers for each tool call. If you have different authorization header for each user request, connecting to a remote MCP server per request is the only way to utilize the tools. If you need to pass only user ID etc., you may want to consider enhancing the backend MCP server to accept those data as part of request parameters.

  7. added
    questionQuestion about using the SDK
    and removed
    enhancementNew feature or request
    on Oct 22, 2025
  8. damianoneill commented on Oct 23, 2025

    @damianoneill
    Contributor

    Hi @seratch I hope your keeping well.

    I'd like to revisit this for a specific use case that differs from the discussion above.

    Our Architecture

    We're using MCPServerStreamableHttp with a singleton connection pattern:

    • One MCP server connection established at application startup
    • Shared across all user requests (multi-tenant SaaS)
    • Static organization-level credentials for connection establishment
    • Need per-request user-specific X-Auth-Token header for authorization

    @seratch stated:

    "Your app has to pass required custom headers when establishing a connection to a remote MCP server, so it's not feasible to override some of the headers for each tool call."

    However, with Streamable HTTP transport, each call_tool() invocation results in a new HTTP POST request over the established SSE stream. Technically, these per-request headers could be injected at the HTTP request level, not the connection level.

    # Current: Static headers set at initialization
    mcp_server = MCPServerStreamableHttp(
        params={
            "url": "https://mcp.example.com/mcp",
            "headers": {
                "Authorization": "Bearer org-static-token",  # Connection-level
                "X-Org-ID": "org-123"  # Connection-level
            }
        }
    )
    
    # Desired: Per-request headers injected at tool call time
    # Request 1: User A
    await session.call_tool("list_devices", {...})
    # HTTP headers: Authorization + X-Org-ID + X-Auth-Token: user-a-token
    
    # Request 2: User B  
    await session.call_tool("list_devices", {...})
    # HTTP headers: Authorization + X-Org-ID + X-Auth-Token: user-b-token

    Each call_tool() makes a fresh HTTP POST to the /mcp endpoint, so headers could theoretically be modified per-call without reconnecting the SSE stream.

    Why Per-Request Connection is Problematic. Creating new MCP connections per-request causes:

    • ~400-700ms latency penalty per request (connection establishment)
    • TCP connection exhaustion under load (thousands of users)
    • Resource overhead (one HTTP client per request vs singleton)

    Our Current Workaround

    We're exploring using contextvars + [httpx.Auth] to inject headers dynamically:

    import contextvars
    import httpx
    
    _auth_token_var: contextvars.ContextVar[str] = contextvars.ContextVar('auth_token')
    
    class ContextAwareAuth(httpx.Auth):
        def auth_flow(self, request):
            token = _auth_token_var.get(None)
            if token:
                request.headers["X-Auth-Token"] = token
            yield request
    
    # In HTTP client factory
    def create_http_client(...):
        return httpx.AsyncClient(
            auth=ContextAwareAuth(),
            ...
        )
    

    Our application serves thousands of concurrent users, and the per-request connection
    approach would require:

    • Maintaining 1000+ concurrent MCP connections (vs 1 singleton)
    • Adding ~500ms to every user interaction
    • Significant infrastructure scaling costs

    This makes per-request connections impractical for production multi-tenant deployment.

    Questions

    1. Is per-request header injection technically feasible for Streamable HTTP transport (since each tool call = new HTTP request). Or is there an MCP protocol constraint I'm missing?
    2. Would the SDK consider supporting this via:
    • Option A: extra_headers parameter in [call_tool()]
    • Option B: Callback function to provide headers per-request
    • Option C: Context-based header injection (reading from [RunContextWrapper])
    1. Are there other multi-tenant users with similar needs?

    Related Use Cases

    • Multi-tenant SaaS where users share agent infrastructure
    • Per-user authorization/auditing requirements
    • Rate limiting per user (not per org)
    • User-specific data isolation enforced by MCP server

    Would appreciate any guidance on whether this is feasible or if we should proceed with the workaround approach.

    As always, thanks for your time.
    Damian.

  9. damianoneill commented on Oct 23, 2025

    @damianoneill
    Contributor

    Should add @seratch happy to contribute code if this is considered for planned new feature.

  10. seratch commented on Oct 23, 2025

    @seratch
    Member

    @damianoneill Thanks for sharing details. Two things I wanted to mention:

    1. IIUC, it seems the underlying official MCP SDK does not allow passing headers for tool calls. If my observation here is not correct, technically it may be feasible to customize the headers per tool call
    2. MCP servers may dynamically change the list of tools in response to list-tools call based on passed headers (especially authorization header); in this scenario, changing headers only for a tool call may result in a runtime permission error (e.g., this user is not allowed to call this tool; tool not found etc.)

    If these issues could be resolved in a nice way, there is no reason to avoid this enhancement on our side.

  11. damianoneill commented on Oct 23, 2025

    @damianoneill
    Contributor

    Hi @seratch, thank you for the thoughtful response.

    You mentioned:

    "IIUC, it seems the underlying official MCP SDK does not allow passing headers for tool calls."

    This is technically correct - the current SDK doesn't have a call_tool(..., extra_headers={}) parameter. However, I've filed a feature request in the python-sdk repository to add this capability:

    Issue: modelcontextprotocol/python-sdk#1509

    The proposal adds an extra_headers parameter to ClientSession.call_tool():

    async def call_tool(
        self,
        name: str,
        arguments: dict[str, Any] | None = None,
        read_timeout_seconds: timedelta | None = None,
        progress_callback: ProgressFnT | None = None,
        *,
        meta: dict[str, Any] | None = None,
        extra_headers: dict[str, str] | None = None,  # NEW
    ) -> types.CallToolResult:

    This follows the precedent of read_timeout_seconds which was added for per-request timeout configuration (Issue #600). The implementation would merge connection-level headers with per-request headers at the HTTP transport layer without affecting the MCP protocol itself.

    If this feature is accepted upstream, it would enable openai-agents-python to expose per-request headers naturally.

    If Issue modelcontextprotocol/python-sdk#1509 is accepted, the feature could be extended to list_tools for per-request headers which should address your concerns regarding dynamic tool lists.

    Once I know more on 1509, I'll drop a note on this ticket.

    Thanks again,
    Damian.

  12. beall49 commented on Oct 23, 2025

    @beall49

    Just chiming in for support, this is a feature (per-run headers for auth) that would be super helpful for my group.

  13. github-actions commented on Oct 31, 2025

    @github-actions
    Contributor

    This issue is stale because it has been open for 7 days with no activity.

  14. github-actions commented on Nov 3, 2025

    @github-actions
    Contributor

    This issue was closed because it has been inactive for 3 days since being marked as stale.

  15. hayescode commented on Nov 26, 2025

    @hayescode

    Ideally we could define tools as callable functions returning a configured tool that take RunContextWrapper so that tools could be configured at runtime with context, instead of a static tool.

    PydanticAI implements it this way and it's pretty handy!

    https://ai.pydantic.dev/toolsets/#preparing-tool-definitions

  16. DeoLeung commented on Dec 1, 2025

    @DeoLeung
    Contributor

    +1, and better expose a way to inject meta as well into call_tool

  17. damianoneill commented on Dec 1, 2025

    @damianoneill
    Contributor

    @everyone this MR is blocked on the underlying sdk - modelcontextprotocol/python-sdk#1509 I've had a MR open for over a month at modelcontextprotocol/python-sdk#1515 without review. If anyone knows anybody on the python-sdk that could help with the review that would be really useful.

    Thanks,
    Damian.

  18. wangxj03 commented on Jan 8, 2026

    @wangxj03

    +1 very nice feature. hope modelcontextprotocol/python-sdk#1509 will be accepted soon 🙏

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions