Skip to content

Commit b504186

Browse files
authored
feat(devframe): serve MCP by default when an agent surface exists (#332)
1 parent e597646 commit b504186

59 files changed

Lines changed: 459 additions & 1039 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 & 3 deletions
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) - 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.
@@ -118,8 +120,6 @@ const myDevframe = defineDevframe({ /* … */ })
118120
await createMcpServer(myDevframe, { transport: 'stdio' })
119121
```
120122

121-
`@modelcontextprotocol/server` is a peer dependency.
122-
123123
## Connecting Claude Desktop
124124

125125
In `claude_desktop_config.json`:

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/1.guide/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ The mounted devframes share one RPC registry, state store, connection, auth gate
114114
pnpm add devframe
115115
```
116116

117-
`devframe` ships ESM-only, no Vite dependency. Adapters with optional peers (the MCP adapter needs `@modelcontextprotocol/server`) surface the requirement at import time.
117+
`devframe` ships ESM-only, no Vite dependency. The CLI adapter's optional peer (`cac`) surfaces its requirement at import time.
118118

119119
## Hello, Devframe
120120

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,28 @@ import myDevframe from './my-tool'
1414
await createMcpServer(myDevframe, { transport: 'stdio' })
1515
```
1616

17-
`@modelcontextprotocol/server` is a peer dependency; `createMcpServer` serves `stdio` through the SDK's `serveStdio`, pinning one server instance per connection.
17+
`createMcpServer` serves `stdio` through the MCP SDK's `serveStdio`, pinning one server instance per connection.
1818

1919
## Route-based server
2020

21-
The dev server exposes the same MCP API over HTTP, live. Whether to expose it is a hosting decision, so pass `mcp` to `createCac` when you assemble the CLI (or 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) - flag your first function and the agent view is on. A devframe with nothing flagged mounts no route and loads no MCP code.
22+
23+
Pin the behavior where you host the tool - it's a hosting decision, so pass `mcp` to `createCac` when you assemble the CLI (or to `createDevServer` / `initDevframe` / `initHub` when you host it programmatically): `true` always mounts, `false` never mounts, an object customises the route:
2224

2325
```ts
2426
import { createCac } from 'devframe/adapters/cac'
2527
import myDevframe from './my-tool'
2628

27-
createCac(myDevframe, { mcp: true }).parse()
29+
createCac(myDevframe, { mcp: true }).parse() // force on; `false` forces off; omit for 'auto'
2830
```
2931

30-
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.
32+
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.
3133

3234
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.
3335

3436
### Origin gate, and opt-in identity
3537

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

3840
`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`:
3941

@@ -51,7 +53,7 @@ Never place the token in a URL, in `__connection.json`, in the instance registry
5153

5254
### Hosted bridges
5355

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

5658
```ts
5759
// Vite (@devframes/vite)
@@ -61,7 +63,7 @@ devframeViteBridge(myDevframe, { mcp: true })
6163
createDevframeNextHandler(myDevframe, { mcp: true })
6264
```
6365

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

6668
## Custom host frameworks
6769

docs/content/2.adapters/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description: 'The lowest-level path is the standard handler, initDevframe(def, {
77

88
The lowest-level path is [the standard handler](/adapters/initiate), `initDevframe(def, { base })`: a Web Standard `(request: Request) => Promise<Response>` for any catch-all route. Every path below builds on it.
99

10-
Adapters wrap it as `createXxx(def, options?)` at `devframe/adapters/<name>`. `cac` and `mcp` need an optional peer ([`cac`](https://github.com/cacjs/cac), [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk)).
10+
Adapters wrap it as `createXxx(def, options?)` at `devframe/adapters/<name>`. `cac` needs an optional peer ([`cac`](https://github.com/cacjs/cac)).
1111

1212
## Comparison
1313

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` | `'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` | `'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/DF0017.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ description: 'Failed to start MCP server ({transport}): {reason}'
1111

1212
The MCP server failed while initializing. Common reasons:
1313

14-
- The `@modelcontextprotocol/server` peer dependency is missing (the stdio and route-based transports both need it).
1514
- The stdio transport threw during `connect()` (e.g. stdin/stdout unavailable).
15+
- The MCP adapter module could not be loaded (e.g. a corrupted install missing `@modelcontextprotocol/server`).
1616

1717
## Fix
1818

19-
- **Missing SDK**: `pnpm add @modelcontextprotocol/server` in the package that imports `devframe/adapters/mcp` or enables `cli.mcp`.
2019
- **Transport failure**: inspect the underlying error attached as `cause`.
20+
- **Broken install**: reinstall dependencies so `@modelcontextprotocol/server` (a dependency of `devframe`) resolves.
2121

2222
## Source
2323

docs/content/6.errors/DF0046.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
---
22
title: 'DF0046: Connector Requires the MCP SDK'
3-
description: 'devframe connect requires the optional peer dependency @modelcontextprotocol/server: {reason}'
3+
description: 'devframe connect requires the optional peer dependency @modelcontextprotocol/client: {reason}'
44
---
55

66
## Message
77

8-
> `devframe connect` requires the optional peer dependency @modelcontextprotocol/server: `{reason}`
8+
> `devframe connect` requires the optional peer dependency @modelcontextprotocol/client: `{reason}`
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/client` could not be imported. The client SDK is an optional peer dependency of `devframe`: only the connector dials other instances, so only it needs the package installed.
1313

1414
## Fix
1515

1616
Install the SDK next to devframe and run the connector again:
1717

1818
```sh
19-
npm install @modelcontextprotocol/server
19+
npm install @modelcontextprotocol/client
2020
devframe connect
2121
```
2222

0 commit comments

Comments
 (0)