From 753c993bb7303ea3c10f8d8776b76c3e650ae474 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 13 Apr 2025 11:19:31 -0400 Subject: [PATCH] Add docs --- docs/docs.json | 8 +- docs/servers/context.mdx | 281 +++++++++++++++++++++++++ docs/servers/fastmcp.mdx | 302 +++++++++++++++++++++++++++ docs/servers/prompts.mdx | 229 ++++++++++++++++++++ docs/servers/resources.mdx | 252 ++++++++++++++++++++++ docs/servers/resources_backup.mdx | 270 ++++++++++++++++++++++++ docs/servers/tools.mdx | 335 ++++++++++++++++++++++++++++++ src/fastmcp/cli/cli.py | 1 - src/fastmcp/server/server.py | 13 +- uv.lock | 80 +++---- 10 files changed, 1725 insertions(+), 46 deletions(-) create mode 100644 docs/servers/context.mdx create mode 100644 docs/servers/fastmcp.mdx create mode 100644 docs/servers/prompts.mdx create mode 100644 docs/servers/resources.mdx create mode 100644 docs/servers/resources_backup.mdx create mode 100644 docs/servers/tools.mdx diff --git a/docs/docs.json b/docs/docs.json index 7cc3c3a23..c9a8330ba 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -37,7 +37,13 @@ }, { "group": "Servers", - "pages": [] + "pages": [ + "servers/fastmcp", + "servers/tools", + "servers/resources", + "servers/prompts", + "servers/context" + ] }, { "group": "Clients", diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx new file mode 100644 index 000000000..229cca457 --- /dev/null +++ b/docs/servers/context.mdx @@ -0,0 +1,281 @@ +--- +title: MCP Context +sidebarTitle: Context +description: Access MCP capabilities like logging, progress, and resources within your tools. +icon: rectangle-code +--- + +When defining FastMCP [Tools](/server/tools), your functions might need to interact with the underlying MCP session or access server capabilities. FastMCP provides the `Context` object for this purpose. + +## What Is Context? + +The `Context` object provides a clean interface to access MCP features within your tool functions, including: + +- **Logging**: Send debug, info, warning, and error messages back to the client +- **Progress Reporting**: Update the client on the progress of long-running operations +- **Resource Access**: Read data from resources registered with the server +- **LLM Sampling**: Request the client's LLM to generate text based on provided messages +- **Request Information**: Access metadata about the current request +- **Server Access**: When needed, access the underlying FastMCP server instance + +## Accessing Context + +To use the context object within your tool function, simply add a parameter to your function signature and type-hint it as `Context`. FastMCP will automatically inject the context instance when your tool is called. + +```python +from fastmcp import FastMCP, Context + +mcp = FastMCP(name="ContextDemo") + +@mcp.tool() +async def process_file(file_uri: str, ctx: Context) -> str: + """Processes a file, using context for logging and resource access.""" + request_id = ctx.request_id + await ctx.info(f"[{request_id}] Starting processing for {file_uri}") + + try: + # Use context to read a resource + contents_list = await ctx.read_resource(file_uri) + if not contents_list: + await ctx.warning(f"Resource {file_uri} is empty.") + return "Resource empty" + + data = contents_list[0].content # Assuming TextResourceContents + await ctx.debug(f"Read {len(data)} bytes from {file_uri}") + + # Report progress + await ctx.report_progress(progress=50, total=100) + + # Simulate work + processed_data = data.upper() # Example processing + + await ctx.report_progress(progress=100, total=100) + await ctx.info(f"Processing complete for {file_uri}") + + return f"Processed data length: {len(processed_data)}" + + except Exception as e: + # Use context to log errors + await ctx.error(f"Error processing {file_uri}: {str(e)}") + raise # Re-raise to send error back to client +``` + +**Key Points:** + +- The parameter name (e.g., `ctx`, `context`) doesn't matter, only the type hint `Context` is important. +- The context parameter can be placed anywhere in your function's signature. +- The context is optional - tools that don't need it can omit the parameter. +- Context is only available within tool functions during a request; attempting to use context methods outside a request will raise errors. +- Context methods are async, so your tool function usually needs to be async as well. + +## Context Capabilities + +### Logging + +Send log messages back to the MCP client. This is useful for debugging and providing visibility into tool execution during a request. + +```python +@mcp.tool() +async def analyze_data(data: list[float], ctx: Context) -> dict: + """Analyze numerical data with logging.""" + await ctx.debug("Starting analysis of numerical data") + await ctx.info(f"Analyzing {len(data)} data points") + + try: + result = sum(data) / len(data) + await ctx.info(f"Analysis complete, average: {result}") + return {"average": result, "count": len(data)} + except ZeroDivisionError: + await ctx.warning("Empty data list provided") + return {"error": "Empty data list"} + except Exception as e: + await ctx.error(f"Analysis failed: {str(e)}") + raise +``` + +**Available Logging Methods:** + +- **`ctx.debug(message: str)`**: Low-level details useful for debugging +- **`ctx.info(message: str)`**: General information about tool execution +- **`ctx.warning(message: str)`**: Potential issues that didn't prevent execution +- **`ctx.error(message: str)`**: Errors that occurred during execution +- **`ctx.log(level: Literal["debug", "info", "warning", "error"], message: str, logger_name: str | None = None)`**: Generic log method supporting custom logger names + +### Progress Reporting + +For long-running tools, notify the client about the progress of the operation. This allows clients to display progress indicators and provide a better user experience. + +```python +@mcp.tool() +async def process_items(items: list[str], ctx: Context) -> dict: + """Process a list of items with progress updates.""" + total = len(items) + results = [] + + for i, item in enumerate(items): + # Report progress as percentage + await ctx.report_progress(progress=i, total=total) + + # Process the item (simulated with a sleep) + await asyncio.sleep(0.1) + results.append(item.upper()) + + # Report 100% completion + await ctx.report_progress(progress=total, total=total) + + return {"processed": len(results), "results": results} +``` + +**Method signature:** + +- **`ctx.report_progress(progress: float, total: float | None = None)`** + - `progress`: Current progress value (e.g., 24) + - `total`: Optional total value (e.g., 100). If provided, clients may interpret this as a percentage. + +Progress reporting requires the client to have sent a `progressToken` in the initial request. If the client doesn't support progress reporting, these calls will have no effect. + +### Resource Access + +Read data from resources registered with your FastMCP server. This allows tools to access files, configuration, or dynamically generated content. + +```python +@mcp.tool() +async def summarize_document(document_uri: str, ctx: Context) -> str: + """Summarize a document by its resource URI.""" + # Read the document content + content_list = await ctx.read_resource(document_uri) + + if not content_list: + return "Document is empty" + + document_text = content_list[0].content + + # Example: Generate a simple summary (length-based) + words = document_text.split() + total_words = len(words) + + await ctx.info(f"Document has {total_words} words") + + # Return a simple summary + if total_words > 100: + summary = " ".join(words[:100]) + "..." + return f"Summary ({total_words} words total): {summary}" + else: + return f"Full document ({total_words} words): {document_text}" +``` + +**Method signature:** + +- **`ctx.read_resource(uri: str | AnyUrl) -> list[ReadResourceContents]`** + - `uri`: The resource URI to read + - Returns a list of resource content parts (usually containing just one item) + +The returned content is typically accessed via `content_list[0].content` and can be text or binary data depending on the resource. + +### LLM Sampling + +Request the client's LLM to generate text based on provided messages. This is useful when your tool needs to leverage the LLM's capabilities to process data or generate responses. + +```python +@mcp.tool() +async def analyze_sentiment(text: str, ctx: Context) -> dict: + """Analyze the sentiment of a text using the client's LLM.""" + # Create a sampling prompt asking for sentiment analysis + prompt = f"Analyze the sentiment of the following text as positive, negative, or neutral. Just output a single word - 'positive', 'negative', or 'neutral'. Text to analyze: {text}" + + # Send the sampling request to the client's LLM + response = await ctx.sample(prompt) + + # Process the LLM's response + sentiment = response.text.strip().lower() + + # Map to standard sentiment values + if "positive" in sentiment: + sentiment = "positive" + elif "negative" in sentiment: + sentiment = "negative" + else: + sentiment = "neutral" + + return {"text": text, "sentiment": sentiment} +``` + +**Method signature:** + +- **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None) -> TextContent | ImageContent`** + - `messages`: A string or list of strings/message objects to send to the LLM + - `system_prompt`: Optional system prompt to guide the LLM's behavior + - `temperature`: Optional sampling temperature (controls randomness) + - `max_tokens`: Optional maximum number of tokens to generate (defaults to 512) + - Returns the LLM's response as TextContent or ImageContent + +When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles. + +```python +@mcp.tool() +async def generate_example(concept: str, ctx: Context) -> str: + """Generate a Python code example for a given concept.""" + # Using a system prompt and a user message + response = await ctx.sample( + messages=f"Write a simple Python code example demonstrating '{concept}'.", + system_prompt="You are an expert Python programmer. Provide concise, working code examples without explanations.", + temperature=0.7, + max_tokens=300 + ) + + code_example = response.text + return f"```python\n{code_example}\n```" +``` + +See [Client Sampling](/client/sampling) for more details on how clients handle these requests. + +### Request Information + +Access metadata about the current request and client. + +```python +@mcp.tool() +async def request_info(ctx: Context) -> dict: + """Return information about the current request.""" + return { + "request_id": ctx.request_id, + "client_id": ctx.client_id or "Unknown client" + } +``` + +**Available Properties:** + +- **`ctx.request_id -> str`**: Get the unique ID for the current MCP request +- **`ctx.client_id -> str | None`**: Get the ID of the client making the request, if provided during initialization + +### Advanced Access + +For advanced use cases, you can access the underlying MCP session and FastMCP server. + +```python +@mcp.tool() +async def advanced_tool(ctx: Context) -> str: + """Demonstrate advanced context access.""" + # Access the FastMCP server instance + server_name = ctx.fastmcp.name + + # Low-level session access (rarely needed) + session = ctx.session + request_context = ctx.request_context + + return f"Server: {server_name}" +``` + +**Advanced Properties:** + +- **`ctx.fastmcp -> FastMCP`**: Access the server instance the context belongs to +- **`ctx.session`**: Access the raw `mcp.server.session.ServerSession` object +- **`ctx.request_context`**: Access the raw `mcp.shared.context.RequestContext` object + + +Direct use of `session` or `request_context` requires understanding the low-level MCP Python SDK and may be less stable than using the methods provided directly on the `Context` object. + + +## Using Context in Other Components + +Currently, Context is primarily designed for use within tool functions. Support for Context in other components like resources and prompts is planned for future releases. \ No newline at end of file diff --git a/docs/servers/fastmcp.mdx b/docs/servers/fastmcp.mdx new file mode 100644 index 000000000..90cc25b97 --- /dev/null +++ b/docs/servers/fastmcp.mdx @@ -0,0 +1,302 @@ +--- +title: The FastMCP Server +sidebarTitle: FastMCP Server +description: Learn about the core FastMCP server class and how to run it. +icon: server +--- + +The central piece of a FastMCP application is the `FastMCP` server class. This class acts as the main container for your application's tools, resources, and prompts, and manages communication with MCP clients. + +## Creating a Server + +Instantiating a server is straightforward. You typically provide a name for your server, which helps identify it in client applications or logs. + +```python +from fastmcp import FastMCP + +# Create a basic server instance +mcp = FastMCP(name="MyAssistantServer") + +# You can also add instructions for how to interact with the server +mcp_with_instructions = FastMCP( + name="HelpfulAssistant", + instructions="This server provides data analysis tools. Call get_average() to analyze numerical data." +) +``` + +The `FastMCP` constructor accepts several arguments: + +* `name`: (Optional) A human-readable name for your server. Defaults to "FastMCP". +* `instructions`: (Optional) Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality. +* `lifespan`: (Optional) An async context manager function for server startup and shutdown logic. See [Lifespan Management](/advanced/lifespan). +* `tags`: (Optional) A set of strings to tag the server itself. +* `**settings`: Keyword arguments corresponding to `ServerSettings` for configuration. See [Configuration](/advanced/configuration). + +## Components + +FastMCP servers expose several types of components to the client: + +### Tools + +Tools are functions that the client can call to perform actions or access external systems. + +```python +@mcp.tool() +def multiply(a: float, b: float) -> float: + """Multiplies two numbers together.""" + return a * b +``` + +See [Tools](/server/tools) for detailed documentation. + +### Resources + +Resources expose data sources that the client can read. + +```python +@mcp.resource("data://config") +def get_config() -> dict: + """Provides the application configuration.""" + return {"theme": "dark", "version": "1.0"} +``` + +See [Resources & Templates](/server/resources) for detailed documentation. + +### Resource Templates + +Resource templates are parameterized resources that allow the client to request specific data. + +```python +@mcp.resource("users://{user_id}/profile") +def get_user_profile(user_id: int) -> dict: + """Retrieves a user's profile by ID.""" + # The {user_id} in the URI is extracted and passed to this function + return {"id": user_id, "name": f"User {user_id}", "status": "active"} +``` + +See [Resources & Templates](/server/resources) for detailed documentation. + +### Prompts + +Prompts are reusable message templates for guiding the LLM. + +```python +@mcp.prompt() +def analyze_data(data_points: list[float]) -> str: + """Creates a prompt asking for analysis of numerical data.""" + formatted_data = ", ".join(str(point) for point in data_points) + return f"Please analyze these data points: {formatted_data}" +``` + +See [Prompts](/server/prompts) for detailed documentation. + +## Running the Server + +FastMCP servers need a transport mechanism to communicate with clients. In the MCP protocol, servers typically run as separate processes that clients connect to. + +### The `__main__` Block Pattern + +The standard way to make your server executable is to include a `run()` call inside an `if __name__ == "__main__":` block: + +```python +# my_server.py +from fastmcp import FastMCP + +mcp = FastMCP(name="MyServer") + +@mcp.tool() +def greet(name: str) -> str: + """Greet a user by name.""" + return f"Hello, {name}!" + +if __name__ == "__main__": + # This code only runs when the file is executed directly + mcp.run() +``` + +This pattern is important because: + +1. **Client Compatibility**: Standard MCP clients (like Claude Desktop) expect to execute your server file directly with `python my_server.py` +2. **Process Isolation**: Each server runs in its own process, allowing clients to manage multiple servers independently +3. **Import Safety**: The main block prevents the server from running when the file is imported by other code + +While this pattern is technically optional when using FastMCP's CLI, it's considered a best practice for maximum compatibility with all MCP clients. + +### Transport Options + +FastMCP supports two transport mechanisms: + +#### STDIO Transport (Default) + +The standard input/output (STDIO) transport is the default and most widely compatible option: + +```python +# Run with stdio (default) +mcp.run() # or explicitly: mcp.run(transport="stdio") +``` + +With STDIO: +- The client starts a new server process for each session +- Communication happens through standard input/output streams +- The server process terminates when the client disconnects +- This is ideal for integrations with tools like Claude Desktop, where each conversation gets its own server instance + +#### SSE Transport (Server-Sent Events) + +For long-running servers that serve multiple clients, FastMCP supports SSE: + +```python +# Run with SSE on default host/port (0.0.0.0:8000) +mcp.run(transport="sse") +``` + +With SSE: +- The server runs as a persistent web server +- Multiple clients can connect simultaneously +- The server stays running until explicitly terminated +- This is ideal for remote access to services + +You can configure the host, port, and log level when running the server: + +```python +# Configure with parameters +mcp.run(transport="sse", host="127.0.0.1", port=8888) + +# Or run asynchronously with the same parameters +import asyncio +asyncio.run(mcp.run_sse_async(host="127.0.0.1", port=8888, log_level="debug")) +``` + +These parameters override any settings defined when creating the FastMCP instance. + +### Using the FastMCP CLI + +The FastMCP CLI provides a convenient way to run servers: + +```bash +# Run a server (defaults to stdio transport) +fastmcp run my_server.py:mcp + +# Explicitly specify a transport +fastmcp run my_server.py:mcp --transport sse + +# Configure SSE transport +fastmcp run my_server.py:mcp --transport sse --host 127.0.0.1 --port 8888 +``` + +The CLI can dynamically find and run FastMCP server objects in your files, but including the `if __name__ == "__main__":` block ensures compatibility with all clients. + + +For more options, including how to set up your server's dependencies or use advanced configurations, see the [CLI Reference](/cli/overview). + + +## Mounting Subservers + +FastMCP allows you to compose complex applications by mounting other FastMCP servers as subservers. This is useful for: + +- Organizing large applications into logical components +- Reusing existing FastMCP servers as parts of a larger system +- Creating domain-specific servers that can be used independently or composed + +```python +from fastmcp import FastMCP + +# Create the main server +main_mcp = FastMCP(name="MainServer") + +# Create a domain-specific subserver +weather_mcp = FastMCP(name="WeatherService") + +@weather_mcp.tool() +def get_forecast(city: str) -> dict: + """Get the weather forecast for a city.""" + return {"city": city, "forecast": "Sunny", "temperature": 72} + +# Create another domain-specific subserver +calculator_mcp = FastMCP(name="CalculatorService") + +@calculator_mcp.tool() +def add(a: float, b: float) -> float: + """Add two numbers.""" + return a + b + +# Mount the subservers with prefixes +main_mcp.mount("weather", weather_mcp) +main_mcp.mount("calc", calculator_mcp) + +# Now main_mcp has access to both subservers' tools: +# - "weather_get_forecast" (from weather_mcp) +# - "calc_add" (from calculator_mcp) + +if __name__ == "__main__": + main_mcp.run() +``` + +### How Mounting Works + +When you mount a server with `main_mcp.mount(prefix, subserver)`: + +1. All tools from the subserver are imported with prefixed names: + - `tool_name` becomes `{prefix}_tool_name` + - Default separator is `_`, but can be customized + +2. All resources and resource templates are imported with prefixed URIs: + - `resource://data` becomes `{prefix}+resource://data` + - Default separator is `+`, but can be customized + +3. All prompts are imported with prefixed names: + - `prompt_name` becomes `{prefix}_prompt_name` + - Default separator is `_`, but can be customized + +4. The subserver's lifespan is managed automatically when the main server starts and stops + +### Customizing Separators + +You can customize the separators used for naming: + +```python +main_mcp.mount( + "weather", + weather_mcp, + tool_separator="-", # Use "weather-get_forecast" instead of "weather_get_forecast" + resource_separator=".", # Use "weather.resource://data" instead of "weather+resource://data" + prompt_separator=":" # Use "weather:prompt_name" instead of "weather_prompt_name" +) +``` + + +Some MCP clients may reject certain separators as invalid. For example, Claude Desktop does not support `/` in tool names. + + +## Server Configuration + +Server behavior, like transport settings (host, port for SSE) and how duplicate components are handled, can be configured via `ServerSettings`. These settings can be passed during `FastMCP` initialization, set via environment variables (prefixed with `FASTMCP_SERVER_`), or loaded from a `.env` file. + +```python +from fastmcp import FastMCP +from fastmcp.settings import DuplicateBehavior + +# Configure during initialization +mcp = FastMCP( + name="ConfiguredServer", + port=8080, # Directly maps to ServerSettings + on_duplicate_tools=DuplicateBehavior.ERROR # Set duplicate handling +) + +# Settings are accessible via mcp.settings +print(mcp.settings.port) # Output: 8080 +print(mcp.settings.on_duplicate_tools) # Output: DuplicateBehavior.ERROR +``` + +### Key Configuration Options + +- **`host`**: Host address for SSE transport (default: "0.0.0.0") +- **`port`**: Port number for SSE transport (default: 8000) +- **`log_level`**: Logging level (default: "INFO") +- **`on_duplicate_tools`**: How to handle duplicate tool registrations +- **`on_duplicate_resources`**: How to handle duplicate resource registrations +- **`on_duplicate_prompts`**: How to handle duplicate prompt registrations + +All of these can be configured directly as parameters when creating the `FastMCP` instance. + +See the [Configuration](/advanced/configuration) page for more details. \ No newline at end of file diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx new file mode 100644 index 000000000..874061223 --- /dev/null +++ b/docs/servers/prompts.mdx @@ -0,0 +1,229 @@ +--- +title: Prompts +sidebarTitle: Prompts +description: Create reusable, parameterized prompt templates for MCP clients. +icon: message-lines +--- + +Prompts are reusable message templates that help LLMs generate structured, purposeful responses. FastMCP simplifies defining these templates, primarily using the `@mcp.prompt` decorator. + +## What Are Prompts? + +Prompts provide parameterized message templates for LLMs. When a client requests a prompt: + +1. FastMCP finds the corresponding prompt definition. +2. If it has parameters, they are validated against your function signature. +3. Your function executes with the validated inputs. +4. The generated message(s) are returned to the LLM to guide its response. + +This allows you to define consistent, reusable templates that LLMs can use across different clients and contexts. + +## Defining Prompts + +### The `@prompt` Decorator + +The most common way to define a prompt is by decorating a Python function. The decorator uses the function name as the prompt's identifier. + +```python +from fastmcp import FastMCP +from fastmcp.prompts.prompt import UserMessage, AssistantMessage, Message + +mcp = FastMCP(name="PromptServer") + +# Basic prompt returning a string (converted to UserMessage) +@mcp.prompt() +def ask_about_topic(topic: str) -> str: + """Generates a user message asking for an explanation of a topic.""" + return f"Can you please explain the concept of '{topic}'?" + +# Prompt returning a specific message type +@mcp.prompt() +def generate_code_request(language: str, task_description: str) -> UserMessage: + """Generates a user message requesting code generation.""" + content = f"Write a {language} function that performs the following task: {task_description}" + return UserMessage(content=content) +``` + +**Key Concepts:** + +* **Name:** By default, the prompt name is taken from the function name. +* **Parameters:** The function parameters define the inputs needed to generate the prompt. +* **Inferred Metadata:** By default: + * Prompt Name: Taken from the function name (`ask_about_topic`). + * Prompt Description: Taken from the function's docstring. + +### Return Values + +FastMCP intelligently handles different return types from your prompt function: + +- **`str`**: Automatically converted to a single `UserMessage`. +- **`Message`** (e.g., `UserMessage`, `AssistantMessage`): Used directly as provided. +- **`dict`**: Parsed as a `Message` object if it has the correct structure. +- **`list[Message]`**: Used as a sequence of messages (a conversation). + +```python +@mcp.prompt() +def roleplay_scenario(character: str, situation: str) -> list[Message]: + """Sets up a roleplaying scenario with initial messages.""" + return [ + UserMessage(f"Let's roleplay. You are {character}. The situation is: {situation}"), + AssistantMessage("Okay, I understand. I am ready. What happens next?") + ] + +@mcp.prompt() +def ask_for_feedback() -> dict: + """Generates a user message asking for feedback.""" + return {"role": "user", "content": "What did you think of my previous response?"} +``` + +### Type Annotations + +Type annotations are important for prompts. They: +1. Inform FastMCP about the expected types for each parameter. +2. Allow validation of parameters received from clients. +3. Are used to generate the prompt's schema for the MCP protocol. + +```python +from pydantic import Field +from typing import Literal, Optional + +@mcp.prompt() +def generate_content_request( + topic: str = Field(description="The main subject to cover"), + format: Literal["blog", "email", "social"] = "blog", + tone: str = "professional", + word_count: Optional[int] = None +) -> str: + """Create a request for generating content in a specific format.""" + prompt = f"Please write a {format} post about {topic} in a {tone} tone." + + if word_count: + prompt += f" It should be approximately {word_count} words long." + + return prompt +``` + +### Required vs. Optional Parameters + +Parameters in your function signature are considered **required** unless they have a default value. + +```python +@mcp.prompt() +def data_analysis_prompt( + data_uri: str, # Required - no default value + analysis_type: str = "summary", # Optional - has default value + include_charts: bool = False # Optional - has default value +) -> str: + """Creates a request to analyze data with specific parameters.""" + prompt = f"Please perform a '{analysis_type}' analysis on the data found at {data_uri}." + if include_charts: + prompt += " Include relevant charts and visualizations." + return prompt +``` + +In this example, the client *must* provide `data_uri`. If `analysis_type` or `include_charts` are omitted, their default values will be used. + +### Prompt Metadata + +While FastMCP infers the name and description from your function, you can override these and add tags using arguments to the `@mcp.prompt` decorator: + +```python +@mcp.prompt( + name="analyze_data_request", # Custom prompt name + description="Creates a request to analyze data with specific parameters", # Custom description + tags={"analysis", "data"} # Optional categorization tags +) +def data_analysis_prompt( + data_uri: str = Field(description="The URI of the resource containing the data."), + analysis_type: str = Field(default="summary", description="Type of analysis.") +) -> str: + """This docstring is ignored when description is provided.""" + return f"Please perform a '{analysis_type}' analysis on the data found at {data_uri}." +``` + +- **`name`**: Sets the explicit prompt name exposed via MCP. +- **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose. +- **`tags`**: A set of strings used to categorize the prompt. Clients *might* use tags to filter or group available prompts. + +### Asynchronous Prompts + +FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as prompts. + +```python +# Synchronous prompt +@mcp.prompt() +def simple_question(question: str) -> str: + """Generates a simple question to ask the LLM.""" + return f"Question: {question}" + +# Asynchronous prompt +@mcp.prompt() +async def data_based_prompt(data_id: str) -> str: + """Generates a prompt based on data that needs to be fetched.""" + # In a real scenario, you might fetch data from a database or API + async with aiohttp.ClientSession() as session: + async with session.get(f"https://api.example.com/data/{data_id}") as response: + data = await response.json() + return f"Analyze this data: {data['content']}" +``` + +Use `async def` when your prompt function performs I/O operations like network requests, database queries, file I/O, or external service calls. + +### The MCP Session + +Prompts can access the MCP features via the `Context` object, just like tools. + +```python +from fastmcp import Context + +@mcp.prompt() +async def generate_report_request(report_type: str, ctx: Context) -> str: + """Generates a request for a report based on available data.""" + # Log the request + await ctx.info(f"Generating prompt for report type: {report_type}") + + # Could potentially use ctx.read_resource to fetch data + # Or ctx.sample to get additional input from the LLM + + return f"Please create a {report_type} report based on the available data." +``` + +Using the `ctx` parameter (based on its `Context` type hint), you can access: + +- **Logging:** `ctx.debug()`, `ctx.info()`, etc. +- **Resource Access:** `ctx.read_resource(uri)` +- **LLM Sampling:** `ctx.sample(...)` +- **Request Info:** `ctx.request_id`, `ctx.client_id` + +Refer to the [Using Context](/server/context) page for more details on these capabilities. + +## Server Behavior + +### Duplicate Prompts + +You can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization. + +```python +from fastmcp import FastMCP +from fastmcp.settings import DuplicateBehavior + +mcp = FastMCP( + name="PromptServer", + on_duplicate_prompts=DuplicateBehavior.ERROR # Raise an error if a prompt name is duplicated +) + +@mcp.prompt() +def greeting(): return "Hello, how can I help you today?" + +# This registration attempt will raise a ValueError because +# "greeting" is already registered and the behavior is ERROR. +# @mcp.prompt() +# def greeting(): return "Hi there! What can I do for you?" +``` + +The `DuplicateBehavior` enum options are: + +- `WARN` (default): Logs a warning, and the new prompt replaces the old one. +- `ERROR`: Raises a `ValueError`, preventing the duplicate registration. +- `REPLACE`: Silently replaces the existing prompt with the new one. +- `IGNORE`: Keeps the original prompt and ignores the new registration attempt. \ No newline at end of file diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx new file mode 100644 index 000000000..3088b3d7a --- /dev/null +++ b/docs/servers/resources.mdx @@ -0,0 +1,252 @@ +--- +title: Resources & Templates +sidebarTitle: Resources & Templates +description: Expose data sources and dynamic content generators to your MCP client. +icon: database +--- + +Resources represent data or files that an MCP client can read, and resource templates extend this concept by allowing clients to request dynamically generated resources based on parameters passed in the URI. + +FastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator. + +## What Are Resources? + +Resources provide read-only access to data for the LLM or client application. When a client requests a resource URI: + +1. FastMCP finds the corresponding resource definition. +2. If it's dynamic (defined by a function), the function is executed. +3. The content (text, JSON, binary data) is returned to the client. + +This allows LLMs to access files, database content, configuration, or dynamically generated information relevant to the conversation. + +## Defining Resources + +### The `@resource` Decorator + +The most common way to define a resource is by decorating a Python function. The decorator requires the resource's unique URI. + +```python +import json +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +# Basic dynamic resource returning a string +@mcp.resource("resource://greeting") +def get_greeting() -> str: + """Provides a simple greeting message.""" + return "Hello from FastMCP Resources!" + +# Resource returning JSON data (dict is auto-serialized) +@mcp.resource("data://config") +def get_config() -> dict: + """Provides application configuration as JSON.""" + return { + "theme": "dark", + "version": "1.2.0", + "features": ["tools", "resources"], + } +``` + +**Key Concepts:** + +* **URI:** The first argument to `@resource` is the unique URI (e.g., `"resource://greeting"`) clients use to request this data. +* **Lazy Loading:** The decorated function (`get_greeting`, `get_config`) is only executed when a client specifically requests that resource URI via `resources/read`. +* **Inferred Metadata:** By default: + * Resource Name: Taken from the function name (`get_greeting`). + * Resource Description: Taken from the function's docstring. + +### Return Values + +FastMCP automatically converts your function's return value into the appropriate MCP resource content: + +- **`str`**: Sent as `TextResourceContents` (with `mime_type="text/plain"` by default). +- **`dict`, `list`, `pydantic.BaseModel`**: Automatically serialized to a JSON string and sent as `TextResourceContents` (with `mime_type="application/json"` by default). +- **`bytes`**: Base64 encoded and sent as `BlobResourceContents`. You should specify an appropriate `mime_type` (e.g., `"image/png"`, `"application/octet-stream"`). +- **`None`**: Results in an empty resource content list being returned. + +### Resource Metadata + +You can customize the resource's properties using arguments in the decorator: + +```python +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +# Example specifying metadata +@mcp.resource( + uri="data://app-status", # Explicit URI (required) + name="ApplicationStatus", # Custom name + description="Provides the current status of the application.", # Custom description + mime_type="application/json", # Explicit MIME type + tags={"monitoring", "status"} # Categorization tags +) +def get_application_status() -> dict: + """Internal function description (ignored if description is provided above).""" + return {"status": "ok", "uptime": 12345, "version": mcp.settings.version} # Example usage +``` + +- **`uri`**: The unique identifier for the resource (required). +- **`name`**: A human-readable name (defaults to function name). +- **`description`**: Explanation of the resource (defaults to docstring). +- **`mime_type`**: Specifies the content type (FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types). +- **`tags`**: A set of strings for categorization, potentially used by clients for filtering. + + +### Asynchronous Resources + +Use `async def` for resource functions that perform I/O operations (e.g., reading from a database or network) to avoid blocking the server. + +```python +import aiofiles +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +@mcp.resource("file:///app/data/important_log.txt", mime_type="text/plain") +async def read_important_log() -> str: + """Reads content from a specific log file asynchronously.""" + try: + async with aiofiles.open("/app/data/important_log.txt", mode="r") as f: + content = await f.read() + return content + except FileNotFoundError: + return "Log file not found." +``` + +### Resource Classes + +While `@mcp.resource` is ideal for dynamic content, you can directly register pre-defined resources (like static files or simple text) using `mcp.add_resource()` and concrete `Resource` subclasses. + +```python +from pathlib import Path +from fastmcp import FastMCP +from fastmcp.resources import FileResource, TextResource, DirectoryResource + +mcp = FastMCP(name="DataServer") + +# 1. Exposing a static file directly +readme_path = Path("./README.md").resolve() +if readme_path.exists(): + # Use a file:// URI scheme + readme_resource = FileResource( + uri=f"file://{readme_path.as_posix()}", + path=readme_path, # Path to the actual file + name="README File", + description="The project's README.", + mime_type="text/markdown", + tags={"documentation"} + ) + mcp.add_resource(readme_resource) + +# 2. Exposing simple, predefined text +notice_resource = TextResource( + uri="resource://notice", + name="Important Notice", + text="System maintenance scheduled for Sunday.", + tags={"notification"} +) +mcp.add_resource(notice_resource) + +# 3. Exposing a directory listing +data_dir_path = Path("./app_data").resolve() +if data_dir_path.is_dir(): + data_listing_resource = DirectoryResource( + uri="resource://data-files", + path=data_dir_path, # Path to the directory + name="Data Directory Listing", + description="Lists files available in the data directory.", + recursive=False # Set to True to list subdirectories + ) + mcp.add_resource(data_listing_resource) # Returns JSON list of files +``` + +**Common Resource Classes:** + +- `TextResource`: For simple string content. +- `BinaryResource`: For raw `bytes` content. +- `FileResource`: Reads content from a local file path. Handles text/binary modes and lazy reading. +- `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`). +- `DirectoryResource`: Lists files in a local directory (returns JSON). +- (`FunctionResource`: Internal class used by `@mcp.resource`). + +Use these when the content is static or sourced directly from a file/URL, bypassing the need for a dedicated Python function. + +## Defining Resource Templates + +Resource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature. + +```python +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +# Template URI includes {city} placeholder +@mcp.resource("data://weather/{city}") +# Function accepts 'city' parameter matching the placeholder +def get_weather_for_city(city: str) -> dict: + """Provides weather information for a specific city.""" + print(f"Server: Generating weather for city: {city}...") + # In reality, call a weather API using the 'city' parameter + temp = 20 + len(city) % 5 # Dummy logic + condition = "Sunny" if len(city) % 2 == 0 else "Cloudy" + return {"city": city.capitalize(), "temperature": temp, "unit": "celsius", "condition": condition} + +# Template with an integer parameter +@mcp.resource("users://{user_id}/profile") +async def get_user_profile(user_id: int) -> dict: + """Retrieves a user's profile information by ID.""" + print(f"Server: Generating profile for user ID: {user_id}...") + # In reality, fetch from database using user_id + # FastMCP uses Pydantic to auto-convert the string URI part to int + if user_id == 1: + return {"id": user_id, "name": "Alice", "email": "alice@example.com", "status": "active"} + elif user_id == 2: + return {"id": user_id, "name": "Bob", "email": "bob@example.com", "status": "inactive"} + else: + # Example of returning an error structure + return {"error": f"User with ID {user_id} not found"} +``` + +**How Templates Work:** + +1. **Definition:** When FastMCP sees `{...}` placeholders in the `@resource` URI and matching function parameters, it registers a `ResourceTemplate`. +2. **Discovery:** Clients list templates via `resources/listResourceTemplates`. +3. **Request & Matching:** A client requests a specific URI, e.g., `data://weather/london`. FastMCP matches this to the `data://weather/{city}` template. +4. **Parameter Extraction:** It extracts the parameter value: `city="london"`. +5. **Type Conversion & Function Call:** It converts the extracted string `"london"` to the type hinted in the function (`str` in this case) and calls `get_weather_for_city(city="london")`. For `users://1/profile`, it converts `"1"` to `int` before calling `get_user_profile(user_id=1)`. +6. **Response:** The function's return value is formatted (e.g., dict to JSON) and sent back as the content of the requested resource URI (`data://weather/london`). + +Templates provide a powerful way to expose parameterized data access points following REST-like principles. + +## Server Behavior + +### Duplicate Resources + +You can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization. + +```python +from fastmcp import FastMCP +from fastmcp.settings import DuplicateBehavior + +mcp = FastMCP( + name="ResourceServer", + on_duplicate_resources=DuplicateBehavior.ERROR # Raise error on duplicates +) + +@mcp.resource("data://config") +def get_config_v1(): return {"version": 1} + +# This registration attempt will raise a ValueError because +# "data://config" is already registered and the behavior is ERROR. +# @mcp.resource("data://config") +# def get_config_v2(): return {"version": 2} +``` + +The `DuplicateBehavior` enum options are: + +- `WARN` (default): Logs a warning, and the new resource/template replaces the old one. +- `ERROR`: Raises a `ValueError`, preventing the duplicate registration. +- `REPLACE`: Silently replaces the existing resource/template with the new one. +- `IGNORE`: Keeps the original resource/template and ignores the new registration attempt. \ No newline at end of file diff --git a/docs/servers/resources_backup.mdx b/docs/servers/resources_backup.mdx new file mode 100644 index 000000000..82bf58254 --- /dev/null +++ b/docs/servers/resources_backup.mdx @@ -0,0 +1,270 @@ +--- +title: Resources & Templates +sidebarTitle: Resources & Templates +description: Expose data sources and dynamic content generators to your MCP client. +icon: database +--- + +Resources represent data or files that an MCP client can read, and resource templates extend this concept by allowing clients to request dynamically generated resources based on parameters passed in the URI. + +FastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator. + +## What Are Resources? + +Resources provide read-only access to data for the LLM or client application. When a client requests a resource URI: + +1. FastMCP finds the corresponding resource definition. +2. If it's dynamic (defined by a function), the function is executed. +3. The content (text, JSON, binary data) is returned to the client. + +This allows LLMs to access files, database content, configuration, or dynamically generated information relevant to the conversation. + +## Defining Resources with `@mcp.resource` + +The most common way to define a resource is by decorating a Python function. The decorator requires the resource's unique URI. + +```python +import json +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +# Basic dynamic resource returning a string +@mcp.resource("resource://greeting") +def get_greeting() -> str: + """Provides a simple greeting message.""" + return "Hello from FastMCP Resources!" + +# Resource returning JSON data (dict is auto-serialized) +@mcp.resource("data://config") +def get_config() -> dict: + """Provides application configuration as JSON.""" + return { + "theme": "dark", + "version": "1.2.0", + "features": ["tools", "resources"], + } +``` + +**Key Concepts:** + +* **URI:** The first argument to `@resource` is the unique URI (e.g., `"resource://greeting"`) clients use to request this data. +* **Lazy Loading:** The decorated function (`get_greeting`, `get_config`) is only executed when a client specifically requests that resource URI via `resources/read`. +* **Inferred Metadata:** By default: + * Resource Name: Taken from the function name (`get_greeting`). + * Resource Description: Taken from the function's docstring. + +### Return Value Handling + +FastMCP automatically converts your function's return value into the appropriate MCP resource content: + +- **`str`**: Sent as `TextResourceContents` (with `mime_type="text/plain"` by default). +- **`dict`, `list`, `pydantic.BaseModel`**: Automatically serialized to a JSON string and sent as `TextResourceContents` (with `mime_type="application/json"` by default). +- **`bytes`**: Base64 encoded and sent as `BlobResourceContents`. You should specify an appropriate `mime_type` (e.g., `"image/png"`, `"application/octet-stream"`). +- **`None`**: Results in an empty resource content list being returned. + +### Resource Metadata + +You can customize the resource's properties using arguments in the decorator: + +```python +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +# Example specifying metadata +@mcp.resource( + uri="data://app-status", # Explicit URI (required) + name="ApplicationStatus", # Custom name + description="Provides the current status of the application.", # Custom description + mime_type="application/json", # Explicit MIME type + tags={"monitoring", "status"} # Categorization tags +) +def get_application_status() -> dict: + """Internal function description (ignored if description is provided above).""" + return {"status": "ok", "uptime": 12345, "version": mcp.settings.version} # Example usage +``` + +- **`uri`**: The unique identifier for the resource (required). +- **`name`**: A human-readable name (defaults to function name). +- **`description`**: Explanation of the resource (defaults to docstring). +- **`mime_type`**: Specifies the content type (FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types). +- **`tags`**: A set of strings for categorization, potentially used by clients for filtering. + +### Using Context in Resources + +Like tools, resource functions can request the `Context` object to access MCP session capabilities. + +```python +from fastmcp import FastMCP, Context +import datetime + +mcp = FastMCP(name="DataServer") + +@mcp.resource("data://server-info", tags={"server", "info"}) +async def get_server_info(ctx: Context) -> dict: + """Provides information about the server using context.""" + await ctx.info(f"Generating server info resource for request {ctx.request_id}") + # You could potentially read other resources via ctx.read_resource here + return { + "server_name": mcp.name, + "timestamp": datetime.datetime.now(datetime.UTC).isoformat(), + "client_id": ctx.client_id or "N/A", + "log_level": mcp.settings.log_level, + } +``` + +### Asynchronous Resources + +Use `async def` for resource functions that perform I/O operations (e.g., reading from a database or network) to avoid blocking the server. + +```python +import aiofiles +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +@mcp.resource("file:///app/data/important_log.txt", mime_type="text/plain") +async def read_important_log() -> str: + """Reads content from a specific log file asynchronously.""" + try: + async with aiofiles.open("/app/data/important_log.txt", mode="r") as f: + content = await f.read() + return content + except FileNotFoundError: + return "Log file not found." +``` + +## (Alternative) Defining Static Resources + +While `@mcp.resource` is ideal for dynamic content, you can directly register pre-defined resources (like static files or simple text) using `mcp.add_resource()` and concrete `Resource` subclasses. + +```python +from pathlib import Path +from fastmcp import FastMCP +from fastmcp.resources import FileResource, TextResource, DirectoryResource + +mcp = FastMCP(name="DataServer") + +# 1. Exposing a static file directly +readme_path = Path("./README.md").resolve() +if readme_path.exists(): + # Use a file:// URI scheme + readme_resource = FileResource( + uri=f"file://{readme_path.as_posix()}", + path=readme_path, # Path to the actual file + name="README File", + description="The project's README.", + mime_type="text/markdown", + tags={"documentation"} + ) + mcp.add_resource(readme_resource) + +# 2. Exposing simple, predefined text +notice_resource = TextResource( + uri="resource://notice", + name="Important Notice", + text="System maintenance scheduled for Sunday.", + tags={"notification"} +) +mcp.add_resource(notice_resource) + +# 3. Exposing a directory listing +data_dir_path = Path("./app_data").resolve() +if data_dir_path.is_dir(): + data_listing_resource = DirectoryResource( + uri="resource://data-files", + path=data_dir_path, # Path to the directory + name="Data Directory Listing", + description="Lists files available in the data directory.", + recursive=False # Set to True to list subdirectories + ) + mcp.add_resource(data_listing_resource) # Returns JSON list of files +``` + +**Common Resource Classes:** + +- `TextResource`: For simple string content. +- `BinaryResource`: For raw `bytes` content. +- `FileResource`: Reads content from a local file path. Handles text/binary modes and lazy reading. +- `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`). +- `DirectoryResource`: Lists files in a local directory (returns JSON). +- (`FunctionResource`: Internal class used by `@mcp.resource`). + +Use these when the content is static or sourced directly from a file/URL, bypassing the need for a dedicated Python function. + +## Defining Resource Templates + +Resource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature. + +```python +from fastmcp import FastMCP + +mcp = FastMCP(name="DataServer") + +# Template URI includes {city} placeholder +@mcp.resource("data://weather/{city}") +# Function accepts 'city' parameter matching the placeholder +def get_weather_for_city(city: str) -> dict: + """Provides weather information for a specific city.""" + print(f"Server: Generating weather for city: {city}...") + # In reality, call a weather API using the 'city' parameter + temp = 20 + len(city) % 5 # Dummy logic + condition = "Sunny" if len(city) % 2 == 0 else "Cloudy" + return {"city": city.capitalize(), "temperature": temp, "unit": "celsius", "condition": condition} + +# Template with an integer parameter +@mcp.resource("users://{user_id}/profile") +async def get_user_profile(user_id: int) -> dict: + """Retrieves a user's profile information by ID.""" + print(f"Server: Generating profile for user ID: {user_id}...") + # In reality, fetch from database using user_id + # FastMCP uses Pydantic to auto-convert the string URI part to int + if user_id == 1: + return {"id": user_id, "name": "Alice", "email": "alice@example.com", "status": "active"} + elif user_id == 2: + return {"id": user_id, "name": "Bob", "email": "bob@example.com", "status": "inactive"} + else: + # Example of returning an error structure + return {"error": f"User with ID {user_id} not found"} +``` + +**How Templates Work:** + +1. **Definition:** When FastMCP sees `{...}` placeholders in the `@resource` URI and matching function parameters, it registers a `ResourceTemplate`. +2. **Discovery:** Clients list templates via `resources/listResourceTemplates`. +3. **Request & Matching:** A client requests a specific URI, e.g., `data://weather/london`. FastMCP matches this to the `data://weather/{city}` template. +4. **Parameter Extraction:** It extracts the parameter value: `city="london"`. +5. **Type Conversion & Function Call:** It converts the extracted string `"london"` to the type hinted in the function (`str` in this case) and calls `get_weather_for_city(city="london")`. For `users://1/profile`, it converts `"1"` to `int` before calling `get_user_profile(user_id=1)`. +6. **Response:** The function's return value is formatted (e.g., dict to JSON) and sent back as the content of the requested resource URI (`data://weather/london`). + +Templates provide a powerful way to expose parameterized data access points following REST-like principles. + +## Server Behavior: Handling Duplicate Resources + +You can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization. + +```python +from fastmcp import FastMCP +from fastmcp.settings import DuplicateBehavior + +mcp = FastMCP( + name="ResourceServer", + on_duplicate_resources=DuplicateBehavior.ERROR # Raise error on duplicates +) + +@mcp.resource("data://config") +def get_config_v1(): return {"version": 1} + +# This registration attempt will raise a ValueError because +# "data://config" is already registered and the behavior is ERROR. +# @mcp.resource("data://config") +# def get_config_v2(): return {"version": 2} +``` + +The `DuplicateBehavior` enum options are: + +- `WARN` (default): Logs a warning, and the new resource/template replaces the old one. +- `ERROR`: Raises a `ValueError`, preventing the duplicate registration. +- `REPLACE`: Silently replaces the existing resource/template with the new one. +- `IGNORE`: Keeps the original resource/template and ignores the new registration attempt. \ No newline at end of file diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx new file mode 100644 index 000000000..feed85711 --- /dev/null +++ b/docs/servers/tools.mdx @@ -0,0 +1,335 @@ +--- +title: Tools +sidebarTitle: Tools +description: Expose functions as executable capabilities for your MCP client. +icon: wrench +--- + +Tools are the core building blocks that allow your LLM to interact with external systems, execute code, and access data that isn't in its training data. In FastMCP, tools are Python functions exposed to LLMs through the MCP protocol. + +## What Are Tools? + +Tools in FastMCP transform regular Python functions into capabilities that LLMs can invoke during conversations. When an LLM decides to use a tool: + +1. It sends a request with parameters based on the tool's schema. +2. FastMCP validates these parameters against your function's signature. +3. Your function executes with the validated inputs. +4. The result is returned to the LLM, which can use it in its response. + +This allows LLMs to perform tasks like querying databases, calling APIs, making calculations, or accessing files—extending their capabilities beyond what's in their training data. + +## Defining Tools + +### The `@tool` Decorator + +Creating a tool is as simple as decorating a Python function with `@mcp.tool()`: + +```python +from fastmcp import FastMCP + +mcp = FastMCP(name="CalculatorServer") + +@mcp.tool() +def add(a: int, b: int) -> int: + """Adds two integer numbers together.""" + return a + b +``` + +When this tool is registered, FastMCP automatically: +- Uses the function name (`add`) as the tool name. +- Uses the function's docstring (`Adds two integer numbers...`) as the tool description. +- Generates an input schema based on the function's parameters and type annotations. +- Handles parameter validation and error reporting. + + +The way you define your Python function dictates how the tool appears and behaves for the LLM client. + +### Type Annotations + +Type annotations are crucial. They: +1. Inform the LLM about the expected type for each parameter. +2. Allow FastMCP to validate the data received from the client. +3. Are used to generate the tool's input schema for the MCP protocol. + +FastMCP supports standard Python type annotations, including those from the `typing` module and Pydantic. + +```python +from typing import Literal, Optional, Union +from pydantic import BaseModel, Field + +# Example using various type hints +@mcp.tool() +def process_data( + data: list[float], # List of floats + operation: Literal["sum", "average", "max"], # Fixed choices + precision: int = 2, # Optional int with default + description: str | None = None # Optional string (can be None) +) -> dict: + """Process numerical data with the specified operation.""" + result = 0.0 + if operation == "sum": + result = sum(data) + elif operation == "average": + result = sum(data) / len(data) if data else 0.0 + elif operation == "max": + result = float(max(data)) if data else 0.0 + + return { + "operation": operation, + "result": round(result, precision), + "description": description + } +``` + +**Supported Type Annotation Examples:** + +| Type Annotation | Example | Description | +| :---------------------- | :---------------------------- | :---------------------------------- | +| Basic types | `int`, `float`, `str`, `bool` | Simple scalar values | +| Container types | `list[str]`, `dict[str, int]` | Collections of items | +| Optional types | `Optional[float]`, `float\|None`| Parameters that may be null/omitted | +| Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types | +| Literal types | `Literal["A", "B"]` | Parameters with specific allowed values | +| Pydantic models | `UserData` | Complex structured data (see below) | + + +**Automatic JSON Parsing:** FastMCP intelligently handles arguments. If a client sends a string that looks like valid JSON (e.g., `"['a', 'b']"`) for a parameter hinted as a structured type (like `list[str]` or a Pydantic model), FastMCP will automatically attempt to parse the JSON string into the expected Python object before validation. This improves robustness when interacting with various clients. + + +### Required vs. Optional Parameters + +Parameters in your function signature are considered **required** unless they have a default value. + +```python +@mcp.tool() +def search_products( + query: str, # Required - no default value + max_results: int = 10, # Optional - has default value + sort_by: str = "relevance" # Optional - has default value +) -> list[dict]: + """Search the product catalog.""" + # Implementation... + print(f"Searching for '{query}', max {max_results}, sorted by {sort_by}") + return [{"id": 1, "name": "Sample Product"}] +``` + +In this example, the LLM *must* provide a `query`. If `max_results` or `sort_by` are omitted, their default values will be used. + +### Structured Inputs + +For tools requiring complex, nested, or well-validated inputs, use Pydantic models. Define a `BaseModel` and use it as a type hint for a parameter. + +```python +from pydantic import BaseModel, Field +from typing import Optional +from datetime import date + +class ReservationRequest(BaseModel): + guest_name: str = Field(description="Full name of the guest making the reservation.") + check_in: date + check_out: date + room_type: Literal["standard", "deluxe", "suite"] = Field(default="standard", description="Type of room requested.") + guests: int = Field(gt=0, description="Number of guests (must be positive).") + special_requests: Optional[str] = Field(default=None, description="Any special requests for the stay.") + +@mcp.tool() +def make_reservation(request: ReservationRequest) -> dict: + """Creates a new hotel reservation based on the provided details.""" + # Pydantic automatically validates the incoming 'request' data + # against the ReservationRequest model before this function runs. + print(f"Making reservation for {request.guest_name}...") + # Implementation... + return { + "reservation_id": "R12345", + "status": "confirmed", + "guest": request.guest_name, + "dates": f"{request.check_in} to {request.check_out}" + } +``` + +Using Pydantic models provides: +- Clear, self-documenting structure for complex inputs. +- Built-in data validation (e.g., `gt=0`, date parsing). +- Automatic generation of detailed JSON schemas for the LLM. +- Easy handling of optional fields and default values. + +### Metadata + +While FastMCP infers the name and description from your function, you can override these and add tags using arguments to the `@mcp.tool` decorator: + +```python +@mcp.tool( + name="find_products", # Custom tool name for the LLM + description="Search the product catalog with optional category filtering.", # Custom description + tags={"catalog", "search"} # Optional tags for organization/filtering +) +def search_products_implementation(query: str, category: str | None = None) -> list[dict]: + """Internal function description (ignored if description is provided above).""" + # Implementation... + print(f"Searching for '{query}' in category '{category}'") + return [{"id": 2, "name": "Another Product"}] +``` + +- **`name`**: Sets the explicit tool name exposed via MCP. +- **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose. +- **`tags`**: A set of strings used to categorize the tool. Clients *might* use tags to filter or group available tools. + + +### Async Tools + +FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as tools. + +```python +# Synchronous tool (suitable for CPU-bound or quick tasks) +@mcp.tool() +def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """Calculate the distance between two coordinates.""" + # Implementation... + return 42.5 + +# Asynchronous tool (ideal for I/O-bound operations) +@mcp.tool() +async def fetch_weather(city: str) -> dict: + """Retrieve current weather conditions for a city.""" + # Use 'async def' for operations involving network calls, file I/O, etc. + # This prevents blocking the server while waiting for external operations. + async with aiohttp.ClientSession() as session: + async with session.get(f"https://api.example.com/weather/{city}") as response: + # Check response status before returning + response.raise_for_status() + return await response.json() +``` + +Use `async def` when your tool needs to perform operations that might wait for external systems (network requests, database queries, file access) to keep your server responsive. + +### Return Values + +FastMCP automatically converts the value returned by your function into the appropriate MCP content format for the client: + +- **`str`**: Sent as `TextContent`. +- **`dict`, `list`, Pydantic `BaseModel`**: Serialized to a JSON string and sent as `TextContent`. +- **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (often within an `EmbeddedResource`). +- **`fastmcp.utilities.types.Image`**: A helper class to easily return image data. Sent as `ImageContent`. +- **`None`**: Results in an empty response (no content is sent back to the client). + +```python +from fastmcp.utilities.types import Image +from PIL import Image as PILImage +import io + +@mcp.tool() +def generate_image(width: int, height: int, color: str) -> Image: + """Generates a solid color image.""" + # Create image using Pillow + img = PILImage.new("RGB", (width, height), color=color) + + # Save to a bytes buffer + buffer = io.BytesIO() + img.save(buffer, format="PNG") + img_bytes = buffer.getvalue() + + # Return using FastMCP's Image helper + return Image(data=img_bytes, format="png") + +@mcp.tool() +def do_nothing() -> None: + """This tool performs an action but returns no data.""" + print("Performing a side effect...") + return None +``` + +### Error Handling + +If your tool encounters an error, simply raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.). + +```python +@mcp.tool() +def divide(a: float, b: float) -> float: + """Divide a by b.""" + if b == 0: + # Raise a standard exception + raise ValueError("Division by zero is not allowed.") + if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): + raise TypeError("Both arguments must be numbers.") + return a / b +``` + +FastMCP automatically catches exceptions raised within your tool function: +1. It converts the exception into an MCP error response, typically including the exception type and message. +2. This error response is sent back to the client/LLM. +3. The LLM can then inform the user or potentially try the tool again with different arguments. + +Using informative exceptions helps the LLM understand failures and react appropriately. + +### Using Context in Tools + +Tools can access MCP features like logging, reading resources, or reporting progress through the `Context` object. To use it, add a parameter to your tool function with the type hint `Context`. + +```python +from fastmcp import FastMCP, Context + +mcp = FastMCP(name="ContextDemo") + +@mcp.tool() +async def process_data(data_uri: str, ctx: Context) -> dict: + """Process data from a resource with progress reporting.""" + await ctx.info(f"Processing data from {data_uri}") + + # Read a resource + resource = await ctx.read_resource(data_uri) + data = resource[0].content if resource else "" + + # Report progress + await ctx.report_progress(progress=50, total=100) + + # Example request to the client's LLM for help + summary = await ctx.sample(f"Summarize this in 10 words: {data[:200]}") + + await ctx.report_progress(progress=100, total=100) + return { + "length": len(data), + "summary": summary.text + } +``` + +The Context object provides access to: + +- **Logging**: `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()` +- **Progress Reporting**: `ctx.report_progress(progress, total)` +- **Resource Access**: `ctx.read_resource(uri)` +- **LLM Sampling**: `ctx.sample(...)` +- **Request Information**: `ctx.request_id`, `ctx.client_id` + +For full documentation on the Context object and all its capabilities, see the [Context Object](/server/context) page. + +## Server Behavior + +### Duplicate Tools + +You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance. + +```python +from fastmcp import FastMCP +from fastmcp.settings import DuplicateBehavior + +mcp = FastMCP( + name="StrictServer", + # Configure behavior for duplicate tool names + on_duplicate_tools=DuplicateBehavior.ERROR +) + +@mcp.tool() +def my_tool(): return "Version 1" + +# This will now raise a ValueError because 'my_tool' already exists +# and on_duplicate_tools is set to ERROR. +# @mcp.tool() +# def my_tool(): return "Version 2" +``` + +The `DuplicateBehavior` enum options are: + +- `WARN` (default): Logs a warning and the new tool replaces the old one. +- `ERROR`: Raises a `ValueError`, preventing the duplicate registration. +- `REPLACE`: Silently replaces the existing tool with the new one. +- `IGNORE`: Keeps the original tool and ignores the new registration attempt. \ No newline at end of file diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index e1b95bd4c..4e2a61818 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -133,7 +133,6 @@ def _import_server(file: Path, server_object: str | None = None): sys.exit(1) module = importlib.util.module_from_spec(spec) - breakpoint() spec.loader.exec_module(module) # If no object specified, try common server names diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index a86c44976..ffe22daa7 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -495,15 +495,20 @@ class FastMCP(Generic[LifespanResultT]): self._mcp_server.create_initialization_options(), ) - async def run_sse_async(self) -> None: + async def run_sse_async( + self, + host: str | None = None, + port: int | None = None, + log_level: str | None = None, + ) -> None: """Run the server using SSE transport.""" starlette_app = self.sse_app() config = uvicorn.Config( starlette_app, - host=self.settings.host, - port=self.settings.port, - log_level=self.settings.log_level.lower(), + host=host or self.settings.host, + port=port or self.settings.port, + log_level=log_level or self.settings.log_level.lower(), ) server = uvicorn.Server(config) await server.serve() diff --git a/uv.lock b/uv.lock index 72cdc17b9..3db7d775a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.10" [[package]] @@ -128,7 +127,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -231,7 +230,7 @@ name = "fancycompleter" version = "0.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline", marker = "sys_platform == 'win32'" }, + { name = "pyreadline", marker = "platform_system == 'Windows'" }, { name = "pyrepl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/95/649d135442d8ecf8af5c7e235550c628056423c96c4bc6787348bdae9248/fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272", size = 10866 } @@ -255,6 +254,7 @@ wheels = [ [[package]] name = "fastmcp" +version = "2.1.1.dev3+4f58d82" source = { editable = "." } dependencies = [ { name = "dotenv" }, @@ -351,15 +351,15 @@ wheels = [ [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, ] [[package]] @@ -415,7 +415,7 @@ wheels = [ [[package]] name = "ipython" -version = "8.34.0" +version = "8.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -430,9 +430,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/18/1a60aa62e9d272fcd7e658a89e1c148da10e1a5d38edcbcd834b52ca7492/ipython-8.34.0.tar.gz", hash = "sha256:c31d658e754673ecc6514583e7dda8069e47136eb62458816b7d1e6625948b5a", size = 5508477 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/77/7d1501e8b539b179936e0d5969b578ed23887be0ab8c63e0120b825bda3e/ipython-8.35.0.tar.gz", hash = "sha256:d200b7d93c3f5883fc36ab9ce28a18249c7706e51347681f80a0aef9895f2520", size = 5605027 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/78/45615356bb973904856808183ae2a5fba1f360e9d682314d79766f4b88f2/ipython-8.34.0-py3-none-any.whl", hash = "sha256:0419883fa46e0baa182c5d50ebb8d6b49df1889fdb70750ad6d8cfe678eda6e3", size = 826731 }, + { url = "https://files.pythonhosted.org/packages/91/bf/17ffca8c8b011d0bac90adb5d4e720cb3ae1fe5ccfdfc14ca31f827ee320/ipython-8.35.0-py3-none-any.whl", hash = "sha256:e6b7470468ba6f1f0a7b116bb688a3ece2f13e2f94138e508201fad677a788ba", size = 830880 }, ] [[package]] @@ -639,7 +639,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.2" +version = "2.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -647,9 +647,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/41/832125a41fe098b58d1fdd04ae819b4dc6b34d6b09ed78304fd93d4bc051/pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e", size = 784742 } +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/c2/0f3baea344d0b15e35cb3e04ad5b953fa05106b76efbf4c782a3f47f22f5/pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7", size = 443295 }, + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, ] [[package]] @@ -781,15 +781,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/05/1b/ea40363be00560804 [[package]] name = "pyright" -version = "1.1.398" +version = "1.1.399" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 }, ] [[package]] @@ -999,27 +999,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.4" +version = "0.11.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } +sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, - { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, - { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, - { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, - { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, - { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, - { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, - { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, - { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, - { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, - { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, - { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, - { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, - { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, - { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, - { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, - { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, + { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, + { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 }, + { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 }, + { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, + { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, + { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, + { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, + { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, + { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, + { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, + { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, + { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, + { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, + { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, + { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, + { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, + { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, ] [[package]] @@ -1189,11 +1189,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.13.1" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, ] [[package]] @@ -1210,11 +1210,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]]