Skip to content

Commit b4e1ac7

Browse files
authored
Merge pull request #19 from patvice/coordinator-refactor
Coordinator refactor
2 parents 5f69499 + 1101d19 commit b4e1ac7

File tree

9 files changed

+187
-194
lines changed

9 files changed

+187
-194
lines changed

lib/ruby_llm/mcp/client.rb

Lines changed: 43 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,223 +1,103 @@
11
# frozen_string_literal: true
22

3+
require "forwardable"
4+
35
module RubyLLM
46
module MCP
57
class Client
6-
PROTOCOL_VERSION = "2025-03-26"
7-
PV_2024_11_05 = "2024-11-05"
8+
extend Forwardable
89

9-
attr_reader :name, :config, :transport_type, :transport, :request_timeout, :reverse_proxy_url, :protocol_version,
10-
:capabilities
10+
attr_reader :name, :config, :transport_type, :request_timeout
1111

12-
def initialize(name:, transport_type:, start: true, request_timeout: 8000, reverse_proxy_url: nil, config: {}) # rubocop:disable Metrics/ParameterLists
12+
def initialize(name:, transport_type:, start: true, request_timeout: 8000, config: {})
1313
@name = name
14-
@config = config
15-
@protocol_version = PROTOCOL_VERSION
16-
@headers = config[:headers] || {}
17-
14+
@config = config.merge(request_timeout: request_timeout)
1815
@transport_type = transport_type.to_sym
19-
@transport = nil
20-
21-
@capabilities = nil
22-
2316
@request_timeout = request_timeout
24-
@reverse_proxy_url = reverse_proxy_url
2517

26-
if start
27-
self.start
28-
end
29-
end
18+
@coordinator = Coordinator.new(self, transport_type: @transport_type, config: @config)
3019

31-
def request(body, **options)
32-
@transport.request(body, **options)
20+
start_transport if start
3321
end
3422

35-
def start
36-
case @transport_type
37-
when :sse
38-
@transport = RubyLLM::MCP::Transport::SSE.new(@config[:url], request_timeout: @request_timeout,
39-
headers: @headers)
40-
when :stdio
41-
@transport = RubyLLM::MCP::Transport::Stdio.new(@config[:command], request_timeout: @request_timeout,
42-
args: @config[:args], env: @config[:env])
43-
when :streamable
44-
@transport = RubyLLM::MCP::Transport::Streamable.new(@config[:url], request_timeout: @request_timeout,
45-
headers: @headers)
46-
else
47-
raise "Invalid transport type: #{transport_type}"
48-
end
23+
def_delegators :@coordinator, :start_transport, :stop_transport, :restart_transport, :alive?, :capabilities
4924

50-
@initialize_response = initialize_request
51-
@capabilities = RubyLLM::MCP::Capabilities.new(@initialize_response["result"]["capabilities"])
52-
notification_request
53-
end
54-
55-
def stop
56-
@transport&.close
57-
@transport = nil
58-
end
59-
60-
def restart!
61-
stop
62-
start
63-
end
64-
65-
def alive?
66-
!!@transport&.alive?
67-
end
25+
alias start start_transport
26+
alias stop stop_transport
27+
alias restart! restart_transport
6828

6929
def tools(refresh: false)
70-
@tools = nil if refresh
71-
@tools ||= fetch_and_create_tools
30+
fetch(:tools, refresh) do
31+
tools_data = @coordinator.tool_list.dig("result", "tools")
32+
build_map(tools_data, MCP::Tool)
33+
end
34+
7235
@tools.values
7336
end
7437

7538
def tool(name, refresh: false)
76-
@tools = nil if refresh
77-
@tools ||= fetch_and_create_tools
39+
tools(refresh: refresh)
7840

7941
@tools[name]
8042
end
8143

8244
def resources(refresh: false)
83-
@resources = nil if refresh
84-
@resources ||= fetch_and_create_resources
45+
fetch(:resources, refresh) do
46+
resources_data = @coordinator.resource_list.dig("result", "resources")
47+
build_map(resources_data, MCP::Resource)
48+
end
49+
8550
@resources.values
8651
end
8752

8853
def resource(name, refresh: false)
89-
@resources = nil if refresh
90-
@resources ||= fetch_and_create_resources
54+
resources(refresh: refresh)
9155

9256
@resources[name]
9357
end
9458

