ruby-mcp-client 0.5.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbd3caf1e80f6c2caf625495072211b827ae08e3542fa33306537918d0b6eb5f
4
- data.tar.gz: 1eea548ba10e7de3fec68a92d449e4552fde3c5920af045e2e828925dba2aad7
3
+ metadata.gz: a6607aff3aa197734ac1b5fd4c2f51bb994df18ffcb0f18928c4f75197aaa8ed
4
+ data.tar.gz: 6921d74355c4497ca944374795469a767873ff8716e54fedbe3438104a4e5b79
5
5
  SHA512:
6
- metadata.gz: e8eae783bd1e6a4db013a934e0686641befaec435fb288531ce85899efc0a66023d3d174dedffae21f75089f406f6368f286f13644701fbdee747492b795535b
7
- data.tar.gz: 534cc5c88f8f3737c5fc3e684174980af74ac18398532e4b5698490b451b5fc5ff0831ca48639119445c71f4afa94ba3fca94ff81edfa03d1e3ba34a73c4f1b1
6
+ metadata.gz: cea51f3ff6046ac619197413ccbe50a18fc3aeef6f881180d9cc5b0ad49b67d1cbe93459005f93f3063ad2dfe699c06bb1556180dc3bd1437d67e38b89549c9a
7
+ data.tar.gz: 8de670f6d7f3fd5a3c9786365e19034ee6d8734ef8d938769818acb606c8f7c13529b8777cdff2a54483398d52dd7847ccddc957a8a4517c16ad31a1b35700b0
data/README.md CHANGED
@@ -36,6 +36,7 @@ with popular AI services with built-in conversions:
36
36
 
37
37
  - `to_openai_tools()` - Formats tools for OpenAI API
38
38
  - `to_anthropic_tools()` - Formats tools for Anthropic Claude API
39
+ - `to_google_tools()` - Formats tools for Google Vertex AI API (automatically removes "$schema" keys not accepted by Vertex AI)
39
40
 
40
41
  ## Usage
41
42
 
@@ -47,21 +48,31 @@ require 'mcp_client'
47
48
  client = MCPClient.create_client(
48
49
  mcp_server_configs: [
49
50
  # Local stdio server
50
- MCPClient.stdio_config(command: 'npx -y @modelcontextprotocol/server-filesystem /home/user'),
51
+ MCPClient.stdio_config(
52
+ command: 'npx -y @modelcontextprotocol/server-filesystem /home/user',
53
+ name: 'filesystem' # Optional name for this server
54
+ ),
51
55
  # Remote HTTP SSE server (with streaming support)
52
56
  MCPClient.sse_config(
53
57
  base_url: 'https://api.example.com/sse',
54
58
  headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
59
+ name: 'api', # Optional name for this server
55
60
  read_timeout: 30, # Optional timeout in seconds (default: 30)
61
+ ping: 10, # Optional ping interval in seconds of inactivity (default: 10)
62
+ # Connection closes automatically after inactivity (2.5x ping interval)
56
63
  retries: 3, # Optional number of retry attempts (default: 0)
57
- retry_backoff: 1 # Optional backoff delay in seconds (default: 1)
64
+ retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
58
65
  # Native support for tool streaming via call_tool_streaming method
59
- ) ]
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)
60
70
  )
61
71
 
62
72
  # Or load server definitions from a JSON file
63
73
  client = MCPClient.create_client(
64
- 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
65
76
  )
66
77
 
67
78
  # MCP server configuration JSON format can be:
