Skip to content

fix(mcp): boot stdio server when invoked via cavemem mcp#16

Open
LoganBresnahan wants to merge 1 commit intoJuliusBrussee:mainfrom
LoganBresnahan:fix/mcp-cli-import
Open

fix(mcp): boot stdio server when invoked via cavemem mcp#16
LoganBresnahan wants to merge 1 commit intoJuliusBrussee:mainfrom
LoganBresnahan:fix/mcp-cli-import

Conversation

@LoganBresnahan
Copy link
Copy Markdown

@LoganBresnahan LoganBresnahan commented Apr 27, 2026

Summary

  • cavemem mcp exits silently without ever connecting an MCP stdio transport.
  • Root cause: the CLI's mcp command does await import('@cavemem/mcp-server') expecting the import side-effect to start the server, but main() is guarded behind isMainEntry(), which returns false for dynamic imports.
  • Fix: export main() and call it explicitly from the CLI.

Repro

# Expected: stays running, serves JSON-RPC over stdio.
# Actual:   exits 0 immediately, transport never connects.
cavemem mcp

Or, with an explicit MCP initialize handshake:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}' \
  | cavemem mcp
# (no response — server never started)

Any host that connects to cavemem mcp over the standard MCP stdio transport sees zero tools.

Root cause

apps/mcp-server/src/server.ts guards main() behind an isMainEntry() check:

if (isMainEntry()) {
  main().catch(...);
}

function isMainEntry(): boolean {
  // import.meta.url = .../mcp-server/dist/server.js
  // process.argv[1]  = .../cli/dist/index.js  (when spawned via `cavemem mcp`)
  return import.meta.url === pathToFileURL(realpathSync(argv)).href;
}

apps/cli/src/commands/mcp.ts (before this PR):

// Delegate: importing runs main() via the server module.
await import('@cavemem/mcp-server');

The comment is wrong — dynamic import never runs main() because isMainEntry() returns false when the module is loaded through the CLI bundle. As a result, the stdio transport is never connected.

Fix

Export main from the server module and have the CLI call it explicitly. The isMainEntry() guard is preserved so the cavemem-mcp bin (direct invocation) still works.

Changes

  • apps/mcp-server/src/server.ts: async function mainexport async function main
  • apps/cli/src/commands/mcp.ts: import { main } and call it
  • apps/mcp-server/test/exports.test.ts: regression test pinning the main export
  • .changeset/fix-mcp-cli-import.md: patch changeset for cavemem + @cavemem/mcp-server

Test plan

  • pnpm typecheck passes
  • pnpm lint (biome) passes
  • pnpm build produces a cavemem dist whose dist/index.js calls await import('./server-XXXX.js').main() and whose server chunk emits export { buildServer, main }
  • apps/mcp-server tests pass (8 incl. new export pin)
  • apps/cli tests pass (4)
  • After packing + global install, cavemem mcp responds to a JSON-RPC initialize request with a valid handshake (server stays alive, lists 4 tools)
  • cavemem-mcp bin still works when invoked directly (regression check on the isMainEntry() path)

The CLI's mcp subcommand did 'await import(@cavemem/mcp-server)' expecting
the import side-effect to start the server, but the server module guards
main() behind an isMainEntry() check. When dynamically imported,
import.meta.url does not match process.argv[1] (the CLI), so main() never
ran and no MCP tools were exposed to the host IDE.

Export main() from the server module and have the CLI call it explicitly.
The isMainEntry() guard remains so the cavemem-mcp bin still works when
invoked directly. Adds a regression test pinning the main export.
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.

1 participant