Skip to content

Devframe ​

Devframe is an asset: define your devtool once, serve it anywhere. You describe a single tool — its RPC surface, its data model, its SPA, its CLI shape — and the same definition deploys through any of the runtime adapters: a standalone CLI, a self-contained static report, an embedded SPA, an MCP server, and more. Devframe is framework- and build-tool-agnostic — it has no Vite dependency and no opinion on what UI framework your SPA uses.

Vite DevTools is built on top of devframe. If you need an integrated multi-tool host (docks, command palette, terminals, cross-tool toasts), mount your devframe into Vite DevTools via the vite adapter — or build your own host adapter targeting any environment you like.

Design principles ​

Devframe keeps its surface focused on one tool, so the same definition stays portable across runtimes:

  • One tool per definition. A devframe describes a single integration. Deploy it through any adapter; host-level features that only matter when several tools share a UI (palettes, cross-tool toasts, unified terminals) come from whichever host you mount into — Vite DevTools is one example.
  • Headless. Hook into onReady, cli.configure, and friends to print your own startup banners and styling — Devframe stays out of the way.
  • App-owned file watching. Wire your own watcher (chokidar, fs.watch, …) and signal change via ctx.rpc.sharedState.set(...) or event-typed RPCs.
  • Context-aware mount paths. Standalone adapters (cli, spa, build) serve at / by default; hosted adapters (vite, embedded) serve at /.<id>/. Override via DevframeDefinition.basePath.
  • SPAs own their base at runtime. Build with relative asset paths (vite.base: './'); connectDevframe discovers the effective base from the executing script's location.
  • CLI flags compose. The cac instance is exposed to both the devframe (cli.configure) and the caller of createCac, so capability flags and app flags merge cleanly.

What Devframe provides ​

SubsystemWhat it does
Devframe DefinitionOne defineDevframe call describes your tool once; the adapters deploy it anywhere.
RPCType-safe bidirectional calls built on birpc + valibot. Supports query, static, action, and event types.
Shared StateObservable, patch-synced state that survives reconnects and bridges server ↔ browser.
JSON-RenderOpt-in data-driven UI — author a view as a serializable spec, render it standalone or in a hub dock with a replaceable frontend.
DiagnosticsCoded warnings/errors via nostics — registered into the host's shared lookup so adapters and consumers share the same surface.
StreamingOne-way (RPC streaming) and two-way (uploads) channel primitives for long-running data.
When ClausesVS Code-style conditional expressions for docks, commands, and custom UI.
UtilitiesBundled helpers under devframe/utils/* — terminal colors, hashing, editor launch, structured-clone serialization, and more.
ClientBrowser-side RPC client (connectDevframe) with auto-auth and WebSocket / static modes.
Agent-NativeOpt-in exposure of your tool's surface to coding agents over MCP.

Install ​

sh
pnpm add devframe

devframe ships ESM-only and has 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:

ts
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',
  cli: {
    distDir: 'client/dist',
  },
  setup(ctx) {
    ctx.rpc.register(defineRpcFunction({
      name: 'my-devframe:hello',
      type: 'static',
      jsonSerializable: true,
      handler: () => ({ message: 'hello' }),
    }))
  },
})

await createCac(devframe).parse()

The same definition can also be deployed through any of the other adapters — for example, mounted into Vite DevTools via the vite adapter.

Run it:

sh
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 server (experimental)

The CLI adapter serves the SPA at / by default. When the same devframe is embedded inside a host (vite, embedded), the default becomes /.my-devframe/. Override either side via defineDevframe({ basePath }).

Adapters at a glance ​

Devframe deploys the same DevframeDefinition through one of these adapters:

AdapterEntryTarget
clicreateCac(d).parse()Standalone CLI with dev / build / mcp subcommands
vitecreatePluginFromDevframe(d, opts?) (from @vitejs/devtools-kit/node)Mount the devframe into Vite DevTools (or another compatible host)
buildcreateBuild(d, opts?)Self-contained static deploy with baked RPC dumps
embeddedcreateEmbedded(d, { ctx })Runtime registration into an existing host
mcpcreateMcpServer(d, opts)Model Context Protocol server

See Adapters for the full reference.

Framework- and build-tool-agnostic ​

Devframe has zero dependencies on Vite or any @vitejs/* package — the same definition runs in any Node environment, with any UI framework, against any build tool. Vite DevTools is one host built on top of devframe; mount your definition there with the vite adapter, or write adapters for any other host.

What's next ​

  • Devframe Definition — understand defineDevframe and the DevframeNodeContext
  • Adapters — pick the right deployment target for your tool
  • RPC — define type-safe server functions your client can call
  • Agent-Native — expose your devframe to Claude Desktop, Cursor, or any MCP client

Released under the MIT License.