Skip to content

Conversation

@aaazzam
Copy link
Collaborator

@aaazzam aaazzam commented Oct 22, 2025

Add native OpenAI widget support

FastMCP now provides first-class support for OpenAI's ChatGPT widgets through the @app.ui.openai.widget decorator. Previously, developers had to manually manage widget HTML resources, construct OpenAI-specific metadata, and transform return values to match OpenAI's {"content": [...], "structuredContent": {...}} wire format. This PR eliminates that boilerplate entirely—just decorate your function and return a str, dict, or tuple[str, dict], and FastMCP handles the rest.

The decorator automatically registers widget HTML as an MCP resource, injects OpenAI metadata (CSP policies, widget templates, invocation status messages), and wraps your function to auto-transform its return value. You write clean Python functions that return natural data types, and the framework translates them to OpenAI's format behind the scenes.

from fastmcp import FastMCP

app = FastMCP("PizzaTracker")

@app.ui.openai.widget(
    name="pizza-map",
    template_uri="ui://widget/pizza-map.html",
    html='<div id="root"></div><script src="https://cdn.com/widget.js"></script>',
    invoking="Hand-tossing a map",
    invoked="Served a fresh map"
)
def show_pizza_map(topping: str) -> dict:
    """Show an interactive pizza map."""
    return {"topping": topping}  # Auto-transformed to OpenAI format!

The decorator follows FastMCP's existing conventions, using name= (consistent with @app.tool(name=...)) and exposing all customization points: widget_description, widget_csp_resources, widget_csp_connect, widget_prefers_border, and more. Smart defaults handle the common cases—for instance, widget_description defaults to "{title} widget UI." when not specified.

This makes OpenAI widgets feel like a natural extension of FastMCP's tool system rather than a separate SDK layer. See examples/widget_test/ for three working patterns showing dict-only, string-only, and combined return types.

Resolves #2014

🤖 Generated with Claude Code

assert resource.meta is not None
assert "openai/widgetCSP" in resource.meta
csp = resource.meta["openai/widgetCSP"]
assert "https://custom.com" in csp["resource_domains"]

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High test

The string
https://custom.com
may be at an arbitrary position in the sanitized URL.
assert "openai/widgetCSP" in resource.meta
csp = resource.meta["openai/widgetCSP"]
assert "https://custom.com" in csp["resource_domains"]
assert "https://another.com" in csp["resource_domains"]

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High test

The string
https://another.com
may be at an arbitrary position in the sanitized URL.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

csp = resource.meta["openai/widgetCSP"]
assert "https://custom.com" in csp["resource_domains"]
assert "https://another.com" in csp["resource_domains"]
assert "wss://websocket.com" in csp["connect_domains"]

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High test

The string
wss://websocket.com
may be at an arbitrary position in the sanitized URL.

Copilot Autofix

AI 6 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

@marvin-context-protocol marvin-context-protocol bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality. labels Oct 22, 2025
Copy link

@BrandonShar BrandonShar left a comment

Choose a reason for hiding this comment

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

This looks like a nice UI! Since the changes supporting meta should land in the sdk tomorrow, I have a small suggestion on response type payloads.


from typing import Any

WidgetToolResponse = dict[str, Any]

Choose a reason for hiding this comment

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

Would it make sense to introduce this as a dataclass/pydantic from the beginning? Support for _meta fields will land in the python sdk tomorrow and that's part of what's necessary to support openai's full protocol. I think string|dict|WidgetToolResponse could make a lot of sense instead of extending the tuple or supporting different return types. Something like a simpler version of the CallToolResult like:

class WidgetToolResponse(BaseModel)
   content: str
   structured_content: dict[str, Any] | None = None
   meta: dict[str, Any] | None = None

@aaazzam
Copy link
Collaborator Author

aaazzam commented Oct 22, 2025

Nice! Welcome your feedback. Lay it on me!

@BrandonShar
Copy link

Just PR'd #2283 to hopefully make meta usage simpler!

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

Labels

feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for OpenAI Apps SDK

3 participants