Skip to content

Commit 41964a6

Browse files
committed
Allow output schema to be array of objects
closes #142
1 parent aba6163 commit 41964a6

File tree

5 files changed

+71
-38
lines changed

5 files changed

+71
-38
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,26 @@ class DataTool < MCP::Tool
546546
end
547547
```
548548

549+
Output schema may also describe an array of objects:
550+
551+
```ruby
552+
class WeatherTool < MCP::Tool
553+
output_schema(
554+
type: "array",
555+
item: {
556+
properties: {
557+
temperature: { type: "number" },
558+
condition: { type: "string" },
559+
humidity: { type: "integer" }
560+
},
561+
required: ["temperature", "condition", "humidity"]
562+
}
563+
)
564+
end
565+
```
566+
Please note: in this case, you must provide `type: "array"`. The default type
567+
for output schemas is `object`.
568+
549569
MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) specifies that:
550570

551571
- **Server Validation**: Servers MUST provide structured results that conform to the output schema

lib/mcp/tool.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ def output_schema(value = NOT_SET)
8484
if value == NOT_SET
8585
output_schema_value
8686
elsif value.is_a?(Hash)
87-
properties = value[:properties] || value["properties"] || {}
88-
required = value[:required] || value["required"] || []
89-
@output_schema_value = OutputSchema.new(properties:, required:)
87+
@output_schema_value = OutputSchema.new(value)
9088
elsif value.is_a?(OutputSchema)
9189
@output_schema_value = value
9290
end

lib/mcp/tool/output_schema.rb

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
11
# frozen_string_literal: true
22

3+
require "json"
34
require "json-schema"
45

56
module MCP
67
class Tool
78
class OutputSchema
89
class ValidationError < StandardError; end
910

10-
attr_reader :properties, :required
11+
attr_reader :schema
1112

12-
def initialize(properties: {}, required: [])
13-
@properties = properties
14-
@required = required.map(&:to_sym)
13+
def initialize(schema = {})
14+
@schema = deep_transform_keys(JSON.parse(JSON.dump(schema)), &:to_sym)
15+
@schema[:type] ||= "object"
1516
validate_schema!
1617
end
1718

1819
def ==(other)
19-
other.is_a?(OutputSchema) && properties == other.properties && required == other.required
20+
other.is_a?(OutputSchema) && schema == other.schema
2021
end
2122

2223
def to_h
23-
{ type: "object" }.tap do |hsh|
24-
hsh[:properties] = properties if properties.any?
25-
hsh[:required] = required if required.any?
26-
end
24+
@schema
2725
end
2826

2927
def validate_result(result)
@@ -35,8 +33,23 @@ def validate_result(result)
3533

3634
private
3735

36+
def deep_transform_keys(hash, &block)
37+
case hash
38+
when Hash
39+
hash.each_with_object({}) do |(key, value), result|
40+
if key.casecmp?("$ref") || key == :$ref
41+
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
42+
end
43+
result[yield(key)] = deep_transform_keys(value, &block)
44+
end
45+
when Array
46+
hash.map { |e| deep_transform_keys(e, &block) }
47+
else
48+
hash
49+
end
50+
end
51+
3852
def validate_schema!
39-
check_for_refs!
4053
schema = to_h
4154
schema_reader = JSON::Schema::Reader.new(
4255
accept_uri: false,
@@ -48,19 +61,6 @@ def validate_schema!
4861
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
4962
end
5063
end
51-
52-
def check_for_refs!(obj = properties)
53-
case obj
54-
when Hash
55-
if obj.key?("$ref") || obj.key?(:$ref)
56-
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
57-
end
58-
59-
obj.each_value { |value| check_for_refs!(value) }
60-
when Array
61-
obj.each { |item| check_for_refs!(item) }
62-
end
63-
end
6464
end
6565
end
6666
end

test/mcp/tool/output_schema_test.rb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@
55
module MCP
66
class Tool
77
class OutputSchemaTest < ActiveSupport::TestCase
8-
test "required arguments are converted to symbols" do
9-
output_schema = OutputSchema.new(properties: { result: { type: "string" } }, required: ["result"])
10-
assert_equal [:result], output_schema.required
11-
end
12-
138
test "to_h returns a hash representation of the output schema" do
149
output_schema = OutputSchema.new(properties: { result: { type: "string" } }, required: [:result])
1510
assert_equal(
16-
{ type: "object", properties: { result: { type: "string" } }, required: [:result] },
11+
{ type: "object", properties: { result: { type: "string" } }, required: ["result"] },
1712
output_schema.to_h,
1813
)
1914
end
@@ -41,7 +36,7 @@ class OutputSchemaTest < ActiveSupport::TestCase
4136

4237
test "valid schema initialization" do
4338
schema = OutputSchema.new(properties: { foo: { type: "string" } }, required: [:foo])
44-
assert_equal({ type: "object", properties: { foo: { type: "string" } }, required: [:foo] }, schema.to_h)
39+
assert_equal({ type: "object", properties: { foo: { type: "string" } }, required: ["foo"] }, schema.to_h)
4540
end
4641

4742
test "invalid schema raises argument error" do
@@ -135,6 +130,26 @@ class OutputSchemaTest < ActiveSupport::TestCase
135130
schema.validate_result(invalid_result)
136131
end
137132
end
133+
134+
test "allow to declare array schemas" do
135+
schema = OutputSchema.new({
136+
type: "array",
137+
items: {
138+
properties: { foo: { type: "string" } },
139+
required: [:foo]
140+
}
141+
})
142+
assert_equal(
143+
{
144+
type: "array",
145+
items: {
146+
properties: { foo: { type: "string" } },
147+
required: ["foo"],
148+
}
149+
},
150+
schema.to_h
151+
)
152+
end
138153
end
139154
end
140155
end

test/mcp/tool_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def call(message, server_context: nil)
267267
title: "Mock Tool",
268268
description: "a mock tool for testing",
269269
inputSchema: { type: "object" },
270-
outputSchema: { type: "object", properties: { result: { type: "string" } }, required: [:result] },
270+
outputSchema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] },
271271
}
272272
assert_equal expected, tool.to_h
273273
end
@@ -292,7 +292,7 @@ class HashOutputSchemaTool < Tool
292292
end
293293

294294
tool = HashOutputSchemaTool
295-
expected = { type: "object", properties: { result: { type: "string" } }, required: [:result] }
295+
expected = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
296296
assert_equal expected, tool.output_schema.to_h
297297
end
298298

@@ -302,7 +302,7 @@ class OutputSchemaObjectTool < Tool
302302
end
303303

304304
tool = OutputSchemaObjectTool
305-
expected = { type: "object", properties: { result: { type: "string" } }, required: [:result] }
305+
expected = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
306306
assert_equal expected, tool.output_schema.to_h
307307
end
308308

@@ -354,7 +354,7 @@ class OutputSchemaObjectTool < Tool
354354
assert_equal "mock_tool", tool.name_value
355355
assert_equal "a mock tool for testing", tool.description
356356
assert_instance_of Tool::OutputSchema, tool.output_schema
357-
expected_output_schema = { type: "object", properties: { result: { type: "string" } }, required: [:result] }
357+
expected_output_schema = { type: "object", properties: { result: { type: "string" } }, required: ["result"] }
358358
assert_equal expected_output_schema, tool.output_schema.to_h
359359
end
360360

@@ -379,7 +379,7 @@ def call(message:, server_context: nil)
379379
expected_input = { type: "object", properties: { message: { type: "string" } }, required: [:message] }
380380
assert_equal expected_input, tool.input_schema.to_h
381381

382-
expected_output = { type: "object", properties: { result: { type: "string" }, success: { type: "boolean" } }, required: [:result, :success] }
382+
expected_output = { type: "object", properties: { result: { type: "string" }, success: { type: "boolean" } }, required: ["result", "success"] }
383383
assert_equal expected_output, tool.output_schema.to_h
384384
end
385385
end

0 commit comments

Comments
 (0)