mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 21:14:17 +02:00
Fix app tool routing: visibility check and middleware propagation (#3591)
This commit is contained in:
parent
204e566227
commit
e5dce51286
4 changed files with 53 additions and 8 deletions
|
|
@ -354,9 +354,12 @@ class FastMCPApp(Provider):
|
|||
if not isinstance(tool, Tool):
|
||||
tool = Tool._ensure_tool(tool)
|
||||
|
||||
# Tag with app name for routing
|
||||
# Tag with app name and visibility for routing
|
||||
meta = dict(tool.meta) if tool.meta else {}
|
||||
meta.setdefault("fastmcp", {})["app"] = self.name
|
||||
ui = meta.setdefault("ui", {})
|
||||
if "visibility" not in ui:
|
||||
ui["visibility"] = ["app"]
|
||||
tool.meta = meta
|
||||
|
||||
self._local._add_component(tool)
|
||||
|
|
|
|||
|
|
@ -194,7 +194,16 @@ class Provider:
|
|||
if tool is not None:
|
||||
meta = tool.meta or {}
|
||||
fastmcp_meta = meta.get("fastmcp")
|
||||
if isinstance(fastmcp_meta, dict) and fastmcp_meta.get("app") == app_name:
|
||||
ui_meta = meta.get("ui")
|
||||
# Must match app name AND have app visibility (not model-only)
|
||||
visibility = (
|
||||
ui_meta.get("visibility", []) if isinstance(ui_meta, dict) else []
|
||||
)
|
||||
if (
|
||||
isinstance(fastmcp_meta, dict)
|
||||
and fastmcp_meta.get("app") == app_name
|
||||
and "app" in visibility
|
||||
):
|
||||
return tool
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -137,11 +137,23 @@ class FastMCPProviderTool(Tool):
|
|||
# Pass exact version so child executes the correct version
|
||||
version = VersionSpec(eq=self.version) if self.version else None
|
||||
|
||||
# If this tool belongs to a FastMCPApp, pass app_name so the
|
||||
# child server routes via get_app_tool (bypassing transforms).
|
||||
app_name: str | None = None
|
||||
meta = self.meta or {}
|
||||
fastmcp_meta = meta.get("fastmcp")
|
||||
if isinstance(fastmcp_meta, dict):
|
||||
app_name = fastmcp_meta.get("app")
|
||||
|
||||
with delegate_span(
|
||||
self._original_name or "", "FastMCPProvider", self._original_name or ""
|
||||
):
|
||||
return await self._server.call_tool(
|
||||
self._original_name, arguments, version=version, task_meta=task_meta
|
||||
self._original_name,
|
||||
arguments,
|
||||
version=version,
|
||||
task_meta=task_meta,
|
||||
app_name=app_name,
|
||||
)
|
||||
|
||||
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
||||
|
|
@ -153,8 +165,14 @@ class FastMCPProviderTool(Tool):
|
|||
# Pass exact version so child executes the correct version
|
||||
version = VersionSpec(eq=self.version) if self.version else None
|
||||
|
||||
app_name: str | None = None
|
||||
meta = self.meta or {}
|
||||
fastmcp_meta = meta.get("fastmcp")
|
||||
if isinstance(fastmcp_meta, dict):
|
||||
app_name = fastmcp_meta.get("app")
|
||||
|
||||
result = await self._server.call_tool(
|
||||
self._original_name, arguments, version=version
|
||||
self._original_name, arguments, version=version, app_name=app_name
|
||||
)
|
||||
# Result from call_tool should always be ToolResult when no task_meta
|
||||
if isinstance(result, mcp.types.CreateTaskResult):
|
||||
|
|
@ -563,8 +581,11 @@ class FastMCPProvider(Provider):
|
|||
return FastMCPProviderTool.wrap(self.server, raw_tool)
|
||||
|
||||
async def get_app_tool(self, app_name: str, tool_name: str) -> Tool | None:
|
||||
"""Delegate to nested server's get_app_tool, bypassing transforms."""
|
||||
return await self.server.get_app_tool(app_name, tool_name)
|
||||
"""Delegate to nested server's get_app_tool, wrapping for middleware."""
|
||||
raw_tool = await self.server.get_app_tool(app_name, tool_name)
|
||||
if raw_tool is None:
|
||||
return None
|
||||
return FastMCPProviderTool.wrap(self.server, raw_tool)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Resource methods
|
||||
|
|
|
|||
|
|
@ -386,8 +386,9 @@ class TestGetAppTool:
|
|||
assert tool is not None
|
||||
assert tool.name == "save_contact"
|
||||
|
||||
async def test_ui_tool_findable_by_app_name(self):
|
||||
"""@app.ui() tools are also tagged with app name."""
|
||||
async def test_ui_tool_not_findable_via_get_app_tool(self):
|
||||
"""@app.ui() tools have model visibility and should NOT be
|
||||
returned by get_app_tool (only app-visible tools are)."""
|
||||
app = FastMCPApp("dashboard")
|
||||
|
||||
@app.ui()
|
||||
|
|
@ -395,6 +396,17 @@ class TestGetAppTool:
|
|||
return "ui"
|
||||
|
||||
tool = await app.get_app_tool("dashboard", "show")
|
||||
assert tool is None
|
||||
|
||||
async def test_model_visible_tool_findable(self):
|
||||
"""@app.tool(model=True) has app visibility and IS findable."""
|
||||
app = FastMCPApp("test")
|
||||
|
||||
@app.tool(model=True)
|
||||
def query(q: str) -> str:
|
||||
return q
|
||||
|
||||
tool = await app.get_app_tool("test", "query")
|
||||
assert tool is not None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue