---
phase: 34-fix-mcp-apps-chatgpt-compatibility
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/types/protocol.rs
  - src/server/typed_tool.rs
  - src/types/ui.rs
  - src/types/mcp_apps.rs
autonomous: true
requirements:
  - CHATGPT-01
  - CHATGPT-02
  - CHATGPT-03
  - CHATGPT-04
  - CHATGPT-05

must_haves:
  truths:
    - "ToolInfo::with_ui() produces nested _meta.ui.resourceUri format (not flat ui/resourceUri)"
    - "ToolInfo::with_ui() includes openai/outputTemplate as ChatGPT alias"
    - "TypedTool::metadata() includes openai/outputTemplate alongside nested ui.resourceUri"
    - "UIMimeType and ExtendedUIMimeType accept text/html;profile=mcp-app"
    - "WidgetMeta emits nested ui.prefersBorder alongside flat openai/widgetPrefersBorder"
    - "ToolUIMetadata uses nested ui object format for serialization and deserialization"
  artifacts:
    - path: "src/types/protocol.rs"
      provides: "Fixed ToolInfo::with_ui() with nested _meta and openai/outputTemplate"
      contains: "openai/outputTemplate"
    - path: "src/server/typed_tool.rs"
      provides: "TypedTool::metadata() with openai/outputTemplate"
      contains: "openai/outputTemplate"
    - path: "src/types/ui.rs"
      provides: "HtmlMcpApp MIME variant and nested ToolUIMetadata"
      contains: "HtmlMcpApp"
    - path: "src/types/mcp_apps.rs"
      provides: "HtmlMcpApp variant in ExtendedUIMimeType and dual-emit WidgetMeta"
      contains: "HtmlMcpApp"
  key_links:
    - from: "src/types/protocol.rs"
      to: "src/server/typed_tool.rs"
      via: "Both produce identical _meta format for tools with UI"
      pattern: "openai/outputTemplate"
    - from: "src/types/ui.rs"
      to: "src/types/mcp_apps.rs"
      via: "Both MIME enums have matching HtmlMcpApp variant"
      pattern: "text/html;profile=mcp-app"
---

<objective>
Fix SDK metadata format and MIME types to be compatible with ChatGPT's MCP Apps implementation.

Purpose: ChatGPT requires nested `_meta.ui.resourceUri` format (not flat `ui/resourceUri`), the `openai/outputTemplate` alias key, and `text/html;profile=mcp-app` MIME type. Current SDK has inconsistent formats and missing keys that prevent ChatGPT from rendering MCP App widgets.

Output: All tool metadata paths produce consistent nested format with ChatGPT alias keys; MIME types accept the profile-based format.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/34-fix-mcp-apps-chatgpt-compatibility/34-CONTEXT.md
@.planning/phases/34-fix-mcp-apps-chatgpt-compatibility/34-RESEARCH.md

@src/types/protocol.rs
@src/server/typed_tool.rs
@src/types/ui.rs
@src/types/mcp_apps.rs

<interfaces>
<!-- Key types and contracts the executor needs -->

From src/types/protocol.rs (line 322-343):
```rust
pub fn with_ui(
    name: impl Into<String>,
    description: Option<String>,
    input_schema: Value,
    ui_resource_uri: impl Into<String>,
) -> Self {
    let mut meta = serde_json::Map::new();
    meta.insert("ui/resourceUri".to_string(), Value::String(ui_resource_uri.into()));
    // PROBLEM: flat key "ui/resourceUri" instead of nested {"ui": {"resourceUri": ...}}
    // MISSING: "openai/outputTemplate" key
}
```

From src/server/typed_tool.rs (line 228-244):
```rust
fn metadata(&self) -> Option<ToolInfo> {
    let meta = self.ui_resource_uri.as_ref().map(|uri| {
        let mut meta = serde_json::Map::new();
        meta.insert("ui".to_string(), serde_json::json!({ "resourceUri": uri }));
        // CORRECT: nested format
        // MISSING: "openai/outputTemplate" key
        meta
    });
}
```

