|
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 | | - PROTOCOL_VERSION = "2025-03-26" |
7 | | - PV_2024_11_05 = "2024-11-05" |
| 8 | + extend Forwardable |
8 | 9 |
|
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 |
11 | 11 |
|
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: {}) |
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 | | - @transport = nil |
20 | | - |
21 | | - @capabilities = nil |
22 | | - |
23 | 16 | @request_timeout = request_timeout |
24 | | - @reverse_proxy_url = reverse_proxy_url |
25 | 17 |
|
26 | | - if start |
27 | | - self.start |
28 | | - end |
29 | | - end |
| 18 | + @coordinator = Coordinator.new(self, transport_type: @transport_type, config: @config) |
30 | 19 |
|
31 | | - def request(body, **options) |
32 | | - @transport.request(body, **options) |
| 20 | + start_transport if start |
33 | 21 | end |
34 | 22 |
|
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 |
49 | 24 |
|
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 |
68 | 28 |
|
69 | 29 | 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 | + |
72 | 35 | @tools.values |
73 | 36 | end |
74 | 37 |
|
75 | 38 | def tool(name, refresh: false) |
76 | | - @tools = nil if refresh |
77 | | - @tools ||= fetch_and_create_tools |
| 39 | + tools(refresh: refresh) |
78 | 40 |
|
79 | 41 | @tools[name] |
80 | 42 | end |
81 | 43 |
|
82 | 44 | 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 | + |
85 | 50 | @resources.values |
86 | 51 | end |
87 | 52 |
|
88 | 53 | def resource(name, refresh: false) |
89 | | - @resources = nil if refresh |
90 | | - @resources ||= fetch_and_create_resources |
| 54 | + resources(refresh: refresh) |
91 | 55 |
|
92 | 56 | @resources[name] |
93 | 57 | end |
94 | 58 |
|
95 | 59 | 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 | + |
98 | 65 | @resource_templates.values |
99 | 66 | end |
100 | 67 |
|
101 | 68 | 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) |
104 | 70 |
|
105 | 71 | @resource_templates[name] |
106 | 72 | end |
107 | 73 |
|
108 | 74 | 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 | + |
111 | 80 | @prompts.values |
112 | 81 | end |
113 | 82 |
|
114 | 83 | def prompt(name, refresh: false) |
115 | | - @prompts = nil if refresh |
116 | | - @prompts ||= fetch_and_create_prompts |
| 84 | + prompts(refresh: refresh) |
117 | 85 |
|
118 | 86 | @prompts[name] |
119 | 87 | end |
120 | 88 |
|
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 | | - |
141 | 89 | private |
142 | 90 |
|
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) |
191 | 94 | end |
192 | 95 |
|
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 |
201 | 100 | 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 |
221 | 101 | end |
222 | 102 | end |
223 | 103 | end |
|
0 commit comments