Skip to content

.NET: (Durable): bind MCP threadId to the current agent and guard cross-agent session dispatch#6531

Open
kshyju wants to merge 2 commits into
mainfrom
kshyju/mcp-cross-agent-threadid-fix
Open

.NET: (Durable): bind MCP threadId to the current agent and guard cross-agent session dispatch#6531
kshyju wants to merge 2 commits into
mainfrom
kshyju/mcp-cross-agent-threadid-fix

Conversation

@kshyju

@kshyju kshyju commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

The Azure Functions MCP tool dispatch path for durable agents (BuiltInFunctions.RunMcpToolAsync) handled the caller-supplied threadId argument differently from the HTTP trigger path (BuiltInFunctions.RunAgentHttpAsync):

  • HTTP path binds the caller's thread_id under the current agent name: new AgentSessionId(agentName, threadIdValue).
  • MCP path ran AgentSessionId.Parse(threadId), which extracts both the agent name and the key from the input string.

Because of that asymmetry, the agent that actually executed under the MCP tool was determined by parsing the caller's threadId rather than by the MCP tool the caller invoked. This PR aligns the MCP path with the HTTP path so a session is always scoped to its dispatching agent, and adds a defense-in-depth guard at the proxy layer so the same invariant is enforced regardless of how the session reaches the proxy.

Description & Review Guide

  • What are the major changes?

    1. Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs: RunMcpToolAsync now constructs new AgentSessionId(agentName, threadId) instead of AgentSessionId.Parse(threadId). This mirrors RunAgentHttpAsync and treats the caller-supplied threadId as a session key under the current MCP tool's agent.
    2. Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs: RunCoreAsync validates that session.SessionId.Name matches the proxy's Name (case-insensitive) before dispatching to the durable client. Throws ArgumentException(paramName: "session") on mismatch with a clear message naming both agents.
    3. Tests (new):
      • tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAIAgentProxyTests.cs: three tests covering the proxy guard (rejects mismatch, allows match, case-insensitive). Uses a small private StubDurableAgentClient; no additional InternalsVisibleTo attributes are added to the production assembly.
      • tests/Microsoft.Agents.AI.DurableTask.UnitTests/AgentSessionIdTests.cs: adds ConstructorTreatsKeyAsOpaqueValue pinning the invariant the primary fix relies on: new AgentSessionId(name, key) keeps Name from the first argument even when the key looks like a serialized session id.
  • What is the impact of these changes?

    • No public API changes.
    • Behavior change for an unusual edge case: callers that hand-crafted a fully serialized AgentSessionId string (e.g. @dafx-<name>@<key>) and passed it as the MCP threadId will now have that entire string treated as the session key under the current agent. Ordinary opaque-string-in / opaque-string-out usage is unaffected.
    • The proxy-layer guard also closes the same class of mismatch for any other future caller of DurableAIAgentProxy.RunCoreAsync.
  • What do you want reviewers to focus on?

    • The choice between the primary fix alone vs. the primary + defense-in-depth guard. They are intentionally redundant: each independently prevents a session whose Name differs from the dispatching agent's name from being executed by the wrong agent.
    • The error message in DurableAIAgentProxy.RunCoreAsync (names only, no key) to confirm it's appropriately scoped.
    • Test approach for DurableAIAgentProxy: I avoided Moq for the internal IDurableAgentClient interface (which would have required adding InternalsVisibleTo("DynamicProxyGenAssembly2") to the production csproj) by using a small hand-written stub inside the test class. Happy to switch if there is a stronger preference.

Related Issue

Fixes #

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • 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.

Validation

  • dotnet build: clean on net8.0 / net9.0 / net10.0.
  • dotnet test tests/Microsoft.Agents.AI.DurableTask.UnitTests: 405 / 405 pass (401 baseline + 4 new) across net8.0 / net9.0 / net10.0.
  • dotnet test tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests: 42 / 42 pass across net8.0 / net9.0 / net10.0.

Copilot AI review requested due to automatic review settings June 16, 2026 00:20
@moonbox3 moonbox3 added the .NET label Jun 16, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 91% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by kshyju's agents

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns the Durable Azure Functions MCP tool dispatch path with the HTTP trigger path so the caller-supplied threadId is always treated as an opaque session key under the currently-dispatching agent, and adds a proxy-level guard to prevent cross-agent session dispatch.

Changes:

  • RunMcpToolAsync now constructs new AgentSessionId(agentName, threadId) instead of parsing the agent name from the caller input.
  • DurableAIAgentProxy.RunCoreAsync now rejects sessions whose SessionId.Name doesn’t match the proxy’s agent name (case-insensitive).
  • Adds unit tests covering the proxy guard and the “constructor treats key as opaque” invariant.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs Binds MCP threadId to the current agent name rather than parsing the agent name from the caller-provided string.
dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs Adds a defense-in-depth check to prevent dispatching a session for agent X through agent Y’s proxy.
dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAIAgentProxyTests.cs Adds unit tests validating mismatch rejection + case-insensitive match behavior.
dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/AgentSessionIdTests.cs Pins that the 2-arg AgentSessionId constructor treats the key as opaque (does not reinterpret serialized IDs).

Comment thread dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
- Rename three test methods to include Async suffix (IDE1006 fix)
- Add CHANGELOG entries for DurableTask and Hosting.AzureFunctions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@moonbox3 moonbox3 added the documentation Improvements or additions to documentation label Jun 16, 2026
@kshyju kshyju requested a review from Copilot June 16, 2026 01:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@kshyju kshyju requested a review from cgillum June 16, 2026 01:08

@cgillum cgillum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation durabletask .NET

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants