Skip to content

fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock#7487

Merged
aharvard merged 6 commits intomainfrom
fix/call-tool-response-camelcase
Mar 11, 2026
Merged

fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock#7487
aharvard merged 6 commits intomainfrom
fix/call-tool-response-camelcase

Conversation

@aharvard
Copy link
Copy Markdown
Collaborator

Summary

Fixes the CallToolResponse API to use camelCase field names matching the MCP spec, and adds proper type discriminator fields to content blocks so generated TypeScript types align with the SDK's CallToolResult.

Problem

  1. CallToolResponse serialized fields as snake_case (is_error, structured_content) but the MCP spec and SDK expect camelCase (isError, structuredContent)
  2. The Content type from rmcp includes a type discriminator on the wire (via #[serde(tag = "type")]) but utoipa can't reflect this for external types, so the generated OpenAPI schema and TypeScript types were missing it
  3. McpAppToolResult was a redundant intermediate type that duplicated CallToolResult

Changes

Rust Backend (agent.rs):

  • Added #[serde(rename_all = "camelCase")] to CallToolResponse
  • Changed content to Vec<Value> with schema-only ContentBlock types that include type discriminator literals ("text", "image", "resource", "audio", "resource_link")
  • Manual ToSchema impls to work around utoipa's inability to reflect rmcp's #[serde(tag = "type")]

Frontend:

  • Removed redundant McpAppToolResult type
  • McpAppRenderer now accepts CallToolResult directly — no conversion useMemo
  • ToolCallWithResponse uses ContentBlock (with proper type field) instead of old Content

Why schema-only types?

rmcp's schemars schema is correct (it reflects the type discriminator), but goose uses utoipa for OpenAPI generation. utoipa can't derive the right schema for rmcp's externally-defined tagged enum. The schema-only types exist solely to produce a correct OpenAPI spec — actual serialization goes through serde_json::Value.

Follows up on #7476

Copilot AI review requested due to automatic review settings February 24, 2026 19:08
@aharvard aharvard force-pushed the fix/call-tool-response-camelcase branch from 4baca4a to 74e2d8e Compare February 24, 2026 19:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the server and desktop UI API types to match the MCP wire spec by emitting camelCase fields for CallToolResponse and ensuring tool content blocks are OpenAPI-discriminated unions (with a literal type field), so generated TypeScript aligns with the SDK’s CallToolResult.

Changes:

  • Backend: serialize CallToolResponse in camelCase and introduce schema-only ContentBlock variants with type discriminators for OpenAPI/TS generation.
  • Frontend: remove the redundant McpAppToolResult and pass CallToolResult through to AppRenderer directly.
  • Regenerate OpenAPI + TypeScript client types to reflect the corrected response shape.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ui/desktop/src/components/ToolCallWithResponse.tsx Switches tool result handling to ContentBlock and adjusts embedded resource rendering.
ui/desktop/src/components/McpApps/types.ts Removes the redundant McpAppToolResult type.
ui/desktop/src/components/McpApps/McpAppRenderer.tsx Accepts CallToolResult directly and updates callTool response field names to camelCase.
ui/desktop/src/api/types.gen.ts Regenerated types: CallToolResponse camelCase + new ContentBlock discriminated union.
ui/desktop/src/api/index.ts Re-exports newly generated API types (including ContentBlock variants).
ui/desktop/openapi.json OpenAPI schema updated for camelCase fields and discriminated ContentBlock schemas.
crates/goose-server/src/routes/agent.rs Implements camelCase CallToolResponse + schema-only content block schemas and serializes content via Value.
crates/goose-server/src/openapi.rs Registers the new schema-only content block schemas for OpenAPI generation.

@aharvard aharvard marked this pull request as draft February 24, 2026 19:15
@aharvard aharvard force-pushed the fix/call-tool-response-camelcase branch from 74e2d8e to 00dfded Compare February 24, 2026 19:41
@aharvard aharvard marked this pull request as ready for review February 24, 2026 21:48
Copilot AI review requested due to automatic review settings February 24, 2026 21:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@DOsinga
Copy link
Copy Markdown
Collaborator

DOsinga commented Feb 25, 2026

Why schema-only types?

I am not sure about this. It looks complicated and since it constructs the types independently of the actual Rust code, only give us the illusion of type safety

@aharvard
Copy link
Copy Markdown
Collaborator Author

Why schema-only types?

I am not sure about this. It looks complicated and since it constructs the types independently of the actual Rust code, only give us the illusion of type safety

The goal is to get a discriminated union for TS, the current way RMCP approaches this lacks the type field to discriminate from. Do you think there something we can adjust in RMCP that makes both Rust and our generated types happier? cc @alexhancock

@jamadeo
Copy link
Copy Markdown
Collaborator

jamadeo commented Mar 10, 2026

@aharvard I pulled your branch and tried adding support to the existing schemars-to-utoipa macros we have in openapi.rs, and I think the result looks like what we want: #7792 (that's a PR targeting this branch)

Feel free to merge it into this if it accomplishes what you're doing. The types are defined slightly differently, but you still get a discriminated union at the end

@aharvard
Copy link
Copy Markdown
Collaborator Author

thanks @jamadeo, just merged in your PR (will work on cleaning up this PR next)

aharvard and others added 3 commits March 10, 2026 16:37
…rs to ContentBlock

- Add #[serde(rename_all = "camelCase")] to CallToolResponse so fields
  serialize as isError, structuredContent (matching MCP spec)
- Define schema-only ContentBlock types with proper 'type' discriminator
  fields (text, image, resource, audio, resource_link) for OpenAPI
- Serialize content via serde_json::Value to bypass utoipa's inability
  to reflect rmcp's #[serde(tag = "type")] on external types
- Replace old Content type with ContentBlock in ToolCallWithResponse
- Remove redundant McpAppToolResult type; use CallToolResult directly
  in McpAppRenderer

Co-authored-by: Goose <[email protected]>
Ai-assisted: true
@aharvard aharvard force-pushed the fix/call-tool-response-camelcase branch from b96683e to fe0dfd3 Compare March 10, 2026 20:46
@aharvard aharvard added this pull request to the merge queue Mar 11, 2026
Merged via the queue into main with commit 76e4966 Mar 11, 2026
19 of 21 checks passed
@aharvard aharvard deleted the fix/call-tool-response-camelcase branch March 11, 2026 00:45
lifeizhou-ap added a commit that referenced this pull request Mar 11, 2026
* main: (45 commits)
  fix: resolve {{ recipe_dir }} in nested sub-recipe paths during secret discovery (#7797)
  Add @DOsinga as CODEOWNER for documentation (#7799)
  feat: Add summarize tool for deterministic reads (#7054)
  fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
  feat: ACP providers for claude code and codex (#6605)
  chore(deps): bump express-rate-limit from 8.2.1 to 8.3.0 in /evals/open-model-gym/mcp-harness (#7703)
  feat(openai): capture reasoning summaries from responses API (#7375)
  Fix some dependencies (#7794)
  fix: improve keyring availability error detection (#7766)
  feat: add MiniMax provider with Anthropic-compatible API (#7640)
  feat: add Tensorix as a declarative provider (#7712)
  fix(security): remove insecure default secret from GOOSE_EXTERNAL_BACKEND (#7783)
  refactor: Convert Tanzu provider to declarative JSON config (#7124)
  replaces https://github.com/block/goose/pull/7340/changes (#7786)
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
  fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
  fix: pass OAuth scopes to DCR and extract granted_scopes from token response (#7571)
  fix: write to real file if config.yaml is symlink (#7669)
  fix: preserve pairings when stopping gateway (#7733)
  ...
lifeizhou-ap added a commit that referenced this pull request Mar 11, 2026
* main: (69 commits)
  fix: resolve {{ recipe_dir }} in nested sub-recipe paths during secret discovery (#7797)
  Add @DOsinga as CODEOWNER for documentation (#7799)
  feat: Add summarize tool for deterministic reads (#7054)
  fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
  feat: ACP providers for claude code and codex (#6605)
  chore(deps): bump express-rate-limit from 8.2.1 to 8.3.0 in /evals/open-model-gym/mcp-harness (#7703)
  feat(openai): capture reasoning summaries from responses API (#7375)
  Fix some dependencies (#7794)
  fix: improve keyring availability error detection (#7766)
  feat: add MiniMax provider with Anthropic-compatible API (#7640)
  feat: add Tensorix as a declarative provider (#7712)
  fix(security): remove insecure default secret from GOOSE_EXTERNAL_BACKEND (#7783)
  refactor: Convert Tanzu provider to declarative JSON config (#7124)
  replaces https://github.com/block/goose/pull/7340/changes (#7786)
  feat(summon): make skill supporting files individually loadable via load() (#7583)
  Keep toast open on failed extension (#7771)
  fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
  fix: pass OAuth scopes to DCR and extract granted_scopes from token response (#7571)
  fix: write to real file if config.yaml is symlink (#7669)
  fix: preserve pairings when stopping gateway (#7733)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants