Comprehensive MCP Apps docs, string CallTool resolution#3575
Conversation
…ui >=0.13.0
Rewrites the apps documentation as a learning journey: overview → Prefab apps
→ FastMCPApp → patterns → dev tools → custom HTML. Adds a new FastMCPApp page
covering composable apps with @app.tool()/@app.ui(), CallTool, forms, actions,
and composition. Teaches Rx() and set_initial_state() as the primary state API.
Adds string-based CallTool resolution so CallTool("save_contact") resolves to
the tool's global key, matching callable ref behavior. Requires prefab-ui 0.13.0
which passes strings through the tool resolver.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02d0780a9c
ℹ️ 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 = _NAME_TO_GLOBAL_KEY.get(fn) | ||
| if global_key is not None: | ||
| tool = _APP_TOOL_REGISTRY.get(global_key) |
There was a problem hiding this comment.
Scope string CallTool resolution to the current app
This new global lookup rewrites every CallTool("save") in the process to a single _NAME_TO_GLOBAL_KEY entry, but that registry is populated by plain tool.name (src/fastmcp/server/app.py:84 and :440). If two FastMCPApp providers both define a backend tool named save—or a regular @mcp.tool(app=True) uses CallTool("save") while some unrelated app backend also uses that name—the later registration wins and the UI will invoke the wrong backend tool. That breaks the main composition/namespacing use case this change is trying to support.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c969954ca1
ℹ️ 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 isinstance(fn, str): | ||
| global_keys = _NAME_TO_GLOBAL_KEY.get(fn) | ||
| if global_keys is not None: | ||
| if len(global_keys) > 1: | ||
| raise ValueError( | ||
| f"Ambiguous tool name {fn!r}: registered by multiple apps. " | ||
| f"Use CallTool(fn) with a function reference to disambiguate." | ||
| ) | ||
| global_key = global_keys[0] | ||
| 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) |
There was a problem hiding this comment.
Don't hijack plain string CallTool references globally
Fresh evidence in this revision: _resolve_tool_ref now consults _NAME_TO_GLOBAL_KEY before falling back to the original string, and src/fastmcp/tools/base.py:495-497 installs that resolver for every Prefab UI, not just FastMCPApp. In a mixed process, a regular @mcp.tool(app=True) view that does CallTool("save") will now be rewritten to an unrelated FastMCPApp backend named save, so the UI silently calls the wrong server tool whenever a matching app backend exists.
Useful? React with 👍 / 👎.
| def _register_global_key(tool: Tool, fn: Any, global_key: str) -> None: | ||
| """Register a tool in both process-level registries.""" | ||
| """Register a tool in all process-level registries.""" | ||
| _APP_TOOL_REGISTRY[global_key] = tool | ||
| _FN_TO_GLOBAL_KEY[id(fn)] = global_key | ||
| _NAME_TO_GLOBAL_KEY.setdefault(tool.name, []).append(global_key) |
There was a problem hiding this comment.
Avoid accumulating stale name→global-key entries
_NAME_TO_GLOBAL_KEY only ever appends, so long-lived processes accumulate duplicate registrations for the same logical backend name. That becomes user-visible on in-process reload paths: FileSystemProvider(reload=True) re-imports modules with importlib.reload() (src/fastmcp/server/providers/filesystem_discovery.py:140-144), and after the first reload a single @app.tool("save") module leaves two global keys in this list, causing _resolve_tool_ref("save") to raise ValueError("Ambiguous tool name") even though there is still only one live 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: f70e85734c
ℹ️ 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".
| self._local._add_component(tool) | ||
|
|
||
| _APP_TOOL_REGISTRY[global_key] = tool | ||
| _NAME_TO_GLOBAL_KEY[tool.name] = global_key |
There was a problem hiding this comment.
Preserve default version selection for string CallTool refs
If a FastMCPApp is assembled with versioned backend Tools via add_tool(), this single tool.name -> global_key mapping pins CallTool("save") to whichever version was registered last. That changes FastMCP's normal behavior of resolving an unversioned call to the highest matching version (LocalProvider._get_tool, src/fastmcp/server/providers/local_provider/local_provider.py:351-369), so registering save@v2 and then save@v1 makes the UI silently call v1.
Useful? React with 👍 / 👎.
| Form.from_model( | ||
| BugReport, | ||
| on_submit=CallTool( | ||
| "create_bug", |
There was a problem hiding this comment.
Make Form.from_model examples match the tool argument shape
This runnable example will fail if copied as-is. Form.from_model(BugReport) creates top-level title/severity/description fields, but FastMCP still exposes a BaseModel parameter under its argument name — tests/tools/tool/test_tool.py:187-215 shows create_user(user: UserInput, ...) expecting {"user": {...}}. Submitting this form therefore sends separate kwargs to create_bug(data: BugReport) instead of the required data object, and the later save_contact(data: ContactModel) example repeats the same mismatch.
Useful? React with 👍 / 👎.
Test Failure AnalysisSummary: The Windows Python 3.10 CI job timed out during the setup of Root Cause: The In the previous successful run (commit Suggested Solution: Add a In @pytest.fixture
@pytest.mark.timeout(30) # Windows spawn-based subprocess startup is slow
def mcp_server_url() -> Generator[str]:
with run_server_in_process(run_mcp_server) as url:
yield f"{url}/mcp"Or alternatively, add the timeout mark to the test class: @pytest.mark.timeout(30)
class TestSupabaseProviderIntegration:
...This is consistent with how other slow tests in the suite handle Windows/subprocess timing (e.g. Detailed AnalysisFailing job: Log excerpt showing the failure: Previous run timing data (commit The supabase tests pass 13 unit tests quickly (no subprocess), then the 14th test is the integration test Why the cache bust matters: The The Related Files
|
The apps documentation was thin reference material — enough for someone who already understood the feature, but not for someone learning it. This rewrites it as a proper learning path: conceptual overview → Prefab apps (with Rx, state, reactivity) → FastMCPApp (composable apps with managed tool binding) → patterns → dev tools → custom HTML escape hatch.
The big addition is the FastMCPApp page, which documents the composable app pattern end-to-end:
@app.tool(),@app.ui(),CallTool, forms, actions, visibility, and composition safety with global keys.On the code side,
CallTool("save_contact")now resolves to the tool's global key just likeCallTool(save_contact)does. A new_NAME_TO_GLOBAL_KEYregistry maps registered tool names to their global keys, and the resolver handles both strings and callables. This requires prefab-ui ≥0.13.0 which passes strings through the tool resolver.