Skip to content

feat: Add Structured Tool Output Support #451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

davidleitw
Copy link
Contributor

@davidleitw davidleitw commented Jun 26, 2025

Description

This PR introduces support for structured tool outputs, a feature defined in the Model Context Protocol (MCP) specification.

The key changes include:

  • Adding an outputSchema field to the Tool definition to describe the structure of the tool's output.
  • Adding a structuredContent field to the CallToolResult to carry the structured data.
  • Implementing mandatory server-side validation for structuredContent against the tool's outputSchema.
  • Providing a series of WithOutput* helper functions for programmatic and type-safe schema definition.
  • Adding NewStructuredToolResult and NewStructuredToolError helpers for creating structured results with backward-compatible JSON text content.

This implementation covers the server-side requirements (MUST validate). The optional client-side validation (SHOULD validate) is left open for discussion.

Type of Change

  • New feature (non-breaking change that adds functionality)
  • MCP spec compatibility implementation
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Tests only (no functional changes)
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly

MCP Spec Compliance

  • This PR implements a feature defined in the MCP specification.
  • Link to relevant spec section: Tool Definition and tools/call Result
  • Implementation follows the specification exactly.

Additional Information

Breaking Change

The exported type mcp.ToolInputSchema has been renamed to mcp.ToolSchema to handle both input and output schemas uniformly.

Any code that directly depends on this type will need to be updated.
For example, mcp.ToolInputSchema{...} should be changed to mcp.ToolSchema{...}.
Code that creates tools using the With...() helper functions is not affected.

Design Decisions

  • Validation Logic Location: The validation logic (ValidateStructuredOutput) is implemented as a method on the Tool object. This aligns with the existing design pattern where the Tool object already contains complex logic (MarshalJSON, GetTitle).
  • JSON Schema Library: The github.com/santhosh-tekuri/jsonschema library was chosen for its performance, comprehensive spec support, and good standing in the Go ecosystem.
  • API Design: The new helper functions (NewStructuredToolResult) automatically serialize the structured content into a JSON string for the text field, ensuring backward compatibility as recommended by the MCP spec.

Client-side Implementation Considerations

Per the MCP spec, a client SHOULD validate the structured output it receives. This implementation is deferred to a future PR to discuss the best approach.

The primary challenge is that the client does not inherently have the outputSchema for a given tool. A potential solution is to have the client fetch all tool schemas via a ListTools call upon initialization (or on the first tool call) and cache them locally. Subsequent CallTool responses could then be validated against this cache.

This approach introduces several design questions:

  • Configuration: How should this validation be enabled? A ClientOption seems like a good approach.
  • Cache Strategy: When should the cache be populated and invalidated?
  • Error Handling: Should a validation failure result in a hard error or a loggable warning?

Feedback on this proposed direction and any alternative solutions would be appreciated.

Summary by CodeRabbit

  • New Features

    • Tools can now define and validate structured output using JSON schemas or Go struct types, with runtime validation of tool results.
    • Tool results may include structured content, ensuring outputs conform to declared schemas.
    • Output schemas are included in tool definitions and are accessible via the client.
  • Bug Fixes

    • Tool output validation errors are now reported as structured errors, improving error clarity for invalid outputs.
  • Tests

    • Extensive new tests for output schema validation, structured tool results, concurrency safety, and client/server schema serialization.

Copy link
Contributor

coderabbitai bot commented Jun 26, 2025

"""

Walkthrough

This change introduces structured tool output support by adding output schema definitions and validation to the tool system. It includes new fields for output schemas and structured content, schema generation from Go types, runtime validation, helper functions for structured results, and comprehensive tests for output schema enforcement and client/server behavior.

Changes

Files / Areas Change Summary
go.mod Added direct dependencies for JSON schema generation and validation; several indirect dependencies added.
mcp/tools.go, mcp/utils.go Added output schema fields and validation logic to Tool and CallToolResult; introduced helper functions for structured results; schema generation from Go types.
mcp/tools_test.go Added and updated tests for output schema presence, marshaling, structured result creation, and validation; deprecated old helpers.
server/server.go, server/server_test.go Enforced output schema validation in tool call handling; added tests for schema enforcement, error handling, and schema inclusion in tool definitions.
client/http_test.go Added test for retrieving tool output schema and verifying property descriptions via the client.

Possibly related issues

Suggested labels

type: enhancement, area: mcp spec

Suggested reviewers

  • pottekkat
  • rwjblue-glean
    """

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 440bc9e and f5cb4bd.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • client/http_test.go (3 hunks)
  • go.mod (1 hunks)
  • mcp/tools.go (9 hunks)
  • mcp/tools_test.go (1 hunks)
  • mcp/utils.go (3 hunks)
  • server/server.go (1 hunks)
  • server/server_test.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/server.go
  • client/http_test.go
  • go.mod
  • mcp/utils.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
server/server_test.go (7)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
mcp/tools.go (8)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: lariel-fernandes
PR: mark3labs/mcp-go#428
File: www/docs/pages/servers/prompts.mdx:218-234
Timestamp: 2025-06-20T20:39:51.870Z
Learning: In the mcp-go library, the GetPromptParams.Arguments field is of type map[string]string, not map[string]interface{}, so direct string access without type assertions is safe and correct.
mcp/tools_test.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
🔇 Additional comments (26)
server/server_test.go (7)

4-4: LGTM: Import addition is appropriate.

The bytes import is correctly added to support JSON pretty-printing in the new structured output validation tests.


2027-2101: LGTM: Comprehensive test for successful structured output validation.

This test thoroughly validates the happy path for structured output validation. It properly:

  • Defines a valid JSON schema for weather data
  • Returns structured content that matches the schema
  • Verifies the response is successful with IsError=false
  • Checks that structured content contains the expected values

The test structure and assertions are well-designed.


2103-2178: LGTM: Well-designed test for structured output validation failure.

This test effectively validates the error handling path when structured output doesn't match the defined schema. It properly:

  • Uses invalid structured content (missing required fields)
  • Verifies that IsError=true when validation fails
  • Checks that the error message indicates schema validation failure

The test complements the success case and ensures robust error handling.


2180-2261: LGTM: Excellent test for WithOutputType functionality.

This test comprehensively validates the new WithOutputType functionality:

  • Properly defines Go structs with jsonschema tags for descriptions
  • Includes helpful comment about comma limitations in jsonschema tags
  • Verifies that generated schemas contain the expected field descriptions
  • Tests successful validation with the NewToolResultStructured helper
  • Performs proper type assertions on the structured content

The test design is thorough and well-documented.


2263-2318: LGTM: Effective test for WithOutputType validation failure.

This test well demonstrates validation failure scenarios:

  • Uses two different struct types to simulate schema mismatch
  • Handler returns data missing required fields from the schema
  • Properly verifies IsError=true and checks for specific error messages
  • Validates that the error message contains "missing properties: "conditions""

The test design effectively demonstrates how validation catches schema mismatches.


2320-2374: LGTM: Critical test for MCP spec compliance.

This test validates an important MCP specification requirement that error responses skip validation:

  • Uses intentionally invalid structured content with IsError=true
  • Verifies that validation is skipped and the invalid content is preserved
  • Ensures the error message is not about validation failure
  • Checks that StructuredContent contains the original invalid data

This test is essential for ensuring MCP spec compliance.


2376-2419: LGTM: Thorough test for tool definition and schema storage.

This test effectively validates the tool registration process:

  • Verifies that WithOutputType generates and stores OutputSchema correctly
  • Checks the server's internal tool registry state
  • Validates that generated schemas contain descriptions from jsonschema struct tags
  • Uses JSON pretty-printing for helpful debugging output during test runs

The test ensures that the complete tool definition workflow functions properly.

mcp/tools_test.go (7)

715-731: LGTM: Comprehensive output schema detection tests.

The tests correctly verify both empty and defined schema scenarios for the HasOutputSchema method.


733-777: LGTM: Thorough JSON marshaling verification.

The test properly validates that output schemas are included in the JSON representation when defined, including verification of schema structure and required fields.


779-798: LGTM: Correct omission behavior test.

Properly verifies that tools without output schemas don't include the outputSchema field in JSON marshaling.


829-868: LGTM: Excellent struct-based schema generation test.

The test demonstrates the new generic WithOutputType[T]() API using a well-defined struct with jsonschema tags, and verifies the generated schema structure.


870-898: LGTM: Well-designed structured result test.

