Add prefab auto-wiring for MCP Apps#3119
Conversation
Tools that return prefab types (UIResponse, Component) automatically get wired to the shared prefab renderer resource. Works via app=True, return type inference, or both.
Test Failure AnalysisSummary: All CI jobs are failing during the Root Cause: The PR adds [tool.uv.sources]
prefab-ui = { path = "../prefab", editable = true }In GitHub Actions, the repository is checked out to Suggested Solution: Update the GitHub Actions workflows to check out the - name: Checkout prefab repository
uses: actions/checkout@v4
with:
repository: jlowin/prefab # or the actual prefab repo path
path: prefabThis assumes:
Alternative Solution: If Detailed AnalysisError from logs (all jobs): Files modified in PR:
The editable dependency works fine for local development where the Related FilesWorkflow files to update:
Configuration files:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2936e25665
ℹ️ 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.uv.sources] | ||
| prefab-ui = { path = "../prefab", editable = true } |
There was a problem hiding this comment.
Remove hardcoded local prefab-ui source override
With [tool.uv.sources] prefab-ui = { path = "../prefab", editable = true } in the repo config, uv sync will always try to resolve prefab-ui from a sibling ../prefab directory. That path doesn’t exist for typical CI or fresh checkouts, so the required workflow in this repo (running uv sync before tests) will fail even though prefab-ui is available on PyPI. This is a regression introduced by the commit because it forces a local-only source override instead of a normal dependency.
Useful? React with 👍 / 👎.
WalkthroughThis PR adds prefab UI support to FastMCP by introducing new MCP example applications demonstrating chart and data table components, updating resource metadata propagation to include meta fields, and integrating prefab UI type handling throughout the tool parsing and decoration pipeline. The implementation uses lazy loading to avoid hard dependencies on the prefab_ui package, with runtime checks guarding all prefab-related functionality. Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fastmcp/resources/types.py (1)
98-99:⚠️ Potential issue | 🟡 MinorInconsistent
metapropagation:FileResource,HttpResource, andDirectoryResourcedon't passmeta.
TextResourceandBinaryResourceforwardself.metatoResourceContent, but the other three concreteResourcesubclasses in this file still constructResourceContentwithout it. Since the baseResourcemodel has ametafield (inherited fromFastMCPComponent), these subclasses silently drop it.Proposed fix
# FileResource.read return ResourceResult( - contents=[ResourceContent(content=content, mime_type=self.mime_type)] + contents=[ResourceContent(content=content, mime_type=self.mime_type, meta=self.meta)] ) # HttpResource.read return ResourceResult( contents=[ - ResourceContent(content=response.text, mime_type=self.mime_type) + ResourceContent(content=response.text, mime_type=self.mime_type, meta=self.meta) ] ) # DirectoryResource.read return ResourceResult( - contents=[ResourceContent(content=content, mime_type=self.mime_type)] + contents=[ResourceContent(content=content, mime_type=self.mime_type, meta=self.meta)] )
🧹 Nitpick comments (3)
src/fastmcp/server/providers/local_provider/decorators/tools.py (2)
185-188: Mutating tool meta after_add_component— verify this is safe.
_maybe_apply_prefab_uiis called after the tool is already stored inprovider._components(line 185). Since_expand_prefab_ui_metamutatestool.metain-place (line 110), this works because the provider holds a reference to the same object. However, if any downstream code copies/snapshots tool metadata at registration time, the expanded meta would be missed. This ordering is worth a brief comment.Alternative: apply prefab wiring before registration
- self._add_component(tool) - if not enabled: - self.disable(keys={tool.key}) - _maybe_apply_prefab_ui(self, tool) + _maybe_apply_prefab_ui(self, tool) + self._add_component(tool) + if not enabled: + self.disable(keys={tool.key})
41-58:get_argsimport can be hoisted to module level.The
from typing import get_argson line 52 is inside the function body. It's a stdlib import with no cost, and hoisting it to the top would follow the existing import style.examples/apps/chart_server.py (1)
71-76: Consider usingLiteralfor thecurveparameter.Using
Literal["linear", "smooth", "step"]instead ofstrwould give MCP clients an enum in the input schema, improving discoverability and preventing invalid values.Proposed change
+from typing import Literal + `@mcp.tool`(app=True) -def sales_trend(curve: str = "linear") -> UIResponse: +def sales_trend(curve: Literal["linear", "smooth", "step"] = "linear") -> UIResponse:
| @mcp.tool(app=True) | ||
| def list_team(department: str | None = None) -> UIResponse: | ||
| """Browse the team directory with sorting and search. | ||
|
|
||
| Args: | ||
| department: Filter by department (e.g. "Engineering", "Design"). | ||
| Leave empty to show everyone. | ||
| """ | ||
| if department: | ||
| rows = [e for e in EMPLOYEES if e["role"].lower() == department.lower()] | ||
| else: | ||
| rows = EMPLOYEES | ||
|
|
There was a problem hiding this comment.
Data field "role" is labeled "Department" everywhere else — naming mismatch.
The parameter is department, the docstring says "department", and the DataTableColumn header on line 146 reads "Department", but the filter and data key is "role". This inconsistency in an example file could confuse users who copy this pattern. Consider renaming the dict key to "department" in EMPLOYEES (and updating the DataTableColumn key accordingly), or renaming the parameter/docstring to "role".
Proposed fix: rename the key in the data
EMPLOYEES = [
{
"name": "Alice Chen",
- "role": "Engineering",
+ "department": "Engineering",
"level": "Senior",(Apply to all entries, then update the filter and DataTable column key)
- rows = [e for e in EMPLOYEES if e["role"].lower() == department.lower()]
+ rows = [e for e in EMPLOYEES if e["department"].lower() == department.lower()]- DataTableColumn(key="role", header="Department", sortable=True),
+ DataTableColumn(key="department", header="Department", sortable=True),| ) | ||
|
|
||
| renderer_key = f"resource:{PREFAB_RENDERER_URI}@" | ||
| if renderer_key in provider._components: | ||
| return |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's look at the actual file to understand the context
echo "=== File context ==="
cat -n src/fastmcp/server/providers/local_provider/decorators/tools.py | sed -n '65,85p'
echo ""
echo "=== Searching for KEY_PREFIX usage ==="
rg -n 'KEY_PREFIX' --type=py -C2
echo ""
echo "=== Searching for .key property/method ==="
rg -nP '^\s+def key\b|^\s+@property' --type=py -A3 | head -80Repository: jlowin/fastmcp
Length of output: 13058
🏁 Script executed:
#!/bin/bash
# Search for how _components keys are constructed
echo "=== Searching for _components key construction ==="
rg -n '_components\[' --type=py -C2
echo ""
echo "=== Searching for resource key format ==="
rg -n 'resource:' --type=py -C2
echo ""
echo "=== Search for PREFAB_RENDERER_URI ==="
rg -n 'PREFAB_RENDERER_URI' --type=py -C2Repository: jlowin/fastmcp
Length of output: 50371
🏁 Script executed:
#!/bin/bash
# Look at FastMCPComponent to understand key format
echo "=== FastMCPComponent class ==="
fd 'component' --type=f --name='*.py' | xargs grep -l 'class FastMCPComponent' | head -5
# Find where keys are generated
echo ""
echo "=== Searching for key generation in _add_component ==="
rg -n '_add_component' --type=py -B2 -A10 | head -100Repository: jlowin/fastmcp
Length of output: 9854
🏁 Script executed:
#!/bin/bash
# Check the Resource.make_key method specifically
echo "=== Resource class key construction ==="
rg -n 'class Resource|KEY_PREFIX|def make_key' src/fastmcp/resources/resource.py -A2
# Look at how the renderer resource is created in the actual function
echo ""
echo "=== Full _ensure_prefab_renderer function ==="
cat -n src/fastmcp/server/providers/local_provider/decorators/tools.py | sed -n '57,100p'Repository: jlowin/fastmcp
Length of output: 2277
🏁 Script executed:
#!/bin/bash
# Check if Resource.make_key is used anywhere in similar scenarios
echo "=== Usage of Resource.make_key in codebase ==="
rg 'Resource\.make_key|Tool\.make_key' --type=py -B2 -A2
# Also check if component.key property is used to check existence
echo ""
echo "=== Pattern of checking component existence ==="
rg 'in provider\._components|_components\[.*\.key' --type=py -B2 -A1 | head -50Repository: jlowin/fastmcp
Length of output: 6569
🏁 Script executed:
#!/bin/bash
# Check what happens when _add_component encounters a duplicate
echo "=== _add_component duplicate handling ==="
sed -n '178,205p' src/fastmcp/server/providers/local_provider/local_provider.py
# Check PREFAB_RENDERER_URI definition
echo ""
echo "=== PREFAB_RENDERER_URI definition ==="
rg -n 'PREFAB_RENDERER_URI' src/fastmcp/server/providers/local_provider/decorators/tools.py | head -5
rg -n 'PREFAB_RENDERER_URI.*=' --type=py -B2 -A2Repository: jlowin/fastmcp
Length of output: 1906
Use Resource.make_key() instead of hardcoded key format.
The manual key f"resource:{PREFAB_RENDERER_URI}@" hardcodes the resource prefix and format. If Resource.KEY_PREFIX or key generation logic changes, this check will silently fail. The codebase already uses Resource.make_key() for similar scenarios (e.g., in local_provider.py). Replace with:
renderer_key = f"{Resource.make_key(PREFAB_RENDERER_URI)}@"
if renderer_key in provider._components:
returnAlternatively, use a provider-level flag as suggested if you prefer to avoid component key format dependency entirely.
* Add prefab auto-wiring for MCP Apps (#3119) Tools that return prefab types (UIResponse, Component) automatically get wired to the shared prefab renderer resource. Works via app=True, return type inference, or both. * Prefab compatibility updates * Use published prefab-ui >=0.6.0, remove local source override * Migrate UIResponse to PrefabApp for Prefab UI integration PrefabApp is a pure data object with to_json(), html(), and csp() methods. Tools can return PrefabApp, bare Components, or ToolResult with structured_content for custom LLM fallback text. * Add Prefab UI apps documentation * Add mini apps and full apps documentation pages Mini apps covers the common single-screen patterns: charts (bar, line, area, pie), data tables with sorting/search/pagination, forms (manual and Pydantic-generated), status displays, conditional content, and layout composition with tabs and accordions. Full apps covers multi-page applications using Pages/Page components, shared state across pages, and using ToolCall with result_key for server-driven state updates. * Reframe apps docs around motivation, add generative UIs page The docs now lead with the problem — MCP tools stuff data into the LLM context window, and building HTML/JS/CSS frontends is a non-starter for Python developers — before introducing Prefab as the solution. Mini apps are framed as the primary use case: focused, single-purpose UIs that present data visually and collect structured input. New generative UIs page covers the concept of LLMs producing component JSON directly, enabling adaptive dashboards, tailored forms, and exploratory workflows. * Tag Prefab docs pages as SOON instead of NEW * Rename Low-Level API to Custom HTML Apps The page is about using the MCP Apps extension directly, not a FastMCP or Prefab internal API. Reframed to make clear this is the open MCP protocol with FastMCP providing convenience wrappers. * Tighten apps docs and widen content area Strip editorial motivation from all app doc pages — let code examples do the talking. Add content-area max-width override (44rem) to style.css. * Restructure apps docs, fix code issues Rename Prefab UI → Prefab Apps, mini-apps → patterns, remove generative-uis and full-apps pages. Rewrite prefab page to lead with what users do (declare a UI, return it) before explaining internals. Patterns page now has fully self-contained copy-pasteable examples with explicit imports and links to prefab docs. Forms show the two-tool pattern (form + handler). Add patterns_server.py example. Code fixes: move get_args to module-level import, remove dead AuthCheckCallable type alias, fix ToolCall→CallTool in all docs. * Remove unused ToolResult import from chart_server * Handle composite Prefab types in type inference and schema suppression _has_prefab_return_type and the output schema suppression logic only checked bare classes, missing unions (Column | None) and Annotated wrappers (Annotated[PrefabApp | None, ...]). Recurse through Union, types.UnionType, and Annotated to detect Prefab types in composite annotations.
When
prefab-uiis installed, tools that return prefab types (UIResponse,Component) get automatically wired to a shared renderer resource atui://prefab/renderer.html. Three paths trigger this:app=Trueon@mcp.tool()-> UIResponseor-> Componentconvert_resulthandling —Tool.convert_result()detects prefab objects at runtime and serializes them as structured contentThe auto-wiring happens in
add_tool()onLocalProvider— it checks whether the tool needs prefab support, lazily registers the renderer resource (once), and expandsmeta["ui"]fromTrueto the fullAppConfigwith the renderer URI and CSP. All prefab imports are guarded behindtry/except ImportErrorso nothing breaks without the optional dependency.