Skip to content

Agent-Native Devframe

Experimental

The agent-native surface (agent field on defineRpcFunction, DevframeAgentHost, and the devframe/adapters/mcp adapter) is experimental and may change without a major version bump until it stabilizes.

Devframe can expose the same surface a browser UI consumes — RPC functions, resources, and shared state — to coding agents (Claude Desktop / Cursor / Zed / Claude Code, or any MCP-speaking client). Agent exposure is opt-in per function; functions stay private by default.

How it works

Three building blocks:

  1. An agent field on defineRpcFunction. Add agent: { description, ... } to opt a function in. Functions without the field stay private.
  2. ctx.agent — a host exposed on DevframeNodeContext. Plugins register tools that aren't backed by an RPC, and expose readable resources (e.g. a Markdown build summary).
  3. The MCP adapter (devframe/adapters/mcp) — translates the agent host into a Model Context Protocol server, over stdio (devframe mcp) or as a Streamable-HTTP route on the dev server (--mcp, advertised in __connection.json).

Exposing an RPC function

ts
import { defineRpcFunction } from 'devframe'

export const getSessionSummary = defineRpcFunction({
  name: 'rolldown-get-session-summary',
  type: 'query',
  args: [v.object({ sessionId: v.string() })],
  returns: v.object({ durationMs: v.number(), chunkCount: v.number() }),
  agent: {
    description: 'Summarize a Rolldown build session. Safe to call freely.',
    title: 'Build summary',
    // safety inferred from `type: 'query'` → 'read'
  },
  setup: ctx => ({
    handler: async ({ sessionId }) => {
      // ...
    },
  }),
})

Agent tools take a single object input. The MCP adapter synthesises arg0, arg1, … from positional args (args: [A, B]); a single object schema (args: [v.object({ ... })]) reads better at the agent boundary because property names are self-describing.

Registering a plugin tool

For tools without a matching RPC — say, an on-demand narrative summary — register them directly:

ts
export default defineDevframe({
  id: 'my-plugin',
  setup(ctx) {
    ctx.agent.registerTool({
      id: 'my-plugin:summarize',
      description: 'Plain-text summary of the current build state.',
      safety: 'read',
      handler: async () => ({
        markdown: buildSummary(),
      }),
    })
  },
})

Registering a resource

Resources surface readable snapshots of state, identified by URI:

ts
ctx.agent.registerResource({
  id: 'current-session',
  name: 'Current Rolldown session',
  description: 'Markdown snapshot of the active build session.',
  mimeType: 'text/markdown',
  read: () => ({ text: renderMarkdown(currentSession) }),
})

Every ctx.rpc.sharedState key is also automatically exposed to MCP as devframe://state/<key>. Pass exposeSharedState: false (or a filter function) to createMcpServer to opt out.

Starting the MCP server

The simplest path is the CLI:

sh
# Run your devtool with an MCP stdio server attached.
devframe mcp

Programmatic equivalent:

ts
import { defineDevframe } from 'devframe'
import { createMcpServer } from 'devframe/adapters/mcp'

const devframe = defineDevframe({ /* … */ })

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

@modelcontextprotocol/sdk is a peer dependency — add it to your package when you want to ship an MCP-enabled devframe.

Connecting Claude Desktop

Add an entry to claude_desktop_config.json:

json
{
  "mcpServers": {
    "my-devframe": {
      "command": "pnpm",
      "args": ["--filter", "my-devframe", "exec", "devframe", "mcp"]
    }
  }
}

Restart Claude Desktop. The tools you flagged with agent: { ... } (plus any registerTool calls) show up in the MCP tool drawer. Resources are reachable as devframe://resource/<id> and devframe://state/<key> URIs.

Writing descriptions agents act on

A tool description is a prompt, not documentation. The agent decides when to call your tool from the description alone, so tell it — state when to reach for the tool, not just what it returns:

ts
// ✗ Bad: describes the mechanism
agent: { description: 'Returns the session summary object.' }
// ✓ Good: tells the agent when and why
agent: { description: 'Summarize the current build session — durations, chunk counts, warnings. Call this before proposing any build-config change.' }

Two conventions:

  • Lead with the action and the trigger. "Call this before/after/when …" steers proactive use; a bare noun phrase gets ignored.
  • State freshness and cost. "Safe to call freely" / "expensive, call once per session" lets the agent budget calls.

Gateway tools

A gateway tool returns instructions and locations instead of doing the work — the pattern for anything the agent can do better directly (reading bundled docs, running a CLI it has shell access to):

ts
ctx.agent.registerTool({
  id: 'my-plugin:docs',
  description: 'Locate the version-accurate docs for this tool. Call before answering questions about its config format.',
  safety: 'read',
  handler: () => ({
    docsPath: resolveInstalledDocsDir(),
    hint: 'Read the file matching your topic; do not rely on training-data knowledge of this config format.',
  }),
})

The agent gets a path and a next step; the actual reading happens with its own tools, which are faster and keep large content out of the MCP payload.

Structured errors

A coded devframe diagnostic thrown from a tool handler crosses the MCP boundary as structured JSON rather than a flattened message:

json
{ "error": { "code": "DF0017", "message": "…", "fix": "…", "docs": "https://devfra.me/errors/df0017" } }

Agents can act on fix directly and follow docs for detail — prefer throwing coded diagnostics from anything agent-reachable.

Safety model

  • Opt-in exposure. Functions opt in via the agent field; everything else stays private.
  • safety — one of 'read', 'action', 'destructive'. Inferred from the RPC type (static/queryread, action/eventaction), with explicit override available.
  • The MCP adapter maps safety to tool annotations (readOnlyHint, destructiveHint). MCP clients use these to decide whether to prompt for confirmation before calling.

CLI

CommandDescription
devframe mcpStart an MCP server on stdio.

Released under the MIT License.