Skip to content
Closed
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
21 changes: 21 additions & 0 deletions docs/decisions/0050-python-mcp-v2-client-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ lifecycle. Constructor combinations that mix those ownership modes or configure
owns raise `ValueError` instead of silently ignoring parameters. Temporarily retained legacy constructor and
`ClientSession` combinations remain compatible and will be removed in vNext.

### Managed connection readiness

For framework-created connections, the lifecycle owner completes transport setup,
configured catalog discovery, recovery, and teardown. Operations wait for that
owner before borrowing a connection; transport availability alone is not readiness.
Public managed catalog refreshes also run on the owner, so discovery recovery and
failure cleanup never wait on a lifecycle request held by another task while
holding the discovery lock. A catalog interrupted by connection replacement is
restarted as a complete snapshot rather than resumed using the previous session's cursor.

Same-header HTTP operations share identity leases and execute concurrently outside
admission synchronization. Identity changes wait for leases to drain. Lease release
is synchronous, so cancellation cannot prevent releasing the last lease.

Separating transport recovery and discovery across invocation tasks was rejected:
it exposes a replacement before its schemas and metadata are ready and creates
cross-task lock-ordering dependencies. Keeping the complete transition on the
existing owner preserves one authority without depending on transport-specific
ContextVar propagation. Caller-supplied connections do not receive these additional
lifecycle or synchronization guarantees.

Catalog page and item limits apply after the MCP SDK parses each response. They do not establish a universal
transport response-size bound; applications must connect to MCP servers trusted to return reasonably sized responses
and configure transport-specific limits where the selected transport exposes them.
Expand Down
3 changes: 2 additions & 1 deletion python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ The vector store API is experimental under the shared `VECTOR_STORES` feature ID
independently listing tools; `SecureMCPToolProxy.refresh_labels()` explicitly performs a bounded tool refresh
before reapplying local policy.
- **`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 HTTP header request scoping** - 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. For managed connections, same-identity tools, generated prompts, and Foundry Toolbox resources share a header snapshot while running concurrently. An admission lock and an idle event coordinate identity changes; different identities wait outside the lock, and active-operation release never awaits a lock. Agent runs reconcile a connected tool before copying its functions; generated calls perform the same check at invocation. Connected run preparation defers missing runtime values; invocation-time resolution remains strict. The lifecycle owner completes identity replacement and discovery before admitting further operations. Dynamic headers on a caller-supplied session with unknown identity remain rejected. Ambient requests retain the bound set. Shared HTTP-client hooks stay tool-scoped and strip injected headers from cross-origin redirects. Session cleanup removes request hooks after transport shutdown and closes framework-created HTTP clients. Failed discovery rolls back partial state; constructor-supplied connections remain caller-owned and do not gain managed synchronization guarantees.
- **MCP concurrent recovery** - Each failed operation records its connection and close generation. Central recovery submits the complete recovery action to the lifecycle owner, which reuses an already-ready replacement and performs discovery/cleanup on that same task. Public managed catalog discovery is owner-serialized and restarts a complete snapshot if the connection changes. Calls wait for lifecycle readiness and reprepare tool arguments/metadata after recovery. Explicit `close()` interrupts older recovery without preventing subsequent explicit connection or discovery. Header admission and recovery each use `request_timeout` as a separate budget. The prior split recovery path and per-admission synchronize request are not used.
- **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.
- **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`.
Expand Down
33 changes: 33 additions & 0 deletions python/packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,39 @@ asyncio.run(main())

You can explore additional agent samples [here](https://github.com/microsoft/agent-framework/tree/main/python/samples/02-agents).

### MCP HTTP concurrency and timeouts

For connections created by `MCPTool`, `MCPStreamableHTTPTool` permits concurrent tool
and prompt calls when their effective HTTP headers match.
Fixed and dynamic headers together identify the
MCP session; header names are compared case-insensitively and values case-sensitively.
A caller with different headers waits for active operations to finish before the
framework reconnects and refreshes session discovery. Foundry Toolbox resource reads
use the same coordination.

Direct `get_prompt()` calls resolve current ambient headers; generated prompts
use their invocation's runtime headers. Prompt arguments never supply authentication.
Foundry Toolbox closes its owned HTTP client on the lifecycle owner after MCP
teardown, even if the caller waiting for `close()` is cancelled.

The lifecycle owner completes connection setup and configured discovery before
admitting operations. Concurrent failures reuse a replacement only after that
replacement's catalogs are ready. Tool retries reprepare arguments and metadata
from the refreshed catalog. Catalog discovery restarts from the beginning if its
managed connection changes; it does not combine pages from different sessions.

Header admission, session reconciliation, and connection recovery use `request_timeout`
as separate budgets (30 seconds by default; `None` disables them). These budgets do
not form an end-to-end deadline. Admission timeout does not cancel another caller's
active tool call. Releasing a call's header lease does not wait on a lock, including
when the call is cancelled.
Calling `close()` interrupts in-flight recovery; it does not permit that recovery
to reopen the session. Subsequent explicit connection or discovery remains supported.

Caller-supplied `mcp_client` and `session` objects retain their existing ownership
and passthrough behavior. Their lifecycle and synchronization remain the caller's
responsibility; these managed-connection readiness guarantees do not apply to them.

### MCP HTTP client ownership and cookies

**Breaking change:** HTTP clients created by `MCPStreamableHTTPTool` no longer persist
Expand Down
Loading
Loading