modelcontextprotocol / modelcontextprotocol/typescript-sdk

McpServer: add tool lifecycle hooks (beforeToolCall, afterToolCall)

Open
#1,928 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement needs decision P3
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:

  1. Wrap every ToolCallback individually before passing it to registerTool()
  2. Drop down to the low-level Server class and reimplement tool registration — but setToolRequestHandlers() is private and calls assertCanSetRequestHandler(), which prevents overriding the tools/call handler 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.