Introduction
Devframe is a framework-neutral foundation for building a devtool once and running it everywhere — inside any host, as a standalone app, or through a coding agent. A devtool here is anything that makes a program's implicit state visible and interactive: an inspector, a build or bundle analyzer, an asset viewer, a state or data explorer, a terminal. You describe such a tool one time, and the same definition mounts almost anywhere. Think of it as unplugin for devtools.
Why it exists
Most devtools rebuild the same plumbing — server–client communication, state synchronization, serialization, static-asset hosting, a web interface — and wire it to one framework's dev server. The same idea then gets rebuilt, slightly differently, for the next framework, so effort fragments across the ecosystem instead of compounding.
Devframe moves that boundary. A capability is defined once against a stable interface and runs on every supported host, so a good tool can be built once, travel further, and improve through the work of more communities.
Who it's for
- Devtool authors who want one tool to run standalone, embed in a host, ship as a CLI or static report, and answer to an agent — without maintaining a separate version per environment.
- Framework and build-tool teams who want to offer devtools without rebuilding shared infrastructure, and to inherit capabilities other communities already built.
- Anyone who wants a tool's state and actions available to both a human UI and a coding agent from one source of truth.
With a coding agent to scaffold the boilerplate, Devframe is also a fast foundation for standing up a bespoke, specific-need, or even one-off devtool.
One definition, one standard handler
Every devframe starts with defineDevframe(), pairing a tool's identity with its capabilities.
import { defineDevframe } from 'devframe'
import { inspectProject } from './rpc'
export default defineDevframe({
id: 'my-tool',
name: 'My Tool',
// package metadata and client entry omitted…
setup(ctx) {
ctx.scope('my-tool').rpc.register(inspectProject)
},
})initDevframe() turns the definition into a live instance whose handler is a Web Standard (request: Request) => Promise<Response>:
import { initDevframe } from 'devframe/initiate'
import devframe from './devframe'
const devtools = initDevframe(devframe, { base: '/__my-tool/' })
devtools.handler
// (request: Request) => Promise<Response>
devtools.nodeMiddleware
// (req, res, next) => void — for Connect-style servers (Vite, Rsbuild)The handler serves the web interface, connection metadata, live RPC, authentication, and optional MCP endpoint under one namespace. Hono and Nitro take Web Standard requests directly; Next.js and SvelteKit expose route handlers; Vite and Rsbuild accept its nodeMiddleware. The live RPC connection attaches via a shared HTTP server, upgrade events, or side-car, advertised through __connection.json. See The Standard Handler.
Adapters as conveniences
Higher-level adapters package the foundation as a standalone CLI, dev server, Vite DevTools plugin, MCP server, or static report:
import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
import { createBuild } from 'devframe/adapters/build'
import { createCac } from 'devframe/adapters/cac'
import { createDevServer } from 'devframe/adapters/dev'
import { createMcpServer } from 'devframe/adapters/mcp'
import devframe from './devframe'
// Pick the entry points your package ships:
export const runCli = () => createCac(devframe).parse()
export const startServer = () => createDevServer(devframe)
export const vitePlugin = createPluginFromDevframe(devframe)
export const startMcp = () => createMcpServer(devframe, { transport: 'stdio' })
export const buildReport = () => createBuild(devframe, { outDir: 'dist-static' })Visual and agentic
One source of truth feeds a visual panel and programmatic consumers. RPC functions stay private by default and opt into agent exposure explicitly: the MCP adapter translates functions, readable resources, and selected shared state into an agent-consumable surface. See Agent-Native.
From one devframe to a hub
@devframes/hub is the composition layer, providing shared concepts — docks, commands, messages, terminals — against a shared context. initHub() puts many devframes behind one Web Standard handler:
import { createUi } from '@devframes/hub-ui'
import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate'
import { createDataInspectorDevframe } from '@devframes/plugin-data-inspector'
import { createTerminalsDevframe } from '@devframes/plugin-terminals'
const hub = initHub({
base: DEVFRAMES_HUB_BASE,
devframes: [createDataInspectorDevframe(), createTerminalsDevframe()],
ui: createUi(),
})
hub.handler
// the whole devtools collection as Request → ResponseThe mounted devframes share one RPC registry, state store, connection, auth gate, and optional aggregate MCP endpoint. The hub is headless: @devframes/hub-ui is a reference interface a product can replace.
Inheriting the ecosystem
Vite DevTools is the first flagship host, using initHub() alongside its own Vite, Rolldown, Vitest, and Oxc tooling. The framework packages — @devframes/vite, @devframes/nuxt, @devframes/next — add conventions over the same handler. See Built with Devframe.
Install
pnpm add devframedevframe ships ESM-only, no Vite dependency. Adapters with optional peers (the MCP adapter needs @modelcontextprotocol/server) surface the requirement at import time.
Hello, Devframe
A minimal devframe with a CLI entry point:
import { defineDevframe, defineRpcFunction } from 'devframe'
import { createCac } from 'devframe/adapters/cac'
const devframe = defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
version: '1.0.0',
packageName: 'my-devframe',
homepage: 'https://github.com/me/my-devframe',
description: 'A one-line summary of what the tool does.',
icon: 'ph:gauge-duotone',
clientAssets: 'client/dist',
setup(ctx) {
ctx.rpc.register(defineRpcFunction({
name: 'my-devframe:hello',
type: 'static',
jsonSerializable: true,
handler: () => ({ message: 'hello' }),
}))
},
})
await createCac(devframe).parse()Run it:
node ./my-devframe.js # dev server on http://localhost:9999/
node ./my-devframe.js build # self-contained static deploy in dist-static/
node ./my-devframe.js mcp # stdio MCP serverThe CLI adapter serves the SPA at /; embedded in a host (vite, embedded) the default becomes /__my-devframe/. Override via defineDevframe({ basePath }).
What Devframe provides
| Subsystem | What it does |
|---|---|
| Devframe Definition | One defineDevframe call describes your tool; adapters deploy it anywhere. |
| RPC | Type-safe bidirectional calls on birpc, validated against any Standard Schema validator. query, static, action, event types. |
| Shared State | Observable, patch-synced state surviving reconnects, server ↔ browser. |
| JSON-Render | Opt-in data-driven UI — a serializable view spec, rendered standalone or in a hub dock. |
| Diagnostics | Coded warnings/errors via nostics, in the host's shared lookup. |
| Streaming | One-way (RPC streaming) and two-way (uploads) channel primitives. |
| When Clauses | VS Code-style conditional expressions for docks, commands, and custom UI. |
| The Standard Handler | initDevframe() — the Web Standard Request → Response boundary. |
| Client | Browser RPC client (connectDevframe), auto-auth, WebSocket / static modes. |
| Agent-Native | Opt-in exposure of your tool's surface to coding agents over MCP. |
What's next
- Devframe Definition —
defineDevframeandDevframeNodeContext - The Standard Handler — mount into any host
- Adapters — convenience entry points
- Hub — compose many devframes