Skip to content

Commit de1e833

Browse files
authored
Merge pull request #144 from nilcolor/output_schema_arrays
Allow output schema to be array of objects
2 parents f7cf080 + 4ee128a commit de1e833

File tree

5 files changed

+72
-38
lines changed

5 files changed

+72
-38
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,27 @@ 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+
567+
Please note: in this case, you must provide `type: "array"`. The default type
568+
for output schemas is `object`.
569+
549570
MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) specifies that:
550571

551572
- **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
@@ -7,23 +7,20 @@ class Tool
77
class OutputSchema
88
class ValidationError < StandardError; end
99

10-
attr_reader :properties, :required
10+
attr_reader :schema
1111

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

1818
def ==(other)
19-
other.is_a?(OutputSchema) && properties == other.properties && required == other.required
19+
other.is_a?(OutputSchema) && schema == other.schema
2020
end
2121

2222
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
23+
@schema
2724
end
2825

2926
def validate_result(result)
@@ -35,8 +32,24 @@ def validate_result(result)
3532

3633
private
3734

35+
def deep_transform_keys(schema, &block)
36+
case schema
37+
when Hash
38+
schema.each_with_object({}) do |(key, value), result|
39+
if key.casecmp?("$ref")
40+
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
41+
end
42+
43+
result[yield(key)] = deep_transform_keys(value, &block)
44+
end
45+
when Array
46+
schema.map { |e| deep_transform_keys(e, &block) }
47+
else
48+
schema
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)