Refactor schema validation to reduce duplication and add additionalProperties support
This refactoring extracts common schema validation logic into a base Schema class, eliminating ~100 lines of duplicate code between InputSchema and OutputSchema. It also adds support for the JSON Schema `additionalProperties` keyword to allow fine-grained control over extra properties in tool arguments. Previously `additionalParameters` could be set only on OutputSchema. Key changes: - Created MCP::Tool::Schema base class consolidating: - Schema validation against JSON Schema Draft 4 metaschema - Deep key transformation (string to symbol conversion) - $ref disallowance checking - Common validation methods - Refactored InputSchema to inherit from Schema: - Simplified constructor to accept full schema hash - Changed required fields storage from symbols to strings - Removed ~40 lines of duplicated validation logic - Refactored OutputSchema to inherit from Schema: - Removed ~54 lines of duplicated validation logic - Now shares all common functionality with InputSchema - Updated Tool.input_schema setter: - Simplified to pass full hash directly to InputSchema - Added additionalProperties support: - By default, tools now allow additional properties (JSON Schema default) - Can explicitly set `additionalProperties: false` to disallow extras - Added comprehensive test coverage for both behaviors
This commit is contained in:
parent
85b81a6a65
commit
a3c24d2bcc
1 changed files with 240 additions and 162 deletions
|
|
@ -1096,6 +1096,62 @@ module MCP
|
|||
assert_equal "OK", response[:result][:content][0][:content]
|
||||
end
|
||||
|
||||
test "tools/call allows additional properties by default" do
|
||||
server = Server.new(
|
||||
tools: [TestTool],
|
||||
configuration: Configuration.new(validate_tool_call_arguments: true),
|
||||
)
|
||||
|
||||
response = server.handle(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "test_tool",
|
||||
arguments: {
|
||||
message: "Hello, world!",
|
||||
other_property: "I am allowed",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert_equal "2.0", response[:jsonrpc]
|
||||
assert_equal 1, response[:id]
|
||||
assert response[:result], "Expected result key in response"
|
||||
assert_equal "text", response[:result][:content][0][:type]
|
||||
assert_equal "OK", response[:result][:content][0][:content]
|
||||
end
|
||||
|
||||
test "tools/call disallows additional properties when additionalProperties set to false" do
|
||||
server = Server.new(
|
||||
tools: [TestToolWithAdditionalPropertiesSetToFalse],
|
||||
configuration: Configuration.new(validate_tool_call_arguments: true),
|
||||
)
|
||||
|
||||
response = server.handle(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "test_tool_with_additional_properties_set_to_false",
|
||||
arguments: {
|
||||
message: "Hello, world!",
|
||||
extra: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert_equal "2.0", response[:jsonrpc]
|
||||
assert_equal 1, response[:id]
|
||||
assert_nil response[:error], "Expected no JSON-RPC error"
|
||||
assert response[:result][:isError]
|
||||
assert_includes response[:result][:content][0][:text], "Invalid arguments"
|
||||
end
|
||||
|
||||
test "tools/call with no args" do
|
||||
server = Server.new(tools: [@tool_with_no_args])
|
||||
|
||||
|
|
@ -1123,7 +1179,19 @@ module MCP
|
|||
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
|
||||
|
||||
class << self
|
||||
def call(message:, server_context: nil)
|
||||
def call(server_context: nil, **kwargs)
|
||||
Tool::Response.new([{ type: "text", content: "OK" }])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestToolWithAdditionalPropertiesSetToFalse < Tool
|
||||
tool_name "test_tool_with_additional_properties_set_to_false"
|
||||
description "a test tool with additionalProperties set to false for testing"
|
||||
input_schema({ properties: { message: { type: "string" } }, required: ["message"], additionalProperties: false })
|
||||
|
||||
class << self
|
||||
def call(server_context: nil, **kwargs)
|
||||
Tool::Response.new([{ type: "text", content: "OK" }])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue