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:
252
docs/MCP Python SDK/01_cli___mcp__command_.md
Normal file
252
docs/MCP Python SDK/01_cli___mcp__command_.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Chapter 1: Your Control Panel - The `mcp` Command-Line Interface
|
||||
|
||||
Welcome to the MCP Python SDK! This is your starting point for building powerful, interactive AI tools.
|
||||
|
||||
Imagine you've just built an amazing new tool using the SDK – maybe a helpful assistant that can answer questions about your documents. How do you actually *run* this tool? How do you test it? How do you connect it to applications like Claude Desktop?
|
||||
|
||||
This is where the `mcp` command-line interface (CLI) comes in. Think of it as your **developer control panel** or **toolkit** for managing your MCP creations right from your terminal (that black window where you type commands). It helps you run, test, and integrate your MCP servers.
|
||||
|
||||
In this chapter, we'll explore the basic commands you'll use most often. Our main goal is to learn how to take a simple server written in a Python file and get it running.
|
||||
|
||||
## What is the `mcp` Command?
|
||||
|
||||
The `mcp` command is a tool you run in your terminal. After installing the `MCP Python SDK` (specifically with the `cli` extras, like `pip install mcp[cli]`), you gain access to this command. It provides several sub-commands to help you manage your MCP development workflow.
|
||||
|
||||
Let's look at the most important ones.
|
||||
|
||||
### Checking Your Setup: `mcp version`
|
||||
|
||||
First things first, let's make sure everything is installed correctly. You can check the installed version of the MCP SDK using this command:
|
||||
|
||||
```bash
|
||||
mcp version
|
||||
```
|
||||
|
||||
**What happens?**
|
||||
|
||||
This command looks up the installed `mcp` package and prints its version number.
|
||||
|
||||
**Example Output:**
|
||||
|
||||
```
|
||||
MCP version 0.1.0
|
||||
```
|
||||
|
||||
If you see a version number, you're good to go! If you get an error, double-check that you've installed the SDK correctly (`pip install mcp[cli]`).
|
||||
|
||||
### Running Your Server: `mcp run`
|
||||
|
||||
This is the command you'll use to execute your MCP server directly. Let's say you have a Python file named `my_first_server.py` that contains your server code.
|
||||
|
||||
**Minimal Server Example (`my_first_server.py`):**
|
||||
|
||||
```python
|
||||
# We'll learn about FastMCP in the next chapter!
|
||||
# For now, just know this creates a basic server.
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Create an instance of our server
|
||||
server = FastMCP(name="MyFirstServer")
|
||||
|
||||
# This is a standard Python check to make sure
|
||||
# the script is being run directly
|
||||
if __name__ == "__main__":
|
||||
# Tell the server to start running
|
||||
print("Starting MyFirstServer...")
|
||||
server.run()
|
||||
print("MyFirstServer finished.") # You might not see this if the server runs forever
|
||||
```
|
||||
|
||||
To run this server, you would open your terminal, navigate to the directory containing `my_first_server.py`, and type:
|
||||
|
||||
```bash
|
||||
mcp run my_first_server.py
|
||||
```
|
||||
|
||||
**What happens?**
|
||||
|
||||
The `mcp run` command will:
|
||||
1. Find your `my_first_server.py` file.
|
||||
2. Look inside for a server object (it tries common names like `mcp`, `server`, or `app` by default, or you can specify one like `my_first_server.py:server`).
|
||||
3. Tell that server object to start running (by calling its `.run()` method).
|
||||
|
||||
Your terminal will likely show output like "Starting MyFirstServer..." and then wait for connections or instructions, depending on how the server is configured. To stop it, you usually press `Ctrl+C`.
|
||||
|
||||
### Developing and Inspecting: `mcp dev`
|
||||
|
||||
When you're building your server, you often want to see what's happening inside – what messages are being sent and received? The `mcp dev` command is perfect for this. It runs your server *and* launches the **MCP Inspector**, a web-based tool that lets you monitor and debug your server in real-time.
|
||||
|
||||
```bash
|
||||
mcp dev my_first_server.py
|
||||
```
|
||||
|
||||
**What happens?**
|
||||
|
||||
1. Similar to `mcp run`, it finds and prepares to run your server (`my_first_server.py`).
|
||||
2. It ensures any necessary helper tools (like the Inspector itself, using `npx`) are available.
|
||||
3. It starts your server.
|
||||
4. It launches the MCP Inspector, which connects to your running server. You'll usually see a URL in your terminal that you can open in your web browser, or sometimes the Inspector might open automatically.
|
||||
|
||||
This is incredibly useful during development for understanding the flow of information.
|
||||
|
||||
*(Note: `mcp dev` might require Node.js and npx to be installed on your system to run the Inspector tool.)*
|
||||
|
||||
### Integrating with Apps: `mcp install`
|
||||
|
||||
Once your server is working, you might want to use it from another application, like the Claude Desktop app. The `mcp install` command helps you register your server with Claude so it appears in the app's list of available tools.
|
||||
|
||||
```bash
|
||||
mcp install my_first_server.py --name "My Awesome Tool"
|
||||
```
|
||||
|
||||
**What happens?**
|
||||
|
||||
1. It finds your `my_first_server.py` file.
|
||||
2. It locates the configuration file for the Claude Desktop app on your computer.
|
||||
3. It adds an entry to that configuration file, telling Claude:
|
||||
* The name you want to use ("My Awesome Tool").
|
||||
* How to run your server (using a command like `uv run --with mcp mcp run /path/to/your/my_first_server.py`). `uv` is a fast tool used behind the scenes to manage the environment and dependencies needed to run your server.
|
||||
* Optionally, any extra Python packages your server needs (`--with some_package`) or environment variables (`--env-var KEY=VALUE`).
|
||||
|
||||
Now, when you open Claude Desktop, "My Awesome Tool" should be available for use! This command essentially automates the process of telling Claude how to find and execute your custom server.
|
||||
|
||||
## How Does `mcp run` Work Under the Hood?
|
||||
|
||||
Let's peek behind the curtain when you execute `mcp run my_first_server.py`. It might seem like magic, but it's a well-defined sequence of steps:
|
||||
|
||||
1. **You type the command:** You enter `mcp run my_first_server.py` in your terminal.
|
||||
2. **OS Executes `mcp`:** Your operating system finds the installed `mcp` script (which is part of the `MCP Python SDK`) and runs it using Python.
|
||||
3. **`Typer` Parses:** The `mcp` script uses a library called `Typer` to understand the command-line arguments. It sees `run` as the command and `my_first_server.py` as the argument.
|
||||
4. **`run` Function Called:** `Typer` directs the execution to the `run` function defined inside the SDK's `cli/cli.py` file.
|
||||
5. **Path Processing:** The `run` function calls internal helpers (like `_parse_file_path`) to find the full path to `my_first_server.py` and check if you specified a particular object within the file (e.g., `my_server.py:my_object`).
|
||||
6. **Server Import:** It then uses another helper (`_import_server`) to dynamically load the Python code from `my_first_server.py` and find the actual server object (like the `server` variable we created).
|
||||
7. **Server Execution:** Finally, it calls the `.run()` method on the imported server object. This is the signal for your server code to start doing its job – listening for connections, processing requests, etc. The specifics of `.run()` depend on the server type, like the [FastMCP Server (`FastMCP`)](02_fastmcp_server___fastmcp__.md) we'll see next.
|
||||
|
||||
Here's a simplified diagram of that flow:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Terminal
|
||||
participant OS
|
||||
participant MCP_CLI as mcp (cli/cli.py)
|
||||
participant ServerCode as my_first_server.py
|
||||
|
||||
User->>Terminal: mcp run my_first_server.py
|
||||
Terminal->>OS: Execute 'mcp' script
|
||||
OS->>MCP_CLI: Start script with args ['run', 'my_first_server.py']
|
||||
MCP_CLI->>MCP_CLI: Parse args (Typer finds 'run' command)
|
||||
MCP_CLI->>MCP_CLI: _parse_file_path('my_first_server.py')
|
||||
MCP_CLI->>MCP_CLI: _import_server(filepath, object_name)
|
||||
MCP_CLI->>ServerCode: Import module & find 'server' object
|
||||
ServerCode-->>MCP_CLI: Return server object
|
||||
MCP_CLI->>ServerCode: server.run()
|
||||
ServerCode->>ServerCode: Start listening/processing...
|
||||
```
|
||||
|
||||
## Diving into the Code (Briefly!)
|
||||
|
||||
You don't need to memorize this, but seeing snippets can help understand the structure.
|
||||
|
||||
**Inside `cli/cli.py` (Simplified):**
|
||||
|
||||
```python
|
||||
# Import the Typer library for creating CLIs
|
||||
import typer
|
||||
# Import helpers to find/load the server code
|
||||
from .helpers import _parse_file_path, _import_server # Fictional helper import
|
||||
|
||||
# Create the main CLI application object
|
||||
app = typer.Typer(name="mcp", help="MCP development tools")
|
||||
|
||||
# Decorator tells Typer this function handles the 'run' command
|
||||
@app.command()
|
||||
def run(
|
||||
file_spec: str = typer.Argument(...), # Expects the file path argument
|
||||
# ... other options like --transport ...
|
||||
) -> None:
|
||||
"""Run a MCP server."""
|
||||
# 1. Find the file and specific server object (if any)
|
||||
file_path, server_object_name = _parse_file_path(file_spec)
|
||||
|
||||
# 2. Load the code and get the server instance
|
||||
server = _import_server(file_path, server_object_name)
|
||||
|
||||
# 3. Tell the server instance to start running
|
||||
server.run() # Additional args like transport might be passed here
|
||||
|
||||
# ... other commands like dev, install, version defined similarly ...
|
||||
|
||||
# Standard Python entry point
|
||||
if __name__ == "__main__":
|
||||
app() # Start the Typer application
|
||||
```
|
||||
|
||||
This shows how `Typer` connects your command (`mcp run`) to the `run` function, which then orchestrates finding and starting your server code.
|
||||
|
||||
**Inside `cli/claude.py` (Simplified `update_claude_config`):**
|
||||
|
||||
```python
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Helper to find where Claude stores its config
|
||||
def get_claude_config_path() -> Path | None:
|
||||
# ... platform specific logic to find the path ...
|
||||
# Returns Path object like /Users/You/Library/Application Support/Claude
|
||||
pass # Implementation details skipped
|
||||
|
||||
def update_claude_config(file_spec: str, server_name: str, ...) -> bool:
|
||||
"""Add or update a FastMCP server in Claude's configuration."""
|
||||
config_dir = get_claude_config_path()
|
||||
if not config_dir:
|
||||
print("Error: Claude config not found.")
|
||||
return False
|
||||
|
||||
config_file = config_dir / "claude_desktop_config.json"
|
||||
|
||||
try:
|
||||
# Read existing config or create an empty one
|
||||
config = json.loads(config_file.read_text()) if config_file.exists() else {}
|
||||
if "mcpServers" not in config:
|
||||
config["mcpServers"] = {}
|
||||
|
||||
# Define how to run the server using 'uv' (a tool for running Python code)
|
||||
# This builds the command: uv run --with mcp mcp run /path/to/server.py
|
||||
run_command = ["uv", "run", "--with", "mcp", "mcp", "run", file_spec]
|
||||
# ... logic to add --with-editable or --with packages ...
|
||||
|
||||
# Add the server entry to the config dictionary
|
||||
config["mcpServers"][server_name] = {
|
||||
"command": "uv",
|
||||
"args": run_command[1:], # Arguments for the uv command
|
||||
# ... potentially add 'env' dictionary here ...
|
||||
}
|
||||
|
||||
# Write the updated configuration back to the file
|
||||
config_file.write_text(json.dumps(config, indent=2))
|
||||
print(f"Successfully installed {server_name} in Claude.")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error updating Claude config: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
This snippet shows the core logic of `mcp install`: find the Claude config file, construct the command needed to run *your* server using `uv` and `mcp run`, and save this information into the JSON configuration file.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've learned about the `mcp` command-line interface – your essential toolkit for managing MCP servers. You now know how to:
|
||||
|
||||
* Check your installation with `mcp version`.
|
||||
* Run a server directly using `mcp run your_server.py`.
|
||||
* Run a server with a debugging inspector using `mcp dev your_server.py`.
|
||||
* Register your server with applications like Claude Desktop using `mcp install your_server.py`.
|
||||
|
||||
This command is your bridge between writing server code and actually using it.
|
||||
|
||||
In the next chapter, we'll dive into the heart of many MCP servers: the [FastMCP Server (`FastMCP`)](02_fastmcp_server___fastmcp__.md), which is the kind of object the `mcp` command typically runs.
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
281
docs/MCP Python SDK/02_fastmcp_server___fastmcp__.md
Normal file
281
docs/MCP Python SDK/02_fastmcp_server___fastmcp__.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Chapter 2: Easier Server Building with `FastMCP`
|
||||
|
||||
In [Chapter 1: Your Control Panel - The `mcp` Command-Line Interface](01_cli___mcp__command_.md), we learned how to use the `mcp` command to run, test, and install MCP servers. We even saw a tiny example of a server file. But how do we *build* that server code without getting lost in complex details?
|
||||
|
||||
Imagine you want to build a simple AI assistant that can just echo back whatever you type. Writing all the code to handle connections, interpret messages according to the MCP protocol, manage capabilities – it sounds like a lot of work just for an echo!
|
||||
|
||||
This is where `FastMCP` comes in. It's designed to make building MCP servers much, much easier.
|
||||
|
||||
## What is `FastMCP`?
|
||||
|
||||
Think of the low-level parts of the MCP protocol like individual kitchen tools: a pot, a pan, a knife, a whisk. You *could* use them all individually to cook a meal, but you'd need to know exactly when and how to use each one.
|
||||
|
||||
`FastMCP` is like a fancy **kitchen multi-cooker**. It bundles many common functions together in an easy-to-use package. You provide the ingredients (your Python functions and data) and press simple buttons (special markers called **decorators** like `@tool`, `@resource`, `@prompt`), and `FastMCP` handles the complex cooking process (managing the low-level MCP details) for you.
|
||||
|
||||
**Key benefits of using `FastMCP`:**
|
||||
|
||||
* **Simplicity:** Hides a lot of the complex MCP protocol details.
|
||||
* **Developer-Friendly:** Uses familiar Python concepts like functions and decorators.
|
||||
* **Less Boilerplate:** Reduces the amount of repetitive setup code you need to write.
|
||||
* **Built-in Features:** Includes easy ways to manage settings, automatically tell clients what your server can do (capability generation), and handle common tasks.
|
||||
|
||||
## Your First `FastMCP` Server: The Foundation
|
||||
|
||||
Let's start with the absolute minimum needed to create a `FastMCP` server.
|
||||
|
||||
**File: `my_simple_server.py`**
|
||||
|
||||
```python
|
||||
# 1. Import the FastMCP class
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# 2. Create an instance of the FastMCP server
|
||||
# Give it a name clients might see.
|
||||
# Optionally, provide general instructions.
|
||||
server = FastMCP(
|
||||
name="MySimpleServer",
|
||||
instructions="This is a very simple example server."
|
||||
)
|
||||
|
||||
# 3. Add the standard Python block to run the server
|
||||
# when the script is executed directly.
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
# This tells FastMCP to start listening for connections
|
||||
server.run()
|
||||
print(f"{server.name} finished.") # Usually only seen after stopping (Ctrl+C)
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
1. **`from mcp.server.fastmcp import FastMCP`**: We import the main `FastMCP` class from the SDK.
|
||||
2. **`server = FastMCP(...)`**: We create our "multi-cooker" object.
|
||||
* `name="MySimpleServer"`: This is a human-readable name for your server. Clients might display this name.
|
||||
* `instructions="..."`: This provides a general description or purpose for the server. Clients can use this to understand what the server does.
|
||||
3. **`if __name__ == "__main__":`**: This is a standard Python pattern. The code inside this block only runs when you execute the script directly (e.g., using `python my_simple_server.py` or `mcp run my_simple_server.py`).
|
||||
4. **`server.run()`**: This is the command that actually starts the server. It tells `FastMCP` to begin listening for incoming connections and handling MCP messages. By default, it uses the "stdio" transport (reading/writing from the terminal), which we discussed briefly in Chapter 1.
|
||||
|
||||
If you save this code as `my_simple_server.py` and run it using `mcp run my_simple_server.py` (as learned in Chapter 1), it will start! It won't *do* much yet, because we haven't added any specific capabilities, but it's a functioning MCP server.
|
||||
|
||||
## Adding Features with Decorators: The "Buttons"
|
||||
|
||||
Our multi-cooker (`FastMCP`) is running, but it doesn't have any cooking programs yet. How do we add features, like our "echo" tool? We use **decorators**.
|
||||
|
||||
Decorators in Python are special markers starting with `@` that you place above a function definition. They modify or enhance the function in some way. `FastMCP` uses decorators like `@server.tool()`, `@server.resource()`, and `@server.prompt()` to easily register your Python functions as capabilities that clients can use.
|
||||
|
||||
Let's add an "echo" tool using the `@server.tool()` decorator.
|
||||
|
||||
**File: `echo_server.py` (Simpler Version)**
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# 1. Create the server instance
|
||||
server = FastMCP(name="EchoServer")
|
||||
|
||||
# 2. Define the tool using the @server.tool decorator
|
||||
@server.tool(name="echo", description="Repeats the input message back.")
|
||||
def echo(message: str) -> str:
|
||||
"""
|
||||
This function is now registered as the 'echo' tool.
|
||||
'message: str' tells FastMCP the tool expects one argument
|
||||
named 'message' which should be a string.
|
||||
'-> str' tells FastMCP the tool will return a string.
|
||||
"""
|
||||
print(f"Tool 'echo' called with message: {message}") # Server-side log
|
||||
# 3. The function's logic directly implements the tool
|
||||
return f"You said: {message}"
|
||||
|
||||
# 4. Standard run block
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run() # Start listening
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
1. **`server = FastMCP(...)`**: Same as before, creates our server object.
|
||||
2. **`@server.tool(...)`**: This is the magic!
|
||||
* We use the `@tool()` method of our `server` object as a decorator.
|
||||
* `name="echo"`: We explicitly tell `FastMCP` that this tool should be called `echo` by clients. If we omitted this, it would default to the function name (`echo`).
|
||||
* `description="..."`: A helpful description for clients.
|
||||
3. **`def echo(message: str) -> str:`**: This is a standard Python function.
|
||||
* `message: str`: This is a **type hint**. It tells `FastMCP` (and other tools) that this function expects one argument named `message`, and that argument should be a string. `FastMCP` uses this information to automatically validate input from clients and generate documentation.
|
||||
* `-> str`: This type hint indicates that the function will return a string. `FastMCP` uses this to know what kind of output to expect.
|
||||
* The function body contains the logic for our tool.
|
||||
4. **`server.run()`**: Starts the server, which now knows about the `echo` tool thanks to the decorator.
|
||||
|
||||
Now, if you run `mcp run echo_server.py`, the server will start and will be capable of responding to requests for the `echo` tool! A client could send a "callTool" request with the name "echo" and an argument `{"message": "Hello!"}`, and `FastMCP` would automatically run your `echo` function and send back the result `"You said: Hello!"`.
|
||||
|
||||
We'll explore `@server.resource()` and `@server.prompt()` in later chapters:
|
||||
* [Chapter 3: FastMCP Resources (`Resource`, `ResourceManager`)](03_fastmcp_resources___resource____resourcemanager__.md)
|
||||
* [Chapter 5: FastMCP Prompts (`Prompt`, `PromptManager`)](05_fastmcp_prompts___prompt____promptmanager__.md)
|
||||
|
||||
## How `FastMCP` Works Under the Hood (Simplified)
|
||||
|
||||
It feels simple to use, but what's `FastMCP` actually doing?
|
||||
|
||||
1. **Initialization:** When you create `FastMCP()`, it sets up internal managers for tools, resources, and prompts (like `_tool_manager`, `_resource_manager`, `_prompt_manager`).
|
||||
2. **Registration:** When Python encounters `@server.tool(...)` above your `echo` function, it calls the `server.tool()` method. This method takes your `echo` function and its details (name, description, parameter types from hints) and registers it with the internal `_tool_manager`.
|
||||
3. **Running:** When you call `server.run()`, `FastMCP` starts the underlying low-level MCP server machinery. This machinery listens for incoming connections (e.g., via stdio or web protocols).
|
||||
4. **Handling Requests:** When a client connects and sends an MCP message like `{"method": "callTool", "params": {"name": "echo", "arguments": {"message": "Test"}}}`:
|
||||
* The low-level server receives the raw message.
|
||||
* `FastMCP`'s core logic takes over. It sees it's a `callTool` request for the tool named `echo`.
|
||||
* It asks its `_tool_manager` if it knows about a tool named `echo`.
|
||||
* The `_tool_manager` finds the registered `echo` function.
|
||||
* `FastMCP` extracts the `arguments` (`{"message": "Test"}`) from the request.
|
||||
* It validates these arguments against the function's signature (`message: str`).
|
||||
* It calls your actual Python `echo` function, passing `"Test"` as the `message` argument.
|
||||
* Your function runs and returns `"You said: Test"`.
|
||||
* `FastMCP` takes this return value, packages it into a valid MCP `callTool` response message, and sends it back to the client via the low-level machinery.
|
||||
|
||||
**Sequence Diagram:**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant FastMCP_Server as FastMCP (echo_server.py)
|
||||
participant ToolManager as _tool_manager
|
||||
participant EchoFunction as echo()
|
||||
|
||||
Client->>+FastMCP_Server: Send MCP Request: callTool(name="echo", args={"message": "Test"})
|
||||
FastMCP_Server->>+ToolManager: Find tool named "echo"
|
||||
ToolManager-->>-FastMCP_Server: Return registered 'echo' function info
|
||||
FastMCP_Server->>+EchoFunction: Call echo(message="Test")
|
||||
EchoFunction-->>-FastMCP_Server: Return "You said: Test"
|
||||
FastMCP_Server->>-Client: Send MCP Response: result="You said: Test"
|
||||
```
|
||||
|
||||
**Looking at the Code (Briefly):**
|
||||
|
||||
You don't need to understand every line, but seeing where things happen can be helpful.
|
||||
|
||||
**Inside `server/fastmcp/server.py` (Simplified `FastMCP.__init__`):**
|
||||
|
||||
```python
|
||||
# (...) imports (...)
|
||||
from .tools import ToolManager
|
||||
from .resources import ResourceManager
|
||||
from .prompts import PromptManager
|
||||
|
||||
class FastMCP:
|
||||
def __init__(
|
||||
self, name: str | None = None, instructions: str | None = None, **settings: Any
|
||||
):
|
||||
# Stores settings like debug mode, log level etc.
|
||||
self.settings = Settings(**settings)
|
||||
|
||||
# Creates the underlying low-level MCP server
|
||||
self._mcp_server = MCPServer(
|
||||
name=name or "FastMCP",
|
||||
instructions=instructions,
|
||||
# ... other low-level setup ...
|
||||
)
|
||||
# Creates the managers to keep track of registered items
|
||||
self._tool_manager = ToolManager(
|
||||
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
|
||||
)
|
||||
self._resource_manager = ResourceManager(
|
||||
# ...
|
||||
)
|
||||
self._prompt_manager = PromptManager(
|
||||
# ...
|
||||
)
|
||||
|
||||
# Connects MCP requests (like 'callTool') to FastMCP methods
|
||||
self._setup_handlers()
|
||||
# (...)
|
||||
```
|
||||
|
||||
This shows that `FastMCP` creates helper objects (`_tool_manager`, etc.) to organize the tools, resources, and prompts you register.
|
||||
|
||||
**Inside `server/fastmcp/server.py` (Simplified `FastMCP.tool` decorator):**
|
||||
|
||||
```python
|
||||
# (...) imports (...)
|
||||
from mcp.types import AnyFunction # Represents any kind of Python function
|
||||
|
||||
class FastMCP:
|
||||
# (...) other methods (...)
|
||||
|
||||
def tool(
|
||||
self, name: str | None = None, description: str | None = None
|
||||
) -> Callable[[AnyFunction], AnyFunction]:
|
||||
"""Decorator to register a tool."""
|
||||
# (...) error checking (...)
|
||||
|
||||
# This is the actual function that gets applied to your 'echo' function
|
||||
def decorator(fn: AnyFunction) -> AnyFunction:
|
||||
# Tells the tool manager to remember this function 'fn'
|
||||
# associating it with the given name and description.
|
||||
# It also inspects 'fn' to figure out its parameters (like 'message: str')
|
||||
self.add_tool(fn, name=name, description=description)
|
||||
return fn # Returns the original function unchanged
|
||||
|
||||
return decorator # Returns the 'decorator' function
|
||||
|
||||
def add_tool(
|
||||
self,
|
||||
fn: AnyFunction,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
) -> None:
|
||||
"""Add a tool to the server."""
|
||||
# This passes the function and its info to the ToolManager
|
||||
self._tool_manager.add_tool(fn, name=name, description=description)
|
||||
|
||||
```
|
||||
|
||||
This shows how the `@server.tool()` decorator ultimately calls `self._tool_manager.add_tool()` to register your function.
|
||||
|
||||
**Inside `server/fastmcp/server.py` (Simplified `FastMCP.call_tool` handler):**
|
||||
|
||||
```python
|
||||
# (...) imports (...)
|
||||
|
||||
class FastMCP:
|
||||
# (...) other methods (...)
|
||||
|
||||
async def call_tool(
|
||||
self, name: str, arguments: dict[str, Any]
|
||||
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
||||
"""Call a tool by name with arguments."""
|
||||
# Gets a 'Context' object (more on this later!)
|
||||
context = self.get_context()
|
||||
# Asks the ToolManager to find and execute the tool
|
||||
# The ToolManager handles finding your 'echo' function,
|
||||
# validating arguments, and calling it.
|
||||
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
||||
# Converts the function's return value (e.g., "You said: Test")
|
||||
# into the format MCP expects for the response.
|
||||
converted_result = _convert_to_content(result)
|
||||
return converted_result
|
||||
|
||||
def _setup_handlers(self) -> None:
|
||||
"""Set up core MCP protocol handlers."""
|
||||
# This line connects the low-level 'callTool' message
|
||||
# to the 'self.call_tool' method shown above.
|
||||
self._mcp_server.call_tool()(self.call_tool)
|
||||
# (...) other handlers for listTools, readResource etc. (...)
|
||||
```
|
||||
|
||||
This shows how an incoming `callTool` message gets routed to the `call_tool` method, which then uses the `_tool_manager` to run your registered function.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've now seen how `FastMCP` provides a much simpler way to build MCP servers compared to handling the low-level protocol directly. Like a multi-cooker, it offers convenient "buttons" (decorators like `@server.tool()`) to add features (like tools) to your server using standard Python functions. It handles the underlying complexity of receiving requests, calling your code, and sending responses.
|
||||
|
||||
You learned how to:
|
||||
* Create a basic `FastMCP` server instance.
|
||||
* Define a Python function that performs a task.
|
||||
* Use the `@server.tool()` decorator to register that function as a tool clients can call.
|
||||
* Understand the basic flow of how `FastMCP` handles a tool call request using its internal managers.
|
||||
|
||||
While our `echo` tool was simple, `FastMCP` provides the foundation for building much more complex and powerful AI agents and tools.
|
||||
|
||||
In the next chapters, we'll explore the other "buttons" on our multi-cooker, starting with how to provide data and files using `@server.resource()` in [Chapter 3: FastMCP Resources (`Resource`, `ResourceManager`)](03_fastmcp_resources___resource____resourcemanager__.md).
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
@@ -0,0 +1,275 @@
|
||||
# Chapter 3: Sharing Data - FastMCP Resources (`Resource`, `ResourceManager`)
|
||||
|
||||
In [Chapter 2: Easier Server Building with `FastMCP`](02_fastmcp_server___fastmcp__.md), we saw how `FastMCP` and the `@server.tool()` decorator make it easy to create servers that can *perform actions* for clients, like our `echo` tool.
|
||||
|
||||
But what if your server just needs to share some *data*? Maybe it has a configuration file the client needs, a list of available items, or some text generated on the fly. You *could* make a tool for each piece of data, but that feels clunky. Isn't there a way for clients to just browse and read data sources directly?
|
||||
|
||||
Yes, there is! Welcome to **FastMCP Resources**.
|
||||
|
||||
## The Digital Library: Resources and the Resource Manager
|
||||
|
||||
Imagine your `FastMCP` server is like a **digital library**. Inside this library, you have various pieces of information:
|
||||
* Simple text notes (like a welcome message).
|
||||
* Static files (like a configuration file or a small image).
|
||||
* Information that changes (like the current time or weather).
|
||||
|
||||
Each piece of information in this library is called a **`Resource`**. Think of each `Resource` as a book, a document, or maybe even a live news feed within the library.
|
||||
|
||||
To access any item in a library, you need its unique identifier – like a call number or an ISBN. In FastMCP, resources are identified by a **URI** (Uniform Resource Identifier). This looks similar to a web URL (like `http://example.com`) but can use different schemes (like `data://`, `file://`, `weather://`). For example, a welcome message might have the URI `data://welcome_message`.
|
||||
|
||||
Now, how do you find out what books are in the library, or add a new one? You talk to the **librarian**. In `FastMCP`, the component that keeps track of all the available resources is called the **`ResourceManager`**.
|
||||
|
||||
* **`Resource`**: A specific piece of data (static, dynamic, file) accessible via a URI. (The book)
|
||||
* **`ResourceManager`**: Manages all the `Resource` objects registered with the `FastMCP` server. (The librarian)
|
||||
* **URI**: The unique address used to find and access a `Resource`. (The call number)
|
||||
|
||||
Clients can ask the `ResourceManager` (via `FastMCP`) to list all available resources (`listResources`) and then request the content of a specific resource using its URI (`readResource`).
|
||||
|
||||
## Adding Books to the Library: Using `@server.resource()`
|
||||
|
||||
Just like `@server.tool()` made it easy to add actions, `FastMCP` provides a simple decorator, `@server.resource()`, to add data resources to your server's library (its `ResourceManager`).
|
||||
|
||||
Let's add a simple, static welcome message to our server.
|
||||
|
||||
**File: `library_server.py` (Version 1)**
|
||||
|
||||
```python
|
||||
# 1. Import FastMCP
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# 2. Create the server instance
|
||||
server = FastMCP(name="LibraryServer")
|
||||
|
||||
# 3. Define a function that returns our static data
|
||||
def get_welcome_message() -> str:
|
||||
"""Returns a simple welcome string."""
|
||||
return "Welcome to the Library Server!"
|
||||
|
||||
# 4. Use the @server.resource() decorator to register the function's result
|
||||
# The URI "data://greeting" will be used by clients to access this.
|
||||
@server.resource(uri="data://greeting", description="A friendly greeting.")
|
||||
def welcome_resource():
|
||||
# This function will be called *when a client reads* the resource.
|
||||
# It just returns the static message.
|
||||
return get_welcome_message() # Or simply: return "Welcome..."
|
||||
|
||||
# Standard run block
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run()
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
*(Self-correction: The previous example was slightly complex with two functions. Let's simplify.)*
|
||||
|
||||
**File: `library_server.py` (Version 1 - Simpler)**
|
||||
|
||||
```python
|
||||
# 1. Import FastMCP
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# 2. Create the server instance
|
||||
server = FastMCP(name="LibraryServer")
|
||||
|
||||
# 3. Use the @server.resource() decorator directly on the function
|
||||
# that provides the data.
|
||||
@server.resource(uri="data://greeting", description="A friendly greeting.")
|
||||
def welcome_message() -> str:
|
||||
"""
|
||||
This function is registered as the resource 'data://greeting'.
|
||||
It will be called when a client reads this resource URI.
|
||||
'-> str' indicates it returns text. FastMCP sets MIME type to text/plain.
|
||||
"""
|
||||
print("Resource 'data://greeting' was read!") # Server-side log
|
||||
return "Welcome to the Library Server! Enjoy your stay."
|
||||
|
||||
# 4. 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 library). Inside, it creates a `ResourceManager` (the librarian).
|
||||
2. **`@server.resource(...)`**: This is our decorator "button".
|
||||
* `uri="data://greeting"`: We assign a unique URI (call number) to this resource. The `data://` part is just a convention here, you can choose meaningful schemes.
|
||||
* `description="..."`: A helpful description for clients browsing the library.
|
||||
3. **`def welcome_message() -> str:`**: This function provides the *content* for the resource.
|
||||
* `-> str`: The type hint tells `FastMCP` this resource provides text data. It will automatically set the `mime_type` to `text/plain`.
|
||||
* The function's body simply returns the string we want to share.
|
||||
* **Important:** This function is only executed when a client actually asks to *read* the resource `data://greeting`. It's not run when the server starts.
|
||||
4. **`server.run()`**: Starts the server. The `ResourceManager` now knows about `data://greeting`.
|
||||
|
||||
If you run this server (`mcp run library_server.py`), a client could:
|
||||
1. Call `listResources` and see `data://greeting` in the list.
|
||||
2. Call `readResource` with the URI `data://greeting`.
|
||||
3. `FastMCP` would ask the `ResourceManager`, find the registered function (`welcome_message`), run it, get the string `"Welcome..."`, and send it back to the client.
|
||||
|
||||
## Dynamic Data: Resources Generated on the Fly
|
||||
|
||||
Resources don't have to be static text. The function you decorate can do calculations, read files, or anything else to generate the data *when it's requested*. This is great for information that changes.
|
||||
|
||||
Let's add a resource that tells the current time.
|
||||
|
||||
**File: `library_server.py` (Version 2)**
|
||||
|
||||
```python
|
||||
import datetime # Need this module to get the current time
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
server = FastMCP(name="LibraryServer")
|
||||
|
||||
@server.resource(uri="data://greeting", description="A friendly greeting.")
|
||||
def welcome_message() -> str:
|
||||
print("Resource 'data://greeting' was read!")
|
||||
return "Welcome to the Library Server! Enjoy your stay."
|
||||
|
||||
# NEW: Add a dynamic resource for the current time
|
||||
@server.resource(uri="time://current", description="The current server time.")
|
||||
def current_time() -> str:
|
||||
"""Returns the current time as a string."""
|
||||
now = datetime.datetime.now()
|
||||
time_str = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"Resource 'time://current' was read! Time is {time_str}")
|
||||
# The function calculates the time *each time* it's called
|
||||
return f"The current server time is: {time_str}"
|
||||
|
||||
# Standard run block
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run()
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
Now, every time a client reads `time://current`, the `current_time` function will execute, get the *latest* time, format it, and return it.
|
||||
|
||||
## Parameterized Data: Resource Templates
|
||||
|
||||
What if you have data related to specific items, like weather information for different cities? You wouldn't want to create a separate resource function for every city (`weather_london`, `weather_paris`, etc.).
|
||||
|
||||
Resource URIs can contain parameters, indicated by curly braces `{}`. When you define a resource with a parameterized URI and a function that accepts arguments matching those parameters, `FastMCP` creates a **Resource Template**.
|
||||
|
||||
**File: `library_server.py` (Version 3)**
|
||||
|
||||
```python
|
||||
import datetime
|
||||
import random # To simulate getting weather data
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
server = FastMCP(name="LibraryServer")
|
||||
|
||||
@server.resource(uri="data://greeting", description="A friendly greeting.")
|
||||
def welcome_message() -> str:
|
||||
return "Welcome to the Library Server! Enjoy your stay."
|
||||
|
||||
@server.resource(uri="time://current", description="The current server time.")
|
||||
def current_time() -> str:
|
||||
now = datetime.datetime.now()
|
||||
return f"The current server time is: {now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
# NEW: Add a resource template for weather
|
||||
# The URI contains a parameter {city_name}
|
||||
@server.resource(uri="weather://forecast/{city_name}",
|
||||
description="Provides a dummy weather forecast.")
|
||||
# The function accepts an argument matching the URI parameter
|
||||
def get_weather_forecast(city_name: str) -> str:
|
||||
"""Generates a fake weather forecast for the given city."""
|
||||
print(f"Resource template 'weather://forecast/{{city}}' read for city: {city_name}")
|
||||
# In a real app, you'd fetch actual weather here based on city_name
|
||||
temperature = random.randint(5, 25)
|
||||
conditions = random.choice(["Sunny", "Cloudy", "Rainy"])
|
||||
return f"Forecast for {city_name.capitalize()}: {temperature}°C, {conditions}"
|
||||
|
||||
# Standard run block
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run()
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
1. **`@server.resource(uri="weather://forecast/{city_name}", ...)`**: We define a URI with a placeholder `{city_name}`.
|
||||
2. **`def get_weather_forecast(city_name: str) -> str:`**: The function signature includes a parameter `city_name` that exactly matches the name inside the curly braces in the URI.
|
||||
3. **How it works:**
|
||||
* When a client asks to read a URI like `weather://forecast/london`, `FastMCP` sees it matches the template.
|
||||
* It extracts the value "london" from the URI.
|
||||
* It calls the `get_weather_forecast` function, passing `"london"` as the `city_name` argument.
|
||||
* The function generates the forecast for London and returns the string.
|
||||
* If the client asks for `weather://forecast/paris`, the same function is called, but with `city_name="paris"`.
|
||||
|
||||
This template approach is very powerful for providing structured data without writing repetitive code. Clients would use `listResourceTemplates` to discover templates like this.
|
||||
|
||||
## How Resources Work Under the Hood
|
||||
|
||||
Using `@server.resource()` feels simple, but what's happening inside `FastMCP`?
|
||||
|
||||
1. **Registration:** When Python processes your code and sees `@server.resource(uri="data://greeting")` above the `welcome_message` function, it calls an internal `server.resource()` method.
|
||||
* This method analyzes the URI and the function.
|
||||
* If the URI has no `{}` parameters and the function takes no arguments (or only a `Context` argument), it creates a `FunctionResource` object. This object essentially wraps your `welcome_message` function, storing its details (URI, description, the function itself).
|
||||
* If the URI *does* have parameters matching the function's arguments (like `weather://forecast/{city_name}` and `get_weather_forecast(city_name: str)`), it creates a `ResourceTemplate` object instead.
|
||||
* It then tells the `ResourceManager` (the librarian) to store this `FunctionResource` or `ResourceTemplate`. (This happens via `_resource_manager.add_resource` or `_resource_manager.add_template`, referencing `server/fastmcp/resources/resource_manager.py`).
|
||||
|
||||
2. **Client Request (`readResource`)**:
|
||||
* A client sends an MCP message: `{"method": "readResource", "params": {"uri": "data://greeting"}}`.
|
||||
* `FastMCP` receives this and calls its internal `read_resource` handler (see `server/fastmcp/server.py`).
|
||||
* The handler asks the `ResourceManager`: "Do you have a resource for the URI `data://greeting`?" (`_resource_manager.get_resource`).
|
||||
* The `ResourceManager` checks its list of concrete resources. It finds the `FunctionResource` associated with `data://greeting`.
|
||||
* `FastMCP` (or the `ResourceManager`) calls the `.read()` method on that `FunctionResource` object (see `server/fastmcp/resources/types.py`).
|
||||
* The `FunctionResource.read()` method executes the original Python function you decorated (`welcome_message()`).
|
||||
* Your function returns the string `"Welcome..."`.
|
||||
* `FastMCP` packages this string into a valid MCP `readResource` response and sends it back to the client.
|
||||
|
||||
3. **Client Request (`readResource` with Template)**:
|
||||
* Client sends: `{"method": "readResource", "params": {"uri": "weather://forecast/london"}}`.
|
||||
* `FastMCP` asks `ResourceManager` for `weather://forecast/london`.
|
||||
* `ResourceManager` checks concrete resources – no match.
|
||||
* `ResourceManager` checks its `ResourceTemplate` list. It finds the `weather://forecast/{city_name}` template matches the requested URI.
|
||||
* It extracts the parameter `{"city_name": "london"}`.
|
||||
* It uses the template to *dynamically create* a temporary `FunctionResource` for this specific request, configured to call `get_weather_forecast(city_name="london")`.
|
||||
* `FastMCP` calls `.read()` on this temporary resource.
|
||||
* The `get_weather_forecast("london")` function runs and returns the forecast string.
|
||||
* `FastMCP` sends the result back.
|
||||
|
||||
**Simplified Sequence Diagram (`readResource` for `data://greeting`):**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant FastMCP_Server as FastMCP (library_server.py)
|
||||
participant ResManager as ResourceManager (_resource_manager)
|
||||
participant FuncResource as FunctionResource (wraps welcome_message)
|
||||
participant WelcomeFunc as welcome_message()
|
||||
|
||||
Client->>+FastMCP_Server: Send MCP Request: readResource(uri="data://greeting")
|
||||
FastMCP_Server->>+ResManager: get_resource("data://greeting")
|
||||
ResManager-->>-FastMCP_Server: Return FunctionResource object
|
||||
FastMCP_Server->>+FuncResource: resource.read()
|
||||
FuncResource->>+WelcomeFunc: Call original function welcome_message()
|
||||
WelcomeFunc-->>-FuncResource: Return "Welcome..."
|
||||
FuncResource-->>-FastMCP_Server: Return "Welcome..."
|
||||
FastMCP_Server->>-Client: Send MCP Response: content="Welcome..."
|
||||
```
|
||||
|
||||
While `@server.resource()` is the easiest way, the SDK also provides classes like `TextResource`, `BinaryResource`, `FileResource` (see `server/fastmcp/resources/types.py`) that you could potentially instantiate and add directly using `server.add_resource(MyTextResource(...))`, but the decorator handles wrapping your functions nicely.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've learned about FastMCP Resources – the way to share data from your server like items in a digital library.
|
||||
|
||||
* **Resources (`Resource`)** are data sources (text, files, dynamic content) identified by **URIs**.
|
||||
* The **`ResourceManager`** keeps track of all registered resources.
|
||||
* The `@server.resource()` decorator is the easiest way to add resources by wrapping Python functions.
|
||||
* Resources can be **static** (returning the same data) or **dynamic** (generating data when read).
|
||||
* **Resource Templates** allow you to handle parameterized URIs (like `weather://forecast/{city}`) efficiently.
|
||||
* Clients use `listResources`, `listResourceTemplates`, and `readResource` to interact with your server's data library.
|
||||
|
||||
Resources are essential for providing context, configuration, or any other data your clients might need to consume without executing a complex action.
|
||||
|
||||
In the next chapter, we'll take a closer look at the other main building block we briefly saw in Chapter 2: [FastMCP Tools (`Tool`, `ToolManager`)](04_fastmcp_tools___tool____toolmanager__.md), and explore how they handle actions and inputs in more detail.
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
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)
|
||||
@@ -0,0 +1,297 @@
|
||||
# Chapter 5: Reusable Chat Starters - FastMCP Prompts (`Prompt`, `PromptManager`)
|
||||
|
||||
In [Chapter 4: FastMCP Tools (`Tool`, `ToolManager`)](04_fastmcp_tools___tool____toolmanager__.md), we learned how to give our server specific *actions* it can perform, like a calculator tool. But modern AI often involves conversations, especially with Large Language Models (LLMs). How do we manage the instructions and conversation starters we send to these models?
|
||||
|
||||
Imagine you want to build an AI assistant tool that can summarize text. You'll need to tell the underlying LLM *what* to do (summarize) and *what* text to summarize. You might also want to provide specific instructions like "Keep the summary under 50 words." You'll probably need variations of this prompt for different tasks. Writing this message structure over and over again in your tool code would be repetitive and hard to manage.
|
||||
|
||||
This is where **FastMCP Prompts** come in. They provide a way to create reusable templates for generating sequences of messages, perfect for starting conversations with LLMs or structuring requests.
|
||||
|
||||
## The Mad Libs Analogy: Prompts and the Prompt Manager
|
||||
|
||||
Think of a **`Prompt`** like a **Mad Libs story template**. A Mad Libs template has a pre-written story with blanks (like `___(noun)___` or `___(verb)___`). You define the structure and the blanks.
|
||||
|
||||
* **`Prompt`**: The Mad Libs template itself. It has a name (like "Vacation Story") and defined blanks. In FastMCP, the "story" is a sequence of messages (usually for an LLM), and the blanks are **`PromptArgument`** objects.
|
||||
* **`PromptArgument`**: Represents a blank in the template. It defines the name of the blank (e.g., `text_to_summarize`), maybe a description, and whether it's required.
|
||||
* **Rendering**: The act of filling in the blanks. You provide values (arguments) for the blanks (`text_to_summarize = "Once upon a time..."`), and the template generates the complete story. In FastMCP, rendering a `Prompt` with arguments produces a list of **`PromptMessage`** objects (like `UserMessage` or `AssistantMessage`). These messages have roles (`user`, `assistant`) and content, ready to be sent to an LLM.
|
||||
* **`PromptManager`**: Like a folder or binder holding all your different Mad Libs templates. It's the part of `FastMCP` that stores and helps you find and use (`render`) your defined `Prompt` templates.
|
||||
|
||||
Clients (like an AI application) can ask the `PromptManager` (via `FastMCP`) to list available prompt templates (`listPrompts`) and then request a specific, filled-in prompt sequence using its name and arguments (`getPrompt`).
|
||||
|
||||
## Creating Your First Prompt Template: Using `@server.prompt()`
|
||||
|
||||
Just like `@server.tool()` and `@server.resource()`, `FastMCP` provides a simple decorator, `@server.prompt()`, to easily define these message templates using Python functions.
|
||||
|
||||
Let's create a prompt template for our text summarization task.
|
||||
|
||||
**File: `summarizer_server.py`**
|
||||
|
||||
```python
|
||||
# 1. Import FastMCP and message types
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.fastmcp.prompts import UserMessage # We'll use this
|
||||
|
||||
# 2. Create the server instance
|
||||
server = FastMCP(name="SummarizerServer")
|
||||
|
||||
# 3. Use the @server.prompt() decorator to define our template
|
||||
@server.prompt(name="summarize_text", description="Generates messages to ask an LLM to summarize text.")
|
||||
def create_summary_prompt(text_to_summarize: str) -> list[UserMessage]:
|
||||
"""
|
||||
This function defines the 'summarize_text' prompt template.
|
||||
'text_to_summarize: str' defines a required argument (a blank).
|
||||
'-> list[UserMessage]' indicates it returns a list of messages.
|
||||
"""
|
||||
print(f"Rendering prompt 'summarize_text' with text: {text_to_summarize[:30]}...") # Log
|
||||
|
||||
# 4. Construct the message(s) based on the arguments
|
||||
# Here, we create a single user message containing instructions and the text.
|
||||
prompt_content = f"Please summarize the following text concisely:\n\n{text_to_summarize}"
|
||||
|
||||
# Return a list containing one UserMessage object
|
||||
return [UserMessage(content=prompt_content)]
|
||||
|
||||
# 5. Standard run block (optional: add a tool that uses this prompt later)
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run()
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
1. **Imports**: We import `FastMCP` and `UserMessage` (a specific type of `PromptMessage`). `AssistantMessage` is also available.
|
||||
2. **`server = FastMCP(...)`**: Creates our server. Internally, this also creates a `PromptManager`.
|
||||
3. **`@server.prompt(...)`**: This decorator registers our function as a prompt template.
|
||||
* `name="summarize_text"`: The name clients will use to request this template.
|
||||
* `description="..."`: A helpful description.
|
||||
4. **`def create_summary_prompt(...)`**: This Python function *builds* the message list when the prompt is rendered.
|
||||
* `text_to_summarize: str`: The type hint defines a required `PromptArgument` named `text_to_summarize`. This is the blank in our Mad Libs.
|
||||
* `-> list[UserMessage]`: The type hint tells `FastMCP` that this function will return a list containing `UserMessage` objects (or compatible types like plain strings or dicts that look like messages).
|
||||
* The function body uses the input argument (`text_to_summarize`) to construct the desired message content.
|
||||
* It returns a list containing a single `UserMessage`. You could return multiple messages (e.g., alternating user/assistant roles) to set up a conversation history.
|
||||
5. **`server.run()`**: Starts the server. The `PromptManager` now knows about the `summarize_text` prompt template.
|
||||
|
||||
**What happens when a client uses this prompt?**
|
||||
|
||||
1. **Discovery (Optional):** A client might call `listPrompts`. The server (using `PromptManager`) would respond with information about the `summarize_text` prompt, including its name, description, and the required argument `text_to_summarize` (string).
|
||||
2. **Rendering Request:** The client wants to generate the messages for summarizing a specific text. It sends an MCP request: `getPrompt` with `name="summarize_text"` and `arguments={"text_to_summarize": "This is the text..."}`.
|
||||
3. **Server-Side Rendering:**
|
||||
* `FastMCP` receives the request and asks its `PromptManager` to render the prompt.
|
||||
* `PromptManager` finds the `Prompt` object associated with `summarize_text`.
|
||||
* It calls the `render` method on the `Prompt` object, which in turn calls your Python function `create_summary_prompt(text_to_summarize="This is the text...")`.
|
||||
* Your function runs, builds the `prompt_content` string, and returns `[UserMessage(content="Please summarize...")]`.
|
||||
* `FastMCP` takes this list of `Message` objects.
|
||||
4. **Response:** `FastMCP` sends the generated message list back to the client in the `getPrompt` response. The client now has the structured message(s) ready to be sent to an LLM.
|
||||
|
||||
```json
|
||||
// Example Client Request (Simplified MCP format)
|
||||
{
|
||||
"method": "getPrompt",
|
||||
"params": {
|
||||
"name": "summarize_text",
|
||||
"arguments": {
|
||||
"text_to_summarize": "The quick brown fox jumps over the lazy dog."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example Server Response (Simplified MCP format)
|
||||
{
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Please summarize the following text concisely:\n\nThe quick brown fox jumps over the lazy dog."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This makes it easy for client applications to get consistently formatted prompts for various tasks without needing to know the exact text structure themselves.
|
||||
|
||||
## Returning Different Message Types
|
||||
|
||||
Your prompt function can return various things, and `FastMCP` will try to convert them into the standard `Message` format (like `UserMessage` or `AssistantMessage`):
|
||||
|
||||
* **A single string:** Automatically converted to `UserMessage(content=TextContent(type="text", text=your_string))`.
|
||||
* **A `Message` object (e.g., `UserMessage`, `AssistantMessage`):** Used directly.
|
||||
* **A dictionary matching the `Message` structure:** e.g., `{"role": "user", "content": "Hello!"}`. Validated and converted.
|
||||
* **A list containing any mix of the above:** Each item is converted/validated.
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
# Import both message types
|
||||
from mcp.server.fastmcp.prompts import UserMessage, AssistantMessage
|
||||
|
||||
server = FastMCP(name="MultiMessageServer")
|
||||
|
||||
@server.prompt(name="greet_user", description="Starts a simple conversation.")
|
||||
def greeting_prompt(user_name: str): # -> returns list of mixed types
|
||||
"""Generates a multi-turn conversation starter."""
|
||||
|
||||
# We can return a list containing different types:
|
||||
return [
|
||||
# A UserMessage object
|
||||
UserMessage(f"Hello {user_name}, tell me about your day."),
|
||||
# A dictionary that looks like an AssistantMessage
|
||||
{"role": "assistant", "content": "I'm ready to listen!"},
|
||||
# A simple string (becomes a UserMessage)
|
||||
"Start whenever you're ready.",
|
||||
]
|
||||
|
||||
# ... (run block) ...
|
||||
```
|
||||
|
||||
This flexibility lets you structure complex conversational prompts easily.
|
||||
|
||||
## How Prompts Work Under the Hood
|
||||
|
||||
Using `@server.prompt()` is straightforward, but what's happening inside `FastMCP` and its `PromptManager`?
|
||||
|
||||
**1. Registration (When the server code loads):**
|
||||
|
||||
* Python executes your `summarizer_server.py`.
|
||||
* It reaches the `@server.prompt(name="summarize_text", ...)` line above `def create_summary_prompt(...)`.
|
||||
* This calls the `server.prompt()` method (in `server/fastmcp/server.py`). This method returns a decorator function that is immediately applied to `create_summary_prompt`.
|
||||
* The decorator function calls `server.add_prompt()`.
|
||||
* `server.add_prompt()` calls `self._prompt_manager.add_prompt()`.
|
||||
* Inside `PromptManager.add_prompt` (in `server/fastmcp/prompts/manager.py`):
|
||||
* It calls `Prompt.from_function(create_summary_prompt, name="summarize_text", ...)` (see `server/fastmcp/prompts/base.py`).
|
||||
* `Prompt.from_function` inspects the `create_summary_prompt` function:
|
||||
* Gets its name (`summarize_text`).
|
||||
* Gets its description (from decorator or docstring).
|
||||
* Looks at the parameters (`text_to_summarize: str`) using Python's introspection to determine the required `PromptArgument`s.
|
||||
* Creates a `Prompt` object storing the function itself (`fn`), its name, description, and the list of arguments.
|
||||
* The `PromptManager` stores this `Prompt` object in its internal dictionary, keyed by the name `"summarize_text"`.
|
||||
|
||||
**2. Rendering (When a client calls `getPrompt`):**
|
||||
|
||||
* A client sends the MCP `getPrompt` request we saw earlier.
|
||||
* `FastMCP` receives this and calls its internal `get_prompt` handler method (defined in `server/fastmcp/server.py`).
|
||||
* This handler calls `self._prompt_manager.render_prompt("summarize_text", {"text_to_summarize": "..."})`.
|
||||
* Inside `PromptManager.render_prompt`:
|
||||
* It looks up `"summarize_text"` in its dictionary and finds the corresponding `Prompt` object.
|
||||
* It calls the `Prompt` object's `render` method: `prompt.render(arguments={"text_to_summarize": "..."})`.
|
||||
* Inside `Prompt.render` (in `server/fastmcp/prompts/base.py`):
|
||||
* It validates that all required arguments (like `text_to_summarize`) were provided.
|
||||
* It calls the original Python function stored in `prompt.fn`: `create_summary_prompt(text_to_summarize="...")`.
|
||||
* Your function executes and returns the list `[UserMessage(...)]`.
|
||||
* The `render` method takes this result, validates that each item is (or can be converted to) a `Message` object, and ensures the final output is a list of `Message`s.
|
||||
* The `PromptManager` receives this validated list of `Message` objects.
|
||||
* `FastMCP` takes the result, packages it into the standard MCP `GetPromptResult` format (which contains the `messages` list), and sends it back to the client.
|
||||
|
||||
**Simplified Sequence Diagram (`getPrompt` for `summarize_text`):**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant FastMCP_Server as FastMCP (server.py)
|
||||
participant PromptMgr as PromptManager (_prompt_manager)
|
||||
participant SummaryPrompt as Prompt (wraps create_summary_prompt)
|
||||
participant PromptFunc as create_summary_prompt()
|
||||
|
||||
Client->>+FastMCP_Server: Send MCP Request: getPrompt(name="summarize_text", args={"text": "..."})
|
||||
FastMCP_Server->>+PromptMgr: render_prompt(name="summarize_text", args={...})
|
||||
PromptMgr->>PromptMgr: Find Prompt object for "summarize_text"
|
||||
PromptMgr->>+SummaryPrompt: prompt.render(arguments={...})
|
||||
SummaryPrompt->>+PromptFunc: Call create_summary_prompt(text_to_summarize="...")
|
||||
PromptFunc-->>-SummaryPrompt: Return [UserMessage(content="Summarize: ...")]
|
||||
SummaryPrompt->>SummaryPrompt: Validate & format message list
|
||||
SummaryPrompt-->>-PromptMgr: Return validated [UserMessage(...)]
|
||||
PromptMgr-->>-FastMCP_Server: Return [UserMessage(...)]
|
||||
FastMCP_Server->>-Client: Send MCP Response: result={messages: [{...}]}
|
||||
```
|
||||
|
||||
**Looking at the Code (Briefly):**
|
||||
|
||||
You don't need to memorize the internal details, but seeing where things happen can clarify the process:
|
||||
|
||||
* **Registration (`@server.prompt` -> `add_prompt` -> `PromptManager.add_prompt`)**:
|
||||
* `server.py`: `FastMCP.prompt` decorator calls `self.add_prompt`.
|
||||
* `server.py`: `FastMCP.add_prompt` calls `self._prompt_manager.add_prompt`.
|
||||
* `manager.py`: `PromptManager.add_prompt` calls `Prompt.from_function` and stores the result.
|
||||
|
||||
```python
|
||||
# Inside server/fastmcp/prompts/manager.py (Simplified PromptManager.add_prompt)
|
||||
from .base import Prompt
|
||||
|
||||
class PromptManager:
|
||||
# ... (init, get_prompt, list_prompts) ...
|
||||
|
||||
def add_prompt(self, prompt: Prompt) -> Prompt:
|
||||
# Check for duplicates...
|
||||
if prompt.name in self._prompts:
|
||||
# ... handle duplicate ...
|
||||
pass
|
||||
# Store the Prompt object
|
||||
self._prompts[prompt.name] = prompt
|
||||
return prompt
|
||||
|
||||
# Note: Prompt.from_function (in base.py) does the function inspection.
|
||||
```
|
||||
|
||||
* **Rendering (`FastMCP.get_prompt` -> `PromptManager.render_prompt` -> `Prompt.render`)**:
|
||||
* `server.py`: `FastMCP.get_prompt` handles incoming requests and calls `self._prompt_manager.render_prompt`.
|
||||
|
||||
```python
|
||||
# Inside server/fastmcp/prompts/manager.py (Simplified PromptManager.render_prompt)
|
||||
class PromptManager:
|
||||
# ... (other methods) ...
|
||||
|
||||
async def render_prompt(self, name, arguments=None):
|
||||
# 1. Find the prompt object by name
|
||||
prompt = self.get_prompt(name)
|
||||
if not prompt:
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
|
||||
# 2. Tell the Prompt object to render itself
|
||||
return await prompt.render(arguments)
|
||||
```
|
||||
|
||||
* `base.py`: `Prompt.render` validates arguments and calls the stored function (`self.fn`). It then processes the function's return value into a list of `Message` objects.
|
||||
|
||||
```python
|
||||
# Inside server/fastmcp/prompts/base.py (Simplified Prompt.render)
|
||||
class Prompt:
|
||||
# ... (init, from_function, PromptArgument) ...
|
||||
|
||||
async def render(self, arguments=None):
|
||||
# Validate required arguments...
|
||||
# ...
|
||||
|
||||
try:
|
||||
# Call the original decorated function
|
||||
result = self.fn(**(arguments or {}))
|
||||
if inspect.iscoroutine(result): # Handle async functions
|
||||
result = await result
|
||||
|
||||
# Convert result to list of Message objects
|
||||
# (Handles strings, dicts, Message objects, lists)
|
||||
messages: list[Message] = []
|
||||
# ... (conversion logic using message_validator) ...
|
||||
return messages
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've learned about FastMCP Prompts, a powerful way to manage reusable message templates, especially useful for interacting with language models.
|
||||
|
||||
* **Prompts (`Prompt`)** are like Mad Libs templates for creating sequences of `UserMessage`s and `AssistantMessage`s.
|
||||
* They use **`PromptArgument`**s to define the "blanks" that need filling.
|
||||
* The **`PromptManager`** keeps track of all defined prompts.
|
||||
* The **`@server.prompt()`** decorator provides an easy way to define a prompt template using a Python function. The function's parameters become arguments, and its return value (string, dict, Message object, or list thereof) defines the generated message sequence.
|
||||
* Clients use `listPrompts` to discover templates and `getPrompt` to render a specific template with arguments, receiving the generated messages back.
|
||||
|
||||
Prompts help keep your LLM interaction logic organized, reusable, and separate from your main tool code.
|
||||
|
||||
In the next chapter, we'll explore a concept that ties tools, resources, and potentially prompts together during a request: [Chapter 6: FastMCP Context (`Context`)](06_fastmcp_context___context__.md). This allows your tools and resources to access server capabilities like logging and progress reporting.
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
315
docs/MCP Python SDK/06_fastmcp_context___context__.md
Normal file
315
docs/MCP Python SDK/06_fastmcp_context___context__.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# Chapter 6: Talking Back - FastMCP Context (`Context`)
|
||||
|
||||
In [Chapter 5: Reusable Chat Starters - FastMCP Prompts (`Prompt`, `PromptManager`)](05_fastmcp_prompts___prompt____promptmanager__.md), we learned how to create reusable message templates for interacting with AI models. We've seen how to build servers with data resources ([Chapter 3](03_fastmcp_resources___resource____resourcemanager__.md)) and action tools ([Chapter 4](04_fastmcp_tools___tool____toolmanager__.md)).
|
||||
|
||||
But imagine you have a tool that takes a while to run, like processing a large file or making a complex calculation. How does your tool communicate back to the user *while* it's running? How can it say "I'm 50% done!" or log important steps? Or what if a tool needs to read some data from one of the server's resources to do its job?
|
||||
|
||||
This is where the **`Context`** object comes in. It's like giving your tool function a temporary **backstage pass** for the specific request it's handling. This pass grants it access to special features like sending logs, reporting progress, or accessing other parts of the server environment related to that request.
|
||||
|
||||
## What is `Context`?
|
||||
|
||||
The `Context` object is a special helper object provided by the `FastMCP` framework. If you define a tool function (or a resource function) that includes a parameter specifically typed as `Context`, `FastMCP` will automatically create and pass this object to your function when it's called.
|
||||
|
||||
Think of it this way:
|
||||
* Each client request (like `callTool` or `readResource`) is like a separate event.
|
||||
* For that specific event, `FastMCP` can provide a `Context` object.
|
||||
* This `Context` object holds information about *that specific request* (like its unique ID).
|
||||
* It also provides methods (functions) to interact with the ongoing session, such as:
|
||||
* Sending log messages back to the client (`ctx.info`, `ctx.debug`, etc.).
|
||||
* Reporting progress updates (`ctx.report_progress`).
|
||||
* Reading data from other resources defined on the server (`ctx.read_resource`).
|
||||
|
||||
It's your function's way of communicating out or accessing shared server capabilities during its execution for a particular request.
|
||||
|
||||
## Getting Access: Asking for the `Context`
|
||||
|
||||
How do you tell `FastMCP` that your function needs this backstage pass? You simply add a parameter to your function definition and use a **type hint** to mark it as `Context`.
|
||||
|
||||
Let's create a tool that simulates a long-running task and uses `Context` to report progress and log messages.
|
||||
|
||||
**File: `long_task_server.py`**
|
||||
|
||||
```python
|
||||
import anyio # For simulating delay with sleep
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
# 1. Import the Context type
|
||||
from mcp.server.fastmcp.server import Context
|
||||
|
||||
# Create the server instance
|
||||
server = FastMCP(name="LongTaskServer")
|
||||
|
||||
# Define our tool function
|
||||
# 2. Add a parameter (e.g., 'ctx') and type hint it as 'Context'
|
||||
@server.tool(name="long_task", description="Simulates a task that takes time.")
|
||||
async def run_long_task(duration_seconds: int, ctx: Context) -> str:
|
||||
"""
|
||||
Simulates a task, reporting progress and logging using Context.
|
||||
"""
|
||||
# 3. Use the context object!
|
||||
await ctx.info(f"Starting long task for {duration_seconds} seconds.")
|
||||
|
||||
total_steps = 5
|
||||
for i in range(total_steps):
|
||||
step = i + 1
|
||||
await ctx.debug(f"Working on step {step}/{total_steps}...")
|
||||
# Simulate work
|
||||
await anyio.sleep(duration_seconds / total_steps)
|
||||
# Report progress (current step, total steps)
|
||||
await ctx.report_progress(step, total_steps)
|
||||
|
||||
await ctx.info("Long task completed!")
|
||||
return f"Finished simulated task of {duration_seconds} seconds."
|
||||
|
||||
# Standard run block
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run()
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
1. **`from mcp.server.fastmcp.server import Context`**: We import the necessary `Context` class.
|
||||
2. **`async def run_long_task(duration_seconds: int, ctx: Context)`**:
|
||||
* We define our tool function as usual.
|
||||
* Crucially, we add a parameter named `ctx`. You can name it anything (like `context`, `req_ctx`), but `ctx` is common.
|
||||
* We add the type hint `: Context` after the parameter name. This is the signal to `FastMCP` to inject the context object here.
|
||||
3. **Using `ctx`**: Inside the function, we can now use the methods provided by the `ctx` object:
|
||||
* `await ctx.info(...)`: Sends an informational log message back to the client connected to this session.
|
||||
* `await ctx.debug(...)`: Sends a debug-level log message. There are also `warning` and `error` methods.
|
||||
* `await ctx.report_progress(step, total_steps)`: Sends a progress update to the client. The client application might display this in a progress bar.
|
||||
|
||||
When a client calls the `long_task` tool, `FastMCP` will:
|
||||
1. See the `ctx: Context` parameter.
|
||||
2. Create a `Context` object specific to this request.
|
||||
3. Call your `run_long_task` function, passing the duration and the newly created `ctx` object.
|
||||
4. Your function runs, and calls like `ctx.info` or `ctx.report_progress` send messages back to the client *during* the execution of the tool.
|
||||
|
||||
## Using `Context` to Access Resources
|
||||
|
||||
The `Context` object isn't just for sending information *out*; it can also be used to access other parts of the server, like reading resources defined using `@server.resource`.
|
||||
|
||||
Let's modify our example. Imagine our long task needs some configuration data stored in a resource.
|
||||
|
||||
**File: `long_task_server_with_resource.py`**
|
||||
|
||||
```python
|
||||
import anyio
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.fastmcp.server import Context
|
||||
|
||||
# Create the server instance
|
||||
server = FastMCP(name="LongTaskServer")
|
||||
|
||||
# Define a simple resource that holds some config data
|
||||
@server.resource(uri="config://task_settings", description="Settings for the long task.")
|
||||
def get_task_settings() -> str:
|
||||
"""Returns task settings as a simple string."""
|
||||
# In a real app, this might load from a file or database
|
||||
print("Resource 'config://task_settings' was read!")
|
||||
return "Default speed: Normal" # Simple example setting
|
||||
|
||||
# Define our tool function
|
||||
@server.tool(name="long_task", description="Simulates a task using config resource.")
|
||||
async def run_long_task(duration_seconds: int, ctx: Context) -> str:
|
||||
"""
|
||||
Simulates a task, reads config via Context, reports progress.
|
||||
"""
|
||||
await ctx.info(f"Starting long task for {duration_seconds} seconds.")
|
||||
|
||||
# 1. Use context to read the resource
|
||||
try:
|
||||
# read_resource returns a list of content chunks
|
||||
resource_contents = await ctx.read_resource("config://task_settings")
|
||||
# Assuming simple text content for this example
|
||||
settings = ""
|
||||
for content_part in resource_contents:
|
||||
if hasattr(content_part, 'content') and isinstance(content_part.content, str):
|
||||
settings = content_part.content
|
||||
break
|
||||
await ctx.info(f"Loaded settings: {settings}")
|
||||
except Exception as e:
|
||||
await ctx.warning(f"Could not read task settings: {e}")
|
||||
|
||||
|
||||
total_steps = 5
|
||||
for i in range(total_steps):
|
||||
step = i + 1
|
||||
await ctx.debug(f"Working on step {step}/{total_steps}...")
|
||||
await anyio.sleep(duration_seconds / total_steps)
|
||||
await ctx.report_progress(step, total_steps)
|
||||
|
||||
await ctx.info("Long task completed!")
|
||||
return f"Finished simulated task of {duration_seconds} seconds using settings."
|
||||
|
||||
# Standard run block
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting {server.name}...")
|
||||
server.run()
|
||||
print(f"{server.name} finished.")
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
1. **`@server.resource(...)`**: We added a simple resource named `config://task_settings` that just returns a string.
|
||||
2. **`resource_contents = await ctx.read_resource("config://task_settings")`**: Inside our `run_long_task` tool, we now use `ctx.read_resource()` to fetch the content of our configuration resource. This allows the tool to dynamically access data managed by the server without having direct access to the resource's implementation function (`get_task_settings`).
|
||||
3. **Processing Content**: The `read_resource` method returns an iterable of `ReadResourceContents` objects (often just one). We extract the string content to use it.
|
||||
|
||||
Now, our tool can both communicate outwards (logs, progress) and interact inwards (read resources) using the same `Context` object, all within the scope of the single request it's handling.
|
||||
|
||||
## How `Context` Works Under the Hood
|
||||
|
||||
It feels like magic that just adding `: Context` gives your function these powers, but it's a well-defined process within `FastMCP`.
|
||||
|
||||
1. **Request Arrives:** A client sends a request, for example, `callTool` for our `long_task`.
|
||||
2. **Low-Level Handling:** The underlying `MCPServer` receives the request and creates a `RequestContext` object. This low-level context holds the raw request details, a reference to the current `ServerSession`, and the request ID.
|
||||
3. **`FastMCP` Takes Over:** The request is routed to the appropriate `FastMCP` handler method (e.g., `FastMCP.call_tool`).
|
||||
4. **Context Creation:** Before calling the actual tool function, `FastMCP` calls its internal `get_context()` method. This method creates the high-level `Context` object we use. It wraps the low-level `RequestContext` and also adds a reference to the `FastMCP` server instance itself.
|
||||
5. **Function Inspection:** The `ToolManager` (when asked to run the tool) inspects the signature of your target function (`run_long_task`). It sees the `ctx: Context` parameter.
|
||||
6. **Injection:** The `ToolManager` (specifically the `Tool.run` method which uses `FuncMetadata.call_fn_with_arg_validation`) knows it needs to provide a `Context` object. It takes the `Context` created in step 4 and passes it as the argument for the `ctx` parameter when calling your `run_long_task` function.
|
||||
7. **Execution:** Your function runs. When you call `ctx.info("...")`, the `Context` object uses its reference to the underlying `RequestContext` and `ServerSession` to send the appropriate log message back to the client via the session. Similarly, `ctx.report_progress` uses the session, and `ctx.read_resource` uses the reference to the `FastMCP` instance to call its `read_resource` method.
|
||||
|
||||
**Simplified Sequence Diagram (`callTool` with `Context`):**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant FastMCPServer as FastMCP (server.py)
|
||||
participant ToolMgr as ToolManager (_tool_manager)
|
||||
participant ToolRunner as Tool.run / FuncMetadata
|
||||
participant YourToolFunc as run_long_task(ctx: Context)
|
||||
participant ContextObj as Context
|
||||
|
||||
Client->>+FastMCPServer: callTool(name="long_task", args={...})
|
||||
FastMCPServer->>FastMCPServer: Create low-level RequestContext
|
||||
FastMCPServer->>+ContextObj: Create Context (wraps RequestContext, FastMCP)
|
||||
FastMCPServer->>+ToolMgr: call_tool(name="long_task", args={...})
|
||||
ToolMgr->>+ToolRunner: run(arguments={...}, context=ContextObj)
|
||||
ToolRunner->>ToolRunner: Inspect run_long_task, see 'ctx: Context'
|
||||
ToolRunner->>+YourToolFunc: Call run_long_task(duration=..., ctx=ContextObj)
|
||||
YourToolFunc->>ContextObj: ctx.info("Starting...")
|
||||
ContextObj->>FastMCPServer: Use session.send_log_message(...)
|
||||
YourToolFunc->>ContextObj: ctx.report_progress(...)
|
||||
ContextObj->>FastMCPServer: Use session.send_progress_notification(...)
|
||||
YourToolFunc->>ContextObj: ctx.read_resource("config://...")
|
||||
ContextObj->>FastMCPServer: Call fastmcp.read_resource("config://...")
|
||||
FastMCPServer-->>ContextObj: Return resource content
|
||||
ContextObj-->>YourToolFunc: Return resource content
|
||||
YourToolFunc-->>-ToolRunner: Return "Finished..."
|
||||
ToolRunner-->>-ToolMgr: Return "Finished..."
|
||||
ToolMgr-->>-FastMCPServer: Return "Finished..."
|
||||
FastMCPServer->>-Client: Send Response: result="Finished..."
|
||||
```
|
||||
|
||||
**Looking at the Code (Briefly):**
|
||||
|
||||
* **Context Creation (`server/fastmcp/server.py`)**: The `FastMCP.get_context` method is responsible for creating the `Context` object when needed, typically just before calling a tool or resource handler. It grabs the low-level context and wraps it.
|
||||
|
||||
```python
|
||||
# Inside server/fastmcp/server.py (Simplified FastMCP.get_context)
|
||||
from mcp.shared.context import RequestContext # Low-level context
|
||||
|
||||
class FastMCP:
|
||||
# ... (other methods) ...
|
||||
|
||||
def get_context(self) -> Context[ServerSession, object]:
|
||||
"""Returns a Context object."""
|
||||
try:
|
||||
# Get the low-level context for the current request
|
||||
request_context: RequestContext | None = self._mcp_server.request_context
|
||||
except LookupError:
|
||||
request_context = None # Not available outside a request
|
||||
|
||||
# Create our high-level Context, passing the low-level one
|
||||
# and a reference to this FastMCP instance ('self')
|
||||
return Context(request_context=request_context, fastmcp=self)
|
||||
```
|
||||
|
||||
* **Context Injection (`server/fastmcp/tools/base.py`)**: The `Tool.from_function` method inspects the function signature to see if a `Context` parameter exists and stores its name (`context_kwarg`). Later, `Tool.run` uses this information (via `FuncMetadata`) to pass the context object when calling your function.
|
||||
|
||||
```python
|
||||
# Inside server/fastmcp/tools/base.py (Simplified Tool.from_function)
|
||||
class Tool(BaseModel):
|
||||
# ... fields ...
|
||||
context_kwarg: str | None = Field(...)
|
||||
|
||||
@classmethod
|
||||
def from_function(cls, fn, ...) -> Tool:
|
||||
# ... other inspection ...
|
||||
context_param_name = None
|
||||
sig = inspect.signature(fn)
|
||||
for param_name, param in sig.parameters.items():
|
||||
# Check if the type hint is Context
|
||||
if param.annotation is Context:
|
||||
context_param_name = param_name
|
||||
break
|
||||
# ... create FuncMetadata, skipping context arg ...
|
||||
return cls(
|
||||
# ...,
|
||||
context_kwarg=context_param_name,
|
||||
# ...
|
||||
)
|
||||
|
||||
# Inside Tool.run (simplified concept)
|
||||
async def run(self, arguments, context=None):
|
||||
# ... validate args ...
|
||||
kwargs_for_fn = validated_args
|
||||
if self.context_kwarg and context:
|
||||
# Add the context object to the arguments passed to the function
|
||||
kwargs_for_fn[self.context_kwarg] = context
|
||||
|
||||
# Call the original function (self.fn)
|
||||
result = await self.fn(**kwargs_for_fn) # Or sync call
|
||||
return result
|
||||
```
|
||||
|
||||
* **Context Implementation (`server/fastmcp/server.py`)**: The `Context` class itself implements methods like `info`, `report_progress`, `read_resource` by calling methods on the stored `_request_context.session` or `_fastmcp` instance.
|
||||
|
||||
```python
|
||||
# Inside server/fastmcp/server.py (Simplified Context methods)
|
||||
class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
|
||||
_request_context: RequestContext[...] | None
|
||||
_fastmcp: FastMCP | None
|
||||
# ... (init, properties) ...
|
||||
|
||||
async def report_progress(self, progress, total=None):
|
||||
# Get progress token from low-level context meta if available
|
||||
progress_token = self.request_context.meta.progressToken if self.request_context.meta else None
|
||||
if progress_token:
|
||||
# Use the session object from the low-level context
|
||||
await self.request_context.session.send_progress_notification(...)
|
||||
|
||||
async def read_resource(self, uri):
|
||||
# Use the stored FastMCP instance
|
||||
assert self._fastmcp is not None
|
||||
return await self._fastmcp.read_resource(uri)
|
||||
|
||||
async def log(self, level, message, ...):
|
||||
# Use the session object from the low-level context
|
||||
await self.request_context.session.send_log_message(...)
|
||||
|
||||
async def info(self, message, **extra):
|
||||
await self.log("info", message, **extra)
|
||||
# ... (debug, warning, error methods) ...
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've learned about the `Context` object in `FastMCP` – your function's essential backstage pass during a request.
|
||||
|
||||
* `Context` provides access to request-specific information and server capabilities.
|
||||
* You gain access by adding a parameter type-hinted as `Context` to your tool or resource function definition.
|
||||
* It allows your functions to:
|
||||
* Send log messages (`ctx.info`, `ctx.debug`, etc.).
|
||||
* Report progress (`ctx.report_progress`).
|
||||
* Read server resources (`ctx.read_resource`).
|
||||
* Access request details (`ctx.request_id`).
|
||||
* `FastMCP` automatically creates and injects the `Context` object when your function is called for a specific request.
|
||||
|
||||
The `Context` object is key to building more interactive and communicative tools and resources that can provide feedback to the user and interact with their environment during execution.
|
||||
|
||||
So far, we've focused on the high-level abstractions `FastMCP` provides (`Tool`, `Resource`, `Prompt`, `Context`). In the next chapter, we'll take a step back and look at the fundamental data structures defined by the MCP specification itself: [Chapter 7: MCP Protocol Types](07_mcp_protocol_types.md). Understanding these types helps clarify the data being exchanged between clients and servers under the hood.
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
258
docs/MCP Python SDK/07_mcp_protocol_types.md
Normal file
258
docs/MCP Python SDK/07_mcp_protocol_types.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Chapter 7: MCP Protocol Types - The Standard Language
|
||||
|
||||
In the previous chapter, [Chapter 6: Talking Back - FastMCP Context (`Context`)](06_fastmcp_context___context__.md), we saw how the `Context` object gives our tools and resources a "backstage pass" to send logs, report progress, and access other server features during a request. We've built up a good understanding of how `FastMCP` helps us create powerful servers with tools ([Chapter 4](04_fastmcp_tools___tool____toolmanager__.md)), resources ([Chapter 3](03_fastmcp_resources___resource____resourcemanager__.md)), and prompts ([Chapter 5](05_fastmcp_prompts___prompt____promptmanager__.md)).
|
||||
|
||||
But have you ever wondered *how* the client and server actually talk to each other under the hood? When your tool function uses `ctx.report_progress()`, how does that message get sent? When a client asks to call a tool, what does that request *look like* electronically?
|
||||
|
||||
Imagine trying to send mail internationally. If everyone used different envelope sizes, address formats, and languages, it would be chaos! Postal services rely on standards. Similarly, for a client (like a chatbot interface) and your MCP server (like your `CalculatorServer`) to communicate reliably, they need a **standard language** and **standard formats** for their messages.
|
||||
|
||||
This is where **MCP Protocol Types** come in. They are the fundamental, standardized data structures – the "digital forms" or "letter formats" – defined by the Model Context Protocol (MCP) specification itself.
|
||||
|
||||
## What are MCP Protocol Types?
|
||||
|
||||
Think of MCP Protocol Types as the official **blueprints** for all the different kinds of messages that can be sent between an MCP client and server. They define precisely what information should be included in each type of message.
|
||||
|
||||
These types cover all the interactions we've implicitly seen:
|
||||
|
||||
* **Requests:** Messages asking the other side to do something (e.g., "Initialize our connection", "List the available tools", "Read this resource", "Call that tool").
|
||||
* **Responses:** Messages sent back after a request, containing either the result or an error (e.g., "Here are the tools", "Here is the resource content", "Here is the result of the tool call", "Sorry, an error occurred").
|
||||
* **Notifications:** Messages sent one-way, just to inform the other side about something without expecting a direct reply (e.g., "Initialization is complete", "Here's a progress update", "Here's a log message").
|
||||
* **Errors:** A specific kind of response indicating something went wrong with a request.
|
||||
|
||||
These types have specific names defined in the `MCP Python SDK`, usually found in the `mcp.types` module. You'll see names that clearly indicate their purpose:
|
||||
|
||||
* `InitializeRequest`: The "form" a client sends to start communication.
|
||||
* `InitializeResult`: The "form" a server sends back confirming initialization.
|
||||
* `ListToolsResult`: The "form" containing the list of tools sent by the server.
|
||||
* `CallToolRequest`: The "form" a client uses to ask the server to run a tool.
|
||||
* `CallToolResult`: The "form" the server sends back with the tool's output.
|
||||
* `ProgressNotification`: The "form" used to send progress updates (like when we used `ctx.report_progress`).
|
||||
* `JSONRPCError`: The standard "form" for reporting errors.
|
||||
|
||||
These are just a few examples; the MCP specification defines many such types to cover all standard interactions.
|
||||
|
||||
## Why Standardized Types? Meet Pydantic
|
||||
|
||||
Why go to the trouble of defining all these specific types? Why not just send messages like "Hey server, run the add tool with 5 and 7"?
|
||||
|
||||
Without standards, communication quickly breaks down:
|
||||
* Did the client send integers or strings for the numbers?
|
||||
* Did the server send the result back as a number or text?
|
||||
* How does the client know if the server understood the request or if an error happened?
|
||||
|
||||
Standardized types solve these problems by ensuring both the client and server agree on the exact structure and data types for every message.
|
||||
|
||||
The `MCP Python SDK` uses a popular Python library called **Pydantic** to define and manage these protocol types. Think of Pydantic as both the **form designer** and the **quality control inspector**:
|
||||
|
||||
1. **Definition:** Pydantic allows the SDK developers to define each protocol type (like `CallToolRequest`) using simple Python classes with type hints. This creates a clear, code-based blueprint for each "form".
|
||||
2. **Validation:** When your server receives a message, Pydantic automatically checks if it perfectly matches the expected structure defined by the corresponding protocol type. Does the `CallToolRequest` actually have a `name` field that's a string? Does it have an `arguments` field that's a dictionary? If not, Pydantic raises an error immediately, preventing bad data from causing problems later. It does the same when your server sends messages back.
|
||||
3. **Type Safety & Developer Experience:** Because the types are clearly defined, your code editor can help you! It knows what fields exist on an `InitializeResult` object, reducing typos and making development faster and less error-prone.
|
||||
|
||||
Pydantic makes the communication reliable and robust by enforcing the MCP standard for every message.
|
||||
|
||||
## Examples in Action: Connecting High-Level to Low-Level
|
||||
|
||||
While `FastMCP` does a great job hiding these low-level details, let's peek behind the curtain and see how our previous examples relate to these protocol types.
|
||||
|
||||
**Scenario 1: Client Listing Tools**
|
||||
|
||||
1. A client wants to know what tools your `CalculatorServer` offers.
|
||||
2. Client sends a message. Under the hood, this message is structured according to the `JSONRPCRequest` format, specifying the method `tools/list`.
|
||||
3. Your `FastMCP` server receives this raw message. Pydantic validates it.
|
||||
4. `FastMCP` understands it's a request for `tools/list` and asks the `ToolManager` ([Chapter 4](04_fastmcp_tools___tool____toolmanager__.md)) for the list of tools.
|
||||
5. The `ToolManager` provides the tool information (name, description, input schema).
|
||||
6. `FastMCP` takes this information and constructs a `ListToolsResult` object. This object is a Pydantic model defined in `mcp.types`.
|
||||
|
||||
```python
|
||||
# Simplified example of creating a ListToolsResult object
|
||||
# (FastMCP does this automatically for you!)
|
||||
from mcp.types import ListToolsResult, Tool
|
||||
|
||||
# ToolManager gathered this info from your @server.tool decorator
|
||||
add_tool_info = Tool(
|
||||
name="add",
|
||||
description="Adds two numbers together.",
|
||||
inputSchema={ # JSON Schema describing expected input
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"num1": {"type": "integer"},
|
||||
"num2": {"type": "integer"}
|
||||
},
|
||||
"required": ["num1", "num2"]
|
||||
}
|
||||
)
|
||||
|
||||
# FastMCP creates the result object
|
||||
result_data = ListToolsResult(
|
||||
tools=[add_tool_info]
|
||||
# nextCursor would be set if paginating
|
||||
)
|
||||
|
||||
# This result_data object is then packaged into a
|
||||
# standard JSONRPCResponse and sent to the client.
|
||||
print(result_data.model_dump_json(indent=2)) # See its JSON form
|
||||
```
|
||||
|
||||
**Example Output (JSON representation):**
|
||||
```json
|
||||
{
|
||||
"_meta": null,
|
||||
"nextCursor": null,
|
||||
"tools": [
|
||||
{
|
||||
"name": "add",
|
||||
"description": "Adds two numbers together.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"num1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"num2": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"num1",
|
||||
"num2"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
This structured JSON, based on the `ListToolsResult` model, is what gets sent back to the client.
|
||||
|
||||
**Scenario 2: Reporting Progress with `Context`**
|
||||
|
||||
1. Your tool function calls `await ctx.report_progress(step, total_steps)` ([Chapter 6](06_fastmcp_context___context__.md)).
|
||||
2. The `Context` object uses the provided `step` and `total_steps` values.
|
||||
3. It looks up the unique `progressToken` associated with the original request that started this tool call.
|
||||
4. It creates a `ProgressNotificationParams` object containing the token and progress values.
|
||||
5. It wraps this in a `ProgressNotification` object.
|
||||
|
||||
```python
|
||||
# Simplified example of creating a ProgressNotification
|
||||
# (Context object does this for you!)
|
||||
from mcp.types import ProgressNotification, ProgressNotificationParams
|
||||
|
||||
# Context gets these values
|
||||
token_from_request = "client_progress_token_123"
|
||||
current_step = 2
|
||||
total_steps = 5
|
||||
progress_value = current_step / total_steps # 0.4
|
||||
|
||||
# Context creates the notification object
|
||||
notification_data = ProgressNotification(
|
||||
method="notifications/progress", # Standard MCP method name
|
||||
params=ProgressNotificationParams(
|
||||
progressToken=token_from_request,
|
||||
progress=progress_value,
|
||||
total=float(total_steps)
|
||||
)
|
||||
)
|
||||
|
||||
# This notification_data is then packaged into a
|
||||
# JSONRPCNotification message and sent to the client.
|
||||
print(notification_data.model_dump_json(indent=2))
|
||||
```
|
||||
|
||||
**Example Output (JSON representation):**
|
||||
```json
|
||||
{
|
||||
"method": "notifications/progress",
|
||||
"params": {
|
||||
"_meta": null,
|
||||
"progressToken": "client_progress_token_123",
|
||||
"progress": 0.4,
|
||||
"total": 5.0
|
||||
}
|
||||
}
|
||||
```
|
||||
This structured JSON notification, based on the `ProgressNotification` model, is sent to the client to update its UI.
|
||||
|
||||
## Do I Need to Use These Directly?
|
||||
|
||||
Probably not, especially when you're starting out and using `FastMCP`!
|
||||
|
||||
The beauty of `FastMCP` and its decorators (`@server.tool`, `@server.resource`) and helpers (`Context`) is that they **abstract away** these low-level protocol types. You work with regular Python functions, arguments, and return values, and `FastMCP` handles the conversion to and from the appropriate MCP Protocol Types automatically using Pydantic.
|
||||
|
||||
However, understanding that these types exist is valuable:
|
||||
|
||||
* **Debugging:** If you encounter communication errors, the error messages might refer to fields within these specific types (e.g., "Invalid params in CallToolRequest"). Knowing the structure helps diagnose the problem.
|
||||
* **Advanced Use:** If you ever need to build a custom MCP client, or interact with an MCP server without using the `MCP Python SDK`'s client helpers, you'll need to construct and parse these types yourself.
|
||||
* **Understanding the Protocol:** Reading the official MCP specification or the SDK's `mcp/types.py` file gives you the ground truth about how communication works.
|
||||
|
||||
Think of it like driving a car. You mostly use the steering wheel, pedals, and shifter (like `FastMCP` abstractions). You don't usually interact directly with the engine pistons or fuel injectors (like MCP Protocol Types). But knowing they exist helps you understand how the car works and what might be wrong if it breaks down.
|
||||
|
||||
## Under the Hood: Messages in Transit
|
||||
|
||||
Let's visualize where these types fit into a simple `callTool` interaction.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ClientApp as Client Application
|
||||
participant ClientSDK as MCP Client SDK
|
||||
participant ServerSDK as MCP Server SDK (FastMCP)
|
||||
participant YourTool as Your @server.tool Function
|
||||
|
||||
ClientApp->>+ClientSDK: Request tool "add" with {num1: 5, num2: 7}
|
||||
ClientSDK->>ClientSDK: Create CallToolRequest object (Pydantic model)
|
||||
ClientSDK->>+ServerSDK: Send JSON message (based on CallToolRequest)
|
||||
ServerSDK->>ServerSDK: Receive JSON, parse into CallToolRequest object (Pydantic validation)
|
||||
ServerSDK->>+YourTool: Call add_numbers(num1=5, num2=7)
|
||||
YourTool-->>-ServerSDK: Return 12
|
||||
ServerSDK->>ServerSDK: Create CallToolResult object (Pydantic model, content=[TextContent(text="12")])
|
||||
ServerSDK->>-ClientSDK: Send JSON message (based on CallToolResult)
|
||||
ClientSDK->>ClientSDK: Receive JSON, parse into CallToolResult object (Pydantic validation)
|
||||
ClientSDK-->>-ClientApp: Return result "12"
|
||||
```
|
||||
|
||||
This shows that the `CallToolRequest` and `CallToolResult` (which are MCP Protocol Types defined as Pydantic models in `mcp/types.py`) are the actual structures being serialized into JSON messages for transmission and parsed back upon receipt.
|
||||
|
||||
You can find the definitions for all these types within the SDK:
|
||||
|
||||
**Inside `mcp/types.py` (Example Snippet):**
|
||||
|
||||
```python
|
||||
# This file defines all the standard MCP types using Pydantic
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal, Any
|
||||
|
||||
# Define the base for parameters of progress notifications
|
||||
class ProgressNotificationParams(NotificationParams):
|
||||
"""Parameters for progress notifications."""
|
||||
progressToken: ProgressToken # Defined elsewhere as str | int
|
||||
progress: float
|
||||
total: float | None = None
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
# Define the notification itself, using the params above
|
||||
class ProgressNotification(
|
||||
Notification[ProgressNotificationParams, Literal["notifications/progress"]]
|
||||
):
|
||||
"""
|
||||
An out-of-band notification used to inform the receiver of a progress update...
|
||||
"""
|
||||
method: Literal["notifications/progress"]
|
||||
params: ProgressNotificationParams
|
||||
|
||||
# --- Other definitions like Tool, Resource, CallToolRequest etc. ---
|
||||
```
|
||||
This snippet shows how Pydantic `BaseModel` is used with standard Python type hints (`float`, `str | int`, `Literal["..."]`) to define the structure and expected data types for the `ProgressNotification`.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've learned about MCP Protocol Types – the standardized "digital forms" that define the structure of all communication (requests, responses, notifications, errors) between MCP clients and servers.
|
||||
|
||||
* They are defined by the **MCP specification**.
|
||||
* The `MCP Python SDK` uses **Pydantic** models (`mcp/types.py`) to represent these types, providing clear definitions and automatic validation for reliable communication.
|
||||
* Examples include `InitializeRequest`, `ListToolsResult`, `CallToolRequest`, `ProgressNotification`, and `JSONRPCError`.
|
||||
* While **`FastMCP` largely hides these details**, understanding them provides valuable context for debugging and appreciating the underlying communication mechanics.
|
||||
|
||||
These types form the bedrock of MCP communication. Now that we understand the messages themselves, we can look at how connections are managed over time. In the next chapter, we'll explore how the SDK manages the ongoing conversation between a client and server using [Chapter 8: Client/Server Sessions (`ClientSession`, `ServerSession`)](08_client_server_sessions___clientsession____serversession__.md).
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
@@ -0,0 +1,305 @@
|
||||
# Chapter 8: Client/Server Sessions (`ClientSession`, `ServerSession`)
|
||||
|
||||
Welcome back! In [Chapter 7: MCP Protocol Types](07_mcp_protocol_types.md), we learned about the standardized "digital forms" – the Pydantic models – that define the structure of messages exchanged between an MCP client and server. We saw examples like `CallToolRequest` and `ProgressNotification`.
|
||||
|
||||
But knowing the *format* of a letter isn't enough. How does a specific conversation between one client and one server actually happen over time? How does the server know which incoming response belongs to which outgoing request it sent earlier? How is the initial connection "hello" handled?
|
||||
|
||||
Imagine you call a large company's support line. You don't just shout into the void; you get connected to a specific operator who handles *your* call from start to finish. This operator keeps track of your requests, finds the answers, and manages the connection until you hang up.
|
||||
|
||||
In the `MCP Python SDK`, this "phone line operator" role is played by **Session** objects: `ClientSession` and `ServerSession`.
|
||||
|
||||
## What's a Session? The Dedicated Conversation Line
|
||||
|
||||
A **Session** object (`ClientSession` or `ServerSession`) manages the state and lifecycle of a **single, ongoing connection** between one MCP client and one MCP server. Think of it as establishing a dedicated phone line for one specific conversation.
|
||||
|
||||
This "operator" handles several crucial tasks for that single connection:
|
||||
|
||||
1. **Initialization:** Manages the initial "handshake" where the client and server introduce themselves, agree on the protocol version, and share their capabilities (like saying "Hello, I can do X, Y, and Z").
|
||||
2. **Sending & Receiving:** Handles the low-level details of sending outgoing messages (requests, notifications) and receiving incoming messages over the communication channel (like Stdio, WebSockets, etc., which we'll cover in [Chapter 9: Communication Transports](09_communication_transports__stdio__sse__websocket__memory_.md)).
|
||||
3. **Request/Response Matching:** When you send a request, it gets a unique ID. When a response comes back later with that same ID, the Session makes sure it's delivered to the part of the code that's waiting for *that specific* answer. It's like the operator remembering who asked which question.
|
||||
4. **State Management:** Keeps track of whether the connection is initializing, active, or closed.
|
||||
5. **Lifecycle:** Manages the setup and eventual teardown (hang-up) of the connection.
|
||||
|
||||
## Two Sides of the Coin: `ClientSession` vs. `ServerSession`
|
||||
|
||||
Why are there two types of sessions? Because the client and server have different roles in the conversation:
|
||||
|
||||
* **`ClientSession`**: Represents the *client's* end of the connection. It's primarily responsible for:
|
||||
* *Initiating* the connection and the handshake (`initialize` request).
|
||||
* *Sending* requests to the server (like `callTool`, `readResource`, `getPrompt`).
|
||||
* *Receiving* responses and notifications *from* the server.
|
||||
* Handling server-initiated requests (like asking the client to generate text if the client has that capability).
|
||||
|
||||
* **`ServerSession`**: Represents the *server's* end of the connection. It's primarily responsible for:
|
||||
* *Responding* to the client's `initialize` request.
|
||||
* *Receiving* requests *from* the client.
|
||||
* *Sending* responses and notifications *back* to the client (like tool results, resource content, log messages, progress updates).
|
||||
* Handling client-initiated notifications (like `initialized`).
|
||||
|
||||
They use the same underlying mechanisms but have different methods tailored to their role (e.g., `ClientSession` has `call_tool`, `ServerSession` has `send_log_message`).
|
||||
|
||||
## How `FastMCP` Uses `ServerSession` (Behind the Scenes)
|
||||
|
||||
If you're building a server using `FastMCP` (as we did in chapters [2](02_fastmcp_server___fastmcp__.md) through [6](06_fastmcp_context___context__.md)), you generally **don't interact with `ServerSession` directly**.
|
||||
|
||||
When a client connects to your `FastMCP` server:
|
||||
1. The underlying transport layer (e.g., Stdio handler) accepts the connection.
|
||||
2. `FastMCP` (or its underlying `MCPServer`) automatically creates a `ServerSession` object specifically for that new client connection.
|
||||
3. This `ServerSession` handles the initialization handshake with the client.
|
||||
4. When the client sends a request (like `callTool`), the `ServerSession` receives it, identifies it, and passes it to the appropriate `FastMCP` handler (which might involve the `ToolManager`).
|
||||
5. When your tool function uses `ctx.info()` or `ctx.report_progress()` ([Chapter 6: FastMCP Context (`Context`)](06_fastmcp_context___context__.md)), the `Context` object talks to its associated `ServerSession` to actually send the `LoggingMessageNotification` or `ProgressNotification` back to the client.
|
||||
6. The `ServerSession` manages this connection until the client disconnects.
|
||||
|
||||
So, `ServerSession` is the hidden engine powering the communication for each connected client in a `FastMCP` server. You benefit from its work without needing to manage it manually.
|
||||
|
||||
## When Might You Use `ClientSession`?
|
||||
|
||||
You would typically use `ClientSession` if you were writing a standalone Python application that needs to *connect to* and *interact with* an existing MCP server (which might be one you built with `FastMCP` or someone else's).
|
||||
|
||||
**Example Scenario: A Simple Client**
|
||||
|
||||
*(This is conceptual; we won't build a full client here.)*
|
||||
|
||||
Imagine you write a script that needs to ask our `CalculatorServer` ([Chapter 4](04_fastmcp_tools___tool____toolmanager__.md)) to add two numbers.
|
||||
|
||||
```python
|
||||
# --- Conceptual Client Code ---
|
||||
import anyio
|
||||
from mcp.client.session import ClientSession
|
||||
# Assume we have transport streams (read_stream, write_stream)
|
||||
# connected to the CalculatorServer (more in Chapter 9)
|
||||
|
||||
async def run_client():
|
||||
# 1. Create a ClientSession using the transport streams
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
try:
|
||||
# 2. Perform the initialization handshake
|
||||
init_result = await session.initialize()
|
||||
print(f"Connected to: {init_result.serverInfo.name}")
|
||||
|
||||
# 3. Send a 'callTool' request using the session
|
||||
tool_result = await session.call_tool(
|
||||
name="add",
|
||||
arguments={"num1": 15, "num2": 27}
|
||||
)
|
||||
|
||||
# 4. Process the result (session handled matching response)
|
||||
# Assuming the result is simple text content
|
||||
if tool_result.content and tool_result.content[0].type == 'text':
|
||||
print(f"Server calculated: {tool_result.content[0].text}") # Expected: 42
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
# In a real script, you'd set up the transport and run this async function
|
||||
# anyio.run(run_client)
|
||||
```
|
||||
|
||||
In this scenario:
|
||||
1. We create the `ClientSession`.
|
||||
2. We explicitly call `session.initialize()` to start the conversation.
|
||||
3. We use `session.call_tool()` to send the request. The `ClientSession` assigns an ID, sends the message, and waits for the specific response with that ID.
|
||||
4. The result comes back directly from the `call_tool` method.
|
||||
|
||||
## How Sessions Work Under the Hood: The Operator's Workflow
|
||||
|
||||
Let's trace the lifecycle and the request/response matching managed by a session. We'll use our phone operator analogy.
|
||||
|
||||
1. **Connection Established:** A communication channel (like Stdio or WebSocket, see [Chapter 9](09_communication_transports__stdio__sse__websocket__memory_.md)) is opened between the client and server.
|
||||
2. **Session Creation:** A `ClientSession` is created on the client side, and a `ServerSession` on the server side, both linked to this channel.
|
||||
3. **Initialization (Handshake):**
|
||||
* `ClientSession` sends an `InitializeRequest` (like calling and saying "Hi, I'm ClientApp v1.0, I support MCP v0.3, can we talk?"). It assigns this request ID 0.
|
||||
* `ServerSession` receives ID 0. It knows this is the `initialize` method. It checks the protocol version, stores the client's capabilities, and prepares its own info.
|
||||
* `ServerSession` sends back an `InitializeResult` linked to ID 0 (like "Yes, I'm CalculatorServer v1.1, I also support v0.3, here are my capabilities...").
|
||||
* `ClientSession` receives the response for ID 0. It checks the server's info and considers the handshake successful.
|
||||
* `ClientSession` sends an `InitializedNotification` (just saying "Okay, great!").
|
||||
* `ServerSession` receives this notification and marks the session as fully initialized. The line is now open for regular business.
|
||||
4. **Client Sends Request:**
|
||||
* `ClientSession` wants to call the `add` tool. It calls `session.call_tool("add", {...})`.
|
||||
* The `ClientSession` assigns a *new* unique ID (e.g., ID 1) to this request.
|
||||
* It stores a "waiting placeholder" (an `anyio` event or future) associated with ID 1.
|
||||
* It sends the `CallToolRequest` message with ID 1 over the channel.
|
||||
5. **Server Processes Request:**
|
||||
* `ServerSession` receives the message with ID 1.
|
||||
* It sees it's a `callTool` request for `add`.
|
||||
* It passes the request details to the `FastMCP` handler (which uses the `ToolManager`).
|
||||
* The tool function `add_numbers(15, 27)` runs and returns `42`.
|
||||
* `FastMCP` gets the result.
|
||||
6. **Server Sends Response:**
|
||||
* `ServerSession` constructs a `CallToolResult` containing `42`.
|
||||
* It sends this result back over the channel, making sure to include the *original* request ID (ID 1).
|
||||
7. **Client Receives Response:**
|
||||
* `ClientSession` receives the message with ID 1.
|
||||
* It looks up ID 1 in its "waiting placeholders".
|
||||
* It finds the placeholder created in step 4 and delivers the received `CallToolResult` to it.
|
||||
* The code that was waiting on `session.call_tool(...)` now receives the result (`42`) and continues execution.
|
||||
8. **Notifications (Example: Progress):**
|
||||
* If the server tool called `ctx.report_progress(...)`, the `Context` tells the `ServerSession`.
|
||||
* `ServerSession` constructs a `ProgressNotification` (which doesn't have a request ID, as it's not a response).
|
||||
* `ServerSession` sends the notification.
|
||||
* `ClientSession` receives the notification. It sees it's not a response to a specific request. It might trigger a callback or event handler registered in the client application to update a progress bar.
|
||||
9. **Hang-up:** When the connection closes (client exits, server shuts down, network error), the sessions clean up their resources.
|
||||
|
||||
**Simplified Sequence Diagram (Client Calls Tool):**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ClientApp
|
||||
participant ClientSess as ClientSession
|
||||
participant ServerSess as ServerSession
|
||||
participant ServerTool as Tool Function (e.g., add_numbers)
|
||||
|
||||
ClientApp->>+ClientSess: call_tool("add", {num1: 15, num2: 27})
|
||||
ClientSess->>ClientSess: Assign Request ID (e.g., 1)
|
||||
ClientSess->>ClientSess: Store 'waiter' for ID 1
|
||||
ClientSess->>+ServerSess: Send CallToolRequest (ID=1, method="tools/call", params={...})
|
||||
ServerSess->>ServerSess: Receive request ID=1
|
||||
ServerSess->>+ServerTool: Dispatch request to tool handler
|
||||
ServerTool-->>-ServerSess: Return result (e.g., 42)
|
||||
ServerSess->>-ClientSess: Send CallToolResult (ID=1, result={content: [{"type": "text", "text": "42"}]})
|
||||
ClientSess->>ClientSess: Receive response ID=1
|
||||
ClientSess->>ClientSess: Match ID=1 to 'waiter'
|
||||
ClientSess-->>-ClientApp: Return result (CallToolResult object)
|
||||
```
|
||||
|
||||
This flow highlights how the session objects act as intermediaries, managing IDs and matching responses back to their original requests.
|
||||
|
||||
## Diving into the Code (Briefly!)
|
||||
|
||||
You typically won't call these methods directly when using `FastMCP` for servers, but seeing the structure helps understand the session's role. These snippets are heavily simplified.
|
||||
|
||||
**Base Class (`shared/session.py`):**
|
||||
|
||||
Both `ClientSession` and `ServerSession` inherit from `BaseSession`, which contains the core logic for sending/receiving and request/response matching.
|
||||
|
||||
```python
|
||||
# Simplified from shared/session.py
|
||||
import anyio
|
||||
from mcp.types import JSONRPCRequest, JSONRPCResponse, JSONRPCError, ErrorData
|
||||
|
||||
class BaseSession:
|
||||
def __init__(self, read_stream, write_stream, ...):
|
||||
self._read_stream = read_stream
|
||||
self._write_stream = write_stream
|
||||
self._response_streams = {} # Stores 'waiters' for responses, keyed by request ID
|
||||
self._request_id_counter = 0
|
||||
# ... other setup ...
|
||||
|
||||
async def send_request(self, request, result_type):
|
||||
# 1. Get a new unique ID
|
||||
request_id = self._request_id_counter
|
||||
self._request_id_counter += 1
|
||||
|
||||
# 2. Create a 'waiter' (memory stream) to receive the response
|
||||
response_receiver, response_sender = anyio.create_memory_object_stream(1)
|
||||
self._response_streams[request_id] = response_sender
|
||||
|
||||
# 3. Format the request with the ID
|
||||
jsonrpc_request = JSONRPCRequest(id=request_id, **request.model_dump())
|
||||
|
||||
# 4. Send it over the write stream
|
||||
await self._write_stream.send(JSONRPCMessage(jsonrpc_request))
|
||||
|
||||
# 5. Wait for the response to arrive on the 'waiter' stream
|
||||
response_or_error = await response_receiver.receive() # Timeout logic omitted
|
||||
|
||||
# 6. Process response/error and return result
|
||||
if isinstance(response_or_error, JSONRPCError):
|
||||
raise McpError(response_or_error.error)
|
||||
else:
|
||||
return result_type.model_validate(response_or_error.result)
|
||||
|
||||
async def _receive_loop(self):
|
||||
# Runs in the background, reading from the read_stream
|
||||
async for message in self._read_stream:
|
||||
if isinstance(message.root, (JSONRPCResponse, JSONRPCError)):
|
||||
# It's a response or error for a request we sent
|
||||
request_id = message.root.id
|
||||
# Find the matching 'waiter' stream
|
||||
response_sender = self._response_streams.pop(request_id, None)
|
||||
if response_sender:
|
||||
# Send the response back to the waiting send_request call
|
||||
await response_sender.send(message.root)
|
||||
else:
|
||||
print(f"Warning: Received response for unknown request ID {request_id}")
|
||||
elif isinstance(message.root, JSONRPCRequest):
|
||||
# It's a new request *from* the other side
|
||||
# Subclasses (Client/ServerSession) handle this differently
|
||||
await self._handle_incoming_request(message.root)
|
||||
elif isinstance(message.root, JSONRPCNotification):
|
||||
# It's a notification *from* the other side
|
||||
await self._handle_incoming_notification(message.root)
|
||||
```
|
||||
|
||||
This shows the core `send_request` logic (assign ID, store waiter, send, wait) and the `_receive_loop` logic (read message, if response -> find waiter, if request/notification -> handle).
|
||||
|
||||
**Server Session (`server/session.py`):**
|
||||
|
||||
Adds server-specific logic, like handling the `initialize` request and sending server-to-client notifications.
|
||||
|
||||
```python
|
||||
# Simplified from server/session.py
|
||||
from mcp.types import InitializeRequest, InitializeResult, InitializedNotification
|
||||
|
||||
class ServerSession(BaseSession):
|
||||
# ... (init with server info, capabilities) ...
|
||||
_initialization_state = InitializationState.NotInitialized
|
||||
_client_params = None # Stores client info after initialization
|
||||
|
||||
async def _handle_incoming_request(self, request: JSONRPCRequest):
|
||||
# Server specifically handles 'initialize' request first
|
||||
if request.method == "initialize":
|
||||
# ... (validate request, store client capabilities in self._client_params) ...
|
||||
self._initialization_state = InitializationState.Initializing
|
||||
init_result = InitializeResult(...) # Build result with server info
|
||||
# Respond directly using the base class's internal send method
|
||||
await self._send_response(request.id, ServerResult(init_result))
|
||||
elif self._initialization_state == InitializationState.Initialized:
|
||||
# For other requests, pass them to the main server logic
|
||||
# (e.g., to FastMCP's request router) via an internal queue
|
||||
await self._pass_request_to_server_handler(request)
|
||||
else:
|
||||
# Error: Request received before initialization complete
|
||||
error = ErrorData(code=..., message="Server not initialized")
|
||||
await self._send_response(request.id, error)
|
||||
|
||||
async def _handle_incoming_notification(self, notification: JSONRPCNotification):
|
||||
if notification.method == "initialized":
|
||||
self._initialization_state = InitializationState.Initialized
|
||||
print("ServerSession: Client initialization complete.")
|
||||
elif self._initialization_state == InitializationState.Initialized:
|
||||
# Pass other notifications to server logic if needed
|
||||
pass
|
||||
else:
|
||||
# Ignore notifications before initialized, or log warning
|
||||
pass
|
||||
|
||||
async def send_log_message(self, level, data, logger=None):
|
||||
# Helper method to send a specific notification type
|
||||
log_notification = LoggingMessageNotification(...)
|
||||
await self.send_notification(ServerNotification(log_notification))
|
||||
|
||||
# ... other methods like send_progress_notification, send_resource_updated ...
|
||||
```
|
||||
|
||||
This highlights how `ServerSession` intercepts the `initialize` request and the `initialized` notification to manage the connection state before passing other messages to the main server logic.
|
||||
|
||||
## Conclusion
|
||||
|
||||
You've now explored `ClientSession` and `ServerSession`, the dedicated operators managing individual communication lines between MCP clients and servers.
|
||||
|
||||
* A **Session** handles the lifecycle of a single connection.
|
||||
* It manages the **initialization handshake**.
|
||||
* It reliably **sends and receives** messages (requests, responses, notifications).
|
||||
* Crucially, it **matches incoming responses to outgoing requests** using unique IDs.
|
||||
* **`ClientSession`** is used by clients to initiate connections and send requests *to* servers.
|
||||
* **`ServerSession`** is used by servers to handle connections and respond *to* clients.
|
||||
* Frameworks like **`FastMCP` manage `ServerSession` automatically** for you; interaction often happens indirectly via the `Context` object.
|
||||
|
||||
Sessions provide the robust foundation for the request-response patterns and asynchronous notifications that make MCP communication work.
|
||||
|
||||
In the final chapter of this foundational series, we'll look at the different ways these sessions can actually transmit their messages back and forth: the various [Chapter 9: Communication Transports (Stdio, SSE, WebSocket, Memory)](09_communication_transports__stdio__sse__websocket__memory_.md).
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
@@ -0,0 +1,317 @@
|
||||
# Chapter 9: Communication Transports (Stdio, SSE, WebSocket, Memory)
|
||||
|
||||
Welcome to the final chapter of our introductory journey into the `MCP Python SDK`! In [Chapter 8: Client/Server Sessions (`ClientSession`, `ServerSession`)](08_client_server_sessions___clientsession____serversession__.md), we learned how `Session` objects manage the ongoing conversation and state for a single connection between a client and a server, like dedicated phone operators handling a call.
|
||||
|
||||
But how do the messages actually *travel* over that phone line? If the client and server are different programs, possibly on different computers, what's the physical wire or digital equivalent carrying the signals?
|
||||
|
||||
Imagine our standardized MCP messages ([Chapter 7: MCP Protocol Types](07_mcp_protocol_types.md)) are like perfectly formatted letters. We need a delivery service to actually move these letters between the sender and receiver. This is where **Communication Transports** come in.
|
||||
|
||||
## What are Communication Transports? The Delivery Service
|
||||
|
||||
Communication Transports define the **actual mechanisms** used to send the serialized MCP messages (those structured JSON strings) back and forth between the client and server processes.
|
||||
|
||||
Think of them as different **delivery services** you can choose from:
|
||||
|
||||
1. **`stdio` (Standard Input/Output): Postal Mail for Processes**
|
||||
* **Mechanism:** Uses the standard input (`stdin`) and standard output (`stdout`) streams of the processes. One process writes messages (as lines of text) to its `stdout`, and the other reads them from its `stdin`.
|
||||
* **Use Case:** Very common for command-line tools or when one process directly starts another (like when `mcp run` executes your server script). It's simple and works well when the client and server are running on the same machine and have a parent-child relationship.
|
||||
|
||||
2. **`sse` (Server-Sent Events): One-Way Radio Broadcast (Server -> Client)**
|
||||
* **Mechanism:** Uses standard web protocols (HTTP). The client makes an initial HTTP request, and the server keeps the connection open, sending messages (events) *to* the client whenever it wants. Client-to-server communication usually happens via separate HTTP POST requests.
|
||||
* **Use Case:** Good for web applications where the server needs to push updates (like notifications, progress) to the client (a web browser) efficiently.
|
||||
|
||||
3. **`websocket`: Dedicated Two-Way Phone Line (Web)**
|
||||
* **Mechanism:** Uses the WebSocket protocol, which provides a persistent, full-duplex (two-way) communication channel over a single TCP connection, typically initiated via an HTTP handshake.
|
||||
* **Use Case:** Ideal for highly interactive web applications (like chat apps, real-time dashboards, or the MCP Inspector) where both the client and server need to send messages to each other at any time with low latency.
|
||||
|
||||
4. **`memory`: Internal Office Courier**
|
||||
* **Mechanism:** Uses in-memory queues within a *single* Python process. Messages are passed directly between the client and server components without going through external pipes or network connections.
|
||||
* **Use Case:** Primarily used for **testing**. It allows you to run both the client and server parts of your code in the same test script and have them communicate directly, making tests faster and self-contained.
|
||||
|
||||
These transports are the concrete implementations that bridge the gap between the abstract `Session` objects (which manage the *conversation*) and the physical reality of sending bytes (the *delivery*).
|
||||
|
||||
## How Transports are Used (Often Indirectly)
|
||||
|
||||
The good news is that if you're using `FastMCP` ([Chapter 2](02_fastmcp_server___fastmcp__.md)) and the `mcp` command-line tool ([Chapter 1](01_cli___mcp__command_.md)), you often **don't need to worry about explicitly choosing or configuring the transport**. The tools handle it for common scenarios:
|
||||
|
||||
* **`mcp run your_server.py`**: By default, this command uses the **`stdio`** transport. It starts your Python script as a child process and communicates with it using `stdin` and `stdout`.
|
||||
* **`mcp dev your_server.py`**: This command also typically runs your server using **`stdio`**. The *MCP Inspector* web application it launches then connects to your server (potentially via a WebSocket proxy managed by the dev tool) to monitor the `stdio` communication.
|
||||
* **`mcp install ...` (for Claude Desktop)**: This usually configures Claude to launch your server using `uv run ... mcp run your_server.py`, again defaulting to **`stdio`** communication between Claude and your server process.
|
||||
|
||||
So, for many typical development and integration tasks, `stdio` is the default and works behind the scenes.
|
||||
|
||||
## Using Transports Programmatically (A Glimpse)
|
||||
|
||||
While `mcp run` handles `stdio` automatically, what if you wanted to build a *custom* server application that listens over WebSockets? Or write tests using the `memory` transport? The SDK provides tools for this.
|
||||
|
||||
You typically use an `async context manager` provided by the SDK for the specific transport. These managers handle setting up the communication channel and yield a pair of streams (`read_stream`, `write_stream`) that the `ClientSession` or `ServerSession` can use.
|
||||
|
||||
**Conceptual Server using Stdio (like `mcp run`)**
|
||||
|
||||
```python
|
||||
# Conceptual code showing how stdio_server might be used
|
||||
import anyio
|
||||
from mcp.server.stdio import stdio_server # Import the stdio transport
|
||||
from mcp.server.mcp_server import MCPServer # Low-level server
|
||||
|
||||
# Assume 'my_actual_server' is your MCPServer instance
|
||||
my_actual_server = MCPServer(name="MyStdioServer")
|
||||
|
||||
async def main():
|
||||
print("Server: Waiting for client over stdio...")
|
||||
# 1. Use the stdio_server context manager
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
# 2. It yields streams connected to stdin/stdout
|
||||
print("Server: Stdio streams acquired. Running server logic.")
|
||||
# 3. Pass streams to the server's run method
|
||||
await my_actual_server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
my_actual_server.create_initialization_options()
|
||||
)
|
||||
print("Server: Stdio streams closed.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
anyio.run(main)
|
||||
except KeyboardInterrupt:
|
||||
print("Server: Exiting.")
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
The `stdio_server()` context manager handles wrapping the process's standard input and output. It provides the `read_stream` (to get messages *from* stdin) and `write_stream` (to send messages *to* stdout) that the underlying `MCPServer` (and thus `FastMCP`) needs to communicate.
|
||||
|
||||
**Conceptual Server using WebSocket (within a web framework)**
|
||||
|
||||
```python
|
||||
# Conceptual code using Starlette web framework
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import WebSocketRoute
|
||||
from starlette.websockets import WebSocket
|
||||
from mcp.server.websocket import websocket_server # Import WS transport
|
||||
from mcp.server.mcp_server import MCPServer # Low-level server
|
||||
|
||||
my_actual_server = MCPServer(name="MyWebSocketServer")
|
||||
|
||||
# Define the WebSocket endpoint handler
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
# 1. Use the websocket_server context manager
|
||||
async with websocket_server(
|
||||
websocket.scope, websocket.receive, websocket.send
|
||||
) as (read_stream, write_stream):
|
||||
# 2. It yields streams connected to this specific WebSocket
|
||||
print(f"Server: WebSocket client connected. Running server logic.")
|
||||
# 3. Pass streams to the server's run method
|
||||
await my_actual_server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
my_actual_server.create_initialization_options()
|
||||
)
|
||||
print("Server: WebSocket client disconnected.")
|
||||
|
||||
# Set up the web application routes
|
||||
routes = [
|
||||
WebSocketRoute("/mcp", endpoint=websocket_endpoint)
|
||||
]
|
||||
app = Starlette(routes=routes)
|
||||
|
||||
# To run this, you'd use an ASGI server like uvicorn:
|
||||
# uvicorn your_module:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
Here, `websocket_server()` adapts the WebSocket connection provided by the web framework (Starlette) into the `read_stream` and `write_stream` expected by the MCP server. Each connecting client gets its own session handled through this endpoint.
|
||||
|
||||
**Conceptual Test using Memory Transport**
|
||||
|
||||
```python
|
||||
import anyio
|
||||
import pytest # Using pytest testing framework
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.server.fastmcp import FastMCP # Using FastMCP for the server part
|
||||
from mcp.shared.memory import create_client_server_memory_streams
|
||||
|
||||
# Define a simple FastMCP server for the test
|
||||
test_server = FastMCP(name="TestServer")
|
||||
@test_server.tool()
|
||||
def ping() -> str:
|
||||
return "pong"
|
||||
|
||||
@pytest.mark.anyio # Mark test to be run with anyio
|
||||
async def test_memory_transport():
|
||||
# 1. Use the memory stream generator
|
||||
async with create_client_server_memory_streams() as (
|
||||
(client_read, client_write), # Client perspective
|
||||
(server_read, server_write) # Server perspective
|
||||
):
|
||||
print("Test: Memory streams created.")
|
||||
# Run server and client concurrently
|
||||
async with anyio.create_task_group() as tg:
|
||||
# 2. Start the server using its streams
|
||||
tg.start_soon(
|
||||
test_server.run, server_read, server_write,
|
||||
test_server.create_initialization_options()
|
||||
)
|
||||
print("Test: Server started in background task.")
|
||||
|
||||
# 3. Create and run client using its streams
|
||||
async with ClientSession(client_read, client_write) as client:
|
||||
print("Test: Client session created. Initializing...")
|
||||
await client.initialize()
|
||||
print("Test: Client initialized. Calling 'ping' tool...")
|
||||
result = await client.call_tool("ping")
|
||||
print(f"Test: Client received result: {result}")
|
||||
# Assert the result is correct
|
||||
assert result.content[0].text == "pong"
|
||||
|
||||
# Cancel server task when client is done (optional)
|
||||
tg.cancel_scope.cancel()
|
||||
print("Test: Finished.")
|
||||
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
`create_client_server_memory_streams()` creates pairs of connected in-memory queues. The server writes to `server_write`, which sends messages to `client_read`. The client writes to `client_write`, which sends messages to `server_read`. This allows direct, in-process communication for testing without actual pipes or network sockets.
|
||||
|
||||
## How Transports Work Under the Hood (Stdio Example)
|
||||
|
||||
Let's focus on the simplest case: `stdio`. How does the `stdio_server` context manager actually work?
|
||||
|
||||
1. **Process Startup:** When you run `mcp run your_server.py`, the `mcp` command starts your `your_server.py` script as a new process. The operating system connects the `stdout` of your server process to the `stdin` of the `mcp` process (or vice versa, depending on perspective, but essentially creating pipes between them).
|
||||
2. **Context Manager:** Inside your server script (when it calls `stdio_server()`), the context manager gets asynchronous wrappers around the process's standard input (`sys.stdin.buffer`) and standard output (`sys.stdout.buffer`), ensuring they handle text encoding (like UTF-8) correctly.
|
||||
3. **Internal Streams:** The context manager also creates internal `anyio` memory streams: `read_stream_writer` / `read_stream` and `write_stream_reader` / `write_stream`. It yields `read_stream` and `write_stream` to your server code.
|
||||
4. **Reader Task (`stdin_reader`)**: The context manager starts a background task that continuously reads lines from the process's actual `stdin`.
|
||||
* For each line received:
|
||||
* It tries to parse the line as a JSON string.
|
||||
* It validates the JSON against the `JSONRPCMessage` Pydantic model ([Chapter 7](07_mcp_protocol_types.md)).
|
||||
* If valid, it puts the `JSONRPCMessage` object onto the `read_stream_writer` (which sends it to the `read_stream` your server is listening on).
|
||||
* If invalid, it might send an `Exception` object instead.
|
||||
5. **Writer Task (`stdout_writer`)**: It starts another background task that continuously reads `JSONRPCMessage` objects from the `write_stream_reader` (which receives messages your server sends to the `write_stream`).
|
||||
* For each message received:
|
||||
* It serializes the `JSONRPCMessage` object back into a JSON string.
|
||||
* It adds a newline character (`\n`) because `stdio` communication is typically line-based.
|
||||
* It writes the resulting string to the process's actual `stdout`.
|
||||
6. **Server Interaction:** Your `MCPServer` (or `FastMCP`) interacts *only* with the yielded `read_stream` and `write_stream`. It doesn't know about `stdin` or `stdout` directly. The transport handles the translation between these memory streams and the actual process I/O.
|
||||
7. **Cleanup:** When the `async with stdio_server()...` block finishes, the background reader/writer tasks are stopped, and the streams are closed.
|
||||
|
||||
**Simplified Sequence Diagram (Stdio Transport during `callTool`)**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ClientProc as Client Process (e.g., mcp CLI)
|
||||
participant ClientStdio as Stdio Client Transport
|
||||
participant ClientSess as ClientSession
|
||||
participant ServerSess as ServerSession
|
||||
participant ServerStdio as Stdio Server Transport
|
||||
participant ServerProc as Server Process (your_server.py)
|
||||
|
||||
Note over ClientProc, ServerProc: OS connects pipes (stdout -> stdin)
|
||||
|
||||
ClientSess->>+ClientStdio: Send CallToolRequest via write_stream
|
||||
ClientStdio->>ClientStdio: Writer task reads from write_stream
|
||||
ClientStdio->>+ClientProc: Serialize & write JSON line to stdout pipe
|
||||
ServerProc->>+ServerStdio: Reader task reads JSON line from stdin pipe
|
||||
ServerStdio->>ServerStdio: Parse & validate JSONRPCMessage
|
||||
ServerStdio->>-ServerSess: Send message via read_stream_writer
|
||||
|
||||
Note over ServerSess: Server processes request...
|
||||
|
||||
ServerSess->>+ServerStdio: Send CallToolResult via write_stream
|
||||
ServerStdio->>ServerStdio: Writer task reads from write_stream
|
||||
ServerStdio->>+ServerProc: Serialize & write JSON line to stdout pipe
|
||||
ClientProc->>+ClientStdio: Reader task reads JSON line from stdin pipe
|
||||
ClientStdio->>ClientStdio: Parse & validate JSONRPCMessage
|
||||
ClientStdio->>-ClientSess: Send message via read_stream_writer
|
||||
```
|
||||
|
||||
This shows how the transport layers (`ClientStdio`, `ServerStdio`) act as intermediaries, translating between the Session's memory streams and the actual process I/O pipes (`stdin`/`stdout`). The other transports (SSE, WebSocket, Memory) perform analogous translation tasks for their respective communication mechanisms.
|
||||
|
||||
## Diving into the Code (Briefly!)
|
||||
|
||||
Let's look at the structure inside the transport files.
|
||||
|
||||
**`server/stdio.py` (Simplified `stdio_server`)**
|
||||
|
||||
```python
|
||||
@asynccontextmanager
|
||||
async def stdio_server(stdin=None, stdout=None):
|
||||
# ... (wrap sys.stdin/stdout if needed) ...
|
||||
|
||||
# Create the internal memory streams
|
||||
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
|
||||
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
|
||||
|
||||
async def stdin_reader(): # Reads from actual stdin
|
||||
try:
|
||||
async with read_stream_writer:
|
||||
async for line in stdin: # Read line from process stdin
|
||||
try:
|
||||
# Validate and parse
|
||||
message = types.JSONRPCMessage.model_validate_json(line)
|
||||
except Exception as exc:
|
||||
await read_stream_writer.send(exc) # Send error upstream
|
||||
continue
|
||||
# Send valid message to the session via internal stream
|
||||
await read_stream_writer.send(message)
|
||||
# ... (error/close handling) ...
|
||||
|
||||
async def stdout_writer(): # Writes to actual stdout
|
||||
try:
|
||||
async with write_stream_reader:
|
||||
# Read message from the session via internal stream
|
||||
async for message in write_stream_reader:
|
||||
# Serialize to JSON string
|
||||
json_str = message.model_dump_json(...)
|
||||
# Write line to process stdout
|
||||
await stdout.write(json_str + "\n")
|
||||
await stdout.flush()
|
||||
# ... (error/close handling) ...
|
||||
|
||||
# Start reader/writer tasks in the background
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(stdin_reader)
|
||||
tg.start_soon(stdout_writer)
|
||||
# Yield the streams the session will use
|
||||
yield read_stream, write_stream
|
||||
# Context manager exit cleans up tasks
|
||||
```
|
||||
|
||||
**`shared/memory.py` (Simplified `create_client_server_memory_streams`)**
|
||||
|
||||
```python
|
||||
@asynccontextmanager
|
||||
async def create_client_server_memory_streams():
|
||||
# Create two pairs of connected memory streams
|
||||
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream(...)
|
||||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream(...)
|
||||
|
||||
# Define the streams from each perspective
|
||||
client_streams = (server_to_client_receive, client_to_server_send)
|
||||
server_streams = (client_to_server_receive, server_to_client_send)
|
||||
|
||||
# Use async context manager to ensure streams are closed properly
|
||||
async with server_to_client_receive, client_to_server_send, \
|
||||
client_to_server_receive, server_to_client_send:
|
||||
# Yield the pairs of streams
|
||||
yield client_streams, server_streams
|
||||
# Streams are automatically closed on exit
|
||||
```
|
||||
|
||||
These snippets illustrate the pattern: set up the external communication (or fake it with memory streams), create internal memory streams for the Session, start background tasks to bridge the two, and yield the internal streams.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Congratulations on reaching the end of this introductory series! You've learned about Communication Transports – the crucial delivery services that move MCP messages between clients and servers.
|
||||
|
||||
* Transports are the **mechanisms** for sending/receiving serialized messages (e.g., `stdio`, `sse`, `websocket`, `memory`).
|
||||
* Each transport suits different scenarios (command-line, web, testing).
|
||||
* Frameworks like `FastMCP` and tools like `mcp run` often handle the **default transport (`stdio`) automatically**.
|
||||
* Transports work by **bridging** the gap between the `Session`'s internal communication streams and the actual external I/O (pipes, sockets, queues).
|
||||
|
||||
Understanding transports completes the picture of how MCP components fit together, from high-level abstractions like `FastMCP` down to the way messages are physically exchanged.
|
||||
|
||||
You now have a solid foundation in the core concepts of the `MCP Python SDK`. From here, you can delve deeper into specific features, explore more complex examples, or start building your own powerful AI tools and integrations! Good luck!
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
51
docs/MCP Python SDK/index.md
Normal file
51
docs/MCP Python SDK/index.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Tutorial: MCP Python SDK
|
||||
|
||||
The **MCP Python SDK** helps developers build applications (clients and servers) that talk to each other using the *Model Context Protocol (MCP)* specification.
|
||||
It simplifies communication by handling the low-level details like standard **message formats** (Abstraction 0), connection **sessions** (Abstraction 1), and different ways to send/receive data (**transports**, Abstraction 2).
|
||||
It also provides a high-level framework, **`FastMCP`** (Abstraction 3), making it easy to create servers that expose **tools** (Abstraction 5), **resources** (Abstraction 4), and **prompts** (Abstraction 6) to clients.
|
||||
The SDK includes **command-line tools** (Abstraction 8) for running and managing these servers.
|
||||
|
||||
|
||||
**Source Repository:** [https://github.com/modelcontextprotocol/python-sdk/tree/d788424caa43599de38cee2f70233282d83e3a34/src/mcp](https://github.com/modelcontextprotocol/python-sdk/tree/d788424caa43599de38cee2f70233282d83e3a34/src/mcp)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A0["MCP Protocol Types"]
|
||||
A1["Client/Server Sessions"]
|
||||
A2["Communication Transports"]
|
||||
A3["FastMCP Server"]
|
||||
A4["FastMCP Resources"]
|
||||
A5["FastMCP Tools"]
|
||||
A6["FastMCP Prompts"]
|
||||
A7["FastMCP Context"]
|
||||
A8["CLI"]
|
||||
A1 -- "Uses MCP Types" --> A0
|
||||
A1 -- "Operates Over Transport" --> A2
|
||||
A2 -- "Serializes/Deserializes MCP..." --> A0
|
||||
A3 -- "Uses Session Logic" --> A1
|
||||
A3 -- "Manages Resources" --> A4
|
||||
A3 -- "Manages Tools" --> A5
|
||||
A3 -- "Manages Prompts" --> A6
|
||||
A8 -- "Runs/Configures Server" --> A3
|
||||
A5 -- "Handlers Can Use Context" --> A7
|
||||
A4 -- "Handlers Can Use Context" --> A7
|
||||
A7 -- "Provides Access To Session" --> A1
|
||||
A7 -- "Provides Access To Server" --> A3
|
||||
```
|
||||
|
||||
## Chapters
|
||||
|
||||
1. [CLI (`mcp` command)](01_cli___mcp__command_.md)
|
||||
2. [FastMCP Server (`FastMCP`)](02_fastmcp_server___fastmcp__.md)
|
||||
3. [FastMCP Resources (`Resource`, `ResourceManager`)](03_fastmcp_resources___resource____resourcemanager__.md)
|
||||
4. [FastMCP Tools (`Tool`, `ToolManager`)](04_fastmcp_tools___tool____toolmanager__.md)
|
||||
5. [FastMCP Prompts (`Prompt`, `PromptManager`)](05_fastmcp_prompts___prompt____promptmanager__.md)
|
||||
6. [FastMCP Context (`Context`)](06_fastmcp_context___context__.md)
|
||||
7. [MCP Protocol Types](07_mcp_protocol_types.md)
|
||||
8. [Client/Server Sessions (`ClientSession`, `ServerSession`)](08_client_server_sessions___clientsession____serversession__.md)
|
||||
9. [Communication Transports (Stdio, SSE, WebSocket, Memory)](09_communication_transports__stdio__sse__websocket__memory_.md)
|
||||
|
||||
|
||||
---
|
||||
|
||||
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)
|
||||
Reference in New Issue
Block a user