ruby-mcp-client 0.9.0 → 0.9.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.
data/README.md CHANGED
@@ -1,1397 +1,388 @@
1
1
  # ruby-mcp-client
2
2
 
3
- This gem provides a Ruby client for the Model Context Protocol (MCP),
4
- enabling integration with external tools and services via a standardized protocol.
3
+ A Ruby client for the Model Context Protocol (MCP), enabling integration with external tools and services via a standardized protocol.
5
4
 
6
5
  ## Installation
7
6
 
8
- Add this line to your application's Gemfile:
9
-
10
7
  ```ruby
8
+ # Gemfile
11
9
  gem 'ruby-mcp-client'
12
10
  ```
13
11
 
14
- And then execute:
15
-
16
12
  ```bash
17
13
  bundle install
18
- ```
19
-
20
- Or install it yourself as:
21
-
22
- ```bash
14
+ # or
23
15
  gem install ruby-mcp-client
24
16
  ```
25
17
 
26
18
  ## Overview
27
19
 
28
- MCP enables AI assistants and other services to discover and invoke external tools
29
- via different transport mechanisms:
30
-
31
- - **Standard I/O**: Local processes implementing the MCP protocol
32
- - **Server-Sent Events (SSE)**: Remote MCP servers over HTTP with streaming support
33
- - **HTTP**: Remote MCP servers over HTTP request/response (non-streaming Streamable HTTP)
34
- - **Streamable HTTP**: Remote MCP servers that use HTTP POST with Server-Sent Event formatted responses
20
+ MCP enables AI assistants to discover and invoke external tools via different transport mechanisms:
35
21
 
36
- The core client resides in `MCPClient::Client` and provides helper methods for integrating
37
- with popular AI services with built-in conversions:
22
+ - **stdio** - Local processes implementing the MCP protocol
23
+ - **SSE** - Server-Sent Events with streaming support
24
+ - **HTTP** - Simple request/response (non-streaming)
25
+ - **Streamable HTTP** - HTTP POST with SSE-formatted responses
38
26
 
39
- - `to_openai_tools()` - Formats tools for OpenAI API
40
- - `to_anthropic_tools()` - Formats tools for Anthropic Claude API
41
- - `to_google_tools()` - Formats tools for Google Vertex AI API (automatically removes "$schema" keys not accepted by Vertex AI)
27
+ Built-in API conversions: `to_openai_tools()`, `to_anthropic_tools()`, `to_google_tools()`
42
28
 
43
29
  ## MCP Protocol Support
44
30
 
45
- This Ruby MCP Client implements the **MCP 2025-06-18** specification with full backward compatibility.
31
+ Implements **MCP 2025-06-18** specification:
46
32
 
47
- ### Key Features
33
+ - **Tools**: list, call, streaming, annotations, structured outputs
34
+ - **Prompts**: list, get with parameters
35
+ - **Resources**: list, read, templates, subscriptions, pagination
36
+ - **Elicitation**: Server-initiated user interactions (stdio, SSE, Streamable HTTP)
37
+ - **Roots**: Filesystem scope boundaries with change notifications
38
+ - **Sampling**: Server-requested LLM completions
39
+ - **Completion**: Autocomplete for prompts/resources
40
+ - **Logging**: Server log messages with level filtering
41
+ - **OAuth 2.1**: PKCE, server discovery, dynamic registration
48
42
 
49
- **MCP 2025-06-18 (Latest):**
50
- - **Structured Tool Outputs** - Tools can declare output schemas and return type-safe, validated structured data
51
- - **Elicitation (Server-initiated User Interactions)** - Servers can request user input during tool execution via bidirectional JSON-RPC (stdio, SSE, and Streamable HTTP transports)
52
- - **Tool Annotations** - Support for tool behavior annotations (readOnly, destructive, requiresConfirmation) for safer tool execution
53
- - **OAuth 2.1 Authorization Framework** - Complete authentication with PKCE, dynamic client registration, server discovery, and runtime configuration
54
- - **Streamable HTTP Transport** - Enhanced transport with Server-Sent Event formatted responses and session management
55
- - **HTTP Redirect Support** - Automatic redirect handling for both SSE and HTTP transports with configurable limits
56
- - **FastMCP Compatibility** - Full compatibility with FastMCP servers including proper line ending handling
57
- - **Prompts Support** - Full implementation of MCP prompts for dynamic content generation
58
- - **Resources Support** - Full MCP resources specification compliance including templates, subscriptions, pagination, and annotations
43
+ ## Quick Connect API (Recommended)
59
44
 
60
- ## Usage
61
-
62
- ### Basic Client Usage
45
+ The simplest way to connect to an MCP server:
63
46
 
64
47
  ```ruby
65
48
  require 'mcp_client'
66
49
 
67
- client = MCPClient.create_client(
68
- mcp_server_configs: [
69
- # Local stdio server
70
- MCPClient.stdio_config(
71
- command: 'npx -y @modelcontextprotocol/server-filesystem /home/user',
72
- name: 'filesystem' # Optional name for this server
73
- ),
74
- # Remote HTTP SSE server (with streaming support)
75
- MCPClient.sse_config(
76
- base_url: 'https://api.example.com/sse',
77
- headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
78
- name: 'sse_api', # Optional name for this server
79
- read_timeout: 30, # Optional timeout in seconds (default: 30)
80
- ping: 10, # Optional ping interval in seconds of inactivity (default: 10)
81
- # Connection closes automatically after inactivity (2.5x ping interval)
82
- retries: 3, # Optional number of retry attempts (default: 0)
83
- retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
84
- # Native support for tool streaming via call_tool_streaming method
85
- logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
86
- ),
87
- # Remote HTTP server (request/response without streaming)
88
- MCPClient.http_config(
89
- base_url: 'https://api.example.com',
90
- endpoint: '/rpc', # Optional JSON-RPC endpoint path (default: '/rpc')
91
- headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
92
- name: 'http_api', # Optional name for this server
93
- read_timeout: 30, # Optional timeout in seconds (default: 30)
94
- retries: 3, # Optional number of retry attempts (default: 3)
95
- retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
96
- logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
97
- ),
98
- # Streamable HTTP server (HTTP POST with SSE responses and session management)
99
- MCPClient.streamable_http_config(
100
- base_url: 'https://api.example.com/mcp',
101
- endpoint: '/rpc', # Optional JSON-RPC endpoint path (default: '/rpc')
102
- headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
103
- name: 'streamable_api', # Optional name for this server
104
- read_timeout: 60, # Optional timeout in seconds (default: 30)
105
- retries: 3, # Optional number of retry attempts (default: 3)
106
- retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
107
- logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
108
- )
109
- ],
110
- # Optional logger for the client and all servers without explicit loggers
111
- logger: Logger.new($stdout, level: Logger::WARN)
50
+ # Auto-detect transport from URL
51
+ client = MCPClient.connect('http://localhost:8000/sse') # SSE
52
+ client = MCPClient.connect('http://localhost:8931/mcp') # Streamable HTTP
53
+ client = MCPClient.connect('npx -y @modelcontextprotocol/server-filesystem /home') # stdio
54
+
55
+ # With options
56
+ client = MCPClient.connect('http://api.example.com/mcp',
57
+ headers: { 'Authorization' => 'Bearer TOKEN' },
58
+ read_timeout: 60,
59
+ retries: 3,
60
+ logger: Logger.new($stdout)
112
61
  )
113
62
 
114
- # Or load server definitions from a JSON file
115
- client = MCPClient.create_client(
116
- server_definition_file: 'path/to/server_definition.json',
117
- logger: Logger.new($stdout, level: Logger::WARN) # Optional logger for client and servers
118
- )
63
+ # Multiple servers
64
+ client = MCPClient.connect(['http://server1/mcp', 'http://server2/sse'])
65
+
66
+ # Force specific transport
67
+ client = MCPClient.connect('http://custom.com/api', transport: :streamable_http)
119
68
 
120
- # MCP server configuration JSON format can be:
121
- # 1. A single server object:
122
- # { "type": "sse", "url": "http://example.com/sse" }
123
- # { "type": "http", "url": "http://example.com", "endpoint": "/rpc" }
124
- # { "type": "streamable_http", "url": "http://example.com/mcp", "endpoint": "/rpc" }
125
- # 2. An array of server objects:
126
- # [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }, { "type": "streamable_http", "url": "http://..." }]
127
- # 3. An object with "mcpServers" key containing named servers:
128
- # { "mcpServers": { "server1": { "type": "sse", "url": "http://..." }, "server2": { "type": "streamable_http", "url": "http://..." } } }
129
- # Note: When using this format, server1/server2 will be accessible by name
130
-
131
- # List available tools
69
+ # Use the client
132
70
  tools = client.list_tools
71
+ result = client.call_tool('example_tool', { param: 'value' })
72
+ client.cleanup
73
+ ```
133
74
 
134
- # Find a server by name
135
- filesystem_server = client.find_server('filesystem')
75
+ **Transport Detection:**
136
76
 
