Skip to content

Adapters

An adapter takes a DevtoolDefinition and deploys it into a specific runtime — a standalone CLI, a Vite plugin, a static snapshot, an SPA, a Kit plugin, an embedded host, or an MCP server. Each adapter ships at its own entry point (devframe/adapters/<name>); the bundler pulls in only the ones you use.

Every adapter factory has the shape createXxx(devtoolDef, options?).

Comparison

AdapterEntryFactoryBest for
clidevframe/adapters/clicreateCli(def, options?)Standalone tools run via node ./my-tool.js
devdevframe/adapters/devcreateDevServer(def, options?)Run the dev server programmatically — drive it from any CLI framework
vitedevframe/adapters/vitecreateVitePlugin(def, options?)Mount a tool's UI inside an existing Vite dev server
builddevframe/adapters/buildcreateBuild(def, options?)Offline reports, CI artifacts, deployable SPA snapshots
kit@vitejs/devtools-kit/nodecreatePluginFromDevframe(def, options?)Integrating into Vite DevTools Kit
embeddeddevframe/adapters/embeddedcreateEmbedded(def, { ctx })Runtime registration into an already-running host
mcpdevframe/adapters/mcpcreateMcpServer(def, options?)Exposing a devtool to coding agents

CLI

The CLI adapter wraps a DevtoolDefinition in a cac-powered command-line interface. From one entry it spins up an h3 dev server with WebSocket RPC, builds static snapshots, builds SPA bundles, or starts an MCP server.

ts
import { defineDevtool } from 'devframe'
import { createCli } from 'devframe/adapters/cli'

const devtool = defineDevtool({
  id: 'my-devtool',
  name: 'My Devtool',
  cli: { distDir: './client/dist' },
  setup(ctx) { /* register docks, RPC, etc. */ },
})

await createCli(devtool).parse()

Running the resulting binary:

sh
my-devtool                     # dev server at http://localhost:9999/
my-devtool --port 8080
my-devtool build --out-dir dist-static
my-devtool build --out-dir dist-static --base /devtools/
my-devtool mcp                 # stdio MCP server (experimental)

Standalone CLI serves the SPA at / by default. The /__devtools/ prefix is for hosted adapters where devframe mounts alongside an existing app — see Mount paths.

Options

createCli(def, options?) accepts:

OptionDefaultDescription
defaultPort9999 (or def.cli?.port)Port used by the dev command when --port isn't provided.
configureCli(cli: CAC) => void — final hook to add commands/flags at the assembly stage, after the definition's cli.configure runs.
onReady(info: { origin, port, app }) => void | Promise<void> — called once the dev server is listening. Use this to print your own startup banner.

createCli returns a CliHandle:

ts
interface CliHandle {
  cli: CAC // raw cac instance — mutate before calling parse()
  parse: (argv?: string[]) => Promise<void>
}

The cli property lets the caller add ad-hoc commands and flags right before parse() when a configureCli callback is inconvenient.

Definition-level cli fields

ts
defineDevtool({
  id: 'my-devtool',
  cli: {
    command: 'my-devtool', // binary name; default: the id
    distDir: './client/dist', // required for dev/build/spa
    port: 7777, // preferred port
    portRange: [7777, 9000], // passed through to get-port-please
    random: false, // passed through to get-port-please
    host: '127.0.0.1', // default host; --host overrides
    open: true, // auto-open the browser on dev start
    auth: false, // skip the trust handshake (single-user localhost)
    configure(cli) { // contribute capability flags/commands
      cli.option('--config <file>', 'Custom config file')
        .option('--no-files', 'Skip file matching')
    },
  },
  setup(ctx, { flags }) {
    // `flags` is the parsed cac flag bag — includes both devframe's
    // built-ins (`--port`, `--host`, `--open`) and anything declared in
    // `cli.configure` or `configureCli`.
  },
})

distDir is the only required field; everything else has sensible defaults. The configure hook runs before the configureCli option passed to createCli, so the final tool author always has the last word on flags.

Headless logging

Devframe leaves startup output to the application. Wire onReady to print your own banner:

ts
await createCli(devtool, {
  onReady({ origin }) {
    console.log(`ESLint Config Inspector ready at ${origin}`)
  },
}).parse()

Structured diagnostics (via logs-sdk) continue to surface through their normal reporters.

Use your own CLI framework

To integrate devframe into an existing commander / yargs program — or to expose a different command structure than createCli's dev / build / mcp triplet — drop down to the peer factories. Same DevtoolDefinition, different shell:

Building blockEntryPurpose
createDevServer(def, opts?)devframe/adapters/devh3 + WebSocket RPC + SPA mount
createBuild(def, opts?)devframe/adapters/buildStatic deploy
createMcpServer(def, opts?)devframe/adapters/mcpstdio MCP server
parseCliFlags(schema, raw)devframe/adapters/cliValidate a flag bag against a CliFlagsSchema

See the Standalone CLI guide for a worked commander example.

Dev

The dev adapter is the building block createCli uses internally — h3 + WebSocket RPC + the author's SPA mounted at the resolved base path. Reach for it directly to mount the dev server inside an existing CLI program (commander, yargs, hand-rolled CAC) or to attach custom middleware to the underlying h3 app.

ts
import { createDevServer } from 'devframe/adapters/dev'
import devtool from './devtool'

const handle = await createDevServer(devtool, {
  port: 7777,
  onReady: ({ origin }) => console.log(`Ready at ${origin}`),
})

// graceful shutdown — SIGINT, hot reload, test teardown
process.on('SIGINT', () => handle.close().then(() => process.exit(0)))

createDevServer returns the underlying StartedServer (origin, port, h3 app, WS server, RPC group, close()) so callers can integrate it into their own process lifecycle.

OptionDefaultDescription
hostdef.cli?.host ?? 'localhost'Bind host.
portresolved via resolveDevServerPortPort to listen on.
flags{}Parsed flag bag forwarded to setup(ctx, { flags }).
distDirdef.cli?.distDirRequired — throws when neither is set.
basePathresolveBasePath(def, 'standalone')Mount path override.
appfresh h3 appPre-configured h3 app to mount onto (custom middleware, auth, extra static assets).
openBrowserresolves from flags.open / def.cli?.openExplicit on/off override. false disables; a string opens that relative path.
onReadyCallback when the WS server is bound.

Port resolution

resolveDevServerPort(def, opts?) resolves a port up-front (to print or log it) before the server starts:

ts
import { resolveDevServerPort } from 'devframe/adapters/dev'

const port = await resolveDevServerPort(devtool, { host: '127.0.0.1' })
// honors def.cli?.port / portRange / random
OptionDefaultDescription
hostdef.cli?.host ?? 'localhost'Bind host (passed to get-port-please for in-use detection).
defaultPortdef.cli?.port ?? 9999Override the preferred port.

Mount paths

A devtool's SPA basePath depends on which adapter is running it:

Adapter kindDefault basePathReason
cli, spa, build (standalone)/The devtool owns the origin.
vite, kit, embedded (hosted)/__<id>/The devtool shares the origin with a host app and namespaces itself.

Override either side explicitly with DevtoolDefinition.basePath:

ts
defineDevtool({
  id: 'my-devtool',
  basePath: '/devtools/', // force this base regardless of adapter
  setup(ctx) { /* … */ },
})

SPA authors should build with relative asset paths (vite.base: './'); the client resolves its connection descriptor relative to the page at runtime. See Client for the discovery rules.

Vite

A thin Vite plugin that mounts a devtool's SPA into an existing Vite dev server as a hosted adapter — the mount path defaults to /__<id>/ to namespace away from the app. The plugin mounts the SPA only; for RPC, use kit or cli.

ts
import { createVitePlugin } from 'devframe/adapters/vite'
import { defineConfig } from 'vite'
import devtool from './devtool'

export default defineConfig({
  plugins: [createVitePlugin(devtool)],
})
OptionDefaultDescription
basedef.basePath ?? '/__<id>/'Mount path inside the Vite dev server.

