Skip to content

Apps Phase 1: docs, examples, app-only tool filtering#3593

Merged
jlowin merged 5 commits intomainfrom
apps-phase-1-final
Mar 24, 2026
Merged

Apps Phase 1: docs, examples, app-only tool filtering#3593
jlowin merged 5 commits intomainfrom
apps-phase-1-final

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Mar 23, 2026

This is the foundation work for making FastMCP the definitive resource for MCP app development.

Documentation — Three new pages: a component cheat sheet for quick reference, an architecture deep-dive explaining how structured content flows from Python to pixels, and a "Give Your Tool a UI" section in the quickstart. The sidebar now flows: Overview → Prefab Apps → FastMCPApp → Components → Patterns → Development → Architecture → Custom HTML.

Examples — Three new complete application examples beyond the existing pattern demos:

  • Inventory tracker — Full CRUD with FastMCPApp, DataTable, search, category filtering, Pydantic form generation
  • Data explorer — Tables, charts, tabs, summary statistics across a sample dataset
  • Approval workflow — Multi-step workflow with approve/reject, action chaining, status management

All existing examples updated from {{ }} template strings to Rx()/set_initial_state() to match the docs. Fixed patterns_server.py for prefab-ui 0.13.x (chart imports moved).

App-only tool filtering — Tools with visibility: ["app"] no longer appear in the model's tool list. They're still callable via get_app_tool when the request carries _meta.fastmcp.app. This is the correct behavior: backend tools are for the UI, not the model.

@marvin-context-protocol marvin-context-protocol Bot added documentation Updates to docs, examples, or guides. Primary change is documentation-related. enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Mar 23, 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: 055825fbf6

ℹ️ 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 639 to +640
tools = await apply_session_transforms([tool])
if tools and is_enabled(tools[0]):
if tools and is_enabled(tools[0]) and _is_model_visible(tools[0]):
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 Keep app-only tools reachable during middleware auth checks

Filtering get_tool() by _is_model_visible() breaks authenticated app UI calls before they ever reach the new get_app_tool() path. FastMCP.call_tool(..., app_name=...) still runs middleware first, and AuthMiddleware.on_call_tool() resolves the target through fastmcp.get_tool() (src/fastmcp/server/middleware/authorization.py:139-143), so any HTTP/SSE server with global auth middleware now rejects every @app.tool() invocation as “tool not found” even when the caller is authorized.

Useful? React with 👍 / 👎.

Comment on lines 649 to +651
all_tools = [t for t in await super().list_tools() if t.name == name]
all_tools = list(await apply_session_transforms(all_tools))
enabled = [t for t in all_tools if is_enabled(t)]
enabled = [t for t in all_tools if is_enabled(t) and _is_model_visible(t)]
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 app-only tools for task result lookup

This same get_tool() filter also breaks task-enabled app tools. FastMCP.tool() still allows app= and task= together (src/fastmcp/server/server.py:1490-1491), and get_tasks() still registers those tools (src/fastmcp/server/server.py:463-480), but tasks/getResult reconstructs the finished component via server.get_tool() (src/fastmcp/server/tasks/requests.py:316-320). After this change, any app-only background tool resolves to None, so fetching the task result fails with “Component not found”.

Useful? React with 👍 / 👎.

jlowin added 2 commits March 22, 2026 21:41
- Charts import: clarify they must come from prefab_ui.components.charts
- DataTable: searchable→search (the actual prop name), remove nonexistent
  table-level sortable prop
- Select: remove nonexistent options prop, show SelectOption children
- Tabs: default_value→value
- Fix search=True in inventory, patterns, datatable examples
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: f5634c9224

ℹ️ 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".

DataTableColumn(key="price", header="Price ($)", sortable=True),
DataTableColumn(key="last_updated", header="Updated", sortable=True),
],
rows=list(_inventory),
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 Bind the main table to reactive inventory state

rows=list(_inventory) bakes a one-time snapshot into the rendered app instead of reading from state. After add_item, update_quantity, or delete_item mutates the inventory, the "All Items" tab keeps showing the original 10 rows until the user reopens the app, so the flagship CRUD example appears not to persist edits.

Useful? React with 👍 / 👎.

