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 {}