137
- # Find tools by name pattern (string or regex)
138
- file_tools = client.find_tools('file')
139
- first_tool = client.find_tool(/^file_/)
77
+ | URL Pattern | Transport |
78
+ |-------------|-----------|
79
+ | Ends with `/sse` | SSE |
80
+ | Ends with `/mcp` | Streamable HTTP |
81
+ | `stdio://command` or Array | stdio |
82
+ | `npx`, `node`, `python`, etc. | stdio |
83
+ | Other HTTP URLs | Auto-detect (Streamable HTTP → SSE → HTTP) |
140
84
 
141
- # Call a specific tool by name
142
- result = client.call_tool('example_tool', { param1: 'value1', param2: 42 })
85
+ ## Working with Tools, Prompts & Resources
143
86
 
144
- # Call a tool on a specific server by name
145
- result = client.call_tool('example_tool', { param1: 'value1' }, server: 'filesystem')
146
- # You can also call a tool on a server directly
147
- result = filesystem_server.call_tool('example_tool', { param1: 'value1' })
87
+ ```ruby
88
+ # Tools
89
+ tools = client.list_tools
90
+ result = client.call_tool('tool_name', { param: 'value' })
91
+ result = client.call_tool('tool_name', { param: 'value' }, server: 'server_name')
148
92
 
149
- # Call multiple tools in batch
93
+ # Batch tool calls
150
94
  results = client.call_tools([
151
- { name: 'tool1', parameters: { key1: 'value1' } },
152
- { name: 'tool2', parameters: { key2: 'value2' }, server: 'filesystem' } # Specify server for a specific tool
95
+ { name: 'tool1', parameters: { key: 'value' } },
96
+ { name: 'tool2', parameters: { key: 'value' }, server: 'specific_server' }
153
97
  ])
154
98
 
155
- # Stream results (supported by the SSE transport)
156
- # Returns an Enumerator that yields results as they become available
157
- client.call_tool_streaming('streaming_tool', { param: 'value' }, server: 'api').each do |chunk|
158
- # Process each chunk as it arrives
99
+ # Streaming (SSE/Streamable HTTP)
100
+ client.call_tool_streaming('tool', { param: 'value' }).each do |chunk|
159
101
  puts chunk
160
102
  end
161
103
 
162
- # === Working with Prompts ===
163
- # List available prompts from all servers
104
+ # Prompts
164
105
  prompts = client.list_prompts
106
+ result = client.get_prompt('greeting', { name: 'Alice' })
165
107
 
166
- # Get a specific prompt with parameters
167
- result = client.get_prompt('greeting', { name: 'Ruby Developer' })
168
-
169
- # Get a prompt from a specific server
170
- result = client.get_prompt('greeting', { name: 'Ruby Developer' }, server: 'filesystem')
171
-
172
- # === Working with Resources ===
173
- # List available resources from all servers (returns hash with 'resources' array and optional 'nextCursor')
108
+ # Resources
174
109
  result = client.list_resources
175
- resources = result['resources'] # Array of Resource objects from all servers
176
- next_cursor = result['nextCursor'] # nil when aggregating multiple servers
177
-
178
- # Get resources from a specific server with pagination support
179
- result = client.servers.first.list_resources
180
- resources = result['resources'] # Array of Resource objects
181
- next_cursor = result['nextCursor'] # For pagination
182
-
183
- # List resources with pagination (only works with single server or client.list_resources with cursor)
184
- result = client.list_resources(cursor: next_cursor) # Uses first server when cursor provided
185
-
186
- # Read a specific resource by URI (returns array of ResourceContent objects)
187
110
  contents = client.read_resource('file:///example.txt')
188
111
  contents.each do |content|
189
- if content.text?
190
- puts content.text # Text content
191
- elsif content.binary?
192
- data = Base64.decode64(content.blob) # Binary content
193
- end
194
- puts content.mime_type if content.mime_type
195
- puts content.annotations if content.annotations # Optional metadata
196
- end
197
-
198
- # Read a resource from a specific server
199
- contents = client.read_resource('file:///example.txt', server: 'filesystem')
200
-
201
- # Format tools for specific AI services
202
- openai_tools = client.to_openai_tools
203
- anthropic_tools = client.to_anthropic_tools
204
- google_tools = client.to_google_tools
205
-
206
- # Register for server notifications
207
- client.on_notification do |server, method, params|
208
- puts "Server notification: #{server.class}[#{server.name}] - #{method} - #{params}"
209
- # Handle specific notifications based on method name
210
- # 'notifications/tools/list_changed' is handled automatically by the client
112
+ puts content.text if content.text?
113
+ data = Base64.decode64(content.blob) if content.binary?
211
114
  end
212
-
213
- # Send custom JSON-RPC requests or notifications
214
- client.send_rpc('custom_method', params: { key: 'value' }, server: :sse) # Uses specific server by type
215
- client.send_rpc('custom_method', params: { key: 'value' }, server: 'filesystem') # Uses specific server by name
216
- result = client.send_rpc('another_method', params: { data: 123 }) # Uses first available server
217
- client.send_notification('status_update', params: { status: 'ready' })
218
-
219
- # Check server connectivity
220
- client.ping # Basic connectivity check (zero-parameter heartbeat call)
221
- client.ping(server_index: 1) # Ping a specific server by index
222
-
223
- # Clear cached tools to force fresh fetch on next list
224
- client.clear_cache
225
- # Clean up connections
226
- client.cleanup
227
115
  ```
228
116
 
229
- ### HTTP Transport Example
117
+ ## MCP 2025-06-18 Features
230
118
 
231
- The HTTP transport provides simple request/response communication with MCP servers:
119
+ ### Tool Annotations
232
120
 
233
121
  ```ruby
234
- require 'mcp_client'
235
- require 'logger'
236
-
237
- # Optional logger for debugging
238
- logger = Logger.new($stdout)
239
- logger.level = Logger::INFO
240
-
241
- # Create an MCP client that connects to an HTTP MCP server
242
- http_client = MCPClient.create_client(
243
- mcp_server_configs: [
244
- MCPClient.http_config(
245
- base_url: 'https://api.example.com',
246
- endpoint: '/mcp', # JSON-RPC endpoint path
247
- headers: {
248
- 'Authorization' => 'Bearer YOUR_API_TOKEN',
249
- 'X-Custom-Header' => 'custom-value'
250
- },
251
- read_timeout: 30, # Timeout in seconds for HTTP requests
252
- retries: 3, # Number of retry attempts on transient errors
253
- retry_backoff: 1, # Base delay in seconds for exponential backoff
254
- logger: logger # Optional logger for debugging HTTP requests
255
- )
256
- ]
257
- )
258
-
259
- # List available tools
260
- tools = http_client.list_tools
261
-
262
- # Call a tool
263
- result = http_client.call_tool('analyze_data', {
264
- dataset: 'sales_2024',
265
- metrics: ['revenue', 'conversion_rate']
266
- })
267
-
268
- # HTTP transport also supports streaming (though implemented as single response)
269
- # This provides API compatibility with SSE transport
270
- http_client.call_tool_streaming('process_batch', { batch_id: 123 }).each do |result|
271
- puts "Processing result: #{result}"
272
- end
273
-
274
- # Send custom JSON-RPC requests
275
- custom_result = http_client.send_rpc('custom_method', params: { key: 'value' })
276
-
277
- # Send notifications (fire-and-forget)
278
- http_client.send_notification('status_update', params: { status: 'processing' })
279
-
280
- # Test connectivity
281
- ping_result = http_client.ping
282
- puts "Server is responsive: #{ping_result.inspect}"
283
-
284
- # Clean up
285
- http_client.cleanup
122
+ tool = client.find_tool('delete_user')
123
+ tool.read_only? # Safe to execute?
124
+ tool.destructive? # Warning: destructive operation
125
+ tool.requires_confirmation? # Needs user confirmation
286
126
  ```
287
127
 
288
- ### Streamable HTTP Transport Example
289
-
290
- The Streamable HTTP transport is designed for servers that use HTTP POST requests but return Server-Sent Event formatted responses. This is commonly used by services like Zapier's MCP implementation:
128
+ ### Structured Outputs
291
129
 
