Repository navigation
Description
Describe the bug
FoundryToolbox connects its MCP session lazily on the first agent run and reuses that
connection for the lifetime of the toolbox. Because of how the MCP streamable-HTTP client is
structured, every subsequent POST is issued from the connect-time task context — so
get_request_context() inside httpx.Auth.auth_flow resolves to the first request's context,
and later requests forward a stale x-agent-foundry-call-id.
To Reproduce
- Build a
FoundryToolboxand attach it to a hosted agent. - Bind a request context with call id
CALL-1and run the agent (this connects the session). - Bind a request context with call id
CALL-2and run the agent again. - Inspect the outbound headers on the second run's toolbox POSTs.
Observed: the second run still sends x-agent-foundry-call-id: CALL-1.
Expected: it sends CALL-2.
Root cause
mcp/client/streamable_http.py creates a task group at connect time and starts post_writer via
tg.start_soon (~lines 647/659), so all POSTs run in the task context captured at connect. The
call id is carried by a plain ContextVar in
azure/ai/agentserver/core/_request_context.py that is re-set per request, and a ContextVar
set in one task is not visible to a task started earlier from a different context. So the value
cannot cross that boundary no matter where it is read.
This also means the natural fix (reading the context inside auth_flow) does not work — I verified
that empirically; the auth flow observes only the connect-time value.
Impact
Scoped, but real. The service currently accepts the stale id — I confirmed that reading both
skill://index.json and a skill's SKILL.md succeeds even with a deliberately bogus call id —
so this does not break toolbox calls today. It does mean that any server-side use of the call id
(request correlation, tracing, per-call policy) attributes every toolbox call after the first to
the first request of the process.
The same pattern is present in the official sample
python/samples/04-hosting/foundry-hosted-agents/responses/foundry_toolbox_mcp_skills, so it is
not specific to a caller's wiring.
Expected behavior
Toolbox requests should carry the call id of the request that triggered them. That likely requires
either establishing the MCP session per request, or propagating the per-request context across the
connect-time task boundary explicitly rather than relying on ContextVar inheritance.
Platform
- Language: Python
- Package:
agent-framework-foundry-hosting
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
guiyangyuan commentedon Aug 18, 2026
Hi westey (@westey-m), I'd like to take this on if the direction below looks right.
I checked the current Python implementation and agree with the task-context diagnosis:
_ToolboxAuthreadsget_request_context().platform_headers()inside the httpx auth flow, while the streamable-HTTP POST writer belongs to the long-lived MCP connection created on the first run. That means resolving theContextVarlater inside auth still observes the connection-time context.My proposed approach is:
FoundryToolbox/ MCP session alive across two request contexts (CALL-1, thenCALL-2) and records the actual outbound request headers. The second toolbox POST must carryCALL-2.foundry_hostingtests.Would you prefer explicit per-operation header propagation, or reconnecting the MCP session per hosted request? I favor explicit propagation because it preserves connection reuse, but I'm happy to follow the maintainers' preferred boundary.
If this direction is acceptable, please assign the issue to Guiyang Yuan (@guiyangyuan) and I'll prepare the test-first fix.
james-tn commentedon Oct 2, 2026
Additional reproduction evidence with these pinned Python packages:
agent-framework-foundry-hosting==1.0.0b260918agent-framework-core==1.19.0azure-ai-agentserver-core==2.1.0mcp==1.30.0The local reproduction uses a loopback MCP server that records and echoes
x-agent-foundry-call-id, a fake credential, and separate asyncio tasks with separately boundFoundryAgentRequestContextvalues. No real user tokens, tenant services, or customer data are involved.CALL-A, then invoke concurrently underCALL-BandCALL-CCALL-ACALL-BREQUEST-1, then invoke underREQUEST-2REQUEST-1This confirms the task-context diagnosis:
_ToolboxAuth._apply_headers()reads the context on every HTTP request, but the MCP writer/request tasks inherit the connection-time context. Moving or repeating that read within the HTTP auth hook does not propagate the invoking task's context across the boundary. These results establish the client-side header defect only; they do not establish how Foundry authorizes requests carrying stale or missing IDs.Relationship to the recently merged mitigation: #8899 changes the Responses integrations/samples to request-owned agent, Toolbox and skills-provider factories, and explicitly leaves this issue open for long-lived connection/header propagation. The current request-owned guidance aligns with the per-turn lifecycle we are using in a native Activity host. This is not a claim that the merged Responses samples validate native Activity hosting.
As checked on October 1, PyPI still publishes
agent-framework-foundry-hosting==1.0.0b260918; the merged changes are not in a newer published hosting package yet. We are retaining the per-turn connection lifecycle, accepting additional handshake/tool/skill-discovery round trips. That transport lifecycle does not recreate the configured OAuth connections or inherently require user consent every turn.For the remaining work, regression coverage should include concurrent callers, no-context initialization, repeated requests by the same user, and resource/skill reads as well as tool calls. It would also help to clarify whether request-owned connections are the intended ongoing contract or whether a future change will explicitly support reuse across request contexts.