Skip to content

MCP tool-call exceptions are recorded as successful tool executions #2055

Description

@Aaronontheweb

Part of #2058.

This issue uses the terms in the engineering glossary: tool call, dispatcher, tool result, tool receipt, and outcome category.

Problem

An MCP tool call that ends in an exception becomes a tool result with no tool receipt. The dispatcher records the call as a success.

Current flow at dev (94d2bfce). This pseudocode is schematic. It omits authorization, redaction, and the output bound.

MCP server or transport throws
  -> McpClientManager.InvokeSharedAsync
       McpException       -> return "Error: MCP tool '<server>/<tool>' failed: <message>"  (no log)
       transport failure  -> log at Debug, reconnect, rethrow
  -> McpToolAdapter.ExecuteAsync
       catch-all          -> return "Error: MCP tool '<name>' failed: <message>"           (no log, no receipt)
  -> DispatchingToolExecutor
       string result      -> receipt category = success
                          -> log "Tool executed: {ToolName} ({Duration}ms, {ResultLength} chars)" at Information

Code anchors:

  • src/Netclaw.Daemon/Mcp/McpClientManager.cs:600-602 and :637-638
  • src/Netclaw.Actors/Tools/McpToolAdapter.cs:153-156 (also :179-181 on the bound-tool path)
  • src/Netclaw.Actors/Tools/DispatchingToolExecutor.cs:280-287 (stream path) and :176-196 (non-stream path)

The glossary flow for a terminal exception is: the dispatcher classifies the receipt and throws, then the actor creates the factual failure result. An MCP tool call never reaches that branch. The adapter converts the exception into a tool result before the dispatcher can classify it.

Effects:

  • At the default log level, daemon.log shows a server that fails each call as a healthy server.
  • The SDK puts the HTTP status and up to 1024 characters of the server response body in HttpRequestException.Message. Netclaw writes none of that text to a log.
  • The receipt says success. Actor logic that trusts the receipt sees a good call.

Affected cases: HTTP 5xx, HTTP 429, other HTTP 4xx, JSON-RPC errors (McpException), and HttpClient timeouts.

Not affected: an isError: true tool result. McpClientManager.ReportToolFailure (:1081) logs that case at Warning. Its receipt category is out of scope for this issue.

#2033 added ToolOutcomeResults and the outcome categories. The workspace tools use them. McpToolAdapter does not.

Expected

Owners:

  • DispatchingToolExecutor owns the tool receipt. The receipt is call-local and ephemeral.
  • McpToolAdapter supplies the outcome category for an MCP tool call.
  • McpClientManager owns the log line.

No durable data changes.

Behavior:

  • A tool-call exception produces a non-success outcome category:
    • HTTP 401 or 403 → access_denied
    • HTTP 404 → not_found
    • all other exceptions → transient_failure
  • Use one of two paths. Do not add a second outcome mechanism.
    • Path A: McpToolAdapter completes the receipt with ToolOutcomeResults and returns the factual result.
    • Path B: McpToolAdapter lets the exception reach the dispatcher. The dispatcher classifies the receipt and throws, per the glossary flow.
  • McpClientManager logs each tool-call exception at Warning. The line names the server, the tool, the HTTP status when present, and the redacted exception (SecretOutputRedactor.RedactForLogging).
  • The dispatcher's Tool executed: line stays unchanged. It reports duration and size. The Warning line from McpClientManager is the failure signal.
  • The tool result stays factual. The receipt grants no authority. Netclaw does not retry or replay the call.

Examples

Positive example. A failed call becomes a failure:

tools/call -> HTTP 500, body "upstream error"
  result:  "Error: MCP tool 'shortener/search-links' failed: ... 500 ... upstream error"
  receipt: category = transient_failure
  log:     [WRN] MCP tool 'shortener/search-links' failed (HTTP 500): ...

Negative example. A tool-level error keeps its current path:

tools/call -> HTTP 200, {"isError": true, "content": [{"type": "text", "text": "Internal Server Error"}]}
  result:  "Error: MCP tool 'shortener/search-links' reported a failure: Internal Server Error"
  log:     [WRN] MCP tool 'shortener/search-links' reported a failure: Internal Server Error   (current)
  receipt: unchanged by this issue

Tests

  • A fake IMcpToolInvoker throws HttpRequestException with StatusCode = 500. The receipt category is transient_failure. One Warning line names the server, the tool, and the status.
  • A fake throws a plain McpException. The receipt category is transient_failure.
  • A fake throws HttpRequestException with StatusCode = 403. The receipt category is access_denied. The server state stays Connected (see Auth-failure detection substring-matches server error text on the MCP tool-call path #2057).
  • An isError: true result keeps its Warning log.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpModel context protocol server / client issues.observability

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions