mirror of
https://github.com/aljazceru/Tutorial-Codebase-Knowledge.git
synced 2025-12-19 07:24:20 +01:00
init push
This commit is contained in:
224
docs/MCP Python SDK/04_fastmcp_tools___tool____toolmanager__.md
Normal file
224
docs/MCP Python SDK/04_fastmcp_tools___tool____toolmanager__.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Chapter 4: FastMCP Tools (`Tool`, `ToolManager`)
|
||||
|
||||
In [Chapter 3: Sharing Data - FastMCP Resources (`Resource`, `ResourceManager`)](03_fastmcp_resources___resource____resourcemanager__.md), 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`**
|
||||
|
||||
```python
|
||||
# 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`):**
|
||||
|
||||
```mermaid
|
||||
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, ...)`.
|
||||
|
||||
```python
|
||||
# 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, ...)`.
|
||||
|
||||
```python
|
||||
# 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`)](05_fastmcp_prompts___prompt____promptmanager__.md).
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
Reference in New Issue
Block a user