ruby-mcp-client 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -11
- data/lib/mcp_client/client.rb +125 -26
- data/lib/mcp_client/config_parser.rb +5 -1
- data/lib/mcp_client/errors.rb +4 -0
- data/lib/mcp_client/server_base.rb +20 -0
- data/lib/mcp_client/server_factory.rb +6 -3
- data/lib/mcp_client/server_sse.rb +463 -153
- data/lib/mcp_client/server_stdio.rb +4 -3
- data/lib/mcp_client/tool.rb +22 -4
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +20 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6607aff3aa197734ac1b5fd4c2f51bb994df18ffcb0f18928c4f75197aaa8ed
|
4
|
+
data.tar.gz: 6921d74355c4497ca944374795469a767873ff8716e54fedbe3438104a4e5b79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cea51f3ff6046ac619197413ccbe50a18fc3aeef6f881180d9cc5b0ad49b67d1cbe93459005f93f3063ad2dfe699c06bb1556180dc3bd1437d67e38b89549c9a
|
7
|
+
data.tar.gz: 8de670f6d7f3fd5a3c9786365e19034ee6d8734ef8d938769818acb606c8f7c13529b8777cdff2a54483398d52dd7847ccddc957a8a4517c16ad31a1b35700b0
|
data/README.md
CHANGED
@@ -48,23 +48,31 @@ require 'mcp_client'
|
|
48
48
|
client = MCPClient.create_client(
|
49
49
|
mcp_server_configs: [
|
50
50
|
# Local stdio server
|
51
|
-
MCPClient.stdio_config(
|
51
|
+
MCPClient.stdio_config(
|
52
|
+
command: 'npx -y @modelcontextprotocol/server-filesystem /home/user',
|
53
|
+
name: 'filesystem' # Optional name for this server
|
54
|
+
),
|
52
55
|
# Remote HTTP SSE server (with streaming support)
|
53
56
|
MCPClient.sse_config(
|
54
57
|
base_url: 'https://api.example.com/sse',
|
55
58
|
headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
|
59
|
+
name: 'api', # Optional name for this server
|
56
60
|
read_timeout: 30, # Optional timeout in seconds (default: 30)
|
57
61
|
ping: 10, # Optional ping interval in seconds of inactivity (default: 10)
|
58
62
|
# Connection closes automatically after inactivity (2.5x ping interval)
|
59
63
|
retries: 3, # Optional number of retry attempts (default: 0)
|
60
|
-
retry_backoff: 1
|
64
|
+
retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
|
61
65
|
# Native support for tool streaming via call_tool_streaming method
|
62
|
-
)
|
66
|
+
logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
|
67
|
+
) ],
|
68
|
+
# Optional logger for the client and all servers without explicit loggers
|
69
|
+
logger: Logger.new($stdout, level: Logger::WARN)
|
63
70
|
)
|
64
71
|
|
65
72
|
# Or load server definitions from a JSON file
|
66
73
|
client = MCPClient.create_client(
|
67
|
-
server_definition_file: 'path/to/server_definition.json'
|
74
|
+
server_definition_file: 'path/to/server_definition.json',
|
75
|
+
logger: Logger.new($stdout, level: Logger::WARN) # Optional logger for client and servers
|
68
76
|
)
|
69
77
|
|
70
78
|
# MCP server configuration JSON format can be:
|
@@ -74,10 +82,14 @@ client = MCPClient.create_client(
|
|
74
82
|
# [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }]
|
75
83
|
# 3. An object with "mcpServers" key containing named servers:
|
76
84
|
# { "mcpServers": { "server1": { "type": "sse", "url": "http://..." } } }
|
85
|
+
# Note: When using this format, server1 will be accessible by name
|
77
86
|
|
78
87
|
# List available tools
|
79
88
|
tools = client.list_tools
|
80
89
|
|
90
|
+
# Find a server by name
|
91
|
+
filesystem_server = client.find_server('filesystem')
|
92
|
+
|
81
93
|
# Find tools by name pattern (string or regex)
|
82
94
|
file_tools = client.find_tools('file')
|
83
95
|
first_tool = client.find_tool(/^file_/)
|
@@ -85,15 +97,20 @@ first_tool = client.find_tool(/^file_/)
|
|
85
97
|
# Call a specific tool by name
|
86
98
|
result = client.call_tool('example_tool', { param1: 'value1', param2: 42 })
|
87
99
|
|
100
|
+
# Call a tool on a specific server by name
|
101
|
+
result = client.call_tool('example_tool', { param1: 'value1' }, server: 'filesystem')
|
102
|
+
# You can also call a tool on a server directly
|
103
|
+
result = filesystem_server.call_tool('example_tool', { param1: 'value1' })
|
104
|
+
|
88
105
|
# Call multiple tools in batch
|
89
106
|
results = client.call_tools([
|
90
107
|
{ name: 'tool1', parameters: { key1: 'value1' } },
|
91
|
-
{ name: 'tool2', parameters: { key2: 'value2' } }
|
108
|
+
{ name: 'tool2', parameters: { key2: 'value2' }, server: 'filesystem' } # Specify server for a specific tool
|
92
109
|
])
|
93
110
|
|
94
111
|
# Stream results (supported by the SSE transport)
|
95
112
|
# Returns an Enumerator that yields results as they become available
|
96
|
-
client.call_tool_streaming('streaming_tool', { param: 'value' }).each do |chunk|
|
113
|
+
client.call_tool_streaming('streaming_tool', { param: 'value' }, server: 'api').each do |chunk|
|
97
114
|
# Process each chunk as it arrives
|
98
115
|
puts chunk
|
99
116
|
end
|
@@ -105,13 +122,14 @@ google_tools = client.to_google_tools
|
|
105
122
|
|
106
123
|
# Register for server notifications
|
107
124
|
client.on_notification do |server, method, params|
|
108
|
-
puts "Server notification: #{server.class} - #{method} - #{params}"
|
125
|
+
puts "Server notification: #{server.class}[#{server.name}] - #{method} - #{params}"
|
109
126
|
# Handle specific notifications based on method name
|
110
127
|
# 'notifications/tools/list_changed' is handled automatically by the client
|
111
128
|
end
|
112
129
|
|
113
130
|
# Send custom JSON-RPC requests or notifications
|
114
|
-
client.send_rpc('custom_method', params: { key: 'value' }, server: :sse) # Uses specific server
|
131
|
+
client.send_rpc('custom_method', params: { key: 'value' }, server: :sse) # Uses specific server by type
|
132
|
+
client.send_rpc('custom_method', params: { key: 'value' }, server: 'filesystem') # Uses specific server by name
|
115
133
|
result = client.send_rpc('another_method', params: { data: 123 }) # Uses first available server
|
116
134
|
client.send_notification('status_update', params: { status: 'ready' })
|
117
135
|
|
@@ -146,7 +164,8 @@ sse_client = MCPClient.create_client(
|
|
146
164
|
read_timeout: 30, # Timeout in seconds for request fulfillment
|
147
165
|
ping: 10, # Send ping after 10 seconds of inactivity
|
148
166
|
# Connection closes automatically after inactivity (2.5x ping interval)
|
149
|
-
retries: 2
|
167
|
+
retries: 2, # Number of retry attempts on transient errors
|
168
|
+
logger: logger # Optional logger for debugging connection issues
|
150
169
|
)
|
151
170
|
]
|
152
171
|
)
|
@@ -342,15 +361,19 @@ Special configuration options:
|
|
342
361
|
|
343
362
|
- **Multiple transports** - Support for both stdio and SSE transports
|
344
363
|
- **Multiple servers** - Connect to multiple MCP servers simultaneously
|
364
|
+
- **Named servers** - Associate names with servers and find/reference them by name
|
365
|
+
- **Server lookup** - Find servers by name using `find_server`
|
366
|
+
- **Tool association** - Each tool knows which server it belongs to
|
345
367
|
- **Tool discovery** - Find tools by name or pattern
|
368
|
+
- **Server disambiguation** - Specify which server to use when tools with same name exist in multiple servers
|
346
369
|
- **Atomic tool calls** - Simple API for invoking tools with parameters
|
347
370
|
- **Batch support** - Call multiple tools in a single operation
|
348
|
-
- **API conversions** - Built-in format conversion for OpenAI and
|
371
|
+
- **API conversions** - Built-in format conversion for OpenAI, Anthropic, and Google Vertex AI APIs
|
349
372
|
- **Thread safety** - Synchronized access for thread-safe operation
|
350
373
|
- **Server notifications** - Support for JSON-RPC notifications
|
351
374
|
- **Custom RPC methods** - Send any custom JSON-RPC method
|
352
375
|
- **Consistent error handling** - Rich error types for better exception handling
|
353
|
-
- **JSON configuration** - Support for server definition files in JSON format
|
376
|
+
- **JSON configuration** - Support for server definition files in JSON format with name retention
|
354
377
|
|
355
378
|
### Server-Sent Events (SSE) Implementation
|
356
379
|
|
@@ -362,6 +385,13 @@ The SSE client implementation provides these key features:
|
|
362
385
|
- **Automatic ping**: Sends ping requests after a configurable period of inactivity (default: 10 seconds)
|
363
386
|
- **Automatic disconnection**: Closes idle connections after inactivity (2.5× ping interval)
|
364
387
|
- **MCP compliant**: Any server communication resets the inactivity timer per specification
|
388
|
+
- **Intelligent reconnection**:
|
389
|
+
- **Ping failure detection**: Tracks consecutive ping failures (when server isn't responding)
|
390
|
+
- **Automatic reconnection**: Attempts to reconnect after 3 consecutive ping failures
|
391
|
+
- **Exponential backoff**: Uses increasing delays between reconnection attempts
|
392
|
+
- **Smart retry limits**: Caps reconnection attempts (default: 5) to avoid infinite loops
|
393
|
+
- **Connection state monitoring**: Properly detects and handles closed connections to prevent errors
|
394
|
+
- **Failure transparency**: Handles reconnection in the background without disrupting client code
|
365
395
|
- **Thread safety**: All operations are thread-safe using monitors and synchronized access
|
366
396
|
- **Reliable error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
|
367
397
|
- **JSON-RPC over SSE**: Full implementation of JSON-RPC 2.0 over SSE transport with initialize handshake
|
data/lib/mcp_client/client.rb
CHANGED
@@ -23,7 +23,7 @@ module MCPClient
|
|
23
23
|
@logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
|
24
24
|
@servers = mcp_server_configs.map do |config|
|
25
25
|
@logger.debug("Creating server with config: #{config.inspect}")
|
26
|
-
MCPClient::ServerFactory.create(config)
|
26
|
+
MCPClient::ServerFactory.create(config, logger: @logger)
|
27
27
|
end
|
28
28
|
@tool_cache = {}
|
29
29
|
# JSON-RPC notification listeners
|
@@ -42,15 +42,35 @@ module MCPClient
|
|
42
42
|
# Lists all available tools from all connected MCP servers
|
43
43
|
# @param cache [Boolean] whether to use cached tools or fetch fresh
|
44
44
|
# @return [Array<MCPClient::Tool>] list of available tools
|
45
|
+
# @raise [MCPClient::Errors::ConnectionError] on authorization failures
|
46
|
+
# @raise [MCPClient::Errors::ToolCallError] if no tools could be retrieved from any server
|
45
47
|
def list_tools(cache: true)
|
46
48
|
return @tool_cache.values if cache && !@tool_cache.empty?
|
47
49
|
|
48
50
|
tools = []
|
51
|
+
connection_errors = []
|
52
|
+
|
49
53
|
servers.each do |server|
|
50
54
|
server.list_tools.each do |tool|
|
51
55
|
@tool_cache[tool.name] = tool
|
52
56
|
tools << tool
|
53
57
|
end
|
58
|
+
rescue MCPClient::Errors::ConnectionError => e
|
59
|
+
# Fast-fail on authorization errors for better user experience
|
60
|
+
# If this is the first server or we haven't collected any tools yet,
|
61
|
+
# raise the auth error directly to avoid cascading error messages
|
62
|
+
raise e if e.message.include?('Authorization failed') && tools.empty?
|
63
|
+
|
64
|
+
# Store the error and try other servers
|
65
|
+
connection_errors << e
|
66
|
+
@logger.error("Server error: #{e.message}")
|
67
|
+
end
|
68
|
+
|
69
|
+
# If we didn't get any tools from any server but have servers configured, report failure
|
70
|
+
if tools.empty? && !servers.empty?
|
71
|
+
raise connection_errors.first if connection_errors.any?
|
72
|
+
|
73
|
+
raise MCPClient::Errors::ToolCallError, 'Failed to retrieve tools from any server'
|
54
74
|
end
|
55
75
|
|
56
76
|
tools
|
@@ -59,21 +79,51 @@ module MCPClient
|
|
59
79
|
# Calls a specific tool by name with the given parameters
|
60
80
|
# @param tool_name [String] the name of the tool to call
|
61
81
|
# @param parameters [Hash] the parameters to pass to the tool
|
82
|
+
# @param server [String, Symbol, Integer, MCPClient::ServerBase, nil] optional server to use
|
62
83
|
# @return [Object] the result of the tool invocation
|
63
|
-
def call_tool(tool_name, parameters)
|
84
|
+
def call_tool(tool_name, parameters, server: nil)
|
64
85
|
tools = list_tools
|
65
|
-
tool = tools.find { |t| t.name == tool_name }
|
66
86
|
|
67
|
-
|
87
|
+
if server
|
88
|
+
# Use the specified server
|
89
|
+
srv = select_server(server)
|
90
|
+
# Find the tool on this specific server
|
91
|
+
tool = tools.find { |t| t.name == tool_name && t.server == srv }
|
92
|
+
unless tool
|
93
|
+
raise MCPClient::Errors::ToolNotFound,
|
94
|
+
"Tool '#{tool_name}' not found on server '#{srv.name || srv.class.name}'"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
# Find the tool across all servers
|
98
|
+
matching_tools = tools.select { |t| t.name == tool_name }
|
99
|
+
|
100
|
+
if matching_tools.empty?
|
101
|
+
raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found"
|
102
|
+
elsif matching_tools.size > 1
|
103
|
+
# If multiple matches, disambiguate with server names
|
104
|
+
server_names = matching_tools.map { |t| t.server&.name || 'unnamed' }
|
105
|
+
raise MCPClient::Errors::AmbiguousToolName,
|
106
|
+
"Multiple tools named '#{tool_name}' found across servers (#{server_names.join(', ')}). " \
|
107
|
+
"Please specify a server using the 'server' parameter."
|
108
|
+
end
|
109
|
+
|
110
|
+
tool = matching_tools.first
|
111
|
+
end
|
68
112
|
|
69
113
|
# Validate parameters against tool schema
|
70
114
|
validate_params!(tool, parameters)
|
71
115
|
|
72
|
-
#
|
73
|
-
server =
|
116
|
+
# Use the tool's associated server
|
117
|
+
server = tool.server
|
74
118
|
raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server
|
75
119
|
|
76
|
-
|
120
|
+
begin
|
121
|
+
server.call_tool(tool_name, parameters)
|
122
|
+
rescue MCPClient::Errors::ConnectionError => e
|
123
|
+
# Add server identity information to the error for better context
|
124
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name
|
125
|
+
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message} (Server: #{server_id})"
|
126
|
+
end
|
77
127
|
end
|
78
128
|
|
79
129
|
# Convert MCP tools to OpenAI function specifications
|
@@ -94,6 +144,9 @@ module MCPClient
|
|
94
144
|
tools.map(&:to_anthropic_tool)
|
95
145
|
end
|
96
146
|
|
147
|
+
# Convert MCP tools to Google Vertex AI tool specifications
|
148
|
+
# @param tool_names [Array<String>, nil] optional list of tool names to include, nil means all tools
|
149
|
+
# @return [Array<Hash>] Google Vertex AI tool specifications with cleaned schemas
|
97
150
|
def to_google_tools(tool_names: nil)
|
98
151
|
tools = list_tools
|
99
152
|
tools = tools.select { |t| tool_names.include?(t.name) } if tool_names
|
@@ -118,6 +171,13 @@ module MCPClient
|
|
118
171
|
@notification_listeners << block
|
119
172
|
end
|
120
173
|
|
174
|
+
# Find a server by name
|
175
|
+
# @param name [String] the name of the server to find
|
176
|
+
# @return [MCPClient::ServerBase, nil] the server with the given name, or nil if not found
|
177
|
+
def find_server(name)
|
178
|
+
@servers.find { |s| s.name == name }
|
179
|
+
end
|
180
|
+
|
121
181
|
# Find all tools whose name matches the given pattern (String or Regexp)
|
122
182
|
# @param pattern [String, Regexp] pattern to match tool names
|
123
183
|
# @return [Array<MCPClient::Tool>] matching tools
|
@@ -134,13 +194,15 @@ module MCPClient
|
|
134
194
|
end
|
135
195
|
|
136
196
|
# Call multiple tools in batch
|
137
|
-
# @param calls [Array<Hash>] array of calls in the form
|
197
|
+
# @param calls [Array<Hash>] array of calls in the form:
|
198
|
+
# { name: tool_name, parameters: {...}, server: optional_server_name }
|
138
199
|
# @return [Array<Object>] array of results for each tool invocation
|
139
200
|
def call_tools(calls)
|
140
201
|
calls.map do |call|
|
141
202
|
name = call[:name] || call['name']
|
142
203
|
params = call[:parameters] || call['parameters'] || {}
|
143
|
-
|
204
|
+
server = call[:server] || call['server']
|
205
|
+
call_tool(name, params, server: server)
|
144
206
|
end
|
145
207
|
end
|
146
208
|
|
@@ -148,24 +210,52 @@ module MCPClient
|
|
148
210
|
# Returns an Enumerator yielding streaming updates if supported.
|
149
211
|
# @param tool_name [String] the name of the tool to call
|
150
212
|
# @param parameters [Hash] the parameters to pass to the tool
|
213
|
+
# @param server [String, Symbol, Integer, MCPClient::ServerBase, nil] optional server to use
|
151
214
|
# @return [Enumerator] streaming enumerator or single-value enumerator
|
152
|
-
def call_tool_streaming(tool_name, parameters)
|
215
|
+
def call_tool_streaming(tool_name, parameters, server: nil)
|
153
216
|
tools = list_tools
|
154
|
-
|
155
|
-
|
217
|
+
|
218
|
+
if server
|
219
|
+
# Use the specified server
|
220
|
+
srv = select_server(server)
|
221
|
+
# Find the tool on this specific server
|
222
|
+
tool = tools.find { |t| t.name == tool_name && t.server == srv }
|
223
|
+
unless tool
|
224
|
+
raise MCPClient::Errors::ToolNotFound,
|
225
|
+
"Tool '#{tool_name}' not found on server '#{srv.name || srv.class.name}'"
|
226
|
+
end
|
227
|
+
else
|
228
|
+
# Find the tool across all servers
|
229
|
+
matching_tools = tools.select { |t| t.name == tool_name }
|
230
|
+
|
231
|
+
if matching_tools.empty?
|
232
|
+
raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found"
|
233
|
+
elsif matching_tools.size > 1
|
234
|
+
# If multiple matches, disambiguate with server names
|
235
|
+
server_names = matching_tools.map { |t| t.server&.name || 'unnamed' }
|
236
|
+
raise MCPClient::Errors::AmbiguousToolName,
|
237
|
+
"Multiple tools named '#{tool_name}' found across servers (#{server_names.join(', ')}). " \
|
238
|
+
"Please specify a server using the 'server' parameter."
|
239
|
+
end
|
240
|
+
|
241
|
+
tool = matching_tools.first
|
242
|
+
end
|
156
243
|
|
157
244
|
# Validate parameters against tool schema
|
158
245
|
validate_params!(tool, parameters)
|
159
|
-
|
160
|
-
|
246
|
+
|
247
|
+
# Use the tool's associated server
|
248
|
+
server = tool.server
|
161
249
|
raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server
|
162
250
|
|
163
|
-
|
251
|
+
begin
|
252
|
+
# Use the streaming API if it's available
|
164
253
|
server.call_tool_streaming(tool_name, parameters)
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
254
|
+
rescue MCPClient::Errors::ConnectionError => e
|
255
|
+
# Add server identity information to the error for better context
|
256
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name
|
257
|
+
msg = "Error calling streaming tool '#{tool_name}': #{e.message} (Server: #{server_id})"
|
258
|
+
raise MCPClient::Errors::ToolCallError, msg
|
169
259
|
end
|
170
260
|
end
|
171
261
|
|
@@ -218,20 +308,24 @@ module MCPClient
|
|
218
308
|
# @param params [Hash] parameters for the notification
|
219
309
|
# @return [void]
|
220
310
|
def process_notification(server, method, params)
|
311
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class
|
221
312
|
case method
|
222
313
|
when 'notifications/tools/list_changed'
|
223
|
-
logger.warn("[#{
|
314
|
+
logger.warn("[#{server_id}] Tool list has changed, clearing tool cache")
|
224
315
|
clear_cache
|
225
316
|
when 'notifications/resources/updated'
|
226
|
-
logger.warn("[#{
|
317
|
+
logger.warn("[#{server_id}] Resource #{params['uri']} updated")
|
227
318
|
when 'notifications/prompts/list_changed'
|
228
|
-
logger.warn("[#{
|
319
|
+
logger.warn("[#{server_id}] Prompt list has changed")
|
229
320
|
when 'notifications/resources/list_changed'
|
230
|
-
logger.warn("[#{
|
321
|
+
logger.warn("[#{server_id}] Resource list has changed")
|
322
|
+
else
|
323
|
+
# Log unknown notification types for debugging purposes
|
324
|
+
logger.debug("[#{server_id}] Received unknown notification: #{method} - #{params}")
|
231
325
|
end
|
232
326
|
end
|
233
327
|
|
234
|
-
# Select a server based on index, type, or instance
|
328
|
+
# Select a server based on index, name, type, or instance
|
235
329
|
# @param server_arg [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
|
236
330
|
# @return [MCPClient::ServerBase]
|
237
331
|
def select_server(server_arg)
|
@@ -246,15 +340,20 @@ module MCPClient
|
|
246
340
|
end
|
247
341
|
when String, Symbol
|
248
342
|
key = server_arg.to_s.downcase
|
343
|
+
|
344
|
+
# First check if it's a server name match
|
345
|
+
srv = @servers.find { |s| s.name && s.name.downcase == key }
|
346
|
+
return srv if srv
|
347
|
+
|
348
|
+
# Then check if it's a server type match
|
249
349
|
srv = @servers.find { |s| s.class.name.split('::').last.downcase.end_with?(key) }
|
250
|
-
raise MCPClient::Errors::ServerNotFound, "Server
|
350
|
+
raise MCPClient::Errors::ServerNotFound, "Server with name or type '#{server_arg}' not found" unless srv
|
251
351
|
|
252
352
|
srv
|
253
353
|
else
|
254
354
|
raise ArgumentError, "Invalid server argument: #{server_arg.inspect}" unless @servers.include?(server_arg)
|
255
355
|
|
256
356
|
server_arg
|
257
|
-
|
258
357
|
end
|
259
358
|
end
|
260
359
|
|
@@ -30,7 +30,11 @@ module MCPClient
|
|
30
30
|
next unless validate_server_config(config, server_name)
|
31
31
|
|
32
32
|
server_config = process_server_config(config, server_name)
|
33
|
-
|
33
|
+
next unless server_config
|
34
|
+
|
35
|
+
# Add server name to the config
|
36
|
+
server_config[:name] = server_name
|
37
|
+
result[server_name] = server_config
|
34
38
|
end
|
35
39
|
|
36
40
|
result
|
data/lib/mcp_client/errors.rb
CHANGED
@@ -23,7 +23,11 @@ module MCPClient
|
|
23
23
|
|
24
24
|
# Raised when there's an error in the MCP server transport
|
25
25
|
class TransportError < MCPError; end
|
26
|
+
|
26
27
|
# Raised when tool parameters fail validation against JSON schema
|
27
28
|
class ValidationError < MCPError; end
|
29
|
+
|
30
|
+
# Raised when multiple tools with the same name exist across different servers
|
31
|
+
class AmbiguousToolName < MCPError; end
|
28
32
|
end
|
29
33
|
end
|
@@ -3,6 +3,16 @@
|
|
3
3
|
module MCPClient
|
4
4
|
# Base class for MCP servers - serves as the interface for different server implementations
|
5
5
|
class ServerBase
|
6
|
+
# @!attribute [r] name
|
7
|
+
# @return [String] the name of the server
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# Initialize the server with a name
|
11
|
+
# @param name [String, nil] server name
|
12
|
+
def initialize(name: nil)
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
6
16
|
# Initialize a connection to the MCP server
|
7
17
|
# @return [Boolean] true if connection successful
|
8
18
|
def connect
|
@@ -45,6 +55,16 @@ module MCPClient
|
|
45
55
|
raise NotImplementedError, 'Subclasses must implement rpc_notify'
|
46
56
|
end
|
47
57
|
|
58
|
+
# Stream a tool call result (default implementation returns single-value stream)
|
59
|
+
# @param tool_name [String] the name of the tool to call
|
60
|
+
# @param parameters [Hash] the parameters to pass to the tool
|
61
|
+
# @return [Enumerator] stream of results
|
62
|
+
def call_tool_streaming(tool_name, parameters)
|
63
|
+
Enumerator.new do |yielder|
|
64
|
+
yielder << call_tool(tool_name, parameters)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
48
68
|
# Ping the MCP server to check connectivity (zero-parameter heartbeat call)
|
49
69
|
# @return [Object] result from the ping request
|
50
70
|
def ping
|
@@ -5,8 +5,9 @@ module MCPClient
|
|
5
5
|
class ServerFactory
|
6
6
|
# Create a server instance based on configuration
|
7
7
|
# @param config [Hash] server configuration
|
8
|
+
# @param logger [Logger, nil] optional logger to use for the server
|
8
9
|
# @return [MCPClient::ServerBase] server instance
|
9
|
-
def self.create(config)
|
10
|
+
def self.create(config, logger: nil)
|
10
11
|
case config[:type]
|
11
12
|
when 'stdio'
|
12
13
|
MCPClient::ServerStdio.new(
|
@@ -14,7 +15,8 @@ module MCPClient
|
|
14
15
|
retries: config[:retries] || 0,
|
15
16
|
retry_backoff: config[:retry_backoff] || 1,
|
16
17
|
read_timeout: config[:read_timeout] || MCPClient::ServerStdio::READ_TIMEOUT,
|
17
|
-
|
18
|
+
name: config[:name],
|
19
|
+
logger: config[:logger] || logger
|
18
20
|
)
|
19
21
|
when 'sse'
|
20
22
|
MCPClient::ServerSSE.new(
|
@@ -24,7 +26,8 @@ module MCPClient
|
|
24
26
|
ping: config[:ping] || 10,
|
25
27
|
retries: config[:retries] || 0,
|
26
28
|
retry_backoff: config[:retry_backoff] || 1,
|
27
|
-
|
29
|
+
name: config[:name],
|
30
|
+
logger: config[:logger] || logger
|
28
31
|
)
|
29
32
|
else
|
30
33
|
raise ArgumentError, "Unknown server type: #{config[:type]}"
|