Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/decisions/0050-python-mcp-v2-client-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,33 @@ Caller-supplied `ClientSession` connections always call the server directly. Age
shared cache store or cache-mode configuration. Reconnect or an effective header-identity change replaces the
framework-owned Client and its per-client cache.

### Concurrent HTTP operations and identity transitions

Concurrent operations on a framework-owned HTTP connection may share a session only when their complete effective
header identity matches. Agent Framework copies owner-tagged header snapshots onto individual outgoing requests,
including redirects, rather than retaining a mutable active-call fallback. The supported MCP SDK propagates the
sending operation's ContextVars into its HTTP tasks. Transport request entry points must retain the owner tag;
unsupported untagged sending methods fail explicitly instead of silently dropping dynamic headers.

Admission tracks network attempts, not complete logical calls. Retries release the failed attempt before recovery,
then reconcile the original identity and rebuild arguments and metadata from the replacement catalog. Identity
changes, explicit discovery, reset, close, and recovery require exclusive access and wait for active attempts.
A queued exclusive operation blocks unrelated arrivals, including new calls with the old identity. Same-identity
callbacks needed by an active attempt may finish first; invocation depth keeps that permission live through nested
operation scopes. Exclusive operations from active callbacks fail explicitly rather than waiting on their parent.
Resource reads enter admission before requiring a ready connection, including during replacement initialization.

This request-owned approach is preferred over a lease spanning an entire logical call: it avoids shared mutable
request headers, permits recovery after releasing the failed attempt, and distinguishes required nested callbacks
from unrelated arrivals. Its cost is explicit admission and callback-lifetime tracking. It does not create independent
server-side session state, so applications must still order state-dependent operations.

Catalog cursors and staged pages belong to one connection. Replacement restarts the complete snapshot once from
its first page; a second replacement fails explicitly. Configured discovery also reloads tools if prompt discovery
replaces their connection. Caller-owned Clients and ClientSessions remain caller-owned and are never recreated.
Admission/reconciliation and recovery have separate `request_timeout` budgets, not one end-to-end deadline;
`None` disables those budgets. Cleanup remains owned by the lifecycle task after waiter cancellation.

### MRTR

For both framework-owned and caller-owned high-level connections, `tools/call`, `prompts/get`, and `resources/read`
Expand Down
23 changes: 23 additions & 0 deletions python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,29 @@ The vector store API is experimental under the shared `VECTOR_STORES` feature ID
- **`additional_tool_argument_names`** (constructor arg on all `MCPTool` subclasses) - Opt extra argument names back into the allowlist. Accepts a `Sequence[str]` (applied to every tool) or a `Mapping[str, Sequence[str]]` keyed by **remote tool name**, where the reserved key `"*"` denotes global extras. It is configured only in user code at construction; there is **no per-call/runtime override**, so a model-issued tool call cannot change which names pass through — but note this constrains the *model*, not the *server*, which still widens the effective allowlist through its schema. To use a server that accepts `additionalProperties: true`, list the extra names here and then either (1) manually extend that tool's `inputSchema` (via the `.functions` list after connecting) so the model is prompted to supply them, or (2) supply the values yourself via `function_invocation_kwargs`. If a normal forwarded argument name is supplied by both the model and `function_invocation_kwargs`, the model-supplied value wins; `_meta` is the exception and only trusted runtime/caller metadata is used.
- **MCP HTTP header request scoping** - `static_headers` supplies fixed, origin-scoped headers without serializing concurrent calls. Generated tool and prompt calls give `header_provider` only host runtime kwargs, separately from model arguments; direct `call_tool` calls give it caller kwargs. Model-over-runtime precedence is confined to outbound tool arguments. Fixed and dynamic headers form the session's complete effective identity (case-insensitive names, case-sensitive values), with dynamic values overriding fixed values of the same name. Agent runs reconcile a connected tool before copying its functions; generated tool and prompt calls perform the same check at invocation. Connected run preparation defers when a provider needs runtime values supplied by invocation middleware; invocation-time resolution remains strict. A different identity waits for in-flight discovery, reconnects framework-created sessions without loading, then refreshes session-derived discovery under the same lock. Because a caller-supplied session's established identity is unknown and the wrapper cannot reconnect it, dynamic header resolution on that wrapper is rejected even while disconnected. Ambient requests retain the bound set. With a shared `http_client`, processing stays scoped to the originating `MCPStreamableHTTPTool`, and every injected header is stripped from cross-origin redirects. The session exit stack removes its request hook after transport shutdown, including failed initialization or discovery, and closes framework-created HTTP clients. Failed discovery resets the connection and discovery flags and rolls back partial function/metadata additions. Framework-created sessions are discarded; constructor-supplied sessions remain caller-owned and reusable across cleanup, close, and reset.
- **MCP lifecycle caller cancellation** - The lifecycle owner skips cancelled queued connect requests and waits for the caller to acknowledge successful setup. If the caller cancels before accepting a newly established connection, the owner tears it down before processing the next request. Cancelling a close waiter does not interrupt teardown, and cancelling a redundant connect does not discard a previously established session.
- **MCP HTTP concurrency** - MCP >=2.2 propagates the sending operation's ContextVars into each outbound HTTP task.
The owner-scoped header hook copies effective headers onto each request, including redirects; do not restore a
mutable instance-level active-header fallback. Same-identity tool, prompt, and Toolbox resource operations may
overlap on one session. Same-identity run preparation uses shared admission rather than draining active calls.
Direct prompts resolve current headers; generated prompts retain explicitly resolved host runtime headers.
Identity changes, discovery, reset, close, and recovery wait for active network attempts on framework-owned
connections even without a header provider; retries release their attempt before recovery, re-admit the original
identity, and use its refreshed argument
allowlist and tool metadata. Failed attempts from the same replaced framework-owned connection share recovery.
Keep the original trace context when rebuilding metadata. Cancelled lifecycle waiters leave their owner running;
new HTTP operations must wait for that setup or teardown to finish. A same-generation nested callback may finish
ahead of a queued identity change, but exclusive operations from active callbacks fail instead of deadlocking.
Track invocation activity by nesting depth. Resource reads enter admission before requiring the connection.
Transport sending methods must tag request ownership or fail explicitly; do not delegate them unscoped.
- **MCP recovery and deadlines** - Failed tool, prompt, discovery, and liveness operations record their connection
and close generation. Central recovery reuses a replacement established by another operation, releases the
lifecycle request lock before discovery, and rejects recovery from before an explicit close. Advance the close
generation before waiting for HTTP exclusivity, including for Toolbox cleanup. Header admission/reconciliation
and connection recovery have separate `request_timeout` budgets; neither wraps the admitted RPC or cleanup.
`None` disables the budgets. Keep recovery's deadline outside the HTTP barrier so waiting for active attempts
is bounded too.
Replacement during discovery discards staged pages and continuation cursors and restarts the complete catalog
once. Another replacement fails explicitly. Configured prompt recovery also refreshes tools from the new session.
- **Streamable HTTP cookies** - Framework-created clients reject response-cookie persistence with or without a `header_provider`; explicit `Cookie` headers from `static_headers` or a provider remain supported. Caller-provided clients retain their cookie behavior and ownership. Applications requiring cookie persistence must scope clients and MCP sessions to one authenticated principal. Cookie rejection does not isolate MCP protocol sessions or other server-side state.
- **`function_invocation_kwargs` and MCP servers** - That dict is shared across every tool in the run, including every attached `MCPTool`, and any name in it reaches a server that declares a matching `inputSchema` property. `header_provider` does not mitigate this — it reads the kwargs without consuming them. To keep a credential out of tool arguments, source it outside `function_invocation_kwargs`: read a `ContextVar` inside the provider (this still allows a different value per request), configure a custom `http_client`, or use `env` for `MCPStdioTool`.
- **Sampling guardrails** (`sampling_callback`) - Passing `client=` advertises `SamplingCapability` so the server can send `sampling/createMessage`. Because remote servers are untrusted (confused-deputy risk), the default `sampling_callback` is **deny-by-default** and applies, in order: a per-session rate limit (`sampling_max_requests`, default `_DEFAULT_SAMPLING_MAX_REQUESTS`), an approval gate (`sampling_approval_callback`), and a `maxTokens` cap (`sampling_max_tokens`, default `_DEFAULT_SAMPLING_MAX_TOKENS`). The approval callback (constructor arg on all subclasses; exported type alias `SamplingApprovalCallback`) receives the raw `CreateMessageRequestParams`, may be sync or async, and must return truthy to approve. When it is `None` (the default) every sampling request is denied; pass `lambda params: True` to restore legacy auto-approve as an explicit opt-in. Requests and denials are logged at WARNING (content is not logged). The per-session counter resets in `_reset_session_state`.
Expand Down
28 changes: 28 additions & 0 deletions python/packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ cookie-bearing clients and MCP tool sessions to one authenticated principal; do
share a user-specific cookie jar across users. Rejecting cookies does not partition
MCP protocol sessions or other server-side state between principals.

### MCP HTTP concurrency and timeouts

`MCPStreamableHTTPTool` and `FoundryToolbox` attach a separate header snapshot to each
outgoing HTTP request. Calls with identical effective static and dynamic headers can
overlap on one MCP session. A different identity waits for active operations before
reconnecting and refreshing discovery. Discovery, reset, close, and recovery are
exclusive against active network attempts on framework-owned MCP connections, with
or without a header provider. Same-identity Agent run preparation does not drain
active calls. Direct prompt calls resolve current headers rather than reusing an
inherited invocation snapshot; generated prompts resolve host runtime kwargs
separately from model arguments. Calls still share server-side session state;
applications must order state-dependent operations themselves.

Resource reads wait for replacement initialization before requiring the connection.
Catalog discovery restarts the complete snapshot once after a connection replacement,
without reusing old pages or cursors. A second replacement fails explicitly.

`request_timeout` bounds header admission and session reconciliation together, and
connection recovery separately from the RPC timeout. These are separate budgets,
not an end-to-end deadline. `None` disables these budgets. An admission timeout does
not cancel another caller's active operation. Cleanup may continue after a timeout
or cancellation so the lifecycle owner can finish releasing resources safely.
Caller-owned MCP clients retain their own RPC timeout policy and are not recreated.

Concurrent failures of the same connection share its replacement. Calling `close()`
interrupts older recovery so it cannot reopen the connection or retry the failed
operation after shutdown. A later explicit `connect()` remains supported.

## 5. Multi-Agent Orchestration

Coordinate multiple agents to collaborate on complex tasks using orchestration patterns:
Expand Down
Loading
Loading