Tools
MCP tools are functions that an MCP server exposes for clients to call. When an LLM decides it needs to take an action — look up data, run a calculation, call an API — it invokes a tool. The MCP server executes the tool and returns the result.
Tools are defined using the @modelcontextprotocol/sdk package. The Agents SDK handles transport and lifecycle; the tool definitions are the same regardless of whether you use createMcpHandler or McpAgent.
Use server.tool() to register a tool on an McpServer instance. Each tool has a name, a description (used by the LLM to decide when to call it), an input schema defined with Zod ↗, and a handler function.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
function createServer() { const server = new McpServer({ name: "Math", version: "1.0.0" });
server.tool( "add", "Add two numbers together", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }), );
return server;}import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
function createServer() { const server = new McpServer({ name: "Math", version: "1.0.0" });
server.tool( "add", "Add two numbers together", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }), );
return server;}The tool handler receives the validated input and must return an object with a content array. Each content item has a type (typically "text") and the corresponding data.
Tool results are returned as an array of content parts. The most common type is text, but you can also return images and embedded resources.
server.tool( "lookup", "Look up a user by ID", { userId: z.string() }, async ({ userId }) => { const user = await db.getUser(userId);
if (!user) { return { isError: true, content: [{ type: "text", text: `User ${userId} not found` }], }; }
return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }], }; },);server.tool( "lookup", "Look up a user by ID", { userId: z.string() }, async ({ userId }) => { const user = await db.getUser(userId);
if (!user) { return { isError: true, content: [{ type: "text", text: `User ${userId} not found` }], }; }
return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }], }; },);Set isError: true to signal that the tool call failed. The LLM receives the error message and can decide how to proceed.
The description parameter is critical — it is what the LLM reads to decide whether and when to call your tool. Write descriptions that are:
- Specific about what the tool does: "Get the current weather for a city" is better than "Weather tool"
- Clear about inputs: "Requires a city name as a string" helps the LLM format the call correctly
- Honest about limitations: "Only supports US cities" prevents the LLM from calling it with unsupported inputs
Tool inputs are defined as Zod schemas and validated automatically before the handler runs. Use Zod's .describe() method to give the LLM context about each parameter.
server.tool( "search", "Search for documents by query", { query: z.string().describe("The search query"), limit: z .number() .min(1) .max(100) .default(10) .describe("Maximum number of results to return"), category: z .enum(["docs", "blog", "api"]) .optional() .describe("Filter by content category"), }, async ({ query, limit, category }) => { const results = await searchIndex(query, { limit, category }); return { content: [{ type: "text", text: JSON.stringify(results) }], }; },);server.tool( "search", "Search for documents by query", { query: z.string().describe("The search query"), limit: z .number() .min(1) .max(100) .default(10) .describe("Maximum number of results to return"), category: z .enum(["docs", "blog", "api"]) .optional() .describe("Filter by content category"), }, async ({ query, limit, category }) => { const results = await searchIndex(query, { limit, category }); return { content: [{ type: "text", text: JSON.stringify(results) }], }; },);For stateless MCP servers, define tools inside a factory function and pass the server to createMcpHandler:
import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
function createServer() { const server = new McpServer({ name: "My Tools", version: "1.0.0" });
server.tool("ping", "Check if the server is alive", {}, async () => ({ content: [{ type: "text", text: "pong" }], }));
return server;}
export default { fetch: (request, env, ctx) => { const server = createServer(); return createMcpHandler(server)(request, env, ctx); },};import { createMcpHandler } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
function createServer() { const server = new McpServer({ name: "My Tools", version: "1.0.0" });
server.tool("ping", "Check if the server is alive", {}, async () => ({ content: [{ type: "text", text: "pong" }], }));
return server;}
export default { fetch: (request: Request, env: Env, ctx: ExecutionContext) => { const server = createServer(); return createMcpHandler(server)(request, env, ctx); },} satisfies ExportedHandler<Env>;For stateful MCP servers, define tools in the init() method of an McpAgent. Tools have access to the agent instance via this, which means they can read and write state.
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "Stateful Tools", version: "1.0.0" });
async init() { this.server.tool( "incrementCounter", "Increment and return a counter", {}, async () => { const count = (this.state?.count ?? 0) + 1; this.setState({ count }); return { content: [{ type: "text", text: `Counter: ${count}` }], }; }, ); }}import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "Stateful Tools", version: "1.0.0" });
async init() { this.server.tool( "incrementCounter", "Increment and return a counter", {}, async () => { const count = (this.state?.count ?? 0) + 1; this.setState({ count }); return { content: [{ type: "text", text: `Counter: ${count}` }], }; }, ); }}