Reference

Plugins

Built-in plugins that extend the Routecraft runtime. Each entry opens its own reference page with the full options and behaviour.

  1. 01
    llmPluginLanguage models.@routecraft/ai

    Configure provider keys, default models, and global LLM defaults for every agent and llm() call in the context.

  2. 02
    embeddingPluginVectors.@routecraft/ai

    Wire an embedding provider for the embedding() destination and downstream clustering with cosine().

  3. 03
    mcpPluginMCP server runtime.@routecraft/ai

    Expose mcp() capabilities over Model Context Protocol, with JWT, OAuth 2.1, and bearer-token verification built in.

  4. 04
    agentPluginAgent registry and harness.@routecraft/ai

    Register named agents, the tools they can call, and shared defaults like system prompt and principal context.

  5. 05
    acpPluginEditor protocol mount.@routecraft/ai

    Serve the Agent Client Protocol so a person can talk to this instance's agents from their editor, on the same server and behind the same wall as everything else.

  6. 06
    httpPluginHTTP routing.@routecraft/routecraft

    Expose routes over HTTP via the http() source, mounted as the catch-all surface on a named server; JWT, JWKS, or API-key auth at the plugin boundary.

  7. 07
    serversPluginNamed listeners.@routecraft/routecraft

    Declare the HTTP listeners the app binds, by name. HTTP, MCP, and custom plugins mount paths on a shared listener; a surface gets a dedicated port by declaring another named server.

  8. 08
    opsPluginHealth and readiness.@routecraft/routecraft

    Serve liveness, readiness, and operational health, mounted on a named server. Route lifecycle and circuit-breaker state are derived from events the framework already emits; indicators cover dependencies it cannot see.

  9. 09
    shellPluginshell() context defaults.@routecraft/os

    Set context-wide defaults for shell(): isolation tier, timeout, and output cap. The lowest of the three precedence layers, under the ROUTECRAFT_SHELL_ISOLATION operator override and the call site.

  10. 10
    remotesPluginRoutes of other instances.@routecraft/routecraft

    Name other running instances, and every dispatchable route they expose through the ops management API becomes a direct endpoint here, for direct(), forward(), agent tools and the local ops listing alike.

Note

Core adapter defaults (cron, direct) are set via dedicated fields on CraftConfig, not via plugins. See Configuration and Merged Options.

First-class config keys

Importing @routecraft/ai augments CraftConfig with first-class keys for the AI plugins. Setting llm, mcp, embedding, agent, or sessions on the config is equivalent to pushing the corresponding plugin onto plugins: [] (sessions maps to sessionsPlugin, documented under Configuration). Lifecycle (apply, start, teardown, plugin events) is identical.

Still supported, and what you want for shared plugin instances or programmatic composition:

import { defineConfig } from '@routecraft/routecraft'
import { llmPlugin, mcpPlugin } from '@routecraft/ai'

export const craftConfig = defineConfig({
  plugins: [
    llmPlugin({ providers: { openai: { apiKey: '...' } } }),
    mcpPlugin({ clients: {} }),
  ],
})

Recommended for declarative configs:

import { defineConfig } from '@routecraft/routecraft'
import '@routecraft/ai' // augments CraftConfig

export const craftConfig = defineConfig({
  llm: { providers: { openai: { apiKey: '...' } } },
  mcp: { clients: {} },
})

The factories listed above remain available unchanged. Use them via plugins: [] when you need to instantiate a plugin once and reuse it (across multiple contexts) or compose plugins programmatically.

Lifecycle

A plugin has one required hook, apply(ctx), and two optional ones, start(ctx) and teardown(ctx, info). Which one a piece of work belongs in is decided by what has to already exist for that work to be correct.

HookRunsThe routes areUse it for
apply(ctx)While the context is being builtNot registered yetResolving config, opening resources, populating the context store
start(ctx)After every route has startedRunningWork that drives routes or needs them able to serve
teardown(ctx, info)During shutdown, or when a build or start failed partwayStopping, stopped, or never startedReleasing what apply opened, stopping what start began
import type { CraftPlugin, CraftContext } from '@routecraft/routecraft'

export function heartbeatPlugin(everyMs = 60_000): CraftPlugin {
  // Keyed by context, not a closure slot: one plugin instance can be applied
  // to more than one context in a process, and a single slot would let the
  // second start overwrite the first context's timer.
  const timers = new WeakMap<CraftContext, ReturnType<typeof setInterval>>()

  return {
    name: 'heartbeat',
    apply(ctx: CraftContext) {
      ctx.logger.debug({}, 'heartbeat configured')
    },
    start(ctx: CraftContext) {
      const timer = setInterval(() => {
        ctx.logger.info({}, 'still running')
      }, everyMs)
      timer.unref?.()
      timers.set(ctx, timer)
    },
    teardown(ctx: CraftContext) {
      const timer = timers.get(ctx)
      if (timer) clearInterval(timer)
      timers.delete(ctx)
    },
  }
}

ContextBuilder.build() applies plugins BEFORE it registers routes, so apply must not assume a route exists. Anything that reads or drives routes belongs in start, which runs once they are up.

Most plugins need only the required hook: a plugin that supplies typed, validated defaults to the context store is a config helper, and apply alone covers it.

A plugin instance may be applied and started against more than one context in a process, so per-run state a hook creates must be keyed by the ctx it was handed rather than held in a closure slot.

Hooks run in registration order and each is awaited before the next plugin's, at both apply and start. A hook that throws during start fails context.start() with the original error, and every applied plugin is torn down in reverse order before it surfaces, including plugins whose start() never ran.

A failure during build() unwinds the same way, so a plugin that opened a resource before a later plugin refused does not leak it: build() returns no context, so nothing else could ever release it. Only plugins whose apply() returned are torn down, and the original error surfaces unchanged even if a teardown throws on the way out.

A stop() that lands mid-boot waits for the hook that is still running before it tears that plugin down, so the order a plugin observes is always its hook entered, its hook settled, then teardown. Anything a hook acquires after its last await is therefore covered by the release that follows it. The wait is unbounded and covers apply and start alone, never the route work that only ends at shutdown, and no further hook runs once teardown has begun.

A hook may shut the context down, but it must never await its own stop(): the hook would wait for a shutdown that is waiting for the hook, and neither settles. Use throw to abort the boot with a reason, which unwinds through the teardown walk and surfaces the error, or call ctx.stop() without awaiting it to stand the context down without failing the boot. The rule is documented rather than enforced, and awaiting it hangs at boot on the first run.

async start(ctx: CraftContext) {
  // Abort the boot, with the reason reaching the operator:
  if (!configured) throw new Error("mail credentials missing");
  // Or request shutdown without failing the boot:
  void ctx.stop();
}

teardown receives a second argument describing how far the context got, so a plugin never has to infer it:

FieldMeans
partialThis is not a fully started context: it never started (a build that failed partway, or an embedder that built and stopped without calling start()) or a start() hook threw. Routes may not be registered and later plugins may never have applied.
startedThis plugin's own start() returned. Always false for a plugin with no start() hook, and during a build-failure unwind.

Ignore both and the hook behaves as it always did, which is correct for any plugin that only closes what apply() opened. A plugin that stops what start() began reads started instead of guarding on its own bookkeeping.

start is awaited, so the context is not ready until every hook has resolved. Use it for startup work that finishes: the deferral plugin scans for deferrals that expired while the process was down, so those escalations reach their routes before new traffic does. Work that does not finish (a poll loop, a subscription) is begun in start and left to the plugin's own timer, as above, rather than awaited inside the hook.

Note

Waiting for a context to be ready

ctx.start() resolves when the context stops, not when it comes up: a context with an indefinite route (an HTTP server, a direct() endpoint) keeps running. Await ctx.whenStarted() for readiness. It resolves once no route is still coming up and every start() hook has finished, and rejects if a hook or the config refuses. A single route failing to come up is not observable there, since the context keeps the others running.

Configuration

craft.config.ts and the merged options resolution order.

Adapters

The connectors that plugins configure defaults for.

AI capabilities

Build the agent or expose capabilities to one.

Previous
Runtime