Skip to main content
Hooks allow you to intercept and modify tool execution at specific lifecycle stages. Use hooks to add cross-cutting concerns like validation, logging, auditing, rate limiting, and data transformation without modifying individual tools.

Core Concepts

Flows

Named execution pipelines with defined stages (e.g., tools:call-tool, http:request)

Hook Types

Will (before), Did (after), Around (wrap), Stage (replace)

Priority

Higher priority runs first for Will/Stage; Did runs in reverse order

Context

Shared state object passed through all stages, allowing data flow between hooks

Hook Types

Runs before a stage executes. Use for:
  • Input validation
  • Pre-processing
  • Short-circuiting execution
  • Setting up context
@ToolHook.Will('execute', { priority: 100 })
async validateInput(ctx: FlowCtxOf<'tools:call-tool'>) {
  const { toolContext } = ctx.state;
  if (!toolContext) return;

  // Validate required fields
  if (!toolContext.input.amount || toolContext.input.amount <= 0) {
    throw new Error('Amount must be greater than 0');
  }
}

Available Flows

FrontMCP provides pre-defined hooks for common flows:
import { ToolHook } from '@frontmcp/sdk';

class MyPlugin {
  @ToolHook.Will('execute')
  async beforeToolExecution(ctx: FlowCtxOf<'tools:call-tool'>) {
    // Hook into tool execution
  }

  @ToolHook.Did('execute')
  async afterToolExecution(ctx: FlowCtxOf<'tools:call-tool'>) {
    // Post-process tool results
  }
}
You can also create custom flows using FlowHooksOf('custom-flow-name') for application-specific pipelines.

Complete Examples

Example 1: Request Validation & Auditing

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

@Plugin({
  name: 'audit-plugin',
  description: 'Validates requests and logs all tool executions',
})
export default class AuditPlugin {
  @ToolHook.Will('execute', { priority: 100 })
  async validateRequest(ctx: FlowCtxOf<'tools:call-tool'>) {
    const { toolContext, tool } = ctx.state;
    if (!toolContext || !tool) return;

    // Enforce tenant isolation
    const tenantId = toolContext.authInfo.user?.tenantId;
    if (!tenantId && tool.metadata.requiresTenant) {
      throw new Error('Tenant ID required for this tool');
    }

    // Rate limiting check
    await this.checkRateLimit(tenantId, tool.fullName);
  }

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

    // Log execution
    await this.auditLog.record({
      toolName: tool.fullName,
      userId: toolContext.authInfo.user?.id,
      tenantId: toolContext.authInfo.user?.tenantId,
      input: toolContext.input,
      output: toolContext.output,
      timestamp: new Date().toISOString(),
      duration: ctx.metrics?.duration,
    });
  }

  private async checkRateLimit(tenantId: string, toolName: string) {
    // Rate limiting logic
  }
}

Example 2: Error Handling & Retries

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

@Plugin({
  name: 'resilience-plugin',
  description: 'Adds retry logic and error handling to tools',
})
export default class ResiliencePlugin {
  @ToolHook.Around('execute')
  async withRetry(ctx: FlowCtxOf<'tools:call-tool'>, next: () => Promise<void>) {
    const { tool, toolContext } = ctx.state;
    if (!tool || !toolContext) return await next();

    const maxRetries = tool.metadata.retries ?? 0;
    let lastError: Error | undefined;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        if (attempt > 0) {
          this.logger.warn(`Retry attempt ${attempt} for ${tool.fullName}`);
          await this.delay(attempt * 1000); // Exponential backoff
        }

        await next();
        return; // Success
      } catch (error) {
        lastError = error as Error;

        // Don't retry on validation errors
        if (error.message.includes('validation')) {
          throw error;
        }

        if (attempt === maxRetries) {
          throw lastError;
        }
      }
    }
  }

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

    // Transform errors into user-friendly messages
    if (ctx.error) {
      this.logger.error('Tool execution failed', {
        tool: toolContext.toolName,
        error: ctx.error.message,
      });

      // Could transform or wrap the error here
    }
  }

  private delay(ms: number) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

Example 3: Data Transformation Pipeline

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

@Plugin({
  name: 'transform-plugin',
  description: 'Transforms inputs and outputs',
})
export default class TransformPlugin {
  @ToolHook.Will('execute', { priority: 50 })
  async normalizeInput(ctx: FlowCtxOf<'tools:call-tool'>) {
    const { toolContext } = ctx.state;
    if (!toolContext) return;

    // Normalize string inputs
    if (typeof toolContext.input === 'object') {
      for (const [key, value] of Object.entries(toolContext.input)) {
        if (typeof value === 'string') {
          toolContext.input[key] = value.trim().toLowerCase();
        }
      }
    }
  }

