Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,23 @@ pnpm run dev

## Serve the static assets

If you want to preview the generated bundles without the MCP servers, start the static file server after running a build:
All of the MCP servers expect the bundled HTML, JS, and CSS to be served from the local static file server. After every build, start the server before launching any MCP processes:

```bash
pnpm run serve
```

The assets are exposed at [`http://localhost:4444`](http://localhost:4444) with CORS enabled so that local tooling (including MCP inspectors) can fetch them.

> **Note:** The Python Pizzaz server caches widget HTML with `functools.lru_cache`. If you rebuild or manually edit files in `assets/`, restart the MCP server so it picks up the updated markup.

## Run the MCP servers

The repository ships several demo MCP servers that highlight different widget bundles:

- **Pizzaz (Node & Python)** – pizza-inspired collection of tools and components
- **Solar system (Python)** – 3D solar system viewer

Every tool response includes plain text content, structured JSON, and `_meta.openai/outputTemplate` metadata so the Apps SDK can hydrate the matching widget.

### Pizzaz Node server

```bash
Expand Down
19 changes: 13 additions & 6 deletions pizzaz_server_node/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function readWidgetHtml(componentName: string): string {
return htmlContents;
}

function widgetMeta(widget: PizzazWidget) {
function widgetDescriptorMeta(widget: PizzazWidget) {
return {
"openai/outputTemplate": widget.templateUri,
"openai/toolInvocation/invoking": widget.invoking,
Expand All @@ -84,6 +84,13 @@ function widgetMeta(widget: PizzazWidget) {
} as const;
}

function widgetInvocationMeta(widget: PizzazWidget) {
return {
"openai/toolInvocation/invoking": widget.invoking,
"openai/toolInvocation/invoked": widget.invoked,
} as const;
}

const widgets: PizzazWidget[] = [
{
id: "pizza-map",
Expand Down Expand Up @@ -152,7 +159,7 @@ const tools: Tool[] = widgets.map((widget) => ({
description: widget.title,
inputSchema: toolInputSchema,
title: widget.title,
_meta: widgetMeta(widget),
_meta: widgetDescriptorMeta(widget),
// To disable the approval prompt for the widgets
annotations: {
destructiveHint: false,
Expand All @@ -166,15 +173,15 @@ const resources: Resource[] = widgets.map((widget) => ({
name: widget.title,
description: `${widget.title} widget markup`,
mimeType: "text/html+skybridge",
_meta: widgetMeta(widget),
_meta: widgetDescriptorMeta(widget),
}));

const resourceTemplates: ResourceTemplate[] = widgets.map((widget) => ({
uriTemplate: widget.templateUri,
name: widget.title,
description: `${widget.title} widget markup`,
mimeType: "text/html+skybridge",
_meta: widgetMeta(widget),
_meta: widgetDescriptorMeta(widget),
}));

function createPizzazServer(): Server {
Expand Down Expand Up @@ -213,7 +220,7 @@ function createPizzazServer(): Server {
uri: widget.templateUri,
mimeType: "text/html+skybridge",
text: widget.html,
_meta: widgetMeta(widget),
_meta: widgetDescriptorMeta(widget),
},
],
};
Expand Down Expand Up @@ -255,7 +262,7 @@ function createPizzazServer(): Server {
structuredContent: {
pizzaTopping: args.pizzaTopping,
},
_meta: widgetMeta(widget),
_meta: widgetInvocationMeta(widget),
};
}
);
Expand Down
25 changes: 6 additions & 19 deletions pizzaz_server_python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,11 @@ def _tool_meta(widget: PizzazWidget) -> Dict[str, Any]:
}


def _embedded_widget_resource(widget: PizzazWidget) -> types.EmbeddedResource:
return types.EmbeddedResource(
type="resource",
resource=types.TextResourceContents(
uri=widget.template_uri,
mimeType=MIME_TYPE,
text=widget.html,
title=widget.title,
),
)
def _tool_invocation_meta(widget: PizzazWidget) -> Dict[str, Any]:
return {
"openai/toolInvocation/invoking": widget.invoking,
"openai/toolInvocation/invoked": widget.invoked,
}


@mcp._mcp_server.list_tools()
Expand Down Expand Up @@ -262,15 +257,7 @@ async def _call_tool_request(req: types.CallToolRequest) -> types.ServerResult:
)

topping = payload.pizza_topping
widget_resource = _embedded_widget_resource(widget)
meta: Dict[str, Any] = {
"openai.com/widget": widget_resource.model_dump(mode="json"),
"openai/outputTemplate": widget.template_uri,
"openai/toolInvocation/invoking": widget.invoking,
"openai/toolInvocation/invoked": widget.invoked,
"openai/widgetAccessible": True,
"openai/resultCanProduceWidget": True,
}
meta = _tool_invocation_meta(widget)

return types.ServerResult(
types.CallToolResult(
Expand Down