
Research
Shai-Hulud Descends to Hades: Miasma Worm Campaign Spreads with New PyPI Wave
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.
@civic/hook-common
Advanced tools
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.
pnpm add @civic/hook-common
The hook-common package provides:
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
};
};
}
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 };
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:
For remote hooks, use respond, continue, or abort result types instead.
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
}
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
};
}
}
The RequestExtra parameter enables powerful request tracking capabilities:
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]
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:
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' };
}
}
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);
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
};
}
}
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
};
}
}
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 SDKToolCallRequestHookResultSchema - 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 validationAbstractHook - Abstract base class for implementing hooks with default pass-through implementationscreateHookRouter - Creates a tRPC router for hook implementationcreateLocalHookClient - Creates a local client for a hook instanceMIT
FAQs
Common utilities and types for implementing MCP server hooks
The npm package @civic/hook-common receives a total of 223 weekly downloads. As such, @civic/hook-common popularity was classified as not popular.
We found that @civic/hook-common demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 13 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.

Security News
RubyGems and Bundler 4.0.13 introduced an opt-in cooldown feature that delays newly published gems during dependency resolution.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.