ruby-mcp-client 0.9.1 → 1.0.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 +42 -6
- data/lib/mcp_client/audio_content.rb +46 -0
- data/lib/mcp_client/auth/oauth_provider.rb +29 -6
- data/lib/mcp_client/auth.rb +68 -8
- data/lib/mcp_client/client.rb +265 -54
- data/lib/mcp_client/elicitation_validator.rb +262 -0
- data/lib/mcp_client/errors.rb +6 -0
- data/lib/mcp_client/http_transport_base.rb +2 -0
- data/lib/mcp_client/json_rpc_common.rb +3 -3
- data/lib/mcp_client/resource.rb +8 -0
- data/lib/mcp_client/resource_link.rb +63 -0
- data/lib/mcp_client/server_http.rb +17 -7
- data/lib/mcp_client/server_sse.rb +9 -6
- data/lib/mcp_client/server_stdio.rb +9 -6
- data/lib/mcp_client/server_streamable_http.rb +23 -10
- data/lib/mcp_client/task.rb +127 -0
- data/lib/mcp_client/tool.rb +73 -9
- data/lib/mcp_client/version.rb +2 -2
- data/lib/mcp_client.rb +27 -0
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e56ef84106ab19bf3028b31dd7ce362cfb58f244743057271f14a83409153f0
|
|
4
|
+
data.tar.gz: 96f0e42875d80032c24056c1dbd9c6417b3db994538838736fa52f303e0d4f85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48b7cd77ab406967bc4cd5fe79c5c1511a8eb2c9e4c9ebc27b31b8386e175a435d1a620ffcfa7871044188061b3f0718875d009f2a4ef7d030786ad11bcbde35
|
|
7
|
+
data.tar.gz: 11f845c21895b67d0a308267972936fcf07df042e33de6417b69ee486f1739a8768f25b698bbbad92fdbef3acf0e6ec42eaffa525896b9e77e50e86960c7daf8
|
data/README.md
CHANGED
|
@@ -28,16 +28,18 @@ Built-in API conversions: `to_openai_tools()`, `to_anthropic_tools()`, `to_googl
|
|
|
28
28
|
|
|
29
29
|
## MCP Protocol Support
|
|
30
30
|
|
|
31
|
-
Implements **MCP 2025-
|
|
31
|
+
Implements **MCP 2025-11-25** specification:
|
|
32
32
|
|
|
33
|
-
- **Tools**: list, call, streaming, annotations, structured outputs
|
|
33
|
+
- **Tools**: list, call, streaming, annotations (hint-style), structured outputs, title
|
|
34
34
|
- **Prompts**: list, get with parameters
|
|
35
|
-
- **Resources**: list, read, templates, subscriptions, pagination
|
|
35
|
+
- **Resources**: list, read, templates, subscriptions, pagination, ResourceLink content
|
|
36
36
|
- **Elicitation**: Server-initiated user interactions (stdio, SSE, Streamable HTTP)
|
|
37
37
|
- **Roots**: Filesystem scope boundaries with change notifications
|
|
38
|
-
- **Sampling**: Server-requested LLM completions
|
|
39
|
-
- **Completion**: Autocomplete for prompts/resources
|
|
38
|
+
- **Sampling**: Server-requested LLM completions with modelPreferences
|
|
39
|
+
- **Completion**: Autocomplete for prompts/resources with context
|
|
40
40
|
- **Logging**: Server log messages with level filtering
|
|
41
|
+
- **Tasks**: Structured task management with progress tracking
|
|
42
|
+
- **Audio**: Audio content type support
|
|
41
43
|
- **OAuth 2.1**: PKCE, server discovery, dynamic registration
|
|
42
44
|
|
|
43
45
|
## Quick Connect API (Recommended)
|
|
@@ -114,12 +116,20 @@ contents.each do |content|
|
|
|
114
116
|
end
|
|
115
117
|
```
|
|
116
118
|
|
|
117
|
-
## MCP 2025-
|
|
119
|
+
## MCP 2025-11-25 Features
|
|
118
120
|
|
|
119
121
|
### Tool Annotations
|
|
120
122
|
|
|
121
123
|
```ruby
|
|
122
124
|
tool = client.find_tool('delete_user')
|
|
125
|
+
|
|
126
|
+
# Hint-style annotations (MCP 2025-11-25)
|
|
127
|
+
tool.read_only_hint? # Defaults to true; tool does not modify environment
|
|
128
|
+
tool.destructive_hint? # Defaults to false; tool may perform destructive updates
|
|
129
|
+
tool.idempotent_hint? # Defaults to false; repeated calls have no additional effect
|
|
130
|
+
tool.open_world_hint? # Defaults to true; tool may interact with external entities
|
|
131
|
+
|
|
132
|
+
# Legacy annotations
|
|
123
133
|
tool.read_only? # Safe to execute?
|
|
124
134
|
tool.destructive? # Warning: destructive operation
|
|
125
135
|
tool.requires_confirmation? # Needs user confirmation
|
|
@@ -293,10 +303,36 @@ client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
|
|
|
293
303
|
# Use tools with Claude API
|
|
294
304
|
```
|
|
295
305
|
|
|
306
|
+
### RubyLLM
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
require 'mcp_client'
|
|
310
|
+
require 'ruby_llm'
|
|
311
|
+
|
|
312
|
+
RubyLLM.configure { |c| c.openai_api_key = ENV['OPENAI_API_KEY'] }
|
|
313
|
+
mcp = MCPClient.connect('http://localhost:8931/mcp') # Playwright MCP
|
|
314
|
+
|
|
315
|
+
# Wrap each MCP tool as a RubyLLM tool
|
|
316
|
+
tools = mcp.list_tools.map do |t|
|
|
317
|
+
tool_name = t.name
|
|
318
|
+
Class.new(RubyLLM::Tool) do
|
|
319
|
+
description t.description
|
|
320
|
+
params t.schema
|
|
321
|
+
define_method(:name) { tool_name }
|
|
322
|
+
define_method(:execute) { |**args| mcp.call_tool(tool_name, args) }
|
|
323
|
+
end.new
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
chat = RubyLLM.chat(model: 'gpt-4o-mini')
|
|
327
|
+
tools.each { |tool| chat.with_tool(tool) }
|
|
328
|
+
response = chat.ask('Navigate to google.com and tell me the page title')
|
|
329
|
+
```
|
|
330
|
+
|
|
296
331
|
See `examples/` for complete implementations:
|
|
297
332
|
- `ruby_openai_mcp.rb`, `openai_ruby_mcp.rb` - OpenAI integration
|
|
298
333
|
- `ruby_anthropic_mcp.rb` - Anthropic integration
|
|
299
334
|
- `gemini_ai_mcp.rb` - Google Vertex AI integration
|
|
335
|
+
- `ruby_llm_mcp.rb` - RubyLLM integration (OpenAI provider)
|
|
300
336
|
|
|
301
337
|
## OAuth 2.1 Authentication
|
|
302
338
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# Representation of MCP audio content (MCP 2025-11-25)
|
|
5
|
+
# Used for base64-encoded audio data in messages and tool results
|
|
6
|
+
class AudioContent
|
|
7
|
+
# @!attribute [r] data
|
|
8
|
+
# @return [String] base64-encoded audio data
|
|
9
|
+
# @!attribute [r] mime_type
|
|
10
|
+
# @return [String] MIME type of the audio (e.g., 'audio/wav', 'audio/mpeg', 'audio/ogg')
|
|
11
|
+
# @!attribute [r] annotations
|
|
12
|
+
# @return [Hash, nil] optional annotations that provide hints to clients
|
|
13
|
+
attr_reader :data, :mime_type, :annotations
|
|
14
|
+
|
|
15
|
+
# Initialize audio content
|
|
16
|
+
# @param data [String] base64-encoded audio data
|
|
17
|
+
# @param mime_type [String] MIME type of the audio
|
|
18
|
+
# @param annotations [Hash, nil] optional annotations that provide hints to clients
|
|
19
|
+
def initialize(data:, mime_type:, annotations: nil)
|
|
20
|
+
raise ArgumentError, 'AudioContent requires data' if data.nil? || data.empty?
|
|
21
|
+
raise ArgumentError, 'AudioContent requires mime_type' if mime_type.nil? || mime_type.empty?
|
|
22
|
+
|
|
23
|
+
@data = data
|
|
24
|
+
@mime_type = mime_type
|
|
25
|
+
@annotations = annotations
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create an AudioContent instance from JSON data
|
|
29
|
+
# @param data [Hash] JSON data from MCP server
|
|
30
|
+
# @return [MCPClient::AudioContent] audio content instance
|
|
31
|
+
def self.from_json(json_data)
|
|
32
|
+
new(
|
|
33
|
+
data: json_data['data'],
|
|
34
|
+
mime_type: json_data['mimeType'],
|
|
35
|
+
annotations: json_data['annotations']
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get the decoded audio content
|
|
40
|
+
# @return [String] decoded binary audio data
|
|
41
|
+
def content
|
|
42
|
+
require 'base64'
|
|
43
|
+
Base64.decode64(@data)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -14,7 +14,7 @@ module MCPClient
|
|
|
14
14
|
# @!attribute [rw] redirect_uri
|
|
15
15
|
# @return [String] OAuth redirect URI
|
|
16
16
|
# @!attribute [rw] scope
|
|
17
|
-
# @return [String, nil] OAuth scope
|
|
17
|
+
# @return [String, Symbol, nil] OAuth scope (use :all for all server-supported scopes)
|
|
18
18
|
# @!attribute [rw] logger
|
|
19
19
|
# @return [Logger] Logger instance
|
|
20
20
|
# @!attribute [rw] storage
|
|
@@ -27,15 +27,19 @@ module MCPClient
|
|
|
27
27
|
# Initialize OAuth provider
|
|
28
28
|
# @param server_url [String] The MCP server URL (used as OAuth resource parameter)
|
|
29
29
|
# @param redirect_uri [String] OAuth redirect URI (default: http://localhost:8080/callback)
|
|
30
|
-
# @param scope [String, nil] OAuth scope
|
|
30
|
+
# @param scope [String, Symbol, nil] OAuth scope (use :all for all server-supported scopes)
|
|
31
31
|
# @param logger [Logger, nil] Optional logger
|
|
32
32
|
# @param storage [Object, nil] Storage backend for tokens and client info
|
|
33
|
-
|
|
33
|
+
# @param client_metadata [Hash] Extra OIDC client metadata fields for DCR registration.
|
|
34
|
+
# Supported keys: :client_name, :client_uri, :logo_uri, :tos_uri, :policy_uri, :contacts
|
|
35
|
+
def initialize(server_url:, redirect_uri: 'http://localhost:8080/callback', scope: nil, logger: nil, storage: nil,
|
|
36
|
+
client_metadata: {})
|
|
34
37
|
self.server_url = server_url
|
|
35
38
|
self.redirect_uri = redirect_uri
|
|
36
39
|
self.scope = scope
|
|
37
40
|
self.logger = logger || Logger.new($stdout, level: Logger::WARN)
|
|
38
41
|
self.storage = storage || MemoryStorage.new
|
|
42
|
+
@extra_client_metadata = client_metadata
|
|
39
43
|
@http_client = create_http_client
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -58,6 +62,14 @@ module MCPClient
|
|
|
58
62
|
refresh_token(token) if token.refresh_token
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
# Return the scopes supported by the authorization server
|
|
66
|
+
# Discovers server metadata and returns the scopes_supported list.
|
|
67
|
+
# @return [Array<String>] supported scopes, or empty array if not advertised
|
|
68
|
+
# @raise [MCPClient::Errors::ConnectionError] if server discovery fails
|
|
69
|
+
def supported_scopes
|
|
70
|
+
@supported_scopes ||= discover_authorization_server.scopes_supported || []
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
# Start OAuth authorization flow
|
|
62
74
|
# @return [String] Authorization URL to redirect user to
|
|
63
75
|
# @raise [MCPClient::Errors::ConnectionError] if server discovery fails
|
|
@@ -328,12 +340,15 @@ module MCPClient
|
|
|
328
340
|
def register_client(server_metadata)
|
|
329
341
|
logger.debug("Registering OAuth client at: #{server_metadata.registration_endpoint}")
|
|
330
342
|
|
|
343
|
+
resolved_scope = scope == :all ? supported_scopes.join(' ') : scope
|
|
344
|
+
|
|
331
345
|
metadata = ClientMetadata.new(
|
|
332
346
|
redirect_uris: [redirect_uri],
|
|
333
347
|
token_endpoint_auth_method: 'none', # Public client
|
|
334
348
|
grant_types: %w[authorization_code refresh_token],
|
|
335
349
|
response_types: ['code'],
|
|
336
|
-
scope:
|
|
350
|
+
scope: resolved_scope,
|
|
351
|
+
**@extra_client_metadata
|
|
337
352
|
)
|
|
338
353
|
|
|
339
354
|
response = @http_client.post(server_metadata.registration_endpoint) do |req|
|
|
@@ -355,7 +370,13 @@ module MCPClient
|
|
|
355
370
|
token_endpoint_auth_method: data['token_endpoint_auth_method'] || 'none',
|
|
356
371
|
grant_types: data['grant_types'] || %w[authorization_code refresh_token],
|
|
357
372
|
response_types: data['response_types'] || ['code'],
|
|
358
|
-
scope: data['scope']
|
|
373
|
+
scope: data['scope'],
|
|
374
|
+
client_name: data['client_name'],
|
|
375
|
+
client_uri: data['client_uri'],
|
|
376
|
+
logo_uri: data['logo_uri'],
|
|
377
|
+
tos_uri: data['tos_uri'],
|
|
378
|
+
policy_uri: data['policy_uri'],
|
|
379
|
+
contacts: data['contacts']
|
|
359
380
|
)
|
|
360
381
|
|
|
361
382
|
# Warn if server changed redirect_uri
|
|
@@ -396,11 +417,13 @@ module MCPClient
|
|
|
396
417
|
# Use the redirect_uri that was actually registered
|
|
397
418
|
registered_redirect_uri = client_info.metadata.redirect_uris.first
|
|
398
419
|
|
|
420
|
+
resolved_scope = scope == :all ? supported_scopes.join(' ') : scope
|
|
421
|
+
|
|
399
422
|
params = {
|
|
400
423
|
response_type: 'code',
|
|
401
424
|
client_id: client_info.client_id,
|
|
402
425
|
redirect_uri: registered_redirect_uri,
|
|
403
|
-
scope:
|
|
426
|
+
scope: resolved_scope,
|
|
404
427
|
state: state,
|
|
405
428
|
code_challenge: pkce.code_challenge,
|
|
406
429
|
code_challenge_method: pkce.code_challenge_method,
|
data/lib/mcp_client/auth.rb
CHANGED
|
@@ -86,21 +86,38 @@ module MCPClient
|
|
|
86
86
|
|
|
87
87
|
# OAuth client metadata for registration and authorization
|
|
88
88
|
class ClientMetadata
|
|
89
|
-
attr_reader :redirect_uris, :token_endpoint_auth_method, :grant_types, :response_types, :scope
|
|
89
|
+
attr_reader :redirect_uris, :token_endpoint_auth_method, :grant_types, :response_types, :scope,
|
|
90
|
+
:client_name, :client_uri, :logo_uri, :tos_uri, :policy_uri, :contacts
|
|
90
91
|
|
|
91
92
|
# @param redirect_uris [Array<String>] List of valid redirect URIs
|
|
92
93
|
# @param token_endpoint_auth_method [String] Authentication method for token endpoint
|
|
93
94
|
# @param grant_types [Array<String>] Supported grant types
|
|
94
95
|
# @param response_types [Array<String>] Supported response types
|
|
95
96
|
# @param scope [String, nil] Requested scope
|
|
97
|
+
# @param client_name [String, nil] Human-readable client name
|
|
98
|
+
# @param client_uri [String, nil] URL of the client home page
|
|
99
|
+
# @param logo_uri [String, nil] URL of the client logo
|
|
100
|
+
# @param tos_uri [String, nil] URL of the client terms of service
|
|
101
|
+
# @param policy_uri [String, nil] URL of the client privacy policy
|
|
102
|
+
# @param contacts [Array<String>, nil] List of contact emails for the client
|
|
103
|
+
# rubocop:disable Metrics/ParameterLists
|
|
96
104
|
def initialize(redirect_uris:, token_endpoint_auth_method: 'none',
|
|
97
105
|
grant_types: %w[authorization_code refresh_token],
|
|
98
|
-
response_types: ['code'], scope: nil
|
|
106
|
+
response_types: ['code'], scope: nil,
|
|
107
|
+
client_name: nil, client_uri: nil, logo_uri: nil,
|
|
108
|
+
tos_uri: nil, policy_uri: nil, contacts: nil)
|
|
109
|
+
# rubocop:enable Metrics/ParameterLists
|
|
99
110
|
@redirect_uris = redirect_uris
|
|
100
111
|
@token_endpoint_auth_method = token_endpoint_auth_method
|
|
101
112
|
@grant_types = grant_types
|
|
102
113
|
@response_types = response_types
|
|
103
114
|
@scope = scope
|
|
115
|
+
@client_name = client_name
|
|
116
|
+
@client_uri = client_uri
|
|
117
|
+
@logo_uri = logo_uri
|
|
118
|
+
@tos_uri = tos_uri
|
|
119
|
+
@policy_uri = policy_uri
|
|
120
|
+
@contacts = contacts
|
|
104
121
|
end
|
|
105
122
|
|
|
106
123
|
# Convert to hash for HTTP requests
|
|
@@ -111,7 +128,13 @@ module MCPClient
|
|
|
111
128
|
token_endpoint_auth_method: @token_endpoint_auth_method,
|
|
112
129
|
grant_types: @grant_types,
|
|
113
130
|
response_types: @response_types,
|
|
114
|
-
scope: @scope
|
|
131
|
+
scope: @scope,
|
|
132
|
+
client_name: @client_name,
|
|
133
|
+
client_uri: @client_uri,
|
|
134
|
+
logo_uri: @logo_uri,
|
|
135
|
+
tos_uri: @tos_uri,
|
|
136
|
+
policy_uri: @policy_uri,
|
|
137
|
+
contacts: @contacts
|
|
115
138
|
}.compact
|
|
116
139
|
end
|
|
117
140
|
end
|
|
@@ -180,7 +203,13 @@ module MCPClient
|
|
|
180
203
|
grant_types: metadata_data[:grant_types] || metadata_data['grant_types'] ||
|
|
181
204
|
%w[authorization_code refresh_token],
|
|
182
205
|
response_types: metadata_data[:response_types] || metadata_data['response_types'] || ['code'],
|
|
183
|
-
scope: metadata_data[:scope] || metadata_data['scope']
|
|
206
|
+
scope: metadata_data[:scope] || metadata_data['scope'],
|
|
207
|
+
client_name: metadata_data[:client_name] || metadata_data['client_name'],
|
|
208
|
+
client_uri: metadata_data[:client_uri] || metadata_data['client_uri'],
|
|
209
|
+
logo_uri: metadata_data[:logo_uri] || metadata_data['logo_uri'],
|
|
210
|
+
tos_uri: metadata_data[:tos_uri] || metadata_data['tos_uri'],
|
|
211
|
+
policy_uri: metadata_data[:policy_uri] || metadata_data['policy_uri'],
|
|
212
|
+
contacts: metadata_data[:contacts] || metadata_data['contacts']
|
|
184
213
|
)
|
|
185
214
|
end
|
|
186
215
|
|
|
@@ -288,10 +317,41 @@ module MCPClient
|
|
|
288
317
|
attr_reader :code_verifier, :code_challenge, :code_challenge_method
|
|
289
318
|
|
|
290
319
|
# Generate PKCE parameters
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
# @param code_verifier [String, nil] Existing code verifier (for deserialization)
|
|
321
|
+
# @param code_challenge [String, nil] Existing code challenge (for deserialization)
|
|
322
|
+
# @param code_challenge_method [String] Challenge method (default: 'S256')
|
|
323
|
+
def initialize(code_verifier: nil, code_challenge: nil, code_challenge_method: nil)
|
|
324
|
+
@code_verifier = code_verifier || generate_code_verifier
|
|
325
|
+
@code_challenge = code_challenge || generate_code_challenge(@code_verifier)
|
|
326
|
+
@code_challenge_method = code_challenge_method || 'S256'
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Convert to hash for serialization
|
|
330
|
+
# @return [Hash] Hash representation
|
|
331
|
+
def to_h
|
|
332
|
+
{
|
|
333
|
+
code_verifier: @code_verifier,
|
|
334
|
+
code_challenge: @code_challenge,
|
|
335
|
+
code_challenge_method: @code_challenge_method
|
|
336
|
+
}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Create PKCE instance from hash
|
|
340
|
+
# @param data [Hash] Hash with PKCE parameters (symbol or string keys)
|
|
341
|
+
# @return [PKCE] New PKCE instance
|
|
342
|
+
# @raise [ArgumentError] If required parameters are missing
|
|
343
|
+
# @note code_challenge_method is optional and defaults to 'S256'.
|
|
344
|
+
# The code_challenge is not re-validated against code_verifier;
|
|
345
|
+
# callers are expected to provide values from a prior to_h round-trip.
|
|
346
|
+
def self.from_h(data)
|
|
347
|
+
verifier = data[:code_verifier] || data['code_verifier']
|
|
348
|
+
challenge = data[:code_challenge] || data['code_challenge']
|
|
349
|
+
method = data[:code_challenge_method] || data['code_challenge_method']
|
|
350
|
+
|
|
351
|
+
raise ArgumentError, 'Missing code_verifier' unless verifier
|
|
352
|
+
raise ArgumentError, 'Missing code_challenge' unless challenge
|
|
353
|
+
|
|
354
|
+
new(code_verifier: verifier, code_challenge: challenge, code_challenge_method: method)
|
|
295
355
|
end
|
|
296
356
|
|
|
297
357
|
private
|