Add fastmcp dev apps command with browser UI preview#3489
Conversation
…mp prefab to 0.10.0
These workflows run Claude to respond to /marvin mentions — linting the repo is unnecessary and fails without renderer deps installed.
The callable resolver now returns ResolvedTool (from prefab_ui) instead of a plain string, carrying metadata like unwrap_result that the renderer needs to correctly handle structuredContent envelopes. The unwrap_result flag is derived from the tool's x-fastmcp-wrap-result output schema marker.
- Replace Tabs with Pages+Select for tool picker (Rx-based reactive state) - Add --reload/--no-reload flag (default: True) to fastmcp dev apps - Kill entire process group on shutdown so port 8000 is freed properly - Suppress uvicorn websockets deprecation warning (websockets-sansio) - Bump prefab-ui to >=0.11.1 (fixes get_renderer_head bug in 0.11.0) - Add farewell tool to greet_server example for multi-tool testing
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f127cfad90
ℹ️ 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".
| user_proc, (app_bridge_js, import_map_json) = await asyncio.gather( | ||
| _start_user_server(server_spec, mcp_port, reload=reload), | ||
| _fetch_app_bridge_bundle(_EXT_APPS_VERSION, _MCP_SDK_VERSION), | ||
| ) |
There was a problem hiding this comment.
Terminate spawned MCP server when startup gather fails
If _fetch_app_bridge_bundle(...) fails (e.g., npm/esm.sh is unreachable), asyncio.gather raises before this tuple assignment completes, so user_proc never gets set even though _start_user_server(...) may already have launched a subprocess. The finally cleanup only kills the server when user_proc is not None, so a failed startup can leave the MCP server orphaned in the background and keep --mcp-port occupied for subsequent runs.
Useful? React with 👍 / 👎.
| loop.add_signal_handler(signal.SIGINT, _on_signal) | ||
| loop.add_signal_handler(signal.SIGTERM, _on_signal) |
There was a problem hiding this comment.
Guard Unix-only signal handling in dev apps runner
This unconditionally registers POSIX signal handlers, but loop.add_signal_handler is unsupported on Windows event loops and raises NotImplementedError, so fastmcp dev apps crashes immediately on win32. The same function later uses os.killpg in cleanup (also Unix-only), so platform guards are needed here as in src/fastmcp/cli/run.py to keep the command usable on Windows.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d2a8ec4dcd
ℹ️ 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".
| await client.get(url, timeout=1.0) | ||
| return True |
There was a problem hiding this comment.
Validate MCP readiness before declaring startup success
_wait_for_server treats any HTTP response as success, so a process already listening on --mcp-port (or any service returning 404/other non-MCP responses on /mcp) will make startup look healthy even if the spawned fastmcp run process failed to bind and exited. In that scenario dev apps continues with the wrong backend and surfaces confusing picker behavior instead of reporting startup failure; readiness should verify an expected MCP response (or at least that the child process is still alive) before returning True.
Useful? React with 👍 / 👎.
Test Failure AnalysisSummary: The Root Cause: Line 133 of Suggested Solution: Remove the # Before
external_functions=list(async_functions.keys()), # type: ignore[unknown-argument]
# After
external_functions=list(async_functions.keys()),Detailed AnalysisFailing check: Log excerpt: Suggested diff (from the ty output): - external_functions=list(async_functions.keys()), # type: ignore[unknown-argument]
+ external_functions=list(async_functions.keys()),Note: There are also Related Files
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ebe3e68af
ℹ️ 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".
| default = ( | ||
| pydantic.fields.PydanticUndefined | ||
| if is_required | ||
| else prop.get("default", "") | ||
| ) |
There was a problem hiding this comment.
Handle optional non-string fields without empty defaults
In _model_from_schema, every non-required property is assigned default="" regardless of its JSON type, so optional integer/number/boolean inputs are modeled with an invalid empty-string default. That default is then used in the picker payload path (/api/launch forwards all submitted fields), which causes blank optional numeric/bool fields to be sent as "" and fail tool argument validation instead of being treated as omitted/null.
Useful? React with 👍 / 👎.
| if not ready: | ||
| logger.error(f"User server did not start on port {mcp_port}") | ||
| return |
There was a problem hiding this comment.
Return failure when MCP server startup times out
When the spawned user server never becomes ready, _body only logs an error and returns, so run_dev_apps resolves normally and the CLI exits with status 0. This makes failed startups (for example, bad server_spec or an unusable --mcp-port) look successful to automation and shell scripts that rely on exit codes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70d7080a34
ℹ️ 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".
| tool = request.query_params.get("tool", "") | ||
| args_raw = request.query_params.get("args", "{}") | ||
| tool_args = json.loads(args_raw) | ||
| host_html = _HOST_HTML_TEMPLATE.format( | ||
| tool_name=tool, |
There was a problem hiding this comment.
Escape tool name before rendering launch HTML
launch() reads tool directly from the query string and passes it into _HOST_HTML_TEMPLATE without escaping, and that template injects {tool_name} into raw HTML (<title> and status text). A crafted URL such as one containing </title><script>... can execute script in the dev UI context, so this should be HTML-escaped (or only rendered via json.dumps into JS text nodes) before interpolation.
Useful? React with 👍 / 👎.
| headers=fwd_headers, | ||
| media_type=content_type or "application/octet-stream", | ||
| ) | ||
| except httpx.ConnectError: |
There was a problem hiding this comment.
Catch stream setup protocol errors in MCP proxy
The proxy only handles httpx.ConnectError, but await client.send(req, stream=True) can also raise httpx.RemoteProtocolError/httpx.ReadError during normal dev reload churn when the child MCP server is restarting. In that case the endpoint returns a 500 traceback instead of a transient upstream-unavailable response, which breaks picker requests during reloads; these setup-time transport errors should be handled alongside connect failures.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 480e8f677c
ℹ️ 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".
| on *dev_port* (with an /mcp proxy to the user's server), then opens | ||
| the browser. | ||
| """ | ||
| mcp_url = f"http://localhost:{mcp_port}/mcp" |
There was a problem hiding this comment.
Honor server path when wiring the MCP proxy URL
run_dev_apps hardcodes mcp_url to http://localhost:{mcp_port}/mcp, but _start_user_server starts fastmcp run without forcing --path. If SERVER_SPEC is a config that sets a non-default HTTP path, the child server listens on that configured path while the dev UI probes and proxies /mcp, causing startup failures or a permanently broken picker/proxy against the wrong endpoint.
Useful? React with 👍 / 👎.
| case "boolean": | ||
| py_type = bool | ||
| case _: | ||
| py_type = str |
There was a problem hiding this comment.
Preserve non-scalar JSON Schema field types in picker forms
The schema-to-model mapper falls back to str for every type other than integer/number/boolean, so object, array, and other structured inputs are rendered and submitted as plain text. For tools whose input schema includes nested/list fields, fastmcp dev apps sends type-mismatched arguments and the downstream callTool path fails validation instead of launching the app.
Useful? React with 👍 / 👎.
Building and testing MCP app tools currently requires a full MCP host client — launch Claude Desktop, connect, call the tool, wait for the result. This PR adds
fastmcp dev apps, a lightweight CLI command that replaces that entire loop with a browser-based preview.The dev server starts your MCP server (with
--reloadby default), serves a Prefab-powered picker that auto-generates forms from each tool's input schema, and renders results in a new tab using the same AppBridge protocol that real hosts use. Pick a tool, fill in args, hit Launch — the full round-trip from tool call to rendered UI, no host required.Also includes:
FastMCPAppprovider for composable MCP applications with@app.ui()entry points and@app.tool()backend toolsCallTool(fn)resolves function references to global MCP tool keys