Skip to main content

Creating Plugins

Build custom plugins to extend FrontMCP with cross-cutting capabilities like caching, authorization, logging, and more.

Plugin Architecture

FrontMCP plugins use the @Plugin decorator and typically extend DynamicPlugin. They can:
  1. Register providers — Services available to the plugin and exported to the host app
  2. Contribute tools, resources, and skills — Add capabilities when the plugin is attached
  3. Intercept flows via hooks — Run code before/after specific stages using @ToolHook and @ListToolsHook
  4. Accept configuration — Via init() for runtime customization
  5. Extend metadata — Add custom fields to tool metadata

Basic Plugin

import { DynamicPlugin, Plugin, ToolHook, FlowCtxOf } from '@frontmcp/sdk';

interface AuditPluginOptions {
  logLevel?: 'info' | 'debug';
}

@Plugin({
  name: 'audit',
  description: 'Logs all tool executions',
})
export default class AuditPlugin extends DynamicPlugin<AuditPluginOptions> {
  options: AuditPluginOptions;

  constructor(options: AuditPluginOptions = {}) {
    super();
    this.options = { logLevel: 'info', ...options };
  }

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

    console.log(`[audit] Tool executed: ${tool.fullName}`);
  }
}

Registering a Plugin

Attach plugins at the app level:
import { App } from '@frontmcp/sdk';
import AuditPlugin from './plugins/audit.plugin';

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

    // Option 2: Use init() with custom options
    AuditPlugin.init({ logLevel: 'debug' }),
  ],
})
export default class MyApp {}

Adding Hooks

Plugins intercept flow stages using @ToolHook and @ListToolsHook decorators:
import { DynamicPlugin, Plugin, ToolHook, FlowCtxOf, FlowHooksOf } from '@frontmcp/sdk';

const ListToolsHook = FlowHooksOf('tools:list-tools');

@Plugin({
  name: 'authorization',
  description: 'Role-based access control for tools',
})
export default class AuthorizationPlugin extends DynamicPlugin {
  // Runs BEFORE tool execution
  @ToolHook.Will('execute', { priority: 900 })
  async validateAccess(flowCtx: FlowCtxOf<'tools:call-tool'>) {
    const { toolContext } = flowCtx.state;
    if (!toolContext) return;
    // Check authorization...
  }

  // Runs AFTER tool listing — filter unauthorized tools
  @ListToolsHook.Did('findTools')
  async filterTools(flowCtx: FlowCtxOf<'tools:list-tools'>) {
    const { tools } = flowCtx.state.required;
    // Filter tools based on user roles
    const filteredTools = tools.filter((t) => this.isToolAllowed(t));
    flowCtx.state.set('tools', filteredTools);
  }

  /** TODO: implement real role check */
  private isToolAllowed(_tool: unknown): boolean {
    return true;
  }
}

Hook Timing

  • .Will(stage) — runs before the stage
  • .Did(stage) — runs after the stage

Priority

Lower numbers run first:
PriorityUse Case
100–500Critical security checks
500–900Authorization, validation
900–1000Standard plugin behavior
1000+Logging, metrics

Dynamic Providers

For plugins that create providers based on configuration:
import { DynamicPlugin, Plugin, ProviderType, ToolHook, FlowCtxOf } from '@frontmcp/sdk';

interface CachePluginOptions {
  type: 'memory' | 'redis';
  host?: string;
  port?: number;
}

const CacheStoreToken = Symbol('CacheStore');

@Plugin({
  name: 'cache',
  description: 'Cache plugin for tool results',
  providers: [
    {
      name: 'cache:memory',
      provide: CacheStoreToken,
      useValue: new MemoryCacheProvider(),
    },
  ],
})
export default class CachePlugin extends DynamicPlugin<CachePluginOptions> {
  static override dynamicProviders(options: CachePluginOptions): ProviderType[] {
    if (options.type === 'redis') {
      return [{
        name: 'cache:redis',
        provide: CacheStoreToken,
        useValue: new RedisCacheProvider(options),
      }];
    }
    return [];
  }

  @ToolHook.Will('execute', { priority: 950 })
  async checkCache(flowCtx: FlowCtxOf<'tools:call-tool'>) {
    const cacheStore = this.get(CacheStoreToken);
    // Check cache and respond early if hit...
  }

  @ToolHook.Did('execute', { priority: 950 })
  async storeCache(flowCtx: FlowCtxOf<'tools:call-tool'>) {
    const cacheStore = this.get(CacheStoreToken);
    // Store result in cache...
  }
}

Extending Tool Metadata

Plugins can add custom fields to tool metadata via global type augmentation:
declare global {
  interface ExtendFrontMcpToolMetadata {
    cache?: { ttl?: number } | true;
  }
}
Tools can then use this metadata:
@Tool({
  name: 'get-user',
  inputSchema: { id: z.string() },
  cache: { ttl: 3600 },
})
export default class GetUserTool extends ToolContext { /* ... */ }

Contributing Tools and Skills

Plugins can contribute tools and skills via the @Plugin decorator:
import { DynamicPlugin, Plugin } from '@frontmcp/sdk';

@Plugin({
  name: 'devops',
  description: 'DevOps tools and workflows',
  tools: [DeployTool, RollbackTool],
  skills: [DeployWorkflowSkill],
})
export default class DevOpsPlugin extends DynamicPlugin {}

Publishing Plugins

# Recommended package structure
my-plugin/
├── src/
   ├── index.ts              # Exports
   ├── my-plugin.plugin.ts   # Plugin class
   └── my-plugin.types.ts    # Types
├── package.json
└── README.md
package.json
{
  "name": "@yourscope/frontmcp-plugin-myfeature",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "keywords": ["frontmcp", "frontmcp-plugin", "mcp", "plugin"],
  "peerDependencies": {
    "@frontmcp/sdk": "^0.4.0"
  }
}

Next Steps

Plugin Guide

Full plugin API reference with hooks, scopes, and DynamicPlugin details

Create a Plugin

Step-by-step tutorial building a real-world plugin

Cache Plugin

Study the built-in cache plugin implementation

Community

Share your plugin with the community