-
Notifications
You must be signed in to change notification settings - Fork 171
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 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| **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`: | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| ```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.
The
structuredContentexample and the cross-host parsing guidance are inconsistent: the example returnsstructuredContent: { results: data }and a human sentence incontent[0].text, but the later widget parser expects JSON incontent[0].textand reads adatafield. Align the field names and update the examplecontentblock(s) to match the documented fallback parsing approach (or adjust the parser guidance to match the example).