Skip to content

Commit fecbf1f

Browse files
authored
refactor(devframe): move cli.mcp to createCac options (#337)
1 parent df71f8c commit fecbf1f

7 files changed

Lines changed: 40 additions & 27 deletions

File tree

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

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,13 @@ 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. 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):
2222

2323
```ts
24-
import { defineDevframe } from 'devframe'
24+
import { createCac } from 'devframe/adapters/cac'
25+
import myDevframe from './my-tool'
2526

26-
export default defineDevframe({
27-
/***/
28-
cli: {
29-
mcp: true,
30-
},
31-
})
27+
createCac(myDevframe, { mcp: true }).parse()
3228
```
3329

3430
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.
@@ -42,11 +38,9 @@ The **origin gate** guards every request: `Origin` must be loopback (or allow-li
4238
`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`:
4339

4440
```ts
45-
export default defineDevframe({
46-
cli: {
47-
mcp: { authorization: process.env.MY_TOKEN },
48-
},
49-
})
41+
createCac(myDevframe, {
42+
mcp: { authorization: process.env.MY_TOKEN },
43+
}).parse()
5044
```
5145

5246
`authorization` takes a bearer token (backed by an env var, never a literal), a `(request) => boolean` callback that governs identity only and cannot relax the origin gate, or `false` for the explicit origin-only default.

examples/files-inspector/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ pnpm -C examples/files-inspector run test # E2E tests
2727
|------|---------|
2828
| `src/devframe.ts` | The single `DevframeDefinition` consumed by every adapter. |
2929
| `src/client/` | Preact SPA: `index.html`, `main.tsx`, `app.tsx`, `routes/*`, `vite.config.ts`. |
30-
| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `mcp`. |
30+
| `bin.mjs` | `createCac(devframe, { mcp: true }).parse()` - exposes `dev`, `build`, `mcp`. |
3131
| `tests/` | E2E tests for CLI dev server and static build. |

examples/files-inspector/bin.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { createCac } from 'devframe/adapters/cac'
44
import devframe from './src/devframe.ts'
55

66
async function main() {
7-
const cli = createCac(devframe)
7+
// Serve the agent surface at `/__mcp` and register for `devframe connect`
8+
// discovery. This loopback demo trusts same-machine callers (`mcp: true`);
9+
// a network-reachable tool would harden it with `mcp: { authorization }`.
10+
const cli = createCac(devframe, { mcp: true })
811
await cli.parse()
912
}
1013

examples/files-inspector/src/devframe.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ export default defineDevframe({
2525
* SPA can call RPC without an OTP round-trip.
2626
*/
2727
auth: false,
28-
/**
29-
* Serve the agent surface at `/__mcp` and register for `devframe connect`
30-
* discovery. This loopback demo trusts same-machine callers (`mcp: true`);
31-
* a network-reachable tool would harden it with `mcp: { authorization }`.
32-
*/
33-
mcp: true,
3428
},
3529
setup(ctx) {
3630
// A scoped context auto-namespaces every registered id with `NAMESPACE:`.

packages/devframe/src/adapters/cac.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// re-exported below so they live alongside the CLI adapter.
99
import type { CAC } from 'cac'
1010
import type { H3 } from 'h3'
11-
import type { DevframeDefinition } from '../types/devframe'
11+
import type { DevframeDefinition, McpRouteOptions } from '../types/devframe'
1212
import process from 'node:process'
1313
import cac from 'cac'
1414
import { colors as c } from 'devframe/utils/colors'
@@ -23,6 +23,21 @@ export type { CliFlagsSchema, InferCliFlags } from './flags'
2323
export interface CreateCacOptions {
2424
/** Default port for `dev` (default: 9999). */
2525
defaultPort?: number
26+
/**
27+
* Expose a route-based MCP server alongside the dev server, speaking the
28+
* MCP Streamable-HTTP transport at `<base>__mcp`. Whether to expose MCP is
29+
* a hosting decision made at the CLI assembly stage, so it lives here rather
30+
* than on the definition.
31+
*
32+
* - `false` / omitted (default): no MCP route is mounted.
33+
* - `true`: mount at the default `__mcp` route with the loopback origin gate.
34+
* - {@link McpRouteOptions}: customise the route path, origin allow-list, and
35+
* opt into an identity check.
36+
*
37+
* The `--mcp` / `--no-mcp` flags override this per run. Falls back to the
38+
* definition's deprecated `cli.mcp` when unset.
39+
*/
40+
mcp?: boolean | McpRouteOptions
2641
/**
2742
* Final CAC hook invoked after devframe's built-in subcommands and
2843
* after the definition's `cli.configure`. Use this to add app-level
@@ -70,7 +85,8 @@ export function createCac(d: DevframeDefinition, options: CreateCacOptions = {})
7085
.option('--no-auth', 'Disable the interactive authentication gate')
7186
// Only `--mcp` is declared: CAC's `--no-*` auto-negation would inject a
7287
// `true` default, silently enabling MCP. Declaring just `--mcp` yields the
73-
// opt-in tri-state: absent → `undefined` (falls through to `cli.mcp`),
88+
// opt-in tri-state: absent → `undefined` (falls through to `options.mcp`,
89+
// then `cli.mcp`),
7490
// `--mcp` → `true`, `--no-mcp` → `false` (handled by CAC's `--no-` prefix).
7591
.option('--mcp', 'Expose an MCP server over HTTP at /__mcp (use --no-mcp to disable)')
7692

@@ -93,10 +109,10 @@ export function createCac(d: DevframeDefinition, options: CreateCacOptions = {})
93109
const flags = resolveTypedFlags(d, rawFlags) as CliFlags
94110
const host = (flags.host as string | undefined) ?? defaultHost
95111
const port = (flags.port as number | undefined) ?? await resolveDevServerPort(d, { host, defaultPort })
96-
// `--mcp` / `--no-mcp` map to a boolean override; when neither is
97-
// passed CAC leaves `mcp` undefined so `createDevServer` falls through
98-
// to `def.cli?.mcp`.
99-
const mcp = flags.mcp as boolean | undefined
112+
// `--mcp` / `--no-mcp` map to a boolean override; when neither is passed
113+
// CAC leaves `mcp` undefined so we fall back to the assembly-stage
114+
// `options.mcp`, and `createDevServer` falls through to `def.cli?.mcp`.
115+
const mcp = (flags.mcp as boolean | undefined) ?? options.mcp
100116
await createDevServer(d, {
101117
host,
102118
port,

packages/devframe/src/types/devframe.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ export interface DevframeCliOptions {
198198
* The `--mcp` / `--no-mcp` CLI flags override this per run. Whether to expose
199199
* MCP is a hosting decision, so programmatic hosts pass it to
200200
* `initDevframe` / `initHub` / `createDevServer` instead.
201+
*
202+
* @deprecated Whether to expose MCP is a hosting decision, not a capability
203+
* of the tool. Pass `mcp` to `createCac` (or the programmatic host) instead.
204+
* This field is still read as a fallback, and will be removed in a future
205+
* release.
201206
*/
202207
mcp?: boolean | McpRouteOptions
203208
/**

tests/__snapshots__/tsnapi/devframe/adapters/cac.snapshot.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface CacHandle {
88
}
99
export interface CreateCacOptions {
1010
defaultPort?: number;
11+
mcp?: boolean | McpRouteOptions;
1112
configureCli?: (_: CAC) => void;
1213
onReady?: (_: {
1314
origin: string;

0 commit comments

Comments
 (0)