Skip to content

Commit 51623a2

Browse files
committed
feat(devframe): serve MCP by default when an agent surface exists
1 parent 4c3a22f commit 51623a2

49 files changed

Lines changed: 533 additions & 178 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/content/1.guide/14.security.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal
7575

7676
- **Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
7777
- **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 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).
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 the `'auto'` default - which mounts the route once agent tools exist - and `mcp: true` are 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), or turn the route off with `mcp: false`. See [MCP](/adapters/mcp).
7979
- **Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
8080
- **Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
8181
- **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.

‎docs/content/1.guide/15.agent-native.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ Every `ctx.rpc.sharedState` key is exposed as a `devframe://state/<key>` resourc
100100

101101
## Starting the MCP server
102102

103-
CLI:
103+
The dev server serves the agent surface over HTTP on its own: the `mcp: 'auto'` default mounts the route at `/__mcp` once anything above exists (an `agent`-flagged RPC, a registered tool or resource) and `@modelcontextprotocol/server` is installed - one flagged function is the whole setup. See the [MCP adapter](/adapters/mcp#route-based-server) for forcing it on or off and hardening the route.
104+
105+
For a stdio server instead, via the CLI:
104106

105107
```sh
106108
# Run your devtool with an MCP stdio server attached.

‎docs/content/1.guide/18.hub-initiate.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The advertised path is hub-base-absolute (`/__devframes/__ws`). Dev-reevaluated
3333

3434
## The namespace
3535

36-
The namespace serves the hub UI at `/` (the `ui.viewer` SPA, or an index document when headless) and each devframe's SPA at `<id>/` with its own `__connection.json` pointing at the shared socket. Hub-level endpoints sit alongside: `embedded.js` (the `ui.embedded` bootstrap), `__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, and the opt-in `__mcp`. The route table is in the [Hub API reference](/references/hub-api#hub-namespace-routes).
36+
The namespace serves the hub UI at `/` (the `ui.viewer` SPA, or an index document when headless) and each devframe's SPA at `<id>/` with its own `__connection.json` pointing at the shared socket. Hub-level endpoints sit alongside: `embedded.js` (the `ui.embedded` bootstrap), `__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, and `__mcp` (mounted by the `'auto'` default once agent tools exist). The route table is in the [Hub API reference](/references/hub-api#hub-namespace-routes).
3737

3838
Devframe ids become URL segments, validated: reserved names throw `DF8000`, non-route-safe `DF8004`.
3939

@@ -82,7 +82,7 @@ Registrations are validated fail-fast: one module per type (`DF8108`), an existi
8282

8383
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.
8484

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.
85+
The aggregate MCP route mounts through the `'auto'` default once any mounted devframe (or an agent-flagged hub command) exposes agent tools; `mcp: true` forces it on, `mcp: false` off. It has its own origin gate, independent of this RPC Auth: the mounted route 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 set `mcp: false`.
8686

8787
## Singular vs hub mounting
8888

‎docs/content/2.adapters/7.mcp.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,28 @@ await createMcpServer(myDevframe, { transport: 'stdio' })
1818

1919
## Route-based server
2020

21-
The dev server exposes the same MCP API over HTTP, live. Enable it with `cli.mcp` (or pass `mcp` to `createDevServer` / `initDevframe` / `initHub` when you host it programmatically):
21+
The dev server exposes the same MCP API over HTTP, live. The default setting is **`'auto'`**: the route mounts once the devframe exposes an agent surface (an `agent`-flagged RPC, a registered tool or resource) and `@modelcontextprotocol/server` is installed - flag your first function and the agent view is on. A devframe with nothing flagged mounts no route and loads no MCP code; a flagged surface whose peer is missing warns once ([`DF0077`](/errors/DF0077)) so you know what to install.
22+
23+
Pin the behavior with `cli.mcp` (or pass `mcp` to `createDevServer` / `initDevframe` / `initHub` when you host it programmatically): `true` always mounts (a missing peer becomes a startup failure, [`DF0017`](/errors/DF0017)), `false` never mounts, an object customises the route:
2224

2325
```ts
2426
import { defineDevframe } from 'devframe'
2527

2628
export default defineDevframe({
2729
/** … */
2830
cli: {
29-
mcp: true,
31+
mcp: true, // force on; `false` forces off; omit for 'auto'
3032
},
3133
})
3234
```
3335

34-
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+
The endpoint speaks Streamable-HTTP at `/__mcp` (`/__<id>/__mcp` under a host framework), sharing its origin/port. `--mcp` / `--no-mcp` override per run; `__connection.json` advertises the mounted route.
3537

3638
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, so 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.
3739

3840
### Origin gate, and opt-in identity
3941

40-
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+
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 - the `'auto'` default and `mcp: true` both mount origin-only, all a local dev tool needs.
4143

4244
`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`:
4345

@@ -57,7 +59,7 @@ Never place the token in a URL, in `__connection.json`, in the instance registry
5759

5860
### Hosted bridges
5961

60-
Both bridges forward it to their side-car dev server, advertising the endpoint in `__connection.json`:
62+
Both bridges forward the setting to their side-car dev server, advertising the mounted endpoint in `__connection.json`:
6163

6264
```ts
6365
// Vite (@devframes/vite)
@@ -67,7 +69,7 @@ devframeViteBridge(myDevframe, { mcp: true })
6769
createDevframeNextHandler(myDevframe, { mcp: true })
6870
```
6971

70-
Both honor the same contract: `mcp: true` is origin-only; add `mcp: { authorization }` to harden.
72+
Both honor the same contract: omitted is `'auto'`, `true` forces the origin-only route on; add `mcp: { authorization }` to harden.
7173

7274
## Custom host frameworks
7375

‎docs/content/3.frameworks/1.vite.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Devframe spawns a separate RPC + WS server and registers Vite middleware at `<ba
4242
| `host` | `def.cli?.host ?? 'localhost'` | Bind host for a pinned side-car. |
4343
| `flags` | none | To `def.setup(ctx, { flags })`. |
4444
| `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` is origin-only (trusts same-machine callers); `McpRouteOptions` can add an `authorization` identity check. |
45+
| `mcp` | `def.cli?.mcp`, then `'auto'` | Expose the MCP route at `<base>__mcp`. `'auto'` mounts once agent tools exist; `true` forces the origin-only route on (trusts same-machine callers); `McpRouteOptions` can add an `authorization` identity check. |
4646

4747
## `devframeVite`: convenience wrapper
4848

‎docs/content/3.frameworks/3.next.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const GET = handler.fetch
4848
| `port` | from `def.cli?.port` | Side-car port. |
4949
| `flags` | none | Passed to `def.setup(ctx, { flags })`. |
5050
| `auth` | `false` | `true` for the OTP gate, or a handler. |
51-
| `mcp` | `def.cli?.mcp` | Expose the MCP route. `true` is origin-only (trusts same-machine callers); `McpRouteOptions` can add an `authorization` identity check. |
51+
| `mcp` | `def.cli?.mcp`, then `'auto'` | Expose the MCP route. `'auto'` mounts once agent tools exist; `true` forces the origin-only route on (trusts same-machine callers); `McpRouteOptions` can add an `authorization` identity check. |
5252
| `key` | `@devframes/next:<id>:<base>` | `globalThis` memoization key. |
5353

5454
## Hosting a hub
@@ -125,7 +125,7 @@ export const POST = (req: Request) => hub.handler(req)
125125
export const DELETE = (req: Request) => hub.handler(req)
126126
```
127127

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.
128+
The aggregate MCP route mounts by default once any mounted devframe exposes agent tools (the `'auto'` setting). Force it on with `mcp: true` (origin-only, trusting same-machine callers), off with `mcp: false`, or add `mcp: { authorization }` for an identity check when the app is reachable beyond localhost.
129129

130130
No native hub UI provider here, so this scope stays quiet; `createDevframeNextHost()` is the low-level `DevframeHost`.
131131

‎docs/content/6.errors/DF0046.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: 'devframe connect requires the optional peer dependency @modelconte
99
1010
## Cause
1111

12-
`devframe connect` was started but `@modelcontextprotocol/server` could not be imported. The SDK is an optional peer dependency of `devframe`, keeping the MCP surface opt-in, so it only needs to be installed where MCP features are used.
12+
`devframe connect` was started but `@modelcontextprotocol/server` could not be imported. The SDK is an optional peer dependency of `devframe`, so it only needs to be installed where MCP features are used.
1313

1414
## Fix
1515

‎docs/content/6.errors/DF0077.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: 'DF0077: Agent Surface Without the MCP Peer'
3+
description: '"{id}" exposes agent tools and MCP defaults to `auto`, but the optional peer @modelcontextprotocol/server could not be imported, so no MCP route was mounted: {reason}'
4+
---
5+
6+
## Message
7+
8+
> "`{id}`" exposes agent tools and MCP defaults to `'auto'`, but the optional peer @modelcontextprotocol/server could not be imported, so no MCP route was mounted: `{reason}`
9+
10+
## Cause
11+
12+
The devframe (or hub) exposes an agent surface - an `agent`-flagged RPC function, or a tool / resource registered on `ctx.agent` - and the `mcp` setting is the `'auto'` default, which mounts the route once that surface exists. The optional peer `@modelcontextprotocol/server` could not be imported, so the tools have nowhere to be served and the route stays unmounted. The warning fires once per instance.
13+
14+
## Example
15+
16+
A definition flags a function for agents but the package that runs it has no MCP SDK installed:
17+
18+
```ts
19+
export const getState = defineRpcFunction({
20+
name: 'get-state',
21+
type: 'query',
22+
jsonSerializable: true,
23+
agent: { description: 'Read the current state.' }, // agent surface exists
24+
setup: ctx => ({ handler: () => ctx.cwd }),
25+
})
26+
```
27+
28+
## Fix
29+
30+
- Install the peer next to `devframe` so the `'auto'` default can serve the surface: `npm install @modelcontextprotocol/server`.
31+
- Or set `mcp: false` (on `cli.mcp`, or the `mcp` option of `initDevframe` / `initHub` / `createDevServer`) to keep the route off and silence the warning.
32+
33+
## Source
34+
35+
- [`packages/devframe/src/adapters/_shared.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/_shared.ts): `loadAutoMcpAdapter()` warns this when the `'auto'` default finds a non-empty agent surface but the peer fails to import; consumed by `initDevframe` and `initHub`.

‎docs/content/6.errors/DF8005.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
---
22
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, so its tools are not exposed over MCP.'
3+
description: 'Devframe "{id}" requests an MCP route, but the hub''s aggregate MCP is off (`mcp: false`), so its tools are not exposed over MCP.'
44
---
55

66
## Message
77

8-
> Devframe "`{id}`" requests an MCP route, but the hub's aggregate MCP is off, so its tools are not exposed over MCP.
8+
> Devframe "`{id}`" requests an MCP route, but the hub's aggregate MCP is off (`mcp: false`), so its tools are not exposed over MCP.
99
1010
## Cause
1111

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 `cli.mcp` enabled while the hub itself has no `mcp` configured, so that devframe's tools are not reachable over MCP.
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 `cli.mcp` enabled while the hub set `mcp: false`, so that devframe's tools are not reachable over MCP.
1313

1414
## Example
1515

16-
The hub below has no `mcp`, so no aggregate route is mounted, but a mounted devframe declares `cli.mcp: true`:
16+
The hub below turned MCP off, but a mounted devframe declares `cli.mcp: true`:
1717

1818
```ts
1919
initHub({
2020
base: DEVFRAMES_HUB_BASE,
21+
mcp: false,
2122
devframes: [myDevframe], // myDevframe sets `cli.mcp: true`, so DF8005
2223
})
2324
```
2425

2526
## Fix
2627

27-
- 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).
28+
- Drop `mcp: false` from `initHub`: the `'auto'` default mounts the aggregate route once agent tools exist, and `mcp: true` / `mcp: { authorization }` force or harden it.
2829
- Or drop `mcp` from the mounted devframe to silence the warning; it has no effect inside a hub.
2930

3031
## Source
3132

32-
- [`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.
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 turned MCP off but the devframe requests one.

‎docs/content/6.errors/index.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ Emitted by `devframe`: the framework-neutral host, RPC, streaming, assets, servi
8181
| [DF0072](/errors/DF0072) | warn | Snapshot Names Unknown RPC Method |
8282
| [DF0073](/errors/DF0073) | error | JSON-Render Spec Does Not Match Its Schema |
8383
| [DF0074](/errors/DF0074) | error | JSON-Render Schema Is Asynchronous |
84+
| [DF0075](/errors/DF0075) | warn | No RPC Transport On This Runtime |
85+
| [DF0076](/errors/DF0076) | error | WebSocket Upgrade Unsupported On This Runtime |
86+
| [DF0077](/errors/DF0077) | warn | Agent Surface Without the MCP Peer |
8487

8588
## Hub: context & lifecycle (DF80xx)
8689

0 commit comments

Comments
 (0)