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 Python 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 from an async Python function that follows a specific pattern for its parameters and return value.
Every tool function follows this structure:
- Must be async: The function is declared with
async defrather than justdef, allowing it to work seamlessly with the SDK's asynchronous architecture - Takes a single parameter: The function accepts
args, a dictionary containing all the arguments Claude passes to your tool - Extracts parameters from args: Inside the function, you retrieve the specific values you need from the
argsdictionary using their parameter names as keys - Performs tool logic: The function executes whatever operations your tool needs to accomplish
- Returns a specific dictionary format: The return value must be a dictionary with a
"content"key - Content is a list: The value of
"content"is a list of content blocks, even if there's only one block - Each block has type and text: Every content block is a dictionary with a
"type"key (typically"text") and a"text"key containing the actual result string
Once you have a function with the correct structure, you transform it into an MCP tool by adding the @tool decorator. This decorator provides the metadata Claude needs to understand what your tool does and how to call it.
The @tool decorator sits directly above your function definition and takes three important parameters:
- name: Specifies the tool's identifier — this is the name Claude will see and use when calling your tool. Choose names that are clear and descriptive, using underscores to separate words.
- description: 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.
- input_schema: Defines what arguments your tool accepts. In this example,
{"param_name": str}tells Claude that the tool expects a parameter called"param_name"whose value should be a string. When Claude calls your tool, it will construct theargsdictionary based on this schema, ensuring thatargs["param_name"]contains a string.
The combination of a well-structured function and a properly configured @tool decorator creates a complete MCP tool that Claude can discover, understand, and use.
After defining one or more tools with the @tool decorator, you package them into an MCP server using create_sdk_mcp_server. This function bundles your tools together and creates a server object that you can connect to your agent.
The create_sdk_mcp_server function takes three parameters. The name parameter identifies your server internally — this is primarily for logging and debugging purposes. The version parameter allows you to track different versions of your server as you evolve its capabilities. The tools parameter is a list of the tool functions you've decorated with @tool. 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 mcp_servers parameter in ClaudeAgentOptions, 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 a dictionary 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 allowed_tools parameter with the mcp__<server_key>__<tool_name> format. The server key "my_server" comes from your mcp_servers dictionary, and the tool name comes from the name parameter in the @tool decorator. 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.
The example begins by defining a global PLAN list that will store the agent's plan throughout the conversation. This state persists across tool calls 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 input_schema specifies that it accepts a "steps" parameter containing a list of strings. Inside the function, we extract the steps from args, iterate through them, and append each one to the PLAN list as a dictionary 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" list 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 read_plan tool takes no parameters, indicated by the empty input_schema. It simply reads the current state of the PLAN list 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 accepts an "index" parameter of type int, converts it from 1-based indexing (which is more natural for Claude) to 0-based indexing (which Python uses), 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" key in the return dictionary 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 functions to create_sdk_mcp_server. The server name "planner" is used for internal identification, the version "1.0.0" helps track changes over time, and the tools list contains all the planning functions 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 parameters:
- mcp_servers: Connects the custom server using the key
"planning", which becomes part of the tool names inallowed_tools - allowed_tools: 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 where the files to organize are located - system_prompt: 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 creates a client session using the familiar async context manager pattern and sends a query that asks Claude to organize flat files into subdirectories. The starting directory structure that Claude will work with looks like this:
The query 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 table format 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 each file 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 uses Bash one final time to verify that all files are in their correct locations, then marks the verification step as done. The final summary shows the complete directory structure and provides a detailed breakdown of what was accomplished. The checkmarks next to each item indicate that Claude tracked every step through to completion. This comprehensive 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.
You've now learned the complete pattern for creating custom MCP tools that extend your agent's capabilities. The process follows a clear sequence: write an async function that takes an args dictionary and returns a dictionary with "content", decorate it with @tool to specify the name, description, and input schema, package your tools into a server using create_sdk_mcp_server, and connect that server through mcp_servers while whitelisting tools with the mcp__<server_key>__<tool_name> format. 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.
