Skip to content

Conversation

anandaroop
Copy link
Contributor

@anandaroop anandaroop commented Sep 26, 2025

This PR enables the inclusion of structuredContent within tool call responses.

result = get_some_result_object()

response = MCP::Tool::Response.new(structured_content: result)

Or, to conform with the spec's suggestion about backward compatibility, both content and structured_content may be provided:

response = MCP::Tool::Response.new(
  [{
    type: "text",
    text: result.to_json
  }],
  structured_content: result
)

Motivation and Context

Currently an MCP::Tool which declares an output_schema but does not provide structuredContent in its response is entirely incompatible with the many MCP clients that are built with the official Python SDK:

See: modelcontextprotocol/python-sdk/src/mcp/client/session.py

In order to make Ruby SDK MCP servers useable with such clients we need to support structuredContent in conformance with the spec for CallToolResult:

See: https://modelcontextprotocol.io/specification/2025-06-18/schema#calltoolresult
See: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content

@honzasterba had kindly submitted a similar PR #81 — this PR follows @koic's suggestion there.

How Has This Been Tested?

In addition to the unit tests, I have field tested this in an MCP server that utilizes this change in:

  • ✅ The MCP Inspector dev tool, which both validates the structuredContent against the output_schema and confirms that the structuredContent matches the serialized content
  • ✅ An agent built with Fast Agent, a framework which makes use of the official Python SDK cited above. This was failing before and motivated this PR. It now works, after this change
  • Claude Code, which does not require the structuredContent, so was fine before and after this change 🤷🏽

Breaking Changes

None.

The tool response can be constructed as before, or in the new style:

# ✅ still works
MCP::Tool::Response.new([{
  type: "text",
  text: result.to_json
}]) 

# ✅ possible now
MCP::Tool::Response.new(structured_content: result) 

# ✅ or both, for backwards compatibility 
MCP::Tool::Response.new(
  [{
    type: "text",
    text: result.to_json
  }],
  structured_content: result
) 

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I have not added documentation yet, but if this PR seems likely to be accepted I can definitely do so! Added now


if structured_content
@structured_content = structured_content
@content ||= [{ type: "text", text: structured_content.to_json }]
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not entirely sure if it should be responsibility of the gem to add the backwards-compatibility content chunk here. @koic @topherbullock thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was in response to this section of the spec:

5.2.6 Structured Content

Structured content is returned as a JSON object in the structuredContent field of a result.

For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block [emphasis added]

Without L21 the user is obligated to redundantly and possibly incorrectly supply the content block themselves.

Copy link
Contributor

Choose a reason for hiding this comment

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

But if we make the gem opinionated like this, there's no way for a gem user to not redundantly include the content chunk. I feel like the gem should let people do whatever they want.

Copy link
Contributor

Choose a reason for hiding this comment

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

This PR should just add support for structured content, and leave the potentially-helpful opinion bit as a follow-up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, fair enough. Given that it is a SHOULD and not a MUST, I think that is reasonable.

I've updated the branch:

  • removed the automatic content chunk
  • updated the README with sample usage of the new structured_content param
  • tested that everything still works in the downstream environments mentioned in the description

@anandaroop anandaroop force-pushed the anandaroop/structured-content branch from 04da1d8 to b629b58 Compare September 27, 2025 01:00
@atesgoral atesgoral merged commit f7cf080 into modelcontextprotocol:main Sep 27, 2025
5 checks passed
koic added a commit that referenced this pull request Oct 3, 2025
Follow-up to #147.

According to the specification schema, `content` is not optional and therefore cannot be omitted:

```typescript
interface CallToolResult {
  _meta?: { [key: string]: unknown };
  content: ContentBlock[];
  isError?: boolean;
  structuredContent?: { [key: string]: unknown };
  [key: string]: unknown;
}
```

https://modelcontextprotocol.io/specification/2025-06-18/schema#calltoolresult

Instead of `nil`, an empty array is set as the default value.

There may be a better value for `content`, but at the very least it should not be missing.
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.

2 participants