MCP Apps: structured CSP/permissions types, resource meta propagation fix, QR example#3031
MCP Apps: structured CSP/permissions types, resource meta propagation fix, QR example#3031
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. WalkthroughThis pull request enhances the MCP Apps UI system by introducing two new structured data models—ResourceCSP and ResourcePermissions—that replace primitive field types in ToolUI and ResourceUI components. The changes include wire-format updates to support domain lists and permission fields, a complete example QR code generator application demonstrating the new UI capabilities, fixes to resource path resolution in the install CLI, and improvements to resource MIME type handling to ensure non-UI resources preserve their correct MIME types instead of defaulting to text/plain. Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ 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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fastmcp/server/apps.py (1)
94-118:⚠️ Potential issue | 🟠 MajorApply UI metadata type upgrades consistently across all decorator styles and MCP object types.
The structured UI types (
ToolUI/ResourceUIwithResourceCSP,ResourcePermissions) were added to server-level decorators (@server.tool(),@server.resource()), but the following are missing the correspondinguiparameter or field:
@resource()standalone decorator — noui: ResourceUI | dictparameter@prompt()standalone decorator — nouiparameterResourceMeta— nouifield (onlymeta: dict[str, Any])PromptMeta— nouifield (onlymeta: dict[str, Any])ResourceTemplate.from_function()— nouiparameter (matches comment at line 290: "doesn't have metadata support yet")Prompt.from_function()— nouiparameterPer the requirement that changes affecting MCP objects must be adopted across Tools, Resources, Resource Templates, and Prompts: add
uiparameter/field to the above, wire format serialization viaui_to_meta_dict(), and add tests forResourceTemplateandPromptUI metadata round-tripping.
🧹 Nitpick comments (2)
docs/development/v3-notes/mcp-apps-notes.mdx (2)
178-184: Convert the build workflow to a Steps component.This is a sequential procedure; please render it with the Steps component for consistency and scanability.
As per coding guidelines Use Steps component for procedures, tutorials, setup guides, and sequential instructions in MDX documentation.
23-57: Provide at least one complete, runnable example for the core pattern.These snippets are illustrative but not end-to-end runnable. Consider adding a full, copy‑pasteable example (with filename) that includes a minimal server + UI resource flow.
As per coding guidelines Always include complete, runnable code examples that users can copy and execute in MDX documentation.
| VIEW_URI = "ui://qr-server/view.html" | ||
|
|
||
| mcp = FastMCP("QR Code Server", stateless_http=True) | ||
|
|
||
| EMBEDDED_VIEW_HTML = """\ |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add module-level type annotations for constants and server instance.
This keeps the example aligned with the repo’s typing standard.
🧾 Suggested update
-VIEW_URI = "ui://qr-server/view.html"
+VIEW_URI: str = "ui://qr-server/view.html"
-mcp = FastMCP("QR Code Server", stateless_http=True)
+mcp: FastMCP = FastMCP("QR Code Server", stateless_http=True)
-EMBEDDED_VIEW_HTML = """\
+EMBEDDED_VIEW_HTML: str = """\| @mcp.tool(ui=ToolUI(resource_uri=VIEW_URI)) | ||
| def generate_qr( | ||
| text: str = "https://gofastmcp.com", | ||
| box_size: int = 10, | ||
| border: int = 4, | ||
| error_correction: str = "M", | ||
| fill_color: str = "black", | ||
| back_color: str = "white", | ||
| ) -> list[types.ImageContent]: | ||
| """Generate a QR code from text. | ||
|
|
||
| Args: | ||
| text: The text/URL to encode | ||
| box_size: Size of each box in pixels (default: 10) | ||
| border: Border size in boxes (default: 4) | ||
| error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%) | ||
| fill_color: Foreground color (hex like #FF0000 or name like red) | ||
| back_color: Background color (hex like #FFFFFF or name like white) | ||
| """ | ||
| error_levels = { | ||
| "L": qrcode.constants.ERROR_CORRECT_L, | ||
| "M": qrcode.constants.ERROR_CORRECT_M, | ||
| "Q": qrcode.constants.ERROR_CORRECT_Q, | ||
| "H": qrcode.constants.ERROR_CORRECT_H, | ||
| } | ||
|
|
||
| qr = qrcode.QRCode( | ||
| version=1, | ||
| error_correction=error_levels.get( | ||
| error_correction.upper(), qrcode.constants.ERROR_CORRECT_M | ||
| ), | ||
| box_size=box_size, | ||
| border=border, | ||
| ) | ||
| qr.add_data(text) | ||
| qr.make(fit=True) | ||
|
|
||
| img = qr.make_image(fill_color=fill_color, back_color=back_color) | ||
| buffer = io.BytesIO() | ||
| img.save(buffer, format="PNG") | ||
| b64 = base64.b64encode(buffer.getvalue()).decode() | ||
| return [types.ImageContent(type="image", data=b64, mimeType="image/png")] |
There was a problem hiding this comment.
Validate QR parameters to avoid silent fallbacks and invalid values.
Right now invalid error_correction falls back silently and negative sizes can slip through.
✅ Suggested validation
- qr = qrcode.QRCode(
- version=1,
- error_correction=error_levels.get(
- error_correction.upper(), qrcode.constants.ERROR_CORRECT_M
- ),
- box_size=box_size,
- border=border,
- )
+ if box_size <= 0:
+ raise ValueError("box_size must be > 0")
+ if border < 0:
+ raise ValueError("border must be >= 0")
+
+ error_key = error_correction.upper()
+ if error_key not in error_levels:
+ raise ValueError("error_correction must be one of: L, M, Q, H")
+
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=error_levels[error_key],
+ box_size=box_size,
+ border=border,
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @mcp.tool(ui=ToolUI(resource_uri=VIEW_URI)) | |
| def generate_qr( | |
| text: str = "https://gofastmcp.com", | |
| box_size: int = 10, | |
| border: int = 4, | |
| error_correction: str = "M", | |
| fill_color: str = "black", | |
| back_color: str = "white", | |
| ) -> list[types.ImageContent]: | |
| """Generate a QR code from text. | |
| Args: | |
| text: The text/URL to encode | |
| box_size: Size of each box in pixels (default: 10) | |
| border: Border size in boxes (default: 4) | |
| error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%) | |
| fill_color: Foreground color (hex like #FF0000 or name like red) | |
| back_color: Background color (hex like #FFFFFF or name like white) | |
| """ | |
| error_levels = { | |
| "L": qrcode.constants.ERROR_CORRECT_L, | |
| "M": qrcode.constants.ERROR_CORRECT_M, | |
| "Q": qrcode.constants.ERROR_CORRECT_Q, | |
| "H": qrcode.constants.ERROR_CORRECT_H, | |
| } | |
| qr = qrcode.QRCode( | |
| version=1, | |
| error_correction=error_levels.get( | |
| error_correction.upper(), qrcode.constants.ERROR_CORRECT_M | |
| ), | |
| box_size=box_size, | |
| border=border, | |
| ) | |
| qr.add_data(text) | |
| qr.make(fit=True) | |
| img = qr.make_image(fill_color=fill_color, back_color=back_color) | |
| buffer = io.BytesIO() | |
| img.save(buffer, format="PNG") | |
| b64 = base64.b64encode(buffer.getvalue()).decode() | |
| return [types.ImageContent(type="image", data=b64, mimeType="image/png")] | |
| `@mcp.tool`(ui=ToolUI(resource_uri=VIEW_URI)) | |
| def generate_qr( | |
| text: str = "https://gofastmcp.com", | |
| box_size: int = 10, | |
| border: int = 4, | |
| error_correction: str = "M", | |
| fill_color: str = "black", | |
| back_color: str = "white", | |
| ) -> list[types.ImageContent]: | |
| """Generate a QR code from text. | |
| Args: | |
| text: The text/URL to encode | |
| box_size: Size of each box in pixels (default: 10) | |
| border: Border size in boxes (default: 4) | |
| error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%) | |
| fill_color: Foreground color (hex like `#FF0000` or name like red) | |
| back_color: Background color (hex like `#FFFFFF` or name like white) | |
| """ | |
| error_levels = { | |
| "L": qrcode.constants.ERROR_CORRECT_L, | |
| "M": qrcode.constants.ERROR_CORRECT_M, | |
| "Q": qrcode.constants.ERROR_CORRECT_Q, | |
| "H": qrcode.constants.ERROR_CORRECT_H, | |
| } | |
| if box_size <= 0: | |
| raise ValueError("box_size must be > 0") | |
| if border < 0: | |
| raise ValueError("border must be >= 0") | |
| error_key = error_correction.upper() | |
| if error_key not in error_levels: | |
| raise ValueError("error_correction must be one of: L, M, Q, H") | |
| qr = qrcode.QRCode( | |
| version=1, | |
| error_correction=error_levels[error_key], | |
| box_size=box_size, | |
| border=border, | |
| ) | |
| qr.add_data(text) | |
| qr.make(fit=True) | |
| img = qr.make_image(fill_color=fill_color, back_color=back_color) | |
| buffer = io.BytesIO() | |
| img.save(buffer, format="PNG") | |
| b64 = base64.b64encode(buffer.getvalue()).decode() | |
| return [types.ImageContent(type="image", data=b64, mimeType="image/png")] |
| if __name__ == "__main__": | ||
| if "--stdio" in sys.argv: | ||
| mcp.run(transport="stdio") | ||
| else: | ||
| mcp.run(transport="streamable-http", host="0.0.0.0", port=3001) |
There was a problem hiding this comment.
Avoid binding to all interfaces by default in the example.
0.0.0.0 exposes the server to the LAN; safer to default to localhost and allow override.
🔒 Suggested safer default
+import os
@@
- mcp.run(transport="streamable-http", host="0.0.0.0", port=3001)
+ host = os.getenv("FASTMCP_EXAMPLE_HOST", "127.0.0.1")
+ mcp.run(transport="streamable-http", host=host, port=3001)🧰 Tools
🪛 Ruff (0.14.14)
[error] 164-164: Possible binding to all interfaces
(S104)
Hosts read CSP/permissions metadata from content items in the resources/read response, not from the resource listing. Our convert_result() was dropping the resource's meta when wrapping plain str/bytes returns, so the host never saw the CSP declarations and blocked external scripts. Also fixes: resource MIME type preservation for ui:// resources, install CLI path resolution for JSON configs, and switches QR example to explicit ToolResult to avoid unneeded structuredContent.
852c7da to
8d3c115
Compare
Phase 1 shipped
ToolUIandResourceUIwith placeholder types for CSP (str) and permissions (list[str]). These didn't match the actual MCP Apps wire format — hosts expect structured objects like{"resourceDomains": ["https://unpkg.com"]}, not a raw string. This fixes the type models and, more critically, fixes a bug where resource metadata (including CSP declarations) wasn't propagated toresources/readresponse content items — meaning hosts never saw CSP directives and blocked all external scripts.The CSP bug was found by comparing wire-level output against the upstream MCP SDK: hosts read
_meta.ui.cspfrom content items inresources/read, not from the resource listing.Resource.convert_result()now propagates the resource's component-levelmeta(includingui) to each content item, and preserves the resource's MIME type forui://resources.Also includes a QR code example (
examples/apps/qr_server/) ported from the ext-apps repo — verified working end-to-end in Goose.