Skip to content

fix(devframe): authenticate HTTP MCP requests - #327

Merged
antfu merged 6 commits into
mainfrom
fix/authenticate-mcp-http
Sep 3, 2026
Merged

fix(devframe): authenticate HTTP MCP requests#327
antfu merged 6 commits into
mainfrom
fix/authenticate-mcp-http

Conversation

@antfubot

@antfubot antfubot commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Why

The route-based MCP endpoint treated a caller-provided Origin as authorization. Origin is 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/hub enabled that route by default. Implements plans/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 against Authorization: Bearer <token>), a (request) => boolean callback, or explicit false for an origin-only local opt-out.
  • mcp: true is shorthand for the bearer read from DEVFRAME_MCP_AUTH_TOKEN. A missing/empty token, or an object config omitting authorization, fails startup with new diagnostic DF0077 instead of mounting an unauthenticated route.
  • Missing/invalid bearer → 401 + WWW-Authenticate: Bearer; disallowed origin stays 403. 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/hub flips its omitted MCP default from enabled to disabled; callers opt in with an explicit policy.
  • devframe connect reads DEVFRAME_MCP_AUTH_TOKEN and presents it as the bearer (never a CLI flag — args are process-visible). ConnectServerOptions.authToken accepts one token or a (record) => string | undefined resolver for fleets with distinct credentials.

Credentials live only in configuration and the Authorization header — 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 DF0077 error page, and the affected API snapshots are updated; runnable examples use an environment-backed policy (or an explicit loopback authorization: false).

Notes for reviewers

  • The DevframeNextHost.mountMcp options parameter is now required (it must carry authorization) — an intentional breaking narrowing captured in the @devframes/next/hub snapshot.
  • Full-workspace build/knip/typecheck can't complete in the sandbox (pre-existing: plugin-git build hits an EACCES, and starter/unbuilt-dist packages fail identically on main). 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.

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.
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
devframe Ready Ready Preview Sep 3, 2026 2:57am UTC

…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
@coldtea-pr-lens

coldtea-pr-lens Bot commented Sep 2, 2026

Copy link
Copy Markdown

◈ PR Lens

🟢 +0 new · 🟠 ~6 changed · 🔴 -0 removed · 2 flows · 23 files · commit ab4c17c


Architecture

Architecture diagram for devframes/devframe at ab4c17c

6 components touched across 4 lanes.

Open full size


Inside the changed components — 2 views

Component view — MCP HTTP Authentication Pipeline

Details the origin gating and bearer/callback authorization pipeline between CLI Connect and the Streamable-HTTP route handler.

Architecture view of Component view — MCP HTTP Authentication Pipeline in devframes/devframe

Component view — Hub & Host Framework MCP Integration

Details hub-level aggregate MCP mounting, DF8005 configuration validation, and Next.js framework host integration.

Architecture view of Component view — Hub & Host Framework MCP Integration in devframes/devframe

Data flow

Data flow diagram for devframes/devframe at ab4c17c

Authenticating and executing an MCP request · Mounting hub aggregate MCP and validating devframe configs

Open full size


The other flows — 1 sequence

Mounting hub aggregate MCP and validating devframe configs

Sequence diagram of Mounting hub aggregate MCP and validating devframe configs in devframes/devframe

Drill down
Client Runtimes & UI — 1 component
🟡 CHANGED CLI Connect Gateway

Discovers active devframe instances and establishes Streamable-HTTP client connections using loopback origin and DEVFRAME_MCP_AUTH_TOKEN bearer authorization headers.

Framework Kits & Hosts — 2 components
🟡 CHANGED Next.js Framework Kit

Hosts devframes inside Next.js App Router applications, exposing route handlers and making aggregate MCP endpoint mounting opt-in with authorization support.

🟡 CHANGED Next.js Reference Hub Host

Reference Next.js host application configuring aggregate devframe hub and MCP route parameters.

Hub Orchestration — 1 component
🟡 CHANGED Hub Core & Lifecycle

Mounts unified multi-devframe instances, resolves MCP authorization options, and emits DF8005 diagnostics when unconfigured hub drops devframe tools.

Devframe Core Engine — 2 components
🟡 CHANGED Single-Tool Instance Shell

Standalone dev server shell configuring runtime context, normalizing MCP authorization options, and mounting HTTP endpoints.

🟡 CHANGED MCP Fetch Handler & Auth Gate

Serves Streamable-HTTP MCP endpoints, enforcing loopback origin checks followed by timing-safe bearer token or callback authorization.


View

  • Architecture lens
  • Data flow lens
  • Expand every detail
  • Show unchanged neighbours

Tip

The boxes under View are live. Tick Architecture lens or Data flow lens to choose which diagrams appear, or Expand every detail to open every drill-down at once. The comment redraws in place a few seconds later.

🪧 More tips
  • Run PR Lens on your own machine: npx skills add coldteadotai/pr-lens installs the agent skill. Then tell your coding agent: "Diagram the change you just made with PR Lens and attach it to the pull request."
  • Draw a diff before it is even a pull request: npx @coldtea/pr-lens-cli analyze --base origin/main reads the diff with your own model key, and npx @coldtea/pr-lens-cli render .pr-lens/graph.json draws the same lenses on your machine.
  • Show unchanged neighbours lists the components this change did not touch alongside the ones it did, so the drill-down shows what the changed code sits next to.
  • GitHub will not let you zoom an image in a comment. The link under each diagram opens it full size on a page of its own, where you can.
  • The CLI's render picks up .github/pr-lens.yml automatically and applies your corrections (renames, exclusions, lane pins) at draw time.
  • Would you rather run it from CI on a key of your own? Add .github/workflows/pr-lens.yml with coldteadotai/pr-lens/packages/action@v0 and a model key in your repository secrets, say GEMINI_API_KEY. The Action asks Gemini by default, or OpenAI and any endpoint speaking /chat/completions through its provider input.
  • PR Lens is free for open source. A star on the repository is what keeps it going.
  • Push a new commit and the whole comment re-renders for the new head. An older run never overwrites a newer one, so a slow render cannot put a stale diagram back.
  • The diagrams follow your GitHub theme, so dark mode gets the dark render and light mode the light one, and the moving dots show this pull request's data in motion.

◈ Rendered by PR Lens · crafted with ❤️ by the Coldtea team · Something drawn wrong?

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
@antfu
antfu merged commit f92c2f8 into main Sep 3, 2026
13 of 14 checks passed
@antfu
antfu deleted the fix/authenticate-mcp-http branch September 3, 2026 03:01
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