feat(qr-server): use Python SDK meta parameter for resource CSP metadata (#303)
* update qr-server w/ python sdk fix * feat(qr-server): use Python SDK meta parameter for resource CSP metadata - Update qr-server to use the new meta parameter on @mcp.resource decorator instead of manually overriding the read_resource handler - Add ui.resourceUri to tool meta alongside legacy ui/resourceUri for forward compatibility - Simplify server.py by removing the custom _read_resource_with_meta handler - Add qr-server to grid screenshot generation tests - Update grid-cell.png screenshot This leverages the Python SDK's native support for resource metadata, eliminating the need for low-level handler manipulation. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
73ab853dc5
commit
de8048cb6d
3 changed files with 15 additions and 46 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 27 KiB |
|
|
@ -2,7 +2,7 @@
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.10"
|
# requires-python = ">=3.10"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "mcp>=1.9.0",
|
# "mcp @ git+https://github.com/modelcontextprotocol/python-sdk@main",
|
||||||
# "qrcode[pil]>=8.0",
|
# "qrcode[pil]>=8.0",
|
||||||
# "uvicorn>=0.34.0",
|
# "uvicorn>=0.34.0",
|
||||||
# "starlette>=0.46.0",
|
# "starlette>=0.46.0",
|
||||||
|
|
@ -30,7 +30,10 @@ PORT = int(os.environ.get("PORT", "3108"))
|
||||||
mcp = FastMCP("QR Code Server", port=PORT, stateless_http=True)
|
mcp = FastMCP("QR Code Server", port=PORT, stateless_http=True)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool(meta={"ui/resourceUri": WIDGET_URI})
|
@mcp.tool(meta={
|
||||||
|
"ui":{"resourceUri": WIDGET_URI},
|
||||||
|
"ui/resourceUri": WIDGET_URI, # legacy support
|
||||||
|
})
|
||||||
def generate_qr(
|
def generate_qr(
|
||||||
text: str = "https://modelcontextprotocol.io",
|
text: str = "https://modelcontextprotocol.io",
|
||||||
box_size: int = 10,
|
box_size: int = 10,
|
||||||
|
|
@ -72,53 +75,17 @@ def generate_qr(
|
||||||
return [types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
return [types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||||
|
|
||||||
|
|
||||||
# Register widget resource using FastMCP decorator (returns HTML string)
|
# IMPORTANT: all the external domains used by app must be listed
|
||||||
@mcp.resource(WIDGET_URI, mime_type="text/html;profile=mcp-app")
|
# in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy
|
||||||
|
@mcp.resource(
|
||||||
|
WIDGET_URI,
|
||||||
|
mime_type="text/html;profile=mcp-app",
|
||||||
|
meta={"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}},
|
||||||
|
)
|
||||||
def widget() -> str:
|
def widget() -> str:
|
||||||
|
"""Widget HTML resource with CSP metadata for external dependencies."""
|
||||||
return Path(__file__).parent.joinpath("widget.html").read_text()
|
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()
|
|
||||||
|
|
||||||
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 __name__ == "__main__":
|
||||||
if "--stdio" in sys.argv:
|
if "--stdio" in sys.argv:
|
||||||
# Claude Desktop mode
|
# Claude Desktop mode
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ const SERVERS = [
|
||||||
},
|
},
|
||||||
{ key: "map-server", name: "Map Server", dir: "map-server" },
|
{ key: "map-server", name: "Map Server", dir: "map-server" },
|
||||||
{ key: "pdf-server", name: "PDF Server", dir: "pdf-server" },
|
{ key: "pdf-server", name: "PDF Server", dir: "pdf-server" },
|
||||||
|
{ key: "qr-server", name: "QR Code Server", dir: "qr-server" },
|
||||||
|
{ key: "say-server", name: "Say Demo", dir: "say-server" },
|
||||||
{
|
{
|
||||||
key: "scenario-modeler",
|
key: "scenario-modeler",
|
||||||
name: "SaaS Scenario Modeler",
|
name: "SaaS Scenario Modeler",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue