fix(mcp): stub unimplemented capability handlers & auto-connect on stdio startup#66
Conversation
…dio startup The macOS MCP server advertises `prompts` and `resources` capabilities during `initialize`, but the request dispatcher never handled `prompts/list`, `resources/list`, or `resources/templates/list` — each fell through to the default branch and returned `-32601 Method not found`. Strict MCP clients (e.g. Claude Code) treat that as a fatal protocol violation and tear down the stdio pipe immediately after init, so the full tool surface was unreachable despite tools/list itself working. Additionally, `runMCPServerAsync` constructed a fresh `ConnectionManager`, registered MCP modes, and started the server without ever opening a single DB connection. Because `--mcp-stdio` runs in a separate process from the GUI, no live adapters are inherited, so any tool that goes through `MCPToolContext.getAdapter` (`list_tables`, `query`, `get_sample_rows`, …) returned `Connection '<uuid>' not found.` even though `list_connections` happily reported the config. Changes: - MCPServer.swift: handle `prompts/list`, `resources/list`, and `resources/templates/list` with empty-array results; alias `notifications/initialized` with the legacy `initialized` notification name. - main.swift: in stdio mode, instantiate a `KeychainService`, then for every non-locked connection, load its DB password (and SSH password if applicable) and call `connectionManager.connect(...)`. Failures are logged to stderr but do not block server startup. Verified on macOS 15.7.5 (arm64) against PostgreSQL 16: full Claude Code MCP handshake succeeds and `list_tables` / `list_schemas` / `query` execute end-to-end against a live DB.
thinhntq
left a comment
There was a problem hiding this comment.
Thanks @nguyen2887 — the diagnosis is spot-on and the fix is the right shape. I pulled the branch, rebuilt the .app against my local SwiftData store (10 non-locked connections, mix of local PG / remote PG / SQLite), and walked the full handshake.
Verified behaviour
| Method | Result |
|---|---|
initialize |
200, advertises tools/resources/prompts/logging |
notifications/initialized |
no response (per spec) |
prompts/list |
{prompts: []} |
resources/list |
{resources: []} |
resources/templates/list |
{resourceTemplates: []} |
tools/list |
13 tools |
ping |
pong: true |
tools/call list_connections |
is_connected: "true" on both SQLite-local and a remote Postgres entry |
| stderr | clean — no spurious warnings |
So both bugs in the PR description reproduce against my saved-connection set, and both fixes work end-to-end. Build is clean (no new warnings vs main).
Two notes — one follow-up suggestion, one minor
-
Sequential auto-connect makes stdio cold-start time =
Σ connect_time(i). With my 10 non-locked connections, theinitializeresponse landed in ~5–15s; with 21 connections including dead remotes the wall time would balloon (TCP timeout on macOS is ~75s and there's no per-connection timeout in this loop). Every Claude Code spawn pays the full reconnect cost. Not blocking for this PR, but I think we'll want a follow-up that:- parallelises with
withTaskGroup, and - wraps each
connectionManager.connect(...)in a~5stimeout so a single dead host can't serial-stall the whole MCP startup.
Happy to take that one if you want to keep this PR scoped.
- parallelises with
-
subscribe: trueis now dishonest. You already flagged this in the PR notes. Sinceresources/listis permanently empty, no spec-compliant client should ever callresources/subscribe, but it's still advertised. I'd lean toward droppingsubscribefrom the capabilities object inhandleInitialize— that's a one-line follow-up, and it keeps the capability surface honest. Either approach is fine with me; whichever you prefer.
LGTM otherwise — approving.
Summary
The macOS MCP stdio server is currently broken for spec-strict clients like Claude Code. Two distinct bugs combine so that tools never actually run end-to-end:
initializeadvertises capabilities the dispatcher does not handle.prompts,resources(withsubscribe), andresources.listChangedare all set totruein theInitializeResult, butMCPServer.handleRequesthas no case forprompts/list,resources/list, orresources/templates/list. They fall through to the default branch and return-32601 Method not found. Claude Code treats unimplemented declared-capability methods as a fatal protocol error and tears the stdio pipe down immediately after init — so eventools/list, which works perfectly, becomes unreachable from the client's POV.runMCPServerAsyncbuilds a freshConnectionManager, callsconnectionRepository.fetchAll()only to register MCP modes, and starts the server. Because--mcp-stdioruns in a separate process from the GUI, no live adapters are inherited. Any tool routed throughMCPToolContext.getAdapter(list_tables,query,get_sample_rows,list_relationships,describe_table,list_schemas,explain_query, all Tier-3 writes) returnsConnection '<uuid>' not found.even thoughlist_connectionsreports the config as available.Changes
macos/Services/MCP/MCPServer.swiftprompts/list→{"prompts": []}resources/list→{"resources": []}resources/templates/list→{"resourceTemplates": []}initializedandnotifications/initialized(the spec uses the namespaced form; some clients still send the bare form).macos/App/Lifecycle/main.swiftKeychainServiceand, for every non-locked connection, loaddb.password.<uuid>(andssh.password.<uuid>when an SSH config is present) and callconnectionManager.connect(...).The empty-array stubs preserve the advertised capabilities so existing clients keep their behavior; if
prompts/resourcesare intended to remain unimplemented, the alternative would be removing them from the capabilities object — happy to switch to that approach if you prefer.Test plan
initialize+notifications/initialized+prompts/list+resources/list+resources/templates/list+tools/list— all return success on the new build.list_connectionsreportsis_connected: trueimmediately after Claude Code spawns the stdio process (no GUI interaction needed).list_schemas,list_tables, and a parameterlessqueryexecute end-to-end against a live PostgreSQL 16 DB.connectionManager.connectentrypoint, so they should benefit identically, but I have not exercised them locally.Notes for reviewers
subscribe: trueinresourcescapabilities untouched. With an emptyresources/list, no client should ever callresources/subscribe, but if you'd rather have it advertised honestly we can drop it.--mcp-stdiopreviously assumed connections would be opened "out of band". After this PR each stdio process opens its own adapters; that means it pays the cost of a realconnect()per process spawn, but that's the only way the MCP tools can actually function without the GUI also being running.