Skip to main content

Hook Types

DecoratorTimingUse Case
@StageDefine execution stageCustom flow steps
@WillBefore stage executesPre-processing, validation
@DidAfter stage executesPost-processing, logging
@AroundWraps stage executionCaching, error handling

FlowHooksOf

Create typed hook decorators for a specific flow:
import { FlowHooksOf } from '@frontmcp/sdk';

const { Stage, Will, Did, Around } = FlowHooksOf('tools:call-tool');

class MyHooks {
  @Will('execute')
  async beforeExecute(ctx: ToolFlowContext) {
    console.log('Before tool execution');
  }

  @Did('execute')
  async afterExecute(ctx: ToolFlowContext) {
    console.log('After tool execution');
  }
}

Available Flows

Flow NameDescription
tools:call-toolTool execution
tools:list-toolsList tools
resources:read-resourceRead resource
resources:list-resourcesList resources
prompts:get-promptGet prompt
prompts:list-promptsList prompts
http:requestHTTP request handling

@Will (Before Hook)

Execute before a flow stage:
const { Will } = FlowHooksOf('tools:call-tool');

class ValidationHooks {
  @Will('execute', { priority: 10 })
  async validateInput(ctx: ToolFlowContext) {
    const input = ctx.state.get('input');
    if (!input) {
      throw new Error('Input is required');
    }
  }

  @Will('execute', { priority: 20 })
  async logStart(ctx: ToolFlowContext) {
    console.log(`Starting tool: ${ctx.state.get('toolName')}`);
  }
}

Priority

Lower priority values execute first:
@Will('execute', { priority: 10 }) // Executes first
@Will('execute', { priority: 20 }) // Executes second

Filter

Conditionally execute hooks:
@Will('execute', {
  filter: (ctx) => ctx.state.get('toolName') === 'sensitive_tool'
})
async logSensitiveAccess(ctx) {
  console.log('Accessing sensitive tool');
}

@Did (After Hook)

Execute after a flow stage:
const { Did } = FlowHooksOf('tools:call-tool');

class AuditHooks {
  @Did('execute')
  async auditToolCall(ctx: ToolFlowContext) {
    const toolName = ctx.state.get('toolName');
    const output = ctx.state.get('output');
    await this.auditService.log({
      tool: toolName,
      timestamp: new Date(),
      success: !ctx.state.get('error'),
    });
  }
}

@Around (Wrapper Hook)

Wrap stage execution for full control:
const { Around } = FlowHooksOf('tools:call-tool');

class CachingHooks {
  @Around('execute')
  async cacheResults(ctx: ToolFlowContext, next: () => Promise<void>) {
    const cacheKey = this.getCacheKey(ctx);
    const cached = await this.cache.get(cacheKey);

    if (cached) {
      ctx.state.set('output', cached);
      return; // Skip actual execution
    }

    await next(); // Execute the stage

    const output = ctx.state.get('output');
    await this.cache.set(cacheKey, output);
  }
}

@Stage (Custom Stage)

Define custom flow stages:
const { Stage, Will, Did } = FlowHooksOf('tools:call-tool');

class CustomFlow {
  @Stage('preProcess')
  async preProcess(ctx: ToolFlowContext) {
    // Custom pre-processing stage
    const input = ctx.state.get('input');
    ctx.state.set('processedInput', transform(input));
  }

  @Will('preProcess')
  async beforePreProcess(ctx) {
    console.log('About to pre-process');
  }

  @Did('preProcess')
  async afterPreProcess(ctx) {
    console.log('Pre-processing complete');
  }
}

Hook Options

interface HookOptions<Ctx = unknown> {
  priority?: number;                              // Execution order (lower = first)
  filter?: (ctx: Ctx) => boolean | Promise<boolean>; // Conditional execution
}

Registering Hooks

In Apps

@App({
  name: 'my-app',
  providers: [AuditHooks, CachingHooks],
  tools: [MyTool],
})
class MyApp {}

In Plugins

@Plugin({
  name: 'audit-plugin',
  providers: [AuditHooks],
})
class AuditPlugin {}

Hook Context

Hooks receive flow context with access to:
interface FlowContext {
  state: FlowState;           // Flow state (input, output, metadata)
  logger: FrontMcpLogger;     // Logger instance
  providers: ProviderRegistry; // Dependency injection
}

