Skip to content

Commit dec5f0e

Browse files
committed
refactor(devframe): keep MCP config off the top-level definition
MCP exposure is a hosting decision, so it stays on the host entry points (initDevframe / initHub / createDevServer / bridges) and the standalone CLI config (cli.mcp), not a top-level DevframeDefinition field. Reverts the top-level DevframeDefinition.mcp addition; cli.mcp remains the definition-side default that createCac reads. Created with the help of an agent.
1 parent e6792d2 commit dec5f0e

14 files changed

Lines changed: 56 additions & 62 deletions

File tree

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

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

1919
## Route-based server
2020

21-
The dev server exposes the same MCP API over HTTP, live. Enable with the top-level `mcp`:
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):
2222

2323
```ts
2424
import { defineDevframe } from 'devframe'
2525

2626
export default defineDevframe({
2727
// …
28-
mcp: true,
28+
cli: {
29+
mcp: true,
30+
},
2931
})
3032
```
3133

@@ -41,12 +43,14 @@ The **origin gate** guards every request: `Origin` must be loopback (or allow-li
4143

4244
```ts
4345
export default defineDevframe({
44-
// A bearer token, backed by your own environment variable (never a literal):
45-
mcp: { authorization: process.env.MY_TOKEN },
46-
// — or a callback identity check (governs identity only; it cannot relax the origin gate):
47-
// mcp: { authorization: request => isTrusted(request) },
48-
// — or the explicit default, origin-only:
49-
// mcp: { authorization: false },
46+
cli: {
47+
// A bearer token, backed by your own environment variable (never a literal):
48+
mcp: { authorization: process.env.MY_TOKEN },
49+
// — or a callback identity check (governs identity only; it cannot relax the origin gate):
50+
// mcp: { authorization: request => isTrusted(request) },
51+
// — or the explicit default, origin-only:
52+
// mcp: { authorization: false },
53+
},
5054
})
5155
```
5256

‎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` | — | To `def.setup(ctx, { flags })`. |
4444
| `auth` | gated (interactive OTP) | `false` to opt out, or a `DevframeAuthHandler` for a custom scheme. |
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. |
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. |
4646

4747
## `devframeVite` — convenience wrapper
4848

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

Lines changed: 1 addition & 1 deletion
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` | — | Passed to `def.setup(ctx, { flags })`. |
5050
| `auth` | `false` | `true` for the OTP gate, or a handler. |
51-
| `mcp` | `def.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` | Expose the MCP route. `true` is origin-only (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

‎examples/files-inspector/src/devframe.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ export default defineDevframe({
1717
icon: 'ph:folder-open-duotone',
1818
basePath: BASE_PATH,
1919
clientAssets: distDir,
20-
// Serve the agent surface over the dev server's `/__mcp` route and register
21-
// the instance for `devframe connect` discovery. This demo binds to loopback
22-
// (`localhost:9876`), so `mcp: true` (the loopback origin gate, trusting
23-
// same-machine callers) is enough - no bearer plumbing. A network-reachable
24-
// tool would harden it with `mcp: { authorization: process.env.MY_TOKEN }`.
25-
mcp: true,
2620
cli: {
2721
command: 'devframe-files-inspector',
2822
port: 9876,
2923
// Single-user localhost demo - skip the trust handshake so the served
3024
// SPA can call RPC without an OTP round-trip.
3125
auth: false,
26+
// Serve the agent surface over the dev server's `/__mcp` route and
27+
// register the instance for `devframe connect` discovery. This demo binds
28+
// to loopback (`localhost:9876`), so `mcp: true` (the loopback origin
29+
// gate, trusting same-machine callers) is enough - no bearer plumbing. A
30+
// network-reachable tool would harden it with
31+
// `mcp: { authorization: process.env.MY_TOKEN }`.
32+
mcp: true,
3233
},
3334
setup(ctx) {
3435
// A scoped context auto-namespaces every registered id with `NAMESPACE:`.

‎packages/devframe/src/adapters/_shared.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ export function resolveMcpConfig(mcp: boolean | McpRouteOptions | undefined): Re
9595

9696
/**
9797
* Resolve the `mcp` entry a `__connection.json` should advertise for a dev
98-
* server started with the given `mcp` option (falling back to `def.mcp`, then
99-
* the deprecated `def.cli?.mcp`, exactly like `createDevServer`), or
100-
* `undefined` when the route is disabled.
98+
* server started with the given `mcp` option (falling back to `def.cli?.mcp`,
99+
* exactly like `createDevServer`), or `undefined` when the route is
100+
* disabled.
101101
*
102102
* Hosted bridges that hand-roll their connection meta pass the side-car
103103
* `port`: the advertised path becomes absolute (the side-car mounts at `/`)
@@ -110,7 +110,7 @@ export function resolveMcpConnectionMeta(
110110
mcp: boolean | McpRouteOptions | undefined,
111111
port?: number,
112112
): ConnectionMeta['mcp'] {
113-
const config = resolveMcpConfig(mcp ?? def.mcp ?? def.cli?.mcp)
113+
const config = resolveMcpConfig(mcp ?? def.cli?.mcp)
114114
if (!config)
115115
return undefined
116116
const route = withoutLeadingSlash(config.path ?? DEVFRAME_MCP_ROUTE)

‎packages/devframe/src/adapters/cac.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ export function createCac(d: DevframeDefinition, options: CreateCacOptions = {})
7070
.option('--no-auth', 'Disable the interactive authentication gate')
7171
// Only `--mcp` is declared: CAC's `--no-*` auto-negation would inject a
7272
// `true` default, silently enabling MCP. Declaring just `--mcp` yields the
73-
// opt-in tri-state — absent → `undefined` (falls through to the
74-
// definition's `mcp`), `--mcp` → `true`, `--no-mcp` → `false` (handled by
75-
// CAC's `--no-` prefix).
73+
// opt-in tri-state — absent → `undefined` (falls through to `cli.mcp`),
74+
// `--mcp` → `true`, `--no-mcp` → `false` (handled by CAC's `--no-` prefix).
7675
.option('--mcp', 'Expose an MCP server over HTTP at /__mcp (use --no-mcp to disable)')
7776

7877
// Register typed flags from the definition ahead of `cli.configure`
@@ -96,7 +95,7 @@ export function createCac(d: DevframeDefinition, options: CreateCacOptions = {})
9695
const port = (flags.port as number | undefined) ?? await resolveDevServerPort(d, { host, defaultPort })
9796
// `--mcp` / `--no-mcp` map to a boolean override; when neither is
9897
// passed CAC leaves `mcp` undefined so `createDevServer` falls through
99-
// to the definition's `mcp` (top-level, then the deprecated `cli.mcp`).
98+
// to `def.cli?.mcp`.
10099
const mcp = flags.mcp as boolean | undefined
101100
await createDevServer(d, {
102101
host,

‎packages/devframe/src/adapters/dev.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ export interface CreateDevServerOptions {
9292
auth?: boolean | DevframeAuthHandler
9393
/**
9494
* Expose a route-based MCP server on the dev server (Streamable-HTTP).
95-
* Overrides the definition's `mcp` (top-level, then the deprecated
96-
* `cli.mcp`); `undefined` falls through to it. `false` disables the route
97-
* regardless of the definition default. See {@link McpRouteOptions}.
95+
* Overrides `def.cli?.mcp`; `undefined` falls through to it. `false`
96+
* disables the route regardless of the definition default. See
97+
* {@link McpRouteOptions}.
9898
*/
9999
mcp?: boolean | McpRouteOptions
100100
/**

‎packages/devframe/src/adapters/initiate.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ export interface InitDevframeOptions {
8989
auth?: boolean | DevframeAuthHandler
9090
/**
9191
* Expose a route-based MCP server (Streamable-HTTP) at `<base>__mcp` and
92-
* advertise it in `__connection.json`. Overrides the definition's `mcp`
93-
* (top-level, then the deprecated `cli.mcp`); `undefined` falls through to
94-
* it. See {@link McpRouteOptions}.
92+
* advertise it in `__connection.json`. Overrides `def.cli?.mcp`;
93+
* `undefined` falls through to it. See {@link McpRouteOptions}.
9594
*/
9695
mcp?: boolean | McpRouteOptions
9796
/**
@@ -309,7 +308,7 @@ export function initDevframe(
309308
// only pulled in (dynamically) when the route is enabled. The resolved
310309
// config trusts same-machine callers by default (origin-only); an object
311310
// config can opt into a bearer/callback identity check.
312-
const mcpConfig = resolveMcpConfig(options.mcp ?? def.mcp ?? def.cli?.mcp)
311+
const mcpConfig = resolveMcpConfig(options.mcp ?? def.cli?.mcp)
313312
let mcpMeta: ConnectionMeta['mcp']
314313
let mcpDispose: (() => Promise<void>) | undefined
315314
if (mcpConfig) {

‎packages/devframe/src/types/devframe.ts‎

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export type McpAuthorization
106106

107107
/**
108108
* Configuration for the route-based MCP server mounted alongside the dev
109-
* server (opt-in via {@link DevframeDefinition.mcp}). The endpoint speaks
109+
* server (opt-in via {@link DevframeCliOptions.mcp}). The endpoint speaks
110110
* the MCP Streamable-HTTP transport over the same origin as the SPA,
111111
* exposing the definition's `ctx.agent` tools + shared-state resources to
112112
* external MCP clients connected to the *running* server.
@@ -184,11 +184,20 @@ export interface DevframeCliOptions {
184184
*/
185185
auth?: boolean | DevframeAuthHandler
186186
/**
187-
* Expose a route-based MCP server alongside the dev server.
187+
* Expose a route-based MCP server alongside the standalone dev server,
188+
* speaking the MCP Streamable-HTTP transport at `/__mcp` (relative to the
189+
* base path). It surfaces the same `ctx.agent` tools + shared-state
190+
* resources as the stdio `mcp` command, but against the live server.
188191
*
189-
* @deprecated Moved to the top-level {@link DevframeDefinition.mcp}. Set
190-
* `mcp` on the definition instead. This field is still read as a fallback
191-
* when the top-level `mcp` is unset, so existing definitions keep working.
192+
* - `false` / omitted (default) — no MCP route is mounted.
193+
* - `true` — mount at the default `__mcp` route with the loopback origin
194+
* gate (trusting same-machine callers).
195+
* - {@link McpRouteOptions} — customise the route path, origin allow-list,
196+
* and opt into an {@link McpAuthorization} identity check.
197+
*
198+
* The `--mcp` / `--no-mcp` CLI flags override this per run. Whether to expose
199+
* MCP is a hosting decision, so programmatic hosts pass it to
200+
* `initDevframe` / `initHub` / `createDevServer` instead.
192201
*/
193202
mcp?: boolean | McpRouteOptions
194203
/**
@@ -416,22 +425,6 @@ export interface DevframeDefinition {
416425
* {@link DevframeCliOptions.distDir} is read as a fallback.
417426
*/
418427
clientAssets?: StaticAssetsSource
419-
/**
420-
* Expose a route-based MCP server alongside the running dev server, speaking
421-
* the MCP Streamable-HTTP transport at `/__mcp` (relative to the base path).
422-
* It surfaces the same `ctx.agent` tools + shared-state resources as the
423-
* stdio `mcp` command, but against the live server.
424-
*
425-
* - `false` / omitted (default) — no MCP route is mounted.
426-
* - `true` — mount at the default `__mcp` route with the loopback origin
427-
* gate (trusting same-machine callers).
428-
* - {@link McpRouteOptions} — customise the route path, origin allow-list,
429-
* and opt into an {@link McpAuthorization} identity check.
430-
*
431-
* The `--mcp` / `--no-mcp` CLI flags override this per run. When unset, the
432-
* deprecated {@link DevframeCliOptions.mcp} is read as a fallback.
433-
*/
434-
mcp?: boolean | McpRouteOptions
435428
/** RPC-level configuration for this devframe (see {@link DevframeRpcOptions}). */
436429
rpc?: DevframeRpcOptions
437430
/** Server-side setup — the primary entrypoint. Runs in every runtime. */

‎packages/hub/src/node/__tests__/initiate.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,14 @@ describe('initHub', () => {
324324
it('warns (DF8005) when a mounted devframe asks for MCP but the hub MCP is off', async () => {
325325
const wsPort = await getPort({ port: 18235, host: '127.0.0.1' })
326326
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
327-
// The hub has no `mcp`, but `beta` declares `mcp: true` — the hub's single
328-
// aggregate route governs MCP, so beta's request is a no-op and warns.
327+
// The hub has no `mcp`, but `beta` declares `cli.mcp: true` — the hub's
328+
// single aggregate route governs MCP, so beta's request is a no-op and warns.
329329
const hub = initHub({
330330
base: DEVFRAMES_HUB_BASE,
331331
auth: false,
332332
host: '127.0.0.1',
333333
ws: { port: wsPort },
334-
devframes: [makeFrame('alpha'), { ...makeFrame('beta'), mcp: true }],
334+
devframes: [makeFrame('alpha'), { ...makeFrame('beta'), cli: { mcp: true } }],
335335
})
336336

337337
try {

0 commit comments

Comments
 (0)