Skip to content

feat(activity): classify tool-call failures with error_class and fault_domain (Spec 113-c) - #1502

Merged
Dumbris merged 6 commits into
mainfrom
113-c-call-error-taxonomy
Oct 7, 2026
Merged

Dumbris merged 6 commits into
mainfrom
113-c-call-error-taxonomy

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Problem

A tool call that fails shows only status=error plus prose in the Activity log. Nobody can tell whether the network dropped, the upstream returned HTTP 502, the upstream answered with a JSON-RPC error, the tool reported a business error (isError:true), the session expired, auth failed, or mcpproxy itself refused the call. Gap G6 of the Anthropic MCP-proxy talk audit (Spec 113, slice 113-c).

What changed

  • New internal/callerr: a pure classifier Classify(result, err, facts) returning error_class (network, timeout, http, jsonrpc, tool_error, session_terminated, auth, proxy_policy, proxy_internal, cancelled), fault_domain (upstream, proxy, client) and upstream_http_status. Rules follow FR-041 in order. Outcome.AuditClass() is a total mapping onto the frozen Spec 107 audit vocabulary; auditToolCall uses it, so the audit line and the activity record agree.
  • internal/transport: a per-call CallRecorder round tripper, installed as the outermost layer in upstreamRoundTripper (covers every CreateHTTPClient branch and SSE). mcp-go flattens non-2xx responses into an untyped string, so the status is recorded at the transport. Requests without a recorder in their context are untouched.
  • core.Client.CallTool marks the call dispatched at the exact point it hands it to mcp-go (every transport) and returns a typed timeout error with an unchanged message (it was a bare fmt.Errorf that dropped the chain).
  • storage.ActivityRecord gains error_class, fault_domain, upstream_http_status, all omitempty. A pre-113 JSON fixture round-trips byte-identical. isError results keep status=error and are told apart by tool_error.
  • Stamped at every tool-call completion path through the one funnel (call_tool_*, legacy call_tool, direct surface, code_execution sub-calls, REST /api/v1/tools/call); limiter sheds are stamped proxy_policy/proxy.
  • REST: contracts.ActivityRecord, SSE completion payload (forwarded verbatim by /events), error_class / fault_domain query filters on list and export. make swagger run, oas/ committed. CLI: activity list|export --error-class --fault-domain, activity show prints an Error class line, -o json|yaml carry the fields. Docs updated.

Tests run

  • go test -race (no skips needed): callerr, transport, storage, contracts, httpapi, audit, upstream/core, cmd/... all green.
  • go test -race -tags server with the CI skip regex: runtime, upstream/..., serveredition/... green; internal/server green when run alone (1150 s). A combined run with runtime hit the 20 m timeout from CPU contention on the shared machine.
  • New: classifier table tests (every FR-041 rule, isError, 502 with untyped error, -32001 after a recorded 200, narrow fallback list), AuditClass mapping, recorder installed in every client branch and concurrency isolation, record JSON compatibility, runtime emit->persist, and an httptest Streamable HTTP upstream integration test (SC-004) asserting persisted record and GET /api/v1/activity JSON for isError, 502 HTML, JSON-RPC -32001, connection reset, timeout, 404 session, 401, pre-dispatch validation and success, plus direct / code_execution / REST write paths.
  • Both golangci-lint runs (bare and --build-tags server): no findings in touched files (remaining findings are pre-existing, in untouched files).
  • ./scripts/test-api-e2e.sh on an isolated port with unique results/log paths (another session was running the same script concurrently; shared /tmp files and fixture port 39933 made the stock run flaky on both this branch and origin/main): 65/66; the one failure, Audit log: server-edition binary present, also fails on origin/main (no server-edition binary built).

Assumptions

  • A call to a server that is not connected, or has no client, is network/upstream; an error completion that no path classified is proxy_internal/proxy; a blocked completion with no noted outcome is proxy_policy/proxy.
  • The fallback text list (FR-049) is connection refused|reset, no such host, broken pipe, unexpected eof, i/o timeout, Client.Timeout exceeded, context deadline exceeded, consulted only when the call never left the proxy or HTTP requests went out with no response, so an upstream's own message is never reclassified.
  • The mcp-go *transport.Error wrapper counts as network only after the recorded non-2xx rule, because mcp-go wraps HTTP errors in it (found by the integration test).
  • cmd/generate-types does not generate ActivityRecord, so the hand-written frontend/src/types/api.ts interface got the three optional fields instead. call_tool_* wrapper records (internal_tool_call) and activity replay are not stamped; the canonical tool_call record is.
  • No config fields added (config-field checklist not applicable). internal/health/** and internal/upstream/managed/client.go untouched.

Spec: #1495
Review: opencode github-copilot/gpt-6.1-sol, 2 rounds, clean; follow-ups: none (round 1 rejected: *transport.Error-before-http ordering is an intentional documented deviation)

…t_domain (Spec 113-c)

Every failed upstream tool call now carries error_class (network, timeout,
http, jsonrpc, tool_error, session_terminated, auth, proxy_policy,
proxy_internal, cancelled), fault_domain (upstream, proxy, client) and, when
known, upstream_http_status, on the activity record, the REST activity API,
the SSE completion payload and mcpproxy activity list|show -o json|yaml.

- internal/callerr: pure classifier (Classify) plus a per-call context note
  and Outcome.AuditClass, a total mapping onto the frozen Spec 107 audit
  vocabulary so the audit line and the activity record agree.
- internal/transport: per-call CallRecorder round tripper, outermost layer of
  every upstream HTTP/SSE client, so the response status survives mcp-go
  flattening non-2xx into an untyped string.
- core.Client marks the call dispatched when it hands it to mcp-go and returns
  a typed timeout error that keeps its message.
- ActivityRecord gains three omitempty fields; old records decode unchanged.
  isError results keep status error and are told apart by tool_error.
- ActivityFilter, REST query params and CLI --error-class/--fault-domain.
- docs and OAS regenerated.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9f24b0e
Status: ✅  Deploy successful!
Preview URL: https://3fa72169.mcpproxy-docs.pages.dev
Branch Preview URL: https://113-c-call-error-taxonomy.mcpproxy-docs.pages.dev

View logs

… status on network, ignore reply POSTs in recorder, always install recorder on OAuth SSE, stamp rejected SSE and output-policy blocks
@codecov-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: 113-c-call-error-taxonomy

Available Artifacts

  • archive-darwin-amd64 (31 MB)
  • archive-darwin-arm64 (28 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (31 MB)
  • archive-windows-arm64 (27 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (27 MB)
  • installer-dmg-darwin-arm64 (24 MB)
  • smart-mcp-proxymcpproxy-goP1L0NC.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 37616353564 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

…nomy

# Conflicts:
#	internal/upstream/core/client.go
@Dumbris
Dumbris enabled auto-merge (squash) October 6, 2026 17:24
…Disconnect logs

The debug/info log lines read the flag without its mutex, racing with
acquireListToolsContext (DATA RACE on the Windows unit-test lane).
…ted after re-init

ErrSessionReestablished (Spec 113-e) did not match transport.ErrSessionTerminated,
so the call-error classifier (Spec 113-c) fell through to class http for a
non-read-only call that hit a terminated session. The not-repeated error now
matches both sentinels; its message is unchanged.
@Dumbris
Dumbris merged commit fa4a8a4 into main Oct 7, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants