Skip to content

Commit cb09074

Browse files
authored
Update chatgpt app cloud example with latest mcp sdk updates (#597)
* Update chatgpt app cloud example with latest mcp sdk updates * Update template data
1 parent 1aa967b commit cb09074

File tree

2 files changed

+70
-236
lines changed
  • examples/cloud/chatgpt_app
  • src/mcp_agent/data/examples/cloud/chatgpt_app

2 files changed

+70
-236
lines changed

examples/cloud/chatgpt_app/main.py

Lines changed: 35 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,19 @@
44
client directory. Each handler returns the HTML shell via an MCP resource and
55
returns structured content so the ChatGPT client can hydrate the widget."""
66

7-
from __future__ import annotations
8-
97
import asyncio
108
from dataclasses import dataclass
9+
from pathlib import Path
1110
from 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
1613
import mcp.types as types
17-
from mcp.server.fastmcp import FastMCP
1814
import 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
2220
from 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-
117100
def _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

src/mcp_agent/data/examples/cloud/chatgpt_app/main.py

Lines changed: 35 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,19 @@
44
client directory. Each handler returns the HTML shell via an MCP resource and
55
returns structured content so the ChatGPT client can hydrate the widget."""
66

7-
from __future__ import annotations
8-
97
import asyncio
108
from dataclasses import dataclass
9+
from pathlib import Path
1110
from 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
1613
import mcp.types as types
17-
from mcp.server.fastmcp import FastMCP
1814
import 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
2220
from 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-
117100
def _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

Comments
 (0)