---
phase: 43-chatgpt-mcp-apps-alignment
plan: 02
type: execute
wave: 2
depends_on: ["43-01"]
files_modified:
  - src/server/core.rs
autonomous: true
requirements: []

must_haves:
  truths:
    - "resources/list response includes openai/* _meta keys on widget ResourceInfo items"
    - "resources/read response merges tool descriptor keys into content _meta alongside display keys"
    - "Non-widget resources are unaffected by _meta propagation"
  artifacts:
    - path: "src/server/core.rs"
      provides: "Post-processing in handle_list_resources and handle_read_resource"
      contains: "uri_to_tool_meta"
  key_links:
    - from: "src/server/core.rs handle_list_resources"
      to: "uri_to_tool_meta index"
      via: "post-process ResourceInfo items with tool _meta"
      pattern: "uri_to_tool_meta\\.get"
    - from: "src/server/core.rs handle_read_resource"
      to: "uri_to_tool_meta index + deep_merge"
      via: "merge descriptor keys into Content::Resource meta"
      pattern: "deep_merge"
---

<objective>
Add _meta propagation to resources/list and resources/read responses using the URI-to-tool-meta index built in Plan 01.

Purpose: Closes Gap 1 (resources/list _meta) and Gap 2 (resources/read _meta merging) so ChatGPT can discover widget metadata from any protocol entry point.
Output: ServerCore post-processes resource handler results to auto-populate _meta from linked tool metadata.
</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
@.planning/phases/43-chatgpt-mcp-apps-alignment/43-CONTEXT.md
@.planning/phases/43-chatgpt-mcp-apps-alignment/43-RESEARCH.md
@.planning/phases/43-chatgpt-mcp-apps-alignment/43-01-SUMMARY.md

@src/server/core.rs
@src/types/ui.rs

<interfaces>
<!-- From Plan 01 output -->

From src/server/core.rs (after Plan 01):
```rust
pub struct ServerCore {
    // ... existing fields ...
    uri_to_tool_meta: HashMap<String, serde_json::Map<String, serde_json::Value>>,
}
```

From src/types/protocol.rs (after Plan 01):
```rust
pub struct ResourceInfo {
    pub uri: String,
    pub name: String,
    pub description: Option<String>,
    pub mime_type: Option<String>,
    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
    pub meta: Option<serde_json::Map<String, serde_json::Value>>,
}

// Content::Resource variant has a meta field
pub enum Content {
    Resource {
        uri: String,
        text: Option<String>,
        mime_type: Option<String>,
        #[serde(rename = "_meta")]
        meta: Option<serde_json::Map<String, serde_json::Value>>,
    },
    // ...
}
```

