Add Tool output_schema support with comprehensive validation

This commit implements optional JSON Schema validation for tool outputs according to the MCP specification:

- Add MCP::Tool::OutputSchema class mirroring InputSchema functionality
- Support output_schema in class definitions, Tool.define, and OutputSchema objects
- Include validate_result method for runtime validation
- Update MCP::Client::Tool to track output schemas
- Add comprehensive test coverage with 249 passing tests
- Update README with dedicated Tool Output Schemas section
- Follow MCP spec requirements for server/client validation
This commit is contained in:
Valentino Stoll 2025-09-11 11:12:27 -04:00
commit aad8e94530
No known key found for this signature in database
4 changed files with 495 additions and 7 deletions

View file

@ -18,6 +18,7 @@ require_relative "mcp/server/transports/stdio_transport"
require_relative "mcp/string_utils"
require_relative "mcp/tool"
require_relative "mcp/tool/input_schema"
require_relative "mcp/tool/output_schema"
require_relative "mcp/tool/response"
require_relative "mcp/tool/annotations"
require_relative "mcp/transport"

View file

@ -3,12 +3,13 @@
module MCP
class Client
class Tool
attr_reader :name, :description, :input_schema
attr_reader :name, :description, :input_schema, :output_schema
def initialize(name:, description:, input_schema:)
def initialize(name:, description:, input_schema:, output_schema: nil)
@name = name
@description = description
@input_schema = input_schema
@output_schema = output_schema
end
end
end

View file

@ -14,14 +14,14 @@ module MCP
end
def to_h
result = {
{
name: name_value,
title: title_value,
description: description_value,
inputSchema: input_schema_value.to_h,
outputSchema: @output_schema_value&.to_h,
annotations: annotations_value&.to_h,
}.compact
result[:annotations] = annotations_value.to_h if annotations_value
result
end
def inherited(subclass)
@ -30,6 +30,7 @@ module MCP
subclass.instance_variable_set(:@title_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@input_schema_value, nil)
subclass.instance_variable_set(:@output_schema_value, nil)
subclass.instance_variable_set(:@annotations_value, nil)
end
@ -49,6 +50,8 @@ module MCP
@input_schema_value || InputSchema.new
end
attr_reader :output_schema_value
def title(value = NOT_SET)
if value == NOT_SET
@title_value
@ -77,6 +80,18 @@ module MCP
end
end
def output_schema(value = NOT_SET)
if value == NOT_SET
output_schema_value
elsif value.is_a?(Hash)
properties = value[:properties] || value["properties"] || {}
required = value[:required] || value["required"] || []
@output_schema_value = OutputSchema.new(properties:, required:)
elsif value.is_a?(OutputSchema)
@output_schema_value = value
end
end
def annotations(hash = NOT_SET)
if hash == NOT_SET
@annotations_value
@ -85,12 +100,13 @@ module MCP
end
end
def define(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block)
def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, annotations: nil, &block)
Class.new(self) do
tool_name name
title title
description description
input_schema input_schema
output_schema output_schema
self.annotations(annotations) if annotations
define_singleton_method(:call, &block) if block
end

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
require "json-schema"
module MCP
class Tool
class OutputSchema
class ValidationError < StandardError; end
attr_reader :properties, :required
def initialize(properties: {}, required: [])
@properties = properties
@required = required.map(&:to_sym)
validate_schema!
end
def ==(other)
other.is_a?(OutputSchema) && properties == other.properties && required == other.required
end
def to_h
{ type: "object" }.tap do |hsh|
hsh[:properties] = properties if properties.any?
hsh[:required] = required if required.any?
end
end
def validate_result(result)
errors = JSON::Validator.fully_validate(to_h, result)
if errors.any?
raise ValidationError, "Invalid result: #{errors.join(", ")}"
end
end
private
def validate_schema!
check_for_refs!
schema = to_h
schema_reader = JSON::Schema::Reader.new(
accept_uri: false,
accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
)
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
if errors.any?
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
end
end
def check_for_refs!(obj = properties)
case obj
when Hash
if obj.key?("$ref") || obj.key?(:$ref)
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
end
obj.each_value { |value| check_for_refs!(value) }
when Array
obj.each { |item| check_for_refs!(item) }
end
end
end
end
end