Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/1.guide/14.security.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal

- **Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
- **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.
- **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).
- **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).
- **Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
- **Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
- **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.
Expand Down
6 changes: 3 additions & 3 deletions docs/content/1.guide/15.agent-native.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ Every `ctx.rpc.sharedState` key is exposed as a `devframe://state/<key>` resourc

## Starting the MCP server

CLI:
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.

For a stdio server instead, via the CLI:

```sh
# Run your devtool with an MCP stdio server attached.
Expand All @@ -118,8 +120,6 @@ const myDevframe = defineDevframe({ /* … */ })
await createMcpServer(myDevframe, { transport: 'stdio' })
```

`@modelcontextprotocol/server` is a peer dependency.

## Connecting Claude Desktop

In `claude_desktop_config.json`:
Expand Down
4 changes: 2 additions & 2 deletions docs/content/1.guide/18.hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The advertised path is hub-base-absolute (`/__devframes/__ws`). Dev-reevaluated

## The namespace

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).
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).

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

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

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.

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 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`.

## Singular vs hub mounting

Expand Down
2 changes: 1 addition & 1 deletion docs/content/1.guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ The mounted devframes share one RPC registry, state store, connection, auth gate
pnpm add devframe
```

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

## Hello, Devframe

Expand Down
16 changes: 9 additions & 7 deletions docs/content/2.adapters/7.mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,28 @@ import myDevframe from './my-tool'
await createMcpServer(myDevframe, { transport: 'stdio' })
```

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

## Route-based server

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):
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.

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:

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

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

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

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.

### Origin gate, and opt-in identity

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

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

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

### Hosted bridges

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

```ts
// Vite (@devframes/vite)
Expand All @@ -61,7 +63,7 @@ devframeViteBridge(myDevframe, { mcp: true })
createDevframeNextHandler(myDevframe, { mcp: true })
```

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

## Custom host frameworks

Expand Down
2 changes: 1 addition & 1 deletion docs/content/2.adapters/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: 'The lowest-level path is the standard handler, initDevframe(def, {

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.

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)).
Adapters wrap it as `createXxx(def, options?)` at `devframe/adapters/<name>`. `cac` needs an optional peer ([`cac`](https://github.com/cacjs/cac)).

## Comparison

Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.frameworks/1.vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Devframe spawns a separate RPC + WS server and registers Vite middleware at `<ba
| `host` | `def.cli?.host ?? 'localhost'` | Bind host for a pinned side-car. |
| `flags` | none | To `def.setup(ctx, { flags })`. |
| `auth` | gated (interactive OTP) | `false` to opt out, or a `DevframeAuthHandler` for a custom scheme. |
| `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. |
| `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. |

## `devframeVite`: convenience wrapper

Expand Down
4 changes: 2 additions & 2 deletions docs/content/3.frameworks/3.next.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const GET = handler.fetch
| `port` | from `def.cli?.port` | Side-car port. |
| `flags` | none | Passed to `def.setup(ctx, { flags })`. |
| `auth` | `false` | `true` for the OTP gate, or a handler. |
| `mcp` | `def.cli?.mcp` | Expose the MCP route. `true` is origin-only (trusts same-machine callers); `McpRouteOptions` can add an `authorization` identity check. |
| `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. |
| `key` | `@devframes/next:<id>:<base>` | `globalThis` memoization key. |

## Hosting a hub
Expand Down Expand Up @@ -125,7 +125,7 @@ export const POST = (req: Request) => hub.handler(req)
export const DELETE = (req: Request) => hub.handler(req)
```

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

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

Expand Down
4 changes: 2 additions & 2 deletions docs/content/6.errors/DF0017.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ description: 'Failed to start MCP server ({transport}): {reason}'

The MCP server failed while initializing. Common reasons:

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

## Fix

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

## Source

Expand Down
8 changes: 4 additions & 4 deletions docs/content/6.errors/DF0046.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
---
title: 'DF0046: Connector Requires the MCP SDK'
description: 'devframe connect requires the optional peer dependency @modelcontextprotocol/server: {reason}'
description: 'devframe connect requires the optional peer dependency @modelcontextprotocol/client: {reason}'
---

## Message

> `devframe connect` requires the optional peer dependency @modelcontextprotocol/server: `{reason}`
> `devframe connect` requires the optional peer dependency @modelcontextprotocol/client: `{reason}`

## Cause

`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.
`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.

## Fix

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

```sh
npm install @modelcontextprotocol/server
npm install @modelcontextprotocol/client
devframe connect
```

Expand Down
2 changes: 1 addition & 1 deletion docs/content/6.errors/DF0051.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The `devframe connect` gateway tool `devframe_connect_call-tool` targeted a live

## Fix

Restart the instance with the `--mcp` flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again.
Restart the instance with the `--mcp` flag (or pass `mcp: true` to `createCac` / its programmatic host) to expose its tools, then list instances again.

## Source

Expand Down
15 changes: 8 additions & 7 deletions docs/content/6.errors/DF8005.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
---
title: 'DF8005: Devframe MCP Ignored While Hub MCP Is Off'
description: 'Devframe "{id}" requests an MCP route, but the hub''s aggregate MCP is off, so its tools are not exposed over MCP.'
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.'
---

## Message

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

## Cause

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.
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 mounted devframe's definition enables MCP through the deprecated `cli.mcp` field while the hub set `mcp: false`, so that devframe's tools are not reachable over MCP.

## Example

The hub below has no `mcp`, so no aggregate route is mounted, but a mounted devframe declares `cli.mcp: true`:
The hub below turned MCP off, but a mounted devframe's definition requests MCP:

```ts
initHub({
base: DEVFRAMES_HUB_BASE,
devframes: [myDevframe], // myDevframe sets `cli.mcp: true`, so DF8005
mcp: false,
devframes: [myDevframe], // myDevframe's definition requests MCP, so DF8005
})
```

## Fix

- 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).
- 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.
- Or drop `mcp` from the mounted devframe to silence the warning; it has no effect inside a hub.

## Source

- [`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.
- [`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.
2 changes: 2 additions & 0 deletions docs/content/6.errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Emitted by `devframe`: the framework-neutral host, RPC, streaming, assets, servi
| [DF0072](/errors/DF0072) | warn | Snapshot Names Unknown RPC Method |
| [DF0073](/errors/DF0073) | error | JSON-Render Spec Does Not Match Its Schema |
| [DF0074](/errors/DF0074) | error | JSON-Render Schema Is Asynchronous |
| [DF0075](/errors/DF0075) | warn | No RPC Transport On This Runtime |
| [DF0076](/errors/DF0076) | error | WebSocket Upgrade Unsupported On This Runtime |

## Hub: context & lifecycle (DF80xx)

Expand Down
2 changes: 1 addition & 1 deletion docs/content/8.references/6.hub-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ What `initHub()` serves under its `base`: [The namespace](/guide/hub-initiate#th
| `__ws` | WebSocket upgrade route |
| `__index.json` | machine-readable index: mounted devframes, endpoints |
| `__client-imports.js` | dock client-script import map for hub UI providers |
| `__mcp` | aggregate MCP endpoint over the tool registry (opt-in `mcp`) |
| `__mcp` | aggregate MCP endpoint over the tool registry (`mcp: 'auto'` default: mounted once agent tools exist) |

## Client runtime options

Expand Down
Loading
Loading