Agent-Native Devframe
Devframe exposes its API (RPC functions, resources, shared state) to agents, over MCP on the node side and WebMCP on the browser side, opt-in per function.
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 server. The same agent field on a client RPC function surfaces it over WebMCP instead.
Exposing an RPC function
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
- 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_summarizetoAgentToolName (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
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
Register a provider for tools derived from state, queried at list/invoke time:
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_changedRegistering 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
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 peer is installed - one flagged function plus one install is the whole setup. See the MCP adapter 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 mcpProgrammatically:
import { defineDevframe } from 'devframe'
import { createMcpServer } from 'devframe/adapters/mcp'
const myDevframe = defineDevframe({ /* … */ })
await createMcpServer(myDevframe, { transport: 'stdio' })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
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 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.
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
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
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
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:'read','action', or'destructive'. Inferred from the RPCtype(static/query→read,action/event→action), overridable.- The adapter maps
safetyto tool annotations (readOnlyHint,destructiveHint).
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). The command table is in the Node-Side API reference.
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.