Skip to main content
Plugins add cross-cutting behavior and can contribute components. Typical uses: auth/session helpers, PII filtering, tracing, logging, caching, error policy, rate-limits.

Basic Plugin

A simple plugin uses the @Plugin decorator:
import { Plugin } from '@frontmcp/sdk';

@Plugin({
  name: 'my-plugin',
  description: 'A basic plugin example',
  providers: [MyProvider],     // plugin-scoped providers
  exports: [MyProvider],       // re-export to host app
  tools: [MyTool],             // contribute tools
})
export default class MyPlugin {}

Using Plugins

Attach plugins at app scope:
import { App } from '@frontmcp/sdk';
import { CachePlugin, CodeCallPlugin } from '@frontmcp/plugins';
import MySimplePlugin from './plugins/my-simple.plugin';

@App({
  id: 'my-app',
  name: 'My App',
  plugins: [
    // Option 1: Pass class directly (uses default options)
    MySimplePlugin,

    // Option 2: Use init() with custom options
    CachePlugin.init({
      type: 'redis',
      config: { host: 'localhost', port: 6379 },
    }),

    // Option 3: Use init() with factory for async/injected config
    CodeCallPlugin.init({
      inject: () => [ConfigService],
      useFactory: (config: ConfigService) => ({
        topK: config.get('CODECALL_TOP_K'),
        mode: config.get('CODECALL_MODE'),
      }),
    }),
  ],
})
export default class MyApp {}

Dynamic Plugin with Options

For plugins that need runtime configuration, extend DynamicPlugin:
import { DynamicPlugin, Plugin, ProviderType } from '@frontmcp/sdk';

interface MyPluginOptions {
  mode: 'fast' | 'safe';
  maxRetries?: number;
}

@Plugin({
  name: 'my-plugin',
  description: 'A configurable plugin',
  providers: [],
})
export default class MyPlugin extends DynamicPlugin<MyPluginOptions> {
  options: MyPluginOptions;

  constructor(options: MyPluginOptions = { mode: 'safe' }) {
    super();
    this.options = {
      maxRetries: 3,
      ...options,
    };
  }

  /**
   * Return providers based on options passed to init().
   */
  static override dynamicProviders(options: MyPluginOptions): ProviderType[] {
    return [
      {
        name: 'my-plugin:config',
        provide: MyPluginConfig,
        useValue: new MyPluginConfig(options),
      },
    ];
  }
}

Type-Safe Options with Zod

For complex options with defaults, use Zod schemas with two types:
my-plugin.types.ts
import { z } from 'zod';

// Inner schema (without outer .default())
const myPluginOptionsObjectSchema = z.object({
  mode: z.enum(['fast', 'safe']).default('safe'),
  maxRetries: z.number().positive().default(3),
  timeout: z.number().positive().optional(),
});

// Full schema for parsing
export const myPluginOptionsSchema = myPluginOptionsObjectSchema.default({});

/** Resolved type - all defaults applied. Use internally. */
export type MyPluginOptions = z.infer<typeof myPluginOptionsSchema>;

/** Input type - fields with defaults are optional. Use for init(). */
export type MyPluginOptionsInput = z.input<typeof myPluginOptionsObjectSchema>;
my-plugin.plugin.ts
import { DynamicPlugin, Plugin, ProviderType } from '@frontmcp/sdk';
import {
  MyPluginOptions,
  MyPluginOptionsInput,
  myPluginOptionsSchema
} from './my-plugin.types';

@Plugin({
  name: 'my-plugin',
  description: 'Plugin with Zod-validated options',
  providers: [],
})
export default class MyPlugin extends DynamicPlugin<MyPluginOptions, MyPluginOptionsInput> {
  options: MyPluginOptions;

  constructor(options: MyPluginOptionsInput = {}) {
    super();
    this.options = myPluginOptionsSchema.parse(options);
  }

  static override dynamicProviders(options: MyPluginOptionsInput): ProviderType[] {
    const parsed = myPluginOptionsSchema.parse(options);
    return [
      {
        name: 'my-plugin:config',
        provide: MyPluginConfig,
        useValue: new MyPluginConfig(parsed),
      },
    ];
  }
}

Adding Hooks

Plugins can intercept flow stages using hooks. Use this.get(Token) to access providers:
import { DynamicPlugin, FlowCtxOf, Plugin, ToolHook } from '@frontmcp/sdk';

// Define options interface
interface CachePluginOptions {
  type: 'memory' | 'redis';
  defaultTTL?: number; // TTL in seconds
}

