feat(server): remote HTTP MCP transport — dark/default-off (AGENT-5)#150
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…(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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.cloudexposure is deferred — a separate owner-gated design.)Handler (
packages/server/src/mcpHttp.ts, mounted aftermountAiRoutesso the full AGENT-6/api/*stack runs first)agentApi-off /OPENBOOK_AGENT_API=0→ 404 (existence hidden).FORWARDED_HEADERpresent → 403 (belt-and-braces; AGENT-6 already 403s a forwarded PAT at resolution).agentToken(PAT) → 401 — PAT only, never guest/jws/hatch.HttpDataClient('', …, {fetchImpl})—fetchImpl=app.request(input, {headers built from scratch}): onlyAuthorization: Bearer <same PAT>+ preservedFORWARDED_HEADER; never the inbound headers, neverLOCAL_OWNER_HEADER/identity.createOpenBookMcpServer(client, {allowDirectEdits:false})+ statelessWebStandardStreamableHTTPServerTransport({sessionIdGenerator:undefined, enableJsonResponse:true}), fresh per request,server.close()infinally.storeis referenced ONLY for theisAgentApiEnabledgate — 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_pageworks;create_page/append_to_page→ inner request scope-gated 403 → toolisError; asserted no page created, suggestions empty.append_to_page→ 1 queued suggestion (applyKind:append_blocks), page content unchanged (not applied); creation (non-destructive) still allowed.initialize/tools/listhandshake over the transport.Notes
@book.dev/mcpadded topackages/server(+build:libs). The mcp↔server cycle is pnpm WARN-only (no nx complaint); harness unmoved.Tests / verify
New
mcpHttp.test.ts(10) incl. the two scope-enforcement proofs.pnpm verifyexit 0; existing stdio MCP e2e 43/43 unchanged.