Skip to content

fix: remote skill install on expired token + MCP unreachable empty response#128

Merged
Minitour merged 5 commits into
developfrom
fix/remote-skill-and-mcp-unreachable
Jun 28, 2026
Merged

fix: remote skill install on expired token + MCP unreachable empty response#128
Minitour merged 5 commits into
developfrom
fix/remote-skill-and-mcp-unreachable

Conversation

@Minitour

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes [Bug] Cannot install a skill from a remote URL #127 — Installing a type: remote skill from a public URL (e.g. raw.githubusercontent.com) failed with "Git integration token has expired" when the user had a stored-but-expired GitHub token. AuthenticatedFetch.ensureFreshIntegration now returns null instead of throwing when the token cannot be refreshed, so the request proceeds unauthenticated. Genuinely private repos still get a 401/403, which the existing handler already converts into a friendly re-auth prompt.

  • Fixes [Bug] When MCP server is unreachable, the local capa server returns an empty response (error) #126 — The local capa server could return net::ERR_EMPTY_RESPONSE (empty TCP response) when an MCP server was unreachable. Three layers of protection were added:

    1. Top-level try/catch in handleRequest — no request can ever close the connection without an HTTP response.
    2. handleGetOAuth2Servers wrapped in try/catch (whole handler + per-server detection loop), mirroring the existing pattern in the configure endpoint.
    3. Timeouts on all outbound network calls: 10 s AbortSignal.timeout on OAuth detection fetches in OAuth2Manager, and a 15 s Promise.race on client.connect() in MCPProxy, so a hung/unreachable server fails fast rather than stalling indefinitely.

Test plan

  • Updated AuthenticatedFetch tests: expired-token case now asserts unauthenticated fetch proceeds (not a throw); refresh-failure-classification tests updated accordingly.
  • Added OAuth2Manager.detectOAuth2Requirement tests: returns null on AbortError (unreachable server) and on non-401 response.
  • Full suite: 1104 pass, 0 fail.

…erver against empty responses

Fixes #127 — remote skills whose URL is on a known git host (e.g.
raw.githubusercontent.com) could not be installed when the user had a
stored-but-expired GitHub token. AuthenticatedFetch now returns null
from ensureFreshIntegration instead of throwing, so public URLs proceed
unauthenticated; private URLs still surface a 401/403 which the existing
handler converts into the friendly re-auth prompt.

Fixes #126 — the local capa server could return net::ERR_EMPTY_RESPONSE
when an MCP server was unreachable:
- Added a top-level try/catch in handleRequest so no request can ever
  close the connection without sending an HTTP response.
- Wrapped handleGetOAuth2Servers (whole handler + per-server OAuth
  detection loop) in try/catch, mirroring the pattern used by the
  configure endpoint.
- Added OAUTH_DETECT_TIMEOUT_MS (10 s) to all outbound fetches in
  OAuth2Manager.detectOAuth2Requirement so a hung server fails fast.
- Added MCP_CONNECT_TIMEOUT_MS (15 s) race in MCPProxy.createHttpClient
  and createStdioClient so a non-responsive MCP server does not stall
  indefinitely.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Minitour Minitour changed the base branch from develop to main June 28, 2026 19:08
@Minitour Minitour changed the base branch from main to develop June 28, 2026 19:08
Minitour and others added 2 commits June 28, 2026 22:11
… fetch

Co-authored-by: Cursor <cursoragent@cursor.com>
…osed errors

Follow-up to the #126 work, addressing two remaining symptoms when an MCP
server is unreachable (e.g. VPN dropped):

1. Stray "The socket connection was closed unexpectedly" printed during
   install even though it succeeded. The connect-timeout added previously
   used a Promise.race that left the losing promise dangling: when the
   socket closed after we'd timed out (or the timer fired after a
   successful connect), the rejection surfaced as an unhandled rejection.
   Replaced it with connectWithTimeout(), which attaches a no-op catch to
   the connect promise and clears the timer in finally. Added process-level
   unhandledRejection/uncaughtException handlers as a safety net for the MCP
   SDK transport's background sockets.

2. The /tools endpoint returned 200 { tools: [] } for unreachable servers,
   so the UI could not tell "no tools" apart from "unreachable". listServerTools
   now accepts options and handleGetServerTools calls it with throwOnError,
   returning 502 with a clear message ("Server unreachable: ..."; the
   OAuth-disconnected message is passed through untouched). The web UI tools
   panel now renders that error, and useServerTools no longer retries so the
   error shows immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/server/index.ts Fixed
Minitour and others added 2 commits June 28, 2026 22:52
… rejections

Half-open MCP servers (accept the TCP connection but never complete the
handshake, e.g. return an empty response) made client.connect() hang until
our timeout fired. Two edge cases could still leak a stray "MCP connect
timed out after 15000ms" unhandled rejection out to the process/UI:

- If client.connect() threw synchronously, the timeout timer was never
  cleared and rejected ~15s later into the void.
- On timeout we left the half-open client/transport open, so its socket
  could emit further late errors.

connectWithTimeout now: builds the timer only after connect starts, always
clears it in finally (covering synchronous throws), best-effort closes the
client + transport on timeout, and keeps the no-op catch on the connect
promise so a late socket-close rejection stays handled. The timeout is now
parameterized so it can be unit-tested with a short window.

Adds tests asserting the timeout path tears down the connection and never
produces an unhandled rejection across the timeout, late-rejection,
success, and synchronous-throw cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
… text

CodeQL (js/stack-trace-exposure) flagged the /tools handler returning raw
error.message to the client, which can leak stack-trace-derived detail.

Both handleGetServerTools and the handleGetOAuth2Servers catch now log the
full detail server-side but return controlled, generic messages: tools
failures report "Server unreachable: \"<id>\" could not be contacted" (or an
auth-required prompt for OAuth-disconnected servers), and the oauth-servers
handler returns a generic "Failed to load OAuth2 servers". The UI still
shows a clear, actionable error without exposing internal exception text.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Minitour Minitour merged commit 84b6075 into develop Jun 28, 2026
7 checks passed
@Minitour Minitour deleted the fix/remote-skill-and-mcp-unreachable branch June 28, 2026 20:13
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.

[Bug] Cannot install a skill from a remote URL [Bug] When MCP server is unreachable, the local capa server returns an empty response (error)

2 participants