Chapter 4: FastMCP Tools (Tool, ToolManager)

In Chapter 3: Sharing Data - FastMCP Resources (Resource, ResourceManager), we learned how to make data available for clients to read using Resource objects, like putting books in a digital library. That’s great for sharing information, but what if we want the client to be able to ask the server to do something?

Imagine you want your server to not just provide data, but to perform calculations, interact with a database, or control some hardware. For example, maybe you want a client application (like an AI assistant) to be able to ask your server, “What’s 5 plus 7?”. The server needs to perform the addition and send back the result.

This is where FastMCP Tools come in. They allow your server to expose functions that clients can call remotely.

The Workshop Analogy: Tools and the Foreman

Think of your FastMCP server as a well-equipped workshop. Inside this workshop, you have various specialized tools:

  • A drill (Tool)
  • A screwdriver (Tool)
  • A calculator (Tool)

Each Tool is designed for a specific job. When someone (a client) needs a job done, they don’t operate the tool directly. Instead, they go to the workshop foreman (the ToolManager) and say:

“I need to use the calculator tool. Please add these numbers: 5 and 7.”

The foreman (ToolManager) knows exactly where the calculator tool is and how it works. It takes the request, operates the calculator with the provided numbers (5, 7), gets the result (12), and gives it back to the person who asked.

  • Tool: A specific function or capability your server offers (like the calculator). It has a name and accepts specific inputs (arguments).
  • ToolManager: The internal manager within FastMCP that keeps track of all available Tool objects and handles requests to use them (the foreman). Clients interact with the ToolManager via FastMCP.

Clients can ask the ToolManager (via FastMCP) to list all available tools (listTools) and then request to execute a specific tool by its name, providing the necessary arguments (callTool).

Adding Tools to Your Workshop: Using @server.tool()

Just like we used @server.resource() to add data “books” to our library, FastMCP provides the @server.tool() decorator to easily add action “tools” to our workshop (managed by the ToolManager).

Let’s create a simple server with a calculator tool that can add two numbers.

File: calculator_server.py

# 1. Import FastMCP
from mcp.server.fastmcp import FastMCP

# 2. Create the server instance
server = FastMCP(name="CalculatorServer")

# 3. Use the @server.tool() decorator to define our tool
@server.tool(name="add", description="Adds two numbers together.")
def add_numbers(num1: int, num2: int) -> int:
  """
  This function is registered as the 'add' tool.
  'num1: int' and 'num2: int' tell FastMCP the tool expects
  two integer arguments named 'num1' and 'num2'.
  '-> int' tells FastMCP the tool will return an integer.
  """
  print(f"Tool 'add' called with {num1} and {num2}") # Server-side log
  # 4. The function's logic performs the action
  result = num1 + num2
  print(f"Returning result: {result}")
  return result

# 5. Standard run block
if __name__ == "__main__":
    print(f"Starting {server.name}...")
    server.run() # Start listening
    print(f"{server.name} finished.")

Explanation:

  1. server = FastMCP(...): Creates our server (the workshop). Internally, this also creates a ToolManager (the foreman).
  2. @server.tool(...): This is our decorator “button” for adding tools.
    • We use the .tool() method of our server object as a decorator.
    • name="add": We tell FastMCP that clients should use the name add to call this tool.
    • description="...": A helpful description for clients.
  3. def add_numbers(num1: int, num2: int) -> int:: This is a standard Python function.
    • num1: int, num2: int: These type hints are crucial! They tell FastMCP what arguments the tool expects (two integers named num1 and num2). FastMCP uses this to validate input from clients and to generate documentation about the tool.
    • -> int: This type hint indicates that the function will return an integer result.
  4. Function Body: This contains the actual logic for our tool – adding the numbers.
  5. server.run(): Starts the server. The ToolManager now knows about the add tool.

If you run this server (mcp run calculator_server.py), a client could:

  1. Call listTools and see the add tool listed, along with its description and expected arguments (num1 (int), num2 (int)).
  2. Call callTool with the name add and arguments like {"num1": 5, "num2": 7}.
  3. FastMCP would ask the ToolManager to execute the add tool. The ToolManager would find your add_numbers function, check that the arguments match (5 and 7 are integers), call the function, get the integer result 12, and send it back to the client.

How Clients Use Tools

You don’t need to worry about writing client code right now, but it’s helpful to understand the basic interaction:

  1. Discovery: The client first asks the server, “What tools do you have?” (using the MCP listTools method). The server, guided by its ToolManager, responds with a list of tools, including their names, descriptions, and what arguments they expect (based on your Python function signature and the @server.tool decorator).
  2. Invocation: The client then decides to use a specific tool. It sends a request like, “Please execute the tool named ‘add’ with these arguments: num1 is 5, num2 is 7.” (using the MCP callTool method).
  3. Execution & Response: The server receives this request. FastMCP hands it off to the ToolManager. The ToolManager finds the correct Python function (add_numbers), validates and passes the arguments (5, 7), executes the function, gets the return value (12), and sends this result back to the client.

The Foreman: ToolManager Behind the Scenes

While you primarily interact with @server.tool(), the ToolManager is the component within FastMCP that does the heavy lifting for tools.

When FastMCP starts, it creates a ToolManager instance. Every time you use the @server.tool() decorator, you’re essentially telling FastMCP to register that function with its ToolManager.

The ToolManager:

  • Keeps a dictionary mapping tool names (like "add") to the corresponding Tool objects (which contain information about your function, its parameters, etc.).
  • Provides the list of tools when FastMCP needs to respond to a listTools request.
  • Looks up the correct Tool object when FastMCP receives a callTool request.
  • Validates the arguments provided by the client against the tool’s expected parameters (using the information gathered from type hints).
  • Calls your actual Python function with the validated arguments.
  • Handles potential errors during tool execution.

You usually don’t need to interact with ToolManager directly; @server.tool() is the convenient interface.

How Tools Work Under the Hood

Let’s trace the journey of our add tool from definition to execution.

1. Registration (When the server code loads):

  • Python executes your calculator_server.py.
  • It reaches the @server.tool(name="add", ...) line above def add_numbers(...).
  • This calls the server.tool() method. Inside FastMCP, this ultimately calls _tool_manager.add_tool().
  • The ToolManager.add_tool method inspects the add_numbers function:
    • Gets its name (add_numbers, but overridden by name="add").
    • Gets its description (from the decorator or docstring).
    • Looks at the parameters (num1: int, num2: int) and return type (-> int) using Python’s introspection features.
    • Uses this information to build a schema describing the expected input arguments (like a mini-form definition).
    • Creates an internal Tool object containing all this information (the function itself, its name, description, argument schema).
  • The ToolManager stores this Tool object in its internal dictionary, keyed by the name "add".

2. Invocation (When a client calls the tool):

  • A client sends an MCP message: {"method": "callTool", "params": {"name": "add", "arguments": {"num1": 5, "num2": 7}}}.
  • FastMCP receives this message and identifies it as a callTool request for the tool named add.
  • FastMCP calls its internal call_tool handler method.
  • This handler asks the ToolManager: “Please execute the tool named add with arguments {'num1': 5, 'num2': 7}.” (calling _tool_manager.call_tool).
  • The ToolManager looks up "add" in its dictionary and finds the corresponding Tool object.
  • The Tool object (or the ToolManager using it) validates the provided arguments ({'num1': 5, 'num2': 7}) against the stored argument schema (checks if num1 and num2 are present and are integers).
  • If validation passes, the Tool object calls the original Python function (add_numbers) with the arguments unpacked: add_numbers(num1=5, num2=7).
  • Your add_numbers function runs, calculates 12, and returns it.
  • The ToolManager receives the result 12.
  • FastMCP takes the result, packages it into a valid MCP callTool response message, and sends it back to the client.

