ruby-mcp-client 0.5.0 → 0.5.1

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: 79a86302428257274c4e620fae0b7ad45c6229cf381a956ef3b0a7e736f17f62
4
- data.tar.gz: 80727b0aa48a992054dafd7c484eb611404b47d878ef2869e59e937a815718dc
3
+ metadata.gz: 16280a305c2b9cd19ecc2a71b8f10d3805644015f34f7eaec28d5846f12fa1db
4
+ data.tar.gz: 48a791c3a88255c25078234819024dace993f7cfc564485f48680a8768b81238
5
5
  SHA512:
6
- metadata.gz: 977acc1ae8ca48a17b7827f006ed82578eb7bc24bf9ba08b03e77493e3447febc82287242a410861fb675b1fb74b0b67525dc4f92c9f8b448cd1cad2e3afc875
7
- data.tar.gz: f940ed03a52065a5d8687d1722a588693d69a9c69f820771b328fed3b1bb69068fc7110dbedc745ea4fc42d047afa8afe975b2ed821aa5d33ac8c4e25fac9da7
6
+ metadata.gz: d4155463bcc691725b3a88e29dbb7df75a5b05a9ca7693d12a4fcc6f43bb57df06966089042dcaebb6f9f0a3b429a1eaa3138bb478ad161aad98163eb6de2763
7
+ data.tar.gz: 6acb25bd0f1c72d640570823c262f548bda1f09322c137789ad36d5950dc80eda6d66e707b0c69229805ffdb575aea086ae999747d74aef9304ad65c46e67039
data/README.md CHANGED
@@ -56,10 +56,22 @@ client = MCPClient.create_client(
56
56
  retries: 3, # Optional number of retry attempts (default: 0)
57
57
  retry_backoff: 1 # Optional backoff delay in seconds (default: 1)
58
58
  # Native support for tool streaming via call_tool_streaming method
59
- )
60
- ]
59
+ ) ]
61
60
  )
62
61
 
62
+ # Or load server definitions from a JSON file
63
+ client = MCPClient.create_client(
64
+ server_definition_file: 'path/to/server_definition.json'
65
+ )
66
+
67
+ # MCP server configuration JSON format can be:
68
+ # 1. A single server object:
69
+ # { "type": "sse", "url": "http://example.com/sse" }
70
+ # 2. An array of server objects:
71
+ # [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }]
72
+ # 3. An object with "mcpServers" key containing named servers:
73
+ # { "mcpServers": { "server1": { "type": "sse", "url": "http://..." } } }
74
+
63
75
  # List available tools
64
76
  tools = client.list_tools
65
77
 
@@ -260,6 +272,62 @@ This client works with any MCP-compatible server, including:
260
272
  - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
261
273
  - Custom servers implementing the MCP protocol
262
274
 
275
+ ### Server Definition Files
276
+
277
+ You can define MCP server configurations in JSON files for easier management:
278
+
279
+ ```json
280
+ {
281
+ "mcpServers": {
282
+ "playwright": {
283
+ "type": "sse",
284
+ "url": "http://localhost:8931/sse",
285
+ "headers": {
286
+ "Authorization": "Bearer TOKEN"
287
+ }
288
+ },
289
+ "filesystem": {
290
+ "type": "stdio",
291
+ "command": "npx",
292
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
293
+ "env": {
294
+ "DEBUG": "true"
295
+ }
296
+ }
297
+ }
298
+ }
299
+ ```
300
+
301
+ A simpler example used in the Playwright demo (found in `examples/playwright_server_definition.json`):
302
+
303
+ ```json
304
+ {
305
+ "mcpServers": {
306
+ "playwright": {
307
+ "url": "http://localhost:8931/sse",
308
+ "headers": {},
309
+ "comment": "Local Playwright MCP Server running on port 8931"
310
+ }
311
+ }
312
+ }
313
+ ```
314
+
315
+ Load this configuration with:
316
+
317
+ ```ruby
318
+ client = MCPClient.create_client(server_definition_file: 'path/to/definition.json')
319
+ ```
320
+
321
+ The JSON format supports:
322
+ 1. A single server object: `{ "type": "sse", "url": "..." }`
323
+ 2. An array of server objects: `[{ "type": "stdio", ... }, { "type": "sse", ... }]`
324
+ 3. An object with named servers under `mcpServers` key (as shown above)
325
+
326
+ Special configuration options:
327
+ - `comment` and `description` are reserved keys that are ignored during parsing and can be used for documentation
328
+ - Server type can be inferred from the presence of either `command` (for stdio) or `url` (for SSE)
329
+ - All string values in arrays (like `args`) are automatically converted to strings
330
+
263
331
  ## Key Features
264
332
 
265
333
  ### Client Features
@@ -274,6 +342,7 @@ This client works with any MCP-compatible server, including:
274
342
  - **Server notifications** - Support for JSON-RPC notifications
275
343
  - **Custom RPC methods** - Send any custom JSON-RPC method
276
344
  - **Consistent error handling** - Rich error types for better exception handling
345
+ - **JSON configuration** - Support for server definition files in JSON format
277
346
 
278
347
  ### Server-Sent Events (SSE) Implementation
279
348
 
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'logger'
5
+
6
+ module MCPClient
7
+ # Parses MCP server definition JSON files into configuration hashes
8
+ class ConfigParser
9
+ # Reserved JSON keys that shouldn't be included in final config
10
+ RESERVED_KEYS = %w[comment description].freeze
11
+
12
+ # @param file_path [String] path to JSON file containing 'mcpServers' definitions
13
+ # @param logger [Logger, nil] optional logger for warnings
14
+ def initialize(file_path, logger: nil)
15
+ @file_path = file_path
16
+ @logger = logger || Logger.new($stdout, level: Logger::WARN)
17
+ end
18
+
19
+ # Parse the JSON config and return a mapping of server names to clean config hashes
20
+ # @return [Hash<String, Hash>] server name => config hash with symbol keys
21
+ def parse
22
+ content = File.read(@file_path)
23
+ data = JSON.parse(content)
24
+
25
+ servers_data = extract_servers_data(data)
26
+ servers_data = filter_reserved_keys(servers_data)
27
+
28
+ result = {}
29
+ servers_data.each do |server_name, config|
30
+ next unless validate_server_config(config, server_name)
31
+
32
+ server_config = process_server_config(config, server_name)
33
+ result[server_name] = server_config if server_config
34
+ end
35
+
36
+ result
37
+ rescue Errno::ENOENT
38
+ raise Errno::ENOENT, "Server definition file not found: #{@file_path}"
39
+ rescue JSON::ParserError => e
40
+ raise JSON::ParserError, "Invalid JSON in #{@file_path}: #{e.message}"
41
+ end
42
+
43
+ # Extract server data from parsed JSON
44
+ # @param data [Object] parsed JSON data
45
+ # @return [Hash] normalized server data
46
+ def extract_servers_data(data)
47
+ if data.is_a?(Hash) && data.key?('mcpServers') && data['mcpServers'].is_a?(Hash)
48
+ data['mcpServers']
49
+ elsif data.is_a?(Array)
50
+ h = {}
51
+ data.each_with_index { |cfg, idx| h[idx.to_s] = cfg }
52
+ h
53
+ elsif data.is_a?(Hash)
54
+ { '0' => data }
55
+ else
56
+ @logger.warn("Invalid root JSON structure in #{@file_path}: #{data.class}")
57
+ {}
58
+ end
59
+ end
60
+
61
+ # Validate server configuration is a hash
62
+ # @param config [Object] server configuration to validate
63
+ # @param server_name [String] name of the server
64
+ # @return [Boolean] true if valid, false otherwise
65
+ def validate_server_config(config, server_name)
66
+ return true if config.is_a?(Hash)
67
+
68
+ @logger.warn("Configuration for server '#{server_name}' is not an object; skipping.")
69
+ false
70
+ end
71
+
72
+ # Process a single server configuration
73
+ # @param config [Hash] server configuration to process
74
+ # @param server_name [String] name of the server
75
+ # @return [Hash, nil] processed configuration or nil if invalid
76
+ def process_server_config(config, server_name)
77
+ type = determine_server_type(config, server_name)
78
+ return nil unless type
79
+
80
+ clean = { type: type.to_s }
81
+ case type.to_s
82
+ when 'stdio'
83
+ parse_stdio_config(clean, config, server_name)
84
+ when 'sse'
85
+ return nil unless parse_sse_config(clean, config, server_name)
86
+ else
87
+ @logger.warn("Unrecognized type '#{type}' for server '#{server_name}'; skipping.")
88
+ return nil
89
+ end
90
+
91
+ clean
92
+ end
93
+
94
+ # Determine the type of server from its configuration
95
+ # @param config [Hash] server configuration
96
+ # @param server_name [String] name of the server for logging
97
+ # @return [String, nil] determined server type or nil if cannot be determined
98
+ def determine_server_type(config, server_name)
99
+ type = config['type']
100
+ return type if type
101
+
102
+ inferred_type = if config.key?('command') || config.key?('args') || config.key?('env')
103
+ 'stdio'
104
+ elsif config.key?('url')
105
+ 'sse'
106
+ end
107
+
108
+ if inferred_type
109
+ @logger.warn("'type' not specified for server '#{server_name}', inferring as '#{inferred_type}'.")
110
+ return inferred_type
111
+ end
112
+
113
+ @logger.warn("Could not determine type for server '#{server_name}' (missing 'command' or 'url'); skipping.")
114
+ nil
115
+ end
116
+
117
+ private
118
+
119
+ # Parse stdio-specific configuration
120
+ # @param clean [Hash] clean configuration hash to update
121
+ # @param config [Hash] raw configuration from JSON
122
+ # @param server_name [String] name of the server for error reporting
123
+ def parse_stdio_config(clean, config, server_name)
124
+ # Command is required
125
+ cmd = config['command']
126
+ unless cmd.is_a?(String)
127
+ @logger.warn("'command' for server '#{server_name}' is not a string; converting to string.")
128
+ cmd = cmd.to_s
129
+ end
130
+
131
+ # Args are optional
132
+ args = config['args']
133
+ if args.is_a?(Array)
134
+ args = args.map(&:to_s)
135
+ elsif args
136
+ @logger.warn("'args' for server '#{server_name}' is not an array; treating as single argument.")
137
+ args = [args.to_s]
138
+ else
139
+ args = []
140
+ end
141
+
142
+ # Environment variables are optional
143
+ env = config['env']
144
+ env = env.is_a?(Hash) ? env.transform_keys(&:to_s) : {}
145
+
146
+ # Update clean config
147
+ clean[:command] = cmd
148
+ clean[:args] = args
149
+ clean[:env] = env
150
+ end
151
+
152
+ # Parse SSE-specific configuration
153
+ # @param clean [Hash] clean configuration hash to update
154
+ # @param config [Hash] raw configuration from JSON
155
+ # @param server_name [String] name of the server for error reporting
156
+ # @return [Boolean] true if parsing succeeded, false if required elements are missing
157
+ def parse_sse_config(clean, config, server_name)
158
+ # URL is required
159
+ source = config['url']
160
+ unless source
161
+ @logger.warn("SSE server '#{server_name}' is missing required 'url' property; skipping.")
162
+ return false
163
+ end
164
+
165
+ unless source.is_a?(String)
166
+ @logger.warn("'url' for server '#{server_name}' is not a string; converting to string.")
167
+ source = source.to_s
168
+ end
169
+
170
+ # Headers are optional
171
+ headers = config['headers']
172
+ headers = headers.is_a?(Hash) ? headers.transform_keys(&:to_s) : {}
173
+
174
+ # Update clean config
175
+ clean[:url] = source
176
+ clean[:headers] = headers
177
+ true
178
+ end
179
+
180
+ # Filter out reserved keys from configuration objects
181
+ # @param data [Hash] configuration data
182
+ # @return [Hash] filtered configuration data
183
+ def filter_reserved_keys(data)
184
+ return data unless data.is_a?(Hash)
185
+
186
+ result = {}
187
+ data.each do |key, value|
188
+ # Skip reserved keys at server level
189
+ next if RESERVED_KEYS.include?(key)
190
+
191
+ # If value is a hash, recursively filter its keys too
192
+ if value.is_a?(Hash)
193
+ filtered_value = value.dup
194
+ RESERVED_KEYS.each { |reserved| filtered_value.delete(reserved) }
195
+ result[key] = filtered_value
196
+ else
197
+ result[key] = value
198
+ end
199
+ end
200
+ result
201
+ end
202
+ end
203
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.5.0'
5
+ VERSION = '0.5.1'
6
6
  end
