Skip to content

fix(mcp): stub unimplemented capability handlers & auto-connect on stdio startup#66

Merged
thinhntq merged 1 commit into
gridex:mainfrom
nguyen2887:fix/mcp-stdio-stub-handlers-and-auto-connect
May 6, 2026
Merged

fix(mcp): stub unimplemented capability handlers & auto-connect on stdio startup#66
thinhntq merged 1 commit into
gridex:mainfrom
nguyen2887:fix/mcp-stdio-stub-handlers-and-auto-connect

Conversation

@nguyen2887
Copy link
Copy Markdown
Contributor

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:

  1. initialize advertises capabilities the dispatcher does not handle. prompts, resources (with subscribe), and resources.listChanged are all set to true in the InitializeResult, but MCPServer.handleRequest has no case for prompts/list, resources/list, or resources/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 even tools/list, which works perfectly, becomes unreachable from the client's POV.
  2. Stdio mode never opens DB connections. runMCPServerAsync builds a fresh ConnectionManager, calls connectionRepository.fetchAll() only to register MCP modes, and starts the server. Because --mcp-stdio runs in a separate process from the GUI, no live adapters are inherited. Any tool routed through MCPToolContext.getAdapter (list_tables, query, get_sample_rows, list_relationships, describe_table, list_schemas, explain_query, all Tier-3 writes) returns Connection '<uuid>' not found. even though list_connections reports the config as available.

Changes

  • macos/Services/MCP/MCPServer.swift
    • Handle prompts/list{"prompts": []}
    • Handle resources/list{"resources": []}
    • Handle resources/templates/list{"resourceTemplates": []}
    • Accept both initialized and notifications/initialized (the spec uses the namespaced form; some clients still send the bare form).
  • macos/App/Lifecycle/main.swift
    • In stdio mode, instantiate a KeychainService and, for every non-locked connection, load db.password.<uuid> (and ssh.password.<uuid> when an SSH config is present) and call connectionManager.connect(...).
    • Failures are logged to stderr but do not block server startup, so a single bad-password connection does not prevent the others from being reachable.

The empty-array stubs preserve the advertised capabilities so existing clients keep their behavior; if prompts / resources are 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

  • Probe directly via stdio: initialize + notifications/initialized + prompts/list + resources/list + resources/templates/list + tools/list — all return success on the new build.
  • Real Claude Code (2.1.118) handshake — server stays connected, all 13 tools surfaced.
  • list_connections reports is_connected: true immediately after Claude Code spawns the stdio process (no GUI interaction needed).
  • list_schemas, list_tables, and a parameterless query execute end-to-end against a live PostgreSQL 16 DB.
  • Verified only against PostgreSQL on macOS arm64 (15.7.5). MySQL / SQLite / Redis / MongoDB / MSSQL paths share the same connectionManager.connect entrypoint, so they should benefit identically, but I have not exercised them locally.

Notes for reviewers

  • I left subscribe: true in resources capabilities untouched. With an empty resources/list, no client should ever call resources/subscribe, but if you'd rather have it advertised honestly we can drop it.
  • --mcp-stdio previously 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 real connect() per process spawn, but that's the only way the MCP tools can actually function without the GUI also being running.

…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 thinhntq self-requested a review May 6, 2026 09:44
@thinhntq thinhntq self-assigned this May 6, 2026
Copy link
Copy Markdown
Contributor

@thinhntq thinhntq left a comment

Choose a reason for hiding this comment

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

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

  1. Sequential auto-connect makes stdio cold-start time = Σ connect_time(i). With my 10 non-locked connections, the initialize response 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 ~5s timeout 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.

  2. subscribe: true is now dishonest. You already flagged this in the PR notes. Since resources/list is permanently empty, no spec-compliant client should ever call resources/subscribe, but it's still advertised. I'd lean toward dropping subscribe from the capabilities object in handleInitialize — 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.

@thinhntq thinhntq merged commit 177927e into gridex:main May 6, 2026
1 check passed
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