Skip to content

Commit d097729

Browse files
committed
remove version and Tools, make client a wrapper class
1 parent 289e462 commit d097729

File tree

9 files changed

+133
-235
lines changed

9 files changed

+133
-235
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ gem 'faraday', '>= 2.0'
603603
The `MCP::Client::Http` class provides a simple HTTP client for interacting with MCP servers:
604604

605605
```ruby
606-
client = MCP::Client::Http.new(url: "https://api.example.com/mcp")
606+
client = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
607607

608608
# List available tools
609609
tools = client.tools
@@ -632,7 +632,7 @@ The HTTP client supports:
632632
By default, the HTTP client has no authentication, but it supports custom headers for authentication. For example, to use Bearer token authentication:
633633

634634
```ruby
635-
client = MCP::Client::Http.new(
635+
client = MCP::Client::HTTP.new(
636636
url: "https://api.example.com/mcp",
637637
headers: {
638638
"Authorization" => "Bearer my_token"
@@ -646,12 +646,11 @@ You can add any custom headers needed for your authentication scheme. The client
646646

647647
### Tool Objects
648648

649-
The client provides wrapper objects for tools returned by the server:
649+
The client provides a wrapper class for tools returned by the server:
650650

651651
- `MCP::Client::Tool` - Represents a single tool with its metadata
652-
- `MCP::Client::Tools` - Collection of tools with enumerable functionality
653652

654-
These objects provide easy access to tool properties like name, description, and input schema.
653+
This class provide easy access to tool properties like name, description, and input schema.
655654

656655
## Releases
657656

lib/mcp.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
require_relative "mcp/tool/annotations"
2323
require_relative "mcp/transport"
2424
require_relative "mcp/version"
25+
require_relative "mcp/client"
26+
require_relative "mcp/client/http"
27+
require_relative "mcp/client/tool"
2528

2629
module MCP
2730
class << self

lib/mcp/client.rb

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,58 @@
11
# frozen_string_literal: true
22

3-
# require "json_rpc_handler"
4-
# require_relative "shared/instrumentation"
5-
# require_relative "shared/methods"
6-
73
module MCP
8-
module Client
9-
# Can be made an abstract class if we need shared behavior
4+
class Client
5+
# Initializes a new MCP::Client instance.
6+
#
7+
# @param transport [Object] The transport object to use for communication with the server.
8+
# The transport should be a duck type that responds to both `#tools` and `#call_tool`.
9+
# This allows the client to list available tools and invoke tool calls via the transport.
10+
#
11+
# @example
12+
# transport = MCP::Client::HTTP.new(url: "http://localhost:3000")
13+
# client = MCP::Client.new(transport: transport)
14+
#
15+
# @note
16+
# The transport does not need to be a specific class, but must implement:
17+
# - #tools
18+
# - #call_tool(tool:, input:)
19+
def initialize(transport:)
20+
@transport = transport
21+
end
22+
23+
# The user may want to access additional transport-specific methods/attributes
24+
# So keeping it public
25+
attr_reader :transport
26+
27+
# Returns the list of tools available from the server.
28+
#
29+
# @return [Array<MCP::Client::Tool>] An array of available tools.
30+
#
31+
# @example
32+
# tools = client.tools
33+
# tools.each do |tool|
34+
# puts tool.name
35+
# end
36+
def tools
37+
@tools ||= transport.tools
38+
end
39+
40+
# Calls a tool via the transport layer.
41+
#
42+
# @param tool [MCP::Client::Tool] The tool to be called.
43+
# @param input [Object, nil] The input to pass to the tool.
44+
# @return [Object] The result of the tool call, as returned by the transport.
45+
#
46+
# @example
47+
# tool = client.tools.first
48+
# result = client.call_tool(tool: tool, input: { foo: "bar" })
49+
#
50+
# @note
51+
# The exact requirements for `input` are determined by the transport layer in use.
52+
# Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
53+
def call_tool(tool:, input: nil)
54+
transport.call_tool(tool: tool, input: input)
55+
end
1056

1157
class RequestHandlerError < StandardError
1258
attr_reader :error_type, :original_error, :request

lib/mcp/client/http.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
# frozen_string_literal: true
22

33
module MCP
4-
module Client
5-
class Http
6-
DEFAULT_VERSION = "0.1.0"
4+
class Client
5+
class HTTP
6+
attr_reader :url
77

8-
attr_reader :url, :version
9-
10-
def initialize(url:, version: DEFAULT_VERSION, headers: {})
8+
def initialize(url:, headers: {})
119
@url = url
12-
@version = version
1310
@headers = headers
1411
end
1512

1613
def tools
1714
response = send_request(method: "tools/list").body
1815

19-
::MCP::Client::Tools.new(response)
16+
response.dig("result", "tools")&.map do |tool|
17+
Tool.new(
18+
name: tool["name"],
19+
description: tool["description"],
20+
input_schema: tool["inputSchema"],
21+
)
22+
end || []
2023
end
2124

2225
def call_tool(tool:, input:)

lib/mcp/client/tool.rb

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,14 @@
22
# frozen_string_literal: true
33

44
module MCP
5-
module Client
5+
class Client
66
class Tool
7-
attr_reader :payload
7+
attr_reader :name, :description, :input_schema
88

9-
def initialize(payload)
10-
@payload = payload
11-
end
12-
13-
def name
14-
payload["name"]
15-
end
16-
17-
def description
18-
payload["description"]
19-
end
20-
21-
def input_schema
22-
payload["inputSchema"]
9+
def initialize(name:, description:, input_schema:)
10+
@name = name
11+
@description = description
12+
@input_schema = input_schema
2313
end
2414
end
2515
end

lib/mcp/client/tools.rb

Lines changed: 0 additions & 30 deletions
This file was deleted.

test/mcp/client/http_test.rb

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,27 @@
66
require "webmock/minitest"
77
require "mcp/client/http"
88
require "mcp/client/tool"
9-
require "mcp/client/tools"
109
require "mcp/client"
1110

1211
module MCP
13-
module Client
14-
class HttpTest < Minitest::Test
15-
def test_initialization_with_default_version
16-
assert_equal("0.1.0", client.version)
17-
assert_equal(url, client.url)
18-
end
19-
20-
def test_initialization_with_custom_version
21-
custom_version = "1.2.3"
22-
client = Http.new(url: url, version: custom_version)
23-
assert_equal(custom_version, client.version)
24-
end
12+
class Client
13+
class HTTPTest < Minitest::Test
14+
# def test_initialization_with_default_version
15+
# assert_equal("0.1.0", client.version)
16+
# assert_equal(url, client.url)
17+
# end
18+
19+
# def test_initialization_with_custom_version
20+
# custom_version = "1.2.3"
21+
# client = HTTP.new(url: url, version: custom_version)
22+
# assert_equal(custom_version, client.version)
23+
# end
2524

2625
def test_raises_load_error_when_faraday_not_available
27-
client = Http.new(url: url)
26+
client = HTTP.new(url: url)
2827

2928
# simulate Faraday not being available
30-
Http.any_instance.stubs(:require).with("faraday").raises(LoadError, "cannot load such file -- faraday")
29+
HTTP.any_instance.stubs(:require).with("faraday").raises(LoadError, "cannot load such file -- faraday")
3130

3231
error = assert_raises(LoadError) do
3332
# I picked #tools arbitrarily.
@@ -41,7 +40,7 @@ def test_raises_load_error_when_faraday_not_available
4140

4241
def test_headers_are_added_to_the_request
4342
headers = { "Authorization" => "Bearer token" }
44-
client = Http.new(url: url, headers: headers)
43+
client = HTTP.new(url: url, headers: headers)
4544
client.stubs(:request_id).returns(mock_request_id)
4645

4746
stub_request(:post, url)
@@ -108,16 +107,16 @@ def test_tools_returns_tools_instance
108107
)
109108

110109
tools = client.tools
111-
assert_instance_of(Tools, tools)
110+
assert_instance_of(Array, tools)
112111
assert_equal(1, tools.count)
113112
assert_equal("test_tool", tools.first.name)
114113
end
115114

116115
def test_call_tool_returns_tool_response
117116
tool = Tool.new(
118-
"name" => "test_tool",
119-
"description" => "A test tool",
120-
"inputSchema" => {
117+
name: "test_tool",
118+
description: "A test tool",
119+
input_schema: {
121120
"type" => "object",
122121
"properties" => {},
123122
},
@@ -172,9 +171,9 @@ def test_call_tool_returns_tool_response
172171

173172
def test_call_tool_handles_empty_response
174173
tool = Tool.new(
175-
"name" => "test_tool",
176-
"description" => "A test tool",
177-
"inputSchema" => {
174+
name: "test_tool",
175+
description: "A test tool",
176+
input_schema: {
178177
"type" => "object",
179178
"properties" => {},
180179
},
@@ -220,9 +219,9 @@ def test_call_tool_handles_empty_response
220219

221220
def test_raises_bad_request_error
222221
tool = Tool.new(
223-
"name" => "test_tool",
224-
"description" => "A test tool",
225-
"inputSchema" => {
222+
name: "test_tool",
223+
description: "A test tool",
224+
input_schema: {
226225
"type" => "object",
227226
"properties" => {},
228227
},
@@ -263,9 +262,9 @@ def test_raises_bad_request_error
263262

264263
def test_raises_unauthorized_error
265264
tool = Tool.new(
266-
"name" => "test_tool",
267-
"description" => "A test tool",
268-
"inputSchema" => {
265+
name: "test_tool",
266+
description: "A test tool",
267+
input_schema: {
269268
"type" => "object",
270269
"properties" => {},
271270
},
@@ -306,9 +305,9 @@ def test_raises_unauthorized_error
306305

307306
def test_raises_forbidden_error
308307
tool = Tool.new(
309-
"name" => "test_tool",
310-
"description" => "A test tool",
311-
"inputSchema" => {
308+
name: "test_tool",
309+
description: "A test tool",
310+
input_schema: {
312311
"type" => "object",
313312
"properties" => {},
314313
},
@@ -349,9 +348,9 @@ def test_raises_forbidden_error
349348

350349
def test_raises_not_found_error
351350
tool = Tool.new(
352-
"name" => "test_tool",
353-
"description" => "A test tool",
354-
"inputSchema" => {
351+
name: "test_tool",
352+
description: "A test tool",
353+
input_schema: {
355354
"type" => "object",
356355
"properties" => {},
357356
},
@@ -392,9 +391,9 @@ def test_raises_not_found_error
392391

393392
def test_raises_unprocessable_entity_error
394393
tool = Tool.new(
395-
"name" => "test_tool",
396-
"description" => "A test tool",
397-
"inputSchema" => {
394+
name: "test_tool",
395+
description: "A test tool",
396+
input_schema: {
398397
"type" => "object",
399398
"properties" => {},
400399
},
@@ -435,9 +434,9 @@ def test_raises_unprocessable_entity_error
435434

436435
def test_raises_internal_error
437436
tool = Tool.new(
438-
"name" => "test_tool",
439-
"description" => "A test tool",
440-
"inputSchema" => {
437+
name: "test_tool",
438+
description: "A test tool",
439+
input_schema: {
441440
"type" => "object",
442441
"properties" => {},
443442
},
@@ -492,7 +491,7 @@ def url
492491

493492
def client
494493
@client ||= begin
495-
client = Http.new(url: url)
494+
client = HTTP.new(url: url)
496495
client.stubs(:request_id).returns(mock_request_id)
497496
client
498497
end

0 commit comments

Comments
 (0)