Jeremiah Lowin commited on
Commit
37a07a4
·
1 Parent(s): de6aafb

Use _ as default mount separator and make configurable

Browse files
examples/mount_example.py CHANGED
@@ -64,10 +64,11 @@ def check_app_status() -> dict[str, str]:
64
 
65
  # Mount sub-applications
66
  app.mount("weather", weather_app)
 
67
  app.mount("news", news_app)
68
 
69
 
70
- async def start_server():
71
  """Print information about mounted resources."""
72
  # Print available tools
73
  tools = app._tool_manager.list_tools()
@@ -105,7 +106,7 @@ async def start_server():
105
 
106
  if __name__ == "__main__":
107
  # First run our async function to display info
108
- asyncio.run(start_server())
109
 
110
  # Then start the server (uncomment to run the server)
111
  # app.run()
 
64
 
65
  # Mount sub-applications
66
  app.mount("weather", weather_app)
67
+
68
  app.mount("news", news_app)
69
 
70
 
71
+ async def get_server_details():
72
  """Print information about mounted resources."""
73
  # Print available tools
74
  tools = app._tool_manager.list_tools()
 
106
 
107
  if __name__ == "__main__":
108
  # First run our async function to display info
109
+ asyncio.run(get_server_details())
110
 
111
  # Then start the server (uncomment to run the server)
112
  # app.run()
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -85,8 +85,6 @@ class PromptManager:
85
 
86
  new_prompt = prompt.copy(updates=dict(name=prefixed_name))
87
 
88
- # Log the import
89
- logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
90
-
91
  # Store the prompt with the prefixed name
92
  self.add_prompt(new_prompt)
 
 
85
 
86
  new_prompt = prompt.copy(updates=dict(name=prefixed_name))
87
 
 
 
 
88
  # Store the prompt with the prefixed name
89
  self.add_prompt(new_prompt)
90
+ logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
src/fastmcp/resources/resource_manager.py CHANGED
@@ -156,11 +156,9 @@ class ResourceManager:
156
 
157
  new_resource = resource.copy(updates=dict(uri=prefixed_uri))
158
 
159
- # Log the import
160
- logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
161
-
162
  # Store directly in resources dictionary
163
  self.add_resource(new_resource)
 
164
 
165
  def import_templates(
166
  self, manager: "ResourceManager", prefix: str | None = None
@@ -188,10 +186,8 @@ class ResourceManager:
188
  updates=dict(uri_template=prefixed_uri_template)
189
  )
190
 
191
- # Log the import
192
- logger.debug(
193
- f"Importing resource template with URI {uri_template} as {prefixed_uri_template}"
194
- )
195
-
196
  # Store directly in templates dictionary
197
  self.add_template(new_template)
 
 
 
 
156
 
157
  new_resource = resource.copy(updates=dict(uri=prefixed_uri))
158
 
 
 
 
159
  # Store directly in resources dictionary
160
  self.add_resource(new_resource)
161
+ logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
162
 
163
  def import_templates(
164
  self, manager: "ResourceManager", prefix: str | None = None
 
186
  updates=dict(uri_template=prefixed_uri_template)
187
  )
188
 
 
 
 
 
 
189
  # Store directly in templates dictionary
190
  self.add_template(new_template)
191
+ logger.debug(
192
+ f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
193
+ )
src/fastmcp/server/server.py CHANGED
@@ -153,6 +153,7 @@ class FastMCP(Generic[LifespanResultT]):
153
 
154
  async def list_tools(self) -> list[MCPTool]:
155
  """List all available tools."""
 
156
  tools = self._tool_manager.list_tools()
157
  return [
158
  MCPTool(
@@ -534,37 +535,51 @@ class FastMCP(Generic[LifespanResultT]):
534
  logger.error(f"Error getting prompt {name}: {e}")
535
  raise ValueError(str(e))
536
 
537
- def mount(self, prefix: str, app: "FastMCP") -> None:
 
 
 
 
 
 
 
538
  """Mount another FastMCP application with a given prefix.
539
 
540
  When an application is mounted:
541
- - The tools are imported with prefixed names
542
- Example: If app has a tool named "get_weather", it will be available as "weather/get_weather"
543
- - The resources are imported with prefixed URIs
544
  Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
545
- - The templates are imported with prefixed URI templates
546
  Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
547
- - The prompts are imported with prefixed names
548
- Example: If app has a prompt named "weather_prompt", it will be available as "weather/weather_prompt"
549
 
550
  Args:
551
  prefix: The prefix to use for the mounted application
552
  app: The FastMCP application to mount
553
  """
 
 
 
 
 
 
 
554
  # Mount the app in the list of mounted apps
555
  self._mounted_apps[prefix] = app
556
 
557
- # Import tools from the mounted app with / delimiter
558
- tool_prefix = f"{prefix}/"
559
  self._tool_manager.import_tools(app._tool_manager, tool_prefix)
560
 
561
- # Import resources and templates from the mounted app with + delimiter
562
- resource_prefix = f"{prefix}+"
563
  self._resource_manager.import_resources(app._resource_manager, resource_prefix)
564
  self._resource_manager.import_templates(app._resource_manager, resource_prefix)
565
 
566
- # Import prompts with / delimiter
567
- prompt_prefix = f"{prefix}/"
568
  self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
569
 
570
  logger.info(f"Mounted app with prefix '{prefix}'")
 
153
 
154
  async def list_tools(self) -> list[MCPTool]:
155
  """List all available tools."""
156
+
157
  tools = self._tool_manager.list_tools()
158
  return [
159
  MCPTool(
 
535
  logger.error(f"Error getting prompt {name}: {e}")
536
  raise ValueError(str(e))
537
 
538
+ def mount(
539
+ self,
540
+ prefix: str,
541
+ app: "FastMCP",
542
+ tool_separator: str | None = None,
543
+ resource_separator: str | None = None,
544
+ prompt_separator: str | None = None,
545
+ ) -> None:
546
  """Mount another FastMCP application with a given prefix.
547
 
548
  When an application is mounted:
549
+ - The tools are imported with prefixed names using the tool_separator
550
+ Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
551
+ - The resources are imported with prefixed URIs using the resource_separator
552
  Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
553
+ - The templates are imported with prefixed URI templates using the resource_separator
554
  Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
555
+ - The prompts are imported with prefixed names using the prompt_separator
556
+ Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
557
 
558
  Args:
559
  prefix: The prefix to use for the mounted application
560
  app: The FastMCP application to mount
561
  """
562
+ if tool_separator is None:
563
+ tool_separator = "_"
564
+ if resource_separator is None:
565
+ resource_separator = "+"
566
+ if prompt_separator is None:
567
+ prompt_separator = "_"
568
+
569
  # Mount the app in the list of mounted apps
570
  self._mounted_apps[prefix] = app
571
 
572
+ # Import tools from the mounted app
573
+ tool_prefix = f"{prefix}{tool_separator}"
574
  self._tool_manager.import_tools(app._tool_manager, tool_prefix)
575
 
576
+ # Import resources and templates from the mounted app
577
+ resource_prefix = f"{prefix}{resource_separator}"
578
  self._resource_manager.import_resources(app._resource_manager, resource_prefix)
579
  self._resource_manager.import_templates(app._resource_manager, resource_prefix)
580
 
581
+ # Import prompts from the mounted app
582
+ prompt_prefix = f"{prefix}{prompt_separator}"
583
  self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
584
 
585
  logger.info(f"Mounted app with prefix '{prefix}'")
src/fastmcp/tools/tool_manager.py CHANGED
@@ -93,4 +93,4 @@ class ToolManager:
93
  new_tool = tool.copy(updates=dict(name=prefixed_name))
94
  # Store the copied tool
95
  self.add_tool(new_tool)
96
- logger.debug(f"Imported tool: {name} as {prefixed_name}")
 
93
  new_tool = tool.copy(updates=dict(name=prefixed_name))
94
  # Store the copied tool
95
  self.add_tool(new_tool)
96
+ logger.debug(f'Imported tool "{name}" as "{prefixed_name}"')
tests/server/test_mount.py CHANGED
@@ -16,12 +16,12 @@ async def test_mount_basic_functionality():
16
  main_app.mount("sub", sub_app)
17
 
18
  # Verify the tool was imported with the prefix
19
- assert "sub/sub_tool" in main_app._tool_manager._tools
20
  assert "sub_tool" in sub_app._tool_manager._tools
21
 
22
  # Verify the original tool still exists in the sub-app
23
- tool = main_app._tool_manager._tools["sub/sub_tool"]
24
- assert tool.name == "sub/sub_tool"
25
  assert callable(tool.fn)
26
 
27
 
@@ -46,8 +46,8 @@ async def test_mount_multiple_apps():
46
  main_app.mount("news", news_app)
47
 
48
  # Verify tools were imported with the correct prefixes
49
- assert "weather/get_forecast" in main_app._tool_manager._tools
50
- assert "news/get_headlines" in main_app._tool_manager._tools
51
 
52
 
53
  async def test_mount_combines_tools():
@@ -68,16 +68,16 @@ async def test_mount_combines_tools():
68
 
69
  # Mount first app
70
  main_app.mount("api", first_app)
71
- assert "api/first_tool" in main_app._tool_manager._tools
72
 
73
  # Mount second app to same prefix
74
  main_app.mount("api", second_app)
75
 
76
  # Verify second tool is there
77
- assert "api/second_tool" in main_app._tool_manager._tools
78
 
79
  # Tools from both mounts are combined
80
- assert "api/first_tool" in main_app._tool_manager._tools
81
 
82
 
83
  async def test_mount_with_resources():
@@ -131,7 +131,7 @@ async def test_mount_with_prompts():
131
  main_app.mount("assistant", assistant_app)
132
 
133
  # Verify the prompt was imported with the prefix
134
- assert "assistant/greeting" in main_app._prompt_manager._prompts
135
 
136
 
137
  async def test_mount_multiple_resource_templates():
@@ -180,5 +180,5 @@ async def test_mount_multiple_prompts():
180
  main_app.mount("sql", sql_app)
181
 
182
  # Verify prompts were imported with correct prefixes
183
- assert "python/review_python" in main_app._prompt_manager._prompts
184
- assert "sql/explain_sql" in main_app._prompt_manager._prompts
 
16
  main_app.mount("sub", sub_app)
17
 
18
  # Verify the tool was imported with the prefix
19
+ assert "sub_sub_tool" in main_app._tool_manager._tools
20
  assert "sub_tool" in sub_app._tool_manager._tools
21
 
22
  # Verify the original tool still exists in the sub-app
23
+ tool = main_app._tool_manager._tools["sub_sub_tool"]
24
+ assert tool.name == "sub_sub_tool"
25
  assert callable(tool.fn)
26
 
27
 
 
46
  main_app.mount("news", news_app)
47
 
48
  # Verify tools were imported with the correct prefixes
49
+ assert "weather_get_forecast" in main_app._tool_manager._tools
50
+ assert "news_get_headlines" in main_app._tool_manager._tools
51
 
52
 
53
  async def test_mount_combines_tools():
 
68
 
69
  # Mount first app
70
  main_app.mount("api", first_app)
71
+ assert "api_first_tool" in main_app._tool_manager._tools
72
 
73
  # Mount second app to same prefix
74
  main_app.mount("api", second_app)
75
 
76
  # Verify second tool is there
77
+ assert "api_second_tool" in main_app._tool_manager._tools
78
 
79
  # Tools from both mounts are combined
80
+ assert "api_first_tool" in main_app._tool_manager._tools
81
 
82
 
83
  async def test_mount_with_resources():
 
131
  main_app.mount("assistant", assistant_app)
132
 
133
  # Verify the prompt was imported with the prefix
134
+ assert "assistant_greeting" in main_app._prompt_manager._prompts
135
 
136
 
137
  async def test_mount_multiple_resource_templates():
 
180
  main_app.mount("sql", sql_app)
181
 
182
  # Verify prompts were imported with correct prefixes
183
+ assert "python_review_python" in main_app._prompt_manager._prompts
184
+ assert "sql_explain_sql" in main_app._prompt_manager._prompts