Skip to content

Add FastMCPApp β€” a Provider for composable MCP applications#3385

Open
jlowin wants to merge 6 commits intomainfrom
feature/fastmcp-app
Open

Add FastMCPApp β€” a Provider for composable MCP applications#3385
jlowin wants to merge 6 commits intomainfrom
feature/fastmcp-app

Conversation

@jlowin
Copy link
Member

@jlowin jlowin commented Mar 4, 2026

Today, app=True on 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_contact becomes crm_save_contact, and the CallTool("save_contact") baked into the UI stops working.

FastMCPApp is 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.

from fastmcp import FastMCP, FastMCPApp

app = FastMCPApp("CRM")

@app.ui()
def contact_form():
    return Form(on_submit=CallTool(save_contact))

@app.tool()
def save_contact(name: str, email: str) -> dict:
    return {"name": name, "email": email}

# Works standalone
app.run()

# Works composed β€” global keys survive the namespace transform
server = FastMCP("Platform")
server.add_provider(app, namespace="crm")

@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's call_tool checks the global key registry as a fallback after normal resolution, so CallTool references 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 of CallTool("string")) is wired defensively β€” it no-ops until Prefab ships set_call_tool_serializer. The FastMCP side is self-contained and independently mergeable.

@marvin-context-protocol marvin-context-protocol bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality. provider Related to the FastMCP Provider class labels Mar 4, 2026
Copy link

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

if tool is None:
from fastmcp.server.app import _APP_TOOL_REGISTRY

tool = _APP_TOOL_REGISTRY.get(name)

Choose a reason for hiding this comment

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

P1 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +401 to +403
global_key = _make_global_key(tool.name)
_stamp_global_key(tool, global_key)
_APP_TOOL_REGISTRY[global_key] = tool

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

@jlowin jlowin added the mcp apps Related to MCP Apps - user-facing applications with frontend bundles served by MCP servers. label Mar 4, 2026
@jlowin jlowin added this to the 3.2 milestone Mar 4, 2026
@jlowin
Copy link
Member Author

jlowin commented Mar 4, 2026

Note: The callable resolver wiring (to_json(tool_resolver=...)) requires a Prefab release with PrefectHQ/prefab#222. The current code detects support via signature inspection and falls back to plain to_json() on older versions, so there's no hard breakage. Once Prefab ships with tool_resolver, we should bump the apps extra to require that version (prefab-ui>=0.X.0).

Copy link

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

Comment on lines +989 to +990
tool = _APP_TOOL_REGISTRY.get(name)
if tool is not None:

Choose a reason for hiding this comment

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

P1 Badge 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 πŸ‘Β / πŸ‘Ž.

Comment on lines +399 to +403
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

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Copy link

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

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):

Choose a reason for hiding this comment

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

P2 Badge 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

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Copy link

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

ui = meta.get("ui", {})
if isinstance(ui, dict) and "globalKey" in ui:
global_key = ui["globalKey"]
_APP_TOOL_REGISTRY[global_key] = tool

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Copy link

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

Comment on lines +402 to +404
global_key = _make_global_key(tool.name)
_stamp_global_key(tool, global_key)

Choose a reason for hiding this comment

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

P2 Badge 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)

Choose a reason for hiding this comment

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

P2 Badge 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 πŸ‘Β / πŸ‘Ž.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. mcp apps Related to MCP Apps - user-facing applications with frontend bundles served by MCP servers. 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