Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bb9e92b
fix: isolate MCP sessions by workflow session
baywet Sep 22, 2026
5232c71
.NET/Python: Address MCP session review feedback
baywet Sep 22, 2026
777056f
Address MCP review feedback
baywet Sep 23, 2026
2182915
chore: linting
baywet Sep 23, 2026
c5a98d7
Merge origin/main into fix/mcp-workflow-session-isolation
baywet Sep 23, 2026
f1dde52
Address MCP review lifecycle comments
Copilot Sep 23, 2026
3293b65
Address MCP session review feedback
Copilot Sep 23, 2026
2ce112b
Fix MCP creation cancellation and run isolation
Copilot Sep 23, 2026
4024d20
Merge branch 'main' into fix/mcp-workflow-session-isolation
baywet Sep 28, 2026
bda47d1
fix: retry MCP waiter after creator cancellation
baywet Sep 28, 2026
f5c133f
chore: Use generated session ID for workflow session context
baywet Sep 28, 2026
5f21a68
chore: Use generated workflow session ID for context
baywet Sep 28, 2026
3c97cc2
Merge origin/main into fix/mcp-workflow-session-isolation
baywet Sep 28, 2026
97c636d
fix: make MCP shutdown cancellation-safe
baywet Sep 28, 2026
bfcd02e
chore: Potential fix for pull request finding 'Statement has no effect'
baywet Sep 29, 2026
d8d8c61
fix: support MCP cancellation on Python 3.10
baywet Sep 29, 2026
46abaee
test: avoid assigning None from MCP shutdown
baywet Sep 29, 2026
b470205
fix: observe failed MCP client creation tasks
baywet Sep 29, 2026
f47001b
chore: Potential fix for pull request finding 'Statement has no effect'
baywet Sep 29, 2026
80988a0
test: await MCP shutdown through gather
baywet Sep 29, 2026
9548e5b
Fix MCP shutdown cancellation draining
baywet Sep 29, 2026
af94e4f
Fix net472 MCP lifetime test build
baywet Sep 29, 2026
dd617dc
Wait for MCP client creators during disposal
baywet Sep 29, 2026
f33b4c6
chore: Potential fix for pull request finding 'Generic catch clause'
baywet Sep 29, 2026
18720f0
Harden nested MCP session lifetimes
baywet Sep 29, 2026
bdee27b
Clarify cancelled MCP task coordination
baywet Sep 29, 2026
1283c99
Harden MCP restore and cancellation handling
baywet Sep 29, 2026
447f8b6
Fix async workflow test naming
baywet Sep 29, 2026
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Declarative.Mcp.DefaultMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,34 @@ Task<McpServerToolResultContent> InvokeToolAsync(
string? connectionName,
CancellationToken cancellationToken = default);
}

/// <summary>
/// Defines the contract for MCP handlers that isolate stateful protocol sessions by workflow session.
/// </summary>
public interface IWorkflowScopedMcpToolHandler : IMcpToolHandler
{
/// <summary>
/// Invokes an MCP tool within the framework-owned workflow session scope.
/// </summary>
/// <param name="serverUrl">The URL of the MCP server.</param>
/// <param name="serverLabel">An optional label identifying the server connection.</param>
/// <param name="toolName">The name of the tool to invoke.</param>
/// <param name="arguments">Optional arguments to pass to the tool.</param>
/// <param name="headers">Optional headers to include in the request.</param>
/// <param name="connectionName">An optional connection name for managed connections.</param>
/// <param name="workflowSessionId">The framework-owned identifier for the current workflow session.</param>
/// <param name="cancellationToken">A token to observe cancellation.</param>
/// <returns>
/// A task representing the asynchronous operation. The result contains a <see cref="McpServerToolResultContent"/>
/// with the tool invocation output.
/// </returns>
Task<McpServerToolResultContent> InvokeToolInWorkflowSessionAsync(
string serverUrl,
string? serverLabel,
string toolName,
IDictionary<string, object?>? arguments,
IDictionary<string, string>? headers,
string? connectionName,
string workflowSessionId,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkf

try
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this.State), cancellationToken).ConfigureAwait(false);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this.State, cancellationToken).ConfigureAwait(false);
object? result = await this.ExecuteAsync(declarativeContext, cancellationToken).ConfigureAwait(false);
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");

if (this.EmitResultEvent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,47 @@

namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter;

internal sealed class DeclarativeWorkflowContext : IWorkflowContext
internal sealed class DeclarativeWorkflowContext : IWorkflowContext, IWorkflowSessionContext
{
internal const string WorkflowSessionIdStateKey = "__declarative_mcp_workflow_session_id";

public static readonly FrozenSet<string> ManagedScopes =
[
VariableScopeNames.Local,
VariableScopeNames.Topic,
VariableScopeNames.Global,
];

public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state)
private DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state, string sessionId)
{
this.Source = source;
this.State = state;
this.SessionId = sessionId;
}

public static async ValueTask<DeclarativeWorkflowContext> CreateAsync(
IWorkflowContext source,
WorkflowFormulaState state,
CancellationToken cancellationToken = default)
{
string generatedSessionId = Guid.NewGuid().ToString("N");
string sessionId = source is IWorkflowSessionContext sessionContext
? sessionContext.SessionId
: await source.ReadOrInitStateAsync(
WorkflowSessionIdStateKey,
() => generatedSessionId,
VariableScopeNames.System,
cancellationToken: cancellationToken).ConfigureAwait(false);
Comment thread
baywet marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.

return new(source, state, sessionId);
}

private IWorkflowContext Source { get; }
public WorkflowFormulaState State { get; }
public IReadOnlyDictionary<string, string>? TraceContext => this.Source.TraceContext;

public string SessionId { get; }

/// <inheritdoc/>
public bool ConcurrentRunsEnabled => this.Source.ConcurrentRunsEnabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ private async ValueTask AdvanceAsync(ChatMessage input, IWorkflowContext context
// No state to restore if we're starting from the beginning.
state.SetInitialized();

DeclarativeWorkflowContext declarativeContext = new(context, state);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, state, cancellationToken).ConfigureAwait(false);

// Conversation id resolution prefers state already persisted by a prior turn,
// so multi-turn invocations reuse the same backend conversation rather than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ public override async ValueTask HandleAsync(TMessage message, IWorkflowContext c
{
if (this._action is not null)
{
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, cancellationToken).ConfigureAwait(false);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this._state, cancellationToken).ConfigureAwait(false);
await this._action.Invoke(declarativeContext, message, cancellationToken).ConfigureAwait(false);
}

if (this._emitResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public ValueTask ResetAsync()
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken).ConfigureAwait(false);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this._session.State, cancellationToken).ConfigureAwait(false);
object? result = await this.ExecuteAsync(declarativeContext, message, cancellationToken).ConfigureAwait(false);
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");

await context.SendResultMessageAsync(this.Id, result, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public ValueTask ResetAsync()
[SendsMessage(typeof(ActionExecutorResult))]
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
DeclarativeWorkflowContext declarativeContext = new(context, this._state);
DeclarativeWorkflowContext declarativeContext =
await DeclarativeWorkflowContext.CreateAsync(context, this._state, cancellationToken).ConfigureAwait(false);

ChatMessage input = (this._inputTransform ?? DefaultInputTransform).Invoke(message);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ internal sealed class InvokeMcpToolExecutor(
{
private const string ApprovalSnapshotStateKey = nameof(_approvalSnapshots);
private const string LegacyApprovalSnapshotStateKey = "_approvalSnapshot";

/// <summary>
/// Snapshots of evaluated parameters captured at approval-request time, keyed by
/// per-invocation request id. Each pending approval lives here until the matching
Expand Down Expand Up @@ -115,7 +114,8 @@ public static bool RequiresNothing(object? message) =>
}

// No approval required - invoke the tool directly
McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync(
McpServerToolResultContent resultContent = await this.InvokeToolAsync(
context,
serverUrl,
serverLabel,
toolName,
Expand Down Expand Up @@ -172,7 +172,8 @@ public async ValueTask CaptureResponseAsync(

Dictionary<string, string>? headers = this.GetHeaders();

McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync(
McpServerToolResultContent resultContent = await this.InvokeToolAsync(
context,
snapshot.ServerUrl,
snapshot.ServerLabel,
snapshot.ToolName,
Expand All @@ -184,6 +185,47 @@ public async ValueTask CaptureResponseAsync(
await this.ProcessResultAsync(context, resultContent, cancellationToken).ConfigureAwait(false);
}

private async Task<McpServerToolResultContent> InvokeToolAsync(
IWorkflowContext context,
string serverUrl,
string? serverLabel,
string toolName,
IDictionary<string, object?>? arguments,
IDictionary<string, string>? headers,
string? connectionName,
CancellationToken cancellationToken)
{
if (mcpToolHandler is IWorkflowScopedMcpToolHandler scopedHandler)
{
string generatedWorkflowSessionId = Guid.NewGuid().ToString("N");
string workflowSessionId = context is IWorkflowSessionContext sessionContext
? sessionContext.SessionId
: await context.ReadOrInitStateAsync(
DeclarativeWorkflowContext.WorkflowSessionIdStateKey,
() => generatedWorkflowSessionId,
VariableScopeNames.System,
cancellationToken).ConfigureAwait(false);
Comment thread
Copilot marked this conversation as resolved.
return await scopedHandler.InvokeToolInWorkflowSessionAsync(
serverUrl,
serverLabel,
toolName,
arguments,
headers,
connectionName,
workflowSessionId,
cancellationToken).ConfigureAwait(false);
}

return await mcpToolHandler.InvokeToolAsync(
serverUrl,
serverLabel,
toolName,
arguments,
headers,
connectionName,
cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Completes the MCP tool invocation by raising the completion event.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
Microsoft.Agents.AI.Workflows.Declarative.Events.ExternalInputResponse.RequestId.get -> string?
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<object?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync<TValue>(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<TValue?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<string!>!>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
Microsoft.Agents.AI.Workflows.Declarative.Events.ExternalInputResponse.RequestId.get -> string?
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<object?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync<TValue>(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<TValue?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<string!>!>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowedEnvi
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.get -> bool
Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowOptions.AllowProcessEnvironmentVariableFallback.init -> void
Microsoft.Agents.AI.Workflows.Declarative.Events.ExternalInputResponse.RequestId.get -> string?
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler
Microsoft.Agents.AI.Workflows.Declarative.IWorkflowScopedMcpToolHandler.InvokeToolInWorkflowSessionAsync(string! serverUrl, string? serverLabel, string! toolName, System.Collections.Generic.IDictionary<string!, object?>? arguments, System.Collections.Generic.IDictionary<string!, string!>? headers, string? connectionName, string! workflowSessionId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Extensions.AI.McpServerToolResultContent!>!
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.ConvertValueWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, Microsoft.Agents.AI.Workflows.Declarative.Kit.VariableType! targetType, string! key, string? scopeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<object?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.EvaluateValueWithSensitivityAsync<TValue>(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<TValue?>!>
static Microsoft.Agents.AI.Workflows.Declarative.Kit.IWorkflowContextExtensions.FormatTemplateWithSensitivityAsync(this Microsoft.Agents.AI.Workflows.IWorkflowContext! context, string! line) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.ObjectModel.Abstractions.EvaluationResult<string!>!>
Expand Down
Loading
Loading