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:
parent
0645351585
commit
2eca3681df
3 changed files with 306 additions and 151 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue