Jeremiah Lowin commited on
Commit
5a21dab
·
1 Parent(s): 88d77c1

Update composition docs

Browse files
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. In general, importing is best for simpler cases because it copies the imported server's components into the main server, treating them as native integrations. Mounting is best for more complex cases where you need to delegate requests to the subserver at runtime.
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
- # --- Define Subservers ---
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
- # Calculator Service
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
- # --- Import Subservers ---
80
  async def setup():
81
- # Import weather service with prefix "weather"
82
  await main_mcp.import_server("weather", weather_mcp)
83
 
84
- # Import calculator service with prefix "calc"
85
- await main_mcp.import_server("calc", calc_mcp)
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`. Their names are automatically prefixed using the `prefix` and a default separator (`_`).
108
  - `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
109
- 2. **Resources**: All resources from `subserver` are added. Their URIs are prefixed using the `prefix` and a default separator (`+`).
110
  - `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="{prefix}+data://info")`.
111
- 3. **Resource Templates**: All templates from `subserver` are added. Their URI *templates* are prefixed similarly to resources.
112
  - `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="{prefix}+data://{id}")`.
113
- 4. **Prompts**: All prompts from `subserver` are added, with names prefixed like tools.
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 from the `subserver` into the `main_mcp` instance at the time the method is called. Changes made to the `subserver` *after* `import_server` is called **will not** be reflected in `main_mcp`. Also, the `subserver`'s `lifespan` context is **not** executed by the main server when using `import_server`.
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
- # --- Define Subserver ---
145
  dynamic_mcp = FastMCP(name="DynamicService")
 
146
  @dynamic_mcp.tool()
147
- def initial_tool(): return "Initial Tool Exists"
 
 
148
 
149
- # --- Define Main Server ---
150
  main_mcp = FastMCP(name="MainAppLive")
151
-
152
- # --- Mount Subserver (Sync operation) ---
153
  main_mcp.mount("dynamic", dynamic_mcp)
154
 
155
- print("Mounted dynamic_mcp.")
156
-
157
- # --- Add a tool AFTER mounting ---
158
  @dynamic_mcp.tool()
159
- def added_later(): return "Tool Added Dynamically!"
 
 
160
 
161
- print("Added 'added_later' tool to dynamic_mcp.")
162
-
163
- # --- Test Access ---
164
  async def test_dynamic_mount():
165
- # Need to use await for get_tools now
166
- tools_before = await main_mcp.get_tools()
167
- print("Tools available via main_mcp:", list(tools_before.keys()))
168
- # Expected: ['dynamic_initial_tool', 'dynamic_added_later']
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 of calling dynamic_added_later:", result[0].text)
174
- # Expected: Tool Added Dynamically!
175
 
176
  if __name__ == "__main__":
177
- # Need async context to test
178
- asyncio.run(test_dynamic_mount())
179
- # To run the server itself:
180
- # main_mcp.run()
181
  ```
182
 
183
  ### How Mounting Works
184
 
185
- Mounting creates a relationship between two servers where one server (the parent) delegates certain operations to another (the mounted server) based on prefixes. When mounting is configured:
186
 
187
  1. **Live Link**: The parent server establishes a connection to the mounted server.
188
- 2. **Dynamic Updates**: Changes made to the mounted server (e.g., adding new tools) are immediately reflected when accessed through the parent server.
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 modes for mounting servers:
213
 
214
- 1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory for optimal performance and observability. In this mode:
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. In this mode:
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 by explicitly setting `as_proxy=False` or `as_proxy=True`.
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 since the proxy server is already designed to be accessed via a client interface.
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 - this will preserve full client lifecycle
248
  main_server.mount("remote", remote_proxy)
249
  ```
250
 
251
- This is particularly useful for incorporating remote servers into your local application architecture.
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  ## Example: Modular Application
255
 
256
- Here's how a modular application might use `import_server`:
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 dict
318
 
319
  data_mcp = FastMCP(name="DataAPI")
320
 
321
  @data_mcp.tool()
322
- def fetch_record(record_id: int) -> dict:
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) -> dict:
328
  """Provides a dummy schema for a table."""
329
  return {"table": table, "columns": ["id", "value"]}
330
  ```
331
-
332
  </CodeGroup>
333
- Now, running `main.py` starts a server that exposes:
334
- - `text_count_words`
 
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.server import 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.server import 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")