292
130
  ```ruby
293
- require 'mcp_client'
294
- require 'logger'
295
-
296
- # Optional logger for debugging
297
- logger = Logger.new($stdout)
298
- logger.level = Logger::INFO
299
-
300
- # Create an MCP client that connects to a Streamable HTTP MCP server
301
- streamable_client = MCPClient.create_client(
302
- mcp_server_configs: [
303
- MCPClient.streamable_http_config(
304
- base_url: 'https://mcp.zapier.com/api/mcp/s/YOUR_SESSION_ID/mcp',
305
- headers: {
306
- 'Authorization' => 'Bearer YOUR_ZAPIER_TOKEN'
307
- },
308
- read_timeout: 60, # Timeout in seconds for HTTP requests
309
- retries: 3, # Number of retry attempts on transient errors
310
- retry_backoff: 2, # Base delay in seconds for exponential backoff
311
- logger: logger # Optional logger for debugging requests
312
- )
313
- ]
314
- )
315
-
316
- # List available tools (server responds with SSE-formatted JSON)
317
- tools = streamable_client.list_tools
318
- puts "Found #{tools.size} tools:"
319
- tools.each { |tool| puts "- #{tool.name}: #{tool.description}" }
320
-
321
- # Call a tool (response will be in SSE format)
322
- result = streamable_client.call_tool('google_calendar_find_event', {
323
- instructions: 'Find today\'s meetings',
324
- calendarid: 'primary'
325
- })
326
-
327
- # The client automatically parses SSE responses like:
328
- # event: message
329
- # data: {"jsonrpc":"2.0","id":1,"result":{"content":[...]}}
330
-
331
- puts "Tool result: #{result.inspect}"
131
+ tool = client.find_tool('get_weather')
132
+ tool.structured_output? # Has output schema?
133
+ tool.output_schema # JSON Schema for output
332
134
 
333
- # Clean up
334
- streamable_client.cleanup
135
+ result = client.call_tool('get_weather', { location: 'SF' })
136
+ data = result['structuredContent'] # Type-safe structured data
335
137
  ```
336
138
 
337
- ### Server-Sent Events (SSE) Example
338
-
339
- The SSE transport provides robust connection handling for remote MCP servers:
139
+ ### Roots
340
140
 
341
141
  ```ruby
342
- require 'mcp_client'
343
- require 'logger'
142
+ # Set filesystem scope boundaries
143
+ client.roots = [
144
+ { uri: 'file:///home/user/project', name: 'Project' },
145
+ { uri: 'file:///var/log', name: 'Logs' }
146
+ ]
147
+
148
+ # Access current roots
149
+ client.roots
150
+ ```
344
151
 
345
- # Optional logger for debugging
346
- logger = Logger.new($stdout)
347
- logger.level = Logger::INFO
152
+ ### Sampling (Server-requested LLM completions)
348
153
 
