ruby_llm-mcp 0.2.1 → 0.3.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 +112 -22
- data/lib/ruby_llm/chat.rb +8 -2
- data/lib/ruby_llm/mcp/capabilities.rb +3 -3
- data/lib/ruby_llm/mcp/client.rb +57 -99
- data/lib/ruby_llm/mcp/coordinator.rb +112 -0
- data/lib/ruby_llm/mcp/errors.rb +5 -3
- data/lib/ruby_llm/mcp/parameter.rb +12 -1
- data/lib/ruby_llm/mcp/prompt.rb +25 -14
- data/lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb +26 -6
- data/lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb +62 -0
- data/lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb +20 -5
- data/lib/ruby_llm/mcp/requests/{completion.rb → completion_prompt.rb} +3 -13
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +40 -0
- data/lib/ruby_llm/mcp/requests/{notification.rb → initialize_notification.rb} +1 -1
- data/lib/ruby_llm/mcp/resource.rb +24 -46
- data/lib/ruby_llm/mcp/resource_template.rb +79 -0
- data/lib/ruby_llm/mcp/tool.rb +67 -21
- data/lib/ruby_llm/mcp/transport/sse.rb +12 -7
- data/lib/ruby_llm/mcp/transport/stdio.rb +51 -14
- data/lib/ruby_llm/mcp/transport/streamable.rb +11 -4
- data/lib/ruby_llm/mcp/version.rb +1 -1
- 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: 53435b09aee8d32aaede3da3463b8fb782a6d00f6abcabc3b9925f1301cd60c9
|
4
|
+
data.tar.gz: 366cf4d89bef7f79904cd897a7db006ee21a8766c8ff1675e51225576e8c87f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94754a9b6d8799252bebfb19ccc3dedfc6767fdbe07468960ed3eb56978954d734425f79b366fb92610f4c059970a0607c249b3c6c35bca943fbab7e5a721e77
|
7
|
+
data.tar.gz: d0e757cdc7d064ffc4334b0b1c2a1ebc1feff3dc684d35d28f0b51ce5761a3c80ad427654767fae97da8c8ddacbc182d3d896ef4dedc45ce4d6d8e6297b81fe3
|
data/README.md
CHANGED
@@ -12,13 +12,16 @@ This project is a Ruby client for the [Model Context Protocol (MCP)](https://mod
|
|
12
12
|
- 🛠️ **Tool Integration**: Automatically converts MCP tools into RubyLLM-compatible tools
|
13
13
|
- 📄 **Resource Management**: Access and include MCP resources (files, data) and resource templates in conversations
|
14
14
|
- 🎯 **Prompt Integration**: Use predefined MCP prompts with arguments for consistent interactions
|
15
|
-
- 🔄 **Real-time Communication**: Efficient bidirectional communication with MCP servers
|
16
15
|
- 🎨 **Enhanced Chat Interface**: Extended RubyLLM chat methods for seamless MCP integration
|
17
16
|
- 📚 **Simple API**: Easy-to-use interface that integrates seamlessly with RubyLLM
|
18
17
|
|
19
18
|
## Installation
|
20
19
|
|
21
|
-
|
20
|
+
```bash
|
21
|
+
bundle add ruby_llm-mcp
|
22
|
+
```
|
23
|
+
|
24
|
+
or add this line to your application's Gemfile:
|
22
25
|
|
23
26
|
```ruby
|
24
27
|
gem 'ruby_llm-mcp'
|
@@ -152,12 +155,12 @@ MCP servers can provide access to resources - structured data that can be includ
|
|
152
155
|
# Get available resources from the MCP server
|
153
156
|
resources = client.resources
|
154
157
|
puts "Available resources:"
|
155
|
-
resources.each do |
|
156
|
-
puts "- #{name}: #{resource.description}"
|
158
|
+
resources.each do |resource|
|
159
|
+
puts "- #{resource.name}: #{resource.description}"
|
157
160
|
end
|
158
161
|
|
159
|
-
# Access a specific resource
|
160
|
-
file_resource =
|
162
|
+
# Access a specific resource by name
|
163
|
+
file_resource = client.resource("project_readme")
|
161
164
|
content = file_resource.content
|
162
165
|
puts "Resource content: #{content}"
|
163
166
|
|
@@ -179,11 +182,11 @@ Resource templates are parameterized resources that can be dynamically configure
|
|
179
182
|
```ruby
|
180
183
|
# Get available resource templates
|
181
184
|
templates = client.resource_templates
|
182
|
-
log_template =
|
185
|
+
log_template = client.resource_template("application_logs")
|
183
186
|
|
184
187
|
# Use a template with parameters
|
185
188
|
chat = RubyLLM.chat(model: "gpt-4")
|
186
|
-
chat.
|
189
|
+
chat.with_resource_template(log_template, arguments: {
|
187
190
|
date: "2024-01-15",
|
188
191
|
level: "error"
|
189
192
|
})
|
@@ -192,7 +195,7 @@ response = chat.ask("What errors occurred on this date?")
|
|
192
195
|
puts response
|
193
196
|
|
194
197
|
# You can also get templated content directly
|
195
|
-
content = log_template.
|
198
|
+
content = log_template.to_content(arguments: {
|
196
199
|
date: "2024-01-15",
|
197
200
|
level: "error"
|
198
201
|
})
|
@@ -204,12 +207,12 @@ puts content
|
|
204
207
|
For resource templates, you can get suggested values for arguments:
|
205
208
|
|
206
209
|
```ruby
|
207
|
-
template = client.
|
210
|
+
template = client.resource_template("user_profile")
|
208
211
|
|
209
212
|
# Search for possible values for a specific argument
|
210
|
-
suggestions = template.
|
213
|
+
suggestions = template.complete("username", "john")
|
211
214
|
puts "Suggested usernames:"
|
212
|
-
suggestions.
|
215
|
+
suggestions.values.each do |value|
|
213
216
|
puts "- #{value}"
|
214
217
|
end
|
215
218
|
puts "Total matches: #{suggestions.total}"
|
@@ -224,15 +227,15 @@ MCP servers can provide predefined prompts that can be used in conversations:
|
|
224
227
|
# Get available prompts from the MCP server
|
225
228
|
prompts = client.prompts
|
226
229
|
puts "Available prompts:"
|
227
|
-
prompts.each do |
|
228
|
-
puts "- #{name}: #{prompt.description}"
|
230
|
+
prompts.each do |prompt|
|
231
|
+
puts "- #{prompt.name}: #{prompt.description}"
|
229
232
|
prompt.arguments.each do |arg|
|
230
233
|
puts " - #{arg.name}: #{arg.description} (required: #{arg.required})"
|
231
234
|
end
|
232
235
|
end
|
233
236
|
|
234
237
|
# Use a prompt in a conversation
|
235
|
-
greeting_prompt =
|
238
|
+
greeting_prompt = client.prompt("daily_greeting")
|
236
239
|
chat = RubyLLM.chat(model: "gpt-4")
|
237
240
|
|
238
241
|
# Method 1: Ask prompt directly
|
@@ -261,15 +264,15 @@ chat = RubyLLM.chat(model: "gpt-4")
|
|
261
264
|
chat.with_tools(*client.tools)
|
262
265
|
|
263
266
|
# Add resources for context
|
264
|
-
chat.with_resource(client.
|
267
|
+
chat.with_resource(client.resource("project_structure"))
|
265
268
|
chat.with_resource(
|
266
|
-
client.
|
269
|
+
client.resource_template("recent_commits"),
|
267
270
|
arguments: { days: 7 }
|
268
271
|
)
|
269
272
|
|
270
273
|
# Add prompts for guidance
|
271
274
|
chat.with_prompt(
|
272
|
-
client.
|
275
|
+
client.prompt("code_review_checklist"),
|
273
276
|
arguments: { focus: "security" }
|
274
277
|
)
|
275
278
|
|
@@ -278,6 +281,88 @@ response = chat.ask("Please review the recent commits using the checklist and su
|
|
278
281
|
puts response
|
279
282
|
```
|
280
283
|
|
284
|
+
## Argument Completion
|
285
|
+
|
286
|
+
Some MCP servers support argument completion for prompts and resource templates:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
# For prompts
|
290
|
+
prompt = client.prompt("user_search")
|
291
|
+
suggestions = prompt.complete("username", "jo")
|
292
|
+
puts "Suggestions: #{suggestions.values}" # ["john", "joanna", "joseph"]
|
293
|
+
|
294
|
+
# For resource templates
|
295
|
+
template = client.resource_template("user_logs")
|
296
|
+
suggestions = template.complete("user_id", "123")
|
297
|
+
puts "Total matches: #{suggestions.total}"
|
298
|
+
puts "Has more results: #{suggestions.has_more}"
|
299
|
+
```
|
300
|
+
|
301
|
+
## Additional Chat Methods
|
302
|
+
|
303
|
+
The gem extends RubyLLM's chat interface with convenient methods for MCP integration:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
chat = RubyLLM.chat(model: "gpt-4")
|
307
|
+
|
308
|
+
# Add a single resource
|
309
|
+
chat.with_resource(resource)
|
310
|
+
|
311
|
+
# Add multiple resources
|
312
|
+
chat.with_resources(resource1, resource2, resource3)
|
313
|
+
|
314
|
+
# Add a resource template with arguments
|
315
|
+
chat.with_resource_template(resource_template, arguments: { key: "value" })
|
316
|
+
|
317
|
+
# Add a prompt with arguments
|
318
|
+
chat.with_prompt(prompt, arguments: { name: "Alice" })
|
319
|
+
|
320
|
+
# Ask using a prompt directly
|
321
|
+
response = chat.ask_prompt(prompt, arguments: { name: "Alice" })
|
322
|
+
```
|
323
|
+
|
324
|
+
## Client Lifecycle Management
|
325
|
+
|
326
|
+
You can manage the MCP client connection lifecycle:
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
client = RubyLLM::MCP.client(name: "my-server", transport_type: :stdio, start: false, config: {...})
|
330
|
+
|
331
|
+
# Manually start the connection
|
332
|
+
client.start
|
333
|
+
|
334
|
+
# Check if connection is alive
|
335
|
+
puts client.alive?
|
336
|
+
|
337
|
+
# Restart the connection
|
338
|
+
client.restart!
|
339
|
+
|
340
|
+
# Stop the connection
|
341
|
+
client.stop
|
342
|
+
```
|
343
|
+
|
344
|
+
## Refreshing Cached Data
|
345
|
+
|
346
|
+
The client caches tools, resources, prompts, and resource templates list calls are cached to reduce round trips back to the MCP server. You can refresh this cache:
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
# Refresh all cached tools
|
350
|
+
tools = client.tools(refresh: true)
|
351
|
+
|
352
|
+
# Refresh a specific tool
|
353
|
+
tool = client.tool("search_files", refresh: true)
|
354
|
+
|
355
|
+
# Same pattern works for resources, prompts, and resource templates
|
356
|
+
resources = client.resources(refresh: true)
|
357
|
+
prompts = client.prompts(refresh: true)
|
358
|
+
templates = client.resource_templates(refresh: true)
|
359
|
+
|
360
|
+
# Or refresh specific items
|
361
|
+
resource = client.resource("project_readme", refresh: true)
|
362
|
+
prompt = client.prompt("daily_greeting", refresh: true)
|
363
|
+
template = client.resource_template("user_logs", refresh: true)
|
364
|
+
```
|
365
|
+
|
281
366
|
## Transport Types
|
282
367
|
|
283
368
|
### SSE (Server-Sent Events)
|
@@ -289,7 +374,8 @@ client = RubyLLM::MCP.client(
|
|
289
374
|
name: "web-mcp-server",
|
290
375
|
transport_type: :sse,
|
291
376
|
config: {
|
292
|
-
url: "https://your-mcp-server.com/mcp/sse"
|
377
|
+
url: "https://your-mcp-server.com/mcp/sse",
|
378
|
+
headers: { "Authorization" => "Bearer your-token" }
|
293
379
|
}
|
294
380
|
)
|
295
381
|
```
|
@@ -329,9 +415,10 @@ client = RubyLLM::MCP.client(
|
|
329
415
|
|
330
416
|
- `name`: A unique identifier for your MCP client
|
331
417
|
- `transport_type`: Either `:sse`, `:streamable`, or `:stdio`
|
418
|
+
- `start`: Whether to automatically start the connection (default: true)
|
332
419
|
- `request_timeout`: Timeout for requests in milliseconds (default: 8000)
|
333
420
|
- `config`: Transport-specific configuration
|
334
|
-
- For SSE: `{ url: "http://..." }`
|
421
|
+
- For SSE: `{ url: "http://...", headers: {...} }`
|
335
422
|
- For Streamable: `{ url: "http://...", headers: {...} }`
|
336
423
|
- For stdio: `{ command: "...", args: [...], env: {...} }`
|
337
424
|
|
@@ -339,13 +426,16 @@ client = RubyLLM::MCP.client(
|
|
339
426
|
|
340
427
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
341
428
|
|
342
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
429
|
+
To install this gem onto your local machine, run `bundle exec rake install`. Run `bundle exec rake` to test specs and run linters. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
343
430
|
|
344
431
|
## Examples
|
345
432
|
|
346
433
|
Check out the `examples/` directory for more detailed usage examples:
|
347
434
|
|
348
|
-
- `examples/
|
435
|
+
- `examples/tools/local_mcp.rb` - Complete example with stdio transport
|
436
|
+
- `examples/tools/sse_mcp_with_gpt.rb` - Example using SSE transport with GPT
|
437
|
+
- `examples/resources/list_resources.rb` - Example of listing and using resources
|
438
|
+
- `examples/prompts/streamable_prompt_call.rb` - Example of using prompts with streamable transport
|
349
439
|
|
350
440
|
## Contributing
|
351
441
|
|
data/lib/ruby_llm/chat.rb
CHANGED
@@ -10,8 +10,14 @@ module RubyLLM
|
|
10
10
|
self
|
11
11
|
end
|
12
12
|
|
13
|
-
def with_resource(resource
|
14
|
-
resource.include(self
|
13
|
+
def with_resource(resource)
|
14
|
+
resource.include(self)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_resource_template(resource_template, arguments: {})
|
19
|
+
resource = resource_template.fetch_resource(arguments: arguments)
|
20
|
+
resource.include(self)
|
15
21
|
self
|
16
22
|
end
|
17
23
|
|
@@ -3,9 +3,9 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module MCP
|
5
5
|
class Capabilities
|
6
|
-
|
6
|
+
attr_accessor :capabilities
|
7
7
|
|
8
|
-
def initialize(capabilities)
|
8
|
+
def initialize(capabilities = {})
|
9
9
|
@capabilities = capabilities
|
10
10
|
end
|
11
11
|
|
@@ -22,7 +22,7 @@ module RubyLLM
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def completion?
|
25
|
-
|
25
|
+
!@capabilities["completions"].nil?
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
data/lib/ruby_llm/mcp/client.rb
CHANGED
@@ -1,145 +1,103 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "forwardable"
|
4
|
+
|
3
5
|
module RubyLLM
|
4
6
|
module MCP
|
5
7
|
class Client
|
6
|
-
|
7
|
-
PV_2024_11_05 = "2024-11-05"
|
8
|
+
extend Forwardable
|
8
9
|
|
9
|
-
attr_reader :name, :config, :transport_type, :
|
10
|
-
:capabilities
|
10
|
+
attr_reader :name, :config, :transport_type, :request_timeout
|
11
11
|
|
12
|
-
def initialize(name:, transport_type:,
|
12
|
+
def initialize(name:, transport_type:, start: true, request_timeout: 8000, config: {})
|
13
13
|
@name = name
|
14
|
-
@config = config
|
15
|
-
@protocol_version = PROTOCOL_VERSION
|
16
|
-
@headers = config[:headers] || {}
|
17
|
-
|
14
|
+
@config = config.merge(request_timeout: request_timeout)
|
18
15
|
@transport_type = transport_type.to_sym
|
19
|
-
|
20
|
-
case @transport_type
|
21
|
-
when :sse
|
22
|
-
@transport = RubyLLM::MCP::Transport::SSE.new(@config[:url], headers: @headers)
|
23
|
-
when :stdio
|
24
|
-
@transport = RubyLLM::MCP::Transport::Stdio.new(@config[:command], args: @config[:args], env: @config[:env])
|
25
|
-
when :streamable
|
26
|
-
@transport = RubyLLM::MCP::Transport::Streamable.new(@config[:url], headers: @headers)
|
27
|
-
else
|
28
|
-
raise "Invalid transport type: #{transport_type}"
|
29
|
-
end
|
30
|
-
@capabilities = nil
|
31
|
-
|
32
16
|
@request_timeout = request_timeout
|
33
|
-
@reverse_proxy_url = reverse_proxy_url
|
34
17
|
|
35
|
-
|
36
|
-
notification_request
|
37
|
-
end
|
18
|
+
@coordinator = Coordinator.new(self, transport_type: @transport_type, config: @config)
|
38
19
|
|
39
|
-
|
40
|
-
@transport.request(body, **options)
|
20
|
+
start_transport if start
|
41
21
|
end
|
42
22
|
|
43
|
-
|
44
|
-
@tools = nil if refresh
|
45
|
-
@tools ||= fetch_and_create_tools
|
46
|
-
end
|
23
|
+
def_delegators :@coordinator, :start_transport, :stop_transport, :restart_transport, :alive?, :capabilities
|
47
24
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
25
|
+
alias start start_transport
|
26
|
+
alias stop stop_transport
|
27
|
+
alias restart! restart_transport
|
52
28
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
29
|
+
def tools(refresh: false)
|
30
|
+
fetch(:tools, refresh) do
|
31
|
+
tools_data = @coordinator.tool_list.dig("result", "tools")
|
32
|
+
build_map(tools_data, MCP::Tool)
|
33
|
+
end
|
57
34
|
|
58
|
-
|
59
|
-
@prompts = nil if refresh
|
60
|
-
@prompts ||= fetch_and_create_prompts
|
35
|
+
@tools.values
|
61
36
|
end
|
62
37
|
|
63
|
-
def
|
64
|
-
|
65
|
-
end
|
38
|
+
def tool(name, refresh: false)
|
39
|
+
tools(refresh: refresh)
|
66
40
|
|
67
|
-
|
68
|
-
RubyLLM::MCP::Requests::ResourceRead.new(self, **args).call
|
41
|
+
@tools[name]
|
69
42
|
end
|
70
43
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
44
|
+
def resources(refresh: false)
|
45
|
+
fetch(:resources, refresh) do
|
46
|
+
resources_data = @coordinator.resource_list.dig("result", "resources")
|
47
|
+
build_map(resources_data, MCP::Resource)
|
48
|
+
end
|
74
49
|
|
75
|
-
|
76
|
-
RubyLLM::MCP::Requests::PromptCall.new(self, **args).call
|
50
|
+
@resources.values
|
77
51
|
end
|
78
52
|
|
79
|
-
|
80
|
-
|
81
|
-
def initialize_request
|
82
|
-
@initialize_response = RubyLLM::MCP::Requests::Initialization.new(self).call
|
83
|
-
@capabilities = RubyLLM::MCP::Capabilities.new(@initialize_response["result"]["capabilities"])
|
84
|
-
end
|
53
|
+
def resource(name, refresh: false)
|
54
|
+
resources(refresh: refresh)
|
85
55
|
|
86
|
-
|
87
|
-
RubyLLM::MCP::Requests::Notification.new(self).call
|
56
|
+
@resources[name]
|
88
57
|
end
|
89
58
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
59
|
+
def resource_templates(refresh: false)
|
60
|
+
fetch(:resource_templates, refresh) do
|
61
|
+
templates_data = @coordinator.resource_template_list.dig("result", "resourceTemplates")
|
62
|
+
build_map(templates_data, MCP::ResourceTemplate)
|
63
|
+
end
|
93
64
|
|
94
|
-
|
95
|
-
RubyLLM::MCP::Requests::ResourceList.new(self).call
|
65
|
+
@resource_templates.values
|
96
66
|
end
|
97
67
|
|
98
|
-
def
|
99
|
-
|
100
|
-
end
|
68
|
+
def resource_template(name, refresh: false)
|
69
|
+
resource_templates(refresh: refresh)
|
101
70
|
|
102
|
-
|
103
|
-
RubyLLM::MCP::Requests::PromptList.new(self).call
|
71
|
+
@resource_templates[name]
|
104
72
|
end
|
105
73
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
@tools = tools_response.map do |tool|
|
111
|
-
RubyLLM::MCP::Tool.new(self, tool)
|
74
|
+
def prompts(refresh: false)
|
75
|
+
fetch(:prompts, refresh) do
|
76
|
+
prompts_data = @coordinator.prompt_list.dig("result", "prompts")
|
77
|
+
build_map(prompts_data, MCP::Prompt)
|
112
78
|
end
|
113
|
-
end
|
114
79
|
|
115
|
-
|
116
|
-
|
117
|
-
resources_response = resources_response["result"]["resources"]
|
80
|
+
@prompts.values
|
81
|
+
end
|
118
82
|
|
119
|
-
|
120
|
-
|
121
|
-
new_resource = RubyLLM::MCP::Resource.new(self, resource, template: set_as_template)
|
122
|
-
resources[new_resource.name] = new_resource
|
123
|
-
end
|
83
|
+
def prompt(name, refresh: false)
|
84
|
+
prompts(refresh: refresh)
|
124
85
|
|
125
|
-
|
86
|
+
@prompts[name]
|
126
87
|
end
|
127
88
|
|
128
|
-
|
129
|
-
prompts_response = prompt_list_request
|
130
|
-
prompts_response = prompts_response["result"]["prompts"]
|
89
|
+
private
|
131
90
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
description: prompt["description"],
|
137
|
-
arguments: prompt["arguments"])
|
91
|
+
def fetch(cache_key, refresh)
|
92
|
+
instance_variable_set("@#{cache_key}", nil) if refresh
|
93
|
+
instance_variable_get("@#{cache_key}") || instance_variable_set("@#{cache_key}", yield)
|
94
|
+
end
|
138
95
|
|
139
|
-
|
96
|
+
def build_map(raw_data, klass)
|
97
|
+
raw_data.each_with_object({}) do |item, acc|
|
98
|
+
instance = klass.new(@coordinator, item)
|
99
|
+
acc[instance.name] = instance
|
140
100
|
end
|
141
|
-
|
142
|
-
prompts
|
143
101
|
end
|
144
102
|
end
|
145
103
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module MCP
|
5
|
+
class Coordinator
|
6
|
+
PROTOCOL_VERSION = "2025-03-26"
|
7
|
+
PV_2024_11_05 = "2024-11-05"
|
8
|
+
|
9
|
+
attr_reader :client, :transport_type, :config, :request_timeout, :headers, :transport, :initialize_response,
|
10
|
+
:capabilities, :protocol_version
|
11
|
+
|
12
|
+
def initialize(client, transport_type:, config: {})
|
13
|
+
@client = client
|
14
|
+
@transport_type = transport_type
|
15
|
+
@config = config
|
16
|
+
|
17
|
+
@protocol_version = PROTOCOL_VERSION
|
18
|
+
@headers = config[:headers] || {}
|
19
|
+
|
20
|
+
@transport = nil
|
21
|
+
@capabilities = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def request(body, **options)
|
25
|
+
@transport.request(body, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_transport
|
29
|
+
case @transport_type
|
30
|
+
when :sse
|
31
|
+
@transport = RubyLLM::MCP::Transport::SSE.new(@config[:url],
|
32
|
+
request_timeout: @config[:request_timeout],
|
33
|
+
headers: @headers)
|
34
|
+
when :stdio
|
35
|
+
@transport = RubyLLM::MCP::Transport::Stdio.new(@config[:command],
|
36
|
+
request_timeout: @config[:request_timeout],
|
37
|
+
args: @config[:args],
|
38
|
+
env: @config[:env])
|
39
|
+
when :streamable
|
40
|
+
@transport = RubyLLM::MCP::Transport::Streamable.new(@config[:url],
|
41
|
+
request_timeout: @config[:request_timeout],
|
42
|
+
headers: @headers)
|
43
|
+
else
|
44
|
+
message = "Invalid transport type: :#{transport_type}. Supported types are :sse, :stdio, :streamable"
|
45
|
+
raise Errors::InvalidTransportType.new(message: message)
|
46
|
+
end
|
47
|
+
|
48
|
+
@initialize_response = initialize_request
|
49
|
+
@capabilities = RubyLLM::MCP::Capabilities.new(@initialize_response["result"]["capabilities"])
|
50
|
+
initialize_notification
|
51
|
+
end
|
52
|
+
|
53
|
+
def stop_transport
|
54
|
+
@transport&.close
|
55
|
+
@transport = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def restart_transport
|
59
|
+
stop_transport
|
60
|
+
start_transport
|
61
|
+
end
|
62
|
+
|
63
|
+
def alive?
|
64
|
+
!!@transport&.alive?
|
65
|
+
end
|
66
|
+
|
67
|
+
def execute_tool(**args)
|
68
|
+
RubyLLM::MCP::Requests::ToolCall.new(self, **args).call
|
69
|
+
end
|
70
|
+
|
71
|
+
def resource_read(**args)
|
72
|
+
RubyLLM::MCP::Requests::ResourceRead.new(self, **args).call
|
73
|
+
end
|
74
|
+
|
75
|
+
def completion_resource(**args)
|
76
|
+
RubyLLM::MCP::Requests::CompletionResource.new(self, **args).call
|
77
|
+
end
|
78
|
+
|
79
|
+
def completion_prompt(**args)
|
80
|
+
RubyLLM::MCP::Requests::CompletionPrompt.new(self, **args).call
|
81
|
+
end
|
82
|
+
|
83
|
+
def execute_prompt(**args)
|
84
|
+
RubyLLM::MCP::Requests::PromptCall.new(self, **args).call
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize_request
|
88
|
+
RubyLLM::MCP::Requests::Initialization.new(self).call
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize_notification
|
92
|
+
RubyLLM::MCP::Requests::InitializeNotification.new(self).call
|
93
|
+
end
|
94
|
+
|
95
|
+
def tool_list
|
96
|
+
RubyLLM::MCP::Requests::ToolList.new(self).call
|
97
|
+
end
|
98
|
+
|
99
|
+
def resource_list
|
100
|
+
RubyLLM::MCP::Requests::ResourceList.new(self).call
|
101
|
+
end
|
102
|
+
|
103
|
+
def resource_template_list
|
104
|
+
RubyLLM::MCP::Requests::ResourceTemplateList.new(self).call
|
105
|
+
end
|
106
|
+
|
107
|
+
def prompt_list
|
108
|
+
RubyLLM::MCP::Requests::PromptList.new(self).call
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/ruby_llm/mcp/errors.rb
CHANGED
@@ -12,15 +12,17 @@ module RubyLLM
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
class CompletionNotAvailable < BaseError; end
|
16
|
+
|
17
|
+
class PromptArgumentError < BaseError; end
|
18
|
+
|
15
19
|
class InvalidProtocolVersionError < BaseError; end
|
16
20
|
|
17
21
|
class SessionExpiredError < BaseError; end
|
18
22
|
|
19
23
|
class TimeoutError < BaseError; end
|
20
24
|
|
21
|
-
class
|
22
|
-
|
23
|
-
class CompletionNotAvailable < BaseError; end
|
25
|
+
class InvalidTransportType < BaseError; end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
@@ -3,7 +3,18 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module MCP
|
5
5
|
class Parameter < RubyLLM::Parameter
|
6
|
-
attr_accessor :items, :properties
|
6
|
+
attr_accessor :items, :properties, :enum, :union_type, :default
|
7
|
+
|
8
|
+
def initialize(name, type: "string", desc: nil, required: true, default: nil, union_type: nil) # rubocop:disable Metrics/ParameterLists
|
9
|
+
super(name, type: type.to_sym, desc: desc, required: required)
|
10
|
+
@properties = {}
|
11
|
+
@union_type = union_type
|
12
|
+
@default = default
|
13
|
+
end
|
14
|
+
|
15
|
+
def item_type
|
16
|
+
@items["type"].to_sym
|
17
|
+
end
|
7
18
|
end
|
8
19
|
end
|
9
20
|
end
|