data/lib/mcp_client.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'mcp_client/server_sse'
9
9
  require_relative 'mcp_client/server_factory'
10
10
  require_relative 'mcp_client/client'
11
11
  require_relative 'mcp_client/version'
12
+ require_relative 'mcp_client/config_parser'
12
13
 
13
14
  # Model Context Protocol (MCP) Client module
14
15
  # Provides a standardized way for agents to communicate with external tools and services
@@ -16,9 +17,31 @@ require_relative 'mcp_client/version'
16
17
  module MCPClient
17
18
  # Create a new MCPClient client
18
19
  # @param mcp_server_configs [Array<Hash>] configurations for MCP servers
20
+ # @param server_definition_file [String, nil] optional path to a JSON file defining server configurations
21
+ # The JSON may be a single server object or an array of server objects.
19
22
  # @return [MCPClient::Client] new client instance
20
- def self.create_client(mcp_server_configs: [])
21
- MCPClient::Client.new(mcp_server_configs: mcp_server_configs)
23
+ def self.create_client(mcp_server_configs: [], server_definition_file: nil)
24
+ require 'json'
25
+ # Start with any explicit configs provided
26
+ configs = Array(mcp_server_configs)
27
+ # Load additional configs from a JSON file if specified
28
+ if server_definition_file
29
+ # Parse JSON definitions into clean config hashes
30
+ parser = MCPClient::ConfigParser.new(server_definition_file)
31
+ parsed = parser.parse
32
+ parsed.each_value do |cfg|
33
+ case cfg[:type].to_s
34
+ when 'stdio'
35
+ # Build command list with args
36
+ cmd_list = [cfg[:command]] + Array(cfg[:args])
37
+ configs << MCPClient.stdio_config(command: cmd_list)
38
+ when 'sse'
39
+ # Use 'url' from parsed config as 'base_url' for SSE config
40
+ configs << MCPClient.sse_config(base_url: cfg[:url], headers: cfg[:headers] || {})
41
+ end
42
+ end
43
+ end
44
+ MCPClient::Client.new(mcp_server_configs: configs)
22
45
  end