349
- # Create an MCP client that connects to a Playwright MCP server via Streamable HTTP
350
- # First run: npx @playwright/mcp@latest --port 8931
351
- mcp_client = MCPClient.create_client(
352
- mcp_server_configs: [
353
- MCPClient.streamable_http_config(
354
- base_url: 'http://localhost:8931/mcp',
355
- read_timeout: 30, # Timeout in seconds for request fulfillment
356
- ping: 10, # Send ping after 10 seconds of inactivity
357
- # Connection closes automatically after inactivity (2.5x ping interval)
358
- retries: 2, # Number of retry attempts on transient errors
359
- logger: logger # Optional logger for debugging connection issues
360
- )
361
- ]
154
+ ```ruby
155
+ # Configure handler when creating client
156
+ client = MCPClient.connect('http://server/mcp',
157
+ sampling_handler: ->(messages, model_prefs, system_prompt, max_tokens) {
158
+ # Process server's LLM request
159
+ {
160
+ 'model' => 'gpt-4',
161
+ 'stopReason' => 'endTurn',
162
+ 'role' => 'assistant',
163
+ 'content' => { 'type' => 'text', 'text' => 'Response here' }
164
+ }
165
+ }
362
166
  )
363
-
364
- # List available tools
365
- tools = mcp_client.list_tools
366
-
367
- # Launch a browser
368
- result = mcp_client.call_tool('browser_install', {})
369
- result = mcp_client.call_tool('browser_navigate', { url: 'about:blank' })
370
- # No browser ID needed with these tool names
371
-
372
- # Create a new page
373
- page_result = mcp_client.call_tool('browser_tab', {action: 'create'})
374
- # No page ID needed with these tool names
375
-
376
- # Navigate to a website
377
- mcp_client.call_tool('browser_navigate', { url: 'https://example.com' })
378
-
379
- # Get page title
380
- title_result = mcp_client.call_tool('browser_snapshot', {})
381
- puts "Page snapshot: #{title_result}"
382
-
383
- # Take a screenshot
384
- screenshot_result = mcp_client.call_tool('browser_take_screenshot', {})
385
-
386
- # Ping the server to verify connectivity
387
- ping_result = mcp_client.ping
388
- puts "Ping successful: #{ping_result.inspect}"
389
-
390
- # Clean up
391
- mcp_client.cleanup
392
167
  ```
393
168
 
394
- See `examples/streamable_http_example.rb` for the full Playwright SSE example.
395
-
396
- ### FastMCP Example
397
-
398
- The repository includes complete FastMCP server examples that demonstrate the Ruby MCP client working with Python FastMCP servers, including full MCP protocol support with tools, prompts, and resources:
399
-
400
- #### Basic FastMCP Example
401
-
402
- **For FastMCP server with SSE transport (includes tools, prompts, and resources):**
403
- ```bash
404
- # From the ruby-mcp-client directory
405
- python examples/echo_server.py
406
- ```
169
+ ### Completion (Autocomplete)
407
170
 
408
171
  ```ruby
409
-
410
- # Run the Ruby client
411
- # bundle exec ruby examples/echo_server_client.rb
412
-
413
- require 'mcp_client'
414
-
415
- # Connect to FastMCP server
416
- client = MCPClient.create_client(
417
- mcp_server_configs: [
418
- MCPClient.sse_config(
419
- base_url: 'http://127.0.0.1:8000/sse',
420
- read_timeout: 30
421
- )
422
- ]
172
+ result = client.complete(
173
+ ref: { type: 'ref/prompt', name: 'greeting' },
174
+ argument: { name: 'name', value: 'A' }
423
175
  )
176
+ # => { 'values' => ['Alice', 'Alex'], 'total' => 100, 'hasMore' => true }
177
+ ```
424
178
 
425
- # List and use tools
426
- tools = client.list_tools
427
- puts "Found #{tools.length} tools:"
428
- tools.each { |tool| puts "- #{tool.name}: #{tool.description}" }
429
-
430
- result = client.call_tool('echo', { message: 'Hello FastMCP!' })
431
-
432
- # List and use prompts
433
- prompts = client.list_prompts
434
- puts "Found #{prompts.length} prompts:"
435
- prompts.each { |prompt| puts "- #{prompt.name}: #{prompt.description}" }
436
-
437
- greeting = client.get_prompt('greeting', { name: 'Ruby Developer' })
179
+ ### Logging
438
180
 
439
- # List and read resources
440
- result = client.list_resources
441
- resources = result['resources']
442
- puts "Found #{resources.length} resources:"
443
- resources.each { |resource| puts "- #{resource.name} (#{resource.uri})" }
181
+ ```ruby
182
+ # Set log level
183
+ client.log_level = 'debug' # debug/info/notice/warning/error/critical
444
184
 
445
- # Read resource (returns array of ResourceContent objects)
446
- contents = client.read_resource('file:///sample/README.md')
447
- contents.each do |content|
448
- puts content.text if content.text?
185
+ # Handle log notifications
186
+ client.on_notification do |server, method, params|
187
+ if method == 'notifications/message'
188
+ puts "[#{params['level']}] #{params['logger']}: #{params['data']}"
189
+ end
449
190
  end
450
191
  ```
451
192
 
452
- #### Streamable HTTP Example
453
-
454
- **For FastMCP server with Streamable HTTP transport (includes tools, prompts, and resources):**
455
- ```bash
456
- # From the ruby-mcp-client directory
457
- python examples/echo_server_streamable.py
458
- ```
193
+ ### Elicitation (Server-initiated user interactions)
459
194
 
460
195
  ```ruby
461
-
462
- # Run the streamable HTTP client
463
- # bundle exec ruby examples/echo_server_streamable_client.rb
464
-
465
- require 'mcp_client'
466
-
467
- # Connect to streamable HTTP server with full MCP protocol support
468
- client = MCPClient.create_client(
469
- mcp_server_configs: [
470
- MCPClient.streamable_http_config(
471
- base_url: 'http://localhost:8931/mcp',
472
- read_timeout: 60
473
- )
474
- ]
196
+ client = MCPClient::Client.new(
197
+ mcp_server_configs: [MCPClient.stdio_config(command: 'python server.py')],
198
+ elicitation_handler: ->(message, schema) {
199
+ puts "Server asks: #{message}"
200
+ # Return: { 'action' => 'accept', 'content' => { 'field' => 'value' } }
201
+ # Or: { 'action' => 'decline' } or { 'action' => 'cancel' }
202
+ }
475
203
  )
476
-
477
- # Full protocol support including real-time notifications
478
- client.on_notification do |method, params|
479
- puts "Server notification: #{method} - #{params}"
480
- end
481
-
482
- # Use all MCP features: tools, prompts, resources
483
- tools = client.list_tools
484
- prompts = client.list_prompts
485
- resources = client.list_resources
486
-
487
- # Real-time progress notifications for long-running tasks
488
- result = client.call_tool('long_task', { duration: 5, steps: 5 })
489
-
490
- # Use the tools
491
- result = client.call_tool('echo', { message: 'Hello from Ruby!' })
492
- result = client.call_tool('reverse', { text: 'FastMCP rocks!' })
493
-
494
- client.cleanup
495
204
  ```
496
205
 
497
- The FastMCP example includes:
498
- - **`echo_server.py`** - A Python FastMCP server with tools, prompts, and resources
499
- - **`echo_server_client.rb`** - Ruby client demonstrating all features including prompts and resources
500
- - **`echo_server_streamable.py`** - Enhanced streamable HTTP server with tools, prompts, and resources
501
- - **`echo_server_streamable_client.rb`** - Ruby client demonstrating streamable HTTP transport
502
- - **`README_ECHO_SERVER.md`** - Complete setup and usage instructions
206
+ ## Advanced Configuration
503
207
 
504
- This example showcases redirect support, proper line ending handling, and seamless integration between Ruby and Python MCP implementations.
505
-
506
- ### Integration Examples
507
-
508
- The repository includes examples for integrating with popular AI APIs:
509
-
510
- #### OpenAI Integration
511
-
512
- Ruby-MCP-Client works with both official and community OpenAI gems:
208
+ For more control, use `create_client` with explicit configs:
513
209
 
514
210
  ```ruby
515
- # Using the openai/openai-ruby gem (official)
516
- require 'mcp_client'
517
- require 'openai'
518
-
519
- # Create MCP client
520
- mcp_client = MCPClient.create_client(
211
+ client = MCPClient.create_client(
521
212
  mcp_server_configs: [
522
- MCPClient.stdio_config(
523
- command: %W[npx -y @modelcontextprotocol/server-filesystem #{Dir.pwd}]
213
+ MCPClient.stdio_config(command: 'npx server', name: 'local'),
214
+ MCPClient.sse_config(
215
+ base_url: 'https://api.example.com/sse',
216
+ headers: { 'Authorization' => 'Bearer TOKEN' },
217
+ read_timeout: 30, ping: 10, retries: 3
218
+ ),
219
+ MCPClient.http_config(
220
+ base_url: 'https://api.example.com',
221
+ endpoint: '/rpc',
222
+ headers: { 'Authorization' => 'Bearer TOKEN' }
223
+ ),
224
+ MCPClient.streamable_http_config(
225
+ base_url: 'https://api.example.com/mcp',
226
+ read_timeout: 60, retries: 3
524
227
  )
525
- ]
526
- )
527
-
528
- # Convert tools to OpenAI format
529
- tools = mcp_client.to_openai_tools
530
-
531
- # Use with OpenAI client
532
- client = OpenAI::Client.new(api_key: ENV['OPENAI_API_KEY'])
533
- response = client.chat.completions.create(
534
- model: 'gpt-4',
535
- messages: [
536
- { role: 'user', content: 'List files in current directory' }
537
228
  ],
538
- tools: tools
539
- )
540
-
541
- # Process tool calls and results
542
- # See examples directory for complete implementation
543
- ```
544
-
545
- ```ruby
546
- # Using the alexrudall/ruby-openai gem (community)
547
- require 'mcp_client'
548
- require 'openai'
549
-
550
- # Create MCP client
551
- mcp_client = MCPClient.create_client(
552
- mcp_server_configs: [
553
- MCPClient.stdio_config(command: 'npx @playwright/mcp@latest')
554
- ]
229
+ logger: Logger.new($stdout)
555
230
  )
556
231
 
557
- # Convert tools to OpenAI format
558
- tools = mcp_client.to_openai_tools
559
-
560
- # Use with Ruby-OpenAI client
561
- client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
562
- # See examples directory for complete implementation
232
+ # Or load from JSON file
233
+ client = MCPClient.create_client(server_definition_file: 'servers.json')
563
234
  ```
564
235
 
565
- #### Anthropic Integration
236
+ ### Faraday Customization
566
237
 
567
238
  ```ruby
568
- require 'mcp_client'
569
- require 'anthropic'
570
-
571
- # Create MCP client
572
- mcp_client = MCPClient.create_client(
573
- mcp_server_configs: [
574
- MCPClient.stdio_config(
575
- command: %W[npx -y @modelcontextprotocol/server-filesystem #{Dir.pwd}]
576
- )
577
- ]
578
- )
579
-
580
- # Convert tools to Anthropic format
581
- claude_tools = mcp_client.to_anthropic_tools
582
-
583
- # Use with Anthropic client
584
- client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
585
- # See examples directory for complete implementation
239
+ MCPClient.http_config(base_url: 'https://internal.company.com') do |faraday|
240
+ faraday.ssl.cert_store = custom_cert_store
241
+ faraday.ssl.verify = true
242
+ end
586
243
  ```
587
244
 
588
- Complete examples can be found in the `examples/` directory:
589
-
590
- **AI Integration Examples:**
591
- - `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
592
- - `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
593
- - `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
594
- - `gemini_ai_mcp.rb` - Integration with Google Vertex AI and Gemini models
595
-
596
- **Transport Examples:**
597
- - `streamable_http_example.rb` - Streamable HTTP transport with Playwright MCP
598
- - `echo_server.py` & `echo_server_client.rb` - FastMCP server example with full setup
599
- - `echo_server_streamable.py` & `echo_server_streamable_client.rb` - Enhanced streamable HTTP server example
600
-
601
- **MCP 2025 Protocol Features:**
602
- - `structured_output_server.py` & `test_structured_outputs.rb` - Structured tool outputs (MCP 2025-06-18)
603
- - `echo_server_with_annotations.py` & `test_tool_annotations.rb` - Tool annotations (MCP 2025-03-26)
604
-
605
- ## MCP Server Compatibility
606
-
607
- This client works with any MCP-compatible server, including:
608
-
609
- - [@modelcontextprotocol/server-filesystem](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem) - File system access
610
- - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
611
- - [FastMCP](https://github.com/jlowin/fastmcp) - Python framework for building MCP servers
612
- - Custom servers implementing the MCP protocol
613
-
614
- ### Server Definition Files
615
-
616
- You can define MCP server configurations in JSON files for easier management:
245
+ ### Server Definition JSON
617
246
 
618
247
  ```json
619
248
  {
620
249
  "mcpServers": {
621
- "playwright": {
622
- "type": "sse",
623
- "url": "http://localhost:8931/mcp",
624
- "headers": {
625
- "Authorization": "Bearer TOKEN"
626
- }
627
- },
628
- "api_server": {
629
- "type": "http",
630
- "url": "https://api.example.com",
631
- "endpoint": "/mcp",
632
- "headers": {
633
- "Authorization": "Bearer API_TOKEN",
634
- "X-Custom-Header": "value"
635
- }
636
- },
637
250
  "filesystem": {
638
251
  "type": "stdio",
639
252
  "command": "npx",
640
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
641
- "env": {
642
- "DEBUG": "true"
643
- }
253
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home"]
254
+ },
255
+ "api": {
256
+ "type": "streamable_http",
257
+ "url": "https://api.example.com/mcp",
258
+ "headers": { "Authorization": "Bearer TOKEN" }
644
259
  }
645
260
  }
646
261
  }
647
262
  ```
648
263
 
649
- A simpler example used in the Playwright demo (found in `examples/sample_server_definition.json`):
264
+ ## AI Integration Examples
650
265
 
651
- ```json
652
- {
653
- "mcpServers": {
654
- "playwright": {
655
- "url": "http://localhost:8931/mcp",
656
- "headers": {},
657
- "comment": "Local Playwright MCP Server running on port 8931"
658
- }
659
- }
660
- }
661
- ```
662
-
663
- Load this configuration with:
266
+ ### OpenAI
664
267
 
665
268
  ```ruby
666
- client = MCPClient.create_client(server_definition_file: 'path/to/definition.json')
667
- ```
668
-
669
- The JSON format supports:
670
- 1. A single server object: `{ "type": "sse", "url": "..." }` or `{ "type": "http", "url": "..." }`
671
- 2. An array of server objects: `[{ "type": "stdio", ... }, { "type": "sse", ... }, { "type": "http", ... }]`
672
- 3. An object with named servers under `mcpServers` key (as shown above)
673
-
674
- Special configuration options:
675
- - `comment` and `description` are reserved keys that are ignored during parsing and can be used for documentation
676
- - Server type can be inferred from the presence of either `command` (for stdio) or `url` (for SSE/HTTP)
677
- - For HTTP servers, `endpoint` specifies the JSON-RPC endpoint path (defaults to '/rpc' if not specified)
678
- - All string values in arrays (like `args`) are automatically converted to strings
679
-
680
- ## Session-Based MCP Protocol Support
681
-
682
- Both HTTP and Streamable HTTP transports now support session-based MCP servers that require session continuity:
683
-
684
- ### Session Management Features
685
-
686
- - **Automatic Session Management**: Captures session IDs from `initialize` response headers
687
- - **Session Header Injection**: Automatically includes `Mcp-Session-Id` header in subsequent requests
688
- - **Session Termination**: Sends HTTP DELETE requests to properly terminate sessions during cleanup
689
- - **Session Validation**: Validates session ID format for security (8-128 alphanumeric characters with hyphens/underscores)
690
- - **Backward Compatibility**: Works with both session-based and stateless MCP servers
691
- - **Session Cleanup**: Properly cleans up session state during connection teardown
692
-
693
- ### Resumability and Redelivery (Streamable HTTP)
694
-
695
- The Streamable HTTP transport provides additional resumability features for reliable message delivery:
696
-
697
- - **Event ID Tracking**: Automatically tracks event IDs from SSE responses
698
- - **Last-Event-ID Header**: Includes `Last-Event-ID` header in requests for resuming from disconnection points
699
- - **Message Replay**: Enables servers to replay missed messages from the last received event
700
- - **Connection Recovery**: Maintains message continuity even with unstable network connections
701
-
702
- ### Security Features
703
-
704
- Both transports implement security best practices:
705
-
706
- - **URL Validation**: Validates server URLs to ensure only HTTP/HTTPS protocols are used
707
- - **Session ID Validation**: Enforces secure session ID formats to prevent malicious injection
708
- - **Security Warnings**: Logs warnings for potentially insecure configurations (e.g., 0.0.0.0 binding)
709
- - **Header Sanitization**: Properly handles and validates all session-related headers
710
-
711
- ### Usage
712
-
713
- The session support is transparent to the user - no additional configuration is required. The client will automatically detect and handle session-based servers by:
714
-
715
- 1. **Session Initialization**: Capturing the `Mcp-Session-Id` header from the `initialize` response
716
- 2. **Session Persistence**: Including this header in all subsequent requests (except `initialize`)
717
- 3. **Session Termination**: Sending HTTP DELETE request with session ID during cleanup
718
- 4. **Resumability** (Streamable HTTP): Tracking event IDs and including `Last-Event-ID` for message replay
719
- 5. **Security Validation**: Validating session IDs and server URLs for security
720
- 6. **Logging**: Comprehensive logging of session activity for debugging purposes
269
+ require 'mcp_client'
270
+ require 'openai'
721
271
 
722
- Example of automatic session termination:
272
+ mcp = MCPClient.connect('npx -y @modelcontextprotocol/server-filesystem .')
273
+ tools = mcp.to_openai_tools
723
274
 
724
- ```ruby
725
- # Session is automatically terminated when client is cleaned up
726
- client = MCPClient.create_client(
727
- mcp_server_configs: [
728
- MCPClient.http_config(base_url: 'https://api.example.com/mcp')
729
- ]
275
+ client = OpenAI::Client.new(api_key: ENV['OPENAI_API_KEY'])
276
+ response = client.chat.completions.create(
277
+ model: 'gpt-4',
278
+ messages: [{ role: 'user', content: 'List files' }],
279
+ tools: tools
730
280
  )
731
-
732
- # Use the client...
733
- tools = client.list_tools
734
-
735
- # Session automatically terminated with HTTP DELETE request
736
- client.cleanup
737
281
  ```
738
282
 
739
- This enables compatibility with MCP servers that maintain state between requests and require session identification.
740
-
741
- ## OAuth 2.1 Authentication
742
-
743
- The Ruby MCP Client includes comprehensive OAuth 2.1 support for secure authentication with MCP servers:
283
+ ### Anthropic
744
284
 
745
285
  ```ruby
746
286
  require 'mcp_client'
287
+ require 'anthropic'
747
288
 
748
- # Create an OAuth-enabled HTTP server
749
- server = MCPClient::OAuthClient.create_http_server(
750
- server_url: 'https://api.example.com/mcp',
751
- redirect_uri: 'http://localhost:8080/callback',
752
- scope: 'mcp:read mcp:write'
753
- )
754
-
755
- # Check if authorization is needed
756
- unless MCPClient::OAuthClient.valid_token?(server)
757
- # Start OAuth flow
758
- auth_url = MCPClient::OAuthClient.start_oauth_flow(server)
759
- puts "Please visit: #{auth_url}"
760
-
761
- # After user authorization, complete the flow
762
- # token = MCPClient::OAuthClient.complete_oauth_flow(server, code, state)
763
- end
764
-
765
- # Use the server normally
766
- server.connect
767
- tools = server.list_tools
768
- ```
769
-
770
- ### Manual OAuth Provider
771
-
772
- For more control over the OAuth flow:
773
-
774
- ```ruby
775
- # Create OAuth provider directly
776
- oauth_provider = MCPClient::Auth::OAuthProvider.new(
777
- server_url: 'https://api.example.com/mcp',
778
- redirect_uri: 'http://localhost:8080/callback',
779
- scope: 'mcp:read mcp:write'
780
- )
781
-
782
- # Update configuration at runtime
783
- oauth_provider.scope = 'mcp:read mcp:write admin'
784
- oauth_provider.redirect_uri = 'http://localhost:9000/callback'
785
-
786
- # Start authorization flow
787
- auth_url = oauth_provider.start_authorization_flow
289
+ mcp = MCPClient.connect('npx -y @modelcontextprotocol/server-filesystem .')
290
+ tools = mcp.to_anthropic_tools
788
291
 
789
- # Complete flow after user authorization
790
- token = oauth_provider.complete_authorization_flow(code, state)
292
+ client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
293
+ # Use tools with Claude API
791
294
  ```
792
295
 
793
- ### Browser-Based OAuth Authentication
296
+ See `examples/` for complete implementations:
297
+ - `ruby_openai_mcp.rb`, `openai_ruby_mcp.rb` - OpenAI integration
298
+ - `ruby_anthropic_mcp.rb` - Anthropic integration
299
+ - `gemini_ai_mcp.rb` - Google Vertex AI integration
794
300
 
795
- For the easiest authentication experience, use the browser-based OAuth flow that automatically handles the entire process:
301
+ ## OAuth 2.1 Authentication
796
302
 
797
303
  ```ruby
798
304
  require 'mcp_client/auth/browser_oauth'
799
305
 
800
- # Create OAuth provider
801
- oauth_provider = MCPClient::Auth::OAuthProvider.new(
306
+ oauth = MCPClient::Auth::OAuthProvider.new(
802
307
  server_url: 'https://api.example.com/mcp',
803
308
  redirect_uri: 'http://localhost:8080/callback',
804
- scope: 'read:tools write:tools' # Optional
309
+ scope: 'mcp:read mcp:write'
805
310
  )
806
311
 
807
- # Create browser OAuth helper
808
- browser_oauth = MCPClient::Auth::BrowserOAuth.new(
809
- oauth_provider,
810
- callback_port: 8080, # Optional: Port for local server (default: 8080)
811
- callback_path: '/callback' # Optional: Callback path (default: '/callback')
812
- )
312
+ browser_oauth = MCPClient::Auth::BrowserOAuth.new(oauth)
313
+ token = browser_oauth.authenticate # Opens browser, handles callback
813
314
 
814
- # Authenticate (opens browser automatically and handles callback)
815
- token = browser_oauth.authenticate(
816
- timeout: 300, # Optional: 5 minutes (default: 300)
817
- auto_open_browser: true # Optional: Auto-open browser (default: true)
315
+ client = MCPClient::Client.new(
316
+ mcp_server_configs: [{
317
+ type: 'streamable_http',
318
+ base_url: 'https://api.example.com/mcp',
319
+ oauth_provider: oauth
320
+ }]
818
321
  )
819
-
820
- # Create authenticated client (use streamable_http for modern MCP servers)
821
- server_config = {
822
- type: 'streamable_http', # or 'http' for simple HTTP-only servers
823
- base_url: 'https://api.example.com/mcp',
824
- oauth_provider: oauth_provider
825
- }
826
-
827
- client = MCPClient::Client.new(mcp_server_configs: [server_config])
828
- ```
829
-
830
- **How it works:**
831
- 1. **Automatically starts a local HTTP server** (using pure Ruby `TCPServer`) to handle the OAuth callback
832
- 2. **Opens your default browser** to the authorization page (macOS: `open`, Linux: `xdg-open`, Windows: `start`)
833
- 3. **Captures the authorization code** automatically from the callback
834
- 4. **Completes the OAuth flow** and stores the access token
835
- 5. **Handles token refresh** automatically when tokens expire
836
-
837
- **Features:**
838
- - **Zero external dependencies** - Uses pure Ruby stdlib (`TCPServer`)
839
- - **User-friendly HTML pages** - Shows success/error pages in the browser
840
- - **Automatic token management** - Handles token refresh transparently
841
- - **Configurable timeout** - Raises `Timeout::Error` if user doesn't authorize in time
842
- - **Token storage** - Supports custom storage backends for persistent tokens
843
- - **Port conflict detection** - Clear error messages if port is already in use
844
- - **Security hardened** - PKCE, CSRF protection, request validation, header limits
845
-
846
- **Error Handling:**
847
- ```ruby
848
- begin
849
- token = browser_oauth.authenticate
850
- rescue Timeout::Error
851
- puts "User took too long to authorize"
852
- rescue MCPClient::Errors::ConnectionError => e
853
- puts "OAuth flow failed: #{e.message}"
854
- # Port already in use, server doesn't support OAuth, etc.
855
- rescue ArgumentError => e
856
- puts "Invalid state parameter (CSRF protection)"
857
- end
858
322
  ```
859
323
 
860
- **Server Requirements:**
861
- - OAuth 2.1 Protocol with PKCE
862
- - Authorization Server Discovery via:
863
- - `/.well-known/oauth-authorization-server` (primary, for self-contained servers)
864
- - `/.well-known/oauth-protected-resource` (fallback, for delegated auth)
865
- - Dynamic Client Registration (recommended)
866
- - Authorization Code Grant Flow
867
-
868
- For a complete working example, see [`examples/oauth_browser_auth.rb`](examples/oauth_browser_auth.rb).
324
+ Features: PKCE, server discovery (`.well-known`), dynamic registration, token refresh.
869
325
 
870
- ### OAuth Features
326
+ See [OAUTH.md](OAUTH.md) for full documentation.
871
327
 
872
- - **OAuth 2.1 compliance** with PKCE for security
873
- - **Automatic server discovery** via `.well-known` endpoints
874
- - **Dynamic client registration** when supported by servers
875
- - **Token refresh** and automatic token management
876
- - **Pluggable storage** for tokens and client credentials
877
- - **Runtime configuration** via getter/setter methods
878
- - **Browser-based authentication** with automatic callback handling
879
-
880
- For complete OAuth documentation, see [OAUTH.md](OAUTH.md).
881
-
882
- ## Resources
883
-
884
- The Ruby MCP Client provides full support for the MCP resources specification, enabling access to files, data, and other content with URI-based identification.
885
-
886
- ### Resource Features
887
-
888
- - **Resource listing** with cursor-based pagination
889
- - **Resource templates** with URI patterns (RFC 6570)
890
- - **Resource subscriptions** for real-time updates
891
- - **Binary content support** with base64 encoding
892
- - **Resource annotations** for metadata (audience, priority, lastModified)
893
- - **ResourceContent objects** for structured content access
894
-
895
- ### Resource API
328
+ ## Server Notifications
896
329
 
897
330
  ```ruby
898
- # Get a server instance
899
- server = client.servers.first # or client.find_server('name')
900
-
901
- # List resources with pagination
902
- result = server.list_resources
903
- resources = result['resources'] # Array of Resource objects
904
- next_cursor = result['nextCursor'] # String cursor for next page (if any)
905
-
906
- # Get next page of resources
907
- if next_cursor
908
- next_result = server.list_resources(cursor: next_cursor)
909
- end
910
-
911
- # Access Resource properties
912
- resource = resources.first
913
- resource.uri # "file:///example.txt"
914
- resource.name # "example.txt"
915
- resource.title # Optional human-readable title
916
- resource.description # Optional description
917
- resource.mime_type # "text/plain"
918
- resource.size # Optional file size in bytes
919
- resource.annotations # Optional metadata hash
920
-
921
- # Read resource contents (returns array of ResourceContent objects)
922
- contents = server.read_resource(resource.uri)
923
-
924
- contents.each do |content|
925
- # Check content type
926
- if content.text?
927
- # Text content
928
- text = content.text
929
- mime = content.mime_type # e.g., "text/plain"
930
- elsif content.binary?
931
- # Binary content (base64 encoded)
932
- blob = content.blob # Base64 string
933
- data = Base64.decode64(blob) # Decoded binary data
934
- end
935
-
936
- # Access optional annotations
937
- if content.annotations
938
- audience = content.annotations['audience'] # e.g., ["user", "assistant"]
939
- priority = content.annotations['priority'] # e.g., 0.5
940
- modified = content.annotations['lastModified'] # ISO 8601 timestamp
331
+ client.on_notification do |server, method, params|
332
+ case method
333
+ when 'notifications/tools/list_changed'
334
+ client.clear_cache # Auto-handled
335
+ when 'notifications/message'
336
+ puts "Log: #{params['data']}"
337
+ when 'notifications/roots/list_changed'
338
+ puts "Roots changed"
941
339
  end
942
340
  end
943
-
944
- # List resource templates
945
- templates_result = server.list_resource_templates
946
- templates = templates_result['resourceTemplates'] # Array of ResourceTemplate objects
947
-
948
- template = templates.first
949
- template.uri_template # "file:///{path}" (RFC 6570 URI template)
950
- template.name # Template name
951
- template.title # Optional title
952
- template.description # Optional description
953
- template.mime_type # Optional MIME type hint
954
-
955
- # Subscribe to resource updates
956
- server.subscribe_resource('file:///watched.txt') # Returns true on success
957
-
958
- # Unsubscribe from resource updates
959
- server.unsubscribe_resource('file:///watched.txt') # Returns true on success
960
-
961
- # Check server capabilities for resources
962
- capabilities = server.capabilities
963
- if capabilities['resources']
964
- can_subscribe = capabilities['resources']['subscribe'] # true/false
965
- list_changed = capabilities['resources']['listChanged'] # true/false
966
- end
967
- ```
968
-
969
- ### Working with ResourceContent
970
-
971
- The `ResourceContent` class provides a structured way to handle both text and binary content:
972
-
973
- ```ruby
974
- # ResourceContent for text
975
- content = MCPClient::ResourceContent.new(
976
- uri: 'file:///example.txt',
977
- name: 'example.txt',
978
- mime_type: 'text/plain',
979
- text: 'File contents here',
980
- annotations: {
981
- 'audience' => ['user'],
982
- 'priority' => 1.0
983
- }
984
- )
985
-
986
- # ResourceContent for binary data
987
- binary_content = MCPClient::ResourceContent.new(
988
- uri: 'file:///image.png',
989
- name: 'image.png',
990
- mime_type: 'image/png',
991
- blob: Base64.strict_encode64(binary_data)
992
- )
993
-
994
- # Access content
995
- if content.text?
996
- puts content.text
997
- elsif content.binary?
998
- data = Base64.decode64(content.blob)
999
- end
1000
341
  ```
1001
342
 
1002
- ## Tool Annotations
1003
-
1004
- MCP 2025-06-18 supports tool annotations that describe tool behavior, enabling safer and more informed tool execution. The Ruby MCP Client provides full support for reading and interpreting these annotations.
343
+ ## Session Management
1005
344
 
1006
- ### Annotation Types
345
+ Both HTTP and Streamable HTTP transports automatically handle session-based servers:
1007
346
 
1008
- Tools can include the following annotations:
347
+ - **Session capture**: Extracts `Mcp-Session-Id` from initialize response
348
+ - **Session persistence**: Includes session header in subsequent requests
349
+ - **Session termination**: Sends DELETE request during cleanup
350
+ - **Resumability** (Streamable HTTP): Tracks event IDs for message replay
1009
351
 
1010
- - **`readOnly`** - Indicates the tool only reads data and doesn't modify state
1011
- - **`destructive`** - Indicates the tool performs potentially dangerous operations (e.g., deleting data)
1012
- - **`requiresConfirmation`** - Indicates the tool should require explicit user confirmation before execution
352
+ No configuration required - works automatically.
1013
353
 
1014
- ### Using Tool Annotations
354
+ ## Server Compatibility
1015
355
 
1016
- ```ruby
1017
- # List tools and check their annotations
1018
- tools = client.list_tools
356
+ Works with any MCP-compatible server:
1019
357
 
1020
- tools.each do |tool|
1021
- puts "Tool: #{tool.name}"
358
+ - [@modelcontextprotocol/server-filesystem](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem)
359
+ - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp)
360
+ - [FastMCP](https://github.com/jlowin/fastmcp)
361
+ - Custom servers implementing MCP protocol
1022
362
 
1023
- # Check annotations using helper methods
1024
- if tool.read_only?
1025
- puts " → Safe to execute (read-only)"
1026
- end
1027
-
1028
- if tool.destructive?
1029
- puts " ⚠️ Warning: This tool is destructive"
1030
- end
1031
-
1032
- if tool.requires_confirmation?
1033
- puts " 🛡️ Requires user confirmation before execution"
1034
- end
1035
-
1036
- # Access raw annotations hash
1037
- if tool.annotations
1038
- puts " Annotations: #{tool.annotations.inspect}"
1039
- end
1040
- end
363
+ ### FastMCP Example
1041
364
 
1042
- # Make informed decisions based on annotations
1043
- tool = client.find_tool('delete_user')
1044
- if tool.destructive? && tool.requires_confirmation?
1045
- # Prompt user for confirmation before executing
1046
- puts "This will delete user data. Are you sure? (y/n)"
1047
- response = gets.chomp
1048
- if response.downcase == 'y'
1049
- result = client.call_tool('delete_user', { user_id: 123 })
1050
- else
1051
- puts "Operation cancelled"
1052
- end
1053
- else
1054
- # Safe to execute without confirmation
1055
- result = client.call_tool('get_user', { user_id: 123 })
1056
- end
365
+ ```bash
366
+ # Start server
367
+ python examples/echo_server_streamable.py
1057
368
  ```
1058
369
 
1059
- ### Tool Annotation Helper Methods
1060
-
1061
- The `Tool` class provides convenient helper methods:
1062
-
1063
- - `tool.read_only?` - Returns true if the tool is marked as read-only
1064
- - `tool.destructive?` - Returns true if the tool is marked as destructive
1065
- - `tool.requires_confirmation?` - Returns true if the tool requires confirmation
1066
- - `tool.annotations` - Returns the raw annotations hash
1067
-
1068
- ### Example
1069
-
1070
- See `examples/test_tool_annotations.rb` for a complete working example demonstrating:
1071
- - Listing tools with their annotations
1072
- - Using annotation helper methods
1073
- - Making annotation-aware decisions about tool execution
1074
- - Handling destructive operations safely
1075
-
1076
- ## Structured Tool Outputs
1077
-
1078
- MCP 2025-06-18 introduces structured tool outputs, allowing tools to declare output schemas and return type-safe, validated data structures instead of just text.
1079
-
1080
- ### Overview
1081
-
1082
- Tools can now specify an `outputSchema` (JSON Schema) that defines the expected structure of their results. When called, they return data in a `structuredContent` field that validates against this schema, providing:
1083
-
1084
- - **Type safety** - Predictable, validated data structures
1085
- - **Better IDE support** - Code completion and type checking
1086
- - **Easier parsing** - No need to parse text or guess formats
1087
- - **Backward compatibility** - Text content is still provided alongside structured data
1088
-
1089
- ### Using Structured Outputs
1090
-
1091
370
  ```ruby
1092
- # List tools and check for structured output support
371
+ # Connect and use
372
+ client = MCPClient.connect('http://localhost:8931/mcp')
1093
373
  tools = client.list_tools
1094
-
1095
- tools.each do |tool|
1096
- if tool.structured_output?
1097
- puts "#{tool.name} supports structured output"
1098
- puts "Output schema: #{tool.output_schema}"
1099
- end
1100
- end
1101
-
1102
- # Call a tool that returns structured output
1103
- result = client.call_tool('get_weather', { location: 'San Francisco' })
1104
-
1105
- # Access structured data (type-safe, validated)
1106
- if result['structuredContent']
1107
- data = result['structuredContent']
1108
- puts "Temperature: #{data['temperature']}°C"
1109
- puts "Conditions: #{data['conditions']}"
1110
- puts "Humidity: #{data['humidity']}%"
1111
- end
1112
-
1113
- # Backward compatibility: text content is still available
1114
- text_content = result['content']&.first&.dig('text')
1115
- parsed = JSON.parse(text_content) if text_content
374
+ result = client.call_tool('echo', { message: 'Hello!' })
1116
375
  ```
1117
376
 
1118
- ### Helper Methods
1119
-
1120
- The `Tool` class provides a convenient helper method:
1121
-
1122
- - `tool.structured_output?` - Returns true if the tool has an output schema defined
1123
- - `tool.output_schema` - Returns the JSON Schema for the tool's output
1124
-
1125
- ### Example
1126
-
1127
- See `examples/test_structured_outputs.rb` for a complete working example demonstrating:
1128
- - Detecting tools with structured output support
1129
- - Accessing output schemas
1130
- - Calling tools and receiving structured data
1131
- - Backward compatibility with text content
1132
-
1133
- The example includes a Python MCP server (`examples/structured_output_server.py`) that provides three tools with structured outputs:
1134
- - `get_weather` - Weather data with temperature, conditions, humidity
1135
- - `analyze_text` - Text analysis with word count, character count, statistics
1136
- - `calculate_stats` - Statistical calculations (mean, median, min, max, etc.)
1137
-
1138
- ## Elicitation (Server-initiated User Interactions) - MCP 2025-06-18
1139
-
1140
- Elicitation enables MCP servers to request user input during tool execution via bidirectional JSON-RPC.
1141
- This allows servers to gather additional information, confirm sensitive operations, or interact with users
1142
- dynamically during the execution of tools.
1143
-
1144
- ### Transport Support
1145
-
1146
- **Fully Supported:**
1147
- - ✅ **stdio** - Full bidirectional JSON-RPC over stdin/stdout
1148
- - ✅ **SSE** - Server sends requests via SSE stream, client responds via HTTP POST
1149
- - ✅ **Streamable HTTP** - Server sends requests via SSE-formatted responses, client responds via HTTP POST
1150
-
1151
- **Not Supported:**
1152
- - ❌ **HTTP** - Pure request-response architecture prevents server-initiated requests
1153
-
1154
- ### Using Elicitation
1155
-
1156
- To use elicitation, register a handler when creating the client:
1157
-
1158
- ```ruby
1159
- # Define an elicitation handler
1160
- elicitation_handler = lambda do |message, requested_schema|
1161
- puts "Server requests: #{message}"
1162
-
1163
- # Show expected input format from schema
1164
- if requested_schema && requested_schema['properties']
1165
- requested_schema['properties'].each do |field, schema|
1166
- puts " #{field}: #{schema['type']}"
1167
- end
1168
- end
1169
-
1170
- # Prompt user and return one of three response types:
1171
-
1172
- # 1. Accept and provide data to the server
1173
- {
1174
- 'action' => 'accept',
1175
- 'content' => {
1176
- 'field_name' => 'user_input_value'
1177
- }
1178
- }
1179
-
1180
- # 2. Decline to provide input
1181
- # { 'action' => 'decline' }
1182
-
1183
- # 3. Cancel the operation
1184
- # { 'action' => 'cancel' }
1185
- end
1186
-
1187
- # Create client with elicitation handler
1188
- # Works with stdio, SSE, and Streamable HTTP transports
1189
- client = MCPClient::Client.new(
1190
- mcp_server_configs: [
1191
- # Stdio transport
1192
- MCPClient.stdio_config(
1193
- command: 'python my_server.py',
1194
- name: 'my-server'
1195
- ),
1196
- # Or SSE transport
1197
- MCPClient.sse_config(
1198
- base_url: 'https://api.example.com/sse',
1199
- name: 'remote-server'
1200
- ),
1201
- # Or Streamable HTTP transport
1202
- MCPClient.streamable_http_config(
1203
- base_url: 'https://api.example.com',
1204
- endpoint: '/mcp',
1205
- name: 'streamable-server'
1206
- )
1207
- ],
1208
- elicitation_handler: elicitation_handler
1209
- )
1210
-
1211
- # Call tools - connection happens automatically
1212
- # Server may send elicitation requests during tool execution
1213
- result = client.call_tool('create_document', { format: 'markdown' }, server: 'my-server')
1214
- ```
1215
-
1216
- ### Response Actions
1217
-
1218
- The elicitation handler should return a hash with an `action` field:
1219
-
1220
- 1. **Accept** - Provide the requested input:
1221
- ```ruby
1222
- {
1223
- 'action' => 'accept',
1224
- 'content' => { 'field' => 'value' } # Must match requested schema
1225
- }
1226
- ```
1227
-
1228
- 2. **Decline** - Refuse to provide input (server should handle gracefully):
1229
- ```ruby
1230
- { 'action' => 'decline' }
1231
- ```
1232
-
1233
- 3. **Cancel** - Cancel the entire operation:
1234
- ```ruby
1235
- { 'action' => 'cancel' }
1236
- ```
1237
-
1238
- ### Example
1239
-
1240
- See `examples/elicitation/test_elicitation.rb` for a complete working example demonstrating:
1241
- - Registering an elicitation handler
1242
- - Handling different message types and schemas
1243
- - Responding with accept/decline/cancel actions
1244
- - Interactive user prompts during tool execution
1245
-
1246
- The example includes a Python MCP server (`examples/elicitation/elicitation_server.py`) that provides tools using elicitation:
1247
- - `create_document` - Requests title and content via elicitation
1248
- - `send_notification` - Confirms before sending a notification
1249
-
1250
- ## Key Features
1251
-
1252
- ### Client Features
1253
-
1254
- - **Multiple transports** - Support for stdio, SSE, HTTP, and Streamable HTTP transports
1255
- - **Multiple servers** - Connect to multiple MCP servers simultaneously
1256
- - **Named servers** - Associate names with servers and find/reference them by name
1257
- - **Server lookup** - Find servers by name using `find_server`
1258
- - **Tool association** - Each tool knows which server it belongs to
1259
- - **Tool discovery** - Find tools by name or pattern
1260
- - **Structured outputs** - Support for MCP 2025-06-18 structured tool outputs with output schemas and type-safe responses
1261
- - **Tool annotations** - Support for readOnly, destructive, and requiresConfirmation annotations with helper methods
1262
- - **Elicitation support** - Server-initiated user interactions during tool execution (stdio, SSE, and Streamable HTTP transports)
1263
- - **Server disambiguation** - Specify which server to use when tools with same name exist in multiple servers
1264
- - **Atomic tool calls** - Simple API for invoking tools with parameters
1265
- - **Batch support** - Call multiple tools in a single operation
1266
- - **Prompts support** - List and get prompts with parameters from MCP servers
1267
- - **Resources support** - List and read resources by URI from MCP servers
1268
- - **API conversions** - Built-in format conversion for OpenAI, Anthropic, and Google Vertex AI APIs
1269
- - **Thread safety** - Synchronized access for thread-safe operation
1270
- - **Server notifications** - Support for JSON-RPC notifications
1271
- - **Custom RPC methods** - Send any custom JSON-RPC method
1272
- - **Consistent error handling** - Rich error types for better exception handling
1273
- - **JSON configuration** - Support for server definition files in JSON format with name retention
1274
-
1275
- ### Server-Sent Events (SSE) Implementation
1276
-
1277
- The SSE client implementation provides these key features:
1278
-
1279
- - **Robust connection handling**: Properly manages HTTP/HTTPS connections with configurable timeouts and retries
1280
- - **Automatic redirect support**: Follows HTTP redirects up to 3 hops for seamless server integration
1281
- - **Advanced connection management**:
1282
- - **Inactivity tracking**: Monitors connection activity to detect idle connections
1283
- - **Automatic ping**: Sends ping requests after a configurable period of inactivity (default: 10 seconds)
1284
- - **Automatic disconnection**: Closes idle connections after inactivity (2.5× ping interval)
1285
- - **MCP compliant**: Any server communication resets the inactivity timer per specification
1286
- - **Intelligent reconnection**:
1287
- - **Ping failure detection**: Tracks consecutive ping failures (when server isn't responding)
1288
- - **Automatic reconnection**: Attempts to reconnect after 3 consecutive ping failures
1289
- - **Exponential backoff**: Uses increasing delays between reconnection attempts
1290
- - **Smart retry limits**: Caps reconnection attempts (default: 5) to avoid infinite loops
1291
- - **Connection state monitoring**: Properly detects and handles closed connections to prevent errors
1292
- - **Failure transparency**: Handles reconnection in the background without disrupting client code
1293
- - **Thread safety**: All operations are thread-safe using monitors and synchronized access
1294
- - **Reliable error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
1295
- - **JSON-RPC over SSE**: Full implementation of JSON-RPC 2.0 over SSE transport with initialize handshake
1296
- - **Streaming support**: Native streaming for real-time updates via the `call_tool_streaming` method, which returns an Enumerator for processing results as they arrive
1297
- - **Notification support**: Built-in handling for JSON-RPC notifications with automatic tool cache invalidation and custom notification callback support
1298
- - **Custom RPC methods**: Send any custom JSON-RPC method or notification through `send_rpc` and `send_notification`
1299
- - **Configurable retries**: All RPC requests support configurable retries with exponential backoff
1300
- - **Consistent logging**: Tagged, leveled logging across all components for better debugging
1301
- - **Graceful fallbacks**: Automatic fallback to synchronous HTTP when SSE connection fails
1302
- - **URL normalization**: Consistent URL handling that respects user-provided formats
1303
- - **Server connectivity check**: Built-in `ping` method to test server connectivity and health
1304
-
1305
- ### HTTP Transport Implementation
1306
-
1307
- The HTTP transport provides a simpler, stateless communication mechanism for MCP servers:
1308
-
1309
- - **Request/Response Model**: Standard HTTP request/response cycle for each JSON-RPC call
1310
- - **Automatic redirect support**: Follows HTTP redirects up to 3 hops for seamless server integration
1311
- - **JSON-Only Responses**: Accepts only `application/json` responses (no SSE support)
1312
- - **Session Support**: Automatic session header (`Mcp-Session-Id`) capture and injection for session-based MCP servers
1313
- - **Session Termination**: Proper session cleanup with HTTP DELETE requests during connection teardown
1314
- - **Session Validation**: Security validation of session IDs to prevent malicious injection
1315
- - **Stateless & Stateful**: Supports both stateless servers and session-based servers that require state continuity
1316
- - **HTTP Headers Support**: Full support for custom headers including authorization, API keys, and other metadata
1317
- - **Reliable Error Handling**: Comprehensive HTTP status code handling with appropriate error mapping
1318
- - **Configurable Retries**: Exponential backoff retry logic for transient network failures
1319
- - **Connection Pooling**: Uses Faraday's connection pooling for efficient HTTP connections
1320
- - **Timeout Management**: Configurable timeouts for both connection establishment and request completion
1321
- - **JSON-RPC over HTTP**: Full JSON-RPC 2.0 implementation over HTTP POST requests
1322
- - **MCP Protocol Compliance**: Supports all standard MCP methods (initialize, tools/list, tools/call)
1323
- - **Custom RPC Methods**: Send any custom JSON-RPC method or notification
1324
- - **Thread Safety**: All operations are thread-safe for concurrent usage
1325
- - **Streaming API Compatibility**: Provides `call_tool_streaming` method for API compatibility (returns single response)
1326
- - **Graceful Degradation**: Simple fallback behavior when complex features aren't needed
1327
-
1328
- ### Streamable HTTP Transport Implementation
1329
-
1330
- The Streamable HTTP transport bridges HTTP and Server-Sent Events, designed for servers that use HTTP POST but return SSE-formatted responses:
1331
-
1332
- - **Hybrid Communication**: HTTP POST requests with Server-Sent Event formatted responses
1333
- - **SSE Response Parsing**: Automatically parses `event:` and `data:` lines from SSE responses
1334
- - **Session Support**: Automatic session header (`Mcp-Session-Id`) capture and injection for session-based MCP servers
1335
- - **Session Termination**: Proper session cleanup with HTTP DELETE requests during connection teardown
1336
- - **Resumability**: Event ID tracking and `Last-Event-ID` header support for message replay after disconnections
1337
- - **Session Validation**: Security validation of session IDs to prevent malicious injection
1338
- - **HTTP Semantics**: Maintains standard HTTP request/response model for client compatibility
1339
- - **Streaming Format Support**: Handles complex SSE responses with multiple fields (event, id, retry, etc.)
1340
- - **Error Handling**: Comprehensive error handling for both HTTP and SSE parsing failures
1341
- - **Headers Optimization**: Includes SSE-compatible headers (`Accept: text/event-stream, application/json`, `Cache-Control: no-cache`)
1342
- - **JSON-RPC Compliance**: Full JSON-RPC 2.0 support over the hybrid HTTP/SSE transport
1343
- - **Retry Logic**: Exponential backoff for both connection and parsing failures
1344
- - **Thread Safety**: All operations are thread-safe for concurrent usage
1345
- - **Malformed Response Handling**: Graceful handling of invalid SSE format or missing data lines
1346
-
1347
377
  ## Requirements
1348
378
 
1349
379
  - Ruby >= 3.2.0
1350
380
  - No runtime dependencies
1351
381
 
1352
- ## Implementing an MCP Server
1353
-
1354
- To implement a compatible MCP server you must:
1355
-
1356
- - Listen on your chosen transport (JSON-RPC stdio, HTTP SSE, HTTP, or Streamable HTTP)
1357
- - Respond to `list_tools` requests with a JSON list of tools
1358
- - Respond to `call_tool` requests by executing the specified tool
1359
- - Return results (or errors) in JSON format
1360
- - Optionally send JSON-RPC notifications for events like tool updates
1361
-
1362
- ### JSON-RPC Notifications
1363
-
1364
- The client supports JSON-RPC notifications from the server:
1365
-
1366
- - Default notification handler for `notifications/tools/list_changed` to automatically clear the tool cache
1367
- - Custom notification handling via the `on_notification` method
1368
- - Callbacks receive the server instance, method name, and parameters
1369
- - Multiple notification listeners can be registered
1370
-
1371
- ## Tool Schema
1372
-
1373
- Each tool is defined by a name, description, and a JSON Schema for its parameters:
1374
-
1375
- ```json
1376
- {
1377
- "name": "example_tool",
1378
- "description": "Does something useful",
1379
- "schema": {
1380
- "type": "object",
1381
- "properties": {
1382
- "param1": { "type": "string" },
1383
- "param2": { "type": "number" }
1384
- },
1385
- "required": ["param1"]
1386
- }
1387
- }
1388
- ```
1389
-
1390
382
  ## License
1391
383
 
1392
- This gem is available as open source under the [MIT License](LICENSE).
384
+ Available as open source under the [MIT License](LICENSE).
1393
385
 
1394
386
  ## Contributing
1395
387
 
1396
- Bug reports and pull requests are welcome on GitHub at
1397
- https://github.com/simonx1/ruby-mcp-client.
388
+ Bug reports and pull requests welcome at https://github.com/simonx1/ruby-mcp-client.