Skip to content

Replace UUID global keys with (app_name, tool_name) registry#3585

Merged
jlowin merged 3 commits intomainfrom
simplify-app-tool-registry
Mar 22, 2026
Merged

Replace UUID global keys with (app_name, tool_name) registry#3585
jlowin merged 3 commits intomainfrom
simplify-app-tool-registry

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Mar 22, 2026

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_TOOLS dict keyed by (app_name, tool_name). The server reads _meta.fastmcp.app from the MCP request to scope the lookup. The Prefab renderer (prefab-ui ≥0.13.1) reads _meta.fastmcp.app from the @app.ui() structured content and echoes it back on every callServerTool call.

app = FastMCPApp("Contacts")

@app.tool()
def save(name: str) -> str:
    # registered as _APP_TOOLS[("Contacts", "save")]
    return name

@app.ui()
def manager() -> PrefabApp:
    # structured content includes _meta.fastmcp.app = "Contacts"
    # CallTool("save") just serializes as "save"
    with Column() as view:
        Button("Save", on_click=CallTool("save"))
    return PrefabApp(view=view)

Two apps with the same tool name are disambiguated by app name, not UUID. Keys are deterministic and survive across processes.

jlowin added 3 commits March 22, 2026 09:32
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.
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Mar 22, 2026
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The static analysis job failed due to a transient network error while downloading the prek linter tool — not due to any code issue in this PR.

Root Cause: The CI runner got a 502 Bad Gateway response when trying to curl the prek installer from GitHub Releases:

curl: (22) The requested URL returned error: 502

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 Analysis

The static_analysis job failed at the "Run prek" step. The prek action (j178/prek-action@v1) resolved version v0.3.6 successfully, then attempted to install it via:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/download/v0.3.6/prek-installer.sh | sh

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
  • .github/workflows/ — CI workflow configuration (not a code issue)

Posted by marvin, the triage bot. This is a transient CI failure — re-running the workflow should resolve it.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

fn_name = getattr(fn, "__name__", None)
if fn_name is not None:
return ResolvedTool(name=fn_name)

P1 Badge Preserve custom tool names for callable CallTool refs

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".

Comment thread src/fastmcp/server/app.py
Comment on lines 78 to 79
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/fastmcp/server/app.py
Comment on lines 219 to 220
app_config = AppConfig(visibility=visibility)
meta: dict[str, Any] = {"ui": app_config_to_meta_dict(app_config)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jlowin
Copy link
Copy Markdown
Member Author

jlowin commented Mar 22, 2026

We're aware this PR's process-level registry (_APP_TOOLS) doesn't survive horizontally scaled environments where each tool call may land in a different process (e.g., Prefect Horizon's lambda handlers). This is a simplification over the previous UUID-based approach — fewer registries, deterministic keys, readable routing — but it's not the final architecture.

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 unwrapResult concern is handled by _meta.fastmcp.wrap_result on the tool result (already set by FastMCP, already read by the renderer at line 191 in actions.ts). The backend-tool Prefab view concern is valid for multi-step flows but out of scope for this simplification.

@jlowin jlowin merged commit d27c26e into main Mar 22, 2026
9 of 11 checks passed
@jlowin jlowin deleted the simplify-app-tool-registry branch March 22, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant