Skip to content

fix(health): Sign-in CTA for call-time-only OAuth servers (MCP-2084)#634

Merged
Dumbris merged 1 commit into
mainfrom
mcp-2084-calltime-oauth-cta
Jun 12, 2026
Merged

fix(health): Sign-in CTA for call-time-only OAuth servers (MCP-2084)#634
Dumbris merged 1 commit into
mainfrom
mcp-2084-calltime-oauth-cta

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 12, 2026

Copy link
Copy Markdown
Member

Problem (MCP-2084)

Remote MCP endpoints like Google's com.googleapis.sqladmin/mcp allow anonymous initialize + tools/list but enforce OAuth only at tools/call. The proxy connected, listed 15 tools, and the Web UI showed the server Connected / Enabled / Ready with no Sign-in affordance — yet every tool call failed at runtime with transport error: authorization required. The operator only discovered the OAuth requirement by failing a call.

This is not a duplicate of MCP-1819: that epic (#628/#629/#630) handles servers that surface login-required at connect time (ErrOAuthPending / 401 during initialize). Here the server connects + lists anonymously and only rejects at call time, so the health classifier never entered a login-required state.

Root cause (file:line)

  • internal/health/calculator.go — the OAuth/login branch is gated entirely on input.OAuthRequired.
  • internal/runtime/runtime.go:1968OAuthRequired: oauthConfig != nil, derived from connect-time OAuth config only. Anonymous connect ⇒ oauthConfig == nilOAuthRequired = false ⇒ login branch skipped ⇒ classified healthy/Ready.
  • internal/upstream/managed/client.go CallTool — a call-time authorization required is not a connection error, so the state machine stays Ready and nothing flags the server.

Fix (backend detection; reuses the existing action=login CTA)

  1. Managed client: on a tools/call that fails with authorization required / 401 from an otherwise-connected server, set a per-server oauthCallRequired flag (extracted into recordCallToolOAuthSignal). Cleared by a successful call or a fresh post-sign-in Connect.
  2. Runtime: read it via new IsOAuthCallRequired() and feed it into the health input as CallTimeOAuthRequired.
  3. Health calculator: a connected server with CallTimeOAuthRequired now returns Level=degraded, Action=login, Summary="Sign-in required". Placed after the connection-state switch so genuine error/disconnected/connecting states keep priority.

Once health.action=login is set, the existing #630 frontend calm Sign-in CTA and #629 macOS CTA render automatically — no new frontend/macOS work.

Tests

  • internal/health: connected + CallTimeOAuthRequired ⇒ degraded/Sign-in required/login; negative — connection errors keep priority.
  • internal/upstream/managed: call-time signal classification (auth string vs 401 vs non-auth error vs connection error); flag clears on reconnect.

Verification

  • go test ./internal/health/ ./internal/upstream/managed/ ./internal/runtime/ -race
  • go build ./... ✅ and go build -tags server ./...
  • ./scripts/run-linter.sh ✅ (0 issues)

Docs

No new config keys, CLI flags, or API endpoints. The documented Unified Health Status contract (action: login) is unchanged — this only makes it fire in a case it previously missed. No docs diff needed.

Related MCP-1819 (#628 / #629 / #630).

…P-2084)

Some remote MCP endpoints (e.g. Google's sqladmin MCP) allow anonymous
`initialize` + `tools/list` but enforce OAuth only at `tools/call`. The proxy
connected, listed 15 tools, and the health classifier reported the server as
fully healthy/Ready with no Sign-in affordance — every actual tool call failed
at runtime with "authorization required", which the operator only discovered by
failing a call.

Root cause: the health calculator's OAuth/login branch is gated entirely on
`OAuthRequired`, which is derived from connect-time OAuth config only. A server
that connects anonymously has `OAuthRequired=false`, and a call-time
"authorization required" is not a connection error, so the state machine stays
Ready and nothing flags the server for sign-in.

Fix (backend detection; reuses the existing action=login Sign-in CTA):
- managed client: on a tools/call that fails with "authorization required" /
  401 from an otherwise-connected server, set a per-server oauthCallRequired
  flag (cleared by a successful call or a fresh post-sign-in Connect).
- runtime: feed the flag into the health calculator input as
  CallTimeOAuthRequired.
- health calculator: a connected server with CallTimeOAuthRequired now returns
  Level=degraded, Action=login, Summary="Sign-in required" — placed after the
  connection-state switch so genuine error/disconnected states keep priority.

This closes the gap MCP-1819 left (which only handled servers surfacing
login-required at connect time). No new config keys, CLI flags, or API
endpoints; the documented health contract (action=login) is unchanged.

Tests: health calculator branch (+ negative: connection errors keep priority),
managed-client call-time signal classification (auth vs 401 vs non-auth vs
connection error). Related MCP-1819 (#628/#629/#630).
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: ca4cb9c
Status: ✅  Deploy successful!
Preview URL: https://98146169.mcpproxy-docs.pages.dev
Branch Preview URL: https://mcp-2084-calltime-oauth-cta.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

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 46.15385% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runtime/runtime.go 0.00% 16 Missing ⚠️
internal/upstream/managed/client.go 68.75% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: mcp-2084-calltime-oauth-cta

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (24 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 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 27398763120 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris Dumbris merged commit 32503ae into main Jun 12, 2026
44 checks passed
Dumbris added a commit that referenced this pull request Jun 12, 2026
#638)

The tool-quarantine change-detection hash covers description, input schema,
and output schema, but GET /api/v1/servers/{id}/tools/{tool}/diff only
returned the description and input schema. When an upstream evolved only its
output schema (e.g. Google sqladmin adding a "POSTGRES_20" enum value), the
visible description was byte-identical, so the flagged tool looked like a
phantom rug-pull false positive.

Add previous_output_schema / current_output_schema (already stored on the
ToolApprovalRecord) to the diff response so an operator can see exactly what
changed before approving. Document the field in docs/api/rest-api.md and add
a regression test for an output-schema-only change.

Note: change-detection itself is correct — the upstream schema genuinely
changed. This deliberately does NOT weaken the rug-pull hash (no
auto-approval of additive enum widenings); the fix is transparency, not
relaxing security.

Related #634
Related MCP-2085
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