feat(mcp): stdio MCP server that starts with nothing - #121
Open
nbkdoesntknowcoding wants to merge 1 commit into
Open
feat(mcp): stdio MCP server that starts with nothing#121nbkdoesntknowcoding wants to merge 1 commit into
nbkdoesntknowcoding wants to merge 1 commit into
Conversation
An agent should be able to discover what a server can do before it
authenticates. Today it cannot: the only MCP endpoint is streamable-HTTP behind
OAuth 2.1, so `tools/list` is a 401, and a directory crawler in a bare container
sees a server that refuses to introduce itself.
The Fastify API cannot serve that discovery, because importing
apps/api/src/mcp/server.ts reaches, before any transport exists:
mcp/server.ts -> mcp/auth.ts -> config/env.ts
process.exit(1) on the first missing variable
mcp/server.ts -> mcp/tools/index.ts -> mcp/tools/record-decision.ts
-> lib/decisions.ts -> queue/embeddings.ts ioredis dials on import
mcp/server.ts -> mcp/tools/dev/index.ts -> db/index.ts postgres.js pool
(Three eager ioredis clients in total: queue/embeddings.ts, queue/pdf-generation.ts,
mcp/tools/query-embedding.ts. Observed as three TCP connects 1.3s into a bare
import, followed by an unhandled ioredis error event.)
So packages/mcp-stdio imports none of it:
initialize / tools/list answered offline from tools.generated.json, captured
at build time from the real createMcpServer() over an
in-memory transport
tools/call forwarded over HTTPS to a workspace
Everything needing a service is on the CALL path, established on first use. A
missing dependency fails one call with a stated cause and a reason code on
stderr (stdio.not_configured, stdio.upstream_unreachable, stdio.upstream_http_error,
stdio.upstream_bad_payload, stdio.tool_error) — never a boot failure, never a
silent catch.
A build-time capture is a second copy of the truth, and second copies rot, so
mcp-stdio-manifest.test.ts regenerates and diffs it. __ui_probe is excluded from
the manifest: it is a development probe registered unconditionally in
mcp/server.ts and live in the hosted tools/list, and it should not appear in a
public directory listing. Left registered upstream — removing it is a separate
decision.
Dockerfile at the repo root builds only this package (its one dependency is the
MCP SDK, not the ~90 production deps of apps/api), installs with a frozen
lockfile and --ignore-scripts so nothing reaches the network mid-install, and
runs as an unprivileged user with no port and no healthcheck — a probe writing
to stdout would corrupt the JSON-RPC stream. node:22-slim rather than
node:20-slim: package.json declares engines.node >= 22.
Also fills in root package.json description/keywords/repository/license/homepage,
which were absent.
Verified with no environment at all (env -i, PATH only):
initialize -> serverInfo {"name":"mnema","title":"Mnema","version":"1.0.0"}
tools/list -> 45 tools
exit code 0
Re-run from a tree mirroring the image layout (source removed, pnpm symlinks
preserved) with the same result. `docker build` itself is NOT yet run — no
container runtime on the authoring machine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An agent should be able to discover what a server can do before it authenticates. Today it cannot — the only MCP endpoint is streamable-HTTP behind OAuth 2.1, so
tools/listfrom a bare container is a 401.Why this can't live in the API
Importing
apps/api/src/mcp/server.tsreaches all of this before any transport exists:apps/api/src/config/env.tsprocess.exit(1)on the first missing variableapps/api/src/queue/embeddings.ts:25apps/api/src/queue/pdf-generation.ts:16apps/api/src/mcp/tools/query-embedding.ts:28apps/api/src/db/index.ts:6postgres.jspoolReached by:
Measured, not assumed: a bare
importof that module with no env exits 1 listing 14 required variables; with stub env it opens three TCP connections to :6379 1.3 s in and emits an unhandled ioredis error.What this adds
packages/mcp-stdioimports none of the above.initialize/tools/list— served offline fromtools.generated.json, captured at build time from the realcreateMcpServer()over an in-memory transport.tools/call— forwarded over HTTPS to a workspace (MNEMA_API_URL+MNEMA_API_KEY).Everything needing a service is on the call path, established on first use. A missing dependency fails one call with a stated cause and a reason code on stderr —
stdio.not_configured,stdio.upstream_unreachable,stdio.upstream_http_error,stdio.upstream_bad_payload,stdio.tool_error— never a boot failure and never a silent catch.A build-time capture is a second copy of the truth, so
apps/api/src/tests/mcp-stdio-manifest.test.tsregenerates it and fails on drift (pnpm --filter @boppl/api mcp:manifestto refresh).__ui_probeis excluded from the manifest — it is a development probe, registered unconditionally inmcp/server.tsand therefore live in the hostedtools/list, and it should not appear in a public directory listing. Left registered upstream; removing it is a separate decision.Dockerfile
Root
Dockerfile, multi-stage. Builds only this package — its single dependency is the MCP SDK, not the ~90 production deps ofapps/api.--frozen-lockfile --ignore-scriptsso nothing reaches the network mid-install; unprivileged user; noEXPOSE, noHEALTHCHECK(a probe writing to stdout would corrupt the JSON-RPC stream).node:22-slim, notnode:20-slim—package.jsondeclaresengines.node >= 22and.nvmrcpins 22.Verified
With no environment at all (
env -i, PATH only):Re-run from a tree mirroring the image layout (source directory removed, pnpm symlinks preserved) — identical result. Also:
pnpm --filter @boppl/mcp-stdiobuild/typecheck/lint clean,pnpm --filter @boppl/api buildclean, manifest drift test 3/3.Not verified:
docker buildhas not been run — there is no container runtime on the authoring machine. The image needs one real build before this is relied on.🤖 Generated with Claude Code