---
phase: 40-review-chatgpt-compatibility-for-apps
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/types/ui.rs
autonomous: true
requirements:
  - COMPAT-01

must_haves:
  truths:
    - "build_meta_map() emits nested ui.resourceUri, legacy flat ui/resourceUri, and openai/outputTemplate"
    - "from_metadata() reads both nested and legacy flat formats (already works, no regression)"
    - "Existing test updated to assert flat key IS present (was asserting absence)"
  artifacts:
    - path: "src/types/ui.rs"
      provides: "Legacy flat key emission in build_meta_map()"
      contains: "ui/resourceUri"
  key_links:
    - from: "ToolUIMetadata::build_meta_map()"
      to: "ToolInfo::with_ui()"
      via: "direct call"
      pattern: "build_meta_map"
    - from: "ToolUIMetadata::build_meta_map()"
      to: "build_ui_meta()"
      via: "direct call"
      pattern: "build_meta_map"
---

<objective>
Add the legacy flat key `ui/resourceUri` to `ToolUIMetadata::build_meta_map()` to match the official ext-apps reference implementation's dual-emit behavior.

Purpose: The official `@modelcontextprotocol/ext-apps` `registerAppTool` emits BOTH `_meta.ui.resourceUri` (nested) AND `_meta["ui/resourceUri"]` (legacy flat key). Our SDK only emits the nested format. Adding the flat key ensures backward compatibility with older MCP hosts that read the legacy format.

Output: Updated `build_meta_map()` emitting 3 keys; updated test asserting all 3 keys present.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@src/types/ui.rs

<interfaces>
<!-- Key function the executor modifies -->

From src/types/ui.rs:
```rust
/// Build a `serde_json::Map` with the standard UI resource `_meta` keys.
pub fn build_meta_map(uri: &str) -> serde_json::Map<String, serde_json::Value> {
    let mut meta = serde_json::Map::with_capacity(2);
    let mut ui_obj = serde_json::Map::with_capacity(1);
    ui_obj.insert("resourceUri".to_string(), serde_json::Value::String(uri.to_string()));
    meta.insert("ui".to_string(), serde_json::Value::Object(ui_obj));
    meta.insert("openai/outputTemplate".to_string(), serde_json::Value::String(uri.to_string()));
    meta
}

/// Used by all four typed tool variants and ToolInfo::with_ui()
pub(crate) fn build_ui_meta(ui_resource_uri: Option<&str>) -> Option<serde_json::Map<String, serde_json::Value>> {
    let uri = ui_resource_uri?;
    Some(ToolUIMetadata::build_meta_map(uri))
}
```

Existing test at line 482 (`test_tool_ui_metadata_to_nested_format`) asserts:
```rust
assert!(map.get("ui/resourceUri").is_none(), "must not have flat ui/resourceUri key");
```
This assertion must be INVERTED.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add legacy flat key to build_meta_map and update tests</name>
  <files>src/types/ui.rs</files>
  <behavior>
    - Test: build_meta_map("ui://test") returns map with key "ui/resourceUri" = "ui://test"
    - Test: build_meta_map("ui://test") returns map with key "ui" containing {"resourceUri": "ui://test"}
    - Test: build_meta_map("ui://test") returns map with key "openai/outputTemplate" = "ui://test"
    - Test: build_meta_map returns map with capacity 3 (3 top-level keys)
    - Test: to_metadata() on ToolUIMetadata with uri emits all 3 keys
    - Test: deep_merge after with_ui() preserves the flat key (chain with_ui then with_meta_entry for ui.prefersBorder, verify ui/resourceUri still present)
  </behavior>
  <action>
1. In `ToolUIMetadata::build_meta_map()` (line 343):
   - Change `with_capacity(2)` to `with_capacity(3)`
   - Add `meta.insert("ui/resourceUri".to_string(), serde_json::Value::String(uri.to_string()));` AFTER the nested ui object insert and BEFORE the openai/outputTemplate insert
   - Update the doc comment to mention the legacy flat key

2. Update the existing test `test_tool_ui_metadata_to_nested_format` (line 482):
   - Change the assertion from `assert!(map.get("ui/resourceUri").is_none(), ...)` to `assert_eq!(map.get("ui/resourceUri"), Some(&serde_json::Value::String("ui://test".to_string())))`

3. Add a new test `test_build_meta_map_emits_all_three_keys`:
   - Call `ToolUIMetadata::build_meta_map("ui://chess/board")`
   - Assert nested `ui.resourceUri` = "ui://chess/board"
   - Assert flat `ui/resourceUri` = "ui://chess/board"
   - Assert `openai/outputTemplate` = "ui://chess/board"
   - Assert map has exactly 3 entries

4. Add a new test `test_deep_merge_preserves_flat_key`:
   - Create a map via `build_meta_map("ui://x")`
   - Deep merge with `{"ui": {"prefersBorder": true}}`
   - Assert `ui/resourceUri` still present (flat key unaffected by deep merge on "ui" object)
   - Assert `ui.resourceUri` still present
   - Assert `ui.prefersBorder` is true
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo test --features mcp-apps types::ui::tests -- --test-threads=1 2>&1 | tail -20</automated>
  </verify>
  <done>build_meta_map() emits 3 keys (nested ui.resourceUri, flat ui/resourceUri, openai/outputTemplate). All tests pass including updated assertion and new tests.</done>
</task>

</tasks>

<verification>
- `cargo test --features mcp-apps types::ui::tests` passes with zero failures
- `cargo clippy --features mcp-apps -- -D warnings` reports zero warnings on ui.rs
- `cargo doc --features mcp-apps --no-deps` builds without warnings
</verification>

<success_criteria>
- build_meta_map() produces exactly 3 top-level keys: "ui", "ui/resourceUri", "openai/outputTemplate"
- All existing tests pass (from_metadata reads both formats correctly)
- New tests verify the legacy flat key presence and deep_merge interaction
</success_criteria>

<output>
After completion, create `.planning/phases/40-review-chatgpt-compatibility-for-apps/40-01-SUMMARY.md`
</output>
