mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 21:14:17 +02:00
Merge pull request #128 from jlowin/mount
Change default mounted tool separator from / to _
This commit is contained in:
commit
6c4d86fa13
7 changed files with 50 additions and 40 deletions
|
|
@ -64,10 +64,11 @@ def check_app_status() -> dict[str, str]:
|
|||
|
||||
# Mount sub-applications
|
||||
app.mount("weather", weather_app)
|
||||
|
||||
app.mount("news", news_app)
|
||||
|
||||
|
||||
async def start_server():
|
||||
async def get_server_details():
|
||||
"""Print information about mounted resources."""
|
||||
# Print available tools
|
||||
tools = app._tool_manager.list_tools()
|
||||
|
|
@ -105,7 +106,7 @@ async def start_server():
|
|||
|
||||
if __name__ == "__main__":
|
||||
# First run our async function to display info
|
||||
asyncio.run(start_server())
|
||||
asyncio.run(get_server_details())
|
||||
|
||||
# Then start the server (uncomment to run the server)
|
||||
# app.run()
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ def _build_uv_command(
|
|||
"""Build the uv run command that runs a MCP server through mcp run."""
|
||||
cmd = ["uv"]
|
||||
|
||||
cmd.extend(["run", "--with", "mcp"])
|
||||
cmd.extend(["run", "--with", "fastmcp"])
|
||||
|
||||
if with_editable:
|
||||
cmd.extend(["--with-editable", str(with_editable)])
|
||||
|
|
@ -76,7 +76,7 @@ def _build_uv_command(
|
|||
cmd.extend(["--with", pkg])
|
||||
|
||||
# Add mcp run command
|
||||
cmd.extend(["mcp", "run", file_spec])
|
||||
cmd.extend(["fastmcp", "run", file_spec])
|
||||
return cmd
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ class PromptManager:
|
|||
|
||||
new_prompt = prompt.copy(updates=dict(name=prefixed_name))
|
||||
|
||||
# Log the import
|
||||
logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
|
||||
|
||||
# Store the prompt with the prefixed name
|
||||
self.add_prompt(new_prompt)
|
||||
logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
|
||||
|
|
|
|||
|
|
@ -156,11 +156,9 @@ class ResourceManager:
|
|||
|
||||
new_resource = resource.copy(updates=dict(uri=prefixed_uri))
|
||||
|
||||
# Log the import
|
||||
logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
|
||||
|
||||
# Store directly in resources dictionary
|
||||
self.add_resource(new_resource)
|
||||
logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
|
||||
|
||||
def import_templates(
|
||||
self, manager: "ResourceManager", prefix: str | None = None
|
||||
|
|
@ -188,10 +186,8 @@ class ResourceManager:
|
|||
updates=dict(uri_template=prefixed_uri_template)
|
||||
)
|
||||
|
||||
# Log the import
|
||||
logger.debug(
|
||||
f"Importing resource template with URI {uri_template} as {prefixed_uri_template}"
|
||||
)
|
||||
|
||||
# Store directly in templates dictionary
|
||||
self.add_template(new_template)
|
||||
logger.debug(
|
||||
f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
async def list_tools(self) -> list[MCPTool]:
|
||||
"""List all available tools."""
|
||||
|
||||
tools = self._tool_manager.list_tools()
|
||||
return [
|
||||
MCPTool(
|
||||
|
|
@ -534,37 +535,51 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
logger.error(f"Error getting prompt {name}: {e}")
|
||||
raise ValueError(str(e))
|
||||
|
||||
def mount(self, prefix: str, app: "FastMCP") -> None:
|
||||
def mount(
|
||||
self,
|
||||
prefix: str,
|
||||
app: "FastMCP",
|
||||
tool_separator: str | None = None,
|
||||
resource_separator: str | None = None,
|
||||
prompt_separator: str | None = None,
|
||||
) -> None:
|
||||
"""Mount another FastMCP application with a given prefix.
|
||||
|
||||
When an application is mounted:
|
||||
- The tools are imported with prefixed names
|
||||
Example: If app has a tool named "get_weather", it will be available as "weather/get_weather"
|
||||
- The resources are imported with prefixed URIs
|
||||
- The tools are imported with prefixed names using the tool_separator
|
||||
Example: If app has a tool named "get_weather", it will be available as "weatherget_weather"
|
||||
- The resources are imported with prefixed URIs using the resource_separator
|
||||
Example: If app has a resource with URI "weather://forecast", it will be available as "weather+weather://forecast"
|
||||
- The templates are imported with prefixed URI templates
|
||||
- The templates are imported with prefixed URI templates using the resource_separator
|
||||
Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
|
||||
- The prompts are imported with prefixed names
|
||||
Example: If app has a prompt named "weather_prompt", it will be available as "weather/weather_prompt"
|
||||
- The prompts are imported with prefixed names using the prompt_separator
|
||||
Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
|
||||
|
||||
Args:
|
||||
prefix: The prefix to use for the mounted application
|
||||
app: The FastMCP application to mount
|
||||
"""
|
||||
if tool_separator is None:
|
||||
tool_separator = "_"
|
||||
if resource_separator is None:
|
||||
resource_separator = "+"
|
||||
if prompt_separator is None:
|
||||
prompt_separator = "_"
|
||||
|
||||
# Mount the app in the list of mounted apps
|
||||
self._mounted_apps[prefix] = app
|
||||
|
||||
# Import tools from the mounted app with / delimiter
|
||||
tool_prefix = f"{prefix}/"
|
||||
# Import tools from the mounted app
|
||||
tool_prefix = f"{prefix}{tool_separator}"
|
||||
self._tool_manager.import_tools(app._tool_manager, tool_prefix)
|
||||
|
||||
# Import resources and templates from the mounted app with + delimiter
|
||||
resource_prefix = f"{prefix}+"
|
||||
# Import resources and templates from the mounted app
|
||||
resource_prefix = f"{prefix}{resource_separator}"
|
||||
self._resource_manager.import_resources(app._resource_manager, resource_prefix)
|
||||
self._resource_manager.import_templates(app._resource_manager, resource_prefix)
|
||||
|
||||
# Import prompts with / delimiter
|
||||
prompt_prefix = f"{prefix}/"
|
||||
# Import prompts from the mounted app
|
||||
prompt_prefix = f"{prefix}{prompt_separator}"
|
||||
self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
|
||||
|
||||
logger.info(f"Mounted app with prefix '{prefix}'")
|
||||
|
|
|
|||
|
|
@ -93,4 +93,4 @@ class ToolManager:
|
|||
new_tool = tool.copy(updates=dict(name=prefixed_name))
|
||||
# Store the copied tool
|
||||
self.add_tool(new_tool)
|
||||
logger.debug(f"Imported tool: {name} as {prefixed_name}")
|
||||
logger.debug(f'Imported tool "{name}" as "{prefixed_name}"')
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ async def test_mount_basic_functionality():
|
|||
main_app.mount("sub", sub_app)
|
||||
|
||||
# Verify the tool was imported with the prefix
|
||||
assert "sub/sub_tool" in main_app._tool_manager._tools
|
||||
assert "sub_sub_tool" in main_app._tool_manager._tools
|
||||
assert "sub_tool" in sub_app._tool_manager._tools
|
||||
|
||||
# Verify the original tool still exists in the sub-app
|
||||
tool = main_app._tool_manager._tools["sub/sub_tool"]
|
||||
assert tool.name == "sub/sub_tool"
|
||||
tool = main_app._tool_manager._tools["sub_sub_tool"]
|
||||
assert tool.name == "sub_sub_tool"
|
||||
assert callable(tool.fn)
|
||||
|
||||
|
||||
|
|
@ -46,8 +46,8 @@ async def test_mount_multiple_apps():
|
|||
main_app.mount("news", news_app)
|
||||
|
||||
# Verify tools were imported with the correct prefixes
|
||||
assert "weather/get_forecast" in main_app._tool_manager._tools
|
||||
assert "news/get_headlines" in main_app._tool_manager._tools
|
||||
assert "weather_get_forecast" in main_app._tool_manager._tools
|
||||
assert "news_get_headlines" in main_app._tool_manager._tools
|
||||
|
||||
|
||||
async def test_mount_combines_tools():
|
||||
|
|
@ -68,16 +68,16 @@ async def test_mount_combines_tools():
|
|||
|
||||
# Mount first app
|
||||
main_app.mount("api", first_app)
|
||||
assert "api/first_tool" in main_app._tool_manager._tools
|
||||
assert "api_first_tool" in main_app._tool_manager._tools
|
||||
|
||||
# Mount second app to same prefix
|
||||
main_app.mount("api", second_app)
|
||||
|
||||
# Verify second tool is there
|
||||
assert "api/second_tool" in main_app._tool_manager._tools
|
||||
assert "api_second_tool" in main_app._tool_manager._tools
|
||||
|
||||
# Tools from both mounts are combined
|
||||
assert "api/first_tool" in main_app._tool_manager._tools
|
||||
assert "api_first_tool" in main_app._tool_manager._tools
|
||||
|
||||
|
||||
async def test_mount_with_resources():
|
||||
|
|
@ -131,7 +131,7 @@ async def test_mount_with_prompts():
|
|||
main_app.mount("assistant", assistant_app)
|
||||
|
||||
# Verify the prompt was imported with the prefix
|
||||
assert "assistant/greeting" in main_app._prompt_manager._prompts
|
||||
assert "assistant_greeting" in main_app._prompt_manager._prompts
|
||||
|
||||
|
||||
async def test_mount_multiple_resource_templates():
|
||||
|
|
@ -180,5 +180,5 @@ async def test_mount_multiple_prompts():
|
|||
main_app.mount("sql", sql_app)
|
||||
|
||||
# Verify prompts were imported with correct prefixes
|
||||
assert "python/review_python" in main_app._prompt_manager._prompts
|
||||
assert "sql/explain_sql" in main_app._prompt_manager._prompts
|
||||
assert "python_review_python" in main_app._prompt_manager._prompts
|
||||
assert "sql_explain_sql" in main_app._prompt_manager._prompts
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue