Merge pull request #28 from jcat4/add-basic-http-client-support
Add basic http client support
This commit is contained in:
commit
97640ed626
9 changed files with 609 additions and 8 deletions
10
Gemfile
10
Gemfile
|
|
@ -6,9 +6,6 @@ source "https://rubygems.org"
|
|||
gemspec
|
||||
|
||||
# Specify development dependencies below
|
||||
gem "minitest", "~> 5.1", require: false
|
||||
gem "mocha"
|
||||
|
||||
gem "rubocop-minitest", require: false
|
||||
gem "rubocop-rake", require: false
|
||||
gem "rubocop-shopify", require: false
|
||||
|
|
@ -21,3 +18,10 @@ gem "activesupport"
|
|||
gem "debug"
|
||||
gem "rake", "~> 13.0"
|
||||
gem "sorbet-static-and-runtime"
|
||||
|
||||
group :test do
|
||||
gem "faraday", ">= 2.0"
|
||||
gem "minitest", "~> 5.1", require: false
|
||||
gem "mocha"
|
||||
gem "webmock"
|
||||
end
|
||||
|
|
|
|||
108
README.md
108
README.md
|
|
@ -22,7 +22,9 @@ Or install it yourself as:
|
|||
$ gem install mcp
|
||||
```
|
||||
|
||||
## MCP Server
|
||||
You may need to add additional dependencies depending on which features you wish to access.
|
||||
|
||||
## Building an MCP Server
|
||||
|
||||
The `MCP::Server` class is the core component that handles JSON-RPC requests and responses.
|
||||
It implements the Model Context Protocol specification, handling model context requests and responses.
|
||||
|
|
@ -218,7 +220,7 @@ $ ruby examples/stdio_server.rb
|
|||
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
### Configuration
|
||||
|
||||
The gem can be configured using the `MCP.configure` block:
|
||||
|
||||
|
|
@ -365,7 +367,7 @@ When an exception occurs:
|
|||
|
||||
If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
|
||||
|
||||
## Tools
|
||||
### Tools
|
||||
|
||||
MCP spec includes [Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) which provide functionality to LLM apps.
|
||||
|
||||
|
|
@ -430,7 +432,7 @@ Tools can include annotations that provide additional metadata about their behav
|
|||
|
||||
Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
|
||||
|
||||
## Prompts
|
||||
### Prompts
|
||||
|
||||
MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
|
||||
|
||||
|
|
@ -556,7 +558,7 @@ The data contains the following keys:
|
|||
`tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
|
||||
This is to avoid potential issues with metric cardinality
|
||||
|
||||
## Resources
|
||||
### Resources
|
||||
|
||||
MCP spec includes [Resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources).
|
||||
|
||||
|
|
@ -612,6 +614,102 @@ server = MCP::Server.new(
|
|||
)
|
||||
```
|
||||
|
||||
## Building an MCP Client
|
||||
|
||||
The `MCP::Client` class provides an interface for interacting with MCP servers.
|
||||
|
||||
This class supports:
|
||||
|
||||
- Tool listing via the `tools/list` method
|
||||
- Tool invocation via the `tools/call` method
|
||||
- Automatic JSON-RPC 2.0 message formatting
|
||||
- UUID request ID generation
|
||||
|
||||
Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
|
||||
Authorization is handled by the transport layer.
|
||||
|
||||
## Transport Layer Interface
|
||||
|
||||
If the transport layer you need is not included in the gem, you can build and pass your own instances so long as they conform to the following interface:
|
||||
|
||||
```ruby
|
||||
class CustomTransport
|
||||
# Sends a JSON-RPC request to the server and returns the raw response.
|
||||
#
|
||||
# @param request [Hash] A complete JSON-RPC request object.
|
||||
# https://www.jsonrpc.org/specification#request_object
|
||||
# @return [Hash] A hash modeling a JSON-RPC response object.
|
||||
# https://www.jsonrpc.org/specification#response_object
|
||||
def send_request(request:)
|
||||
# Your transport-specific logic here
|
||||
# - HTTP: POST to endpoint with JSON body
|
||||
# - WebSocket: Send message over WebSocket
|
||||
# - stdio: Write to stdout, read from stdin
|
||||
# - etc.
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### HTTP Transport Layer
|
||||
|
||||
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
|
||||
|
||||
You'll need to add `faraday` as a dependency in order to use the HTTP transport layer:
|
||||
|
||||
```ruby
|
||||
gem 'mcp'
|
||||
gem 'faraday', '>= 2.0'
|
||||
```
|
||||
|
||||
Example usage:
|
||||
|
||||
```ruby
|
||||
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
|
||||
client = MCP::Client.new(transport: http_transport)
|
||||
|
||||
# List available tools
|
||||
tools = client.tools
|
||||
tools.each do |tool|
|
||||
puts <<~TOOL_INFORMATION
|
||||
Tool: #{tool.name}
|
||||
Description: #{tool.description}
|
||||
Input Schema: #{tool.input_schema}
|
||||
TOOL_INFORMATION
|
||||
end
|
||||
|
||||
# Call a specific tool
|
||||
response = client.call_tool(
|
||||
tool: tools.first,
|
||||
arguments: { message: "Hello, world!" }
|
||||
)
|
||||
```
|
||||
|
||||
#### HTTP Authorization
|
||||
|
||||
By default, the HTTP transport layer provides no authentication to the server, but you can provide custom headers if you need authentication. For example, to use Bearer token authentication:
|
||||
|
||||
```ruby
|
||||
http_transport = MCP::Client::HTTP.new(
|
||||
url: "https://api.example.com/mcp",
|
||||
headers: {
|
||||
"Authorization" => "Bearer my_token"
|
||||
}
|
||||
)
|
||||
|
||||
client = MCP::Client.new(transport: http_transport)
|
||||
client.tools # will make the call using Bearer auth
|
||||
```
|
||||
|
||||
You can add any custom headers needed for your authentication scheme, or for any other purpose. The client will include these headers on every request.
|
||||
|
||||
### Tool Objects
|
||||
|
||||
The client provides a wrapper class for tools returned by the server:
|
||||
|
||||
- `MCP::Client::Tool` - Represents a single tool with its metadata
|
||||
|
||||
This class provide easy access to tool properties like name, description, and input schema.
|
||||
|
||||
## Releases
|
||||
|
||||
This gem is published to [RubyGems.org](https://rubygems.org/gems/mcp)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ require_relative "mcp/tool/response"
|
|||
require_relative "mcp/tool/annotations"
|
||||
require_relative "mcp/transport"
|
||||
require_relative "mcp/version"
|
||||
require_relative "mcp/client"
|
||||
require_relative "mcp/client/http"
|
||||
require_relative "mcp/client/tool"
|
||||
|
||||
module MCP
|
||||
class << self
|
||||
|
|
|
|||
88
lib/mcp/client.rb
Normal file
88
lib/mcp/client.rb
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MCP
|
||||
class Client
|
||||
# Initializes a new MCP::Client instance.
|
||||
#
|
||||
# @param transport [Object] The transport object to use for communication with the server.
|
||||
# The transport should be a duck type that responds to `send_request`. See the README for more details.
|
||||
#
|
||||
# @example
|
||||
# transport = MCP::Client::HTTP.new(url: "http://localhost:3000")
|
||||
# client = MCP::Client.new(transport: transport)
|
||||
def initialize(transport:)
|
||||
@transport = transport
|
||||
end
|
||||
|
||||
# The user may want to access additional transport-specific methods/attributes
|
||||
# So keeping it public
|
||||
attr_reader :transport
|
||||
|
||||
# Returns the list of tools available from the server.
|
||||
# Each call will make a new request – the result is not cached.
|
||||
#
|
||||
# @return [Array<MCP::Client::Tool>] An array of available tools.
|
||||
#
|
||||
# @example
|
||||
# tools = client.tools
|
||||
# tools.each do |tool|
|
||||
# puts tool.name
|
||||
# end
|
||||
def tools
|
||||
response = transport.send_request(request: {
|
||||
jsonrpc: JsonRpcHandler::Version::V2_0,
|
||||
id: request_id,
|
||||
method: "tools/list",
|
||||
})
|
||||
|
||||
response.dig("result", "tools")&.map do |tool|
|
||||
Tool.new(
|
||||
name: tool["name"],
|
||||
description: tool["description"],
|
||||
input_schema: tool["inputSchema"],
|
||||
)
|
||||
end || []
|
||||
end
|
||||
|
||||
# Calls a tool via the transport layer.
|
||||
#
|
||||
# @param tool [MCP::Client::Tool] The tool to be called.
|
||||
# @param arguments [Object, nil] The arguments to pass to the tool.
|
||||
# @return [Object] The result of the tool call, as returned by the transport.
|
||||
#
|
||||
# @example
|
||||
# tool = client.tools.first
|
||||
# result = client.call_tool(tool: tool, arguments: { foo: "bar" })
|
||||
#
|
||||
# @note
|
||||
# The exact requirements for `arguments` are determined by the transport layer in use.
|
||||
# Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
|
||||
def call_tool(tool:, arguments: nil)
|
||||
response = transport.send_request(request: {
|
||||
jsonrpc: JsonRpcHandler::Version::V2_0,
|
||||
id: request_id,
|
||||
method: "tools/call",
|
||||
params: { name: tool.name, arguments: arguments },
|
||||
})
|
||||
|
||||
response.dig("result", "content")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_id
|
||||
SecureRandom.uuid
|
||||
end
|
||||
|
||||
class RequestHandlerError < StandardError
|
||||
attr_reader :error_type, :original_error, :request
|
||||
|
||||
def initialize(message, request, error_type: :internal_error, original_error: nil)
|
||||
super(message)
|
||||
@request = request
|
||||
@error_type = error_type
|
||||
@original_error = original_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
88
lib/mcp/client/http.rb
Normal file
88
lib/mcp/client/http.rb
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MCP
|
||||
class Client
|
||||
class HTTP
|
||||
attr_reader :url
|
||||
|
||||
def initialize(url:, headers: {})
|
||||
@url = url
|
||||
@headers = headers
|
||||
end
|
||||
|
||||
def send_request(request:)
|
||||
method = request[:method] || request["method"]
|
||||
params = request[:params] || request["params"]
|
||||
|
||||
client.post("", request).body
|
||||
rescue Faraday::BadRequestError => e
|
||||
raise RequestHandlerError.new(
|
||||
"The #{method} request is invalid",
|
||||
{ method:, params: },
|
||||
error_type: :bad_request,
|
||||
original_error: e,
|
||||
)
|
||||
rescue Faraday::UnauthorizedError => e
|
||||
raise RequestHandlerError.new(
|
||||
"You are unauthorized to make #{method} requests",
|
||||
{ method:, params: },
|
||||
error_type: :unauthorized,
|
||||
original_error: e,
|
||||
)
|
||||
rescue Faraday::ForbiddenError => e
|
||||
raise RequestHandlerError.new(
|
||||
"You are forbidden to make #{method} requests",
|
||||
{ method:, params: },
|
||||
error_type: :forbidden,
|
||||
original_error: e,
|
||||
)
|
||||
rescue Faraday::ResourceNotFound => e
|
||||
raise RequestHandlerError.new(
|
||||
"The #{method} request is not found",
|
||||
{ method:, params: },
|
||||
error_type: :not_found,
|
||||
original_error: e,
|
||||
)
|
||||
rescue Faraday::UnprocessableEntityError => e
|
||||
raise RequestHandlerError.new(
|
||||
"The #{method} request is unprocessable",
|
||||
{ method:, params: },
|
||||
error_type: :unprocessable_entity,
|
||||
original_error: e,
|
||||
)
|
||||
rescue Faraday::Error => e # Catch-all
|
||||
raise RequestHandlerError.new(
|
||||
"Internal error handling #{method} request",
|
||||
{ method:, params: },
|
||||
error_type: :internal_error,
|
||||
original_error: e,
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :headers
|
||||
|
||||
def client
|
||||
require_faraday!
|
||||
@client ||= Faraday.new(url) do |faraday|
|
||||
faraday.request(:json)
|
||||
faraday.response(:json)
|
||||
faraday.response(:raise_error)
|
||||
|
||||
headers.each do |key, value|
|
||||
faraday.headers[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def require_faraday!
|
||||
require "faraday"
|
||||
rescue LoadError
|
||||
raise LoadError, "The 'faraday' gem is required to use the MCP client HTTP transport. " \
|
||||
"Add it to your Gemfile: gem 'faraday', '>= 2.0'" \
|
||||
"See https://rubygems.org/gems/faraday for more details."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
lib/mcp/client/tool.rb
Normal file
15
lib/mcp/client/tool.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MCP
|
||||
class Client
|
||||
class Tool
|
||||
attr_reader :name, :description, :input_schema
|
||||
|
||||
def initialize(name:, description:, input_schema:)
|
||||
@name = name
|
||||
@description = description
|
||||
@input_schema = input_schema
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
212
test/mcp/client/http_test.rb
Normal file
212
test/mcp/client/http_test.rb
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "faraday"
|
||||
require "webmock/minitest"
|
||||
require "mcp/client/http"
|
||||
require "mcp/client/tool"
|
||||
require "mcp/client"
|
||||
|
||||
module MCP
|
||||
class Client
|
||||
class HTTPTest < Minitest::Test
|
||||
def test_raises_load_error_when_faraday_not_available
|
||||
client = HTTP.new(url: url)
|
||||
|
||||
# simulate Faraday not being available
|
||||
HTTP.any_instance.stubs(:require).with("faraday").raises(LoadError, "cannot load such file -- faraday")
|
||||
|
||||
error = assert_raises(LoadError) do
|
||||
# This should immediately try to instantiate the client and fail
|
||||
client.send_request(request: {})
|
||||
end
|
||||
|
||||
assert_includes(error.message, "The 'faraday' gem is required to use the MCP client HTTP transport")
|
||||
assert_includes(error.message, "Add it to your Gemfile: gem 'faraday', '>= 2.0'")
|
||||
end
|
||||
|
||||
def test_headers_are_added_to_the_request
|
||||
headers = { "Authorization" => "Bearer token" }
|
||||
client = HTTP.new(url: url, headers: headers)
|
||||
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(
|
||||
headers: {
|
||||
"Authorization" => "Bearer token",
|
||||
"Content-Type" => "application/json",
|
||||
},
|
||||
body: request.to_json,
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { tools: [] } }.to_json,
|
||||
)
|
||||
|
||||
# The test passes if the request is made with the correct headers
|
||||
# If headers are wrong, the stub_request won't match and will raise
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
def test_send_request_returns_faraday_response
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { tools: [] } }.to_json,
|
||||
)
|
||||
|
||||
response = client.send_request(request: request)
|
||||
assert_instance_of(Hash, response)
|
||||
assert_equal({ "result" => { "tools" => [] } }, response)
|
||||
end
|
||||
|
||||
def test_send_request_raises_bad_request_error
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(status: 400)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
assert_equal("The tools/list request is invalid", error.message)
|
||||
assert_equal(:bad_request, error.error_type)
|
||||
assert_equal({ method: "tools/list", params: nil }, error.request)
|
||||
end
|
||||
|
||||
def test_send_request_raises_unauthorized_error
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(status: 401)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
assert_equal("You are unauthorized to make tools/list requests", error.message)
|
||||
assert_equal(:unauthorized, error.error_type)
|
||||
assert_equal({ method: "tools/list", params: nil }, error.request)
|
||||
end
|
||||
|
||||
def test_send_request_raises_forbidden_error
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(status: 403)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
assert_equal("You are forbidden to make tools/list requests", error.message)
|
||||
assert_equal(:forbidden, error.error_type)
|
||||
assert_equal({ method: "tools/list", params: nil }, error.request)
|
||||
end
|
||||
|
||||
def test_send_request_raises_not_found_error
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(status: 404)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
assert_equal("The tools/list request is not found", error.message)
|
||||
assert_equal(:not_found, error.error_type)
|
||||
assert_equal({ method: "tools/list", params: nil }, error.request)
|
||||
end
|
||||
|
||||
def test_send_request_raises_unprocessable_entity_error
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(status: 422)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
assert_equal("The tools/list request is unprocessable", error.message)
|
||||
assert_equal(:unprocessable_entity, error.error_type)
|
||||
assert_equal({ method: "tools/list", params: nil }, error.request)
|
||||
end
|
||||
|
||||
def test_send_request_raises_internal_error
|
||||
request = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test_id",
|
||||
method: "tools/list",
|
||||
}
|
||||
|
||||
stub_request(:post, url)
|
||||
.with(body: request.to_json)
|
||||
.to_return(status: 500)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.send_request(request: request)
|
||||
end
|
||||
|
||||
assert_equal("Internal error handling tools/list request", error.message)
|
||||
assert_equal(:internal_error, error.error_type)
|
||||
assert_equal({ method: "tools/list", params: nil }, error.request)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stub_request(method, url)
|
||||
WebMock.stub_request(method, url)
|
||||
end
|
||||
|
||||
def url
|
||||
"http://example.com"
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= HTTP.new(url: url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
33
test/mcp/client/tool_test.rb
Normal file
33
test/mcp/client/tool_test.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "mcp/client/tool"
|
||||
|
||||
module MCP
|
||||
class Client
|
||||
class ToolTest < Minitest::Test
|
||||
def setup
|
||||
@tool = Tool.new(
|
||||
name: "test_tool",
|
||||
description: "A test tool",
|
||||
input_schema: { "type" => "object", "properties" => { "foo" => { "type" => "string" } } },
|
||||
)
|
||||
end
|
||||
|
||||
def test_name_returns_name
|
||||
assert_equal("test_tool", @tool.name)
|
||||
end
|
||||
|
||||
def test_description_returns_description
|
||||
assert_equal("A test tool", @tool.description)
|
||||
end
|
||||
|
||||
def test_input_schema_returns_input_schema
|
||||
assert_equal(
|
||||
{ "type" => "object", "properties" => { "foo" => { "type" => "string" } } },
|
||||
@tool.input_schema,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
60
test/mcp/client_test.rb
Normal file
60
test/mcp/client_test.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "securerandom"
|
||||
|
||||
module MCP
|
||||
class ClientTest < Minitest::Test
|
||||
def test_tools_sends_request_to_transport_and_returns_tools_array
|
||||
transport = mock
|
||||
mock_response = {
|
||||
"result" => {
|
||||
"tools" => [
|
||||
{ "name" => "tool1", "description" => "tool1", "inputSchema" => {} },
|
||||
{ "name" => "tool2", "description" => "tool2", "inputSchema" => {} },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# Only checking for the essential parts of the request
|
||||
transport.expects(:send_request).with do |args|
|
||||
args in { request: { method: "tools/list", jsonrpc: "2.0" } }
|
||||
end.returns(mock_response).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
tools = client.tools
|
||||
|
||||
assert_equal(2, tools.size)
|
||||
assert_equal("tool1", tools.first.name)
|
||||
assert_equal("tool2", tools.last.name)
|
||||
end
|
||||
|
||||
def test_call_tool_sends_request_to_transport_and_returns_content
|
||||
transport = mock
|
||||
tool = MCP::Client::Tool.new(name: "tool1", description: "tool1", input_schema: {})
|
||||
arguments = { foo: "bar" }
|
||||
mock_response = {
|
||||
"result" => { "content" => "result" },
|
||||
}
|
||||
|
||||
# Only checking for the essential parts of the request
|
||||
transport.expects(:send_request).with do |args|
|
||||
args in {
|
||||
request: {
|
||||
method: "tools/call",
|
||||
jsonrpc: "2.0",
|
||||
params: {
|
||||
name: "tool1",
|
||||
arguments: arguments,
|
||||
},
|
||||
},
|
||||
}
|
||||
end.returns(mock_response).once
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
result = client.call_tool(tool: tool, arguments: arguments)
|
||||
|
||||
assert_equal("result", result)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue