From 5151f218cfd906de30b9f80438d6039211dddc82 Mon Sep 17 00:00:00 2001 From: Martin Melka Date: Tue, 15 Jul 2025 15:59:37 +0200 Subject: [PATCH 01/22] Do not silently exclude the "context" key from JSON body (#1153) --- src/fastmcp/server/openapi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index a17aeab0e..6aa39ed6f 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -459,9 +459,7 @@ class OpenAPITool(Tool): params_to_exclude.add(p.name) body_params = { - k: v - for k, v in arguments.items() - if k not in params_to_exclude and k != "context" + k: v for k, v in arguments.items() if k not in params_to_exclude } if body_params: From 7966514a8623232552e3cfc00aec9a33394a565c Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Tue, 15 Jul 2025 16:01:41 +0200 Subject: [PATCH 02/22] Resolve #1139 -- Implement include_context argument in Context.sample (#1141) --- docs/servers/sampling.mdx | 1 + src/fastmcp/server/context.py | 3 +++ tests/server/proxy/test_proxy_client.py | 1 + 3 files changed, 5 insertions(+) diff --git a/docs/servers/sampling.mdx b/docs/servers/sampling.mdx index 02a8241e9..7682d8087 100644 --- a/docs/servers/sampling.mdx +++ b/docs/servers/sampling.mdx @@ -137,6 +137,7 @@ async def creative_writing(topic: str, ctx: Context) -> str: response = await ctx.sample( messages=f"Write a creative short story about {topic}", model_preferences="claude-3-sonnet", # Prefer a specific model + include_context="thisServer", # Use the server's context temperature=0.9, # High creativity max_tokens=1000 ) diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 0ed229912..6ccd93ae8 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -16,6 +16,7 @@ from mcp.shared.context import RequestContext from mcp.types import ( ContentBlock, CreateMessageResult, + IncludeContext, ModelHint, ModelPreferences, Root, @@ -272,6 +273,7 @@ class Context: self, messages: str | list[str | SamplingMessage], system_prompt: str | None = None, + include_context: IncludeContext | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None, @@ -304,6 +306,7 @@ class Context: result: CreateMessageResult = await self.session.create_message( messages=sampling_messages, system_prompt=system_prompt, + include_context=include_context, temperature=temperature, max_tokens=max_tokens, model_preferences=self._parse_model_preferences(model_preferences), diff --git a/tests/server/proxy/test_proxy_client.py b/tests/server/proxy/test_proxy_client.py index e67e383e0..ffd17116f 100644 --- a/tests/server/proxy/test_proxy_client.py +++ b/tests/server/proxy/test_proxy_client.py @@ -29,6 +29,7 @@ def fastmcp_server(): result = await context.sample( "Hello, world!", system_prompt="You love FastMCP", + include_context="thisServer", temperature=0.5, max_tokens=100, model_preferences="gpt-4o", From 64d469be98cd8b7359bfd646126ec89f18fe79ac Mon Sep 17 00:00:00 2001 From: nate nowack Date: Tue, 15 Jul 2025 09:03:56 -0500 Subject: [PATCH 03/22] Add no-commit-to-branch hook to prevent direct commits to main (#1149) Co-authored-by: Claude --- .github/workflows/run-static.yml | 2 ++ .pre-commit-config.yaml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/run-static.yml b/.github/workflows/run-static.yml index bb4ade8f4..4ef8d2430 100644 --- a/.github/workflows/run-static.yml +++ b/.github/workflows/run-static.yml @@ -44,3 +44,5 @@ jobs: run: uv sync --dev - name: Run pre-commit uses: pre-commit/action@v3.0.1 + env: + SKIP: no-commit-to-branch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b1efe160..bca0329bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,9 @@ repos: hooks: - id: pyright-pretty files: ^src/|^tests/ + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: no-commit-to-branch + args: [--branch, main] From 0977d552ffaca315ca3b7040d0b25fed6928bb80 Mon Sep 17 00:00:00 2001 From: nate nowack Date: Tue, 15 Jul 2025 09:08:56 -0500 Subject: [PATCH 04/22] Fix tool output schema generation to respect Pydantic serialization aliases (#1148) Co-authored-by: Claude --- src/fastmcp/tools/tool.py | 4 +- tests/tools/test_tool.py | 82 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 42223be29..ae07b6216 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -399,7 +399,7 @@ class ParsedFunction: try: type_adapter = get_cached_typeadapter(clean_output_type) - base_schema = type_adapter.json_schema() + base_schema = type_adapter.json_schema(mode="serialization") # Generate schema for wrapped type if it's non-object # because MCP requires that output schemas are objects @@ -410,7 +410,7 @@ class ParsedFunction: # Use the wrapped result schema directly wrapped_type = _WrappedResult[clean_output_type] wrapped_adapter = get_cached_typeadapter(wrapped_type) - output_schema = wrapped_adapter.json_schema() + output_schema = wrapped_adapter.json_schema(mode="serialization") output_schema["x-fastmcp-wrap-result"] = True else: output_schema = base_schema diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index 93410f6be..de006ae34 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -1291,6 +1291,88 @@ class TestUnionReturnTypes: assert result2.structured_content == {"result": "error occurred"} +class TestSerializationAlias: + """Tests for Pydantic field serialization alias support in tool output schemas.""" + + def test_output_schema_respects_serialization_alias(self): + """Test that Tool.from_function generates output schema using serialization alias.""" + from pydantic import AliasChoices, BaseModel, Field + + class Component(BaseModel): + """Model with multiple validation aliases but specific serialization alias.""" + + component_id: str = Field( + validation_alias=AliasChoices("id", "componentId"), + serialization_alias="componentId", + description="The ID of the component", + ) + + async def get_component( + component_id: str, + ) -> Annotated[Component, Field(description="The component.")]: + # API returns data with 'id' field + api_data = {"id": component_id} + return Component.model_validate(api_data) + + tool = Tool.from_function(get_component, name="get-component") + + # The output schema should use the serialization alias 'componentId' + # not the first validation alias 'id' + assert tool.output_schema is not None + + # Check the wrapped result schema + assert "properties" in tool.output_schema + assert "result" in tool.output_schema["properties"] + assert "$defs" in tool.output_schema + + # Find the Component definition + component_def = list(tool.output_schema["$defs"].values())[0] + + # Should have 'componentId' not 'id' in properties + assert "componentId" in component_def["properties"] + assert "id" not in component_def["properties"] + + # Should require 'componentId' not 'id' + assert "componentId" in component_def["required"] + assert "id" not in component_def.get("required", []) + + async def test_tool_execution_with_serialization_alias(self): + """Test that tool execution works correctly with serialization aliases.""" + from pydantic import AliasChoices, BaseModel, Field + + from fastmcp import Client, FastMCP + + class Component(BaseModel): + """Model with multiple validation aliases but specific serialization alias.""" + + component_id: str = Field( + validation_alias=AliasChoices("id", "componentId"), + serialization_alias="componentId", + description="The ID of the component", + ) + + mcp = FastMCP("TestServer") + + @mcp.tool + async def get_component( + component_id: str, + ) -> Annotated[Component, Field(description="The component.")]: + # API returns data with 'id' field + api_data = {"id": component_id} + return Component.model_validate(api_data) + + async with Client(mcp) as client: + # Execute the tool - this should work without validation errors + result = await client.call_tool( + "get_component", {"component_id": "test123"} + ) + + # The result should contain the serialized form with 'componentId' + assert result.structured_content is not None + assert result.structured_content["result"]["componentId"] == "test123" + assert "id" not in result.structured_content["result"] + + class TestToolTitle: """Tests for tool title functionality.""" From 13dd2bfa0ccc5e6904a26d3f231eceebf45515cd Mon Sep 17 00:00:00 2001 From: tommitt <43843689+tommitt@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:09:29 +0200 Subject: [PATCH 05/22] Upgrade Eunomia authorization docs (#1144) --- docs/integrations/eunomia-authorization.mdx | 66 ++++++++++++++------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/docs/integrations/eunomia-authorization.mdx b/docs/integrations/eunomia-authorization.mdx index e18ff0779..e69607006 100644 --- a/docs/integrations/eunomia-authorization.mdx +++ b/docs/integrations/eunomia-authorization.mdx @@ -6,13 +6,17 @@ icon: shield-check tag: NEW --- -Add **policy-based authorization** to your FastMCP servers with minimal code changes using Eunomia authorization middleware. +Add **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Eunomia][eunomia-github] authorization middleware**. -Control which actions MCP clients can perform on your server by restricting how the agent can access resources, tools and prompts by using JSON-based policies, while obtaining a comprehensive audit log of all access attempts and violations. +Control which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic JSON-based policies and obtain a comprehensive audit log of all access attempts and violations. -## Eunomia Authorization Middleware +## How it Works -The middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks. +Exploiting FastMCP's [Middleware][fastmcp-middleare], the Eunomia middleware intercepts all MCP requests to your server and, then, automatically maps MCP methods to authorization checks. + +### Listing Operations + +The middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies. ```mermaid sequenceDiagram @@ -21,15 +25,36 @@ sequenceDiagram participant MCPServer as FastMCP Server participant EunomiaServer as Eunomia Server - MCPClient->>EunomiaMiddleware: MCP Request - Note over MCPClient, EunomiaMiddleware: Middleware intercepts request to server - EunomiaMiddleware->>EunomiaServer: Authorization Check - EunomiaServer->>EunomiaMiddleware: Authorization Decision (allow/deny) - EunomiaMiddleware-->>MCPClient: MCP Unauthorized Error (if denied) - EunomiaMiddleware->>MCPServer: MCP Request (if allowed) - MCPServer-->>MCPClient: MCP Response (if allowed) + MCPClient->>EunomiaMiddleware: MCP Listing Request (e.g., tools/list) + EunomiaMiddleware->>MCPServer: MCP Listing Request + MCPServer-->>EunomiaMiddleware: MCP Listing Response + EunomiaMiddleware->>EunomiaServer: Authorization Checks + EunomiaServer->>EunomiaMiddleware: Authorization Decisions + EunomiaMiddleware-->>MCPClient: Filtered MCP Listing Response ``` +### Execution Operations + +The middleware behaves as a firewall for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies. + +```mermaid +sequenceDiagram + participant MCPClient as MCP Client + participant EunomiaMiddleware as Eunomia Middleware + participant MCPServer as FastMCP Server + participant EunomiaServer as Eunomia Server + + MCPClient->>EunomiaMiddleware: MCP Execution Request (e.g., tools/call) + EunomiaMiddleware->>EunomiaServer: Authorization Check + EunomiaServer->>EunomiaMiddleware: Authorization Decision + EunomiaMiddleware-->>MCPClient: MCP Unauthorized Error (if denied) + EunomiaMiddleware->>MCPServer: MCP Execution Request (if allowed) + MCPServer-->>EunomiaMiddleware: MCP Execution Response (if allowed) + EunomiaMiddleware-->>MCPClient: MCP Execution Response (if allowed) +``` + +## Add Authorization to Your Server + Eunomia is an AI-specific standalone authorization server that handles policy decisions. You must have an Eunomia server running alongside your FastMCP server for the middleware to function. @@ -49,11 +74,11 @@ First, install the `eunomia-mcp` package: pip install eunomia-mcp ``` -Then create a FastMCP server and add the Eunomia middleware with a few lines of code: +Then create a FastMCP server and add the Eunomia middleware in one line: ```python server.py from fastmcp import FastMCP -from eunomia_mcp import create_eunomia_middleware +from eunomia_mcp import EunomiaMcpMiddleware mcp = FastMCP("Secure FastMCP Server 🔒") @@ -62,12 +87,11 @@ def add(a: int, b: int) -> int: """Add two numbers""" return a + b -middleware = [create_eunomia_middleware()] -app = mcp.http_app(middleware=middleware) +middleware = EunomiaMcpMiddleware() +app = mcp.add_middleware(middleware) if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8080) + mcp.run() ``` ### Configure Access Policies @@ -97,12 +121,14 @@ Start your FastMCP server normally: python server.py ``` -The middleware will now intercept all MCP requests and check them against your policies. Requests include agent identification through headers like `X-Agent-ID`, `X-User-ID`, or `Authorization` and an automatic mapping of MCP methods to authorization resources and actions. +The middleware will now intercept all MCP requests and check them against your policies. Requests include agent identification through headers like `X-Agent-ID`, `X-User-ID`, `User-Agent`, or `Authorization` and an automatic mapping of MCP methods to authorization resources and actions. For detailed policy configuration, custom authentication, and advanced deployment patterns, visit the [Eunomia MCP Middleware - repository][eunomia-github]. + repository][eunomia-mcp-github]. -[eunomia-github]: https://github.com/whataboutyou-ai/eunomia/tree/main/pkgs/extensions/mcp +[eunomia-github]: https://github.com/whataboutyou-ai/eunomia +[eunomia-mcp-github]: https://github.com/whataboutyou-ai/eunomia/tree/main/pkgs/extensions/mcp +[fastmcp-middleare]: /servers/middleware From 5fc7aa04aa82da6c06e757b4916df977a3f88f0f Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:16:27 -0700 Subject: [PATCH 06/22] Update README.md (#1165) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0bd6a3a1..ed003a62e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The fast, Pythonic way to build MCP servers and clients. -*FastMCP is made with 💙 by [Prefect](https://www.prefect.io/)* +*FastMCP is made with ☕️ by [Prefect](https://www.prefect.io/)* [![Docs](https://img.shields.io/badge/docs-gofastmcp.com-blue)](https://gofastmcp.com) [![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp) From eca3174922aa1d3257a6a1be66880275da81c7f6 Mon Sep 17 00:00:00 2001 From: itaru2622 <70509350+itaru2622@users.noreply.github.com> Date: Fri, 18 Jul 2025 03:46:23 +0900 Subject: [PATCH 07/22] fix: _replace_ref_with_defs; ensure ref_path is string, because some REST servers use "$ref" as property name of schema. (#1164) --- src/fastmcp/utilities/openapi.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/fastmcp/utilities/openapi.py b/src/fastmcp/utilities/openapi.py index f0cb2ea03..cfd559bf8 100644 --- a/src/fastmcp/utilities/openapi.py +++ b/src/fastmcp/utilities/openapi.py @@ -1068,15 +1068,16 @@ def _replace_ref_with_defs( """ schema = info.copy() if ref_path := schema.get("$ref"): - if ref_path.startswith("#/components/schemas/"): - schema_name = ref_path.split("/")[-1] - schema["$ref"] = f"#/$defs/{schema_name}" - elif not ref_path.startswith("#/"): - raise ValueError( - f"External or non-local reference not supported: {ref_path}. " - f"FastMCP only supports local schema references starting with '#/'. " - f"Please include all schema definitions within the OpenAPI document." - ) + if isinstance(ref_path, str): + if ref_path.startswith("#/components/schemas/"): + schema_name = ref_path.split("/")[-1] + schema["$ref"] = f"#/$defs/{schema_name}" + elif not ref_path.startswith("#/"): + raise ValueError( + f"External or non-local reference not supported: {ref_path}. " + f"FastMCP only supports local schema references starting with '#/'. " + f"Please include all schema definitions within the OpenAPI document." + ) elif properties := schema.get("properties"): if "$ref" in properties: schema["properties"] = _replace_ref_with_defs(properties) From ceb8d85826ba8bacb8cd4f4d3184c3bce49a021d Mon Sep 17 00:00:00 2001 From: Ka Date: Fri, 18 Jul 2025 03:01:03 +0800 Subject: [PATCH 08/22] feat(settings): add log level normalization (#1171) --- src/fastmcp/settings.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 11b0a5f0f..e864ca691 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -5,7 +5,7 @@ import warnings from pathlib import Path from typing import Annotated, Any, Literal -from pydantic import Field, model_validator +from pydantic import Field, field_validator, model_validator from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, @@ -99,7 +99,16 @@ class Settings(BaseSettings): home: Path = Path.home() / ".fastmcp" test_mode: bool = False + log_level: LOG_LEVEL = "INFO" + + @field_validator("log_level", mode="before") + @classmethod + def normalize_log_level(cls, v): + if isinstance(v, str): + return v.upper() + return v + enable_rich_tracebacks: Annotated[ bool, Field( From ebe817601c483337c067a67ba7dc28aaf2d9f2cf Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 17 Jul 2025 12:02:30 -0700 Subject: [PATCH 09/22] add server name to mounted server warnings (#1147) --- src/fastmcp/prompts/prompt_manager.py | 2 +- src/fastmcp/resources/resource_manager.py | 4 ++-- src/fastmcp/tools/tool_manager.py | 2 +- tests/server/test_mount.py | 9 ++++++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/fastmcp/prompts/prompt_manager.py b/src/fastmcp/prompts/prompt_manager.py index 0f7d216f8..c373d01d1 100644 --- a/src/fastmcp/prompts/prompt_manager.py +++ b/src/fastmcp/prompts/prompt_manager.py @@ -78,7 +78,7 @@ class PromptManager: except Exception as e: # Skip failed mounts silently, matches existing behavior logger.warning( - f"Failed to get prompts from mounted server '{mounted.prefix}': {e}" + f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) continue diff --git a/src/fastmcp/resources/resource_manager.py b/src/fastmcp/resources/resource_manager.py index 8620d4114..aa6198111 100644 --- a/src/fastmcp/resources/resource_manager.py +++ b/src/fastmcp/resources/resource_manager.py @@ -109,7 +109,7 @@ class ResourceManager: except Exception as e: # Skip failed mounts silently, matches existing behavior logger.warning( - f"Failed to get resources from mounted server '{mounted.prefix}': {e}" + f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) continue @@ -157,7 +157,7 @@ class ResourceManager: except Exception as e: # Skip failed mounts silently, matches existing behavior logger.warning( - f"Failed to get templates from mounted server '{mounted.prefix}': {e}" + f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) continue diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 29bb956c3..90737984b 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -76,7 +76,7 @@ class ToolManager: except Exception as e: # Skip failed mounts silently, matches existing behavior logger.warning( - f"Failed to get tools from mounted server '{mounted.prefix}': {e}" + f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) continue diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index baf4086a4..56369ebb8 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -317,15 +317,18 @@ class TestMultipleServerMount: record.message for record in caplog.records if record.levelname == "WARNING" ] assert any( - "Failed to get tools from mounted server 'unreachable'" in msg + "Failed to get tools from server: 'FastMCP', mounted at: 'unreachable'" + in msg for msg in warning_messages ) assert any( - "Failed to get resources from mounted server 'unreachable'" in msg + "Failed to get resources from server: 'FastMCP', mounted at: 'unreachable'" + in msg for msg in warning_messages ) assert any( - "Failed to get prompts from mounted server 'unreachable'" in msg + "Failed to get prompts from server: 'FastMCP', mounted at: 'unreachable'" + in msg for msg in warning_messages ) From 3543c13ca9787e36ee252d430b62c6deb9a02d33 Mon Sep 17 00:00:00 2001 From: Martin Melka Date: Fri, 18 Jul 2025 14:26:42 +0200 Subject: [PATCH 10/22] Fix nesting when making OpenAPI arrays and objects optional (#1178) --- src/fastmcp/utilities/openapi.py | 48 ++++++++++++++++++- .../openapi/test_optional_parameters.py | 8 ++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/fastmcp/utilities/openapi.py b/src/fastmcp/utilities/openapi.py index cfd559bf8..cfb604b90 100644 --- a/src/fastmcp/utilities/openapi.py +++ b/src/fastmcp/utilities/openapi.py @@ -1114,10 +1114,56 @@ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]: # Create a new schema that allows null in addition to the original type if "type" in schema: original_type = schema["type"] + if isinstance(original_type, str): # Single type - make it a union with null nullable_schema = schema.copy() - nullable_schema["anyOf"] = [{"type": original_type}, {"type": "null"}] + + nested_non_nullable_schema = { + "type": original_type, + } + + # If the original type is an array, move the array-specific properties into the now-nested schema + # https://json-schema.org/understanding-json-schema/reference/array + if original_type == "array": + for array_property in [ + "items", + "prefixItems", + "unevaluatedItems", + "contains", + "minContains", + "maxContains", + "minItems", + "maxItems", + "uniqueItems", + ]: + if array_property in nullable_schema: + nested_non_nullable_schema[array_property] = nullable_schema[ + array_property + ] + del nullable_schema[array_property] + + # If the original type is an object, move the object-specific properties into the now-nested schema + # https://json-schema.org/understanding-json-schema/reference/object + elif original_type == "object": + for object_property in [ + "properties", + "patternProperties", + "additionalProperties", + "unevaluatedProperties", + "required", + "propertyNames", + "minProperties", + "maxProperties", + ]: + if object_property in nullable_schema: + nested_non_nullable_schema[object_property] = nullable_schema[ + object_property + ] + del nullable_schema[object_property] + + nullable_schema["anyOf"] = [nested_non_nullable_schema, {"type": "null"}] + # Remove the original type since we're using anyOf del nullable_schema["type"] return nullable_schema diff --git a/tests/server/openapi/test_optional_parameters.py b/tests/server/openapi/test_optional_parameters.py index fda714d9c..ca84cab3f 100644 --- a/tests/server/openapi/test_optional_parameters.py +++ b/tests/server/openapi/test_optional_parameters.py @@ -95,8 +95,6 @@ async def test_optional_parameter_allows_null_for_type(param_schema): # Should have anyOf with the original type and null assert "anyOf" in optional_param_schema assert {"type": "null"} in optional_param_schema["anyOf"] - # Check that original schema is preserved (either simple type or complex schema) - if "type" in param_schema: - assert {"type": param_schema["type"]} in optional_param_schema["anyOf"] - else: - assert param_schema in optional_param_schema["anyOf"] + + # Check that original schema is fully preserved under anyOf + assert param_schema in optional_param_schema["anyOf"] From c289f172176cf18547f6b354aad5c5583cafdbad Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 18 Jul 2025 10:19:16 -0500 Subject: [PATCH 11/22] update api ref for new `mdxify` version --- docs/docs.json | 47 +- docs/python-sdk/fastmcp-cli-claude.mdx | 4 +- docs/python-sdk/fastmcp-cli-cli.mdx | 78 ++-- .../fastmcp-cli-install-__init__.mdx | 9 + .../fastmcp-cli-install-claude_code.mdx | 68 +++ .../fastmcp-cli-install-claude_desktop.mdx | 55 +++ .../python-sdk/fastmcp-cli-install-cursor.mdx | 78 ++++ .../fastmcp-cli-install-mcp_config.mdx | 46 ++ .../python-sdk/fastmcp-cli-install-shared.mdx | 31 ++ docs/python-sdk/fastmcp-cli-run.mdx | 18 +- .../python-sdk/fastmcp-client-auth-bearer.mdx | 4 +- docs/python-sdk/fastmcp-client-auth-oauth.mdx | 98 +++- docs/python-sdk/fastmcp-client-client.mdx | 441 +++++++++++++++++- .../python-sdk/fastmcp-client-elicitation.mdx | 18 + docs/python-sdk/fastmcp-client-logging.mdx | 8 +- docs/python-sdk/fastmcp-client-messages.mdx | 107 +++++ .../fastmcp-client-oauth_callback.mdx | 10 +- docs/python-sdk/fastmcp-client-progress.mdx | 19 +- docs/python-sdk/fastmcp-client-roots.mdx | 4 +- docs/python-sdk/fastmcp-client-sampling.mdx | 2 +- docs/python-sdk/fastmcp-client-transports.mdx | 124 ++++- docs/python-sdk/fastmcp-exceptions.mdx | 18 +- docs/python-sdk/fastmcp-mcp_config.mdx | 150 ++++++ docs/python-sdk/fastmcp-prompts-prompt.mdx | 50 +- .../fastmcp-prompts-prompt_manager.mdx | 54 ++- .../python-sdk/fastmcp-resources-resource.mdx | 50 +- .../fastmcp-resources-resource_manager.mdx | 84 +++- .../python-sdk/fastmcp-resources-template.mdx | 65 ++- docs/python-sdk/fastmcp-resources-types.mdx | 69 ++- docs/python-sdk/fastmcp-server-auth-auth.mdx | 22 +- .../fastmcp-server-auth-providers-bearer.mdx | 97 +++- ...stmcp-server-auth-providers-bearer_env.mdx | 4 +- ...astmcp-server-auth-providers-in_memory.mdx | 83 +++- docs/python-sdk/fastmcp-server-context.mdx | 193 +++++++- .../fastmcp-server-dependencies.mdx | 6 +- .../python-sdk/fastmcp-server-elicitation.mdx | 54 +++ docs/python-sdk/fastmcp-server-http.mdx | 16 +- docs/python-sdk/fastmcp-server-low_level.mdx | 18 + ...stmcp-server-middleware-error_handling.mdx | 26 +- .../fastmcp-server-middleware-logging.mdx | 26 +- .../fastmcp-server-middleware-middleware.mdx | 84 +++- ...astmcp-server-middleware-rate_limiting.mdx | 60 ++- .../fastmcp-server-middleware-timing.mdx | 80 +++- docs/python-sdk/fastmcp-server-openapi.mdx | 47 +- docs/python-sdk/fastmcp-server-proxy.mdx | 224 ++++++++- docs/python-sdk/fastmcp-server-server.mdx | 262 +++++++++-- docs/python-sdk/fastmcp-settings.mdx | 20 +- docs/python-sdk/fastmcp-tools-tool.mdx | 72 ++- .../python-sdk/fastmcp-tools-tool_manager.mdx | 56 ++- .../fastmcp-tools-tool_transform.mdx | 114 ++++- docs/python-sdk/fastmcp-utilities-cache.mdx | 8 +- docs/python-sdk/fastmcp-utilities-cli.mdx | 25 + .../fastmcp-utilities-components.mdx | 57 ++- .../fastmcp-utilities-exceptions.mdx | 4 +- docs/python-sdk/fastmcp-utilities-http.mdx | 2 +- docs/python-sdk/fastmcp-utilities-inspect.mdx | 63 ++- .../fastmcp-utilities-json_schema.mdx | 2 +- .../fastmcp-utilities-json_schema_type.mdx | 110 +++++ docs/python-sdk/fastmcp-utilities-logging.mdx | 4 +- docs/python-sdk/fastmcp-utilities-openapi.mdx | 84 +++- docs/python-sdk/fastmcp-utilities-tests.mdx | 14 +- docs/python-sdk/fastmcp-utilities-types.mdx | 53 ++- 62 files changed, 3415 insertions(+), 384 deletions(-) create mode 100644 docs/python-sdk/fastmcp-cli-install-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-cli-install-claude_code.mdx create mode 100644 docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx create mode 100644 docs/python-sdk/fastmcp-cli-install-cursor.mdx create mode 100644 docs/python-sdk/fastmcp-cli-install-mcp_config.mdx create mode 100644 docs/python-sdk/fastmcp-cli-install-shared.mdx create mode 100644 docs/python-sdk/fastmcp-client-elicitation.mdx create mode 100644 docs/python-sdk/fastmcp-client-messages.mdx create mode 100644 docs/python-sdk/fastmcp-mcp_config.mdx create mode 100644 docs/python-sdk/fastmcp-server-elicitation.mdx create mode 100644 docs/python-sdk/fastmcp-server-low_level.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-cli.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-json_schema_type.mdx diff --git a/docs/docs.json b/docs/docs.json index fac25bb32..9d87f0ed4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -65,7 +65,10 @@ { "group": "Essentials", "icon": "cube", - "pages": ["servers/server", "deployment/running-server"] + "pages": [ + "servers/server", + "deployment/running-server" + ] }, { "group": "Core Components", @@ -93,7 +96,9 @@ { "group": "Authentication", "icon": "shield-check", - "pages": ["servers/auth/bearer"] + "pages": [ + "servers/auth/bearer" + ] } ] }, @@ -103,7 +108,10 @@ { "group": "Essentials", "icon": "cube", - "pages": ["clients/client", "clients/transports"] + "pages": [ + "clients/client", + "clients/transports" + ] }, { "group": "Core Operations", @@ -129,7 +137,10 @@ { "group": "Authentication", "icon": "user-shield", - "pages": ["clients/auth/oauth", "clients/auth/bearer"] + "pages": [ + "clients/auth/oauth", + "clients/auth/bearer" + ] } ] }, @@ -174,12 +185,17 @@ }, { "anchor": "What's New", - "pages": ["updates", "changelog"] + "pages": [ + "updates", + "changelog" + ] }, { "anchor": "Community", "icon": "users", - "pages": ["community/showcase"] + "pages": [ + "community/showcase" + ] } ] }, @@ -191,6 +207,7 @@ "icon": "python", "pages": [ "python-sdk/fastmcp-exceptions", + "python-sdk/fastmcp-mcp_config", "python-sdk/fastmcp-settings", { "group": "fastmcp.cli", @@ -198,6 +215,17 @@ "python-sdk/fastmcp-cli-__init__", "python-sdk/fastmcp-cli-claude", "python-sdk/fastmcp-cli-cli", + { + "group": "install", + "pages": [ + "python-sdk/fastmcp-cli-install-__init__", + "python-sdk/fastmcp-cli-install-claude_code", + "python-sdk/fastmcp-cli-install-claude_desktop", + "python-sdk/fastmcp-cli-install-cursor", + "python-sdk/fastmcp-cli-install-mcp_config", + "python-sdk/fastmcp-cli-install-shared" + ] + }, "python-sdk/fastmcp-cli-run" ] }, @@ -214,7 +242,9 @@ ] }, "python-sdk/fastmcp-client-client", + "python-sdk/fastmcp-client-elicitation", "python-sdk/fastmcp-client-logging", + "python-sdk/fastmcp-client-messages", "python-sdk/fastmcp-client-oauth_callback", "python-sdk/fastmcp-client-progress", "python-sdk/fastmcp-client-roots", @@ -262,7 +292,9 @@ }, "python-sdk/fastmcp-server-context", "python-sdk/fastmcp-server-dependencies", + "python-sdk/fastmcp-server-elicitation", "python-sdk/fastmcp-server-http", + "python-sdk/fastmcp-server-low_level", { "group": "middleware", "pages": [ @@ -293,13 +325,14 @@ "pages": [ "python-sdk/fastmcp-utilities-__init__", "python-sdk/fastmcp-utilities-cache", + "python-sdk/fastmcp-utilities-cli", "python-sdk/fastmcp-utilities-components", "python-sdk/fastmcp-utilities-exceptions", "python-sdk/fastmcp-utilities-http", "python-sdk/fastmcp-utilities-inspect", "python-sdk/fastmcp-utilities-json_schema", + "python-sdk/fastmcp-utilities-json_schema_type", "python-sdk/fastmcp-utilities-logging", - "python-sdk/fastmcp-utilities-mcp_config", "python-sdk/fastmcp-utilities-openapi", "python-sdk/fastmcp-utilities-tests", "python-sdk/fastmcp-utilities-types" diff --git a/docs/python-sdk/fastmcp-cli-claude.mdx b/docs/python-sdk/fastmcp-cli-claude.mdx index b56b63338..37fcedbf2 100644 --- a/docs/python-sdk/fastmcp-cli-claude.mdx +++ b/docs/python-sdk/fastmcp-cli-claude.mdx @@ -10,7 +10,7 @@ Claude app integration utilities. ## Functions -### `get_claude_config_path` +### `get_claude_config_path` ```python get_claude_config_path() -> Path | None @@ -20,7 +20,7 @@ get_claude_config_path() -> Path | None Get the Claude config directory based on platform. -### `update_claude_config` +### `update_claude_config` ```python update_claude_config(file_spec: str, server_name: str) -> bool diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx index 3ab68da9a..51e8bd4e1 100644 --- a/docs/python-sdk/fastmcp-cli-cli.mdx +++ b/docs/python-sdk/fastmcp-cli-cli.mdx @@ -6,77 +6,66 @@ sidebarTitle: cli # `fastmcp.cli.cli` -FastMCP CLI tools. +FastMCP CLI tools using Cyclopts. ## Functions -### `version` +### `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 +version() ``` -Run a MCP server with the MCP Inspector. +Display version information and platform details. -### `run` +### `dev` ```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, 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 +dev(server_spec: str) -> None ``` -Run a MCP server or connect to a remote one. +Run an MCP server with the MCP Inspector for development. + +**Args:** +- `server_spec`: Python file to run, optionally with \:object suffix + + +### `run` + +```python +run(server_spec: str) -> None +``` + + +Run an 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. +1. Module approach: server.py - runs the module directly, looking for an object named 'mcp', 'server', or '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 Server arguments can be passed after -- : fastmcp run server.py -- --config config.json --debug +**Args:** +- `server_spec`: Python file, object specification (file\:obj), or URL -### `install` + +### `inspect` ```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 +inspect(server_spec: str) -> None ``` -Install a MCP server in the Claude desktop app. +Inspect an MCP server and generate a JSON report. -Environment variables are preserved once added and only updated if new values -are explicitly provided. - - -### `inspect` - -```python -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. +This command analyzes an MCP server and generates a comprehensive JSON report +containing information about the server's name, instructions, version, tools, +prompts, resources, templates, and capabilities. **Examples:** @@ -85,3 +74,6 @@ 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 +**Args:** +- `server_spec`: Python file to inspect, optionally with \:object suffix + diff --git a/docs/python-sdk/fastmcp-cli-install-__init__.mdx b/docs/python-sdk/fastmcp-cli-install-__init__.mdx new file mode 100644 index 000000000..3909565f2 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-install-__init__.mdx @@ -0,0 +1,9 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.cli.install` + + +Install subcommands for FastMCP CLI using Cyclopts. diff --git a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx new file mode 100644 index 000000000..207756e69 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx @@ -0,0 +1,68 @@ +--- +title: claude_code +sidebarTitle: claude_code +--- + +# `fastmcp.cli.install.claude_code` + + +Claude Code integration for FastMCP install using Cyclopts. + +## Functions + +### `find_claude_command` + +```python +find_claude_command() -> str | None +``` + + +Find the Claude Code CLI command. + +Checks common installation locations since 'claude' is often a shell alias +that doesn't work with subprocess calls. + + +### `check_claude_code_available` + +```python +check_claude_code_available() -> bool +``` + + +Check if Claude Code CLI is available. + + +### `install_claude_code` + +```python +install_claude_code(file: Path, server_object: str | None, name: str) -> bool +``` + + +Install FastMCP server in Claude Code. + +**Args:** +- `file`: Path to the server file +- `server_object`: Optional server object name (for \:object suffix) +- `name`: Name for the server in Claude Code +- `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 + +**Returns:** +- True if installation was successful, False otherwise + + +### `claude_code_command` + +```python +claude_code_command(server_spec: str) -> None +``` + + +Install an MCP server in Claude Code. + +**Args:** +- `server_spec`: Python file to install, optionally with \:object suffix + diff --git a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx new file mode 100644 index 000000000..ceb3ee328 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx @@ -0,0 +1,55 @@ +--- +title: claude_desktop +sidebarTitle: claude_desktop +--- + +# `fastmcp.cli.install.claude_desktop` + + +Claude Desktop integration for FastMCP install using Cyclopts. + +## Functions + +### `get_claude_config_path` + +```python +get_claude_config_path() -> Path | None +``` + + +Get the Claude config directory based on platform. + + +### `install_claude_desktop` + +```python +install_claude_desktop(file: Path, server_object: str | None, name: str) -> bool +``` + + +Install FastMCP server in Claude Desktop. + +**Args:** +- `file`: Path to the server file +- `server_object`: Optional server object name (for \:object suffix) +- `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 + +**Returns:** +- True if installation was successful, False otherwise + + +### `claude_desktop_command` + +```python +claude_desktop_command(server_spec: str) -> None +``` + + +Install an MCP server in Claude Desktop. + +**Args:** +- `server_spec`: Python file to install, optionally with \:object suffix + diff --git a/docs/python-sdk/fastmcp-cli-install-cursor.mdx b/docs/python-sdk/fastmcp-cli-install-cursor.mdx new file mode 100644 index 000000000..29c20d827 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-install-cursor.mdx @@ -0,0 +1,78 @@ +--- +title: cursor +sidebarTitle: cursor +--- + +# `fastmcp.cli.install.cursor` + + +Cursor integration for FastMCP install using Cyclopts. + +## Functions + +### `generate_cursor_deeplink` + +```python +generate_cursor_deeplink(server_name: str, server_config: StdioMCPServer) -> str +``` + + +Generate a Cursor deeplink for installing the MCP server. + +**Args:** +- `server_name`: Name of the server +- `server_config`: Server configuration + +**Returns:** +- Deeplink URL that can be clicked to install the server + + +### `open_deeplink` + +```python +open_deeplink(deeplink: str) -> bool +``` + + +Attempt to open a deeplink URL using the system's default handler. + +**Args:** +- `deeplink`: The deeplink URL to open + +**Returns:** +- True if the command succeeded, False otherwise + + +### `install_cursor` + +```python +install_cursor(file: Path, server_object: str | None, name: str) -> bool +``` + + +Install FastMCP server in Cursor. + +**Args:** +- `file`: Path to the server file +- `server_object`: Optional server object name (for \:object suffix) +- `name`: Name for the server in Cursor +- `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 + +**Returns:** +- True if installation was successful, False otherwise + + +### `cursor_command` + +```python +cursor_command(server_spec: str) -> None +``` + + +Install an MCP server in Cursor. + +**Args:** +- `server_spec`: Python file to install, optionally with \:object suffix + diff --git a/docs/python-sdk/fastmcp-cli-install-mcp_config.mdx b/docs/python-sdk/fastmcp-cli-install-mcp_config.mdx new file mode 100644 index 000000000..f22480400 --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-install-mcp_config.mdx @@ -0,0 +1,46 @@ +--- +title: mcp_config +sidebarTitle: mcp_config +--- + +# `fastmcp.cli.install.mcp_config` + + +MCP configuration JSON generation for FastMCP install using Cyclopts. + +## Functions + +### `install_mcp_config` + +```python +install_mcp_config(file: Path, server_object: str | None, name: str) -> bool +``` + + +Generate MCP configuration JSON for manual installation. + +**Args:** +- `file`: Path to the server file +- `server_object`: Optional server object name (for \:object suffix) +- `name`: Name for the server in MCP 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 +- `copy`: If True, copy to clipboard instead of printing to stdout + +**Returns:** +- True if generation was successful, False otherwise + + +### `mcp_config_command` + +```python +mcp_config_command(server_spec: str) -> None +``` + + +Generate MCP configuration JSON for manual installation. + +**Args:** +- `server_spec`: Python file to install, optionally with \:object suffix + diff --git a/docs/python-sdk/fastmcp-cli-install-shared.mdx b/docs/python-sdk/fastmcp-cli-install-shared.mdx new file mode 100644 index 000000000..5279742eb --- /dev/null +++ b/docs/python-sdk/fastmcp-cli-install-shared.mdx @@ -0,0 +1,31 @@ +--- +title: shared +sidebarTitle: shared +--- + +# `fastmcp.cli.install.shared` + + +Shared utilities for install commands. + +## Functions + +### `parse_env_var` + +```python +parse_env_var(env_var: str) -> tuple[str, str] +``` + + +Parse environment variable string in format KEY=VALUE. + + +### `process_common_args` + +```python +process_common_args(server_spec: str, server_name: str | None, with_packages: list[str], env_vars: list[str], env_file: Path | None) -> tuple[Path, str | None, str, list[str], dict[str, str] | None] +``` + + +Process common arguments shared by all install commands. + diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx index 78adc9056..16cb54405 100644 --- a/docs/python-sdk/fastmcp-cli-run.mdx +++ b/docs/python-sdk/fastmcp-cli-run.mdx @@ -6,11 +6,11 @@ sidebarTitle: run # `fastmcp.cli.run` -FastMCP run command implementation. +FastMCP run command implementation with enhanced type hints. ## Functions -### `is_url` +### `is_url` ```python is_url(path: str) -> bool @@ -20,7 +20,7 @@ is_url(path: str) -> bool Check if a string is a URL. -### `parse_file_path` +### `parse_file_path` ```python parse_file_path(server_spec: str) -> tuple[Path, str | None] @@ -36,7 +36,7 @@ Parse a file path that may include a server object specification. - Tuple of (file_path, server_object) -### `import_server` +### `import_server` ```python import_server(file: Path, server_object: str | None = None) -> Any @@ -53,7 +53,7 @@ Import a MCP server from a file. - The server object -### `create_client_server` +### `create_client_server` ```python create_client_server(url: str) -> Any @@ -69,7 +69,7 @@ Create a FastMCP server from a client URL. - A FastMCP server instance -### `import_server_with_args` +### `import_server_with_args` ```python import_server_with_args(file: Path, server_object: str | None = None, server_args: list[str] | None = None) -> Any @@ -87,10 +87,10 @@ Import a server with optional command line arguments. - The imported server object -### `run_command` +### `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_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True) -> None ``` @@ -101,6 +101,8 @@ Run a MCP server or connect to a remote one. - `transport`: Transport protocol to use - `host`: Host to bind to when using http transport - `port`: Port to bind to when using http transport +- `path`: Path to bind to when using http transport - `log_level`: Log level - `server_args`: Additional arguments to pass to the server +- `show_banner`: Whether to show the server banner diff --git a/docs/python-sdk/fastmcp-client-auth-bearer.mdx b/docs/python-sdk/fastmcp-client-auth-bearer.mdx index c83e354b5..c04905b14 100644 --- a/docs/python-sdk/fastmcp-client-auth-bearer.mdx +++ b/docs/python-sdk/fastmcp-client-auth-bearer.mdx @@ -7,11 +7,11 @@ sidebarTitle: bearer ## Classes -### `BearerAuth` +### `BearerAuth` **Methods:** -#### `auth_flow` +#### `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 index 19ad489e9..fe24548ee 100644 --- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx +++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx @@ -7,16 +7,46 @@ sidebarTitle: oauth ## Functions -### `default_cache_dir` +### `default_cache_dir` ```python default_cache_dir() -> Path ``` -### `OAuth` +### `discover_oauth_metadata` ```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 +discover_oauth_metadata(server_base_url: str, httpx_kwargs: dict[str, Any] | None = None) -> OAuthMetadata | None +``` + + +Discover OAuth metadata from the server using RFC 8414 well-known endpoint. + +**Args:** +- `server_base_url`: Base URL of the OAuth server (e.g., "https\://example.com") +- `httpx_kwargs`: Additional kwargs for httpx client + +**Returns:** +- OAuth metadata if found, None otherwise + + +### `check_if_auth_required` + +```python +check_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool +``` + + +Check if the MCP endpoint requires authentication by making a test request. + +**Returns:** +- True if auth appears to be required, False otherwise + + +### `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) -> OAuthClientProvider ``` @@ -38,23 +68,7 @@ httpx.AsyncClient (or appropriate FastMCP client/transport instance) ## 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` +### `FileTokenStorage` File-based token storage implementation for OAuth credentials and tokens. @@ -65,7 +79,7 @@ Each instance is tied to a specific server URL for proper token isolation. **Methods:** -#### `get_base_url` +#### `get_base_url` ```python get_base_url(url: str) -> str @@ -74,7 +88,7 @@ get_base_url(url: str) -> str Extract the base URL (scheme + host) from a URL. -#### `get_cache_key` +#### `get_cache_key` ```python get_cache_key(self) -> str @@ -83,7 +97,43 @@ get_cache_key(self) -> str Generate a safe filesystem key from the server's base URL. -#### `clear` +#### `get_tokens` + +```python +get_tokens(self) -> OAuthToken | None +``` + +Load tokens from file storage. + + +#### `set_tokens` + +```python +set_tokens(self, tokens: OAuthToken) -> None +``` + +Save tokens to file storage. + + +#### `get_client_info` + +```python +get_client_info(self) -> OAuthClientInformationFull | None +``` + +Load client information from file storage. + + +#### `set_client_info` + +```python +set_client_info(self, client_info: OAuthClientInformationFull) -> None +``` + +Save client information to file storage. + + +#### `clear` ```python clear(self) -> None @@ -92,7 +142,7 @@ clear(self) -> None Clear all cached data for this server. -#### `clear_all` +#### `clear_all` ```python clear_all(cls, cache_dir: Path | None = None) -> None diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx index 3b99527e7..a1ff804bf 100644 --- a/docs/python-sdk/fastmcp-client-client.mdx +++ b/docs/python-sdk/fastmcp-client-client.mdx @@ -7,7 +7,16 @@ sidebarTitle: client ## Classes -### `Client` +### `ClientSessionState` + + +Holds all session-related state for a Client instance. + +This allows clean separation of configuration (which is copied) from +session state (which should be fresh for each new client instance). + + +### `Client` MCP client that delegates connection management to a Transport instance. @@ -16,14 +25,34 @@ 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. +This client supports reentrant context managers (multiple concurrent +`async with client:` blocks) using reference counting and background session +management. This allows efficient session reuse in any scenario with +nested or concurrent client usage. + +MCP SDK 1.10 introduced automatic list_tools() calls during call_tool() +execution. This created a race condition where events could be reset while +other tasks were waiting on them, causing deadlocks. The issue was exposed +in proxy scenarios but affects any reentrant usage. + +The solution uses reference counting to track active context managers, +a background task to manage the session lifecycle, events to coordinate +between tasks, and ensures all session state changes happen within a lock. +Events are only created when needed, never reset outside locks. + +This design prevents race conditions where tasks wait on events that get +replaced by other tasks, ensuring reliable coordination in concurrent scenarios. + **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 +- `transport`: +Connection source specification, which can be\: + + - ClientTransport\: Direct transport instance + - FastMCP\: In-process FastMCP server + - AnyUrl or 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 @@ -35,20 +64,22 @@ 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") +```python +# Connect to FastMCP server +client = Client("http://localhost:8080") async with client: - # List available resources resources = await client.list_resources() + # List available resources + resources = await client.list_resources() - # Call a tool result = await client.call_tool("my_tool", {"param": - "value"}) + # Call a tool + result = await client.call_tool("my_tool", {"param": "value"}) ``` **Methods:** -#### `session` +#### `session` ```python session(self) -> ClientSession @@ -57,7 +88,7 @@ session(self) -> ClientSession Get the current active session. Raises RuntimeError if not connected. -#### `initialize_result` +#### `initialize_result` ```python initialize_result(self) -> mcp.types.InitializeResult @@ -66,7 +97,7 @@ initialize_result(self) -> mcp.types.InitializeResult Get the result of the initialization request. -#### `set_roots` +#### `set_roots` ```python set_roots(self, roots: RootsList | RootsHandler) -> None @@ -75,7 +106,7 @@ 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` +#### `set_sampling_callback` ```python set_sampling_callback(self, sampling_callback: SamplingHandler) -> None @@ -84,7 +115,16 @@ set_sampling_callback(self, sampling_callback: SamplingHandler) -> None Set the sampling callback for the client. -#### `is_connected` +#### `set_elicitation_callback` + +```python +set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None +``` + +Set the elicitation callback for the client. + + +#### `is_connected` ```python is_connected(self) -> bool @@ -92,3 +132,368 @@ is_connected(self) -> bool Check if the client is currently connected. + +#### `new` + +```python +new(self) -> Client[ClientTransportT] +``` + +Create a new client instance with the same configuration but fresh session state. + +This creates a new client with the same transport, handlers, and configuration, +but with no active session. Useful for creating independent sessions that don't +share state with the original client. + +**Returns:** +- A new Client instance with the same configuration but disconnected state. + + +#### `close` + +```python +close(self) +``` + +#### `ping` + +```python +ping(self) -> bool +``` + +Send a ping request. + + +#### `cancel` + +```python +cancel(self, request_id: str | int, reason: str | None = None) -> None +``` + +Send a cancellation notification for an in-progress request. + + +#### `progress` + +```python +progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None +``` + +Send a progress notification. + + +#### `set_logging_level` + +```python +set_logging_level(self, level: mcp.types.LoggingLevel) -> None +``` + +Send a logging/setLevel request. + + +#### `send_roots_list_changed` + +```python +send_roots_list_changed(self) -> None +``` + +Send a roots/list_changed notification. + + +#### `list_resources_mcp` + +```python +list_resources_mcp(self) -> mcp.types.ListResourcesResult +``` + +Send a resources/list request and return the complete MCP protocol result. + +**Returns:** +- mcp.types.ListResourcesResult: The complete response object from the protocol, +containing the list of resources and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_resources` + +```python +list_resources(self) -> list[mcp.types.Resource] +``` + +Retrieve a list of resources available on the server. + +**Returns:** +- list\[mcp.types.Resource]: A list of Resource objects. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_resource_templates_mcp` + +```python +list_resource_templates_mcp(self) -> mcp.types.ListResourceTemplatesResult +``` + +Send a resources/listResourceTemplates request and return the complete MCP protocol result. + +**Returns:** +- mcp.types.ListResourceTemplatesResult: The complete response object from the protocol, +containing the list of resource templates and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_resource_templates` + +```python +list_resource_templates(self) -> list[mcp.types.ResourceTemplate] +``` + +Retrieve a list of resource templates available on the server. + +**Returns:** +- list\[mcp.types.ResourceTemplate]: A list of ResourceTemplate objects. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `read_resource_mcp` + +```python +read_resource_mcp(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult +``` + +Send a resources/read request and return the complete MCP protocol result. + +**Args:** +- `uri`: The URI of the resource to read. Can be a string or an AnyUrl object. + +**Returns:** +- mcp.types.ReadResourceResult: The complete response object from the protocol, +containing the resource contents and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `read_resource` + +```python +read_resource(self, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] +``` + +Read the contents of a resource or resolved template. + +**Args:** +- `uri`: The URI of the resource to read. Can be a string or an AnyUrl object. + +**Returns:** +- list\[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: A list of content +objects, typically containing either text or binary data. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_prompts_mcp` + +```python +list_prompts_mcp(self) -> mcp.types.ListPromptsResult +``` + +Send a prompts/list request and return the complete MCP protocol result. + +**Returns:** +- mcp.types.ListPromptsResult: The complete response object from the protocol, +containing the list of prompts and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_prompts` + +```python +list_prompts(self) -> list[mcp.types.Prompt] +``` + +Retrieve a list of prompts available on the server. + +**Returns:** +- list\[mcp.types.Prompt]: A list of Prompt objects. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `get_prompt_mcp` + +```python +get_prompt_mcp(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`: The name of the prompt to retrieve. +- `arguments`: Arguments to pass to the prompt. Defaults to None. + +**Returns:** +- mcp.types.GetPromptResult: The complete response object from the protocol, +containing the prompt messages and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `get_prompt` + +```python +get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult +``` + +Retrieve a rendered prompt message list from the server. + +**Args:** +- `name`: The name of the prompt to retrieve. +- `arguments`: Arguments to pass to the prompt. Defaults to None. + +**Returns:** +- mcp.types.GetPromptResult: The complete response object from the protocol, +containing the prompt messages and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `complete_mcp` + +```python +complete_mcp(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.CompleteResult +``` + +Send a completion request and return the complete MCP protocol result. + +**Args:** +- `ref`: The reference to complete. +- `argument`: Arguments to pass to the completion request. + +**Returns:** +- mcp.types.CompleteResult: The complete response object from the protocol, +containing the completion and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `complete` + +```python +complete(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.Completion +``` + +Send a completion request to the server. + +**Args:** +- `ref`: The reference to complete. +- `argument`: Arguments to pass to the completion request. + +**Returns:** +- mcp.types.Completion: The completion object. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_tools_mcp` + +```python +list_tools_mcp(self) -> mcp.types.ListToolsResult +``` + +Send a tools/list request and return the complete MCP protocol result. + +**Returns:** +- mcp.types.ListToolsResult: The complete response object from the protocol, +containing the list of tools and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `list_tools` + +```python +list_tools(self) -> list[mcp.types.Tool] +``` + +Retrieve a list of tools available on the server. + +**Returns:** +- list\[mcp.types.Tool]: A list of Tool objects. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `call_tool_mcp` + +```python +call_tool_mcp(self, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.CallToolResult +``` + +Send a tools/call request and return the complete MCP protocol result. + +This method returns the raw CallToolResult object, which includes an isError flag +and other metadata. It does not raise an exception if the tool call results in an error. + +**Args:** +- `name`: The name of the tool to call. +- `arguments`: Arguments to pass to the tool. +- `timeout`: The timeout for the tool call. Defaults to None. +- `progress_handler`: The progress handler to use for the tool call. Defaults to None. + +**Returns:** +- mcp.types.CallToolResult: The complete response object from the protocol, +containing the tool result and any additional metadata. + +**Raises:** +- `RuntimeError`: If called while the client is not connected. + + +#### `call_tool` + +```python +call_tool(self, name: str, arguments: dict[str, Any] | None = None, timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, raise_on_error: bool = True) -> CallToolResult +``` + +Call a tool on the server. + +Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error. + +**Args:** +- `name`: The name of the tool to call. +- `arguments`: Arguments to pass to the tool. Defaults to None. +- `timeout`: The timeout for the tool call. Defaults to None. +- `progress_handler`: The progress handler to use for the tool call. Defaults to None. + +**Returns:** +- +The content returned by the tool. If the tool returns structured +outputs, they are returned as a dataclass (if an output schema +is available) or a dictionary; otherwise, a list of content +blocks is returned. Note: to receive both structured and +unstructured outputs, use call_tool_mcp instead and access the +raw result object. + +**Raises:** +- `ToolError`: If the tool call results in an error. +- `RuntimeError`: If called while the client is not connected. + + +### `CallToolResult` diff --git a/docs/python-sdk/fastmcp-client-elicitation.mdx b/docs/python-sdk/fastmcp-client-elicitation.mdx new file mode 100644 index 000000000..5e8957f32 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-elicitation.mdx @@ -0,0 +1,18 @@ +--- +title: elicitation +sidebarTitle: elicitation +--- + +# `fastmcp.client.elicitation` + +## Functions + +### `create_elicitation_callback` + +```python +create_elicitation_callback(elicitation_handler: ElicitationHandler) -> ElicitationFnT +``` + +## Classes + +### `ElicitResult` diff --git a/docs/python-sdk/fastmcp-client-logging.mdx b/docs/python-sdk/fastmcp-client-logging.mdx index 83da895c3..e4a36db09 100644 --- a/docs/python-sdk/fastmcp-client-logging.mdx +++ b/docs/python-sdk/fastmcp-client-logging.mdx @@ -7,7 +7,13 @@ sidebarTitle: logging ## Functions -### `create_log_callback` +### `default_log_handler` + +```python +default_log_handler(message: LogMessage) -> None +``` + +### `create_log_callback` ```python create_log_callback(handler: LogHandler | None = None) -> LoggingFnT diff --git a/docs/python-sdk/fastmcp-client-messages.mdx b/docs/python-sdk/fastmcp-client-messages.mdx new file mode 100644 index 000000000..ef1b87436 --- /dev/null +++ b/docs/python-sdk/fastmcp-client-messages.mdx @@ -0,0 +1,107 @@ +--- +title: messages +sidebarTitle: messages +--- + +# `fastmcp.client.messages` + +## Classes + +### `MessageHandler` + + +This class is used to handle MCP messages sent to the client. It is used to handle all messages, +requests, notifications, and exceptions. Users can override any of the hooks + + +**Methods:** + +#### `dispatch` + +```python +dispatch(self, message: Message) -> None +``` + +#### `on_message` + +```python +on_message(self, message: Message) -> None +``` + +#### `on_request` + +```python +on_request(self, message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]) -> None +``` + +#### `on_ping` + +```python +on_ping(self, message: mcp.types.PingRequest) -> None +``` + +#### `on_list_roots` + +```python +on_list_roots(self, message: mcp.types.ListRootsRequest) -> None +``` + +#### `on_create_message` + +```python +on_create_message(self, message: mcp.types.CreateMessageRequest) -> None +``` + +#### `on_notification` + +```python +on_notification(self, message: mcp.types.ServerNotification) -> None +``` + +#### `on_exception` + +```python +on_exception(self, message: Exception) -> None +``` + +#### `on_progress` + +```python +on_progress(self, message: mcp.types.ProgressNotification) -> None +``` + +#### `on_logging_message` + +```python +on_logging_message(self, message: mcp.types.LoggingMessageNotification) -> None +``` + +#### `on_tool_list_changed` + +```python +on_tool_list_changed(self, message: mcp.types.ToolListChangedNotification) -> None +``` + +#### `on_resource_list_changed` + +```python +on_resource_list_changed(self, message: mcp.types.ResourceListChangedNotification) -> None +``` + +#### `on_prompt_list_changed` + +```python +on_prompt_list_changed(self, message: mcp.types.PromptListChangedNotification) -> None +``` + +#### `on_resource_updated` + +```python +on_resource_updated(self, message: mcp.types.ResourceUpdatedNotification) -> None +``` + +#### `on_cancelled` + +```python +on_cancelled(self, message: mcp.types.CancelledNotification) -> None +``` diff --git a/docs/python-sdk/fastmcp-client-oauth_callback.mdx b/docs/python-sdk/fastmcp-client-oauth_callback.mdx index e251c5ac4..beddcd64f 100644 --- a/docs/python-sdk/fastmcp-client-oauth_callback.mdx +++ b/docs/python-sdk/fastmcp-client-oauth_callback.mdx @@ -15,7 +15,7 @@ and display styled responses to users. ## Functions -### `create_callback_html` +### `create_callback_html` ```python create_callback_html(message: str, is_success: bool = True, title: str = 'FastMCP OAuth', server_url: str | None = None) -> str @@ -25,7 +25,7 @@ create_callback_html(message: str, is_success: bool = True, title: str = 'FastMC Create a styled HTML response for OAuth callbacks. -### `create_oauth_callback_server` +### `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 @@ -46,17 +46,17 @@ Create an OAuth callback server. ## Classes -### `CallbackResponse` +### `CallbackResponse` **Methods:** -#### `from_dict` +#### `from_dict` ```python from_dict(cls, data: dict[str, str]) -> CallbackResponse ``` -#### `to_dict` +#### `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 index aecd0f37b..123839072 100644 --- a/docs/python-sdk/fastmcp-client-progress.mdx +++ b/docs/python-sdk/fastmcp-client-progress.mdx @@ -5,4 +5,21 @@ sidebarTitle: progress # `fastmcp.client.progress` -*This module is empty or contains only private/internal implementations.* +## Functions + +### `default_progress_handler` + +```python +default_progress_handler(progress: float, total: float | None, message: str | None) -> None +``` + + +Default handler for progress notifications. + +Logs progress updates at debug level, properly handling missing total or message values. + +**Args:** +- `progress`: Current progress value +- `total`: Optional total expected value +- `message`: Optional status message + diff --git a/docs/python-sdk/fastmcp-client-roots.mdx b/docs/python-sdk/fastmcp-client-roots.mdx index a081bc2fa..842c63d5a 100644 --- a/docs/python-sdk/fastmcp-client-roots.mdx +++ b/docs/python-sdk/fastmcp-client-roots.mdx @@ -7,13 +7,13 @@ sidebarTitle: roots ## Functions -### `convert_roots_list` +### `convert_roots_list` ```python convert_roots_list(roots: RootsList) -> list[mcp.types.Root] ``` -### `create_roots_callback` +### `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 index 53d3893de..ac90fdedd 100644 --- a/docs/python-sdk/fastmcp-client-sampling.mdx +++ b/docs/python-sdk/fastmcp-client-sampling.mdx @@ -7,7 +7,7 @@ sidebarTitle: sampling ## Functions -### `create_sampling_callback` +### `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 index adbab20ee..8e29e754f 100644 --- a/docs/python-sdk/fastmcp-client-transports.mdx +++ b/docs/python-sdk/fastmcp-client-transports.mdx @@ -7,7 +7,7 @@ sidebarTitle: transports ## Functions -### `infer_transport` +### `infer_transport` ```python infer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport @@ -57,13 +57,13 @@ transport = infer_transport(config) ## Classes -### `SessionKwargs` +### `SessionKwargs` Keyword arguments for the MCP ClientSession constructor. -### `ClientTransport` +### `ClientTransport` Abstract base class for different MCP client transport mechanisms. @@ -72,25 +72,79 @@ A Transport is responsible for establishing and managing connections to an MCP server, and providing a ClientSession within an async context. -### `WSTransport` +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` + +Establishes a connection and yields an active ClientSession. + +The ClientSession is *not* expected to be initialized in this context manager. + +The session is guaranteed to be valid only within the scope of the +async context manager. Connection setup and teardown are handled +within this context. + +**Args:** +- `**session_kwargs`: Keyword arguments to pass to the ClientSession + constructor (e.g., callbacks, timeouts). + + +#### `close` + +```python +close(self) +``` + +Close the transport. + + +### `WSTransport` Transport implementation that connects to an MCP server via WebSockets. -### `SSETransport` +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` + +### `SSETransport` Transport implementation that connects to an MCP server via Server-Sent Events. -### `StreamableHttpTransport` +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` + +### `StreamableHttpTransport` Transport implementation that connects to an MCP server via Streamable HTTP Requests. -### `StdioTransport` +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` + +### `StdioTransport` Base transport for connecting to an MCP server via subprocess with stdio. @@ -99,37 +153,63 @@ This is a base class that can be subclassed for specific command-based transports like Python, Node, Uvx, etc. -### `PythonStdioTransport` +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` + +#### `connect` + +```python +connect(self, **session_kwargs: Unpack[SessionKwargs]) -> ClientSession | None +``` + +#### `disconnect` + +```python +disconnect(self) +``` + +#### `close` + +```python +close(self) +``` + +### `PythonStdioTransport` Transport for running Python scripts. -### `FastMCPStdioTransport` +### `FastMCPStdioTransport` Transport for running FastMCP servers using the FastMCP CLI. -### `NodeStdioTransport` +### `NodeStdioTransport` Transport for running Node.js scripts. -### `UvxStdioTransport` +### `UvxStdioTransport` Transport for running commands via the uvx tool. -### `NpxStdioTransport` +### `NpxStdioTransport` Transport for running commands via the npx tool. -### `FastMCPTransport` +### `FastMCPTransport` In-memory transport for FastMCP servers. @@ -140,7 +220,15 @@ 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` +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` + +### `MCPConfigTransport` Transport for connecting to one or more MCP servers defined in an MCPConfig. @@ -190,3 +278,11 @@ async with client: icons = await client.read_resource("weather://weather/icons/sunny") ``` + +**Methods:** + +#### `connect_session` + +```python +connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] +``` diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx index 6b54286b5..26e62012a 100644 --- a/docs/python-sdk/fastmcp-exceptions.mdx +++ b/docs/python-sdk/fastmcp-exceptions.mdx @@ -10,55 +10,55 @@ Custom exceptions for FastMCP. ## Classes -### `FastMCPError` +### `FastMCPError` Base error for FastMCP. -### `ValidationError` +### `ValidationError` Error in validating parameters or return values. -### `ResourceError` +### `ResourceError` Error in resource operations. -### `ToolError` +### `ToolError` Error in tool operations. -### `PromptError` +### `PromptError` Error in prompt operations. -### `InvalidSignature` +### `InvalidSignature` Invalid signature for use with FastMCP. -### `ClientError` +### `ClientError` Error in client operations. -### `NotFoundError` +### `NotFoundError` Object not found. -### `DisabledError` +### `DisabledError` Object is disabled. diff --git a/docs/python-sdk/fastmcp-mcp_config.mdx b/docs/python-sdk/fastmcp-mcp_config.mdx new file mode 100644 index 000000000..3c6839dad --- /dev/null +++ b/docs/python-sdk/fastmcp-mcp_config.mdx @@ -0,0 +1,150 @@ +--- +title: mcp_config +sidebarTitle: mcp_config +--- + +# `fastmcp.mcp_config` + + +Canonical MCP Configuration Format. + +This module defines the standard configuration format for Model Context Protocol (MCP) servers. +It provides a client-agnostic, extensible format that can be used across all MCP implementations. + +The configuration format supports both stdio and remote (HTTP/SSE) transports, with comprehensive +field definitions for server metadata, authentication, and execution parameters. + +Example configuration: + { + "mcpServers": { + "my-server": { + "command": "npx", + "args": ["-y", "@my/mcp-server"], + "env": {"API_KEY": "secret"}, + "timeout": 30000, + "description": "My MCP server" + } + } + } + + +## Functions + +### `infer_transport_type_from_url` + +```python +infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse'] +``` + + +Infer the appropriate transport type from the given URL. + + +### `update_config_file` + +```python +update_config_file(file_path: Path, server_name: str, server_config: StdioMCPServer | RemoteMCPServer) -> None +``` + + +Update MCP configuration file with new server, preserving existing fields. + + +## Classes + +### `StdioMCPServer` + + +MCP server configuration for stdio transport. + +This is the canonical configuration format for MCP servers using stdio transport. + + +**Methods:** + +#### `to_transport` + +```python +to_transport(self) -> StdioTransport +``` + +### `RemoteMCPServer` + + +MCP server configuration for HTTP/SSE transport. + +This is the canonical configuration format for MCP servers using remote transports. + + +**Methods:** + +#### `to_transport` + +```python +to_transport(self) -> StreamableHttpTransport | SSETransport +``` + +### `MCPConfig` + + +Canonical MCP configuration format. + +This defines the standard configuration format for Model Context Protocol servers. +The format is designed to be client-agnostic and extensible for future use cases. + + +**Methods:** + +#### `from_dict` + +```python +from_dict(cls, config: dict[str, Any]) -> MCPConfig +``` + +Parse MCP configuration from dictionary format. + + +#### `to_dict` + +```python +to_dict(self) -> dict[str, Any] +``` + +Convert MCPConfig to dictionary format, preserving all fields. + + +#### `write_to_file` + +```python +write_to_file(self, file_path: Path) -> None +``` + +Write configuration to JSON file. + + +#### `from_file` + +```python +from_file(cls, file_path: Path) -> MCPConfig +``` + +Load configuration from JSON file. + + +#### `add_server` + +```python +add_server(self, name: str, server: StdioMCPServer | RemoteMCPServer) -> None +``` + +Add or update a server in the configuration. + + +#### `remove_server` + +```python +remove_server(self, name: str) -> None +``` + +Remove a server from the configuration. + diff --git a/docs/python-sdk/fastmcp-prompts-prompt.mdx b/docs/python-sdk/fastmcp-prompts-prompt.mdx index 726962933..13019b9f3 100644 --- a/docs/python-sdk/fastmcp-prompts-prompt.mdx +++ b/docs/python-sdk/fastmcp-prompts-prompt.mdx @@ -10,10 +10,10 @@ Base classes for FastMCP prompts. ## Functions -### `Message` +### `Message` ```python -Message(content: str | MCPContent, role: Role | None = None, **kwargs: Any) -> PromptMessage +Message(content: str | ContentBlock, role: Role | None = None, **kwargs: Any) -> PromptMessage ``` @@ -22,13 +22,13 @@ A user-friendly constructor for PromptMessage. ## Classes -### `PromptArgument` +### `PromptArgument` An argument that can be passed to a prompt. -### `Prompt` +### `Prompt` A prompt template that can be rendered with parameters. @@ -36,7 +36,19 @@ A prompt template that can be rendered with parameters. **Methods:** -#### `to_mcp_prompt` +#### `enable` + +```python +enable(self) -> None +``` + +#### `disable` + +```python +disable(self) -> None +``` + +#### `to_mcp_prompt` ```python to_mcp_prompt(self, **overrides: Any) -> MCPPrompt @@ -45,10 +57,10 @@ to_mcp_prompt(self, **overrides: Any) -> MCPPrompt Convert the prompt to an MCP prompt. -#### `from_function` +#### `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 +from_function(fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionPrompt ``` Create a Prompt from a function. @@ -60,7 +72,16 @@ The function can return: - A sequence of any of the above -### `FunctionPrompt` +#### `render` + +```python +render(self, arguments: dict[str, Any] | None = None) -> list[PromptMessage] +``` + +Render the prompt with arguments. + + +### `FunctionPrompt` A prompt that is a function. @@ -68,10 +89,10 @@ A prompt that is a function. **Methods:** -#### `from_function` +#### `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 +from_function(cls, fn: Callable[..., PromptResult | Awaitable[PromptResult]], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, enabled: bool | None = None) -> FunctionPrompt ``` Create a Prompt from a function. @@ -82,3 +103,12 @@ The function can return: - A dict (converted to a message) - A sequence of any of the above + +#### `render` + +```python +render(self, arguments: dict[str, Any] | None = None) -> list[PromptMessage] +``` + +Render the prompt with arguments. + diff --git a/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx index 2ba84f742..a0c1f88cf 100644 --- a/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx +++ b/docs/python-sdk/fastmcp-prompts-prompt_manager.mdx @@ -7,7 +7,7 @@ sidebarTitle: prompt_manager ## Classes -### `PromptManager` +### `PromptManager` Manages FastMCP prompts. @@ -15,7 +15,7 @@ Manages FastMCP prompts. **Methods:** -#### `mount` +#### `mount` ```python mount(self, server: MountedServer) -> None @@ -24,7 +24,43 @@ mount(self, server: MountedServer) -> None Adds a mounted server as a source for prompts. -#### `add_prompt_from_fn` +#### `has_prompt` + +```python +has_prompt(self, key: str) -> bool +``` + +Check if a prompt exists. + + +#### `get_prompt` + +```python +get_prompt(self, key: str) -> Prompt +``` + +Get prompt by key. + + +#### `get_prompts` + +```python +get_prompts(self) -> dict[str, Prompt] +``` + +Gets the complete, unfiltered inventory of all prompts. + + +#### `list_prompts` + +```python +list_prompts(self) -> list[Prompt] +``` + +Lists all prompts, applying protocol filtering. + + +#### `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 @@ -33,7 +69,7 @@ add_prompt_from_fn(self, fn: Callable[..., PromptResult | Awaitable[PromptResult Create a prompt from a function. -#### `add_prompt` +#### `add_prompt` ```python add_prompt(self, prompt: Prompt) -> Prompt @@ -41,3 +77,13 @@ add_prompt(self, prompt: Prompt) -> Prompt Add a prompt to the manager. + +#### `render_prompt` + +```python +render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult +``` + +Internal API for servers: Finds and renders a prompt, respecting the +filtered protocol path. + diff --git a/docs/python-sdk/fastmcp-resources-resource.mdx b/docs/python-sdk/fastmcp-resources-resource.mdx index ac6c40139..46969c09c 100644 --- a/docs/python-sdk/fastmcp-resources-resource.mdx +++ b/docs/python-sdk/fastmcp-resources-resource.mdx @@ -10,7 +10,7 @@ Base classes and interfaces for FastMCP resources. ## Classes -### `Resource` +### `Resource` Base class for all resources. @@ -18,13 +18,25 @@ Base class for all resources. **Methods:** -#### `from_function` +#### `enable` ```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 +enable(self) -> None ``` -#### `set_default_mime_type` +#### `disable` + +```python +disable(self) -> None +``` + +#### `from_function` + +```python +from_function(fn: Callable[..., Any], uri: str | AnyUrl, name: str | None = None, title: 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 @@ -33,7 +45,7 @@ set_default_mime_type(cls, mime_type: str | None) -> str Set default MIME type if not provided. -#### `set_default_name` +#### `set_default_name` ```python set_default_name(self) -> Self @@ -42,7 +54,16 @@ set_default_name(self) -> Self Set default name from URI if not provided. -#### `to_mcp_resource` +#### `read` + +```python +read(self) -> str | bytes +``` + +Read the resource content. + + +#### `to_mcp_resource` ```python to_mcp_resource(self, **overrides: Any) -> MCPResource @@ -51,7 +72,7 @@ to_mcp_resource(self, **overrides: Any) -> MCPResource Convert the resource to an MCPResource. -#### `key` +#### `key` ```python key(self) -> str @@ -63,7 +84,7 @@ keys having a certain value, as the same tool loaded from different hierarchies of servers may have different keys. -### `FunctionResource` +### `FunctionResource` A resource that defers data loading by wrapping a function. @@ -80,11 +101,20 @@ The function can return: **Methods:** -#### `from_function` +#### `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 +from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl, name: str | None = None, title: 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. + +#### `read` + +```python +read(self) -> str | bytes +``` + +Read the resource by calling the wrapped function. + diff --git a/docs/python-sdk/fastmcp-resources-resource_manager.mdx b/docs/python-sdk/fastmcp-resources-resource_manager.mdx index f3b4fa65f..52a8c766f 100644 --- a/docs/python-sdk/fastmcp-resources-resource_manager.mdx +++ b/docs/python-sdk/fastmcp-resources-resource_manager.mdx @@ -10,7 +10,7 @@ Resource manager functionality. ## Classes -### `ResourceManager` +### `ResourceManager` Manages FastMCP resources. @@ -18,7 +18,7 @@ Manages FastMCP resources. **Methods:** -#### `mount` +#### `mount` ```python mount(self, server: MountedServer) -> None @@ -27,7 +27,43 @@ mount(self, server: MountedServer) -> None Adds a mounted server as a source for resources and templates. -#### `add_resource_or_template_from_fn` +#### `get_resources` + +```python +get_resources(self) -> dict[str, Resource] +``` + +Get all registered resources, keyed by URI. + + +#### `get_resource_templates` + +```python +get_resource_templates(self) -> dict[str, ResourceTemplate] +``` + +Get all registered templates, keyed by URI template. + + +#### `list_resources` + +```python +list_resources(self) -> list[Resource] +``` + +Lists all resources, applying protocol filtering. + + +#### `list_resource_templates` + +```python +list_resource_templates(self) -> list[ResourceTemplate] +``` + +Lists all templates, applying protocol filtering. + + +#### `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 @@ -48,7 +84,7 @@ Add a resource or template to the manager from a function. - returns the existing resource or template. -#### `add_resource_from_fn` +#### `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 @@ -69,7 +105,7 @@ Add a resource to the manager from a function. - returns the existing resource. -#### `add_resource` +#### `add_resource` ```python add_resource(self, resource: Resource) -> Resource @@ -83,7 +119,7 @@ will be used as the storage key. To overwrite it, call Resource.with_key() before calling this method. -#### `add_template_from_fn` +#### `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 @@ -92,7 +128,7 @@ add_template_from_fn(self, fn: Callable[..., Any], uri_template: str, name: str Create a template from a function. -#### `add_template` +#### `add_template` ```python add_template(self, template: ResourceTemplate) -> ResourceTemplate @@ -109,3 +145,37 @@ ResourceTemplate.with_key() before calling this method. - The added template. If a template with the same URI already exists, - returns the existing template. + +#### `has_resource` + +```python +has_resource(self, uri: AnyUrl | str) -> bool +``` + +Check if a resource exists. + + +#### `get_resource` + +```python +get_resource(self, uri: AnyUrl | str) -> Resource +``` + +Get resource by URI, checking concrete resources first, then templates. + +**Args:** +- `uri`: The URI of the resource to get + +**Raises:** +- `NotFoundError`: If no resource or template matching the URI is found. + + +#### `read_resource` + +```python +read_resource(self, uri: AnyUrl | str) -> str | bytes +``` + +Internal API for servers: Finds and reads a resource, respecting the +filtered protocol path. + diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx index 99f8218e1..014d0b121 100644 --- a/docs/python-sdk/fastmcp-resources-template.mdx +++ b/docs/python-sdk/fastmcp-resources-template.mdx @@ -10,13 +10,13 @@ Resource template functionality. ## Functions -### `build_regex` +### `build_regex` ```python build_regex(template: str) -> re.Pattern ``` -### `match_uri_template` +### `match_uri_template` ```python match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None @@ -24,7 +24,7 @@ match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None ## Classes -### `ResourceTemplate` +### `ResourceTemplate` A template for dynamically creating resources. @@ -32,13 +32,25 @@ A template for dynamically creating resources. **Methods:** -#### `from_function` +#### `enable` ```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 +enable(self) -> None ``` -#### `set_default_mime_type` +#### `disable` + +```python +disable(self) -> None +``` + +#### `from_function` + +```python +from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, title: 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 @@ -47,7 +59,7 @@ set_default_mime_type(cls, mime_type: str | None) -> str Set default MIME type if not provided. -#### `matches` +#### `matches` ```python matches(self, uri: str) -> dict[str, Any] | None @@ -56,7 +68,25 @@ matches(self, uri: str) -> dict[str, Any] | None Check if URI matches template and extract parameters. -#### `to_mcp_template` +#### `read` + +```python +read(self, arguments: dict[str, Any]) -> str | bytes +``` + +Read the resource content. + + +#### `create_resource` + +```python +create_resource(self, uri: str, params: dict[str, Any]) -> Resource +``` + +Create a resource from the template with the given parameters. + + +#### `to_mcp_template` ```python to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate @@ -65,7 +95,7 @@ to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate Convert the resource template to an MCPResourceTemplate. -#### `from_mcp_template` +#### `from_mcp_template` ```python from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate @@ -74,7 +104,7 @@ from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object. -#### `key` +#### `key` ```python key(self) -> str @@ -86,7 +116,7 @@ keys having a certain value, as the same tool loaded from different hierarchies of servers may have different keys. -### `FunctionResourceTemplate` +### `FunctionResourceTemplate` A template for dynamically creating resources. @@ -94,10 +124,19 @@ A template for dynamically creating resources. **Methods:** -#### `from_function` +#### `read` ```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 +read(self, arguments: dict[str, Any]) -> str | bytes +``` + +Read the resource content. + + +#### `from_function` + +```python +from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, title: 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 index 7fa595b8e..cc71f5c6b 100644 --- a/docs/python-sdk/fastmcp-resources-types.mdx +++ b/docs/python-sdk/fastmcp-resources-types.mdx @@ -10,19 +10,41 @@ Concrete resource implementations. ## Classes -### `TextResource` +### `TextResource` A resource that reads from a string. -### `BinaryResource` +**Methods:** + +#### `read` + +```python +read(self) -> str +``` + +Read the text content. + + +### `BinaryResource` A resource that reads from bytes. -### `FileResource` +**Methods:** + +#### `read` + +```python +read(self) -> bytes +``` + +Read the binary content. + + +### `FileResource` A resource that reads from a file. @@ -32,7 +54,7 @@ Set is_binary=True to read file as binary data instead of text. **Methods:** -#### `validate_absolute_path` +#### `validate_absolute_path` ```python validate_absolute_path(cls, path: Path) -> Path @@ -41,7 +63,7 @@ validate_absolute_path(cls, path: Path) -> Path Ensure path is absolute. -#### `set_binary_from_mime_type` +#### `set_binary_from_mime_type` ```python set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool @@ -50,13 +72,33 @@ set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool Set is_binary based on mime_type if not explicitly set. -### `HttpResource` +#### `read` + +```python +read(self) -> str | bytes +``` + +Read the file content. + + +### `HttpResource` A resource that reads from an HTTP endpoint. -### `DirectoryResource` +**Methods:** + +#### `read` + +```python +read(self) -> str | bytes +``` + +Read the HTTP content. + + +### `DirectoryResource` A resource that lists files in a directory. @@ -64,7 +106,7 @@ A resource that lists files in a directory. **Methods:** -#### `validate_absolute_path` +#### `validate_absolute_path` ```python validate_absolute_path(cls, path: Path) -> Path @@ -73,7 +115,7 @@ validate_absolute_path(cls, path: Path) -> Path Ensure path is absolute. -#### `list_files` +#### `list_files` ```python list_files(self) -> list[Path] @@ -81,3 +123,12 @@ list_files(self) -> list[Path] List files in the directory. + +#### `read` + +```python +read(self) -> str +``` + +Read the directory listing. + diff --git a/docs/python-sdk/fastmcp-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx index 5fd5cce45..d7e2e4960 100644 --- a/docs/python-sdk/fastmcp-server-auth-auth.mdx +++ b/docs/python-sdk/fastmcp-server-auth-auth.mdx @@ -7,4 +7,24 @@ sidebarTitle: auth ## Classes -### `OAuthProvider` +### `OAuthProvider` + +**Methods:** + +#### `verify_token` + +```python +verify_token(self, token: str) -> AccessToken | None +``` + +Verify a bearer token and return access info if valid. + +This method implements the TokenVerifier protocol by delegating +to our existing load_access_token method. + +**Args:** +- `token`: The token string to validate + +**Returns:** +- AccessToken object if valid, None if invalid or expired + diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx index f6a6285be..f68341f8d 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx @@ -7,23 +7,23 @@ sidebarTitle: bearer ## Classes -### `JWKData` +### `JWKData` JSON Web Key data structure. -### `JWKSData` +### `JWKSData` JSON Web Key Set data structure. -### `RSAKeyPair` +### `RSAKeyPair` **Methods:** -#### `generate` +#### `generate` ```python generate(cls) -> 'RSAKeyPair' @@ -35,7 +35,7 @@ Generate an RSA key pair for testing. - (private_key_pem, public_key_pem) -#### `create_token` +#### `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 @@ -57,13 +57,96 @@ Generate a test JWT token for testing purposes. - Signed JWT token string -### `BearerAuthProvider` +### `BearerAuthProvider` Simple JWT Bearer Token validator for hosted MCP servers. -Uses RS256 asymmetric encryption. Supports either static public key +Uses RS256 asymmetric encryption by default but supports all JWA algorithms. 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. + +**Methods:** + +#### `load_access_token` + +```python +load_access_token(self, token: str) -> AccessToken | None +``` + +Validates the provided JWT bearer token. + +**Args:** +- `token`: The JWT token string to validate + +**Returns:** +- AccessToken object if valid, None if invalid or expired + + +#### `verify_token` + +```python +verify_token(self, token: str) -> AccessToken | None +``` + +Verify a bearer token and return access info if valid. + +This method implements the TokenVerifier protocol by delegating +to our existing load_access_token method. + +**Args:** +- `token`: The JWT token string to validate + +**Returns:** +- AccessToken object if valid, None if invalid or expired + + +#### `get_client` + +```python +get_client(self, client_id: str) -> OAuthClientInformationFull | None +``` + +#### `register_client` + +```python +register_client(self, client_info: OAuthClientInformationFull) -> None +``` + +#### `authorize` + +```python +authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str +``` + +#### `load_authorization_code` + +```python +load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None +``` + +#### `exchange_authorization_code` + +```python +exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken +``` + +#### `load_refresh_token` + +```python +load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None +``` + +#### `exchange_refresh_token` + +```python +exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken +``` + +#### `revoke_token` + +```python +revoke_token(self, token: AccessToken | RefreshToken) -> None +``` diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx index e1984efb6..10bc8637b 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-bearer_env.mdx @@ -7,13 +7,13 @@ sidebarTitle: bearer_env ## Classes -### `EnvBearerAuthProviderSettings` +### `EnvBearerAuthProviderSettings` Settings for the BearerAuthProvider. -### `EnvBearerAuthProvider` +### `EnvBearerAuthProvider` A BearerAuthProvider that loads settings from environment variables. Any diff --git a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx index c11f3b87e..cb97653c8 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx @@ -7,9 +7,90 @@ sidebarTitle: in_memory ## Classes -### `InMemoryOAuthProvider` +### `InMemoryOAuthProvider` An in-memory OAuth provider for testing purposes. It simulates the OAuth 2.1 flow locally without external calls. + +**Methods:** + +#### `get_client` + +```python +get_client(self, client_id: str) -> OAuthClientInformationFull | None +``` + +#### `register_client` + +```python +register_client(self, client_info: OAuthClientInformationFull) -> None +``` + +#### `authorize` + +```python +authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str +``` + +Simulates user authorization and generates an authorization code. +Returns a redirect URI with the code and state. + + +#### `load_authorization_code` + +```python +load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None +``` + +#### `exchange_authorization_code` + +```python +exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken +``` + +#### `load_refresh_token` + +```python +load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None +``` + +#### `exchange_refresh_token` + +```python +exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken +``` + +#### `load_access_token` + +```python +load_access_token(self, token: str) -> AccessToken | None +``` + +#### `verify_token` + +```python +verify_token(self, token: str) -> AccessToken | None +``` + +Verify a bearer token and return access info if valid. + +This method implements the TokenVerifier protocol by delegating +to our existing load_access_token method. + +**Args:** +- `token`: The token string to validate + +**Returns:** +- AccessToken object if valid, None if invalid or expired + + +#### `revoke_token` + +```python +revoke_token(self, token: AccessToken | RefreshToken) -> None +``` + +Revokes an access or refresh token and its counterpart. + diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx index 4cc497740..b41e84aa0 100644 --- a/docs/python-sdk/fastmcp-server-context.mdx +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -7,7 +7,7 @@ sidebarTitle: context ## Functions -### `set_context` +### `set_context` ```python set_context(context: Context) -> Generator[Context, None, None] @@ -15,7 +15,7 @@ set_context(context: Context) -> Generator[Context, None, None] ## Classes -### `Context` +### `Context` Context object providing access to MCP capabilities. @@ -53,7 +53,7 @@ The context is optional - tools that don't need it can omit the parameter. **Methods:** -#### `request_context` +#### `request_context` ```python request_context(self) -> RequestContext @@ -64,7 +64,50 @@ Access to the underlying request context. If called outside of a request context, this will raise a ValueError. -#### `client_id` +#### `report_progress` + +```python +report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None +``` + +Report progress for the current operation. + +**Args:** +- `progress`: Current progress value e.g. 24 +- `total`: Optional total value e.g. 100 + + +#### `read_resource` + +```python +read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents] +``` + +Read a resource by URI. + +**Args:** +- `uri`: Resource URI to read + +**Returns:** +- The resource content as either text or bytes + + +#### `log` + +```python +log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None) -> None +``` + +Send a log message to the client. + +**Args:** +- `message`: Log message +- `level`: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical", +"alert", or "emergency". Default is "info". +- `logger_name`: Optional logger name + + +#### `client_id` ```python client_id(self) -> str | None @@ -73,7 +116,7 @@ client_id(self) -> str | None Get the client ID if available. -#### `request_id` +#### `request_id` ```python request_id(self) -> str @@ -82,7 +125,7 @@ request_id(self) -> str Get the unique ID for this request. -#### `session_id` +#### `session_id` ```python session_id(self) -> str | None @@ -99,16 +142,148 @@ the same client session. - for stdio and in-memory transports which don't use session IDs. -#### `session` +#### `session` ```python -session(self) +session(self) -> ServerSession ``` Access to the underlying session for advanced usage. -#### `get_http_request` +#### `debug` + +```python +debug(self, message: str, logger_name: str | None = None) -> None +``` + +Send a debug log message. + + +#### `info` + +```python +info(self, message: str, logger_name: str | None = None) -> None +``` + +Send an info log message. + + +#### `warning` + +```python +warning(self, message: str, logger_name: str | None = None) -> None +``` + +Send a warning log message. + + +#### `error` + +```python +error(self, message: str, logger_name: str | None = None) -> None +``` + +Send an error log message. + + +#### `list_roots` + +```python +list_roots(self) -> list[Root] +``` + +List the roots available to the server, as indicated by the client. + + +#### `send_tool_list_changed` + +```python +send_tool_list_changed(self) -> None +``` + +Send a tool list changed notification to the client. + + +#### `send_resource_list_changed` + +```python +send_resource_list_changed(self) -> None +``` + +Send a resource list changed notification to the client. + + +#### `send_prompt_list_changed` + +```python +send_prompt_list_changed(self) -> None +``` + +Send a prompt list changed notification to the client. + + +#### `sample` + +```python +sample(self, messages: str | list[str | SamplingMessage], system_prompt: str | None = None, include_context: IncludeContext | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> ContentBlock +``` + +Send a sampling request to the client and await the response. + +Call this method at any time to have the server request an LLM +completion from the client. The client must be appropriately configured, +or the request will error. + + +#### `elicit` + +```python +elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation +``` + +#### `elicit` + +```python +elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation +``` + +#### `elicit` + +```python +elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation +``` + +#### `elicit` + +```python +elicit(self, message: str, response_type: type[T] | list[str] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation +``` + +Send an elicitation request to the client and await the response. + +Call this method at any time to request additional information from +the user through the client. The client must support elicitation, +or the request will error. + +Note that the MCP protocol only supports simple object schemas with +primitive types. You can provide a dataclass, TypedDict, or BaseModel to +comply. If you provide a primitive type, an object schema with a single +"value" field will be generated for the MCP interaction and +automatically deconstructed into the primitive type upon response. + +If the response_type is None, the generated schema will be that of an +empty object in order to comply with the MCP protocol requirements. +Clients must send an empty object ("{}")in response. + +**Args:** +- `message`: A human-readable message explaining what information is needed +- `response_type`: The type of the response, which should be a primitive +type or dataclass or BaseModel. If it is a primitive type, an +object schema with a single "value" field will be generated. + + +#### `get_http_request` ```python get_http_request(self) -> Request diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx index dce54051b..7fd03e574 100644 --- a/docs/python-sdk/fastmcp-server-dependencies.mdx +++ b/docs/python-sdk/fastmcp-server-dependencies.mdx @@ -7,19 +7,19 @@ sidebarTitle: dependencies ## Functions -### `get_context` +### `get_context` ```python get_context() -> Context ``` -### `get_http_request` +### `get_http_request` ```python get_http_request() -> Request ``` -### `get_http_headers` +### `get_http_headers` ```python get_http_headers(include_all: bool = False) -> dict[str, str] diff --git a/docs/python-sdk/fastmcp-server-elicitation.mdx b/docs/python-sdk/fastmcp-server-elicitation.mdx new file mode 100644 index 000000000..f3878efd7 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-elicitation.mdx @@ -0,0 +1,54 @@ +--- +title: elicitation +sidebarTitle: elicitation +--- + +# `fastmcp.server.elicitation` + +## Functions + +### `get_elicitation_schema` + +```python +get_elicitation_schema(response_type: type[T]) -> dict[str, Any] +``` + + +Get the schema for an elicitation response. + +**Args:** +- `response_type`: The type of the response + + +### `validate_elicitation_json_schema` + +```python +validate_elicitation_json_schema(schema: dict[str, Any]) -> None +``` + + +Validate that a JSON schema follows MCP elicitation requirements. + +This ensures the schema is compatible with MCP elicitation requirements: +- Must be an object schema +- Must only contain primitive field types (string, number, integer, boolean) +- Must be flat (no nested objects or arrays of objects) +- Allows const fields (for Literal types) and enum fields (for Enum types) +- Only primitive types and their nullable variants are allowed + +**Args:** +- `schema`: The JSON schema to validate + +**Raises:** +- `TypeError`: If the schema doesn't meet MCP elicitation requirements + + +## Classes + +### `AcceptedElicitation` + + +Result when user accepts the elicitation. + + +### `ScalarElicitationType` diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx index 75afb765f..48902385d 100644 --- a/docs/python-sdk/fastmcp-server-http.mdx +++ b/docs/python-sdk/fastmcp-server-http.mdx @@ -7,13 +7,13 @@ sidebarTitle: http ## Functions -### `set_http_request` +### `set_http_request` ```python set_http_request(request: Request) -> Generator[Request, None, None] ``` -### `setup_auth_middleware_and_routes` +### `setup_auth_middleware_and_routes` ```python setup_auth_middleware_and_routes(auth: OAuthProvider) -> tuple[list[Middleware], list[BaseRoute], list[str]] @@ -29,7 +29,7 @@ Set up authentication middleware and routes if auth is enabled. - Tuple of (middleware, auth_routes, required_scopes) -### `create_base_app` +### `create_base_app` ```python create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan @@ -48,7 +48,7 @@ Create a base Starlette app with common middleware and routes. - A Starlette application -### `create_sse_app` +### `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 @@ -70,7 +70,7 @@ Returns: A Starlette application with RequestContextMiddleware -### `create_streamable_http_app` +### `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 @@ -96,17 +96,17 @@ Return an instance of the StreamableHTTP server app. ## Classes -### `StarletteWithLifespan` +### `StarletteWithLifespan` **Methods:** -#### `lifespan` +#### `lifespan` ```python lifespan(self) -> Lifespan ``` -### `RequestContextMiddleware` +### `RequestContextMiddleware` Middleware that stores each request in a ContextVar diff --git a/docs/python-sdk/fastmcp-server-low_level.mdx b/docs/python-sdk/fastmcp-server-low_level.mdx new file mode 100644 index 000000000..2f7197358 --- /dev/null +++ b/docs/python-sdk/fastmcp-server-low_level.mdx @@ -0,0 +1,18 @@ +--- +title: low_level +sidebarTitle: low_level +--- + +# `fastmcp.server.low_level` + +## Classes + +### `LowLevelServer` + +**Methods:** + +#### `create_initialization_options` + +```python +create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, **kwargs: Any) -> InitializationOptions +``` diff --git a/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx b/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx index 735b3c3e5..842c31ce0 100644 --- a/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx +++ b/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx @@ -10,7 +10,7 @@ Error handling middleware for consistent error responses and tracking. ## Classes -### `ErrorHandlingMiddleware` +### `ErrorHandlingMiddleware` Middleware that provides consistent error handling and logging. @@ -21,7 +21,16 @@ proper MCP error responses. Also tracks error patterns for monitoring. **Methods:** -#### `get_error_stats` +#### `on_message` + +```python +on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Handle errors for all messages. + + +#### `get_error_stats` ```python get_error_stats(self) -> dict[str, int] @@ -30,7 +39,7 @@ get_error_stats(self) -> dict[str, int] Get error statistics for monitoring. -### `RetryMiddleware` +### `RetryMiddleware` Middleware that implements automatic retry logic for failed requests. @@ -38,3 +47,14 @@ Middleware that implements automatic retry logic for failed requests. Retries requests that fail with transient errors, using exponential backoff to avoid overwhelming the server or external dependencies. + +**Methods:** + +#### `on_request` + +```python +on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Implement retry logic for requests. + diff --git a/docs/python-sdk/fastmcp-server-middleware-logging.mdx b/docs/python-sdk/fastmcp-server-middleware-logging.mdx index c45e3096a..9beb8300a 100644 --- a/docs/python-sdk/fastmcp-server-middleware-logging.mdx +++ b/docs/python-sdk/fastmcp-server-middleware-logging.mdx @@ -10,7 +10,7 @@ Comprehensive logging middleware for FastMCP servers. ## Classes -### `LoggingMiddleware` +### `LoggingMiddleware` Middleware that provides comprehensive request and response logging. @@ -19,7 +19,18 @@ Logs all MCP messages with configurable detail levels. Useful for debugging, monitoring, and understanding server usage patterns. -### `StructuredLoggingMiddleware` +**Methods:** + +#### `on_message` + +```python +on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Log all messages. + + +### `StructuredLoggingMiddleware` Middleware that provides structured JSON logging for better log analysis. @@ -27,3 +38,14 @@ Middleware that provides structured JSON logging for better log analysis. Outputs structured logs that are easier to parse and analyze with log aggregation tools like ELK stack, Splunk, or cloud logging services. + +**Methods:** + +#### `on_message` + +```python +on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Log structured message information. + diff --git a/docs/python-sdk/fastmcp-server-middleware-middleware.mdx b/docs/python-sdk/fastmcp-server-middleware-middleware.mdx index 179864e5d..f9cf66a64 100644 --- a/docs/python-sdk/fastmcp-server-middleware-middleware.mdx +++ b/docs/python-sdk/fastmcp-server-middleware-middleware.mdx @@ -7,7 +7,7 @@ sidebarTitle: middleware ## Functions -### `make_middleware_wrapper` +### `make_middleware_wrapper` ```python make_middleware_wrapper(middleware: Middleware, call_next: CallNext[T, R]) -> CallNext[T, R] @@ -21,21 +21,11 @@ passed to other functions that expect a call_next function. ## Classes -### `CallNext` +### `CallNext` -### `CallToolResult` +### `ServerResultProtocol` -### `ListToolsResult` - -### `ListResourcesResult` - -### `ListResourceTemplatesResult` - -### `ListPromptsResult` - -### `ServerResultProtocol` - -### `MiddlewareContext` +### `MiddlewareContext` Unified context for all middleware operations. @@ -43,14 +33,76 @@ Unified context for all middleware operations. **Methods:** -#### `copy` +#### `copy` ```python copy(self, **kwargs: Any) -> MiddlewareContext[T] ``` -### `Middleware` +### `Middleware` Base class for FastMCP middleware with dispatching hooks. + +**Methods:** + +#### `on_message` + +```python +on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any +``` + +#### `on_request` + +```python +on_request(self, context: MiddlewareContext[mt.Request], call_next: CallNext[mt.Request, Any]) -> Any +``` + +#### `on_notification` + +```python +on_notification(self, context: MiddlewareContext[mt.Notification], call_next: CallNext[mt.Notification, Any]) -> Any +``` + +#### `on_call_tool` + +```python +on_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, mt.CallToolResult]) -> mt.CallToolResult +``` + +#### `on_read_resource` + +```python +on_read_resource(self, context: MiddlewareContext[mt.ReadResourceRequestParams], call_next: CallNext[mt.ReadResourceRequestParams, mt.ReadResourceResult]) -> mt.ReadResourceResult +``` + +#### `on_get_prompt` + +```python +on_get_prompt(self, context: MiddlewareContext[mt.GetPromptRequestParams], call_next: CallNext[mt.GetPromptRequestParams, mt.GetPromptResult]) -> mt.GetPromptResult +``` + +#### `on_list_tools` + +```python +on_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, list[Tool]]) -> list[Tool] +``` + +#### `on_list_resources` + +```python +on_list_resources(self, context: MiddlewareContext[mt.ListResourcesRequest], call_next: CallNext[mt.ListResourcesRequest, list[Resource]]) -> list[Resource] +``` + +#### `on_list_resource_templates` + +```python +on_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, list[ResourceTemplate]]) -> list[ResourceTemplate] +``` + +#### `on_list_prompts` + +```python +on_list_prompts(self, context: MiddlewareContext[mt.ListPromptsRequest], call_next: CallNext[mt.ListPromptsRequest, list[Prompt]]) -> list[Prompt] +``` diff --git a/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx b/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx index a983ce3f4..1c6da0c11 100644 --- a/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx +++ b/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx @@ -10,25 +10,53 @@ Rate limiting middleware for protecting FastMCP servers from abuse. ## Classes -### `RateLimitError` +### `RateLimitError` Error raised when rate limit is exceeded. -### `TokenBucketRateLimiter` +### `TokenBucketRateLimiter` Token bucket implementation for rate limiting. -### `SlidingWindowRateLimiter` +**Methods:** + +#### `consume` + +```python +consume(self, tokens: int = 1) -> bool +``` + +Try to consume tokens from the bucket. + +**Args:** +- `tokens`: Number of tokens to consume + +**Returns:** +- True if tokens were available and consumed, False otherwise + + +### `SlidingWindowRateLimiter` Sliding window rate limiter implementation. -### `RateLimitingMiddleware` +**Methods:** + +#### `is_allowed` + +```python +is_allowed(self) -> bool +``` + +Check if a request is allowed. + + +### `RateLimitingMiddleware` Middleware that implements rate limiting to prevent server abuse. @@ -37,7 +65,18 @@ Uses a token bucket algorithm by default, allowing for burst traffic while maintaining a sustainable long-term rate. -### `SlidingWindowRateLimitingMiddleware` +**Methods:** + +#### `on_request` + +```python +on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Apply rate limiting to requests. + + +### `SlidingWindowRateLimitingMiddleware` Middleware that implements sliding window rate limiting. @@ -45,3 +84,14 @@ Middleware that implements sliding window rate limiting. Uses a sliding window approach which provides more precise rate limiting but uses more memory to track individual request timestamps. + +**Methods:** + +#### `on_request` + +```python +on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Apply sliding window rate limiting to requests. + diff --git a/docs/python-sdk/fastmcp-server-middleware-timing.mdx b/docs/python-sdk/fastmcp-server-middleware-timing.mdx index c2805a3f7..dafcc1586 100644 --- a/docs/python-sdk/fastmcp-server-middleware-timing.mdx +++ b/docs/python-sdk/fastmcp-server-middleware-timing.mdx @@ -10,7 +10,7 @@ Timing middleware for measuring and logging request performance. ## Classes -### `TimingMiddleware` +### `TimingMiddleware` Middleware that logs the execution time of requests. @@ -19,7 +19,18 @@ Only measures and logs timing for request messages (not notifications). Provides insights into performance characteristics of your MCP server. -### `DetailedTimingMiddleware` +**Methods:** + +#### `on_request` + +```python +on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time request execution and log the results. + + +### `DetailedTimingMiddleware` Enhanced timing middleware with per-operation breakdowns. @@ -27,3 +38,68 @@ Enhanced timing middleware with per-operation breakdowns. Provides detailed timing information for different types of MCP operations, allowing you to identify performance bottlenecks in specific operations. + +**Methods:** + +#### `on_call_tool` + +```python +on_call_tool(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time tool execution. + + +#### `on_read_resource` + +```python +on_read_resource(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time resource reading. + + +#### `on_get_prompt` + +```python +on_get_prompt(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time prompt retrieval. + + +#### `on_list_tools` + +```python +on_list_tools(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time tool listing. + + +#### `on_list_resources` + +```python +on_list_resources(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time resource listing. + + +#### `on_list_resource_templates` + +```python +on_list_resource_templates(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time resource template listing. + + +#### `on_list_prompts` + +```python +on_list_prompts(self, context: MiddlewareContext, call_next: CallNext) -> Any +``` + +Time prompt listing. + diff --git a/docs/python-sdk/fastmcp-server-openapi.mdx b/docs/python-sdk/fastmcp-server-openapi.mdx index e57a6fd18..bc1a1f730 100644 --- a/docs/python-sdk/fastmcp-server-openapi.mdx +++ b/docs/python-sdk/fastmcp-server-openapi.mdx @@ -10,13 +10,13 @@ FastMCP server implementation for OpenAPI integration. ## Classes -### `MCPType` +### `MCPType` Type of FastMCP component to create from a route. -### `RouteType` +### `RouteType` Deprecated: Use MCPType instead. @@ -24,31 +24,64 @@ Deprecated: Use MCPType instead. This enum is kept for backward compatibility and will be removed in a future version. -### `RouteMap` +### `RouteMap` Mapping configuration for HTTP routes to FastMCP component types. -### `OpenAPITool` +### `OpenAPITool` Tool implementation for OpenAPI endpoints. -### `OpenAPIResource` +**Methods:** + +#### `run` + +```python +run(self, arguments: dict[str, Any]) -> ToolResult +``` + +Execute the HTTP request based on the route configuration. + + +### `OpenAPIResource` Resource implementation for OpenAPI endpoints. -### `OpenAPIResourceTemplate` +**Methods:** + +#### `read` + +```python +read(self) -> str | bytes +``` + +Fetch the resource data by making an HTTP request. + + +### `OpenAPIResourceTemplate` Resource template implementation for OpenAPI endpoints. -### `FastMCPOpenAPI` +**Methods:** + +#### `create_resource` + +```python +create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource +``` + +Create a resource with the given parameters. + + +### `FastMCPOpenAPI` FastMCP server implementation that creates components from an OpenAPI schema. diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx index e480b9167..db83a5611 100644 --- a/docs/python-sdk/fastmcp-server-proxy.mdx +++ b/docs/python-sdk/fastmcp-server-proxy.mdx @@ -5,27 +5,144 @@ sidebarTitle: proxy # `fastmcp.server.proxy` +## Functions + +### `default_proxy_roots_handler` + +```python +default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList +``` + + +A handler that forwards the list roots request from the remote server to the proxy's connected clients and relays the response back to the remote server. + + ## Classes -### `ProxyToolManager` +### `ProxyToolManager` A ToolManager that sources its tools from a remote client in addition to local and mounted tools. -### `ProxyResourceManager` +**Methods:** + +#### `get_tools` + +```python +get_tools(self) -> dict[str, Tool] +``` + +Gets the unfiltered tool inventory including local, mounted, and proxy tools. + + +#### `list_tools` + +```python +list_tools(self) -> list[Tool] +``` + +Gets the filtered list of tools including local, mounted, and proxy tools. + + +#### `call_tool` + +```python +call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult +``` + +Calls a tool, trying local/mounted first, then proxy if not found. + + +### `ProxyResourceManager` A ResourceManager that sources its resources from a remote client in addition to local and mounted resources. -### `ProxyPromptManager` +**Methods:** + +#### `get_resources` + +```python +get_resources(self) -> dict[str, Resource] +``` + +Gets the unfiltered resource inventory including local, mounted, and proxy resources. + + +#### `get_resource_templates` + +```python +get_resource_templates(self) -> dict[str, ResourceTemplate] +``` + +Gets the unfiltered template inventory including local, mounted, and proxy templates. + + +#### `list_resources` + +```python +list_resources(self) -> list[Resource] +``` + +Gets the filtered list of resources including local, mounted, and proxy resources. + + +#### `list_resource_templates` + +```python +list_resource_templates(self) -> list[ResourceTemplate] +``` + +Gets the filtered list of templates including local, mounted, and proxy templates. + + +#### `read_resource` + +```python +read_resource(self, uri: AnyUrl | str) -> str | bytes +``` + +Reads a resource, trying local/mounted first, then proxy if not found. + + +### `ProxyPromptManager` A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts. -### `ProxyTool` +**Methods:** + +#### `get_prompts` + +```python +get_prompts(self) -> dict[str, Prompt] +``` + +Gets the unfiltered prompt inventory including local, mounted, and proxy prompts. + + +#### `list_prompts` + +```python +list_prompts(self) -> list[Prompt] +``` + +Gets the filtered list of prompts including local, mounted, and proxy prompts. + + +#### `render_prompt` + +```python +render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult +``` + +Renders a prompt, trying local/mounted first, then proxy if not found. + + +### `ProxyTool` A Tool that represents and executes a tool on a remote server. @@ -33,7 +150,7 @@ A Tool that represents and executes a tool on a remote server. **Methods:** -#### `from_mcp_tool` +#### `from_mcp_tool` ```python from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool @@ -42,7 +159,16 @@ 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` +#### `run` + +```python +run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult +``` + +Executes the tool by making a call through the client. + + +### `ProxyResource` A Resource that represents and reads a resource from a remote server. @@ -50,7 +176,7 @@ A Resource that represents and reads a resource from a remote server. **Methods:** -#### `from_mcp_resource` +#### `from_mcp_resource` ```python from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> ProxyResource @@ -59,7 +185,16 @@ from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> Prox Factory method to create a ProxyResource from a raw MCP resource schema. -### `ProxyTemplate` +#### `read` + +```python +read(self) -> str | bytes +``` + +Read the resource content from the remote server. + + +### `ProxyTemplate` A ResourceTemplate that represents and creates resources from a remote server template. @@ -67,7 +202,7 @@ A ResourceTemplate that represents and creates resources from a remote server te **Methods:** -#### `from_mcp_template` +#### `from_mcp_template` ```python from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate @@ -76,7 +211,16 @@ from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) Factory method to create a ProxyTemplate from a raw MCP template schema. -### `ProxyPrompt` +#### `create_resource` + +```python +create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource +``` + +Create a resource from the template by calling the remote server. + + +### `ProxyPrompt` A Prompt that represents and renders a prompt from a remote server. @@ -84,7 +228,7 @@ A Prompt that represents and renders a prompt from a remote server. **Methods:** -#### `from_mcp_prompt` +#### `from_mcp_prompt` ```python from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt @@ -93,9 +237,63 @@ from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPromp Factory method to create a ProxyPrompt from a raw MCP prompt schema. -### `FastMCPProxy` +#### `render` + +```python +render(self, arguments: dict[str, Any]) -> list[PromptMessage] +``` + +Render the prompt by making a call through the client. + + +### `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. +It uses specialized managers that fulfill requests via a client factory. + + +### `ProxyClient` + + +A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients. +Supports forwarding roots, sampling, elicitation, logging, and progress. + + +**Methods:** + +#### `default_sampling_handler` + +```python +default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult +``` + +A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server. + + +#### `default_elicitation_handler` + +```python +default_elicitation_handler(cls, message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult +``` + +A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server. + + +#### `default_log_handler` + +```python +default_log_handler(cls, message: LogMessage) -> None +``` + +A handler that forwards the log notification from the remote server to the proxy's connected clients. + + +#### `default_progress_handler` + +```python +default_progress_handler(cls, progress: float, total: float | None, message: str | None) -> None +``` + +A handler that forwards the progress notification from the remote server to the proxy's connected clients. diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx index 8e6cc2bf5..986e382bd 100644 --- a/docs/python-sdk/fastmcp-server-server.mdx +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -10,7 +10,23 @@ FastMCP - A more ergonomic interface for MCP servers. ## Functions -### `add_resource_prefix` +### `default_lifespan` + +```python +default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any] +``` + + +Default lifespan context manager that does nothing. + +**Args:** +- `server`: The server instance this lifespan is managing + +**Returns:** +- An empty context object + + +### `add_resource_prefix` ```python add_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str @@ -48,7 +64,7 @@ add_resource_prefix("resource:///absolute/path", "prefix") - `ValueError`: If the URI doesn't match the expected protocol\://path format -### `remove_resource_prefix` +### `remove_resource_prefix` ```python remove_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str @@ -87,7 +103,7 @@ remove_resource_prefix("resource://prefix//absolute/path", "prefix") - `ValueError`: If the URI doesn't match the expected protocol\://path format -### `has_resource_prefix` +### `has_resource_prefix` ```python has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool @@ -127,32 +143,44 @@ False ## Classes -### `FastMCP` +### `FastMCP` **Methods:** -#### `settings` +#### `settings` ```python settings(self) -> Settings ``` -#### `name` +#### `name` ```python name(self) -> str ``` -#### `instructions` +#### `instructions` ```python instructions(self) -> str | None ``` -#### `run` +#### `run_async` ```python -run(self, transport: Transport | None = None, **transport_kwargs: Any) -> None +run_async(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None +``` + +Run the FastMCP server asynchronously. + +**Args:** +- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http") + + +#### `run` + +```python +run(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None ``` Run the FastMCP server. Note this is a synchronous function. @@ -161,13 +189,76 @@ Run the FastMCP server. Note this is a synchronous function. - `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http") -#### `add_middleware` +#### `add_middleware` ```python add_middleware(self, middleware: Middleware) -> None ``` -#### `custom_route` +#### `get_tools` + +```python +get_tools(self) -> dict[str, Tool] +``` + +Get all registered tools, indexed by registered key. + + +#### `get_tool` + +```python +get_tool(self, key: str) -> Tool +``` + +#### `get_resources` + +```python +get_resources(self) -> dict[str, Resource] +``` + +Get all registered resources, indexed by registered key. + + +#### `get_resource` + +```python +get_resource(self, key: str) -> Resource +``` + +#### `get_resource_templates` + +```python +get_resource_templates(self) -> dict[str, ResourceTemplate] +``` + +Get all registered resource templates, indexed by registered key. + + +#### `get_resource_template` + +```python +get_resource_template(self, key: str) -> ResourceTemplate +``` + +Get a registered resource template by key. + + +#### `get_prompts` + +```python +get_prompts(self) -> dict[str, Prompt] +``` + +List all available prompts. + + +#### `get_prompt` + +```python +get_prompt(self, key: str) -> Prompt +``` + +#### `custom_route` ```python custom_route(self, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) @@ -188,10 +279,10 @@ Starlette's reverse URL lookup feature) - `include_in_schema`: Whether to include in OpenAPI schema, defaults to True -#### `add_tool` +#### `add_tool` ```python -add_tool(self, tool: Tool) -> None +add_tool(self, tool: Tool) -> Tool ``` Add a tool to the server. @@ -202,8 +293,11 @@ with the Context type annotation. See the @tool decorator for examples. **Args:** - `tool`: The Tool instance to register +**Returns:** +- The tool instance that was added to the server. -#### `remove_tool` + +#### `remove_tool` ```python remove_tool(self, name: str) -> None @@ -218,19 +312,19 @@ Remove a tool from the server. - `NotFoundError`: If the tool is not found -#### `tool` +#### `tool` ```python tool(self, name_or_fn: AnyFunction) -> FunctionTool ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool] ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool @@ -254,6 +348,7 @@ This decorator supports multiple calling patterns: - `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 +- `output_schema`: Optional JSON schema for the tool's output - `annotations`: Optional annotations about the tool's behavior - `exclude_args`: Optional list of argument names to exclude from the tool schema - `enabled`: Optional boolean to enable or disable the tool @@ -284,10 +379,10 @@ server.tool(my_function, name="custom_name") ``` -#### `add_resource` +#### `add_resource` ```python -add_resource(self, resource: Resource) -> None +add_resource(self, resource: Resource) -> Resource ``` Add a resource to the server. @@ -295,11 +390,14 @@ Add a resource to the server. **Args:** - `resource`: A Resource instance to add +**Returns:** +- The resource instance that was added to the server. -#### `add_template` + +#### `add_template` ```python -add_template(self, template: ResourceTemplate) -> None +add_template(self, template: ResourceTemplate) -> ResourceTemplate ``` Add a resource template to the server. @@ -307,8 +405,11 @@ Add a resource template to the server. **Args:** - `template`: A ResourceTemplate instance to add +**Returns:** +- The template instance that was added to the server. -#### `add_resource_fn` + +#### `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 @@ -328,7 +429,7 @@ has parameters, it will be registered as a template resource. - `tags`: Optional set of tags for categorizing the resource -#### `resource` +#### `resource` ```python resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate] @@ -386,10 +487,10 @@ async def get_weather(city: str) -> str: ``` -#### `add_prompt` +#### `add_prompt` ```python -add_prompt(self, prompt: Prompt) -> None +add_prompt(self, prompt: Prompt) -> Prompt ``` Add a prompt to the server. @@ -397,20 +498,23 @@ Add a prompt to the server. **Args:** - `prompt`: A Prompt instance to add +**Returns:** +- The prompt instance that was added to the server. -#### `prompt` + +#### `prompt` ```python prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt] ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt @@ -487,7 +591,44 @@ Decorator to register a prompt. ``` -#### `sse_app` +#### `run_stdio_async` + +```python +run_stdio_async(self, show_banner: bool = True) -> None +``` + +Run the server using stdio transport. + + +#### `run_http_async` + +```python +run_http_async(self, show_banner: bool = True, transport: Literal['http', 'streamable-http', 'sse'] = 'http', host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, stateless_http: bool | None = None) -> None +``` + +Run the server using HTTP transport. + +**Args:** +- `transport`: Transport protocol to use - either "streamable-http" (default) or "sse" +- `host`: Host address to bind to (defaults to settings.host) +- `port`: Port to bind to (defaults to settings.port) +- `log_level`: Log level for the server (defaults to settings.log_level) +- `path`: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path) +- `uvicorn_config`: Additional configuration for the Uvicorn server +- `middleware`: A list of middleware to apply to the app +- `stateless_http`: Whether to use stateless HTTP (defaults to settings.stateless_http) + + +#### `run_sse_async` + +```python +run_sse_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None +``` + +Run the server using SSE transport. + + +#### `sse_app` ```python sse_app(self, path: str | None = None, message_path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan @@ -501,7 +642,7 @@ Create a Starlette app for the SSE server. - `middleware`: A list of middleware to apply to the app -#### `streamable_http_app` +#### `streamable_http_app` ```python streamable_http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan @@ -514,7 +655,7 @@ Create a Starlette app for the StreamableHTTP server. - `middleware`: A list of middleware to apply to the app -#### `http_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['http', 'streamable-http', 'sse'] = 'http') -> StarletteWithLifespan @@ -531,7 +672,13 @@ Create a Starlette app using the specified HTTP transport. - A Starlette application configured with the specified transport -#### `mount` +#### `run_streamable_http_async` + +```python +run_streamable_http_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None +``` + +#### `mount` ```python mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None @@ -585,7 +732,48 @@ automatically determined based on whether the server has a custom lifespan - `prompt_separator`: Deprecated. Separator character for prompt names. -#### `from_openapi` +#### `import_server` + +```python +import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None, tool_separator: str | None = None, resource_separator: str | None = None, prompt_separator: str | None = None) -> None +``` + +Import the MCP objects from another FastMCP server into this one, +optionally with a given prefix. + +Note that when a server is *imported*, its objects are immediately +registered to the importing server. This is a one-time operation and +future changes to the imported server will not be reflected in the +importing server. Server-level configurations and lifespans are not imported. + +When a server is imported with a prefix: +- The tools are imported with prefixed names + Example: If server has a tool named "get_weather", it will be + available as "prefix_get_weather" +- The resources are imported with prefixed URIs using the new format + Example: If server has a resource with URI "weather://forecast", it will + be available as "weather://prefix/forecast" +- The templates are imported with prefixed URI templates using the new format + Example: If server has a template with URI "weather://location/{id}", it will + be available as "weather://prefix/location/{id}" +- The prompts are imported with prefixed names + Example: If server has a prompt named "weather_prompt", it will be available as + "prefix_weather_prompt" + +When a server is imported without a prefix (prefix=None), its tools, resources, +templates, and prompts are imported with their original names. + +**Args:** +- `server`: The FastMCP server to import +- `prefix`: Optional prefix to use for the imported server's objects. If None, +objects are imported with their original names. +- `tool_separator`: Deprecated. Separator for tool names. +- `resource_separator`: Deprecated and ignored. Prefix is now +applied using the protocol\://prefix/path format +- `prompt_separator`: Deprecated. Separator 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 @@ -594,7 +782,7 @@ from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route Create a FastMCP server from an OpenAPI specification. -#### `from_fastapi` +#### `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 @@ -603,7 +791,7 @@ from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] Create a FastMCP server from a FastAPI application. -#### `as_proxy` +#### `as_proxy` ```python as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy @@ -617,7 +805,7 @@ instance or any value accepted as the `transport` argument of `fastmcp.client.Client` constructor. -#### `from_client` +#### `from_client` ```python from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy @@ -626,4 +814,4 @@ from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPPr Create a FastMCP proxy server from a FastMCP client. -### `MountedServer` +### `MountedServer` diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx index 6725277cb..7f546c31b 100644 --- a/docs/python-sdk/fastmcp-settings.mdx +++ b/docs/python-sdk/fastmcp-settings.mdx @@ -7,7 +7,7 @@ sidebarTitle: settings ## Classes -### `ExtendedEnvSettingsSource` +### `ExtendedEnvSettingsSource` A special EnvSettingsSource that allows for multiple env var prefixes to be used. @@ -17,15 +17,15 @@ Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used. **Methods:** -#### `get_field_value` +#### `get_field_value` ```python get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool] ``` -### `ExtendedSettingsConfigDict` +### `ExtendedSettingsConfigDict` -### `Settings` +### `Settings` FastMCP settings. @@ -33,13 +33,13 @@ FastMCP settings. **Methods:** -#### `settings_customise_sources` +#### `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` +#### `settings` ```python settings(self) -> Self @@ -49,7 +49,13 @@ This property is for backwards compatibility with FastMCP < 2.8.0, which accessed fastmcp.settings.settings -#### `setup_logging` +#### `normalize_log_level` + +```python +normalize_log_level(cls, v) +``` + +#### `setup_logging` ```python setup_logging(self) -> Self diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx index 07aef85a9..60c8fff9a 100644 --- a/docs/python-sdk/fastmcp-tools-tool.mdx +++ b/docs/python-sdk/fastmcp-tools-tool.mdx @@ -7,7 +7,7 @@ sidebarTitle: tool ## Functions -### `default_serializer` +### `default_serializer` ```python default_serializer(data: Any) -> str @@ -15,7 +15,17 @@ default_serializer(data: Any) -> str ## Classes -### `Tool` +### `ToolResult` + +**Methods:** + +#### `to_mcp_result` + +```python +to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] +``` + +### `Tool` Internal tool registration info. @@ -23,46 +33,82 @@ Internal tool registration info. **Methods:** -#### `to_mcp_tool` +#### `enable` + +```python +enable(self) -> None +``` + +#### `disable` + +```python +disable(self) -> None +``` + +#### `to_mcp_tool` ```python to_mcp_tool(self, **overrides: Any) -> MCPTool ``` -#### `from_function` +#### `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 +from_function(fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> FunctionTool ``` Create a Tool from a function. -#### `from_tool` +#### `run` ```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 +run(self, arguments: dict[str, Any]) -> ToolResult ``` -### `FunctionTool` +Run the tool with arguments. + +This method is not implemented in the base Tool class and must be +implemented by subclasses. + +`run()` can EITHER return a list of ContentBlocks, or a tuple of +(list of ContentBlocks, dict of structured output). + + +#### `from_tool` + +```python +from_tool(cls, tool: Tool, transform_fn: Callable[..., Any] | None = None, name: str | None = None, title: str | None | NotSetT = NotSet, transform_args: dict[str, ArgTransform] | None = None, description: str | None | NotSetT = NotSet, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, output_schema: dict[str, Any] | None | Literal[False] = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> TransformedTool +``` + +### `FunctionTool` **Methods:** -#### `from_function` +#### `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 +from_function(cls, fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> FunctionTool ``` Create a Tool from a function. -### `ParsedFunction` +#### `run` + +```python +run(self, arguments: dict[str, Any]) -> ToolResult +``` + +Run the tool with arguments. + + +### `ParsedFunction` **Methods:** -#### `from_function` +#### `from_function` ```python -from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True) -> ParsedFunction +from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True, wrap_non_object_output_schema: bool = True) -> ParsedFunction ``` diff --git a/docs/python-sdk/fastmcp-tools-tool_manager.mdx b/docs/python-sdk/fastmcp-tools-tool_manager.mdx index 75328aca1..f9b6b61f7 100644 --- a/docs/python-sdk/fastmcp-tools-tool_manager.mdx +++ b/docs/python-sdk/fastmcp-tools-tool_manager.mdx @@ -7,7 +7,7 @@ sidebarTitle: tool_manager ## Classes -### `ToolManager` +### `ToolManager` Manages FastMCP tools. @@ -15,7 +15,7 @@ Manages FastMCP tools. **Methods:** -#### `mount` +#### `mount` ```python mount(self, server: MountedServer) -> None @@ -24,7 +24,43 @@ mount(self, server: MountedServer) -> None Adds a mounted server as a source for tools. -#### `add_tool_from_fn` +#### `has_tool` + +```python +has_tool(self, key: str) -> bool +``` + +Check if a tool exists. + + +#### `get_tool` + +```python +get_tool(self, key: str) -> Tool +``` + +Get tool by key. + + +#### `get_tools` + +```python +get_tools(self) -> dict[str, Tool] +``` + +Gets the complete, unfiltered inventory of all tools. + + +#### `list_tools` + +```python +list_tools(self) -> list[Tool] +``` + +Lists all tools, applying protocol filtering. + + +#### `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 @@ -33,7 +69,7 @@ add_tool_from_fn(self, fn: Callable[..., Any], name: str | None = None, descript Add a tool to the server. -#### `add_tool` +#### `add_tool` ```python add_tool(self, tool: Tool) -> Tool @@ -42,7 +78,7 @@ add_tool(self, tool: Tool) -> Tool Register a tool with the server. -#### `remove_tool` +#### `remove_tool` ```python remove_tool(self, key: str) -> None @@ -56,3 +92,13 @@ Remove a tool from the server. **Raises:** - `NotFoundError`: If the tool is not found + +#### `call_tool` + +```python +call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult +``` + +Internal API for servers: Finds and calls a tool, respecting the +filtered protocol path. + diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx index 6a7ea8ceb..f39ece8f4 100644 --- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx +++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx @@ -5,9 +5,66 @@ sidebarTitle: tool_transform # `fastmcp.tools.tool_transform` +## Functions + +### `forward` + +```python +forward(**kwargs) -> ToolResult +``` + + +Forward to parent tool with argument transformation applied. + +This function can only be called from within a transformed tool's custom +function. It applies argument transformation (renaming, validation) before +calling the parent tool. + +For example, if the parent tool has args `x` and `y`, but the transformed +tool has args `a` and `b`, and an `transform_args` was provided that maps `x` to +`a` and `y` to `b`, then `forward(a=1, b=2)` will call the parent tool with +`x=1` and `y=2`. + +**Args:** +- `**kwargs`: Arguments to forward to the parent tool (using transformed names). + +**Returns:** +- The ToolResult from the parent tool execution. + +**Raises:** +- `RuntimeError`: If called outside a transformed tool context. +- `TypeError`: If provided arguments don't match the transformed schema. + + +### `forward_raw` + +```python +forward_raw(**kwargs) -> ToolResult +``` + + +Forward directly to parent tool without transformation. + +This function bypasses all argument transformation and validation, calling the parent +tool directly with the provided arguments. Use this when you need to call the parent +with its original parameter names and structure. + +For example, if the parent tool has args `x` and `y`, then `forward_raw(x=1, +y=2)` will call the parent tool with `x=1` and `y=2`. + +**Args:** +- `**kwargs`: Arguments to pass directly to the parent tool (using original names). + +**Returns:** +- The ToolResult from the parent tool execution. + +**Raises:** +- `RuntimeError`: If called outside a transformed tool context. + + ## Classes -### `ArgTransform` +### `ArgTransform` Configuration for transforming a parent tool's argument. @@ -69,26 +126,46 @@ ArgTransform(name="new_name", description="New desc", default=None, type=int) ``` -### `TransformedTool` +### `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. +structured output control, 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. +with transformed arguments. Output schemas and structured outputs are automatically +inherited from the parent tool but can be overridden or disabled. **Methods:** -#### `from_tool` +#### `run` ```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 +run(self, arguments: dict[str, Any]) -> ToolResult +``` + +Run the tool with context set for forward() functions. + +This method executes the tool's function while setting up the context +that allows forward() and forward_raw() to work correctly within custom +functions. + +**Args:** +- `arguments`: Dictionary of arguments to pass to the tool's function. + +**Returns:** +- ToolResult object containing content and optional structured output. + + +#### `from_tool` + +```python +from_tool(cls, tool: Tool, name: str | None = None, title: str | None | NotSetT = NotSet, description: str | None | NotSetT = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None = None, output_schema: dict[str, Any] | None | Literal[False] = None, serializer: Callable[[Any], str] | None = None, enabled: bool | None = None) -> TransformedTool ``` Create a transformed tool from a parent tool. @@ -99,6 +176,7 @@ Create a transformed tool from a parent tool. to call the parent tool. Functions with **kwargs receive transformed argument names. - `name`: New name for the tool. Defaults to parent tool's name. +- `title`: New title for the tool. Defaults to parent tool's title. - `transform_args`: Optional transformations for parent tool arguments. Only specified arguments are transformed, others pass through unchanged\: - Simple rename (str) @@ -107,6 +185,10 @@ Only specified arguments are transformed, others pass through unchanged\: - `description`: New description. Defaults to parent's description. - `tags`: New tags. Defaults to parent's tags. - `annotations`: New annotations. Defaults to parent's annotations. +- `output_schema`: Control output schema for structured outputs\: +- None (default)\: Inherit from transform_fn if available, then parent tool +- dict\: Use custom output schema +- False\: Disable output schema and structured outputs - `serializer`: New serializer. Defaults to parent's serializer. **Returns:** @@ -137,3 +219,23 @@ async def flexible(**kwargs) -> str: Tool.from_tool(parent, transform_fn=flexible, transform_args={"a": "x"}) ``` +# Control structured outputs and schemas +```python +# Custom output schema +Tool.from_tool(parent, output_schema={ + "type": "object", + "properties": {"status": {"type": "string"}} +}) + +# Disable structured outputs +Tool.from_tool(parent, output_schema=False) + +# Return ToolResult for full control +async def custom_output(**kwargs) -> ToolResult: + result = await forward(**kwargs) + return ToolResult( + content=[TextContent(text="Summary")], + structured_content={"processed": True} + ) +``` + diff --git a/docs/python-sdk/fastmcp-utilities-cache.mdx b/docs/python-sdk/fastmcp-utilities-cache.mdx index 49b0794a2..d06374aeb 100644 --- a/docs/python-sdk/fastmcp-utilities-cache.mdx +++ b/docs/python-sdk/fastmcp-utilities-cache.mdx @@ -7,23 +7,23 @@ sidebarTitle: cache ## Classes -### `TimedCache` +### `TimedCache` **Methods:** -#### `set` +#### `set` ```python set(self, key: Any, value: Any) -> None ``` -#### `get` +#### `get` ```python get(self, key: Any) -> Any ``` -#### `clear` +#### `clear` ```python clear(self) -> None diff --git a/docs/python-sdk/fastmcp-utilities-cli.mdx b/docs/python-sdk/fastmcp-utilities-cli.mdx new file mode 100644 index 000000000..c6c159042 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-cli.mdx @@ -0,0 +1,25 @@ +--- +title: cli +sidebarTitle: cli +--- + +# `fastmcp.utilities.cli` + +## Functions + +### `log_server_banner` + +```python +log_server_banner(server: FastMCP[Any], transport: Literal['stdio', 'http', 'sse', 'streamable-http']) -> None +``` + + +Creates and logs a formatted banner with server information and logo. + +**Args:** +- `transport`: The transport protocol being used +- `server_name`: Optional server name to display +- `host`: Host address (for HTTP transports) +- `port`: Port number (for HTTP transports) +- `path`: Server path (for HTTP transports) + diff --git a/docs/python-sdk/fastmcp-utilities-components.mdx b/docs/python-sdk/fastmcp-utilities-components.mdx index 8a27b2ac7..7c0c1a29a 100644 --- a/docs/python-sdk/fastmcp-utilities-components.mdx +++ b/docs/python-sdk/fastmcp-utilities-components.mdx @@ -7,7 +7,7 @@ sidebarTitle: components ## Classes -### `FastMCPComponent` +### `FastMCPComponent` Base class for FastMCP tools, prompts, resources, and resource templates. @@ -15,7 +15,7 @@ Base class for FastMCP tools, prompts, resources, and resource templates. **Methods:** -#### `key` +#### `key` ```python key(self) -> str @@ -27,13 +27,13 @@ keys having a certain value, as the same tool loaded from different hierarchies of servers may have different keys. -#### `with_key` +#### `with_key` ```python with_key(self, key: str) -> Self ``` -#### `enable` +#### `enable` ```python enable(self) -> None @@ -42,7 +42,7 @@ enable(self) -> None Enable the component. -#### `disable` +#### `disable` ```python disable(self) -> None @@ -50,3 +50,50 @@ disable(self) -> None Disable the component. + +#### `copy` + +```python +copy(self) -> Self +``` + +Create a copy of the component. + + +### `MirroredComponent` + + +Base class for components that are mirrored from a remote server. + +Mirrored components cannot be enabled or disabled directly. Call copy() first +to create a local version you can modify. + + +**Methods:** + +#### `enable` + +```python +enable(self) -> None +``` + +Enable the component. + + +#### `disable` + +```python +disable(self) -> None +``` + +Disable the component. + + +#### `copy` + +```python +copy(self) -> Self +``` + +Create a copy of the component that can be modified. + diff --git a/docs/python-sdk/fastmcp-utilities-exceptions.mdx b/docs/python-sdk/fastmcp-utilities-exceptions.mdx index 6b33526dc..be51a4213 100644 --- a/docs/python-sdk/fastmcp-utilities-exceptions.mdx +++ b/docs/python-sdk/fastmcp-utilities-exceptions.mdx @@ -7,13 +7,13 @@ sidebarTitle: exceptions ## Functions -### `iter_exc` +### `iter_exc` ```python iter_exc(group: BaseExceptionGroup) ``` -### `get_catch_handlers` +### `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 index 661f4e575..609c407e8 100644 --- a/docs/python-sdk/fastmcp-utilities-http.mdx +++ b/docs/python-sdk/fastmcp-utilities-http.mdx @@ -7,7 +7,7 @@ sidebarTitle: http ## Functions -### `find_available_port` +### `find_available_port` ```python find_available_port() -> int diff --git a/docs/python-sdk/fastmcp-utilities-inspect.mdx b/docs/python-sdk/fastmcp-utilities-inspect.mdx index f7b09c229..850983f9d 100644 --- a/docs/python-sdk/fastmcp-utilities-inspect.mdx +++ b/docs/python-sdk/fastmcp-utilities-inspect.mdx @@ -8,33 +8,86 @@ sidebarTitle: inspect Utilities for inspecting FastMCP instances. +## Functions + +### `inspect_fastmcp_v2` + +```python +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 + + +### `inspect_fastmcp_v1` + +```python +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 + + +### `inspect_fastmcp` + +```python +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 + + ## Classes -### `ToolInfo` +### `ToolInfo` Information about a tool. -### `PromptInfo` +### `PromptInfo` Information about a prompt. -### `ResourceInfo` +### `ResourceInfo` Information about a resource. -### `TemplateInfo` +### `TemplateInfo` Information about a resource template. -### `FastMCPInfo` +### `FastMCPInfo` Information extracted from a FastMCP instance. diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx index 282c03745..451b3dbff 100644 --- a/docs/python-sdk/fastmcp-utilities-json_schema.mdx +++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx @@ -7,7 +7,7 @@ sidebarTitle: json_schema ## Functions -### `compress_schema` +### `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 diff --git a/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx b/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx new file mode 100644 index 000000000..50fba7654 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx @@ -0,0 +1,110 @@ +--- +title: json_schema_type +sidebarTitle: json_schema_type +--- + +# `fastmcp.utilities.json_schema_type` + + +Convert JSON Schema to Python types with validation. + +The json_schema_to_type function converts a JSON Schema into a Python type that can be used +for validation with Pydantic. It supports: + +- Basic types (string, number, integer, boolean, null) +- Complex types (arrays, objects) +- Format constraints (date-time, email, uri) +- Numeric constraints (minimum, maximum, multipleOf) +- String constraints (minLength, maxLength, pattern) +- Array constraints (minItems, maxItems, uniqueItems) +- Object properties with defaults +- References and recursive schemas +- Enums and constants +- Union types + +Example: + ```python + schema = { + "type": "object", + "properties": { + "name": {"type": "string", "minLength": 1}, + "age": {"type": "integer", "minimum": 0}, + "email": {"type": "string", "format": "email"} + }, + "required": ["name", "age"] + } + + # Name is optional and will be inferred from schema's "title" property if not provided + Person = json_schema_to_type(schema) + # Creates a validated dataclass with name, age, and optional email fields + ``` + + +## Functions + +### `json_schema_to_type` + +```python +json_schema_to_type(schema: Mapping[str, Any], name: str | None = None) -> type +``` + + +Convert JSON schema to appropriate Python type with validation. + +**Args:** +- `schema`: A JSON Schema dictionary defining the type structure and validation rules +- `name`: Optional name for object schemas. Only allowed when schema type is "object". +If not provided for objects, name will be inferred from schema's "title" +property or default to "Root". + +**Returns:** +- A Python type (typically a dataclass for objects) with Pydantic validation + +**Raises:** +- `ValueError`: If a name is provided for a non-object schema + +**Examples:** + +Create a dataclass from an object schema: +```python +schema = { + "type": "object", + "title": "Person", + "properties": { + "name": {"type": "string", "minLength": 1}, + "age": {"type": "integer", "minimum": 0}, + "email": {"type": "string", "format": "email"} + }, + "required": ["name", "age"] +} + +Person = json_schema_to_type(schema) +# Creates a dataclass with name, age, and optional email fields: +# @dataclass +# class Person: +# name: str +# age: int +# email: str | None = None +``` +Person(name="John", age=30) + +Create a scalar type with constraints: +```python +schema = { + "type": "string", + "minLength": 3, + "pattern": "^[A-Z][a-z]+$" +} + +NameType = json_schema_to_type(schema) +# Creates Annotated[str, StringConstraints(min_length=3, pattern="^[A-Z][a-z]+$")] + +@dataclass +class Name: + name: NameType +``` + + +## Classes + +### `JSONSchema` diff --git a/docs/python-sdk/fastmcp-utilities-logging.mdx b/docs/python-sdk/fastmcp-utilities-logging.mdx index 03ca4a1bb..f4eb26666 100644 --- a/docs/python-sdk/fastmcp-utilities-logging.mdx +++ b/docs/python-sdk/fastmcp-utilities-logging.mdx @@ -10,7 +10,7 @@ Logging utilities for FastMCP. ## Functions -### `get_logger` +### `get_logger` ```python get_logger(name: str) -> logging.Logger @@ -26,7 +26,7 @@ Get a logger nested under FastMCP namespace. - a configured logger instance -### `configure_logging` +### `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 diff --git a/docs/python-sdk/fastmcp-utilities-openapi.mdx b/docs/python-sdk/fastmcp-utilities-openapi.mdx index e64157c68..4cb84f5f4 100644 --- a/docs/python-sdk/fastmcp-utilities-openapi.mdx +++ b/docs/python-sdk/fastmcp-utilities-openapi.mdx @@ -7,7 +7,48 @@ sidebarTitle: openapi ## Functions -### `parse_openapi_to_http_routes` +### `format_array_parameter` + +```python +format_array_parameter(values: list, parameter_name: str, is_query_parameter: bool = False) -> str | list +``` + + +Format an array parameter according to OpenAPI specifications. + +**Args:** +- `values`: List of values to format +- `parameter_name`: Name of the parameter (for error messages) +- `is_query_parameter`: If True, can return list for explode=True behavior + +**Returns:** +- String (comma-separated) or list (for query params with explode=True) + + +### `format_deep_object_parameter` + +```python +format_deep_object_parameter(param_value: dict, parameter_name: str) -> dict[str, str] +``` + + +Format a dictionary parameter for deepObject style serialization. + +According to OpenAPI 3.0 spec, deepObject style with explode=true serializes +object properties as separate query parameters with bracket notation. + +For example: {"id": "123", "type": "user"} becomes: +param[id]=123¶m[type]=user + +**Args:** +- `param_value`: Dictionary value to format +- `parameter_name`: Name of the parameter + +**Returns:** +- Dictionary with bracketed parameter names as keys + + +### `parse_openapi_to_http_routes` ```python parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute] @@ -20,7 +61,7 @@ using the openapi-pydantic library. Supports both OpenAPI 3.0.x and 3.1.x versions. -### `clean_schema_for_display` +### `clean_schema_for_display` ```python clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None @@ -30,7 +71,7 @@ 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` +### `generate_example_from_schema` ```python generate_example_from_schema(schema: JsonSchema | None) -> Any @@ -41,7 +82,7 @@ Generate a simple example value from a JSON schema dictionary. Very basic implementation focusing on types. -### `format_json_for_description` +### `format_json_for_description` ```python format_json_for_description(data: Any, indent: int = 2) -> str @@ -51,7 +92,7 @@ format_json_for_description(data: Any, indent: int = 2) -> str Formats Python data as a JSON string block for markdown. -### `format_description_with_responses` +### `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 @@ -74,33 +115,54 @@ including its description, whether it is required, and its content schema. - and the request body. +### `extract_output_schema_from_responses` + +```python +extract_output_schema_from_responses(responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None) -> dict[str, Any] | None +``` + + +Extract output schema from OpenAPI responses for use as MCP tool output schema. + +This function finds the first successful response (200, 201, 202, 204) with a +JSON-compatible content type and extracts its schema. If the schema is not an +object type, it wraps it to comply with MCP requirements. + +**Args:** +- `responses`: Dictionary of ResponseInfo objects keyed by status code +- `schema_definitions`: Optional schema definitions to include in the output schema + +**Returns:** +- MCP-compliant output schema with potential wrapping, or None if no suitable schema found + + ## Classes -### `ParameterInfo` +### `ParameterInfo` Represents a single parameter for an HTTP operation in our IR. -### `RequestBodyInfo` +### `RequestBodyInfo` Represents the request body for an HTTP operation in our IR. -### `ResponseInfo` +### `ResponseInfo` Represents response information in our IR. -### `HTTPRoute` +### `HTTPRoute` Intermediate Representation for a single OpenAPI operation. -### `OpenAPIParser` +### `OpenAPIParser` Unified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1. @@ -108,7 +170,7 @@ Unified parser for OpenAPI schemas with generic type parameters to handle both 3 **Methods:** -#### `parse` +#### `parse` ```python parse(self) -> list[HTTPRoute] diff --git a/docs/python-sdk/fastmcp-utilities-tests.mdx b/docs/python-sdk/fastmcp-utilities-tests.mdx index 78e0180c1..f8f1d8fb8 100644 --- a/docs/python-sdk/fastmcp-utilities-tests.mdx +++ b/docs/python-sdk/fastmcp-utilities-tests.mdx @@ -7,7 +7,7 @@ sidebarTitle: tests ## Functions -### `temporary_settings` +### `temporary_settings` ```python temporary_settings(**kwargs: Any) @@ -20,7 +20,7 @@ Temporarily override FastMCP setting values. - `**kwargs`: The settings to override, including nested settings. -### `run_server_in_process` +### `run_server_in_process` ```python run_server_in_process(server_fn: Callable[..., None], *args, **kwargs) -> Generator[str, None, None] @@ -40,3 +40,13 @@ not pickleable, so we need a function that creates and runs one. **Returns:** - The server URL. + +### `caplog_for_fastmcp` + +```python +caplog_for_fastmcp(caplog) +``` + + +Context manager to capture logs from FastMCP loggers even when propagation is disabled. + diff --git a/docs/python-sdk/fastmcp-utilities-types.mdx b/docs/python-sdk/fastmcp-utilities-types.mdx index 19a5b7b45..378e0e076 100644 --- a/docs/python-sdk/fastmcp-utilities-types.mdx +++ b/docs/python-sdk/fastmcp-utilities-types.mdx @@ -10,7 +10,7 @@ Common types used across FastMCP. ## Functions -### `get_cached_typeadapter` +### `get_cached_typeadapter` ```python get_cached_typeadapter(cls: T) -> TypeAdapter[T] @@ -23,7 +23,7 @@ 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` +### `issubclass_safe` ```python issubclass_safe(cls: type, base: type) -> bool @@ -33,7 +33,7 @@ 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` +### `is_class_member_of_type` ```python is_class_member_of_type(cls: type, base: type) -> bool @@ -46,7 +46,7 @@ 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` +### `find_kwarg_by_type` ```python find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None @@ -58,15 +58,40 @@ 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. +### `replace_type` + +```python +replace_type(type_, type_map: dict[type, type]) +``` + + +Given a (possibly generic, nested, or otherwise complex) type, replaces all +instances of old_type with new_type. + +This is useful for transforming types when creating tools. + +**Args:** +- `type_`: The type to replace instances of old_type with new_type. +- `old_type`: The type to replace. +- `new_type`: The type to replace old_type with. + +**Examples:** + +>>> replace_type(list\[int | bool], {int: str}) +list\[str | bool] +>>> replace_type(list\[list\[int]], {int: str}) +list\[list\[str]] + + ## Classes -### `FastMCPBaseModel` +### `FastMCPBaseModel` Base model for FastMCP models. -### `Image` +### `Image` Helper class for returning images from tools. @@ -74,16 +99,16 @@ Helper class for returning images from tools. **Methods:** -#### `to_image_content` +#### `to_image_content` ```python -to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> ImageContent +to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.ImageContent ``` Convert to MCP ImageContent. -### `Audio` +### `Audio` Helper class for returning audio from tools. @@ -91,13 +116,13 @@ Helper class for returning audio from tools. **Methods:** -#### `to_audio_content` +#### `to_audio_content` ```python -to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> AudioContent +to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.AudioContent ``` -### `File` +### `File` Helper class for returning audio from tools. @@ -105,8 +130,8 @@ Helper class for returning audio from tools. **Methods:** -#### `to_resource_content` +#### `to_resource_content` ```python -to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> EmbeddedResource +to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.EmbeddedResource ``` From 1e473cd0ef17ee033da48ca4fc1bf725a703967c Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 18 Jul 2025 11:10:26 -0500 Subject: [PATCH 12/22] skip on rate limit --- tests/integration_tests/conftest.py | 24 +++++++++++++++++++ .../test_github_mcp_remote.py | 1 + 2 files changed, 25 insertions(+) create mode 100644 tests/integration_tests/conftest.py diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py new file mode 100644 index 000000000..f1779f228 --- /dev/null +++ b/tests/integration_tests/conftest.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Convert BrokenResourceError failures to skips""" + outcome = yield + report = outcome.get_result() + + # Only process actual failures during the call phase, not xfails + if ( + report.when == "call" + and report.failed + and not hasattr(report, "wasxfail") + and call.excinfo + and call.excinfo.typename == "BrokenResourceError" + ): + # Convert to a skip + report.outcome = "skipped" + report.longrepr = ( + "/Users/nate/github.com/jlowin/fastmcp/tests/integration_tests/conftest.py", + None, + "Skipped: Skipping due to GitHub API rate limit (429)", + ) diff --git a/tests/integration_tests/test_github_mcp_remote.py b/tests/integration_tests/test_github_mcp_remote.py index dedb34bc5..fb122a2e6 100644 --- a/tests/integration_tests/test_github_mcp_remote.py +++ b/tests/integration_tests/test_github_mcp_remote.py @@ -14,6 +14,7 @@ GITHUB_REMOTE_MCP_URL = "https://api.githubcopilot.com/mcp/" HEADER_AUTHORIZATION = "Authorization" FASTMCP_GITHUB_TOKEN = os.getenv("FASTMCP_GITHUB_TOKEN") + # Skip tests if no GitHub token is available pytestmark = pytest.mark.xfail( not FASTMCP_GITHUB_TOKEN, From 163db94266d644bc36f9979aedfa99626b8352a1 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 18 Jul 2025 11:19:18 -0500 Subject: [PATCH 13/22] more specific --- tests/integration_tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index f1779f228..f2d1e76ce 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -3,7 +3,7 @@ import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): - """Convert BrokenResourceError failures to skips""" + """Convert BrokenResourceError failures to skips only for GitHub rate limits""" outcome = yield report = outcome.get_result() @@ -14,8 +14,10 @@ def pytest_runtest_makereport(item, call): and not hasattr(report, "wasxfail") and call.excinfo and call.excinfo.typename == "BrokenResourceError" + and item.module.__name__ == "tests.integration_tests.test_github_mcp_remote" ): - # Convert to a skip + # Only skip if the test is in the GitHub remote test module + # This prevents catching unrelated BrokenResourceErrors report.outcome = "skipped" report.longrepr = ( "/Users/nate/github.com/jlowin/fastmcp/tests/integration_tests/conftest.py", From 283bf6855054841d85b2029d79caaf17e3778e3e Mon Sep 17 00:00:00 2001 From: nate nowack Date: Fri, 18 Jul 2025 15:19:24 -0500 Subject: [PATCH 14/22] Update tests/integration_tests/conftest.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/integration_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index f2d1e76ce..2af11340f 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -20,7 +20,7 @@ def pytest_runtest_makereport(item, call): # This prevents catching unrelated BrokenResourceErrors report.outcome = "skipped" report.longrepr = ( - "/Users/nate/github.com/jlowin/fastmcp/tests/integration_tests/conftest.py", + os.path.abspath(__file__), None, "Skipped: Skipping due to GitHub API rate limit (429)", ) From ce49880aeb0ab4a987fc1163b459d735425055ae Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Fri, 18 Jul 2025 15:27:52 -0500 Subject: [PATCH 15/22] fix missing import --- tests/integration_tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 2af11340f..d45e7b292 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -1,3 +1,5 @@ +import os + import pytest From ea54851736605b3b0b62e05c9a5638fc9dc53fe8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:31:11 -0400 Subject: [PATCH 16/22] Fix `mcp-json` output format to include server name (#1185) Co-authored-by: Claude --- docs/integrations/mcp-json-configuration.mdx | 108 ++++++++++-------- docs/patterns/cli.mdx | 40 ++++--- src/fastmcp/cli/install/__init__.py | 4 +- .../install/{mcp_config.py => mcp_json.py} | 17 +-- tests/cli/test_install.py | 4 +- 5 files changed, 94 insertions(+), 79 deletions(-) rename src/fastmcp/cli/install/{mcp_config.py => mcp_json.py} (93%) diff --git a/docs/integrations/mcp-json-configuration.mdx b/docs/integrations/mcp-json-configuration.mdx index a713f7d7b..9a185043c 100644 --- a/docs/integrations/mcp-json-configuration.mdx +++ b/docs/integrations/mcp-json-configuration.mdx @@ -98,23 +98,25 @@ Generate configuration and output to stdout (useful for piping): fastmcp install mcp-json server.py ``` -This outputs the server configuration JSON that you add to the `mcpServers` object: +This outputs the server configuration JSON with the server name as the root key: ```json { - "command": "uv", - "args": [ - "run", - "--with", - "fastmcp", - "fastmcp", - "run", - "/absolute/path/to/server.py" - ] + "My Server": { + "command": "uv", + "args": [ + "run", + "--with", + "fastmcp", + "fastmcp", + "run", + "/absolute/path/to/server.py" + ] + } } ``` -To use this in a client configuration file, add it under a server name in the `mcpServers` object: +To use this in a client configuration file, add it to the `mcpServers` object in your client's configuration: ```json { @@ -134,6 +136,10 @@ To use this in a client configuration file, add it under a server name in the `m } ``` + +Different MCP clients may have specific configuration requirements or formatting needs. Always consult your client's documentation to ensure proper integration. + + ## Configuration Options ### Server Naming @@ -177,8 +183,8 @@ mcp = FastMCP( ```bash # Individual environment variables fastmcp install mcp-json server.py \ - --env-var API_KEY=your-secret-key \ - --env-var DEBUG=true + --env API_KEY=your-secret-key \ + --env DEBUG=true # Load from .env file fastmcp install mcp-json server.py --env-file .env @@ -219,15 +225,17 @@ fastmcp install mcp-json dice_server.py Output: ```json { - "command": "uv", - "args": [ - "run", - "--with", - "fastmcp", - "fastmcp", - "run", - "/home/user/dice_server.py" - ] + "Dice Server": { + "command": "uv", + "args": [ + "run", + "--with", + "fastmcp", + "fastmcp", + "run", + "/home/user/dice_server.py" + ] + } } ``` @@ -238,29 +246,31 @@ fastmcp install mcp-json api_server.py \ --name "Production API Server" \ --with requests \ --with python-dotenv \ - --env-var API_BASE_URL=https://api.example.com \ - --env-var TIMEOUT=30 + --env API_BASE_URL=https://api.example.com \ + --env TIMEOUT=30 ``` Output: ```json { - "command": "uv", - "args": [ - "run", - "--with", - "fastmcp", - "--with", - "python-dotenv", - "--with", - "requests", - "fastmcp", - "run", - "/home/user/api_server.py" - ], - "env": { - "API_BASE_URL": "https://api.example.com", - "TIMEOUT": "30" + "Production API Server": { + "command": "uv", + "args": [ + "run", + "--with", + "fastmcp", + "--with", + "python-dotenv", + "--with", + "requests", + "fastmcp", + "run", + "/home/user/api_server.py" + ], + "env": { + "API_BASE_URL": "https://api.example.com", + "TIMEOUT": "30" + } } } ``` @@ -278,7 +288,7 @@ Use in shell scripts: ```bash #!/bin/bash CONFIG=$(fastmcp install mcp-json server.py --name "CI Server") -echo "$CONFIG" | jq '.command' +echo "$CONFIG" | jq '."CI Server".command' # Output: "uv" ``` @@ -306,22 +316,22 @@ Use the JSON configuration with any application that supports the MCP protocol ## Configuration Format -The generated configuration follows the standard MCP server specification: +The generated configuration outputs a server object with the server name as the root key: ```json { - "mcpServers": { - "": { - "command": "", - "args": ["", "", "..."], - "env": { - "": "" - } + "": { + "command": "", + "args": ["", "", "..."], + "env": { + "": "" } } } ``` +To use this in an MCP client, add it to the client's `mcpServers` configuration object. + **Fields:** - `command`: The executable to run (always `uv` for FastMCP servers) - `args`: Command-line arguments including dependencies and server path diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index 7bedf4e41..dce1b5b32 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -185,7 +185,7 @@ The `install` command supports the same `file.py:object` notation as the `run` c | Server Name | `--name`, `-n` | Custom name for the server (defaults to server's name attribute or file name) | | Editable Package | `--with-editable`, `-e` | Directory containing pyproject.toml to install in editable mode | | Additional Packages | `--with` | Additional packages to install (can be used multiple times) | -| Environment Variables | `--env-var`, `-v` | Environment variables in KEY=VALUE format (can be used multiple times) | +| Environment Variables | `--env` | Environment variables in KEY=VALUE format (can be used multiple times) | | Environment File | `--env-file`, `-f` | Load environment variables from a .env file | **Examples** @@ -198,13 +198,13 @@ fastmcp install claude-desktop server.py fastmcp install claude-desktop server.py:my_server # With custom name and dependencies -fastmcp install claude-desktop server.py:my_server -n "My Analysis Server" --with pandas +fastmcp install claude-desktop server.py:my_server --name "My Analysis Server" --with pandas # Install in Claude Code with environment variables -fastmcp install claude-code server.py --env-var API_KEY=secret --env-var DEBUG=true +fastmcp install claude-code server.py --env API_KEY=secret --env DEBUG=true # Install in Cursor with environment variables -fastmcp install cursor server.py --env-var API_KEY=secret --env-var DEBUG=true +fastmcp install cursor server.py --env API_KEY=secret --env DEBUG=true # Install with environment file fastmcp install cursor server.py --env-file .env @@ -225,29 +225,31 @@ The `mcp-json` subcommand generates standard MCP JSON configuration that can be - Sharing server configurations with others - Integration with custom tooling -The generated JSON follows the standard `mcpServers` format used by Claude Desktop, VS Code, Cursor, and other MCP clients: +The generated JSON follows the standard MCP server configuration format used by Claude Desktop, VS Code, Cursor, and other MCP clients, with the server name as the root key: ```json { - "mcpServers": { - "server-name": { - "command": "uv", - "args": [ - "run", - "--with", - "fastmcp", - "fastmcp", - "run", - "/path/to/server.py" - ], - "env": { - "API_KEY": "value" - } + "server-name": { + "command": "uv", + "args": [ + "run", + "--with", + "fastmcp", + "fastmcp", + "run", + "/path/to/server.py" + ], + "env": { + "API_KEY": "value" } } } ``` + +To use this configuration with your MCP client, you'll typically need to add it to the client's `mcpServers` object. Consult your client's documentation for any specific configuration requirements or formatting needs. + + **Options specific to mcp-json:** | Option | Flag | Description | diff --git a/src/fastmcp/cli/install/__init__.py b/src/fastmcp/cli/install/__init__.py index 78b788628..a5fa48f90 100644 --- a/src/fastmcp/cli/install/__init__.py +++ b/src/fastmcp/cli/install/__init__.py @@ -5,7 +5,7 @@ import cyclopts from .claude_code import claude_code_command from .claude_desktop import claude_desktop_command from .cursor import cursor_command -from .mcp_config import mcp_config_command +from .mcp_json import mcp_json_command # Create a cyclopts app for install subcommands install_app = cyclopts.App( @@ -17,4 +17,4 @@ install_app = cyclopts.App( install_app.command(claude_code_command, name="claude-code") install_app.command(claude_desktop_command, name="claude-desktop") install_app.command(cursor_command, name="cursor") -install_app.command(mcp_config_command, name="mcp-json") +install_app.command(mcp_json_command, name="mcp-json") diff --git a/src/fastmcp/cli/install/mcp_config.py b/src/fastmcp/cli/install/mcp_json.py similarity index 93% rename from src/fastmcp/cli/install/mcp_config.py rename to src/fastmcp/cli/install/mcp_json.py index 29eb898c5..91bd40ece 100644 --- a/src/fastmcp/cli/install/mcp_config.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -16,7 +16,7 @@ from .shared import process_common_args logger = get_logger(__name__) -def install_mcp_config( +def install_mcp_json( file: Path, server_object: str | None, name: str, @@ -65,15 +65,18 @@ def install_mcp_config( # Add fastmcp run command args.extend(["fastmcp", "run", server_spec]) - # Build MCP server configuration (just the server object, not the wrapper) - config = { + # Build MCP server configuration + server_config = { "command": "uv", "args": args, } # Add environment variables if provided if env_vars: - config["env"] = env_vars + server_config["env"] = env_vars + + # Wrap with server name as root key + config = {name: server_config} # Convert to JSON json_output = json.dumps(config, indent=2) @@ -93,13 +96,13 @@ def install_mcp_config( return False -def mcp_config_command( +def mcp_json_command( server_spec: str, *, server_name: Annotated[ str | None, cyclopts.Parameter( - name=["--server-name", "-n"], + name=["--name", "-n"], help="Custom name for the server in MCP config", ), ] = None, @@ -151,7 +154,7 @@ def mcp_config_command( server_spec, server_name, with_packages, env_vars, env_file ) - success = install_mcp_config( + success = install_mcp_json( file=file, server_object=server_object, name=name, diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py index 55fe056d9..a16a1e61d 100644 --- a/tests/cli/test_install.py +++ b/tests/cli/test_install.py @@ -124,7 +124,7 @@ class TestMcpJsonInstall: def test_mcp_json_basic(self): """Test basic mcp-json install command parsing.""" command, bound, _ = install_app.parse_args( - ["mcp-json", "server.py", "--server-name", "test-server"] + ["mcp-json", "server.py", "--name", "test-server"] ) assert command is not None @@ -134,7 +134,7 @@ class TestMcpJsonInstall: def test_mcp_json_with_copy(self): """Test mcp-json install with copy to clipboard option.""" command, bound, _ = install_app.parse_args( - ["mcp-json", "server.py", "--server-name", "test-server", "--copy"] + ["mcp-json", "server.py", "--name", "test-server", "--copy"] ) assert bound.arguments["copy"] is True From 1c20ad77c9fa2079bb972dbdfbb153b588ea06d7 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:37:46 -0400 Subject: [PATCH 17/22] Remove deprecated proxy creation (#1186) --- src/fastmcp/server/server.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 626fbe53a..240c29a26 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -1662,8 +1662,7 @@ class FastMCP(Generic[LifespanResultT]): resource_separator: Deprecated. Separator character for resource URIs. prompt_separator: Deprecated. Separator character for prompt names. """ - from fastmcp.client.transports import FastMCPTransport - from fastmcp.server.proxy import FastMCPProxy, ProxyClient + from fastmcp.server.proxy import FastMCPProxy # Deprecated since 2.9.0 # Prior to 2.9.0, the first positional argument was the prefix and the @@ -1715,7 +1714,7 @@ class FastMCP(Generic[LifespanResultT]): as_proxy = server._has_lifespan if as_proxy and not isinstance(server, FastMCPProxy): - server = FastMCPProxy(ProxyClient(transport=FastMCPTransport(server))) + server = FastMCP.as_proxy(server) # Delegate mounting to all three managers mounted_server = MountedServer( From 589845028448dcebae1bd28bf2468ad930971367 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:08:47 -0400 Subject: [PATCH 18/22] Only configure logging one time --- src/fastmcp/__init__.py | 9 +++++++-- src/fastmcp/settings.py | 13 +------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/fastmcp/__init__.py b/src/fastmcp/__init__.py index 76038ca2d..a3c93e072 100644 --- a/src/fastmcp/__init__.py +++ b/src/fastmcp/__init__.py @@ -1,10 +1,15 @@ """FastMCP - An ergonomic MCP interface.""" import warnings -from importlib.metadata import version +from importlib.metadata import version as _version from fastmcp.settings import Settings +from fastmcp.utilities.logging import configure_logging as _configure_logging settings = Settings() +_configure_logging( + level=settings.log_level, + enable_rich_tracebacks=settings.enable_rich_tracebacks, +) from fastmcp.server.server import FastMCP from fastmcp.server.context import Context @@ -13,7 +18,7 @@ import fastmcp.server from fastmcp.client import Client from . import client -__version__ = version("fastmcp") +__version__ = _version("fastmcp") # ensure deprecation warnings are displayed by default diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index e864ca691..47e499162 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -5,7 +5,7 @@ import warnings from pathlib import Path from typing import Annotated, Any, Literal -from pydantic import Field, field_validator, model_validator +from pydantic import Field, field_validator from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, @@ -171,17 +171,6 @@ class Settings(BaseSettings): ), ] = None - @model_validator(mode="after") - def setup_logging(self) -> Self: - """Finalize the settings.""" - from fastmcp.utilities.logging import configure_logging - - configure_logging( - self.log_level, enable_rich_tracebacks=self.enable_rich_tracebacks - ) - - return self - # HTTP settings host: str = "127.0.0.1" port: int = 8000 From 06b291365e11add485167a0910e48ed66d7535b3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:21:08 -0400 Subject: [PATCH 19/22] Only configure logging one time (#1187) --- src/fastmcp/__init__.py | 9 +++++++-- src/fastmcp/settings.py | 13 +------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/fastmcp/__init__.py b/src/fastmcp/__init__.py index 76038ca2d..a3c93e072 100644 --- a/src/fastmcp/__init__.py +++ b/src/fastmcp/__init__.py @@ -1,10 +1,15 @@ """FastMCP - An ergonomic MCP interface.""" import warnings -from importlib.metadata import version +from importlib.metadata import version as _version from fastmcp.settings import Settings +from fastmcp.utilities.logging import configure_logging as _configure_logging settings = Settings() +_configure_logging( + level=settings.log_level, + enable_rich_tracebacks=settings.enable_rich_tracebacks, +) from fastmcp.server.server import FastMCP from fastmcp.server.context import Context @@ -13,7 +18,7 @@ import fastmcp.server from fastmcp.client import Client from . import client -__version__ = version("fastmcp") +__version__ = _version("fastmcp") # ensure deprecation warnings are displayed by default diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index e864ca691..47e499162 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -5,7 +5,7 @@ import warnings from pathlib import Path from typing import Annotated, Any, Literal -from pydantic import Field, field_validator, model_validator +from pydantic import Field, field_validator from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, @@ -171,17 +171,6 @@ class Settings(BaseSettings): ), ] = None - @model_validator(mode="after") - def setup_logging(self) -> Self: - """Finalize the settings.""" - from fastmcp.utilities.logging import configure_logging - - configure_logging( - self.log_level, enable_rich_tracebacks=self.enable_rich_tracebacks - ) - - return self - # HTTP settings host: str = "127.0.0.1" port: int = 8000 From 30912c69c68de6bb28944d21efc5ddffffba13c8 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:21:34 -0400 Subject: [PATCH 20/22] Run integration tests as separate CI job on Ubuntu only - Add pytest marker for integration tests - Configure automatic marking for tests/integration_tests/ folder via pytest hook - Split CI workflow into two jobs: regular tests (all platforms) and integration tests (Ubuntu only) - Integration tests can now be retried independently if they fail --- .github/workflows/run-tests.yml | 25 +++++++++++++++++++++++-- pyproject.toml | 9 +++++++++ tests/conftest.py | 9 +++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index bb9c7fb36..c9d212b4f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -46,7 +46,28 @@ jobs: - name: Install FastMCP run: uv sync --locked - - name: Run tests - run: uv run pytest tests + - name: Run tests (excluding integration) + run: uv run pytest tests -m "not integration" + + run_integration_tests: + name: "Run integration tests" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: "3.10" + + - name: Install FastMCP + run: uv sync --locked + + - name: Run integration tests + run: uv run pytest tests -m "integration" env: FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index 3e3dea61c..94fde3974 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,15 @@ env = [ 'D:FASTMCP_LOG_LEVEL=DEBUG', 'D:FASTMCP_ENABLE_RICH_TRACEBACKS=0', ] +markers = [ + "integration: marks tests as integration tests (deselect with '-m \"not integration\"')", +] +# Automatically mark all tests in integration_tests folder +pythonpath = ["."] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] [tool.pyright] include = ["src", "tests"] diff --git a/tests/conftest.py b/tests/conftest.py index e69de29bb..4fba247c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest + + +def pytest_collection_modifyitems(items): + """Automatically mark tests in integration_tests folder with 'integration' marker.""" + for item in items: + # Check if the test is in the integration_tests folder + if "integration_tests" in str(item.fspath): + item.add_marker(pytest.mark.integration) From 85ebe5b65704a06423635b274235d90abeea1ac2 Mon Sep 17 00:00:00 2001 From: hopeful0 Date: Sun, 20 Jul 2025 03:51:58 +0800 Subject: [PATCH 21/22] Add StatefulProxyClient (#1109) --- src/fastmcp/server/proxy.py | 42 ++++++ .../proxy/test_stateful_proxy_client.py | 120 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tests/server/proxy/test_stateful_proxy_client.py diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py index fa4c29ed6..576b75ef2 100644 --- a/src/fastmcp/server/proxy.py +++ b/src/fastmcp/server/proxy.py @@ -580,3 +580,45 @@ class ProxyClient(Client[ClientTransportT]): """ ctx = get_context() await ctx.report_progress(progress, total, message) + + +class StatefulProxyClient(ProxyClient[ClientTransportT]): + """ + A proxy client that provides a stateful client factory for the proxy server. + + The stateful proxy client bound its copy to the server session. + And it will be disconnected when the session is exited. + + This is useful to proxy a stateful mcp server such as the Playwright MCP server. + Note that it is essential to ensure that the proxy server itself is also stateful. + """ + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + """ + The stateful proxy client will be forced disconnected when the session is exited. + So we do nothing here. + """ + pass + + def new_stateful(self) -> Client[ClientTransportT]: + """ + Create a new stateful proxy client instance with the same configuration. + + Use this method as the client factory for stateful proxy server. + """ + session = get_context().session + proxy_client = session.__dict__.get("_proxy_client", None) + + if proxy_client is None: + proxy_client = self.new() + logger.debug(f"{proxy_client} created for {session}") + session.__dict__["_proxy_client"] = proxy_client + + async def _on_session_exit(): + proxy_client: Client = session.__dict__.pop("_proxy_client") + logger.debug(f"{proxy_client} will be disconnect") + await proxy_client._disconnect(force=True) + + session._exit_stack.push_async_callback(_on_session_exit) + + return proxy_client diff --git a/tests/server/proxy/test_stateful_proxy_client.py b/tests/server/proxy/test_stateful_proxy_client.py new file mode 100644 index 000000000..52bf86f33 --- /dev/null +++ b/tests/server/proxy/test_stateful_proxy_client.py @@ -0,0 +1,120 @@ +import asyncio + +import pytest +from anyio import create_task_group +from mcp.types import LoggingLevel + +from fastmcp import Client, Context, FastMCP +from fastmcp.client.logging import LogMessage +from fastmcp.client.transports import FastMCPTransport +from fastmcp.exceptions import ToolError +from fastmcp.server.proxy import FastMCPProxy, StatefulProxyClient +from fastmcp.utilities.tests import find_available_port + + +@pytest.fixture +def fastmcp_server(): + mcp = FastMCP("TestServer") + + states: dict[int, int] = {} + + @mcp.tool + async def log( + message: str, level: LoggingLevel, logger: str, context: Context + ) -> None: + await context.log(message=message, level=level, logger_name=logger) + + @mcp.tool + async def stateful_put(value: int, context: Context) -> None: + """put a value associated with the server session""" + key = id(context.session) + states[key] = value + + @mcp.tool + async def stateful_get(context: Context) -> int: + """get the value associated with the server session""" + key = id(context.session) + try: + return states[key] + except KeyError: + raise ToolError("Value not found") + + return mcp + + +@pytest.fixture +async def stateful_proxy_server(fastmcp_server: FastMCP): + client = StatefulProxyClient(transport=FastMCPTransport(fastmcp_server)) + return FastMCPProxy(client_factory=client.new_stateful) + + +@pytest.fixture +async def stateless_server(stateful_proxy_server: FastMCP): + port = find_available_port() + url = f"http://127.0.0.1:{port}/mcp/" + + task = asyncio.create_task( + stateful_proxy_server.run_http_async( + host="127.0.0.1", port=port, stateless_http=True + ) + ) + async with Client(transport=url) as client: + assert await client.ping() + yield url + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + +class TestStatefulProxyClient: + async def test_concurrent_log_requests_no_mixing( + self, stateful_proxy_server: FastMCP + ): + """Test that concurrent log requests don't mix handlers (fixes #1068).""" + results: dict[str, LogMessage] = {} + + async def log_handler_a(message: LogMessage) -> None: + results["logger_a"] = message + + async def log_handler_b(message: LogMessage) -> None: + results["logger_b"] = message + + async with ( + Client(stateful_proxy_server, log_handler=log_handler_a) as client_a, + Client(stateful_proxy_server, log_handler=log_handler_b) as client_b, + ): + async with create_task_group() as tg: + tg.start_soon( + client_a.call_tool, + "log", + {"message": "Hello, world!", "level": "info", "logger": "a"}, + ) + tg.start_soon( + client_b.call_tool, + "log", + {"message": "Hello, world!", "level": "info", "logger": "b"}, + ) + + assert results["logger_a"].logger == "a" + assert results["logger_b"].logger == "b" + + async def test_stateful_proxy(self, stateful_proxy_server: FastMCP): + """Test that the state shared across multiple calls for the same client (fixes #959).""" + async with Client(stateful_proxy_server) as client: + with pytest.raises(ToolError, match="Value not found"): + await client.call_tool("stateful_get", {}) + + await client.call_tool("stateful_put", {"value": 1}) + result = await client.call_tool("stateful_get", {}) + assert result.data == 1 + + async def test_stateless_proxy(self, stateless_server: str): + """Test that the state will not be shared across different calls, + even if they are from the same client.""" + async with Client(stateless_server) as client: + await client.call_tool("stateful_put", {"value": 1}) + + with pytest.raises(ToolError, match="Value not found"): + await client.call_tool("stateful_get", {}) From 1b953820b3c2948e2542dc978c11593e0c23ab97 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 19 Jul 2025 14:52:47 -0500 Subject: [PATCH 22/22] =?UTF-8?q?[=F0=9F=90=B6]=20Transform=20MCP=20Server?= =?UTF-8?q?=20Tools=20(#1132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- docs/clients/transports.mdx | 72 ++++++ docs/patterns/tool-transformation.mdx | 35 +++ src/fastmcp/client/transports.py | 18 +- src/fastmcp/mcp_config.py | 215 +++++++++-------- src/fastmcp/server/proxy.py | 16 +- src/fastmcp/server/server.py | 16 +- src/fastmcp/tools/tool_manager.py | 40 +++- src/fastmcp/tools/tool_transform.py | 98 +++++++- src/fastmcp/utilities/mcp_config.py | 26 +++ tests/server/proxy/test_proxy_server.py | 27 +++ tests/server/test_tool_transformation.py | 40 ++++ tests/{utilities => }/test_mcp_config.py | 282 ++++++++++++++++++++++- tests/tools/test_tool_manager.py | 80 +++++++ 13 files changed, 857 insertions(+), 108 deletions(-) create mode 100644 src/fastmcp/utilities/mcp_config.py create mode 100644 tests/server/test_tool_transformation.py rename tests/{utilities => }/test_mcp_config.py (50%) diff --git a/docs/clients/transports.mdx b/docs/clients/transports.mdx index 8be8c26d4..78d488af4 100644 --- a/docs/clients/transports.mdx +++ b/docs/clients/transports.mdx @@ -308,3 +308,75 @@ async with client: answer = await client.call_tool("assistant_ask", {"question": "What?"}) ``` +### Tool Transformation with FastMCP and MCPConfig + +FastMCP supports basic tool transformations to be defined alongside the MCP Servers in the MCPConfig file. + +```python +config = { + "mcpServers": { + "weather": { + "url": "https://weather.example.com/mcp", + "transport": "http", + "tools": { } # <--- This is the tool transformation section + } + } +} +``` + +With these transformations, you can transform (change) the name, title, description, tags, enablement, and arguments of a tool. + +For each argument the tool takes, you can transform (change) the name, description, default, visibility, whether it's required, and you can provide example values. + +In the following example, we're transforming the `weather_get_forecast` tool to only retrieve the weather for `Miami` and hiding the `city` argument from the client. + +```python +tool_transformations = { + "weather_get_forecast": { + "name": "miami_weather", + "description": "Get the weather for Miami", + "arguments": { + "city": { + "name": "city", + "default": "Miami", + "hide": True, + } + } + } +} + +config = { + "mcpServers": { + "weather": { + "url": "https://weather.example.com/mcp", + "transport": "http", + "tools": tool_transformations + } + } +} +``` + +#### Allowlisting and Blocklisting Tools + +Tools can be allowlisted or blocklisted from the client by applying `tags` to the tools on the server. In the following example, we're allowlisting only tools marked with the `forecast` tag, all other tools will be unavailable to the client. + +```python +tool_transformations = { + "weather_get_forecast": { + "enabled": True, + "tags": ["forecast"] + } +} + + +config = { + "mcpServers": { + "weather": { + "url": "https://weather.example.com/mcp", + "transport": "http", + "tools": tool_transformations, + "include_tags": ["forecast"] + } + } +} +``` \ No newline at end of file diff --git a/docs/patterns/tool-transformation.mdx b/docs/patterns/tool-transformation.mdx index 17462fd50..753a28765 100644 --- a/docs/patterns/tool-transformation.mdx +++ b/docs/patterns/tool-transformation.mdx @@ -441,6 +441,41 @@ mcp.add_tool(new_tool) In the above example, `**kwargs` receives the renamed argument `b`, not the original argument `y`. It is therefore recommended to use with `forward()`, not `forward_raw()`. +## Modifying MCP Tools with MCPConfig + +When running MCP Servers under FastMCP with `MCPConfig`, you can also apply a subset of tool transformations +directly in the MCPConfig json file. + +```json +{ + "mcpServers": { + "weather": { + "url": "https://weather.example.com/mcp", + "transport": "http", + "tools": { + "weather_get_forecast": { + "name": "miami_weather", + "description": "Get the weather for Miami", + "arguments": { + "city": { + "name": "city", + "default": "Miami", + "hide": True, + } + } + } + } + } + } +} +``` + +The `tools` section is a dictionary of tool names to tool configurations. Each tool configuration is a +dictionary of tool properties. + +See the [MCPConfigTransport](/clients/transports#tool-transformation-with-fastmcp-and-mcpconfig) documentation for more details. + + ## Output Schema Control diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 8cf90dfff..2ecdcf8cc 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -732,7 +732,7 @@ class MCPConfigTransport(ClientTransport): 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. + all servers on a single FastMCP instance, with each server's name, by default, 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`. @@ -772,7 +772,9 @@ class MCPConfigTransport(ClientTransport): ``` """ - def __init__(self, config: MCPConfig | dict): + def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True): + from fastmcp.utilities.mcp_config import composite_server_from_mcp_config + if isinstance(config, dict): config = MCPConfig.from_dict(config) self.config = config @@ -787,15 +789,11 @@ class MCPConfigTransport(ClientTransport): # otherwise create a composite client else: - composite_server = FastMCP() - - for name, server in self.config.mcpServers.items(): - composite_server.mount( - prefix=name, - server=FastMCP.as_proxy(backend=server.to_transport()), + self.transport = FastMCPTransport( + mcp=composite_server_from_mcp_config( + self.config, name_as_prefix=name_as_prefix ) - - self.transport = FastMCPTransport(mcp=composite_server) + ) @contextlib.asynccontextmanager async def connect_session( diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py index 029c802d1..8d5576ec5 100644 --- a/src/fastmcp/mcp_config.py +++ b/src/fastmcp/mcp_config.py @@ -23,17 +23,29 @@ Example configuration: from __future__ import annotations import datetime -import json import re from pathlib import Path from typing import TYPE_CHECKING, Annotated, Any, Literal from urllib.parse import urlparse import httpx -from pydantic import AnyUrl, BaseModel, ConfigDict, Field +from pydantic import ( + AnyUrl, + BaseModel, + ConfigDict, + Field, + ValidationInfo, + model_validator, +) +from typing_extensions import Self, override + +from fastmcp.tools.tool_transform import ToolTransformConfig +from fastmcp.utilities.types import FastMCPBaseModel if TYPE_CHECKING: from fastmcp.client.transports import ( + ClientTransport, + FastMCPTransport, SSETransport, StdioTransport, StreamableHttpTransport, @@ -60,6 +72,39 @@ def infer_transport_type_from_url( return "http" +class _TransformingMCPServerMixin(FastMCPBaseModel): + """A mixin that enables wrapping an MCP Server with tool transforms.""" + + tools: dict[str, ToolTransformConfig] = Field(...) + """The multi-tool transform to apply to the tools.""" + + include_tags: set[str] | None = Field( + default=None, + description="The tags to include in the proxy.", + ) + + exclude_tags: set[str] | None = Field( + default=None, + description="The tags to exclude in the proxy.", + ) + + def to_transport(self) -> FastMCPTransport: + """Get the transport for the server.""" + from fastmcp.client.transports import FastMCPTransport + from fastmcp.server.server import FastMCP + + transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] + + wrapped_mcp_server = FastMCP.as_proxy( + transport, + tool_transformations=self.tools, + include_tags=self.include_tags, + exclude_tags=self.exclude_tags, + ) + + return FastMCPTransport(wrapped_mcp_server) + + class StdioMCPServer(BaseModel): """MCP server configuration for stdio transport. @@ -101,6 +146,10 @@ class StdioMCPServer(BaseModel): ) +class TransformingStdioMCPServer(_TransformingMCPServerMixin, StdioMCPServer): + """A Stdio server with tool transforms.""" + + class RemoteMCPServer(BaseModel): """MCP server configuration for HTTP/SSE transport. @@ -162,120 +211,106 @@ class RemoteMCPServer(BaseModel): ) +class TransformingRemoteMCPServer(_TransformingMCPServerMixin, RemoteMCPServer): + """A Remote server with tool transforms.""" + + +TransformingMCPServerTypes = TransformingStdioMCPServer | TransformingRemoteMCPServer + +CanonicalMCPServerTypes = StdioMCPServer | RemoteMCPServer + +MCPServerTypes = TransformingMCPServerTypes | CanonicalMCPServerTypes + + class MCPConfig(BaseModel): + """A configuration object for MCP Servers that conforms to the canonical MCP configuration format + while adding additional fields for enabling FastMCP-specific features like tool transformations + and filtering by tags. + + For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class. + """ + + mcpServers: dict[str, MCPServerTypes] + + model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields + + @model_validator(mode="before") + def validate_mcp_servers(self, info: ValidationInfo) -> dict[str, Any]: + """Validate the MCP servers.""" + if not isinstance(self, dict): + raise ValueError("MCPConfig format requires a dictionary of servers.") + + if "mcpServers" not in self: + self = {"mcpServers": self} + + return self + + def add_server(self, name: str, server: MCPServerTypes) -> None: + """Add or update a server in the configuration.""" + self.mcpServers[name] = server + + @classmethod + def from_dict(cls, config: dict[str, Any]) -> Self: + """Parse MCP configuration from dictionary format.""" + return cls.model_validate(config) + + def to_dict(self) -> dict[str, Any]: + """Convert MCPConfig to dictionary format, preserving all fields.""" + return self.model_dump(exclude_none=True) + + def write_to_file(self, file_path: Path) -> None: + """Write configuration to JSON file.""" + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(self.model_dump_json(indent=2)) + + @classmethod + def from_file(cls, file_path: Path) -> Self: + """Load configuration from JSON file.""" + if file_path.exists(): + if content := file_path.read_text().strip(): + return cls.model_validate_json(content) + + return cls(mcpServers={}) + + +class CanonicalMCPConfig(MCPConfig): """Canonical MCP configuration format. This defines the standard configuration format for Model Context Protocol servers. The format is designed to be client-agnostic and extensible for future use cases. """ - mcpServers: dict[str, StdioMCPServer | RemoteMCPServer] + mcpServers: dict[str, CanonicalMCPServerTypes] - model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields - - @classmethod - def from_dict(cls, config: dict[str, Any]) -> MCPConfig: - """Parse MCP configuration from dictionary format.""" - # Handle case where config is just the mcpServers object - if "mcpServers" not in config and any( - isinstance(v, dict) and ("command" in v or "url" in v) - for v in config.values() - ): - # This looks like a bare mcpServers object - servers_dict = config - else: - # Standard format with mcpServers wrapper - servers_dict = config.get("mcpServers", {}) - - # Parse each server configuration - parsed_servers = {} - for name, server_config in servers_dict.items(): - if not isinstance(server_config, dict): - continue - - # Determine if this is stdio or remote based on fields - if "command" in server_config: - parsed_servers[name] = StdioMCPServer.model_validate(server_config) - elif "url" in server_config: - parsed_servers[name] = RemoteMCPServer.model_validate(server_config) - else: - # Skip invalid server configs but preserve them as raw dicts - # This allows for forward compatibility with unknown server types - continue - - # Create config with any extra top-level fields preserved - config_data = {k: v for k, v in config.items() if k != "mcpServers"} - config_data["mcpServers"] = parsed_servers - - return cls.model_validate(config_data) - - def to_dict(self) -> dict[str, Any]: - """Convert MCPConfig to dictionary format, preserving all fields.""" - # Start with all extra fields at the top level - result = self.model_dump(exclude={"mcpServers"}, exclude_none=True) - - # Add mcpServers with all fields preserved - result["mcpServers"] = { - name: server.model_dump(exclude_none=True) - for name, server in self.mcpServers.items() - } - - return result - - def write_to_file(self, file_path: Path) -> None: - """Write configuration to JSON file.""" - file_path.parent.mkdir(parents=True, exist_ok=True) - with open(file_path, "w") as f: - json.dump(self.to_dict(), f, indent=2) - - @classmethod - def from_file(cls, file_path: Path) -> MCPConfig: - """Load configuration from JSON file.""" - if not file_path.exists(): - return cls(mcpServers={}) - with open(file_path) as f: - content = f.read().strip() - if not content: - return cls(mcpServers={}) - data = json.loads(content) - return cls.from_dict(data) - - def add_server(self, name: str, server: StdioMCPServer | RemoteMCPServer) -> None: + @override + def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None: """Add or update a server in the configuration.""" self.mcpServers[name] = server - def remove_server(self, name: str) -> None: - """Remove a server from the configuration.""" - if name in self.mcpServers: - del self.mcpServers[name] - def update_config_file( file_path: Path, server_name: str, - server_config: StdioMCPServer | RemoteMCPServer, + server_config: CanonicalMCPServerTypes, ) -> None: - """Update MCP configuration file with new server, preserving existing fields.""" + """Update an MCP configuration file from a server object, preserving existing fields. + + This is used for updating the mcpServer configurations of third-party tools so we do not + worry about transforming server objects here.""" config = MCPConfig.from_file(file_path) # If updating an existing server, merge with existing configuration # to preserve any unknown fields - if server_name in config.mcpServers: - existing_server = config.mcpServers[server_name] + if existing_server := config.mcpServers.get(server_name): # Get the raw dict representation of both servers existing_dict = existing_server.model_dump() + new_dict = server_config.model_dump(exclude_none=True) # Merge, with new values taking precedence - merged_dict = {**existing_dict, **new_dict} + merged_config = server_config.model_validate({**existing_dict, **new_dict}) - # Create new server instance with merged data - if "command" in merged_dict: - merged_server = StdioMCPServer.model_validate(merged_dict) - else: - merged_server = RemoteMCPServer.model_validate(merged_dict) - - config.add_server(server_name, merged_server) + config.add_server(server_name, merged_config) else: config.add_server(server_name, server_config) diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py index 576b75ef2..cfd8ba57a 100644 --- a/src/fastmcp/server/proxy.py +++ b/src/fastmcp/server/proxy.py @@ -36,6 +36,9 @@ from fastmcp.server.dependencies import get_context from fastmcp.server.server import FastMCP from fastmcp.tools.tool import Tool, ToolResult from fastmcp.tools.tool_manager import ToolManager +from fastmcp.tools.tool_transform import ( + apply_transformations_to_tools, +) from fastmcp.utilities.components import MirroredComponent from fastmcp.utilities.logging import get_logger @@ -71,7 +74,12 @@ class ProxyToolManager(ToolManager): else: raise e - return all_tools + transformed_tools = apply_transformations_to_tools( + tools=all_tools, + transformations=self.transformations, + ) + + return transformed_tools async def list_tools(self) -> list[Tool]: """Gets the filtered list of tools including local, mounted, and proxy tools.""" @@ -469,7 +477,11 @@ class FastMCPProxy(FastMCP): raise ValueError("Must specify 'client_factory'") # Replace the default managers with our specialized proxy managers. - self._tool_manager = ProxyToolManager(client_factory=self.client_factory) + self._tool_manager = ProxyToolManager( + client_factory=self.client_factory, + # Propagate the transformations from the base class tool manager + transformations=self._tool_manager.transformations, + ) self._resource_manager = ProxyResourceManager( client_factory=self.client_factory ) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 240c29a26..33036d39d 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -26,6 +26,7 @@ from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions from mcp.server.stdio import stdio_server from mcp.types import ( AnyFunction, + CallToolRequestParams, ContentBlock, GetPromptResult, ToolAnnotations, @@ -60,6 +61,7 @@ from fastmcp.server.middleware import Middleware, MiddlewareContext from fastmcp.settings import Settings from fastmcp.tools import ToolManager from fastmcp.tools.tool import FunctionTool, Tool, ToolResult +from fastmcp.tools.tool_transform import ToolTransformConfig from fastmcp.utilities.cache import TimedCache from fastmcp.utilities.cli import log_server_banner from fastmcp.utilities.components import FastMCPComponent @@ -138,6 +140,7 @@ class FastMCP(Generic[LifespanResultT]): resource_prefix_format: Literal["protocol", "path"] | None = None, mask_error_details: bool | None = None, tools: list[Tool | Callable[..., Any]] | None = None, + tool_transformations: dict[str, ToolTransformConfig] | None = None, dependencies: list[str] | None = None, include_tags: set[str] | None = None, exclude_tags: set[str] | None = None, @@ -167,6 +170,7 @@ class FastMCP(Generic[LifespanResultT]): self._tool_manager = ToolManager( duplicate_behavior=on_duplicate_tools, mask_error_details=mask_error_details, + transformations=tool_transformations, ) self._resource_manager = ResourceManager( duplicate_behavior=on_duplicate_resources, @@ -650,7 +654,7 @@ class FastMCP(Generic[LifespanResultT]): key=context.message.name, arguments=context.message.arguments or {} ) - mw_context = MiddlewareContext( + mw_context = MiddlewareContext[CallToolRequestParams]( message=mcp.types.CallToolRequestParams(name=key, arguments=arguments), source="client", type="request", @@ -806,6 +810,16 @@ class FastMCP(Generic[LifespanResultT]): except RuntimeError: pass # No context available + def add_tool_transformation( + self, tool_name: str, transformation: ToolTransformConfig + ) -> None: + """Add a tool transformation.""" + self._tool_manager.add_tool_transformation(tool_name, transformation) + + def remove_tool_transformation(self, tool_name: str) -> None: + """Remove a tool transformation.""" + self._tool_manager.remove_tool_transformation(tool_name) + @overload def tool( self, diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 90737984b..18ea883c0 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -10,6 +10,10 @@ from fastmcp import settings from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.settings import DuplicateBehavior from fastmcp.tools.tool import Tool, ToolResult +from fastmcp.tools.tool_transform import ( + ToolTransformConfig, + apply_transformations_to_tools, +) from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: @@ -25,10 +29,12 @@ class ToolManager: self, duplicate_behavior: DuplicateBehavior | None = None, mask_error_details: bool | None = None, + transformations: dict[str, ToolTransformConfig] | None = None, ): self._tools: dict[str, Tool] = {} self._mounted_servers: list[MountedServer] = [] self.mask_error_details = mask_error_details or settings.mask_error_details + self.transformations = transformations or {} # Default to "warn" if None is provided if duplicate_behavior is None: @@ -82,7 +88,13 @@ class ToolManager: # Finally, add local tools, which always take precedence all_tools.update(self._tools) - return all_tools + + transformed_tools = apply_transformations_to_tools( + tools=all_tools, + transformations=self.transformations, + ) + + return transformed_tools async def has_tool(self, key: str) -> bool: """Check if a tool exists.""" @@ -109,6 +121,15 @@ class ToolManager: tools_dict = await self._load_tools(via_server=True) return list(tools_dict.values()) + @property + def _tools_transformed(self) -> list[str]: + """Get the local tools.""" + + return [ + transformation.name or tool_name + for tool_name, transformation in self.transformations.items() + ] + def add_tool_from_fn( self, fn: Callable[..., Any], @@ -155,6 +176,21 @@ class ToolManager: self._tools[tool.key] = tool return tool + def add_tool_transformation( + self, tool_name: str, transformation: ToolTransformConfig + ) -> None: + """Add a tool transformation.""" + self.transformations[tool_name] = transformation + + def get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None: + """Get a tool transformation.""" + return self.transformations.get(tool_name) + + def remove_tool_transformation(self, tool_name: str) -> None: + """Remove a tool transformation.""" + if tool_name in self.transformations: + del self.transformations[tool_name] + def remove_tool(self, key: str) -> None: """Remove a tool from the server. @@ -175,7 +211,7 @@ class ToolManager: filtered protocol path. """ # 1. Check local tools first. The server will have already applied its filter. - if key in self._tools: + if key in self._tools or key in self._tools_transformed: tool = await self.get_tool(key) if not tool: raise NotFoundError(f"Tool {key!r} not found") diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index 09c89cc2f..aca45d8f8 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -4,14 +4,22 @@ import inspect from collections.abc import Callable from contextvars import ContextVar from dataclasses import dataclass -from typing import Any, Literal +from typing import Annotated, Any, Literal from mcp.types import ToolAnnotations from pydantic import ConfigDict +from pydantic.fields import Field +from pydantic.functional_validators import BeforeValidator from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content +from fastmcp.utilities.components import FastMCPComponent, _convert_set_default_none from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.types import NotSet, NotSetT, get_cached_typeadapter +from fastmcp.utilities.types import ( + FastMCPBaseModel, + NotSet, + NotSetT, + get_cached_typeadapter, +) logger = get_logger(__name__) @@ -193,6 +201,30 @@ class ArgTransform: ) +class ArgTransformConfig(FastMCPBaseModel): + """A model for requesting a single argument transform.""" + + name: str | None = Field(default=None, description="The new name for the argument.") + description: str | None = Field( + default=None, description="The new description for the argument." + ) + default: str | int | float | bool | None = Field( + default=None, description="The new default value for the argument." + ) + hide: bool = Field( + default=False, description="Whether to hide the argument from the tool." + ) + required: Literal[True] | None = Field( + default=None, description="Whether the argument is required." + ) + examples: Any | None = Field(default=None, description="Examples of the argument.") + + def to_arg_transform(self) -> ArgTransform: + """Convert the argument transform to a FastMCP argument transform.""" + + return ArgTransform(**self.model_dump(exclude_unset=True)) # pyright: ignore[reportAny] + + class TransformedTool(Tool): """A tool that is transformed from another tool. @@ -798,3 +830,65 @@ class TransformedTool(Tool): return any( p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() ) + + +class ToolTransformConfig(FastMCPComponent): + """Provides a way to transform a tool.""" + + name: str | None = Field(default=None, description="The new name for the tool.") + + title: str | None = Field( + default=None, + description="The new title of the tool.", + ) + description: str | None = Field( + default=None, + description="The new description of the tool.", + ) + tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field( + default_factory=set, + description="The new tags for the tool.", + ) + + enabled: bool = Field( + default=True, + description="Whether the tool is enabled.", + ) + + arguments: dict[str, ArgTransformConfig] = Field( + default_factory=dict, + description="A dictionary of argument transforms to apply to the tool.", + ) + + def apply(self, tool: Tool) -> TransformedTool: + """Create a TransformedTool from a provided tool and this transformation configuration.""" + + tool_changes = self.model_dump(exclude_unset=True, exclude={"arguments"}) + + return TransformedTool.from_tool( + tool=tool, + **tool_changes, + transform_args={k: v.to_arg_transform() for k, v in self.arguments.items()}, + ) + + +def apply_transformations_to_tools( + tools: dict[str, Tool], + transformations: dict[str, ToolTransformConfig], +) -> dict[str, Tool]: + """Apply a list of transformations to a list of tools. Tools that do not have any transforamtions + are left unchanged. + """ + + transformed_tools = {} + + for tool_name, tool in tools.items(): + if transformation := transformations.get(tool_name): + transformed_tools[transformation.name or tool_name] = transformation.apply( + tool + ) + continue + + transformed_tools[tool_name] = tool + + return transformed_tools diff --git a/src/fastmcp/utilities/mcp_config.py b/src/fastmcp/utilities/mcp_config.py new file mode 100644 index 000000000..3d868bd06 --- /dev/null +++ b/src/fastmcp/utilities/mcp_config.py @@ -0,0 +1,26 @@ +from fastmcp.mcp_config import MCPConfig +from fastmcp.server.server import FastMCP + + +def composite_server_from_mcp_config( + config: MCPConfig, name_as_prefix: bool = True +) -> FastMCP: + """A utility function to create a composite server from an MCPConfig.""" + composite_server = FastMCP() + + mount_mcp_config_into_server(config, composite_server, name_as_prefix) + + return composite_server + + +def mount_mcp_config_into_server( + config: MCPConfig, + server: FastMCP, + name_as_prefix: bool = True, +) -> None: + """A utility function to mount the servers from an MCPConfig into a FastMCP server.""" + for name, mcp_server in config.mcpServers.items(): + server.mount( + prefix=name if name_as_prefix else None, + server=FastMCP.as_proxy(backend=mcp_server.to_transport()), + ) diff --git a/tests/server/proxy/test_proxy_server.py b/tests/server/proxy/test_proxy_server.py index 3aabb3287..dea8d3259 100644 --- a/tests/server/proxy/test_proxy_server.py +++ b/tests/server/proxy/test_proxy_server.py @@ -12,6 +12,9 @@ from fastmcp.client import Client from fastmcp.client.transports import FastMCPTransport, StreamableHttpTransport from fastmcp.exceptions import ToolError from fastmcp.server.proxy import FastMCPProxy, ProxyClient +from fastmcp.tools.tool_transform import ( + ToolTransformConfig, +) USERS = [ {"id": "1", "name": "Alice", "active": True}, @@ -118,6 +121,30 @@ class TestTools: assert "error_tool" in tools assert "tool_without_description" in tools + async def test_get_transformed_tools( + self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy + ): + """An explicit None description should change the tool description to None.""" + + fastmcp_server.add_tool_transformation( + "add", ToolTransformConfig(name="add_transformed") + ) + tools = await proxy_server.get_tools() + assert "add_transformed" in tools + assert "add" not in tools + + async def test_call_transformed_tools( + self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy + ): + """An explicit None description should change the tool description to None.""" + + fastmcp_server.add_tool_transformation( + "add", ToolTransformConfig(name="add_transformed") + ) + async with Client(proxy_server) as client: + result = await client.call_tool("add_transformed", {"a": 1, "b": 2}) + assert result.data == 3 + async def test_tool_without_description(self, proxy_server): tools = await proxy_server.get_tools() assert tools["tool_without_description"].description is None diff --git a/tests/server/test_tool_transformation.py b/tests/server/test_tool_transformation.py new file mode 100644 index 000000000..7c2095482 --- /dev/null +++ b/tests/server/test_tool_transformation.py @@ -0,0 +1,40 @@ +from fastmcp import FastMCP +from fastmcp.tools.tool_transform import ToolTransformConfig + + +async def test_tool_transformation_in_tool_manager(): + """Test that tool transformations are applied in the tool manager.""" + mcp = FastMCP("Test Server") + + @mcp.tool() + def echo(message: str) -> str: + """Echo back the message provided.""" + return message + + mcp.add_tool_transformation("echo", ToolTransformConfig(name="echo_transformed")) + + tools_dict = await mcp._tool_manager.get_tools() + tools = list(tools_dict.values()) + assert len(tools) == 1 + assert "echo_transformed" in tools_dict + assert tools_dict["echo_transformed"].name == "echo_transformed" + + +async def test_transformed_tool_filtering(): + """Test that tool transformations are applied in the tool manager.""" + mcp = FastMCP("Test Server", include_tags={"enabled_tools"}) + + @mcp.tool() + def echo(message: str) -> str: + """Echo back the message provided.""" + return message + + tools = list(await mcp._list_tools()) + assert len(tools) == 0 + + mcp.add_tool_transformation( + "echo", ToolTransformConfig(name="echo_transformed", tags={"enabled_tools"}) + ) + + tools = list(await mcp._list_tools()) + assert len(tools) == 1 diff --git a/tests/utilities/test_mcp_config.py b/tests/test_mcp_config.py similarity index 50% rename from tests/utilities/test_mcp_config.py rename to tests/test_mcp_config.py index b0d5822cf..ac7a8cff9 100644 --- a/tests/utilities/test_mcp_config.py +++ b/tests/test_mcp_config.py @@ -1,16 +1,31 @@ import inspect +import tempfile +from collections.abc import AsyncGenerator from pathlib import Path +from typing import Any + +import pytest from fastmcp.client.auth.bearer import BearerAuth from fastmcp.client.auth.oauth import OAuthClientProvider from fastmcp.client.client import Client from fastmcp.client.logging import LogMessage from fastmcp.client.transports import ( + MCPConfigTransport, SSETransport, StdioTransport, StreamableHttpTransport, ) -from fastmcp.mcp_config import MCPConfig, RemoteMCPServer, StdioMCPServer +from fastmcp.mcp_config import ( + CanonicalMCPConfig, + CanonicalMCPServerTypes, + MCPConfig, + MCPServerTypes, + RemoteMCPServer, + StdioMCPServer, + TransformingStdioMCPServer, +) +from fastmcp.tools.tool import Tool as FastMCPTool def test_parse_single_stdio_config(): @@ -29,6 +44,74 @@ def test_parse_single_stdio_config(): assert transport.args == ["hello"] +def test_parse_extra_keys(): + config = { + "mcpServers": { + "test_server": { + "command": "echo", + "args": ["hello"], + "leaf_extra": "leaf_extra", + } + }, + "root_extra": "root_extra", + } + mcp_config = MCPConfig.from_dict(config) + + serialized_mcp_config = mcp_config.to_dict() + assert serialized_mcp_config["root_extra"] == "root_extra" + assert ( + serialized_mcp_config["mcpServers"]["test_server"]["leaf_extra"] == "leaf_extra" + ) + + +def test_parse_mcpservers_at_root(): + config = { + "test_server": { + "command": "echo", + "args": ["hello"], + } + } + + mcp_config = MCPConfig.from_dict(config) + + serialized_mcp_config = mcp_config.model_dump() + assert serialized_mcp_config["mcpServers"]["test_server"]["command"] == "echo" + assert serialized_mcp_config["mcpServers"]["test_server"]["args"] == ["hello"] + + +def test_parse_mcpservers_discriminator(): + """Test that the MCPConfig discriminator produces StdioMCPServer for a non-transforming server + and TransformingStdioMCPServer for a transforming server.""" + + config = { + "test_server": { + "command": "echo", + "args": ["hello"], + }, + "test_server_two": {"command": "echo", "args": ["hello"], "tools": {}}, + } + + mcp_config = MCPConfig.from_dict(config) + + test_server: MCPServerTypes = mcp_config.mcpServers["test_server"] + assert isinstance(test_server, StdioMCPServer) + + test_server_two: MCPServerTypes = mcp_config.mcpServers["test_server_two"] + assert isinstance(test_server_two, TransformingStdioMCPServer) + + canonical_mcp_config = CanonicalMCPConfig.from_dict(config) + + canonical_test_server: CanonicalMCPServerTypes = canonical_mcp_config.mcpServers[ + "test_server" + ] + assert isinstance(canonical_test_server, StdioMCPServer) + + canonical_test_server_two: CanonicalMCPServerTypes = ( + canonical_mcp_config.mcpServers["test_server_two"] + ) + assert isinstance(canonical_test_server_two, StdioMCPServer) + + def test_parse_single_remote_config(): config = { "mcpServers": { @@ -244,6 +327,172 @@ async def test_multi_client_with_logging(tmp_path: Path): assert MESSAGES[0].data == "test 42" +async def test_multi_client_with_transforms(tmp_path: Path): + """ + Tests that transforms are properly applied to the tools. + """ + server_script = inspect.cleandoc(""" + from fastmcp import FastMCP + + mcp = FastMCP() + + @mcp.tool + def add(a: int, b: int) -> int: + return a + b + + if __name__ == '__main__': + mcp.run() + """) + + script_path = tmp_path / "test.py" + script_path.write_text(server_script) + + config = { + "mcpServers": { + "test_1": { + "command": "python", + "args": [str(script_path)], + "tools": { + "add": { + "name": "transformed_add", + "arguments": { + "a": {"name": "transformed_a"}, + "b": {"name": "transformed_b"}, + }, + } + }, + }, + "test_2": { + "command": "python", + "args": [str(script_path)], + }, + } + } + + client = Client[MCPConfigTransport](config) + + async with client: + tools = await client.list_tools() + tools_by_name = {tool.name: tool for tool in tools} + assert len(tools) == 2 + assert "test_1_transformed_add" in tools_by_name + + result = await client.call_tool( + "test_1_transformed_add", {"transformed_a": 1, "transformed_b": 2} + ) + assert result.data == 3 + + +async def test_canonical_multi_client_with_transforms(tmp_path: Path): + """Test that transforms are not applied to servers in a canonical MCPConfig.""" + server_script = inspect.cleandoc(""" + from fastmcp import FastMCP + + mcp = FastMCP() + + @mcp.tool + def add(a: int, b: int) -> int: + return a + b + + if __name__ == '__main__': + mcp.run() + """) + + script_path = tmp_path / "test.py" + script_path.write_text(server_script) + + config = CanonicalMCPConfig( + mcpServers={ + "test_1": { + "command": "python", + "args": [str(script_path)], + "tools": { # <--- Will be ignored as its not valid for a canonical MCPConfig + "add": { + "name": "transformed_add", + "arguments": { + "a": {"name": "transformed_a"}, + "b": {"name": "transformed_b"}, + }, + } + }, + }, + "test_2": { + "command": "python", + "args": [str(script_path)], + }, + } # type: ignore[reportUnknownArgumentType] + ) + + client = Client(config) + + async with client: + tools = await client.list_tools() + tools_by_name = {tool.name: tool for tool in tools} + assert len(tools) == 2 + assert "test_1_transformed_add" not in tools_by_name + + +async def test_multi_client_transform_with_filtering(tmp_path: Path): + """ + Tests that tag-based filtering works when using a transforming MCPConfig. + """ + server_script = inspect.cleandoc(""" + from fastmcp import FastMCP + + mcp = FastMCP() + + @mcp.tool + def add(a: int, b: int) -> int: + return a + b + + @mcp.tool + def subtract(a: int, b: int) -> int: + return a - b + + if __name__ == '__main__': + mcp.run() + """) + + script_path = tmp_path / "test.py" + script_path.write_text(server_script) + + config = { + "mcpServers": { + "test_1": { + "command": "python", + "args": [str(script_path)], + "tools": { + "add": { + "name": "transformed_add", + "tags": ["keep"], + "arguments": { + "a": {"name": "transformed_a"}, + "b": {"name": "transformed_b"}, + }, + }, + }, + "include_tags": ["keep"], + }, + "test_2": { + "command": "python", + "args": [str(script_path)], + }, + } + } + + client = Client[MCPConfigTransport](config) + + async with client: + tools = await client.list_tools() + tools_by_name = {tool.name: tool for tool in tools} + assert len(tools) == 3 + assert "test_1_transformed_add" in tools_by_name + assert "test_1_add" not in tools_by_name + assert "test_1_subtract" not in tools_by_name + assert "test_2_add" in tools_by_name + assert "test_2_subtract" in tools_by_name + + async def test_multi_client_with_elicitation(tmp_path: Path): """ Tests that elicitation is properly forwarded to the ultimate client. @@ -284,3 +533,34 @@ async def test_multi_client_with_elicitation(tmp_path: Path): async with Client(config, elicitation_handler=elicitation_handler) as client: result = await client.call_tool("test_server_elicit_test", {}) assert result.data == 42 + + +def sample_tool_fn(arg1: int, arg2: str) -> str: + return f"Hello, world! {arg1} {arg2}" + + +@pytest.fixture +def sample_tool() -> FastMCPTool: + return FastMCPTool.from_function(sample_tool_fn, name="sample_tool") + + +@pytest.fixture +async def test_script(tmp_path: Path) -> AsyncGenerator[Path, Any]: + with tempfile.NamedTemporaryFile() as f: + f.write(b""" + from fastmcp import FastMCP + + mcp = FastMCP() + + @mcp.tool + def fetch(url: str) -> str: + + return f"Hello, world! {url}" + + if __name__ == '__main__': + mcp.run() + """) + + yield Path(f.name) + + pass diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index 42ef6038e..065d90607 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -12,6 +12,7 @@ from fastmcp import Context, FastMCP from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.tools import FunctionTool, ToolManager from fastmcp.tools.tool import Tool +from fastmcp.tools.tool_transform import ArgTransformConfig, ToolTransformConfig from fastmcp.utilities.tests import caplog_for_fastmcp from fastmcp.utilities.types import Image @@ -262,6 +263,52 @@ class TestAddTools: assert result.fn.__name__ == "replacement_fn" +class TestListTools: + async def test_list_tools_with_transformed_names(self): + """Test listing tools with transformations.""" + + tool_manager = ToolManager() + + def add(a: int, b: int) -> int: + return a + b + + tool = Tool.from_function(add) + tool_manager.add_tool(tool) + + tool_manager.add_tool_transformation( + "add", ToolTransformConfig(name="add_transformed") + ) + tools = await tool_manager.list_tools() + tools_by_name = {tool.name: tool for tool in tools} + assert "add_transformed" in tools_by_name + assert "add" not in tools_by_name + + async def test_list_tools_with_transforms(self): + """Test listing tools with transformations.""" + + tool_manager = ToolManager() + + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + tool = Tool.from_function(add) + tool_manager.add_tool(tool) + + tool_manager.add_tool_transformation( + "add", + ToolTransformConfig( + name="add_transformed", description=None, tags={"enabled_tools"} + ), + ) + tools = await tool_manager.list_tools() + tools_by_name = {tool.name: tool for tool in tools} + assert "add_transformed" in tools_by_name + assert "add" not in tools_by_name + assert tools_by_name["add_transformed"].description is None + assert tools_by_name["add_transformed"].tags == {"enabled_tools"} + + class TestToolTags: """Test functionality related to tool tags.""" @@ -431,6 +478,39 @@ class TestCallTools: with pytest.raises(NotFoundError, match="Tool 'unknown' not found"): await manager.call_tool("unknown", {"a": 1}) + async def test_call_transformed_tool(self): + manager = ToolManager() + + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + tool = Tool.from_function(add) + manager.add_tool(tool) + + manager.add_tool_transformation( + "add", + ToolTransformConfig( + name="add_transformed", + description=None, + tags={"enabled_tools"}, + arguments={ + "a": ArgTransformConfig( + name="a_transformed", description=None, default=1 + ), + "b": ArgTransformConfig( + name="b_transformed", description=None, default=2 + ), + }, + ), + ) + + result = await manager.call_tool( + "add_transformed", {"a_transformed": 1, "b_transformed": 2} + ) + assert result.content[0].text == "3" # type: ignore[attr-defined] + assert result.structured_content == {"result": 3} + async def test_call_tool_with_list_int_input(self): def sum_vals(vals: list[int]) -> int: return sum(vals)