Spaces:
Running
Running
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
|
| 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(
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 "
|
| 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 "
|
| 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
|
| 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
|
| 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
|
| 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
|
|
|
|
| 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 "
|
| 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["
|
| 24 |
-
assert tool.name == "
|
| 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 "
|
| 50 |
-
assert "
|
| 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 "
|
| 72 |
|
| 73 |
# Mount second app to same prefix
|
| 74 |
main_app.mount("api", second_app)
|
| 75 |
|
| 76 |
# Verify second tool is there
|
| 77 |
-
assert "
|
| 78 |
|
| 79 |
# Tools from both mounts are combined
|
| 80 |
-
assert "
|
| 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 "
|
| 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 "
|
| 184 |
-
assert "
|
|
|
|
| 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
|