Skip to content

Route app tool calls through provider chain#3587

Merged
jlowin merged 1 commit intomainfrom
app-tool-provider-chain
Mar 22, 2026
Merged

Route app tool calls through provider chain#3587
jlowin merged 1 commit intomainfrom
app-tool-provider-chain

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Mar 22, 2026

The previous PR (#3585) replaced UUID global keys with an _APP_TOOLS registry keyed by (app_name, tool_name). That was simpler but still process-level state — it doesn't survive horizontal scaling where each tool call can land in a different lambda handler.

This moves routing into the provider tree itself. Provider gets a new get_app_tool(app_name, tool_name) method with a default implementation that checks _get_tool() and matches meta.fastmcp.app. Aggregate, wrapped, and FastMCPProvider override to delegate through the tree, bypassing transforms at every level.

The result: no process-level registry at all. The provider tree IS the registry, and it exists in every process because it's constructed from code at import time.

app = FastMCPApp("Contacts")

@app.tool()
def save(name: str) -> str:
    return name

server = FastMCP("Platform")
server.add_provider(app, namespace="crm")

# Normal resolution: "crm_save" (namespaced)
# App routing: server.get_app_tool("Contacts", "save") → found directly

Tested through 4 levels of nesting (FastMCPApp → namespace → FastMCPProvider → outer server). Two apps with the same tool name are disambiguated by app name. Auth checks still apply.

…istry

Adds Provider.get_app_tool(app_name, tool_name) — a dedicated method for
finding app-visible tools by their original name, bypassing transforms.
AggregateProvider queries children, WrappedProvider delegates to inner,
FastMCPProvider delegates to nested server. The default implementation
checks _get_tool and matches meta.fastmcp.app.

This replaces the process-level _APP_TOOLS registry. Tool routing now
works through the provider tree, which exists in every process — no
shared state needed for horizontal scaling.
@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. provider Related to the FastMCP Provider class labels Mar 22, 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: 79450bcc1f

ℹ️ 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 on lines +193 to +197
tool = await self._get_tool(tool_name)
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:
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 Exclude model-only @app.ui() tools from app routing

Provider.get_app_tool() now accepts any tool whose metadata contains fastmcp.app, and FastMCP.call_tool() prefers that path whenever _meta.fastmcp.app is present (src/fastmcp/server/server.py:1108-1121). @app.ui() tools already carry that tag so they become callable from inside the UI, which breaks the documented visibility=["model"] contract in docs/apps/interactive-apps.mdx:104 and lets a UI invoke its own entry-point or other model-only entry tools.

Useful? React with 👍 / 👎.

Comment on lines +565 to +567
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)
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 Wrap mounted app tools before returning them

When app routing crosses a mounted FastMCP, this method returns the child server's raw Tool instead of a FastMCPProviderTool. The parent then executes that object directly via tool._run(...) in src/fastmcp/server/server.py:1108-1128, so the mounted server's middleware never runs for UI-originated CallTools. Any nested FastMCPApp that depends on child middleware for logging, caching, rate limiting, or similar behavior will now behave differently from normal mounted tool calls.

Useful? React with 👍 / 👎.

@jlowin jlowin merged commit 96497ac into main Mar 22, 2026
9 of 10 checks passed
@jlowin jlowin deleted the app-tool-provider-chain branch March 22, 2026 23:29
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. provider Related to the FastMCP Provider class 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