// Injection token for the cache store
const CacheStoreToken = Symbol('CacheStore');

@Plugin({
  name: 'cache',
  description: 'Cache plugin for tool results',
  providers: [
    {
      name: 'cache:store',
      provide: CacheStoreToken,
      useValue: new MemoryCacheProvider(),
    },
  ],
})
export default class CachePlugin extends DynamicPlugin<CachePluginOptions> {
  options: CachePluginOptions;

  constructor(options: CachePluginOptions = { type: 'memory', defaultTTL: 3600 }) {
    super();
    this.options = options;
  }

  // Hook BEFORE 'execute' stage
  @ToolHook.Will('execute', { priority: 1000 })
  async checkCache(flowCtx: FlowCtxOf<'tools:call-tool'>) {
    const { tool, toolContext } = flowCtx.state;
    if (!tool || !toolContext) return;

    const { cache } = toolContext.metadata;
    if (!cache) return;

    // Access provider via this.get()
    const cacheStore = this.get(CacheStoreToken);
    const key = this.buildKey(tool.fullName, toolContext.input);
    const cached = await cacheStore.getValue(key);

    if (cached !== undefined) {
      // Set output and bypass execution
      flowCtx.state.rawOutput = cached;
      toolContext.respond(cached);
    }
  }

  // Hook AFTER 'execute' stage
  @ToolHook.Did('execute', { priority: 1000 })
  async storeCache(flowCtx: FlowCtxOf<'tools:call-tool'>) {
    const { tool, toolContext } = flowCtx.state;
    if (!tool || !toolContext) return;

    const { cache } = toolContext.metadata;
    if (!cache) return;

    const cacheStore = this.get(CacheStoreToken);
    const key = this.buildKey(tool.fullName, toolContext.input);
    const ttl = cache === true ? this.options.defaultTTL : cache.ttl;

    await cacheStore.setValue(key, toolContext.output, ttl);
  }

  private buildKey(toolName: string, input: unknown): string {
    return `${toolName}:${JSON.stringify(input)}`;
  }
}

Available Hooks

ToolHook (tools:call-tool)

Intercept tool execution flow:
StagePhaseDescription
parseInputpreParse and validate request
findToolpreLocate the requested tool
createToolCallContextpreCreate execution context
acquireQuotapreRate limiting
acquireSemaphorepreConcurrency control
validateInputexecuteValidate tool input against schema
executeexecuteRun the tool
validateOutputexecuteValidate tool output
releaseSemaphorefinalizeRelease concurrency
releaseQuotafinalizeRelease rate limit
finalizefinalizeFormat and return response

ListToolsHook (tools:list-tools)

Intercept tool listing flow:
StagePhaseDescription
parseInputpreParse request
findToolsexecuteCollect available tools
resolveConflictsexecuteHandle name conflicts
parseToolspostFormat tool descriptors

Hook Timing

  • .Will(stage) - runs before the stage
  • .Did(stage) - runs after the stage
@ToolHook.Will('execute', { priority: 1000 })  // Before tool runs
async beforeExecute(flowCtx: FlowCtxOf<'tools:call-tool'>) { }

@ToolHook.Did('execute', { priority: 1000 })   // After tool runs
async afterExecute(flowCtx: FlowCtxOf<'tools:call-tool'>) { }

DynamicPlugin API

class DynamicPlugin<TOptions, TInput = TOptions> {
  // Access a provider registered in the plugin or app
  get<T>(token: Reference<T>): T;

  // Static method to create configured plugin instance
  // Pass to the plugins array in @App()
  static init(options: TInput);

  // Override to provide dynamic providers based on options
  static dynamicProviders?(options: TInput): ProviderType[];
}
Type ParameterDescription
TOptionsResolved options type (after defaults). Use internally.
TInputInput options type (for init()). Defaults to TOptions.
The init() method accepts your plugin’s input options type and returns a provider configuration that the framework uses internally. You don’t need to import or reference the return type—just pass the result directly to the plugins array.

Extending Tool Metadata

Plugins can extend the global tool metadata interface:
declare global {
  interface ExtendFrontMcpToolMetadata {
    cache?: { ttl?: number; slideWindow?: boolean } | true;
  }
}
Tools can then use this metadata:
@Tool({
  name: 'my-tool',
  metadata: {
    cache: { ttl: 3600 },
  },
})

Composition

Plugins compose depth-first at the app level. Later plugins can depend on providers exported by earlier ones.
Put organization-wide concerns (auth, audit, tracing) in plugins so all generated and inline components inherit the behavior without boilerplate.