mirror of
https://github.com/aljazceru/Tutorial-Codebase-Knowledge.git
synced 2025-12-19 15:34:23 +01:00
init push
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user