Skip to content

Route app tool calls via ___-prefixed names#3667

Merged
jlowin merged 1 commit intomainfrom
app-tool-prefixed-names
Mar 28, 2026
Merged

Route app tool calls via ___-prefixed names#3667
jlowin merged 1 commit intomainfrom
app-tool-prefixed-names

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Mar 28, 2026

MCP hosts (Goose, MCP Jam, etc.) don't forward _meta on callServerTool, which meant app tool routing was broken in every real deployment. The previous approach injected _meta.fastmcp.app into structured content and expected hosts to echo it back — they don't.

This replaces that with a self-contained approach: the Prefab resolver writes AppName___tool_name on the wire (e.g. CallTool("store_files") becomes "Files___store_files"), and the server parses the ___ separator to route via get_app_tool, bypassing transforms. No host cooperation needed.

app = FastMCPApp("Files")

@app.tool()
def store_files(files: list[dict]) -> list[dict]:
    # app-only tool — hidden from model, but the UI can call it
    # as "Files___store_files" on the wire
    ...

@app.ui()
def file_manager() -> PrefabApp:
    # CallTool("store_files") in the UI resolves to "Files___store_files"
    Button("Upload", on_click=CallTool("store_files", ...))
    ...

Both string and callable tool references get prefixed. App names containing ___ are rejected at construction time. The app_name parameter is removed from call_tool entirely — routing is derived from the tool name alone.

Hosts (Goose, MCP Jam) don't forward _meta on callServerTool, which
broke app tool routing entirely. Encode the app identity in the tool
name on the wire instead: the resolver writes "AppName___tool_name",
and the server parses it to route via get_app_tool.
@marvin-context-protocol marvin-context-protocol Bot added breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. mcp apps Related to MCP Apps - user-facing applications with frontend bundles served by MCP servers. server Related to FastMCP server implementation or server-side functionality. labels Mar 28, 2026
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

Here are some automated review suggestions for this pull request.

Reviewed commit: 773f6586e0

ℹ️ 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/apps/app.py
Comment on lines +64 to +65
if app_name and "___" not in name:
return f"{app_name}___{name}"
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 Prefix app tool refs even when tool names contain "___"

The resolver currently skips prefixing whenever a tool reference contains "___", which breaks legitimate app-local tool names like save___draft: CallTool("save___draft") is serialized unchanged, then call_tool parses it as app=save/tool=draft and returns Unknown tool. This regression affects both string and callable refs (via __name__/metadata) and makes valid tool names unreachable unless users manually hardcode fully-qualified names. The skip condition should only treat names already prefixed for the same app as resolved.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

App names are validated at construction time — FastMCPApp("my___app") raises ValueError. So the only way ___ appears in a tool name on the wire is through the resolver prefix.

The guard if "___" not in fn prevents double-prefixing when a name has already been resolved (e.g. passed through from another layer). A tool literally named save___draft would be unusual, but if it existed, it would still work through normal get_tool resolution — the ___ fallback only activates when normal lookup fails.

Comment on lines +1128 to +1129
tool: Tool | None = await self.get_tool(name, version=version)
if tool is None and "___" in name:
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 Route "___"-qualified app calls before generic lookup

For names like Files___store_files, call_tool now tries get_tool first and only falls back to app routing if that fails. If any normal/model-visible tool is registered with the same name, app UI calls are silently redirected to the wrong tool instead of the app-scoped backend tool, which can execute unintended logic and bypass app isolation assumptions. Prefixed names should be resolved through get_app_tool first (or generic tools using this reserved pattern should be rejected) to keep routing deterministic.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The normal-first order is intentional. A tool named Files___store_files would have to be explicitly registered with that exact name — nobody does that accidentally. The ___ separator is reserved for app routing, and app names are validated to not contain it.

Trying app routing first would add a provider tree walk to every single call_tool invocation, even for non-app tools. The current order keeps the common path fast and only does the bypass lookup when normal resolution fails and the name has the separator.

@jlowin jlowin mentioned this pull request Mar 28, 2026
@jlowin jlowin merged commit 5683c0f into main Mar 28, 2026
11 of 15 checks passed
@jlowin jlowin deleted the app-tool-prefixed-names branch March 28, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. mcp apps Related to MCP Apps - user-facing applications with frontend bundles served by MCP servers. 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