# Agent-Native Devframe

> Source: <https://devfra.me/guide/agent-native>
> Published: 2026-09-23 16:30:34.691504+00:00

# Agent-Native Devframe

Devframe exposes its API (RPC functions, resources, shared state) to agents, over MCP on the node side and [WebMCP](#browser-side-tools-over-webmcp) on the browser side, opt-in per function.

## [How it works](#how-it-works)

Three pieces: the **`agent` field** on `defineRpcFunction`, **` ctx.agent`** (non-RPC tools + resources), and the **MCP adapter** (` devframe/adapters/mcp`) serving an [MCP](https://modelcontextprotocol.io) server. The same `agent` field on a *client* RPC function surfaces it [over WebMCP](#browser-side-tools-over-webmcp) instead.

## [Exposing an RPC function](#exposing-an-rpc-function)

``` js
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 }) => {
      // ...
    },
  }),
})
```

## [Tool ids and wire names](#tool-ids-and-wire-names)

- **The id** registers/invokes in devframe, colon-namespaced:`devframes:plugin:<slug>:<fn>` (built-in devframe RPCs),`devframe:<area>:<fn>` (built-ins), command ids.
- **The wire name** is what MCP clients call, constrained to`^[a-zA-Z0-9_-]{1,128}$` ; runs outside that set collapse to`_` , truncated to 128.

```
devframe:state:read          → devframe_state_read
devframes:plugin:git:status  → devframes_plugin_git_status
my-plugin:summarize          → my-plugin_summarize
```

`toAgentToolName` (`devframe/utils/agent-tool-name`, client-safe) predicts a wire name; two ids sanitizing alike keep the first, the later hidden with `DF0047`.

## [Registering a devframe tool](#registering-a-devframe-tool)

Tools without a matching RPC register directly.

```
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(),
      }),
    })
  },
})
```

## [Deriving tools from other state](#deriving-tools-from-other-state)

Register a **provider** for tools derived from state, queried at list/invoke time:

``` js
const handle = ctx.agent.registerToolProvider(() =>
  currentCommands()
    .filter(command => command.agent)
    .map(command => toAgentTool(command)),
)

// After the underlying state changes, nudge connected MCP clients:
handle.notifyChanged() // fires tools/list_changed
```

## [Registering a resource](#registering-a-resource)

Readable snapshots by URI:

```
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 exposed as a `devframe://state/<key>` resource and via the **`devframe:state:read` tool** (wire `devframe_state_read`): no args → key list, `key` → its value. `exposeSharedState: false` (or a filter) on `createMcpServer` opts out.

## [Starting the MCP server](#starting-the-mcp-server)

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) and the optional [`@devframes/agentic`](https://devfra.me/adapters/mcp) peer is installed - one flagged function plus one install is the whole setup. See the [MCP adapter](https://devfra.me/adapters/mcp#route-based-server) for forcing it on or off and hardening the route.

For a stdio server instead, via the CLI:

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

Programmatically:

``` js
import { defineDevframe } from 'devframe'
import { createMcpServer } from 'devframe/adapters/mcp'

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

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

## [Connecting Claude Desktop](#connecting-claude-desktop)

In `claude_desktop_config.json`:

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

Restart; tools appear in the drawer, resources as `devframe://resource/<id>` / `devframe://state/<key>` URIs.

## [Browser-side tools over WebMCP](#browser-side-tools-over-webmcp)

The same `agent` signature works on the browser side: a client RPC function (a function the node side calls on the browser, registered on `rpc.client` or through a scoped `client.scope('my-plugin').rpc.register(...)`) carrying an `agent` field is mirrored onto the page's [WebMCP](https://github.com/webmachinelearning/webmcp) model context (`document.modelContext` / `navigator.modelContext`) as a callable tool, so in-page and browser-integrated agents can drive browser-side functionality directly. Wire names, `arg0`/` arg1`/… input schemas, and safety annotations match the MCP projection above.

``` js
const rpc = await connectDevframe()

rpc.client.register({
  name: 'my-plugin:highlight-node',
  type: 'action',
  jsonSerializable: true,
  agent: {
    description: 'Highlight a node in the open inspector view. Use it to point the user at a finding.',
  },
  handler: (id: string) => highlightNode(id),
})
```

`connectDevframe()` wires this on its own when the browser provides a model context; `webmcp: false` keeps the browser side off the WebMCP surface. `registerWebMcpTools(collector)` (from `devframe/client`) applies the same projection to a hand-built collector and returns a dispose that unregisters every tool.

`registerWebMcpTools` tracks the current draft (`AbortSignal`-based unregistration) and earlier handle-returning drafts, but the browser API may still change.
## [Writing descriptions agents act on](#writing-descriptions-agents-act-on)

Describe *when* to use a tool, not just its return:

```
// ✗ 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.' }
```

## [Gateway tools](#gateway-tools)

A gateway tool returns *instructions and locations*, not work agents do better:

```
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.',
  }),
})
```

## [Structured errors](#structured-errors)

A coded diagnostic thrown from a handler crosses the MCP boundary as JSON:

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

Prefer coded diagnostics anywhere agent-reachable: agents act on `fix` and follow `docs`.

## [Safety model](#safety-model)

- **`safety`** :`'read'` ,`'action'` , or`'destructive'` . Inferred from the RPC`type` (`static` /`query` →`read` ,`action` /`event` →`action` ), overridable.
- The adapter maps `safety` to tool annotations (`readOnlyHint` ,`destructiveHint` ).

## [CLI](#cli)

`<your-app> mcp` starts the MCP server on `stdio`; `<your-app> dev --mcp` serves the agent-consumable API on `/__mcp`; `devframe connect` discovers running devframes and proxies their tools ([MCP adapter](https://devfra.me/adapters/mcp#discovery-devframe-connect)). The command table is in the [Node-Side API reference](https://devfra.me/references/node-api#mcp-cli-commands).

Security

Devframe tools are secure by default: connections bind to localhost, and dev-mode RPC requires a trust handshake before accepting a browser.

Hub

@devframes/hub orchestrates many devtools sharing a UI: a dock registry, terminal aggregation, message/toast queue, and command palette. It ships no UI; hub UI providers provide their own atop the hub's RPC + shared-state protocol.
