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
{{ message }}
Repository navigation
Commit e6792d2
Browse filesBrowse the repository at this point in the historyBrowse files
refactor(devframe): trust same-machine MCP callers, make identity opt-in
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.
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
@@ -75,7 +75,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal
75
75
76
76
-**Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
77
77
-**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.
78
-
-**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).
78
+
-**The MCP route trusts same-machine callers, harden it when that's not your boundary.**The origin gate keeps browsers and remote hosts out (loopback-only, `Origin`-less rejected), so `mcp: true` is enough for a local dev tool.`Origin` proves nothing about *which* local process is calling, though — so when the route is reachable beyond loopback (a widened `allowedOrigins`, a hosted app) or exposes destructive tools, add an identity check with `mcp: { authorization }` (a bearer from an env var, or a callback). See[MCP](/adapters/mcp).
79
79
-**Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
80
80
-**Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
81
81
-**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
+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
@@ -82,7 +82,7 @@ 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.
85
+
The aggregate MCP route has its own origin gate, independent of this RPC Auth: `mcp: true` trusts same-machine callers, and `mcp: { authorization }` adds an identity check when the hub is reachable beyond loopback. A mounted devframe's own `mcp` setting is ignored — the hub exposes one aggregate route over them all, and warns ([`DF8005`](/errors/DF8005)) when a devframe asks for MCP while the hub's is off.
The dev server exposes the same MCP API over HTTP, live. Enable with `cli.mcp`:
21
+
The dev server exposes the same MCP API over HTTP, live. Enable with the top-level `mcp`:
22
22
23
23
```ts
24
24
import { defineDevframe } from'devframe'
25
25
26
26
exportdefaultdefineDevframe({
27
27
// …
28
-
cli: {
29
-
// Reads the bearer from DEVFRAME_MCP_AUTH_TOKEN.
30
-
mcp: true,
31
-
},
28
+
mcp: true,
32
29
})
33
30
```
34
31
35
32
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.
36
33
37
34
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
35
39
-
### Two gates: origin hardening and identity
36
+
### Origin gate, and opt-in identity
40
37
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'] } }`.
38
+
The **origin gate**guards every request: `Origin`must be loopback (or allow-listed), and `Origin`-less requests are rejected (a disallowed origin gets `403`). This is DNS-rebinding hardening that keeps browsers and remote hosts out, and it trusts same-machine callers — so `mcp: true` is all a local dev tool needs.
42
39
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:
40
+
`Origin` proves nothing about *who* is calling, though: a native process on the same box can send any `Origin`. When a same-machine process isn't your trust boundary — a LAN/tunnel origin, a shared/CI host, a destructive tool surface — layer on an **identity check** with `authorization`:
46
41
47
42
```ts
48
43
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):
A request presents the bearer as `Authorization: Bearer <token>`, matched in constant time; a missing or wrong bearer gets `401` with a `WWW-Authenticate: Bearer` challenge. The origin gate always runs first, so a disallowed origin is `403` regardless of the credential. Widen the origin allow-list for a tunnel/LAN reach with `mcp: { authorization: process.env.MY_TOKEN, allowedOrigins: ['https://tunnel.example.com'] }`.
54
+
60
55
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.
//Optional identity check on top of the origin gate; omit for origin-only.
83
+
//authorization: process.env.MY_TOKEN,
89
84
})
90
85
// route every method on /__mcp to mcp.fetch(request)
91
86
```
@@ -109,6 +104,6 @@ Two gateway tools (`devframe:connect:*` ids — see [tool ids and wire names](/g
109
104
110
105
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.
111
106
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.
107
+
Most instances trust same-machine callers, so the connector reaches them with no credential. For an instance you *hardened* with a bearer, the connector reads `DEVFRAME_MCP_AUTH_TOKEN` and presents it (never a CLI flag — command-line arguments are visible to other processes). Connect to a fleet with distinct credentials by driving `startConnectServer` with a per-instance `authToken` resolver.
113
108
114
109
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`| Expose the MCP route at `<base>__mcp`. `true`requires the `DEVFRAME_MCP_AUTH_TOKEN` bearer; `McpRouteOptions`carries an explicit `authorization`. |
45
+
|`mcp`|`def.mcp`| Expose the MCP route at `<base>__mcp`. `true`is origin-only (trusts same-machine callers); `McpRouteOptions`can add an `authorization` identity check. |
Copy file name to clipboardExpand all lines: docs/content/3.frameworks/3.next.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,7 +48,7 @@ export const GET = handler.fetch
48
48
|`port`| from `def.cli?.port`| Side-car port. |
49
49
|`flags`| — | Passed to `def.setup(ctx, { flags })`. |
50
50
|`auth`|`false`|`true` for the OTP gate, or a handler. |
51
-
|`mcp`|`def.cli?.mcp`| Expose the MCP route. `true`requires the `DEVFRAME_MCP_AUTH_TOKEN` bearer; `McpRouteOptions`carries an explicit `authorization`. |
51
+
|`mcp`|`def.mcp`| Expose the MCP route. `true`is origin-only (trusts same-machine callers); `McpRouteOptions`can add an `authorization` identity check. |
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 }`.
128
+
The aggregate MCP route is off by default. Opt in with `mcp: true` (origin-only, trusting same-machine callers), or `mcp: { authorization }` to add an identity check when the app is reachable beyond localhost.
129
129
130
130
No native hub UI provider here, so this scope stays quiet; `createDevframeNextHost()` is the low-level `DevframeHost`.
title: 'DF8005: Devframe MCP Ignored While Hub MCP Is Off'
3
+
description: 'Devframe "{id}" requests an MCP route, but the hub''s aggregate MCP is off — its tools are not exposed over MCP.'
4
+
---
5
+
6
+
## Message
7
+
8
+
> Devframe "`{id}`" requests an MCP route, but the hub's aggregate MCP is off — its tools are not exposed over MCP.
9
+
10
+
## Cause
11
+
12
+
A hub exposes **one aggregate MCP endpoint** over every mounted devframe (tool ids are already namespaced per plugin), so a mounted devframe's own `mcp` setting is ignored — the hub's own `mcp` governs the route. This warning fires when a devframe is mounted with `mcp` enabled (top-level `mcp`, or the deprecated `cli.mcp`) while the hub itself has no `mcp` configured, so that devframe's tools are not reachable over MCP.
13
+
14
+
## Example
15
+
16
+
```ts
17
+
initHub({
18
+
base: DEVFRAMES_HUB_BASE,
19
+
// No `mcp` here → no aggregate route…
20
+
devframes: [
21
+
myDevframe, // …but this devframe declares `mcp: true` → DF8005
22
+
],
23
+
})
24
+
```
25
+
26
+
## Fix
27
+
28
+
- Enable the hub's own aggregate MCP so the devframe's tools are surfaced: pass `mcp` to `initHub` (`mcp: true` for the loopback origin gate, or `mcp: { authorization }` to add an identity check).
29
+
- Or drop `mcp` from the mounted devframe to silence the warning — it has no effect inside a hub.
30
+
31
+
## Source
32
+
33
+
-[`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub()` emits this while mounting each devframe when the hub has no MCP but the devframe requests one.
0 commit comments