{"slug": "agent-native-devframe", "title": "Agent-Native Devframe", "summary": "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.", "body_md": "# Agent-Native Devframe\n\nDevframe 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.\n\n## [How it works](#how-it-works)\n\nThree 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.\n\n## [Exposing an RPC function](#exposing-an-rpc-function)\n\n``` js\nimport { defineRpcFunction } from 'devframe'\n\nexport const getSessionSummary = defineRpcFunction({\n  name: 'rolldown-get-session-summary',\n  type: 'query',\n  args: [v.object({ sessionId: v.string() })],\n  returns: v.object({ durationMs: v.number(), chunkCount: v.number() }),\n  agent: {\n    description: 'Summarize a Rolldown build session. Safe to call freely.',\n    title: 'Build summary',\n    // safety inferred from `type: 'query'` → 'read'\n  },\n  setup: ctx => ({\n    handler: async ({ sessionId }) => {\n      // ...\n    },\n  }),\n})\n```\n\n## [Tool ids and wire names](#tool-ids-and-wire-names)\n\n- **The id** registers/invokes in devframe, colon-namespaced:`devframes:plugin:<slug>:<fn>` (built-in devframe RPCs),`devframe:<area>:<fn>` (built-ins), command ids.\n- **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.\n\n```\ndevframe:state:read          → devframe_state_read\ndevframes:plugin:git:status  → devframes_plugin_git_status\nmy-plugin:summarize          → my-plugin_summarize\n```\n\n`toAgentToolName` (`devframe/utils/agent-tool-name`, client-safe) predicts a wire name; two ids sanitizing alike keep the first, the later hidden with `DF0047`.\n\n## [Registering a devframe tool](#registering-a-devframe-tool)\n\nTools without a matching RPC register directly.\n\n```\nexport default defineDevframe({\n  id: 'my-plugin',\n  setup(ctx) {\n    ctx.agent.registerTool({\n      id: 'my-plugin:summarize',\n      description: 'Plain-text summary of the current build state.',\n      safety: 'read',\n      handler: async () => ({\n        markdown: buildSummary(),\n      }),\n    })\n  },\n})\n```\n\n## [Deriving tools from other state](#deriving-tools-from-other-state)\n\nRegister a **provider** for tools derived from state, queried at list/invoke time:\n\n``` js\nconst handle = ctx.agent.registerToolProvider(() =>\n  currentCommands()\n    .filter(command => command.agent)\n    .map(command => toAgentTool(command)),\n)\n\n// After the underlying state changes, nudge connected MCP clients:\nhandle.notifyChanged() // fires tools/list_changed\n```\n\n## [Registering a resource](#registering-a-resource)\n\nReadable snapshots by URI:\n\n```\nctx.agent.registerResource({\n  id: 'current-session',\n  name: 'Current Rolldown session',\n  description: 'Markdown snapshot of the active build session.',\n  mimeType: 'text/markdown',\n  read: () => ({ text: renderMarkdown(currentSession) }),\n})\n```\n\nEvery `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.\n\n## [Starting the MCP server](#starting-the-mcp-server)\n\nThe 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.\n\nFor a stdio server instead, via the CLI:\n\n```\n# Run your devtool with an MCP stdio server attached.\ndevframe mcp\n```\n\nProgrammatically:\n\n``` js\nimport { defineDevframe } from 'devframe'\nimport { createMcpServer } from 'devframe/adapters/mcp'\n\nconst myDevframe = defineDevframe({ /* … */ })\n\nawait createMcpServer(myDevframe, { transport: 'stdio' })\n```\n\n## [Connecting Claude Desktop](#connecting-claude-desktop)\n\nIn `claude_desktop_config.json`:\n\n```\n{\n  \"mcpServers\": {\n    \"my-tool\": {\n      \"command\": \"pnpm\",\n      \"args\": [\"--filter\", \"my-tool\", \"exec\", \"devframe\", \"mcp\"]\n    }\n  }\n}\n```\n\nRestart; tools appear in the drawer, resources as `devframe://resource/<id>` / `devframe://state/<key>` URIs.\n\n## [Browser-side tools over WebMCP](#browser-side-tools-over-webmcp)\n\nThe 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.\n\n``` js\nconst rpc = await connectDevframe()\n\nrpc.client.register({\n  name: 'my-plugin:highlight-node',\n  type: 'action',\n  jsonSerializable: true,\n  agent: {\n    description: 'Highlight a node in the open inspector view. Use it to point the user at a finding.',\n  },\n  handler: (id: string) => highlightNode(id),\n})\n```\n\n`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.\n\n`registerWebMcpTools` tracks the current draft (`AbortSignal`-based unregistration) and earlier handle-returning drafts, but the browser API may still change.\n## [Writing descriptions agents act on](#writing-descriptions-agents-act-on)\n\nDescribe *when* to use a tool, not just its return:\n\n```\n// ✗ Bad: describes the mechanism\nagent: { description: 'Returns the session summary object.' }\n// ✓ Good: tells the agent when and why\nagent: { description: 'Summarize the current build session: durations, chunk counts, warnings. Call this before proposing any build-config change.' }\n```\n\n## [Gateway tools](#gateway-tools)\n\nA gateway tool returns *instructions and locations*, not work agents do better:\n\n```\nctx.agent.registerTool({\n  id: 'my-plugin:docs',\n  description: 'Locate the version-accurate docs for this tool. Call before answering questions about its config format.',\n  safety: 'read',\n  handler: () => ({\n    docsPath: resolveInstalledDocsDir(),\n    hint: 'Read the file matching your topic; do not rely on training-data knowledge of this config format.',\n  }),\n})\n```\n\n## [Structured errors](#structured-errors)\n\nA coded diagnostic thrown from a handler crosses the MCP boundary as JSON:\n\n```\n{ \"error\": { \"code\": \"DF0017\", \"message\": \"…\", \"fix\": \"…\", \"docs\": \"https://devfra.me/errors/df0017\" } }\n```\n\nPrefer coded diagnostics anywhere agent-reachable: agents act on `fix` and follow `docs`.\n\n## [Safety model](#safety-model)\n\n- **`safety`** :`'read'` ,`'action'` , or`'destructive'` . Inferred from the RPC`type` (`static` /`query` →`read` ,`action` /`event` →`action` ), overridable.\n- The adapter maps `safety` to tool annotations (`readOnlyHint` ,`destructiveHint` ).\n\n## [CLI](#cli)\n\n`<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).\n\nSecurity\n\nDevframe tools are secure by default: connections bind to localhost, and dev-mode RPC requires a trust handshake before accepting a browser.\n\nHub\n\n@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.", "url": "https://wpnews.pro/news/agent-native-devframe", "canonical_source": "https://devfra.me/guide/agent-native", "published_at": "2026-09-23 16:30:34.691504+00:00", "updated_at": "2026-09-23 16:30:36.538289+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "ai-tools"], "entities": ["Devframe", "Model Context Protocol", "WebMCP", "Rolldown", "@devframes/agentic", "defineRpcFunction", "ctx.agent", "devframe/adapters/mcp"], "alternates": {"html": "https://wpnews.pro/news/agent-native-devframe", "markdown": "https://wpnews.pro/news/agent-native-devframe.md", "text": "https://wpnews.pro/news/agent-native-devframe.txt", "jsonld": "https://wpnews.pro/news/agent-native-devframe.jsonld"}}