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 +4 -4
- data/README.md +71 -2
- data/lib/mcp_client/config_parser.rb +203 -0
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +31 -4
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16280a305c2b9cd19ecc2a71b8f10d3805644015f34f7eaec28d5846f12fa1db
|
4
|
+
data.tar.gz: 48a791c3a88255c25078234819024dace993f7cfc564485f48680a8768b81238
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/mcp_client/version.rb
CHANGED
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
|
-
|
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.
|
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-
|
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
|