44client directory. Each handler returns the HTML shell via an MCP resource and
55returns structured content so the ChatGPT client can hydrate the widget."""
66
7- from __future__ import annotations
8-
97import asyncio
108from dataclasses import dataclass
9+ from pathlib import Path
1110from random import choice
12- from typing import Any , Dict , List
11+ from typing import Any , Dict
1312
14- from starlette .routing import Mount
15- from starlette .staticfiles import StaticFiles
1613import mcp .types as types
17- from mcp .server .fastmcp import FastMCP
1814import uvicorn
19- from mcp_agent .app import MCPApp
20- from pathlib import Path
15+ from mcp .server .fastmcp import FastMCP
16+ from starlette .routing import Mount
17+ from starlette .staticfiles import StaticFiles
2118
19+ from mcp_agent .app import MCPApp
2220from mcp_agent .server .app_server import create_mcp_server_for_app
2321
2422
@@ -76,7 +74,7 @@ class CoinFlipWidget:
7674 identifier = "coin-flip" ,
7775 title = "Flip a Coin" ,
7876 # OpenAI Apps heavily cache resource by URI, so use a date-based URI to bust the cache when updating the app.
79- template_uri = "ui://widget/coin-flip-10-22 -2025-15-48 .html" ,
77+ template_uri = "ui://widget/coin-flip-10-27 -2025-16-34 .html" ,
8078 invoking = "Preparing for coin flip" ,
8179 invoked = "Flipping the coin..." ,
8280 html = INLINE_HTML_TEMPLATE , # Use INLINE_HTML_TEMPLATE or DEPLOYED_HTML_TEMPLATE
@@ -99,21 +97,6 @@ def _resource_description() -> str:
9997 return "Coin flip widget markup"
10098
10199
102- def _tool_meta () -> Dict [str , Any ]:
103- return {
104- "openai/outputTemplate" : WIDGET .template_uri ,
105- "openai/toolInvocation/invoking" : WIDGET .invoking ,
106- "openai/toolInvocation/invoked" : WIDGET .invoked ,
107- "openai/widgetAccessible" : True ,
108- "openai/resultCanProduceWidget" : True ,
109- "annotations" : {
110- "destructiveHint" : False ,
111- "openWorldHint" : False ,
112- "readOnlyHint" : True ,
113- },
114- }
115-
116-
117100def _embedded_widget_resource () -> types .EmbeddedResource :
118101 return types .EmbeddedResource (
119102 type = "resource" ,
@@ -126,110 +109,44 @@ def _embedded_widget_resource() -> types.EmbeddedResource:
126109 )
127110
128111
129- @mcp ._mcp_server .list_tools ()
130- async def _list_tools () -> List [types .Tool ]:
131- return [
132- types .Tool (
133- name = WIDGET .identifier ,
134- title = WIDGET .title ,
135- inputSchema = {"type" : "object" , "properties" : {}},
136- description = WIDGET .title ,
137- _meta = _tool_meta (),
138- )
139- ]
140-
141-
142- @mcp ._mcp_server .list_resources ()
143- async def _list_resources () -> List [types .Resource ]:
144- return [
145- types .Resource (
146- name = WIDGET .title ,
147- title = WIDGET .title ,
148- uri = WIDGET .template_uri ,
149- description = _resource_description (),
150- mimeType = MIME_TYPE ,
151- _meta = _tool_meta (),
152- )
153- ]
154-
155-
156- @mcp ._mcp_server .list_resource_templates ()
157- async def _list_resource_templates () -> List [types .ResourceTemplate ]:
158- return [
159- types .ResourceTemplate (
160- name = WIDGET .title ,
161- title = WIDGET .title ,
162- uriTemplate = WIDGET .template_uri ,
163- description = _resource_description (),
164- mimeType = MIME_TYPE ,
165- _meta = _tool_meta (),
166- )
167- ]
168-
169-
170- async def _handle_read_resource (req : types .ReadResourceRequest ) -> types .ServerResult :
171- if str (req .params .uri ) != WIDGET .template_uri :
172- return types .ServerResult (
173- types .ReadResourceResult (
174- contents = [],
175- _meta = {"error" : f"Unknown resource: { req .params .uri } " },
176- )
177- )
178-
179- contents = [
180- types .TextResourceContents (
181- uri = WIDGET .template_uri ,
182- mimeType = MIME_TYPE ,
183- text = WIDGET .html ,
184- _meta = _tool_meta (),
185- )
186- ]
187-
188- return types .ServerResult (types .ReadResourceResult (contents = contents ))
189-
190-
191- async def _call_tool_request (req : types .CallToolRequest ) -> types .ServerResult :
192- if req .params .name != WIDGET .identifier :
193- return types .ServerResult (
194- types .CallToolResult (
195- content = [
196- types .TextContent (
197- type = "text" ,
198- text = f"Unknown tool: { req .params .name } " ,
199- )
200- ],
201- isError = True ,
202- )
203- )
204-
205- widget_resource = _embedded_widget_resource ()
206- meta : Dict [str , Any ] = {
207- "openai.com/widget" : widget_resource .model_dump (mode = "json" ),
112+ def _tool_meta () -> Dict [str , Any ]:
113+ return {
114+ "openai.com/widget" : _embedded_widget_resource ().model_dump (mode = "json" ),
208115 "openai/outputTemplate" : WIDGET .template_uri ,
209116 "openai/toolInvocation/invoking" : WIDGET .invoking ,
210117 "openai/toolInvocation/invoked" : WIDGET .invoked ,
211118 "openai/widgetAccessible" : True ,
212119 "openai/resultCanProduceWidget" : True ,
213120 }
214121
215- flip_result = choice (["heads" , "tails" ])
216122
217- return types .ServerResult (
218- types .CallToolResult (
219- content = [
220- types .TextContent (
221- type = "text" ,
222- text = WIDGET .response_text ,
223- )
224- ],
225- structuredContent = {"flipResult" : flip_result },
226- _meta = meta ,
227- )
228- )
123+ @app .tool (
124+ name = WIDGET .identifier ,
125+ title = WIDGET .title ,
126+ description = "Flip a coin and get heads or tails." ,
127+ annotations = types .ToolAnnotations (
128+ destructiveHint = False ,
129+ openWorldHint = False ,
130+ readOnlyHint = True ,
131+ ),
132+ structured_output = True ,
133+ meta = _tool_meta (),
134+ )
135+ async def flip_coin () -> Dict [str , str ]:
136+ """Flip a coin and get heads or tails."""
137+ flip_result = choice (["heads" , "tails" ])
138+ return {"flipResult" : flip_result }
229139
230140
231- mcp ._mcp_server .request_handlers [types .CallToolRequest ] = _call_tool_request
232- mcp ._mcp_server .request_handlers [types .ReadResourceRequest ] = _handle_read_resource
141+ @mcp .resource (
142+ uri = WIDGET .template_uri ,
143+ title = WIDGET .title ,
144+ description = _resource_description (),
145+ mime_type = MIME_TYPE ,
146+ )
147+ def get_widget_html () -> str :
148+ """Provide the HTML template for the coin flip widget."""
149+ return WIDGET .html
233150
234151
235152# NOTE: This main function is for local testing; it spins up the MCP server (SSE) and
0 commit comments