---
phase: 42-add-outputschema-top-level-support
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/types/protocol.rs
  - src/server/typed_tool.rs
  - pmcp-macros/src/tool.rs
autonomous: true
requirements:
  - OS-01
  - OS-02
  - OS-03
  - OS-04

must_haves:
  truths:
    - "ToolInfo serializes outputSchema as a top-level sibling to inputSchema"
    - "ToolAnnotations no longer contains output_schema field or with_output_schema method"
    - "TypedToolWithOutput::metadata() sets output_schema on ToolInfo directly"
    - "#[tool(output_type)] macro emits top-level output_schema on ToolInfo"
    - "pmcp:outputTypeName remains in annotations as PMCP codegen extension"
  artifacts:
    - path: "src/types/protocol.rs"
      provides: "ToolInfo.output_schema field, ToolInfo::with_output_schema() builder, ToolAnnotations::with_output_type_name()"
      contains: "pub output_schema: Option<Value>"
    - path: "src/server/typed_tool.rs"
      provides: "TypedToolWithOutput::metadata() rewired to top-level output_schema"
      contains: "output_schema: self.output_schema.clone()"
    - path: "pmcp-macros/src/tool.rs"
      provides: "Macro codegen emitting top-level output_schema"
      contains: "with_output_schema"
  key_links:
    - from: "src/server/typed_tool.rs"
      to: "src/types/protocol.rs"
      via: "TypedToolWithOutput::metadata() sets ToolInfo.output_schema"
      pattern: "output_schema: self\\.output_schema"
    - from: "pmcp-macros/src/tool.rs"
      to: "src/types/protocol.rs"
      via: "Macro emits .with_output_schema() on ToolInfo"
      pattern: "with_output_schema"
---

<objective>
Migrate output_schema from ToolAnnotations to a top-level field on ToolInfo, aligning with MCP spec 2025-06-18. Remove output_schema field and with_output_schema() from ToolAnnotations. Add with_output_type_name() to ToolAnnotations. Update TypedToolWithOutput::metadata() and macro codegen to target top-level field. Fix all ToolInfo struct literal sites to compile.

Purpose: Align PMCP SDK with MCP specification 2025-06-18 which defines outputSchema as a top-level Tool field.
Output: Compiling codebase with output_schema on ToolInfo, removed from ToolAnnotations.
</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/42-add-outputschema-top-level-support/42-CONTEXT.md
@.planning/phases/42-add-outputschema-top-level-support/42-RESEARCH.md

<interfaces>
<!-- Current ToolAnnotations (to be modified) -->
From src/types/protocol.rs:
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
    pub title: Option<String>,
    pub read_only_hint: Option<bool>,
    pub destructive_hint: Option<bool>,
    pub idempotent_hint: Option<bool>,
    pub open_world_hint: Option<bool>,
    // PMCP Extensions:
    #[serde(rename = "pmcp:outputSchema", skip_serializing_if = "Option::is_none")]
    pub output_schema: Option<Value>,        // REMOVE
    #[serde(rename = "pmcp:outputTypeName", skip_serializing_if = "Option::is_none")]
    pub output_type_name: Option<String>,    // KEEP
}

// REMOVE this method:
pub fn with_output_schema(mut self, schema: Value, type_name: impl Into<String>) -> Self

// ADD this method:
pub fn with_output_type_name(mut self, name: impl Into<String>) -> Self
```

<!-- Current ToolInfo (to be modified) -->
From src/types/protocol.rs:
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct ToolInfo {
    pub name: String,
    pub description: Option<String>,
    pub input_schema: Value,
    // ADD: pub output_schema: Option<Value>,  (after input_schema)
    pub annotations: Option<ToolAnnotations>,
    pub _meta: Option<serde_json::Map<String, Value>>,
    pub execution: Option<Value>,
}
```

<!-- ToolInfo constructors that need output_schema: None -->
- ToolInfo::new() at line 265
- ToolInfo::with_annotations() at line 306
- ToolInfo::with_ui() at line 327

<!-- ToolInfo struct literals in typed_tool.rs -->
- TypedTool::metadata() at line 230
- TypedSyncTool::metadata() at line 392
- TypedToolWithOutput::metadata() at line 711

<!-- ToolInfo struct literals in other files -->
- wasm_server.rs line 429
- wasm_typed_tool.rs lines 108, 203
- workflow/prompt_handler.rs: ~15 sites in test code (lines 1065, 1248, 1355, 1450, 1565, 1573, 1710, 1778, 1850, 1945, 2074, 2220, 2338)
- workflow/conversion.rs: line 145 (different struct -- workflow::conversion::ToolInfo, NOT protocol::ToolInfo)
- server/mod.rs: lines 436, 2348 (workflow::conversion::ToolInfo, not protocol::ToolInfo)
- server/builder.rs: line 677 (workflow::conversion::ToolInfo, not protocol::ToolInfo)
- protocol.rs test code: lines 2032, 2047

<!-- TypedToolWithOutput has_annotations check at line 705-709 -->
From src/server/typed_tool.rs:
```rust
let has_annotations = annotations.read_only_hint.is_some()
    || annotations.destructive_hint.is_some()
    || annotations.idempotent_hint.is_some()
    || annotations.open_world_hint.is_some()
    || annotations.output_schema.is_some();  // CHANGE to output_type_name
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Migrate output_schema from ToolAnnotations to ToolInfo</name>
  <files>src/types/protocol.rs</files>
  <action>
1. In `ToolAnnotations` struct (~line 97):
   - REMOVE the `output_schema: Option<Value>` field and its `#[serde(rename = "pmcp:outputSchema"...)]` attribute
   - KEEP `output_type_name: Option<String>` with its existing serde rename

2. In `ToolAnnotations` impl:
   - REMOVE `with_output_schema()` method entirely (the one taking schema + type_name)
   - ADD `with_output_type_name()` method:
     ```rust
     pub fn with_output_type_name(mut self, name: impl Into<String>) -> Self {
         self.output_type_name = Some(name.into());
         self
     }
     ```
   - Update any doc comments that reference `with_output_schema` to reference `with_output_type_name`

3. In `ToolInfo` struct (~line 239):
   - ADD field after `input_schema`:
     ```rust
     /// JSON Schema for the tool's output type (MCP spec 2025-06-18).
     #[serde(skip_serializing_if = "Option::is_none")]
     pub output_schema: Option<Value>,
     ```
   - The existing `#[serde(rename_all = "camelCase")]` on ToolInfo handles `output_schema` -> `outputSchema` automatically

4. In `ToolInfo` impl:
   - In `new()` constructor: add `output_schema: None` to struct literal
   - In `with_annotations()` constructor: add `output_schema: None` to struct literal
   - In `with_ui()` constructor: add `output_schema: None` to struct literal
   - ADD builder method:
     ```rust
     /// Set the output schema for this tool (MCP spec 2025-06-18).
     ///
     /// The output schema declares the JSON Schema that the tool's structured
     /// output conforms to, enabling clients to validate and type-check results.
     pub fn with_output_schema(mut self, schema: serde_json::Value) -> Self {
         self.output_schema = Some(schema);
         self
     }
     ```
   - Update `with_annotations()` doc comment example to not show `with_output_schema` on annotations

5. In protocol.rs test code (struct literals around lines 2032, 2047): these use `..tool` spread from `ToolInfo::new()` so they should compile automatically once `new()` is updated.

6. Update any doc examples in protocol.rs that reference `annotations.with_output_schema(...)` to use the new top-level pattern.
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo check --lib 2>&1 | head -50</automated>
  </verify>
  <done>ToolAnnotations has no output_schema field or with_output_schema method. ToolInfo has output_schema field and with_output_schema builder. ToolAnnotations has with_output_type_name method. All ToolInfo constructors set output_schema: None.</done>
</task>

<task type="auto">
  <name>Task 2: Rewire TypedToolWithOutput, all struct literal sites, and macro codegen</name>
  <files>src/server/typed_tool.rs, src/server/wasm_server.rs, src/server/wasm_typed_tool.rs, pmcp-macros/src/tool.rs</files>
  <action>
1. In `src/server/typed_tool.rs`:
   - `TypedTool::metadata()` (~line 230): add `output_schema: None` to the ToolInfo struct literal
   - `TypedSyncTool::metadata()` (~line 392): add `output_schema: None` to the ToolInfo struct literal
   - `TypedToolWithOutput::metadata()` (~line 693-720): Rewire completely:
     - Change annotation building: instead of `annotations.with_output_schema(schema, type_name)`, use `annotations.with_output_type_name(type_name)` (only puts type name in annotations)
     - Update `has_annotations` check (~line 705-709): replace `annotations.output_schema.is_some()` with `annotations.output_type_name.is_some()`
     - In ToolInfo struct literal: add `output_schema: self.output_schema.clone()` as top-level field
   - Update the test `test_typed_tool_with_output_with_ui_and_output_schema_coexist` (~line 829): change assertions from checking `annotations.output_schema` to checking `tool_info.output_schema` at top level. Also verify `annotations.output_type_name` is present.

2. In `src/server/wasm_server.rs`:
   - `info()` method (~line 429): add `output_schema: None` to ToolInfo struct literal

3. In `src/server/wasm_typed_tool.rs`:
   - `WasmTypedTool::info()` (~line 108): add `output_schema: None` to ToolInfo struct literal
   - `WasmTypedToolWithOutput::info()` (~line 203): add `output_schema: None` to ToolInfo struct literal (or set it from self if it has output_schema)

4. In `pmcp-macros/src/tool.rs`:
   - In `generate_definition_code()` (~line 231-303):
   - The current code for when `output_type` is specified builds annotation chain with `.with_output_schema(Self::output_schema(), #output_type_name)` and then wraps in `ToolInfo::with_annotations()`
   - Change to: build annotation chain with `.with_output_type_name(#output_type_name)` instead
   - Then chain `.with_output_schema(Self::output_schema())` on the resulting ToolInfo
   - Both cfg paths (schema-generation and non-schema-generation) need updating
   - The `output_schema()` function generation stays the same -- only the call site where it's injected into ToolInfo vs annotations changes

5. NOTE: `workflow/conversion::ToolInfo` is a SEPARATE struct from `protocol::ToolInfo`. It has only {name, description, input_schema} and does NOT need output_schema. The sites in server/mod.rs, server/builder.rs, and workflow/prompt_handler.rs that construct `workflow::conversion::ToolInfo` do NOT need changes (they're a different type).
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo check --workspace 2>&1 | head -80</automated>
  </verify>
  <done>Full workspace compiles with zero errors. TypedToolWithOutput sets output_schema on ToolInfo top-level. Macro emits top-level output_schema. All ToolInfo struct literal sites have output_schema field. pmcp:outputTypeName remains in annotations.</done>
</task>

</tasks>

<verification>
```bash
# Full workspace compile check
cargo check --workspace

# Run existing tests to verify nothing broke
cargo test --workspace -- --test-threads=1 2>&1 | tail -20

# Verify JSON serialization: outputSchema is top-level sibling to inputSchema
cargo test tool_annotations_test -- --test-threads=1 2>&1 | tail -20
```
</verification>

<success_criteria>
- `cargo check --workspace` passes with zero errors
- `cargo test --workspace` passes (tests from plan 02 may fail until updated -- that's expected)
- `output_schema` field exists on `ToolInfo`, not on `ToolAnnotations`
- `with_output_schema()` is a method on `ToolInfo`, not on `ToolAnnotations`
- `with_output_type_name()` exists on `ToolAnnotations`
- Macro codegen emits `.with_output_schema()` on ToolInfo, `.with_output_type_name()` on annotations
</success_criteria>

<output>
After completion, create `.planning/phases/42-add-outputschema-top-level-support/42-01-SUMMARY.md`
</output>
