Skip to content

feat: add structured output support for MCP tools #357

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

tzolov
Copy link
Contributor

@tzolov tzolov commented Jun 30, 2025

  • Add JsonSchemaValidator interface and DefaultJsonSchemaValidator implementation
  • Extend Tool schema to support outputSchema field for defining expected output structure
  • Add structuredContent field to CallToolResult for validated structured responses
  • Implement automatic validation of tool outputs against their defined schemas
  • Add comprehensive test coverage for structured output validation scenarios
  • Add json-schema-validator and json-unit-assertj dependencies for validation and testing
  • Update McpServer builders to accept custom JsonSchemaValidator instances
  • Ensure backward compatibility with existing tools without output schemas

Part of #285

This PR implements compulsory server-side validation for MCP tools with output schemas, adding support for JSON schema validation of tool outputs according to the MCP specification. Tools can now define output schemas that are automatically validated on the server side, ensuring structured responses conform to expected formats before being sent to clients.
Spec Reference: https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content

Motivation and Context

The MCP specification requires that servers with tools that have output schemas must provide structured results that conform to those schemas. This change implements that mandatory server-side validation requirement by:

  • Enabling tools to define expected output structure via JSON schemas
  • Automatically validating tool responses against their defined schemas on the server side
  • Providing clear error messages when validation fails
  • Ensuring backward compatibility with existing tools

How Has This Been Tested?

Test coverage has been added across all server transport implementations:

  • WebFluxSseIntegrationTests.java - Added 4 new test methods for structured output validation
  • WebMvcSseIntegrationTests.java - Added 4 new test methods for structured output validation
  • HttpServletSseServerTransportProviderIntegrationTests.java - Added 4 new test methods for structured output validation
  • McpSchemaTests.java - Added 5 new test methods for Tool schema serialization/deserialization with output schemas

Breaking Changes

No breaking changes. This is a backward-compatible addition:

  • Existing tools without output schemas continue to work unchanged
  • New outputSchema and structuredContent fields are optional
  • Default JsonSchemaValidator is provided automatically for server-side validation
  • All existing APIs maintain their current behavior

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

Implementation Details:

  • Added JsonSchemaValidator interface with DefaultJsonSchemaValidator implementation using json-schema-validator library
  • Extended Tool record with optional outputSchema field
  • Extended CallToolResult record with optional structuredContent field
  • Implemented StructuredOutputCallToolHandler wrapper that validates outputs automatically on the server side
  • Added comprehensive test utilities using json-unit-assertj for JSON assertions

Design Decisions:

  • Server-side validation is performed automatically when tools have output schemas defined
  • Invalid outputs are converted to error responses with descriptive messages before being sent to clients
  • Structured content is also serialized to text content for backward compatibility
  • Custom JsonSchemaValidator implementations can be provided via builder methods
  • Validation only occurs for tools that explicitly define output schemas
  • This implements the compulsory server-side validation requirement from the MCP specification

Dependencies Added:

  • json-schema-validator (1.5.7) for JSON schema validation on the server
  • json-unit-assertj for enhanced JSON testing capabilities

Scope:
This PR focuses exclusively on server-side validation as required by the MCP specification. Client-side validation is not included in this implementation.

- Add JsonSchemaValidator interface and DefaultJsonSchemaValidator implementation
- Extend Tool schema to support outputSchema field for defining expected output structure
- Add structuredContent field to CallToolResult for validated structured responses
- Implement automatic validation of tool outputs against their defined schemas
- Add comprehensive test coverage for structured output validation scenarios
- Add json-schema-validator and json-unit-assertj dependencies for validation and testing
- Update McpServer builders to accept custom JsonSchemaValidator instances
- Ensure backward compatibility with existing tools without output schemas

This implements the MCP specification requirement that tools with output schemas
must provide structured results conforming to those schemas, with automatic
validation and error handling for non-conforming outputs.
https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content

Signed-off-by: Christian Tzolov <[email protected]>
.containsEntry("operation", "2 + 3")
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
}
else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Didn't quite understand why this is necessary since we are hardcoding the structured content in the above result.

mcp/pom.xml Outdated
@@ -83,6 +83,15 @@
<artifactId>reactor-core</artifactId>
</dependency>

<!-- Json validator dependency.
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment doesn't seem right since this will always be a dependency for the default implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, this comment is incorrect.
I'm conscious of adding new dependencies to the MCP core. Initially I thought we could make it optional, but that would require moving the DefaultJsonSchemaValidator to a separate module or using reflection magic. This feels like overkill for now - we can always refactor later if needed.

// }
// }
return new CallToolResult(
"Tool call with non-empty outputSchema must have a result with structured content", true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Error message could be reworded for the client. Something like "Response missing structured content which is expected when calling tool with non-empty outputSchema"


// Check if validation passed
if (!validationResult.isEmpty()) {
logger.warn("Validation failed: structuredContent does not match tool outputSchema. "
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Since we already have validation failed in the error StructuredOutputCallToolHandler, maybe we can omit it in this message.

Comment on lines 363 to 364
// Map<String, Object> structuredOutput = new
// ObjectMapper().readValue(
Copy link
Contributor

Choose a reason for hiding this comment

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

If we do implicitly convert here, we should reuse the ObjectMapper between operations, since ObjectMapper instances have internal caches and are fairly expensive memory-wise.

Comment on lines 356 to 360
// if (!Utils.isEmpty(result.content())) {
// // TODO If the tesult.content() contains json text we can try to
// // convert it into a structured content (Experimental)
// var tc = result.content().stream().filter(c -> c instanceof
// McpSchema.TextContent).findFirst();
Copy link
Contributor

Choose a reason for hiding this comment

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

Ambivalent on this, I think it's surprising behavior with nontrivial overhead - IMO we should just pass through the raw result and allow consumers to migrate on their own.

I assume this is a mirror of converting structuredOutput into a TextContent block, but I think it's too expensive to justify.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree. Thats why I commented it out but forgot to remove it.

schemaNode.put("additionalProperties", false);
}

JsonSchema jsonSchema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth peeking inside to see what this is doing, in case it's worth caching this as a field.

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 topic of schema caching is important.
I will add some basic support.


private static final Logger logger = LoggerFactory.getLogger(DefaultJsonSchemaValidator.class);

private ObjectMapper objectMapper = new ObjectMapper();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: assigned value here appears redundant with default constructor

…ependencies

- Add schema caching to DefaultJsonSchemaValidator for better performance
- Make validator fields final and improve constructor design

Signed-off-by: Christian Tzolov <[email protected]>
}

// Validate the result against the output schema
var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent());
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 wonder if we need to make the sever json schema validation optional?
Would validation enabled by default have some regression impact on existing MCP server implementations?

Copy link
Contributor

@pantanurag555 pantanurag555 Jul 2, 2025

Choose a reason for hiding this comment

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

There shouldn't be any regression since none of the existing servers will have tools with output schemas.

Copy link
Contributor

Choose a reason for hiding this comment

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

I can see a niche use case for making it optional, for example if you're writing an MCP server proxy and want to force the upstream server to handle validation instead.

Also think it's fine to enable by default.

}

@Override
public ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we should extend the signature to include an unique schemaName. Then we can expose a method purgeCache(schemaName) or similar

private final JsonSchemaFactory schemaFactory;

// TODO: Implement a strategy to purge the cache (TTL, size limit, etc.)
private final ConcurrentHashMap<String, JsonSchema> schemaCache;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe some simple, interval based purge all strategy would be good enough.

@tzolov
Copy link
Contributor Author

tzolov commented Jul 2, 2025

I moved the JsonSchemaValidator classes from server to spec package. So we can reuse them for other validation cases.

* @throws IllegalArgumentException if jsonSchemaValidator is null
*/
public AsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonSchemaValidator) {
Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: might be nice to add @Nullable and then use the JetBrains @Contract annotation in the Assert class to have control flow-aware null warnings in the IDE. Not urgent or required though.

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.

3 participants