feat: add HTTP transport and standalone CLI#10
Conversation
Adds two ways to use the server beyond stdio, backported (and corrected) from the klikli-dev fork: - HTTP (Streamable) transport via --http/--port/--host. Uses a proper per-session transport map (genuinely concurrent), defaults host to 127.0.0.1 so the SDK enables DNS-rebinding protection, and exposes the endpoint at /mcp. Server construction is refactored into a per-session createServer() factory shared by stdio and HTTP. - minecraft-dev-cli: invokes the existing tools directly (no MCP client) for scripts/skills/automation. Flags-only args (--key value / --key=value) to avoid JSON-quoting pain in PowerShell and other shells. Java is only verified for real tool calls, so help/list-tools work without a JDK. Deps: add express + @types/express; raise @modelcontextprotocol/sdk floor to ^1.24.0 (createMcpExpressApp). New scripts: cli, dev:http. README documents both features (keeping existing centered formatting). Tests: - __tests__/tools/cli-args.test.ts: flag parsing unit tests - __tests__/tools/cli.e2e.test.ts: CLI process E2E (help/list-tools/errors) - __tests__/manual/mcp/http-server-smoke.test.ts: HTTP transport E2E (list tools, read resource, concurrent session isolation, bad-request) - helpers: cli-runner.ts, mcp-http.ts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- cli.ts: replace JSON.parse value-guessing with schema-driven coercion. String fields now keep their literal text, so `--version 1.20` stays "1.20" (was parsed to the number 1.2 and failed Zod) and `--query 42` stays "42". number/integer/boolean/array/object are coerced per the tool's inputSchema. A bare `--` now errors instead of producing an empty-string key. - index.ts: resolve the MCP session id from `mcp-session-id` header OR a `?sessionId=` query param (browser EventSource clients can't set headers), normalizing the query value into the header so the SDK transport's internal validation accepts it. Applied to POST and GET/DELETE handlers. - mcp-http.ts test helper: ignore child stdout (was piped but never consumed) so a full pipe buffer can't block the server process. Tests: schema-coercion unit cases (incl. version-string regression and bare `--`), plus an HTTP E2E asserting the `?sessionId=` query-param fallback works end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the Gemini review in f8782eb:
All green after the changes: default suite 24 files / 237 tests; HTTP smoke 6/6 (incl. the query-param fallback). |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a standalone CLI (minecraft-dev-cli) and an HTTP transport mode (--http) using Express and the MCP Streamable HTTP transport to allow concurrent client sessions. It also adds comprehensive E2E tests and updates the documentation. The review feedback suggests adding error handling to handleSessionRequest in src/index.ts to improve observability, and refining the CLI argument parser in src/cli.ts to correctly handle parameter values that start with --.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const handleSessionRequest = async (req: ExpressRequest, res: ExpressResponse) => { | ||
| const sessionId = resolveSessionId(req); | ||
| const transport = sessionId ? transports[sessionId] : undefined; | ||
| if (!transport) { | ||
| res.status(400).send('Invalid or missing session ID'); | ||
| return; | ||
| } | ||
| await transport.handleRequest(req, res); | ||
| }; |
There was a problem hiding this comment.
The handleSessionRequest function handles GET (SSE stream) and DELETE (session teardown) requests but lacks error handling around transport.handleRequest(req, res). If the transport throws or rejects during these operations, the error will not be logged via the application's logger, reducing observability. Wrapping this call in a try/catch block to log the error is recommended.
const handleSessionRequest = async (req: ExpressRequest, res: ExpressResponse) => {
const sessionId = resolveSessionId(req);
const transport = sessionId ? transports[sessionId] : undefined;
if (!transport) {
res.status(400).send('Invalid or missing session ID');
return;
}
try {
await transport.handleRequest(req, res);
} catch (error) {
logger.error('Error handling HTTP session request', error);
if (!res.headersSent) {
res.status(500).send('Internal Server Error');
}
}
};| } else { | ||
| key = arg.slice(2); | ||
| const nextArg = args[i + 1]; | ||
| if (nextArg && !nextArg.startsWith('--')) { | ||
| value = nextArg; | ||
| i++; | ||
| } | ||
| } |
There was a problem hiding this comment.
The argument parser currently treats any argument starting with -- as a new flag. This prevents passing parameter values that start with -- (such as negative numbers or search queries starting with double dashes like --help or --target) as space-separated arguments, parsing them as bare boolean flags instead. To fix this, we can check the schema to see if the current flag expects a value, and if so, consume the next argument even if it starts with --.
} else {
key = arg.slice(2);
const nextArg = args[i + 1];
const expectedType = properties?.[key]?.type;
const expectsValue = expectedType !== undefined && expectedType !== 'boolean';
if (nextArg && (expectsValue || !nextArg.startsWith('--'))) {
value = nextArg;
i++;
}
}The POST /mcp handler already logs and returns 500 on transport errors; mirror that in handleSessionRequest (GET SSE stream + DELETE teardown) so failures are logged and surfaced as 500 instead of going unhandled. Guards on res.headersSent since the SSE stream may have already started. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Backports two genuinely useful features from the klikli-dev fork — reimplemented and corrected for our repo. (The fork's skill, package rename, README de-centering, and bun lockfile were deliberately skipped.)
1. HTTP (Streamable) transport
Run the server over HTTP for clients/editors that can't use stdio:
minecraft-dev-mcp --http --port 3000 # endpoint: http://127.0.0.1:3000/mcpinitialize, torn down on close) — genuinely supports concurrent clients. The fork shared a single transport across all requests; this does not.127.0.0.1, so the SDK's DNS-rebinding protection turns on automatically.--host 0.0.0.0is opt-in (the fork defaulted to0.0.0.0).createServer()factory shared by stdio and HTTP.2. Standalone CLI (
minecraft-dev-cli)Invokes the existing tools directly — no MCP client — for scripts, skills, and automation:
--key value/--key=value) to avoid the JSON-quoting pain positional JSON arguments cause in PowerShell and other shells.help/list-toolswork without a JDK (fork required Java for everything).tools+handleToolCallexports — no tool-layer changes.Supporting changes
express+@types/express; raised@modelcontextprotocol/sdkfloor to^1.24.0(required bycreateMcpExpressApp).cli,dev:http. New bin:minecraft-dev-cli.Testing
npm test(default suite)New coverage:
__tests__/tools/cli-args.test.ts— flag-parsing unit tests__tests__/tools/cli.e2e.test.ts— CLI process E2E (help, list-tools, unknown-tool, positional-arg rejection); no Java/network, runs in CI__tests__/manual/mcp/http-server-smoke.test.ts— spawns the real--httpserver and drives it via the MCP Streamable HTTP client: tool listing, resource read, concurrent session isolation, bad-request400/-32000, GET-without-session400cli-runner.ts,mcp-http.tsThe HTTP smoke lives in the manual suite (parity with the existing stdio smoke — spawns the Java-gated server + hits the live Mojang manifest) and is picked up by
npm run test:manual:mcp.Note
Left
versionat1.1.0— this is a feature add, so consider bumping to1.2.0before release.🤖 Generated with Claude Code