serversPlugin
import { serversPlugin } from '@routecraft/routecraft'
Owns every HTTP listener in the process. Each entry under defineConfig({ servers }) declares one named listener; surfaces (the http plugin, the MCP HTTP transport, custom plugins) select a listener by name and mount paths on it instead of binding their own socket. A surface that needs an isolated port declares another named server and points at it; there is no separate standalone mode.
Materialised by the servers config key; the factory is exported for programmatic composition.
import { defineConfig, jwks } from '@routecraft/routecraft'
// The `mcp` first-class config key registers only when @routecraft/ai is imported.
import '@routecraft/ai'
export const craftConfig = defineConfig({
servers: {
default: {
host: '0.0.0.0',
port: 8080,
auth: jwks({ jwksUrl: '...', issuer: '...', audience: '...' }),
},
internal: { host: '127.0.0.1', port: 9090 },
},
http: {}, // mounts at "/" on "default"
mcp: { transport: 'http' }, // mounts at "/mcp" on "default"
})
Options
Each entry in the servers record:
Mounts and claims
Surfaces register an HttpMount via requireWebIngress(ctx, name) during plugin apply(). A mount declares every path it answers as claims (exact, prefix, or pattern); claims are evaluated once during context start, validated against every other mount on the same server, and only then does the listener bind. Conflicts (duplicate exact path and method, overlapping prefixes, two catch-all / fallbacks, a route under another mount's prefix) fail loudly with RC5003 before anything accepts traffic; registration order never decides ownership. See Custom mounts for the plugin-facing walkthrough.
Authentication on a shared listener is three-tiered: the server verifies credentials (this plugin's auth), the mount decides admission and refusal format (its handler pulls verification through a memoized authenticate() on the mount context, so public paths never trigger it), and routes check authority with .authorize().
Lifecycle
apply(ctx)publishes one ingress registry per declared server on the context store. No socket is bound.start(ctx)validates every mount's claims, then binds each listener (Bun.serveon Bun,node:httpon Node 22+) and emitsserver:listening { server, host, port }. A bind failure emitsserver:failedand fails the context start withRC5019.teardown(ctx, info)closes gracefully (stop accepting, reap idle connections, drain in-flight up toshutdownGrace, then force-close) and emitsserver:closed { server }.- The plugin declares
keepsAlive, so a context whose routes have all completed keeps running untilstop()while a listener is up.
Events
See Server events: server:listening, server:failed, server:closed, each carrying the server name.
Related
- Configuration -- the
serversfirst-class config key. - httpPlugin -- the catch-all HTTP surface.
- mcpPlugin -- the MCP surface (
transport: 'http',server,path).