---
phase: 38-cache-toolinfo-at-registration-to-avoid-per-request-cloning
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/server/builder.rs
  - src/server/core.rs
  - src/server/wasm_server.rs
autonomous: true
requirements:
  - CACHE-01

must_haves:
  truths:
    - "handle_list_tools returns cached ToolInfo without calling handler.metadata()"
    - "handle_call_tool uses cached ToolInfo for widget enrichment without calling handler.metadata()"
    - "handle_list_prompts returns cached PromptInfo without calling handler.metadata()"
    - "Task routing uses cached ToolInfo execution config without calling handler.metadata()"
    - "WasmMcpServer list_tools and list_prompts use cached info without calling tool.info()/prompt.info()"
    - "prompt_workflow() uses cached tool_infos instead of calling handler.metadata()"
  artifacts:
    - path: "src/server/builder.rs"
      provides: "tool_infos and prompt_infos cache fields in ServerCoreBuilder, populated at add_tool/add_prompt time"
      contains: "tool_infos"
    - path: "src/server/core.rs"
      provides: "tool_infos and prompt_infos fields in ServerCore, used in all 4 hot-path call sites"
      contains: "tool_infos"
    - path: "src/server/wasm_server.rs"
      provides: "tool_infos and prompt_infos fields in WasmMcpServerBuilder and WasmMcpServer"
      contains: "tool_infos"
  key_links:
    - from: "src/server/builder.rs"
      to: "src/server/core.rs"
      via: "build() passes tool_infos and prompt_infos to ServerCore::new()"
      pattern: "self\\.tool_infos"
    - from: "src/server/builder.rs tool()"
      to: "src/server/builder.rs tool_infos"
      via: "handler.metadata() called once at registration, result stored in tool_infos"
      pattern: "handler\\.metadata\\(\\)"
    - from: "src/server/core.rs handle_list_tools"
      to: "src/server/core.rs tool_infos"
      via: "self.tool_infos.values().cloned().collect() replaces per-handler iteration"
      pattern: "tool_infos\\.values\\(\\)"
---

<objective>
Cache ToolInfo and PromptInfo at registration time in ServerCoreBuilder and WasmMcpServerBuilder, then use cached metadata in all hot-path request handlers (handle_list_tools, handle_call_tool, handle_list_prompts, task routing) instead of calling handler.metadata() per request.

Purpose: Eliminate per-request metadata cloning (especially expensive input_schema deep-clones) on every list/call operation. With streamable HTTP causing frequent re-initialization, these are hot paths.

Output: Modified builder.rs, core.rs, and wasm_server.rs with cached metadata fields populated at registration and consumed in request handlers.
</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/38-cache-toolinfo-at-registration-to-avoid-per-request-cloning/38-CONTEXT.md
@.planning/phases/38-cache-toolinfo-at-registration-to-avoid-per-request-cloning/38-RESEARCH.md

@src/server/builder.rs
@src/server/core.rs
@src/server/wasm_server.rs

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

From src/server/builder.rs:
```rust
pub struct ServerCoreBuilder {
    tools: HashMap<String, Arc<dyn ToolHandler>>,
    prompts: HashMap<String, Arc<dyn PromptHandler>>,
    // ... other fields
}

pub fn tool(mut self, name: impl Into<String>, handler: impl ToolHandler + 'static) -> Self
pub fn tool_arc(mut self, name: impl Into<String>, handler: Arc<dyn ToolHandler>) -> Self
pub fn prompt(mut self, name: impl Into<String>, handler: impl PromptHandler + 'static) -> Self
pub fn prompt_arc(mut self, name: impl Into<String>, handler: Arc<dyn PromptHandler>) -> Self
pub fn prompt_workflow(mut self, workflow: SequentialWorkflow) -> Result<Self>
pub fn build(self) -> Result<ServerCore>
```

From src/server/core.rs:
```rust
pub struct ServerCore {
    tools: HashMap<String, Arc<dyn ToolHandler>>,
    prompts: HashMap<String, Arc<dyn PromptHandler>>,
    // ... other fields
}

pub fn new(
    info: Implementation,
    capabilities: ServerCapabilities,
    tools: HashMap<String, Arc<dyn ToolHandler>>,
    prompts: HashMap<String, Arc<dyn PromptHandler>>,
    // ... 8 more params
) -> Self
```

From src/server/wasm_server.rs:
```rust
pub struct WasmMcpServer {
    tools: HashMap<String, Box<dyn WasmTool>>,
    prompts: HashMap<String, Box<dyn WasmPrompt>>,
    // ... other fields
}

pub struct WasmMcpServerBuilder {
    tools: HashMap<String, Box<dyn WasmTool>>,
    prompts: HashMap<String, Box<dyn WasmPrompt>>,
    // ... other fields
}
```

From src/server/mod.rs (traits):
```rust
pub trait ToolHandler: Send + Sync {
    fn metadata(&self) -> Option<ToolInfo>;
    // ...
}
pub trait PromptHandler: Send + Sync {
    fn metadata(&self) -> Option<PromptInfo>;
    // ...
}
```

From src/types/protocol.rs:
```rust
pub struct ToolInfo { pub name: String, pub description: Option<String>, pub input_schema: Value, ... }
pub struct PromptInfo { pub name: String, pub description: Option<String>, pub arguments: Option<Vec<PromptArgument>>, ... }
```

From src/server/wasm_server.rs (traits):
```rust
pub trait WasmTool: Send + Sync { fn info(&self) -> ToolInfo; ... }
pub trait WasmPrompt: Send + Sync { fn info(&self) -> PromptInfo; ... }
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add cache fields to builders and populate at registration</name>
  <files>src/server/builder.rs, src/server/core.rs, src/server/wasm_server.rs</files>
  <action>
**ServerCoreBuilder (src/server/builder.rs):**

1. Add two new fields to the `ServerCoreBuilder` struct (after `prompts`):
   ```rust
   tool_infos: HashMap<String, ToolInfo>,
   prompt_infos: HashMap<String, PromptInfo>,
   ```
   Add necessary imports for `ToolInfo` and `PromptInfo` from `crate::types::protocol`.

2. Initialize both as `HashMap::new()` in `ServerCoreBuilder::new()`.

3. Update `tool()` method (line 130): After inserting the handler into `self.tools`, call `handler.metadata()` on the Arc, build a fallback `ToolInfo` if None, override `info.name` with the registered name, and insert into `self.tool_infos`:
   ```rust
   pub fn tool(mut self, name: impl Into<String>, handler: impl ToolHandler + 'static) -> Self {
       let name = name.into();
       let handler = Arc::new(handler) as Arc<dyn ToolHandler>;
       // Cache metadata at registration time
       let mut info = handler.metadata().unwrap_or_else(|| {
           ToolInfo::new(name.clone(), None, serde_json::json!({}))
       });
       info.name.clone_from(&name);
       self.tool_infos.insert(name.clone(), info);
       self.tools.insert(name, handler);
       // ... capability update unchanged
       self
   }
   ```

4. Update `tool_arc()` method (line 148): Same cache pattern -- call `handler.metadata()`, override name, insert into `tool_infos`.

5. Update `prompt()` method (line 165): Same pattern with `handler.metadata()` -> `PromptInfo`. Build fallback PromptInfo if None (name only, empty description, no arguments). Override `info.name`. Insert into `self.prompt_infos`.

6. Update `prompt_arc()` method (line 187): Same cache pattern for prompts.

7. Update `prompt_workflow()` method (line 621): Replace the tool registry loop (lines 636-647) that iterates `self.tools` and calls `handler.metadata()` with iteration over `self.tool_infos`:
   ```rust
   for (name, info) in &self.tool_infos {
       tool_registry.insert(
           Arc::from(name.as_str()),
           workflow::conversion::ToolInfo {
               name: info.name.clone(),
               description: info.description.clone().unwrap_or_default(),
               input_schema: info.input_schema.clone(),
           },
       );
   }
   ```
   Note: `prompt_workflow()` also calls `self.prompt()` to register the workflow prompt handler -- this will automatically populate `prompt_infos` via the updated `prompt()` method.

8. Update `build()` method (line 700): Pass `self.tool_infos` and `self.prompt_infos` to `ServerCore::new()` -- add them as new parameters after `prompts`.

**ServerCore (src/server/core.rs):**

9. Add two new fields to the `ServerCore` struct (after `prompts`):
   ```rust
   /// Cached tool metadata (populated at registration, immutable)
   tool_infos: HashMap<String, ToolInfo>,
   /// Cached prompt metadata (populated at registration, immutable)
   prompt_infos: HashMap<String, PromptInfo>,
   ```
   Add necessary imports for `ToolInfo` and `PromptInfo`.

10. Update `ServerCore::new()` (line 175): Add `tool_infos: HashMap<String, ToolInfo>` and `prompt_infos: HashMap<String, PromptInfo>` parameters after the `prompts` parameter. Store them in Self.

**WasmMcpServerBuilder and WasmMcpServer (src/server/wasm_server.rs):**

11. Add `tool_infos: HashMap<String, ToolInfo>` and `prompt_infos: HashMap<String, PromptInfo>` to `WasmMcpServerBuilder` struct. Initialize as `HashMap::new()` in `new()`.

12. Update `WasmMcpServerBuilder::tool()` (line 328): After inserting the tool, call `tool.info()` (WasmTool::info returns ToolInfo directly, not Option), insert into `self.tool_infos`. Note: must call `tool.info()` BEFORE `Box::new(tool)` moves ownership, so capture info first:
    ```rust
    pub fn tool<T: WasmTool + 'static>(mut self, name: impl Into<String>, tool: T) -> Self {
        let name = name.into();
        let info = tool.info();
        self.tool_infos.insert(name.clone(), info);
        self.tools.insert(name, Box::new(tool));
        self.capabilities.tools = Some(Default::default());
        self
    }
    ```

13. Update `WasmMcpServerBuilder::prompt()` (line 346): Same pattern -- call `prompt.info()` before boxing, insert into `self.prompt_infos`.

14. Add `tool_infos: HashMap<String, ToolInfo>` and `prompt_infos: HashMap<String, PromptInfo>` to `WasmMcpServer` struct.

15. Update `WasmMcpServerBuilder::build()` (line 353): Pass `self.tool_infos` and `self.prompt_infos` to WasmMcpServer.

**IMPORTANT:** The `wasm_typed_tool.rs` extension methods (`tool_typed`, `tool_typed_with_schema`) delegate to `self.tool(name, tool)`, so they automatically get caching. No changes needed there.
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo build 2>&1 | tail -5</automated>
  </verify>
  <done>All three structs (ServerCoreBuilder, ServerCore, WasmMcpServerBuilder, WasmMcpServer) have tool_infos and prompt_infos fields. All builder registration methods populate the cache. build() passes caches through to the server structs. Project compiles.</done>
</task>

<task type="auto">
  <name>Task 2: Replace all per-request metadata() calls with cache lookups</name>
  <files>src/server/core.rs, src/server/wasm_server.rs</files>
  <action>
**ServerCore (src/server/core.rs) -- 4 call sites:**

1. **handle_list_tools** (around line 245-259): Replace the entire `self.tools.iter().map(|(name, handler)| { ... handler.metadata() ... })` block with:
   ```rust
   let tools: Vec<ToolInfo> = self.tool_infos.values().cloned().collect();
   ```
   Remove the per-handler iteration, fallback ToolInfo creation, and name override -- all of that now happens at registration time.

2. **handle_call_tool widget enrichment** (around line 356-359): Replace:
   ```rust
   if let Some(info) = handler.metadata() {
       call_result = call_result.with_widget_enrichment(info, value);
   }
   ```
   with:
   ```rust
   if let Some(info) = self.tool_infos.get(&req.name) {
       call_result = call_result.with_widget_enrichment(info.clone(), value);
   }
   ```

3. **handle_list_prompts** (around line 365-405): Replace the entire `self.prompts.iter().map(...)` block with:
   ```rust
   let prompts: Vec<PromptInfo> = self.prompt_infos.values().cloned().collect();
   ```

4. **Task routing tool_requires_task** (around line 731-734): Replace:
   ```rust
   let tool_execution = self.tools.get(&req.name)
       .and_then(|h| h.metadata())
       .and_then(|m| m.execution);
   ```
   with:
   ```rust
   let tool_execution = self.tool_infos.get(&req.name)
       .and_then(|m| m.execution.clone());
   ```

**WasmMcpServer (src/server/wasm_server.rs) -- 2 call sites:**

5. **handle_list_tools** (around line 141-142): Replace:
   ```rust
   let tools: Vec<ToolInfo> = self.tools.values().map(|tool| tool.info()).collect();
   ```
   with:
   ```rust
   let tools: Vec<ToolInfo> = self.tool_infos.values().cloned().collect();
   ```

6. **handle_list_prompts** (around line 263-264): Replace:
   ```rust
   let prompts: Vec<PromptInfo> = self.prompts.values().map(|prompt| prompt.info()).collect();
   ```
   with:
   ```rust
   let prompts: Vec<PromptInfo> = self.prompt_infos.values().cloned().collect();
   ```

**IMPORTANT:** Do NOT add any fallback to handler.metadata() -- the cache is the source of truth. A cache miss means the tool was not registered.

**IMPORTANT:** Do NOT touch WasmMcpServer::handle_call_tool() -- it does NOT call tool.info(), only tool.execute(). No change needed there.

**Verification grep:** After changes, run `grep -n "handler.metadata()\|\.metadata()\|tool\.info()\|prompt\.info()" src/server/core.rs src/server/wasm_server.rs` -- there should be ZERO remaining per-request metadata() calls in hot paths. The only remaining calls should be in builder.rs registration methods (called once at startup).
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo build && cargo test --test builder_tests --test server_core_tests --test typed_tool_tests -- --test-threads=1 2>&1 | tail -20</automated>
  </verify>
  <done>All 6 per-request metadata() call sites replaced with cache lookups. No handler.metadata() or tool.info() calls remain in core.rs or wasm_server.rs hot paths. All existing tests pass. `cargo build` succeeds with zero warnings.</done>
</task>

</tasks>

<verification>
1. `cargo build` -- compiles with zero errors
2. `cargo clippy -- -D warnings` -- zero clippy warnings
3. `cargo test --test builder_tests -- --test-threads=1` -- builder tests pass (including prompt_workflow tests)
4. `cargo test --test typed_tool_tests -- --test-threads=1` -- typed tool tests pass
5. `cargo test -- --test-threads=1` -- full test suite passes
6. Grep verification: `grep -rn "handler\.metadata()\|\.metadata()" src/server/core.rs src/server/wasm_server.rs` shows zero matches in hot-path handler methods
</verification>

<success_criteria>
- ServerCoreBuilder and WasmMcpServerBuilder populate tool_infos/prompt_infos caches at registration
- ServerCore and WasmMcpServer store and use cached metadata in all request handlers
- Zero per-request handler.metadata() or tool.info() calls remain in core.rs and wasm_server.rs
- prompt_workflow() uses self.tool_infos instead of iterating handlers
- No trait signature changes (ToolHandler::metadata() stays fn metadata(&self) -> Option<ToolInfo>)
- All existing tests pass
- Zero clippy warnings
</success_criteria>

<output>
After completion, create `.planning/phases/38-cache-toolinfo-at-registration-to-avoid-per-request-cloning/38-01-SUMMARY.md`
</output>
