Register tools in your MCP server using McpServer.registerTool() in TypeScript or the @mcp.tool() decorator in Python. Each tool needs a name, description, input schema with validation, and an async handler that returns structured content. Tools are the primary way AI clients interact with your server to perform actions.
Defining Tools in Your MCP Server
Tools are the most powerful primitive in the Model Context Protocol. They let AI clients call functions on your server to fetch data, run computations, interact with APIs, or modify state. Each tool has a name, a description that helps the AI decide when to use it, an input schema for validation, and an async handler that performs the work.
This tutorial walks through tool registration in both TypeScript and Python, covering parameter validation, return formats, and best practices for writing tools that AI models can use reliably.
Prerequisites
- Node.js 18+ or Python 3.10+ installed
- MCP TypeScript SDK installed (@modelcontextprotocol/sdk) or MCP Python SDK installed (mcp)
- Basic understanding of MCP architecture (hosts, clients, servers)
- A working MCP server skeleton with transport configured
Step-by-step guide
Import the SDK and create a server instance
Import the SDK and create a server instance
Start by importing McpServer from the MCP TypeScript SDK and creating a new server instance. The server name and version are sent to clients during the initialization handshake, so use a descriptive name. In Python, import FastMCP and instantiate it with a name.
1// TypeScript2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";3import { z } from "zod";45const server = new McpServer({6 name: "my-tools-server",7 version: "1.0.0",8});910// Python11from mcp.server.fastmcp import FastMCP1213mcp = FastMCP("my-tools-server")Expected result: A server instance ready to accept tool registrations.
Register a basic tool with input schema
Register a basic tool with input schema
Use server.registerTool() to define a tool. The first argument is the tool name (use kebab-case). The second argument is a configuration object with a description and inputSchema using Zod validators. The third argument is the async handler function that receives validated parameters and returns a result object. In Python, use the @mcp.tool() decorator with type hints and a docstring.
1// TypeScript2server.registerTool("get-weather", {3 description: "Get current weather for a city",4 inputSchema: {5 city: z.string().describe("City name, e.g. San Francisco"),6 units: z.enum(["celsius", "fahrenheit"]).default("celsius")7 .describe("Temperature units"),8 },9}, async ({ city, units }) => {10 const temp = await fetchWeather(city, units);11 return {12 content: [{ type: "text", text: `${city}: ${temp}° ${units}` }],13 };14});1516# Python17@mcp.tool()18async def get_weather(city: str, units: str = "celsius") -> str:19 """Get current weather for a city.2021 Args:22 city: City name, e.g. San Francisco23 units: Temperature units (celsius or fahrenheit)24 """25 temp = await fetch_weather(city, units)26 return f"{city}: {temp}° {units}"Expected result: The tool appears in the server's tool list and can be called by any MCP client.
Return structured content from tools
Return structured content from tools
MCP tool results use a structured content array, not plain strings. Each item in the content array has a type (text, image, or resource). For most tools, you return one text item. For richer responses, return multiple content items. Always set isError to true when the tool encounters a failure so the AI can handle it gracefully.
1// Successful result2return {3 content: [4 { type: "text", text: "Operation completed successfully" },5 { type: "text", text: JSON.stringify(data, null, 2) },6 ],7 isError: false,8};910// Error result11return {12 content: [13 { type: "text", text: "Error: City not found in weather database" },14 ],15 isError: true,16};Expected result: The AI client receives structured content and can distinguish success from failure.
Register multiple tools for a cohesive API
Register multiple tools for a cohesive API
Most MCP servers expose several related tools. Group them logically and use consistent naming conventions. Each tool should do one thing well. Prefix related tools with a common namespace if your server covers multiple domains.
1// TypeScript — multiple related tools2server.registerTool("db-query", {3 description: "Run a read-only SQL query against the database",4 inputSchema: {5 sql: z.string().describe("SELECT query to execute"),6 },7}, async ({ sql }) => {8 if (!sql.trim().toUpperCase().startsWith("SELECT")) {9 return { content: [{ type: "text", text: "Error: Only SELECT queries allowed" }], isError: true };10 }11 const rows = await pool.query(sql);12 return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };13});1415server.registerTool("db-list-tables", {16 description: "List all tables in the database",17 inputSchema: {},18}, async () => {19 const tables = await pool.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");20 return { content: [{ type: "text", text: JSON.stringify(tables, null, 2) }] };21});Expected result: Multiple tools registered and discoverable by connected MCP clients.
Connect transport and start the server
Connect transport and start the server
After registering all tools, connect a transport and start the server. For local development and CLI integrations, use StdioServerTransport. This reads JSON-RPC messages from stdin and writes responses to stdout. For teams building complex MCP server ecosystems, RapidDev can help design the tool architecture and transport layer to ensure reliability at scale.
1// TypeScript2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";34const transport = new StdioServerTransport();5await server.connect(transport);6console.error("Server running on stdio");78# Python9if __name__ == "__main__":10 mcp.run(transport="stdio")Expected result: The server starts, listens on stdio, and responds to tool/list and tool/call requests from any MCP client.
Complete working example
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";3import { z } from "zod";45const server = new McpServer({6 name: "weather-tools-server",7 version: "1.0.0",8});910server.registerTool("get-weather", {11 description: "Get current weather for a city",12 inputSchema: {13 city: z.string().describe("City name, e.g. San Francisco"),14 units: z.enum(["celsius", "fahrenheit"]).default("celsius")15 .describe("Temperature units"),16 },17}, async ({ city, units }) => {18 try {19 const response = await fetch(20 `https://api.weather.example/v1/current?city=${encodeURIComponent(city)}&units=${units}`21 );22 if (!response.ok) {23 return {24 content: [{ type: "text", text: `Error: Weather API returned ${response.status}` }],25 isError: true,26 };27 }28 const data = await response.json();29 return {30 content: [{ type: "text", text: `${city}: ${data.temperature}° ${units}, ${data.condition}` }],31 };32 } catch (error) {33 return {34 content: [{ type: "text", text: `Error: ${(error as Error).message}` }],35 isError: true,36 };37 }38});3940server.registerTool("get-forecast", {41 description: "Get 5-day weather forecast for a city",42 inputSchema: {43 city: z.string().describe("City name"),44 days: z.number().min(1).max(7).default(5).describe("Number of days"),45 },46}, async ({ city, days }) => {47 try {48 const response = await fetch(49 `https://api.weather.example/v1/forecast?city=${encodeURIComponent(city)}&days=${days}`50 );51 const data = await response.json();52 const formatted = data.forecast53 .map((d: { date: string; high: number; low: number; condition: string }) =>54 `${d.date}: ${d.high}°/${d.low}° — ${d.condition}`55 )56 .join("\n");57 return { content: [{ type: "text", text: formatted }] };58 } catch (error) {59 return {60 content: [{ type: "text", text: `Error: ${(error as Error).message}` }],61 isError: true,62 };63 }64});6566const transport = new StdioServerTransport();67await server.connect(transport);68console.error("Weather MCP server running on stdio");Common mistakes when defining tools in an MCP server
Why it's a problem: Logging to stdout instead of stderr
How to avoid: Use console.error() for all logging. stdout is reserved for the MCP JSON-RPC protocol. Any stray stdout output corrupts the message stream.
Why it's a problem: Throwing exceptions instead of returning isError
How to avoid: Wrap handler logic in try/catch and return { content: [...], isError: true } so the AI client can handle the error gracefully.
Why it's a problem: Writing vague tool descriptions
How to avoid: Be specific in both the tool description and parameter .describe() strings. The AI uses these to decide when to call your tool and what arguments to pass.
Why it's a problem: Skipping input validation beyond Zod schema
How to avoid: Zod handles type validation, but add business logic checks inside the handler (e.g., rejecting non-SELECT SQL queries) for safety.
Why it's a problem: Registering tools after connecting transport
How to avoid: Register all tools before calling server.connect(). Tools registered after connection are not advertised to the client.
Best practices
- Use kebab-case for tool names (get-weather, not getWeather) for consistency across the MCP ecosystem
- Write tool descriptions as if explaining to a person what the tool does and when to use it
- Add .describe() to every Zod parameter so the AI knows what values to pass
- Return isError: true for all failure cases instead of throwing exceptions
- Keep each tool focused on a single action — split complex workflows into multiple tools
- Validate and sanitize all inputs inside the handler, even with Zod schemas in place
- Log tool invocations to stderr for debugging without disrupting the protocol
- Test each tool individually with the MCP Inspector before connecting to an AI client
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building an MCP server in TypeScript. Show me how to register a tool with McpServer.registerTool() that takes validated parameters using Zod and returns structured content. Include error handling with isError: true.
Add a new MCP tool called [tool-name] to my server. Use server.registerTool() with a Zod input schema that validates [describe parameters]. The handler should [describe action] and return { content: [{ type: "text", text: result }] }. Handle errors by returning isError: true.
Frequently asked questions
How many tools can I register on a single MCP server?
There is no hard limit in the protocol, but aim for 10-20 focused tools per server. If you need more, split into multiple specialized servers. AI models may struggle to choose the right tool when presented with too many options.
Can I register tools dynamically at runtime?
Yes, you can call registerTool() at any time and send a notifications/tools/list_changed notification to inform connected clients. However, most clients expect tools to be stable after the initial handshake.
What happens if two tools have the same name?
The second registration overwrites the first. Tool names must be unique within a server. Use namespaced prefixes like db-query and api-query if you have similar tools for different domains.
Do I need to use Zod for input schemas in TypeScript?
The McpServer high-level API uses Zod for inputSchema definitions. If you use the lower-level Server class directly, you can provide raw JSON Schema objects instead, but Zod is recommended for type safety and validation.
Can MCP tools return images or files?
Yes. Use content items with type: 'image' and a base64-encoded data field, or type: 'resource' with an embedded resource. Text is the most common return type, but the protocol supports rich content.
How do I make a tool available only to certain clients?
MCP does not have built-in per-tool access control. Implement authorization logic inside the tool handler based on request metadata, or run separate server instances for different access levels. For complex access control architectures, the RapidDev team can help design a multi-server setup.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation