Skip to content

feat: add HTTP transport and standalone CLI#10

Merged
GhostTypes merged 3 commits into
mainfrom
feat/cli-and-http-transport
Jun 8, 2026
Merged

feat: add HTTP transport and standalone CLI#10
GhostTypes merged 3 commits into
mainfrom
feat/cli-and-http-transport

Conversation

@GhostTypes

Copy link
Copy Markdown
Contributor

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/mcp
  • Per-session transport map (created on initialize, torn down on close) — genuinely supports concurrent clients. The fork shared a single transport across all requests; this does not.
  • Secure default: binds 127.0.0.1, so the SDK's DNS-rebinding protection turns on automatically. --host 0.0.0.0 is opt-in (the fork defaulted to 0.0.0.0).
  • Server construction refactored into a per-session 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:

minecraft-dev-cli list-tools
minecraft-dev-cli get_minecraft_source --version 1.21.10 --className net.minecraft.world.entity.Entity --mapping yarn
  • Flags-only (--key value / --key=value) to avoid the JSON-quoting pain positional JSON arguments cause in PowerShell and other shells.
  • Java is verified only for real tool calls, so help / list-tools work without a JDK (fork required Java for everything).
  • Reuses our existing tools + handleToolCall exports — no tool-layer changes.

Supporting changes

  • Deps: express + @types/express; raised @modelcontextprotocol/sdk floor to ^1.24.0 (required by createMcpExpressApp).
  • Scripts: cli, dev:http. New bin: minecraft-dev-cli.
  • README: documents both features, keeping the existing centered formatting.

Testing

Suite Result
npm test (default suite) 24 files / 234 tests passed
HTTP transport E2E (manual) 5/5 passed
typecheck / build / biome lint clean

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 --http server and drives it via the MCP Streamable HTTP client: tool listing, resource read, concurrent session isolation, bad-request 400/-32000, GET-without-session 400
  • helpers: cli-runner.ts, mcp-http.ts

The 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 version at 1.1.0 — this is a feature add, so consider bumping to 1.2.0 before release.

🤖 Generated with Claude Code

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>
@GhostTypes GhostTypes self-assigned this Jun 7, 2026
@GhostTypes GhostTypes added the enhancement New feature or request label Jun 7, 2026
gemini-code-assist[bot]

This comment was marked as resolved.

- 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>
@GhostTypes

Copy link
Copy Markdown
Contributor Author

Addressed the Gemini review in f8782eb:

cli.ts arg coercion (HIGH) — fixed. Real bug: the old JSON.parse-everything approach turned --version 1.20 into the number 1.2 and --query 42 into 42, both failing the string-typed Zod schemas. Now coercion is schema-driven off each tool's inputSchema.properties[key].type — string fields keep their literal text; only number/integer/boolean/array/object are converted. A bare -- now errors instead of producing an empty-string key.

index.ts session id (HIGH ×2) — fixed, with a correction. Added a ?sessionId= query-param fallback for browser EventSource clients that can't set headers. Note the suggested one-liner wouldn't have worked on its own: the SDK transport validates the session only from req.headers['mcp-session-id'] internally (streamableHttp.js:547), so a query-only id would pass our map lookup but then be rejected inside handleRequest. The fix resolves header-OR-query and normalizes the query value into the header so the transport accepts it. Applied to POST and GET/DELETE. Covered by a new E2E test that drives a full handshake using only ?sessionId=.

mcp-http.ts stdout pipe (MEDIUM) — fixed. Set stdio: ['ignore', 'ignore', 'pipe']. We only read stderr (logger writes to a file), so ignoring stdout removes any pipe-buffer-block risk.

All green after the changes: default suite 24 files / 237 tests; HTTP smoke 6/6 (incl. the query-param fallback).

@GhostTypes

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread src/index.ts
Comment on lines +282 to +290
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);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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');
        }
      }
    };

Comment thread src/cli.ts
Comment on lines +165 to +172
} else {
key = arg.slice(2);
const nextArg = args[i + 1];
if (nextArg && !nextArg.startsWith('--')) {
value = nextArg;
i++;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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>
@GhostTypes GhostTypes merged commit 61ad3dc into main Jun 8, 2026
1 check passed
@GhostTypes GhostTypes deleted the feat/cli-and-http-transport branch June 8, 2026 00:04
GhostTypes added a commit that referenced this pull request Jun 10, 2026
Ships patched Forge/NeoForge JAR support (#9) and HTTP transport + standalone CLI (#10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant