MCP

Exposes a devframe's agent-facing API as a Model Context Protocol server: coding agents call flagged RPCs and read resources.

Exposes a devframe's agent-facing API as a Model Context Protocol server: coding agents call flagged RPCs and read resources.

import { createMcpServer } from 'devframe/adapters/mcp'
import myDevframe from './my-tool'

await createMcpServer(myDevframe, { transport: 'stdio' })

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

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

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 per run; __connection.json advertises the mounted route.

The endpoint is stateless: it serves the 2026-07-28 revision 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 - 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:

createCac(myDevframe, {
  mcp: { authorization: process.env.MY_TOKEN },
}).parse()

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.

A request presents the bearer as Authorization: Bearer <token>, matched in constant time; a missing or wrong bearer gets 401 with a WWW-Authenticate: Bearer challenge. The origin gate always runs first, so a disallowed origin is 403 regardless of the credential. Widen the origin allow-list for a tunnel/LAN reach with mcp: { authorization: process.env.MY_TOKEN, allowedOrigins: ['https://tunnel.example.com'] }.

Never place the token in a URL, in __connection.json, in the instance registry, in logs, or on the command line; it belongs only in configuration and the Authorization header.

Hosted bridges

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

// Vite (@devframes/vite)
devframeViteBridge(myDevframe, { mcp: true })

// Next.js (@devframes/next)
createDevframeNextHandler(myDevframe, { mcp: true })

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

Custom host frameworks

createMcpFetchHandler(ctx, options) returns the endpoint as a Request → Response handler plus a dispose(); mount it on any fetch server.

import { createMcpFetchHandler } from 'devframe/adapters/mcp'

const mcp = createMcpFetchHandler(ctx, {
  serverName: 'my-tool (devframe)',
  serverVersion: '1.0.0',
  exposeSharedState: true,
  // Optional identity check on top of the origin gate; omit for origin-only.
  // authorization: process.env.MY_TOKEN,
})
// route every method on /__mcp to mcp.fetch(request)

Discovery: devframe connect

The devframe bin ships an MCP connector (next-devtools-mcp-style) that finds every running devframe. Configure once:

{
  "mcpServers": {
    "devframe": { "command": "npx", "args": ["devframe", "connect"] }
  }
}

Two gateway tools (devframe:connect:* ids; see tool ids and wire names):

  • devframe_connect_list-instances: list running dev servers and their MCP tools.
  • devframe_connect_call-tool: invoke one tool on a running devframe ({ port, tool, args }) over Streamable-HTTP.

Discovery reads the instance registry: every createDevServer writes ~/.devframe/instances/<pid>-<port>.json, dialed with a loopback origin. In-process host frameworks register via registerDevframeInstance (devframe/node). --port <n> probes a port; DEVFRAME_INSTANCES_DIR relocates the registry, DEVFRAME_DISABLE_INSTANCE_REGISTRY=1 opts out.

Most instances trust same-machine callers, so the connector reaches them with no credential. For an instance you hardened with a bearer, the connector reads DEVFRAME_MCP_AUTH_TOKEN and presents it (never a CLI flag, since command-line arguments are visible to other processes). Connect to a fleet with distinct credentials by driving startConnectServer with a per-instance authToken resolver.

See Agent-Native for the API and safety model.