Skip to content

feat(server): remote HTTP MCP transport — dark/default-off (AGENT-5)#150

Merged
eliotlim merged 2 commits into
mainfrom
feat/agent-5-remote-mcp
Jul 13, 2026
Merged

feat(server): remote HTTP MCP transport — dark/default-off (AGENT-5)#150
eliotlim merged 2 commits into
mainfrom
feat/agent-5-remote-mcp

Conversation

@eliotlim

Copy link
Copy Markdown
Owner

What

Exposes OpenBook's own MCP server to external agents (Claude Desktop, Cursor, IDEs) over a remote streamable-HTTP transport at ALL /api/mcp. Board: AGENT-5 (Epic: Agent-first-class, Wave-2, owner-gated). Ships dark / default-off, loopback/LAN-only. (Public *.book.cloud exposure is deferred — a separate owner-gated design.)

Handler (packages/server/src/mcpHttp.ts, mounted after mountAiRoutes so the full AGENT-6 /api/* stack runs first)

  1. agentApi-off / OPENBOOK_AGENT_API=0404 (existence hidden).
  2. FORWARDED_HEADER present → 403 (belt-and-braces; AGENT-6 already 403s a forwarded PAT at resolution).
  3. No agentToken (PAT) → 401 — PAT only, never guest/jws/hatch.
  4. Loopback HttpDataClient('', …, {fetchImpl})fetchImpl = app.request(input, {headers built from scratch}): only Authorization: Bearer <same PAT> + preserved FORWARDED_HEADER; never the inbound headers, never LOCAL_OWNER_HEADER/identity.
  5. createOpenBookMcpServer(client, {allowDirectEdits:false}) + stateless WebStandardStreamableHTTPServerTransport({sessionIdGenerator:undefined, enableJsonResponse:true}), fresh per request, server.close() in finally.

store is referenced ONLY for the isAgentApiEnabled gate — never in the data path. The MCP server holds only the PAT-looped client, so an MCP bug can't out-privilege the PAT's REST access.

Security model proven by tests

  • Read PAT: read_page works; create_page/append_to_page → inner request scope-gated 403 → tool isError; asserted no page created, suggestions empty.
  • Write PAT: append_to_page1 queued suggestion (applyKind:append_blocks), page content unchanged (not applied); creation (non-destructive) still allowed.
  • Plus: dark-gate 404, no-PAT 401, forwarded-PAT refused, real initialize/tools/list handshake over the transport.

Notes

  • Forwarded PAT → 403 (AGENT-6 mw fires first), not 401 as the ticket said — either way refused; loopback/LAN-only holds structurally.
  • Dep: @book.dev/mcp added to packages/server (+ build:libs). The mcp↔server cycle is pnpm WARN-only (no nx complaint); harness unmoved.
  • Rate-limit (deferred LOW-3): each MCP call = 1 outer + N inner requests, all counting against the per-token 120/min limit — a heavy client could 429 sooner. Left untouched per the design's deferral.

Tests / verify

New mcpHttp.test.ts (10) incl. the two scope-enforcement proofs. pnpm verify exit 0; existing stdio MCP e2e 43/43 unchanged.

Mount OpenBook's own MCP server at ALL /api/mcp (new mcpHttp.ts), wired in
createApp beside mountAiRoutes, behind the AGENT-6 request pipeline.

- DARK by default: OPENBOOK_AGENT_API=0 kill-switch or agentApi-off → 404
  (existence hidden); requires a PAT (never guest/jws/hatch) → else 401;
  belt-and-braces forwarded-reject keeps it loopback/LAN-only.
- Load-bearing invariant: the handler holds ONLY a PAT-looped HttpDataClient
  (new HttpDataClient('', undefined, {fetchImpl})) that re-enters the app via
  app.request carrying ONLY Authorization: <same PAT> (+ preserved
  FORWARDED_HEADER) — never store/LocalDataClient, never LOCAL_OWNER_HEADER or
  identity JWS, never the inbound headers. Scope is enforced FOR FREE by the
  loop-back: a read PAT's write tool hits an inner 403; a write PAT's edits
  land as reviewable suggestions (allowDirectEdits:false).
- Stateless WebStandardStreamableHTTPServerTransport (sessionIdGenerator
  undefined, enableJsonResponse) — no session to hijack, fresh per request.
- Adds @book.dev/mcp to @book.dev/server (dev-only cycle for e2e) and builds it
  in build:libs before server.
- In-process HTTP e2e (mcpHttp.test.ts) mirroring the stdio e2e over HTTP plus
  the security assertions: dark-gate 404, no-PAT 401, forwarded reject,
  handshake/tools-list/read tools, read-PAT write refused, write-PAT →
  suggestion (visible in the review pane, not applied).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app.book.pub Ready Ready Preview, Comment Jul 13, 2026 12:53pm

Request Review

…(AGENT-5 review)

Review hardening (security SOUND, code APPROVE-WITH-NITS):
- Add a 1 MiB `bodyLimit` middleware fronting `/api/mcp` (DoS parity with the
  asset/relay/awareness routes) → oversized JSON-RPC body → 413.
- Regression test pinning the load-bearing invariant: an inbound MCP request that
  smuggles a VALID owner identity JWS AND the loopback-owner hatch secret alongside
  a non-owner PAT still cannot read/write an owner-only page — the loop-back runs as
  the PAT principal ONLY (inbound identity / LOCAL_OWNER headers never propagate;
  would also fail if the server were handed a store/LocalDataClient).
- Nits: reorder the handler so the agentApi gate is read only on the no-PAT path
  (no redundant re-read once agentToken is set) while still hiding existence (404)
  for a disabled probe; soften the overstated mount-ordering comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
@eliotlim eliotlim merged commit 6d184b6 into main Jul 13, 2026
10 checks passed
@eliotlim eliotlim deleted the feat/agent-5-remote-mcp branch July 13, 2026 13:11
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.

1 participant