Comment on lines +276 to +278
on_success=[
SetState("recent_additions", RESULT),
ShowToast("Item added!", variant="success"),
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 Refresh visible inventory state after add_item succeeds

This callback stores the updated inventory in recent_additions, but that state key is never rendered anywhere in this view. In the current example, submitting the add form only shows a toast; the new item does not appear in either the filter list or the main inventory view until the app is reloaded.

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

ℹ️ 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".

update_quantity,
arguments={"item_id": STATE.adjust_id, "delta": -1},
on_success=[
SetState("filtered_items", RESULT),
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 active category filters after quantity or delete actions

When a user has already applied a category filter in the Actions tab, this writes RESULT back into filtered_items, but update_quantity() and delete_item() return list(_inventory) rather than the currently filtered subset. After any stock adjustment or delete, the UI jumps back to showing all items even though selected_category is still set, so the example no longer reflects the chosen filter.

Useful? React with 👍 / 👎.

Comment on lines +412 to +415
count = sum(1 for it in _inventory if it["category"] == cat)
total_qty = sum(
it["quantity"] for it in _inventory if it["category"] == cat
)
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 Drive inventory summary cards from reactive state

These totals are computed once from _inventory during render instead of from app state, so add/update/delete operations never refresh the three category summary cards until the app is reopened. That leaves the example showing contradictory counts immediately after the user edits stock.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

@strawgate strawgate left a comment

Choose a reason for hiding this comment

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

very cool

might be worth reviewing fstrings and docstrings across the example servers

Comment thread docs/apps/components.mdx Outdated
A dropdown for choosing from a list of options. Pass a flat list of strings, or structured `SelectOption` objects for custom labels.

```python
from prefab_ui.components import Select
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

add SelectOption

We should really wire up pytest-examples...

Comment thread docs/apps/components.mdx Outdated
Renders vertical or horizontal bar charts. Each `ChartSeries` maps a key from your data to a colored bar group. Multiple series produce grouped (or stacked) bars.

```python
from prefab_ui.components import BarChart, ChartSeries
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Earlier we say charts must be imported from components.charts

Comment thread docs/apps/components.mdx Outdated
Displays proportional data as slices. Set `inner_radius` for a donut chart. Unlike bar/line charts, `PieChart` uses `data_key` for the numeric value and `name_key` for the label — it doesn't use `ChartSeries`.

```python
from prefab_ui.components import PieChart
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Earlier we say charts must be imported from components.charts

Comment thread docs/apps/components.mdx Outdated
Plots data points connected by lines. Shares the same API as `BarChart` — use `series`, `x_axis`, and optionally `curve` to control interpolation.

```python
from prefab_ui.components import LineChart, ChartSeries
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Earlier we say charts must be imported from components.charts

)

with Row(gap=2, css_class="mt-4"):
Badge(f"Region: {STATE.selected_region}")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this the right syntax for pulling from state? wouldnt this evaluate at call time?

mcp = FastMCP("Approvals Server", providers=[app])

if __name__ == "__main__":
mcp.run(transport="http")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we say this script takes a --stdio arg in the docstring

mcp = FastMCP("Contacts Server", providers=[app])

if __name__ == "__main__":
mcp.run(transport="http")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we say passing --stdio makes it a stdio server in the docs

@jlowin
Copy link
Copy Markdown
Member Author

jlowin commented Mar 24, 2026

Re: the f"Region: {STATE.selected_region}" question — this does evaluate at build time, but str(Rx("selected_region")) returns "{{ selected_region }}", so the f-string produces "Region: {{ selected_region }}" which the renderer evaluates reactively. Rx objects are designed for this — their __str__ returns the template expression.

All other review comments addressed in baa49c9: chart imports fixed to prefab_ui.components.charts, SelectOption added to import, --stdio claims removed from docstrings.

@jlowin jlowin merged commit c04ce89 into main Mar 24, 2026
9 checks passed
@jlowin jlowin deleted the apps-phase-1-final branch March 24, 2026 17:35
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: A single test in tests/client/test_streamable_http.py timed out on the Windows / Python 3.10 runner, hitting the 5-second pytest-timeout limit. All other platforms (Ubuntu 3.10, Ubuntu 3.13) passed.

Root Cause: This is a flaky test on Windows, not a regression introduced by this PR. The failure is a timing/concurrency issue with uvicorn's ASGI lifecycle on Windows — the runner logs show ERROR: ASGI callable returned without completing response and the test hung in backports.asyncio.runner (the Python 3.10 polyfill for asyncio.Runner, which was added in 3.11). A subsequent CI run on the same merged commit (run 23503619868) passed cleanly on Windows Python 3.10 with no code changes.

Suggested Solution: No code fix is needed — this is pre-existing Windows flakiness. The codebase already acknowledges this with the @pytest.mark.skipif(sys.platform == 'win32', ...) guard on TestTimeout. If these timeouts become more frequent, consider adding a similar skipif or flaky marker to the fixture-heavy tests in test_streamable_http.py.

Detailed Analysis

Failing job: Tests: Python 3.10 on windows-latest (Job ID 68405231126)

Log excerpt:

tests\client\test_streamable_http.py +++++++++++++++++++++++++++++++++++ Timeout +++++++++++++++++++++++++++++++++++
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Captured stdout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
INFO:     127.0.0.1:57505 - "POST /mcp HTTP/1.1" 200 OK
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Captured stderr ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ERROR:    ASGI callable returned without completing response.
~~~~~~~~~~~~~~~~~~~~~~~~~ Stack of MainThread (2852) ~~~~~~~~~~~~~~~~~~~~~~~~~~
  ...
  File "backports/asyncio/runner/runner.py", line 175, in run
    return self._loop.run_until_complete(task)

The timeout hit a test that was using the streamable_http_server fixture from run_server_async. The test itself was waiting in the asyncio event loop (via the backports.asyncio polyfill), suggesting uvicorn's graceful shutdown or the server task cancellation hung past the 5-second limit on Windows.

Evidence this is flaky (not a regression):

  • This PR's pre-merge CI run (23501020378) passed on Windows Python 3.10
  • The post-merge run on the same SHA (23503619868) also passed
  • Only the merge commit run (23503603640) failed — identical code, different timing
Related Files
  • tests/client/test_streamable_http.py — failing test file; already has a skipif(sys.platform == 'win32') on TestTimeout class acknowledging Windows flakiness
  • src/fastmcp/utilities/tests.pyrun_server_async fixture utility; includes a timeout=2.0 on Windows cleanup specifically for this kind of issue

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

Labels

documentation Updates to docs, examples, or guides. Primary change is documentation-related. enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants