You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The default declarative MCP handlers cached connected clients by endpoint, label, connection name, and headers. When one handler instance served multiple workflow sessions, those sessions could therefore reuse the same stateful MCP protocol session. This change fixes the MCP client session isolation/cache scoping bug while preserving session continuity within one workflow session and the existing endpoint, header, label, and connection isolation.
Description & Review Guide
What are the major changes? Added framework-owned workflow session identity to the Python invocation contract and a workflow-scoped .NET handler contract, propagated trusted session IDs from workflow execution contexts, and included that identity in default MCP client cache keys. Added regression coverage for separate workflows, same-workflow continuation, cache-key discrimination, and concurrent cache creation.
What is the impact of these changes? Separate workflow sessions no longer share a stateful MCP protocol session through a shared default handler. Calls within the same workflow session continue to reuse the cached MCP client when all existing cache identity dimensions also match. Custom handlers using the existing .NET contract remain compatible.
What do you want reviewers to focus on? Please review the workflow-owned scope propagation and the compatibility behavior for custom handlers and direct unscoped handler calls.
Related Issue
No public issue is available for this MCP client session isolation/cache scoping bug.
Contribution Checklist
The code builds clean without any errors or warnings
All unit tests pass, and I have added new tests where possible
This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.
The reason will be displayed to describe this comment to others. Learn more.
MAF Automated Review — Iteration 1
Result: Findings reported Scope: full PR (1 commit(s)): bb9e92b189ed Model:gpt-5.6-sol-fast
Overview
The change adds workflow identity to both default MCP cache keys while preserving the existing endpoint, label, connection, and header dimensions, and its regression tests cover normal isolation, continuation, and same-key creation coalescing. Provider-backed calls also retain their per-invocation lifecycle and shutdown guards. Two residual gaps remain: the .NET compatibility fallback is scoped to a reusable workflow object rather than an execution session, and Python's completed-entry LRU does not bound the now-per-workflow set of concurrent connection attempts.
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (2 medium) across 2 files. Details are attached to the affected lines below.
Addressed the latest review summary item about .NET global client lock contention in current branch changes: cache misses now reserve/coalesce an in-flight creation under _clientLock, run CreateClientAsync outside the global lock, and publish/evict under the lock afterward. Added NoProvider_ConcurrentWorkflowSessionCreations_DoNotSerializeHandshakeAsync to cover concurrent workflow handshakes.
Ensure cleanup-originated cancellation is surfaced only after all claimed MCP sessions and completion signals have drained in both implementations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: aafca87b-cf2e-4c72-b5cb-ffcad6e1b1e0
Use awaited task results instead of Task.IsCompletedSuccessfully so the regression test compiles on .NET Framework.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: aafca87b-cf2e-4c72-b5cb-ffcad6e1b1e0
Track each cache client creator through its final semaphore release so handler disposal cannot tear down synchronization resources while creation is still exiting.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: aafca87b-cf2e-4c72-b5cb-ffcad6e1b1e0
Await the cancelled invocation through asyncio.gather so the test's coordination effect is explicit to code-quality analysis.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: aafca87b-cf2e-4c72-b5cb-ffcad6e1b1e0
When DisposeAsync encounters a client still in use, it waits on client.Disposed.Task rather than disposing it itself. The final invocation then runs DisposeCachedClientAsync; if session cleanup throws OperationCanceledException, this line marks the task successful, so handler disposal can report success despite failed cleanup. Signal the actual disposal failure through the completion task and let shutdown wait for all clients before reporting it.
Avoid rotating session IDs on continued agent turns
A second Workflow.as_agent() turn also enters this Entry action, but _ensure_state_initialized preserves the existing declarative state for a list[Message] continuation. Unconditionally rotating the ID here makes that conversation open a new MCP protocol session on every turn. Check whether a message-list turn already has initialized state before calling _ensure_state_initialized, and rotate only on a fresh run; add a two-turn agent regression.
If handler disposal wins after CreateClientAsync succeeds, the creator disposes its new, uncached connection. A cancellation from that disposal reaches the creator, but the creation-lifetime signal is still completed successfully here. DisposeAsync only awaits that signal and can return without reporting the failed orphan cleanup. Record the cleanup failure in the lifetime result, drain all creator lifetimes before reporting it, and cover this disposal race in a test.
Report active invocation cleanup failures during shutdown
If DisposeAsync starts while a cached client has an active invocation, that invocation disposes the client after it releases its lease. A cancellation during session cleanup propagates to the invocation, but this line marks the client's completion as successful, so DisposeAsync can return without reporting the failed cleanup. Complete Disposed with the cleanup exception after removing the retired client, and test shutdown while an active invocation's disposal throws OperationCanceledException.
Surface client creation cleanup failures during shutdown
If aclose() starts while an MCP client is being created, the creator closes the newly connected entry in this branch. When that close raises CancelledError, the creator reports it, but the shared future is completed with only the expected closed-handler error. _drain_shutdown ignores that error, so shutdown can report success without reporting the failed orphan cleanup. Preserve the cleanup outcome separately from the error sent to waiters and surface it after draining all entries; add a shutdown-during-creation cleanup-cancellation test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
documentationUsage: [Issues, PRs], Target: documentation in the code base and learn docs.NETUsage: [Issues, PRs], Target: .NetpythonUsage: [Issues, PRs], Target: PythonworkflowsUsage: [Issues, PRs], Target: Workflows
4 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation & Context
The default declarative MCP handlers cached connected clients by endpoint, label, connection name, and headers. When one handler instance served multiple workflow sessions, those sessions could therefore reuse the same stateful MCP protocol session. This change fixes the MCP client session isolation/cache scoping bug while preserving session continuity within one workflow session and the existing endpoint, header, label, and connection isolation.
Description & Review Guide
Related Issue
No public issue is available for this MCP client session isolation/cache scoping bug.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.