Spaces:
Running
Running
| --- | |
| title: Server Composition | |
| sidebarTitle: Composition | |
| description: Combine multiple FastMCP servers into a single, larger application using mounting. | |
| icon: puzzle-piece | |
| --- | |
| 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 the `server.mount()` method, allowing you to combine multiple `FastMCP` instances into a single, unified server. | |
| ## 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. | |
| ## Mounting Subservers | |
| The `mount()` method attaches all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) onto another (the *main server*). A `prefix` is added to avoid naming conflicts. | |
| ```python | |
| from fastmcp import FastMCP | |
| from typing import dict, list | |
| # --- Define Subservers --- | |
| # Weather Service | |
| weather_mcp = FastMCP(name="WeatherService") | |
| def get_forecast(city: str) -> dict: | |
| """Get weather forecast.""" | |
| return {"city": city, "forecast": "Sunny"} | |
| def list_supported_cities() -> list[str]: | |
| """List cities with weather support.""" | |
| return ["London", "Paris", "Tokyo"] | |
| # Calculator Service | |
| calc_mcp = FastMCP(name="CalculatorService") | |
| def add(a: int, b: int) -> int: | |
| """Add two numbers.""" | |
| return a + b | |
| def explain_addition() -> str: | |
| """Explain the concept of addition.""" | |
| return "Addition is the process of combining two or more numbers." | |
| # --- Define Main Server --- | |
| main_mcp = FastMCP(name="MainApp") | |
| # --- Mount Subservers --- | |
| # Mount weather service with prefix "weather" | |
| main_mcp.mount("weather", weather_mcp) | |
| # Mount calculator service with prefix "calc" | |
| main_mcp.mount("calc", calc_mcp) | |
| # --- Now, main_mcp contains combined components --- | |
| # Tools: | |
| # - "weather_get_forecast" | |
| # - "calc_add" | |
| # Resources: | |
| # - "weather+data://cities/supported" (prefixed URI) | |
| # Prompts: | |
| # - "calc_explain_addition" | |
| if __name__ == "__main__": | |
| # Run the main server, which now includes components from both subservers | |
| main_mcp.run() | |
| ``` | |
| ### How Mounting Works | |
| When you call `main_mcp.mount(prefix, subserver)`: | |
| 1. **Tools**: All tools from `subserver` are added to `main_mcp`. Their names are automatically prefixed using the `prefix` and a default separator (`_`). | |
| - `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`. | |
| 2. **Resources**: All resources from `subserver` are added. Their URIs are prefixed using the `prefix` and a default separator (`+`). | |
| - `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="{prefix}+data://info")`. | |
| 3. **Resource Templates**: All templates from `subserver` are added. Their URI *templates* are prefixed similarly to resources. | |
| - `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="{prefix}+data://{id}")`. | |
| 4. **Prompts**: All prompts from `subserver` are added, with names prefixed like tools. | |
| - `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`. | |
| 5. **Lifespan Management**: If the `subserver` has a `lifespan` function defined, it will be automatically executed within the `main_mcp`'s lifespan context. This ensures that setup and teardown logic for the subserver runs correctly. | |
| ### Customizing Separators | |
| You might prefer different separators for the prefixed names and URIs. You can customize these when calling `mount()`: | |
| ```python | |
| main_mcp.mount( | |
| prefix="api", | |
| app=some_subserver, | |
| tool_separator="/", # Tool name becomes: "api/sub_tool_name" | |
| resource_separator=":", # Resource URI becomes: "api:data://sub_resource" | |
| prompt_separator="." # Prompt name becomes: "api.sub_prompt_name" | |
| ) | |
| ``` | |
| <Warning> | |
| Be cautious when choosing separators. Some MCP clients (like Claude Desktop) might have restrictions on characters allowed in tool names (e.g., `/` might not be supported). The defaults (`_` for names, `+` for URIs) are generally safe. | |
| </Warning> | |
| ## Example: Modular Application | |
| ```python | |
| # modules/text_utils.py | |
| from fastmcp import FastMCP | |
| from typing import list | |
| text_mcp = FastMCP(name="TextUtilities") | |
| @text_mcp.tool() | |
| def count_words(text: str) -> int: | |
| """Counts words in a text.""" | |
| return len(text.split()) | |
| @text_mcp.resource("resource://stopwords") | |
| def get_stopwords() -> list[str]: | |
| """Return a list of common stopwords.""" | |
| return ["the", "a", "is", "in"] | |
| # ------------------------------ | |
| # modules/data_api.py | |
| from fastmcp import FastMCP | |
| import random | |
| from typing import dict | |
| data_mcp = FastMCP(name="DataAPI") | |
| @data_mcp.tool() | |
| def fetch_record(record_id: int) -> dict: | |
| """Fetches a dummy data record.""" | |
| return {"id": record_id, "value": random.random()} | |
| @data_mcp.resource("data://schema/{table}") | |
| def get_table_schema(table: str) -> dict: | |
| """Provides a dummy schema for a table.""" | |
| return {"table": table, "columns": ["id", "value"]} | |
| # ------------------------------ | |
| # main_app.py | |
| from fastmcp import FastMCP | |
| from modules.text_utils import text_mcp # Import server instances | |
| from modules.data_api import data_mcp | |
| app = FastMCP(name="MainApplication") | |
| # Mount the utility servers | |
| app.mount("text", text_mcp) | |
| app.mount("data", data_mcp) | |
| @app.tool() | |
| def process_and_analyze(record_id: int) -> str: | |
| """Fetches a record and analyzes its string representation.""" | |
| # In a real application, you'd use proper methods to interact between | |
| # mounted tools rather than accessing internal managers | |
| # Get record data | |
| record = {"id": record_id, "value": random.random()} | |
| # Count words in the record string representation | |
| word_count = len(str(record).split()) | |
| return ( | |
| f"Record {record_id} has {word_count} words in its string " | |
| f"representation." | |
| ) | |
| if __name__ == "__main__": | |
| app.run() | |
| ``` | |
| Now, running `main_app.py` starts a server that exposes: | |
| - `text_count_words` | |
| - `data_fetch_record` | |
| - `process_and_analyze` | |
| - `text+resource://stopwords` | |
| - `data+data://schema/{table}` (template) | |
| This pattern promotes code organization and reuse within your FastMCP projects. |