How to Create Custom Tools in MCP Servers Using the TypeScript SDK
To create custom tools in MCP servers using the TypeScript SDK, define a Zod schema for input validation, create a metadata configuration object with name and description, implement an async handler that returns a CallToolResult, and register the tool with the McpServer instance via server.registerTool().
The Model Context Protocol (MCP) enables servers to expose functionality to clients through declarative, schema-validated tools. When you create custom tools in MCP servers using the TypeScript SDK, you follow the modular registration pattern established in the modelcontextprotocol/servers repository. This guide demonstrates the exact implementation used by built-in tools, covering schema definition, handler implementation, and server integration.
Define the Tool's Input Schema with Zod
Every custom tool requires strict input validation to ensure type safety. In src/everything/tools/echo.ts, the EchoSchema uses Zod to describe expected arguments:
import { z } from "zod";
export const EchoSchema = z.object({
message: z.string().describe("Message to echo"),
});
The schema automatically validates incoming arguments against defined types before your handler executes, preventing invalid data from reaching your business logic.
Describe the Tool with Metadata
Create a configuration object containing the tool's name, title, description, and inputSchema. In the modelcontextprotocol/servers codebase, this metadata object accompanies the schema definition in the same file:
const name = "echo";
const config = {
title: "Echo Tool",
description: "Echoes back the input string",
inputSchema: EchoSchema,
};
This metadata registers with the MCP server and becomes visible to clients during tool discovery.
Implement the Handler Function
The handler receives raw arguments, validates them against your Zod schema using .parse(), and returns a CallToolResult. The registerEchoTool function in src/everything/tools/echo.ts demonstrates this pattern:
export const registerEchoTool = (server: McpServer) => {
server.registerTool(name, config, async (args): Promise<CallToolResult> => {
const validated = EchoSchema.parse(args);
return {
content: [{ type: "text", text: `Echo: ${validated.message}` }],
};
});
};
The handler must return an object with a content array containing typed content blocks (text, images, or resources).
Register the Tool with the Server
Export a registration function that accepts an McpServer instance and calls server.registerTool(). Centralize all tool registrations in src/everything/tools/index.ts to maintain clean separation of concerns:
import { registerEchoTool } from "./echo.js";
export const registerTools = (server: McpServer) => {
// Existing tools...
registerEchoTool(server);
};
This centralization pattern allows the server bootstrap code to import a single registerTools function that wires up all available functionality.
Wire Registration into Server Startup
The server entry point in src/everything/server/index.ts initializes the McpServer and invokes your registration functions during the startup sequence:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerTools, registerConditionalTools } from "./tools/index.js";
export const start = async () => {
const server = new McpServer();
registerTools(server);
// Conditional tools are added after client capability negotiation
server.on("clientReady", () => registerConditionalTools(server));
await server.listen();
};
Tools registered via registerTools() become immediately available to clients, while conditional tools wait for the clientReady event.
Complete Working Example: Sum Calculator
Below is a minimal end-to-end implementation adding a get-sum tool that adds two numbers.
Create the Tool Implementation
Create src/everything/tools/get-sum.ts with the following content:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
export const SumSchema = z.object({
a: z.number().describe("First operand"),
b: z.number().describe("Second operand"),
});
const name = "get-sum";
const config = {
title: "Sum Numbers",
description: "Returns the sum of two numbers",
inputSchema: SumSchema,
};
export const registerGetSumTool = (server: McpServer) => {
server.registerTool(name, config, async (args): Promise<CallToolResult> => {
const { a, b } = SumSchema.parse(args);
return { content: [{ type: "text", text: `Result: ${a + b}` }] };
});
};
Register the New Tool
Update src/everything/tools/index.ts to include the new registration:
import { registerGetSumTool } from "./get-sum.js";
export const registerTools = (server: McpServer) => {
// ...other tools
registerGetSumTool(server);
};
Invoke from a Client
Once the server is running, clients call the tool by name with validated arguments:
{
"tool": "get-sum",
"arguments": { "a": 3, "b": 7 }
}
The server responds with:
{
"content": [{ "type": "text", "text": "Result: 10" }]
}
Summary
- Input Validation: Define Zod schemas in your tool file to enforce type safety and validate client inputs before execution.
- Metadata Configuration: Create a config object with
name,title,description, andinputSchemafor tool discovery and documentation. - Handler Implementation: Write async functions that parse validated arguments using
Schema.parse()and returnCallToolResultobjects containing content arrays. - Registration Pattern: Export
register*Toolfunctions that callserver.registerTool(), then import these intosrc/everything/tools/index.tsfor centralized management. - Server Integration: Wire tool registration into the server startup flow in
src/everything/server/index.tsto make tools available immediately or conditionally after client capability negotiation.
Frequently Asked Questions
What is the purpose of the Zod schema in MCP tool creation?
The Zod schema enforces runtime type validation for tool inputs. When you create custom tools in MCP servers using the TypeScript SDK, the schema automatically validates incoming arguments against defined types before your handler executes, ensuring that only properly formatted data reaches your business logic.
How does the server.registerTool() method work?
The server.registerTool() method, as implemented in the MCP SDK, accepts three parameters: a string name, a configuration object containing metadata and the input schema, and an async handler function. This method binds the tool to the McpServer instance and makes it discoverable and callable by MCP clients through the standardized protocol.
Can tools be registered conditionally based on client capabilities?
Yes. The modelcontextprotocol/servers repository demonstrates conditional registration in src/everything/server/index.ts using the clientReady event. Tools that require specific client capabilities should be registered inside the server.on("clientReady", () => registerConditionalTools(server)) callback rather than in the initial registerTools() call, as seen in the alternative registration pattern used in src/sequentialthinking/lib.ts.
Where should custom tool files be located in the project structure?
Custom tool implementations belong in the src/everything/tools/ directory, with each tool typically residing in its own file (e.g., get-sum.ts). Registration functions are then centralized in src/everything/tools/index.ts, following the pattern established by built-in tools like echo and demonstrated in the reference implementation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →