-
Notifications
You must be signed in to change notification settings - Fork 180
modified create-mcp-app skill to support chatGPT #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,11 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: Create MCP App | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: This skill should be used when the user asks to "create an MCP App", "add a UI to an MCP tool", "build an interactive MCP View", "scaffold an MCP App", or needs guidance on MCP Apps SDK patterns, UI-resource registration, MCP App lifecycle, or host integration. Provides comprehensive guidance for building MCP Apps with interactive UIs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: This skill should be used when the user asks to "create an MCP App", "add a UI to an MCP tool", "build an interactive MCP View", "scaffold an MCP App", or needs guidance on MCP Apps SDK patterns, UI-resource registration, MCP App lifecycle, or host integration. Provides comprehensive guidance for building MCP Apps with interactive UIs that work across both Claude and ChatGPT. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create MCP App | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Build interactive UIs that run inside MCP-enabled hosts like Claude Desktop. An MCP App combines an MCP tool with an HTML resource to display rich, interactive content. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Build interactive UIs that run inside MCP-enabled hosts like Claude Desktop and ChatGPT. An MCP App combines an MCP tool with an HTML resource to display rich, interactive content. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Core Concept: Tool + Resource | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -16,7 +16,7 @@ Every MCP App requires two parts linked together: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 3. **Link** - The tool's `_meta.ui.resourceUri` references the resource | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Host calls tool → Server returns result → Host renders resource UI → UI receives result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Host calls tool -> Server returns result -> Host renders resource UI -> UI receives result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Quick Start Decision Tree | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -302,6 +302,249 @@ async function toggleFullscreen() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| See `examples/shadertoy-server/` for complete implementation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## ChatGPT Compliance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ChatGPT enforces additional metadata requirements beyond what Claude needs. If you are building an MCP App that must work in ChatGPT (or both Claude and ChatGPT), apply everything in this section. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Reference: https://developers.openai.com/apps-sdk/build/mcp-server/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ### Tool Annotations (Required) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Every tool registered with `registerAppTool` must include an `annotations` object describing its impact. ChatGPT uses these hints to decide how to gate tool invocations. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| registerAppTool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "my-tool", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "My Tool", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Does something useful", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputSchema: { query: z.string() }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readOnlyHint: true, // true if the tool only reads data (search, lookup) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| destructiveHint: false, // true if the tool deletes or modifies data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openWorldHint: false, // false if the tool targets a bounded set of resources | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _meta: { ui: { resourceUri } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async ({ query }) => { /* handler */ } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Choose values that accurately describe the tool's behavior: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - A weather lookup: `readOnlyHint: true, destructiveHint: false, openWorldHint: false` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - A file deletion tool: `readOnlyHint: false, destructiveHint: true, openWorldHint: false` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - A web search tool: `readOnlyHint: true, destructiveHint: false, openWorldHint: true` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Claude ignores these annotations, so including them is safe for cross-host apps. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ### `structuredContent` in Tool Responses (Required) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ChatGPT expects tool results to use the `structuredContent` field for data that both the model and the widget consume. The `content` text array serves as a narrative fallback for the model. An optional `_meta` sibling carries widget-only data that is never sent to the model. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Model + widget: concise JSON the widget renders and the model reasons about | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| structuredContent: { results: data }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Model only: text narration for non-UI hosts or model context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { type: "text", text: "Found 5 results for your query." }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Widget only (optional): large or sensitive data the model should not see | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _meta: { rawPayload: largeObject }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+343
to
+356
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **Claude compatibility:** Claude delivers `content` to the widget via `ontoolresult` but may not pass `structuredContent`. Write the widget's result parser to check `structuredContent` first, then fall back to parsing JSON from `content[0].text`: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+341
to
+360
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to find a unified approach that works on all platforms (and update all of our examples to use that). Is the difference that ChatGPT always shows |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```typescript | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function parseResult(result: CallToolResult) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ChatGPT path: structuredContent is present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const structured = result.structuredContent as Record<string, unknown> | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (structured?.data) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { data: structured.data }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Claude path: data embedded as JSON in content text | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = result.content?.find((c) => c.type === "text"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (text && "text" in text) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(text.text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.data) return { data: parsed.data }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+366
to
+374
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (structured?.data) { | |
| return { data: structured.data }; | |
| } | |
| // Claude path: data embedded as JSON in content text | |
| const text = result.content?.find((c) => c.type === "text"); | |
| if (text && "text" in text) { | |
| const parsed = JSON.parse(text.text); | |
| if (parsed.data) return { data: parsed.data }; | |
| if (structured?.results) { | |
| return { results: structured.results }; | |
| } | |
| // Claude path: data embedded as JSON in content text | |
| const text = result.content?.find((c) => c.type === "text"); | |
| if (text && "text" in text) { | |
| try { | |
| const parsed = JSON.parse(text.text as string) as Record<string, unknown>; | |
| if (parsed && typeof parsed === "object" && "results" in parsed) { | |
| return { results: (parsed as { results: unknown }).results }; | |
| } | |
| } catch { | |
| // Ignore JSON parse errors and fall through to the error return below. | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a bug. The spec explicitly allows "empty or omitted":
ext-apps/specification/2026-01-26/apps.mdx
Lines 114 to 145 in 0bbbfee
| interface McpUiResourceCsp { | |
| /** | |
| * Origins for network requests (fetch/XHR/WebSocket) | |
| * | |
| * - Empty or omitted = no external connections (secure default) | |
| * - Maps to CSP `connect-src` directive | |
| * | |
| * @example | |
| * ["https://api.weather.com", "wss://realtime.service.com"] | |
| */ | |
| connectDomains?: string[], | |
| /** | |
| * Origins for static resources (images, scripts, stylesheets, fonts, media) | |
| * | |
| * - Empty or omitted = no external resources (secure default) | |
| * - Wildcard subdomains supported: `https://*.example.com` | |
| * - Maps to CSP `img-src`, `script-src`, `style-src`, `font-src`, `media-src` directives | |
| * | |
| * @example | |
| * ["https://cdn.jsdelivr.net", "https://*.cloudflare.com"] | |
| */ | |
| resourceDomains?: string[], | |
| /** | |
| * Origins for nested iframes | |
| * | |
| * - Empty or omitted = no nested iframes allowed (`frame-src 'none'`) | |
| * - Maps to CSP `frame-src` directive | |
| * | |
| * @example | |
| * ["https://www.youtube.com", "https://player.vimeo.com"] | |
| */ | |
| frameDomains?: string[], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is domain: "https://my-weather-app.example.com" correct? I was under the impression that it would be something like domain: "my-weather-app-example-com" for ChatGPT, per:
ext-apps/specification/2026-01-26/apps.mdx
Lines 199 to 218 in 0bbbfee
| /** | |
| * Dedicated origin for view | |
| * | |
| * Optional domain for the view's sandbox origin. Useful when views need | |
| * stable, dedicated origins for OAuth callbacks, CORS policies, or API key allowlists. | |
| * | |
| * **Host-dependent:** The format and validation rules for this field are | |
| * determined by each host. Servers MUST consult host-specific documentation | |
| * for the expected domain format. Common patterns include: | |
| * - Hash-based subdomains (e.g., `{hash}.claudemcpcontent.com`) | |
| * - URL-derived subdomains (e.g., `www-example-com.oaiusercontent.com`) | |
| * | |
| * If omitted, Host uses default sandbox origin (typically per-conversation). | |
| * | |
| * @example | |
| * "a904794854a047f6.claudemcpcontent.com" | |
| * @example | |
| * "www-example-com.oaiusercontent.com" | |
| */ | |
| domain?: string, |
In either case, I think we could add this to our example in the patterns guide, and that way we can omit it from the skill itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm misunderstanding, I think this is not true (per the referenced example).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think we should include this in any of our documentation.
If there is a single URL (or small number of URLs) that serve documentation for these extensions, we could possibly point to those, but only if the documentation presents the extensions as progressive enhancements (i.e., provides explicit guidance to not rely on those extensions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think we should position this as a "ChatGPT Compliance" checklist. To whatever extent a checklist is necessary (which it may not be, given other changes), it should be presented as a generic checklist.
The checklist is also duplicative with the "Common Mistakes to Avoid" section below. We should go with one or the other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm, ChatGPT requires these explicit annotations for all MCP tools (regardless of
true/false)?If that is the case, it might be more effective to simply add them to all of our examples. That way the agent should pick up on the pattern without having to call it out in the skill.
Another possibility (not necessarily better) would be to enforce their presence in the type signature of
registerAppTool.