Skip to content

feat(mcp): add gitnexus mcp --http server with Streamable HTTP and legacy SSE transports#2141

Open
blueroseslol wants to merge 3 commits into
abhigyanpatwari:mainfrom
blueroseslol:feat/mcp-http-sse-transport
Open

feat(mcp): add gitnexus mcp --http server with Streamable HTTP and legacy SSE transports#2141
blueroseslol wants to merge 3 commits into
abhigyanpatwari:mainfrom
blueroseslol:feat/mcp-http-sse-transport

Conversation

@blueroseslol

@blueroseslol blueroseslol commented Jun 10, 2026

Copy link
Copy Markdown

Closes #2142

Summary

feat(mcp): add gitnexus mcp --http server with Streamable HTTP and legacy SSE transports

Full details and test results: blueroseslol#3

Original PR with CI-ready changes: blueroseslol#2

…mable HTTP transports

Adds a dedicated MCP-only HTTP server (gitnexus/src/mcp/http-transport.ts)
started via `gitnexus mcp --http [--port 3000] [--host 0.0.0.0] [--auth-token <t>]`.
Serves modern Streamable HTTP at POST /mcp and legacy SSE at GET /sse + POST
/messages, reusing the transport-agnostic createMCPServer(). stdio remains the
default for `gitnexus mcp` (no breaking change).

- Reuse the proven StreamableHTTP session machinery by extracting
  createStreamableHttpHandler() from server/mcp-http.ts; mountMCPEndpoints keeps
  its signature and /api/mcp behavior.
- Add createSseHandlers() implementing the legacy SSEServerTransport round-trip.
- Optional bearer-token auth middleware; warn when binding non-loopback without
  a token. Proper CORS (incl. Private Network Access) headers.
- Replace console.log in mcp-http.ts with the core logger.
- Tests: auth middleware, /health, Streamable + SSE round-trips, unknown-session
  404s, auth enforcement, and mountMCPEndpoints refactor safety (16/16 pass).
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

@blueroseslol is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown
Contributor

CI Report

Some checks failed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
❌ Tests failure unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
11028 11011 1 16 582s

1 failed / 11011 passed

16 test(s) skipped — expand for details
  • COBOL pipeline benchmark > scales with file count
  • C++ ADL emit benchmark > emit phase scales sub-quadratically with co-scaled files and sites
  • C++ pipeline benchmark > scales with file count
  • C# pipeline benchmark > scales with file count — namespaces spread across the solution
  • C# pipeline benchmark > scales with file count — all types in one (global) namespace bucket
  • C# pipeline benchmark > scales with file count — all types in one (named) namespace bucket
  • Go pipeline benchmark > scales with file count (workers enabled)
  • Go pipeline benchmark — worker pool (issue Worker idle timeout kills long Go scope extraction and surfaces as Napi::Error during analyze #1848) > does not quarantine the large generated Go file on sub-batch idle timeout
  • Go structural interface detection benchmark > scales linearly with interface × struct count
  • Go structural interface detection split-phase benchmark > separates index-build and detection time
  • PHP pipeline benchmark > scales with file count (workers enabled)
  • Ruby pipeline benchmark > scales with file count (workers enabled)
  • Rust pipeline benchmark > scales with file count (workers enabled)
  • Vue pipeline benchmark > scales with component count
  • run.cjs direct-exec entrypoint (fix(cli): steer docs, skills, and hooks through a CLI-neutral project-local runner (#1939) #1945) > resolves a .cmd shim via the Windows shell branch, passing args and exit code
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 75.7% 37325/49304 N/A% 🟢 ███████████████░░░░░
Branches 63.57% 23126/36375 N/A% 🟢 ████████████░░░░░░░░
Functions 81.22% 4023/4953 N/A% 🟢 ████████████████░░░░
Lines 79.4% 33702/42443 N/A% 🟢 ███████████████░░░░░

📋 View full run · Generated by CI

@magyargergo magyargergo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tri-review: gitnexus mcp --http server

Reviewed three ways — GitNexus swarm (risk, test/CI, security) and Compound-Engineering personas (correctness, adversarial, maintainability, testing), both Claude; and Codex (the one independent engine, ran live). So six Claude lanes + Codex: security findings with Codex + a Claude lane agreeing are strongly corroborated; Claude-only items are "consistent across personas," weighted accordingly below.

🔴 CI is failing — merge blocker (reproduced)

tests / ubuntu / coverage and CI Gate are red, and this is a regression from the PR, not a flake. The four new mcp options (--http, -p/--port, --host, --auth-token) have no entries in help-i18n.ts OPTION_DESCRIPTION_KEYS and no help.option.* keys in en.ts/zh-CN.ts, so test/unit/cli-index-help.test.ts ("localizes every registered CLI command and option description") fails. I reproduced it locally — the assertion surfaces the short --port description ("HTTP port (only with --http). Default: 3000"); the other three are long enough that Commander line-wraps them past the substring check, but all four still render in English inside zh-CN help. Fix: add the four mcp|… mappings (-p, --port can reuse help.option.port) plus the new keys in both bundles.

Security posture (Codex + security + adversarial + risk)

gitnexus mcp --http with no other flags binds 0.0.0.0 unauthenticated, every response sets Access-Control-Allow-Private-Network: true, and CORS uses origin: true (reflects any Origin). The composition means a web page the user merely visits can fetch() the local MCP server cross-origin and read all indexed-repo intelligence. Two independent fixes:

  • (a) Don't emit the PNA header / don't reflect arbitrary origins by default (gate behind an explicit opt-in or an Origin allowlist).
  • (b) Default --host to 127.0.0.1 (matching serve and eval-server, which already do) — the current warn-only on non-loopback is not a control.

Other corroborated findings

  • Pre-auth body parsingexpress.json({ limit: '10mb' }) is global and runs before the per-route auth middleware, so an unauthenticated client forces a 10 MB parse per request; body-parser errors (malformed/oversized JSON) also bypass the handlers' JSON-RPC error envelope and return Express's default error. (Codex P2 + security + adversarial)
  • Legacy SSE session leakcreateSseHandlers has no TTL sweep, while createStreamableHttpHandler in the same file does; abandoned /sse connections leak a Server per session if the socket close never fires. The module's own header comment claims sessions are "cleaned up … after SESSION_TTL_MS of inactivity," but the SSE path never implements it (no lastActivity, no setInterval). (Codex + adversarial, P2)
  • Timing-unsafe bearer compareheader === expected is not constant-time (the comment even advertises length fast-fail). Use crypto.timingSafeEqual. Codex + security + correctness + risk agree on the issue; security graded P1, risk P3 for a local-tool threat model — posting as P2.

Maintainability (Claude, single-engine but high-confidence)

The new files comment exclusively in Chinese, and mcp-http.ts translated its pre-existing English doc comments to Chinese (pure churn on a file already English-commented like the rest of gitnexus/src); the new test file uses Chinese it() names, which hurt vitest -t discoverability. Separately, mcp/http-transport.ts imports from server/mcp-http.ts, inverting the established server/ → mcp/ layering (no cycle today, but the arrow now points the wrong way — consider moving the factories into the mcp/ layer).

Lower-priority (body-only; Claude single-lane)

  • Streamable session map has no cap → an initialize flood holds a Server per request for the full 30-min TTL (adversarial, P2).
  • POST /mcp with no session-id + a non-initialize body connect()s a Server that is never close()d and never enters the map, so the TTL sweep can't reclaim it (adversarial, P3).
  • Number(options.port) yields NaN on --port abc with no validation, and the --http branch lacks the EADDRINUSE try/catch that serveCommand has, so a bad port / busy port surfaces as a raw unhandled rejection (correctness/testing, P3).

Test coverage gaps (testing lane, Claude)

The headline feature startMcpHttpServer (194 lines) has zero tests; the single test touching the Streamable POST branch wraps the call in a silent catch {} with no assertions; and createAuthMiddleware is unit-tested in isolation but never verified as actually wired to the /mcp, /sse, /messages routes. A port-0 smoke test (start → GET /health → assert 200; POST /mcp without token → assert 401) would close most of this. The 16 existing unit tests pass in <1 s with no handle leaks in isolation.

Validated / clean — credit where due

  • The refactor of mountMCPEndpoints into createStreamableHttpHandler/createSseHandlers is logic-preserving for the existing gitnexus serve /api/mcp route — correctness verified it against the base file and empirically (GET notification stream, DELETE session eviction, 404/400/401 bodies all match), and Codex + risk concurred.
  • SDK usage is correct for @modelcontextprotocol/sdk 1.29.0, including the handlePostMessage 3rd-arg parsed-body call.
  • The stdio sentinel is undisturbed in HTTP mode (the logger writes to stderr; http-transport.ts is dynamically imported after the sentinel installs).
  • No bidi / zero-width / homoglyph Unicode in executable code (only Chinese comment/string text); root prettier clean. The Vercel check failure is authorization-only, unrelated to the diff.

Automated multi-tool digest (GitNexus swarm + Compound-Engineering + Codex). Two of the three methods are the same engine (Claude); only Codex is independent — weight accordingly. Verify before acting.

Comment thread gitnexus/src/cli/index.ts
'(Streamable HTTP at POST /mcp + legacy SSE at GET /sse, POST /messages).',
)
.option('--http', 'Serve MCP over HTTP instead of stdio (for remote clients)')
.option('-p, --port <port>', 'HTTP port (only with --http). Default: 3000', '3000')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 - CI blocker (reproduced). This option (and --http, --host, --auth-token) has no entry in help-i18n.ts OPTION_DESCRIPTION_KEYS and no help.option.* key in en.ts/zh-CN.ts, so test/unit/cli-index-help.test.ts ("localizes every registered...") fails - tests / ubuntu / coverage + CI Gate are red. The assertion only surfaces this short --port string; the other three line-wrap past its substring check but are equally untranslated in zh-CN help. Fix: add mcp|-p, --port <port> -> help.option.port (reusable) and new keys for the other three. [reproduced]


// Chrome 130+ Private Network Access 预检支持
app.use((_req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Private-Network', 'true');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 security (Codex + security + adversarial + risk). Emitting Access-Control-Allow-Private-Network: true on every response, combined with cors({ origin: true }) below (reflects any Origin) and the default 0.0.0.0 unauthenticated bind, lets any web page the user visits fetch() this local MCP server cross-origin and read all indexed-repo intelligence. Gate the PNA header behind an explicit opt-in and restrict CORS to an allowlist (or deny cross-origin when no auth token is set). [code-read]

Comment thread gitnexus/src/cli/index.ts
.option('-p, --port <port>', 'HTTP port (only with --http). Default: 3000', '3000')
.option(
'--host <host>',
'HTTP bind address (only with --http). Default: 0.0.0.0 (all interfaces). Use 127.0.0.1 for loopback only.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 security (Codex + security + risk). Default --host of 0.0.0.0 exposes the unauthenticated MCP server on all interfaces out of the box; the warn-only on non-loopback (http-transport.ts:88) is not a control. serve (line 131 above) and eval-server already default to 127.0.0.1 - match them, and require an explicit --host 0.0.0.0 for remote use. [code-read]

const expected = `Bearer ${authToken}`;

// 字符串比较(长度不同则快速失败)
if (typeof header === 'string' && header === expected) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 security (Codex + security + correctness + risk). header === expected is not constant-time - it short-circuits on the first differing byte (the comment even advertises length fast-fail), a timing oracle on the bearer token. Use crypto.timingSafeEqual over equal-length buffers. (security graded P1, risk P3 for a loopback dev-tool; P2 here.) [code-read]

);

// 解析 JSON 请求体(最大 10MB)
app.use(express.json({ limit: '10mb' }));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 (Codex + security + adversarial). express.json({ limit: '10mb' }) is global and runs before the per-route auth middleware (line 121), so unauthenticated clients force a 10 MB parse per request (pre-auth DoS amplification). Body-parser errors also throw before the route .catch, so malformed/oversized JSON returns Express's default error instead of the JSON-RPC envelope every other path uses. Apply the parser per-route after auth, and add a terminal JSON-RPC error handler. [code-read]

const transport = new SSEServerTransport(messagesPath, res);
const server = createMCPServer(backend);

sseSessions.set(transport.sessionId, { server, transport });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 (Codex + adversarial). sseSessions has no TTL sweep or cap - unlike createStreamableHttpHandler in this same file, which runs a setInterval idle eviction. Abandoned /sse connections (network drop where socket close never fires) leak a Server per session indefinitely. The module header comment claims sessions are evicted "after SESSION_TTL_MS of inactivity," but the SSE path never implements it (no lastActivity, no timer). Add a lastActivity field + a sweep mirroring the Streamable handler. [code-read]

* stdio 仍是 `gitnexus mcp` 的默认模式(无破坏性变更)。
*
* 安全注意事项:
* - 默认绑定 0.0.0.0(所有接口),如不提供 --auth-token 会输出警告

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 maintainability (Claude lanes). This new file (and cli/mcp.ts's new block, and the new test's it() names) comments exclusively in Chinese, and server/mcp-http.ts translated its pre-existing English doc comments to Chinese - the rest of gitnexus/src is English-only, so this is a permanent convention break plus pure churn, and Chinese test names hurt vitest -t discoverability. Recommend English comments/test names to match the codebase. [code-read]

@magyargergo

Copy link
Copy Markdown
Collaborator

@blueroseslol I ran out automated review agents and it left these comments. Let me know what you think.

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.

GitNexus MCP SSE & Custom Embedding — Feature Development Report

2 participants