Skip to content

feat(health): degrade on rolling tool-call failure rate (Spec 113-d) - #1504

Merged
Dumbris merged 4 commits into
mainfrom
113-d-health-call-failure-rate
Oct 6, 2026
Merged

Dumbris merged 4 commits into
mainfrom
113-d-health-call-failure-rate

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Problem

A connected upstream whose tool calls mostly fail still reported healthy: health looked only at connection and OAuth state. Gap G7 of the Anthropic MCP-proxy talk audit (Spec 113, FR-060..FR-068).

What changed

  • New internal/upstream/callstats: a 30 x 10 s bucketed rolling window (5 min) per server, a registry, and a denylist predicate Classify. Not counted at all: isError tool results, limiter rejections, ErrConnectionGenerationChanged, caller cancellation (including the plain-text "context cancelled" form), auth-required errors. Everything else returned by the dispatch is a counted failure; a normal result is a counted success. Proxy policy/quarantine refusals and argument validation happen before dispatch and never reach the recorder.
  • upstream.Manager records at callTool (covers direct surface, call_tool_*, REST) and code_execution records via RecordClientCallOutcome (it bypasses the manager). The window and notifier state are dropped in RemoveServer inside the same critical section as the client delete. Nothing in managed/client.go or ActivityRecord.
  • health.CalculateHealth is now a wrapper: only a ready, enabled, healthy result is downgraded to degraded when calls >= 5 and failures/calls > 0.5. Summary: "N of M tool calls failed in the last 5 min"; detail names the dominant failure kind; action view_logs. Disabled, quarantined, connecting, unhealthy, OAuth and refresh outcomes all take precedence (table tests). Thresholds are constants (health.CallFailureWindow, CallFailureMinSamples, CallFailureRatio); no config field.
  • The three HealthCalculatorInput construction sites (runtime.go, server.go, mcp.go) read Manager.CallStats; a drift-guard test fails if a site stops assigning CallsInWindow.
  • T199 (SSE): the existing servers.changed producers are all connection-state driven, so there is no poll-and-diff to ride on. Added a push path: Manager.SetCallHealthObserver, wired in runtime/lifecycle.go to emitServersChanged("call_failure_rate") (already coalesced). Debounced to one per 10 s per server, with a per-bucket recheck timer while the window holds calls so passive recovery or expiry-driven degradation also notifies.
  • Docs: docs/designs/2025-12-10-unified-health-status.md.

Tests run

  • TDD: callstats, calculator and manager tests written first and observed failing (compile errors / red assertions) before the implementation.
  • go test -race on internal/health, internal/upstream/..., internal/runtime/...: pass.
  • go test -race -tags server -skip "E2E|Binary|MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" ./internal/server/... ./internal/httpapi/...: pass.
  • SC-005 coverage: TestGetAllServers_HealthDegradesOnCallFailureRate (runtime projection, 6 of 10 -> degraded), window expiry in callstats and notifier tests with an injected clock.
  • Personal and -tags server builds; golangci-lint (both runs): no findings in touched files (the local linter reports 15/18 pre-existing issues in untouched files).
  • scripts/test-api-e2e.sh on an isolated port (18468): 65/66 pass. The one failure, "Audit log: server-edition binary present", needs a prebuilt mcpproxy-server binary (environmental, unrelated to this change).
  • No REST/OAS/config changes, so no make swagger.

Assumptions

  • Thresholds live in internal/health (not callstats) so the calculator does not import the managed-client dependency tree; callstats takes the window length as its own constant of the same value.
  • HealthCalculatorInput gets a third field, DominantCallFailureKind, so the detail text can name the kind without health importing callstats.
  • The failure kind is derived from the error text (mcp-go discards HTTP status and JSON-RPC codes); 113-c's classifier is not merged, so the local predicate is used as the spec allows. Untyped errors count as failures of kind "other".
  • A caller deadline that expires after dispatch counts as a timeout failure (only cancellation is excluded).

Spec: #1495
Review: opencode github-copilot/gpt-6.1-sol, 5 rounds, clean; earlier rounds fixed text-form cancellation, expiry-driven notification, in-flight resurrection of a removed window and a late-drop wipe; the latest full re-review (3 chunks) found 3 mediums (text-cancel exclusion without a done caller ctx, call window not dropped on config-change client replacement, stale notifier timer callbacks), all fixed, incremental round CLEAN; follow-ups: none.

…te (Spec 113-d)

A connected server whose tool calls mostly fail still reported healthy,
because health only looked at connection and OAuth state. Keep a rolling
5 min per-server window of call outcomes and report degraded when at
least 5 counted calls were made and more than 50% failed.

- new internal/upstream/callstats: 30x10s bucketed window, registry, and a
  denylist predicate (isError results, limiter/generation refusals, caller
  cancellation and auth-required errors are not counted)
- upstream.Manager records outcomes at the dispatch choke point
  (Manager.callTool) and for code_execution via RecordClientCallOutcome;
  the window is dropped with the server in RemoveServer
- health.CalculateHealth wraps the existing calculator: only a ready,
  enabled, healthy result is downgraded, so every existing non-healthy
  outcome keeps precedence
- all three HealthCalculatorInput construction sites read Manager.CallStats;
  a drift-guard test keeps them in step
- debounced servers.changed nudge when the verdict flips, including
  passive recovery when failures age out of the window
@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: b4dad46
Status: ✅  Deploy successful!
Preview URL: https://1f5896d9.mcpproxy-docs.pages.dev
Branch Preview URL: https://113-d-health-call-failure-ra.mcpproxy-docs.pages.dev

View logs

…all window on client replacement, guard stale notifier timers (Spec 113-d review r1)
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: 113-d-health-call-failure-rate

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-go883VMC.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 37439812051 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@codecov-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 85.56701% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/upstream/callstats/window.go 76.74% 18 Missing and 2 partials ⚠️
internal/upstream/manager_callhealth_notify.go 81.60% 11 Missing and 5 partials ⚠️
internal/runtime/lifecycle.go 25.00% 2 Missing and 1 partial ⚠️
internal/upstream/manager.go 92.68% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Dumbris
Dumbris enabled auto-merge (squash) October 6, 2026 17:24
@Dumbris
Dumbris merged commit 9cd1cd9 into main Oct 6, 2026
60 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