Skip to content

Add fastmcp dev apps command with browser UI preview#3489

Merged
jlowin merged 22 commits intomainfrom
feature/fastmcp-app
Mar 14, 2026
Merged

Add fastmcp dev apps command with browser UI preview#3489
jlowin merged 22 commits intomainfrom
feature/fastmcp-app

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Mar 14, 2026

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.

pip install "fastmcp[apps]"
fastmcp dev apps server.py
# Picker UI at http://localhost:8080, MCP server on :8000

The dev server starts your MCP server (with --reload by 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:

  • FastMCPApp provider for composable MCP applications with @app.ui() entry points and @app.tool() backend tools
  • Callable tool resolver so Prefab's CallTool(fn) resolves function references to global MCP tool keys
  • Contacts example demonstrating the full stack (forms, search, callable refs)

jlowin added 13 commits March 3, 2026 20:33
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
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. cli Related to FastMCP CLI commands (run, dev, install) or CLI functionality. labels Mar 14, 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: 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".

Comment thread src/fastmcp/cli/apps_dev.py Outdated
Comment on lines +842 to +845
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),
)
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 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 👍 / 👎.

Comment thread src/fastmcp/cli/apps_dev.py Outdated
Comment on lines +891 to +892
loop.add_signal_handler(signal.SIGINT, _on_signal)
loop.add_signal_handler(signal.SIGTERM, _on_signal)
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 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 👍 / 👎.

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

Comment on lines +807 to +808
await client.get(url, timeout=1.0)
return True
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 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 👍 / 👎.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The ty type checker fails because src/fastmcp/experimental/transforms/code_mode.py has an unused # type: ignore[unknown-argument] comment that ty now correctly flags as unnecessary.

Root Cause: Line 133 of src/fastmcp/experimental/transforms/code_mode.py contains # type: ignore[unknown-argument] on the external_functions=list(async_functions.keys()) argument to pydantic_monty.Monty(...). A newer version of ty (or an updated type stub for pydantic-monty) now correctly resolves external_functions as a valid argument, making the suppression comment stale.

Suggested Solution: Remove the # type: ignore[unknown-argument] comment from line 133:

# Before
external_functions=list(async_functions.keys()),  # type: ignore[unknown-argument]

# After
external_functions=list(async_functions.keys()),
Detailed Analysis

Failing check: ty check (exit code 1)

Log excerpt:

ty check.................................................................[41mFailed[49m
- hook id: ty
- exit code: 1

  warning[unused-type-ignore-comment]: Unused blanket `type: ignore` directive
     --> src/fastmcp/experimental/transforms/code_mode.py:133:63
      |
  131 |             code,
  132 |             inputs=list(inputs.keys()),
  133 |             external_functions=list(async_functions.keys()),  # type: ignore[unknown-argument]
      |                                                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  134 |         )
  135 |         run_kwargs: dict[str, Any] = {"external_functions": async_functions}
      |
  help: Remove the unused suppression comment

  Found 1 diagnostic

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 loq (file size) violations reported, but those are currently non-enforced ("loq violations not enforced... yet!") and did not cause the failure.

Related Files
  • src/fastmcp/experimental/transforms/code_mode.py:133 — contains the unused type: ignore comment that needs to be removed

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

Comment thread src/fastmcp/cli/apps_dev.py Outdated
Comment on lines +323 to +327
default = (
pydantic.fields.PydanticUndefined
if is_required
else prop.get("default", "")
)
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 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 👍 / 👎.

Comment thread src/fastmcp/cli/apps_dev.py Outdated
Comment on lines +860 to +862
if not ready:
logger.error(f"User server did not start on port {mcp_port}")
return
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 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 👍 / 👎.

@jlowin jlowin added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. and removed enhancement Improvement to existing functionality. For issues and smaller PR improvements. labels Mar 14, 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: 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".

Comment on lines +641 to +645
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,
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 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:
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 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 👍 / 👎.

@jlowin jlowin merged commit e35a74f into main Mar 14, 2026
6 of 9 checks passed
@jlowin jlowin deleted the feature/fastmcp-app branch March 14, 2026 16:29
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: 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"
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 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 👍 / 👎.

Comment on lines +315 to +318
case "boolean":
py_type = bool
case _:
py_type = str
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 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 👍 / 👎.

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

Labels

cli Related to FastMCP CLI commands (run, dev, install) or CLI functionality. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant