diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7f921d0f5cae35693337db95852477b14d6708e..143d664f75d9b67c9ac22e19b25a008547094be1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -fail_fast: true +fail_fast: false repos: - repo: https://github.com/abravalheri/validate-pyproject diff --git a/CLAUDE.md b/CLAUDE.md index 24f9846b1a59ddf7f80d99ab3ae9b333668de06a..1da05926021b342f6e80cb398b575985b5196372 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,4 +27,9 @@ Only use HTTP transport when testing network-specific features. Prefer Streamabl # Only when network testing is required async with Client(transport=StreamableHttpTransport(server_url)) as client: result = await client.ping() -``` \ No newline at end of file +``` + +## Development Workflow + +- You must always run pre-commit if you open a PR, because it is run as part of a required check. +- When opening PRs, apply labels appropriately for bugs/breaking changes/enhancements/features. Generally, improvements are enhancements (not features) unless told otherwise. \ No newline at end of file diff --git a/docs/changelog.mdx b/docs/changelog.mdx index d99e49e8367c577f5525dfc12d14472d547326f9..284b863440d5c6d7cc0e196171576ab6ffa4e4d6 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -1,5 +1,5 @@ --- -mode: center +icon: "list-check" --- diff --git a/docs/clients/advanced-features.mdx b/docs/clients/advanced-features.mdx deleted file mode 100644 index cee3c461b4a48c595e85b4969874b697407da541..0000000000000000000000000000000000000000 --- a/docs/clients/advanced-features.mdx +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: Advanced Features -sidebarTitle: Advanced Features -description: Learn about the advanced features of the FastMCP Client. -icon: stars ---- - -import { VersionBadge } from '/snippets/version-badge.mdx' - -In addition to basic server interaction, FastMCP clients can also handle more advanced features and server interaction patterns. The `Client` constructor accepts additional configuration to handle these server requests. - - -To enable many of these features, you must provide an appropriate handler or callback function. For example. In most cases, if you do not provide a handler, FastMCP's default handler will emit a `DEBUG` level log. - - -## Logging and Notifications - - -MCP servers can emit logs to clients. To process these logs, you can provide a `log_handler` to the client. - -The `log_handler` must be an async function that accepts a single argument, which is an instance of `fastmcp.client.logging.LogMessage`. This has attributes like `level`, `logger`, and `data`. - -```python {2, 12} -from fastmcp import Client -from fastmcp.client.logging import LogMessage - -async def log_handler(message: LogMessage): - level = message.level.upper() - logger = message.logger or 'default' - data = message.data - print(f"[Server Log - {level}] {logger}: {data}") - -client_with_logging = Client( - ..., - log_handler=log_handler, -) -``` -## Progress Monitoring - - - -MCP servers can report progress during long-running operations. The client can set a progress handler to receive and process these updates. - -```python {2, 13} -from fastmcp import Client -from fastmcp.client.progress import ProgressHandler - -async def my_progress_handler( - progress: float, - total: float | None, - message: str | None -) -> None: - print(f"Progress: {progress} / {total} ({message})") - -client = Client( - ..., - progress_handler=my_progress_handler -) -``` - -By default, FastMCP uses a handler that logs progress updates at the debug level. This default handler properly handles cases where `total` or `message` might be None. - -You can override the progress handler for specific tool calls: - -```python -# Client uses the default debug logger for progress -client = Client(...) - -async with client: - # Use default progress handler (debug logging) - result1 = await client.call_tool("long_task", {"param": "value"}) - - # Override with custom progress handler just for this call - result2 = await client.call_tool( - "another_task", - {"param": "value"}, - progress_handler=my_progress_handler - ) -``` - -A typical progress update includes: -- Current progress value (e.g., 2 of 5 steps completed) -- Total expected value (may be None) -- Status message (may be None) - -## LLM Sampling - - - -MCP Servers can request LLM completions from clients. The client can provide a `sampling_handler` to handle these requests. The sampling handler receives a list of messages and other parameters from the server, and should return a string completion. - -The following example uses the `marvin` library to generate a completion: - -```python {8-17, 21} -import marvin -from fastmcp import Client -from fastmcp.client.sampling import ( - SamplingMessage, - SamplingParams, - RequestContext, -) - -async def sampling_handler( - messages: list[SamplingMessage], - params: SamplingParams, - context: RequestContext -) -> str: - return await marvin.say_async( - message=[m.content.text for m in messages], - instructions=params.systemPrompt, - ) - -client = Client( - ..., - sampling_handler=sampling_handler, -) -``` - - -## Roots - - - -Roots are a way for clients to inform servers about the resources they have access to or certain boundaries on their access. The server can use this information to adjust behavior or provide more accurate responses. - -Servers can request roots from clients, and clients can notify servers when their roots change. - -To set the roots when creating a client, users can either provide a list of roots (which can be a list of strings) or an async function that returns a list of roots. - - -```python Static Roots {5} -from fastmcp import Client - -client = Client( - ..., - roots=["/path/to/root1", "/path/to/root2"], -) -``` -```python Dynamic Roots Callback {4-6, 10} -from fastmcp import Client -from fastmcp.client.roots import RequestContext - -async def roots_callback(context: RequestContext) -> list[str]: - print(f"Server requested roots (Request ID: {context.request_id})") - return ["/path/to/root1", "/path/to/root2"] - -client = Client( - ..., - roots=roots_callback, -) -``` - \ No newline at end of file diff --git a/docs/clients/auth/bearer.mdx b/docs/clients/auth/bearer.mdx index edc41e32d1a0069496622ce1b540978f46b90b48..478e1a95765ea71c700f5e6d14ff03092e5d43fd 100644 --- a/docs/clients/auth/bearer.mdx +++ b/docs/clients/auth/bearer.mdx @@ -3,7 +3,7 @@ title: Bearer Token Authentication sidebarTitle: Bearer Auth description: Authenticate your FastMCP client with a Bearer token. icon: key -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/clients/auth/oauth.mdx b/docs/clients/auth/oauth.mdx index e9d2e5a29e84f0418615ff50aa57facd4edb4a82..31b7fbdf429b20ea5ec8763ff50d5fd3337962d4 100644 --- a/docs/clients/auth/oauth.mdx +++ b/docs/clients/auth/oauth.mdx @@ -3,7 +3,7 @@ title: OAuth Authentication sidebarTitle: OAuth description: Authenticate your FastMCP client via OAuth 2.1. icon: window -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index 3c4db65aa091d141505ca17bf6aa2fee98342cf8..c56ce634e920bb32c14251cf44de6b13509d17b3 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -1,7 +1,7 @@ --- -title: Client Overview +title: The FastMCP Client sidebarTitle: Overview -description: Learn how to use the FastMCP Client to interact with MCP servers. +description: Programmatic client for interacting with MCP servers through a well-typed, Pythonic interface. icon: user-robot --- @@ -9,270 +9,213 @@ import { VersionBadge } from '/snippets/version-badge.mdx' -The `fastmcp.Client` provides a high-level, asynchronous interface for interacting with any Model Context Protocol (MCP) server, whether it's built with FastMCP or another implementation. It simplifies communication by handling protocol details and connection management. +The central piece of MCP client applications is the `fastmcp.Client` class. This class provides a **programmatic interface** for interacting with any Model Context Protocol (MCP) server, handling protocol details and connection management automatically. -## FastMCP Client +The FastMCP Client is designed for deterministic, controlled interactions rather than autonomous behavior, making it ideal for: -The FastMCP Client architecture separates the protocol logic (`Client`) from the connection mechanism (`Transport`). +- **Testing MCP servers** during development +- **Building deterministic applications** that need reliable MCP interactions +- **Creating the foundation for agentic or LLM-based clients** with structured, type-safe operations -- **`Client`**: Handles sending MCP requests (like `tools/call`, `resources/read`), receiving responses, and managing callbacks. -- **`Transport`**: Responsible for establishing and maintaining the connection to the server (e.g., via WebSockets, SSE, Stdio, or in-memory). +All client operations require using the `async with` context manager for proper connection lifecycle management. -### Transports -Clients must be initialized with a `transport`. You can either provide an already instantiated transport object, or provide a transport source and let FastMCP attempt to infer the correct transport to use. + +This is not an agentic client - it requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems. + -The following inference rules are used to determine the appropriate `ClientTransport` based on the input type: +## Creating a Client -1. **`ClientTransport` Instance**: If you provide an already instantiated transport object, it's used directly. -2. **`FastMCP` Instance**: Creates a `FastMCPTransport` for efficient in-memory communication (ideal for testing). This also works with a **FastMCP 1.0 server** created via `mcp.server.fastmcp.FastMCP`. -3. **`Path` or `str` pointing to an existing file**: - * If it ends with `.py`: Creates a `PythonStdioTransport` to run the script using `python`. - * If it ends with `.js`: Creates a `NodeStdioTransport` to run the script using `node`. -4. **`AnyUrl` or `str` pointing to a URL that begins with `http://` or `https://`**: - * Creates a `StreamableHttpTransport` -5. **`MCPConfig` or dictionary matching MCPConfig schema**: Creates a client that connects to one or more MCP servers specified in the config. -6. **Other**: Raises a `ValueError` if the type cannot be inferred. +Creating a client is straightforward. You provide a server source and the client automatically infers the appropriate transport mechanism. ```python import asyncio from fastmcp import Client, FastMCP -# Example transports (more details in Transports page) -server_instance = FastMCP(name="TestServer") # In-memory server -http_url = "https://example.com/mcp" # HTTP server URL -server_script = "my_mcp_server.py" # Path to a Python server file +# In-memory server (ideal for testing) +server = FastMCP("TestServer") +client = Client(server) -# Client automatically infers the transport type -client_in_memory = Client(server_instance) -client_http = Client(http_url) +# HTTP server +client = Client("https://example.com/mcp") -client_stdio = Client(server_script) +# Local Python script +client = Client("my_mcp_server.py") -print(client_in_memory.transport) -print(client_http.transport) -print(client_stdio.transport) +async def main(): + async with client: + # Basic server interaction + await client.ping() + + # List available operations + tools = await client.list_tools() + resources = await client.list_resources() + prompts = await client.list_prompts() + + # Execute operations + result = await client.call_tool("example_tool", {"param": "value"}) + print(result) -# Expected Output (types may vary slightly based on environment): -# -# -# +asyncio.run(main()) ``` -You can also initialize a client from an MCP configuration dictionary or `MCPConfig` file: +## Client-Transport Architecture -```python -from fastmcp import Client +The FastMCP Client separates concerns between protocol and connection: -config = { - "mcpServers": { - "local": {"command": "python", "args": ["local_server.py"]}, - "remote": {"url": "https://example.com/mcp"}, - } -} +- **`Client`**: Handles MCP protocol operations (tools, resources, prompts) and manages callbacks +- **`Transport`**: Establishes and maintains the connection (WebSockets, HTTP, Stdio, in-memory) + +### Transport Inference + +The client automatically infers the appropriate transport based on the input: + +1. **`FastMCP` instance** → In-memory transport (perfect for testing) +2. **File path ending in `.py`** → Python Stdio transport +3. **File path ending in `.js`** → Node.js Stdio transport +4. **URL starting with `http://` or `https://`** → HTTP transport +5. **`MCPConfig` dictionary** → Multi-server client + +```python +from fastmcp import Client, FastMCP -client_config = Client(config) +# Examples of transport inference +client_memory = Client(FastMCP("TestServer")) +client_script = Client("./server.py") +client_http = Client("https://api.example.com/mcp") ``` + -For more control over connection details (like headers for SSE, environment variables for Stdio), you can instantiate the specific `ClientTransport` class yourself and pass it to the `Client`. See the [Transports](/clients/transports) page for details. +For testing and development, always prefer the in-memory transport by passing a `FastMCP` server directly to the client. This eliminates network complexity and separate processes. -### Multi-Server Clients +## Configuration-Based Clients -FastMCP supports creating clients that connect to multiple MCP servers through a single client interface using a standard MCP configuration format (`MCPConfig`). This configuration approach makes it easy to connect to multiple specialized servers or create composable systems with a simple, declarative syntax. - - -The MCP configuration format follows an emerging standard and may evolve as the specification matures. FastMCP will strive to maintain compatibility with future versions, but be aware that field names or structure might change. - - -When you create a client with an `MCPConfig` containing multiple servers: +Create clients from MCP configuration dictionaries, which can include multiple servers. While there is no official standard for MCP configuration format, FastMCP follows established conventions used by tools like Claude Desktop. -1. FastMCP creates a composite client that internally mounts all servers using their config names as prefixes -2. Tools and resources from each server are accessible with appropriate prefixes in the format `servername_toolname` and `protocol://servername/resource/path` -3. You interact with this as a single unified client, with requests automatically routed to the appropriate server +### Configuration Format ```python -from fastmcp import Client - -# Create a standard MCP configuration with multiple servers config = { "mcpServers": { - # A remote HTTP server - "weather": { - "url": "https://weather-api.example.com/mcp", - "transport": "streamable-http" + "server_name": { + # Remote HTTP/SSE server + "transport": "streamable-http", # or "sse" + "url": "https://api.example.com/mcp", + "headers": {"Authorization": "Bearer token"}, + "auth": "oauth" # or bearer token string }, - # A local server running via stdio - "assistant": { + "local_server": { + # Local stdio server + "transport": "stdio" "command": "python", - "args": ["./my_assistant_server.py"], - "env": {"DEBUG": "true"} + "args": ["./server.py", "--verbose"], + "env": {"DEBUG": "true"}, + "cwd": "/path/to/server", } } } - -# Create a client that connects to both servers -client = Client(config) - -async def main(): - async with client: - # Access tools from different servers with prefixes - weather_data = await client.call_tool("weather_get_forecast", {"city": "London"}) - response = await client.call_tool("assistant_answer_question", {"question": "What's the capital of France?"}) - - # Access resources with prefixed URIs - weather_icons = await client.read_resource("weather://weather/icons/sunny") - templates = await client.read_resource("resource://assistant/templates/list") - - print(f"Weather: {weather_data}") - print(f"Assistant: {response}") - -if __name__ == "__main__": - asyncio.run(main()) ``` -If your configuration has only a single server, FastMCP will create a direct client to that server without any prefixing. +### Multi-Server Example -## Client Usage +```python +config = { + "mcpServers": { + "weather": {"url": "https://weather-api.example.com/mcp"}, + "assistant": {"command": "python", "args": ["./assistant_server.py"]} + } +} -### Connection Lifecycle +client = Client(config) -The client operates asynchronously and must be used within an `async with` block. This context manager handles establishing the connection, initializing the MCP session, and cleaning up resources upon exit. +async with client: + # Tools are prefixed with server names + weather_data = await client.call_tool("weather_get_forecast", {"city": "London"}) + response = await client.call_tool("assistant_answer_question", {"question": "What's the capital of France?"}) + + # Resources use prefixed URIs + icons = await client.read_resource("weather://weather/icons/sunny") + templates = await client.read_resource("resource://assistant/templates/list") +``` -```python -import asyncio -from fastmcp import Client +## Connection Lifecycle -client = Client("my_mcp_server.py") # Assumes my_mcp_server.py exists +The client operates asynchronously and uses context managers for connection management: -async def main(): - # Connection is established here +```python +async def example(): + client = Client("my_mcp_server.py") + + # Connection established here async with client: - print(f"Client connected: {client.is_connected()}") - - # Make MCP calls within the context + print(f"Connected: {client.is_connected()}") + + # Make multiple calls within the same session tools = await client.list_tools() - print(f"Available tools: {tools}") - - if any(tool.name == "greet" for tool in tools): - result = await client.call_tool("greet", {"name": "World"}) - print(f"Greet result: {result}") - - # Connection is closed automatically here - print(f"Client connected: {client.is_connected()}") - -if __name__ == "__main__": - asyncio.run(main()) + result = await client.call_tool("greet", {"name": "World"}) + + # Connection closed automatically here + print(f"Connected: {client.is_connected()}") ``` -You can make multiple calls to the server within the same `async with` block using the established session. - -### Client Methods +## Operations -The `Client` provides methods corresponding to standard MCP requests: +FastMCP clients can interact with several types of server components: - -The standard client methods return user-friendly representations that may change as the protocol evolves. For consistent access to the complete data structure, use the `*_mcp` methods described later. - +### Tools -#### Tool Operations +Tools are server-side functions that the client can execute with arguments. -* **`list_tools()`**: Retrieves a list of tools available on the server. - ```python +```python +async with client: + # List available tools tools = await client.list_tools() - # tools -> list[mcp.types.Tool] - ``` -* **`call_tool(name: str, arguments: dict[str, Any] | None = None, timeout: float | None = None, progress_handler: ProgressHandler | None = None)`**: Executes a tool on the server. - ```python - result = await client.call_tool("add", {"a": 5, "b": 3}) - # result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...] - print(result[0].text) # Assuming TextContent, e.g., '8' - - # With timeout (aborts if execution takes longer than 2 seconds) - result = await client.call_tool("long_running_task", {"param": "value"}, timeout=2.0) - # With progress handler (to track execution progress) - result = await client.call_tool( - "long_running_task", - {"param": "value"}, - progress_handler=my_progress_handler - ) - ``` - * Arguments are passed as a dictionary. FastMCP servers automatically handle JSON string parsing for complex types if needed. - * Returns a list of content objects (usually `TextContent` or `ImageContent`). - * The optional `timeout` parameter limits the maximum execution time (in seconds) for this specific call, overriding any client-level timeout. - * The optional `progress_handler` parameter receives progress updates during execution, overriding any client-level progress handler. - -#### Resource Operations - -* **`list_resources()`**: Retrieves a list of static resources. - ```python - resources = await client.list_resources() - # resources -> list[mcp.types.Resource] - ``` -* **`list_resource_templates()`**: Retrieves a list of resource templates. - ```python - templates = await client.list_resource_templates() - # templates -> list[mcp.types.ResourceTemplate] - ``` -* **`read_resource(uri: str | AnyUrl)`**: Reads the content of a resource or a resolved template. - ```python - # Read a static resource - readme_content = await client.read_resource("file:///path/to/README.md") - # readme_content -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] - print(readme_content[0].text) # Assuming text + # Execute a tool + result = await client.call_tool("multiply", {"a": 5, "b": 3}) + print(result[0].text) # "15" +``` - # Read a resource generated from a template - weather_content = await client.read_resource("data://weather/london") - print(weather_content[0].text) # Assuming text JSON - ``` +See [Tools](/clients/tools) for detailed documentation. -#### Prompt Operations +### Resources -* **`list_prompts()`**: Retrieves available prompt templates. -* **`get_prompt(name: str, arguments: dict[str, Any] | None = None)`**: Retrieves a rendered prompt message list. +Resources are data sources that the client can read, either static or templated. -### Raw MCP Protocol Objects +```python +async with client: + # List available resources + resources = await client.list_resources() + + # Read a resource + content = await client.read_resource("file:///config/settings.json") + print(content[0].text) +``` - +See [Resources](/clients/resources) for detailed documentation. -The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly. +### Prompts - -The standard client methods (without `_mcp`) return user-friendly representations of MCP data, while `*_mcp` methods will always return the complete MCP protocol objects. As the protocol evolves, changes to these user-friendly representations may occur and could potentially be breaking. If you need consistent, stable access to the full data structure, prefer using the `*_mcp` methods. - +Prompts are reusable message templates that can accept arguments. ```python -# Standard method - returns just the list of tools -tools = await client.list_tools() -# tools -> list[mcp.types.Tool] - -# Raw MCP method - returns the full protocol object -result = await client.list_tools_mcp() -# result -> mcp.types.ListToolsResult -tools = result.tools +async with client: + # List available prompts + prompts = await client.list_prompts() + + # Get a rendered prompt + messages = await client.get_prompt("analyze_data", {"data": [1, 2, 3]}) + print(messages.messages) ``` -Available raw MCP methods: - -* **`list_tools_mcp()`**: Returns `mcp.types.ListToolsResult` -* **`call_tool_mcp(name, arguments)`**: Returns `mcp.types.CallToolResult` -* **`list_resources_mcp()`**: Returns `mcp.types.ListResourcesResult` -* **`list_resource_templates_mcp()`**: Returns `mcp.types.ListResourceTemplatesResult` -* **`read_resource_mcp(uri)`**: Returns `mcp.types.ReadResourceResult` -* **`list_prompts_mcp()`**: Returns `mcp.types.ListPromptsResult` -* **`get_prompt_mcp(name, arguments)`**: Returns `mcp.types.GetPromptResult` -* **`complete_mcp(ref, argument)`**: Returns `mcp.types.CompleteResult` +See [Prompts](/clients/prompts) for detailed documentation. -These methods are especially useful for debugging or when you need to access metadata or fields that aren't exposed by the simplified methods. +### Server Connectivity -### Additional Features - -#### Pinging the Server - -The client can be used to ping the server to verify connectivity. +Use `ping()` to verify the server is reachable: ```python async with client: @@ -280,93 +223,69 @@ async with client: print("Server is reachable") ``` -#### Session Management +## Client Configuration + +Clients can be configured with additional handlers and settings for specialized use cases. -When using stdio transports, clients support a `keep_alive` feature (enabled by default) that maintains subprocess sessions between connection contexts. You can manually control this behavior using the client's `close()` method. +### Callback Handlers -When `keep_alive=False`, the client will automatically close the session when the context manager exits. +The client supports several callback handlers for advanced server interactions: ```python from fastmcp import Client +from fastmcp.client.logging import LogMessage -client = Client("my_mcp_server.py") # keep_alive=True by default +async def log_handler(message: LogMessage): + print(f"Server log: {message.data}") -async def example(): - async with client: - await client.ping() - - async with client: - await client.ping() # Same subprocess as above -``` - - -For detailed examples and configuration options, see [Session Management in Transports](/clients/transports#session-management). - - -#### Timeouts - - - -You can control request timeouts at both the client level and individual request level: +async def progress_handler(progress: float, total: float | None, message: str | None): + print(f"Progress: {progress}/{total} - {message}") -```python -from fastmcp import Client -from fastmcp.exceptions import McpError +async def sampling_handler(messages, params, context): + # Integrate with your LLM service here + return "Generated response" -# Client with a global 5-second timeout for all requests client = Client( - my_mcp_server, - timeout=5.0 # Default timeout in seconds + "my_mcp_server.py", + log_handler=log_handler, + progress_handler=progress_handler, + sampling_handler=sampling_handler, + timeout=30.0 ) - -async with client: - # This uses the global 5-second timeout - result1 = await client.call_tool("quick_task", {"param": "value"}) - - # This specifies a 10-second timeout for this specific call - result2 = await client.call_tool("slow_task", {"param": "value"}, timeout=10.0) - - try: - # This will likely timeout - result3 = await client.call_tool("medium_task", {"param": "value"}, timeout=0.01) - except McpError as e: - # Handle timeout error - print(f"The task timed out: {e}") ``` - -Timeout behavior varies between transport types: +The `Client` constructor accepts several configuration options: -- With **SSE** transport, the per-request (tool call) timeout **always** takes precedence, regardless of which is lower. -- With **HTTP** transport, the **lower** of the two timeouts (client or tool call) takes precedence. +- `transport`: Transport instance or source for automatic inference +- `log_handler`: Handle server log messages +- `progress_handler`: Monitor long-running operations +- `sampling_handler`: Respond to server LLM requests +- `roots`: Provide local context to servers +- `timeout`: Default timeout for requests (in seconds) -For consistent behavior across all transports, we recommend explicitly setting timeouts at the individual tool call level when needed, rather than relying on client-level timeouts. - +### Transport Configuration -#### Error Handling +For detailed transport configuration (headers, authentication, environment variables), see the [Transports](/clients/transports) documentation. -When a `call_tool` request results in an error on the server (e.g., the tool function raised an exception), the `client.call_tool()` method will raise a `fastmcp.exceptions.ClientError`. +## Next Steps -```python -async def safe_call_tool(): - async with client: - try: - # Assume 'divide' tool exists and might raise ZeroDivisionError - result = await client.call_tool("divide", {"a": 10, "b": 0}) - print(f"Result: {result}") - except ClientError as e: - print(f"Tool call failed: {e}") - except ConnectionError as e: - print(f"Connection failed: {e}") - except Exception as e: - print(f"An unexpected error occurred: {e}") - -# Example Output if division by zero occurs: -# Tool call failed: Division by zero is not allowed. -``` +Explore the detailed documentation for each operation type: -Other errors, like connection failures, will raise standard Python exceptions (e.g., `ConnectionError`, `TimeoutError`). +### Core Operations +- **[Tools](/clients/tools)** - Execute server-side functions and handle results +- **[Resources](/clients/resources)** - Access static and templated resources +- **[Prompts](/clients/prompts)** - Work with message templates and argument serialization + +### Advanced Features +- **[Logging](/clients/logging)** - Handle server log messages +- **[Progress](/clients/progress)** - Monitor long-running operations +- **[Sampling](/clients/sampling)** - Respond to server LLM requests +- **[Roots](/clients/roots)** - Provide local context to servers + +### Connection Details +- **[Transports](/clients/transports)** - Configure connection methods and parameters +- **[Authentication](/clients/auth/oauth)** - Set up OAuth and bearer token authentication -The client transport often has its own error-handling mechanisms, so you can not always trap errors like those raised by `call_tool` outside of the `async with` block. Instead, you can use `call_tool_mcp()` to get the raw `mcp.types.CallToolResult` object and handle errors yourself by checking its `isError` attribute. - +The FastMCP Client is designed as a foundational tool. Use it directly for deterministic operations, or build higher-level agentic systems on top of its reliable, type-safe interface. + \ No newline at end of file diff --git a/docs/clients/logging.mdx b/docs/clients/logging.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9c28a5d251d192695b94fc61d92eb31451e71b19 --- /dev/null +++ b/docs/clients/logging.mdx @@ -0,0 +1,63 @@ +--- +title: Server Logging +sidebarTitle: Logging +description: Receive and handle log messages from MCP servers. +icon: receipt +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +MCP servers can emit log messages to clients. The client can handle these logs through a log handler callback. + +## Setting Up Log Handling + +Provide a `log_handler` function when creating the client: + +```python +from fastmcp import Client +from fastmcp.client.logging import LogMessage + +async def log_handler(message: LogMessage): + level = message.level.upper() + logger = message.logger or 'server' + data = message.data + print(f"[{level}] {logger}: {data}") + +client = Client( + "my_mcp_server.py", + log_handler=log_handler, +) +``` + +## LogMessage Structure + +The `log_handler` receives a `LogMessage` object with: + +- **`level`**: Log level (e.g., "debug", "info", "warning", "error") +- **`logger`**: Logger name (optional, may be None) +- **`data`**: The actual log message content + +```python +async def detailed_log_handler(message: LogMessage): + if message.level == "error": + print(f"ERROR: {message.data}") + elif message.level == "warning": + print(f"WARNING: {message.data}") + else: + print(f"{message.level.upper()}: {message.data}") +``` + +## Default Log Handling + +If you don't provide a custom `log_handler`, FastMCP uses a default handler that emits DEBUG level logs: + +```python +# Without custom handler - uses default DEBUG logging +client = Client("my_mcp_server.py") + +async with client: + # Server logs will be emitted at DEBUG level + await client.call_tool("some_tool") +``` \ No newline at end of file diff --git a/docs/clients/progress.mdx b/docs/clients/progress.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bd500fa2650d7ba6be945d3bf2b358293a10376a --- /dev/null +++ b/docs/clients/progress.mdx @@ -0,0 +1,59 @@ +--- +title: Progress Monitoring +sidebarTitle: Progress +description: Handle progress notifications from long-running server operations. +icon: bars-progress +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +MCP servers can report progress during long-running operations. The client can receive these updates through a progress handler. + +## Setting Up Progress Handling + +Set a progress handler when creating the client: + +```python +from fastmcp import Client + +async def my_progress_handler( + progress: float, + total: float | None, + message: str | None +) -> None: + if total is not None: + percentage = (progress / total) * 100 + print(f"Progress: {percentage:.1f}% - {message or ''}") + else: + print(f"Progress: {progress} - {message or ''}") + +client = Client( + "my_mcp_server.py", + progress_handler=my_progress_handler +) +``` + +## Per-Call Progress Handler + +Override the progress handler for specific tool calls: + +```python +async with client: + # Override with specific progress handler for this call + result = await client.call_tool( + "long_running_task", + {"param": "value"}, + progress_handler=my_progress_handler + ) +``` + +## Handler Parameters + +The progress handler receives: + +- **`progress`** (float): Current progress value +- **`total`** (float | None): Expected total value (may be None) +- **`message`** (str | None): Optional status message (may be None) + diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx new file mode 100644 index 0000000000000000000000000000000000000000..0ba4d276588704c4cfa860bb2029ce9d6969d3f6 --- /dev/null +++ b/docs/clients/prompts.mdx @@ -0,0 +1,187 @@ +--- +title: Prompts +sidebarTitle: Prompts +description: Use server-side prompt templates with automatic argument serialization. +icon: message-lines +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Prompts are reusable message templates exposed by MCP servers. They can accept arguments to generate personalized message sequences for LLM interactions. + +## Listing Prompts + +Use `list_prompts()` to retrieve all available prompt templates: + +```python +async with client: + prompts = await client.list_prompts() + # prompts -> list[mcp.types.Prompt] + + for prompt in prompts: + print(f"Prompt: {prompt.name}") + print(f"Description: {prompt.description}") + if prompt.arguments: + print(f"Arguments: {[arg.name for arg in prompt.arguments]}") +``` + +## Using Prompts + +### Basic Usage + +Request a rendered prompt using `get_prompt()` with the prompt name and arguments: + +```python +async with client: + # Simple prompt without arguments + result = await client.get_prompt("welcome_message") + # result -> mcp.types.GetPromptResult + + # Access the generated messages + for message in result.messages: + print(f"Role: {message.role}") + print(f"Content: {message.content}") +``` + +### Prompts with Arguments + +Pass arguments as a dictionary to customize the prompt: + +```python +async with client: + # Prompt with simple arguments + result = await client.get_prompt("user_greeting", { + "name": "Alice", + "role": "administrator" + }) + + # Access the personalized messages + for message in result.messages: + print(f"Generated message: {message.content}") +``` + +## Automatic Argument Serialization + + + +FastMCP automatically serializes complex arguments to JSON strings as required by the MCP specification. This allows you to pass typed objects directly: + +```python +from dataclasses import dataclass + +@dataclass +class UserData: + name: str + age: int + +async with client: + # Complex arguments are automatically serialized + result = await client.get_prompt("analyze_user", { + "user": UserData(name="Alice", age=30), # Automatically serialized to JSON + "preferences": {"theme": "dark"}, # Dict serialized to JSON string + "scores": [85, 92, 78], # List serialized to JSON string + "simple_name": "Bob" # Strings passed through unchanged + }) +``` + +The client handles serialization using `pydantic_core.to_json()` for consistent formatting. FastMCP servers can automatically deserialize these JSON strings back to the expected types. + +### Serialization Examples + +```python +async with client: + result = await client.get_prompt("data_analysis", { + # These will be automatically serialized to JSON strings: + "config": { + "format": "csv", + "include_headers": True, + "delimiter": "," + }, + "filters": [ + {"field": "age", "operator": ">", "value": 18}, + {"field": "status", "operator": "==", "value": "active"} + ], + # This remains a string: + "report_title": "Monthly Analytics Report" + }) +``` + +## Working with Prompt Results + +The `get_prompt()` method returns a `GetPromptResult` object containing a list of messages: + +```python +async with client: + result = await client.get_prompt("conversation_starter", {"topic": "climate"}) + + # Access individual messages + for i, message in enumerate(result.messages): + print(f"Message {i + 1}:") + print(f" Role: {message.role}") + print(f" Content: {message.content.text if hasattr(message.content, 'text') else message.content}") +``` + +## Raw MCP Protocol Access + +For access to the complete MCP protocol objects, use the `*_mcp` methods: + +```python +async with client: + # Raw MCP method returns full protocol object + prompts_result = await client.list_prompts_mcp() + # prompts_result -> mcp.types.ListPromptsResult + + prompt_result = await client.get_prompt_mcp("example_prompt", {"arg": "value"}) + # prompt_result -> mcp.types.GetPromptResult +``` + +## Multi-Server Clients + +When using multi-server clients, prompts are accessible without prefixing (unlike tools): + +```python +async with client: # Multi-server client + # Prompts from any server are directly accessible + result1 = await client.get_prompt("weather_prompt", {"city": "London"}) + result2 = await client.get_prompt("assistant_prompt", {"query": "help"}) +``` + +## Common Prompt Patterns + +### System Messages + +Many prompts generate system messages for LLM configuration: + +```python +async with client: + result = await client.get_prompt("system_configuration", { + "role": "helpful assistant", + "expertise": "python programming" + }) + + # Typically returns messages with role="system" + system_message = result.messages[0] + print(f"System prompt: {system_message.content}") +``` + +### Conversation Templates + +Prompts can generate multi-turn conversation templates: + +```python +async with client: + result = await client.get_prompt("interview_template", { + "candidate_name": "Alice", + "position": "Senior Developer" + }) + + # Multiple messages for a conversation flow + for message in result.messages: + print(f"{message.role}: {message.content}") +``` + + +Prompt arguments and their expected types depend on the specific prompt implementation. Check the server's documentation or use `list_prompts()` to see available arguments for each prompt. + \ No newline at end of file diff --git a/docs/clients/resources.mdx b/docs/clients/resources.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ecad582e067d647929a7408438e7cd97df5b7e24 --- /dev/null +++ b/docs/clients/resources.mdx @@ -0,0 +1,171 @@ +--- +title: Resource Operations +sidebarTitle: Resources +description: Access static and templated resources from MCP servers. +icon: folder-open +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Resources are data sources exposed by MCP servers. They can be static files or dynamic templates that generate content based on parameters. + +## Types of Resources + +MCP servers expose two types of resources: + +- **Static Resources**: Fixed content accessible via URI (e.g., configuration files, documentation) +- **Resource Templates**: Dynamic resources that accept parameters to generate content (e.g., API endpoints, database queries) + +## Listing Resources + +### Static Resources + +Use `list_resources()` to retrieve all static resources available on the server: + +```python +async with client: + resources = await client.list_resources() + # resources -> list[mcp.types.Resource] + + for resource in resources: + print(f"Resource URI: {resource.uri}") + print(f"Name: {resource.name}") + print(f"Description: {resource.description}") + print(f"MIME Type: {resource.mimeType}") +``` + +### Resource Templates + +Use `list_resource_templates()` to retrieve available resource templates: + +```python +async with client: + templates = await client.list_resource_templates() + # templates -> list[mcp.types.ResourceTemplate] + + for template in templates: + print(f"Template URI: {template.uriTemplate}") + print(f"Name: {template.name}") + print(f"Description: {template.description}") +``` + +## Reading Resources + +### Static Resources + +Read a static resource using its URI: + +```python +async with client: + # Read a static resource + content = await client.read_resource("file:///path/to/README.md") + # content -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] + + # Access text content + if hasattr(content[0], 'text'): + print(content[0].text) + + # Access binary content + if hasattr(content[0], 'blob'): + print(f"Binary data: {len(content[0].blob)} bytes") +``` + +### Resource Templates + +Read from a resource template by providing the URI with parameters: + +```python +async with client: + # Read a resource generated from a template + # For example, a template like "weather://{{city}}/current" + weather_content = await client.read_resource("weather://london/current") + + # Access the generated content + print(weather_content[0].text) # Assuming text JSON response +``` + +## Content Types + +Resources can return different content types: + +### Text Resources + +```python +async with client: + content = await client.read_resource("resource://config/settings.json") + + for item in content: + if hasattr(item, 'text'): + print(f"Text content: {item.text}") + print(f"MIME type: {item.mimeType}") +``` + +### Binary Resources + +```python +async with client: + content = await client.read_resource("resource://images/logo.png") + + for item in content: + if hasattr(item, 'blob'): + print(f"Binary content: {len(item.blob)} bytes") + print(f"MIME type: {item.mimeType}") + + # Save to file + with open("downloaded_logo.png", "wb") as f: + f.write(item.blob) +``` + +## Working with Multi-Server Clients + +When using multi-server clients, resource URIs are automatically prefixed with the server name: + +```python +async with client: # Multi-server client + # Access resources from different servers + weather_icons = await client.read_resource("weather://weather/icons/sunny") + templates = await client.read_resource("resource://assistant/templates/list") + + print(f"Weather icon: {weather_icons[0].blob}") + print(f"Templates: {templates[0].text}") +``` + +## Raw MCP Protocol Access + +For access to the complete MCP protocol objects, use the `*_mcp` methods: + +```python +async with client: + # Raw MCP methods return full protocol objects + resources_result = await client.list_resources_mcp() + # resources_result -> mcp.types.ListResourcesResult + + templates_result = await client.list_resource_templates_mcp() + # templates_result -> mcp.types.ListResourceTemplatesResult + + content_result = await client.read_resource_mcp("resource://example") + # content_result -> mcp.types.ReadResourceResult +``` + +## Common Resource URI Patterns + +Different MCP servers may use various URI schemes: + +```python +# File system resources +"file:///path/to/file.txt" + +# Custom protocol resources +"weather://london/current" +"database://users/123" + +# Generic resource protocol +"resource://config/settings" +"resource://templates/email" +``` + + +Resource URIs and their formats depend on the specific MCP server implementation. Check the server's documentation for available resources and their URI patterns. + \ No newline at end of file diff --git a/docs/clients/roots.mdx b/docs/clients/roots.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2a8d8c1f93cf11214836c133b8fdb8cb7da7f511 --- /dev/null +++ b/docs/clients/roots.mdx @@ -0,0 +1,42 @@ +--- +title: Client Roots +sidebarTitle: Roots +description: Provide local context and resource boundaries to MCP servers. +icon: folder-tree +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Roots are a way for clients to inform servers about the resources they have access to. Servers can use this information to adjust behavior or provide more relevant responses. + +## Setting Static Roots + +Provide a list of roots when creating the client: + + +```python Static Roots +from fastmcp import Client + +client = Client( + "my_mcp_server.py", + roots=["/path/to/root1", "/path/to/root2"] +) +``` + +```python Dynamic Roots Callback +from fastmcp import Client +from fastmcp.client.roots import RequestContext + +async def roots_callback(context: RequestContext) -> list[str]: + print(f"Server requested roots (Request ID: {context.request_id})") + return ["/path/to/root1", "/path/to/root2"] + +client = Client( + "my_mcp_server.py", + roots=roots_callback +) +``` + + diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx new file mode 100644 index 0000000000000000000000000000000000000000..25d035478e0ca31106b352a202cb80ba99ce44af --- /dev/null +++ b/docs/clients/sampling.mdx @@ -0,0 +1,91 @@ +--- +title: LLM Sampling +sidebarTitle: Sampling +description: Handle server-initiated LLM sampling requests. +icon: robot +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +MCP servers can request LLM completions from clients. The client handles these requests through a sampling handler callback. + +## Setting Up Sampling Handling + +Provide a `sampling_handler` function when creating the client: + +```python +from fastmcp import Client +from fastmcp.client.sampling import ( + SamplingMessage, + SamplingParams, + RequestContext, +) + +async def sampling_handler( + messages: list[SamplingMessage], + params: SamplingParams, + context: RequestContext +) -> str: + # Your LLM integration logic here + # Extract text from messages and generate a response + return "Generated response based on the messages" + +client = Client( + "my_mcp_server.py", + sampling_handler=sampling_handler, +) +``` + +## Handler Parameters + +The sampling handler receives three parameters: + +### SamplingMessage + +- **`role`**: Message role (e.g., "user", "assistant", "system") +- **`content`**: Message content (usually has `.text` attribute) + +### SamplingParams + +- **`systemPrompt`**: System prompt string (optional) +- **`maxTokens`**: Maximum tokens to generate (optional) +- **`temperature`**: Sampling temperature (optional) +- **`topP`**: Top-p sampling parameter (optional) +- **`stopSequences`**: List of stop sequences (optional) + +### RequestContext + +- **`request_id`**: Unique identifier for the sampling request + +## Basic Example + +```python +from fastmcp import Client +from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext + +async def basic_sampling_handler( + messages: list[SamplingMessage], + params: SamplingParams, + context: RequestContext +) -> str: + # Extract message content + conversation = [] + for message in messages: + content = message.content.text if hasattr(message.content, 'text') else str(message.content) + conversation.append(f"{message.role}: {content}") + + # Use the system prompt if provided + system_prompt = params.systemPrompt or "You are a helpful assistant." + + # Here you would integrate with your preferred LLM service + # This is just a placeholder response + return f"Response based on conversation: {' | '.join(conversation)}" + +client = Client( + "my_mcp_server.py", + sampling_handler=basic_sampling_handler +) +``` + diff --git a/docs/clients/tools.mdx b/docs/clients/tools.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3821725cb2ad9f075ee9247f6aded185a38ccc96 --- /dev/null +++ b/docs/clients/tools.mdx @@ -0,0 +1,143 @@ +--- +title: Tool Operations +sidebarTitle: Tools +description: Discover and execute server-side tools with the FastMCP client. +icon: wrench +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Tools are executable functions exposed by MCP servers. The FastMCP client provides methods to discover available tools and execute them with arguments. + +## Discovering Tools + +Use `list_tools()` to retrieve all tools available on the server: + +```python +async with client: + tools = await client.list_tools() + # tools -> list[mcp.types.Tool] + + for tool in tools: + print(f"Tool: {tool.name}") + print(f"Description: {tool.description}") + if tool.inputSchema: + print(f"Parameters: {tool.inputSchema}") +``` + +## Executing Tools + +### Basic Execution + +Execute a tool using `call_tool()` with the tool name and arguments: + +```python +async with client: + # Simple tool call + result = await client.call_tool("add", {"a": 5, "b": 3}) + # result -> list[mcp.types.TextContent | mcp.types.ImageContent | ...] + + # Access the result content + print(result[0].text) # Assuming TextContent, e.g., '8' +``` + +### Advanced Execution Options + +The `call_tool()` method supports additional parameters for timeout control and progress monitoring: + +```python +async with client: + # With timeout (aborts if execution takes longer than 2 seconds) + result = await client.call_tool( + "long_running_task", + {"param": "value"}, + timeout=2.0 + ) + + # With progress handler (to track execution progress) + result = await client.call_tool( + "long_running_task", + {"param": "value"}, + progress_handler=my_progress_handler + ) +``` + +**Parameters:** +- `name`: The tool name (string) +- `arguments`: Dictionary of arguments to pass to the tool (optional) +- `timeout`: Maximum execution time in seconds (optional, overrides client-level timeout) +- `progress_handler`: Progress callback function (optional, overrides client-level handler) + +## Handling Results + +Tool execution returns a list of content objects. The most common types are: + +- **`TextContent`**: Text-based results with a `.text` attribute +- **`ImageContent`**: Image data with image-specific attributes +- **`BlobContent`**: Binary data content + +```python +async with client: + result = await client.call_tool("get_weather", {"city": "London"}) + + for content in result: + if hasattr(content, 'text'): + print(f"Text result: {content.text}") + elif hasattr(content, 'data'): + print(f"Binary data: {len(content.data)} bytes") +``` + +## Error Handling + +### Exception-Based Error Handling + +By default, `call_tool()` raises a `ToolError` if the tool execution fails: + +```python +from fastmcp.exceptions import ToolError + +async with client: + try: + result = await client.call_tool("potentially_failing_tool", {"param": "value"}) + print("Tool succeeded:", result) + except ToolError as e: + print(f"Tool failed: {e}") +``` + +### Manual Error Checking + +For more granular control, use `call_tool_mcp()` which returns the raw MCP protocol object with an `isError` flag: + +```python +async with client: + result = await client.call_tool_mcp("potentially_failing_tool", {"param": "value"}) + # result -> mcp.types.CallToolResult + + if result.isError: + print(f"Tool failed: {result.content}") + else: + print(f"Tool succeeded: {result.content}") +``` + +## Argument Handling + +Arguments are passed as a dictionary to the tool: + +```python +async with client: + # Simple arguments + result = await client.call_tool("greet", {"name": "World"}) + + # Complex arguments + result = await client.call_tool("process_data", { + "config": {"format": "json", "validate": True}, + "items": [1, 2, 3, 4, 5], + "metadata": {"source": "api", "version": "1.0"} + }) +``` + + +For multi-server clients, tool names are automatically prefixed with the server name (e.g., `weather_get_forecast` for a tool named `get_forecast` on the `weather` server). + \ No newline at end of file diff --git a/docs/deployment/asgi.mdx b/docs/deployment/asgi.mdx index 06da46374011e7342b887fdb55f16ddd240c4eb2..e5e6b6c71777e20e43ff9334560d77bd667a290e 100644 --- a/docs/deployment/asgi.mdx +++ b/docs/deployment/asgi.mdx @@ -48,7 +48,7 @@ Both approaches return a Starlette application that can be integrated with other The returned app stores the `FastMCP` instance on `app.state.fastmcp_server`, so you can access it from custom middleware or routes via `request.app.state.fastmcp_server`. -The MCP server's endpoint is mounted at the root path `/mcp` for Streamable HTTP transport, and `/sse` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method: +The MCP server's endpoint is mounted at the root path `/mcp/` for Streamable HTTP transport, and `/sse/` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method: ```python # For Streamable HTTP transport @@ -96,7 +96,13 @@ mcp = FastMCP("MyServer") # Define custom middleware custom_middleware = [ - Middleware(CORSMiddleware, allow_origins=["*"]), + Middleware( + CORSMiddleware, + allow_origins=["https://example.com", "https://app.example.com"], + allow_credentials=True, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["Content-Type", "Authorization"], + ), ] # Create ASGI app with custom middleware @@ -131,7 +137,7 @@ app = Starlette( ) ``` -The MCP endpoint will be available at `/mcp-server/mcp` of the resulting Starlette app. +The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app. For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized. @@ -161,7 +167,7 @@ app = Starlette( ) ``` -In this setup, the MCP server is accessible at the `/outer/inner/mcp` path of the resulting Starlette app. +In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path of the resulting Starlette app. For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the *outer* Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized. @@ -188,7 +194,7 @@ app = FastAPI(lifespan=mcp_app.lifespan) app.mount("/mcp-server", mcp_app) ``` -The MCP endpoint will be available at `/mcp-server/mcp` of the resulting FastAPI app. +The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting FastAPI app. For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting FastAPI app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized. diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx index 5b6a7eadd36e8768f0328fa16014cc015bcd3b79..591cba32c2e620ee7de3db5b8c547f102120fd26 100644 --- a/docs/deployment/running-server.mdx +++ b/docs/deployment/running-server.mdx @@ -105,7 +105,7 @@ When using Stdio transport, you will typically *not* run the server yourself as Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is the recommended transport for web-based deployments. -To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp`). +To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp/`). ```python {6} server.py from fastmcp import FastMCP @@ -120,7 +120,7 @@ import asyncio from fastmcp import Client async def example(): - async with Client("http://127.0.0.1:8000/mcp") as client: + async with Client("http://127.0.0.1:8000/mcp/") as client: await client.ping() if __name__ == "__main__": @@ -168,7 +168,7 @@ New applications should use Streamable HTTP transport instead. Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects. -To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse`) and message path (`/messages/`). +To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse/`) and message path (`/messages/`). ```python {6} server.py @@ -186,7 +186,7 @@ from fastmcp.client.transports import SSETransport async def example(): async with Client( - transport=SSETransport("http://127.0.0.1:8000/sse") + transport=SSETransport("http://127.0.0.1:8000/sse/") ) as client: await client.ping() diff --git a/docs/docs.json b/docs/docs.json index 0c0b83e5392939c5fe6eb21534bdb5ecba03e794..66971c813823a5c83291a4fd48d666d4449875b7 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,178 +1,296 @@ { - "$schema": "https://mintlify.com/docs.json", - "appearance": { - "default": "system", - "strict": false + "$schema": "https://mintlify.com/docs.json", + "appearance": { + "default": "system", + "strict": false + }, + "background": { + "color": { + "dark": "#222831", + "light": "#EEEEEE" }, - "background": { - "color": { - "dark": "#222831", - "light": "#EEEEEE" - }, - "decoration": "windows" - }, - "banner": { - "content": "[FastMCP Cloud](https://fastmcp.link/x0Kyhy2) is coming!" - }, - "colors": { - "dark": "#f72585", - "light": "#4cc9f0", - "primary": "#2d00f7" - }, - "description": "The fast, Pythonic way to build MCP servers and clients.", - "favicon": { - "dark": "/assets/favicon.ico", - "light": "/assets/favicon.ico" - }, - "footer": { - "socials": { - "bluesky": "https://bsky.app/profile/jlowin.dev", - "github": "https://github.com/jlowin/fastmcp", - "x": "https://x.com/jlowin" - } - }, - "integrations": { - "ga4": { - "measurementId": "G-64R5W1TJXG" - } - }, - "name": "FastMCP", - "navbar": { - "primary": { - "href": "https://github.com/jlowin/fastmcp", - "type": "github" - } - }, - "navigation": { + "decoration": "windows" + }, + "banner": { + "content": "[FastMCP Cloud](https://fastmcp.link/x0Kyhy2) is coming!" + }, + "colors": { + "dark": "#f72585", + "light": "#4cc9f0", + "primary": "#2d00f7" + }, + "description": "The fast, Pythonic way to build MCP servers and clients.", + "favicon": { + "dark": "/assets/favicon.ico", + "light": "/assets/favicon.ico" + }, + "footer": { + "socials": { + "bluesky": "https://bsky.app/profile/jlowin.dev", + "github": "https://github.com/jlowin/fastmcp", + "x": "https://x.com/jlowin" + } + }, + "integrations": { + "ga4": { + "measurementId": "G-64R5W1TJXG" + } + }, + "name": "FastMCP", + "navbar": { + "primary": { + "href": "https://github.com/jlowin/fastmcp", + "type": "github" + } + }, + "navigation": { + "tabs": [ + { + "tab": "Documentation", "anchors": [ - { - "anchor": "Documentation", - "groups": [ - { - "group": "Get Started", - "pages": [ - "getting-started/welcome", - "getting-started/installation", - "getting-started/quickstart", - "updates" - ] - }, - { - "group": "Servers", - "pages": [ - "servers/fastmcp", - { - "group": "Core Components", - "icon": "toolbox", - "pages": [ - "servers/tools", - "servers/resources", - "servers/prompts", - "servers/context" - ] - }, - { - "group": "Authentication", - "icon": "shield-check", - "pages": [ - "servers/auth/bearer" - ] - }, - "servers/middleware", - "servers/openapi", - "servers/proxy", - "servers/composition", - { - "group": "Deployment", - "icon": "upload", - "pages": [ - "deployment/running-server", - "deployment/asgi" - ] - } - ] - }, - { - "group": "Clients", - "pages": [ - "clients/client", - "clients/transports", - { - "group": "Authentication", - "icon": "user-shield", - "pages": [ - "clients/auth/oauth", - "clients/auth/bearer" - ] - }, - "clients/advanced-features" - ] - }, - { - "group": "Integrations", - "pages": [ - "integrations/anthropic", - "integrations/claude-desktop", - "integrations/openai", - "integrations/gemini", - "integrations/contrib" - ] - }, - { - "group": "Patterns", - "pages": [ - "patterns/tool-transformation", - "patterns/decorating-methods", - "patterns/http-requests", - "patterns/testing", - "patterns/cli" - ] - } - ], - "icon": "book" - }, - { - "anchor": "Tutorials", - "groups": [ - { - "group": "MCP", + { + "anchor": "Documentation", + "groups": [ + { + "group": "Get Started", + "pages": [ + "getting-started/welcome", + "getting-started/installation", + "getting-started/quickstart" + ] + }, + { + "group": "Servers", + "pages": [ + "servers/server", + { + "group": "Core Components", + "icon": "toolbox", + "pages": [ + "servers/tools", + "servers/resources", + "servers/prompts", + "servers/context" + ] + }, + { + "group": "Authentication", + "icon": "shield-check", + "pages": ["servers/auth/bearer"] + }, + "servers/middleware", + "servers/openapi", + "servers/proxy", + "servers/composition", + { + "group": "Deployment", + "icon": "upload", + "pages": ["deployment/running-server", "deployment/asgi"] + } + ] + }, + { + "group": "Clients", + "pages": [ + "clients/client", + { + "group": "Core Operations", + "icon": "handshake", + "pages": [ + "clients/tools", + "clients/resources", + "clients/prompts" + ] + }, + { + "group": "Advanced Features", + "icon": "stars", + "pages": [ + "clients/logging", + "clients/progress", + "clients/sampling", + "clients/roots" + ] + }, + "clients/transports", + { + "group": "Authentication", + "icon": "user-shield", + "pages": ["clients/auth/oauth", "clients/auth/bearer"] + } + ] + }, + { + "group": "Integrations", + "pages": [ + "integrations/anthropic", + "integrations/claude-desktop", + "integrations/openai", + "integrations/gemini", + "integrations/contrib" + ] + }, + { + "group": "Patterns", + "pages": [ + "patterns/tool-transformation", + "patterns/decorating-methods", + "patterns/http-requests", + "patterns/testing", + "patterns/cli" + ] + }, + { + "group": "Tutorials", + "pages": [ + "tutorials/mcp", + "tutorials/create-mcp-server", + "tutorials/rest-api" + ] + } + ], + "icon": "book" + }, + { + "anchor": "What's New", + "pages": ["updates", "changelog"] + }, + + { + "anchor": "Community", + "icon": "users", + "pages": ["community/showcase"] + } + ] + }, + { + "tab": "SDK Reference", + "anchors": [ + { + "anchor": "Python SDK", + "icon": "python", + "pages": [ + "python-sdk/fastmcp-exceptions", + "python-sdk/fastmcp-settings", + { + "group": "fastmcp.cli", + "pages": [ + "python-sdk/fastmcp-cli-__init__", + "python-sdk/fastmcp-cli-claude", + "python-sdk/fastmcp-cli-cli", + "python-sdk/fastmcp-cli-run" + ] + }, + { + "group": "fastmcp.client", + "pages": [ + "python-sdk/fastmcp-client-__init__", + { + "group": "auth", + "pages": [ + "python-sdk/fastmcp-client-auth-__init__", + "python-sdk/fastmcp-client-auth-bearer", + "python-sdk/fastmcp-client-auth-oauth" + ] + }, + "python-sdk/fastmcp-client-client", + "python-sdk/fastmcp-client-logging", + "python-sdk/fastmcp-client-oauth_callback", + "python-sdk/fastmcp-client-progress", + "python-sdk/fastmcp-client-roots", + "python-sdk/fastmcp-client-sampling", + "python-sdk/fastmcp-client-transports" + ] + }, + { + "group": "fastmcp.prompts", + "pages": [ + "python-sdk/fastmcp-prompts-__init__", + "python-sdk/fastmcp-prompts-prompt", + "python-sdk/fastmcp-prompts-prompt_manager" + ] + }, + { + "group": "fastmcp.resources", + "pages": [ + "python-sdk/fastmcp-resources-__init__", + "python-sdk/fastmcp-resources-resource", + "python-sdk/fastmcp-resources-resource_manager", + "python-sdk/fastmcp-resources-template", + "python-sdk/fastmcp-resources-types" + ] + }, + { + "group": "fastmcp.server", + "pages": [ + "python-sdk/fastmcp-server-__init__", + { + "group": "auth", + "pages": [ + "python-sdk/fastmcp-server-auth-__init__", + "python-sdk/fastmcp-server-auth-auth", + { + "group": "providers", "pages": [ - "tutorials/mcp", - "tutorials/create-mcp-server", - "tutorials/rest-api" + "python-sdk/fastmcp-server-auth-providers-__init__", + "python-sdk/fastmcp-server-auth-providers-bearer", + "python-sdk/fastmcp-server-auth-providers-bearer_env", + "python-sdk/fastmcp-server-auth-providers-in_memory" ] - } - ], - "icon": "graduation-cap" - }, - { - "anchor": "Changelog", - "icon": "list-check", + } + ] + }, + "python-sdk/fastmcp-server-context", + "python-sdk/fastmcp-server-dependencies", + "python-sdk/fastmcp-server-http", + "python-sdk/fastmcp-server-middleware", + "python-sdk/fastmcp-server-openapi", + "python-sdk/fastmcp-server-proxy", + "python-sdk/fastmcp-server-server" + ] + }, + { + "group": "fastmcp.tools", "pages": [ - "changelog" + "python-sdk/fastmcp-tools-__init__", + "python-sdk/fastmcp-tools-tool", + "python-sdk/fastmcp-tools-tool_manager", + "python-sdk/fastmcp-tools-tool_transform" ] - }, - { - "anchor": "Community", - "icon": "users", + }, + { + "group": "fastmcp.utilities", "pages": [ - "community/showcase" + "python-sdk/fastmcp-utilities-__init__", + "python-sdk/fastmcp-utilities-cache", + "python-sdk/fastmcp-utilities-components", + "python-sdk/fastmcp-utilities-exceptions", + "python-sdk/fastmcp-utilities-http", + "python-sdk/fastmcp-utilities-json_schema", + "python-sdk/fastmcp-utilities-logging", + "python-sdk/fastmcp-utilities-mcp_config", + "python-sdk/fastmcp-utilities-openapi", + "python-sdk/fastmcp-utilities-types" ] - } + } + ] + } ] + } + ] + }, + "redirects": [ + { + "destination": "/servers/proxy", + "source": "/patterns/proxy" }, - "redirects": [ - { - "destination": "/servers/proxy", - "source": "/patterns/proxy" - }, - { - "destination": "/servers/composition", - "source": "/patterns/composition" - } - ], - "search": { - "prompt": "Search the docs..." - }, - "theme": "mint" -} \ No newline at end of file + { + "destination": "/servers/composition", + "source": "/patterns/composition" + } + ], + "search": { + "prompt": "Search the docs..." + }, + "theme": "mint" +} diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx index c5cdaea692b88596159c50b8384e25d42d23d8ef..e9ebe860148670672aa9a1c8896ee628324ea47f 100644 --- a/docs/integrations/anthropic.mdx +++ b/docs/integrations/anthropic.mdx @@ -3,7 +3,7 @@ title: Anthropic API + FastMCP sidebarTitle: Anthropic API description: Call FastMCP servers from the Anthropic API icon: message-smile -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx index 1959e8fa3d590fd2e0667997b89d20e4292e5b51..ab9e68ce0087e330c7ceffe9c8ba0aaebaecb07c 100644 --- a/docs/integrations/gemini.mdx +++ b/docs/integrations/gemini.mdx @@ -3,7 +3,7 @@ title: Gemini SDK + FastMCP sidebarTitle: Gemini SDK description: Call FastMCP servers from the Google Gemini SDK icon: message-smile -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx index b68ebcc18d91998e2ca7d09aea504132fda08d36..ba0d00941f9852ce6699f7876f768bc67abce5da 100644 --- a/docs/integrations/openai.mdx +++ b/docs/integrations/openai.mdx @@ -3,7 +3,7 @@ title: OpenAI API + FastMCP sidebarTitle: OpenAI API description: Call FastMCP servers from the OpenAI API icon: message-smile -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index 19e1ad1ab0c5b836e4966a02809384bfee56783f..84654975bdb905fc1e81519788662fb135cb2263 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -21,6 +21,7 @@ fastmcp --help | `run` | Run a FastMCP server directly | Uses your current environment; you are responsible for ensuring all dependencies are available | | `dev` | Run a server with the MCP Inspector for testing | Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | | `install` | Install a server in the Claude desktop app | Creates an isolated environment; dependencies must be explicitly specified with `--with` and/or `--with-editable` | +| `inspect` | Generate a JSON report about a FastMCP server | Uses your current environment; you are responsible for ensuring all dependencies are available | | `version` | Display version information | N/A | ## Command Details @@ -179,6 +180,29 @@ fastmcp install server.py:my_server fastmcp install server.py:my_server -n "My Analysis Server" --with pandas ``` +### `inspect` + + + +Generate a detailed JSON report about a FastMCP server, including information about its tools, prompts, resources, and capabilities. + +```bash +fastmcp inspect server.py +``` + +The command supports the same server specification format as `run` and `install`: + +```bash +# Auto-detect server object +fastmcp inspect server.py + +# Specify server object +fastmcp inspect server.py:my_server + +# Custom output location +fastmcp inspect server.py --output analysis.json +``` + ### `version` Display version information about FastMCP and related components. diff --git a/docs/python-sdk/fastmcp-cli-__init__.mdx b/docs/python-sdk/fastmcp-cli-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d2873740af0239b7c53fd903fecb2641715e9625 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-__init__.mdx @@ -0,0 +1,9 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.cli` + + +FastMCP CLI package. diff --git a/docs/python-sdk/fastmcp-cli-claude.mdx b/docs/python-sdk/fastmcp-cli-claude.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6ea44b33e86f3ec2ccbc4cead82aa3fcc6c8dcb1 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-claude.mdx @@ -0,0 +1,43 @@ +--- +title: claude +sidebarTitle: claude +--- + +# `fastmcp.cli.claude` + + +Claude app integration utilities. + +## Functions + +### `get_claude_config_path` + +```python +get_claude_config_path() -> Path | None +``` + + +Get the Claude config directory based on platform. + + +### `update_claude_config` + +```python +update_claude_config(file_spec: str, server_name: str) -> bool +``` + + +Add or update a FastMCP server in Claude's configuration. + +**Args:** +- `file_spec`: Path to the server file, optionally with \:object suffix +- `server_name`: Name for the server in Claude's config +- `with_editable`: Optional directory to install in editable mode +- `with_packages`: Optional list of additional packages to install +- `env_vars`: Optional dictionary of environment variables. These are merged with +any existing variables, with new values taking precedence. + +**Raises:** +- `RuntimeError`: If Claude Desktop's config directory is not found, indicating +Claude Desktop may not be installed or properly set up. + diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1ebb968b2c1f582db04b7e5b11b53d1d5e183dc1 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-cli.mdx @@ -0,0 +1,65 @@ +--- +title: cli +sidebarTitle: cli +--- + +# `fastmcp.cli.cli` + + +FastMCP CLI tools. + +## Functions + +### `version` + +```python +version(ctx: Context) +``` + +### `dev` + +```python +dev(server_spec: str = typer.Argument(..., help='Python file to run, optionally with :object suffix'), with_editable: Annotated[Path | None, typer.Option('--with-editable', '-e', help='Directory containing pyproject.toml to install in editable mode', exists=True, file_okay=False, resolve_path=True)] = None, with_packages: Annotated[list[str], typer.Option('--with', help='Additional packages to install')] = [], inspector_version: Annotated[str | None, typer.Option('--inspector-version', help='Version of the MCP Inspector to use')] = None, ui_port: Annotated[int | None, typer.Option('--ui-port', help='Port for the MCP Inspector UI')] = None, server_port: Annotated[int | None, typer.Option('--server-port', help='Port for the MCP Inspector Proxy server')] = None) -> None +``` + + +Run a MCP server with the MCP Inspector. + + +### `run` + +```python +run(ctx: typer.Context, server_spec: str = typer.Argument(..., help='Python file, object specification (file:obj), or URL'), transport: Annotated[str | None, typer.Option('--transport', '-t', help='Transport protocol to use (stdio, streamable-http, or sse)')] = None, host: Annotated[str | None, typer.Option('--host', help='Host to bind to when using http transport (default: 127.0.0.1)')] = None, port: Annotated[int | None, typer.Option('--port', '-p', help='Port to bind to when using http transport (default: 8000)')] = None, log_level: Annotated[str | None, typer.Option('--log-level', '-l', help='Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')] = None) -> None +``` + + +Run a MCP server or connect to a remote one. + +The server can be specified in three ways: +1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app. + +2. Import approach: server.py:app - imports and runs the specified server object. + +3. URL approach: http://server-url - connects to a remote server and creates a proxy. + + + +Note: This command runs the server directly. You are responsible for ensuring +all dependencies are available. + +Server arguments can be passed after -- : +fastmcp run server.py -- --config config.json --debug + + +### `install` + +```python +install(server_spec: str = typer.Argument(..., help='Python file to run, optionally with :object suffix'), server_name: Annotated[str | None, typer.Option('--name', '-n', help="Custom name for the server (defaults to server's name attribute or file name)")] = None, with_editable: Annotated[Path | None, typer.Option('--with-editable', '-e', help='Directory containing pyproject.toml to install in editable mode', exists=True, file_okay=False, resolve_path=True)] = None, with_packages: Annotated[list[str], typer.Option('--with', help='Additional packages to install')] = [], env_vars: Annotated[list[str], typer.Option('--env-var', '-v', help='Environment variables in KEY=VALUE format')] = [], env_file: Annotated[Path | None, typer.Option('--env-file', '-f', help='Load environment variables from a .env file', exists=True, file_okay=True, dir_okay=False, resolve_path=True)] = None) -> None +``` + + +Install a MCP server in the Claude desktop app. + +Environment variables are preserved once added and only updated if new values +are explicitly provided. + diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7505c7fb44c474f1b85cb0f78f852c1795032a3d --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-run.mdx @@ -0,0 +1,106 @@ +--- +title: run +sidebarTitle: run +--- + +# `fastmcp.cli.run` + + +FastMCP run command implementation. + +## Functions + +### `is_url` + +```python +is_url(path: str) -> bool +``` + + +Check if a string is a URL. + + +### `parse_file_path` + +```python +parse_file_path(server_spec: str) -> tuple[Path, str | None] +``` + + +Parse a file path that may include a server object specification. + +**Args:** +- `server_spec`: Path to file, optionally with \:object suffix + +**Returns:** +- Tuple of (file_path, server_object) + + +### `import_server` + +```python +import_server(file: Path, server_object: str | None = None) -> Any +``` + + +Import a MCP server from a file. + +**Args:** +- `file`: Path to the file +- `server_object`: Optional object name in format "module\:object" or just "object" + +**Returns:** +- The server object + + +### `create_client_server` + +```python +create_client_server(url: str) -> Any +``` + + +Create a FastMCP server from a client URL. + +**Args:** +- `url`: The URL to connect to + +**Returns:** +- A FastMCP server instance + + +### `import_server_with_args` + +```python +import_server_with_args(file: Path, server_object: str | None = None, server_args: list[str] | None = None) -> Any +``` + + +Import a server with optional command line arguments. + +**Args:** +- `file`: Path to the server file +- `server_object`: Optional server object name +- `server_args`: Optional command line arguments to inject + +**Returns:** +- The imported server object + + +### `run_command` + +```python +run_command(server_spec: str, transport: str | None = None, host: str | None = None, port: int | None = None, log_level: str | None = None, server_args: list[str] | None = None) -> None +``` + + +Run a MCP server or connect to a remote one. + +**Args:** +- `server_spec`: Python file, object specification (file\:obj), or URL +- `transport`: Transport protocol to use +- `host`: Host to bind to when using http transport +- `port`: Port to bind to when using http transport +- `log_level`: Log level +- `server_args`: Additional arguments to pass to the server + diff --git a/docs/python-sdk/fastmcp-client-__init__.mdx b/docs/python-sdk/fastmcp-client-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bc145d4b7794589047dcb94437cad738eb42bedf --- /dev/null +++ b/docs/python-sdk/fastmcp-client-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.client` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-client-auth-__init__.mdx b/docs/python-sdk/fastmcp-client-auth-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..28242780d7d51b1ddc08682125e30e82dc8b66d7 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-auth-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.client.auth` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-client-auth-bearer.mdx b/docs/python-sdk/fastmcp-client-auth-bearer.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ab0c15240f74fccafdedd536a94f08a2b73ad1d2 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-auth-bearer.mdx @@ -0,0 +1,18 @@ +--- +title: bearer +sidebarTitle: bearer +--- + +# `fastmcp.client.auth.bearer` + +## Classes + +### `BearerAuth` + +**Methods:** + +#### `auth_flow` + +```python +auth_flow(self, request) +``` diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f10afba36470bf8bf698e1869c6147a56e87fd6b --- /dev/null +++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx @@ -0,0 +1,102 @@ +--- +title: oauth +sidebarTitle: oauth +--- + +# `fastmcp.client.auth.oauth` + +## Functions + +### `default_cache_dir` + +```python +default_cache_dir() -> Path +``` + +### `OAuth` + +```python +OAuth(mcp_url: str, scopes: str | list[str] | None = None, client_name: str = 'FastMCP Client', token_storage_cache_dir: Path | None = None, additional_client_metadata: dict[str, Any] | None = None) -> _MCPOAuthClientProvider +``` + + +Create an OAuthClientProvider for an MCP server. + +This is intended to be provided to the `auth` parameter of an +httpx.AsyncClient (or appropriate FastMCP client/transport instance) + +**Args:** +- `mcp_url`: Full URL to the MCP endpoint (e.g. "http\://host/mcp/sse/") +- `scopes`: OAuth scopes to request. Can be a +- `client_name`: Name for this client during registration +- `token_storage_cache_dir`: Directory for FileTokenStorage +- `additional_client_metadata`: Extra fields for OAuthClientMetadata + +**Returns:** +- OAuthClientProvider + + +## Classes + +### `ServerOAuthMetadata` + + +More flexible OAuth metadata model that accepts broader ranges of values +than the restrictive MCP standard model. + +This handles real-world OAuth servers like PayPal that may support +additional methods not in the MCP specification. + + +### `OAuthClientProvider` + + +OAuth client provider with more flexible OAuth metadata discovery. + + +### `FileTokenStorage` + + +File-based token storage implementation for OAuth credentials and tokens. +Implements the mcp.client.auth.TokenStorage protocol. + +Each instance is tied to a specific server URL for proper token isolation. + + +**Methods:** + +#### `get_base_url` + +```python +get_base_url(url: str) -> str +``` + +Extract the base URL (scheme + host) from a URL. + + +#### `get_cache_key` + +```python +get_cache_key(self) -> str +``` + +Generate a safe filesystem key from the server's base URL. + + +#### `clear` + +```python +clear(self) -> None +``` + +Clear all cached data for this server. + + +#### `clear_all` + +```python +clear_all(cls, cache_dir: Path | None = None) -> None +``` + +Clear all cached data for all servers. + diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx new file mode 100644 index 0000000000000000000000000000000000000000..4c3f252bf11598535f6a4f4463618ad3175ecb5d --- /dev/null +++ b/docs/python-sdk/fastmcp-client-client.mdx @@ -0,0 +1,94 @@ +--- +title: client +sidebarTitle: client +--- + +# `fastmcp.client.client` + +## Classes + +### `Client` + + + + MCP client that delegates connection management to a Transport instance. + + The Client class is responsible for MCP protocol logic, while the Transport + handles connection establishment and management. Client provides methods for + working with resources, prompts, tools and other MCP capabilities. + + Args: + transport: Connection source specification, which can be: + - ClientTransport: Direct transport instance + - FastMCP: In-process FastMCP server + - AnyUrl | str: URL to connect to + - Path: File path for local socket + - MCPConfig: MCP server configuration + - dict: Transport configuration + roots: Optional RootsList or RootsHandler for filesystem access + sampling_handler: Optional handler for sampling requests + log_handler: Optional handler for log messages + message_handler: Optional handler for protocol messages + progress_handler: Optional handler for progress notifications + timeout: Optional timeout for requests (seconds or timedelta) + init_timeout: Optional timeout for initial connection (seconds or timedelta). + Set to 0 to disable. If None, uses the value in the FastMCP global settings. + + Examples: + ```python # Connect to FastMCP server client = + Client("http://localhost:8080") + + async with client: + # List available resources resources = await client.list_resources() + + # Call a tool result = await client.call_tool("my_tool", {"param": + "value"}) + ``` + + +**Methods:** + +#### `session` + +```python +session(self) -> ClientSession +``` + +Get the current active session. Raises RuntimeError if not connected. + + +#### `initialize_result` + +```python +initialize_result(self) -> mcp.types.InitializeResult +``` + +Get the result of the initialization request. + + +#### `set_roots` + +```python +set_roots(self, roots: RootsList | RootsHandler) -> None +``` + +Set the roots for the client. This does not automatically call `send_roots_list_changed`. + + +#### `set_sampling_callback` + +```python +set_sampling_callback(self, sampling_callback: SamplingHandler) -> None +``` + +Set the sampling callback for the client. + + +#### `is_connected` + +```python +is_connected(self) -> bool +``` + +Check if the client is currently connected. + diff --git a/docs/python-sdk/fastmcp-client-logging.mdx b/docs/python-sdk/fastmcp-client-logging.mdx new file mode 100644 index 0000000000000000000000000000000000000000..84d201db77485e40d338955685b17e7fafbd4e18 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-logging.mdx @@ -0,0 +1,14 @@ +--- +title: logging +sidebarTitle: logging +--- + +# `fastmcp.client.logging` + +## Functions + +### `create_log_callback` + +```python +create_log_callback(handler: LogHandler | None = None) -> LoggingFnT +``` diff --git a/docs/python-sdk/fastmcp-client-oauth_callback.mdx b/docs/python-sdk/fastmcp-client-oauth_callback.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6eab9de3a71fc0503f8a1cd7776156dd53e3550f --- /dev/null +++ b/docs/python-sdk/fastmcp-client-oauth_callback.mdx @@ -0,0 +1,63 @@ +--- +title: oauth_callback +sidebarTitle: oauth_callback +--- + +# `fastmcp.client.oauth_callback` + + + +OAuth callback server for handling authorization code flows. + +This module provides a reusable callback server that can handle OAuth redirects +and display styled responses to users. + + +## Functions + +### `create_callback_html` + +```python +create_callback_html(message: str, is_success: bool = True, title: str = 'FastMCP OAuth', server_url: str | None = None) -> str +``` + + +Create a styled HTML response for OAuth callbacks. + + +### `create_oauth_callback_server` + +```python +create_oauth_callback_server(port: int, callback_path: str = '/callback', server_url: str | None = None, response_future: asyncio.Future | None = None) -> Server +``` + + +Create an OAuth callback server. + +**Args:** +- `port`: The port to run the server on +- `callback_path`: The path to listen for OAuth redirects on +- `server_url`: Optional server URL to display in success messages +- `response_future`: Optional future to resolve when OAuth callback is received + +**Returns:** +- Configured uvicorn Server instance (not yet running) + + +## Classes + +### `CallbackResponse` + +**Methods:** + +#### `from_dict` + +```python +from_dict(cls, data: dict[str, str]) -> CallbackResponse +``` + +#### `to_dict` + +```python +to_dict(self) -> dict[str, str] +``` diff --git a/docs/python-sdk/fastmcp-client-progress.mdx b/docs/python-sdk/fastmcp-client-progress.mdx new file mode 100644 index 0000000000000000000000000000000000000000..aecd0f37b0e4c242ad06935835cb20fb08c6693d --- /dev/null +++ b/docs/python-sdk/fastmcp-client-progress.mdx @@ -0,0 +1,8 @@ +--- +title: progress +sidebarTitle: progress +--- + +# `fastmcp.client.progress` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-client-roots.mdx b/docs/python-sdk/fastmcp-client-roots.mdx new file mode 100644 index 0000000000000000000000000000000000000000..820e1d0a7556418d0a2f8e5905d0d7423d5af7b3 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-roots.mdx @@ -0,0 +1,20 @@ +--- +title: roots +sidebarTitle: roots +--- + +# `fastmcp.client.roots` + +## Functions + +### `convert_roots_list` + +```python +convert_roots_list(roots: RootsList) -> list[mcp.types.Root] +``` + +### `create_roots_callback` + +```python +create_roots_callback(handler: RootsList | RootsHandler) -> ListRootsFnT +``` diff --git a/docs/python-sdk/fastmcp-client-sampling.mdx b/docs/python-sdk/fastmcp-client-sampling.mdx new file mode 100644 index 0000000000000000000000000000000000000000..be78badebe6d700f266704ab71ae3cebd99f84f8 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-sampling.mdx @@ -0,0 +1,14 @@ +--- +title: sampling +sidebarTitle: sampling +--- + +# `fastmcp.client.sampling` + +## Functions + +### `create_sampling_callback` + +```python +create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT +``` diff --git a/docs/python-sdk/fastmcp-client-transports.mdx b/docs/python-sdk/fastmcp-client-transports.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a4f9d22e64185c36da46f2865e77dbe58f378a8c --- /dev/null +++ b/docs/python-sdk/fastmcp-client-transports.mdx @@ -0,0 +1,191 @@ +--- +title: transports +sidebarTitle: transports +--- + +# `fastmcp.client.transports` + +## Functions + +### `infer_transport` + +```python +infer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport +``` + + + + Infer the appropriate transport type from the given transport argument. + + This function attempts to infer the correct transport type from the provided + argument, handling various input types and converting them to the appropriate + ClientTransport subclass. + + The function supports these input types: + - ClientTransport: Used directly without modification + - FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport + - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js) + - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints) + - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers + + For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`. + + For MCPConfig with multiple servers, a composite client is created where each server + is mounted with its name as prefix. This allows accessing tools and resources from multiple + servers through a single unified client interface, using naming patterns like + `servername_toolname` for tools and `protocol://servername/path` for resources. + If the MCPConfig contains only one server, a direct connection is established without prefixing. + + Examples: + ```python + # Connect to a local Python script + transport = infer_transport("my_script.py") + + # Connect to a remote server via HTTP + transport = infer_transport("http://example.com/mcp") + + # Connect to multiple servers using MCPConfig + config = { + "mcpServers": { + "weather": {"url": "http://weather.example.com/mcp"}, + "calendar": {"url": "http://calendar.example.com/mcp"} + } + } + transport = infer_transport(config) + ``` + + +## Classes + +### `SessionKwargs` + + +Keyword arguments for the MCP ClientSession constructor. + + +### `ClientTransport` + + +Abstract base class for different MCP client transport mechanisms. + +A Transport is responsible for establishing and managing connections +to an MCP server, and providing a ClientSession within an async context. + + +### `WSTransport` + + +Transport implementation that connects to an MCP server via WebSockets. + + +### `SSETransport` + + +Transport implementation that connects to an MCP server via Server-Sent Events. + + +### `StreamableHttpTransport` + + +Transport implementation that connects to an MCP server via Streamable HTTP Requests. + + +### `StdioTransport` + + +Base transport for connecting to an MCP server via subprocess with stdio. + +This is a base class that can be subclassed for specific command-based +transports like Python, Node, Uvx, etc. + + +### `PythonStdioTransport` + + +Transport for running Python scripts. + + +### `FastMCPStdioTransport` + + +Transport for running FastMCP servers using the FastMCP CLI. + + +### `NodeStdioTransport` + + +Transport for running Node.js scripts. + + +### `UvxStdioTransport` + + +Transport for running commands via the uvx tool. + + +### `NpxStdioTransport` + + +Transport for running commands via the npx tool. + + +### `FastMCPTransport` + + +In-memory transport for FastMCP servers. + +This transport connects directly to a FastMCP server instance in the same +Python process. It works with both FastMCP 2.x servers and FastMCP 1.0 +servers from the low-level MCP SDK. This is particularly useful for unit +tests or scenarios where client and server run in the same runtime. + + +### `MCPConfigTransport` + + +Transport for connecting to one or more MCP servers defined in an MCPConfig. + + This transport provides a unified interface to multiple MCP servers defined in an MCPConfig + object or dictionary matching the MCPConfig schema. It supports two key scenarios: + + 1. If the MCPConfig contains exactly one server, it creates a direct transport to that server. + 2. If the MCPConfig contains multiple servers, it creates a composite client by mounting + all servers on a single FastMCP instance, with each server's name used as its mounting prefix. + + In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}` + and resources with the pattern `protocol://{server_name}/path/to/resource`. + + This is particularly useful for creating clients that need to interact with multiple specialized + MCP servers through a single interface, simplifying client code. + + Examples: + ```python + from fastmcp import Client + from fastmcp.utilities.mcp_config import MCPConfig + + # Create a config with multiple servers + config = { + "mcpServers": { + "weather": { + "url": "https://weather-api.example.com/mcp", + "transport": "streamable-http" + }, + "calendar": { + "url": "https://calendar-api.example.com/mcp", + "transport": "streamable-http" + } + } + } + + # Create a client with the config + client = Client(config) + + async with client: + # Access tools with prefixes + weather = await client.call_tool("weather_get_forecast", {"city": "London"}) + events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"}) + + # Access resources with prefixed URIs + icons = await client.read_resource("weather://weather/icons/sunny") + ``` + diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9726d1cde6fd3473e2c6faf2abf3c3faadc22982 --- /dev/null +++ b/docs/python-sdk/fastmcp-exceptions.mdx @@ -0,0 +1,65 @@ +--- +title: exceptions +sidebarTitle: exceptions +--- + +# `fastmcp.exceptions` + + +Custom exceptions for FastMCP. + +## Classes + +### `FastMCPError` + + +Base error for FastMCP. + + +### `ValidationError` + + +Error in validating parameters or return values. + + +### `ResourceError` + + +Error in resource operations. + + +### `ToolError` + + +Error in tool operations. + + +### `PromptError` + + +Error in prompt operations. + + +### `InvalidSignature` + + +Invalid signature for use with FastMCP. + + +### `ClientError` + + +Error in client operations. + + +### `NotFoundError` + + +Object not found. + + +### `DisabledError` + + +Object is disabled. + diff --git a/docs/python-sdk/fastmcp-prompts-__init__.mdx b/docs/python-sdk/fastmcp-prompts-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8ef80b59e55804ee2fdd8c528aba08824f25396d --- /dev/null +++ b/docs/python-sdk/fastmcp-prompts-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.prompts` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-prompts-prompt.mdx b/docs/python-sdk/fastmcp-prompts-prompt.mdx new file mode 100644 index 0000000000000000000000000000000000000000..60028f3167441742d513f4228cb252cebd9d5e0a --- /dev/null +++ b/docs/python-sdk/fastmcp-prompts-prompt.mdx @@ -0,0 +1,84 @@ +--- +title: prompt +sidebarTitle: prompt +--- + +# `fastmcp.prompts.prompt` + + +Base classes for FastMCP prompts. + +## Functions + +### `Message` + +```python +Message(content: str | MCPContent, role: Role | None = None, **kwargs: Any) -> PromptMessage +``` + + +A user-friendly constructor for PromptMessage. + + +## Classes + +### `PromptArgument` + + +An argument that can be passed to a prompt. + + +### `Prompt` + + +A prompt template that can be rendered with parameters. + + +**Methods:** + +#### `to_mcp_prompt` + +```python +to_mcp_prompt(self, **overrides: Any) -> MCPPrompt +``` + +Convert the prompt to an MCP prompt. + + +#### `from_function` + +```python +from_function(fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionPrompt +``` + +Create a Prompt from a function. + +The function can return: +- A string (converted to a message) +- A Message object +- A dict (converted to a message) +- A sequence of any of the above + + +### `FunctionPrompt` + + +A prompt that is a function. + + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionPrompt +``` + +Create a Prompt from a function. + +The function can return: +- A string (converted to a message) +- A Message object +- A dict (converted to a message) +- A sequence of any of the above + diff --git a/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx new file mode 100644 index 0000000000000000000000000000000000000000..041337c28f7ca01e31bfe8fc906d1a5dee1f00ec --- /dev/null +++ b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx @@ -0,0 +1,43 @@ +--- +title: prompt_manager +sidebarTitle: prompt_manager +--- + +# `fastmcp.prompts.prompt_manager` + +## Classes + +### `PromptManager` + + +Manages FastMCP prompts. + + +**Methods:** + +#### `mount` + +```python +mount(self, server: MountedServer) -> None +``` + +Adds a mounted server as a source for prompts. + + +#### `add_prompt_from_fn` + +```python +add_prompt_from_fn(self, fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, description: str | None = None, tags: set[str] | None = None) -> FunctionPrompt +``` + +Create a prompt from a function. + + +#### `add_prompt` + +```python +add_prompt(self, prompt: Prompt) -> Prompt +``` + +Add a prompt to the manager. + diff --git a/docs/python-sdk/fastmcp-resources-__init__.mdx b/docs/python-sdk/fastmcp-resources-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..cc5fd27869b9edeb25d700189571af4bb2c911fd --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.resources` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-resources-resource.mdx b/docs/python-sdk/fastmcp-resources-resource.mdx new file mode 100644 index 0000000000000000000000000000000000000000..dcfc51f002db8383ea46d65986aff270034210c8 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-resource.mdx @@ -0,0 +1,90 @@ +--- +title: resource +sidebarTitle: resource +--- + +# `fastmcp.resources.resource` + + +Base classes and interfaces for FastMCP resources. + +## Classes + +### `Resource` + + +Base class for all resources. + + +**Methods:** + +#### `from_function` + +```python +from_function(fn: Callable[[], Any], uri: str | AnyUrl, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResource +``` + +#### `set_default_mime_type` + +```python +set_default_mime_type(cls, mime_type: str | None) -> str +``` + +Set default MIME type if not provided. + + +#### `set_default_name` + +```python +set_default_name(self) -> Self +``` + +Set default name from URI if not provided. + + +#### `to_mcp_resource` + +```python +to_mcp_resource(self, **overrides: Any) -> MCPResource +``` + +Convert the resource to an MCPResource. + + +#### `key` + +```python +key(self) -> str +``` + +The key of the component. This is used for internal bookkeeping +and may reflect e.g. prefixes or other identifiers. You should not depend on +keys having a certain value, as the same tool loaded from different +hierarchies of servers may have different keys. + + +### `FunctionResource` + + +A resource that defers data loading by wrapping a function. + +The function is only called when the resource is read, allowing for lazy loading +of potentially expensive data. This is particularly useful when listing resources, +as the function won't be called until the resource is actually accessed. + +The function can return: +- str for text content (default) +- bytes for binary content +- other types will be converted to JSON + + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[[], Any], uri: str | AnyUrl, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResource +``` + +Create a FunctionResource from a function. + diff --git a/docs/python-sdk/fastmcp-resources-resource_manager.mdx b/docs/python-sdk/fastmcp-resources-resource_manager.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9adb43e83495c30dfe9638ae5e2bd030aa41e185 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-resource_manager.mdx @@ -0,0 +1,111 @@ +--- +title: resource_manager +sidebarTitle: resource_manager +--- + +# `fastmcp.resources.resource_manager` + + +Resource manager functionality. + +## Classes + +### `ResourceManager` + + +Manages FastMCP resources. + + +**Methods:** + +#### `mount` + +```python +mount(self, server: MountedServer) -> None +``` + +Adds a mounted server as a source for resources and templates. + + +#### `add_resource_or_template_from_fn` + +```python +add_resource_or_template_from_fn(self, fn: Callable[..., Any], uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> Resource | ResourceTemplate +``` + +Add a resource or template to the manager from a function. + +**Args:** +- `fn`: The function to register as a resource or template +- `uri`: The URI for the resource or template +- `name`: Optional name for the resource or template +- `description`: Optional description of the resource or template +- `mime_type`: Optional MIME type for the resource or template +- `tags`: Optional set of tags for categorizing the resource or template + +**Returns:** +- The added resource or template. If a resource or template with the same URI already exists, +- returns the existing resource or template. + + +#### `add_resource_from_fn` + +```python +add_resource_from_fn(self, fn: Callable[..., Any], uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> Resource +``` + +Add a resource to the manager from a function. + +**Args:** +- `fn`: The function to register as a resource +- `uri`: The URI for the resource +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource + +**Returns:** +- The added resource. If a resource with the same URI already exists, +- returns the existing resource. + + +#### `add_resource` + +```python +add_resource(self, resource: Resource) -> Resource +``` + +Add a resource to the manager. + +**Args:** +- `resource`: A Resource instance to add. The resource's .key attribute +will be used as the storage key. To overwrite it, call +Resource.with_key() before calling this method. + + +#### `add_template_from_fn` + +```python +add_template_from_fn(self, fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> ResourceTemplate +``` + +Create a template from a function. + + +#### `add_template` + +```python +add_template(self, template: ResourceTemplate) -> ResourceTemplate +``` + +Add a template to the manager. + +**Args:** +- `template`: A ResourceTemplate instance to add. The template's .key attribute +will be used as the storage key. To overwrite it, call +ResourceTemplate.with_key() before calling this method. + +**Returns:** +- The added template. If a template with the same URI already exists, +- returns the existing template. + diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c1810f0976cfe4ee32c1e092b49ea2ffc01b8088 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-template.mdx @@ -0,0 +1,104 @@ +--- +title: template +sidebarTitle: template +--- + +# `fastmcp.resources.template` + + +Resource template functionality. + +## Functions + +### `build_regex` + +```python +build_regex(template: str) -> re.Pattern +``` + +### `match_uri_template` + +```python +match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None +``` + +## Classes + +### `ResourceTemplate` + + +A template for dynamically creating resources. + + +**Methods:** + +#### `from_function` + +```python +from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResourceTemplate +``` + +#### `set_default_mime_type` + +```python +set_default_mime_type(cls, mime_type: str | None) -> str +``` + +Set default MIME type if not provided. + + +#### `matches` + +```python +matches(self, uri: str) -> dict[str, Any] | None +``` + +Check if URI matches template and extract parameters. + + +#### `to_mcp_template` + +```python +to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate +``` + +Convert the resource template to an MCPResourceTemplate. + + +#### `from_mcp_template` + +```python +from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate +``` + +Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object. + + +#### `key` + +```python +key(self) -> str +``` + +The key of the component. This is used for internal bookkeeping +and may reflect e.g. prefixes or other identifiers. You should not depend on +keys having a certain value, as the same tool loaded from different +hierarchies of servers may have different keys. + + +### `FunctionResourceTemplate` + + +A template for dynamically creating resources. + + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionResourceTemplate +``` + +Create a template from a function. + diff --git a/docs/python-sdk/fastmcp-resources-types.mdx b/docs/python-sdk/fastmcp-resources-types.mdx new file mode 100644 index 0000000000000000000000000000000000000000..675b44cc1187ce08b89b7e10a666c360c3b321c4 --- /dev/null +++ b/docs/python-sdk/fastmcp-resources-types.mdx @@ -0,0 +1,83 @@ +--- +title: types +sidebarTitle: types +--- + +# `fastmcp.resources.types` + + +Concrete resource implementations. + +## Classes + +### `TextResource` + + +A resource that reads from a string. + + +### `BinaryResource` + + +A resource that reads from bytes. + + +### `FileResource` + + +A resource that reads from a file. + +Set is_binary=True to read file as binary data instead of text. + + +**Methods:** + +#### `validate_absolute_path` + +```python +validate_absolute_path(cls, path: Path) -> Path +``` + +Ensure path is absolute. + + +#### `set_binary_from_mime_type` + +```python +set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool +``` + +Set is_binary based on mime_type if not explicitly set. + + +### `HttpResource` + + +A resource that reads from an HTTP endpoint. + + +### `DirectoryResource` + + +A resource that lists files in a directory. + + +**Methods:** + +#### `validate_absolute_path` + +```python +validate_absolute_path(cls, path: Path) -> Path +``` + +Ensure path is absolute. + + +#### `list_files` + +```python +list_files(self) -> list[Path] +``` + +List files in the directory. + diff --git a/docs/python-sdk/fastmcp-server-__init__.mdx b/docs/python-sdk/fastmcp-server-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..157a018cefd405f0cb1e3339d132fd5abeeb909a --- /dev/null +++ b/docs/python-sdk/fastmcp-server-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.server` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-server-auth-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c86f070058cd508e53c696e9307a8c2fafbcdb9b --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.server.auth` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8a20aa71629dd34e0cd3703f80ab2d461f9b8646 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-auth.mdx @@ -0,0 +1,10 @@ +--- +title: auth +sidebarTitle: auth +--- + +# `fastmcp.server.auth.auth` + +## Classes + +### `OAuthProvider` diff --git a/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9de7cce8a523f2787a77b74e2dc7eeb713c8033a --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.server.auth.providers` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5e85ee1e98a1a8ff5e698402306fea1d25f16c8c --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx @@ -0,0 +1,69 @@ +--- +title: bearer +sidebarTitle: bearer +--- + +# `fastmcp.server.auth.providers.bearer` + +## Classes + +### `JWKData` + + +JSON Web Key data structure. + + +### `JWKSData` + + +JSON Web Key Set data structure. + + +### `RSAKeyPair` + +**Methods:** + +#### `generate` + +```python +generate(cls) -> 'RSAKeyPair' +``` + +Generate an RSA key pair for testing. + +**Returns:** +- (private_key_pem, public_key_pem) + + +#### `create_token` + +```python +create_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str +``` + +Generate a test JWT token for testing purposes. + +**Args:** +- `private_key_pem`: RSA private key in PEM format +- `subject`: Subject claim (usually user ID) +- `issuer`: Issuer claim +- `audience`: Audience claim - can be a string or list of strings (optional) +- `scopes`: List of scopes to include +- `expires_in_seconds`: Token expiration time in seconds +- `additional_claims`: Any additional claims to include +- `kid`: Key ID for JWKS lookup (optional) + +**Returns:** +- Signed JWT token string + + +### `BearerAuthProvider` + + +Simple JWT Bearer Token validator for hosted MCP servers. +Uses RS256 asymmetric encryption. Supports either static public key +or JWKS URI for key rotation. + +Note that this provider DOES NOT permit client registration or revocation, or any OAuth flows. +It is intended to be used with a control plane that manages clients and tokens. + diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f64c65c84e3de6f62a1aebe27d49d1415a9a7c57 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx @@ -0,0 +1,22 @@ +--- +title: bearer_env +sidebarTitle: bearer_env +--- + +# `fastmcp.server.auth.providers.bearer_env` + +## Classes + +### `EnvBearerAuthProviderSettings` + + +Settings for the BearerAuthProvider. + + +### `EnvBearerAuthProvider` + + +A BearerAuthProvider that loads settings from environment variables. Any +providing setting will always take precedence over the environment +variables. + diff --git a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ef34ce2fb5ee870ecefb8fa8f783012337f4f57e --- /dev/null +++ b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx @@ -0,0 +1,15 @@ +--- +title: in_memory +sidebarTitle: in_memory +--- + +# `fastmcp.server.auth.providers.in_memory` + +## Classes + +### `InMemoryOAuthProvider` + + +An in-memory OAuth provider for testing purposes. +It simulates the OAuth 2.1 flow locally without external calls. + diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ea1d92643386d05344939680668dbdd99e23931f --- /dev/null +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -0,0 +1,118 @@ +--- +title: context +sidebarTitle: context +--- + +# `fastmcp.server.context` + +## Functions + +### `set_context` + +```python +set_context(context: Context) -> Generator[Context, None, None] +``` + +## Classes + +### `Context` + + +Context object providing access to MCP capabilities. + +This provides a cleaner interface to MCP's RequestContext functionality. +It gets injected into tool and resource functions that request it via type hints. + +To use context in a tool function, add a parameter with the Context type annotation: + +```python +@server.tool +def my_tool(x: int, ctx: Context) -> str: + # Log messages to the client + ctx.info(f"Processing {x}") + ctx.debug("Debug info") + ctx.warning("Warning message") + ctx.error("Error message") + + # Report progress + ctx.report_progress(50, 100, "Processing") + + # Access resources + data = ctx.read_resource("resource://data") + + # Get request info + request_id = ctx.request_id + client_id = ctx.client_id + + return str(x) +``` + +The context parameter name can be anything as long as it's annotated with Context. +The context is optional - tools that don't need it can omit the parameter. + + +**Methods:** + +#### `request_context` + +```python +request_context(self) -> RequestContext +``` + +Access to the underlying request context. + +If called outside of a request context, this will raise a ValueError. + + +#### `client_id` + +```python +client_id(self) -> str | None +``` + +Get the client ID if available. + + +#### `request_id` + +```python +request_id(self) -> str +``` + +Get the unique ID for this request. + + +#### `session_id` + +```python +session_id(self) -> str | None +``` + +Get the MCP session ID for HTTP transports. + +Returns the session ID that can be used as a key for session-based +data storage (e.g., Redis) to share data between tool calls within +the same client session. + +**Returns:** +- The session ID for HTTP transports (SSE, StreamableHTTP), or None +- for stdio and in-memory transports which don't use session IDs. + + +#### `session` + +```python +session(self) +``` + +Access to the underlying session for advanced usage. + + +#### `get_http_request` + +```python +get_http_request(self) -> Request +``` + +Get the active starlette request. + diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx new file mode 100644 index 0000000000000000000000000000000000000000..0d6c3707497a8d93bd8e15f3e6f1f410ade67b92 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-dependencies.mdx @@ -0,0 +1,36 @@ +--- +title: dependencies +sidebarTitle: dependencies +--- + +# `fastmcp.server.dependencies` + +## Functions + +### `get_context` + +```python +get_context() -> Context +``` + +### `get_http_request` + +```python +get_http_request() -> Request +``` + +### `get_http_headers` + +```python +get_http_headers(include_all: bool = False) -> dict[str, str] +``` + + +Extract headers from the current HTTP request if available. + +Never raises an exception, even if there is no active HTTP request (in which case +an empty dict is returned). + +By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients. +If `include_all` is True, all headers are returned. + diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx new file mode 100644 index 0000000000000000000000000000000000000000..63f2768cb061c37d6b5379d37af92ca1b8fa8b1c --- /dev/null +++ b/docs/python-sdk/fastmcp-server-http.mdx @@ -0,0 +1,113 @@ +--- +title: http +sidebarTitle: http +--- + +# `fastmcp.server.http` + +## Functions + +### `set_http_request` + +```python +set_http_request(request: Request) -> Generator[Request, None, None] +``` + +### `setup_auth_middleware_and_routes` + +```python +setup_auth_middleware_and_routes(auth: OAuthProvider) -> tuple[list[Middleware], list[BaseRoute], list[str]] +``` + + +Set up authentication middleware and routes if auth is enabled. + +**Args:** +- `auth`: The OAuthProvider authorization server provider + +**Returns:** +- Tuple of (middleware, auth_routes, required_scopes) + + +### `create_base_app` + +```python +create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan +``` + + +Create a base Starlette app with common middleware and routes. + +**Args:** +- `routes`: List of routes to include in the app +- `middleware`: List of middleware to include in the app +- `debug`: Whether to enable debug mode +- `lifespan`: Optional lifespan manager for the app + +**Returns:** +- A Starlette application + + +### `create_sse_app` + +```python +create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: OAuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan +``` + + +Return an instance of the SSE server app. + +**Args:** +- `server`: The FastMCP server instance +- `message_path`: Path for SSE messages +- `sse_path`: Path for SSE connections +- `auth`: Optional auth provider +- `debug`: Whether to enable debug mode +- `routes`: Optional list of custom routes +- `middleware`: Optional list of middleware + +Returns: + A Starlette application with RequestContextMiddleware + + +### `create_streamable_http_app` + +```python +create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, auth: OAuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan +``` + + +Return an instance of the StreamableHTTP server app. + +**Args:** +- `server`: The FastMCP server instance +- `streamable_http_path`: Path for StreamableHTTP connections +- `event_store`: Optional event store for session management +- `auth`: Optional auth provider +- `json_response`: Whether to use JSON response format +- `stateless_http`: Whether to use stateless mode (new transport per request) +- `debug`: Whether to enable debug mode +- `routes`: Optional list of custom routes +- `middleware`: Optional list of middleware + +**Returns:** +- A Starlette application with StreamableHTTP support + + +## Classes + +### `StarletteWithLifespan` + +**Methods:** + +#### `lifespan` + +```python +lifespan(self) -> Lifespan +``` + +### `RequestContextMiddleware` + + +Middleware that stores each request in a ContextVar + diff --git a/docs/python-sdk/fastmcp-server-middleware.mdx b/docs/python-sdk/fastmcp-server-middleware.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ec8eab24292b39ad91b44d8397797169122609d8 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-middleware.mdx @@ -0,0 +1,56 @@ +--- +title: middleware +sidebarTitle: middleware +--- + +# `fastmcp.server.middleware` + +## Functions + +### `make_middleware_wrapper` + +```python +make_middleware_wrapper(middleware: Middleware, call_next: CallNext[T, R]) -> CallNext[T, R] +``` + + +Create a wrapper that applies a single middleware to a context. The +closure bakes in the middleware and call_next function, so it can be +passed to other functions that expect a call_next function. + + +## Classes + +### `CallNext` + +### `CallToolResult` + +### `ListToolsResult` + +### `ListResourcesResult` + +### `ListResourceTemplatesResult` + +### `ListPromptsResult` + +### `ServerResultProtocol` + +### `MiddlewareContext` + + +Unified context for all middleware operations. + + +**Methods:** + +#### `copy` + +```python +copy(self, **kwargs: Any) -> MiddlewareContext[T] +``` + +### `Middleware` + + +Base class for FastMCP middleware with dispatching hooks. + diff --git a/docs/python-sdk/fastmcp-server-openapi.mdx b/docs/python-sdk/fastmcp-server-openapi.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d2490cea7a497fdb337b74c405726c9d0b405f6c --- /dev/null +++ b/docs/python-sdk/fastmcp-server-openapi.mdx @@ -0,0 +1,58 @@ +--- +title: openapi +sidebarTitle: openapi +--- + +# `fastmcp.server.openapi` + + +FastMCP server implementation for OpenAPI integration. + +## Classes + +### `MCPType` + + +Type of FastMCP component to create from a route. + + +### `RouteType` + + +Deprecated: Use MCPType instead. + +This enum is kept for backward compatibility and will be removed in a future version. + + +### `RouteMap` + + +Mapping configuration for HTTP routes to FastMCP component types. + + +### `OpenAPITool` + + +Tool implementation for OpenAPI endpoints. + + +### `OpenAPIResource` + + +Resource implementation for OpenAPI endpoints. + + +### `OpenAPIResourceTemplate` + + +Resource template implementation for OpenAPI endpoints. + + +### `FastMCPOpenAPI` + + +FastMCP server implementation that creates components from an OpenAPI schema. + +This class parses an OpenAPI specification and creates appropriate FastMCP components +(Tools, Resources, ResourceTemplates) based on route mappings. + diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bad549605ec19e98e221653f855b36d545c6b779 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-proxy.mdx @@ -0,0 +1,101 @@ +--- +title: proxy +sidebarTitle: proxy +--- + +# `fastmcp.server.proxy` + +## Classes + +### `ProxyToolManager` + + +A ToolManager that sources its tools from a remote client in addition to local and mounted tools. + + +### `ProxyResourceManager` + + +A ResourceManager that sources its resources from a remote client in addition to local and mounted resources. + + +### `ProxyPromptManager` + + +A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts. + + +### `ProxyTool` + + +A Tool that represents and executes a tool on a remote server. + + +**Methods:** + +#### `from_mcp_tool` + +```python +from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool +``` + +Factory method to create a ProxyTool from a raw MCP tool schema. + + +### `ProxyResource` + + +A Resource that represents and reads a resource from a remote server. + + +**Methods:** + +#### `from_mcp_resource` + +```python +from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> ProxyResource +``` + +Factory method to create a ProxyResource from a raw MCP resource schema. + + +### `ProxyTemplate` + + +A ResourceTemplate that represents and creates resources from a remote server template. + + +**Methods:** + +#### `from_mcp_template` + +```python +from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate +``` + +Factory method to create a ProxyTemplate from a raw MCP template schema. + + +### `ProxyPrompt` + + +A Prompt that represents and renders a prompt from a remote server. + + +**Methods:** + +#### `from_mcp_prompt` + +```python +from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt +``` + +Factory method to create a ProxyPrompt from a raw MCP prompt schema. + + +### `FastMCPProxy` + + +A FastMCP server that acts as a proxy to a remote MCP-compliant server. +It uses specialized managers that fulfill requests via an HTTP client. + diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2b3c1ed83133f2dc417184204b29c3e1e032f07d --- /dev/null +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -0,0 +1,542 @@ +--- +title: server +sidebarTitle: server +--- + +# `fastmcp.server.server` + + +FastMCP - A more ergonomic interface for MCP servers. + +## Functions + +### `add_resource_prefix` + +```python +add_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str +``` + + +Add a prefix to a resource URI. + + Args: + uri: The original resource URI + prefix: The prefix to add + + Returns: + The resource URI with the prefix added + + Examples: + >>> add_resource_prefix("resource://path/to/resource", "prefix") + "resource://prefix/path/to/resource" # with new style + >>> add_resource_prefix("resource://path/to/resource", "prefix") + "prefix+resource://path/to/resource" # with legacy style + >>> add_resource_prefix("resource:///absolute/path", "prefix") + "resource://prefix//absolute/path" # with new style + + Raises: + ValueError: If the URI doesn't match the expected protocol://path format + + +### `remove_resource_prefix` + +```python +remove_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str +``` + + +Remove a prefix from a resource URI. + + Args: + uri: The resource URI with a prefix + prefix: The prefix to remove + prefix_format: The format of the prefix to remove + Returns: + The resource URI with the prefix removed + + Examples: + >>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix") + "resource://path/to/resource" # with new style + >>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix") + "resource://path/to/resource" # with legacy style + >>> remove_resource_prefix("resource://prefix//absolute/path", "prefix") + "resource:///absolute/path" # with new style + + Raises: + ValueError: If the URI doesn't match the expected protocol://path format + + +### `has_resource_prefix` + +```python +has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool +``` + + +Check if a resource URI has a specific prefix. + + Args: + uri: The resource URI to check + prefix: The prefix to look for + + Returns: + True if the URI has the specified prefix, False otherwise + + Examples: + >>> has_resource_prefix("resource://prefix/path/to/resource", "prefix") + True # with new style + >>> has_resource_prefix("prefix+resource://path/to/resource", "prefix") + True # with legacy style + >>> has_resource_prefix("resource://other/path/to/resource", "prefix") + False + + Raises: + ValueError: If the URI doesn't match the expected protocol://path format + + +## Classes + +### `FastMCP` + +**Methods:** + +#### `settings` + +```python +settings(self) -> Settings +``` + +#### `name` + +```python +name(self) -> str +``` + +#### `instructions` + +```python +instructions(self) -> str | None +``` + +#### `run` + +```python +run(self, transport: Literal['stdio', 'streamable-http', 'sse'] | None = None, **transport_kwargs: Any) -> None +``` + +Run the FastMCP server. Note this is a synchronous function. + +**Args:** +- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http") + + +#### `add_middleware` + +```python +add_middleware(self, middleware: Middleware) -> None +``` + +#### `custom_route` + +```python +custom_route(self, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) +``` + +Decorator to register a custom HTTP route on the FastMCP server. + +Allows adding arbitrary HTTP endpoints outside the standard MCP protocol, +which can be useful for OAuth callbacks, health checks, or admin APIs. +The handler function must be an async function that accepts a Starlette +Request and returns a Response. + +**Args:** +- `path`: URL path for the route (e.g., "/oauth/callback") +- `methods`: List of HTTP methods to support (e.g., ["GET", "POST"]) +- `name`: Optional name for the route (to reference this route with +Starlette's reverse URL lookup feature) +- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True + + +#### `add_tool` + +```python +add_tool(self, tool: Tool) -> None +``` + +Add a tool to the server. + +The tool function can optionally request a Context object by adding a parameter +with the Context type annotation. See the @tool decorator for examples. + +**Args:** +- `tool`: The Tool instance to register + + +#### `remove_tool` + +```python +remove_tool(self, name: str) -> None +``` + +Remove a tool from the server. + +**Args:** +- `name`: The name of the tool to remove + +**Raises:** +- `NotFoundError`: If the tool is not found + + +#### `tool` + +```python +tool(self, name_or_fn: AnyFunction) -> FunctionTool +``` + +#### `tool` + +```python +tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool] +``` + +#### `tool` + +```python +tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool +``` + +Decorator to register a tool. + +Tools can optionally request a Context object by adding a parameter with the +Context type annotation. The context provides access to MCP capabilities like +logging, progress reporting, and resource access. + +This decorator supports multiple calling patterns: +- @server.tool (without parentheses) +- @server.tool (with empty parentheses) +- @server.tool("custom_name") (with name as first argument) +- @server.tool(name="custom_name") (with name as keyword argument) +- server.tool(function, name="custom_name") (direct function call) + +**Args:** +- `name_or_fn`: Either a function (when used as @tool), a string name, or None +- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn) +- `description`: Optional description of what the tool does +- `tags`: Optional set of tags for categorizing the tool +- `annotations`: Optional annotations about the tool's behavior (e.g. {"is_async"\: True}) +- `exclude_args`: Optional list of argument names to exclude from the tool schema +- `enabled`: Optional boolean to enable or disable the tool + + +#### `add_resource` + +```python +add_resource(self, resource: Resource) -> None +``` + +Add a resource to the server. + +**Args:** +- `resource`: A Resource instance to add + + +#### `add_template` + +```python +add_template(self, template: ResourceTemplate) -> None +``` + +Add a resource template to the server. + +**Args:** +- `template`: A ResourceTemplate instance to add + + +#### `add_resource_fn` + +```python +add_resource_fn(self, fn: AnyFunction, uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> None +``` + +Add a resource or template to the server from a function. + +If the URI contains parameters (e.g. "resource://{param}") or the function +has parameters, it will be registered as a template resource. + +**Args:** +- `fn`: The function to register as a resource +- `uri`: The URI for the resource +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource + + +#### `resource` + +```python +resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate] +``` + +Decorator to register a function as a resource. + +The function will be called when the resource is read to generate its content. +The function can return: +- str for text content +- bytes for binary content +- other types will be converted to JSON + +Resources can optionally request a Context object by adding a parameter with the +Context type annotation. The context provides access to MCP capabilities like +logging, progress reporting, and session information. + +If the URI contains parameters (e.g. "resource://{param}") or the function +has parameters, it will be registered as a template resource. + +**Args:** +- `uri`: URI for the resource (e.g. "resource\://my-resource" or "resource\://{param}") +- `name`: Optional name for the resource +- `description`: Optional description of the resource +- `mime_type`: Optional MIME type for the resource +- `tags`: Optional set of tags for categorizing the resource +- `enabled`: Optional boolean to enable or disable the resource + + +#### `add_prompt` + +```python +add_prompt(self, prompt: Prompt) -> None +``` + +Add a prompt to the server. + +**Args:** +- `prompt`: A Prompt instance to add + + +#### `prompt` + +```python +prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt +``` + +#### `prompt` + +```python +prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt] +``` + +#### `prompt` + +```python +prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt +``` + +Decorator to register a prompt. + + Prompts can optionally request a Context object by adding a parameter with the + Context type annotation. The context provides access to MCP capabilities like + logging, progress reporting, and session information. + + This decorator supports multiple calling patterns: + - @server.prompt (without parentheses) + - @server.prompt() (with empty parentheses) + - @server.prompt("custom_name") (with name as first argument) + - @server.prompt(name="custom_name") (with name as keyword argument) + - server.prompt(function, name="custom_name") (direct function call) + + Args: + name_or_fn: Either a function (when used as @prompt), a string name, or None + name: Optional name for the prompt (keyword-only, alternative to name_or_fn) + description: Optional description of what the prompt does + tags: Optional set of tags for categorizing the prompt + enabled: Optional boolean to enable or disable the prompt + + Example: + @server.prompt + def analyze_table(table_name: str) -> list\[Message]: + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: +{schema}" + } + ] + + @server.prompt() + def analyze_with_context(table_name: str, ctx: Context) -> list\[Message]: + ctx.info(f"Analyzing table {table_name}") + schema = read_table_schema(table_name) + return [ + { + "role": "user", + "content": f"Analyze this schema: +{schema}" + } + ] + + @server.prompt("custom_name") + def analyze_file(path: str) -> list\[Message]: + content = await read_file(path) + return [ + { + "role": "user", + "content": { + "type": "resource", + "resource": { + "uri": f"file://{path}", + "text": content + } + } + } + ] + + @server.prompt(name="custom_name") + def another_prompt(data: str) -> list\[Message]: + return [{"role": "user", "content": data}] + + # Direct function call + server.prompt(my_function, name="custom_name") + + +#### `sse_app` + +```python +sse_app(self, path: str | None = None, message_path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan +``` + +Create a Starlette app for the SSE server. + +**Args:** +- `path`: The path to the SSE endpoint +- `message_path`: The path to the message endpoint +- `middleware`: A list of middleware to apply to the app + + +#### `streamable_http_app` + +```python +streamable_http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan +``` + +Create a Starlette app for the StreamableHTTP server. + +**Args:** +- `path`: The path to the StreamableHTTP endpoint +- `middleware`: A list of middleware to apply to the app + + +#### `http_app` + +```python +http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['streamable-http', 'sse'] = 'streamable-http') -> StarletteWithLifespan +``` + +Create a Starlette app using the specified HTTP transport. + +**Args:** +- `path`: The path for the HTTP endpoint +- `middleware`: A list of middleware to apply to the app +- `transport`: Transport protocol to use - either "streamable-http" (default) or "sse" + +**Returns:** +- A Starlette application configured with the specified transport + + +#### `mount` + +```python +mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None +``` + +Mount another FastMCP server on this server with an optional prefix. + +Unlike importing (with import_server), mounting establishes a dynamic connection +between servers. When a client interacts with a mounted server's objects through +the parent server, requests are forwarded to the mounted server in real-time. +This means changes to the mounted server are immediately reflected when accessed +through the parent. + +When a server is mounted with a prefix: +- Tools from the mounted server are accessible with prefixed names. + Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather". +- Resources are accessible with prefixed URIs. + Example: If server has a resource with URI "weather://forecast", it will be available as + "weather://prefix/forecast". +- Templates are accessible with prefixed URI templates. + Example: If server has a template with URI "weather://location/{id}", it will be available + as "weather://prefix/location/{id}". +- Prompts are accessible with prefixed names. + Example: If server has a prompt named "weather_prompt", it will be available as + "prefix_weather_prompt". + +When a server is mounted without a prefix (prefix=None), its tools, resources, templates, +and prompts are accessible with their original names. Multiple servers can be mounted +without prefixes, and they will be tried in order until a match is found. + +There are two modes for mounting servers: +1. Direct mounting (default when server has no custom lifespan): The parent server + directly accesses the mounted server's objects in-memory for better performance. + In this mode, no client lifecycle events occur on the mounted server, including + lifespan execution. + +2. Proxy mounting (default when server has a custom lifespan): The parent server + treats the mounted server as a separate entity and communicates with it via a + Client transport. This preserves all client-facing behaviors, including lifespan + execution, but with slightly higher overhead. + +**Args:** +- `server`: The FastMCP server to mount. +- `prefix`: Optional prefix to use for the mounted server's objects. If None, +the server's objects are accessible with their original names. +- `as_proxy`: Whether to treat the mounted server as a proxy. If None (default), +automatically determined based on whether the server has a custom lifespan +(True if it has a custom lifespan, False otherwise). +- `tool_separator`: Deprecated. Separator character for tool names. +- `resource_separator`: Deprecated. Separator character for resource URIs. +- `prompt_separator`: Deprecated. Separator character for prompt names. + + +#### `from_openapi` + +```python +from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI +``` + +Create a FastMCP server from an OpenAPI specification. + + +#### `from_fastapi` + +```python +from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI +``` + +Create a FastMCP server from a FastAPI application. + + +#### `as_proxy` + +```python +as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy +``` + +Create a FastMCP proxy server for the given backend. + +The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client` +instance or any value accepted as the ``transport`` argument of +:class:`~fastmcp.client.Client`. This mirrors the convenience of the +``Client`` constructor. + + +#### `from_client` + +```python +from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy +``` + +Create a FastMCP proxy server from a FastMCP client. + + +### `MountedServer` diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fd3e3d791b98b7fae21170e5a9cca4433e61ca4d --- /dev/null +++ b/docs/python-sdk/fastmcp-settings.mdx @@ -0,0 +1,59 @@ +--- +title: settings +sidebarTitle: settings +--- + +# `fastmcp.settings` + +## Classes + +### `ExtendedEnvSettingsSource` + + +A special EnvSettingsSource that allows for multiple env var prefixes to be used. + +Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used. + + +**Methods:** + +#### `get_field_value` + +```python +get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool] +``` + +### `ExtendedSettingsConfigDict` + +### `Settings` + + +FastMCP settings. + + +**Methods:** + +#### `settings_customise_sources` + +```python +settings_customise_sources(cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource) -> tuple[PydanticBaseSettingsSource, ...] +``` + +#### `settings` + +```python +settings(self) -> Self +``` + +This property is for backwards compatibility with FastMCP < 2.8.0, +which accessed fastmcp.settings.settings + + +#### `setup_logging` + +```python +setup_logging(self) -> Self +``` + +Finalize the settings. + diff --git a/docs/python-sdk/fastmcp-tools-__init__.mdx b/docs/python-sdk/fastmcp-tools-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5b7c8b04dc4db831f2c71330530bbdd3a594e98f --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-__init__.mdx @@ -0,0 +1,8 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.tools` + +*This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7cae406aaa56c04304a346c9102c6b09023ca0bb --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-tool.mdx @@ -0,0 +1,68 @@ +--- +title: tool +sidebarTitle: tool +--- + +# `fastmcp.tools.tool` + +## Functions + +### `default_serializer` + +```python +default_serializer(data: Any) -> str +``` + +## Classes + +### `Tool` + + +Internal tool registration info. + + +**Methods:** + +#### `to_mcp_tool` + +```python +to_mcp_tool(self, **overrides: Any) -> MCPTool +``` + +#### `from_function` + +```python +from_function(fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> FunctionTool +``` + +Create a Tool from a function. + + +#### `from_tool` + +```python +from_tool(cls, tool: Tool, transform_fn: Callable[..., Any] | None = None, name: str | None = None, transform_args: dict[str, ArgTransform] | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> TransformedTool +``` + +### `FunctionTool` + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> FunctionTool +``` + +Create a Tool from a function. + + +### `ParsedFunction` + +**Methods:** + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True) -> ParsedFunction +``` diff --git a/docs/python-sdk/fastmcp-tools-tool_manager.mdx b/docs/python-sdk/fastmcp-tools-tool_manager.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fad031d72a4d093c3cf457ca6b22e93a31fe267e --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-tool_manager.mdx @@ -0,0 +1,58 @@ +--- +title: tool_manager +sidebarTitle: tool_manager +--- + +# `fastmcp.tools.tool_manager` + +## Classes + +### `ToolManager` + + +Manages FastMCP tools. + + +**Methods:** + +#### `mount` + +```python +mount(self, server: MountedServer) -> None +``` + +Adds a mounted server as a source for tools. + + +#### `add_tool_from_fn` + +```python +add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, exclude_args: list[str] | None = None) -> Tool +``` + +Add a tool to the server. + + +#### `add_tool` + +```python +add_tool(self, tool: Tool) -> Tool +``` + +Register a tool with the server. + + +#### `remove_tool` + +```python +remove_tool(self, key: str) -> None +``` + +Remove a tool from the server. + +**Args:** +- `key`: The key of the tool to remove + +**Raises:** +- `NotFoundError`: If the tool is not found + diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx new file mode 100644 index 0000000000000000000000000000000000000000..abee7d5eb6dafb12c076be65bf5c7b011a800874 --- /dev/null +++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx @@ -0,0 +1,117 @@ +--- +title: tool_transform +sidebarTitle: tool_transform +--- + +# `fastmcp.tools.tool_transform` + +## Classes + +### `ArgTransform` + + +Configuration for transforming a parent tool's argument. + + This class allows fine-grained control over how individual arguments are transformed + when creating a new tool from an existing one. You can rename arguments, change their + descriptions, add default values, or hide them from clients while passing constants. + + Attributes: + name: New name for the argument. Use None to keep original name, or ... for no change. + description: New description for the argument. Use None to remove description, or ... for no change. + default: New default value for the argument. Use ... for no change. + default_factory: Callable that returns a default value. Cannot be used with default. + type: New type for the argument. Use ... for no change. + hide: If True, hide this argument from clients but pass a constant value to parent. + required: If True, make argument required (remove default). Use ... for no change. + examples: Examples for the argument. Use ... for no change. + + Examples: + # Rename argument 'old_name' to 'new_name' + ArgTransform(name="new_name") + + # Change description only + ArgTransform(description="Updated description") + + # Add a default value (makes argument optional) + ArgTransform(default=42) + + # Add a default factory (makes argument optional) + ArgTransform(default_factory=lambda: time.time()) + + # Change the type + ArgTransform(type=str) + + # Hide the argument entirely from clients + ArgTransform(hide=True) + + # Hide argument but pass a constant value to parent + ArgTransform(hide=True, default="constant_value") + + # Hide argument but pass a factory-generated value to parent + ArgTransform(hide=True, default_factory=lambda: uuid.uuid4().hex) + + # Make an optional parameter required (removes any default) + ArgTransform(required=True) + + # Combine multiple transformations + ArgTransform(name="new_name", description="New desc", default=None, type=int) + + +### `TransformedTool` + + +A tool that is transformed from another tool. + +This class represents a tool that has been created by transforming another tool. +It supports argument renaming, schema modification, custom function injection, +and provides context for the forward() and forward_raw() functions. + +The transformation can be purely schema-based (argument renaming, dropping, etc.) +or can include a custom function that uses forward() to call the parent tool +with transformed arguments. + + +**Methods:** + +#### `from_tool` + +```python +from_tool(cls, tool: Tool, name: str | None = None, description: str | None = None, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> TransformedTool +``` + +Create a transformed tool from a parent tool. + +**Args:** +- `tool`: The parent tool to transform. +- `transform_fn`: Optional custom function. Can use forward() and forward_raw() +to call the parent tool. Functions with **kwargs receive transformed +argument names. +- `name`: New name for the tool. Defaults to parent tool's name. +- `transform_args`: Optional transformations for parent tool arguments. +Only specified arguments are transformed, others pass through unchanged\: +- str\: Simple rename +- ArgTransform\: Complex transformation (rename/description/default/drop) +- None\: Drop the argument +- `description`: New description. Defaults to parent's description. +- `tags`: New tags. Defaults to parent's tags. +- `annotations`: New annotations. Defaults to parent's annotations. +- `serializer`: New serializer. Defaults to parent's serializer. + +**Returns:** +- TransformedTool with the specified transformations. + +Examples: +- # Transform specific arguments only +- Tool.from_tool(parent, transform_args={"old": "new"}) # Others unchanged +- # Custom function with partial transforms +- async def custom(x: int, y: int) -> str: +result = await forward(x=x, y=y) +return f"Custom: {result}" +- Tool.from_tool(parent, transform_fn=custom, transform_args={"a": "x", "b": "y"}) +- # Using **kwargs (gets all args, transformed and untransformed) +- async def flexible(**kwargs) -> str: +result = await forward(**kwargs) +return f"Got: {kwargs}" +- Tool.from_tool(parent, transform_fn=flexible, transform_args={"a": "x"}) + diff --git a/docs/python-sdk/fastmcp-utilities-__init__.mdx b/docs/python-sdk/fastmcp-utilities-__init__.mdx new file mode 100644 index 0000000000000000000000000000000000000000..12e12b4eda55d7e2666d5f4f6086dd9171faec26 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-__init__.mdx @@ -0,0 +1,9 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.utilities` + + +FastMCP utility modules. diff --git a/docs/python-sdk/fastmcp-utilities-cache.mdx b/docs/python-sdk/fastmcp-utilities-cache.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ab41395d9e561dcf06693bd2c398b062761c2656 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-cache.mdx @@ -0,0 +1,30 @@ +--- +title: cache +sidebarTitle: cache +--- + +# `fastmcp.utilities.cache` + +## Classes + +### `TimedCache` + +**Methods:** + +#### `set` + +```python +set(self, key: Any, value: Any) -> None +``` + +#### `get` + +```python +get(self, key: Any) -> Any +``` + +#### `clear` + +```python +clear(self) -> None +``` diff --git a/docs/python-sdk/fastmcp-utilities-components.mdx b/docs/python-sdk/fastmcp-utilities-components.mdx new file mode 100644 index 0000000000000000000000000000000000000000..61434c7d52ba6f89daf9efb9e22004007d0085f4 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-components.mdx @@ -0,0 +1,52 @@ +--- +title: components +sidebarTitle: components +--- + +# `fastmcp.utilities.components` + +## Classes + +### `FastMCPComponent` + + +Base class for FastMCP tools, prompts, resources, and resource templates. + + +**Methods:** + +#### `key` + +```python +key(self) -> str +``` + +The key of the component. This is used for internal bookkeeping +and may reflect e.g. prefixes or other identifiers. You should not depend on +keys having a certain value, as the same tool loaded from different +hierarchies of servers may have different keys. + + +#### `with_key` + +```python +with_key(self, key: str) -> Self +``` + +#### `enable` + +```python +enable(self) -> None +``` + +Enable the component. + + +#### `disable` + +```python +disable(self) -> None +``` + +Disable the component. + diff --git a/docs/python-sdk/fastmcp-utilities-exceptions.mdx b/docs/python-sdk/fastmcp-utilities-exceptions.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2d480a14669cade382ef0e632a5882dc39a71b3e --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-exceptions.mdx @@ -0,0 +1,20 @@ +--- +title: exceptions +sidebarTitle: exceptions +--- + +# `fastmcp.utilities.exceptions` + +## Functions + +### `iter_exc` + +```python +iter_exc(group: BaseExceptionGroup) +``` + +### `get_catch_handlers` + +```python +get_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]] +``` diff --git a/docs/python-sdk/fastmcp-utilities-http.mdx b/docs/python-sdk/fastmcp-utilities-http.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6e5e4b75ff2257130349c90a8a7879ecee24c234 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-http.mdx @@ -0,0 +1,18 @@ +--- +title: http +sidebarTitle: http +--- + +# `fastmcp.utilities.http` + +## Functions + +### `find_available_port` + +```python +find_available_port() -> int +``` + + +Find an available port by letting the OS assign one. + diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ad68473a0755bc23e0a33292dfc590922a3ea222 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx @@ -0,0 +1,25 @@ +--- +title: json_schema +sidebarTitle: json_schema +--- + +# `fastmcp.utilities.json_schema` + +## Functions + +### `compress_schema` + +```python +compress_schema(schema: dict, prune_params: list[str] | None = None, prune_defs: bool = True, prune_additional_properties: bool = True, prune_titles: bool = False) -> dict +``` + + +Remove the given parameters from the schema. + +**Args:** +- `schema`: The schema to compress +- `prune_params`: List of parameter names to remove from properties +- `prune_defs`: Whether to remove unused definitions +- `prune_additional_properties`: Whether to remove additionalProperties\: false +- `prune_titles`: Whether to remove title fields from the schema + diff --git a/docs/python-sdk/fastmcp-utilities-logging.mdx b/docs/python-sdk/fastmcp-utilities-logging.mdx new file mode 100644 index 0000000000000000000000000000000000000000..90e294f6aa0f9a7e1dfbe235dfc673156bd2ce05 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-logging.mdx @@ -0,0 +1,41 @@ +--- +title: logging +sidebarTitle: logging +--- + +# `fastmcp.utilities.logging` + + +Logging utilities for FastMCP. + +## Functions + +### `get_logger` + +```python +get_logger(name: str) -> logging.Logger +``` + + +Get a logger nested under FastMCP namespace. + +**Args:** +- `name`: the name of the logger, which will be prefixed with 'FastMCP.' + +**Returns:** +- a configured logger instance + + +### `configure_logging` + +```python +configure_logging(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | int = 'INFO', logger: logging.Logger | None = None, enable_rich_tracebacks: bool = True) -> None +``` + + +Configure logging for FastMCP. + +**Args:** +- `logger`: the logger to configure +- `level`: the log level to use + diff --git a/docs/python-sdk/fastmcp-utilities-mcp_config.mdx b/docs/python-sdk/fastmcp-utilities-mcp_config.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b74dfcfa0089bfdce5eb4c5694022e2bef177cdc --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-mcp_config.mdx @@ -0,0 +1,50 @@ +--- +title: mcp_config +sidebarTitle: mcp_config +--- + +# `fastmcp.utilities.mcp_config` + +## Functions + +### `infer_transport_type_from_url` + +```python +infer_transport_type_from_url(url: str | AnyUrl) -> Literal['streamable-http', 'sse'] +``` + + +Infer the appropriate transport type from the given URL. + + +## Classes + +### `StdioMCPServer` + +**Methods:** + +#### `to_transport` + +```python +to_transport(self) -> StdioTransport +``` + +### `RemoteMCPServer` + +**Methods:** + +#### `to_transport` + +```python +to_transport(self) -> StreamableHttpTransport | SSETransport +``` + +### `MCPConfig` + +**Methods:** + +#### `from_dict` + +```python +from_dict(cls, config: dict[str, Any]) -> MCPConfig +``` diff --git a/docs/python-sdk/fastmcp-utilities-openapi.mdx b/docs/python-sdk/fastmcp-utilities-openapi.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7b7d0aa6217b61e824136cc096297916c37adbe6 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-openapi.mdx @@ -0,0 +1,118 @@ +--- +title: openapi +sidebarTitle: openapi +--- + +# `fastmcp.utilities.openapi` + +## Functions + +### `parse_openapi_to_http_routes` + +```python +parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute] +``` + + +Parses an OpenAPI schema dictionary into a list of HTTPRoute objects +using the openapi-pydantic library. + +Supports both OpenAPI 3.0.x and 3.1.x versions. + + +### `clean_schema_for_display` + +```python +clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None +``` + + +Clean up a schema dictionary for display by removing internal/complex fields. + + +### `generate_example_from_schema` + +```python +generate_example_from_schema(schema: JsonSchema | None) -> Any +``` + + +Generate a simple example value from a JSON schema dictionary. +Very basic implementation focusing on types. + + +### `format_json_for_description` + +```python +format_json_for_description(data: Any, indent: int = 2) -> str +``` + + +Formats Python data as a JSON string block for markdown. + + +### `format_description_with_responses` + +```python +format_description_with_responses(base_description: str, responses: dict[str, Any], parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str +``` + + +Formats the base description string with response, parameter, and request body information. + +**Args:** +- `base_description`: The initial description to be formatted. +- `responses`: A dictionary of response information, keyed by status code. +- `parameters`: A list of parameter information, +including path and query parameters. Each parameter includes details such as name, +location, whether it is required, and a description. +- `request_body`: Information about the request body, +including its description, whether it is required, and its content schema. + +**Returns:** +- The formatted description string with additional details about responses, parameters, +- and the request body. + + +## Classes + +### `ParameterInfo` + + +Represents a single parameter for an HTTP operation in our IR. + + +### `RequestBodyInfo` + + +Represents the request body for an HTTP operation in our IR. + + +### `ResponseInfo` + + +Represents response information in our IR. + + +### `HTTPRoute` + + +Intermediate Representation for a single OpenAPI operation. + + +### `OpenAPIParser` + + +Unified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1. + + +**Methods:** + +#### `parse` + +```python +parse(self) -> list[HTTPRoute] +``` + +Parse the OpenAPI schema into HTTP routes. + diff --git a/docs/python-sdk/fastmcp-utilities-types.mdx b/docs/python-sdk/fastmcp-utilities-types.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3810bb878042f7fce7d0b8c4748daf56ed3c7f63 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-types.mdx @@ -0,0 +1,112 @@ +--- +title: types +sidebarTitle: types +--- + +# `fastmcp.utilities.types` + + +Common types used across FastMCP. + +## Functions + +### `get_cached_typeadapter` + +```python +get_cached_typeadapter(cls: T) -> TypeAdapter[T] +``` + + +TypeAdapters are heavy objects, and in an application context we'd typically +create them once in a global scope and reuse them as often as possible. +However, this isn't feasible for user-generated functions. Instead, we use a +cache to minimize the cost of creating them as much as possible. + + +### `issubclass_safe` + +```python +issubclass_safe(cls: type, base: type) -> bool +``` + + +Check if cls is a subclass of base, even if cls is a type variable. + + +### `is_class_member_of_type` + +```python +is_class_member_of_type(cls: type, base: type) -> bool +``` + + +Check if cls is a member of base, even if cls is a type variable. + +Base can be a type, a UnionType, or an Annotated type. Generic types are not +considered members (e.g. T is not a member of list\[T]). + + +### `find_kwarg_by_type` + +```python +find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None +``` + + +Find the name of the kwarg that is of type kwarg_type. + +Includes union types that contain the kwarg_type, as well as Annotated types. + + +## Classes + +### `FastMCPBaseModel` + + +Base model for FastMCP models. + + +### `Image` + + +Helper class for returning images from tools. + + +**Methods:** + +#### `to_image_content` + +```python +to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> ImageContent +``` + +Convert to MCP ImageContent. + + +### `Audio` + + +Helper class for returning audio from tools. + + +**Methods:** + +#### `to_audio_content` + +```python +to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> AudioContent +``` + +### `File` + + +Helper class for returning audio from tools. + + +**Methods:** + +#### `to_resource_content` + +```python +to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> EmbeddedResource +``` diff --git a/docs/servers/auth/bearer.mdx b/docs/servers/auth/bearer.mdx index 257238bc176fb80a9c7ba68534a63db356106605..900bebe5ab41764de605621165fb927bcc5e6ce8 100644 --- a/docs/servers/auth/bearer.mdx +++ b/docs/servers/auth/bearer.mdx @@ -3,7 +3,7 @@ title: Bearer Token Authentication sidebarTitle: Bearer Auth description: Secure your FastMCP server's HTTP endpoints by validating JWT Bearer tokens. icon: key -tag: "New!" +tag: NEW --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index 4862e0c2ae8da4d809eba17e0dac2289b05f2ccf..80c79978134f7bc7b934564aa8369345c35cca38 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -57,6 +57,84 @@ def generate_code_request(language: str, task_description: str) -> PromptMessage Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists. +### Argument Types + + + +The MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP: + +1. **Automatically converts** string arguments from MCP clients to the expected types +2. **Generates helpful descriptions** showing the exact JSON string format needed +3. **Preserves direct usage** - you can still call prompts with properly typed arguments + +Since the MCP specification only allows string arguments, clients need to know what string format to use for complex types. FastMCP solves this by automatically enhancing the argument descriptions with JSON schema information, making it clear to both humans and LLMs how to format their arguments. + + + +```python Python Code +@mcp.prompt +def analyze_data( + numbers: list[int], + metadata: dict[str, str], + threshold: float +) -> str: + """Analyze numerical data.""" + avg = sum(numbers) / len(numbers) + return f"Average: {avg}, above threshold: {avg > threshold}" +``` + +```json Resulting MCP Prompt +{ + "name": "analyze_data", + "description": "Analyze numerical data.", + "arguments": [ + { + "name": "numbers", + "description": "Provide as a JSON string matching the following schema: {\"items\":{\"type\":\"integer\"},\"type\":\"array\"}", + "required": true + }, + { + "name": "metadata", + "description": "Provide as a JSON string matching the following schema: {\"additionalProperties\":{\"type\":\"string\"},\"type\":\"object\"}", + "required": true + }, + { + "name": "threshold", + "description": "Provide as a JSON string matching the following schema: {\"type\":\"number\"}", + "required": true + } + ] +} +``` + + + +**MCP clients will call this prompt with string arguments:** +```json +{ + "numbers": "[1, 2, 3, 4, 5]", + "metadata": "{\"source\": \"api\", \"version\": \"1.0\"}", + "threshold": "2.5" +} +``` + +**But you can still call it directly with proper types:** +```python +# This also works for direct calls +result = await prompt.render({ + "numbers": [1, 2, 3, 4, 5], + "metadata": {"source": "api", "version": "1.0"}, + "threshold": 2.5 +}) +``` + + +Keep your type annotations simple when using this feature. Complex nested types or custom classes may not convert reliably from JSON strings. The automatically generated schema descriptions are the only guidance users receive about the expected format. + +Good choices: `list[int]`, `dict[str, str]`, `float`, `bool` +Avoid: Complex Pydantic models, deeply nested structures, custom classes + + ### Return Values FastMCP intelligently handles different return types from your prompt function: @@ -78,33 +156,6 @@ def roleplay_scenario(character: str, situation: str) -> list[Message]: ] ``` -### 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 diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx index 3a791450579393f7ba2be475088c2c27ece48e02..f38834980d31c47b189fd964032e1ed7743a397a 100644 --- a/docs/servers/resources.mdx +++ b/docs/servers/resources.mdx @@ -2,7 +2,7 @@ title: Resources & Templates sidebarTitle: Resources description: Expose data sources and dynamic content generators to your MCP client. -icon: database +icon: folder-open --- import { VersionBadge } from "/snippets/version-badge.mdx" diff --git a/docs/servers/fastmcp.mdx b/docs/servers/server.mdx similarity index 98% rename from docs/servers/fastmcp.mdx rename to docs/servers/server.mdx index 12aa08fd8bdf309e2bc0fac0b51d2156efe9fa0f..1cb5f089b5858d0c2421c05992301908aee9e1ed 100644 --- a/docs/servers/fastmcp.mdx +++ b/docs/servers/server.mdx @@ -1,7 +1,7 @@ --- title: The FastMCP Server -sidebarTitle: FastMCP Servers -description: Learn about the core FastMCP server class and how to run it. +sidebarTitle: Overview +description: The core FastMCP server class for building MCP applications with tools, resources, and prompts. icon: server --- diff --git a/docs/tutorials/rest-api.mdx b/docs/tutorials/rest-api.mdx index d0a51e80a4d0e46197e79ffbdbc4741a436eb073..1b6ae12887273de3ecb4e8a98117cb73e25b09cd 100644 --- a/docs/tutorials/rest-api.mdx +++ b/docs/tutorials/rest-api.mdx @@ -103,7 +103,7 @@ from fastmcp import Client async def main(): # Connect to the MCP server we just created - async with Client("http://127.0.0.1:8000/mcp") as client: + async with Client("http://127.0.0.1:8000/mcp/") as client: # List the tools that were automatically generated tools = await client.list_tools() diff --git a/docs/updates.mdx b/docs/updates.mdx index 28d25ccb75667f0899ff1769f766fe9d1bd328cc..27f7afb3916e0f1e60146ece8101c19da7c5afc3 100644 --- a/docs/updates.mdx +++ b/docs/updates.mdx @@ -2,7 +2,7 @@ title: "FastMCP Updates" sidebarTitle: "Updates" icon: "sparkles" -tag: "New!" +tag: NEW --- diff --git a/justfile b/justfile index 6d897b2e9b71d026dc32652c7b8d63f17d20b7ac..fc2f335013f6f865d00d428bf074068381921d23 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,8 @@ +# Build the project build: uv sync +# Run tests test: build uv run --frozen pytest -xvs tests @@ -8,5 +10,21 @@ test: build typecheck: uv run --frozen pyright +# Serve documentation locally docs: - cd docs && npx mintlify dev + cd docs && npx mint@latest dev + +# Generate API reference documentation for all modules +api-ref-all: + uvx --with-editable . --refresh-package mdxify mdxify@latest --all --root-module fastmcp --anchor-name "SDK Reference" + +# Generate API reference for specific modules (e.g., just api-ref prefect.flows prefect.tasks) +api-ref *MODULES: + uvx --with-editable . --refresh-package mdxify mdxify@latest {{MODULES}} --root-module fastmcp --anchor-name "SDK Reference" + +# Clean up API reference documentation +api-ref-clean: + rm -rf docs/python-sdk + +copy-context: + uvx --with-editable . --refresh-package copychat copychat@latest src/ docs/ -x changelog.mdx -x python-sdk/ -v \ No newline at end of file diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index f3e3262703532a1c04c701809a8182bbe36665cd..d3e34524e2b56c1f2c42424ef5d38a3954dcebb9 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -1,5 +1,6 @@ """FastMCP CLI tools.""" +import asyncio import importlib.metadata import importlib.util import os @@ -11,6 +12,7 @@ from typing import Annotated import dotenv import typer +from pydantic import TypeAdapter from rich.console import Console from rich.table import Table from typer import Context, Exit @@ -19,6 +21,7 @@ import fastmcp from fastmcp.cli import claude from fastmcp.cli import run as run_module from fastmcp.server.server import FastMCP +from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp from fastmcp.utilities.logging import get_logger logger = get_logger("cli") @@ -435,3 +438,98 @@ def install( else: logger.error(f"Failed to install {name} in Claude app") sys.exit(1) + + +@app.command() +def inspect( + server_spec: str = typer.Argument( + ..., + help="Python file to inspect, optionally with :object suffix", + ), + output: Annotated[ + Path, + typer.Option( + "--output", + "-o", + help="Output file path for the JSON report (default: server-info.json)", + ), + ] = Path("server-info.json"), +) -> None: + """Inspect a FastMCP server and generate a JSON report. + + This command analyzes a FastMCP server (v1.x or v2.x) and generates + a comprehensive JSON report containing information about the server's + name, instructions, version, tools, prompts, resources, templates, + and capabilities. + + Examples: + fastmcp inspect server.py + fastmcp inspect server.py -o report.json + fastmcp inspect server.py:mcp -o analysis.json + fastmcp inspect path/to/server.py:app -o /tmp/server-info.json + """ + + # Parse the server specification + file, server_object = run_module.parse_file_path(server_spec) + + logger.debug( + "Inspecting server", + extra={ + "file": str(file), + "server_object": server_object, + "output": str(output), + }, + ) + + try: + # Import the server + server = run_module.import_server(file, server_object) + + # Get server information + async def get_info(): + return await inspect_fastmcp(server) + + try: + # Try to use existing event loop if available + asyncio.get_running_loop() + # If there's already a loop running, we need to run in a thread + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, get_info()) + info = future.result() + except RuntimeError: + # No running loop, safe to use asyncio.run + info = asyncio.run(get_info()) + + info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2) + + # Ensure output directory exists + output.parent.mkdir(parents=True, exist_ok=True) + + # Write JSON report (always pretty-printed) + with output.open("w", encoding="utf-8") as f: + f.write(info_json.decode("utf-8")) + + logger.info(f"Server inspection complete. Report saved to {output}") + + # Print summary to console + console.print( + f"[bold green]✓[/bold green] Inspected server: [bold]{info.name}[/bold]" + ) + console.print(f" Tools: {len(info.tools)}") + console.print(f" Prompts: {len(info.prompts)}") + console.print(f" Resources: {len(info.resources)}") + console.print(f" Templates: {len(info.templates)}") + console.print(f" Report saved to: [cyan]{output}[/cyan]") + + except Exception as e: + logger.error( + f"Failed to inspect server: {e}", + extra={ + "server_spec": server_spec, + "error": str(e), + }, + ) + console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}") + sys.exit(1) diff --git a/src/fastmcp/client/auth/oauth.py b/src/fastmcp/client/auth/oauth.py index ca92a5cc3d64c5586f5a1d580d2cc7692aac3f59..b858cc17d536e4d0beff91015c2d1c7a52c79863 100644 --- a/src/fastmcp/client/auth/oauth.py +++ b/src/fastmcp/client/auth/oauth.py @@ -306,8 +306,7 @@ def OAuth( httpx.AsyncClient (or appropriate FastMCP client/transport instance) Args: - mcp_url: Full URL to the MCP endpoint (e.g., - "http://host/mcp/sse") + mcp_url: Full URL to the MCP endpoint (e.g. "http://host/mcp/sse/") scopes: OAuth scopes to request. Can be a space-separated string or a list of strings. client_name: Name for this client during registration diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 54baa80be1d9216f4c966faaf332c56c72a5d1b9..e58b381b1be9c56e0828ddb2088d4cd53c8561fb 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -7,6 +7,7 @@ from typing import Any, Generic, Literal, cast, overload import anyio import httpx import mcp.types +import pydantic_core from exceptiongroup import catch from mcp import ClientSession from mcp.types import ContentBlock @@ -508,13 +509,13 @@ class Client(Generic[ClientTransportT]): # --- Prompt --- async def get_prompt_mcp( - self, name: str, arguments: dict[str, str] | None = None + self, name: str, arguments: dict[str, Any] | None = None ) -> mcp.types.GetPromptResult: """Send a prompts/get request and return the complete MCP protocol result. Args: name (str): The name of the prompt to retrieve. - arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None. + arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None. Returns: mcp.types.GetPromptResult: The complete response object from the protocol, @@ -523,17 +524,32 @@ class Client(Generic[ClientTransportT]): Raises: RuntimeError: If called while the client is not connected. """ - result = await self.session.get_prompt(name=name, arguments=arguments) + # Serialize arguments for MCP protocol - convert non-string values to JSON + serialized_arguments: dict[str, str] | None = None + if arguments: + serialized_arguments = {} + for key, value in arguments.items(): + if isinstance(value, str): + serialized_arguments[key] = value + else: + # Use pydantic_core.to_json for consistent serialization + serialized_arguments[key] = pydantic_core.to_json(value).decode( + "utf-8" + ) + + result = await self.session.get_prompt( + name=name, arguments=serialized_arguments + ) return result async def get_prompt( - self, name: str, arguments: dict[str, str] | None = None + self, name: str, arguments: dict[str, Any] | None = None ) -> mcp.types.GetPromptResult: """Retrieve a rendered prompt message list from the server. Args: name (str): The name of the prompt to retrieve. - arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None. + arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None. Returns: mcp.types.GetPromptResult: The complete response object from the protocol, diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 778c58447580531e6453e46c75e0d5e8892e7e03..15349102952d77ee01e6ca8e04396f3c55cf1e76 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -9,6 +9,7 @@ import warnings from collections.abc import AsyncIterator, Callable from pathlib import Path from typing import Any, Literal, TypedDict, TypeVar, cast, overload +from urllib.parse import urlparse, urlunparse import anyio import httpx @@ -159,6 +160,13 @@ class SSETransport(ClientTransport): url = str(url) if not isinstance(url, str) or not url.startswith("http"): raise ValueError("Invalid HTTP/S URL provided for SSE.") + + # Ensure the URL path ends with a trailing slash to avoid automatic redirects + parsed = urlparse(url) + if not parsed.path.endswith("/"): + parsed = parsed._replace(path=parsed.path + "/") + url = urlunparse(parsed) + self.url = url self.headers = headers or {} self._set_auth(auth) @@ -227,6 +235,13 @@ class StreamableHttpTransport(ClientTransport): url = str(url) if not isinstance(url, str) or not url.startswith("http"): raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.") + + # Ensure the URL path ends with a trailing slash to avoid automatic redirects + parsed = urlparse(url) + if not parsed.path.endswith("/"): + parsed = parsed._replace(path=parsed.path + "/") + url = urlunparse(parsed) + self.url = url self.headers = headers or {} self._set_auth(auth) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 28bc977f258d726c4ee5b8bc337a37b3507a1f8f..a7103be8a9d061a028ceeb2b7d237048215d45f3 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -3,15 +3,16 @@ from __future__ import annotations as _annotations import inspect +import json from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Sequence -from typing import TYPE_CHECKING, Any +from typing import Any import pydantic_core from mcp.types import ContentBlock, PromptMessage, Role, TextContent from mcp.types import Prompt as MCPPrompt from mcp.types import PromptArgument as MCPPromptArgument -from pydantic import Field, TypeAdapter, validate_call +from pydantic import Field, TypeAdapter from fastmcp.exceptions import PromptError from fastmcp.server.dependencies import get_context @@ -24,10 +25,6 @@ from fastmcp.utilities.types import ( get_cached_typeadapter, ) -if TYPE_CHECKING: - pass - - logger = get_logger(__name__) @@ -180,17 +177,43 @@ class FunctionPrompt(Prompt): arguments: list[PromptArgument] = [] if "properties" in parameters: for param_name, param in parameters["properties"].items(): + arg_description = param.get("description") + + # For non-string parameters, append JSON schema info to help users + # understand the expected format when passing as strings (MCP requirement) + if param_name in sig.parameters: + sig_param = sig.parameters[param_name] + if ( + sig_param.annotation != inspect.Parameter.empty + and sig_param.annotation is not str + and param_name != context_kwarg + ): + # Get the JSON schema for this specific parameter type + try: + param_adapter = get_cached_typeadapter(sig_param.annotation) + param_schema = param_adapter.json_schema() + + # Create compact schema representation + schema_str = json.dumps(param_schema, separators=(",", ":")) + + # Append schema info to description + schema_note = f"Provide as a JSON string matching the following schema: {schema_str}" + if arg_description: + arg_description = f"{arg_description}\n\n{schema_note}" + else: + arg_description = schema_note + except Exception: + # If schema generation fails, skip enhancement + pass + arguments.append( PromptArgument( name=param_name, - description=param.get("description"), + description=arg_description, required=param_name in parameters.get("required", []), ) ) - # ensure the arguments are properly cast - fn = validate_call(fn) - return cls( name=func_name, description=description, @@ -200,6 +223,60 @@ class FunctionPrompt(Prompt): fn=fn, ) + def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]: + """Convert string arguments to expected types based on function signature.""" + from fastmcp.server.context import Context + + sig = inspect.signature(self.fn) + converted_kwargs = {} + + # Find context parameter name if any + context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context) + + for param_name, param_value in kwargs.items(): + if param_name in sig.parameters: + param = sig.parameters[param_name] + + # Skip Context parameters - they're handled separately + if param_name == context_param_name: + converted_kwargs[param_name] = param_value + continue + + # If parameter has no annotation or annotation is str, pass as-is + if ( + param.annotation == inspect.Parameter.empty + or param.annotation is str + ): + converted_kwargs[param_name] = param_value + # If argument is not a string, pass as-is (already properly typed) + elif not isinstance(param_value, str): + converted_kwargs[param_name] = param_value + else: + # Try to convert string argument using type adapter + try: + adapter = get_cached_typeadapter(param.annotation) + # Try JSON parsing first for complex types + try: + converted_kwargs[param_name] = adapter.validate_json( + param_value + ) + except (ValueError, TypeError, pydantic_core.ValidationError): + # Fallback to direct validation + converted_kwargs[param_name] = adapter.validate_python( + param_value + ) + except (ValueError, TypeError, pydantic_core.ValidationError) as e: + # If conversion fails, provide informative error + raise PromptError( + f"Could not convert argument '{param_name}' with value '{param_value}' " + f"to expected type {param.annotation}. Error: {e}" + ) + else: + # Parameter not in function signature, pass as-is + converted_kwargs[param_name] = param_value + + return converted_kwargs + async def render( self, arguments: dict[str, Any] | None = None, @@ -222,6 +299,9 @@ class FunctionPrompt(Prompt): if context_kwarg and context_kwarg not in kwargs: kwargs[context_kwarg] = get_context() + # Convert string arguments to expected types when needed + kwargs = self._convert_string_arguments(kwargs) + # Call function and check if result is a coroutine result = self.fn(**kwargs) if inspect.iscoroutine(result): diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index 763f90f4f1387b303d2befb87513705de62e4918..6ffffa909f35125b3a0781c09703c4c0de076262 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -17,7 +17,7 @@ from mcp.shared.auth import ( OAuthClientInformationFull, OAuthToken, ) -from pydantic import SecretStr +from pydantic import AnyHttpUrl, SecretStr, ValidationError from fastmcp.server.auth.auth import ( ClientRegistrationOptions, @@ -89,7 +89,7 @@ class RSAKeyPair: self, subject: str = "fastmcp-user", issuer: str = "https://fastmcp.example.com", - audience: str | None = None, + audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, @@ -102,7 +102,7 @@ class RSAKeyPair: private_key_pem: RSA private key in PEM format subject: Subject claim (usually user ID) issuer: Issuer claim - audience: Audience claim (optional) + audience: Audience claim - can be a string or list of strings (optional) scopes: List of scopes to include expires_in_seconds: Token expiration time in seconds additional_claims: Any additional claims to include @@ -161,7 +161,7 @@ class BearerAuthProvider(OAuthProvider): public_key: str | None = None, jwks_uri: str | None = None, issuer: str | None = None, - audience: str | None = None, + audience: str | list[str] | None = None, required_scopes: list[str] | None = None, ): """ @@ -171,7 +171,7 @@ class BearerAuthProvider(OAuthProvider): public_key: RSA public key in PEM format (for static key) jwks_uri: URI to fetch keys from (for key rotation) issuer: Expected issuer claim (optional) - audience: Expected audience claim (optional) + audience: Expected audience claim - can be a string or list of strings (optional) required_scopes: List of required scopes for access (optional) """ if not (public_key or jwks_uri): @@ -179,8 +179,16 @@ class BearerAuthProvider(OAuthProvider): if public_key and jwks_uri: raise ValueError("Provide either public_key or jwks_uri, not both") + # Only pass issuer to parent if it's a valid URL, otherwise use default + # This allows the issuer claim validation to work with string issuers per RFC 7519 + try: + issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com" + except ValidationError: + # Issuer is not a valid URL, use default for parent class + issuer_url = "https://fastmcp.example.com" + super().__init__( - issuer_url=issuer or "https://fastmcp.example.com", + issuer_url=issuer_url, client_registration_options=ClientRegistrationOptions(enabled=False), revocation_options=RevocationOptions(enabled=False), required_scopes=required_scopes, @@ -304,11 +312,25 @@ class BearerAuthProvider(OAuthProvider): # Validate audience if configured if self.audience: aud = claims.get("aud") - if isinstance(aud, list): - if self.audience not in aud: + + # Handle different combinations of audience types + if isinstance(self.audience, list): + # self.audience is a list - check if any expected audience is present + if isinstance(aud, list): + # Both are lists - check for intersection + if not any(expected in aud for expected in self.audience): + return None + else: + # aud is a string - check if it's in our expected list + if aud not in self.audience: + return None + else: + # self.audience is a string - use original logic + if isinstance(aud, list): + if self.audience not in aud: + return None + elif aud != self.audience: return None - elif aud != self.audience: - return None # Extract claims - prefer client_id over sub for OAuth application identification client_id = claims.get("client_id") or claims.get("sub") or "unknown" diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index b9121dfe87ad9f67137cdcf83ab38939aa71cf11..7041ba97538b48b957122206022c0041125b9550 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -158,6 +158,10 @@ def create_sse_app( A Starlette application with RequestContextMiddleware """ + # Ensure the message_path ends with a trailing slash to avoid automatic redirects + if not message_path.endswith("/"): + message_path = message_path + "/" + server_routes: list[BaseRoute] = [] server_middleware: list[Middleware] = [] @@ -305,6 +309,10 @@ def create_streamable_http_app( # Re-raise other RuntimeErrors if they don't match the specific message raise + # Ensure the streamable_http_path ends with a trailing slash to avoid automatic redirects + if not streamable_http_path.endswith("/"): + streamable_http_path = streamable_http_path + "/" + # Add StreamableHTTP routes with or without auth if auth: auth_middleware, auth_routes, required_scopes = ( diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 96fd657b38cc1b6cfcee705c2657427f9bda3e75..cd082166bf1d880088b9921fbc8afce04bd42038 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -192,9 +192,9 @@ class Settings(BaseSettings): # HTTP settings host: str = "127.0.0.1" port: int = 8000 - sse_path: str = "/sse" + sse_path: str = "/sse/" message_path: str = "/messages/" - streamable_http_path: str = "/mcp" + streamable_http_path: str = "/mcp/" debug: bool = False # error handling diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py new file mode 100644 index 0000000000000000000000000000000000000000..da73acf7224d9c70f88fb31ab86ef0fd2145aaaf --- /dev/null +++ b/src/fastmcp/utilities/inspect.py @@ -0,0 +1,326 @@ +"""Utilities for inspecting FastMCP instances.""" + +from __future__ import annotations + +import importlib.metadata +from dataclasses import dataclass +from typing import Any + +from mcp.server.fastmcp import FastMCP as FastMCP1x + +import fastmcp +from fastmcp.server.server import FastMCP + + +@dataclass +class ToolInfo: + """Information about a tool.""" + + key: str + name: str + description: str | None + input_schema: dict[str, Any] + annotations: dict[str, Any] | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class PromptInfo: + """Information about a prompt.""" + + key: str + name: str + description: str | None + arguments: list[dict[str, Any]] | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class ResourceInfo: + """Information about a resource.""" + + key: str + uri: str + name: str | None + description: str | None + mime_type: str | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class TemplateInfo: + """Information about a resource template.""" + + key: str + uri_template: str + name: str | None + description: str | None + mime_type: str | None = None + tags: list[str] | None = None + enabled: bool | None = None + + +@dataclass +class FastMCPInfo: + """Information extracted from a FastMCP instance.""" + + name: str + instructions: str | None + fastmcp_version: str + mcp_version: str + server_version: str + tools: list[ToolInfo] + prompts: list[PromptInfo] + resources: list[ResourceInfo] + templates: list[TemplateInfo] + capabilities: dict[str, Any] + + +async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: + """Extract information from a FastMCP v2.x instance. + + Args: + mcp: The FastMCP v2.x instance to inspect + + Returns: + FastMCPInfo dataclass containing the extracted information + """ + # Get all the components using FastMCP2's direct methods + tools_dict = await mcp.get_tools() + prompts_dict = await mcp.get_prompts() + resources_dict = await mcp.get_resources() + templates_dict = await mcp.get_resource_templates() + + # Extract detailed tool information + tool_infos = [] + for key, tool in tools_dict.items(): + # Convert to MCP tool to get input schema + mcp_tool = tool.to_mcp_tool(name=key) + tool_infos.append( + ToolInfo( + key=key, + name=tool.name or key, + description=tool.description, + input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {}, + annotations=tool.annotations.model_dump() if tool.annotations else None, + tags=list(tool.tags) if tool.tags else None, + enabled=tool.enabled, + ) + ) + + # Extract detailed prompt information + prompt_infos = [] + for key, prompt in prompts_dict.items(): + prompt_infos.append( + PromptInfo( + key=key, + name=prompt.name or key, + description=prompt.description, + arguments=[arg.model_dump() for arg in prompt.arguments] + if prompt.arguments + else None, + tags=list(prompt.tags) if prompt.tags else None, + enabled=prompt.enabled, + ) + ) + + # Extract detailed resource information + resource_infos = [] + for key, resource in resources_dict.items(): + resource_infos.append( + ResourceInfo( + key=key, + uri=key, # For v2, key is the URI + name=resource.name, + description=resource.description, + mime_type=resource.mime_type, + tags=list(resource.tags) if resource.tags else None, + enabled=resource.enabled, + ) + ) + + # Extract detailed template information + template_infos = [] + for key, template in templates_dict.items(): + template_infos.append( + TemplateInfo( + key=key, + uri_template=key, # For v2, key is the URI template + name=template.name, + description=template.description, + mime_type=template.mime_type, + tags=list(template.tags) if template.tags else None, + enabled=template.enabled, + ) + ) + + # Basic MCP capabilities that FastMCP supports + capabilities = { + "tools": {"listChanged": True}, + "resources": {"subscribe": False, "listChanged": False}, + "prompts": {"listChanged": False}, + "logging": {}, + } + + return FastMCPInfo( + name=mcp.name, + instructions=mcp.instructions, + fastmcp_version=fastmcp.__version__, + mcp_version=importlib.metadata.version("mcp"), + server_version=fastmcp.__version__, # v2.x uses FastMCP version + tools=tool_infos, + prompts=prompt_infos, + resources=resource_infos, + templates=template_infos, + capabilities=capabilities, + ) + + +async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo: + """Extract information from a FastMCP v1.x instance using a Client. + + Args: + mcp: The FastMCP v1.x instance to inspect + + Returns: + FastMCPInfo dataclass containing the extracted information + """ + from fastmcp import Client + + # Use a client to interact with the FastMCP1x server + async with Client(mcp) as client: + # Get components via client calls (these return MCP objects) + mcp_tools = await client.list_tools() + mcp_prompts = await client.list_prompts() + mcp_resources = await client.list_resources() + + # Try to get resource templates (FastMCP 1.x does have templates) + try: + mcp_templates = await client.list_resource_templates() + except Exception: + mcp_templates = [] + + # Extract detailed tool information from MCP Tool objects + tool_infos = [] + for mcp_tool in mcp_tools: + # Extract annotations if they exist + annotations = None + if hasattr(mcp_tool, "annotations") and mcp_tool.annotations: + if hasattr(mcp_tool.annotations, "model_dump"): + annotations = mcp_tool.annotations.model_dump() + elif isinstance(mcp_tool.annotations, dict): + annotations = mcp_tool.annotations + else: + annotations = None + + tool_infos.append( + ToolInfo( + key=mcp_tool.name, # For 1.x, key and name are the same + name=mcp_tool.name, + description=mcp_tool.description, + input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {}, + annotations=annotations, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Extract detailed prompt information from MCP Prompt objects + prompt_infos = [] + for mcp_prompt in mcp_prompts: + # Convert arguments if they exist + arguments = None + if hasattr(mcp_prompt, "arguments") and mcp_prompt.arguments: + arguments = [arg.model_dump() for arg in mcp_prompt.arguments] + + prompt_infos.append( + PromptInfo( + key=mcp_prompt.name, # For 1.x, key and name are the same + name=mcp_prompt.name, + description=mcp_prompt.description, + arguments=arguments, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Extract detailed resource information from MCP Resource objects + resource_infos = [] + for mcp_resource in mcp_resources: + resource_infos.append( + ResourceInfo( + key=str(mcp_resource.uri), # For 1.x, key and uri are the same + uri=str(mcp_resource.uri), + name=mcp_resource.name, + description=mcp_resource.description, + mime_type=mcp_resource.mimeType, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Extract detailed template information from MCP ResourceTemplate objects + template_infos = [] + for mcp_template in mcp_templates: + template_infos.append( + TemplateInfo( + key=str( + mcp_template.uriTemplate + ), # For 1.x, key and uriTemplate are the same + uri_template=str(mcp_template.uriTemplate), + name=mcp_template.name, + description=mcp_template.description, + mime_type=mcp_template.mimeType, + tags=None, # 1.x doesn't have tags + enabled=None, # 1.x doesn't have enabled field + ) + ) + + # Basic MCP capabilities + capabilities = { + "tools": {"listChanged": True}, + "resources": {"subscribe": False, "listChanged": False}, + "prompts": {"listChanged": False}, + "logging": {}, + } + + return FastMCPInfo( + name=mcp.name, + instructions=getattr(mcp, "instructions", None), + fastmcp_version=fastmcp.__version__, # Report current fastmcp version + mcp_version=importlib.metadata.version("mcp"), + server_version="1.0", # FastMCP 1.x version + tools=tool_infos, + prompts=prompt_infos, + resources=resource_infos, + templates=template_infos, # FastMCP1x does have templates + capabilities=capabilities, + ) + + +def _is_fastmcp_v1(mcp: Any) -> bool: + """Check if the given instance is a FastMCP v1.x instance.""" + + # Check if it's an instance of FastMCP1x and not FastMCP2 + return isinstance(mcp, FastMCP1x) and not isinstance(mcp, FastMCP) + + +async def inspect_fastmcp(mcp: FastMCP[Any] | Any) -> FastMCPInfo: + """Extract information from a FastMCP instance into a dataclass. + + This function automatically detects whether the instance is FastMCP v1.x or v2.x + and uses the appropriate extraction method. + + Args: + mcp: The FastMCP instance to inspect (v1.x or v2.x) + + Returns: + FastMCPInfo dataclass containing the extracted information + """ + if _is_fastmcp_v1(mcp): + return await inspect_fastmcp_v1(mcp) + else: + return await inspect_fastmcp_v2(mcp) diff --git a/src/fastmcp/utilities/json_schema.py b/src/fastmcp/utilities/json_schema.py index 87ea4a789ceb39e201ed04f58c054875c127488f..ca5fe37c9e9151816718dac30f5e1b65a1fdfa99 100644 --- a/src/fastmcp/utilities/json_schema.py +++ b/src/fastmcp/utilities/json_schema.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +from collections import defaultdict def _prune_param(schema: dict, param: str) -> dict: @@ -24,25 +25,77 @@ def _prune_param(schema: dict, param: str) -> dict: return schema +def _prune_unused_defs(schema: dict) -> dict: + """Walk the schema and prune unused defs.""" + + root_defs: set[str] = set() + referenced_by: defaultdict[str, list] = defaultdict(list) + + defs = schema.get("$defs") + if defs is None: + return schema + + def walk( + node: object, current_def: str | None = None, skip_defs: bool = False + ) -> None: + if isinstance(node, dict): + # Process $ref for definition tracking + ref = node.get("$ref") + if isinstance(ref, str) and ref.startswith("#/$defs/"): + def_name = ref.split("/")[-1] + if current_def: + referenced_by[def_name].append(current_def) + else: + root_defs.add(def_name) + + # Walk children + for k, v in node.items(): + if skip_defs and k == "$defs": + continue + + walk(v, current_def=current_def) + + elif isinstance(node, list): + for v in node: + walk(v) + + # Traverse the schema once, skipping the $defs + walk(schema, skip_defs=True) + + # Now figure out what defs reference other defs + for def_name, value in defs.items(): + walk(value, current_def=def_name) + + # Figure out what defs were referenced directly or recursively + def def_is_referenced(def_name): + if def_name in root_defs: + return True + references = referenced_by.get(def_name) + if references: + for reference in references: + if def_is_referenced(reference): + return True + return False + + # Remove orphaned definitions if requested + for def_name in list(defs): + if not def_is_referenced(def_name): + defs.pop(def_name) + if not defs: + schema.pop("$defs", None) + + return schema + + def _walk_and_prune( schema: dict, - prune_defs: bool = False, prune_titles: bool = False, prune_additional_properties: bool = False, ) -> dict: - """Walk the schema and optionally prune titles, unused definitions, and additionalProperties: false.""" - - # Will only be used if prune_defs is True - used_defs: set[str] = set() + """Walk the schema and optionally prune titles and additionalProperties: false.""" def walk(node: object) -> None: if isinstance(node, dict): - # Process $ref for definition tracking - if prune_defs: - ref = node.get("$ref") - if isinstance(ref, str) and ref.startswith("#/$defs/"): - used_defs.add(ref.split("/")[-1]) - # Remove title if requested if prune_titles and "title" in node: node.pop("title") @@ -62,18 +115,8 @@ def _walk_and_prune( for v in node: walk(v) - # Traverse the schema once walk(schema) - # Remove orphaned definitions if requested - if prune_defs: - defs = schema.get("$defs", {}) - for def_name in list(defs): - if def_name not in used_defs: - defs.pop(def_name) - if not defs: - schema.pop("$defs", None) - return schema @@ -109,12 +152,13 @@ def compress_schema( schema = _prune_param(schema, param=param) # Do a single walk to handle pruning operations - if prune_defs or prune_titles or prune_additional_properties: + if prune_titles or prune_additional_properties: schema = _walk_and_prune( schema, - prune_defs=prune_defs, prune_titles=prune_titles, prune_additional_properties=prune_additional_properties, ) + if prune_defs: + schema = _prune_unused_defs(schema) return schema diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py index 176cb097d50ab1bfdb0c4c673fcc580735817d63..40300d7ebc1a023a9bf4ff481130021a9259af03 100644 --- a/src/fastmcp/utilities/mcp_config.py +++ b/src/fastmcp/utilities/mcp_config.py @@ -1,9 +1,11 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING, Annotated, Any, Literal from urllib.parse import urlparse -from pydantic import AnyUrl, Field +import httpx +from pydantic import AnyUrl, ConfigDict, Field from fastmcp.utilities.types import FastMCPBaseModel @@ -28,7 +30,8 @@ def infer_transport_type_from_url( parsed_url = urlparse(url) path = parsed_url.path - if "/sse/" in path or path.rstrip("/").endswith("/sse"): + # Match /sse followed by /, ?, &, or end of string + if re.search(r"/sse(/|\?|&|$)", path): return "sse" else: return "streamable-http" @@ -57,12 +60,14 @@ class RemoteMCPServer(FastMCPBaseModel): headers: dict[str, str] = Field(default_factory=dict) transport: Literal["streamable-http", "sse"] | None = None auth: Annotated[ - str | Literal["oauth"] | None, + str | Literal["oauth"] | httpx.Auth | None, Field( - description='Either a string representing a Bearer token or the literal "oauth" to use OAuth authentication.' + description='Either a string representing a Bearer token, the literal "oauth" to use OAuth authentication, or an httpx.Auth instance for custom authentication.', ), ] = None + model_config = ConfigDict(arbitrary_types_allowed=True) + def to_transport(self) -> StreamableHttpTransport | SSETransport: from fastmcp.client.transports import SSETransport, StreamableHttpTransport diff --git a/src/fastmcp/utilities/openapi.py b/src/fastmcp/utilities/openapi.py index 44c0216f64f0d441f2687a23aef692402e8dd30c..0cae85cb24ee577f12da0130a40b8cf7fd5064e7 100644 --- a/src/fastmcp/utilities/openapi.py +++ b/src/fastmcp/utilities/openapi.py @@ -262,16 +262,18 @@ class OpenAPIParser( if isinstance(resolved_schema, (self.schema_cls)): # Convert schema to dictionary - return resolved_schema.model_dump( + result = resolved_schema.model_dump( mode="json", by_alias=True, exclude_none=True ) elif isinstance(resolved_schema, dict): - return resolved_schema + result = resolved_schema else: logger.warning( f"Expected Schema after resolving, got {type(resolved_schema)}. Returning empty dict." ) - return {} + result = {} + + return _replace_ref_with_defs(result) except Exception as e: logger.error(f"Failed to extract schema as dict: {e}", exc_info=False) return {} diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index 6f59c96fe065f5c3416aeb2e7ed47e7a01246ad9..ac7e529b12ce51f47442a64210491340f18a1c1d 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -67,7 +67,7 @@ def mcp_server_url(rsa_key_pair: RSAKeyPair) -> Generator[str]: public_key=rsa_key_pair.public_key, run_kwargs=dict(transport="streamable-http"), ) as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" class TestRSAKeyPair: @@ -446,6 +446,45 @@ class TestBearerToken: access_token = await provider.load_access_token(token) assert access_token is not None + async def test_provider_with_multiple_expected_audiences( + self, rsa_key_pair: RSAKeyPair + ): + """Test provider configured with multiple expected audiences.""" + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="https://test.example.com", + audience=["https://api.example.com", "https://other-api.example.com"], + ) + + # Token with single audience that matches one of the expected + token1 = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + audience="https://api.example.com", + ) + access_token1 = await provider.load_access_token(token1) + assert access_token1 is not None + + # Token with multiple audiences, one of which matches + token2 = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + additional_claims={ + "aud": ["https://api.example.com", "https://third-party.example.com"] + }, + ) + access_token2 = await provider.load_access_token(token2) + assert access_token2 is not None + + # Token with audience that doesn't match any expected + token3 = rsa_key_pair.create_token( + subject="test-user", + issuer="https://test.example.com", + audience="https://wrong-api.example.com", + ) + access_token3 = await provider.load_access_token(token3) + assert access_token3 is None + async def test_scope_extraction_string( self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider ): @@ -539,6 +578,59 @@ class TestBearerToken: assert access_token is not None assert access_token.client_id == "app456" # Should prefer client_id over sub + async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair): + """Test that string (non-URL) issuers are supported per RFC 7519.""" + # Create provider with string issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="my-service", # String issuer, not a URL + ) + + # Create token with matching string issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="my-service", # Same string issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is not None + assert access_token.client_id == "test-user" + + async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair): + """Test that mismatched string issuers are rejected.""" + # Create provider with one string issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="my-service", + ) + + # Create token with different string issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="other-service", # Different string issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is None + + async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair): + """Test that URL issuers still work after the fix.""" + # Create provider with URL issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="https://my-auth-server.com", # URL issuer + ) + + # Create token with matching URL issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="https://my-auth-server.com", # Same URL issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is not None + assert access_token.client_id == "test-user" + class TestFastMCPBearerAuth: def test_bearer_auth(self): @@ -606,7 +698,7 @@ class TestFastMCPBearerAuth: auth_kwargs=dict(required_scopes=["read", "write"]), run_kwargs=dict(transport="streamable-http"), ) as url: - mcp_server_url = f"{url}/mcp" + mcp_server_url = f"{url}/mcp/" with pytest.raises(httpx.HTTPStatusError) as exc_info: async with Client(mcp_server_url, auth=BearerAuth(token)) as client: tools = await client.list_tools() # noqa: F841 @@ -629,7 +721,7 @@ class TestFastMCPBearerAuth: auth_kwargs=dict(required_scopes=["read", "write"]), run_kwargs=dict(transport="streamable-http"), ) as url: - mcp_server_url = f"{url}/mcp" + mcp_server_url = f"{url}/mcp/" async with Client(mcp_server_url, auth=BearerAuth(token)) as client: tools = await client.list_tools() assert tools diff --git a/tests/auth/test_oauth_client.py b/tests/auth/test_oauth_client.py index 292f6c4af48ffed2b1638dc8576c20e96dda6621..f36cf4c916d22fa1e460469eaf8ab8cb3f63c146 100644 --- a/tests/auth/test_oauth_client.py +++ b/tests/auth/test_oauth_client.py @@ -44,7 +44,7 @@ def run_server(host: str, port: int, **kwargs) -> None: @pytest.fixture(scope="module") def streamable_http_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="streamable-http") as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" @pytest.fixture() diff --git a/tests/client/test_client.py b/tests/client/test_client.py index d975ed77b37cd12026c3ec4934a902e7aee6ee01..55210c432d330b41eac6dffbe2a0e364a4097bb6 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -220,6 +220,101 @@ async def test_get_prompt_mcp(fastmcp_server): assert result.description == "Example greeting prompt." +async def test_client_serializes_all_non_string_arguments(): + """Test that client always serializes non-string arguments to JSON, regardless of server types.""" + server = FastMCP("TestServer") + + @server.prompt + def echo_args(arg1: str, arg2: str, arg3: str) -> str: + """Server accepts all string args but client sends mixed types.""" + return f"arg1: {arg1}, arg2: {arg2}, arg3: {arg3}" + + client = Client(transport=FastMCPTransport(server)) + + async with client: + result = await client.get_prompt( + "echo_args", + { + "arg1": "hello", # string - should pass through + "arg2": [1, 2, 3], # list - should be JSON serialized + "arg3": {"key": "value"}, # dict - should be JSON serialized + }, + ) + + content = result.messages[0].content.text # type: ignore[attr-defined] + assert "arg1: hello" in content + assert "arg2: [1,2,3]" in content # JSON serialized list + assert 'arg3: {"key":"value"}' in content # JSON serialized dict + + +async def test_client_server_type_conversion_integration(): + """Test that client serialization works with server-side type conversion.""" + server = FastMCP("TestServer") + + @server.prompt + def typed_prompt(numbers: list[int], config: dict[str, str]) -> str: + """Server expects typed args - will convert from JSON strings.""" + return f"Got {len(numbers)} numbers and {len(config)} config items" + + client = Client(transport=FastMCPTransport(server)) + + async with client: + result = await client.get_prompt( + "typed_prompt", + {"numbers": [1, 2, 3, 4], "config": {"theme": "dark", "lang": "en"}}, + ) + + content = result.messages[0].content.text # type: ignore[attr-defined] + assert "Got 4 numbers and 2 config items" in content + + +async def test_client_serialization_error(): + """Test client error when object cannot be serialized.""" + import pydantic_core + + server = FastMCP("TestServer") + + @server.prompt + def any_prompt(data: str) -> str: + return f"Got: {data}" + + # Create an unserializable object + class UnserializableClass: + def __init__(self): + self.func = lambda x: x # functions can't be JSON serialized + + client = Client(transport=FastMCPTransport(server)) + + async with client: + with pytest.raises( + pydantic_core.PydanticSerializationError, match="Unable to serialize" + ): + await client.get_prompt("any_prompt", {"data": UnserializableClass()}) + + +async def test_server_deserialization_error(): + """Test server error when JSON string cannot be converted to expected type.""" + from mcp import McpError + + server = FastMCP("TestServer") + + @server.prompt + def strict_typed_prompt(numbers: list[int]) -> str: + """Expects list of integers but will receive invalid JSON.""" + return f"Got {len(numbers)} numbers" + + client = Client(transport=FastMCPTransport(server)) + + async with client: + with pytest.raises(McpError, match="Error rendering prompt"): + await client.get_prompt( + "strict_typed_prompt", + { + "numbers": "not valid json" # This will fail server-side conversion + }, + ) + + async def test_read_resource_invalid_uri(fastmcp_server): """Test reading a resource with an invalid URI.""" client = Client(transport=FastMCPTransport(fastmcp_server)) @@ -735,7 +830,8 @@ class TestInferTransport: "http://example.com/api/sse/stream", "https://localhost:8080/mcp/sse/endpoint", "http://example.com/api/sse", - "https://localhost:8080/mcp/sse", + "http://example.com/api/sse/", + "https://localhost:8080/mcp/sse/", "http://example.com/api/sse?param=value", "https://localhost:8080/mcp/sse/?param=value", "https://localhost:8000/mcp/sse?x=1&y=2", @@ -744,6 +840,7 @@ class TestInferTransport: "path_with_sse_directory", "path_with_sse_subdirectory", "path_ending_with_sse", + "path_ending_with_sse_slash", "path_ending_with_sse_https", "path_with_sse_and_query_params", "path_with_sse_slash_and_query_params", @@ -758,7 +855,7 @@ class TestInferTransport: "url", [ "http://example.com/api", - "https://localhost:8080/mcp", + "https://localhost:8080/mcp/", "http://example.com/asset/image.jpg", "https://localhost:8080/sservice/endpoint", "https://example.com/assets/file", @@ -779,7 +876,7 @@ class TestInferTransport: config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", "headers": {"Authorization": "Bearer 123"}, }, } @@ -787,7 +884,7 @@ class TestInferTransport: transport = infer_transport(config) assert isinstance(transport, MCPConfigTransport) assert isinstance(transport.transport, SSETransport) - assert transport.transport.url == "http://localhost:8000/sse" + assert transport.transport.url == "http://localhost:8000/sse/" assert transport.transport.headers == {"Authorization": "Bearer 123"} def test_infer_local_transport_from_config(self): @@ -825,7 +922,7 @@ class TestInferTransport: "args": ["hello"], }, "remote": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", "headers": {"Authorization": "Bearer 123"}, }, } diff --git a/tests/client/test_openapi.py b/tests/client/test_openapi.py index d97f89eb90e4487d025deca34b8ab6362590ce03..2ee4727a90fc09fcd77ef43d1ea1abd8f8bc5941 100644 --- a/tests/client/test_openapi.py +++ b/tests/client/test_openapi.py @@ -57,12 +57,12 @@ class TestClientHeaders: @pytest.fixture(scope="class") def shttp_server(self) -> Generator[str, None, None]: with run_server_in_process(run_server, transport="streamable-http") as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" @pytest.fixture(scope="class") def sse_server(self) -> Generator[str, None, None]: with run_server_in_process(run_server, transport="sse") as url: - yield f"{url}/sse" + yield f"{url}/sse/" @pytest.fixture(scope="class") def proxy_server(self, shttp_server: str) -> Generator[str, None, None]: @@ -71,7 +71,7 @@ class TestClientHeaders: shttp_url=shttp_server, transport="streamable-http", ) as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" async def test_client_headers_sse_resource(self, sse_server: str): async with Client( diff --git a/tests/client/test_sse.py b/tests/client/test_sse.py index df24d1d8e2fcc817c08e12fe5b1880e365bed3d3..f428c5c30406b17db00a9abf0b3c1a39e1eb3610 100644 --- a/tests/client/test_sse.py +++ b/tests/client/test_sse.py @@ -70,7 +70,7 @@ def run_server(host: str, port: int, **kwargs) -> None: @pytest.fixture(autouse=True, scope="module") def sse_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="sse") as url: - yield f"{url}/sse" + yield f"{url}/sse/" async def test_ping(sse_server: str): @@ -92,7 +92,7 @@ async def test_http_headers(sse_server: str): def run_nested_server(host: str, port: int) -> None: - app = fastmcp_server().sse_app(path="/mcp/sse", message_path="/mcp/messages") + app = fastmcp_server().sse_app(path="/mcp/sse/", message_path="/mcp/messages") mount = Starlette(routes=[Mount("/nest-inner", app=app)]) mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)]) server = uvicorn.Server( @@ -114,7 +114,7 @@ async def test_nested_sse_server_resolves_correctly(): with run_server_in_process(run_nested_server) as url: async with Client( - transport=SSETransport(f"{url}/nest-outer/nest-inner/mcp/sse") + transport=SSETransport(f"{url}/nest-outer/nest-inner/mcp/sse/") ) as client: result = await client.ping() assert result is True diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index 7e95e27b224c1eb91ce13e6830d8740821fbf329..5b182c9333951c46693d8493487d0fa60d0076f9 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -79,7 +79,7 @@ def run_server(host: str, port: int, stateless_http: bool = False, **kwargs) -> def run_nested_server(host: str, port: int) -> None: - mcp_app = fastmcp_server().http_app(path="/final/mcp") + mcp_app = fastmcp_server().http_app(path="/final/mcp/") mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)]) mount2 = Starlette( @@ -105,9 +105,9 @@ async def streamable_http_server( with run_server_in_process( run_server, stateless_http=stateless_http, transport="streamable-http" ) as url: - async with Client(transport=StreamableHttpTransport(f"{url}/mcp")) as client: + async with Client(transport=StreamableHttpTransport(f"{url}/mcp/")) as client: assert await client.ping() - yield f"{url}/mcp" + yield f"{url}/mcp/" async def test_ping(streamable_http_server: str): @@ -156,7 +156,7 @@ async def test_nested_streamable_http_server_resolves_correctly(): with run_server_in_process(run_nested_server) as url: async with Client( - transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp") + transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp/") ) as client: result = await client.ping() assert result is True diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index 6c8fc9862ad27e9e7aeb36108e3c10ba3dd9ba33..e2bc66708f035c4ddcd65845767c29fe2af66e69 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -123,7 +123,7 @@ class TestDeprecatedServerInitKwargs: debug=False, host="127.0.0.1", port=9999, - sse_path="/sse", + sse_path="/sse/", message_path="/msg", streamable_http_path="/http", json_response=False, @@ -162,7 +162,7 @@ class TestDeprecatedServerInitKwargs: assert server._deprecated_settings.debug is False assert server._deprecated_settings.host == "127.0.0.1" assert server._deprecated_settings.port == 9999 - assert server._deprecated_settings.sse_path == "/sse" + assert server._deprecated_settings.sse_path == "/sse/" assert server._deprecated_settings.message_path == "/msg" assert server._deprecated_settings.streamable_http_path == "/http" assert server._deprecated_settings.json_response is False diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index a0cda7b2dd1635de1572916c9b69c42e00d32c82..be5d0a1f3fb175948e7f0dada845783bcdcb17fd 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -240,3 +240,245 @@ class TestRenderPrompt: ), ) ] + + +class TestPromptTypeConversion: + async def test_list_of_integers_as_string_args(self): + """Test that prompts can handle complex types passed as strings from MCP spec.""" + + def sum_numbers(numbers: list[int]) -> str: + """Calculate the sum of a list of numbers.""" + total = sum(numbers) + return f"The sum is: {total}" + + prompt = Prompt.from_function(sum_numbers) + + # MCP spec only allows string arguments, so this should work + # after we implement type conversion + result_from_string = await prompt.render( + arguments={"numbers": "[1, 2, 3, 4, 5]"} + ) + assert result_from_string == [ + PromptMessage( + role="user", content=TextContent(type="text", text="The sum is: 15") + ) + ] + + # Both should work now with string conversion + result_from_list_string = await prompt.render( + arguments={"numbers": "[1, 2, 3, 4, 5]"} + ) + assert result_from_list_string == result_from_string + + async def test_various_type_conversions(self): + """Test type conversion for various data types.""" + + def process_data( + name: str, + age: int, + scores: list[float], + metadata: dict[str, str], + active: bool, + ) -> str: + return f"{name} ({age}): {len(scores)} scores, active={active}, metadata keys={list(metadata.keys())}" + + prompt = Prompt.from_function(process_data) + + # All arguments as strings (as MCP would send them) + result = await prompt.render( + arguments={ + "name": "Alice", + "age": "25", + "scores": "[1.5, 2.0, 3.5]", + "metadata": '{"project": "test", "version": "1.0"}', + "active": "true", + } + ) + + expected_text = ( + "Alice (25): 3 scores, active=True, metadata keys=['project', 'version']" + ) + assert result == [ + PromptMessage( + role="user", content=TextContent(type="text", text=expected_text) + ) + ] + + async def test_type_conversion_error_handling(self): + """Test that informative errors are raised for invalid type conversions.""" + from fastmcp.exceptions import PromptError + + def typed_prompt(numbers: list[int]) -> str: + return f"Got {len(numbers)} numbers" + + prompt = Prompt.from_function(typed_prompt) + + # Test with invalid JSON - should raise PromptError due to exception handling in render() + with pytest.raises(PromptError) as exc_info: + await prompt.render(arguments={"numbers": "not valid json"}) + + assert f"Error rendering prompt {prompt.name}" in str(exc_info.value) + + async def test_json_parsing_fallback(self): + """Test that JSON parsing falls back to direct validation when needed.""" + + def data_prompt(value: int) -> str: + return f"Value: {value}" + + prompt = Prompt.from_function(data_prompt) + + # This should work with JSON parsing (integer as string) + result1 = await prompt.render(arguments={"value": "42"}) + assert result1 == [ + PromptMessage( + role="user", content=TextContent(type="text", text="Value: 42") + ) + ] + + # This should work with direct validation (already an integer string) + result2 = await prompt.render(arguments={"value": "123"}) + assert result2 == [ + PromptMessage( + role="user", content=TextContent(type="text", text="Value: 123") + ) + ] + + async def test_mixed_string_and_typed_args(self): + """Test mixing string args (no conversion) with typed args (conversion needed).""" + + def mixed_prompt(message: str, count: int) -> str: + return f"{message} (repeated {count} times)" + + prompt = Prompt.from_function(mixed_prompt) + + result = await prompt.render( + arguments={ + "message": "Hello world", # str - no conversion needed + "count": "3", # int - conversion needed + } + ) + + assert result == [ + PromptMessage( + role="user", + content=TextContent(type="text", text="Hello world (repeated 3 times)"), + ) + ] + + +class TestPromptArgumentDescriptions: + def test_enhanced_descriptions_for_non_string_types(self): + """Test that non-string argument types get enhanced descriptions with JSON schema.""" + + def analyze_data( + name: str, + numbers: list[int], + metadata: dict[str, str], + threshold: float, + active: bool, + ) -> str: + """Analyze numerical data.""" + return f"Analyzed {name}" + + prompt = Prompt.from_function(analyze_data) + + assert prompt.arguments is not None + # Check that string parameter has no schema enhancement + name_arg = next((arg for arg in prompt.arguments if arg.name == "name"), None) + assert name_arg is not None + assert name_arg.description is None # No enhancement for string types + + # Check that non-string parameters have schema enhancements + numbers_arg = next( + (arg for arg in prompt.arguments if arg.name == "numbers"), None + ) + assert numbers_arg is not None + assert numbers_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in numbers_arg.description + ) + assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + + metadata_arg = next( + (arg for arg in prompt.arguments if arg.name == "metadata"), None + ) + assert metadata_arg is not None + assert metadata_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) + + threshold_arg = next( + (arg for arg in prompt.arguments if arg.name == "threshold"), None + ) + assert threshold_arg is not None + assert threshold_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in threshold_arg.description + ) + assert '{"type":"number"}' in threshold_arg.description + + active_arg = next( + (arg for arg in prompt.arguments if arg.name == "active"), None + ) + assert active_arg is not None + assert active_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in active_arg.description + ) + assert '{"type":"boolean"}' in active_arg.description + + def test_enhanced_descriptions_with_existing_descriptions(self): + """Test that existing parameter descriptions are preserved with schema appended.""" + from typing import Annotated + + from pydantic import Field + + def documented_prompt( + numbers: Annotated[ + list[int], Field(description="A list of integers to process") + ], + ) -> str: + """Process numbers.""" + return "processed" + + prompt = Prompt.from_function(documented_prompt) + + assert prompt.arguments is not None + numbers_arg = next( + (arg for arg in prompt.arguments if arg.name == "numbers"), None + ) + assert numbers_arg is not None + # Should have both the original description and the schema + assert numbers_arg.description is not None + assert "A list of integers to process" in numbers_arg.description + assert "\n\n" in numbers_arg.description # Should have newline separator + assert ( + "Provide as a JSON string matching the following schema:" + in numbers_arg.description + ) + + def test_string_parameters_no_enhancement(self): + """Test that string parameters don't get schema enhancement.""" + + def string_only_prompt(message: str, name: str) -> str: + return f"{message}, {name}" + + prompt = Prompt.from_function(string_only_prompt) + + assert prompt.arguments is not None + for arg in prompt.arguments: + # String parameters should not have schema enhancement + if arg.description is not None: + assert ( + "Provide as a JSON string matching the following schema:" + not in arg.description + ) diff --git a/tests/server/http/test_custom_routes.py b/tests/server/http/test_custom_routes.py index 5c988d1d4c55b93cd18263e2b8aaa8f6243b7f1a..c43444756e8d682e596286ba84f63b508f7a8de9 100644 --- a/tests/server/http/test_custom_routes.py +++ b/tests/server/http/test_custom_routes.py @@ -55,7 +55,7 @@ class TestCustomRoutes: """Test that custom routes are included when using create_sse_app directly.""" # Create the app by calling the constructor function directly app = create_sse_app( - server=server_with_custom_route, message_path="/message", sse_path="/sse" + server=server_with_custom_route, message_path="/message", sse_path="/sse/" ) # Verify that the custom route is included diff --git a/tests/server/http/test_http_dependencies.py b/tests/server/http/test_http_dependencies.py index 81a52a90c5c9fc1c2af52af297e4abc48ce8b06a..514f0a9d35d9f33815900834e35890a6f30c8825 100644 --- a/tests/server/http/test_http_dependencies.py +++ b/tests/server/http/test_http_dependencies.py @@ -45,13 +45,13 @@ def run_server(host: str, port: int, **kwargs) -> None: @pytest.fixture(autouse=True, scope="module") def shttp_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="streamable-http") as url: - yield f"{url}/mcp" + yield f"{url}/mcp/" @pytest.fixture(autouse=True, scope="module") def sse_server() -> Generator[str, None, None]: with run_server_in_process(run_server, transport="sse") as url: - yield f"{url}/sse" + yield f"{url}/sse/" async def test_http_headers_resource_shttp(shttp_server: str): diff --git a/tests/server/http/test_http_middleware.py b/tests/server/http/test_http_middleware.py index 7a1ab22f4fa9b5a8e3c5410101e908dfb8a43404..0c36d0522b4cbbfc2068fb0bc0b20eab4a1be7e4 100644 --- a/tests/server/http/test_http_middleware.py +++ b/tests/server/http/test_http_middleware.py @@ -126,7 +126,7 @@ async def test_create_sse_app_with_custom_middleware(): app = create_sse_app( server=server, message_path="/message", - sse_path="/sse", + sse_path="/sse/", middleware=custom_middleware, routes=additional_routes, ) diff --git a/tests/server/test_app_state.py b/tests/server/test_app_state.py index 60908940097f8ba8b77da59ca2e585a5bac90a60..eeccd6feeccd12f9b0b01f1c7cd0ec4b4b59fd6e 100644 --- a/tests/server/test_app_state.py +++ b/tests/server/test_app_state.py @@ -16,11 +16,11 @@ def test_http_app_sse_sets_mcp_server_state(): def test_create_streamable_http_app_sets_state(): server = FastMCP(name="StateTest") - app = create_streamable_http_app(server, "/mcp") + app = create_streamable_http_app(server, "/mcp/") assert app.state.fastmcp_server is server def test_create_sse_app_sets_state(): server = FastMCP(name="StateTest") - app = create_sse_app(server, message_path="/message", sse_path="/sse") + app = create_sse_app(server, message_path="/message", sse_path="/sse/") assert app.state.fastmcp_server is server diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 46658395b2ffd8f18fbe44487ec57083c042265f..30304531a78fab0e6f95a3751fbbc4f0746ef2c1 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -273,7 +273,9 @@ class TestMultipleServerMount: main_app.mount(working_app, "working") # Use an unreachable port - unreachable_client = Client(transport=SSETransport("http://127.0.0.1:9999/sse")) + unreachable_client = Client( + transport=SSETransport("http://127.0.0.1:9999/sse/") + ) # Create a proxy server that will fail to connect unreachable_proxy = FastMCP.as_proxy(unreachable_client) diff --git a/tests/server/test_proxy.py b/tests/server/test_proxy.py index 68524ad2ff801ae2b77e5370150b21db54aa9a5c..612f8bfc8c84323c8918714956e99834506f140c 100644 --- a/tests/server/test_proxy.py +++ b/tests/server/test_proxy.py @@ -102,10 +102,10 @@ async def test_as_proxy_with_transport(fastmcp_server): def test_as_proxy_with_url(): """FastMCP.as_proxy should accept a URL without connecting.""" - proxy = FastMCP.as_proxy("http://example.com/mcp") + proxy = FastMCP.as_proxy("http://example.com/mcp/") assert isinstance(proxy, FastMCPProxy) assert isinstance(proxy.client.transport, StreamableHttpTransport) - assert proxy.client.transport.url == "http://example.com/mcp" + assert proxy.client.transport.url == "http://example.com/mcp/" class TestTools: diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 6198a9f70f2693c60cd1c3ff796440a92ea8be5f..11ca06b26bd030551f9361707831abd50c536b5d 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -1785,6 +1785,62 @@ class TestPrompts: assert prompts[0].arguments[1].name == "optional" assert prompts[0].arguments[1].required is False + async def test_list_prompts_with_enhanced_descriptions(self): + """Test that enhanced descriptions with JSON schema are visible via MCP protocol.""" + mcp = FastMCP() + + @mcp.prompt + def analyze_data( + name: str, numbers: list[int], metadata: dict[str, str], threshold: float + ) -> str: + """Analyze some data.""" + return f"Analyzed {name}" + + async with Client(mcp) as client: + prompts = await client.list_prompts() + assert len(prompts) == 1 + prompt = prompts[0] + assert prompt.name == "analyze_data" + assert prompt.description == "Analyze some data." + + # Find each argument and verify schema enhancements + assert prompt.arguments is not None + args_by_name = {arg.name: arg for arg in prompt.arguments} + + # String parameter should not have schema enhancement + name_arg = args_by_name["name"] + assert name_arg.description is None + + # Non-string parameters should have schema enhancements + numbers_arg = args_by_name["numbers"] + assert numbers_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in numbers_arg.description + ) + assert ( + '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + ) + + metadata_arg = args_by_name["metadata"] + assert metadata_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) + + threshold_arg = args_by_name["threshold"] + assert threshold_arg.description is not None + assert ( + "Provide as a JSON string matching the following schema:" + in threshold_arg.description + ) + assert '{"type":"number"}' in threshold_arg.description + async def test_get_prompt(self): """Test getting a prompt through MCP protocol.""" mcp = FastMCP() diff --git a/tests/utilities/openapi/test_openapi_advanced.py b/tests/utilities/openapi/test_openapi_advanced.py index 6b7ec8af3ff6344d7e2a2960c4fc8ad6cd281b43..979ca9b28974b6900062c89a033eee590b1f56d4 100644 --- a/tests/utilities/openapi/test_openapi_advanced.py +++ b/tests/utilities/openapi/test_openapi_advanced.py @@ -294,6 +294,28 @@ def test_complex_schema_route_count(parsed_complex_routes): assert len(parsed_complex_routes) == 3 +def test_complex_schema_ref_rewriting(parsed_complex_routes): + """Test that all #/components references have been rewritten.""" + + def no_components(value): + if isinstance(value, dict): + for k, v in value.items(): + if k == "$ref": + assert not v.startswith("#/components/"), ( + f"reference '{v}' was not rewritten" + ) + else: + no_components(v) + elif isinstance(value, list): + for v in value: + no_components(v) + + for route in parsed_complex_routes: + no_components(route.schema_definitions) + for param in route.parameters: + no_components(param.schema_) + + def test_complex_schema_list_users_query_param_limit(complex_route_map): """Test that a reference to a limit query parameter is correctly resolved.""" list_users = complex_route_map["listUsers"] diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py new file mode 100644 index 0000000000000000000000000000000000000000..72373824251f1aaf9952c414523b8edd1cf6095e --- /dev/null +++ b/tests/utilities/test_inspect.py @@ -0,0 +1,388 @@ +"""Tests for the inspect.py module.""" + +# Import FastMCP1x for testing (always available since mcp is a dependency) +from mcp.server.fastmcp import FastMCP as FastMCP1x + +import fastmcp +from fastmcp import Client, FastMCP +from fastmcp.utilities.inspect import ( + FastMCPInfo, + ToolInfo, + _is_fastmcp_v1, + inspect_fastmcp, + inspect_fastmcp_v1, +) + + +class TestFastMCPInfo: + """Tests for the FastMCPInfo dataclass.""" + + def test_fastmcp_info_creation(self): + """Test that FastMCPInfo can be created with all required fields.""" + tool = ToolInfo( + key="tool1", name="tool1", description="Test tool", input_schema={} + ) + info = FastMCPInfo( + name="TestServer", + instructions="Test instructions", + fastmcp_version="1.0.0", + mcp_version="1.0.0", + server_version="1.0.0", + tools=[tool], + prompts=[], + resources=[], + templates=[], + capabilities={"tools": {"listChanged": True}}, + ) + + assert info.name == "TestServer" + assert info.instructions == "Test instructions" + assert info.fastmcp_version == "1.0.0" + assert info.mcp_version == "1.0.0" + assert info.server_version == "1.0.0" + assert len(info.tools) == 1 + assert info.tools[0].name == "tool1" + assert info.capabilities == {"tools": {"listChanged": True}} + + def test_fastmcp_info_with_none_instructions(self): + """Test that FastMCPInfo works with None instructions.""" + info = FastMCPInfo( + name="TestServer", + instructions=None, + fastmcp_version="1.0.0", + mcp_version="1.0.0", + server_version="1.0.0", + tools=[], + prompts=[], + resources=[], + templates=[], + capabilities={}, + ) + + assert info.instructions is None + + +class TestGetFastMCPInfo: + """Tests for the get_fastmcp_info function.""" + + async def test_empty_server(self): + """Test get_fastmcp_info with an empty server.""" + mcp = FastMCP("EmptyServer", instructions="Empty server for testing") + + info = await inspect_fastmcp(mcp) + + assert info.name == "EmptyServer" + assert info.instructions == "Empty server for testing" + assert info.fastmcp_version == fastmcp.__version__ + assert info.mcp_version is not None + assert info.server_version == fastmcp.__version__ # v2.x uses FastMCP version + assert info.tools == [] + assert info.prompts == [] + assert info.resources == [] + assert info.templates == [] + assert "tools" in info.capabilities + assert "resources" in info.capabilities + assert "prompts" in info.capabilities + assert "logging" in info.capabilities + + async def test_server_with_tools(self): + """Test get_fastmcp_info with a server that has tools.""" + mcp = FastMCP("ToolServer") + + @mcp.tool + def add_numbers(a: int, b: int) -> int: + return a + b + + @mcp.tool + def greet(name: str) -> str: + return f"Hello, {name}!" + + info = await inspect_fastmcp(mcp) + + assert info.name == "ToolServer" + assert len(info.tools) == 2 + tool_names = [tool.name for tool in info.tools] + assert "add_numbers" in tool_names + assert "greet" in tool_names + + async def test_server_with_resources(self): + """Test get_fastmcp_info with a server that has resources.""" + mcp = FastMCP("ResourceServer") + + @mcp.resource("resource://static") + def get_static_data() -> str: + return "Static data" + + @mcp.resource("resource://dynamic/{param}") + def get_dynamic_data(param: str) -> str: + return f"Dynamic data: {param}" + + info = await inspect_fastmcp(mcp) + + assert info.name == "ResourceServer" + assert len(info.resources) == 1 # Static resource + assert len(info.templates) == 1 # Dynamic resource becomes template + resource_uris = [res.uri for res in info.resources] + template_uris = [tmpl.uri_template for tmpl in info.templates] + assert "resource://static" in resource_uris + assert "resource://dynamic/{param}" in template_uris + + async def test_server_with_prompts(self): + """Test get_fastmcp_info with a server that has prompts.""" + mcp = FastMCP("PromptServer") + + @mcp.prompt + def analyze_data(data: str) -> list: + return [{"role": "user", "content": f"Analyze: {data}"}] + + @mcp.prompt("custom_prompt") + def custom_analysis(text: str) -> list: + return [{"role": "user", "content": f"Custom: {text}"}] + + info = await inspect_fastmcp(mcp) + + assert info.name == "PromptServer" + assert len(info.prompts) == 2 + prompt_names = [prompt.name for prompt in info.prompts] + assert "analyze_data" in prompt_names + assert "custom_prompt" in prompt_names + + async def test_comprehensive_server(self): + """Test get_fastmcp_info with a server that has all component types.""" + mcp = FastMCP("ComprehensiveServer", instructions="A server with everything") + + # Add a tool + @mcp.tool + def calculate(x: int, y: int) -> int: + return x * y + + # Add a resource + @mcp.resource("resource://data") + def get_data() -> str: + return "Some data" + + # Add a template + @mcp.resource("resource://item/{id}") + def get_item(id: str) -> str: + return f"Item {id}" + + # Add a prompt + @mcp.prompt + def analyze(content: str) -> list: + return [{"role": "user", "content": content}] + + info = await inspect_fastmcp(mcp) + + assert info.name == "ComprehensiveServer" + assert info.instructions == "A server with everything" + assert info.fastmcp_version == fastmcp.__version__ + + # Check all components are present + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "calculate" in tool_names + + assert len(info.resources) == 1 + resource_uris = [res.uri for res in info.resources] + assert "resource://data" in resource_uris + + assert len(info.templates) == 1 + template_uris = [tmpl.uri_template for tmpl in info.templates] + assert "resource://item/{id}" in template_uris + + assert len(info.prompts) == 1 + prompt_names = [prompt.name for prompt in info.prompts] + assert "analyze" in prompt_names + + # Check capabilities + assert "tools" in info.capabilities + assert "resources" in info.capabilities + assert "prompts" in info.capabilities + assert "logging" in info.capabilities + + async def test_server_no_instructions(self): + """Test get_fastmcp_info with a server that has no instructions.""" + mcp = FastMCP("NoInstructionsServer") + + info = await inspect_fastmcp(mcp) + + assert info.name == "NoInstructionsServer" + assert info.instructions is None + + async def test_server_with_client_integration(self): + """Test that the extracted info matches what a client would see.""" + mcp = FastMCP("IntegrationServer") + + @mcp.tool + def test_tool() -> str: + return "test" + + @mcp.resource("resource://test") + def test_resource() -> str: + return "test resource" + + @mcp.prompt + def test_prompt() -> list: + return [{"role": "user", "content": "test"}] + + # Get info using our function + info = await inspect_fastmcp(mcp) + + # Verify using client + async with Client(mcp) as client: + tools = await client.list_tools() + resources = await client.list_resources() + prompts = await client.list_prompts() + + assert len(info.tools) == len(tools) + assert len(info.resources) == len(resources) + assert len(info.prompts) == len(prompts) + + assert info.tools[0].name == tools[0].name + assert info.resources[0].uri == str(resources[0].uri) + assert info.prompts[0].name == prompts[0].name + + +class TestFastMCP1xCompatibility: + """Tests for FastMCP 1.x compatibility.""" + + async def test_fastmcp1x_detection(self): + """Test that FastMCP1x instances are correctly detected.""" + mcp1x = FastMCP1x("Test1x") + mcp2x = FastMCP("Test2x") + + assert _is_fastmcp_v1(mcp1x) is True + assert _is_fastmcp_v1(mcp2x) is False + + async def test_fastmcp1x_empty_server(self): + """Test get_fastmcp_info_v1 with an empty FastMCP1x server.""" + mcp = FastMCP1x("Test1x") + + info = await inspect_fastmcp_v1(mcp) + + assert info.name == "Test1x" + assert info.instructions is None + assert info.fastmcp_version == fastmcp.__version__ + assert info.mcp_version is not None + assert info.server_version == "1.0" # v1.x servers use "1.0" + assert info.tools == [] + assert info.prompts == [] + assert info.resources == [] + assert info.templates == [] # No templates added in this test + assert "tools" in info.capabilities + + async def test_fastmcp1x_with_tools(self): + """Test get_fastmcp_info_v1 with a FastMCP1x server that has tools.""" + mcp = FastMCP1x("Test1x") + + @mcp.tool() + def add_numbers(a: int, b: int) -> int: + return a + b + + @mcp.tool() + def greet(name: str) -> str: + return f"Hello, {name}!" + + info = await inspect_fastmcp_v1(mcp) + + assert info.name == "Test1x" + assert len(info.tools) == 2 + tool_names = [tool.name for tool in info.tools] + assert "add_numbers" in tool_names + assert "greet" in tool_names + + async def test_fastmcp1x_with_resources(self): + """Test get_fastmcp_info_v1 with a FastMCP1x server that has resources.""" + mcp = FastMCP1x("Test1x") + + @mcp.resource("resource://data") + def get_data() -> str: + return "Some data" + + info = await inspect_fastmcp_v1(mcp) + + assert info.name == "Test1x" + assert len(info.resources) == 1 + resource_uris = [res.uri for res in info.resources] + assert "resource://data" in resource_uris + assert len(info.templates) == 0 # No templates added in this test + + async def test_fastmcp1x_with_prompts(self): + """Test get_fastmcp_info_v1 with a FastMCP1x server that has prompts.""" + mcp = FastMCP1x("Test1x") + + @mcp.prompt("analyze") + def analyze_data(data: str) -> list: + return [{"role": "user", "content": f"Analyze: {data}"}] + + info = await inspect_fastmcp_v1(mcp) + + assert info.name == "Test1x" + assert len(info.prompts) == 1 + prompt_names = [prompt.name for prompt in info.prompts] + assert "analyze" in prompt_names + + async def test_dispatcher_with_fastmcp1x(self): + """Test that the main get_fastmcp_info function correctly dispatches to v1.""" + mcp = FastMCP1x("Test1x") + + @mcp.tool() + def test_tool() -> str: + return "test" + + info = await inspect_fastmcp(mcp) + + assert info.name == "Test1x" + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "test_tool" in tool_names + assert len(info.templates) == 0 # No templates added in this test + + async def test_dispatcher_with_fastmcp2x(self): + """Test that the main get_fastmcp_info function correctly dispatches to v2.""" + mcp = FastMCP("Test2x") + + @mcp.tool + def test_tool() -> str: + return "test" + + info = await inspect_fastmcp(mcp) + + assert info.name == "Test2x" + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "test_tool" in tool_names + + async def test_fastmcp1x_vs_fastmcp2x_comparison(self): + """Test that both versions can be inspected and compared.""" + mcp1x = FastMCP1x("Test1x") + mcp2x = FastMCP("Test2x") + + @mcp1x.tool() + def tool1x() -> str: + return "1x" + + @mcp2x.tool + def tool2x() -> str: + return "2x" + + info1x = await inspect_fastmcp(mcp1x) + info2x = await inspect_fastmcp(mcp2x) + + assert info1x.name == "Test1x" + assert info2x.name == "Test2x" + assert len(info1x.tools) == 1 + assert len(info2x.tools) == 1 + + tool1x_names = [tool.name for tool in info1x.tools] + tool2x_names = [tool.name for tool in info2x.tools] + assert "tool1x" in tool1x_names + assert "tool2x" in tool2x_names + + # Check server versions + assert info1x.server_version == "1.0" + assert info2x.server_version == fastmcp.__version__ + + # No templates added in these tests + assert len(info1x.templates) == 0 + assert len(info2x.templates) == 0 diff --git a/tests/utilities/test_json_schema.py b/tests/utilities/test_json_schema.py index a1b5f15841c3a6eb9e5e8a66576eeb0b0e728589..55c97022473222ec0d61e23ccab51b1481827a01 100644 --- a/tests/utilities/test_json_schema.py +++ b/tests/utilities/test_json_schema.py @@ -1,14 +1,11 @@ from fastmcp.utilities.json_schema import ( _prune_param, + _prune_unused_defs, _walk_and_prune, compress_schema, ) - -# Create wrappers for backward compatibility with tests -def _prune_unused_defs(schema): - """Wrapper for _walk_and_prune that only prunes definitions.""" - return _walk_and_prune(schema, prune_defs=True) +# Wrapper for backward compatibility with tests def _prune_additional_properties(schema): @@ -95,6 +92,21 @@ class TestPruneUnusedDefs: assert "nested_def" in result["$defs"] assert "unused_def" not in result["$defs"] + def test_nested_references_removed(self): + """Test that definitions referenced via nesting in unused defs are removed.""" + schema = { + "properties": {}, + "$defs": { + "foo_def": { + "type": "object", + "properties": {"nested": {"$ref": "#/$defs/nested_def"}}, + }, + "nested_def": {"type": "string"}, + }, + } + result = _prune_unused_defs(schema) + assert "$defs" not in result + def test_array_references_kept(self): """Test that definitions referenced in array items are kept.""" schema = { diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py index f5dee613db63d25560407be4fa32a3e6117d203b..7775e12bcaac60e5df1d0d3b8b8b521f012e79b3 100644 --- a/tests/utilities/test_mcp_config.py +++ b/tests/utilities/test_mcp_config.py @@ -39,7 +39,7 @@ def test_parse_single_remote_config(): mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, StreamableHttpTransport) - assert transport.url == "http://localhost:8000" + assert transport.url == "http://localhost:8000/" def test_parse_remote_config_with_transport(): @@ -54,28 +54,28 @@ def test_parse_remote_config_with_transport(): mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, SSETransport) - assert transport.url == "http://localhost:8000" + assert transport.url == "http://localhost:8000/" def test_parse_remote_config_with_url_inference(): config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", } } } mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, SSETransport) - assert transport.url == "http://localhost:8000/sse" + assert transport.url == "http://localhost:8000/sse/" def test_parse_multiple_servers(): config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", }, "test_server_2": { "command": "echo", @@ -172,7 +172,7 @@ async def test_remote_config_sse_with_auth_token(): config = { "mcpServers": { "test_server": { - "url": "http://localhost:8000/sse", + "url": "http://localhost:8000/sse/", "auth": "test_token", } }