// Access state
const input = ctx.state.get('input');
ctx.state.set('output', result);

// Access providers
const service = ctx.providers.get(ServiceToken);

// Access logger
ctx.logger.info('Processing');

Error Handling

Errors in hooks are caught and logged:
@Will('execute')
async validateInput(ctx) {
  // Throwing stops execution and reports error
  throw new InvalidInputError('Validation failed');
}

@Around('execute')
async errorHandler(ctx, next) {
  try {
    await next();
  } catch (error) {
    ctx.logger.error('Execution failed', { error });
    // Re-throw or handle
    throw error;
  }
}

Full Example

import { FlowHooksOf, Provider, Plugin, App, FrontMcp, Tool, ToolContext } from '@frontmcp/sdk';
import { z } from 'zod';

const { Will, Did, Around } = FlowHooksOf('tools:call-tool');

// Audit hook provider
@Provider()
class AuditHooks {
  private auditLog: Array<{ tool: string; timestamp: Date; duration: number }> = [];

  @Will('execute', { priority: 0 })
  async recordStart(ctx) {
    ctx.state.set('_auditStartTime', Date.now());
  }

  @Did('execute', { priority: 1000 })
  async recordEnd(ctx) {
    const startTime = ctx.state.get('_auditStartTime');
    const duration = Date.now() - startTime;
    const toolName = ctx.state.get('toolName');

    this.auditLog.push({
      tool: toolName,
      timestamp: new Date(),
      duration,
    });

    ctx.logger.info(`Tool ${toolName} completed in ${duration}ms`);
  }

  getAuditLog() {
    return this.auditLog;
  }
}

// Rate limiting hook
@Provider()
class RateLimitHooks {
  private calls = new Map<string, number[]>();
  private maxCallsPerMinute = 60;

  @Will('execute', { priority: 5 })
  async checkRateLimit(ctx) {
    const toolName = ctx.state.get('toolName');
    const now = Date.now();
    const oneMinuteAgo = now - 60000;

    const recentCalls = (this.calls.get(toolName) || [])
      .filter(t => t > oneMinuteAgo);

    if (recentCalls.length >= this.maxCallsPerMinute) {
      throw new RateLimitError(60);
    }

    recentCalls.push(now);
    this.calls.set(toolName, recentCalls);
  }
}

// Caching hook
@Provider()
class CacheHooks {
  private cache = new Map<string, { value: unknown; expires: number }>();
  private ttl = 60000; // 1 minute

  @Around('execute', {
    filter: (ctx) => ctx.state.get('toolMetadata')?.annotations?.idempotentHint === true
  })
  async cacheIdempotent(ctx, next) {
    const toolName = ctx.state.get('toolName');
    const input = ctx.state.get('input');
    const cacheKey = `${toolName}:${JSON.stringify(input)}`;

    const cached = this.cache.get(cacheKey);
    if (cached && cached.expires > Date.now()) {
      ctx.logger.debug('Cache hit', { toolName });
      ctx.state.set('output', cached.value);
      return;
    }

    await next();

    const output = ctx.state.get('output');
    this.cache.set(cacheKey, {
      value: output,
      expires: Date.now() + this.ttl,
    });
  }
}

// Plugin bundling hooks
@Plugin({
  name: 'observability',
  description: 'Audit logging, rate limiting, and caching',
  providers: [AuditHooks, RateLimitHooks, CacheHooks],
})
class ObservabilityPlugin {}

// Sample tool
@Tool({
  name: 'get_data',
  inputSchema: { key: z.string() },
  annotations: { idempotentHint: true },
})
class GetDataTool extends ToolContext {
  async execute(input: { key: string }) {
    return { data: `Value for ${input.key}` };
  }
}

// App using plugin
@App({
  name: 'data-app',
  plugins: [ObservabilityPlugin],
  tools: [GetDataTool],
})
class DataApp {}

@FrontMcp({
  info: { name: 'Observable Server', version: '1.0.0' },
  apps: [DataApp],
})
export default class ObservableServer {}

HookRegistry

Hook registry API

Flow Types

Flow type definitions

@Plugin

Create plugins

Customize Flows

Flow customization guide