@@ -71,10 +82,14 @@ client = MCPClient.create_client(
71
82
  # [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }]
72
83
  # 3. An object with "mcpServers" key containing named servers:
73
84
  # { "mcpServers": { "server1": { "type": "sse", "url": "http://..." } } }
85
+ # Note: When using this format, server1 will be accessible by name
74
86
 
75
87
  # List available tools
76
88
  tools = client.list_tools
77
89
 
90
+ # Find a server by name
91
+ filesystem_server = client.find_server('filesystem')
92
+
78
93
  # Find tools by name pattern (string or regex)
79
94
  file_tools = client.find_tools('file')
80
95
  first_tool = client.find_tool(/^file_/)
@@ -82,15 +97,20 @@ first_tool = client.find_tool(/^file_/)
82
97
  # Call a specific tool by name
83
98
  result = client.call_tool('example_tool', { param1: 'value1', param2: 42 })
84
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
+
85
105
  # Call multiple tools in batch
86
106
  results = client.call_tools([
87
107
  { name: 'tool1', parameters: { key1: 'value1' } },
88
- { name: 'tool2', parameters: { key2: 'value2' } }
108
+ { name: 'tool2', parameters: { key2: 'value2' }, server: 'filesystem' } # Specify server for a specific tool
89
109
  ])
90
110
 
91
111
  # Stream results (supported by the SSE transport)
92
112
  # Returns an Enumerator that yields results as they become available
93
- 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|
94
114
  # Process each chunk as it arrives
95
115
  puts chunk
96
116
  end
@@ -98,16 +118,18 @@ end
98
118
  # Format tools for specific AI services
99
119
  openai_tools = client.to_openai_tools
100
120
  anthropic_tools = client.to_anthropic_tools
121
+ google_tools = client.to_google_tools
101
122
 
102
123
  # Register for server notifications
103
124
  client.on_notification do |server, method, params|
104
- puts "Server notification: #{server.class} - #{method} - #{params}"
125
+ puts "Server notification: #{server.class}[#{server.name}] - #{method} - #{params}"
105
126
  # Handle specific notifications based on method name
106
127
  # 'notifications/tools/list_changed' is handled automatically by the client
107
128
  end
108
129
 
109
130
  # Send custom JSON-RPC requests or notifications
110
- 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
111
133
  result = client.send_rpc('another_method', params: { data: 123 }) # Uses first available server
112
134
  client.send_notification('status_update', params: { status: 'ready' })
113
135
 
@@ -139,7 +161,11 @@ sse_client = MCPClient.create_client(
139
161
  mcp_server_configs: [
140
162
  MCPClient.sse_config(
141
163
  base_url: 'http://localhost:8931/sse',
142
- read_timeout: 30, # Timeout in seconds
164
+ read_timeout: 30, # Timeout in seconds for request fulfillment
165
+ ping: 10, # Send ping after 10 seconds of inactivity
166
+ # Connection closes automatically after inactivity (2.5x ping interval)
167
+ retries: 2, # Number of retry attempts on transient errors
168
+ logger: logger # Optional logger for debugging connection issues
143
169
  )
144
170
  ]
145
171
  )
@@ -262,6 +288,7 @@ Complete examples can be found in the `examples/` directory:
262
288
  - `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
263
289
  - `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
264
290
  - `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
291
+ - `gemini_ai_mcp.rb` - Integration with Google Vertex AI and Gemini models
265
292
  - `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
266
293
 
267
294
  ## MCP Server Compatibility
@@ -334,21 +361,37 @@ Special configuration options:
334
361
 
335
362
  - **Multiple transports** - Support for both stdio and SSE transports
336
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
337
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
338
369
  - **Atomic tool calls** - Simple API for invoking tools with parameters
339
370
  - **Batch support** - Call multiple tools in a single operation
340
- - **API conversions** - Built-in format conversion for OpenAI and Anthropic APIs
371
+ - **API conversions** - Built-in format conversion for OpenAI, Anthropic, and Google Vertex AI APIs
341
372
  - **Thread safety** - Synchronized access for thread-safe operation
342
373
  - **Server notifications** - Support for JSON-RPC notifications
343
374
  - **Custom RPC methods** - Send any custom JSON-RPC method
344
375
  - **Consistent error handling** - Rich error types for better exception handling
345
- - **JSON configuration** - Support for server definition files in JSON format
376
+ - **JSON configuration** - Support for server definition files in JSON format with name retention
346
377
 
347
378
  ### Server-Sent Events (SSE) Implementation
348
379
 
349
380
  The SSE client implementation provides these key features:
350
381
 
351
382
  - **Robust connection handling**: Properly manages HTTP/HTTPS connections with configurable timeouts and retries
383
+ - **Advanced connection management**:
384
+ - **Inactivity tracking**: Monitors connection activity to detect idle connections
385
+ - **Automatic ping**: Sends ping requests after a configurable period of inactivity (default: 10 seconds)
386
+ - **Automatic disconnection**: Closes idle connections after inactivity (2.5× ping interval)
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
352
395
  - **Thread safety**: All operations are thread-safe using monitors and synchronized access
353
396
  - **Reliable error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
354
397
  - **JSON-RPC over SSE**: Full implementation of JSON-RPC 2.0 over SSE transport with initialize handshake
@@ -411,4 +454,4 @@ This gem is available as open source under the [MIT License](LICENSE).
411
454
  ## Contributing
412
455
 
413
456
  Bug reports and pull requests are welcome on GitHub at
414
- https://github.com/simonx1/ruby-mcp-client.
457
+ https://github.com/simonx1/ruby-mcp-client.
@@ -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
- raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" unless tool
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
- # Find the server that owns this tool
73
- server = find_server_for_tool(tool)
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
- server.call_tool(tool_name, parameters)
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,15 @@ 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
150
+ def to_google_tools(tool_names: nil)
151
+ tools = list_tools
152
+ tools = tools.select { |t| tool_names.include?(t.name) } if tool_names
153
+ tools.map(&:to_google_tool)
154
+ end
155
+
97
156
  # Clean up all server connections
98
157
  def cleanup
99
158
  servers.each(&:cleanup)
@@ -112,6 +171,13 @@ module MCPClient
112
171
  @notification_listeners << block
113
172
  end
114
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
+
115
181
  # Find all tools whose name matches the given pattern (String or Regexp)
116
182
  # @param pattern [String, Regexp] pattern to match tool names
117
183
  # @return [Array<MCPClient::Tool>] matching tools
@@ -128,13 +194,15 @@ module MCPClient
128
194
  end
129
195
 
130
196
  # Call multiple tools in batch
131
- # @param calls [Array<Hash>] array of calls in the form { name: tool_name, parameters: {...} }
197
+ # @param calls [Array<Hash>] array of calls in the form:
198
+ # { name: tool_name, parameters: {...}, server: optional_server_name }
132
199
  # @return [Array<Object>] array of results for each tool invocation
133
200
  def call_tools(calls)
134
201
  calls.map do |call|
135
202
  name = call[:name] || call['name']
136
203
  params = call[:parameters] || call['parameters'] || {}
137
- call_tool(name, params)
204
+ server = call[:server] || call['server']
205
+ call_tool(name, params, server: server)
138
206
  end
139
207
  end
140
208
 
@@ -142,24 +210,52 @@ module MCPClient
142
210
  # Returns an Enumerator yielding streaming updates if supported.
143
211
  # @param tool_name [String] the name of the tool to call
144
212
  # @param parameters [Hash] the parameters to pass to the tool
213
+ # @param server [String, Symbol, Integer, MCPClient::ServerBase, nil] optional server to use
145
214
  # @return [Enumerator] streaming enumerator or single-value enumerator
146
- def call_tool_streaming(tool_name, parameters)
215
+ def call_tool_streaming(tool_name, parameters, server: nil)
147
216
  tools = list_tools
148
- tool = tools.find { |t| t.name == tool_name }
149
- raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" unless tool
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
150
243
 
151
244
  # Validate parameters against tool schema
152
245
  validate_params!(tool, parameters)
153
- # Find the server that owns this tool
154
- server = find_server_for_tool(tool)
246
+
247
+ # Use the tool's associated server
248
+ server = tool.server
155
249
  raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server
156
250
 
157
- if server.respond_to?(:call_tool_streaming)
251
+ begin
252
+ # Use the streaming API if it's available
158
253
  server.call_tool_streaming(tool_name, parameters)
159
- else
160
- Enumerator.new do |yielder|
161
- yielder << server.call_tool(tool_name, parameters)
162
- end
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
163
259
  end
164
260
  end
165
261
 
@@ -212,20 +308,24 @@ module MCPClient
212
308
  # @param params [Hash] parameters for the notification
213
309
  # @return [void]
214
310
  def process_notification(server, method, params)
311
+ server_id = server.name ? "#{server.class}[#{server.name}]" : server.class
215
312
  case method
216
313
  when 'notifications/tools/list_changed'
217
- logger.warn("[#{server.class}] Tool list has changed, clearing tool cache")
314
+ logger.warn("[#{server_id}] Tool list has changed, clearing tool cache")
218
315
  clear_cache
219
316
  when 'notifications/resources/updated'
220
- logger.warn("[#{server.class}] Resource #{params['uri']} updated")
317
+ logger.warn("[#{server_id}] Resource #{params['uri']} updated")
221
318
  when 'notifications/prompts/list_changed'
222
- logger.warn("[#{server.class}] Prompt list has changed")
319
+ logger.warn("[#{server_id}] Prompt list has changed")
223
320
  when 'notifications/resources/list_changed'
224
- logger.warn("[#{server.class}] Resource list has changed")
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}")
225
325
  end
