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:
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>;
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
Intercept tool execution flow:
| Stage | Phase | Description |
|---|
parseInput | pre | Parse and validate request |
findTool | pre | Locate the requested tool |
createToolCallContext | pre | Create execution context |
acquireQuota | pre | Rate limiting |
acquireSemaphore | pre | Concurrency control |
validateInput | execute | Validate tool input against schema |
execute | execute | Run the tool |
validateOutput | execute | Validate tool output |
releaseSemaphore | finalize | Release concurrency |
releaseQuota | finalize | Release rate limit |
finalize | finalize | Format and return response |
Intercept tool listing flow:
| Stage | Phase | Description |
|---|
parseInput | pre | Parse request |
findTools | execute | Collect available tools |
resolveConflicts | execute | Handle name conflicts |
parseTools | post | Format 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 Parameter | Description |
|---|
TOptions | Resolved options type (after defaults). Use internally. |
TInput | Input 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.
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.