Simplified Sequence Diagram (callTool for add):

sequenceDiagram
    participant Client
    participant FastMCP_Server as FastMCP (calculator_server.py)
    participant ToolMgr as ToolManager (_tool_manager)
    participant AddTool as Tool (wraps add_numbers)
    participant AddFunc as add_numbers()

    Client->>+FastMCP_Server: Send MCP Request: callTool(name="add", args={"num1": 5, "num2": 7})
    FastMCP_Server->>+ToolMgr: call_tool(name="add", args={...})
    ToolMgr->>ToolMgr: Find Tool object for "add"
    ToolMgr->>+AddTool: tool.run(arguments={...})
    AddTool->>AddTool: Validate args against schema
    AddTool->>+AddFunc: Call add_numbers(num1=5, num2=7)
    AddFunc-->>-AddTool: Return 12
    AddTool-->>-ToolMgr: Return 12
    ToolMgr-->>-FastMCP_Server: Return 12
    FastMCP_Server->>-Client: Send MCP Response: result=12

Looking at the Code (Briefly):

You don’t need to memorize this, but seeing the structure can help.

  • Registration (@server.tool -> add_tool -> ToolManager.add_tool):
    • In server/fastmcp/server.py, the FastMCP.tool decorator returns an inner function that calls self.add_tool(fn, ...).
    • FastMCP.add_tool simply calls self._tool_manager.add_tool(fn, ...).
    # Inside server/fastmcp/tools/tool_manager.py (Simplified ToolManager.add_tool)
    from .base import Tool # Tool class definition is in base.py
    
    class ToolManager:
        # ... (init, get_tool, list_tools) ...
    
        def add_tool(self, fn, name=None, description=None) -> Tool:
            # 1. Create a Tool object from the function
            tool = Tool.from_function(fn, name=name, description=description)
            # 2. Check for duplicates (optional warning)
            if tool.name in self._tools:
                # ... handle duplicate ...
                pass
            # 3. Store the Tool object in the dictionary
            self._tools[tool.name] = tool
            logger.debug(f"Registered tool: {tool.name}")
            return tool
    
  • Invocation (FastMCP.call_tool -> ToolManager.call_tool -> Tool.run):
    • In server/fastmcp/server.py, the FastMCP.call_tool method (which handles incoming callTool requests) calls self._tool_manager.call_tool(name, arguments, ...).
    # Inside server/fastmcp/tools/tool_manager.py (Simplified ToolManager.call_tool)
    class ToolManager:
        # ... (init, add_tool, list_tools) ...
    
        async def call_tool(self, name, arguments, context=None):
            # 1. Find the tool by name
            tool = self.get_tool(name)
            if not tool:
                raise ToolError(f"Unknown tool: {name}")
    
            # 2. Tell the Tool object to run with the arguments
            logger.debug(f"Calling tool: {name} with args: {arguments}")
            result = await tool.run(arguments, context=context)
            return result
    
    • The Tool.run method (in server/fastmcp/tools/base.py) handles argument validation (using the FuncMetadata generated during registration) and finally calls your original Python function (add_numbers).

Conclusion

You’ve now learned about FastMCP Tools, the way to expose actions and computations from your server for clients to execute.

  • Tools (Tool) are server-side functions callable by clients, identified by a name.
  • The ToolManager is the internal component that registers and dispatches tool calls (like a workshop foreman).
  • The @server.tool() decorator is the easy way to register a Python function as a tool.
  • Type hints in your function signature are essential for defining the tool’s arguments and return type, enabling automatic validation and documentation.
  • Clients use listTools to discover tools and callTool to execute them.

Tools are fundamental for building interactive applications where the client needs the server to perform specific tasks beyond just retrieving data.

In the next chapter, we’ll explore another powerful feature of FastMCP for interacting with Large Language Models: Chapter 5: FastMCP Prompts (Prompt, PromptManager).


Generated by AI Codebase Knowledge Builder