226
326
  end
227
327
 
228
- # Select a server based on index, type, or instance
328
+ # Select a server based on index, name, type, or instance
229
329
  # @param server_arg [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
230
330
  # @return [MCPClient::ServerBase]
231
331
  def select_server(server_arg)
@@ -240,15 +340,20 @@ module MCPClient
240
340
  end
241
341
  when String, Symbol
242
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
243
349
  srv = @servers.find { |s| s.class.name.split('::').last.downcase.end_with?(key) }
244
- raise MCPClient::Errors::ServerNotFound, "Server of type #{server_arg} not found" unless srv
350
+ raise MCPClient::Errors::ServerNotFound, "Server with name or type '#{server_arg}' not found" unless srv
245
351
 
246
352
  srv
247
353
  else
248
354
  raise ArgumentError, "Invalid server argument: #{server_arg.inspect}" unless @servers.include?(server_arg)
249
355
 
250
356
  server_arg
251
-
252
357
  end
253
358
  end
254
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
- result[server_name] = server_config if server_config
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
@@ -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,16 +15,19 @@ 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
- logger: config[:logger]
18
+ name: config[:name],
19
+ logger: config[:logger] || logger
18
20
  )
19
21
  when 'sse'
20
22
  MCPClient::ServerSSE.new(
21
23
  base_url: config[:base_url],
22
24
  headers: config[:headers] || {},
23
25
  read_timeout: config[:read_timeout] || 30,
26
+ ping: config[:ping] || 10,
24
27
  retries: config[:retries] || 0,
25
28
  retry_backoff: config[:retry_backoff] || 1,
26
- logger: config[:logger]
29
+ name: config[:name],
30
+ logger: config[:logger] || logger
27
31
  )
28
32
  else
29
33
  raise ArgumentError, "Unknown server type: #{config[:type]}"