diff --git a/docs/development/upgrade-guide.mdx b/docs/development/upgrade-guide.mdx index e340276410..36003d1560 100644 --- a/docs/development/upgrade-guide.mdx +++ b/docs/development/upgrade-guide.mdx @@ -10,6 +10,31 @@ This guide provides migration instructions for breaking changes and major update ## v3.0.0 +### Provider Architecture + +FastMCP v3 introduces a unified provider architecture for sourcing components. All tools, resources, and prompts now flow through providers: + +- **LocalProvider** stores decorator-registered components (`@mcp.tool`, etc.) +- **FastMCPProvider** wraps another FastMCP server for composition +- **ProxyProvider** connects to remote MCP servers +- **TransformingProvider** adds namespacing and renaming + +See [Providers](/servers/providers/overview) for the complete documentation. + +### Mount Namespace Parameter + +The `prefix` parameter for `mount()` has been renamed to `namespace`: + + +```python Before +main.mount(subserver, prefix="api") +``` + +```python After +main.mount(subserver, namespace="api") +``` + + ### Component Enable/Disable The `enabled` field and `enable()`/`disable()` methods have been removed from component objects. Use server or provider methods instead: diff --git a/docs/docs.json b/docs/docs.json index 60a072fcbd..e9ddc44d6b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -20,10 +20,7 @@ "primary": "#2d00f7" }, "contextual": { - "options": [ - "copy", - "view" - ] + "options": ["copy", "view"] }, "description": "The fast, Pythonic way to build MCP servers and clients.", "errors": { @@ -95,7 +92,7 @@ "pages": [ "servers/server", { - "group": "Core Components", + "group": "Components", "icon": "toolbox", "pages": [ "servers/tools", @@ -104,20 +101,30 @@ ] }, { - "group": "Advanced Features", + "group": "Providers", + "icon": "layer-group", + "pages": [ + "servers/providers/overview", + "servers/providers/local", + "servers/providers/mounting", + "servers/providers/namespacing", + "servers/providers/proxy", + "servers/providers/custom" + ] + }, + { + "group": "Features", "icon": "stars", "pages": [ - "servers/composition", + "servers/tasks", "servers/context", "servers/elicitation", "servers/icons", "servers/logging", "servers/middleware", "servers/progress", - "servers/proxy", "servers/sampling", - "servers/storage-backends", - "servers/tasks" + "servers/storage-backends" ] }, { @@ -150,10 +157,7 @@ { "group": "Essentials", "icon": "cube", - "pages": [ - "clients/client", - "clients/transports" - ] + "pages": ["clients/client", "clients/transports"] }, { "group": "Core Operations", @@ -180,16 +184,18 @@ { "group": "Authentication", "icon": "user-shield", - "pages": [ - "clients/auth/oauth", - "clients/auth/bearer" - ] + "pages": ["clients/auth/oauth", "clients/auth/bearer"] } ] }, { "group": "Integrations", "pages": [ + { + "group": "Providers", + "icon": "globe", + "pages": ["integrations/fastapi", "integrations/openapi"] + }, { "group": "Authentication", "icon": "key", @@ -236,14 +242,6 @@ "integrations/gemini", "integrations/openai" ] - }, - { - "group": "API Integration", - "icon": "globe", - "pages": [ - "integrations/fastapi", - "integrations/openapi" - ] } ] }, @@ -476,6 +474,7 @@ "group": "fastmcp.utilities", "pages": [ "python-sdk/fastmcp-utilities-__init__", + "python-sdk/fastmcp-utilities-async_utils", "python-sdk/fastmcp-utilities-auth", "python-sdk/fastmcp-utilities-cli", "python-sdk/fastmcp-utilities-components", @@ -543,12 +542,20 @@ }, "redirects": [ { - "destination": "/servers/proxy", + "destination": "/servers/providers/proxy", "source": "/patterns/proxy" }, { - "destination": "/servers/composition", + "destination": "/servers/providers/mounting", "source": "/patterns/composition" + }, + { + "destination": "/servers/providers/proxy", + "source": "/servers/proxy" + }, + { + "destination": "/servers/providers/mounting", + "source": "/servers/composition" } ], "search": { diff --git a/docs/integrations/fastapi.mdx b/docs/integrations/fastapi.mdx index 683bc244e3..bb1ec2e378 100644 --- a/docs/integrations/fastapi.mdx +++ b/docs/integrations/fastapi.mdx @@ -12,6 +12,10 @@ FastMCP provides two powerful ways to integrate with FastAPI applications: 1. **[Generate an MCP server FROM your FastAPI app](#generating-an-mcp-server)** - Convert existing API endpoints into MCP tools 2. **[Mount an MCP server INTO your FastAPI app](#mounting-an-mcp-server)** - Add MCP functionality to your web application + +When generating an MCP server from FastAPI, FastMCP uses OpenAPIProvider (v3.0.0+) under the hood to source tools from your FastAPI app's OpenAPI spec. See [Providers](/servers/providers/overview) to understand how FastMCP sources components. + + Generating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters. diff --git a/docs/integrations/openapi.mdx b/docs/integrations/openapi.mdx index 8662a0c05c..ed102986a2 100644 --- a/docs/integrations/openapi.mdx +++ b/docs/integrations/openapi.mdx @@ -11,6 +11,10 @@ import { VersionBadge } from '/snippets/version-badge.mdx' FastMCP can automatically generate an MCP server from any OpenAPI specification, allowing AI models to interact with existing APIs through the MCP protocol. Instead of manually creating tools and resources, you provide an OpenAPI spec and FastMCP intelligently converts API endpoints into the appropriate MCP components. + +Under the hood, OpenAPI integration uses OpenAPIProvider (v3.0.0+) to source tools from the specification. See [Providers](/servers/providers/overview) to understand how FastMCP sources components. + + Generating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters. diff --git a/docs/patterns/tool-transformation.mdx b/docs/patterns/tool-transformation.mdx index b37808f3b7..1e5f44082d 100644 --- a/docs/patterns/tool-transformation.mdx +++ b/docs/patterns/tool-transformation.mdx @@ -698,7 +698,7 @@ This pattern provides several benefits: - **Scalable**: Easily add new tools by wrapping additional client methods ### Adapting Remote or Generated Tools -This is one of the most common reasons to use tool transformation. Tools from remote MCP servers (via a [proxy](/servers/proxy)) or generated from an [OpenAPI spec](/integrations/openapi) are often too generic for direct use by an LLM. You can use transformation to create a simpler, more intuitive version for your specific needs. +This is one of the most common reasons to use tool transformation. Tools from remote MCP servers (via a [proxy](/servers/providers/proxy)) or generated from an [OpenAPI spec](/integrations/openapi) are often too generic for direct use by an LLM. You can use transformation to create a simpler, more intuitive version for your specific needs. ### Chaining Transformations You can chain transformations by using an already transformed tool as the parent for a new transformation. This lets you build up complex behaviors in layers, for example, first renaming arguments, and then adding validation logic to the renamed tool. diff --git a/docs/servers/composition.mdx b/docs/servers/composition.mdx deleted file mode 100644 index 6c614000c6..0000000000 --- a/docs/servers/composition.mdx +++ /dev/null @@ -1,326 +0,0 @@ ---- -title: Server Composition -sidebarTitle: Composition -description: Combine multiple FastMCP servers into a single, larger application using mounting and importing. -icon: puzzle-piece ---- -import { VersionBadge } from '/snippets/version-badge.mdx' - - - -As your MCP applications grow, you might want to organize your tools, resources, and prompts into logical modules or reuse existing server components. FastMCP supports composition through two methods: - -- **`import_server`**: For a one-time copy of components with prefixing (static composition). -- **`mount`**: For creating a live link where the main server delegates requests to the subserver (dynamic composition). - -## Why Compose Servers? - -- **Modularity**: Break down large applications into smaller, focused servers (e.g., a `WeatherServer`, a `DatabaseServer`, a `CalendarServer`). -- **Reusability**: Create common utility servers (e.g., a `TextProcessingServer`) and mount them wherever needed. -- **Teamwork**: Different teams can work on separate FastMCP servers that are later combined. -- **Organization**: Keep related functionality grouped together logically. - -### Importing vs Mounting - -The choice of importing or mounting depends on your use case and requirements. - -| Feature | Importing | Mounting | -|---------|----------------|---------| -| **Method** | `FastMCP.import_server(server, prefix=None)` | `FastMCP.mount(server, prefix=None)` | -| **Composition Type** | One-time copy (static) | Live link (dynamic) | -| **Updates** | Changes to subserver NOT reflected | Changes to subserver immediately reflected | -| **Performance** | Fast - no runtime delegation | Slower - affected by slowest mounted server | -| **Prefix** | Optional - omit for original names | Optional - omit for original names | -| **Best For** | Bundling finalized components, performance-critical setups | Modular runtime composition | - -### Proxy Servers - -FastMCP supports [MCP proxying](/servers/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting. - - - -You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time. - -Prefixing rules for tools, prompts, resources, and templates are identical across importing, mounting, and proxies. When prefixes are used, resource URIs are prefixed using path format (since 2.4.0): `resource://prefix/path/to/resource`. - -## Importing (Static Composition) - -The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). An optional `prefix` can be provided to avoid naming conflicts. If no prefix is provided, components are imported without modification. When multiple servers are imported with the same prefix (or no prefix), the most recently imported server's components take precedence. - -```python -from fastmcp import FastMCP -import asyncio - -# Define subservers -weather_mcp = FastMCP(name="WeatherService") - -@weather_mcp.tool -def get_forecast(city: str) -> dict: - """Get weather forecast.""" - return {"city": city, "forecast": "Sunny"} - -@weather_mcp.resource("data://cities/supported") -def list_supported_cities() -> list[str]: - """List cities with weather support.""" - return ["London", "Paris", "Tokyo"] - -# Define main server -main_mcp = FastMCP(name="MainApp") - -# Import subserver -async def setup(): - await main_mcp.import_server(weather_mcp, prefix="weather") - -# Result: main_mcp now contains prefixed components: -# - Tool: "weather_get_forecast" -# - Resource: "data://weather/cities/supported" - -if __name__ == "__main__": - asyncio.run(setup()) - main_mcp.run() -``` - -### How Importing Works - -When you call `await main_mcp.import_server(subserver, prefix={whatever})`: - -1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`. - - `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`. -2. **Resources**: All resources are added with both URIs and names prefixed. - - URI: `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="data://{prefix}/info")`. - - Name: `resource.name` becomes `"{prefix}_{resource.name}"`. -3. **Resource Templates**: Templates are prefixed similarly to resources. - - URI: `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="data://{prefix}/{id}")`. - - Name: `template.name` becomes `"{prefix}_{template.name}"`. -4. **Prompts**: All prompts are added with names prefixed using `{prefix}_`. - - `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`. - -Note that `import_server` performs a **one-time copy** of components. Changes made to the `subserver` *after* importing **will not** be reflected in `main_mcp`. The `subserver`'s `lifespan` context is also **not** executed by the main server. - - -The `prefix` parameter is optional. If omitted, components are imported without modification. - - -#### Importing Without Prefixes - - - -You can also import servers without specifying a prefix, which copies components using their original names: - -```python - -from fastmcp import FastMCP -import asyncio - -# Define subservers -weather_mcp = FastMCP(name="WeatherService") - -@weather_mcp.tool -def get_forecast(city: str) -> dict: - """Get weather forecast.""" - return {"city": city, "forecast": "Sunny"} - -@weather_mcp.resource("data://cities/supported") -def list_supported_cities() -> list[str]: - """List cities with weather support.""" - return ["London", "Paris", "Tokyo"] - -# Define main server -main_mcp = FastMCP(name="MainApp") - -# Import subserver -async def setup(): - # Import without prefix - components keep original names - await main_mcp.import_server(weather_mcp) - -# Result: main_mcp now contains: -# - Tool: "get_forecast" (original name preserved) -# - Resource: "data://cities/supported" (original URI preserved) - -if __name__ == "__main__": - asyncio.run(setup()) - main_mcp.run() -``` - -#### Conflict Resolution - - - -When importing multiple servers with the same prefix, or no prefix, components from the **most recently imported** server take precedence. - - - - -## Mounting (Live Linking) - -The `mount()` method creates a **live link** between the `main_mcp` server and the `subserver`. Instead of copying components, requests for components matching the optional `prefix` are **delegated** to the `subserver` at runtime. If no prefix is provided, the subserver's components are accessible without prefixing. When multiple servers are mounted with the same prefix (or no prefix), the most recently mounted server takes precedence for conflicting component names. - -```python -import asyncio -from fastmcp import FastMCP, Client - -# Define subserver -dynamic_mcp = FastMCP(name="DynamicService") - -@dynamic_mcp.tool -def initial_tool(): - """Initial tool demonstration.""" - return "Initial Tool Exists" - -# Mount subserver (synchronous operation) -main_mcp = FastMCP(name="MainAppLive") -main_mcp.mount(dynamic_mcp, prefix="dynamic") - -# Add a tool AFTER mounting - it will be accessible through main_mcp -@dynamic_mcp.tool -def added_later(): - """Tool added after mounting.""" - return "Tool Added Dynamically!" - -# Testing access to mounted tools -async def test_dynamic_mount(): - tools = await main_mcp.get_tools() - print("Available tools:", list(tools.keys())) - # Shows: ['dynamic_initial_tool', 'dynamic_added_later'] - - async with Client(main_mcp) as client: - result = await client.call_tool("dynamic_added_later") - print("Result:", result.data) - # Shows: "Tool Added Dynamically!" - -if __name__ == "__main__": - asyncio.run(test_dynamic_mount()) -``` - -### How Mounting Works - -When mounting is configured: - -1. **Live Link**: The parent server establishes a connection to the mounted server. -2. **Dynamic Updates**: Changes to the mounted server are immediately reflected when accessed through the parent. -3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server. -4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime. - -The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts. This includes prefixing both the URIs/keys and the names of resources and templates for better identification in multi-server configurations. - - - The `prefix` parameter is optional. If omitted, components are mounted without modification. - - - -When mounting servers, custom HTTP routes defined with `@server.custom_route()` are also forwarded to the parent server, making them accessible through the parent's HTTP application. - - -#### Performance Considerations - -Due to the "live link", operations like `list_tools()` on the parent server will be impacted by the speed of the slowest mounted server. In particular, HTTP-based mounted servers can introduce significant latency (300-400ms vs 1-2ms for local tools), and this slowdown affects the whole server, not just interactions with the HTTP-proxied tools. If performance is important, importing tools via [`import_server()`](#importing-static-composition) may be a more appropriate solution as it copies components once at startup rather than delegating requests at runtime. - -#### Mounting Without Prefixes - - - -You can also mount servers without specifying a prefix, which makes components accessible without prefixing. This works identically to [importing without prefixes](#importing-without-prefixes), including [conflict resolution](#conflict-resolution). - - - - -### Direct vs. Proxy Mounting - - - -FastMCP supports two mounting modes: - -1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory. - - No client lifecycle events occur on the mounted server - - The mounted server's lifespan context is not executed - - Communication is handled through direct method calls - -2. **Proxy Mounting**: The parent server treats the mounted server as a separate entity and communicates with it through a client interface. - - Full client lifecycle events occur on the mounted server - - The mounted server's lifespan is executed when a client connects - - Communication happens via an in-memory Client transport - -```python -# Direct mounting (default when no custom lifespan) -main_mcp.mount(api_server, prefix="api") - -# Proxy mounting (preserves full client lifecycle) -main_mcp.mount(api_server, prefix="api", as_proxy=True) - -# Mounting without a prefix (components accessible without prefixing) -main_mcp.mount(api_server) -``` - -FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior with the `as_proxy` parameter. - -#### Interaction with Proxy Servers - -When using `FastMCP.as_proxy()` to create a proxy server, mounting that server will always use proxy mounting: - -```python -# Create a proxy for a remote server -remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp")) - -# Mount the proxy (always uses proxy mounting) -main_server.mount(remote_proxy, prefix="remote") -``` - - - -## Tag Filtering with Composition - - - -When using `include_tags` or `exclude_tags` on a parent server, these filters apply **recursively** to all components, including those from mounted or imported servers. This allows you to control which components are exposed at the parent level, regardless of how your application is composed. - -```python -import asyncio -from fastmcp import FastMCP, Client - -# Create a subserver with tools tagged for different environments -api_server = FastMCP(name="APIServer") - -@api_server.tool(tags={"production"}) -def prod_endpoint() -> str: - """Production-ready endpoint.""" - return "Production data" - -@api_server.tool(tags={"development"}) -def dev_endpoint() -> str: - """Development-only endpoint.""" - return "Debug data" - -# Mount the subserver with production tag filtering at parent level -prod_app = FastMCP(name="ProductionApp", include_tags={"production"}) -prod_app.mount(api_server, prefix="api") - -# Test the filtering -async def test_filtering(): - async with Client(prod_app) as client: - tools = await client.list_tools() - print("Available tools:", [t.name for t in tools]) - # Shows: ['api_prod_endpoint'] - # The 'api_dev_endpoint' is filtered out - - # Calling the filtered tool raises an error - try: - await client.call_tool("api_dev_endpoint") - except Exception as e: - print(f"Filtered tool not accessible: {e}") - -if __name__ == "__main__": - asyncio.run(test_filtering()) -``` - -### How Recursive Filtering Works - -Tag filters apply in the following order: - -1. **Child Server Filters**: Each mounted/imported server first applies its own `include_tags`/`exclude_tags` to its components. -2. **Parent Server Filters**: The parent server then applies its own `include_tags`/`exclude_tags` to all components, including those from child servers. - -This ensures that parent server tag policies act as a global policy for everything the parent server exposes, no matter how your application is composed. - - -This filtering applies to both **listing** (e.g., `list_tools()`) and **execution** (e.g., `call_tool()`). Filtered components are neither visible nor executable through the parent server. - diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx index 8ffd683c0a..234de648f1 100644 --- a/docs/servers/middleware.mdx +++ b/docs/servers/middleware.mdx @@ -436,7 +436,7 @@ This creates the following execution flow: ## Server Composition and Middleware -When using [Server Composition](/servers/composition) with `mount` or `import_server`, middleware behavior follows these rules: +When using [Mounting Servers](/servers/providers/mounting) with `mount` or `import_server`, middleware behavior follows these rules: 1. **Parent server middleware** runs for all requests, including those routed to mounted servers 2. **Mounted server middleware** only runs for requests handled by that specific server @@ -458,7 +458,7 @@ def child_tool() -> str: return "from child" # Mount the child server -parent.mount(child, prefix="child") +parent.mount(child, namespace="child") ``` When a client calls "child_tool", the request will flow through the parent's authentication middleware first, then route to the child server where it will go through the child's logging middleware. diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index 8822a820a9..1e1df2fad3 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -94,7 +94,8 @@ def data_analysis_prompt( - A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information + Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead. + A boolean to enable or disable the prompt. See [Visibility Control](#visibility-control) for the recommended approach. @@ -268,32 +269,37 @@ def data_analysis_prompt( In this example, the client *must* provide `data_uri`. If `analysis_type` or `include_charts` are omitted, their default values will be used. -### Disabling Prompts +### Visibility Control - + -You can control the visibility and availability of prompts by enabling or disabling them. Disabled prompts will not appear in the list of available prompts, and attempting to call a disabled prompt will result in an "Unknown prompt" error. - -By default, all prompts are enabled. You can disable a prompt upon creation using the `enabled` parameter in the decorator: +You can control which prompts are visible to clients using server-level visibility control. Disabled prompts don't appear in `list_prompts` and can't be called. ```python -@mcp.prompt(enabled=False) -def experimental_prompt(): - """This prompt is not ready for use.""" - return "This is an experimental prompt." -``` +from fastmcp import FastMCP -You can also toggle a prompt's state programmatically after it has been created: +mcp = FastMCP("MyServer") -```python -@mcp.prompt -def seasonal_prompt(): return "Happy Holidays!" +@mcp.prompt(tags={"public"}) +def public_prompt(topic: str) -> str: + return f"Discuss: {topic}" -# Disable and re-enable the prompt -seasonal_prompt.disable() -seasonal_prompt.enable() +@mcp.prompt(tags={"internal"}) +def internal_prompt() -> str: + return "Internal system prompt" + +# Disable specific prompts by key +mcp.disable(keys=["prompt:internal_prompt"]) + +# Disable prompts by tag +mcp.disable(tags={"internal"}) + +# Or use allowlist mode - only show prompts with specific tags +mcp.enable(tags={"public"}, only=True) ``` +See [Local Provider](/servers/providers/local#visibility-control) for the complete visibility control API including key formats and tag-based filtering. + ### Async Prompts FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as prompts. @@ -349,9 +355,9 @@ def example_prompt() -> str: return "Hello!" # These operations trigger notifications: -mcp.add_prompt(example_prompt) # Sends prompts/list_changed notification -example_prompt.disable() # Sends prompts/list_changed notification -example_prompt.enable() # Sends prompts/list_changed notification +mcp.add_prompt(example_prompt) # Sends prompts/list_changed notification +mcp.disable(keys=["prompt:example_prompt"]) # Sends prompts/list_changed notification +mcp.enable(keys=["prompt:example_prompt"]) # Sends prompts/list_changed notification ``` Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications. diff --git a/docs/servers/providers/custom.mdx b/docs/servers/providers/custom.mdx new file mode 100644 index 0000000000..2360ba583d --- /dev/null +++ b/docs/servers/providers/custom.mdx @@ -0,0 +1,241 @@ +--- +title: Custom Providers +sidebarTitle: Custom +description: Build providers that source components from any data source +icon: code +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Custom providers let you source components from anywhere - databases, APIs, configuration systems, or dynamic runtime logic. If you can write Python code to fetch or generate a component, you can wrap it in a provider. + +## When to Build Custom + +The built-in providers handle common cases: decorators (`LocalProvider`), composition (`FastMCPProvider`), and proxying (`ProxyProvider`). Build a custom provider when your components come from somewhere else: + +- **Database-backed tools**: Admin users define tools in a database, and your server exposes them dynamically +- **API-backed resources**: Resources that fetch content from external services on demand +- **Configuration-driven components**: Components loaded from YAML/JSON config files at startup +- **Multi-tenant systems**: Different users see different tools based on their permissions +- **Plugin systems**: Third-party code registers components at runtime + +## Providers vs Middleware + +Both providers and [middleware](/servers/middleware) can influence what components a client sees, but they work at different levels. + +**Providers** are objects that source components. They make it easy to reason about where tools, resources, and prompts come from - a database, another server, an API. + +**Middleware** intercepts individual requests. It's well-suited for request-specific decisions like logging, rate limiting, or authentication. + +You *could* use middleware to dynamically add tools based on request context. But it's often cleaner to have a provider source all possible tools, then use middleware or [visibility controls](/servers/providers/local#visibility-control) to filter what each request can see. This separation makes it easier to reason about how components are sourced and how they interact with other server machinery. + +## The Provider Interface + +A provider must implement `list_*` methods that return available components: + +```python +from fastmcp.server.providers import Provider +from fastmcp.tools import Tool +from fastmcp.resources import Resource +from fastmcp.prompts import Prompt + +class MyProvider(Provider): + async def list_tools(self) -> list[Tool]: + """Return all tools this provider offers.""" + return [] + + async def list_resources(self) -> list[Resource]: + """Return all resources this provider offers.""" + return [] + + async def list_prompts(self) -> list[Prompt]: + """Return all prompts this provider offers.""" + return [] +``` + +You only need to implement the methods for component types you provide. The base class returns empty lists by default. + +The `get_*` methods (`get_tool`, `get_resource`, `get_prompt`) have default implementations that search through the list results. Override them only if you can fetch individual components more efficiently than iterating the full list. + +## What Providers Return + +Providers return component objects that are ready to use. When a client calls a tool, FastMCP invokes the tool's function - your provider isn't involved in execution. This means the `Tool`, `Resource`, or `Prompt` you return must actually work. + +The easiest way to create components is from functions: + +```python +from fastmcp.tools import Tool + +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + +tool = Tool.from_function(add) +``` + +The function's type hints become the input schema, and the docstring becomes the description. You can override these: + +```python +tool = Tool.from_function( + add, + name="calculator_add", + description="Add two integers together" +) +``` + +Similar `from_function` methods exist for `Resource` and `Prompt`. + +## Registering Providers + +Add providers when creating the server: + +```python +mcp = FastMCP( + "MyServer", + providers=[ + DatabaseProvider(db_url), + ConfigProvider(config_path), + ] +) +``` + +Or add them after creation: + +```python +mcp = FastMCP("MyServer") +mcp.add_provider(DatabaseProvider(db_url)) +``` + +## A Simple Provider + +Here's a minimal provider that serves tools from a dictionary: + +```python +from fastmcp import FastMCP +from fastmcp.server.providers import Provider +from fastmcp.tools import Tool + +class DictProvider(Provider): + def __init__(self, tools: dict[str, Callable]): + super().__init__() + self._tools = [ + Tool.from_function(fn, name=name) + for name, fn in tools.items() + ] + + async def list_tools(self) -> list[Tool]: + return self._tools +``` + +Use it like this: + +```python +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + +def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + +mcp = FastMCP("Calculator", providers=[ + DictProvider({"add": add, "multiply": multiply}) +]) +``` + +## Lifecycle Management + +Providers often need to set up connections when the server starts and clean them up when it stops. Override the `lifespan` method: + +```python +from contextlib import asynccontextmanager +from collections.abc import AsyncIterator + +class DatabaseProvider(Provider): + def __init__(self, db_url: str): + super().__init__() + self.db_url = db_url + self.db = None + + @asynccontextmanager + async def lifespan(self) -> AsyncIterator[None]: + self.db = await connect_database(self.db_url) + try: + yield + finally: + await self.db.close() + + async def list_tools(self) -> list[Tool]: + rows = await self.db.fetch("SELECT * FROM tools") + return [self._make_tool(row) for row in rows] +``` + +FastMCP calls your provider's `lifespan` during server startup and shutdown. The connection is available to your methods while the server runs. + +## Full Example: API-Backed Resources + +Here's a complete provider that fetches resources from an external REST API: + +```python +from contextlib import asynccontextmanager +from fastmcp.server.providers import Provider +from fastmcp.resources import Resource +import httpx + +class ApiResourceProvider(Provider): + """Provides resources backed by an external API.""" + + def __init__(self, base_url: str, api_key: str): + super().__init__() + self.base_url = base_url + self.api_key = api_key + self.client = None + + @asynccontextmanager + async def lifespan(self) -> AsyncIterator[None]: + self.client = httpx.AsyncClient( + base_url=self.base_url, + headers={"Authorization": f"Bearer {self.api_key}"} + ) + try: + yield + finally: + await self.client.aclose() + + async def list_resources(self) -> list[Resource]: + response = await self.client.get("/resources") + response.raise_for_status() + return [ + self._make_resource(item) + for item in response.json()["items"] + ] + + def _make_resource(self, data: dict) -> Resource: + resource_id = data["id"] + + async def read_content() -> str: + response = await self.client.get( + f"/resources/{resource_id}/content" + ) + return response.text + + return Resource.from_function( + read_content, + uri=f"api://resources/{resource_id}", + name=data["name"], + description=data.get("description", ""), + mime_type=data.get("mime_type", "text/plain") + ) +``` + +Register it like any other provider: + +```python +from fastmcp import FastMCP + +mcp = FastMCP("API Resources", providers=[ + ApiResourceProvider("https://api.example.com", "my-api-key") +]) +``` diff --git a/docs/servers/providers/local.mdx b/docs/servers/providers/local.mdx new file mode 100644 index 0000000000..b2e474a451 --- /dev/null +++ b/docs/servers/providers/local.mdx @@ -0,0 +1,238 @@ +--- +title: Local Provider +sidebarTitle: Local +description: The default provider for decorator-registered components +icon: house +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +`LocalProvider` stores components that you define directly on your server. When you use `@mcp.tool`, `@mcp.resource`, or `@mcp.prompt`, you're adding components to your server's `LocalProvider`. + +## How It Works + +Every FastMCP server has a `LocalProvider` as its first provider. Components registered via decorators or direct methods are stored here: + +```python +from fastmcp import FastMCP + +mcp = FastMCP("MyServer") + +# These are stored in the server's `LocalProvider` +@mcp.tool +def greet(name: str) -> str: + """Greet someone by name.""" + return f"Hello, {name}!" + +@mcp.resource("data://config") +def get_config() -> str: + """Return configuration data.""" + return '{"version": "1.0"}' + +@mcp.prompt +def analyze(topic: str) -> str: + """Create an analysis prompt.""" + return f"Please analyze: {topic}" +``` + +The `LocalProvider` is always queried first when clients request components, ensuring that your directly-defined components take precedence over those from mounted or proxied servers. + +## Component Registration + +### Using Decorators + +The most common way to register components: + +```python +@mcp.tool +def my_tool(x: int) -> str: + return str(x) + +@mcp.resource("data://info") +def my_resource() -> str: + return "info" + +@mcp.prompt +def my_prompt(topic: str) -> str: + return f"Discuss: {topic}" +``` + +### Using Direct Methods + +You can also add pre-built component objects: + +```python +from fastmcp.tools import Tool + +# Create a tool object +my_tool = Tool.from_function(some_function, name="custom_tool") + +# Add it to the server +mcp.add_tool(my_tool) +mcp.add_resource(my_resource) +mcp.add_prompt(my_prompt) +``` + +### Removing Components + +Remove components by name or URI: + +```python +mcp.remove_tool("my_tool") +mcp.remove_resource("data://info") +mcp.remove_prompt("my_prompt") +``` + +## Duplicate Handling + +When you try to add a component that already exists, the behavior depends on the `on_duplicate` setting: + +| Mode | Behavior | +|------|----------| +| `"error"` (default) | Raise `ValueError` | +| `"warn"` | Log warning and replace | +| `"replace"` | Silently replace | +| `"ignore"` | Keep existing component | + +Configure this when creating the server: + +```python +mcp = FastMCP("MyServer", on_duplicate="warn") +``` + +## Visibility Control + + + +You can enable or disable components at runtime. Disabled components don't appear in listings and can't be called. + +### Disabling Components + +Use `disable()` to hide components: + +```python +# Disable by key +mcp.disable(keys=["tool:admin_action"]) + +# Disable by tag +mcp.disable(tags={"internal"}) + +# Disable multiple +mcp.disable(keys=["tool:debug"], tags={"deprecated"}) +``` + +### Enabling Components + +Use `enable()` to re-enable disabled components: + +```python +# Re-enable by key +mcp.enable(keys=["tool:admin_action"]) + +# Re-enable by tag +mcp.enable(tags={"internal"}) +``` + +### Allowlist Mode + +Use `only=True` to switch to allowlist mode, where **only** the specified components are visible: + +```python +# Only show public tools +mcp.enable(tags={"public"}, only=True) + +# Only show specific tools +mcp.enable(keys=["tool:safe_tool", "tool:read_only"], only=True) +``` + +### Component Keys + +Keys follow the format `{type}:{identifier}`: + +| Component Type | Key Format | Example | +|----------------|------------|---------| +| Tool | `tool:{name}` | `tool:greet` | +| Resource | `resource:{uri}` | `resource:data://config` | +| Template | `template:{uri}` | `template:data://{id}` | +| Prompt | `prompt:{name}` | `prompt:analyze` | + +### Using Tags + +Add tags when defining components to enable group-based visibility control: + +```python +@mcp.tool(tags={"admin", "dangerous"}) +def delete_all() -> str: + """Delete everything.""" + return "Deleted" + +@mcp.tool(tags={"public", "safe"}) +def get_status() -> str: + """Get system status.""" + return "OK" + +# Hide all admin tools +mcp.disable(tags={"admin"}) + +# Or only show safe tools +mcp.enable(tags={"safe"}, only=True) +``` + +### Automatic Notifications + +When you enable or disable components, FastMCP automatically sends `list_changed` notifications to connected clients. Clients that support notifications will refresh their component lists automatically. + +## Standalone LocalProvider + +You can create a LocalProvider independently and attach it to multiple servers: + +```python +from fastmcp import FastMCP +from fastmcp.server.providers import LocalProvider + +# Create a reusable provider +shared_tools = LocalProvider() + +@shared_tools.tool +def greet(name: str) -> str: + return f"Hello, {name}!" + +@shared_tools.resource("data://version") +def get_version() -> str: + return "1.0.0" + +# Attach to multiple servers +server1 = FastMCP("Server1", providers=[shared_tools]) +server2 = FastMCP("Server2", providers=[shared_tools]) +``` + +This is useful for: +- Sharing components across servers +- Testing components in isolation +- Building reusable component libraries + +## Provider-Level Visibility + +Visibility can be controlled at the provider level as well: + +```python +from fastmcp.server.providers import LocalProvider + +provider = LocalProvider() + +@provider.tool(tags={"internal"}) +def internal_tool() -> str: + return "Internal" + +# Disable at provider level +provider.disable(tags={"internal"}) + +# This affects all servers using this provider +mcp = FastMCP("Server", providers=[provider]) +``` + + +When a component is disabled at the provider level, it's hidden from all servers using that provider. Server-level visibility adds another layer of filtering on top of provider-level visibility. + diff --git a/docs/servers/providers/mounting.mdx b/docs/servers/providers/mounting.mdx new file mode 100644 index 0000000000..887a82b873 --- /dev/null +++ b/docs/servers/providers/mounting.mdx @@ -0,0 +1,240 @@ +--- +title: Mounting Servers +sidebarTitle: Mounting +description: Compose servers by mounting one inside another +icon: puzzle-piece +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Mounting lets you combine multiple FastMCP servers into one. When you mount a server, all its components become available through the parent. Under the hood, FastMCP uses `FastMCPProvider` (v3.0.0+) to source components from the mounted server. + +## Why Mount Servers + +Large applications benefit from modular organization. Rather than defining all components in one massive file, create focused servers for specific domains and combine them: + +- **Modularity**: Break down applications into smaller, focused servers +- **Reusability**: Create utility servers and mount them wherever needed +- **Teamwork**: Different teams can work on separate servers +- **Organization**: Keep related functionality grouped together + +## Basic Mounting + +Use `mount()` to add another server's components to your server: + +```python +from fastmcp import FastMCP + +# Create focused subservers +weather_server = FastMCP("Weather") + +@weather_server.tool +def get_forecast(city: str) -> str: + """Get weather forecast for a city.""" + return f"Sunny in {city}" + +@weather_server.resource("data://cities") +def list_cities() -> list[str]: + """List supported cities.""" + return ["London", "Paris", "Tokyo"] + +# Create main server and mount the subserver +main = FastMCP("MainApp") +main.mount(weather_server) + +# Now main has access to get_forecast and data://cities +``` + +## Namespacing + + + +When mounting multiple servers, use namespaces to avoid naming conflicts: + +```python +weather = FastMCP("Weather") +calendar = FastMCP("Calendar") + +@weather.tool +def get_data() -> str: + return "Weather data" + +@calendar.tool +def get_data() -> str: + return "Calendar data" + +main = FastMCP("Main") +main.mount(weather, namespace="weather") +main.mount(calendar, namespace="calendar") + +# Tools are now: +# - weather_get_data +# - calendar_get_data +``` + +### How Namespacing Works + +| Component Type | Without Namespace | With `namespace="api"` | +|----------------|-------------------|------------------------| +| Tool | `my_tool` | `api_my_tool` | +| Prompt | `my_prompt` | `api_my_prompt` | +| Resource | `data://info` | `data://api/info` | +| Template | `data://{id}` | `data://api/{id}` | + +Namespacing uses [`TransformingProvider`](/servers/providers/namespacing) under the hood. + +## Mounting vs Importing + +FastMCP offers two ways to combine servers: + +| Feature | `mount()` | `import_server()` | +|---------|-----------|-------------------| +| **Link Type** | Live (dynamic) | One-time copy (static) | +| **Updates** | Changes reflected immediately | Changes not reflected | +| **Performance** | Runtime delegation | Faster - no delegation | +| **Use Case** | Modular runtime composition | Bundling finalized components | + +### Live Mounting + +With `mount()`, changes to the subserver are immediately reflected: + +```python +main = FastMCP("Main") +main.mount(dynamic_server, namespace="dynamic") + +# Add a tool AFTER mounting - it's accessible through main +@dynamic_server.tool +def added_later() -> str: + return "Added after mounting!" + +# This works because mount() creates a live link +``` + +### Static Importing + +With `import_server()`, components are copied once at import time: + +```python +main = FastMCP("Main") + +async def setup(): + await main.import_server(static_server, namespace="static") + +# Changes to static_server after this point are NOT reflected in main +``` + +## Direct vs Proxy Mounting + + + +FastMCP supports two mounting modes: + +### Direct Mounting (Default) + +The parent server directly accesses the mounted server's objects in memory: + +```python +main.mount(subserver, namespace="api") +``` + +- No client lifecycle events on mounted server +- Mounted server's lifespan is not executed +- Communication via direct method calls + +### Proxy Mounting + +The parent server treats the mounted server as a separate entity: + +```python +main.mount(subserver, namespace="api", as_proxy=True) +``` + +- Full client lifecycle events on mounted server +- Mounted server's lifespan is executed +- Communication via in-memory Client transport + +FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan. Override with `as_proxy=True` or `as_proxy=False`. + +## Tag Filtering + + + +Parent server tag filters apply recursively to mounted servers: + +```python +api_server = FastMCP("API") + +@api_server.tool(tags={"production"}) +def prod_endpoint() -> str: + return "Production data" + +@api_server.tool(tags={"development"}) +def dev_endpoint() -> str: + return "Debug data" + +# Mount with production filter +prod_app = FastMCP("Production") +prod_app.mount(api_server, namespace="api") +prod_app.enable(tags={"production"}, only=True) + +# Only prod_endpoint (namespaced as api_prod_endpoint) is visible +``` + +## Performance Considerations + +When using live mounting, operations like `list_tools()` on the parent server are affected by the performance of all mounted servers. This is particularly noticeable with: + +- HTTP-based mounted servers (300-400ms vs 1-2ms for local tools) +- Mounted servers with slow initialization +- Deep mounting hierarchies + +If low latency is critical, consider: +- Using `import_server()` for static composition +- Implementing caching strategies +- Limiting mounting depth + +## Custom Routes + + + +Custom HTTP routes defined with `@server.custom_route()` are also forwarded when mounting: + +```python +subserver = FastMCP("Sub") + +@subserver.custom_route("/health", methods=["GET"]) +async def health_check(): + return {"status": "ok"} + +main = FastMCP("Main") +main.mount(subserver, namespace="sub") + +# /health is now accessible through main's HTTP app +``` + +## Conflict Resolution + + + +When mounting multiple servers with the same namespace (or no namespace), the **most recently mounted** server takes precedence for conflicting component names: + +```python +server_a = FastMCP("A") +server_b = FastMCP("B") + +@server_a.tool +def shared_tool() -> str: + return "From A" + +@server_b.tool +def shared_tool() -> str: + return "From B" + +main = FastMCP("Main") +main.mount(server_a) +main.mount(server_b) + +# shared_tool returns "From B" (most recently mounted) +``` diff --git a/docs/servers/providers/namespacing.mdx b/docs/servers/providers/namespacing.mdx new file mode 100644 index 0000000000..b50d64cbaa --- /dev/null +++ b/docs/servers/providers/namespacing.mdx @@ -0,0 +1,159 @@ +--- +title: Namespacing +sidebarTitle: Namespacing +description: Namespace and rename components with TransformingProvider +icon: wand-magic-sparkles +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +`TransformingProvider` wraps another provider and modifies its components, enabling namespacing and renaming. When you use `mount(namespace="...")`, `TransformingProvider` is used under the hood. + +## Why Transform + +When composing servers, you may encounter naming conflicts: + +```python +weather = FastMCP("Weather") +calendar = FastMCP("Calendar") + +@weather.tool +def get_data() -> str: + return "Weather" + +@calendar.tool +def get_data() -> str: # Same name! + return "Calendar" + +main = FastMCP("Main") +main.mount(weather) +main.mount(calendar) # Conflict: which get_data? +``` + +Transformations solve this by prefixing or renaming components. + +## Namespacing + +The most common transformation is namespacing, which prefixes all component names: + +```python +main = FastMCP("Main") +main.mount(weather, namespace="weather") +main.mount(calendar, namespace="calendar") + +# Tools are now: +# - weather_get_data +# - calendar_get_data +``` + +### Namespace Rules + +| Component Type | Original | With `namespace="api"` | +|----------------|----------|------------------------| +| Tool | `my_tool` | `api_my_tool` | +| Prompt | `my_prompt` | `api_my_prompt` | +| Resource | `data://info` | `data://api/info` | +| Template | `data://{id}` | `data://api/{id}` | + +Tools and prompts get an underscore prefix. Resources get a path-style prefix. + +## Tool Renaming + +For more control, rename specific tools explicitly: + +```python +from fastmcp.server.providers import FastMCPProvider + +provider = FastMCPProvider(weather_server).with_transforms( + namespace="weather", + tool_renames={"verbose_tool_name": "short"} +) + +# "verbose_tool_name" → "short" (explicit rename, bypasses namespace) +# "other_tool" → "weather_other_tool" (namespace applied) +``` + +Explicit renames take precedence over namespacing. + +## Direct Provider Usage + +You can use `TransformingProvider` directly via `with_transforms()`: + +```python +from fastmcp.server.providers import LocalProvider + +provider = LocalProvider() + +@provider.tool +def greet(name: str) -> str: + return f"Hello, {name}!" + +# Apply transformations +namespaced = provider.with_transforms(namespace="api") + +# Or use shorthand +namespaced = provider.with_namespace("api") + +# Register with server +mcp = FastMCP("Server", providers=[namespaced]) +# Tool is now: api_greet +``` + +## Stacking Transformations + +Transformations can be stacked - each `with_transforms()` creates a new wrapper: + +```python +provider = ( + LocalProvider() + .with_transforms(namespace="inner") + .with_transforms(namespace="outer") +) + +# Tool "foo" becomes: +# 1. inner_foo (first transform) +# 2. outer_inner_foo (second transform) +``` + +You can also combine namespacing with renaming at different levels: + +```python +provider = ( + LocalProvider() + .with_transforms(namespace="api") + .with_transforms(tool_renames={"api_verbose": "short"}) +) + +# "verbose" → "api_verbose" (namespace) → "short" (rename) +``` + +## Use Cases + +### Avoiding Conflicts + +```python +# Two servers with same tool names +main.mount(server_a, namespace="a") +main.mount(server_b, namespace="b") +``` + +### API Versioning + +```python +# Mount different versions +main.mount(v1_server, namespace="v1") +main.mount(v2_server, namespace="v2") +``` + +### Aliasing Long Names + +```python +provider = FastMCPProvider(server).with_transforms( + tool_renames={ + "get_user_authentication_status": "auth_status", + "retrieve_configuration_settings": "get_config" + } +) +``` diff --git a/docs/servers/providers/overview.mdx b/docs/servers/providers/overview.mdx new file mode 100644 index 0000000000..fe895a7a2f --- /dev/null +++ b/docs/servers/providers/overview.mdx @@ -0,0 +1,67 @@ +--- +title: Providers +sidebarTitle: Overview +description: How FastMCP sources tools, resources, and prompts +icon: layer-group +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Every FastMCP server has one or more component providers. A provider is a source of tools, resources, and prompts - it's what makes components available to clients. + +## What Is a Provider? + +When a client connects to your server and asks "what tools do you have?", FastMCP asks each provider that question and combines the results. When a client calls a specific tool, FastMCP finds which provider has it and delegates the call. + +You're already using providers. When you write `@mcp.tool`, you're adding a tool to your server's `LocalProvider` - the default provider that stores components you define directly in code. You just don't have to think about it for simple servers. + +Providers become important when your components come from multiple sources: another FastMCP server to include, a remote MCP server to proxy, or a database where tools are defined dynamically. Each source gets its own provider, and FastMCP queries them all seamlessly. + +## Why Providers? + +The provider abstraction solves a common problem: as servers grow, you need to organize components across multiple sources without tangling everything together. + +**Composition**: Break a large server into focused modules. A "weather" server and a "calendar" server can each be developed independently, then mounted into a main server. Each mounted server becomes a `FastMCPProvider`. + +**Proxying**: Expose a remote MCP server through your local server. Maybe you're bridging transports (remote HTTP to local stdio) or aggregating multiple backends. Remote connections become `ProxyProvider` instances. + +**Dynamic sources**: Load tools from a database, generate them from an OpenAPI spec, or create them based on user permissions. Custom providers let components come from anywhere. + +## Built-in Providers + +FastMCP includes providers for common patterns: + +| Provider | What it does | How you use it | +|----------|--------------|----------------| +| `LocalProvider` | Stores components you define in code | `@mcp.tool`, `mcp.add_tool()` | +| `FastMCPProvider` | Wraps another FastMCP server | `mcp.mount(server)` | +| `ProxyProvider` | Connects to remote MCP servers | `FastMCP.as_proxy(client)` | +| `TransformingProvider` | Adds prefixes to avoid name collisions | `mcp.mount(server, namespace="api")` | + +Most users only interact with `LocalProvider` (through decorators) and occasionally mount or proxy other servers. The provider abstraction stays invisible until you need it. + +## Provider Order + +When a client requests a tool, FastMCP queries providers in registration order. The first provider that has the tool handles the request. + +`LocalProvider` is always first, so your decorator-defined tools take precedence. Additional providers are queried in the order you added them. This means if two providers have a tool with the same name, the first one wins. + +## When to Care About Providers + +**You can ignore providers entirely** if you're building a simple server with decorators. Just use `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` - FastMCP handles the rest. + +**Learn about providers when** you want to: +- [Mount another server](/servers/providers/mounting) into yours +- [Proxy a remote server](/servers/providers/proxy) through yours +- [Control visibility](/servers/providers/local#visibility-control) of components +- [Build dynamic sources](/servers/providers/custom) like database-backed tools + +## Next Steps + +- [Local](/servers/providers/local) - How decorators and visibility control work +- [Mounting](/servers/providers/mounting) - Compose servers together +- [Proxying](/servers/providers/proxy) - Connect to remote servers +- [Namespacing](/servers/providers/namespacing) - Avoid name collisions +- [Custom](/servers/providers/custom) - Build your own providers diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx new file mode 100644 index 0000000000..779cfc90ac --- /dev/null +++ b/docs/servers/providers/proxy.mdx @@ -0,0 +1,275 @@ +--- +title: Remote Proxies +sidebarTitle: Proxying +description: Expose remote MCP servers through your local server +icon: arrows-retweet +--- + +import { VersionBadge } from '/snippets/version-badge.mdx' + + + +Proxying lets you expose a remote MCP server's tools, resources, and prompts through your local server. Under the hood, FastMCP uses `ProxyProvider` (v3.0.0+) to source components from a client connection. + +## Why Proxy Servers + +Proxying lets you: + +- **Bridge transports**: Expose a remote SSE server via local stdio +- **Aggregate servers**: Combine multiple remote servers into one +- **Add security**: Act as a controlled gateway to backend servers +- **Simplify access**: Single endpoint even if backends change + +```mermaid +sequenceDiagram + participant Client as Your Client + participant Proxy as FastMCP Proxy + participant Backend as Remote Server + + Client->>Proxy: MCP Request (stdio) + Proxy->>Backend: MCP Request (HTTP) + Backend-->>Proxy: MCP Response + Proxy-->>Client: MCP Response +``` + +## Quick Start + + + +Create a proxy using `FastMCP.as_proxy()`: + +```python +from fastmcp import FastMCP +from fastmcp.server.proxy import ProxyClient + +# Create a proxy to a remote server +proxy = FastMCP.as_proxy( + ProxyClient("http://example.com/mcp"), + name="MyProxy" +) + +if __name__ == "__main__": + proxy.run() +``` + +This gives you: +- Safe concurrent request handling +- Automatic forwarding of MCP features (sampling, elicitation, etc.) +- Session isolation to prevent context mixing + +## Transport Bridging + +A common use case is bridging transports - making a remote server available locally: + +```python +from fastmcp import FastMCP +from fastmcp.server.proxy import ProxyClient + +# Bridge remote HTTP to local stdio +remote_proxy = FastMCP.as_proxy( + ProxyClient("http://example.com/mcp/sse"), + name="Remote-to-Local" +) + +# Run locally via stdio for Claude Desktop +if __name__ == "__main__": + remote_proxy.run() # Defaults to stdio +``` + +Or expose a local server via HTTP: + +```python +# Bridge local server to HTTP +local_proxy = FastMCP.as_proxy( + ProxyClient("local_server.py"), + name="Local-to-HTTP" +) + +if __name__ == "__main__": + local_proxy.run(transport="http", host="0.0.0.0", port=8080) +``` + +## Session Isolation + + + +ProxyClient provides session isolation - each request gets its own isolated backend session: + +```python +from fastmcp.server.proxy import ProxyClient + +# Each request creates a fresh backend session (recommended) +proxy = FastMCP.as_proxy(ProxyClient("backend_server.py")) + +# Multiple clients can use this proxy simultaneously: +# - Client A calls a tool → gets isolated session +# - Client B calls a tool → gets different session +# - No context mixing +``` + +### Shared Sessions + +If you pass an already-connected client, the proxy reuses that session: + +```python +from fastmcp import Client + +async with Client("backend_server.py") as connected_client: + # This proxy reuses the connected session + proxy = FastMCP.as_proxy(connected_client) + + # ⚠️ Warning: All requests share the same session +``` + + +Shared sessions may cause context mixing in concurrent scenarios. Use only in single-threaded situations or with explicit synchronization. + + +## MCP Feature Forwarding + + + +ProxyClient automatically forwards MCP protocol features: + +| Feature | Description | +|---------|-------------| +| Roots | Filesystem root access requests | +| Sampling | LLM completion requests | +| Elicitation | User input requests | +| Logging | Log messages from backend | +| Progress | Progress notifications | + +```python +from fastmcp.server.proxy import ProxyClient + +# All features forwarded automatically +backend = ProxyClient("advanced_backend.py") +proxy = FastMCP.as_proxy(backend) + +# When the backend: +# - Requests LLM sampling → forwarded to your client +# - Logs messages → appear in your client +# - Reports progress → shown in your client +``` + +### Disabling Features + +Selectively disable forwarding: + +```python +backend = ProxyClient( + "backend_server.py", + sampling_handler=None, # Disable LLM sampling + log_handler=None # Disable log forwarding +) +``` + +## Configuration-Based Proxies + + + +Create proxies from configuration dictionaries: + +```python +from fastmcp import FastMCP + +config = { + "mcpServers": { + "default": { + "url": "https://example.com/mcp", + "transport": "http" + } + } +} + +proxy = FastMCP.as_proxy(config, name="Config-Based Proxy") +``` + +### Multi-Server Proxies + +Combine multiple servers with automatic namespacing: + +```python +config = { + "mcpServers": { + "weather": { + "url": "https://weather-api.example.com/mcp", + "transport": "http" + }, + "calendar": { + "url": "https://calendar-api.example.com/mcp", + "transport": "http" + } + } +} + +# Creates unified proxy with prefixed components: +# - weather_get_forecast +# - calendar_add_event +composite = FastMCP.as_proxy(config, name="Composite") +``` + +## Component Prefixing + +Proxied components follow standard prefixing rules: + +| Component Type | Pattern | +|----------------|---------| +| Tools | `{prefix}_{tool_name}` | +| Prompts | `{prefix}_{prompt_name}` | +| Resources | `protocol://{prefix}/path` | +| Templates | `protocol://{prefix}/...` | + +## Mirrored Components + + + +Components from a proxy server are "mirrored" - they reflect the remote server's state and cannot be modified directly. + +To modify a proxied component (like disabling it), create a local copy: + +```python +proxy = FastMCP.as_proxy("backend_server.py") + +# Get mirrored tool +mirrored_tool = await proxy.get_tool("useful_tool") + +# Create modifiable local copy +local_tool = mirrored_tool.copy() + +# Add to your own server +my_server = FastMCP("MyServer") +my_server.add_tool(local_tool) + +# Now you can control visibility +my_server.disable(keys=[local_tool.key]) +``` + +## Performance Considerations + +Proxying introduces network latency: + +| Operation | Local | Proxied (HTTP) | +|-----------|-------|----------------| +| `list_tools()` | 1-2ms | 300-400ms | +| `call_tool()` | 1-2ms | 200-500ms | + +When mounting proxy servers, this latency affects all operations on the parent server. + +For low-latency requirements, consider using [`import_server()`](/servers/providers/mounting#static-importing) to copy tools at startup. + +## Advanced: FastMCPProxy Class + +For explicit session control, use `FastMCPProxy` directly: + +```python +from fastmcp.server.proxy import FastMCPProxy, ProxyClient + +# Custom session factory +def create_client(): + return ProxyClient("backend_server.py") + +proxy = FastMCPProxy(client_factory=create_client) +``` + +This gives you full control over session creation and reuse strategies. diff --git a/docs/servers/proxy.mdx b/docs/servers/proxy.mdx deleted file mode 100644 index 401005dea1..0000000000 --- a/docs/servers/proxy.mdx +++ /dev/null @@ -1,349 +0,0 @@ ---- -title: Proxy Servers -sidebarTitle: Proxy Servers -description: Use FastMCP to act as an intermediary or change transport for other MCP servers. -icon: arrows-retweet ---- -import { VersionBadge } from '/snippets/version-badge.mdx' - - - -FastMCP provides a powerful proxying capability that allows one FastMCP server instance to act as a frontend for another MCP server (which could be remote, running on a different transport, or even another FastMCP instance). This is achieved using the `FastMCP.as_proxy()` class method. - -## What is Proxying? - -Proxying means setting up a FastMCP server that doesn't implement its own tools or resources directly. Instead, when it receives a request (like `tools/call` or `resources/read`), it forwards that request to a *backend* MCP server, receives the response, and then relays that response back to the original client. - -```mermaid -sequenceDiagram - participant ClientApp as Your Client (e.g., Claude Desktop) - participant FastMCPProxy as FastMCP Proxy Server - participant BackendServer as Backend MCP Server (e.g., remote SSE) - - ClientApp->>FastMCPProxy: MCP Request (e.g. stdio) - Note over FastMCPProxy, BackendServer: Proxy forwards the request - FastMCPProxy->>BackendServer: MCP Request (e.g. sse) - BackendServer-->>FastMCPProxy: MCP Response (e.g. sse) - Note over ClientApp, FastMCPProxy: Proxy relays the response - FastMCPProxy-->>ClientApp: MCP Response (e.g. stdio) -``` - -### Key Benefits - - - -- **Session Isolation**: Each request gets its own isolated session, ensuring safe concurrent operations -- **Transport Bridging**: Expose servers running on one transport via a different transport -- **Advanced MCP Features**: Automatic forwarding of sampling, elicitation, logging, and progress -- **Security**: Acts as a controlled gateway to backend servers -- **Simplicity**: Single endpoint even if backend location or transport changes - -### Performance Considerations - -When using proxy servers, especially those connecting to HTTP-based backend servers, be aware that latency can be significant. Operations like `list_tools()` may take hundreds of milliseconds compared to 1-2ms for local tools. When mounting proxy servers, this latency affects all operations on the parent server, not just interactions with the proxied tools. - -If low latency is a requirement for your use-case, consider using [`import_server()`](/servers/composition#importing-static-composition) to copy tools at startup rather than proxying them at runtime. - -## Quick Start - - - -The recommended way to create a proxy is using `ProxyClient`, which provides full MCP feature support with automatic session isolation: - -```python -from fastmcp import FastMCP -from fastmcp.server.proxy import ProxyClient - -# Create a proxy with full MCP feature support -proxy = FastMCP.as_proxy( - ProxyClient("backend_server.py"), - name="MyProxy" -) - -# Run the proxy (e.g., via stdio for Claude Desktop) -if __name__ == "__main__": - proxy.run() -``` - -This single setup gives you: -- Safe concurrent request handling -- Automatic forwarding of advanced MCP features (sampling, elicitation, etc.) -- Session isolation to prevent context mixing -- Full compatibility with all MCP clients - -You can also pass a FastMCP [client transport](/clients/transports) (or parameter that can be inferred to a transport) to `as_proxy()`. This will automatically create a `ProxyClient` instance for you. - -Finally, you can pass a regular FastMCP `Client` instance to `as_proxy()`. This will work for many use cases, but may break if advanced MCP features like sampling or elicitation are invoked by the server. - -## Session Isolation & Concurrency - - - -FastMCP proxies provide session isolation to ensure safe concurrent operations. The session strategy depends on how the proxy is configured: - -### Fresh Sessions - -When you pass a disconnected client (which is the normal case), each request gets its own isolated backend session: - -```python -from fastmcp.server.proxy import ProxyClient - -# Each request creates a fresh backend session (recommended) -proxy = FastMCP.as_proxy(ProxyClient("backend_server.py")) - -# Multiple clients can use this proxy simultaneously without interference: -# - Client A calls a tool -> gets isolated backend session -# - Client B calls a tool -> gets different isolated backend session -# - No context mixing between requests -``` - -### Session Reuse with Connected Clients - -When you pass an already-connected client, the proxy will reuse that session for all requests: - -```python -from fastmcp import Client - -# Create and connect a client -async with Client("backend_server.py") as connected_client: - # This proxy will reuse the connected session for all requests - proxy = FastMCP.as_proxy(connected_client) - - # ⚠️ Warning: All requests share the same backend session - # This may cause context mixing in concurrent scenarios -``` - -**Important**: Using shared sessions with concurrent requests from multiple clients may lead to context mixing and race conditions. This approach should only be used in single-threaded scenarios or when you have explicit synchronization. - -## Transport Bridging - -A common use case is bridging transports - exposing a server running on one transport via a different transport. For example, making a remote SSE server available locally via stdio: - -```python -from fastmcp import FastMCP -from fastmcp.server.proxy import ProxyClient - -# Bridge remote SSE server to local stdio -remote_proxy = FastMCP.as_proxy( - ProxyClient("http://example.com/mcp/sse"), - name="Remote-to-Local Bridge" -) - -# Run locally via stdio for Claude Desktop -if __name__ == "__main__": - remote_proxy.run() # Defaults to stdio transport -``` - -Or expose a local server via HTTP for remote access: - -```python -# Bridge local server to HTTP -local_proxy = FastMCP.as_proxy( - ProxyClient("local_server.py"), - name="Local-to-HTTP Bridge" -) - -# Run via HTTP for remote clients -if __name__ == "__main__": - local_proxy.run(transport="http", host="0.0.0.0", port=8080) -``` - - -## Advanced MCP Features - - - -`ProxyClient` automatically forwards advanced MCP protocol features between the backend server and clients connected to the proxy, ensuring full MCP compatibility. - -### Supported Features - -- **Roots**: Forwards filesystem root access requests to the client -- **Sampling**: Forwards LLM completion requests from backend to client -- **Elicitation**: Forwards user input requests to the client -- **Logging**: Forwards log messages from backend through to client -- **Progress**: Forwards progress notifications during long operations - -```python -from fastmcp.server.proxy import ProxyClient - -# ProxyClient automatically handles all these features -backend = ProxyClient("advanced_backend.py") -proxy = FastMCP.as_proxy(backend) - -# When the backend server: -# - Requests LLM sampling -> forwarded to your client -# - Logs messages -> appear in your client -# - Reports progress -> shown in your client -# - Needs user input -> prompts your client -``` - -### Customizing Feature Support - -You can selectively disable forwarding by passing `None` for specific handlers: - -```python -# Disable sampling but keep other features -backend = ProxyClient( - "backend_server.py", - sampling_handler=None, # Disable LLM sampling forwarding - log_handler=None # Disable log forwarding -) -``` - -When you use a transport string directly with `FastMCP.as_proxy()`, it automatically creates a `ProxyClient` internally to ensure full feature support. - -## Configuration-Based Proxies - - - -You can create a proxy directly from a configuration dictionary that follows the MCPConfig schema. This is useful for quickly setting up proxies to remote servers without manually configuring each connection detail. - -```python -from fastmcp import FastMCP - -# Create a proxy directly from a config dictionary -config = { - "mcpServers": { - "default": { # For single server configs, 'default' is commonly used - "url": "https://example.com/mcp", - "transport": "http" - } - } -} - -# Create a proxy to the configured server (auto-creates ProxyClient) -proxy = FastMCP.as_proxy(config, name="Config-Based Proxy") - -# Run the proxy with stdio transport for local access -if __name__ == "__main__": - proxy.run() -``` - - -The MCPConfig format follows an emerging standard for MCP server configuration and may evolve as the specification matures. While FastMCP aims to maintain compatibility with future versions, be aware that field names or structure might change. - - -### Multi-Server Configurations - -You can create a proxy to multiple servers by specifying multiple entries in the config. They are automatically mounted with their config names as prefixes: - -```python -# Multi-server configuration -config = { - "mcpServers": { - "weather": { - "url": "https://weather-api.example.com/mcp", - "transport": "http" - }, - "calendar": { - "url": "https://calendar-api.example.com/mcp", - "transport": "http" - } - } -} - -# Create a unified proxy to multiple servers -composite_proxy = FastMCP.as_proxy(config, name="Composite Proxy") - -# Tools, resources, prompts, and templates are accessible with prefixes: -# - Tools: weather_get_forecast, calendar_add_event -# - Prompts: weather_daily_summary, calendar_quick_add -# - Resources: weather://weather/icons/sunny, calendar://calendar/events/today -# - Templates: weather://weather/locations/{id}, calendar://calendar/events/{date} -``` - -## Component Prefixing - -When proxying one or more servers, component names are prefixed the same way as with mounting and importing: - -- Tools: `{prefix}_{tool_name}` -- Prompts: `{prefix}_{prompt_name}` -- Resources: `protocol://{prefix}/path/to/resource` (default path format) -- Resource templates: `protocol://{prefix}/...` and template names are also prefixed - -These rules apply uniformly whether you: -- Mount a proxy on another server -- Create a multi-server proxy from an `MCPConfig` -- Use `FastMCP.as_proxy()` directly - -## Mirrored Components - - - -When you access tools, resources, or prompts from a proxy server, they are "mirrored" from the remote server. Mirrored components cannot be modified directly since they reflect the state of the remote server. For example, you can not simply "disable" a mirrored component. - -However, you can create a copy of a mirrored component and store it as a new locally-defined component. Local components always take precedence over mirrored ones because the proxy server will check its own registry before it attempts to engage the remote server. - -Therefore, to enable or disable a proxy tool, resource, or prompt, you should first create a local copy and add it to your own server. Here's an example of how to do that for a tool: - -```python -# Create your own server -my_server = FastMCP("MyServer") - -# Get a proxy server -proxy = FastMCP.as_proxy("backend_server.py") - -# Get mirrored components from proxy -mirrored_tool = await proxy.get_tool("useful_tool") - -# Create a local copy that you can modify -local_tool = mirrored_tool.copy() - -# Add the local copy to your server -my_server.add_tool(local_tool) - -# Now you can disable YOUR copy -local_tool.disable() -``` - - -## `FastMCPProxy` Class - -Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally don't need to interact with this class directly, but it's available if needed for advanced scenarios. - -### Direct Usage - -```python -from fastmcp.server.proxy import FastMCPProxy, ProxyClient - -# Provide a client factory for explicit session control -def create_client(): - return ProxyClient("backend_server.py") - -proxy = FastMCPProxy(client_factory=create_client) -``` - -### Parameters - -- **`client_factory`**: A callable that returns a `Client` instance when called. This gives you full control over session creation and reuse strategies. - -### Explicit Session Management - -`FastMCPProxy` requires explicit session management - no automatic detection is performed. You must choose your session strategy: - -```python -# Share session across all requests (be careful with concurrency) -shared_client = ProxyClient("backend_server.py") -def shared_session_factory(): - return shared_client - -proxy = FastMCPProxy(client_factory=shared_session_factory) - -# Create fresh sessions per request (recommended) -def fresh_session_factory(): - return ProxyClient("backend_server.py") - -proxy = FastMCPProxy(client_factory=fresh_session_factory) -``` - -For automatic session strategy selection, use the convenience method `FastMCP.as_proxy()` instead. - -```python -# Custom factory with specific configuration -def custom_client_factory(): - client = ProxyClient("backend_server.py") - # Add any custom configuration here - return client - -proxy = FastMCPProxy(client_factory=custom_client_factory) -``` diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx index 2e81127a74..1668bdf583 100644 --- a/docs/servers/resources.mdx +++ b/docs/servers/resources.mdx @@ -103,7 +103,8 @@ def get_application_status() -> dict: - A boolean to enable or disable the resource. See [Disabling Resources](#disabling-resources) for more information + Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead. + A boolean to enable or disable the resource. See [Visibility Control](#visibility-control) for the recommended approach. @@ -191,32 +192,35 @@ The `meta` field in `ResourceContent` is for runtime metadata specific to this r You can still return plain `str` or `bytes` from your resource functions—`ResourceContent` is opt-in for when you need to include metadata. -### Disabling Resources +### Visibility Control - + -You can control the visibility and availability of resources and templates by enabling or disabling them. Disabled resources will not appear in the list of available resources or templates, and attempting to read a disabled resource will result in an "Unknown resource" error. - -By default, all resources are enabled. You can disable a resource upon creation using the `enabled` parameter in the decorator: +You can control which resources are visible to clients using server-level visibility control. Disabled resources don't appear in `list_resources` and can't be read. ```python -@mcp.resource("data://secret", enabled=False) -def get_secret_data(): - """This resource is currently disabled.""" - return "Secret data" -``` +from fastmcp import FastMCP -You can also toggle a resource's state programmatically after it has been created: +mcp = FastMCP("MyServer") -```python -@mcp.resource("data://config") -def get_config(): return {"version": 1} +@mcp.resource("data://public", tags={"public"}) +def get_public(): return "public" -# Disable and re-enable the resource -get_config.disable() -get_config.enable() +@mcp.resource("data://secret", tags={"internal"}) +def get_secret(): return "secret" + +# Disable specific resources by key +mcp.disable(keys=["resource:data://secret"]) + +# Disable resources by tag +mcp.disable(tags={"internal"}) + +# Or use allowlist mode - only show resources with specific tags +mcp.enable(tags={"public"}, only=True) ``` +See [Local Provider](/servers/providers/local#visibility-control) for the complete visibility control API including key formats and tag-based filtering. + ### Accessing MCP Context @@ -367,9 +371,9 @@ def example_resource() -> str: return "Hello!" # These operations trigger notifications: -mcp.add_resource(example_resource) # Sends resources/list_changed notification -example_resource.disable() # Sends resources/list_changed notification -example_resource.enable() # Sends resources/list_changed notification +mcp.add_resource(example_resource) # Sends resources/list_changed notification +mcp.disable(keys=["resource:data://example"]) # Sends resources/list_changed notification +mcp.enable(keys=["resource:data://example"]) # Sends resources/list_changed notification ``` Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications. diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx index 8d8d362f84..70547ebee1 100644 --- a/docs/servers/server.mdx +++ b/docs/servers/server.mdx @@ -266,7 +266,7 @@ For more complex web applications, consider [mounting your MCP server into a Fas FastMCP supports composing multiple servers together using `import_server` (static copy) and `mount` (live link). This allows you to organize large applications into modular components or reuse existing servers. -See the [Server Composition](/servers/composition) guide for full details, best practices, and examples. +See the [Mounting Servers](/servers/providers/mounting) guide for full details, best practices, and examples. ```python # Example: Importing a subserver @@ -277,11 +277,11 @@ main = FastMCP(name="Main") sub = FastMCP(name="Sub") @sub.tool -def hello(): +def hello(): return "hi" # Mount directly -main.mount(sub, prefix="sub") +main.mount(sub, namespace="sub") ``` ## Proxying Servers @@ -292,7 +292,7 @@ FastMCP can act as a proxy for any MCP server (local or remote) using `FastMCP.a Proxies automatically handle concurrent operations safely by creating fresh sessions for each request when using disconnected clients. -See the [Proxying Servers](/servers/proxy) guide for details and advanced usage. +See the [Remote Proxies](/servers/providers/proxy) guide for details and advanced usage. ```python from fastmcp import FastMCP, Client diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index 2a5be722c3..942638a7dc 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -78,7 +78,8 @@ def search_products_implementation(query: str, category: str | None = None) -> l - A boolean to enable or disable the tool. See [Disabling Tools](#disabling-tools) for more information + Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead. + A boolean to enable or disable the tool. See [Visibility Control](#visibility-control) for the recommended approach. @@ -750,32 +751,39 @@ def divide(a: float, b: float) -> float: When `mask_error_details=True`, only error messages from `ToolError` will include details, other exceptions will be converted to a generic message. -## Disabling Tools +## Visibility Control - + -You can control the visibility and availability of tools by enabling or disabling them. This is useful for feature flagging, maintenance, or dynamically changing the toolset available to a client. Disabled tools will not appear in the list of available tools returned by `list_tools`, and attempting to call a disabled tool will result in an "Unknown tool" error, just as if the tool did not exist. - -By default, all tools are enabled. You can disable a tool upon creation using the `enabled` parameter in the decorator: +You can control which tools are visible to clients using server-level visibility control. Disabled tools don't appear in `list_tools` and can't be called. ```python -@mcp.tool(enabled=False) -def maintenance_tool(): - """This tool is currently under maintenance.""" - return "This tool is disabled." -``` +from fastmcp import FastMCP -You can also toggle a tool's state programmatically after it has been created: +mcp = FastMCP("MyServer") -```python -@mcp.tool -def dynamic_tool(): - return "I am a dynamic tool." +@mcp.tool(tags={"admin"}) +def admin_action() -> str: + """Admin-only action.""" + return "Done" -# Disable and re-enable the tool -dynamic_tool.disable() -dynamic_tool.enable() +@mcp.tool(tags={"public"}) +def public_action() -> str: + """Public action.""" + return "Done" + +# Disable specific tools by key +mcp.disable(keys=["tool:admin_action"]) + +# Disable tools by tag +mcp.disable(tags={"admin"}) + +# Or use allowlist mode - only show tools with specific tags +mcp.enable(tags={"public"}, only=True) ``` + +See [Local Provider](/servers/providers/local#visibility-control) for the complete visibility control API including key formats and tag-based filtering. + ## MCP Annotations @@ -878,10 +886,10 @@ def example_tool() -> str: return "Hello!" # These operations trigger notifications: -mcp.add_tool(example_tool) # Sends tools/list_changed notification -example_tool.disable() # Sends tools/list_changed notification -example_tool.enable() # Sends tools/list_changed notification -mcp.remove_tool("example_tool") # Sends tools/list_changed notification +mcp.add_tool(example_tool) # Sends tools/list_changed notification +mcp.disable(keys=["tool:example_tool"]) # Sends tools/list_changed notification +mcp.enable(keys=["tool:example_tool"]) # Sends tools/list_changed notification +mcp.remove_tool("example_tool") # Sends tools/list_changed notification ``` Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications. diff --git a/docs/snippets/version-badge.mdx b/docs/snippets/version-badge.mdx index 97d721e027..3442524b86 100644 --- a/docs/snippets/version-badge.mdx +++ b/docs/snippets/version-badge.mdx @@ -1,6 +1,6 @@ export const VersionBadge = ({ version }) => { return ( - + New in version {version} );