Internal Release 0.4.0

- Added support for handle_json method from JSON RPC gem to allow consumers to pass JSON strings
- Added context parameter to prompt templating system

- Updated tool and prompt registration to use class references instead of instances
- Changed tool response is_error to isError to match protocol specification
This commit is contained in:
Topher Bullock 2025-03-25 10:28:31 -04:00
commit 2eca3681df
3 changed files with 306 additions and 151 deletions

View file

@ -11,19 +11,21 @@ module ModelContextProtocol
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
]
def template(args)
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new("Hello, friend!")),
],
)
class << self
def template(args, context:)
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new("Hello, friend!")),
],
)
end
end
end
test "#template returns a Result with description and messages" do
prompt = TestPrompt.new
prompt = TestPrompt
expected_template_result = {
description: "Hello, world!",
@ -33,7 +35,7 @@ module ModelContextProtocol
],
}
result = prompt.template({ "test_argument" => "Hello, friend!" })
result = prompt.template({ "test_argument" => "Hello, friend!" }, context: { user_id: 123 })
assert_equal expected_template_result, result.to_h
end
@ -46,20 +48,22 @@ module ModelContextProtocol
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
]
def template(args)
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new(args["test_argument"])),
],
)
class << self
def template(args, context:)
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new(args["test_argument"])),
],
)
end
end
end
prompt = MockPrompt.new
prompt = MockPrompt
assert_equal "my_mock_prompt", prompt.name
assert_equal "my_mock_prompt", prompt.name_value
assert_equal "a mock prompt for testing", prompt.description
assert_equal "test_argument", prompt.arguments.first.name
assert_equal "Test argument", prompt.arguments.first.description
@ -73,7 +77,7 @@ module ModelContextProtocol
],
}
result = prompt.template({ "test_argument" => "Hello, friend!" })
result = prompt.template({ "test_argument" => "Hello, friend!" }, context: { user_id: 123 })
assert_equal expected_template_result, result.to_h
end
@ -84,20 +88,22 @@ module ModelContextProtocol
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
]
def template(args)
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new(args["test_argument"])),
],
)
class << self
def template(args, context:)
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new(args["test_argument"])),
],
)
end
end
end
prompt = DefaultNamePrompt.new
prompt = DefaultNamePrompt
assert_equal "default_name_prompt", prompt.name
assert_equal "default_name_prompt", prompt.name_value
assert_equal "a mock prompt for testing", prompt.description
assert_equal "test_argument", prompt.arguments.first.name
end
@ -109,17 +115,19 @@ module ModelContextProtocol
arguments: [
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
],
) do |args|
) do |args, context:|
content = Content::Text.new(args["test_argument"] + " user: #{context[:user_id]}")
Prompt::Result.new(
description: "Hello, world!",
messages: [
Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
Prompt::Message.new(role: "assistant", content: Content::Text.new(args["test_argument"])),
Prompt::Message.new(role: "assistant", content:),
],
)
end
assert_equal "mock_prompt", prompt.name
assert_equal "mock_prompt", prompt.name_value
assert_equal "a mock prompt for testing", prompt.description
assert_equal "test_argument", prompt.arguments.first.name
@ -127,11 +135,11 @@ module ModelContextProtocol
description: "Hello, world!",
messages: [
{ role: "user", content: { text: "Hello, world!" } },
{ role: "assistant", content: { text: "Hello, friend!" } },
{ role: "assistant", content: { text: "Hello, friend! user: 123" } },
],
}
result = prompt.template({ "test_argument" => "Hello, friend!" })
result = prompt.template({ "test_argument" => "Hello, friend!" }, context: { user_id: 123 })
assert_equal expected, result.to_h
end
end

View file

@ -1,12 +1,13 @@
# frozen_string_literal: true
require "test_helper"
require "json"
module ModelContextProtocol
class ServerTest < ActiveSupport::TestCase
include InstrumentationTestHelper
setup do
@tool = Tool.new(name: "test_tool", description: "Test tool", input_schema: {})
@tool = Tool.define(name: "test_tool", description: "Test tool", input_schema: {})
@prompt = Prompt.define(
name: "test_prompt",
@ -64,6 +65,25 @@ module ModelContextProtocol
assert_instrumentation_data({ method: "ping" })
end
test "#handle_json ping request returns empty response" do
request = JSON.generate({
jsonrpc: "2.0",
method: "ping",
id: "123",
})
response = JSON.parse(@server.handle_json(request), symbolize_names: true)
assert_equal(
{
"jsonrpc": "2.0",
"id": "123",
"result": {},
},
response,
)
assert_instrumentation_data({ method: "ping" })
end
test "#handle initialize request returns protocol info, server info, and capabilities" do
request = {
jsonrpc: "2.0",
@ -121,6 +141,20 @@ module ModelContextProtocol
assert_instrumentation_data({ method: "tools/list" })
end
test "#handle_json tools/list returns available tools" do
request = JSON.generate({
jsonrpc: "2.0",
method: "tools/list",
id: 1,
})
response = JSON.parse(@server.handle_json(request), symbolize_names: true)
result = response[:result]
assert_kind_of Array, result[:tools]
assert_equal "test_tool", result[:tools][0][:name]
assert_equal "Test tool", result[:tools][0][:description]
end
test "#tools_list_handler sets the tools/list handler" do
@server.tools_list_handler do
[{ name: "hammer", description: "Hammer time!" }]
@ -160,6 +194,25 @@ module ModelContextProtocol
assert_instrumentation_data({ method: "tools/call", tool_name: })
end
test "#handle_json tools/call executes tool and returns result" do
tool_name = "test_tool"
tool_args = { arg: "value" }
tool_response = Tool::Response.new([{ result: "success" }])
@tool.expects(:call).with(**tool_args, context: @server.context).returns(tool_response)
request = JSON.generate({
jsonrpc: "2.0",
method: "tools/call",
params: { name: tool_name, arguments: tool_args },
id: 1,
})
response = JSON.parse(@server.handle_json(request), symbolize_names: true)
assert_equal tool_response.to_h, response[:result]
assert_instrumentation_data({ method: "tools/call", tool_name: })
end
test "#handle tools/call returns internal error and reports exception if the tool raises an error" do
exception = StandardError.new("Tool error")
@tool.expects(:call).raises(exception)
@ -189,6 +242,26 @@ module ModelContextProtocol
assert_instrumentation_data({ method: "tools/call", tool_name: "test_tool", error: :internal_error })
end
test "#handle_json returns internal error and reports exception if the tool raises an error" do
exception = StandardError.new("Tool error")
@tool.expects(:call).raises(exception)
request = JSON.generate({
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "test_tool",
arguments: {},
},
id: 1,
})
response = JSON.parse(@server.handle_json(request), symbolize_names: true)
assert_equal "Internal error", response[:error][:message]
assert_equal "Internal error calling tool test_tool", response[:error][:data]
assert_instrumentation_data({ method: "tools/call", tool_name: "test_tool", error: :internal_error })
end
test "#handle tools/call returns error for unknown tool" do
request = {
jsonrpc: "2.0",
@ -206,6 +279,21 @@ module ModelContextProtocol
assert_instrumentation_data({ method: "tools/call", error: :tool_not_found })
end
test "#handle_json returns error for unknown tool" do
request = JSON.generate({
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "unknown_tool",
arguments: {},
},
id: 1,
})
response = JSON.parse(@server.handle_json(request), symbolize_names: true)
assert_equal "Internal error", response[:error][:message]
end
test "#tools_call_handler sets the tools/call handler" do
@server.tools_call_handler do |request|
tool_name = request[:name]
@ -220,7 +308,7 @@ module ModelContextProtocol
}
response = @server.handle(request)
assert_equal({ content: "my_tool called successfully", is_error: false }, response[:result])
assert_equal({ content: "my_tool called successfully", isError: false }, response[:result])
assert_instrumentation_data({ method: "tools/call", tool_name: "my_tool" })
end

View file

@ -9,18 +9,20 @@ module ModelContextProtocol
description "a test tool for testing"
input_schema [{ type: "text", name: "message" }]
def call(message)
Tool::Response.new([{ type: "text", content: "OK" }])
class << self
def call(message)
Tool::Response.new([{ type: "text", content: "OK" }])
end
end
end
test "#to_h returns a hash with name, description, and inputSchema" do
tool = Tool.new(name: "mock_tool", description: "a mock tool for testing")
tool = Tool.define(name: "mock_tool", description: "a mock tool for testing")
assert_equal tool.to_h, { name: "mock_tool", description: "a mock tool for testing", inputSchema: nil }
end
test "#call invokes the tool block and returns the response" do
tool = TestTool.new
tool = TestTool
response = tool.call("test")
assert_equal response.content, [{ type: "text", content: "OK" }]
assert_equal response.is_error, false
@ -33,8 +35,8 @@ module ModelContextProtocol
input_schema [{ type: "text", name: "message" }]
end
tool = MockTool.new
assert_equal tool.name, "my_mock_tool"
tool = MockTool
assert_equal tool.name_value, "my_mock_tool"
assert_equal tool.description, "a mock tool for testing"
assert_equal tool.input_schema, [{ type: "text", name: "message" }]
end
@ -45,9 +47,9 @@ module ModelContextProtocol
input_schema [{ type: "text", name: "message" }]
end
tool = DefaultNameTool.new
tool = DefaultNameTool
assert_equal tool.name, "default_name_tool"
assert_equal tool.name_value, "default_name_tool"
assert_equal tool.description, "a mock tool for testing"
assert_equal tool.input_schema, [{ type: "text", name: "message" }]
end
@ -57,7 +59,7 @@ module ModelContextProtocol
Tool::Response.new([{ type: "text", content: "OK" }])
end
assert_equal tool.name, "mock_tool"
assert_equal tool.name_value, "mock_tool"
assert_equal tool.description, "a mock tool for testing"
assert_equal tool.input_schema, nil
end