Fix false positive 'Needs Login' for connected MCP servers#2931
Fix false positive 'Needs Login' for connected MCP servers#2931omar-inkeep merged 2 commits intomainfrom
Conversation
Transient server errors (e.g. Linear returning 500) were misclassified as auth failures because detectAuthenticationRequired() only checked whether the server supports OAuth, not whether a credential already existed. Introduces three-tier error classification in dbResultToMcpTool: 1. Transient errors (timeout, 5xx, network) → 'unavailable' 2. Auth errors (401, 403, UnauthorizedError) → 'needs_auth' 3. Other errors + credential exists → 'unavailable' (not auth failure) 4. Other errors + no credential → probe OAuth metadata Also expands isTimeoutOrConnectionError() to cover McpError InternalError, StreamableHTTPError 5xx, ECONNREFUSED, ENOTFOUND, and other network codes. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
🦋 Changeset detectedLatest commit: 8fe313d The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
TL;DR — Fixes false positive "Needs Login" status on MCP server detail pages by introducing a three-tier error classification. Transient server errors (5xx, timeouts, network failures) and unknown errors when a valid credential exists now resolve to Key changes
Summary | 3 files | 1 commit | base: Three-tier error classification for MCP server status
The new The catch block in
|
There was a problem hiding this comment.
Solid fix for a real UX problem. The three-tier classification logic is well-structured and the test coverage is thorough. One correctness concern about ErrorCode.InternalError being overly broad, and a minor observation about the string-matching heuristic in isAuthenticationError.
Claude Opus | 𝕏
| return ( | ||
| error.code === ErrorCode.RequestTimeout || | ||
| error.code === ErrorCode.ConnectionClosed || | ||
| error.code === ErrorCode.InternalError |
There was a problem hiding this comment.
ErrorCode.InternalError (-32603) is the standard JSON-RPC "Internal error" code. MCP servers can return this for any server-side bug — not just transient/connectivity issues. Classifying it unconditionally as a timeout/connection error means a server that returns -32603 for a persistent misconfiguration will be shown as "Unavailable" forever instead of "Unhealthy".
This is the right pragmatic tradeoff for the false-positive fix (better to show "Unavailable" than "Needs Login"), but it's worth noting that the function name isTimeoutOrConnectionError is now a bit misleading — it's really isTransientOrServerError. Consider renaming or adding a brief inline note about why InternalError is included.
| } | ||
|
|
||
| if (error instanceof StreamableHTTPError) { | ||
| return error.code !== undefined && error.code >= 500; |
There was a problem hiding this comment.
Nit: StreamableHTTPError.code is typed number | undefined. The guard error.code !== undefined && error.code >= 500 is correct. However, the SDK also uses code -1 for unexpected content types — that would not match >= 500, which is the right behavior. Just calling this out as already-correct.
| if ( | ||
| cause?.code === 'ETIMEDOUT' || | ||
| cause?.code === 'ECONNABORTED' || | ||
| cause?.code === 'ECONNRESET' | ||
| message.includes('unauthorized') || | ||
| message.includes('forbidden') || | ||
| message.includes('invalid_token') || | ||
| message.includes('token expired') || | ||
| message.includes('invalid_grant') |
There was a problem hiding this comment.
The string-matching heuristic in isAuthenticationError is inherently fragile — any Error whose message happens to contain "unauthorized" or "forbidden" (e.g., "Forbidden by CORS policy", "unauthorized field access in GraphQL schema") would be misclassified as an auth error. This isn't new to this PR (the previous code had similar heuristics), but worth noting that the structured checks (instanceof UnauthorizedError, StreamableHTTPError status codes) should catch the vast majority of real auth errors. The string matching is a fallback for non-MCP-SDK errors — consider whether it's actually needed now that the structured checks exist.
| }); | ||
|
|
||
| it('should set status to needs_auth when no credential and OAuth is detected', async () => { | ||
| mockMcpConnect.mockRejectedValue(new Error('Connection refused')); |
There was a problem hiding this comment.
This test injects new Error('Connection refused') which does NOT match isTimeoutOrConnectionError (the string check looks for 'timed out', 'timeout', 'fetch failed', and the cause code check is not triggered since there's no .cause). So it falls through to the else branch and hits detectAuthenticationRequired. The test name says "no credential and OAuth is detected" which is correct, but the error message 'Connection refused' is slightly misleading — it might suggest this would be caught as a transient error. Consider using a more neutral error like new Error('unknown issue') to make it clearer this exercises the fallthrough path.
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
This is a well-designed fix that correctly addresses the false positive "Needs Login" problem. The three-tier error classification logic is sound:
- Transient errors (timeouts, 5xx, network codes) →
unavailable - Auth errors (401/403, UnauthorizedError) →
needs_auth - Unknown errors with existing credential →
unavailable(core fix)
The implementation is clean, the error handling is explicit with appropriate logging, and the 9 new unit tests provide good coverage of the classification branches.
💭 Consider (4) 💭
💭 1) tools.ts:78-88 Consolidate transient network error codes
Issue: The inline transientCodes array duplicates most of RETRYABLE_NODE_ERROR_CODES from retry/retryable-errors.ts (7 of 8 codes overlap).
Why: Two parallel definitions of transient network codes can drift independently, making maintenance harder.
Fix: Consider importing RETRYABLE_NODE_ERROR_CODES and extending it with the additional codes (ECONNABORTED, ENOTFOUND) needed for MCP-specific detection.
Refs: retryable-errors.ts:31-39
💭 2) dbResultToMcpTool.test.ts Test remaining McpError codes
Issue: isTimeoutOrConnectionError handles RequestTimeout, ConnectionClosed, and InternalError, but only InternalError is tested.
Why: The other codes would catch regressions in timeout/connection handling.
Fix: Optionally add tests for McpError(ErrorCode.RequestTimeout) and McpError(ErrorCode.ConnectionClosed).
Refs: tools.ts:54-60
💭 3) dbResultToMcpTool.test.ts Test message-based auth error detection
Issue: isAuthenticationError checks for 'invalid_token', 'token expired', 'invalid_grant' in error messages, but these patterns aren't tested.
Why: OAuth providers may return these patterns when tokens expire; testing ensures correct needs_auth classification.
Fix: Consider adding a test like mockMcpConnect.mockRejectedValue(new Error('OAuth error: invalid_grant')).
Refs: tools.ts:109-119
💭 4) dbResultToMcpTool.test.ts Additional network error code coverage
Issue: Only ECONNREFUSED is tested among 8 transient codes.
Why: Adding ENOTFOUND (DNS failure) would increase confidence in the cause.code branch, though the logic is identical for all codes.
Fix: Low priority — optionally add one more network code test.
Refs: tools.ts:78-90
✅ APPROVE
Summary: This is a clean, well-tested fix. The three-tier classification correctly prevents false "Needs Login" prompts when users have valid credentials but the MCP server returns transient errors. The suggestions above are minor enhancements for test coverage and code consistency — none are blocking. 🎉
Reviewers (6)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-tests |
3 | 0 | 3 | 0 | 0 | 0 | 0 |
pr-review-consistency |
1 | 0 | 1 | 0 | 0 | 0 | 0 |
pr-review-standards |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-errors |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-types |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
pr-review-product |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Total | 4 | 0 | 4 | 0 | 0 | 0 | 0 |
Ito Test Report ✅10 test cases ran. 10 passed. The unified run passed all 10 of 10 test cases with zero failures, confirming expected MCP detail/list behavior, create flow behavior, security handling, and permission boundaries. Most importantly, transient credentialed failures consistently rendered as Unavailable (not Needs Login), explicit or missing-auth states correctly showed Needs Login with Connect (including single OAuth start on rapid clicks), loading states converged to Unavailable without regression, user-scoped custom server creation preserved stable Needs Login on first load/reload, XSS and unsafe URL inputs were safely blocked or rendered inert, and the view-only authorization concern was validated as a permission-simulation artifact rather than a confirmed product defect. ✅ Passed (10)Commit: Tell us how we did: Give Ito Feedback |
Tools with fake/unreachable URLs now correctly classify as 'unavailable' (transient) instead of 'unhealthy' due to expanded transient error detection. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
PR Review Summary
(0) Total Issues | Risk: Low
Delta Review
This is a re-review scoped to changes since the last automated review.
Commits since last review: 1
8fe313d96— Update integration tests: unreachable MCP servers are now 'unavailable'
Changes reviewed:
agents-api/src/__tests__/manage/routes/crud/tools.test.ts(+4/-4)
The delta updates integration test assertions from unhealthy → unavailable across 4 test cases:
- Filter tools by status query parameter
- Get tool by ID
- Create new tool
This is a straightforward test alignment with the core fix behavior — transient/unreachable MCP servers now correctly return unavailable status instead of unhealthy, preventing false "Needs Login" prompts.
No new findings — the test changes accurately reflect the intended behavior change.
✅ APPROVE
Summary: Delta LGTM. The integration test updates correctly align with the three-tier error classification implemented in the core fix. Combined with the prior review's approval, this PR is ready to merge. 🚀
Reviewers (0)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
| Total | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Note: Delta was trivial (test expectation updates only) — no sub-reviewers dispatched.
|
No documentation changes needed for this PR. The fix improves the internal accuracy of status classification (transient errors → |
Ito Test Report ✅13 test cases ran. 2 additional findings, 11 passed. Overall, 11 of 13 test cases passed, confirming that MCP status and auth-state handling is largely correct across list/details, navigation refresh loops, mobile viewport, deep-link stale-state scenarios, and API boundary checks (including query tampering rejection and unauthorized tenant/project access protection), with transient failures on already-credentialed tools consistently shown as Unavailable and true auth failures (401/403 or OAuth-required without credentials) shown as Needs Login with expected Connect behavior. The two confirmed defects were a high-severity race where rapid repeated Connect clicks can start duplicate OAuth launches before the disabled/loading state applies, and a medium-severity Composio error-handling issue where auth-check service failures can be misclassified as Needs Login instead of Unavailable, leading to unnecessary reconnect prompts. ✅ Passed (11)ℹ️ Additional Findings (2)
|
* Fix false positive 'Needs Login' for MCP servers with valid credentials Transient server errors (e.g. Linear returning 500) were misclassified as auth failures because detectAuthenticationRequired() only checked whether the server supports OAuth, not whether a credential already existed. Introduces three-tier error classification in dbResultToMcpTool: 1. Transient errors (timeout, 5xx, network) → 'unavailable' 2. Auth errors (401, 403, UnauthorizedError) → 'needs_auth' 3. Other errors + credential exists → 'unavailable' (not auth failure) 4. Other errors + no credential → probe OAuth metadata Also expands isTimeoutOrConnectionError() to cover McpError InternalError, StreamableHTTPError 5xx, ECONNREFUSED, ENOTFOUND, and other network codes. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * Update integration tests: unreachable MCP servers are now 'unavailable' Tools with fake/unreachable URLs now correctly classify as 'unavailable' (transient) instead of 'unhealthy' due to expanded transient error detection. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Summary
unavailable, auth errors (401/403) →needs_auth, unknown errors with existing credential →unavailable(notneeds_auth)isTimeoutOrConnectionError()to coverMcpError(InternalError),StreamableHTTPError5xx,ECONNREFUSED,ENOTFOUND, and other network codesTest plan
McpError(500)indiscoverToolsFromServerfor a tool with a valid credential → confirm UI shows "Unavailable" instead of "Needs Login"🤖 Generated with Claude Code