Fix mimeType, CSP settings and QR code example (#111)
- Fix RESOURCE_MIME_TYPE to match spec: "text/html;profile=mcp-app" - Add CSP field to McpUiSandboxResourceReadyNotification type and schema - Update basic-host to extract and pass CSP metadata from resources - Update sandbox.ts to dynamically inject CSP meta tag into inner iframe - Remove static CSP from sandbox.html (now dynamic based on resource metadata) - Update QR server to use custom read_resource handler for _meta injection - Update requirements.txt to require mcp[cli]>=1.23.3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9d21d8bc08
commit
6a8f5031be
3 changed files with 133 additions and 61 deletions
|
|
@ -1,2 +1,2 @@
|
|||
mcp[cli]>=1.0.0
|
||||
mcp[cli]>=1.23.3
|
||||
qrcode[pil]>=7.4
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from pathlib import Path
|
|||
import qrcode
|
||||
import uvicorn
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.types import ImageContent
|
||||
from mcp import types
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
WIDGET_URI = "ui://qr-server/widget.html"
|
||||
|
|
@ -28,7 +28,7 @@ def generate_qr(
|
|||
error_correction: str = "M",
|
||||
fill_color: str = "black",
|
||||
back_color: str = "white",
|
||||
) -> list[ImageContent]:
|
||||
) -> list[types.ImageContent]:
|
||||
"""Generate a QR code from text.
|
||||
|
||||
Args:
|
||||
|
|
@ -59,31 +59,55 @@ def generate_qr(
|
|||
buffer = io.BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
b64 = base64.b64encode(buffer.getvalue()).decode()
|
||||
return [ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||
return [types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||
|
||||
|
||||
# IMPORTANT: resourceDomains needed for CSP to allow loading SDK from unpkg.com
|
||||
# Without this, hosts enforcing CSP will block the external script import
|
||||
@mcp.resource(WIDGET_URI, mime_type="text/html")
|
||||
def widget() -> dict:
|
||||
# Register widget resource using FastMCP decorator (returns HTML string)
|
||||
@mcp.resource(WIDGET_URI, mime_type="text/html;profile=mcp-app")
|
||||
def widget() -> str:
|
||||
return Path(__file__).parent.joinpath("widget.html").read_text()
|
||||
|
||||
|
||||
# Override the read_resource handler to inject _meta into the response
|
||||
# This is needed because FastMCP doesn't support custom _meta on resources
|
||||
_low_level_server = mcp._mcp_server
|
||||
|
||||
|
||||
async def _read_resource_with_meta(req: types.ReadResourceRequest):
|
||||
"""Custom handler that injects CSP metadata for the widget resource."""
|
||||
uri = str(req.params.uri)
|
||||
html = Path(__file__).parent.joinpath("widget.html").read_text()
|
||||
return {
|
||||
"text": html,
|
||||
"_meta": {
|
||||
"ui": {
|
||||
"csp": {
|
||||
"resourceDomains": ["https://unpkg.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# HACK: Bypass SDK's restrictive mime_type validation
|
||||
# The SDK pattern doesn't allow ";profile=mcp-app" but MCP spec requires it for widgets
|
||||
# https://github.com/modelcontextprotocol/python-sdk/pull/1755
|
||||
for resource in mcp._resource_manager._resources.values():
|
||||
if str(resource.uri) == WIDGET_URI:
|
||||
object.__setattr__(resource, 'mime_type', 'text/html;profile=mcp-app')
|
||||
if uri == WIDGET_URI:
|
||||
# NOTE: Must use model_validate with '_meta' key (not 'meta') due to Pydantic alias behavior
|
||||
content = types.TextResourceContents.model_validate({
|
||||
"uri": WIDGET_URI,
|
||||
"mimeType": "text/html;profile=mcp-app",
|
||||
"text": html,
|
||||
# IMPORTANT: all the external domains used by app must be listed
|
||||
# in the _meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy
|
||||
"_meta": {"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}}
|
||||
})
|
||||
return types.ServerResult(
|
||||
types.ReadResourceResult(contents=[content])
|
||||
)
|
||||
|
||||
# Fallback for other resources (shouldn't happen for this server)
|
||||
return types.ServerResult(
|
||||
types.ReadResourceResult(
|
||||
contents=[
|
||||
types.TextResourceContents(
|
||||
uri=uri,
|
||||
mimeType="text/plain",
|
||||
text="Resource not found"
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Replace the handler after FastMCP has registered its own
|
||||
_low_level_server.request_handlers[types.ReadResourceRequest] = _read_resource_with_meta
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--stdio" in sys.argv:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<body>
|
||||
<div id="qr"></div>
|
||||
<script type="module">
|
||||
import { App, PostMessageTransport } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.0.4";
|
||||
import { App, PostMessageTransport } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.0.1";
|
||||
|
||||
const app = new App({ name: "QR Widget", version: "1.0.0" });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue