Record MCP tool-call exceptions as failed outcomes (#2055) - #2060
Merged
Aaronontheweb merged 2 commits intoAug 26, 2026
Merged
Aaronontheweb merged 2 commits into
Aaronontheweb merged 2 commits into
Conversation
Aaronontheweb
marked this pull request as ready for review
August 26, 2026 04:58
An MCP tool call that ended in an exception became a tool result with no tool receipt. The dispatcher then recorded the call as a success. The daemon log showed no failure. McpToolAdapter now completes the receipt from the caught exception. An HTTP 401 or 403 gives AccessDenied. An HTTP 404 gives NotFound. Every other exception gives TransientFailure. The result text stays the same. McpClientManager.InvokeSharedAsync no longer converts an McpException to a result string. The adapter is the only converter. The manager logs one redacted Warning for each failed call. The line names the server, the tool, and the HTTP status when the server sent one. A caller cancellation writes no log. The tool lookup moved out of the try block, so an unavailable tool does not log as a failed call. This commit implements design D1 and D2 of the mcp-tool-outcome-receipts change. It also adds the diagnostics row for the new Warning line and bumps the netclaw-operations skill version.
The 404 branch in McpToolAdapter had no test. A deletion of that branch kept every adapter test green. The new test drives the shared failure helper with an HTTP 404 and asserts the NotFound receipt category. The helper also asserts that the result text names the tool.
Aaronontheweb
force-pushed
the
fix/mcp-tool-exception-receipts
branch
from
August 26, 2026 08:39
d0e48da to
359f95e
Compare
Aaronontheweb
added a commit
that referenced
this pull request
Aug 26, 2026
….1 test alone Two Windows-only tests failed on PR #2060. The job log is here: https://github.com/netclaw-dev/netclaw/actions/runs/32948913877/job/98115679851 Failure 1: ToolApprovalActorTests.Version_two_omission_emits_one_bounded_actor_diagnostic threw AskTimeoutException after 5 seconds. AkkaToolApprovalService hard-coded a 5 second Ask budget on four calls. The actor answers GetUnapprovedPatterns on its own mailbox. It takes a file lock, reads the store, and migrates v2 records to v3. A busy runner starves the thread pool, so the first cold store access loses a 5 second budget. Fix 1: each Ask now uses SessionConfig.ToolExecutionTimeout as its timeout. The default is 90 seconds. Both constructors take a required SessionConfig parameter. The daemon already registers SessionConfig as a singleton, so the DI wiring needs no change. The bound stays finite because some approval record callers pass CancellationToken.None. An unbounded Ask on that path suspends the session actor mailbox forever after a lost reply. Failure 2: ShellToolTests.Windows_power_shell_51_executes_through_the_selected_host returned "Error: Command timed out after 90 seconds." The test starts a real powershell.exe inside the parallel unit test host. Fix 2: the test now carries Trait("Category", "NativeShell"). The parallel CI test step excludes that category on every operating system. A new Windows-only step runs the category alone after the parallel step, so the live process does not share CPU with other test collections. Both steps are solution-level, so the exclusion and the serial run cover the same set of projects. Missing_selected_executable_fails_without_fallback still proves host selection deterministically. Related: #1409, #1378
Aaronontheweb
commented
Aug 26, 2026
| { | ||
| StatusCode: HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden | ||
| } => context.AccessDenied(text), | ||
| HttpRequestException { StatusCode: HttpStatusCode.NotFound } => context.NotFound(text), |
Collaborator
Author
There was a problem hiding this comment.
I guess we're treating it as a transient failure because at some point we downloaded the MCP tool definition and we're subscribed to updates but the call we're making didn't reach that JSON RPC endpoint so it must be temporarily offline.
Aaronontheweb
added a commit
that referenced
this pull request
Aug 26, 2026
…lone Two Windows-only tests failed on PR #2060. A re-run of the same commit passed. Both tests lost a fixed wall-clock budget on a slow runner. The job log is here: https://github.com/netclaw-dev/netclaw/actions/runs/32948913877/job/98115679851 Failure 1: ToolApprovalActorTests.Version_two_omission_emits_one_bounded_actor_diagnostic threw AskTimeoutException after 5 seconds. AkkaToolApprovalService hard-coded a 5 second Ask budget on four calls. The approval actor opens the approvals file on its own mailbox. It takes a file lock, reads the file, and migrates v2 records to v3. A busy runner starves the thread pool, so the first cold read lost 5 seconds. Fix 1: the four Ask calls now use a service constant of 30 seconds. The budget covers that file round-trip only. It does not cover the wait for a person to approve the tool call. The budget stays finite because some callers pass CancellationToken.None. A lost reply then suspends the caller mailbox forever. Failure 2: ShellToolTests.Windows_power_shell_51_executes_through_the_selected_host returned "Error: Command timed out after 90 seconds." The test starts a real powershell.exe inside the parallel unit test host. Fix 2: the test now carries Trait("Category", "NativeShell"). The parallel CI test step excludes that category on every operating system. A new Windows-only step runs the category alone after the parallel step, so the live process does not share CPU with other test collections. Both steps are solution-level, so the exclusion and the serial run cover the same set of projects. Related: #1409, #1378
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
An MCP tool call that ends in an exception now produces a failure receipt and one Warning log line.
Before this change the dispatcher recorded such a call as a success, and the daemon log showed no failure.
The model-facing result text does not change.
Closes #2055
Stacked on #2059. Review and merge that pull request first.
Design refs
McpToolAdaptercompletes the receipt from the caught exception.McpClientManager.InvokeSharedAsyncrethrows a non-transport exception and logs once.D3 and D4 stay for the next pull requests in the stack. An
HttpRequestExceptionwith status 500 is still atransport failure here, so it still triggers one reconnect. #2056 changes that.
What changed
src/Netclaw.Actors/Tools/McpToolAdapter.cs: the catch-all returns the same error string throughToolOutcomeResults. An HTTP 401 or 403 givesAccessDenied. An HTTP 404 givesNotFound. Every otherexception gives
TransientFailure. The bound-tool path does not change.src/Netclaw.Daemon/Mcp/McpClientManager.cs: the tool lookup moved out of thetryblock. One catch logs aredacted Warning and rethrows a non-transport exception. The
McpException-to-string branch is gone, so theadapter is the only converter. A caller cancellation writes no log and starts no reconnect.
feeds/skills/.system/files/netclaw-operations/references/diagnostics.md: one new row for the Warning line.feeds/skills/.system/files/netclaw-operations/SKILL.md: version 2.65.1 to 2.65.2.openspec/changes/mcp-tool-outcome-receipts/tasks.md: tasks 2.1, 2.2, 2.3, and 5.2 are complete.Log template:
MCP tool '{Tool}' invocation failed{HttpStatus}, for exampleMCP tool 'shortener/search-links' invocation failed (HTTP 500).Tests added
src/Netclaw.Actors.Tests/Tools/McpToolAdapterTests.cs:TransientFailurereceipt.AccessDeniedreceipt.NotFoundreceipt.McpExceptiongives aTransientFailurereceipt.src/Netclaw.Daemon.Tests/Mcp/McpClientManagerLifecycleTests.cs:application MCP failurecase now expects theMcpExceptionto reach the caller.Gate outputs
dotnet build src/Netclaw.Daemon/Netclaw.Daemon.csproj -nologo -v q: 0 warnings, 0 errors.dotnet test src/Netclaw.Actors.Tests: 3615 passed, 0 failed, 3 skipped.dotnet test src/Netclaw.Daemon.Tests: 1041 passed, 0 failed.dotnet slopwatch analyze: 0 issues.pwsh ./scripts/Add-FileHeaders.ps1 -Verify: all files have headers.Adversarial review
not_foundhad no test. The pull request adds one adapter test.(HTTP 500)suffix in the Warning line is asserted in the pull request for Every HttpRequestException on an MCP tool call forces a full reconnect #2056,where an HTTP 500 no longer reconnects.
IsTransportOrSessionFailure, which does not unwrap. A wrapped status maps totransient_failure.HttpStatuslog field carries display text (" (HTTP 500)"). A numeric field needs asecond template. The display text keeps the fewest moving parts.
SecretOutputRedactorhas its own tests.netclaw-operations/references/diagnostics.md.evals/README.mdrequires no new case for a reference-tablerow.