modelcontextprotocol / modelcontextprotocol/typescript-sdk
McpServer: add tool lifecycle hooks (beforeToolCall, afterToolCall)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
Problem
McpServer has no mechanism for server-wide cross-cutting concerns on tool calls.
In setToolRequestHandlers(), the tools/call handler validates input, calls executeToolHandler(), validates output, and returns the result. There is no interception point between execution and response — no hook, no middleware, no event.
Today, to apply any behavior to all tool results (logging, metrics, output management, error enrichment), server authors must either:
- Wrap every
ToolCallbackindividually before passing it toregisterTool() - Drop down to the low-level
Serverclass and reimplement tool registration — butsetToolRequestHandlers()is private and callsassertCanSetRequestHandler(), which prevents overriding thetools/callhandler externally
Neither option scales for cross-cutting concerns that should apply uniformly to all tools.
Proposal
Add an optional hooks object to the McpServer constructor:
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{
hooks: {
beforeToolCall: async ({ name, arguments: args }) => {
console.log(`Calling tool: ${name}`);
},
afterToolCall: async ({ name, result }) => {
console.log(`Tool ${name} returned ${JSON.stringify(result).length} bytes`);
return result; // optionally transform
},
},
}
);
The implementation would be a small addition in setToolRequestHandlers(), in the tools/call request handler, around the existing executeToolHandler() call:
// before execution
if (this._hooks?.beforeToolCall) {
await this._hooks.beforeToolCall({ name: request.params.name, arguments: args });
}
const result = await this.executeToolHandler(tool, args, ctx);
// after execution (before output validation)
if (this._hooks?.afterToolCall) {
result = await this._hooks.afterToolCall({ name: request.params.name, result });
}
Use cases
- Logging/auditing: Log every tool invocation and result size
- Metrics and timing: Record latency per tool call
- Output management: Save large results to file/storage, return a reference instead
- Error enrichment: Add context to error results before returning
- Rate limiting: Throttle tool calls server-wide
Scope
- Optional, additive — zero breaking changes
- Small surface area: one new type, one optional constructor parameter
- Consistent with established patterns (Express middleware, gRPC interceptors)
I noticed this gap while building MCP servers that need cross-cutting concerns across all tools. Happy to submit a PR if this direction is acceptable.
Labels to request: enhancement
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in McpServer's constructor and setToolRequestHandlers(), then trace executeToolHandler() and the existing tools/call validation flow. Review how the optional hook types should fit this API and verify the proposed beforeToolCall and afterToolCall points, including result transformation before output validation. Done means the hooks apply consistently to server-wide tool calls without breaking existing construction or execution.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100