Use this adapter when a devtool's UI is purely static and you want to surface it during Vite serve without shipping a separate dev server. Set DevtoolDefinition.basePath on the definition for a custom path that stays consistent across adapters.

Build

Produces a self-contained static deploy of a devtool:

  1. Copies the author's SPA dist (cli.distDir or options.distDir) into <outDir>.
  2. Runs setup(ctx) with mode: 'build'.
  3. Collects RPC dumps for every 'static' function and any 'query' function with dump.inputs / snapshot: true.
  4. Writes <outDir>/__connection.json ({ backend: 'static' }) and sharded dump files under <outDir>/__rpc-dump/ — both at the SPA root so the deployed client discovers them via relative paths from document.baseURI.
  5. When def.spa is set, also writes <outDir>/spa-loader.json describing how the SPA hydrates its data.
ts
import { createBuild } from 'devframe/adapters/build'
import devtool from './devtool'

await createBuild(devtool, {
  outDir: 'dist-static',
  base: '/',
})
OptionDefaultDescription
outDirdist-staticOutput directory. Cleared on each build.
base/Absolute URL base the output is served from.
distDirdef.cli?.distDirOverride the SPA dist directory.

The resulting directory hosts on any static web server (serve, nginx, GitHub Pages, …). The client auto-detects static mode by resolving ./__connection.json against document.baseURI and runs in read-only form.

createBuild copies the SPA verbatim, so deploying under a custom URL base just means building the SPA with relative asset paths (vite.base: './') — the client discovers the effective base at runtime.

When def.spa is set on the definition, createBuild also writes spa-loader.json next to index.html describing how the deployed SPA sources its data:

  • 'none' — use the baked RPC dump only (read-only static view).
  • 'query' — hydrate from URL search params.
  • 'upload' — accept a drag-and-drop file.

Deployed SPAs that use setupBrowser ship their own client entry that registers the handlers.

Kit

Wraps a DevtoolDefinition so Vite DevTools Kit's plugin-scan picks it up. The factory lives in @vitejs/devtools-kit/node — kit owns docking and process management while devframe stays portable.

ts
import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
import devtool from './devtool'

export default function myVitePlugin() {
  return createPluginFromDevframe(devtool)
}

The returned object has the shape { name, devtools: { setup, capabilities } }. Use this adapter when your devtool should live inside the Vite DevTools dock alongside other integrations. Kit synthesises an iframe dock entry from the definition's id / name / icon / basePath; for richer kit-specific behaviour (extra terminals, commands, dock overrides) pass options.setup. See the DevTools Kit → DevTools Plugin page for the Vite-specific guide.

OptionDefaultDescription
namedevframe:<id>Override the Vite plugin name.
basedef.basePath ?? /.${id}/Mount path override.
dock{}Overrides for the synthesized iframe dock entry (category, icon, when).
setupAdditional kit-only setup hook; receives the kit-augmented context.

Embedded

Register a devtool into an already-running context at runtime. Mirrors Kit's internal plugin-scan, but for callers that need dynamic, post-startup registration. The host decides the mount path; embedded is a hosted adapter and inherits the /__<id>/ default when one is needed.

ts
import { createEmbedded } from 'devframe/adapters/embedded'
import devtool from './devtool'

await createEmbedded(devtool, { ctx: existingCtx })
OptionRequiredDescription
ctxTarget DevToolsNodeContext the devtool is registered into.

Useful when a host loads devtools based on runtime conditions (feature flags, user opt-in, dynamic discovery) rather than static config.

MCP

Experimental

The agent-native surface is experimental and may change without a major version bump.

Translates a devtool's agent host into a Model Context Protocol server so coding agents (Claude Desktop, Cursor, Zed, Claude Code) can call flagged RPCs and read exposed resources.

ts
import { createMcpServer } from 'devframe/adapters/mcp'
import devtool from './devtool'

await createMcpServer(devtool, { transport: 'stdio' })

@modelcontextprotocol/sdk is a peer dependency — install it when shipping MCP support. The current transport is stdio.

See the Agent-Native page for the full API, safety model, and Claude Desktop integration example.

Released under the MIT License.