Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
5a21dab
1
Parent(s): 88d77c1
Update composition docs
Browse files- docs/servers/composition.mdx +76 -126
- examples/complex_inputs.py +1 -1
- examples/desktop.py +1 -1
docs/servers/composition.mdx
CHANGED
|
@@ -22,8 +22,7 @@ As your MCP applications grow, you might want to organize your tools, resources,
|
|
| 22 |
|
| 23 |
### Importing vs Mounting
|
| 24 |
|
| 25 |
-
The choice of importing or mounting depends on your use case and requirements.
|
| 26 |
-
|
| 27 |
|
| 28 |
| Feature | Importing | Mounting |
|
| 29 |
|---------|----------------|---------|
|
|
@@ -36,7 +35,6 @@ The choice of importing or mounting depends on your use case and requirements. I
|
|
| 36 |
|
| 37 |
FastMCP supports [MCP proxying](/patterns/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.
|
| 38 |
|
| 39 |
-
|
| 40 |
## Importing (Static Composition)
|
| 41 |
|
| 42 |
The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). A `prefix` is added to avoid naming conflicts.
|
|
@@ -45,9 +43,7 @@ The `import_server()` method copies all components (tools, resources, templates,
|
|
| 45 |
from fastmcp import FastMCP
|
| 46 |
import asyncio
|
| 47 |
|
| 48 |
-
#
|
| 49 |
-
|
| 50 |
-
# Weather Service
|
| 51 |
weather_mcp = FastMCP(name="WeatherService")
|
| 52 |
|
| 53 |
@weather_mcp.tool()
|
|
@@ -60,43 +56,19 @@ def list_supported_cities() -> list[str]:
|
|
| 60 |
"""List cities with weather support."""
|
| 61 |
return ["London", "Paris", "Tokyo"]
|
| 62 |
|
| 63 |
-
#
|
| 64 |
-
calc_mcp = FastMCP(name="CalculatorService")
|
| 65 |
-
|
| 66 |
-
@calc_mcp.tool()
|
| 67 |
-
def add(a: int, b: int) -> int:
|
| 68 |
-
"""Add two numbers."""
|
| 69 |
-
return a + b
|
| 70 |
-
|
| 71 |
-
@calc_mcp.prompt()
|
| 72 |
-
def explain_addition() -> str:
|
| 73 |
-
"""Explain the concept of addition."""
|
| 74 |
-
return "Addition is the process of combining two or more numbers."
|
| 75 |
-
|
| 76 |
-
# --- Define Main Server ---
|
| 77 |
main_mcp = FastMCP(name="MainApp")
|
| 78 |
|
| 79 |
-
#
|
| 80 |
async def setup():
|
| 81 |
-
# Import weather service with prefix "weather"
|
| 82 |
await main_mcp.import_server("weather", weather_mcp)
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# --- Now, main_mcp contains *copied* components ---
|
| 88 |
-
# Tools:
|
| 89 |
-
# - "weather_get_forecast"
|
| 90 |
-
# - "calc_add"
|
| 91 |
-
# Resources:
|
| 92 |
-
# - "weather+data://cities/supported" (prefixed URI)
|
| 93 |
-
# Prompts:
|
| 94 |
-
# - "calc_explain_addition"
|
| 95 |
|
| 96 |
if __name__ == "__main__":
|
| 97 |
-
# In a real app, you might run this async or setup imports differently
|
| 98 |
asyncio.run(setup())
|
| 99 |
-
# Run the main server, which now includes components from both subservers
|
| 100 |
main_mcp.run()
|
| 101 |
```
|
| 102 |
|
|
@@ -104,34 +76,16 @@ if __name__ == "__main__":
|
|
| 104 |
|
| 105 |
When you call `await main_mcp.import_server(prefix, subserver)`:
|
| 106 |
|
| 107 |
-
1. **Tools**: All tools from `subserver` are added to `main_mcp`
|
| 108 |
- `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
|
| 109 |
-
2. **Resources**: All resources
|
| 110 |
- `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="{prefix}+data://info")`.
|
| 111 |
-
3. **Resource Templates**:
|
| 112 |
- `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="{prefix}+data://{id}")`.
|
| 113 |
-
4. **Prompts**: All prompts
|
| 114 |
- `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`.
|
| 115 |
|
| 116 |
-
Note that `import_server` performs a **one-time copy** of components
|
| 117 |
-
|
| 118 |
-
### Customizing Separators
|
| 119 |
-
|
| 120 |
-
You might prefer different separators for the prefixed names and URIs. You can customize these when calling `import_server()`:
|
| 121 |
-
|
| 122 |
-
```python
|
| 123 |
-
await main_mcp.import_server(
|
| 124 |
-
prefix="api",
|
| 125 |
-
app=some_subserver,
|
| 126 |
-
tool_separator="/", # Tool name becomes: "api/sub_tool_name"
|
| 127 |
-
resource_separator=":", # Resource URI becomes: "api:data://sub_resource"
|
| 128 |
-
prompt_separator="." # Prompt name becomes: "api.sub_prompt_name"
|
| 129 |
-
)
|
| 130 |
-
```
|
| 131 |
-
|
| 132 |
-
<Warning>
|
| 133 |
-
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.
|
| 134 |
-
</Warning>
|
| 135 |
|
| 136 |
## Mounting (Live Linking)
|
| 137 |
|
|
@@ -141,88 +95,65 @@ The `mount()` method creates a **live link** between the `main_mcp` server and t
|
|
| 141 |
import asyncio
|
| 142 |
from fastmcp import FastMCP, Client
|
| 143 |
|
| 144 |
-
#
|
| 145 |
dynamic_mcp = FastMCP(name="DynamicService")
|
|
|
|
| 146 |
@dynamic_mcp.tool()
|
| 147 |
-
def initial_tool():
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
#
|
| 150 |
main_mcp = FastMCP(name="MainAppLive")
|
| 151 |
-
|
| 152 |
-
# --- Mount Subserver (Sync operation) ---
|
| 153 |
main_mcp.mount("dynamic", dynamic_mcp)
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
# --- Add a tool AFTER mounting ---
|
| 158 |
@dynamic_mcp.tool()
|
| 159 |
-
def added_later():
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
# --- Test Access ---
|
| 164 |
async def test_dynamic_mount():
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
async with Client(main_mcp) as client:
|
| 171 |
-
# Call the dynamically added tool via the main server
|
| 172 |
result = await client.call_tool("dynamic_added_later")
|
| 173 |
-
print("Result
|
| 174 |
-
#
|
| 175 |
|
| 176 |
if __name__ == "__main__":
|
| 177 |
-
|
| 178 |
-
asyncio.run(test_dynamic_mount())
|
| 179 |
-
# To run the server itself:
|
| 180 |
-
# main_mcp.run()
|
| 181 |
```
|
| 182 |
|
| 183 |
### How Mounting Works
|
| 184 |
|
| 185 |
-
|
| 186 |
|
| 187 |
1. **Live Link**: The parent server establishes a connection to the mounted server.
|
| 188 |
-
2. **Dynamic Updates**: Changes
|
| 189 |
3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server.
|
| 190 |
4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime.
|
| 191 |
|
| 192 |
The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts.
|
| 193 |
|
| 194 |
-
### Customizing Separators
|
| 195 |
-
|
| 196 |
-
Similar to `import_server`, you can customize the separators for the prefixed names and URIs:
|
| 197 |
-
|
| 198 |
-
```python
|
| 199 |
-
main_mcp.mount(
|
| 200 |
-
prefix="api",
|
| 201 |
-
app=some_subserver,
|
| 202 |
-
tool_separator="/", # Tool name becomes: "api/sub_tool_name"
|
| 203 |
-
resource_separator=":", # Resource URI becomes: "api:data://sub_resource"
|
| 204 |
-
prompt_separator="." # Prompt name becomes: "api.sub_prompt_name"
|
| 205 |
-
)
|
| 206 |
-
```
|
| 207 |
-
|
| 208 |
### Direct vs. Proxy Mounting
|
| 209 |
|
| 210 |
<VersionBadge version="2.2.7" />
|
| 211 |
|
| 212 |
-
FastMCP supports two
|
| 213 |
|
| 214 |
-
1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory
|
| 215 |
- No client lifecycle events occur on the mounted server
|
| 216 |
- The mounted server's lifespan context is not executed
|
| 217 |
- Communication is handled through direct method calls
|
| 218 |
|
| 219 |
-
2. **Proxy Mounting**: The parent server treats the mounted server as a separate entity and communicates with it through a client interface.
|
| 220 |
- Full client lifecycle events occur on the mounted server
|
| 221 |
- The mounted server's lifespan is executed when a client connects
|
| 222 |
- Communication happens via an in-memory Client transport
|
| 223 |
-
- This preserves all client-facing behaviors but is slightly less efficient
|
| 224 |
-
|
| 225 |
-
You can control which mode to use with the `as_proxy` parameter:
|
| 226 |
|
| 227 |
```python
|
| 228 |
# Direct mounting (default when no custom lifespan)
|
|
@@ -232,41 +163,64 @@ main_mcp.mount("api", api_server)
|
|
| 232 |
main_mcp.mount("api", api_server, as_proxy=True)
|
| 233 |
```
|
| 234 |
|
| 235 |
-
FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior
|
| 236 |
|
| 237 |
#### Interaction with Proxy Servers
|
| 238 |
|
| 239 |
-
When using `FastMCP.from_client()` to create a proxy server, mounting that server will always use proxy mounting
|
| 240 |
|
| 241 |
```python
|
| 242 |
-
from fastmcp import FastMCP, Client
|
| 243 |
-
|
| 244 |
# Create a proxy for a remote server
|
| 245 |
remote_proxy = FastMCP.from_client(Client("http://example.com/mcp"))
|
| 246 |
|
| 247 |
-
# Mount the proxy
|
| 248 |
main_server.mount("remote", remote_proxy)
|
| 249 |
```
|
| 250 |
|
| 251 |
-
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
## Example: Modular Application
|
| 255 |
|
| 256 |
-
Here's
|
| 257 |
|
| 258 |
<CodeGroup>
|
| 259 |
```python main.py
|
| 260 |
from fastmcp import FastMCP
|
| 261 |
import asyncio
|
| 262 |
-
|
| 263 |
-
# Import the servers (see other files)
|
| 264 |
from modules.text_server import text_mcp
|
| 265 |
from modules.data_server import data_mcp
|
|
|
|
| 266 |
|
| 267 |
app = FastMCP(name="MainApplication")
|
| 268 |
|
| 269 |
-
# Setup function for async imports
|
| 270 |
async def setup():
|
| 271 |
# Import the utility servers
|
| 272 |
await app.import_server("text", text_mcp)
|
|
@@ -275,9 +229,6 @@ async def setup():
|
|
| 275 |
@app.tool()
|
| 276 |
def process_and_analyze(record_id: int) -> str:
|
| 277 |
"""Fetches a record and analyzes its string representation."""
|
| 278 |
-
# In a real application, you'd use proper methods to interact between
|
| 279 |
-
# imported tools rather than accessing internal managers
|
| 280 |
-
|
| 281 |
# Get record data
|
| 282 |
record = {"id": record_id, "value": random.random()}
|
| 283 |
|
|
@@ -290,11 +241,10 @@ def process_and_analyze(record_id: int) -> str:
|
|
| 290 |
)
|
| 291 |
|
| 292 |
if __name__ == "__main__":
|
| 293 |
-
# Run async setup before starting the server
|
| 294 |
asyncio.run(setup())
|
| 295 |
-
# Run the server
|
| 296 |
app.run()
|
| 297 |
```
|
|
|
|
| 298 |
```python modules/text_server.py
|
| 299 |
from fastmcp import FastMCP
|
| 300 |
|
|
@@ -314,26 +264,26 @@ def get_stopwords() -> list[str]:
|
|
| 314 |
```python modules/data_server.py
|
| 315 |
from fastmcp import FastMCP
|
| 316 |
import random
|
| 317 |
-
from typing import
|
| 318 |
|
| 319 |
data_mcp = FastMCP(name="DataAPI")
|
| 320 |
|
| 321 |
@data_mcp.tool()
|
| 322 |
-
def fetch_record(record_id: int) ->
|
| 323 |
"""Fetches a dummy data record."""
|
| 324 |
return {"id": record_id, "value": random.random()}
|
| 325 |
|
| 326 |
@data_mcp.resource("data://schema/{table}")
|
| 327 |
-
def get_table_schema(table: str) ->
|
| 328 |
"""Provides a dummy schema for a table."""
|
| 329 |
return {"table": table, "columns": ["id", "value"]}
|
| 330 |
```
|
| 331 |
-
|
| 332 |
</CodeGroup>
|
| 333 |
-
|
| 334 |
-
|
|
|
|
| 335 |
- `data_fetch_record`
|
| 336 |
-
- `process_and_analyze`
|
| 337 |
- `text+resource://stopwords`
|
| 338 |
- `data+data://schema/{table}` (template)
|
| 339 |
|
|
|
|
| 22 |
|
| 23 |
### Importing vs Mounting
|
| 24 |
|
| 25 |
+
The choice of importing or mounting depends on your use case and requirements.
|
|
|
|
| 26 |
|
| 27 |
| Feature | Importing | Mounting |
|
| 28 |
|---------|----------------|---------|
|
|
|
|
| 35 |
|
| 36 |
FastMCP supports [MCP proxying](/patterns/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.
|
| 37 |
|
|
|
|
| 38 |
## Importing (Static Composition)
|
| 39 |
|
| 40 |
The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). A `prefix` is added to avoid naming conflicts.
|
|
|
|
| 43 |
from fastmcp import FastMCP
|
| 44 |
import asyncio
|
| 45 |
|
| 46 |
+
# Define subservers
|
|
|
|
|
|
|
| 47 |
weather_mcp = FastMCP(name="WeatherService")
|
| 48 |
|
| 49 |
@weather_mcp.tool()
|
|
|
|
| 56 |
"""List cities with weather support."""
|
| 57 |
return ["London", "Paris", "Tokyo"]
|
| 58 |
|
| 59 |
+
# Define main server
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
main_mcp = FastMCP(name="MainApp")
|
| 61 |
|
| 62 |
+
# Import subserver
|
| 63 |
async def setup():
|
|
|
|
| 64 |
await main_mcp.import_server("weather", weather_mcp)
|
| 65 |
|
| 66 |
+
# Result: main_mcp now contains prefixed components:
|
| 67 |
+
# - Tool: "weather_get_forecast"
|
| 68 |
+
# - Resource: "weather+data://cities/supported"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
if __name__ == "__main__":
|
|
|
|
| 71 |
asyncio.run(setup())
|
|
|
|
| 72 |
main_mcp.run()
|
| 73 |
```
|
| 74 |
|
|
|
|
| 76 |
|
| 77 |
When you call `await main_mcp.import_server(prefix, subserver)`:
|
| 78 |
|
| 79 |
+
1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.
|
| 80 |
- `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
|
| 81 |
+
2. **Resources**: All resources are added with URIs prefixed using `{prefix}+`.
|
| 82 |
- `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="{prefix}+data://info")`.
|
| 83 |
+
3. **Resource Templates**: Templates are prefixed similarly to resources.
|
| 84 |
- `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="{prefix}+data://{id}")`.
|
| 85 |
+
4. **Prompts**: All prompts are added with names prefixed like tools.
|
| 86 |
- `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`.
|
| 87 |
|
| 88 |
+
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
## Mounting (Live Linking)
|
| 91 |
|
|
|
|
| 95 |
import asyncio
|
| 96 |
from fastmcp import FastMCP, Client
|
| 97 |
|
| 98 |
+
# Define subserver
|
| 99 |
dynamic_mcp = FastMCP(name="DynamicService")
|
| 100 |
+
|
| 101 |
@dynamic_mcp.tool()
|
| 102 |
+
def initial_tool():
|
| 103 |
+
"""Initial tool demonstration."""
|
| 104 |
+
return "Initial Tool Exists"
|
| 105 |
|
| 106 |
+
# Mount subserver (synchronous operation)
|
| 107 |
main_mcp = FastMCP(name="MainAppLive")
|
|
|
|
|
|
|
| 108 |
main_mcp.mount("dynamic", dynamic_mcp)
|
| 109 |
|
| 110 |
+
# Add a tool AFTER mounting - it will be accessible through main_mcp
|
|
|
|
|
|
|
| 111 |
@dynamic_mcp.tool()
|
| 112 |
+
def added_later():
|
| 113 |
+
"""Tool added after mounting."""
|
| 114 |
+
return "Tool Added Dynamically!"
|
| 115 |
|
| 116 |
+
# Testing access to mounted tools
|
|
|
|
|
|
|
| 117 |
async def test_dynamic_mount():
|
| 118 |
+
tools = await main_mcp.get_tools()
|
| 119 |
+
print("Available tools:", list(tools.keys()))
|
| 120 |
+
# Shows: ['dynamic_initial_tool', 'dynamic_added_later']
|
| 121 |
+
|
|
|
|
| 122 |
async with Client(main_mcp) as client:
|
|
|
|
| 123 |
result = await client.call_tool("dynamic_added_later")
|
| 124 |
+
print("Result:", result[0].text)
|
| 125 |
+
# Shows: "Tool Added Dynamically!"
|
| 126 |
|
| 127 |
if __name__ == "__main__":
|
| 128 |
+
asyncio.run(test_dynamic_mount())
|
|
|
|
|
|
|
|
|
|
| 129 |
```
|
| 130 |
|
| 131 |
### How Mounting Works
|
| 132 |
|
| 133 |
+
When mounting is configured:
|
| 134 |
|
| 135 |
1. **Live Link**: The parent server establishes a connection to the mounted server.
|
| 136 |
+
2. **Dynamic Updates**: Changes to the mounted server are immediately reflected when accessed through the parent.
|
| 137 |
3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server.
|
| 138 |
4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime.
|
| 139 |
|
| 140 |
The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts.
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
### Direct vs. Proxy Mounting
|
| 143 |
|
| 144 |
<VersionBadge version="2.2.7" />
|
| 145 |
|
| 146 |
+
FastMCP supports two mounting modes:
|
| 147 |
|
| 148 |
+
1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory.
|
| 149 |
- No client lifecycle events occur on the mounted server
|
| 150 |
- The mounted server's lifespan context is not executed
|
| 151 |
- Communication is handled through direct method calls
|
| 152 |
|
| 153 |
+
2. **Proxy Mounting**: The parent server treats the mounted server as a separate entity and communicates with it through a client interface.
|
| 154 |
- Full client lifecycle events occur on the mounted server
|
| 155 |
- The mounted server's lifespan is executed when a client connects
|
| 156 |
- Communication happens via an in-memory Client transport
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
```python
|
| 159 |
# Direct mounting (default when no custom lifespan)
|
|
|
|
| 163 |
main_mcp.mount("api", api_server, as_proxy=True)
|
| 164 |
```
|
| 165 |
|
| 166 |
+
FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior with the `as_proxy` parameter.
|
| 167 |
|
| 168 |
#### Interaction with Proxy Servers
|
| 169 |
|
| 170 |
+
When using `FastMCP.from_client()` to create a proxy server, mounting that server will always use proxy mounting:
|
| 171 |
|
| 172 |
```python
|
|
|
|
|
|
|
| 173 |
# Create a proxy for a remote server
|
| 174 |
remote_proxy = FastMCP.from_client(Client("http://example.com/mcp"))
|
| 175 |
|
| 176 |
+
# Mount the proxy (always uses proxy mounting)
|
| 177 |
main_server.mount("remote", remote_proxy)
|
| 178 |
```
|
| 179 |
|
| 180 |
+
## Customizing Separators
|
| 181 |
|
| 182 |
+
Both `import_server()` and `mount()` allow you to customize the separators used for prefixing components:
|
| 183 |
+
|
| 184 |
+
<CodeGroup>
|
| 185 |
+
|
| 186 |
+
```python import_server
|
| 187 |
+
await main_mcp.import_server(
|
| 188 |
+
prefix="api",
|
| 189 |
+
app=some_subserver,
|
| 190 |
+
tool_separator="/", # Tool name becomes: "api/sub_tool_name"
|
| 191 |
+
resource_separator=":", # Resource URI becomes: "api:data://sub_resource"
|
| 192 |
+
prompt_separator="." # Prompt name becomes: "api.sub_prompt_name"
|
| 193 |
+
)
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
```python mount
|
| 197 |
+
main_mcp.mount(
|
| 198 |
+
prefix="api",
|
| 199 |
+
app=some_subserver,
|
| 200 |
+
tool_separator="/", # Tool name becomes: "api/sub_tool_name"
|
| 201 |
+
resource_separator=":", # Resource URI becomes: "api:data://sub_resource"
|
| 202 |
+
prompt_separator="." # Prompt name becomes: "api.sub_prompt_name"
|
| 203 |
+
)
|
| 204 |
+
```
|
| 205 |
+
</CodeGroup>
|
| 206 |
+
<Warning>
|
| 207 |
+
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.
|
| 208 |
+
</Warning>
|
| 209 |
|
| 210 |
## Example: Modular Application
|
| 211 |
|
| 212 |
+
Here's a modular application structure using `import_server`:
|
| 213 |
|
| 214 |
<CodeGroup>
|
| 215 |
```python main.py
|
| 216 |
from fastmcp import FastMCP
|
| 217 |
import asyncio
|
|
|
|
|
|
|
| 218 |
from modules.text_server import text_mcp
|
| 219 |
from modules.data_server import data_mcp
|
| 220 |
+
import random
|
| 221 |
|
| 222 |
app = FastMCP(name="MainApplication")
|
| 223 |
|
|
|
|
| 224 |
async def setup():
|
| 225 |
# Import the utility servers
|
| 226 |
await app.import_server("text", text_mcp)
|
|
|
|
| 229 |
@app.tool()
|
| 230 |
def process_and_analyze(record_id: int) -> str:
|
| 231 |
"""Fetches a record and analyzes its string representation."""
|
|
|
|
|
|
|
|
|
|
| 232 |
# Get record data
|
| 233 |
record = {"id": record_id, "value": random.random()}
|
| 234 |
|
|
|
|
| 241 |
)
|
| 242 |
|
| 243 |
if __name__ == "__main__":
|
|
|
|
| 244 |
asyncio.run(setup())
|
|
|
|
| 245 |
app.run()
|
| 246 |
```
|
| 247 |
+
|
| 248 |
```python modules/text_server.py
|
| 249 |
from fastmcp import FastMCP
|
| 250 |
|
|
|
|
| 264 |
```python modules/data_server.py
|
| 265 |
from fastmcp import FastMCP
|
| 266 |
import random
|
| 267 |
+
from typing import Dict
|
| 268 |
|
| 269 |
data_mcp = FastMCP(name="DataAPI")
|
| 270 |
|
| 271 |
@data_mcp.tool()
|
| 272 |
+
def fetch_record(record_id: int) -> Dict:
|
| 273 |
"""Fetches a dummy data record."""
|
| 274 |
return {"id": record_id, "value": random.random()}
|
| 275 |
|
| 276 |
@data_mcp.resource("data://schema/{table}")
|
| 277 |
+
def get_table_schema(table: str) -> Dict:
|
| 278 |
"""Provides a dummy schema for a table."""
|
| 279 |
return {"table": table, "columns": ["id", "value"]}
|
| 280 |
```
|
|
|
|
| 281 |
</CodeGroup>
|
| 282 |
+
|
| 283 |
+
Running `main.py` starts a server that exposes these prefixed components:
|
| 284 |
+
- `text_count_words`
|
| 285 |
- `data_fetch_record`
|
| 286 |
+
- `process_and_analyze` (defined in main app)
|
| 287 |
- `text+resource://stopwords`
|
| 288 |
- `data+data://schema/{table}` (template)
|
| 289 |
|
examples/complex_inputs.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Annotated
|
|
| 8 |
|
| 9 |
from pydantic import BaseModel, Field
|
| 10 |
|
| 11 |
-
from fastmcp
|
| 12 |
|
| 13 |
mcp = FastMCP("Shrimp Tank")
|
| 14 |
|
|
|
|
| 8 |
|
| 9 |
from pydantic import BaseModel, Field
|
| 10 |
|
| 11 |
+
from fastmcp import FastMCP
|
| 12 |
|
| 13 |
mcp = FastMCP("Shrimp Tank")
|
| 14 |
|
examples/desktop.py
CHANGED
|
@@ -6,7 +6,7 @@ A simple example that exposes the desktop directory as a resource.
|
|
| 6 |
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
-
from fastmcp
|
| 10 |
|
| 11 |
# Create server
|
| 12 |
mcp = FastMCP("Demo")
|
|
|
|
| 6 |
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
+
from fastmcp import FastMCP
|
| 10 |
|
| 11 |
# Create server
|
| 12 |
mcp = FastMCP("Demo")
|