Skip to content

Conversation

@alex210501
Copy link
Contributor

@alex210501 alex210501 commented Aug 30, 2025

Description

This PR returns the OutputSchema from the tool definition as per the MCP spec: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema

The behaviour of WithOutputSchema() also changed as RawOutputSchema is not longer populated, but OutputSchema is from the T generic type. The only way now to set RawOutputSchema is through the WithRawOutputSchema() method.

Fixes #563

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • 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: Link text
  • Implementation follows the specification exactly

Additional Information

Summary by CodeRabbit

  • New Features
    • Added typed output schema support for tools, enabling clearer, structured output definitions while retaining backward compatibility.
  • Bug Fixes
    • Improved validation to prevent conflicting output schema configurations, returning a clear error when both pathways are used simultaneously.
  • Documentation
    • Updated references and naming to reflect the new output schema approach.
  • Tests
    • Adjusted tests to validate the new output schema behavior and JSON representation.
  • Refactor
    • Streamlined schema handling to consistently populate and serialize the structured output schema.

alex210501 and others added 2 commits August 30, 2025 14:11
This commit returns the `OutputSchema` from the tool definition as per
the MCP spec: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema

The behaviour of `WithOutputSchema()` also changed as `RawOutputSchema`
is not longer populated, but `OutputSchema` is from the `T` generic
type. The only way now to set `RawOutputSchema` is through the
`WithRawOutputSchema()` method.
…-output-schema

fix(tool): Return the `OutputSchema` from the tool definition
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 30, 2025

Walkthrough

Introduces typed ToolArgumentsSchema with aliases for input/output schemas, adds a public Tool.OutputSchema, updates marshaling to handle OutputSchema vs RawOutputSchema with conflict checks, and adjusts WithOutputSchema to populate OutputSchema. Tests updated to assert OutputSchema instead of RawOutputSchema.

Changes

Cohort / File(s) Summary
Tool schema types and marshaling
mcp/tools.go
Added ToolArgumentsSchema with Defs/Type/Properties/Required and moved MarshalJSON to it; added public Tool.OutputSchema (ToolOutputSchema); kept RawOutputSchema path with conflict checks; updated WithOutputSchema to populate OutputSchema and enforce type "object".
Tests update for OutputSchema
mcp/tools_test.go
Updated tests to assert and roundtrip Tool.OutputSchema instead of Tool.RawOutputSchema; maintained checks for presence of outputSchema in JSON.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement UnmarshalJSON for Tool to deserialize outputSchema into RawOutputSchema [#563] No UnmarshalJSON for Tool added; changes focus on typed OutputSchema and marshaling, not deserialization.
Ensure clients can receive and store server outputSchema on Tool [#563] OutputSchema is now a public field and marshaling updated, but absence of Tool.UnmarshalJSON makes client-side deserialization status unclear.
Add tests for outputSchema deserialization on Tool [#563] Tests updated for OutputSchema usage but no explicit tests for Tool.UnmarshalJSON deserialization of outputSchema.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Introduced public Tool.OutputSchema and typed ToolArgumentsSchema, plus aliases ToolInputSchema/ToolOutputSchema (mcp/tools.go) The linked issue requests adding UnmarshalJSON to Tool; introducing new public schema types/fields is not required for that fix.
Modified WithOutputSchema to populate OutputSchema and force type "object" (mcp/tools.go) This alters tool construction behavior beyond implementing deserialization for outputSchema requested in the issue.
Added conflict check between RawOutputSchema and OutputSchema during MarshalJSON (mcp/tools.go) Error-path and validation changes are not part of the requested UnmarshalJSON implementation.

Possibly related issues

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@alex210501
Copy link
Contributor Author

As mentioned in the PR description, I've performed the change on the WithOutputSchema that basically set the OutputSchema from the passed type T, and RawOutputSchema is not set anymore.

This is quite different from what has been done on WithInputSchema(), so let me know your thought about this implementation, and if I need to perform a change in this method too 🤓

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcp/tools.go (1)

631-649: Include empty “properties” and explicit “additionalProperties”: false for object schemas
Per MCP spec, “properties” is optional; to satisfy OpenAI function-calling you must emit an empty object and disallow additional properties when type is “object.”
Apply:

 if tis.Properties != nil {
   m["properties"] = tis.Properties
 } else if tis.Type == "object" {
-  m["properties"] = map[string]any{}
+  m["properties"] = map[string]any{}
+  m["additionalProperties"] = false
 }
🧹 Nitpick comments (6)
mcp/tools.go (4)

568-574: Typed OutputSchema added—consider Unmarshal symmetry and raw retention

Default unmarshaling will populate OutputSchema, but we lose lossless round‑trip and backward access to RawOutputSchema. Suggest adding Tool.UnmarshalJSON to populate both OutputSchema (typed) and RawOutputSchema (raw) for symmetry with input and to fully address #563.

Example addition outside this hunk:

+// UnmarshalJSON implements custom JSON unmarshaling for Tool to retain raw and typed schemas.
+func (t *Tool) UnmarshalJSON(data []byte) error {
+	type wire struct {
+		Meta         *Meta             `json:"_meta,omitempty"`
+		Name         string            `json:"name"`
+		Description  string            `json:"description,omitempty"`
+		InputSchema  json.RawMessage   `json:"inputSchema"`
+		OutputSchema json.RawMessage   `json:"outputSchema,omitempty"`
+		Annotations  ToolAnnotation    `json:"annotations"`
+	}
+	var w wire
+	if err := json.Unmarshal(data, &w); err != nil {
+		return err
+	}
+	t.Meta = w.Meta
+	t.Name = w.Name
+	t.Description = w.Description
+	t.Annotations = w.Annotations
+
+	// Input schema: prefer typed, fall back to raw
+	if len(w.InputSchema) > 0 {
+		var typedIn ToolInputSchema
+		if err := json.Unmarshal(w.InputSchema, &typedIn); err == nil && typedIn.Type != "" {
+			t.InputSchema = typedIn
+			t.RawInputSchema = nil
+		} else {
+			t.InputSchema = ToolInputSchema{}
+			t.RawInputSchema = append(json.RawMessage(nil), w.InputSchema...)
+		}
+	}
+
+	// Output schema: populate typed and retain raw for lossless round-trip
+	if len(w.OutputSchema) > 0 {
+		_ = json.Unmarshal(w.OutputSchema, &t.OutputSchema) // best-effort
+		t.RawOutputSchema = append(json.RawMessage(nil), w.OutputSchema...)
+	}
+	return nil
+}

604-612: Conflict error mentions InputSchema for an OutputSchema conflict

The wrapped error message is misleading when both OutputSchema and RawOutputSchema are set.

Apply:

- if t.OutputSchema.Type != "" {
-   return nil, fmt.Errorf("tool %s has both OutputSchema and RawOutputSchema set: %w", t.Name, errToolSchemaConflict)
- }
+ if t.OutputSchema.Type != "" {
+   return nil, fmt.Errorf("tool %s has both OutputSchema and RawOutputSchema set: provide either OutputSchema or RawOutputSchema, not both", t.Name)
+ }

(Optional) Add a dedicated var:

+var errToolOutputSchemaConflict = errors.New("provide either OutputSchema or RawOutputSchema, not both")

And use it here.


619-629: Avoid lossy output schemas: include root-level additionalProperties (optional)

ToolArgumentsSchema drops root additionalProperties and similar keywords on unmarshal/remarshal.

Apply:

 type ToolArgumentsSchema struct {
   Defs       map[string]any `json:"$defs,omitempty"`
   Type       string         `json:"type"`
   Properties map[string]any `json:"properties,omitempty"`
   Required   []string       `json:"required,omitempty"`
+  AdditionalProperties any  `json:"additionalProperties,omitempty"`
 }

794-803: Harden WithOutputSchema: ensure non-nil map and validate type

Minor robustness: initialize Properties if nil; optionally validate non-object types.

 if err := json.Unmarshal(mcpSchema, &t.OutputSchema); err != nil {
   // Skip and maintain backward compatibility
   return
 }
 // Always set the type to "object" as of the current MCP spec
 t.OutputSchema.Type = "object"
+if t.OutputSchema.Properties == nil {
+  t.OutputSchema.Properties = map[string]any{}
+}
mcp/tools_test.go (2)

595-597: assert.NotNil on a struct is ineffective; assert concrete fields

OutputSchema is a struct and never nil. Assert Type/properties and JSON presence instead.

Apply:

- // Check that RawOutputSchema was set
- assert.NotNil(t, tool.OutputSchema)
+ // Check that OutputSchema was set as an object with properties
+ assert.Equal(t, "object", tool.OutputSchema.Type)
+ // Marshal and verify structure is present in JSON below

598-610: Add round-trip and conflict tests for output schema (covers #563 semantics)

Ensure unmarshaling populates OutputSchema and that dual schema conflicts error.

Proposed additions outside this hunk:

func TestUnmarshalToolWithOutputSchema(t *testing.T) {
	type TestOutput struct {
		Name string `json:"name"`
	}
	tool := NewTool("t", WithOutputSchema[TestOutput](), WithString("input", Required()))
	data, err := json.Marshal(tool)
	assert.NoError(t, err)

	var out Tool
	assert.NoError(t, json.Unmarshal(data, &out))
	assert.Equal(t, "t", out.Name)
	assert.Equal(t, "object", out.OutputSchema.Type)
}

func TestToolWithBothOutputSchemasError(t *testing.T) {
	type TestOutput struct{ X string `json:"x"` }
	tool := NewTool("dual-output", WithOutputSchema[TestOutput]())
	tool.RawOutputSchema = json.RawMessage(`{"type":"object","properties":{"y":{"type":"string"}}}`)
	_, err := json.Marshal(tool)
	assert.Error(t, err)
}

Want me to push these test updates?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4e353ac and 77aeb97.

📒 Files selected for processing (2)
  • mcp/tools.go (3 hunks)
  • mcp/tools_test.go (1 hunks)
🧰 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: 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.
📚 Learning: 2025-03-04T07:00:57.111Z
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.

Applied to files:

  • mcp/tools_test.go
  • mcp/tools.go
📚 Learning: 2025-03-04T06:59:43.882Z
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.

Applied to files:

  • mcp/tools_test.go
  • mcp/tools.go
📚 Learning: 2025-03-04T07:00:57.111Z
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.

Applied to files:

  • mcp/tools_test.go
  • mcp/tools.go

@ezynda3 ezynda3 merged commit b924391 into mark3labs:main Sep 2, 2025
4 checks passed
@matheuscscp
Copy link

@ezynda3 @alex210501 The commit b924391 is breaking the entire library, please see #572

@alex210501
Copy link
Contributor Author

@matheuscscp Taking a look

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.

bug: Tool struct missing UnmarshalJSON method prevents clients from receiving outputSchema

3 participants