Add FastMCPApp β a Provider for composable MCP applications#3385
Add FastMCPApp β a Provider for composable MCP applications#3385
Conversation
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 665b8aec85
βΉοΈ 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".
src/fastmcp/server/server.py
Outdated
| if tool is None: | ||
| from fastmcp.server.app import _APP_TOOL_REGISTRY | ||
|
|
||
| tool = _APP_TOOL_REGISTRY.get(name) |
There was a problem hiding this comment.
Restrict global-key fallback to this server's providers
The new fallback resolves tools from a process-wide _APP_TOOL_REGISTRY without checking whether the tool belongs to the current FastMCP instance. In deployments that host multiple servers in one process, a client connected to server B can invoke a backend tool that was only mounted on server A if it knows the global key, because the lookup here returns the foreign tool directly and execution continues. This breaks server isolation and can leak cross-tenant functionality.
Useful? React with πΒ / π.
src/fastmcp/server/app.py
Outdated
| global_key = _make_global_key(tool.name) | ||
| _stamp_global_key(tool, global_key) | ||
| _APP_TOOL_REGISTRY[global_key] = tool |
There was a problem hiding this comment.
Preserve callable-to-key mapping in add_tool path
When FastMCPApp.add_tool auto-generates a global key, it only stores _APP_TOOL_REGISTRY[global_key] = tool and never records the callable in _FN_TO_GLOBAL_KEY. As a result, CallTool(fn) for tools added programmatically cannot resolve to the global key and falls back to the plain function name, which reintroduces the namespace-breakage this feature is intended to prevent in composed servers.
Useful? React with πΒ / π.
|
Note: The callable resolver wiring ( |
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9177a423e0
βΉοΈ 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".
src/fastmcp/server/server.py
Outdated
| tool = _APP_TOOL_REGISTRY.get(name) | ||
| if tool is not None: |
There was a problem hiding this comment.
Enforce visibility checks in global-key fallback
When get_tool() returns None because a tool is disabled (for example via server.disable(...) or session visibility transforms), call_tool() falls back to _APP_TOOL_REGISTRY and executes the raw tool object without re-applying visibility filtering. In that case, anyone who knows the global key can still invoke a tool that operators explicitly hid, which breaks expected disable/visibility controls for app tools.
Useful? React with πΒ / π.
src/fastmcp/server/app.py
Outdated
| if not isinstance(ui, dict) or "globalKey" not in ui: | ||
| # Assign global key for backend tools | ||
| global_key = _make_global_key(tool.name) | ||
| _stamp_global_key(tool, global_key) | ||
| _APP_TOOL_REGISTRY[global_key] = tool |
There was a problem hiding this comment.
Register existing globalKey in add_tool
FastMCPApp.add_tool only inserts into _APP_TOOL_REGISTRY when it generates a new key, so a programmatically added Tool that already has meta["ui"]["globalKey"] is never registered for global-key lookup. That makes server.call_tool(<that_global_key>) fail even though the tool advertises a global key, which breaks the βalready configuredβ branch of this API.
Useful? React with πΒ / π.
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f74db072b6
βΉοΈ 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".
src/fastmcp/server/server.py
Outdated
| skip_auth, token = _get_auth_context() | ||
| if not skip_auth and tool.auth is not None: | ||
| ctx = AuthContext(token=token, component=tool) | ||
| if not await run_auth_checks(tool.auth, ctx): |
There was a problem hiding this comment.
Catch AuthorizationError in global-key auth fallback
The global-key branch in call_tool() invokes run_auth_checks() directly but, unlike the normal get_tool() path, it does not handle AuthorizationError. Because run_auth_checks explicitly allows checks to raise AuthorizationError, a global-key call can now surface that exception to clients instead of being normalized to NotFoundError, which leaks denial details and makes global-key behavior inconsistent with regular tool lookup.
Useful? React with πΒ / π.
| def _register_global_key(tool: Tool, fn: Any, global_key: str) -> None: | ||
| """Register a tool in both process-level registries.""" | ||
| _APP_TOOL_REGISTRY[global_key] = tool | ||
| _FN_TO_GLOBAL_KEY[id(fn)] = global_key |
There was a problem hiding this comment.
Avoid overwriting callable key mapping across apps
The callable resolver map keeps only one global key per id(fn), so registering the same function object in a second FastMCPApp silently overwrites the first mapping. In that setup, CallTool(shared_fn) in the first app resolves to the second appβs key and can execute the wrong backend tool, which breaks isolation when reusable helper functions are shared between apps.
Useful? React with πΒ / π.
β¦mp prefab to 0.10.0
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ff28a6d20
βΉοΈ 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".
src/fastmcp/server/app.py
Outdated
| ui = meta.get("ui", {}) | ||
| if isinstance(ui, dict) and "globalKey" in ui: | ||
| global_key = ui["globalKey"] | ||
| _APP_TOOL_REGISTRY[global_key] = tool |
There was a problem hiding this comment.
Register global key only after add_tool succeeds
add_tool() inserts the tool into _APP_TOOL_REGISTRY (and potentially _FN_TO_GLOBAL_KEY) before calling self._local._add_component(tool). If _add_component raises (for example, duplicate tool name or version-mixing errors from LocalProvider(on_duplicate="error")), the method fails but the global key remains registered, so FastMCP.call_tool(<globalKey>) can still execute a tool that was never successfully added to the app.
Useful? React with πΒ / π.
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 483b0304ea
βΉοΈ 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".
| global_key = _make_global_key(tool.name) | ||
| _stamp_global_key(tool, global_key) | ||
|
|
There was a problem hiding this comment.
Preserve app-only visibility when auto-adding tools
FastMCPApp.add_tool treats tools without meta["ui"]["globalKey"] as backend app tools, but this branch only stamps globalKey and never sets ui.visibility to ['app'] like @app.tool does. In add_tool(fn) flows, backend helpers are therefore emitted without visibility metadata, so MCP clients that rely on ui.visibility can surface them as model-callable tools instead of app-only actions.
Useful? React with πΒ / π.
| if tool is None: | ||
| from fastmcp.server.app import get_global_tool | ||
|
|
||
| tool = get_global_tool(name) |
There was a problem hiding this comment.
Respect version constraints in global-key tool lookup
The global-key fallback path bypasses the version argument entirely: after get_tool(name, version=version) returns None, it calls get_global_tool(name) with no version check and executes whatever tool is registered under that key. This makes call_tool(..., version=...) inconsistent for global keys and can run a tool even when the requested version spec should reject it.
Useful? React with πΒ / π.
Today,
app=Trueon a tool is the way to build an MCP app β it wires up the Prefab renderer and CSP, and the tool returns a component tree. That works great for standalone servers, but breaks down under composition. When a server is mounted under a namespace,save_contactbecomescrm_save_contact, and theCallTool("save_contact")baked into the UI stops working.FastMCPAppis a Provider that makes applications a first-class concept. It binds entry-point tools (the model calls these) together with backend tools (the UI calls these via CallTool), and gives backend tools global keys β UUID-suffixed stable identifiers that survive namespace transforms.@app.ui()registers model-visible entry points with auto-wired Prefab renderer and CSP.@app.tool()registers app-visible backend tools with global keys. The server'scall_toolchecks the global key registry as a fallback after normal resolution, soCallToolreferences reach the right tool regardless of how the server is composed. Auth still applies to global-key routed tools.The callable resolver (
CallTool(fn)instead ofCallTool("string")) is wired defensively β it no-ops until Prefab shipsset_call_tool_serializer. The FastMCP side is self-contained and independently mergeable.