Skip to content

Commit 75f2ae8

Browse files
committed
add client
add a client with http transport
1 parent a74f422 commit 75f2ae8

File tree

7 files changed

+1623
-0
lines changed

7 files changed

+1623
-0
lines changed

examples/http_client_example.rb

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative "../lib/mcp"
5+
6+
# Example of using the MCP HTTP client
7+
def main
8+
# Create an HTTP transport pointing to your MCP server
9+
transport = MCP::Client::Transports::HTTP.new(
10+
url: "http://localhost:3000/mcp",
11+
timeout: 30,
12+
headers: {
13+
"Authorization" => "Bearer your-token-here", # Optional authentication
14+
},
15+
)
16+
17+
# Create the client with the transport
18+
client = MCP::Client.new(transport: transport)
19+
20+
begin
21+
puts "Pinging..."
22+
client.ping(request_id: "ping-001")
23+
24+
# Initialize the session
25+
puts "Initializing session..."
26+
result = client.initialize_session(
27+
protocol_version: "2025-03-26",
28+
capabilities: {},
29+
client_info: {
30+
name: "example_client",
31+
version: "1.0.0",
32+
},
33+
request_id: "init-001", # Custom request ID
34+
)
35+
36+
puts "Connected to server: #{client.server_info[:name]} v#{client.server_info[:version]}"
37+
puts "Protocol version: #{result[:protocolVersion]}"
38+
puts "Server capabilities: #{client.capabilities.keys.join(", ")}"
39+
40+
# List available tools
41+
if client.capabilities[:tools]
42+
puts "Listing tools..."
43+
tools_result = client.list_tools(request_id: "tools-list-001")
44+
tools = tools_result[:tools] || []
45+
if tools.any?
46+
tools.each do |tool|
47+
puts "- #{tool[:name]}: #{tool[:description]}"
48+
end
49+
else
50+
puts "No tools available"
51+
end
52+
puts
53+
54+
# Call a tool if available
55+
if tools.any?
56+
tool_name = tools.first[:name]
57+
puts "Calling tool: #{tool_name}"
58+
begin
59+
result = client.call_tool(
60+
name: tool_name,
61+
arguments: {},
62+
request_id: "tool-call-001", # Custom request ID
63+
)
64+
puts "Tool result: #{result}"
65+
rescue MCP::Client::ClientError => e
66+
puts "Tool call failed: #{e.message}"
67+
end
68+
puts
69+
end
70+
end
71+
72+
# List available prompts (using auto-incrementing ID)
73+
if client.capabilities[:prompts]
74+
puts "Listing prompts..."
75+
prompts_result = client.list_prompts # No custom ID - will auto-increment
76+
prompts = prompts_result[:prompts] || []
77+
if prompts.any?
78+
prompts.each do |prompt|
79+
puts "- #{prompt[:name]}: #{prompt[:description]}"
80+
end
81+
else
82+
puts "No prompts available"
83+
end
84+
puts
85+
86+
# Get a prompt if available
87+
if prompts.any?
88+
prompt_name = prompts.first[:name]
89+
puts "Getting prompt: #{prompt_name}"
90+
begin
91+
result = client.get_prompt(
92+
name: prompt_name,
93+
arguments: {},
94+
request_id: "prompt-get-001", # Custom request ID
95+
)
96+
puts "Prompt result: #{result[:description]}"
97+
puts "Messages: #{result[:messages].length} message(s)"
98+
rescue MCP::Client::ClientError => e
99+
puts "Prompt get failed: #{e.message}"
100+
end
101+
puts
102+
end
103+
end
104+
105+
# List available resources
106+
if client.capabilities[:resources]
107+
puts "Listing resources..."
108+
resources_result = client.list_resources # Auto-incrementing ID
109+
resources = resources_result[:resources] || []
110+
if resources.any?
111+
resources.each do |resource|
112+
puts "- #{resource[:uri]}: #{resource[:name]} (#{resource[:mimeType]})"
113+
end
114+
else
115+
puts "No resources available"
116+
end
117+
puts
118+
119+
# Read a resource if available
120+
if resources.any?
121+
resource_uri = resources.first[:uri]
122+
puts "Reading resource: #{resource_uri}"
123+
begin
124+
result = client.read_resource(
125+
uri: resource_uri,
126+
request_id: "resource-read-001", # Custom request ID
127+
)
128+
contents = result[:contents] || []
129+
contents.each do |content|
130+
puts "Content type: #{content[:mimeType]}"
131+
if content[:text]
132+
puts "Text content: #{content[:text][0..100]}#{"..." if content[:text].length > 100}"
133+
elsif content[:blob]
134+
puts "Binary content: #{content[:blob].length} bytes"
135+
end
136+
end
137+
rescue MCP::Client::ClientError => e
138+
puts "Resource read failed: #{e.message}"
139+
end
140+
puts
141+
end
142+
end
143+
rescue MCP::Client::ClientError => e
144+
puts "Client error: #{e.message}"
145+
exit(1)
146+
rescue MCP::Client::Transports::HTTP::HTTPError => e
147+
puts "HTTP error: #{e.message}"
148+
puts "Status code: #{e.status_code}" if e.status_code
149+
exit(1)
150+
rescue => e
151+
puts "Unexpected error: #{e.message}"
152+
puts e.backtrace.join("\n")
153+
exit(1)
154+
end
155+
end
156+
157+
if __FILE__ == $0
158+
main
159+
end

lib/mcp.rb

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

33
require_relative "mcp/server"
4+
require_relative "mcp/client"
5+
require_relative "mcp/client/transports/http"
46
require_relative "mcp/string_utils"
57
require_relative "mcp/tool"
68
require_relative "mcp/tool/input_schema"

lib/mcp/client.rb

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
require_relative "methods"
5+
6+
module MCP
7+
class Client
8+
class ClientError < StandardError
9+
attr_reader :error_type, :original_error
10+
11+
def initialize(message, error_type: :client_error, original_error: nil)
12+
super(message)
13+
@error_type = error_type
14+
@original_error = original_error
15+
end
16+
end
17+
18+
attr_reader :transport, :server_info, :capabilities
19+
20+
def initialize(transport:)
21+
@transport = transport
22+
@request_id = 0
23+
@server_info = nil
24+
@capabilities = nil
25+
end
26+
27+
def initialize_session(protocol_version: "2025-03-26", capabilities: {}, client_info: {}, request_id: nil)
28+
params = {
29+
protocolVersion: protocol_version,
30+
capabilities: capabilities,
31+
clientInfo: client_info,
32+
}
33+
34+
request = build_request(Methods::INITIALIZE, params, request_id)
35+
response = @transport.send_request(request)
36+
37+
if response[:error]
38+
raise ClientError.new(
39+
"Failed to initialize: #{response[:error][:message]}",
40+
error_type: :initialization_error,
41+
)
42+
end
43+
44+
result = response[:result]
45+
@server_info = result[:serverInfo]
46+
@capabilities = result[:capabilities]
47+
48+
result
49+
end
50+
51+
def ping(request_id: nil)
52+
request = build_request(Methods::PING, nil, request_id)
53+
response = @transport.send_request(request)
54+
55+
if response[:error]
56+
raise ClientError.new("Ping failed: #{response[:error][:message]}")
57+
end
58+
59+
response[:result]
60+
end
61+
62+
def list_tools(cursor: nil, request_id: nil)
63+
params = cursor ? { cursor: cursor } : nil
64+
request = build_request(Methods::TOOLS_LIST, params, request_id)
65+
response = @transport.send_request(request)
66+
67+
if response[:error]
68+
raise ClientError.new("Failed to list tools: #{response[:error][:message]}")
69+
end
70+
71+
response[:result]
72+
end
73+
74+
def call_tool(name:, arguments: {}, request_id: nil)
75+
params = { name: name }
76+
params[:arguments] = arguments unless arguments.empty?
77+
78+
request = build_request(Methods::TOOLS_CALL, params, request_id)
79+
response = @transport.send_request(request)
80+
81+
if response[:error]
82+
raise ClientError.new("Failed to call tool '#{name}': #{response[:error][:message]}")
83+
end
84+
85+
response[:result]
86+
end
87+
88+
def list_prompts(cursor: nil, request_id: nil)
89+
params = cursor ? { cursor: cursor } : nil
90+
request = build_request(Methods::PROMPTS_LIST, params, request_id)
91+
response = @transport.send_request(request)
92+
93+
if response[:error]
94+
raise ClientError.new("Failed to list prompts: #{response[:error][:message]}")
95+
end
96+
97+
response[:result]
98+
end
99+
100+
def get_prompt(name:, arguments: {}, request_id: nil)
101+
params = { name: name }
102+
params[:arguments] = arguments unless arguments.empty?
103+
104+
request = build_request(Methods::PROMPTS_GET, params, request_id)
105+
response = @transport.send_request(request)
106+
107+
if response[:error]
108+
raise ClientError.new("Failed to get prompt '#{name}': #{response[:error][:message]}")
109+
end
110+
111+
response[:result]
112+
end
113+
114+
def list_resources(cursor: nil, request_id: nil)
115+
params = cursor ? { cursor: cursor } : nil
116+
request = build_request(Methods::RESOURCES_LIST, params, request_id)
117+
response = @transport.send_request(request)
118+
119+
if response[:error]
120+
raise ClientError.new("Failed to list resources: #{response[:error][:message]}")
121+
end
122+
123+
response[:result]
124+
end
125+
126+
def read_resource(uri:, request_id: nil)
127+
request = build_request(Methods::RESOURCES_READ, { uri: uri }, request_id)
128+
response = @transport.send_request(request)
129+
130+
if response[:error]
131+
raise ClientError.new("Failed to read resource '#{uri}': #{response[:error][:message]}")
132+
end
133+
134+
response[:result]
135+
end
136+
137+
def list_resource_templates(cursor: nil, request_id: nil)
138+
params = cursor ? { cursor: cursor } : nil
139+
request = build_request(Methods::RESOURCES_TEMPLATES_LIST, params, request_id)
140+
response = @transport.send_request(request)
141+
142+
if response[:error]
143+
raise ClientError.new("Failed to list resource templates: #{response[:error][:message]}")
144+
end
145+
146+
response[:result]
147+
end
148+
149+
def subscribe_resource(uri:, request_id: nil)
150+
request = build_request(Methods::RESOURCES_SUBSCRIBE, { uri: uri }, request_id)
151+
response = @transport.send_request(request)
152+
153+
if response[:error]
154+
raise ClientError.new("Failed to subscribe to resource '#{uri}': #{response[:error][:message]}")
155+
end
156+
157+
response[:result]
158+
end
159+
160+
def unsubscribe_resource(uri:, request_id: nil)
161+
request = build_request(Methods::RESOURCES_UNSUBSCRIBE, { uri: uri }, request_id)
162+
response = @transport.send_request(request)
163+
164+
if response[:error]
165+
raise ClientError.new("Failed to unsubscribe from resource '#{uri}': #{response[:error][:message]}")
166+
end
167+
168+
response[:result]
169+
end
170+
171+
def set_logging_level(level:, request_id: nil)
172+
request = build_request(Methods::LOGGING_SET_LEVEL, { level: level }, request_id)
173+
response = @transport.send_request(request)
174+
175+
if response[:error]
176+
raise ClientError.new("Failed to set logging level: #{response[:error][:message]}")
177+
end
178+
179+
response[:result]
180+
end
181+
182+
def complete(ref:, argument:, request_id: nil)
183+
params = {
184+
ref: ref,
185+
argument: argument,
186+
}
187+
188+
request = build_request(Methods::COMPLETION_COMPLETE, params, request_id)
189+
response = @transport.send_request(request)
190+
191+
if response[:error]
192+
raise ClientError.new("Failed to get completions: #{response[:error][:message]}")
193+
end
194+
195+
response[:result]
196+
end
197+
198+
private
199+
200+
def build_request(method, params = nil, request_id = nil)
201+
request = {
202+
jsonrpc: "2.0",
203+
method: method,
204+
id: request_id.nil? ? next_request_id : request_id,
205+
}
206+
207+
request[:params] = params if params
208+
request
209+
end
210+
211+
def next_request_id
212+
@request_id += 1
213+
end
214+
end
215+
end

0 commit comments

Comments
 (0)