---
phase: 39-add-deep-merge-for-ui-meta-key-to-prevent-collision
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/types/ui.rs
  - src/types/protocol.rs
autonomous: true
requirements:
  - MERGE-01
must_haves:
  truths:
    - "deep_merge recursively merges nested JSON objects in-place"
    - "Arrays are replaced entirely by overlay, not concatenated"
    - "Last-in wins at leaf level with tracing::debug on collision"
    - "ToolInfo::with_meta_entry adds a single key composably via deep merge"
  artifacts:
    - path: "src/types/ui.rs"
      provides: "deep_merge standalone function"
      contains: "pub fn deep_merge"
    - path: "src/types/protocol.rs"
      provides: "ToolInfo::with_meta_entry builder method"
      contains: "fn with_meta_entry"
  key_links:
    - from: "src/types/protocol.rs"
      to: "src/types/ui.rs"
      via: "with_meta_entry calls deep_merge"
      pattern: "deep_merge"
---

<objective>
Add the deep_merge function for serde_json::Map and ToolInfo::with_meta_entry builder method.

Purpose: Provide the foundational merge primitive that prevents _meta collision when multiple builder methods contribute keys. This plan creates the contracts and utilities that Plan 02 will use to update all metadata() implementations.

Output: deep_merge() in ui.rs, with_meta_entry() on ToolInfo, comprehensive unit tests for both.
</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/39-add-deep-merge-for-ui-meta-key-to-prevent-collision/39-CONTEXT.md
@.planning/phases/39-add-deep-merge-for-ui-meta-key-to-prevent-collision/39-RESEARCH.md

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

From src/types/ui.rs:
```rust
pub fn build_meta_map(uri: &str) -> serde_json::Map<String, serde_json::Value> {
    // Builds {"ui": {"resourceUri": "<uri>"}, "openai/outputTemplate": "<uri>"}
}
```

From src/types/protocol.rs:
```rust
pub struct ToolInfo {
    pub name: String,
    pub description: Option<String>,
    pub input_schema: Value,
    pub annotations: Option<ToolAnnotations>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _meta: Option<serde_json::Map<String, Value>>,
    pub execution: Option<Value>,
}

// Existing replace-all method (keep as-is, do NOT modify):
pub fn with_meta(mut self, meta: serde_json::Map<String, Value>) -> Self {
    self._meta = Some(meta);
    self
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add deep_merge function with unit tests</name>
  <files>src/types/ui.rs</files>
  <behavior>
    - deep_merge with disjoint keys: base={a:1}, overlay={b:2} -> {a:1, b:2}
    - deep_merge with nested objects: base={ui:{resourceUri:"x"}}, overlay={ui:{prefersBorder:true}} -> {ui:{resourceUri:"x", prefersBorder:true}}
    - deep_merge leaf collision last-in wins: base={key:"old"}, overlay={key:"new"} -> {key:"new"}
    - deep_merge array replaced not concatenated: base={tags:["a","b"]}, overlay={tags:["c"]} -> {tags:["c"]}
    - deep_merge empty overlay: base={a:1}, overlay={} -> {a:1} (no change)
    - deep_merge empty base: base={}, overlay={b:2} -> {b:2}
    - deep_merge deeply nested (3 levels): base={a:{b:{c:1}}}, overlay={a:{b:{d:2}}} -> {a:{b:{c:1, d:2}}}
  </behavior>
  <action>
    Add `pub fn deep_merge(base: &mut serde_json::Map<String, serde_json::Value>, overlay: serde_json::Map<String, serde_json::Value>)` to src/types/ui.rs, placed after `build_meta_map`.

    Implementation per research:
    - Iterate overlay entries
    - If both base[key] and overlay_value are Object: recurse into the nested objects
    - If base has the key but types differ (or non-object): `tracing::debug!(key = %key, "deep_merge: overwriting existing _meta key")` then insert (last-in wins)
    - If base doesn't have the key: just insert

    Write tests in the existing `#[cfg(test)] mod tests` block at the bottom of ui.rs. Use `serde_json::json!` macro for concise test data.
  </action>
  <verify>
    <automated>cargo test deep_merge -p pmcp --lib</automated>
  </verify>
  <done>deep_merge function exists, is pub, handles all 7 test cases: disjoint keys, nested object merge, leaf collision, array replacement, empty overlay, empty base, 3-level deep nesting</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add ToolInfo::with_meta_entry builder method with tests</name>
  <files>src/types/protocol.rs</files>
  <behavior>
    - with_meta_entry on empty _meta: ToolInfo with _meta=None, call with_meta_entry("ui", json!({...})) -> _meta contains the key
    - with_meta_entry merges with existing: ToolInfo with _meta containing "ui" key, call with_meta_entry("execution", json!({...})) -> both keys present
    - with_meta_entry deep merges nested objects: ToolInfo with _meta={ui:{resourceUri:"x"}}, call with_meta_entry("ui", json!({prefersBorder:true})) -> ui has both keys
    - with_meta_entry chained calls: .with_meta_entry("a", v1).with_meta_entry("b", v2) -> both present
    - Existing with_meta (replace-all) still works unchanged
  </behavior>
  <action>
    Add `pub fn with_meta_entry(mut self, key: impl Into<String>, value: serde_json::Value) -> Self` to the `impl ToolInfo` block in protocol.rs.

    Implementation:
    - `let meta = self._meta.get_or_insert_with(serde_json::Map::new);`
    - Create a single-entry overlay map: `let mut overlay = serde_json::Map::with_capacity(1); overlay.insert(key.into(), value);`
    - Call `crate::types::ui::deep_merge(meta, overlay);`
    - Return self

    Add `#[allow(clippy::used_underscore_binding)]` attribute (matching existing `with_meta` pattern).

    Add rustdoc with example showing composable usage:
    ```rust
    /// Add a single key-value pair to `_meta`, merging with existing entries.
    ///
    /// If the key already exists and both values are objects, they are
    /// deep-merged. Otherwise the new value replaces the old (last-in wins).
    ```

    Write tests in the existing `#[cfg(test)] mod tests` block in protocol.rs (or add one if none exists near ToolInfo). Keep existing `with_meta` tests untouched.
  </action>
  <verify>
    <automated>cargo test with_meta_entry -p pmcp --lib</automated>
  </verify>
  <done>ToolInfo::with_meta_entry exists, composes with deep_merge, existing with_meta (replace-all) is unchanged, all 5 test behaviors pass</done>
</task>

</tasks>

<verification>
- `cargo test deep_merge -p pmcp --lib` -- all deep_merge unit tests pass
- `cargo test with_meta_entry -p pmcp --lib` -- all with_meta_entry tests pass
- `cargo test tool_info_with_ui -p pmcp --lib` -- existing UI tests still pass (regression check)
- `cargo clippy -p pmcp -- -D warnings` -- zero warnings
</verification>

<success_criteria>
- deep_merge function is pub in src/types/ui.rs with full test coverage
- ToolInfo::with_meta_entry is pub in src/types/protocol.rs with full test coverage
- Existing ToolInfo::with_meta (replace-all) is untouched
- Zero clippy warnings, all existing tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/39-add-deep-merge-for-ui-meta-key-to-prevent-collision/39-01-SUMMARY.md`
</output>
