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
skills: [MySkill], // contribute skills
})
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)}`;
}
}
Contributing Skills
Plugins can contribute skills that teach AI how to perform workflows using the plugin’s tools:
import { Plugin, Skill, Tool, ToolContext } from '@frontmcp/sdk';
import { z } from 'zod';
// Plugin tool
const deployInputSchema = z.object({
environment: z.enum(['staging', 'production']),
version: z.string(),
});
@Tool({
name: 'deploy_application',
description: 'Deploy application to specified environment',
inputSchema: deployInputSchema,
})
class DeployTool extends ToolContext<typeof deployInputSchema> {
async execute(input) {
return { success: true, environment: input.environment };
}
}
// Plugin skill that uses the tool
@Skill({
name: 'deploy-workflow',
description: 'Complete deployment workflow',
instructions: `
## Deployment Workflow
1. Use deploy_application to deploy
2. Verify deployment success
`,
tools: [
{ tool: DeployTool, purpose: 'Deploy the application', required: true },
],
tags: ['devops', 'deployment'],
})
class DeployWorkflowSkill {}
@Plugin({
name: 'devops-plugin',
description: 'DevOps tools and deployment workflows',
tools: [DeployTool],
skills: [DeployWorkflowSkill],
})
export class DevOpsPlugin {}
Plugin skills are automatically adopted into the app’s skill registry when the plugin is attached. They appear in searchSkills results alongside app-level skills and can be loaded with loadSkill.
Use plugin skills to bundle workflow knowledge with the tools they use. This creates self-contained, reusable functionality modules.
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 },
},
})
Plugin Scope
By default, plugins operate at the app scope - their hooks only fire for requests to that specific app.
For cross-app functionality, you can use server scope.
App Scope (Default)
Hooks fire only for requests to the app where the plugin is registered:
@Plugin({
name: 'my-plugin',
scope: 'app', // Default - can be omitted
})
export default class MyPlugin {
@ToolHook.Will('execute')
async beforeTool(ctx: FlowCtxOf<'tools:call-tool'>) {
// Only fires for tools in this app
}
}
Server Scope
Hooks fire at the gateway level for all apps in the server:
@Plugin({
name: 'global-logger',
scope: 'server', // Hooks fire for all apps
})
export default class GlobalLoggerPlugin {
@ToolHook.Did('execute')
async logToolCall(ctx: FlowCtxOf<'tools:call-tool'>) {
// Fires for every tool call across all apps
console.log(`Tool called: ${ctx.state.tool?.fullName}`);
}
}
Server-scoped plugins can only be used in non-standalone apps (standalone: false).
Using scope: 'server' in a standalone app will throw an InvalidPluginScopeError.
When to Use Each Scope
| Use Case | Scope | Example |
|---|
| App-specific caching | app | Cache responses for one app only |
| Per-app rate limiting | app | Different limits per app |
| Global audit logging | server | Log all tool calls across apps |
| Cross-app orchestration | server | Access tools from multiple apps |
| Authentication gateway | server | Validate tokens before any app |
Accessing Other Apps (Server Scope)
Server-scoped plugins can access other apps via scope.apps:
@Plugin({
name: 'cross-app-plugin',
scope: 'server',
})
export default class CrossAppPlugin {
@ToolHook.Will('execute')
async checkOtherApps(ctx: FlowCtxOf<'tools:call-tool'>) {
// Access all registered apps
const apps = ctx.scope.apps.getAll();
// Get a specific app by ID
const otherApp = ctx.scope.apps.get('other-app-id');
}
}
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.