Skip to content

Plugins

air ships 19 built-in plugins. Add them to the use array — they execute in array order.

Usage

typescript
import {
  defineServer,
  cachePlugin,
  retryPlugin,
  authPlugin,
  timeoutPlugin,
} from '@airmcp-dev/core';

defineServer({
  use: [
    authPlugin({ type: 'api-key', keys: [process.env.MCP_API_KEY!] }),
    timeoutPlugin(10_000),
    retryPlugin({ maxRetries: 3 }),
    cachePlugin({ ttlMs: 60_000 }),
  ],
  tools: [ /* ... */ ],
});

Order matters. Above: auth → timeout → retry on failure → cache result.

Plugin vs Factory

Plugins can be passed in two forms:

typescript
// 1. Factory function (accepts options, returns plugin) — most built-in plugins
use: [cachePlugin({ ttlMs: 60_000 })]

// 2. Plugin object directly
use: [myPlugin]

Internally, resolvePlugin calls factory functions to produce AirPlugin objects. Omitting options uses defaults:

typescript
use: [cachePlugin()]   // ttlMs: 60_000 (default)

Plugin validation

Every plugin is validated on registration. meta.name is required:

typescript
// ✅ OK
const myPlugin: AirPlugin = {
  meta: { name: 'my-plugin', version: '1.0.0' },
  middleware: [ /* ... */ ],
};

// ❌ Error: plugin.meta.name is required
const badPlugin: AirPlugin = {
  meta: {} as any,
  middleware: [],
};

Plugin categories

Stability

PluginDescription
timeoutPluginAbort calls exceeding time limit (default: 30s)
retryPluginRetry failed calls with exponential backoff (default: 3 retries, 200ms)
circuitBreakerPluginStop calling after consecutive failures (default: 5 failures, 30s reset)
fallbackPluginReturn fallback value on error

Performance

PluginDescription
cachePluginCache results by param hash (default: 60s TTL, max 1000 entries)
dedupPluginDeduplicate concurrent identical calls
queuePluginLimit concurrent executions (default: 10)

Security

PluginDescription
authPluginAPI key or Bearer token auth
sanitizerPluginStrip HTML/scripts from input (default: max 10000 chars)
validatorPluginCustom validation rules

Network

PluginDescription
corsPluginCORS headers for HTTP/SSE transport
webhookPluginSend tool results to a webhook URL

Data

PluginDescription
transformPluginTransform params or results
i18nPluginLocalize tool responses

Monitoring

PluginDescription
jsonLoggerPluginStructured JSON logging
perUserRateLimitPluginPer-user rate limiting

Dev / Test

PluginDescription
dryrunPluginSkip handler execution (middleware testing)

Builtin plugins (always active)

Two plugins are auto-registered — don't add them to use:

builtinLoggerPlugin

Auto-logs every tool call. Level controlled by logging.level.

Output format:

12:34:56.789 search (45ms) [a1b2c3d4-e5f6-...]

On error:

12:34:56.789 search ERROR: Connection refused [a1b2c3d4-e5f6-...]

builtinMetricsPlugin

Auto-collects per-tool call count, error count, total duration, average duration, and last called timestamp.

typescript
import { getMetrics, resetMetrics } from '@airmcp-dev/core';

const metrics = getMetrics();
// {
//   search: {
//     calls: 150,
//     errors: 3,
//     totalDuration: 6750,
//     avgDuration: 45,
//     lastCalledAt: 1710000000000,
//   },
//   greet: {
//     calls: 50,
//     errors: 0,
//     totalDuration: 100,
//     avgDuration: 2,
//     lastCalledAt: 1710000001000,
//   },
// }

resetMetrics();  // Clear all metrics

Plugin execution order

Request arrives

  errorBoundaryMiddleware.before   (builtin — error boundary)
  validationMiddleware.before      (builtin — input validation)
  builtinLoggerPlugin.before       (builtin)
  builtinMetricsPlugin.before      (builtin)

  use[0].before (authPlugin)       ← user plugins in order
  use[1].before (timeoutPlugin)
  use[2].before (retryPlugin)
  use[3].before (cachePlugin)

  handler()                        ← tool handler

  use[3].after (cachePlugin)       ← same order (NOT reverse)
  use[2].after (retryPlugin)
  use[1].after (timeoutPlugin)
  use[0].after (authPlugin)
  builtinMetricsPlugin.after       (builtin)
  builtinLoggerPlugin.after        (builtin)

Response returned

INFO

After middleware runs in registration order, not reverse. This differs from Express.

Lifecycle hooks

Plugins can hook into server lifecycle:

typescript
interface PluginHooks {
  onInit?: (ctx: PluginContext) => Promise<void> | void;     // After server init
  onStart?: (ctx: PluginContext) => Promise<void> | void;    // On server.start()
  onStop?: (ctx: PluginContext) => Promise<void> | void;     // On server.stop()
  onToolRegister?: (tool: AirToolDef, ctx: PluginContext) => AirToolDef | void;  // On tool registration (sync)
}

Execution order: onInitonStart → (server running) → onStop. Multiple plugins' same hooks run in registration order.

onToolRegister is synchronous. Return a modified tool object to change it, or undefined to leave it unchanged.

PluginContext

typescript
interface PluginContext {
  serverName: string;
  config: Record<string, any>;
  state: Record<string, any>;          // Same as server.state
  log: (level: string, message: string, data?: any) => void;
}

Using ctx.log:

typescript
hooks: {
  onInit: (ctx) => {
    ctx.log('info', 'Plugin initialized', { serverName: ctx.serverName });
    // [air:plugin] Plugin initialized { serverName: 'my-server' }
  },
}

Custom plugins

See Custom Plugins for details.

Released under the Apache-2.0 License.