Tests the new NewToolResultStructured helper function thoroughly, including verification that JSON serialization matches the structured content.


1024-1074: LGTM: Excellent thread safety testing.

The concurrent access test with 100 goroutines properly verifies that the lazy validator initialization is thread-safe and that all goroutines see consistent results.


1124-1180: LGTM: Robust error consistency verification.

This test effectively validates that concurrent validator initialization with invalid schemas returns consistent error states across all goroutines, which is crucial for thread safety.

mcp/tools.go (12)

4-14: LGTM: Appropriate dependencies added.

The new imports for JSON schema generation (invopop/jsonschema) and validation (santhosh-tekuri/jsonschema) are correctly added to support the structured output functionality.


44-45: LGTM: Well-designed structured content field.

The StructuredContent field in CallToolResult is appropriately typed as any with omitempty to support flexible structured data while keeping JSON clean when not used.


478-483: LGTM: Thread-safe validator state design.

The validatorState struct with sync.Once ensures thread-safe lazy initialization of the JSON schema validator while storing any initialization errors for consistent error reporting.


493-494: LGTM: Clean output schema integration.

The OutputSchema field is properly integrated with omitempty for JSON marshaling, and the internal fields for validation state are correctly excluded from serialization with json:"-".

Also applies to: 500-503


525-557: LGTM: Robust thread-safe validator initialization.

The ensureOutputSchemaValidator method properly uses sync.Once to ensure single initialization while storing any compilation errors in the shared state for consistent error reporting across concurrent access.


601-604: LGTM: Conditional output schema marshaling.

The JSON marshaling correctly includes the outputSchema field only when it's defined, maintaining clean JSON structure for tools without output schemas.


618-632: LGTM: Consistent JSON marshaling pattern.

The ToolInputSchema marshaling follows the same pattern as the parent Tool struct, properly handling optional fields with appropriate type checks.


670-680: LGTM: Proper constructor initialization.

The NewTool constructor correctly initializes the new validatorState field immediately for thread safety, preventing potential nil pointer issues in concurrent access scenarios.


1181-1193: LGTM: Clean output schema configuration.

The WithOutputSchema function properly uses the ExtractMCPSchema utility to clean the schema and falls back gracefully if cleaning fails.


1199-1220: LGTM: Elegant generic schema generation.

The WithOutputType[T]() function provides a clean, type-safe API for defining output schemas using Go structs with jsonschema tags. The pre-compilation and caching of the validator is an excellent performance optimization.


1222-1261: LGTM: Comprehensive schema compilation.

The compileOutputSchema function properly handles the full pipeline: JSON schema generation, version override for compatibility, MCP cleaning, and validator compilation. The error handling is appropriate throughout.


1263-1305: LGTM: Robust validation implementation.

The ValidateOutput method correctly implements the MCP specification behavior:

  • Skips validation for error results (IsError true)
  • Skips validation when no schema is defined
  • Handles both structured and primitive data types
  • Provides clear error messages

The JSON marshaling/unmarshaling approach for struct conversion is appropriate for ensuring compatibility with the validator.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@davidleitw davidleitw closed this Jun 26, 2025
@davidleitw davidleitw reopened this Jun 26, 2025
@davidleitw
Copy link
Contributor Author

@coderabbitai review

Copy link
Contributor

coderabbitai bot commented Jun 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

coderabbitai bot commented Jun 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a CodeRabbit configuration file in the repository
ls -la | grep -E "\.coderabbit\.(yaml|yml)"

# If the file exists, show its contents
if [ -f ".coderabbit.yaml" ]; then
    echo "=== .coderabbit.yaml contents ==="
    cat .coderabbit.yaml
elif [ -f ".coderabbit.yml" ]; then
    echo "=== .coderabbit.yml contents ==="
    cat .coderabbit.yml
else
    echo "No CodeRabbit configuration file found in repository root"
fi

# Check the PR status to confirm it's a draft
gh pr view --json isDraft,title,number

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Check the current branch and repository context
echo "Current branch: $(git branch --show-current)"
echo "Repository remote:"
git remote -v | head -1

# Try to get PR information using the URL from the context
# Extract PR number from the URL pattern
echo "Checking PR #451 specifically"
gh pr view 451 --json isDraft,title,state 2>/dev/null || echo "Could not retrieve PR #451 information"

Length of output: 435


@davidleitw I can see you're working on PR #451 "feat: Add Structured Tool Output Support" which is currently in draft status. It looks like your message got cut off with just "@" at the end.

Here are your options to get a CodeRabbit review:

Option 1: Trigger a review right now (for this draft)
Simply use the command: @coderabbitai review

Option 2: Convert from draft to ready for review
This will automatically trigger a review since there's no custom configuration disabling it.

Option 3: Configure CodeRabbit to review drafts by default
Since there's no .coderabbit.yaml file in your repository, you can create one with:

# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
  review_status: false  # Disable status messages
  drafts: true          # Enable reviews for draft PRs

What would you like to do? If you want me to review the current changes, just reply with @coderabbitai review and I'll analyze the code changes in your draft PR.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
mcp/tools.go (4)

1219-1244: Add validation for existing schema type.

Same issue as WithOutputBoolean - needs to validate existing schema type.


1246-1271: Add validation for existing schema type.

Same issue as WithOutputBoolean - needs to validate existing schema type.


1273-1299: Add validation for existing schema type.

Same issue as WithOutputBoolean - needs to validate existing schema type.


1301-1326: Add validation for existing schema type.

Same issue as WithOutputBoolean - needs to validate existing schema type.

🧹 Nitpick comments (4)
mcp/utils.go (1)

356-406: Well-implemented structured content helpers with proper backwards compatibility.

These functions correctly implement the MCP protocol guideline that tools returning structured content should also provide functionally equivalent unstructured content. The error handling is robust and follows established patterns.

Consider improving the error message format for consistency:

-		jsonText = fmt.Sprintf("Error serializing structured content: %v", err)
+		jsonText = fmt.Sprintf("error serializing structured content: %v", err)

This follows Go convention of starting error messages with lowercase letters.

mcp/tools.go (3)

522-547: Consider making the validator key unique per tool.

Using a hardcoded key "output-schema-validator" could potentially cause conflicts if the compiler instance is shared across multiple tools.

Consider using a unique key per tool:

-	const validatorKey = "output-schema-validator"
+	validatorKey := fmt.Sprintf("output-schema-validator-%s", t.Name)

549-566: Enhance error message with schema details.

The error message could be more helpful by including the expected schema type.

 	if result.StructuredContent == nil {
-		return fmt.Errorf("tool %s has output schema but structuredContent is nil", t.Name)
+		return fmt.Errorf("tool %s expects output of type %s but structuredContent is nil", t.Name, t.OutputSchema.Type)
 	}

570-602: Consider adjusting the initial map capacity.

The map could potentially need up to 6 fields, but the current capacity might be slightly low for optimal performance.

This is a minor optimization - the current implementation is functionally correct.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f5b048 and 58ef0d3.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • go.mod (1 hunks)
  • mcp/tools.go (8 hunks)
  • mcp/tools_test.go (1 hunks)
  • mcp/types_test.go (1 hunks)
  • mcp/utils.go (1 hunks)
  • server/server.go (1 hunks)
  • server/server_test.go (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
server/server_test.go (3)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
mcp/tools.go (3)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
🧬 Code Graph Analysis (5)
server/server.go (2)
mcp/tools.go (1)
  • Tool (477-495)
mcp/utils.go (1)
  • NewToolResultErrorFromErr (326-339)
mcp/utils.go (2)
mcp/tools.go (1)
  • CallToolResult (39-48)
mcp/types.go (3)
  • Content (827-829)
  • TextContent (833-838)
  • TextContent (840-840)
mcp/types_test.go (1)
mcp/tools.go (6)
  • Tool (477-495)
  • Title (787-791)
  • Description (771-775)
  • ToolSchema (604-608)
  • Properties (1013-1017)
  • ToolAnnotation (627-638)
mcp/tools_test.go (3)
mcp/tools.go (19)
  • NewTool (655-683)
  • WithOutputString (1248-1271)
  • Required (779-783)
  • WithString (947-965)
  • Description (771-775)
  • DefaultString (799-803)
  • Properties (1013-1017)
  • WithOutputNumber (1221-1244)
  • Min (859-863)
  • Max (851-855)
  • DefaultNumber (843-847)
  • WithOutputBoolean (1194-1217)
  • DefaultBool (879-883)
  • WithOutputArray (1303-1326)
  • WithStringItems (1101-1113)
  • WithNumberItems (1141-1153)
  • WithOutputObject (1275-1299)
  • ToolSchema (604-608)
  • WithOutputSchema (1186-1190)
mcp/utils.go (3)
  • NewStructuredToolResult (360-381)
  • NewStructuredToolError (385-406)
  • NewToolResultText (245-254)
mcp/types.go (3)
  • Content (827-829)
  • TextContent (833-838)
  • TextContent (840-840)
mcp/tools.go (1)
mcp/types.go (1)
  • Annotations (805-818)
🔇 Additional comments (21)
go.mod (1)

7-7: Verify the dependency version is current and secure.

The added JSON schema validation library supports the new structured output functionality. Please ensure this version is up-to-date and free from known vulnerabilities.

#!/bin/bash
# Check for the latest version and any security advisories for the jsonschema library
echo "Checking latest version..."
go list -m -versions github.com/santhosh-tekuri/jsonschema | tail -1

echo "Checking for known vulnerabilities..."
go list -json -m github.com/santhosh-tekuri/[email protected] | jq -r '.Version'

# Check GitHub releases for this repository
curl -s https://api.github.com/repos/santhosh-tekuri/jsonschema/releases/latest | jq -r '.tag_name'
server/server.go (1)

1037-1039: I couldn’t locate the ValidateStructuredOutput method with the initial AST query. Let’s search the entire repo using a text search:

#!/bin/bash
# Search for any occurrences of ValidateStructuredOutput in the codebase
rg "ValidateStructuredOutput" -n .
mcp/types_test.go (1)

72-115: Comprehensive test coverage for Tool serialization.

The test thoroughly covers JSON serialization/deserialization of the Tool struct, including the new Title field and nested annotations. The test structure follows best practices and provides good coverage of the functionality.

server/server_test.go (3)

1113-1113: LGTM! Type rename aligns with breaking change.

The change from mcp.ToolInputSchema to mcp.ToolSchema correctly implements the breaking change mentioned in the PR description, unifying input and output schemas under a single type.


2025-2092: Excellent test coverage for successful structured output validation.

This test thoroughly validates the success path for structured output schema validation. The test setup correctly uses the new output schema builders and verifies that:

  • Valid structured content passes validation
  • The IsError flag is properly set to false
  • Structured content is preserved and accessible in the response

2094-2161: Well-designed test for structured output validation failure.

This test effectively validates the error path when structured output doesn't match the defined schema. The test correctly:

  • Uses the same tool schema as the success test for consistency
  • Provides invalid structured content (missing required fields)
  • Verifies that IsError is set to true
  • Checks that the error message indicates schema validation failure
mcp/tools_test.go (7)

716-732: Good coverage of HasOutputSchema method.

These tests properly validate the HasOutputSchema method behavior for both empty and defined output schemas. The test cases are clear and focused.


734-792: Comprehensive JSON marshaling tests for output schema.

These tests thoroughly validate the JSON serialization behavior:

  • Correctly tests that outputSchema is included when defined
  • Verifies that outputSchema is omitted when empty
  • Provides detailed validation of schema structure and properties

794-928: Excellent coverage of output schema builder functions.

These tests thoroughly validate all the WithOutput* functions:

  • Proper schema type setting and property creation
  • Correct handling of required fields and descriptions
  • Validation of PropertyOptions like defaults, constraints, and nested structures
  • Good use of assertions to verify schema details

930-982: Solid tests for structured result helper functions.

These tests effectively validate the NewStructuredToolResult and NewStructuredToolError functions:

  • Proper IsError flag setting (false for success, true for error)
  • Structured content preservation
  • Consistent JSON text content generation
  • Good verification of dual content representation

984-1072: Comprehensive CallToolResult marshaling tests.

These tests thoroughly validate JSON serialization of CallToolResult:

  • Proper handling of structured content in JSON
  • Correct text content representation as JSON string
  • Appropriate isError field behavior (omitted when false, present when true)
  • Good verification of round-trip consistency

1074-1119: Great integration test for multiple output properties.

This test effectively validates the combination of multiple output property types:

  • Tests integration of string, number, boolean, array, and object properties
  • Verifies correct required field handling across property types
  • Provides good validation of complex schema JSON structure
  • Serves as an excellent integration test for the output schema system

1121-1156: Complete coverage of ValidateStructuredOutput method.

These tests thoroughly validate all scenarios for structured output validation:

  • Tools without output schema (no validation performed)
  • Tools with schema but missing structured content (error case)
  • Valid structured content (validation passes)
  • Invalid structured content (validation fails with appropriate error)

The test cases are well-designed and cover the complete validation logic flow.

mcp/tools.go (8)

4-11: Import additions look good.

The new imports are appropriate for the structured output validation functionality.


42-43: LGTM! Structured content field properly added.

The StructuredContent field is correctly typed as any with appropriate JSON tagging for backward compatibility.


480-490: Tool struct enhancements are well-designed.

The additions properly support output schema validation with performance optimization through cached compilation. The breaking change to unify schema types is clearly documented.


502-509: Title retrieval logic is well-implemented.

The precedence order (direct field → annotations → empty) provides good flexibility and backward compatibility.


604-625: Schema type rename and marshaling logic are well-implemented.

The unified ToolSchema type and its custom marshaling properly handle edge cases while maintaining clean JSON output.


658-668: Tool constructor properly initializes new fields.

The output schema initialization follows the same pattern as input schema for consistency.


710-716: WithTitle option correctly implemented.

Follows the established pattern for tool configuration options.


1184-1190: WithOutputSchema implementation is correct.

This function properly sets the entire output schema structure.

Copy link
Collaborator

@pottekkat pottekkat left a comment

Choose a reason for hiding this comment

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

In general the proposal is ok. Can you add more examples directly in PR description so that it is easy for reviewers to review this?

mcp/tools.go Outdated
@@ -487,14 +499,83 @@ func (t Tool) GetName() string {
return t.Name
}

// GetTitle returns the display title for the tool.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see the need for this. There should only be one way to set the title. The MCP spec defines it as an annotation so it makes more sense for it to stay an annotation.

Also this is unreleated and totally out of scope for the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry about that — the changes in these commits actually belong to #431. I accidentally included them in this draft PR by mistake. I’ll remove them shortly. Apologies for the confusion!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, hmm, even in the other PR, I'm not sure we need this. It is already supported through tool annotations right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The PR is associated with the following task: #418

As I understand it, objects like Prompt and Resource do not have a ToolAnnotation.Title field. Instead, their annotations are implemented using:

type Annotations struct {
  // Describes who the intended customer of this object or data is.
  //
  // It can include multiple entries to indicate content useful for multiple
  // audiences (e.g., ["user", "assistant"]).
  Audience []Role `json:"audience,omitempty"`

  // Describes how important this data is for operating the server.
  //
  // A value of 1 means "most important," and indicates that the data is
  // effectively required, while 0 means "least important," and indicates that
  // the data is entirely optional.
  Priority float64 `json:"priority,omitempty"`
}

// Annotated is the base for objects that include optional annotations for the
// client. The client can use annotations to inform how objects are used or
// displayed.
type Annotated struct {
  Annotations *Annotations `json:"annotations,omitempty"`
}

Given this, it seems our intention is to provide a consistent way to access a DisplayName (or similar property) across different object types—such as tools, prompts, and resources—so that this name can be presented in UI or any human-facing context.

This aligns with what the MCP documentation describes, where it encourages tools to optionally include a human-readable name for display purposes:

title: Optional human-readable name of the tool for display purposes.

It seems that the changes in our PR are consistent with the intent of the MCP documentation and serve a useful purpose in standardizing how human-readable names are exposed across object types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, can you remove the unrelated changes from this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I'm on it — I'll remove the unrelated changes right away.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @pottekkat

I’ve removed the unrelated code and verified all tests pass, Thx

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks.

mcp/tools.go Outdated
Comment on lines 1221 to 1220
func WithOutputNumber(name string, opts ...PropertyOption) ToolOption {
return func(t *Tool) {
// Initialize output schema if not set
if t.OutputSchema.Type == "" {
t.OutputSchema.Type = "object"
}

schema := map[string]any{
"type": "number",
}

for _, opt := range opts {
opt(schema)
}

// Remove required from property schema and add to OutputSchema.required
if required, ok := schema["required"].(bool); ok && required {
delete(schema, "required")
t.OutputSchema.Required = append(t.OutputSchema.Required, name)
}

t.OutputSchema.Properties[name] = schema
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need a separate schema generation function for each datatype because of backwards compatibility? Like can we reuse the same methods we use for input schema?

Copy link
Contributor Author

@davidleitw davidleitw Jun 26, 2025

Choose a reason for hiding this comment

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

That's a good point. The initial approach was to create explicit WithOutput... functions to mirror the existing With... API for consistency, which led to the code duplication you pointed out.

After thinking through the best way to resolve this, we can refactor the implementation to eliminate the repetition while keeping the public API stable.

Here are the two main approaches:


Option 1: Internal Helper Function (Recommended)

We can create a single, private applyProperty helper that does all the heavy lifting. To make it type-safe, we can define constants for the property types instead of using raw strings. The public functions would just become one-line calls to it. This solves the duplication problem without any breaking changes.

// Define a type for safety and clarity
type PropertyType string

const (
	PropertyTypeString  PropertyType = "string"
	PropertyTypeNumber  PropertyType = "number"
	PropertyTypeBoolean PropertyType = "boolean"
	PropertyTypeObject  PropertyType = "object"
	PropertyTypeArray   PropertyType = "array"
)

// The private helper does all the work, now with a typed propertyType
func applyProperty(target *ToolSchema, name string, propertyType PropertyType, opts ...PropertyOption) {
    if target.Type == "" {
        target.Type = "object"
    }
    if target.Properties == nil {
        target.Properties = make(map[string]any)
    }
    
    schema := map[string]any{"type": propertyType}
    for _, opt := range opts {
        opt(schema)
    }
    
    if required, ok := schema["required"].(bool); ok && required {
        delete(schema, "required")
        target.Required = append(target.Required, name)
    }
    
    target.Properties[name] = schema
}

// Public APIs just become simple wrappers
func WithNumber(name string, opts ...PropertyOption) ToolOption {
    return func(t *Tool) {
        applyProperty(&t.InputSchema, name, PropertyTypeNumber, opts...)
    }
}

func WithOutputNumber(name string, opts ...PropertyOption) ToolOption {
    return func(t *Tool) {
        applyProperty(&t.OutputSchema, name, PropertyTypeNumber, opts...)
    }
}

Option 2: Parameterized Functions

Alternatively, we could merge the functions and add a target parameter to specify input or output. This would be a much cleaner API, but it's a major breaking change that would require every user to update their tool definitions.

type SchemaTarget int
const (
    InputSchema SchemaTarget = iota
    OutputSchema
)

// One function to rule them all
func WithNumber(name string, target SchemaTarget, opts ...PropertyOption) ToolOption {
    // ... implementation that switches on 'target'
}

Conclusion

Option 1 (the internal helper) is the clear path forward. It's the pragmatic choice that gives us the maintainability win of a single internal logic source without forcing a painful, breaking migration on our users.

Option 2 would be the textbook answer for a new API, but the cost of the breaking change is too high for an established one.

My preference is for Option 1, as it gives us the maintainability win without the breaking change. What are your thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Come one man, I do not want to talk to an AI. I'm ok with AI assistance but this whole PR and proposal is AI generated. We are spending our free time to maintain this and this is very inconsiderate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I admit that I used AI assistance for the summary part, but the core code was written by me. I just wanted to make sure my explanation was clear and avoid any potential misunderstandings, so I asked AI to help organize it. I’m really sorry if that caused any inconvenience.

As for the applyProperty idea, I genuinely think it's a good approach. Even though we still have two sets of ToolOptions like WithNumber and WithOutputNumber, the overall code avoids duplication by handling everything through a unified applyProperty function.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah but it makes it hard to review. We already augment the reviews with AI and when we try to review, we want to have a discussion and think through the problem. We are moving much faster than most open source projects even with the number of PRs. I'm sure the AI answer is correct, but it takes away the meaningful discussion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, I understand. In future discussions, I’ll respond directly based on my own understanding. I reflected on it just now and realized that the AI did generate a lot of unnecessary extra information.

@davidleitw
Copy link
Contributor Author

In general the proposal is ok. Can you add more examples directly in PR description so that it is easy for reviewers to review this?

I've put together some examples to help you quickly understand what this PR does and how to use the new output schema feature. These should give you a good sense of the changes without having to dig through all the diff details.

Usage Examples

This PR adds Output Schema support to MCP tools. Basically, you can now define what your tools should return, and the server will validate it automatically. Here's how it works:

Example 1: Basic Weather Tool with Output Schema

Say you want a weather tool that always returns temperature and condition in a predictable format:

// Define a tool with structured output
weatherTool := mcp.NewTool(
    "get_weather",
    mcp.WithDescription("Get current weather for a location"),
    // Input schema
    mcp.WithString("location", mcp.Description("City name"), mcp.Required()),
    
    // Output schema - NEW FEATURE
    mcp.WithOutputString("condition", mcp.Description("Weather condition"), mcp.Required()),
    mcp.WithOutputNumber("temperature", mcp.Description("Temperature in Celsius"), mcp.Required()),
    mcp.WithOutputNumber("humidity", mcp.Description("Humidity percentage")),
)

// Tool handler implementation
func handleWeather(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    location := request.Arguments["location"].(string)
    
    // Business logic here...
    weatherData := map[string]any{
        "condition":   "Sunny",
        "temperature": 25.5,
        "humidity":    60,
    }
    
    // Return structured data - automatically validated against output schema
    return mcp.NewStructuredToolResult(weatherData), nil
}

The WithOutput* functions work just like the input schema ones you're already familiar with. When you return data, the server checks it matches what you promised. The NewStructuredToolResult helper creates both the structured response and a JSON fallback for older clients.

Example 2: Complex Output with Nested Objects

Need something more complex? You can nest objects and arrays too:

// Tool with complex output structure
userTool := mcp.NewTool(
    "get_user_profile",
    mcp.WithDescription("Fetch user profile data"),
    mcp.WithString("user_id", mcp.Required()),
    
    // Complex output schema
    mcp.WithOutputString("user_id", mcp.Required()),
    mcp.WithOutputString("name", mcp.Required()),
    mcp.WithOutputObject("preferences", mcp.Description("User preferences"),
        mcp.Properties(map[string]any{
            "theme":    map[string]any{"type": "string"},
            "language": map[string]any{"type": "string"},
            "notifications": map[string]any{"type": "boolean"},
        }),
    ),
    mcp.WithOutputArray("tags", mcp.Description("User tags"), mcp.WithStringItems()),
)

Example 3: Error Handling with Structured Output

Even when things go wrong, you can still return structured data that matches your schema:

func handleUserProfile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    userID := request.Arguments["user_id"].(string)
    
    user, err := fetchUser(userID)
    if err != nil {
        // Return structured error that matches output schema
        errorData := map[string]any{
            "user_id": userID,
            "name":    "",
            "preferences": map[string]any{},
            "tags":    []string{},
        }
        return mcp.NewStructuredToolError(errorData), nil
    }
    
    // Return successful structured data
    return mcp.NewStructuredToolResult(user), nil
}

@pottekkat
Copy link
Collaborator

@davidleitw How does the structured errors work? Does the spec require errors to also be returned in this format?

@pottekkat
Copy link
Collaborator

I will work on implementing this as a way to test it since I already require this for the MCP server I'm building. Looking at the examples, it feels intuitive and easy to use and does not break any existing code. If I find any issues with using it, I will report.

@pottekkat
Copy link
Collaborator

We might need to reiterate on the design. There are better ways to do it without duplicating logic or mimicking the current tool schema creation flow. Based on internal discussion, @davidleitw and I will come up with more intuitive/Go-idiomatic ways to implement this.

@davidleitw
Copy link
Contributor Author

I've reviewed the implementations of the other official MCP SDKs. I'll also summarize that along with the discussion I had with @pottekkat yesterday and post it here shortly.

@davidleitw
Copy link
Contributor Author

davidleitw commented Jun 28, 2025

TL;DR: Structured Output Interface Changes for go-mcp

  • When IsError is true or the tool has no Output Schema defined, output validation should be skipped
  • Provide multiple helper functions to assist users in returning results within MCP tools:
    • Set only StructuredContent → the helper will automatically copy the same content to Content
    • Support explicitly setting both Content and StructuredContent
    • Support returning StructuredContent with IsError set to true.
  • For defining Output Schemas, instead of using a builder pattern like we do for Input Schemas, we’re considering defining a struct and leveraging struct tags or Go generics to provide a cleaner interface.

Still organizing the details — more to come soon.
I'm currently compiling how each language SDK implements Output Schema. I’ll share the details shortly.

@davidleitw
Copy link
Contributor Author

I've grouped the remaining unimplemented parts into two topics, summarizing how they're described in the MCP docs, how the SDK interprets them, and related implementation details:

  • Should CallToolResult's Structured Content, if present, match the Content field?
    • The current PR's helper NewStructuredToolResult makes them identical. This may change after discussion.
  • When IsError is True, should CallToolResult.Content still be validated against the Output Schema?

Servers MUST return structured results that conform to the schema when defined (In MCP docs)

One design consideration (not part of the protocol): should mcp-go provide WithOutputXXX methods for Output Schema, similar to Input Schema? After discussing with @pottekkat, we think a more idiomatic Go approach might be better.

TL;DR and conclusions are already provided above.
Below is some related survey material—if you're interested in this topic, feel free to start the discussion based on the conclusions above.


First, I’d like to focus on discussing the implementation of Structured Content in relation to how it's defined in the MCP documentation. The main goal is to examine how the behavior is described in the spec and how it's interpreted and implemented in the Python SDK.

In the Structured Content section, it mentions:

For backwards compatibility, a tool that returns structured content SHOULD also return functionally equivalent unstructured content. (For example, serialized JSON can be returned in a block.)

However, what I’ve observed is that most official SDKs do not strictly enforce consistency between the two. Instead, the behavior depends on the form of the tool’s return value.

In the Python SDK, when the MCP Server receives a CallToolResult, it processes the result using the logic found at the following location: tool_call

The Python MCP Server automatically determines how to handle tool results based on their return type.

# output normalization
unstructured_content: UnstructuredContent
maybe_structured_content: StructuredContent | None
if isinstance(results, tuple) and len(results) == 2:
    # tool returned both structured and unstructured content
    unstructured_content, maybe_structured_content = cast(CombinationContent, results)
elif isinstance(results, dict):
    # tool returned structured content only
    maybe_structured_content = cast(StructuredContent, results)
    unstructured_content = [types.TextContent(type="text", text=json.dumps(results, indent=2))]
elif hasattr(results, "__iter__"):
    # tool returned unstructured content only
    unstructured_content = cast(UnstructuredContent, results)
    maybe_structured_content = None
else:
    return self._make_error_result(f"Unexpected return type from tool: {type(results).__name__}")

# output validation
if tool and tool.outputSchema is not None:
    if maybe_structured_content is None:
        return self._make_error_result(
            "Output validation error: outputSchema defined but no structured output returned"
        )
    else:
        try:
            jsonschema.validate(instance=maybe_structured_content, schema=tool.outputSchema)
        except jsonschema.ValidationError as e:
            return self._make_error_result(f"Output validation error: {e.message}")

In the combination mode test at tests/server/test_lowlevel_output_validation.py, the tool function returns like this:

async def call_tool_handler(name: str, arguments: dict[str, Any]) -> tuple[list[TextContent], dict[str, Any]]:
    if name == "process":
        content = [TextContent(type="text", text="Processing complete")]
        data = {"result": "success", "count": 10}
        return (content, data)

This tuple return triggers the first branch isinstance(results, tuple), treating "Processing complete" as the content, and {"result": "success", "count": 10} as the structured content, allowing the two to be completely different.

When the tool directly returns a dict, like in the example at tests/server/test_lowlevel_output_validation.py:get_info

async def call_tool_handler(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
    if name == "get_info":
        return {"status": "ok", "data": {"value": 42}}

It falls into the second branch isinstance(results, dict), and the system sets:

  • maybe_structured_content = the original dict {"status": "ok", "data": {"value": 42}}
  • unstructured_content = JSON-serialized version of the dict as Text Content

After the above logic is processed, it proceeds to the server-side Output Schema validation step.

This code highlights the key behavioral differences between tools with and without an Output Schema:

  • Tools without an Output Schema: The system performs no validation and accepts any return type. Whether the result is pure content (maybe_structured_content = None), a plain dict, or a combination of both, it will be processed normally.
  • Tools with an Output Schema: The system enforces validation on maybe_structured_content.

If the tool returns only content (e.g., the test case in tests/server/test_lowlevel_output_validation.py:214-240), since maybe_structured_content = None, it will immediately trigger the first error branch.If structured content is present but does not conform to the schema, a JSON Schema validation error will be raised.

I originally thought that in Python, this kind of code meant that even when an error occurs, it would still go through the Output Schema validation process. But after reviewing some test cases, I realized that it actually doesn’t happen at all.

The reason is simple:

async def handler(req: types.CallToolRequest):
    try:
        # ... tool invocation logic ...
        # ... output validation logic ...
        return types.ServerResult(types.CallToolResult(...))
    except Exception as e:
        return self._make_error_result(str(e))  # directly returns, skips all validation

Key Insight:

  • When the tool function raises any exception, it is caught by the outermost except Exception.
  • It then immediately calls _make_error_result() and returns, completely skipping the subsequent output validation logic.
  • The CallToolResult produced by _make_error_result() contains only the error message and no structuredContent.

So the actual behavior is:

  • Normal case: The tool returns a result → output validation runs → result is returned.
  • Error case: The tool raises an exception → error is returned immediately, skipping all validation.
def error_tool_fn() -> None:
    raise ValueError("Test error")

That’s why tool functions that raise exceptions end up producing a result with isError = true in the tests.


Now, let's look at how the TypeScript SDK handles this. To be honest, the behavior is pretty similar, but TypeScript has a special feature: you can manually set isError = true, which makes error handling a bit different.

I took a look at the TypeScript SDK's source code, and its approach is quite interesting. The whole flow goes something like this:

  1. First, it checks if the tool exists and isn't disabled (if there's an issue here, it throws an McpError directly).
  2. It uses Zod to validate input arguments (this also throws an McpError on failure).
  3. It executes the tool's callback function.
  4. If the callback throws an exception, it's caught and wrapped into a CallToolResult with isError = true.
  5. Finally, it checks the output schema, but here's the key part: it only performs validation if !result.isError (an error here throws another McpError).

The biggest difference from the Python SDK is in the error handling. Python's approach is more direct; it wraps the entire process in a try-catch block. As soon as an error occurs, it immediately returns an error result, and none of the subsequent logic runs. TypeScript's approach is more granular, distinguishing between different types of errors:

  • System-level errors (like tool not found, invalid argument format, or schema validation failure) will throw an McpError directly.
  • Tool execution errors (an exception thrown inside your callback function) are "swallowed" and packaged into a normal CallToolResult marked with the isError flag.
# Python's approach: exit immediately
try:
    # All logic is wrapped in here
    return types.ServerResult(types.CallToolResult(...))
except Exception as e:
    return self._make_error_result(str(e))  # Return directly, nothing else runs
// TypeScript's approach: convert the error but continue the flow
try {
    result = await Promise.resolve(cb(args, extra));
} catch (error) {
    result = {
        content: [{ type: "text", text: error.message }],
        isError: true,  // Set a flag
    };
}

// Continue execution, but check the flag
if (tool.outputSchema && !result.isError) {
    // Skip validation if there's an error
}

For example, you can manually control errors like this:

// Handling a business logic error yourself
if (user.permission < REQUIRED_LEVEL) {
    return {
        content: [{ type: "text", text: "Permission denied" }],
        structuredContent: { errorCode: 403, message: "Permission denied" },
        isError: true  // Set this manually to skip schema validation
    };
}

Or you can just throw an exception and let the system handle it automatically:

// Throw an exception, and the system will automatically wrap it as an isError: true result
if (database.isDown()) {
    throw new Error("Database connection failed");
}

I think this design is a great reference for our mcp-go implementation. Go developers are used to handling errors explicitly and don't rely much on exceptions. If we also use an IsError flag to decide whether to validate, it gives developers building on MCP very clear control over the flow.

Another benefit of this design is that you can still provide structured content even when there's an error (though it won't be validated). This could be really useful in some scenarios, like if you want to return structured error information like an error code or reason, perhaps by adding fields like error_code or error_reason to a struct.


In summary, both the Python and TypeScript SDKs achieve the same core goal: when a tool execution fails, output schema validation is skipped. This aligns perfectly with the spirit of the MCP specification, as error results are primarily intended to help the LLM understand what went wrong and attempt self-correction

The key difference lies in their implementation style and philosophy:

  • The Python SDK uses a more implicit approach through a global try-except mechanism. Any exception thrown by a tool will automatically interrupt the flow and skip validation
  • The TypeScript SDK, on the other hand, offers more explicit control by allowing the developer (or the system) to set the isError flag in the returned result. The validation logic then actively checks this flag to decide whether to proceed

Put simply, one relies on implicit exception handling, while the other uses explicit flag checking for control


Reflecting this conclusion, we propose the following concrete design directions for mcp-go:

  • Validation Logic: Clearly define that output validation must be skipped when IsError is true or when the tool does not define an OutputSchema
  • Helper Functions: Provide various helper functions to simplify result construction within tools:
    • Set only StructuredContent: The helper will automatically serialize and copy it toContent
    • Explicitly set both fields: Allow developers to define different values for Content and StructuredContent
    • Structured error responses: Support returning StructuredContent even when IsError is true
  • Schema Definition API: For OutputSchema, instead of following the builder-style API used for InputSchema, we’re considering a more idiomatic Go approach—defining a struct and leveraging struct tags or generics to provide a cleaner and more expressive interface

@davidleitw
Copy link
Contributor Author

Based on the TLDR, the following changes were made:

    1. Output validation logic:
    • The server now skips output validation if IsError is true or if the tool has no OutputSchema defined.
    1. Helper functions:
    • Four new generic helper functions were added in mcp/utils.go to support:
      • Returning only StructuredContent (with automatic fallback to Content)
      • Explicitly setting both Content and StructuredContent
      • Returning StructuredContent with IsError set to true
    1. Output schema definition:
    • The old builder pattern (WithOutput... functions) was removed.
    • A new generic function, mcp.WithOutputType[T any](), was introduced, allowing developers to define output schemas using Go structs with jsonschema tags.

New tests added:

  • Server-side unit tests:
    Tests for WithOutputType covering successful validation, failed validation, and skipping validation on error.
    A new test (TestMCPServer_ToolDefinition_IncludesOutputSchema) ensures that output schemas (including descriptions) are correctly generated and registered on the server.
  • Client-side integration test:
    A new test (TestHTTPClient_ListTools_WithOutputSchema) verifies that, after client initialization, calling ListTools retrieves the correct output schema (with descriptions) from the server.

Example usage:

type WeatherData struct {
    Temperature float64 `json:"temperature" jsonschema:"description=The temperature in Celsius."`
    Conditions  string  `json:"conditions" jsonschema:"description=Weather conditions (e.g. Cloudy)."`
}

// Register the tool with output schema
tool := mcp.NewTool("get_weather", mcp.WithOutputType[WeatherData]())
server.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // Return structured content using the helper
    return mcp.NewToolResultStructured(WeatherData{
        Temperature: 23.5,
        Conditions:  "Cloudy",
    }), nil
})

This example shows how to define a tool with a structured output schema and return structured content using the new API.

@davidleitw davidleitw marked this pull request as ready for review June 28, 2025 18:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
mcp/tools.go (2)

686-691: Undocumented addition of WithDescription helper.

This helper function is a useful addition but wasn't mentioned in the PR description. Consider documenting this in the PR summary.


1189-1222: Schema generation with compatibility workaround.

The implementation correctly handles schema generation and compilation. The Draft-07 version override is a necessary workaround for validator compatibility - this is well-documented in the code.

Consider documenting this Draft-07 limitation in the package documentation or README to help users understand the schema version constraints.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58ef0d3 and ae446b4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • client/http_test.go (2 hunks)
  • go.mod (1 hunks)
  • mcp/tools.go (7 hunks)
  • mcp/tools_test.go (1 hunks)
  • mcp/utils.go (1 hunks)
  • server/server.go (1 hunks)
  • server/server_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/server.go
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
go.mod (3)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
client/http_test.go (5)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
mcp/tools_test.go (5)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
server/server_test.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
mcp/utils.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
mcp/tools.go (7)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: lariel-fernandes
PR: mark3labs/mcp-go#428
File: www/docs/pages/servers/prompts.mdx:218-234
Timestamp: 2025-06-20T20:39:51.870Z
Learning: In the mcp-go library, the GetPromptParams.Arguments field is of type map[string]string, not map[string]interface{}, so direct string access without type assertions is safe and correct.
🧬 Code Graph Analysis (1)
mcp/utils.go (2)
mcp/types.go (1)
  • Content (827-829)
mcp/tools.go (1)
  • CallToolResult (40-49)
🔇 Additional comments (27)
go.mod (1)

7-8: Dependencies are current and vulnerability-free

  • github.com/invopop/jsonschema v0.13.0 matches the latest stable release
  • github.com/santhosh-tekuri/jsonschema v1.2.4 matches the latest stable release
  • No security advisories found in the GitHub Vulnerability Database for either package

No further action required.

client/http_test.go (1)

117-176: Well-structured integration test for output schema support.

The test effectively validates the end-to-end functionality of output schema serialization and retrieval. The test structure is clear and follows good testing practices:

  • Proper setup with server and client initialization
  • Uses realistic test data with struct tags for schema generation
  • Verifies both the presence and content of the output schema
  • Includes proper cleanup with defer httpServer.Close()
mcp/utils.go (4)

356-367: Good error handling in JSON marshaling helper.

The marshalToContent function properly handles JSON marshaling errors by returning an error message as text content, ensuring the function never fails catastrophically.


369-377: Clean generic API for structured tool results.

The NewToolResultStructured function provides a clean, type-safe way to create tool results with structured content while maintaining backwards compatibility through automatic JSON content generation.


379-386: Flexible helper for mixed content scenarios.

The NewToolResultWithStructured function appropriately handles cases where developers need both custom content and structured data, providing flexibility for complex tool responses.


388-405: Consistent error handling variants.

The error-specific helper functions (NewToolResultErrorStructured and NewToolResultErrorWithStructured) maintain the same clean API pattern while properly setting the IsError flag, ensuring consistency with the MCP specification.

mcp/tools_test.go (5)

716-731: Good basic functionality testing.

The tests for HasOutputSchema properly validate both empty and populated schema scenarios with clear assertions.


734-777: Thorough JSON marshaling validation.

The test comprehensively validates that output schemas are correctly included in marshaled JSON, checking both the presence of the schema and its internal structure.


829-867: Excellent struct-based schema generation test.

The TestWithOutputType function effectively validates the new generic schema generation feature using Go structs with jsonschema tags, confirming the schema is properly generated and included.


870-897: Comprehensive structured result testing.

The test validates that NewToolResultStructured correctly creates structured content, sets appropriate flags, and generates proper JSON fallback content for backwards compatibility.


965-1021: Thorough validation logic testing.

The validation tests comprehensively cover the key scenarios:

  • Tools without output schema skip validation
  • Error results skip validation regardless of schema presence
  • Tools with output schema require structured content
  • Structured content is validated against the compiled schema

This properly reflects the MCP specification requirements.

server/server_test.go (8)

4-4: LGTM!

The bytes import is appropriately added to support JSON schema formatting in the new test functions.


1114-1114: Type rename aligns with breaking change.

The change from mcp.ToolInputSchema to mcp.ToolSchema correctly reflects the documented breaking change in this PR.


2026-2101: Well-structured test for successful validation.

The test comprehensively validates that tools with properly structured output pass validation. Good coverage of both the text content and structured content in the response.


2102-2178: Comprehensive validation failure test.

The test effectively validates that schema violations are caught and properly reported with IsError=true and descriptive error messages.


2180-2258: Excellent test coverage with important limitation documented.

The test thoroughly validates the WithOutputType API and properly documents the jsonschema parser limitation about commas in descriptions. The schema generation verification is particularly valuable.


2260-2315: Effective schema mismatch testing.

Good approach using two different structs to simulate schema validation failure. The test correctly verifies that missing required properties are caught.


2317-2371: Critical edge case well tested.

Excellent test validating that error responses (IsError=true) correctly bypass schema validation, aligning with MCP specification behavior.


2373-2416: Validates schema registration and storage.

Good test ensuring that output schemas generated from Go types are properly stored in the server's tool registry with descriptions intact.

mcp/tools.go (8)

3-13: LGTM!

The jsonschema imports are appropriately chosen - invopop/jsonschema for generation and santhosh-tekuri/jsonschema for validation.


43-44: Structured content field properly added.

The StructuredContent field is correctly typed as any with omitempty tag for backward compatibility.


