Skip to content

feat(mcp): HTTP transport + serve-mcp CLI for ChatGPT App Directory#238

Draft
kaghni wants to merge 1 commit into
mainfrom
mcp-http-transport
Draft

feat(mcp): HTTP transport + serve-mcp CLI for ChatGPT App Directory#238
kaghni wants to merge 1 commit into
mainfrom
mcp-http-transport

Conversation

@kaghni

@kaghni kaghni commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the smallest possible HTTP wrapper around the existing 3 MCP tools so Hivemind can appear in the ChatGPT App Directory. The local stdio MCP server (used by Hermes/Cursor/Claude Code) is unchanged — this PR only adds a second transport reachable over the network.

  • New shared src/mcp/tools.ts so stdio and HTTP transports register the same tool handlers (single source of truth). Tool annotations set: all 3 readOnlyHint: true, openWorldHint: false, destructiveHint: false — the initial virtual fs implementation #1 cited rejection cause in OpenAI's App Submission Guidelines.
  • New src/mcp/http-server.ts using StreamableHTTPServerTransport from @modelcontextprotocol/sdk v1.29 — the transport Apps SDK actually negotiates.
  • New hivemind serve-mcp CLI command for hosting in a container.
  • New deploy/mcp/{Dockerfile,k8s.yaml,README.md} — starting templates for SRE.
  • 5 unit tests in tests/shared/mcp-http-server.test.ts.

Auth (no new auth code)

The user's Auth0 JWT arrives in Authorization: Bearer …. We don't validate it locally — DeeplakeApi forwards it to deeplake-api, which validates it via the existing internal/auth/jwt.go. Missing/malformed token → 401 with WWW-Authenticate: Bearer realm="<auth0-issuer>" so ChatGPT discovers where to send the user to sign in. The auth flow that ChatGPT runs is the standard Auth0 Universal Login — the same screen deeplake.ai users already see today.

Org resolution

HIVEMIND_MCP_ORG_ID + HIVEMIND_MCP_WORKSPACE + HIVEMIND_MCP_TABLE + HIVEMIND_MCP_SESSIONS_TABLE pin the org/workspace/tables at boot. Needed for the activeloop deployment because GET /organizations returns 34 orgs in unstable order — auto-picking the first org defaulted to a test org during the smoke. Multi-tenant auto-resolution (picking the user's primary org reliably) is a follow-up.

If none of the env pins are set, the server falls back to "first org from GET /organizations" + workspaceId=\"default\" + memory/sessions table names (matching the CLI defaults in src/config.ts:55-58).

Verification

$ npx vitest run tests/shared/mcp-http-server.test.ts
✓ Test Files  1 passed (1)
✓      Tests  5 passed (5)

$ docker build -f deploy/mcp/Dockerfile -t hivemind-mcp:smoke .
# ✓ image builds (~150 MB Alpine, runs as non-root uid 10001)

$ docker run --rm -p 18787:8787 \
    -e HIVEMIND_MCP_AUTH_ISSUER=https://auth-beta.deeplake.ai/ \
    hivemind-mcp:smoke
$ curl http://127.0.0.1:18787/health
{\"status\":\"ok\",\"version\":\"0.7.67\"}
$ curl -i -X POST http://127.0.0.1:18787/mcp -H 'Content-Type: application/json' -d '{}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm=\"https://auth-beta.deeplake.ai/\", error=\"invalid_token\", ...

End-to-end smoke against api.deeplake.ai production succeeded using a real Auth0 token from ~/.deeplake/credentials.json (org pinned to activeloop / workspace hivemind via env vars):

  • initialize → session assigned, capabilities advertised
  • tools/list → 3 tools with the read-only annotations
  • tools/call hivemind_index limit=3 → 3 real summaries from the activeloop/hivemind workspace
  • tools/call hivemind_search query=\"chatgpt\" → 4 KB of real matching content

The full chain works: Bearer JWT → MCP HTTP transport → deeplake-api validation → memory query → tool result. No deeplake-api changes needed.

Dockerfile caveat

Uses npm install --ignore-scripts as a workaround for an existing package-lock.json drift on origin/main (pg@8.21 in package.json, ^8.11 in lockfile). Should switch back to npm ci (no --ignore-scripts) once the lockfile is regenerated in a separate cleanup PR.

Not in this PR

  • Deploy to a real public HTTPS URL — separate SRE handoff
  • Auth0 dashboard client registration for ChatGPT — separate owner action
  • OpenAI Platform Dashboard business verification + app draft + submission — separate owner action
  • Multi-tenant org auto-resolution — follow-up if/when the env-var pinning model isn't enough

Test plan

  • npm test -- tests/shared/mcp-http-server.test.ts (5/5)
  • docker build + docker run smoke
  • End-to-end against production deeplake-api with a real Auth0 token
  • CI passes (note: tree-sitter type errors in src/graph/extract/typescript.ts are pre-existing on origin/main; not from this PR)
  • Reviewer manual: hivemind serve-mcp boots, missing env yields a clear message

Summary by CodeRabbit

  • New Features

    • HTTP MCP server supporting OAuth-based authentication, session management, and health checks
    • New serve-mcp CLI command for running the server
  • Documentation

    • Comprehensive deployment guide including Docker containerization and Kubernetes manifests with ingress routing

Adds a Streamable HTTP wrapper around the existing 3 MCP tools
(hivemind_search/read/index) so remote clients like the ChatGPT
App Directory can reach them. The stdio MCP server used by local
agents (Hermes/Cursor/Claude Code) is unchanged.

What's new:

- src/mcp/tools.ts: factor out the 3 tool registrations into a
  shared module so stdio and HTTP transports stay single-source-
  of-truth. Adds read-only annotations (readOnlyHint: true,
  openWorldHint: false, destructiveHint: false) — the #1 cited
  rejection cause in OpenAI's App Submission Guidelines.
- src/mcp/server.ts: refactored to import from tools.ts (stdio
  behavior unchanged).
- src/mcp/http-server.ts: new HTTP MCP server using the official
  @modelcontextprotocol/sdk's StreamableHTTPServerTransport. One
  McpServer + transport per session (keyed by Mcp-Session-Id
  header per the SDK's pattern). Health + 401+WWW-Authenticate +
  routing.
- src/cli/serve-mcp.ts + dispatch wire-up in src/cli/index.ts:
  the `hivemind serve-mcp` command for hosting in a container.
  Configured via env vars (see deploy/mcp/README.md).
- tests/shared/mcp-http-server.test.ts: 5 tests covering health,
  401+WWW-Authenticate shape, malformed-auth, 404, and no-leak
  of auth metadata on /health.
- deploy/mcp/{Dockerfile,k8s.yaml,README.md}: starting templates
  for SRE — multi-stage Alpine image with non-root user, k8s
  manifest with ingress rules tuned for SSE (proxy-buffering off,
  long timeouts).

Auth model: the user's Auth0 JWT arrives in Authorization: Bearer.
We don't validate it locally — DeeplakeApi forwards it to
deeplake-api, which validates it via the existing
internal/auth/jwt.go. Missing token => 401 with WWW-Authenticate
pointing at the Auth0 issuer (the discovery mechanism ChatGPT
needs).

Org resolution: defaults to `GET /organizations` and picking the
first org for the user. Env vars HIVEMIND_MCP_ORG_ID +
HIVEMIND_MCP_WORKSPACE + HIVEMIND_MCP_TABLE +
HIVEMIND_MCP_SESSIONS_TABLE pin the org/workspace/tables at boot
for single-tenant deployments — needed for the activeloop
deployment because /organizations returns 34 orgs in unstable
order and the user's primary may not be first. Multi-tenant
auto-resolution is captured as a follow-up.

End-to-end smoke against api.deeplake.ai production succeeded:
initialize, tools/list, hivemind_index, and hivemind_search all
return real data over the HTTP transport using a real Auth0
token forwarded through to deeplake-api.

Dockerfile uses `npm install --ignore-scripts` as a workaround
for an existing package-lock.json drift on origin/main (pg@8.21
in package.json, ^8.11 in lockfile). Should switch back to
`npm ci` once the lockfile is regenerated in a separate PR.
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces an HTTP transport for the Hivemind MCP server, enabling ChatGPT App Directory integration. It refactors tool logic into a reusable registration function, implements a stateful HTTP server with per-session MCP contexts and Bearer token auth, wires a new CLI command, adds contract tests, and provides Docker and Kubernetes deployment infrastructure.

Changes

HTTP MCP Server with Deployment

Layer / File(s) Summary
Shared tool registration
src/mcp/tools.ts, src/mcp/server.ts
hivemind_search, hivemind_read, and hivemind_index tools are moved into a reusable registerHivemindTools(server, getContext) function. New types ServerContext, ContextResult, and GetContext define the context provisioning contract. Stdio server refactored to use the registration API.
HTTP MCP server implementation
src/mcp/http-server.ts
HTTP server with /health (GET) and /mcp (POST/GET SSE) routes. Extracts Bearer tokens, returns 401 with WWW-Authenticate when missing, manages per-session MCP server and transport instances keyed by mcp-session-id, lazily resolves user's primary organization via GET /organizations, and provides graceful shutdown.
CLI command wiring
src/cli/index.ts, src/cli/serve-mcp.ts
New hivemind serve-mcp command reads HIVEMIND_MCP_* and HIVEMIND_API_URL env vars, validates auth issuer presence and port range, boots the HTTP server, logs readiness, handles SIGINT/SIGTERM, and returns exit codes.
HTTP server contract tests
tests/shared/mcp-http-server.test.ts
Vitest suite validating /health returns 200 with status and version, /mcp without Bearer token returns 401 with WWW-Authenticate realm and invalid_token, malformed headers return 401 (not 500), and unknown routes return 404.
Deployment infrastructure
deploy/mcp/Dockerfile, deploy/mcp/k8s.yaml, deploy/mcp/README.md
Multi-stage Dockerfile with build/runtime separation on Node 24 alpine, non-root user, and health check. Kubernetes manifest with Namespace, ConfigMap, Deployment (2 replicas, security hardening), ClusterIP Service, and Ingress routing /mcp and /health to the service over TLS with SSE-friendly nginx settings. README documents build, local run, Kubernetes deployment, environment variables, caveats (token check only, no JWT validation, no rate limiting), and verification steps.

Sequence Diagram

sequenceDiagram
  participant Client
  participant HTTPServer as HTTP Server
  participant Session as MCP Session
  participant Transport as StreamableHTTPServerTransport
  participant DeeplakeApi as Deeplake API
  
  Client->>HTTPServer: POST /mcp with Bearer token
  HTTPServer->>HTTPServer: Extract + validate Bearer token
  alt No Bearer Token
    HTTPServer-->>Client: 401 with WWW-Authenticate
  else Token Present
    HTTPServer->>HTTPServer: Get/create session by mcp-session-id
    alt Session is New
      HTTPServer->>Session: Create McpServer instance
      HTTPServer->>Transport: Create StreamableHTTPServerTransport
      HTTPServer->>Session: registerHivemindTools(server, getContext)
      HTTPServer->>Transport: Connect server to transport
    end
    HTTPServer->>HTTPServer: buildGetContext callback
    HTTPServer->>DeeplakeApi: Resolve primary organization
    HTTPServer->>Session: handleRequest(JSON body)
    Transport->>Session: Process tool call
    Session-->>Transport: Tool response
    Transport-->>HTTPServer: Serialized response
    HTTPServer-->>Client: JSON or SSE stream
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A new HTTP pathway springs to life,
Where sessions bloom and tokens thrive,
From CLI whispers to K8s dreams,
The Hivemind flows through streaming streams—
Tools shared, deployed, and tests confirmed!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the summary, implementation details, auth approach, org resolution, and verification; however, it does not include a version bump section as required by the template. Add a Version Bump section indicating which version bump type (patch/minor/major) is appropriate and update package.json accordingly, or explicitly state if no version bump is needed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding HTTP transport and a CLI command to expose Hivemind to ChatGPT App Directory.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mcp-http-transport

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/mcp/http-server.ts
function extractBearer(req: http.IncomingMessage): string | null {
const header = req.headers["authorization"];
if (!header || typeof header !== "string") return null;
const m = header.match(/^Bearer\s+(.+)$/i);
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🔴 Lines 71.00% (🎯 90%) 284 / 400
🔴 Statements 72.73% (🎯 90%) 336 / 462
🔴 Functions 74.07% (🎯 90%) 40 / 54
🔴 Branches 65.82% (🎯 90%) 208 / 316
File Coverage — 5 files changed
File Stmts Branches Functions Lines
src/cli/index.ts 🔴 84.6% 🔴 84.6% 🟢 100.0% 🔴 81.8%
src/cli/serve-mcp.ts 🔴 0.0% 🔴 0.0% 🔴 0.0% 🔴 0.0%
src/mcp/http-server.ts 🔴 49.0% 🔴 39.8% 🔴 61.9% 🔴 51.0%
src/mcp/server.ts 🔴 86.7% 🔴 66.7% 🔴 66.7% 🔴 86.7%
src/mcp/tools.ts 🟢 98.2% 🔴 85.0% 🟢 100.0% 🟢 100.0%

Generated for commit 3e47025.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
tests/shared/mcp-http-server.test.ts (1)

64-65: ⚡ Quick win

Harden WWW-Authenticate assertions to exact parameter checks.

toContain(...) can pass even with malformed header structure/order. Parse the challenge (or assert a strict regex) and validate scheme + exact realm and error parameter values.

Suggested test tightening
-    expect(wwwAuth).toContain(`realm="${AUTH_ISSUER}"`);
-    expect(wwwAuth).toContain("invalid_token");
+    expect(wwwAuth).toMatch(
+      new RegExp(`^Bearer\\s+.*\\brealm="${AUTH_ISSUER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\b.*\\berror="invalid_token"\\b`),
+    );
...
-    expect(resp.headers.get("www-authenticate")).toContain(`realm="${AUTH_ISSUER}"`);
+    expect(resp.headers.get("www-authenticate")).toMatch(
+      new RegExp(`^Bearer\\s+.*\\brealm="${AUTH_ISSUER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\b`),
+    );

As per coding guidelines: tests/**: Prefer asserting on specific values (paths, messages) over generic substrings.

Also applies to: 82-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/shared/mcp-http-server.test.ts` around lines 64 - 65, The current
assertions on the WWW-Authenticate header are too loose; update the test that
reads the header into wwwAuth to parse the challenge and assert exact scheme and
parameter values instead of using toContain. Specifically, split wwwAuth to
validate the auth scheme (e.g., "Bearer"), parse the comma-separated key="value"
params into a map, then assert params.realm === AUTH_ISSUER and params.error ===
"invalid_token" (also tighten the similar assertion around the other occurrence
at the later assertion). Use the existing wwwAuth and AUTH_ISSUER symbols to
locate the header and replace the substring checks with these precise checks.
src/cli/serve-mcp.ts (1)

52-56: 💤 Low value

Consider logging the actual resolved port for consistency.

The startHttpMcpServer function returns the actual bound port (which can differ from the requested port if the OS assigns one). While your validation on lines 38–42 rejects port <= 0 (so OS-assigned ports are not supported), it would be slightly more consistent with the server's design to destructure and log the actual port.

♻️ Optional refactor to use actualPort
-    const { close } = await startHttpMcpServer({
+    const { close, port: actualPort } = await startHttpMcpServer({
       port, host, authIssuer, apiUrl,
       orgId, workspaceId, memoryTable, sessionsTable,
     });
-    log(`hivemind serve-mcp ready on http://${host}:${port}/mcp`);
+    log(`hivemind serve-mcp ready on http://${host}:${actualPort}/mcp`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/serve-mcp.ts` around lines 52 - 56, Destructure the actual bound port
returned by startHttpMcpServer (e.g., const { close, port: actualPort } = await
startHttpMcpServer(...)) and use that actualPort in the log call instead of the
requested port variable; update the log invocation at the current log(...) call
to reference actualPort so the printed URL reflects the server's resolved port
while keeping the existing validation around the requested port.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/mcp/k8s.yaml`:
- Around line 113-115: The comment above the Ingress spec is inaccurate: it
claims routes for "/mcp" and "/.well-known" but the Ingress only defines "/mcp"
(Prefix) and "/health" (Exact) in the rules; update the comment to reflect the
actual configured paths or add the missing "/.well-known" path rule to the
Ingress spec. Specifically, edit the comment that references "Routes
api.deeplake.ai/mcp and /.well-known paths" to instead list "/mcp and /health"
(or add a new path block for "/.well-known" pointing to the MCP service if that
behavior is required), and ensure the Ingress path types (Prefix/Exact) and
backend service name in the spec (the MCP service block) match the comment.

In `@deploy/mcp/README.md`:
- Around line 10-19: Add the four optional environment variables that
src/cli/serve-mcp.ts reads to the configuration table: HIVEMIND_MCP_ORG_ID
(optional pin to a specific org ID, skips /organizations lookup),
HIVEMIND_MCP_WORKSPACE (optional workspace ID, default "default"),
HIVEMIND_MCP_TABLE (optional memory table name, default "memory"), and
HIVEMIND_MCP_SESSIONS_TABLE (optional sessions table name, default "sessions");
update the table rows to include each Var, its default/value note, and a short
description so the README matches the comments and behavior in serve-mcp.ts.

In `@src/mcp/http-server.ts`:
- Around line 321-327: The shutdown order in the close function is reversed:
call session.transport.close() for every active session first (iterating over
sessions.values() and awaiting each transport.close() with errors caught) and
only after all transports have been closed await the httpServer.close() Promise;
update the close function (referencing close, sessions, session.transport.close,
and httpServer.close) to perform transport shutdown before invoking and awaiting
httpServer.close() to prevent hanging SSE responses.
- Around line 121-133: The readBody function currently buffers the entire
request with no size limit and can exhaust memory; modify readBody(req) to
enforce a sane max payload (e.g., MAX_BODY_BYTES constant) by tracking
cumulative bytes while iterating the async chunks, stop reading and return a
clear sentinel (e.g., throw or return a specific error/marker like {tooLarge:
true} or throw a custom PayloadTooLargeError) as soon as the limit is exceeded,
and ensure you do not concatenate further chunks; then update handleMcpRequest
to detect that sentinel or catch the custom error and respond with HTTP 413
instead of treating it as a generic 500.
- Around line 223-250: The current lookup reuses sessions by mcp-session-id
alone; change the logic so sessions are bound to the bearer token: when creating
a session in createSession, record the token on the session object (e.g.,
session.token or session.ownerToken) and when looking up an existing session
(sessionIdHeader / existingId) ensure the incoming token matches session.token
before reusing it; register sessions in st.sessions under a composite key or
check token equality after lookup (use session.transport.sessionId as the
session id) and when handling teardown/DELETE remove the stored session from
st.sessions so the old token/state is evicted; update the registration block
that uses session.transport.sessionId to include the token binding so subsequent
lookups validate token equality.

In `@tests/shared/mcp-http-server.test.ts`:
- Around line 38-40: The afterEach teardown currently calls await
running.close() unguarded and will throw if beforeEach failed to assign running;
update afterEach to first check that the test server handle exists (e.g., if
(running) or if (running != null)) before calling close so teardown is skipped
when setup failed; ensure the variable referenced is the same test-scoped symbol
named running used in beforeEach/afterEach.

---

Nitpick comments:
In `@src/cli/serve-mcp.ts`:
- Around line 52-56: Destructure the actual bound port returned by
startHttpMcpServer (e.g., const { close, port: actualPort } = await
startHttpMcpServer(...)) and use that actualPort in the log call instead of the
requested port variable; update the log invocation at the current log(...) call
to reference actualPort so the printed URL reflects the server's resolved port
while keeping the existing validation around the requested port.

In `@tests/shared/mcp-http-server.test.ts`:
- Around line 64-65: The current assertions on the WWW-Authenticate header are
too loose; update the test that reads the header into wwwAuth to parse the
challenge and assert exact scheme and parameter values instead of using
toContain. Specifically, split wwwAuth to validate the auth scheme (e.g.,
"Bearer"), parse the comma-separated key="value" params into a map, then assert
params.realm === AUTH_ISSUER and params.error === "invalid_token" (also tighten
the similar assertion around the other occurrence at the later assertion). Use
the existing wwwAuth and AUTH_ISSUER symbols to locate the header and replace
the substring checks with these precise checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 31c7a1a6-1791-4380-905d-073c13c5ca85

📥 Commits

Reviewing files that changed from the base of the PR and between c5fccad and 6c073e9.

📒 Files selected for processing (9)
  • deploy/mcp/Dockerfile
  • deploy/mcp/README.md
  • deploy/mcp/k8s.yaml
  • src/cli/index.ts
  • src/cli/serve-mcp.ts
  • src/mcp/http-server.ts
  • src/mcp/server.ts
  • src/mcp/tools.ts
  • tests/shared/mcp-http-server.test.ts

Comment thread deploy/mcp/k8s.yaml
Comment on lines +113 to +115
# Routes api.deeplake.ai/mcp and /.well-known paths to the MCP service.
# Everything else on api.deeplake.ai (the existing API) stays on its
# current backend — adjust the host's other path rules accordingly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Misleading comment about routed paths.

The comment states "Routes api.deeplake.ai/mcp and /.well-known paths" but the actual Ingress spec only defines two paths: /mcp (Prefix, lines 136–142) and /health (Exact, lines 143–149). There are no /.well-known paths configured.

📝 Suggested fix
-# Routes api.deeplake.ai/mcp and /.well-known paths to the MCP service.
+# Routes api.deeplake.ai/mcp and /health paths to the MCP service.
 # Everything else on api.deeplake.ai (the existing API) stays on its
 # current backend — adjust the host's other path rules accordingly.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Routes api.deeplake.ai/mcp and /.well-known paths to the MCP service.
# Everything else on api.deeplake.ai (the existing API) stays on its
# current backend — adjust the host's other path rules accordingly.
# Routes api.deeplake.ai/mcp and /health paths to the MCP service.
# Everything else on api.deeplake.ai (the existing API) stays on its
# current backend — adjust the host's other path rules accordingly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/mcp/k8s.yaml` around lines 113 - 115, The comment above the Ingress
spec is inaccurate: it claims routes for "/mcp" and "/.well-known" but the
Ingress only defines "/mcp" (Prefix) and "/health" (Exact) in the rules; update
the comment to reflect the actual configured paths or add the missing
"/.well-known" path rule to the Ingress spec. Specifically, edit the comment
that references "Routes api.deeplake.ai/mcp and /.well-known paths" to instead
list "/mcp and /health" (or add a new path block for "/.well-known" pointing to
the MCP service if that behavior is required), and ensure the Ingress path types
(Prefix/Exact) and backend service name in the spec (the MCP service block)
match the comment.

Comment thread deploy/mcp/README.md
Comment on lines +10 to +19
## Configuration (env vars)

All read by `src/cli/serve-mcp.ts`:

| Var | Default | Notes |
|---|---|---|
| `HIVEMIND_MCP_AUTH_ISSUER` | _(required)_ | Auth0 issuer URL (trailing slash). Advertised in `WWW-Authenticate` on 401 so ChatGPT can discover the auth server. e.g. `https://auth-beta.deeplake.ai/` |
| `HIVEMIND_MCP_PORT` | `8787` | Listen port |
| `HIVEMIND_MCP_HOST` | `0.0.0.0` | Bind host |
| `HIVEMIND_API_URL` | `https://api.deeplake.ai` | deeplake-api base URL |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the optional org/workspace pinning environment variables.

The configuration table is missing four optional environment variables that src/cli/serve-mcp.ts reads (lines 46–49 and documented in comments on lines 17–21):

  • HIVEMIND_MCP_ORG_ID — pin to a specific org ID (skips /organizations lookup)
  • HIVEMIND_MCP_WORKSPACE — workspace ID (default "default")
  • HIVEMIND_MCP_TABLE — memory table name (default "memory")
  • HIVEMIND_MCP_SESSIONS_TABLE — sessions table name (default "sessions")

These should be added to the table for completeness.

📝 Suggested addition to the configuration table
 | Var | Default | Notes |
 |---|---|---|
 | `HIVEMIND_MCP_AUTH_ISSUER` | _(required)_ | Auth0 issuer URL (trailing slash). Advertised in `WWW-Authenticate` on 401 so ChatGPT can discover the auth server. e.g. `https://auth-beta.deeplake.ai/` |
 | `HIVEMIND_MCP_PORT` | `8787` | Listen port |
 | `HIVEMIND_MCP_HOST` | `0.0.0.0` | Bind host |
 | `HIVEMIND_API_URL` | `https://api.deeplake.ai` | deeplake-api base URL |
+| `HIVEMIND_MCP_ORG_ID` | _(optional)_ | Pin to a specific org ID (skips /organizations lookup) |
+| `HIVEMIND_MCP_WORKSPACE` | `default` | Workspace ID |
+| `HIVEMIND_MCP_TABLE` | `memory` | Memory table name |
+| `HIVEMIND_MCP_SESSIONS_TABLE` | `sessions` | Sessions table name |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/mcp/README.md` around lines 10 - 19, Add the four optional environment
variables that src/cli/serve-mcp.ts reads to the configuration table:
HIVEMIND_MCP_ORG_ID (optional pin to a specific org ID, skips /organizations
lookup), HIVEMIND_MCP_WORKSPACE (optional workspace ID, default "default"),
HIVEMIND_MCP_TABLE (optional memory table name, default "memory"), and
HIVEMIND_MCP_SESSIONS_TABLE (optional sessions table name, default "sessions");
update the table rows to include each Var, its default/value note, and a short
description so the README matches the comments and behavior in serve-mcp.ts.

Comment thread src/mcp/http-server.ts
Comment on lines +121 to +133
async function readBody(req: http.IncomingMessage): Promise<unknown> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk as Buffer);
}
if (chunks.length === 0) return undefined;
const raw = Buffer.concat(chunks).toString("utf8");
if (!raw) return undefined;
try {
return JSON.parse(raw);
} catch {
return undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cap buffered request size before parsing JSON.

This reads the entire POST body into memory with no limit. Because extractBearer() only checks header shape, a single oversized /mcp request with Authorization: Bearer x can exhaust memory before anything downstream rejects it. Return 413 once the payload crosses a sane cap.

Suggested fix
-async function readBody(req: http.IncomingMessage): Promise<unknown> {
+async function readBody(req: http.IncomingMessage, maxBytes = 1024 * 1024): Promise<unknown> {
   const chunks: Buffer[] = [];
+  let total = 0;
   for await (const chunk of req) {
-    chunks.push(chunk as Buffer);
+    const buf = chunk as Buffer;
+    total += buf.length;
+    if (total > maxBytes) {
+      throw new Error("payload_too_large");
+    }
+    chunks.push(buf);
   }
   if (chunks.length === 0) return undefined;

You'll also want handleMcpRequest() to translate that sentinel into an HTTP 413 instead of the generic 500.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/http-server.ts` around lines 121 - 133, The readBody function
currently buffers the entire request with no size limit and can exhaust memory;
modify readBody(req) to enforce a sane max payload (e.g., MAX_BODY_BYTES
constant) by tracking cumulative bytes while iterating the async chunks, stop
reading and return a clear sentinel (e.g., throw or return a specific
error/marker like {tooLarge: true} or throw a custom PayloadTooLargeError) as
soon as the limit is exceeded, and ensure you do not concatenate further chunks;
then update handleMcpRequest to detect that sentinel or catch the custom error
and respond with HTTP 413 instead of treating it as a generic 500.

Comment thread src/mcp/http-server.ts
Comment on lines +223 to +250
const sessionIdHeader = req.headers["mcp-session-id"];
const existingId = typeof sessionIdHeader === "string" ? sessionIdHeader : undefined;

let session = existingId ? st.sessions.get(existingId) : undefined;
if (!session) {
// New session — body must be the MCP `initialize` request.
session = await createSession(token, st.options);
// The transport assigns the session id on init; store under that id once known.
// We use the transport's sessionId (set by the SDK after handleRequest).
}

const body = req.method === "POST" ? await readBody(req) : undefined;

try {
await session.transport.handleRequest(req, res, body);
} catch (err) {
if (!st.options.silent) {
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`mcp http error: ${msg}\n`);
}
if (!res.headersSent) sendJson(res, 500, { error: "internal_error" });
return;
}

// After the SDK assigns sessionId, register under that id for subsequent lookups.
const assignedId = session.transport.sessionId;
if (assignedId && !st.sessions.has(assignedId)) {
st.sessions.set(assignedId, session);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bind reused sessions to the bearer token and evict them on teardown.

Line 226 trusts mcp-session-id alone. Once a session exists, a request with any Bearer token will reuse it, and buildGetContext() continues querying with the original state.token. That makes the session id the real auth boundary and also leaves the old token/state resident after DELETE.

Suggested fix
   let session = existingId ? st.sessions.get(existingId) : undefined;
+  if (session && session.token !== token) {
+    send401(res, st.options.authIssuer, "Bearer token does not match the active MCP session.");
+    return;
+  }
   if (!session) {
     // New session — body must be the MCP `initialize` request.
     session = await createSession(token, st.options);
     // The transport assigns the session id on init; store under that id once known.
     // We use the transport's sessionId (set by the SDK after handleRequest).
@@
-  const assignedId = session.transport.sessionId;
+  const assignedId = session.transport.sessionId ?? existingId;
   if (assignedId && !st.sessions.has(assignedId)) {
     st.sessions.set(assignedId, session);
   }
+  if (req.method === "DELETE" && assignedId) {
+    st.sessions.delete(assignedId);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/http-server.ts` around lines 223 - 250, The current lookup reuses
sessions by mcp-session-id alone; change the logic so sessions are bound to the
bearer token: when creating a session in createSession, record the token on the
session object (e.g., session.token or session.ownerToken) and when looking up
an existing session (sessionIdHeader / existingId) ensure the incoming token
matches session.token before reusing it; register sessions in st.sessions under
a composite key or check token equality after lookup (use
session.transport.sessionId as the session id) and when handling teardown/DELETE
remove the stored session from st.sessions so the old token/state is evicted;
update the registration block that uses session.transport.sessionId to include
the token binding so subsequent lookups validate token equality.

Comment thread src/mcp/http-server.ts
Comment on lines +321 to +327
const close = async (): Promise<void> => {
await new Promise<void>((resolve, reject) =>
httpServer.close(err => (err ? reject(err) : resolve())),
);
for (const session of sessions.values()) {
await session.transport.close().catch(() => {});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close active transports before awaiting httpServer.close().

httpServer.close() waits for open /mcp SSE responses to finish. Those responses only terminate when session.transport.close() runs, so the current order can hang shutdown indefinitely under an active session.

Suggested fix
   const close = async (): Promise<void> => {
-    await new Promise<void>((resolve, reject) =>
-      httpServer.close(err => (err ? reject(err) : resolve())),
-    );
-    for (const session of sessions.values()) {
-      await session.transport.close().catch(() => {});
-    }
+    await Promise.all(
+      Array.from(sessions.values(), session => session.transport.close().catch(() => {})),
+    );
     sessions.clear();
+    await new Promise<void>((resolve, reject) =>
+      httpServer.close(err => (err ? reject(err) : resolve())),
+    );
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const close = async (): Promise<void> => {
await new Promise<void>((resolve, reject) =>
httpServer.close(err => (err ? reject(err) : resolve())),
);
for (const session of sessions.values()) {
await session.transport.close().catch(() => {});
}
const close = async (): Promise<void> => {
await Promise.all(
Array.from(sessions.values(), session => session.transport.close().catch(() => {})),
);
sessions.clear();
await new Promise<void>((resolve, reject) =>
httpServer.close(err => (err ? reject(err) : resolve())),
);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/http-server.ts` around lines 321 - 327, The shutdown order in the
close function is reversed: call session.transport.close() for every active
session first (iterating over sessions.values() and awaiting each
transport.close() with errors caught) and only after all transports have been
closed await the httpServer.close() Promise; update the close function
(referencing close, sessions, session.transport.close, and httpServer.close) to
perform transport shutdown before invoking and awaiting httpServer.close() to
prevent hanging SSE responses.

Comment on lines +38 to +40
afterEach(async () => {
await running.close();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard teardown when setup fails before assignment.

If beforeEach throws before running is set, afterEach can throw and obscure the real failure.

Minimal safeguard
 interface RunningServer {
   baseUrl: string;
   close: () => Promise<void>;
 }
...
 describe("HTTP MCP server", () => {
-  let running: RunningServer;
+  let running: RunningServer | undefined;
...
   afterEach(async () => {
-    await running.close();
+    await running?.close();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/shared/mcp-http-server.test.ts` around lines 38 - 40, The afterEach
teardown currently calls await running.close() unguarded and will throw if
beforeEach failed to assign running; update afterEach to first check that the
test server handle exists (e.g., if (running) or if (running != null)) before
calling close so teardown is skipped when setup failed; ensure the variable
referenced is the same test-scoped symbol named running used in
beforeEach/afterEach.

@kaghni

kaghni commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Status: how close to ChatGPT App Directory appearance

Engineering — done

Where Status
Hivemind privacy at deeplake.ai/privacy (incl. SOC-2, BYOC, full CCPA categories) deeplake-ui #284 open, awaiting legal review + merge
Footer + cookie banner point at /privacy (no external link) same PR included
HTTP MCP wrapper (serve-mcp, StreamableHTTPServerTransport, tool annotations) this PR 5/5 unit tests pass, Docker image builds, container boots, real-token end-to-end verified
Dockerfile + k8s manifest + SRE handoff doc this PR included (deploy/mcp/{Dockerfile,k8s.yaml,README.md})

Both PRs are ready for the review/merge cycle. CodeRabbit + codex haven't been triggered yet on either.

Remaining — 5 things, none mine alone

In strict critical-path order:

# Task Owner Time Blocks
1 PR #284 merge + ship so deeplake.ai/privacy returns 200 Legal review → merge a few days for legal Submission field "Privacy policy URL"
2 This PR merge + image build Code review → merge 1-2 days SRE deploy
3 Activeloop business verification in OpenAI Platform Dashboard Activeloop owner role ~1 hour one-time Submission
4 SRE deploy the MCP container (handoff doc in deploy/mcp/README.md) so api.deeplake.ai/mcp returns 401 + WWW-Authenticate to the public internet SRE 1-3 days after merge Auth0 callback URL, ChatGPT connector test, screenshots
5 Auth0 client registration (new "Hivemind for ChatGPT" app, auth-code+PKCE, callback https://chatgpt.com/aip/{g-app-id}/oauth/callback) Auth0 admin 15 min g-app-id comes from OpenAI after we save the app draft
6 Capture 5 ChatGPT screenshots (dev-mode connector against live URL) At a machine with a ChatGPT account 30 min App draft
7 Save + submit the OpenAI app draft Activeloop owner ~2 hours OpenAI review
8 OpenAI review OpenAI 2-5 business days SLA Listing goes live

Calendar estimate

Assuming nothing blocks:

  • Days 1-3: legal reviews #284 → merge → ship; code review on this PR → merge.
  • Day 3: SRE deploys MCP container; Auth0 admin registers client.
  • Day 4: screenshots against live URL; OpenAI app draft.
  • Day 5: submit.
  • Days 5-10: OpenAI review SLA.
  • ~Day 10: approved → appears in directory.

~1-2 weeks from today, gated by OpenAI's 2-5 business day review.

Parallelizable while waiting

  • coderabbit + codex review loop on this PR and #284 (per the pr-review-cycle playbook)
  • Pre-draft the OpenAI submission copy (name, tagline, long description with Beyond-Base-ChatGPT framing, per-tool annotation justifications) so submission day is mechanical

@kaghni kaghni marked this pull request as draft June 7, 2026 21:21
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.

2 participants