Skip to main content
RapidDev - Software Development Agency
mcp-tutorial

How to define tools in an MCP server

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.

What you'll learn

  • How to register tools using McpServer.registerTool() in TypeScript
  • How to define tools with the @mcp.tool() decorator in Python
  • How to define input schemas with Zod for parameter validation
  • How to return structured tool results and handle errors
  • How to test registered tools with the MCP Inspector
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read20-30 minMCP TypeScript SDK 1.x, MCP Python SDK 1.xMarch 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1// TypeScript
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3import { z } from "zod";
4
5const server = new McpServer({
6 name: "my-tools-server",
7 version: "1.0.0",
8});
9
10// Python
11from mcp.server.fastmcp import FastMCP
12
13mcp = FastMCP("my-tools-server")

Expected result: A server instance ready to accept tool registrations.

2

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.

typescript
1// TypeScript
2server.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});
15
16# Python
17@mcp.tool()
18async def get_weather(city: str, units: str = "celsius") -> str:
19 """Get current weather for a city.
20
21 Args:
22 city: City name, e.g. San Francisco
23 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.

3

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.

typescript
1// Successful result
2return {
3 content: [
4 { type: "text", text: "Operation completed successfully" },
5 { type: "text", text: JSON.stringify(data, null, 2) },
6 ],
7 isError: false,
8};
9
10// Error result
11return {
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.

4

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.

typescript
1// TypeScript — multiple related tools
2server.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});
14
15server.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.

5

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.

typescript
1// TypeScript
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4const transport = new StdioServerTransport();
5await server.connect(transport);
6console.error("Server running on stdio");
7
8# Python
9if __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

src/index.ts
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3import { z } from "zod";
4
5const server = new McpServer({
6 name: "weather-tools-server",
7 version: "1.0.0",
8});
9
10server.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});
39
40server.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.forecast
53 .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});
65
66const 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.

ChatGPT Prompt

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.

MCP Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.