484-495: Tool struct properly extended for output schemas.

Good design choices:

  • OutputSchema as json.RawMessage provides flexibility
  • Internal validation fields correctly hidden with json:"-"
  • Type rename aligns with the breaking change

502-556: Well-designed validation architecture.

The validation implementation demonstrates good design patterns:

  • Lazy compilation of validators for efficiency
  • Clear separation between public and private methods
  • Proper nil checks and error handling

561-584: JSON marshaling correctly updated.

The marshaling logic properly handles the new OutputSchema field with appropriate nil checking.


590-611: Type rename and marshaling logic are correct.

The ToolSchema type properly replaces ToolInputSchema with clean JSON marshaling that omits empty fields.


1171-1187: Clean generic API for output schema definition.

Excellent use of Go generics to provide a type-safe way to define output schemas. The graceful error handling allows tools to work even if schema compilation fails.


1224-1262: Comprehensive validation logic with proper error handling.

The validation implementation correctly:

  • Skips validation for error responses (MCP spec compliance)
  • Handles various structured content types
  • Provides clear error messages

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
server/server_test.go (1)

2373-2416: Thorough verification of schema storage in the server registry!

The test properly verifies that output schemas with descriptions are correctly stored in the server's tool registry.

Consider adding an assertion to verify that HasOutputSchema() returns true for the registered tool:

 registeredTool, ok := server.tools[tool.Name]
 require.True(t, ok, "Tool should be registered on the server")
 require.NotNil(t, registeredTool.Tool.OutputSchema, "OutputSchema should not be nil")
+assert.True(t, registeredTool.Tool.HasOutputSchema(), "HasOutputSchema should return true")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae446b4 and 43b576e.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • client/http_test.go (3 hunks)
  • go.mod (1 hunks)
  • mcp/tools.go (7 hunks)
  • mcp/tools_test.go (1 hunks)
  • mcp/utils.go (1 hunks)
  • server/server.go (1 hunks)
  • server/server_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • go.mod
  • server/server.go
  • client/http_test.go
  • mcp/utils.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
mcp/tools_test.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
mcp/tools.go (7)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: lariel-fernandes
PR: mark3labs/mcp-go#428
File: www/docs/pages/servers/prompts.mdx:218-234
Timestamp: 2025-06-20T20:39:51.870Z
Learning: In the mcp-go library, the GetPromptParams.Arguments field is of type map[string]string, not map[string]interface{}, so direct string access without type assertions is safe and correct.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
server/server_test.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
🔇 Additional comments (17)
mcp/tools_test.go (6)

716-732: LGTM!

The tests for HasOutputSchema method are concise and cover both empty and defined schema cases correctly.


734-777: LGTM!

Comprehensive test that verifies JSON marshaling includes the output schema with all its properties and required fields.


816-827: Good cleanup of deprecated tests.

The comments clearly indicate the migration path to the new generic functions NewToolResultStructured[T]() and NewToolResultErrorStructured[T]().


829-867: Excellent test for struct-based schema generation!

The test properly demonstrates the use of WithOutputType[T]() with jsonschema tags and verifies that the generated schema includes all expected properties and descriptions.


870-962: Comprehensive test coverage for new helper functions!

All four helper function tests properly verify:

  • Structured content assignment
  • JSON marshaling to text content
  • IsError flag handling
  • Content preservation when custom content is provided

996-1021: Excellent edge case coverage for output validation!

The test properly verifies the conditional validation logic:

  • No validation when output schema is not defined
  • Validation skipped for error results (IsError = true)
  • Validation error when structured content is missing but required
server/server_test.go (5)

1114-1114: Correct type name update.

This fix aligns with the breaking change that renames ToolInputSchema to ToolSchema for unified schema handling.


2027-2101: Well-structured integration test for successful validation!

The test properly verifies that the server accepts valid structured content matching the defined output schema.


2103-2178: Good test coverage for validation failure scenarios!

The test correctly verifies that the server sets IsError=true and provides an appropriate error message when structured content fails validation.


2180-2258: Excellent tests with helpful documentation!

Both tests properly verify WithOutputType validation behavior. The comments about jsonschema tag limitations (no comma support) are particularly valuable for future maintainers.

The success test thoroughly verifies that descriptions from struct tags are preserved in the generated schema, while the failure test cleverly uses different struct types to simulate validation errors.

Also applies to: 2260-2315


2317-2371: Important test for error response handling!

This test correctly verifies that schema validation is bypassed when IsError=true, preserving the original error content. This behavior aligns with the MCP specification.

mcp/tools.go (6)

4-4: LGTM - Necessary imports for output schema functionality.

The added imports support JSON schema generation from Go structs and schema validation, which are core to the structured output feature.

Also applies to: 11-12


43-44: LGTM - Proper addition of structured content field.

The StructuredContent field correctly implements the MCP specification for structured tool outputs with appropriate JSON marshaling behavior.


502-506: LGTM - Correct output schema detection.

The nil check is appropriate for json.RawMessage type and correctly identifies when an output schema is defined.


1224-1262: Well-implemented validation logic with comprehensive type handling.

The method correctly:

  • Skips validation when IsError is true (per MCP spec)
  • Handles various Go types appropriately for JSON schema validation
  • Uses JSON marshaling round-trip for struct conversion, ensuring compatibility

The validation logic aligns well with the MCP specification requirements.


580-583: LGTM - Proper JSON marshaling of output schema.

The output schema is correctly included in the tool's JSON representation when defined.


1198-1202: Appropriate workaround for validator compatibility.

The manual override to JSON Schema Draft-07 is well-documented and necessary for compatibility with the santhosh-tekuri/jsonschema validator library.

