ruby_llm_swarm-mcp 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
- data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
- data/lib/ruby_llm/chat.rb +34 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
- data/lib/ruby_llm/mcp/attachment.rb +18 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +401 -0
- data/lib/ruby_llm/mcp/completion.rb +16 -0
- data/lib/ruby_llm/mcp/configuration.rb +310 -0
- data/lib/ruby_llm/mcp/content.rb +28 -0
- data/lib/ruby_llm/mcp/elicitation.rb +48 -0
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +91 -0
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
- data/lib/ruby_llm/mcp/native/client.rb +387 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
- data/lib/ruby_llm/mcp/native/messages.rb +36 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
- data/lib/ruby_llm/mcp/progress.rb +35 -0
- data/lib/ruby_llm/mcp/prompt.rb +132 -0
- data/lib/ruby_llm/mcp/railtie.rb +14 -0
- data/lib/ruby_llm/mcp/resource.rb +112 -0
- data/lib/ruby_llm/mcp/resource_template.rb +85 -0
- data/lib/ruby_llm/mcp/result.rb +108 -0
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +152 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
- data/lib/ruby_llm/mcp/tool.rb +228 -0
- data/lib/ruby_llm/mcp/version.rb +7 -0
- data/lib/ruby_llm/mcp.rb +125 -0
- data/lib/tasks/release.rake +23 -0
- metadata +184 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module JsonRpc
|
|
7
|
+
VERSION = "2.0"
|
|
8
|
+
|
|
9
|
+
module ErrorCodes
|
|
10
|
+
PARSE_ERROR = -32_700
|
|
11
|
+
INVALID_REQUEST = -32_600
|
|
12
|
+
METHOD_NOT_FOUND = -32_601
|
|
13
|
+
INVALID_PARAMS = -32_602
|
|
14
|
+
INTERNAL_ERROR = -32_603
|
|
15
|
+
|
|
16
|
+
SERVER_ERROR_MIN = -32_099
|
|
17
|
+
SERVER_ERROR_MAX = -32_000
|
|
18
|
+
|
|
19
|
+
SERVER_ERROR = -32_000
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class EnvelopeValidator
|
|
23
|
+
attr_reader :envelope, :errors
|
|
24
|
+
|
|
25
|
+
def initialize(envelope)
|
|
26
|
+
@envelope = envelope
|
|
27
|
+
@errors = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def valid?
|
|
31
|
+
validate!
|
|
32
|
+
@errors.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def error_message
|
|
36
|
+
@errors.join("; ") if @errors.any?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def notification?
|
|
40
|
+
!envelope.key?("id") && envelope.key?("method")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def request?
|
|
44
|
+
envelope.key?("id") && envelope.key?("method") &&
|
|
45
|
+
!envelope.key?("result") && !envelope.key?("error")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def response?
|
|
49
|
+
envelope.key?("id") && !envelope.key?("method") &&
|
|
50
|
+
(envelope.key?("result") || envelope.key?("error"))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def validate!
|
|
56
|
+
@errors = []
|
|
57
|
+
|
|
58
|
+
unless envelope.is_a?(Hash)
|
|
59
|
+
@errors << "Envelope must be an object"
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
unless envelope["jsonrpc"] == VERSION
|
|
64
|
+
@errors << "Missing or invalid 'jsonrpc' field (must be '#{VERSION}')"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Determine message type and validate
|
|
68
|
+
# Priority: response > request > notification (to catch malformed messages)
|
|
69
|
+
if envelope.key?("id") && (envelope.key?("result") || envelope.key?("error"))
|
|
70
|
+
validate_response!
|
|
71
|
+
elsif envelope.key?("id") && envelope.key?("method")
|
|
72
|
+
validate_request!
|
|
73
|
+
elsif !envelope.key?("id") && envelope.key?("method")
|
|
74
|
+
validate_notification!
|
|
75
|
+
else
|
|
76
|
+
@errors << "Message must be a request, response, or notification"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_notification!
|
|
81
|
+
unless envelope["method"].is_a?(String) && !envelope["method"].empty?
|
|
82
|
+
@errors << "Notification must have a non-empty 'method' string"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if envelope.key?("id")
|
|
86
|
+
@errors << "Notification must not have 'id' field"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if envelope.key?("params") && !structured_value?(envelope["params"])
|
|
90
|
+
@errors << "Notification 'params' must be an object or array"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_request!
|
|
95
|
+
unless envelope["method"].is_a?(String) && !envelope["method"].empty?
|
|
96
|
+
@errors << "Request must have a non-empty 'method' string"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
unless valid_id?(envelope["id"])
|
|
100
|
+
@errors << "Request 'id' must be a string, number, or null"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if envelope.key?("params") && !structured_value?(envelope["params"])
|
|
104
|
+
@errors << "Request 'params' must be an object or array"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if envelope.key?("result") || envelope.key?("error")
|
|
108
|
+
@errors << "Request must not have 'result' or 'error' fields"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_response!
|
|
113
|
+
unless valid_id?(envelope["id"])
|
|
114
|
+
@errors << "Response 'id' must be a string, number, or null"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if envelope.key?("method")
|
|
118
|
+
@errors << "Response must not have 'method' field"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
has_result = envelope.key?("result")
|
|
122
|
+
has_error = envelope.key?("error")
|
|
123
|
+
|
|
124
|
+
if has_result && has_error
|
|
125
|
+
@errors << "Response must have either 'result' or 'error', not both"
|
|
126
|
+
elsif !has_result && !has_error
|
|
127
|
+
@errors << "Response must have either 'result' or 'error'"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if has_error
|
|
131
|
+
validate_error_object!(envelope["error"])
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_error_object!(error)
|
|
136
|
+
unless error.is_a?(Hash)
|
|
137
|
+
@errors << "Error must be an object"
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
unless error["code"].is_a?(Integer)
|
|
142
|
+
@errors << "Error 'code' must be an integer"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
unless error["message"].is_a?(String)
|
|
146
|
+
@errors << "Error 'message' must be a string"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if error.key?("data") && !valid_data_value?(error["data"])
|
|
150
|
+
@errors << "Error 'data' must be a valid JSON value"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def valid_id?(id)
|
|
155
|
+
id.is_a?(String) || id.is_a?(Numeric) || id.nil?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def structured_value?(value)
|
|
159
|
+
value.is_a?(Hash) || value.is_a?(Array)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def valid_data_value?(value)
|
|
163
|
+
value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(TrueClass) ||
|
|
164
|
+
value.is_a?(FalseClass) || value.nil? || value.is_a?(Hash) || value.is_a?(Array)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module MCP
|
|
7
|
+
module Native
|
|
8
|
+
module Messages
|
|
9
|
+
# Helper methods for message construction
|
|
10
|
+
module Helpers
|
|
11
|
+
def generate_id
|
|
12
|
+
SecureRandom.uuid
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_progress_token(params, tracking_progress: false)
|
|
16
|
+
return params unless tracking_progress
|
|
17
|
+
|
|
18
|
+
params[:_meta] ||= {}
|
|
19
|
+
params[:_meta][:progressToken] = generate_id
|
|
20
|
+
params
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_cursor(params, cursor)
|
|
24
|
+
return params unless cursor
|
|
25
|
+
|
|
26
|
+
params[:cursor] = cursor
|
|
27
|
+
params
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def format_completion_context(context)
|
|
31
|
+
return nil if context.nil?
|
|
32
|
+
|
|
33
|
+
{ arguments: context }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Messages
|
|
7
|
+
# Notification message builders
|
|
8
|
+
# Notifications do not have IDs and do not expect responses
|
|
9
|
+
module Notifications
|
|
10
|
+
extend Helpers
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def initialized
|
|
15
|
+
{
|
|
16
|
+
jsonrpc: JSONRPC_VERSION,
|
|
17
|
+
method: METHOD_NOTIFICATION_INITIALIZED
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cancelled(request_id:, reason:)
|
|
22
|
+
{
|
|
23
|
+
jsonrpc: JSONRPC_VERSION,
|
|
24
|
+
method: METHOD_NOTIFICATION_CANCELLED,
|
|
25
|
+
params: {
|
|
26
|
+
requestId: request_id,
|
|
27
|
+
reason: reason
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def roots_list_changed
|
|
33
|
+
{
|
|
34
|
+
jsonrpc: JSONRPC_VERSION,
|
|
35
|
+
method: METHOD_NOTIFICATION_ROOTS_LIST_CHANGED
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Messages
|
|
7
|
+
# Request message builders
|
|
8
|
+
# All methods return a JSON-RPC request body ready to be sent
|
|
9
|
+
module Requests
|
|
10
|
+
extend Helpers
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def initialize(protocol_version:, capabilities:)
|
|
15
|
+
{
|
|
16
|
+
jsonrpc: JSONRPC_VERSION,
|
|
17
|
+
id: generate_id,
|
|
18
|
+
method: METHOD_INITIALIZE,
|
|
19
|
+
params: {
|
|
20
|
+
protocolVersion: protocol_version,
|
|
21
|
+
capabilities: capabilities,
|
|
22
|
+
clientInfo: {
|
|
23
|
+
name: "RubyLLM-MCP Client",
|
|
24
|
+
version: RubyLLM::MCP::VERSION
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ping(tracking_progress: false)
|
|
31
|
+
params = add_progress_token({}, tracking_progress: tracking_progress)
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
jsonrpc: JSONRPC_VERSION,
|
|
35
|
+
id: generate_id,
|
|
36
|
+
method: METHOD_PING,
|
|
37
|
+
params: params
|
|
38
|
+
}.tap { |body| body.delete(:params) if params.empty? }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def tool_list(cursor: nil, tracking_progress: false)
|
|
42
|
+
params = {}
|
|
43
|
+
params = add_cursor(params, cursor)
|
|
44
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
jsonrpc: JSONRPC_VERSION,
|
|
48
|
+
id: generate_id,
|
|
49
|
+
method: METHOD_TOOLS_LIST,
|
|
50
|
+
params: params
|
|
51
|
+
}.tap { |body| body.delete(:params) if params.empty? }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tool_call(name:, parameters: {}, tracking_progress: false)
|
|
55
|
+
params = {
|
|
56
|
+
name: name,
|
|
57
|
+
arguments: parameters
|
|
58
|
+
}
|
|
59
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
jsonrpc: JSONRPC_VERSION,
|
|
63
|
+
id: generate_id,
|
|
64
|
+
method: METHOD_TOOLS_CALL,
|
|
65
|
+
params: params
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resource_list(cursor: nil, tracking_progress: false)
|
|
70
|
+
params = {}
|
|
71
|
+
params = add_cursor(params, cursor)
|
|
72
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
jsonrpc: JSONRPC_VERSION,
|
|
76
|
+
id: generate_id,
|
|
77
|
+
method: METHOD_RESOURCES_LIST,
|
|
78
|
+
params: params
|
|
79
|
+
}.tap { |body| body.delete(:params) if params.empty? }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resource_read(uri:, tracking_progress: false)
|
|
83
|
+
params = { uri: uri }
|
|
84
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
jsonrpc: JSONRPC_VERSION,
|
|
88
|
+
id: generate_id,
|
|
89
|
+
method: METHOD_RESOURCES_READ,
|
|
90
|
+
params: params
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resource_template_list(cursor: nil, tracking_progress: false)
|
|
95
|
+
params = {}
|
|
96
|
+
params = add_cursor(params, cursor)
|
|
97
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
jsonrpc: JSONRPC_VERSION,
|
|
101
|
+
id: generate_id,
|
|
102
|
+
method: METHOD_RESOURCES_TEMPLATES_LIST,
|
|
103
|
+
params: params
|
|
104
|
+
}.tap { |body| body.delete(:params) if params.empty? }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def resources_subscribe(uri:, tracking_progress: false)
|
|
108
|
+
params = { uri: uri }
|
|
109
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
jsonrpc: JSONRPC_VERSION,
|
|
113
|
+
id: generate_id,
|
|
114
|
+
method: METHOD_RESOURCES_SUBSCRIBE,
|
|
115
|
+
params: params
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def prompt_list(cursor: nil, tracking_progress: false)
|
|
120
|
+
params = {}
|
|
121
|
+
params = add_cursor(params, cursor)
|
|
122
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
jsonrpc: JSONRPC_VERSION,
|
|
126
|
+
id: generate_id,
|
|
127
|
+
method: METHOD_PROMPTS_LIST,
|
|
128
|
+
params: params
|
|
129
|
+
}.tap { |body| body.delete(:params) if params.empty? }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def prompt_call(name:, arguments: {}, tracking_progress: false)
|
|
133
|
+
params = {
|
|
134
|
+
name: name,
|
|
135
|
+
arguments: arguments
|
|
136
|
+
}
|
|
137
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
jsonrpc: JSONRPC_VERSION,
|
|
141
|
+
id: generate_id,
|
|
142
|
+
method: METHOD_PROMPTS_GET,
|
|
143
|
+
params: params
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def completion_resource(uri:, argument:, value:, context: nil, tracking_progress: false)
|
|
148
|
+
params = {
|
|
149
|
+
ref: {
|
|
150
|
+
type: REF_TYPE_RESOURCE,
|
|
151
|
+
uri: uri
|
|
152
|
+
},
|
|
153
|
+
argument: {
|
|
154
|
+
name: argument,
|
|
155
|
+
value: value
|
|
156
|
+
},
|
|
157
|
+
context: format_completion_context(context)
|
|
158
|
+
}.compact
|
|
159
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
jsonrpc: JSONRPC_VERSION,
|
|
163
|
+
id: generate_id,
|
|
164
|
+
method: METHOD_COMPLETION_COMPLETE,
|
|
165
|
+
params: params
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def completion_prompt(name:, argument:, value:, context: nil, tracking_progress: false)
|
|
170
|
+
params = {
|
|
171
|
+
ref: {
|
|
172
|
+
type: REF_TYPE_PROMPT,
|
|
173
|
+
name: name
|
|
174
|
+
},
|
|
175
|
+
argument: {
|
|
176
|
+
name: argument,
|
|
177
|
+
value: value
|
|
178
|
+
},
|
|
179
|
+
context: format_completion_context(context)
|
|
180
|
+
}.compact
|
|
181
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
182
|
+
|
|
183
|
+
{
|
|
184
|
+
jsonrpc: JSONRPC_VERSION,
|
|
185
|
+
id: generate_id,
|
|
186
|
+
method: METHOD_COMPLETION_COMPLETE,
|
|
187
|
+
params: params
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def logging_set_level(level:, tracking_progress: false)
|
|
192
|
+
params = { level: level }
|
|
193
|
+
params = add_progress_token(params, tracking_progress: tracking_progress)
|
|
194
|
+
|
|
195
|
+
{
|
|
196
|
+
jsonrpc: JSONRPC_VERSION,
|
|
197
|
+
id: generate_id,
|
|
198
|
+
method: METHOD_LOGGING_SET_LEVEL,
|
|
199
|
+
params: params
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Messages
|
|
7
|
+
# Response message builders
|
|
8
|
+
# Responses are sent in reply to requests from the server
|
|
9
|
+
module Responses
|
|
10
|
+
extend Helpers
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def ping(id:)
|
|
15
|
+
{
|
|
16
|
+
jsonrpc: JSONRPC_VERSION,
|
|
17
|
+
id: id,
|
|
18
|
+
result: {}
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def roots_list(id:, roots_paths:)
|
|
23
|
+
roots_response = roots_paths.map do |path|
|
|
24
|
+
{
|
|
25
|
+
uri: "file://#{path}",
|
|
26
|
+
name: File.basename(path, ".*")
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
jsonrpc: JSONRPC_VERSION,
|
|
32
|
+
id: id,
|
|
33
|
+
result: {
|
|
34
|
+
roots: roots_response
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sampling_create_message(id:, message:, model:)
|
|
40
|
+
stop_reason = if message.respond_to?(:stop_reason) && message.stop_reason
|
|
41
|
+
snake_to_camel(message.stop_reason)
|
|
42
|
+
else
|
|
43
|
+
"endTurn"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
jsonrpc: JSONRPC_VERSION,
|
|
48
|
+
id: id,
|
|
49
|
+
result: {
|
|
50
|
+
role: message.role,
|
|
51
|
+
content: format_content(message.content),
|
|
52
|
+
model: model,
|
|
53
|
+
stopReason: stop_reason
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def elicitation(id:, action:, content: nil)
|
|
59
|
+
{
|
|
60
|
+
jsonrpc: JSONRPC_VERSION,
|
|
61
|
+
id: id,
|
|
62
|
+
result: {
|
|
63
|
+
action: action,
|
|
64
|
+
content: content
|
|
65
|
+
}.compact
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def error(id:, message:, code: JsonRpc::ErrorCodes::SERVER_ERROR, data: nil)
|
|
70
|
+
error_object = {
|
|
71
|
+
code: code,
|
|
72
|
+
message: message
|
|
73
|
+
}
|
|
74
|
+
error_object[:data] = data if data
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
jsonrpc: JSONRPC_VERSION,
|
|
78
|
+
id: id,
|
|
79
|
+
error: error_object
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_content(content)
|
|
84
|
+
if content.is_a?(RubyLLM::Content)
|
|
85
|
+
if content.text.none? && content.attachments.any?
|
|
86
|
+
attachment = content.attachments.first
|
|
87
|
+
{ type: attachment.type, data: attachment.content, mime_type: attachment.mime_type }
|
|
88
|
+
else
|
|
89
|
+
{ type: "text", text: content.text }
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
{ type: "text", text: content }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
private_class_method :format_content
|
|
96
|
+
|
|
97
|
+
def snake_to_camel(str)
|
|
98
|
+
parts = str.split("_")
|
|
99
|
+
parts.first + parts[1..].map(&:capitalize).join
|
|
100
|
+
end
|
|
101
|
+
private_class_method :snake_to_camel
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
# Centralized message builders for MCP JSON-RPC communication
|
|
7
|
+
# All message construction happens here, returning properly formatted bodies
|
|
8
|
+
module Messages
|
|
9
|
+
JSONRPC_VERSION = JsonRpc::VERSION
|
|
10
|
+
|
|
11
|
+
# Request methods
|
|
12
|
+
METHOD_INITIALIZE = "initialize"
|
|
13
|
+
METHOD_PING = "ping"
|
|
14
|
+
METHOD_TOOLS_LIST = "tools/list"
|
|
15
|
+
METHOD_TOOLS_CALL = "tools/call"
|
|
16
|
+
METHOD_RESOURCES_LIST = "resources/list"
|
|
17
|
+
METHOD_RESOURCES_READ = "resources/read"
|
|
18
|
+
METHOD_RESOURCES_TEMPLATES_LIST = "resources/templates/list"
|
|
19
|
+
METHOD_RESOURCES_SUBSCRIBE = "resources/subscribe"
|
|
20
|
+
METHOD_PROMPTS_LIST = "prompts/list"
|
|
21
|
+
METHOD_PROMPTS_GET = "prompts/get"
|
|
22
|
+
METHOD_COMPLETION_COMPLETE = "completion/complete"
|
|
23
|
+
METHOD_LOGGING_SET_LEVEL = "logging/setLevel"
|
|
24
|
+
|
|
25
|
+
# Notification methods
|
|
26
|
+
METHOD_NOTIFICATION_INITIALIZED = "notifications/initialized"
|
|
27
|
+
METHOD_NOTIFICATION_CANCELLED = "notifications/cancelled"
|
|
28
|
+
METHOD_NOTIFICATION_ROOTS_LIST_CHANGED = "notifications/roots/list_changed"
|
|
29
|
+
|
|
30
|
+
# Reference types
|
|
31
|
+
REF_TYPE_PROMPT = "ref/prompt"
|
|
32
|
+
REF_TYPE_RESOURCE = "ref/resource"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
class Notification
|
|
7
|
+
attr_reader :type, :params
|
|
8
|
+
|
|
9
|
+
def initialize(response)
|
|
10
|
+
@type = response["method"]
|
|
11
|
+
@params = response["params"]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Protocol
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
LATEST_PROTOCOL_VERSION = "2025-06-18"
|
|
10
|
+
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"
|
|
11
|
+
SUPPORTED_PROTOCOL_VERSIONS = [
|
|
12
|
+
LATEST_PROTOCOL_VERSION,
|
|
13
|
+
"2025-03-26",
|
|
14
|
+
"2024-11-05",
|
|
15
|
+
"2024-10-07"
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def supported_version?(version)
|
|
19
|
+
SUPPORTED_PROTOCOL_VERSIONS.include?(version)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def supported_versions
|
|
23
|
+
SUPPORTED_PROTOCOL_VERSIONS
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def latest_version
|
|
27
|
+
LATEST_PROTOCOL_VERSION
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def default_negotiated_version
|
|
31
|
+
DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|