Skip to content

modified create-mcp-app skill to support chatGPT#440

Open
mandpd wants to merge 1 commit intomodelcontextprotocol:mainfrom
mandpd:main
Open

modified create-mcp-app skill to support chatGPT#440
mandpd wants to merge 1 commit intomodelcontextprotocol:mainfrom
mandpd:main

Conversation

@mandpd
Copy link

@mandpd mandpd commented Feb 9, 2026

Motivation and Context

The existing create-mcp-app SKILL.md only covers building MCP Apps for Claude. ChatGPT enforces additional metadata requirements (tool annotations, structuredContent responses,
widget CSP, widget domain) that are not documented in the skill. Developers building cross-host MCP Apps have to discover these requirements through trial and error against the ChatGPT
template configuration UI.

This change adds a comprehensive "ChatGPT Compliance" section so the skill can guide developers through both hosts' requirements in a single reference.

How Has This Been Tested?

  • Built and deployed a weather forecast MCP App against both Claude and ChatGPT using the guidance in this skill
  • Verified that adding annotations, structuredContent, _meta.ui.csp, and _meta.ui.domain resolves ChatGPT's template configuration errors ("Widget CSP is not set", "Widget
    domain is not set")
  • Confirmed that the dual-path structuredContent / content text parsing pattern works in both Claude (which delivers content) and ChatGPT (which delivers structuredContent)
  • Verified that all additions (annotations, CSP, domain) are safely ignored by Claude

Breaking Changes

None. All additions are purely documentation. Existing MCP Apps built with the original skill continue to work in Claude without modification.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Key additions to the skill:

  • Tool annotations -- readOnlyHint, destructiveHint, openWorldHint with guidance on choosing values for different tool types
  • structuredContent response pattern -- three-field response shape (structuredContent, content, _meta) with a cross-host parsing pattern that checks structuredContent
    first and falls back to content text
  • Widget CSP and domain -- complete registerAppResource examples comparing Claude-only vs ChatGPT-compatible registration
  • Transport requirements -- documents ChatGPT's HTTPS-only constraint with generic tunnelling service guidance
  • File parameter inputs -- ChatGPT's openai/fileParams extension for user-uploaded files
  • Compliance checklist -- 8-item pre-submission checklist
  • 4 new common mistakes (items 9-12) covering ChatGPT-specific pitfalls
  • Testing with ChatGPT subsection with tunnelling workflow

Reference: https://developers.openai.com/apps-sdk/build/mcp-server/

Copilot AI review requested due to automatic review settings February 9, 2026 00:53
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the create-mcp-app skill documentation to cover MCP App requirements for ChatGPT in addition to Claude, so developers can build cross-host compatible MCP Apps without trial-and-error in ChatGPT’s template UI.

Changes:

  • Expands the skill intro to explicitly target both Claude and ChatGPT.
  • Adds a comprehensive “ChatGPT Compliance” section (tool annotations, structuredContent, widget CSP/domain, transport notes, file params, checklist).
  • Extends “Common Mistakes” and “Testing” sections with ChatGPT-specific guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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 };
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the parseResult example, JSON.parse(text.text) can throw (especially given the earlier example content text is not JSON). Wrap the parse in a try/catch (or validate the string before parsing) so a non-JSON text block doesn’t crash the widget, and keep the parsed field name consistent with the structuredContent shape you recommend.

Suggested change
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.
}

Copilot uses AI. Check for mistakes.
Comment on lines +343 to +356
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 },
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structuredContent example and the cross-host parsing guidance are inconsistent: the example returns structuredContent: { results: data } and a human sentence in content[0].text, but the later widget parser expects JSON in content[0].text and reads a data field. Align the field names and update the example content block(s) to match the documented fallback parsing approach (or adjust the parser guidance to match the example).

Copilot uses AI. Check for mistakes.
Copy link
Member

@jonathanhefner jonathanhefner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I lean more toward including some of this information in our docs (where necessary), and pointing the skill to those (a la #416).

I also strongly prefer to avoid vendor-specific guidance as much as possible. (And I wonder if some of these issues are transient, i.e., oversights that OpenAI will fix.)

If there are enough persistent issues, then it may be worth creating a dedicated "Troubleshooting" guide, where we can call out vendor-specific problems that occur during the testing phase.

Comment on lines +311 to +339
### 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.
Copy link
Member

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.

Comment on lines +341 to +360
### `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`:
Copy link
Member

Choose a reason for hiding this comment

The 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 structuredContent to the model, whereas Claude does not? (I know that was the case for OpenAI Apps SDK, but I didn't realize they were doing the same for MCP Apps.)

Comment on lines +381 to +401
### Widget CSP (Required for Submission)

The resource contents must include a `_meta.ui.csp` object declaring the widget's Content Security Policy. ChatGPT sandboxes widgets in an iframe and enforces this CSP. Without it, the ChatGPT template configuration will show: *"Widget CSP is not set for this template."*

```typescript
_meta: {
ui: {
csp: {
// Domains the widget may fetch() or XMLHttpRequest to
connectDomains: ["https://api.example.com"],

// Domains the widget may load images, fonts, or scripts from
resourceDomains: ["https://cdn.example.com"],

// Domains the widget may embed in sub-iframes (avoid if possible --
// declaring frameDomains triggers heightened security review)
frameDomains: [],
},
},
},
```
Copy link
Member

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":

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[],

Comment on lines +414 to +425
### Widget Domain (Required for Submission)

The resource contents must include a `_meta.ui.domain` with a unique HTTPS URL. ChatGPT renders the widget at `<domain>.web-sandbox.oaiusercontent.com`. Without it, the ChatGPT template configuration will show: *"Widget domain is not set for this template."*

```typescript
_meta: {
ui: {
domain: "https://my-weather-app.example.com",
csp: { /* ... */ },
},
},
```
Copy link
Member

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:

/**
* 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.


Replace the placeholder with your actual production domain before submitting.

Claude ignores this metadata, so including it is safe for cross-host apps.
Copy link
Member

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).

Comment on lines +492 to +533
### ChatGPT-Specific Widget APIs (`window.openai`)

ChatGPT exposes optional host APIs on `window.openai` inside the widget iframe:

- `uploadFile` / `getFileDownloadUrl` -- image and file handling
- `requestModal` -- host-owned modal overlays
- `requestCheckout` -- Instant Checkout (when enabled)

These are ChatGPT-only and not part of the MCP Apps standard. Use them for enhanced UX but keep the core bridge on `app.callServerTool()` / `ontoolresult` for portability.

### File Parameter Inputs (ChatGPT Extension)

For tools that accept user-uploaded files, ChatGPT requires a specific input schema shape and a `_meta.openai/fileParams` declaration:

```typescript
registerAppTool(
server,
"analyze-image",
{
title: "Analyze Image",
description: "Analyze an uploaded image",
inputSchema: {
imageFile: z.object({
download_url: z.string(),
file_id: z.string(),
}),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
_meta: {
ui: { resourceUri },
"openai/fileParams": ["imageFile"],
},
},
async ({ imageFile }) => { /* handler */ }
);
```

Files are objects with `download_url` and `file_id` fields only. Nested file structures are not supported. This is a ChatGPT-specific extension and will be ignored by Claude.
Copy link
Member

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).

Comment on lines +535 to +546
### ChatGPT Compliance Checklist

Use this checklist when preparing an MCP App for ChatGPT submission:

- [ ] **Tool annotations** -- every tool has `annotations: { readOnlyHint, destructiveHint, openWorldHint }`
- [ ] **`structuredContent`** -- tool handlers return `structuredContent` alongside `content`
- [ ] **Widget CSP** -- resource contents include `_meta.ui.csp` with `connectDomains` and `resourceDomains`
- [ ] **Widget domain** -- resource contents include `_meta.ui.domain` with a unique HTTPS URL
- [ ] **HTTPS transport** -- server is accessible over HTTPS (use a tunnelling service for local dev)
- [ ] **Widget parser** -- client-side result parsing checks `structuredContent` first, falls back to `content` text
- [ ] **No secrets in responses** -- `structuredContent`, `content`, and `_meta` must not contain API keys or tokens
- [ ] **File params** (if applicable) -- file inputs use `z.object({ download_url, file_id })` with `_meta["openai/fileParams"]`
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants