fix(devframe): authenticate HTTP MCP requests - #327
Conversation
The route-based MCP endpoint treated a caller-provided Origin as authorization, so any local process (or a native client spoofing an Origin) could invoke privileged agent tools. Origin is DNS-rebinding hardening, not identity. Add an independent identity gate to the MCP route, checked after the origin gate: - McpRouteOptions.authorization: a bearer token string (constant-time compared), a (request) => boolean callback, or false for an origin-only local opt-out. - mcp: true is shorthand for the bearer read from DEVFRAME_MCP_AUTH_TOKEN; a missing token or an object without authorization fails startup with new diagnostic DF0077 rather than mounting an unauthenticated route. - Missing/invalid bearer -> 401 + WWW-Authenticate: Bearer; disallowed origin stays 403. A callback governs identity only and cannot relax the origin gate. - @devframes/next/hub now defaults MCP to disabled; callers opt in with an explicit policy. - devframe connect reads DEVFRAME_MCP_AUTH_TOKEN and presents it as the bearer; ConnectServerOptions.authToken accepts one token or a per-instance resolver. Credentials live only in configuration and the Authorization header. Created with the help of an agent.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…nnect flow The hub-next example now enables its aggregate MCP route with the env-backed mcp: true policy, so the Next server needs the bearer at boot and the connect spec's spawned connector needs the same one. Share one token between playwright.config's hub-next webServer env and the withConnectClient spawn env.
…http # Conflicts: # packages/devframe/src/adapters/mcp/fetch.ts
◈ PR Lens
Architecture 6 components touched across 4 lanes. Inside the changed components — 2 viewsComponent view — MCP HTTP Authentication Pipeline Details the origin gating and bearer/callback authorization pipeline between CLI Connect and the Streamable-HTTP route handler. Component view — Hub & Host Framework MCP Integration Details hub-level aggregate MCP mounting, DF8005 configuration validation, and Next.js framework host integration. Data flow
The other flows — 1 sequence
Drill down
|
Reshape the route-based MCP auth model per review: the origin gate already keeps browsers and remote hosts out, so a same-machine caller is trusted by default. mcp: true is now origin-only (no bearer, no env var), and authorization (bearer or callback) is opt-in hardening for when a same-machine process is not the trust boundary (LAN/tunnel origin, shared/CI host, destructive tools). - McpRouteOptions.authorization is optional, defaulting to origin-only; drop the DEVFRAME_MCP_AUTH_TOKEN server shorthand and the DF0077 startup failure. - Promote mcp to a top-level DevframeDefinition.mcp option; cli.mcp stays as a deprecated fallback (mirrors clientAssets/cli.distDir). - createMcpFetchHandler / DevframeNextHost.mountMcp authorization is optional again (reverts the breaking narrowing). - Hub: warn (DF8005) when a mounted devframe requests MCP but the hub's aggregate MCP is off, since the hub's single route governs it. - Revert the now-unneeded e2e/example bearer plumbing; the connector keeps authToken as an opt-in for hardened instances. Created with the help of an agent.
MCP exposure is a hosting decision, so it stays on the host entry points (initDevframe / initHub / createDevServer / bridges) and the standalone CLI config (cli.mcp), not a top-level DevframeDefinition field. Reverts the top-level DevframeDefinition.mcp addition; cli.mcp remains the definition-side default that createCac reads. Created with the help of an agent.
…http # Conflicts: # docs/content/1.guide/14.security.md # docs/content/2.adapters/7.mcp.md # examples/files-inspector/src/devframe.ts # examples/hub-next/src/client/devframe/next-devframe-hub.ts # packages/devframe/src/adapters/initiate.ts # packages/devframe/src/adapters/mcp/__tests__/mcp-http.test.ts # packages/devframe/src/adapters/mcp/fetch.ts # packages/devframe/src/types/devframe.ts # packages/hub/src/node/initiate.ts # packages/next/src/hub.ts # plans/README.md
Why
The route-based MCP endpoint treated a caller-provided
Originas authorization.Originis DNS-rebinding/cross-site hardening, but a native client can send any value — so a reachable route could invoke privileged agent tools without proving identity.@devframes/next/hubenabled that route by default. Implementsplans/002-authenticate-mcp-http.md.What changed
An independent identity gate now guards the MCP route, checked after the origin gate (the two stay separate defenses):
McpRouteOptions.authorization— a non-empty bearer token string (constant-time compared againstAuthorization: Bearer <token>), a(request) => booleancallback, or explicitfalsefor an origin-only local opt-out.mcp: trueis shorthand for the bearer read fromDEVFRAME_MCP_AUTH_TOKEN. A missing/empty token, or an object config omittingauthorization, fails startup with new diagnosticDF0077instead of mounting an unauthenticated route.401+WWW-Authenticate: Bearer; disallowed origin stays403. Origin runs first; a callback governs identity only and cannot relax the origin gate. Responses are generic — nothing reveals whether a token was close.@devframes/next/hubflips its omitted MCP default from enabled to disabled; callers opt in with an explicit policy.devframe connectreadsDEVFRAME_MCP_AUTH_TOKENand presents it as the bearer (never a CLI flag — args are process-visible).ConnectServerOptions.authTokenaccepts one token or a(record) => string | undefinedresolver for fleets with distinct credentials.Credentials live only in configuration and the
Authorizationheader — never in URLs, connection metadata, the instance registry, logs, diagnostics, or command-line arguments.Docs (security, hub-initiate, MCP adapter, Vite/Next framework pages), the
DF0077error page, and the affected API snapshots are updated; runnable examples use an environment-backed policy (or an explicit loopbackauthorization: false).Notes for reviewers
DevframeNextHost.mountMcpoptions parameter is now required (it must carryauthorization) — an intentional breaking narrowing captured in the@devframes/next/hubsnapshot.build/knip/typecheckcan't complete in the sandbox (pre-existing:plugin-gitbuild hits anEACCES, andstarter/unbuilt-distpackages fail identically onmain). The changed packages (devframe,@devframes/hub,@devframes/next) build and typecheck clean, and all targeted MCP/host/connector test suites pass.This PR was created with the help of an agent.