23
46
 
24
47
  # Create a standard server configuration for stdio
@@ -35,13 +58,17 @@ module MCPClient
35
58
  # @param base_url [String] base URL for the server
36
59
  # @param headers [Hash] HTTP headers to include in requests
37
60
  # @param read_timeout [Integer] read timeout in seconds (default: 30)
61
+ # @param retries [Integer] number of retry attempts (default: 0)
62
+ # @param retry_backoff [Integer] backoff delay in seconds (default: 1)
38
63
  # @return [Hash] server configuration
39
- def self.sse_config(base_url:, headers: {}, read_timeout: 30)
64
+ def self.sse_config(base_url:, headers: {}, read_timeout: 30, retries: 0, retry_backoff: 1)
40
65
  {
41
66
  type: 'sse',
42
67
  base_url: base_url,
43
68
  headers: headers,
44
- read_timeout: read_timeout
69
+ read_timeout: read_timeout,
70
+ retries: retries,
71
+ retry_backoff: retry_backoff
45
72
  }
46
73
  end
47
74
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Szymon Kurcab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-25 00:00:00.000000000 Z
11
+ date: 2025-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -106,6 +106,7 @@ files:
106
106
  - README.md
107
107
  - lib/mcp_client.rb
108
108
  - lib/mcp_client/client.rb
109
+ - lib/mcp_client/config_parser.rb
109
110
  - lib/mcp_client/errors.rb
110
111
  - lib/mcp_client/server_base.rb
111
112
  - lib/mcp_client/server_factory.rb