cd /news/ai-agents/agent-native-devframe · home topics ai-agents article
[ARTICLE · art-138338] src=devfra.me ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Agent-Native Devframe

Devframe released an agent-native development framework that exposes its API — RPC functions, resources, and shared state — to AI agents over MCP on the node side and WebMCP on the browser side, opt-in per function. The framework adds an `agent` field to `defineRpcFunction`, a `ctx.agent` API for registering non-RPC tools and resources, and an MCP adapter at `devframe/adapters/mcp` that serves an MCP server, with the dev server mounting the route at `/__mcp` by default when an agent-flagged RPC, tool, or resource exists and the optional `@devframes/agentic` peer is installed. MCP wire names are constrained to `^[a-zA-Z0-9_-]{1,128}$`, and colliding sanitized ids keep the first while the later is hidden with error code DF0047.

read5 min views2 publishedSep 23, 2026
Agent-Native Devframe
Image: source

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_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 #

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_changed

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 #

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:

devframe mcp

Programmatically:

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 /queryread ,action /eventaction ), overridable.
  • The adapter maps safety to 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.

── more in #ai-agents 4 stories · sorted by recency
devfra.me · · #ai-agents
Guide
── more on @devframe 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/agent-native-devfram…] indexed:0 read:5min 2026-09-23 ·