Skip to content

fix(mcp): mark a server needs_auth when a tool call is rejected with 401 - #55013

Closed
potoior wants to merge 4 commits into
anomalyco:devfrom
potoior:mcp-needs-auth
Closed

potoior wants to merge 4 commits into
anomalyco:devfrom
potoior:mcp-needs-auth

Conversation

@potoior

@potoior potoior commented Oct 13, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #53942

Type of change

  • Bug fix

What does this PR do?

When an MCP server keeps rejecting every refreshed token after a failed OAuth refresh, each tools/call fails with HTTP 401 but the server status stays "Connected" — no signal to re-auth, and it keeps refreshing on every call.

This detects auth failures precisely instead of matching error message text:

  • packages/core/src/mcp/client.ts — a real HTTP 401 (SdkHttpError.status === 401) or the SDK's UnauthorizedError surfaces as the existing MCP.NeedsAuthError. Errors from other APIs that merely echo "401" no longer count as auth failures.
  • packages/core/src/mcp/index.ts — callTool catches MCP.NeedsAuthError and, while holding the server lock and only when the connection is still the live one, stops the server, marks it needs_auth, and publishes StatusChanged so /mcps and the UI show it. The error is no longer double-wrapped.

Re-authing from /mcps (or a credential change via the existing reconnect path) restarts the server and restores the tools.

How did you verify your code works?

  • packages/core typecheck clean.
  • 3/3 new auth tests pass in mcp.test.ts.
  • The remaining 2 pre-existing mcp.test.ts failures reproduce identically on unmodified v2 (container-environment timing).

Screenshots / recordings

N/A — backend change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #53804, #49773

When an MCP server keeps rejecting every refreshed token, each tools/call fails with a 401 but the server status stays connected, so the UI gives no signal and the client keeps refreshing on every call (issue anomalyco#53942).

callTool now detects auth failures (an SDK UnauthorizedError, or a plain Error whose message carries HTTP 401 / Unauthorized) and stops the server, marks it needs_auth, and publishes StatusChanged, so the rejected tools stop being surfaced and the refresh loop breaks. Re-auth from /mcps restores it; credential reconnects already restart needs_auth servers.

Fixes anomalyco#53942
Replace the regex match on error message text with a precise check: only a
real HTTP 401 (SdkHttpError.status === 401) or the SDK's UnauthorizedError
surfaces as MCP.NeedsAuthError and takes a server into needs_auth. Messages
from other APIs that merely echo "401"/"unauthorized" no longer tear a
server down. Teardown now runs under the server lock and only stops a still
live connection, and the tool call error is no longer double-wrapped.
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@potoior

potoior commented Oct 13, 2026

Copy link
Copy Markdown
Author

Hi @Hona @thdxr — this PR is ready for review. CI is green (check-compliance, check-standards, check-duplicates all pass) and it is linked to issue #53942. Could you take a look / approve when you have a moment? Thanks!

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works. I ran OpenCode from source against a local MCP server that answers tools/call with 401. On the base, the call fails and /mcps still says Connected. With this PR, the call reports that authentication is required and /mcps shows Sign in required. mcp.test.ts and the packages/core typecheck pass, apart from one test that fails on the base too. The SDK throws the issue's "401 after re-authentication" error as an SdkHttpError with status 401, which the new check catches.

Public API: unchanged.

What I'd fix before merging:

  • The handler doesn't check that the 401 came from the connection that's live now, and it sets the status outside the lock (see inline).
  • The two "does not mark needs_auth" tests add little. Detection now looks at the HTTP status, so the "body echoes 401" test can't catch a regression. I'd keep just the 401 test so the change stays small.

Related: #53804, #49773

Before

After

Effect.gen(function* () {
// Teardown serializes with other lifecycle ops and only stops a server that is still live,
// mirroring recover(), in case the connection was replaced or stopped while the call ran.
yield* Effect.gen(function* () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't check that the 401 came from the connection that's live now:

  • target.entry.client == null only checks that some client exists. If the server reconnected while this call was running (for example, the user just re-authenticated and reconnect started a fresh connection), a late 401 from the old connection stops the new one.
  • The needs_auth assignment and the StatusChanged publish below run outside the lock, and they also run when the guard returned early. That can overwrite disabled, or a newer connected status.

I'd capture the connection the call ran on and do everything inside the lock only when entry.client === connection: stop the server, set the status and publish. That's the same check recover() and whenLive() use. Note that recovering() may swap the connection after a session expiry, so the live connection isn't necessarily the one you started with.

…tion

Capture the connection the call ran on and, on a NeedsAuthError, stop the
server, set the status and publish only while holding the lock and when
entry.client is still that exact connection. This mirrors recover()/whenLive()
so a late 401 from a connection that was replaced (e.g. during re-auth) can't
tear down the newer connection or overwrite a newer disabled/connected status.
@potoior

potoior commented Oct 13, 2026

Copy link
Copy Markdown
Author

Addressed the review feedback: the MCP auth handling now captures the connection the call actually ran on and, on a NeedsAuthError, only stops the server / sets needs_auth status / publishes while holding the lock and when entry.client is still that exact connection. This mirrors recover()/whenLive() so a late 401 from a connection that was replaced during re-auth can't tear down the newer connection or overwrite a newer status.

@booi

booi commented Oct 24, 2026

Copy link
Copy Markdown

Would love to see this merged. I think this blocks Google Workspace MCPs which i think is a fairly high priority set of MCPs.

Baha-Abdelatif added a commit to Baha-Abdelatif/opencode that referenced this pull request Nov 10, 2026
Adapted from anomalyco#55013 to the 2.2.7 connection module. A real HTTP 401 or SDK UnauthorizedError during a tool call surfaces as NeedsAuthError; the server is stopped and marked needs_auth (only if still live on the connection the call ran on), so the UI offers a login instead of every call failing (anomalyco#53942). Errors that merely mention 401 do not count.
@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: server stays "connected" while tool calls fail with 401 after re-authentication

2 participants