You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: docs/content/1.guide/14.security.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -73,7 +73,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal
73
73
74
74
-**Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
75
75
-**Keep `auth: false` local.** The hosted bridges (`devframeViteBridge`, `@devframes/next`'s handler) gate their side-car by default; opt out with an explicit `auth: false` only when the host framework owns the trust boundary another way.
76
-
-**The MCP route requires an origin.**The route-based MCP server rejects requests without a loopback or allow-listed `Origin`, so an arbitrary local process can't reach it — see [MCP](/adapters/mcp).
76
+
-**The MCP route authenticates the caller.**`Origin` hardens the route-based MCP server against DNS-rebinding, but proves nothing about identity — a native client can send any `Origin`. So the route also requires a bearer: `mcp: true` reads it from `DEVFRAME_MCP_AUTH_TOKEN`, and the route refuses to mount ([`DF0077`](/errors/DF0077)) without a policy. Treat the two checks as separate defenses — see [MCP](/adapters/mcp).
77
77
-**Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
78
78
-**Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
79
79
-**Origin-lock remote docks.** When a hub embeds a remote-UI dock, keep `originLock` on (the default) so its session token is only honored on a connection whose `Origin` matches the dock's own.
Copy file name to clipboardExpand all lines: docs/content/1.guide/18.hub-initiate.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,6 +82,8 @@ Registrations are validated fail-fast: one module per type (`DF8108`), an existi
82
82
83
83
The hub's **single Auth** is one gate at the shared transport for every mounted devframe, built-ins, and the MCP route; one handshake (OTP, magic link, or pre-shared token) unlocks the namespace; `auth: false` disables it for localhost.
84
84
85
+
The aggregate MCP route carries its **own** identity gate independent of this RPC Auth, since it grants agent clients privileged tool access: `mcp: true` requires the `DEVFRAME_MCP_AUTH_TOKEN` bearer (startup fails with [`DF0077`](/errors/DF0077) without it), or pass `mcp: { authorization }` explicitly. `Origin` remains request hardening, not identity.
86
+
85
87
## Singular vs hub mounting
86
88
87
89
A devframe's SPA and RPC client are byte-identical in both cases; only the environment differs:
Copy file name to clipboardExpand all lines: docs/content/2.adapters/7.mcp.md
+31-1Lines changed: 31 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,14 +26,38 @@ import { defineDevframe } from 'devframe'
26
26
exportdefaultdefineDevframe({
27
27
// …
28
28
cli: {
29
+
// Reads the bearer from DEVFRAME_MCP_AUTH_TOKEN.
29
30
mcp: true,
30
31
},
31
32
})
32
33
```
33
34
34
35
The endpoint speaks Streamable-HTTP at `/__mcp` (`/__<id>/__mcp` under a host framework), sharing its origin/port. `--mcp` / `--no-mcp` override; `__connection.json` advertises it.
35
36
36
-
The endpoint is **stateless**: it serves the [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28) per request through the SDK's `createMcpHandler`, building a fresh MCP server for each request — every HTTP request stands alone, with no `Mcp-Session-Id` to correlate. 2025-era clients are still served through the SDK's stateless legacy path. An origin gate requires `Origin` be loopback (or allow-listed) and rejects `Origin`-less requests. Widen for a tunnel/LAN origin with `cli: { mcp: { allowedOrigins: ['https://tunnel.example.com'] } }`.
37
+
The endpoint is **stateless**: it serves the [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28) per request through the SDK's `createMcpHandler`, building a fresh MCP server for each request — every HTTP request stands alone, with no `Mcp-Session-Id` to correlate. 2025-era clients are still served through the SDK's stateless legacy path.
38
+
39
+
### Two gates: origin hardening and identity
40
+
41
+
The route exposes privileged agent tools, so every request clears two independent gates. The **origin gate** requires `Origin` be loopback (or allow-listed) and rejects `Origin`-less requests — DNS-rebinding hardening that proves nothing about *who* is calling, since a native client can send any `Origin`. Widen it for a tunnel/LAN origin with `cli: { mcp: { authorization: process.env.MY_TOKEN, allowedOrigins: ['https://tunnel.example.com'] } }`.
42
+
43
+
The **identity gate** then proves the caller. `mcp: true` reads its bearer from the `DEVFRAME_MCP_AUTH_TOKEN` environment variable; a request presents it as `Authorization: Bearer <token>` and it is matched in constant time. A missing or wrong bearer gets `401` with a `WWW-Authenticate: Bearer` challenge; a disallowed origin gets `403`. Startup fails with [`DF0077`](/errors/DF0077) — the route is never mounted — when `mcp: true` finds no environment token, or an object config omits `authorization`.
44
+
45
+
An object config sets the policy explicitly:
46
+
47
+
```ts
48
+
exportdefaultdefineDevframe({
49
+
cli: {
50
+
// A bearer from your own environment variable:
51
+
mcp: { authorization: process.env.MY_TOKEN },
52
+
// — or a callback identity check (governs identity only; it cannot relax the origin gate):
// — or an origin-only opt-out for a loopback-bound local tool that owns its trust boundary another way:
55
+
// mcp: { authorization: false },
56
+
},
57
+
})
58
+
```
59
+
60
+
Never place the token in a URL, in `__connection.json`, in the instance registry, in logs, or on the command line — it belongs only in configuration and the `Authorization` header.
// route every method on /__mcp to mcp.fetch(request)
63
91
```
@@ -81,4 +109,6 @@ Two gateway tools (`devframe:connect:*` ids — see [tool ids and wire names](/g
81
109
82
110
Discovery reads the **instance registry**: every `createDevServer` writes `~/.devframe/instances/<pid>-<port>.json`, dialed with a loopback origin. In-process host frameworks register via `registerDevframeInstance` (`devframe/node`). `--port <n>` probes a port; `DEVFRAME_INSTANCES_DIR` relocates the registry, `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` opts out.
83
111
112
+
The connector reads `DEVFRAME_MCP_AUTH_TOKEN` and presents it as the bearer to each instance's authenticated route (never a CLI flag — command-line arguments are visible to other processes). An instance whose route requires a different bearer reports auth-required rather than being reached; connect to a fleet with distinct credentials by driving `startConnectServer` with a per-instance `authToken` resolver.
113
+
84
114
See [Agent-Native](/guide/agent-native) for the API and safety model.
Copy file name to clipboardExpand all lines: docs/content/3.frameworks/1.vite.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,7 +42,7 @@ Devframe spawns a separate RPC + WS server and registers Vite middleware at `<ba
42
42
|`host`|`def.cli?.host ?? 'localhost'`| Bind host for a pinned side-car. |
43
43
|`flags`| — | To `def.setup(ctx, { flags })`. |
44
44
|`auth`| gated (interactive OTP) |`false` to opt out, or a `DevframeAuthHandler` for a custom scheme. |
45
-
|`mcp`|`def.cli?.mcp`|`true` or `McpRouteOptions` to expose the MCP route at `<base>__mcp`. |
45
+
|`mcp`|`def.cli?.mcp`|Expose the MCP route at `<base>__mcp`. `true` requires the `DEVFRAME_MCP_AUTH_TOKEN` bearer; `McpRouteOptions` carries an explicit `authorization`. |
The aggregate MCP route is off by default — it exposes privileged agent tools. Opt in with an authorization policy: `mcp: true` (requiring the `DEVFRAME_MCP_AUTH_TOKEN` bearer) or `mcp: { authorization }`.
129
+
127
130
No native hub UI provider here, so this scope stays quiet; `createDevframeNextHost()` is the low-level `DevframeHost`.
description: 'The route-based MCP server needs an authorization policy, but none is configured.'
4
+
---
5
+
6
+
## Message
7
+
8
+
> The route-based MCP server needs an authorization policy, but none is configured — refusing to mount an unauthenticated agent endpoint.
9
+
10
+
## Cause
11
+
12
+
The route-based MCP endpoint exposes privileged agent tools to any process that can reach it. `Origin` hardens the request against DNS-rebinding but proves nothing about *who* is calling, so the route also requires an identity policy. This diagnostic fires when that policy is absent:
13
+
14
+
-`mcp: true` (the shorthand) reads its bearer from the `DEVFRAME_MCP_AUTH_TOKEN` environment variable, and the variable is missing or empty.
15
+
- An object MCP config omits the required `authorization` field (or sets it to an empty string).
16
+
17
+
Startup fails and the route is never mounted, rather than exposing the endpoint unauthenticated.
18
+
19
+
## Example
20
+
21
+
```ts
22
+
// ✗ throws DF0077 when DEVFRAME_MCP_AUTH_TOKEN is unset
23
+
awaitcreateDevServer(def, { mcp: true })
24
+
25
+
// ✗ throws DF0077 — object config with no authorization
- Set the `DEVFRAME_MCP_AUTH_TOKEN` environment variable to the bearer the `mcp: true` shorthand requires.
45
+
- Or pass an explicit `authorization` on the MCP options — a non-empty bearer token string, a `(request) => boolean` callback, or `false` for an origin-only local opt-out.
46
+
47
+
## Source
48
+
49
+
-[`packages/devframe/src/adapters/_shared.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/_shared.ts) — `resolveMcpConfig()` throws this when the `mcp: true` shorthand has no environment token, or an object config omits `authorization`.
0 commit comments