  @ToolHook.Did('execute', { priority: 50 })
  async enrichOutput(ctx: FlowCtxOf<'tools:call-tool'>) {
    const { toolContext, tool } = ctx.state;
    if (!toolContext?.output || typeof toolContext.output !== 'object') {
      return;
    }

    // Add metadata to all responses
    toolContext.output = {
      ...toolContext.output,
      _metadata: {
        toolName: tool?.fullName,
        executedAt: new Date().toISOString(),
        version: tool?.metadata.version || '1.0.0',
      },
    };
  }
}

Priority System

Priority determines execution order. Higher priority runs first for Will/Stage hooks, and last for Did hooks.
class PriorityExample {
  @ToolHook.Will('execute', { priority: 100 })
  async firstValidation() {
    // Runs first
  }

  @ToolHook.Will('execute', { priority: 50 })
  async secondValidation() {
    // Runs second
  }

  @ToolHook.Did('execute', { priority: 100 })
  async firstCleanup() {
    // Runs last (Did reverses order)
  }

  @ToolHook.Did('execute', { priority: 50 })
  async secondCleanup() {
    // Runs first
  }
}

Flow Context

The flow context (FlowCtxOf<'flow-name'>) contains:
  • state - Shared state between hooks (tool, toolContext, request, response, etc.)
  • error - Any error that occurred during execution
  • metrics - Timing and performance data
  • logger - Logger instance for this flow
@ToolHook.Will('execute')
async logToolInfo(ctx: FlowCtxOf<'tools:call-tool'>) {
  const { tool, toolContext } = ctx.state;

  ctx.logger.info('Executing tool', {
    name: tool?.fullName,
    input: toolContext?.input,
  });
}

Hook Registry API

For advanced use cases, you can programmatically access and manage hooks using the Hook Registry API. This is useful when building custom flow orchestration or when you need to dynamically query which hooks are registered.

Accessing the Hook Registry

import { FlowCtxOf } from '@frontmcp/sdk';

class MyPlugin {
  @ToolHook.Will('execute')
  async checkRegisteredHooks(ctx: FlowCtxOf<'tools:call-tool'>) {
    const hookRegistry = ctx.scope.providers.getHooksRegistry();

    // Now you can use registry methods
    const hooks = hookRegistry.getFlowHooks('tools:call-tool');
  }
}

Registry Methods

Retrieves all hooks registered for a specific flow.
const hooks = hookRegistry.getFlowHooks('tools:call-tool');
// Returns: HookEntry[] - all hooks for this flow

Example: Owner-Scoped Hook Filtering

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

@Plugin({
  name: 'tool-isolation-plugin',
  description: 'Demonstrates owner-scoped hook retrieval',
})
export default class ToolIsolationPlugin {
  @ToolHook.Will('execute')
  async filterHooksByOwner(ctx: FlowCtxOf<'tools:call-tool'>) {
    const { toolContext } = ctx.state;
    if (!toolContext) return;

    const hookRegistry = ctx.scope.providers.getHooksRegistry();
    const toolOwnerId = toolContext.tool?.metadata.owner?.id;

    // Get only hooks relevant to this specific tool
    const relevantHooks = hookRegistry.getFlowHooksForOwner('tools:call-tool', toolOwnerId);

    ctx.logger.info(`Found ${relevantHooks.length} hooks for this tool`, {
      toolOwnerId,
      hooks: relevantHooks.map((h) => h.metadata.stage),
    });
  }
}
The Hook Registry API is an advanced feature primarily intended for framework developers and complex plugin authors. Most users should use the decorator-based approach (@ToolHook, @HttpHook, etc.) instead.

Best Practices

Package related hooks into plugins to reuse across multiple apps:
@Plugin({
  name: 'my-hooks',
  description: 'Reusable hook collection',
})
export default class MyHooksPlugin {
  @ToolHook.Will('execute')
  async myValidation(ctx) {
    // Validation logic
  }
}

// Use in app
@App({
  plugins: [MyHooksPlugin],
})
  • Validation: High priority (90-100)
  • Transformation: Medium priority (40-60)
  • Logging/Metrics: Low priority (1-20)
This ensures validation runs before transformation, and logging captures everything.
@ToolHook.Will('execute')
async safeValidation(ctx) {
  try {
    await this.validate(ctx.state.toolContext?.input);
  } catch (error) {
    // Transform validation errors
    throw new ValidationError(`Invalid input: ${error.message}`);
  }
}
@ToolHook.Will('execute')
async startTimer(ctx) {
  ctx.state.startTime = Date.now();
}

@ToolHook.Did('execute')
async endTimer(ctx) {
  const duration = Date.now() - ctx.state.startTime;
  this.logger.info(`Execution took ${duration}ms`);
}
Hooks run for every tool execution. Keep them fast:
  • Use caching for expensive operations
  • Delegate heavy work to background jobs
  • Consider async/non-blocking operations