9559
def resource_templates(refresh: false)
96-
@resource_templates = nil if refresh
97-
@resource_templates ||= fetch_and_create_resource_templates
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
64+
9865
@resource_templates.values
9966
end
10067

10168
def resource_template(name, refresh: false)
102-
@resource_templates = nil if refresh
103-
@resource_templates ||= fetch_and_create_resource_templates
69+
resource_templates(refresh: refresh)
10470

10571
@resource_templates[name]
10672
end
10773

10874
def prompts(refresh: false)
109-
@prompts = nil if refresh
110-
@prompts ||= fetch_and_create_prompts
75+
fetch(:prompts, refresh) do
76+
prompts_data = @coordinator.prompt_list.dig("result", "prompts")
77+
build_map(prompts_data, MCP::Prompt)
78+
end
79+
11180
@prompts.values
11281
end
11382

11483
def prompt(name, refresh: false)
115-
@prompts = nil if refresh
116-
@prompts ||= fetch_and_create_prompts
84+
prompts(refresh: refresh)
11785

11886
@prompts[name]
11987
end
12088

121-
def execute_tool(**args)
122-
RubyLLM::MCP::Requests::ToolCall.new(self, **args).call
123-
end
124-
125-
def resource_read_request(**args)
126-
RubyLLM::MCP::Requests::ResourceRead.new(self, **args).call
127-
end
128-
129-
def completion_resource(**args)
130-
RubyLLM::MCP::Requests::CompletionResource.new(self, **args).call
131-
end
132-
133-
def completion_prompt(**args)
134-
RubyLLM::MCP::Requests::CompletionPrompt.new(self, **args).call
135-
end
136-
137-
def execute_prompt(**args)
138-
RubyLLM::MCP::Requests::PromptCall.new(self, **args).call
139-
end
140-
14189
private
14290

143-
def initialize_request
144-
RubyLLM::MCP::Requests::Initialization.new(self).call
145-
end
146-
147-
def notification_request
148-
RubyLLM::MCP::Requests::Notification.new(self).call
149-
end
150-
151-
def tool_list_request
152-
RubyLLM::MCP::Requests::ToolList.new(self).call
153-
end
154-
155-
def resources_list_request
156-
RubyLLM::MCP::Requests::ResourceList.new(self).call
157-
end
158-
159-
def resource_template_list_request
160-
RubyLLM::MCP::Requests::ResourceTemplateList.new(self).call
161-
end
162-
163-
def prompt_list_request
164-
RubyLLM::MCP::Requests::PromptList.new(self).call
165-
end
166-
167-
def fetch_and_create_tools
168-
tools_response = tool_list_request
169-
tools_response = tools_response["result"]["tools"]
170-
171-
tools = {}
172-
tools_response.each do |tool|
173-
new_tool = RubyLLM::MCP::Tool.new(self, tool)
174-
tools[new_tool.name] = new_tool
175-
end
176-
177-
tools
178-
end
179-
180-
def fetch_and_create_resources
181-
resources_response = resources_list_request
182-
resources_response = resources_response["result"]["resources"]
183-
184-
resources = {}
185-
resources_response.each do |resource|
186-
new_resource = RubyLLM::MCP::Resource.new(self, resource)
187-
resources[new_resource.name] = new_resource
188-
end
189-
190-
resources
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)
19194
end
19295

193-
def fetch_and_create_resource_templates
194-
resource_templates_response = resource_template_list_request
195-
resource_templates_response = resource_templates_response["result"]["resourceTemplates"]
196-
197-
resource_templates = {}
198-
resource_templates_response.each do |resource_template|
199-
new_resource_template = RubyLLM::MCP::ResourceTemplate.new(self, resource_template)
200-
resource_templates[new_resource_template.name] = new_resource_template
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
201100
end
202-
203-
resource_templates
204-
end
205-
206-
def fetch_and_create_prompts
207-
prompts_response = prompt_list_request
208-
prompts_response = prompts_response["result"]["prompts"]
209-
210-
prompts = {}
211-
prompts_response.each do |prompt|
212-
new_prompt = RubyLLM::MCP::Prompt.new(self,
213-
name: prompt["name"],
214-
description: prompt["description"],
215-
arguments: prompt["arguments"])
216-
217-
prompts[new_prompt.name] = new_prompt
218-
end
219-
220-
prompts
221101
end
222102
end
223103
end

lib/ruby_llm/mcp/coordinator.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)