Repository navigation
Description
Description
When GitHubCopilotAgent runs against MCP HTTP tools, the underlying Copilot SDK successfully invokes the tool and returns the result, but the result is wrapped in an opaque AIContent { RawRepresentation = ... } instead of being projected into FunctionResultContent. The LLM receives the tool's output bytes embedded in its conversation context but with no semantic marker that this is the result of its tool call.
Every model tested (gpt-4.1, gpt-5-mini, claude-sonnet-4.6) interprets this as "tool call never completed" and emits a fabricated timeout error in its final response (commonly MCP error -32001: Request timed out, which is not a real protocol code).
The Python equivalent of this bug has been fixed in #4734, #4814, #4828. The .NET equivalent is unfixed in current main (= 1.6.1-preview.260514.1).
Reproduction
- Configure a
GitHubCopilotAgentwith one or more MCP HTTP servers exposing read-only tools (e.g.msgraph-admin,sn-query-table). - Set
SessionConfig.Hooks.OnPreToolUseandOnPostToolUseto increment counters. - Issue a request that requires a single tool call returning a JSON payload of any size.
- Inspect the response stream's content types and the hook counters.
Expected behavior
OnPostToolUsefires for every successful tool return (matches Python testtest_hooks_e2e.py::test that postToolUse hook is invoked after model runs a tool).RunAsyncresponse stream contains aFunctionResultContent(or equivalent) item per tool call.- The LLM recognizes the tool result and proceeds with the task.
Actual behavior
OnPreToolUsefires correctly (one fire per tool invocation).OnPostToolUsefires only for tool returns that have a structured failure body (e.g.{success:false, ...}). It does NOT fire for successful tool returns.RunAsyncresponse stream contains zeroFunctionCallContentand zeroFunctionResultContentitems — onlyTextContent. Counted viaresponse.Messages.SelectMany(m => m.Contents).Where(c => c is X).- The tool's bytes ARE present in the LLM's input on the next turn (input token counts jump from ~6 KB to ~70-110 KB after the call), proving data flow is intact.
- The LLM cannot interpret the bytes as a tool result and fabricates a timeout error in its final response.
Empirical evidence (3 runs, 3 different model families, identical pattern)
Across runs against MCP HTTP tools (msgraph-admin, sn-query-table, pnp-admin, exchange-admin):
| Run | Executor model | OnPreToolUse fires | OnPostToolUse fires | API server confirmed successes |
|---|---|---|---|---|
| 1 | gpt-4.1 | 7 | 0 | 5 |
| 2 | gpt-5-mini | 4 | 0 | 4 |
| 3 | claude-sonnet-4.6 | 8 | 0 | 6 |
OnPostToolUse fired exactly once across these 19 invocations, and only for pnp-admin's structured-failure response. All 15 successful MCP returns failed to fire OnPostToolUse.
In each case the LLM's final output (persisted via our journey messages table) contained text along the lines of:
"Request to Microsoft Graph timed out while retrieving users"(gpt-4.1)"MCP error -32001: Request timed out... Retries also timed out"(gpt-5-mini)"Microsoft Graph API calls are timing out consistently (MCP error -32001). Three successive Graph API calls with progressively smaller \$top values all timed out, indicating a backend connectivity issue rather than a query error"(claude-sonnet-4.6 — note the inferential reasoning about a non-existent timeout)
Root cause (proposed)
In dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs on main, the event-dispatch switch in RunCoreStreamingAsync (around line 167) has no case for tool-execution-complete events. They fall through to default: and are wrapped via ConvertToAgentResponseUpdate(SessionEvent) as new AIContent { RawRepresentation = sessionEvent }.
Specifically:
case AssistantMessageDeltaEvent:→TextContentcase AssistantMessageEvent:→AIContent { RawRepresentation }case AssistantUsageEvent:→UsageContentcase SessionIdleEvent / SessionErrorEvent:→ handleddefault:(includes tool execution events) → opaqueAIContent
There is no projection of tool execution events into FunctionCallContent or FunctionResultContent.
Related: #4633 documents the broader "AIContent { RawRepresentation } serializes as empty objects" issue for the same code paths. #4732 is also relevant — CopySessionConfig() hardcodes Streaming = true, forcing every consumer through the streaming path that has this bug.
Proposed fix
Add explicit cases in the event-dispatch switch for tool execution events, projecting them into FunctionCallContent (on ToolExecutionStartEvent / equivalent) and FunctionResultContent (on ToolExecutionCompleteEvent / equivalent). Mirror the Python fixes in #4734, #4814, #4828.
Package versions
Microsoft.Agents.AI.GitHub.Copilot 1.3.0-preview.260423.1GitHub.Copilot.SDK 0.2.2Microsoft.Agents.AI.Workflows 1.3.0- .NET 10.0
- Bug also confirmed in current
main(=1.6.1-preview.260514.1) via source inspection ofGitHubCopilotAgent.cs.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-].NET: [Bug]: GitHubCopilotAgent silently drops MCP tool result events — projected as opaque AIContent instead of FunctionResultContent, causing models to fabricate timeout errors[/-][+]Python: .NET: [Bug]: GitHubCopilotAgent silently drops MCP tool result events — projected as opaque AIContent instead of FunctionResultContent, causing models to fabricate timeout errors[/+]Fix ParseToolArguments to handle non-object JSON gracefully (microsof…
Address review: broaden ParseToolArguments catch to prevent any strea…
Add regression test for MCP tool result event projection (microsoft#5897
Address review feedback for microsoft#5897: Python: .NET: [Bug]: GitH…
Address review feedback for microsoft#5897: Python: .NET: [Bug]: GitH…
fix(microsoft#5897): Address review feedback for ParseArguments robus…
Fix non-generic IDictionary key handling in ParseArguments (microsoft…
Fix missing 'using System' in ToolExecutionEventProjectionTests
.NET: Project ToolExecution events as FunctionCallContent/FunctionRes…