Skip to content

Commit e92e4ec

Browse files
committed
make transports implement template methods instead
1 parent 281cb4c commit e92e4ec

File tree

5 files changed

+205
-450
lines changed

5 files changed

+205
-450
lines changed

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -588,26 +588,30 @@ otherwise `resources/read` requests will be a no-op.
588588
## Building an MCP Client
589589

590590
The `MCP::Client` class provides an interface for interacting with MCP servers.
591-
Clients are initialized with a transport layer instance that instructs them how to interact with the server.
591+
Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
592+
593+
## Transport Layer Interface
592594

593595
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:
594596

595597
```ruby
596598
class CustomTransport
597-
def tools
598-
# Must return Array<MCP::Client::Tool>
599-
end
600-
601-
def call_tool(tool:, input:)
602-
# tool: MCP::Client::Tool
603-
# input: Hash - the arguments to pass to the tool
604-
# Returns: Hash - the content from the response (typically response.dig("result", "content"))
599+
# Sends a JSON-RPC request to the server and returns the raw response
600+
#
601+
# @param request [Hash] A complete JSON-RPC request object.
602+
# https://www.jsonrpc.org/specification#request_object
603+
# @return [Hash] A hash modeling a JSON-RPC response object:
604+
# https://www.jsonrpc.org/specification#response_object
605+
def send_request(request:)
606+
# Your transport-specific logic here
607+
# - HTTP: POST to endpoint with JSON body
608+
# - WebSocket: Send message over WebSocket
609+
# - stdio: Write to stdout, read from stdin
610+
# - etc.
605611
end
606612
end
607613
```
608614

609-
**Note:** We strongly recommend returning `MCP::Client::Tool` instances rather than custom tool objects with the same interface, as this ensures compatibility with future SDK features and provides a consistent interface.
610-
611615
### HTTP Transport Layer
612616

613617
Use the `MCP::Client::Http` transport to interact with MCP servers using simple HTTP requests.
@@ -685,3 +689,4 @@ Releases are triggered by PRs to the `main` branch updating the version number i
685689
1. **Merge your PR to the main branch** - This will automatically trigger the release workflow via GitHub Actions
686690

687691
When changes are merged to the `main` branch, the GitHub Actions workflow (`.github/workflows/release.yml`) is triggered and the gem is published to RubyGems.
692+

lib/mcp/client.rb

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
module MCP
44
class Client
5+
JSON_RPC_VERSION = "2.0"
6+
57
# Initializes a new MCP::Client instance.
68
#
79
# @param transport [Object] The transport object to use for communication with the server.
@@ -35,7 +37,25 @@ def initialize(transport:)
3537
# puts tool.name
3638
# end
3739
def tools
38-
transport.tools
40+
request = {
41+
jsonrpc: JSON_RPC_VERSION,
42+
id: request_id,
43+
method: "tools/list",
44+
mcp: {
45+
jsonrpc: JSON_RPC_VERSION,
46+
id: request_id,
47+
method: "tools/list",
48+
}.compact,
49+
}
50+
51+
response = transport.send_request(request: request)
52+
response.dig("result", "tools")&.map do |tool|
53+
Tool.new(
54+
name: tool["name"],
55+
description: tool["description"],
56+
input_schema: tool["inputSchema"],
57+
)
58+
end || []
3959
end
4060

4161
# Calls a tool via the transport layer.
@@ -52,7 +72,27 @@ def tools
5272
# The exact requirements for `input` are determined by the transport layer in use.
5373
# Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
5474
def call_tool(tool:, input: nil)
55-
transport.call_tool(tool: tool, input: input)
75+
request = {
76+
jsonrpc: JSON_RPC_VERSION,
77+
id: request_id,
78+
method: "tools/call",
79+
params: { name: tool.name, arguments: input },
80+
mcp: {
81+
jsonrpc: JSON_RPC_VERSION,
82+
id: request_id,
83+
method: "tools/call",
84+
params: { name: tool.name, arguments: input },
85+
}.compact,
86+
}
87+
88+
response = transport.send_request(request: request)
89+
response.dig("result", "content")
90+
end
91+
92+
private
93+
94+
def request_id
95+
SecureRandom.uuid_v7
5696
end
5797

5898
class RequestHandlerError < StandardError

lib/mcp/client/http.rb

Lines changed: 26 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,62 +10,11 @@ def initialize(url:, headers: {})
1010
@headers = headers
1111
end
1212

13-
def tools
14-
response = send_request(method: "tools/list").body
13+
def send_request(request:)
14+
method = request[:method] || request["method"]
15+
params = request[:params] || request["params"]
1516

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 || []
23-
end
24-
25-
def call_tool(tool:, input:)
26-
response = send_request(
27-
method: "tools/call",
28-
params: { name: tool.name, arguments: input },
29-
).body
30-
31-
response.dig("result", "content")
32-
end
33-
34-
private
35-
36-
attr_reader :headers
37-
38-
def client
39-
require_faraday!
40-
@client ||= Faraday.new(url) do |faraday|
41-
faraday.request(:json)
42-
faraday.response(:json)
43-
faraday.response(:raise_error)
44-
45-
headers.each do |key, value|
46-
faraday.headers[key] = value
47-
end
48-
end
49-
end
50-
51-
def require_faraday!
52-
require "faraday"
53-
rescue LoadError
54-
raise LoadError, "The 'faraday' gem is required to use the MCP client HTTP transport. " \
55-
"Add it to your Gemfile: gem 'faraday', '>= 2.0'"
56-
end
57-
58-
def send_request(method:, params: nil)
59-
client.post(
60-
"",
61-
{
62-
jsonrpc: "2.0",
63-
id: request_id,
64-
method:,
65-
params:,
66-
mcp: { jsonrpc: "2.0", id: request_id, method:, params: }.compact,
67-
}.compact,
68-
)
17+
client.post("", request).body
6918
rescue Faraday::BadRequestError => e
7019
raise RequestHandlerError.new(
7120
"The #{method} request is invalid",
@@ -110,8 +59,28 @@ def send_request(method:, params: nil)
11059
)
11160
end
11261

113-
def request_id
114-
SecureRandom.uuid_v7
62+
private
63+
64+
attr_reader :headers
65+
66+
def client
67+
require_faraday!
68+
@client ||= Faraday.new(url) do |faraday|
69+
faraday.request(:json)
70+
faraday.response(:json)
71+
faraday.response(:raise_error)
72+
73+
headers.each do |key, value|
74+
faraday.headers[key] = value
75+
end
76+
end
77+
end
78+
79+
def require_faraday!
80+
require "faraday"
81+
rescue LoadError
82+
raise LoadError, "The 'faraday' gem is required to use the MCP client HTTP transport. " \
83+
"Add it to your Gemfile: gem 'faraday', '>= 2.0'"
11584
end
11685
end
11786
end

0 commit comments

Comments
 (0)