Replace UUID global keys with (app_name, tool_name) registry#3585
Replace UUID global keys with (app_name, tool_name) registry#3585
Conversation
Collapses three module-level registries (_APP_TOOL_REGISTRY,
_FN_TO_GLOBAL_KEY, _NAME_TO_GLOBAL_KEY) into one: _APP_TOOLS keyed by
(app_name, tool_name). Removes UUID generation, global key stamping in
metadata, and the complex resolver that mapped callables and strings
through multiple fallback paths.
The server now reads _meta.fastmcp.app from the MCP request (set by the
Prefab renderer) and routes directly to the named app's tool. Two apps
with the same tool name are disambiguated by app name, not by UUID.
The resolver is simplified to pass-through: CallTool("save") serializes
as "save", and the server resolves it at call time using the app context.
The @app.ui() decorator stores the FastMCPApp name in the tool's metadata. When the tool result is serialized, _prefab_to_json injects it as _meta.fastmcp.app in the structured content. The Prefab renderer reads this on init and echoes it back as _meta.fastmcp.app on every callServerTool call, completing the routing loop.
Test Failure AnalysisSummary: The static analysis job failed due to a transient network error while downloading the Root Cause: The CI runner got a This is an infrastructure/network blip on GitHub's side, unrelated to the changes in this PR. Suggested Solution: Simply re-run the failed job. No code changes are needed. Detailed AnalysisThe The curl returned HTTP 502, causing exit code 22. This is a transient GitHub infrastructure issue — the release asset URL was temporarily unavailable. Related Files
Posted by marvin, the triage bot. This is a transient CI failure — re-running the workflow should resolve it. |
There was a problem hiding this comment.
💡 Codex Review
fastmcp/src/fastmcp/server/app.py
Lines 94 to 96 in dec0405
When a backend tool is renamed, e.g. @app.tool("custom_save"), _resolve_tool_ref(save) now falls through to save.__name__ because app.tool() does not attach __fastmcp__ metadata. That makes CallTool(save) serialize as "save", but the app registry entry is stored under "custom_save", so app-originated calls fail with Unknown tool. This regresses a previously working path for function references with custom tool names.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if isinstance(fn, str): | ||
| global_key = _NAME_TO_GLOBAL_KEY.get(fn) | ||
| if global_key is not None: | ||
| tool = _APP_TOOL_REGISTRY.get(global_key) | ||
| unwrap = bool( | ||
| tool is not None | ||
| and tool.output_schema | ||
| and tool.output_schema.get("x-fastmcp-wrap-result") | ||
| ) | ||
| return ResolvedTool(name=global_key, unwrap_result=unwrap) | ||
| return ResolvedTool(name=fn) |
There was a problem hiding this comment.
Keep unwrapResult metadata when serializing CallTool names
FastMCP wraps non-object tool outputs under {"result": ...} whenever the tool schema has x-fastmcp-wrap-result. Before this refactor the resolver looked up the tool and set ResolvedTool.unwrap_result; now the string path always returns ResolvedTool(name=fn) and never preserves that flag. Any app UI that calls a str/list/primitive backend tool and reads RESULT in on_success will now receive the wrapper object instead of the raw value, breaking patterns like SetState(..., RESULT).
Useful? React with 👍 / 👎.
| app_config = AppConfig(visibility=visibility) | ||
| meta: dict[str, Any] = {"ui": app_config_to_meta_dict(app_config)} |
There was a problem hiding this comment.
Tag backend-generated Prefab views with the app identity
The new routing depends on _meta.fastmcp.app, but app.tool() still writes only meta["ui"]. For backend tools that return a PrefabApp or component (multi-step flows, follow-up dialogs, etc.), _get_fastmcp_app_name() therefore returns None, so Tool.convert_result() omits the app tag. Any CallTool(...) rendered from that returned view will then resolve by the untransformed tool name and break once the app is mounted under a namespace.
Useful? React with 👍 / 👎.
|
We're aware this PR's process-level registry ( The next step is making app-visible tools survive transforms in the provider chain itself, so tool routing works from the provider tree at call time without any process-level state. That's a separate PR. Re: Codex comments — the |
The old FastMCPApp routing used UUID-suffixed global keys (like
save_contact-a1b2c3d4), three module-level registries, and a multi-fallback resolver that mapped callables and strings through several paths. It worked, but the machinery was hard to follow and the keys were process-local.This replaces all of it with a single
_APP_TOOLSdict keyed by(app_name, tool_name). The server reads_meta.fastmcp.appfrom the MCP request to scope the lookup. The Prefab renderer (prefab-ui ≥0.13.1) reads_meta.fastmcp.appfrom the@app.ui()structured content and echoes it back on everycallServerToolcall.Two apps with the same tool name are disambiguated by app name, not UUID. Keys are deterministic and survive across processes.