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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +277 -0
  4. data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
  5. data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
  6. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  19. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  20. data/lib/ruby_llm/chat.rb +34 -0
  21. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
  25. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
  26. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
  27. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
  28. data/lib/ruby_llm/mcp/attachment.rb +18 -0
  29. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  30. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
  31. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  32. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  33. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  34. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
  35. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  36. data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
  37. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
  38. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
  39. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  40. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  41. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  42. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  43. data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
  44. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
  45. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  46. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  47. data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
  48. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  49. data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
  50. data/lib/ruby_llm/mcp/auth.rb +359 -0
  51. data/lib/ruby_llm/mcp/client.rb +401 -0
  52. data/lib/ruby_llm/mcp/completion.rb +16 -0
  53. data/lib/ruby_llm/mcp/configuration.rb +310 -0
  54. data/lib/ruby_llm/mcp/content.rb +28 -0
  55. data/lib/ruby_llm/mcp/elicitation.rb +48 -0
  56. data/lib/ruby_llm/mcp/error.rb +34 -0
  57. data/lib/ruby_llm/mcp/errors.rb +91 -0
  58. data/lib/ruby_llm/mcp/logging.rb +16 -0
  59. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
  60. data/lib/ruby_llm/mcp/native/client.rb +387 -0
  61. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  62. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  63. data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
  64. data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
  65. data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
  66. data/lib/ruby_llm/mcp/native/messages.rb +36 -0
  67. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  68. data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
  69. data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
  70. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  71. data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
  72. data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
  73. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
  74. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  75. data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
  76. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  77. data/lib/ruby_llm/mcp/native.rb +12 -0
  78. data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
  79. data/lib/ruby_llm/mcp/progress.rb +35 -0
  80. data/lib/ruby_llm/mcp/prompt.rb +132 -0
  81. data/lib/ruby_llm/mcp/railtie.rb +14 -0
  82. data/lib/ruby_llm/mcp/resource.rb +112 -0
  83. data/lib/ruby_llm/mcp/resource_template.rb +85 -0
  84. data/lib/ruby_llm/mcp/result.rb +108 -0
  85. data/lib/ruby_llm/mcp/roots.rb +45 -0
  86. data/lib/ruby_llm/mcp/sample.rb +152 -0
  87. data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
  88. data/lib/ruby_llm/mcp/tool.rb +228 -0
  89. data/lib/ruby_llm/mcp/version.rb +7 -0
  90. data/lib/ruby_llm/mcp.rb +125 -0
  91. data/lib/tasks/release.rake +23 -0
  92. 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