From src/types/ui.rs:
```rust
pub fn deep_merge(
    base: &mut serde_json::Map<String, Value>,
    overlay: serde_json::Map<String, Value>,
);
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Post-process handle_list_resources to populate ResourceInfo._meta</name>
  <files>src/server/core.rs</files>
  <action>
Modify `handle_list_resources()` in ServerCore to post-process the result from the resource handler, populating `_meta` on ResourceInfo items that match entries in the `uri_to_tool_meta` index.

After `handler.list(req.cursor.clone(), extra).await` returns the result, iterate over `result.resources` and enrich matching items:

```rust
async fn handle_list_resources(
    &self,
    req: &ListResourcesParams,
    auth_context: Option<AuthContext>,
) -> Result<ListResourcesResult> {
    let mut result = match &self.resources {
        Some(handler) => {
            // ... existing code to create extra and call handler ...
            handler.list(req.cursor.clone(), extra).await?
        },
        None => ListResourcesResult {
            resources: vec![],
            next_cursor: None,
        },
    };

    // Enrich ResourceInfo items with tool _meta for widget resources
    for resource in &mut result.resources {
        if let Some(tool_meta) = self.uri_to_tool_meta.get(&resource.uri) {
            resource.meta = Some(tool_meta.clone());
        }
    }

    Ok(result)
}
```

Key points:
- Only widget resources (those with URIs in the index) get _meta -- non-widget resources are unaffected.
- The `uri_to_tool_meta` index was built from tool `_meta` at ServerCore construction (Plan 01), containing only `openai/*` prefixed keys per user decision.
- The `match` block needs to return `Result<ListResourcesResult>` before the enrichment loop, so unwrap the `?` inside the match arm and make `result` mutable.
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo check 2>&1 | tail -5</automated>
  </verify>
  <done>handle_list_resources post-processes results to populate ResourceInfo._meta from uri_to_tool_meta index. Non-widget resources are unaffected.</done>
</task>

<task type="auto">
  <name>Task 2: Post-process handle_read_resource to merge descriptor keys into content _meta</name>
  <files>src/server/core.rs</files>
  <action>
Modify `handle_read_resource()` in ServerCore to post-process the result, merging tool descriptor keys into Content::Resource _meta using `deep_merge`.

After `handler.read(&req.uri, extra).await` returns the result, iterate over `result.contents` and merge matching entries:

```rust
async fn handle_read_resource(
    &self,
    req: &ReadResourceParams,
    auth_context: Option<AuthContext>,
) -> Result<ReadResourceResult> {
    let handler = self.resources.as_ref().ok_or_else(|| {
        Error::internal(format!("Resource handler not available for '{}'", req.uri))
    })?;

    let request_id = format!("read_{}", req.uri);
    let extra = RequestHandlerExtra::new(
        request_id.clone(),
        self.cancellation_manager
            .create_token(request_id.clone())
            .await,
    )
    .with_auth_context(auth_context);

    let mut result = handler.read(&req.uri, extra).await?;

    // Merge tool descriptor keys into content _meta for widget resources.
    // Display keys (from ChatGptAdapter/WidgetMeta) are already in content meta.
    // Descriptor keys (openai/outputTemplate, openai/widgetAccessible, etc.) come
    // from the linked tool's _meta via the uri_to_tool_meta index.
    for content in &mut result.contents {
        if let Content::Resource { uri, meta, .. } = content {
            if let Some(tool_meta) = self.uri_to_tool_meta.get(uri.as_str()) {
                let content_meta = meta.get_or_insert_with(serde_json::Map::new);
                crate::types::ui::deep_merge(content_meta, tool_meta.clone());
            }
        }
    }

    Ok(result)
}
```

Key points:
- Uses `deep_merge` from `src/types/ui.rs` (Phase 39) to combine display keys with descriptor keys. Per user decision: merge happens at handler level, not in ChatGptAdapter.
- Only Content::Resource variants with URIs in the index get merged -- regular file resources are unaffected (Research Pitfall 5).
- The `get_or_insert_with` pattern handles the case where content meta is None (creates empty map, then merges into it).
- Import `crate::types::ui::deep_merge` if not already imported (check existing imports at top of core.rs).

After both tasks, run the full test suite to verify nothing broke.
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo test --test-threads=1 2>&1 | tail -20</automated>
  </verify>
  <done>handle_read_resource merges tool descriptor keys (openai/* from uri_to_tool_meta) into content _meta alongside existing display keys. Non-widget resources unaffected. All tests pass.</done>
</task>

</tasks>

<verification>
```bash
# Full test suite
cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo test --test-threads=1

# Clippy clean
cargo clippy --all-targets -- -D warnings

# Verify the 3 protocol methods have correct _meta behavior:
# - tools/list: unchanged (all _meta keys, handled by ToolInfo)
# - tools/call: filtered to openai/toolInvocation/* only (Plan 01)
# - resources/list: openai/* keys from linked tool (this plan, Task 1)
# - resources/read: merged display + descriptor keys (this plan, Task 2)
```
</verification>

<success_criteria>
- resources/list responses include openai/* _meta keys on widget ResourceInfo items (auto-populated from linked tool)
- resources/read responses include both display keys (widgetCSP, prefersBorder) AND descriptor keys (outputTemplate, widgetAccessible, invoking, invoked) via deep_merge
- Non-widget resources have no _meta added
- All existing tests pass
- Zero clippy warnings
</success_criteria>

<output>
After completion, create `.planning/phases/43-chatgpt-mcp-apps-alignment/43-02-SUMMARY.md`
</output>
