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 +4 -4
- data/README.md +55 -12
- data/lib/mcp_client/client.rb +131 -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 +7 -3
- data/lib/mcp_client/server_sse.rb +511 -108
- data/lib/mcp_client/server_stdio.rb +4 -3
- data/lib/mcp_client/tool.rb +50 -4
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +22 -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
@@ -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(
|
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
|
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
|
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.
|
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,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
|
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
|
-
|
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
|
-
|
149
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
251
|
+
begin
|
252
|
+
# Use the streaming API if it's available
|
158
253
|
server.call_tool_streaming(tool_name, parameters)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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("[#{
|
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("[#{
|
317
|
+
logger.warn("[#{server_id}] Resource #{params['uri']} updated")
|
221
318
|
when 'notifications/prompts/list_changed'
|
222
|
-
logger.warn("[#{
|
319
|
+
logger.warn("[#{server_id}] Prompt list has changed")
|
223
320
|
when 'notifications/resources/list_changed'
|
224
|
-
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}")
|
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
|
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
|
-
|
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,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
|
-
|
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
|
-
|
29
|
+
name: config[:name],
|
30
|
+
logger: config[:logger] || logger
|
27
31
|
)
|
28
32
|
else
|
29
33
|
raise ArgumentError, "Unknown server type: #{config[:type]}"
|