Fix app tool routing: visibility check and middleware propagation (#3591)

This commit is contained in:
Jeremiah Lowin 2026-03-22 19:59:23 -04:00 committed by GitHub
commit e5dce51286
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 53 additions and 8 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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