In the previous lesson, you learned how to extend your agent's capabilities by connecting external MCP servers. These pre-built servers provide powerful functionality, but sometimes you need tools that are specific to your application's needs — tools that don't exist in any external package. This is where creating your own tools becomes essential.
In this lesson, you'll learn how to transform ordinary TypeScript functions into MCP-compatible tools that Claude can use just like any built-in or external tool. By the end of this lesson, you'll understand the complete pattern for creating custom MCP tools and have a reusable approach for building tools that match your specific application needs.
Before you can create a tool, you need to understand the structure that the SDK expects. Every tool is built using the tool() function, which takes four parameters: a name, a description, a Zod schema for validation, and an async callback function.
Every tool follows this structure:
- Name parameter: A string identifier that Claude will use when calling your tool. Choose names that are clear and descriptive, using underscores to separate words.
- Description parameter: Provides Claude with information about what the tool does and when to use it. This description is crucial because Claude reads it to decide whether your tool is appropriate for the current task, so make it specific and actionable.
- Schema parameter: A Zod schema object that defines what arguments your tool accepts. In this example,
{ param_name: z.string() }tells Claude that the tool expects a parameter calledparam_namewhose value should be a string. - Callback function: An async function that receives the validated arguments and performs the tool's logic. The
argsparameter is already validated and typed according to your schema, so you can destructure it directly. - Return format: The callback must return an object with a
contentkey containing an array of content blocks, even if there's only one block. Each block has atype(typically ) and a key containing the actual result string.
The tool() function from the SDK provides everything Claude needs to understand what your tool does and how to call it. Unlike decorator-based approaches, TypeScript uses a functional pattern where you call tool() with all the necessary metadata and implementation.
The tool() function takes four parameters in order:
- name: Specifies the tool's identifier — this is the name Claude will see and use when calling your tool.
- description: Provides Claude with information about what the tool does and when to use it.
- schema: A Zod schema object that defines the tool's parameters. Zod provides type-safe validation with schemas like
z.string(),z.number(),z.array(), andz.object(). When Claude calls your tool, the SDK validates the arguments against this schema before passing them to your callback. - callback: An async function that receives the validated arguments and implements the tool's logic.
The combination of these four parameters creates a complete MCP tool that Claude can discover, understand, and use. The Zod schema ensures type safety at runtime, catching invalid arguments before they reach your implementation.
After defining one or more tools with the tool() function, you package them into an MCP server using createSdkMcpServer. This function bundles your tools together and creates a server object that you can connect to your agent.
The createSdkMcpServer function takes an object with three properties. The name property identifies your server internally — this is primarily for logging and debugging purposes. The version property allows you to track different versions of your server as you evolve its capabilities. The tools property is an array of the tool objects you've created with the tool() function. The function returns a server object that you can then connect to your agent, which we'll explore in the next section.
With your server created, you connect it to your agent through the mcpServers parameter in the Options interface, just as you did with external servers in the previous lesson.
The pattern here is identical to what you learned when connecting external MCP servers. You provide an object where the key (in this case "my_server") becomes the server key used in tool names, and the value is the server object you created. Just as with external servers, you must whitelist your custom tools using the allowedTools parameter with the mcp__<server_key>__<tool_name> format. The server key "my_server" comes from your mcpServers object, and the tool name comes from the first parameter in the tool() function. This consistent pattern means that once you understand how to connect external MCP servers, you already know how to connect your own custom servers.
To demonstrate custom tool creation, we'll build a complete planning system consisting of three tools that work together to help your agent approach complex tasks methodically:
- add_to_plan: A tool that lets Claude break down complex tasks into multiple steps, organizing its thoughts before taking action.
- read_plan: A tool that lets Claude view its current plan and see which steps are complete and which are still pending.
- mark_step_done: A tool that lets Claude mark individual steps as complete after verifying they've been executed successfully.
These tools work together to transform Claude from an agent that simply executes commands into an agent that plans, executes, and tracks its progress systematically. We'll see this system in action as we build an agent that organizes flat files into a proper directory structure — a task complex enough to benefit from thoughtful planning but simple enough to understand how the planning tools enable more structured behavior.
We'll start by looking at the state management and imports.
The example begins by importing the necessary functions from the SDK, including tool for creating tools and createSdkMcpServer for packaging them. We also import Zod for schema validation and Node.js path utilities for working with file paths. The PLAN constant is defined with a TypeScript type annotation that specifies it's an array of objects, each containing a step string and a status that can only be "pending" or "done". This state persists throughout the conversation because it's defined at the module level. All three planning tools will read from and write to this shared state, allowing them to work together as a cohesive system. With the state defined, we can now create the first tool that adds steps to the plan.
The first tool allows Claude to add multiple steps to the plan at once, organizing its thoughts before taking action.
The schema uses z.array(z.string()) to specify that the tool accepts a steps parameter containing an array of strings. Inside the callback function, we destructure the steps from the validated args, iterate through them, and push each one to the PLAN array as an object with the step text and a "pending" status. The function returns a confirmation message telling Claude how many steps were added. This return format follows the required structure with a content array containing a text block. Now let's look at the tool that allows Claude to read its current plan.
The second tool allows Claude to view its current plan and see which steps are complete and which are still pending.
The readPlanTool takes no parameters, indicated by the empty schema object {}. The callback function doesn't need to destructure any args since there are none. It simply reads the current state of the PLAN array and formats it as a numbered list with checkmark icons showing which steps are complete. If the plan is empty, it returns a simple message saying so. Otherwise, it builds a formatted string that shows each step with its status, making it easy for Claude to see what work remains. Next, we'll implement the tool that marks steps as complete.
The third tool allows Claude to mark individual steps as complete after verifying they've been executed successfully.
The tool uses z.number() in its schema to specify that it accepts an index parameter of type number. The callback destructures the index, converts it from 1-based indexing (which is more natural for Claude) to 0-based indexing (which TypeScript arrays use), and updates the status of that step to "done". Notice that this tool includes error handling — if the index is invalid, it returns a result with isError: true, which tells Claude that something went wrong. This optional isError property in the return object allows your tools to communicate failures back to the agent in a structured way. With all three tools defined, we can now create the server and configure the agent.
With all three tools defined, the next step is to package them into an MCP server that can be connected to your agent.
The code creates the planner server by passing all three tool objects to createSdkMcpServer. The server name "planner" is used for internal identification, the version "1.0.0" helps track changes over time, and the tools array contains all the planning tools we just defined. This server object is now ready to be connected to an agent through the configuration options.
Now we configure the agent to use the planning tools alongside built-in tools, and we provide a system prompt that enforces methodical planning behavior.
The configuration connects the custom server and establishes the agent's behavior through several key properties:
- mcpServers: Connects the custom server using the key
"planning", which becomes part of the tool names inallowedTools. - allowedTools: Includes all three planning tools using the
mcp__planning__<tool_name>format, plus several built-in tools likeBash,Write, andReadthat the agent will need to actually perform the file organization task. - cwd: Sets the working directory to a
projectsubdirectory usingresolve(__dirname, "project")to construct the absolute path. - systemPrompt: Explicitly instructs Claude to use the planning tools before taking action — this prompt is crucial because it establishes the behavior pattern you want. Without it, Claude might skip the planning phase and go straight to executing commands.
Finally, we run the agent with a task that benefits from the planning system — organizing flat files into a proper directory structure.
The code calls query() with a prompt that asks Claude to organize flat files into subdirectories and the options we configured. The query() function returns an async iterator that yields messages as Claude works through the task. We pass this iterator to displayResponse() to show the agent's progress. The starting directory structure that Claude will work with looks like this:
The prompt explicitly mentions creating a plan, which reinforces the system prompt's instructions. The task is complex enough that it benefits from planning — Claude needs to examine multiple files, decide on an appropriate directory structure, create directories, and move files. By combining the planning tools with built-in tools like Bash and Read, Claude can approach this task methodically rather than making ad-hoc decisions. Let's now examine the output to see how Claude uses these custom planning tools in practice.
When you run the complete example, Claude begins by analyzing the current state of the project directory before creating any plan.
Claude starts by explaining its approach and then uses the built-in Bash tool to explore the directory structure. It follows this with multiple calls to the Read tool to examine the contents of each file. This demonstrates that Claude is gathering information before making any decisions about how to organize the files. The agent is being methodical, which is exactly what the system prompt instructed it to do. Notice that Claude hasn't used any of the custom planning tools yet — it's still in the information-gathering phase.
After analyzing the files, Claude uses the custom planning tool to create a structured plan.
Claude explicitly states that it's creating a plan and then calls mcp__planning__add_to_plan. After adding the steps to the plan, Claude presents a detailed analysis that shows it understands the current state and has a clear vision for the proposed organization. The structured presentation makes it easy to see which files will go into which directories and why. This structured presentation demonstrates that Claude is thinking through the problem systematically before taking action. The custom planning tool has enabled Claude to externalize its reasoning process, making it visible and trackable.
With the plan in place, Claude begins executing the steps and marking them complete as it goes.
Claude creates the directories using Bash and immediately marks that step as done using mcp__planning__mark_step_done. It then moves the files and marks each corresponding step complete. This pattern shows that Claude is following the plan it created and tracking its progress as instructed by the system prompt. The custom planning tools have transformed Claude from an agent that simply executes commands into an agent that plans, executes, and verifies its work in a structured way. Each call to mark_step_done represents Claude acknowledging that it has completed a specific part of the overall task.
After completing all the steps, Claude verifies the reorganization and provides a comprehensive summary.
Claude provides a comprehensive summary showing the complete directory structure transformation and a detailed breakdown of what was accomplished. The final summary demonstrates the value of the planning system — Claude didn't just complete the task, it completed it methodically, tracked its progress, verified the results, and provided clear documentation of what was done. The structured approach enabled by the custom planning tools resulted in a professional, well-organized outcome with full transparency into the process.
You've now learned the complete pattern for creating custom MCP tools that extend your agent's capabilities. The process follows a clear sequence: use the tool() function with a name, description, Zod schema, and async callback that returns an object with content, package your tools into a server using createSdkMcpServer, and connect that server through mcpServers while whitelisting tools with the mcp__<server_key>__<tool_name> format. The Zod schemas provide type-safe validation, ensuring that arguments are validated before reaching your implementation. In the upcoming practice exercises, you'll apply these concepts by creating your own custom tools for different use cases and building agents that combine custom tools with built-in and external tools to solve complex problems.
