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:

OptionTypeDefaultDescription
portnumber-- (required)Port to bind. Use 0 to let the OS choose; the resolved port arrives on server:listening.
hoststring127.0.0.1Host to bind. Use 0.0.0.0 to expose externally.
allowedHostnamesreadonly string[][]Additional exact trusted public hostnames for MCP and ACP. No scheme, port, path or wildcard. Bound hostnames and supported loopback aliases are also accepted; forwarded request headers never establish trust.
kind'http''http'Listener kind. Reserved for future non-HTTP listeners; only 'http' is valid today.
authValidatorAuthOptions--Server-level credential validator inherited by every mount that sets no auth of its own. Verification only: each mount keeps its admission policy and refusal format. A mount removes the wall with auth: false, which keeps this validator reachable for routes that pull identity.
shutdownGraceDuration30000How long a graceful close may drain in-flight work before the listener is force-closed.

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.serve on Bun, node:http on Node 22+) and emits server:listening { server, host, port }. A bind failure emits server:failed and fails the context start with RC5019.
  • teardown(ctx, info) closes gracefully (stop accepting, reap idle connections, drain in-flight up to shutdownGrace, then force-close) and emits server:closed { server }.
  • The plugin declares keepsAlive, so a context whose routes have all completed keeps running until stop() while a listener is up.

Events

See Server events: server:listening, server:failed, server:closed, each carrying the server name.

  • Configuration -- the servers first-class config key.
  • httpPlugin -- the catch-all HTTP surface.
  • mcpPlugin -- the MCP surface (transport: 'http', server, path).