From src/types/ui.rs (line 116-172):
```rust
pub enum UIMimeType {
    HtmlMcp,       // "text/html+mcp"
    HtmlSkybridge, // "text/html+skybridge"
    // MISSING: HtmlMcpApp for "text/html;profile=mcp-app"
}

pub struct ToolUIMetadata {
    #[serde(rename = "ui/resourceUri")]  // PROBLEM: flat key
    pub ui_resource_uri: Option<String>,
}
```

From src/types/mcp_apps.rs (line 624-711):
```rust
pub enum ExtendedUIMimeType {
    HtmlMcp,       // "text/html+mcp"
    HtmlSkybridge, // "text/html+skybridge"
    HtmlPlain,     // "text/html"
    UriList,       // "text/uri-list"
    RemoteDom,
    RemoteDomReact,
    // MISSING: HtmlMcpApp for "text/html;profile=mcp-app"
}

pub struct WidgetMeta {
    #[serde(rename = "openai/widgetPrefersBorder")]
    pub prefers_border: Option<bool>,
    // MISSING: dual-emit nested ui.prefersBorder alongside flat key
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Fix tool _meta format and add MIME type variants</name>
  <files>src/types/protocol.rs, src/types/ui.rs, src/types/mcp_apps.rs</files>
  <behavior>
    - Test: ToolInfo::with_ui("tool", None, schema, "ui://w/x.html") produces _meta with nested {"ui": {"resourceUri": "ui://w/x.html"}} (not flat "ui/resourceUri")
    - Test: ToolInfo::with_ui() also produces _meta with {"openai/outputTemplate": "ui://w/x.html"}
    - Test: UIMimeType::HtmlMcpApp.as_str() == "text/html;profile=mcp-app"
    - Test: UIMimeType::from_str("text/html;profile=mcp-app") == Ok(HtmlMcpApp)
    - Test: UIMimeType::HtmlMcpApp.is_chatgpt() == true (ChatGPT uses this format)
    - Test: ExtendedUIMimeType::HtmlMcpApp.as_str() == "text/html;profile=mcp-app"
    - Test: ExtendedUIMimeType::from_str("text/html;profile=mcp-app") == Ok(HtmlMcpApp)
    - Test: ExtendedUIMimeType::HtmlMcpApp.is_chatgpt() == true
    - Test: ToolUIMetadata serialization uses nested {"ui": {"resourceUri": ...}} format
    - Test: ToolUIMetadata::from_metadata() reads from both nested "ui" object and legacy flat "ui/resourceUri" key
  </behavior>
  <action>
1. In protocol.rs `ToolInfo::with_ui()`: Change flat `"ui/resourceUri"` insert to nested format. Add `"openai/outputTemplate"` as sibling key:
   ```rust
   let uri = ui_resource_uri.into();
   meta.insert("ui".to_string(), serde_json::json!({ "resourceUri": &uri }));
   meta.insert("openai/outputTemplate".to_string(), Value::String(uri));
   ```

2. In ui.rs `UIMimeType`: Add `HtmlMcpApp` variant with `as_str()` returning `"text/html;profile=mcp-app"`, `is_chatgpt()` returning true, `FromStr` accepting it. Keep HtmlSkybridge for backward compat.

3. In ui.rs `ToolUIMetadata`: Refactor to use nested format. Change `from_metadata()` to check both nested `"ui"` key (look for `resourceUri` inside) and legacy flat `"ui/resourceUri"` key. Change `to_metadata()` to emit nested format plus `"openai/outputTemplate"`.

4. In mcp_apps.rs `ExtendedUIMimeType`: Add `HtmlMcpApp` variant with matching `as_str()`, `is_chatgpt()` returning true, `FromStr` accepting it.

5. Update all existing doctests that reference `"ui/resourceUri"` flat key to use the new nested format.

6. Add unit tests for all new behaviors listed in the behavior block above.
  </action>
  <verify>
    <automated>cargo test -p pmcp --lib types::protocol --lib types::ui --lib types::mcp_apps -- --test-threads=1</automated>
  </verify>
  <done>ToolInfo::with_ui() produces nested _meta with both ui.resourceUri and openai/outputTemplate; both MIME enums have HtmlMcpApp variant; ToolUIMetadata uses nested format with backward-compatible parsing; all tests pass.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Fix TypedTool metadata and WidgetMeta dual-emit</name>
  <files>src/server/typed_tool.rs, src/types/mcp_apps.rs</files>
  <behavior>
    - Test: TypedTool with ui_resource_uri set produces _meta containing both {"ui": {"resourceUri": "..."}} AND {"openai/outputTemplate": "..."}
    - Test: WidgetMeta with prefers_border(true) produces both {"openai/widgetPrefersBorder": true} and nested {"ui": {"prefersBorder": true}} in to_meta_map()
    - Test: WidgetMeta with prefers_border(true).domain("x.com") produces ui.prefersBorder in nested ui object but domain stays as "openai/widgetDomain" only (domain is ChatGPT-specific)
    - Test: WidgetMeta with no fields set produces empty map (no spurious ui key)
  </behavior>
  <action>
1. In typed_tool.rs `TypedTool::metadata()`: Add `"openai/outputTemplate"` key alongside existing nested `"ui"` key:
   ```rust
   let meta = self.ui_resource_uri.as_ref().map(|uri| {
       let mut meta = serde_json::Map::new();
       meta.insert("ui".to_string(), serde_json::json!({ "resourceUri": uri }));
       meta.insert("openai/outputTemplate".to_string(), Value::String(uri.clone()));
       meta
   });
   ```

2. In mcp_apps.rs `WidgetMeta::to_meta_map()`: Override to emit BOTH formats for `prefersBorder` (the only field with an MCP standard equivalent). After the default serde serialization (which produces flat `openai/*` keys), also insert a nested `"ui"` object containing `prefersBorder` if it is set. ChatGPT-only fields (domain, csp, description) stay flat `openai/*` only.

   Implementation approach: Override `to_meta_map()` to do serde serialization first (gets flat keys), then if `prefers_border` is Some, insert/merge into a `"ui"` nested object:
   ```rust
   pub fn to_meta_map(&self) -> serde_json::Map<String, serde_json::Value> {
       let mut map = serde_json::to_value(self)
           .ok()
           .and_then(|v| v.as_object().cloned())
           .unwrap_or_default();
       // Dual-emit: add nested ui object for MCP standard fields
       if let Some(prefers) = self.prefers_border {
           let ui_obj = serde_json::json!({ "prefersBorder": prefers });
           map.insert("ui".to_string(), ui_obj);
       }
       map
   }
   ```

3. Add unit tests for all behaviors in the behavior block.
  </action>
  <verify>
    <automated>cargo test -p pmcp --lib server::typed_tool --lib types::mcp_apps -- --test-threads=1</automated>
  </verify>
  <done>TypedTool metadata includes openai/outputTemplate; WidgetMeta dual-emits prefersBorder in both flat openai/* and nested ui formats; all tests pass.</done>
</task>

</tasks>

<verification>
```bash
# Full pmcp crate test suite
cargo test -p pmcp --lib -- --test-threads=1

# Verify no clippy warnings
cargo clippy -p pmcp -- -D warnings

# Verify doctests
cargo test -p pmcp --doc -- --test-threads=1
```
</verification>

<success_criteria>
- ToolInfo::with_ui() produces `{"_meta": {"ui": {"resourceUri": "..."}, "openai/outputTemplate": "..."}}`
- TypedTool::metadata() produces identical _meta format to ToolInfo::with_ui()
- Both UIMimeType and ExtendedUIMimeType accept "text/html;profile=mcp-app"
- WidgetMeta.to_meta_map() produces both "openai/widgetPrefersBorder" and "ui.prefersBorder"
- ToolUIMetadata reads/writes nested format, backward-compatible with flat format
- All existing tests pass (no regressions)
- Zero clippy warnings
</success_criteria>

<output>
After completion, create `.planning/phases/34-fix-mcp-apps-chatgpt-compatibility/34-01-SUMMARY.md`
</output>
