@civic/hook-common
v0.9.0
Published
Common utilities and types for implementing MCP server hooks
Readme
@civic/hook-common
Common utilities and types for implementing MCP (Model Context Protocol) server hooks.
This package provides the core functionality for creating hooks that can intercept and modify tool calls in MCP servers.
Think of it as a middleware layer that allows you to analyze, modify, or validate tool calls before they are executed, and to process the responses from those tool calls after execution.
It is designed to be used in combination with the Passthrough Proxy MCP server.
Installation
pnpm add @civic/hook-commonOverview
The hook-common package provides:
- Type definitions for tool calls, hook responses, and metadata
- Base client class for implementing hooks
- Utilities for processing and validating hook interactions
Core Concepts
Tool Calls
Tool calls use the MCP SDK types directly:
import type { CallToolRequest, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
// Tool call request structure
interface CallToolRequest {
method: "tools/call";
params: {
name: string;
arguments?: unknown;
_meta?: {
sessionId?: string;
// other metadata
};
};
}Hook Results
Hooks return discriminated unions based on the result type:
// For request processing
type ToolCallRequestHookResult =
| { resultType: "continue"; request: CallToolRequest }
| { resultType: "abort"; reason: string; body?: unknown }
| { resultType: "respond"; response: CallToolResult }
| {
resultType: "continueAsync";
request: CallToolRequest;
response: CallToolResult;
callback: (response: CallToolResult | null, error: HookChainError | null) => void | Promise<void>;
};
// For response processing
type ToolCallResponseHookResult =
| { resultType: "continue"; response: CallToolResult }
| { resultType: "abort"; reason: string; body?: unknown };continueAsync Result Type (v0.7.0+)
The continueAsync result type allows a hook to return an immediate response to the client while continuing async processing through the remaining hooks:
async processCallToolRequest(
request: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolRequestHookResult> {
return {
resultType: "continueAsync",
request,
response: {
content: [{ type: "text", text: "Processing started..." }]
},
callback: async (finalResponse, error) => {
if (error) {
console.error("Processing failed:", error);
// Handle error (e.g., send notification, log to external service)
} else {
console.log("Processing completed:", finalResponse);
// Handle success (e.g., update database, send notification)
}
}
};
}Important Limitation: The continueAsync result type is NOT supported over tRPC (RemoteHookClient). Callbacks cannot be serialized and sent over the network. Only use continueAsync with:
- Local hooks (LocalHookClient)
- Direct Hook instances
For remote hooks, use respond, continue, or abort result types instead.
Hook Interface
The interface for implementing hooks (v0.4.1+):
interface RequestExtra {
requestId: string | number;
sessionId?: string;
}
interface Hook {
get name(): string;
// Request processing methods - receive RequestExtra as second parameter
processCallToolRequest?(
request: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolRequestHookResult>;
processListToolsRequest?(
request: ListToolsRequest,
requestExtra: RequestExtra
): Promise<ListToolsRequestHookResult>;
processInitializeRequest?(
request: InitializeRequest,
requestExtra: RequestExtra
): Promise<InitializeRequestHookResult>;
processListResourcesRequest?(
request: ListResourcesRequestWithContext,
requestExtra: RequestExtra
): Promise<ListResourcesRequestHookResult>;
processListResourceTemplatesRequest?(
request: ListResourceTemplatesRequestWithContext,
requestExtra: RequestExtra
): Promise<ListResourceTemplatesRequestHookResult>;
processReadResourceRequest?(
request: ReadResourceRequestWithContext,
requestExtra: RequestExtra
): Promise<ReadResourceRequestHookResult>;
// Response processing methods - receive RequestExtra as third parameter
processCallToolResult?(
response: CallToolResult,
originalCallToolRequest: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolResponseHookResult>;
processListToolsResult?(
response: ListToolsResult,
originalRequest: ListToolsRequest,
requestExtra: RequestExtra
): Promise<ListToolsResponseHookResult>;
processInitializeResult?(
response: InitializeResult,
originalRequest: InitializeRequest,
requestExtra: RequestExtra
): Promise<InitializeResponseHookResult>;
processListResourcesResult?(
response: ListResourcesResult,
originalRequest: ListResourcesRequestWithContext,
requestExtra: RequestExtra
): Promise<ListResourcesResponseHookResult>;
processListResourceTemplatesResult?(
response: ListResourceTemplatesResult,
originalRequest: ListResourceTemplatesRequestWithContext,
requestExtra: RequestExtra
): Promise<ListResourceTemplatesResponseHookResult>;
processReadResourceResult?(
response: ReadResourceResult,
originalRequest: ReadResourceRequestWithContext,
requestExtra: RequestExtra
): Promise<ReadResourceResponseHookResult>;
// ... other methods follow the same pattern
}Creating a Hook
To create a custom hook, extend the AbstractHook class:
import { AbstractHook } from '@civic/hook-common';
import type {
CallToolRequest,
CallToolResult,
RequestExtra,
CallToolRequestHookResult,
CallToolResponseHookResult
} from '@civic/hook-common';
export class MyCustomHook extends AbstractHook {
get name(): string {
return 'my-custom-hook';
}
async processCallToolRequest(
request: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolRequestHookResult> {
// Use requestId for tracking
console.log(`[${requestExtra.requestId}] Processing request for tool: ${request.params.name}`);
console.log(`Session ID: ${requestExtra.sessionId}`);
// Optionally modify the tool call
const modifiedRequest = {
...request,
params: {
...request.params,
arguments: {
...request.params.arguments,
injected: 'value'
}
}
};
// Return response
return {
resultType: 'continue',
request: modifiedRequest
};
}
async processCallToolResult(
response: CallToolResult,
originalCallToolRequest: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolResponseHookResult> {
// Use the same requestId to correlate with the request
console.log(`[${requestExtra.requestId}] Processing response for tool: ${originalCallToolRequest.params.name}`);
// Optionally modify the response
return {
resultType: 'continue',
response: response
};
}
}Request Tracking with RequestExtra (v0.4.1+)
The RequestExtra parameter enables powerful request tracking capabilities:
- Request ID: Unique identifier for each request, enabling correlation between request and response processing
- Session ID: Consistent identifier throughout a client session, useful for session-based analytics
Use Cases
- Request/Response Correlation: Match responses to their originating requests without maintaining state
- Distributed Tracing: Track requests across multiple hooks and services
- Rate Limiting: Implement per-session or per-request rate limiting
- Audit Logging: Create comprehensive audit trails with request/session context
- Performance Monitoring: Measure processing time for each request
Hook Execution Flow
Request Processing: When a tool is called, hooks process the request in order
Tool Execution: If all hooks return "continue", the tool executes
Response Processing: Hooks process the tool's response in reverse order
Error Processing (v0.4.2+): Hooks can intercept and handle errors through dedicated error callbacks
graph LR
A[Tool Call] --> B[Hook 1 Request]
B --> C[Hook 2 Request]
C --> D[Tool Execution]
D --> E[Hook 2 Response/Error]
E --> F[Hook 1 Response/Error]
F --> G[Final Response]Error Handling (v0.4.2+)
Hooks can now process errors that occur during request processing:
interface Hook {
// Error processing methods
processCallToolError?(
error: HookChainError,
originalRequest: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolErrorHookResult>;
processListToolsError?(
error: HookChainError,
originalRequest: ListToolsRequest,
requestExtra: RequestExtra
): Promise<ListToolsErrorHookResult>;
// ... other error methods
}Error processing allows hooks to:
- Transform errors: Modify error messages or codes
- Recover from errors: Convert an error into a successful response
- Pass through errors: Let the error continue unchanged
export class ErrorHandlingHook extends AbstractHook {
async processCallToolError(
error: HookChainError,
originalRequest: CallToolRequest,
requestExtra: RequestExtra
): Promise<CallToolErrorHookResult> {
// Log the error
console.error(`Error in tool ${originalRequest.params.name}:`, error);
// Option 1: Transform the error
throw new Error(`Custom error: ${error.message}`);
// Option 2: Recover from the error
return {
resultType: 'respond',
response: {
content: [{ type: 'text', text: 'Recovered from error' }]
}
};
// Option 3: Pass through unchanged
return { resultType: 'continue' };
}
}Type Safety
All types are exported with Zod schemas for runtime validation:
import {
ToolCallRequestHookResultSchema,
ToolCallResponseHookResultSchema
} from '@civic/hook-common';
// Validate hook request results
const validatedRequestResult = ToolCallRequestHookResultSchema.parse(hookRequestResult);
// Validate hook response results
const validatedResponseResult = ToolCallResponseHookResultSchema.parse(hookResponseResult);Examples
Logging Hook
export class LoggingHook extends AbstractHook {
get name(): string {
return 'logging-hook';
}
async processCallToolRequest(request: CallToolRequest): Promise<ToolCallRequestHookResult> {
console.log(`[${new Date().toISOString()}] Tool called: ${toolCall.params.name}`);
console.log('Arguments:', JSON.stringify(toolCall.params.arguments, null, 2));
return {
resultType: 'continue',
request: toolCall
};
}
async processCallToolResult(
response: CallToolResult,
originalCallToolRequest: CallToolRequest
): Promise<ToolCallResponseHookResult> {
console.log(`[${new Date().toISOString()}] Response from: ${originalCallToolRequest.params.name}`);
console.log('Response:', JSON.stringify(response, null, 2));
return {
resultType: 'continue',
response: response
};
}
}Validation Hook
export class ValidationHook extends AbstractHook {
get name(): string {
return 'validation-hook';
}
async processCallToolRequest(request: CallToolRequest): Promise<ToolCallRequestHookResult> {
// Validate tool calls
if (toolCall.params.name === 'dangerous-tool') {
return {
resultType: 'abort',
reason: 'This tool is not allowed',
body: null
};
}
return {
resultType: 'continue',
request: toolCall
};
}
async processCallToolResult(
response: CallToolResult,
originalCallToolRequest: CallToolRequest
): Promise<ToolCallResponseHookResult> {
return {
resultType: 'continue',
response: response
};
}
}API Reference
Types
Hook- Interface for implementing hooksToolCallRequestHookResult- Result type for request processingToolCallResponseHookResult- Result type for response processingListToolsRequestHookResult- Result type for tools list request processingListToolsResponseHookResult- Result type for tools list response processingToolCallTransportErrorHookResult- Result type for tool call transport error processingListToolsTransportErrorHookResult- Result type for tools list transport error processingInitializeRequestHookResult- Result type for initialize request processingInitializeResponseHookResult- Result type for initialize response processingInitializeTransportErrorHookResult- Result type for initialize transport error processingTransportError- Error type for transport-layer errorsCallToolRequest,CallToolResult,ListToolsRequest,ListToolsResult,InitializeRequest,InitializeResult- Re-exported from MCP SDK
Schemas
ToolCallRequestHookResultSchema- Zod schema for request hook result validationToolCallResponseHookResultSchema- Zod schema for response hook result validationListToolsRequestHookResultSchema- Zod schema for tools list request result validationListToolsResponseHookResultSchema- Zod schema for tools list response result validationToolCallTransportErrorHookResultSchema- Zod schema for tool call transport error validationListToolsTransportErrorHookResultSchema- Zod schema for tools list transport error validationInitializeRequestHookResultSchema- Zod schema for initialize request validationInitializeResponseHookResultSchema- Zod schema for initialize response validationInitializeTransportErrorHookResultSchema- Zod schema for initialize transport error validationTransportErrorSchema- Zod schema for transport error validation
Classes
AbstractHook- Abstract base class for implementing hooks with default pass-through implementations
Utilities
createHookRouter- Creates a tRPC router for hook implementationcreateLocalHookClient- Creates a local client for a hook instance
License
MIT