mcp/tools.go Outdated
Comment on lines 1171 to 1220
// WithOutputType sets the output schema for the Tool using Go generics and struct tags.
// This replaces the builder pattern with a cleaner interface based on struct definitions.
func WithOutputType[T any]() ToolOption {
return func(t *Tool) {
var zero T
validator, schemaBytes, err := compileOutputSchema(zero)
if err != nil {
// Skip setting output schema if compilation fails
// This allows the tool to work without validation
return
}

t.OutputSchema = schemaBytes
t.outputValidator = validator
t.outputType = reflect.TypeOf(zero)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling for schema compilation failures.

The function silently ignores compilation errors, which could hide important issues during development. Consider logging the error or providing a way for developers to detect when schema compilation fails.

 func WithOutputType[T any]() ToolOption {
     return func(t *Tool) {
         var zero T
         validator, schemaBytes, err := compileOutputSchema(zero)
         if err != nil {
-            // Skip setting output schema if compilation fails
-            // This allows the tool to work without validation
+            // Log compilation failure for debugging
+            // Consider: panic, log.Printf, or return error-aware ToolOption
             return
         }
         // ... rest of function
     }
 }
🤖 Prompt for AI Agents
In mcp/tools.go around lines 1171 to 1187, the WithOutputType function currently
ignores errors from compileOutputSchema, which can hide important issues. Modify
the function to log the error or return it so developers can detect schema
compilation failures. For example, add error logging inside the if err != nil
block or change the function signature to return an error and propagate it
accordingly.

@davidleitw davidleitw force-pushed the structured_output branch 3 times, most recently from aa0c7b8 to 025783b Compare June 29, 2025 04:23
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa0c7b8 and 025783b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • client/http_test.go (3 hunks)
  • go.mod (1 hunks)
  • mcp/tools.go (7 hunks)
  • mcp/tools_test.go (1 hunks)
  • mcp/utils.go (1 hunks)
  • server/server.go (1 hunks)
  • server/server_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • go.mod
  • server/server.go
  • client/http_test.go
  • mcp/utils.go
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
mcp/tools.go (7)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: lariel-fernandes
PR: mark3labs/mcp-go#428
File: www/docs/pages/servers/prompts.mdx:218-234
Timestamp: 2025-06-20T20:39:51.870Z
Learning: In the mcp-go library, the GetPromptParams.Arguments field is of type map[string]string, not map[string]interface{}, so direct string access without type assertions is safe and correct.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
mcp/tools_test.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
server/server_test.go (6)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
🧬 Code Graph Analysis (2)
mcp/tools.go (2)
mcp/types.go (1)
  • Annotations (805-818)
mcp/utils.go (1)
  • ToBoolPtr (868-870)
mcp/tools_test.go (3)
mcp/tools.go (7)
  • NewTool (662-688)
  • WithOutputSchema (1183-1187)
  • WithDescription (709-713)
  • WithString (944-962)
  • Required (776-780)
  • WithOutputType (1195-1214)
  • CallToolResult (41-50)
mcp/utils.go (7)
  • NewToolResultStructured (371-377)
  • NewTextContent (199-204)
  • NewImageContent (208-214)
  • NewToolResultWithStructured (380-386)
  • NewToolResultErrorStructured (390-396)
  • NewToolResultErrorWithStructured (399-405)
  • NewToolResultText (245-254)
mcp/types.go (3)
  • Content (827-829)
  • TextContent (833-838)
  • TextContent (840-840)
🔇 Additional comments (26)
server/server_test.go (8)

4-4: LGTM: Import addition supports test functionality.

The bytes import is appropriately added to support JSON pretty printing in the new structured output validation tests.


114-114: LGTM: Type rename aligns with API unification.

The change from mcp.ToolInputSchema to mcp.ToolSchema correctly implements the breaking change mentioned in the PR to unify input and output schema handling.


2030-2101: LGTM: Comprehensive success case validation test.

This test thoroughly validates the structured output validation success path:

  • Properly defines output schema with required fields
  • Creates tool with WithOutputSchema
  • Verifies successful validation when structured content matches schema
  • Correctly asserts IsError is false and structured content is preserved

The test design aligns with MCP specification requirements for structured output validation.


2106-2178: LGTM: Thorough validation failure test coverage.

This test effectively verifies validation failure scenarios:

  • Uses same schema as success test for consistency
  • Returns structured content that violates required fields
  • Properly asserts IsError is true when validation fails
  • Verifies error message contains validation failure details

The test correctly demonstrates server behavior when tool output doesn't match the defined schema.


2182-2258: LGTM: Excellent test of the new WithOutputType API.

This test comprehensively validates the new generic WithOutputType[T]() functionality:

  • Demonstrates Go struct-based schema definition with jsonschema tags
  • Verifies generated schema includes field descriptions
  • Tests successful validation with NewToolResultStructured helper
  • Properly validates structured content preservation

The test effectively showcases the idiomatic Go approach for output schema definition mentioned in the PR objectives.


2262-2315: LGTM: Validation failure test with WithOutputType.

This test properly verifies validation failure when using WithOutputType:

  • Uses separate types to simulate schema mismatch (missing required field)
  • Correctly expects validation failure and IsError true
  • Verifies specific error message about missing properties

The test design effectively demonstrates validation enforcement with the new type-based API.


2319-2371: LGTM: Critical test for error response validation skip.

This test validates a crucial MCP specification requirement:

  • Verifies that when IsError is true, schema validation is skipped
  • Uses intentionally invalid structured content that would normally fail validation
  • Confirms original error content is preserved without validation interference
  • Uses NewToolResultErrorStructured helper appropriately

This behavior is essential for proper error handling in the MCP protocol.


2376-2416: LGTM: Schema generation and storage verification.

This test ensures proper integration of output schemas into the server's tool registry:

  • Verifies WithOutputType generates schema with descriptions from struct tags
  • Confirms schema is properly stored in server's internal tool registry
  • Validates that generated schema contains expected field descriptions
  • Includes helpful debug output for manual verification

The test provides confidence that the schema generation and storage mechanisms work correctly.

mcp/tools.go (12)

4-14: LGTM! Appropriate imports for structured output functionality.

The new imports are well-chosen for the structured output schema feature:

  • bytes for JSON schema resource handling
  • sync for thread-safe validator initialization
  • invopop/jsonschema for Go struct to JSON schema generation
  • santhosh-tekuri/jsonschema for JSON schema validation

44-45: Well-designed structured content field.

The addition of StructuredContent field to CallToolResult properly supports structured tool outputs while maintaining backward compatibility through the omitempty JSON tag.


478-483: Excellent thread-safe design for validator state.

The validatorState struct with sync.Once ensures thread-safe initialization of validators while storing any initialization errors for consistent error reporting across goroutines.


492-504: Well-structured Tool enhancement for output schemas.

The new fields properly support structured outputs:

  • OutputSchema for the JSON schema definition
  • outputType for Go type information
  • validatorState for thread-safe validator management

The pointer to validatorState correctly avoids copy issues during concurrent access.


511-515: Correct output schema detection logic.

The HasOutputSchema method properly checks for nil OutputSchema, which correctly detects all valid output schemas regardless of their JSON Schema type (object, string, array, etc.).


525-557: Excellent thread-safe validator initialization.

The ensureOutputSchemaValidator method uses sync.Once correctly to ensure the validator is compiled exactly once. The error handling stores initialization errors in the shared state for consistent error reporting across goroutines.


559-576: Appropriate validation entry point with proper checks.

The ValidateStructuredOutput method correctly:

  • Skips validation when no output schema is defined
  • Validates that structured content is provided when a schema exists
  • Delegates to the thread-safe validator initialization

601-604: Proper JSON marshaling with backward compatibility.

The marshaling correctly includes the outputSchema field only when defined, maintaining backward compatibility while exposing the schema when present.


1181-1187: Simple and effective raw schema configuration.

The WithOutputSchema function provides a straightforward way to set output schemas using raw JSON, maintaining consistency with the existing API patterns.


1195-1214: Well-designed generic schema generation.

The WithOutputType[T]() function elegantly replaces the builder pattern with a cleaner struct-based approach. The silent error handling on compilation failure aligns with the project's API design philosophy of simplicity over defensive programming.


1216-1249: Robust schema compilation with compatibility override.

The compileOutputSchema function properly:

  • Uses the invopop/jsonschema reflector for Go struct analysis
  • Overrides schema version to Draft-07 for validator compatibility
  • Provides comprehensive error handling for marshal/compile failures

1251-1293: Comprehensive validation logic with proper skip conditions.

The ValidateOutput method correctly implements the MCP specification requirements:

  • Skips validation when IsError is true (per TypeScript SDK pattern)
  • Skips validation when no output schema is defined
  • Handles struct-to-JSON conversion for validation
  • Provides clear error messages for validation failures
mcp/tools_test.go (6)

716-731: Comprehensive output schema detection tests.

These tests properly verify the HasOutputSchema method behavior for both empty tools and tools with defined schemas, ensuring correct detection logic.


733-777: Thorough JSON marshaling verification.

This test comprehensively validates that tools with output schemas correctly include the schema in their JSON representation, with proper structure validation.


828-867: Excellent struct-based schema generation test.

The test validates the WithOutputType function using a realistic struct with JSON schema annotations, confirming proper schema generation and JSON marshaling.


869-962: Comprehensive structured result helper tests.

These tests thoroughly validate the new generic helper functions (NewToolResultStructured, NewToolResultWithStructured, etc.), ensuring proper content marshaling and error flag handling.


1023-1073: Excellent thread safety validation.

This test properly verifies that ensureOutputSchemaValidator is thread-safe by running concurrent goroutines and ensuring consistent validator initialization without race conditions.


1123-1179: Robust error consistency testing.

This test validates that error handling during validator initialization is consistent across concurrent access, ensuring that all goroutines receive the same error state. This is crucial for maintaining predictable behavior in multi-threaded environments.

@davidleitw
Copy link
Contributor Author

@coderabbitai review

Copy link
Contributor

coderabbitai bot commented Jun 29, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

This commit replaces the builder-style `WithOutput*` functions with a single, type-safe generic function, `WithOutputType[T any]`.

Developers can now define a tool's output schema using a standard Go struct with `json` and `jsonschema` tags, simplifying the API and improving developer experience.

Key changes:
- Adds `invopop/jsonschema` for schema generation from structs.
- Improves server-side validation to correctly skip validation on tool errors.
- Adds new generic helper functions (`NewToolResultStructured`, etc.) for creating structured results.
- Updates and adds tests to cover the new API and validation logic.
@stonemax
Copy link

stonemax commented Jul 23, 2025

Will this feature(Structured Tool Output) be released soon?

@davidleitw
Copy link
Contributor Author

Hi @ezynda3

Would you be available to briefly review this when you have time? If there's anything that needs to be revised, I'm happy to make improvements. Thank you

In principle, my implementation follows the requirements outlined in the MCP spec as closely as possible — for example, the server side enforces output schema validation.

@elben-van
Copy link

I also hope this feature can be released. I've been waiting for a few weeks😂

@davidleitw
Copy link
Contributor Author

Nice community

@davidleitw davidleitw closed this Jul 28, 2025
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