model-context-protocol-rb 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fffc016471f9e02baa6f9288860d65c7e9f4c9b573589d4abb31d824318b5ea1
4
- data.tar.gz: 59b220de0291942879c34d7ca8cc47173b10d692a13e271c1458a59a7f2ff9fb
3
+ metadata.gz: aa9e49b895a1502fe4887f7e2089c24b69b980d9ac7c27754f97c52ad1541c7b
4
+ data.tar.gz: 92e343e66c69cd1e5ae9f9d8adb83567303a2aa5e48021b1dc964c172128426d
5
5
  SHA512:
6
- metadata.gz: abda740fa404a916e3b30aa3b8e8c06e1731bb4e810b44b3d60f91742f92298fa66d2b4165f102679a49ee2246da6223ece27ad68302e96040148c2a83d00b3d
7
- data.tar.gz: bc71626a2bec6b61a4e8e3de7a6ef8a44fb64a9e8d56f163734ff8a7e4ef42d05f552b0918f16e798ed15749b145fa901cbc4747a798ca9a274ef941b8f08e44
6
+ metadata.gz: bcbfcb95e17f7e0deaa368f7e2b3dc7915be124b4c98b2b86b8e94c446fe18bd7d9e8c7dbfb6e4301edf6b1ad28e491701fec85bbd31dcd2d79d8b91a3f0c8c1
7
+ data.tar.gz: 284e545287ecacb83d42c7a5f72dbe9b0f5ab359d50c9d0cfcde1eb2e00d9c330e9cce3b6bcb15e68fb48963e8778888ca3dbdf1fab5ac0a0d27d36f1ca4e6bd
data/.solargraph.yml ADDED
@@ -0,0 +1,13 @@
1
+ include:
2
+ - "**/*.rb"
3
+ exclude:
4
+ - spec/**/*
5
+ - ".bundle/**"
6
+ require: []
7
+ domains: []
8
+ require_paths: []
9
+ plugins:
10
+ - solargraph-standardrb
11
+ reporters:
12
+ - standardrb
13
+ max_files: 5000
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-04-04
4
+
5
+ - Added support for environment variables to MCP servers (thanks @hmk):
6
+ - `require_environment_variable` method to specify required environment variables
7
+ - `set_environment_variable` method to programmatically set environment variables
8
+ - Environment variables accessible within tool/prompt/resource handlers
9
+ - Added `respond_with` helper methods to simplify response creation:
10
+ - For tools: text, image, resource, and error responses
11
+ - For prompts: formatted message responses
12
+ - For resources: text and binary responses
13
+ - Improved development tooling:
14
+ - Generated executable now loads all test classes
15
+ - Fixed test support classes for better compatibility with MCP inspector
16
+ - Organized test tools, prompts, and resources in dedicated directories
17
+
18
+ ## [0.3.0] - 2025-03-11
19
+
20
+ - (Breaking) Replaced router initialization in favor of registry initialization during server configuration. The server now relies on the registry for auto-discovery of prompts, resources, and tools; this requires the use of SDK-provided builders to facilitate.
21
+ - (Breaking) Implemented the use of `Data` objects across the implementation. As a result, responses from custom handlers must now respond with an object that responds to `serialized`.
22
+ - Refactored the implementation to maintain separation of concerns and improve testability/maintainability.
23
+ - Improved test coverage.
24
+ - Improved development tooling.
25
+
3
26
  ## [0.2.0] - 2025-01-13
4
27
 
5
28
  - Added a basic, synchronous server implementation that routes requests to custom handlers.
@@ -8,6 +31,8 @@
8
31
 
9
32
  - Initial release
10
33
 
11
- [Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.2.0...HEAD
34
+ [Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.1...HEAD
35
+ [0.3.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.0...v0.3.1
36
+ [0.3.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.2.0...v0.3.0
12
37
  [0.2.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.1.0...v0.2.0
13
38
  [0.1.0]: https://github.com/dickdavis/model-context-protocol-rb/releases/tag/v0.1.0
data/README.md CHANGED
@@ -2,38 +2,57 @@
2
2
 
3
3
  An implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2024-11-05/) in Ruby.
4
4
 
5
+ This SDK is experimental and subject to change. The initial focus is to implement MCP server support with the goal of providing a stable API by version `0.4`. MCP client support will follow.
6
+
7
+ You are welcome to contribute.
8
+
9
+ TODO's:
10
+
11
+ * [Completion](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/completion/)
12
+ * [Logging](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging/)
13
+ * [Pagination](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/pagination/)
14
+ * [Prompt list changed notifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#list-changed-notification)
15
+ * [Resource list changed notifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#list-changed-notification)
16
+ * [Resource subscriptions](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#subscriptions)
17
+ * [Resource templates](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#resource-templates)
18
+ * [Tool list changed notifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#list-changed-notification)
19
+
5
20
  ## Usage
6
21
 
7
- Include `model-context-protocol-rb` in your project.
22
+ Include `model_context_protocol` in your project.
8
23
 
9
24
  ```ruby
10
- require 'model-context-protocol-rb'
25
+ require 'model_context_protocol'
11
26
  ```
12
27
 
13
- # Building an MCP Server
28
+ ### Building an MCP Server
14
29
 
15
- Build a simple MCP server by routing methods to your custom handlers. Then, configure and run the server.
30
+ Build a simple MCP server by registering your prompts, resources, and tools. Then, configure and run the server.
16
31
 
17
32
  ```ruby
18
33
  server = ModelContextProtocol::Server.new do |config|
19
- config.name = "My MCP Server"
20
- config.router = router
34
+ config.name = "MCP Development Server"
21
35
  config.version = "1.0.0"
22
36
  config.enable_log = true
23
- config.router = ModelContextProtocol::Router.new do
24
- prompts do
25
- list Prompt::List, broadcast_changes: true
26
- get Prompt::Get
37
+
38
+ # Environment Variables - https://modelcontextprotocol.io/docs/tools/debugging#environment-variables
39
+ # Require specific environment variables to be set
40
+ config.require_environment_variable("API_KEY")
41
+
42
+ # Set environment variables programmatically
43
+ config.set_environment_variable("DEBUG_MODE", "true")
44
+
45
+ config.registry = ModelContextProtocol::Server::Registry.new do
46
+ prompts list_changed: true do
47
+ register TestPrompt
27
48
  end
28
49
 
29
- resources do
30
- list Resource::List, broadcast_changes: true
31
- read Resource::Read, allow_subscriptions: true
50
+ resources list_changed: true, subscribe: true do
51
+ register TestResource
32
52
  end
33
53
 
34
- tools do
35
- list Tool::List, broadcast_changes: true
36
- call Tool::Call
54
+ tools list_changed: true do
55
+ register TestTool
37
56
  end
38
57
  end
39
58
  end
@@ -41,9 +60,236 @@ end
41
60
  server.start
42
61
  ```
43
62
 
44
- Messages from the MCP client will be routed to the appropriate custom handler. Your customer handler must respond to `call`; the router will pass the message to the handler as an argument.
63
+ Messages from the MCP client will be routed to the appropriate custom handler. This SDK provides several classes that should be used to build your handlers.
64
+
65
+ #### ModelContextProtocol::Server::Prompt
66
+
67
+ The `ModelContextProtocol::Server::Prompt` base class allows subclasses to define a prompt that the MCP client can use. Define the [appropriate metadata](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/) in the `with_metadata` block.
68
+
69
+ Then implement the `call` method to build your prompt. Use the `respond_with` instance method to ensure your prompt responds with appropriately formatted response data.
70
+
71
+ This is an example prompt that returns a properly formatted response:
72
+
73
+ ```ruby
74
+ class TestPrompt < ModelContextProtocol::Server::Prompt
75
+ with_metadata do
76
+ {
77
+ name: "Test Prompt",
78
+ description: "A test prompt",
79
+ arguments: [
80
+ {
81
+ name: "message",
82
+ description: "The thing to do",
83
+ required: true
84
+ },
85
+ {
86
+ name: "other",
87
+ description: "Another thing to do",
88
+ required: false
89
+ }
90
+ ]
91
+ }
92
+ end
93
+
94
+ def call
95
+ messages = [
96
+ {
97
+ role: "user",
98
+ content: {
99
+ type: "text",
100
+ text: "Do this: #{params["message"]}"
101
+ }
102
+ }
103
+ ]
104
+
105
+ respond_with messages: messages
106
+ end
107
+ end
108
+ ```
109
+
110
+ #### ModelContextProtocol::Server::Resource
111
+
112
+ The `ModelContextProtocol::Server::Resource` base class allows subclasses to define a resource that the MCP client can use. Define the [appropriate metadata](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/) in the `with_metadata` block.
113
+
114
+ Then, implement the `call` method to build your resource. Use the `respond_with` instance method to ensure your resource responds with appropriately formatted response data.
115
+
116
+ This is an example resource that returns a text response:
117
+
118
+ ```ruby
119
+ class TestResource < ModelContextProtocol::Server::Resource
120
+ with_metadata do
121
+ {
122
+ name: "Test Resource",
123
+ description: "A test resource",
124
+ mime_type: "text/plain",
125
+ uri: "resource://test-resource"
126
+ }
127
+ end
128
+
129
+ def call
130
+ respond_with :text, text: "Here's the data"
131
+ end
132
+ end
133
+ ```
134
+
135
+ This is an example resource that returns binary data:
136
+
137
+ ```ruby
138
+ class TestBinaryResource < ModelContextProtocol::Server::Resource
139
+ with_metadata do
140
+ {
141
+ name: "Project Logo",
142
+ description: "The logo for the project",
143
+ mime_type: "image/jpeg",
144
+ uri: "resource://project-logo"
145
+ }
146
+ end
147
+
148
+ def call
149
+ # In a real implementation, we would retrieve the binary resource
150
+ data = "dGVzdA=="
151
+ respond_with :binary, blob: data
152
+ end
153
+ end
154
+ ```
155
+
156
+ #### ModelContextProtocol::Server::Tool
157
+
158
+ The `ModelContextProtocol::Server::Tool` base class allows subclasses to define a tool that the MCP client can use. Define the [appropriate metadata](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/) in the `with_metadata` block.
159
+
160
+ Then implement the `call` method to build your tool. Use the `respond_with` instance method to ensure your tool responds with appropriately formatted response data.
161
+
162
+ This is an example tool that returns a text response:
163
+
164
+ ```ruby
165
+ class TestToolWithTextResponse < ModelContextProtocol::Server::Tool
166
+ with_metadata do
167
+ {
168
+ name: "double",
169
+ description: "Doubles the provided number",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ number: {
174
+ type: "string",
175
+ }
176
+ },
177
+ required: ["number"]
178
+ }
179
+ }
180
+ end
181
+
182
+ def call
183
+ number = params["number"].to_i
184
+ result = number * 2
185
+ respond_with :text, text: "#{number} doubled is #{result}"
186
+ end
187
+ end
188
+ ```
189
+
190
+ This is an example of a tool that returns an image:
191
+
192
+ ```ruby
193
+ class TestToolWithImageResponse < ModelContextProtocol::Server::Tool
194
+ with_metadata do
195
+ {
196
+ name: "custom-chart-generator",
197
+ description: "Generates a chart in various formats",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ chart_type: {
202
+ type: "string",
203
+ description: "Type of chart (pie, bar, line)"
204
+ },
205
+ format: {
206
+ type: "string",
207
+ description: "Image format (jpg, svg, etc)"
208
+ }
209
+ },
210
+ required: ["chart_type", "format"]
211
+ }
212
+ }
213
+ end
214
+
215
+ def call
216
+ # Map format to mime type
217
+ mime_type = case params["format"].downcase
218
+ when "svg"
219
+ "image/svg+xml"
220
+ when "jpg", "jpeg"
221
+ "image/jpeg"
222
+ else
223
+ "image/png"
224
+ end
225
+
226
+ # In a real implementation, we would generate an actual chart
227
+ # This is a small valid base64 encoded string (represents "test")
228
+ chart_data = "dGVzdA=="
229
+ respond_with :image, data: chart_data, mime_type:
230
+ end
231
+ end
232
+ ```
233
+
234
+ If you don't provide a mime type, it will default to `image/png`.
45
235
 
46
- Your handler should return a valid JSONRPC 2.0 response.
236
+ ```ruby
237
+ class TestToolWithImageResponseDefaultMimeType < ModelContextProtocol::Server::Tool
238
+ with_metadata do
239
+ {
240
+ name: "other-custom-chart-generator",
241
+ description: "Generates a chart",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ chart_type: {
246
+ type: "string",
247
+ description: "Type of chart (pie, bar, line)"
248
+ }
249
+ },
250
+ required: ["chart_type"]
251
+ }
252
+ }
253
+ end
254
+
255
+ def call
256
+ # In a real implementation, we would generate an actual chart
257
+ # This is a small valid base64 encoded string (represents "test")
258
+ chart_data = "dGVzdA=="
259
+ respond_with :image, data: chart_data
260
+ end
261
+ end
262
+ ```
263
+
264
+ This is an example of a tool that returns a resource response:
265
+
266
+ ```ruby
267
+ class TestToolWithResourceResponse < ModelContextProtocol::Server::Tool
268
+ with_metadata do
269
+ {
270
+ name: "document-finder",
271
+ description: "Finds a the document with the given title",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ title: {
276
+ type: "string",
277
+ description: "The title of the document"
278
+ }
279
+ },
280
+ required: ["title"]
281
+ }
282
+ }
283
+ end
284
+
285
+ def call
286
+ title = params["title"].downcase
287
+ # In a real implementation, we would do a lookup to get the document data
288
+ document = "richtextdata"
289
+ respond_with :resource, uri: "resource://document/#{title}", text: document, mime_type: "application/rtf"
290
+ end
291
+ end
292
+ ```
47
293
 
48
294
  ## Installation
49
295
 
@@ -67,7 +313,17 @@ gem install model-context-protocol-rb
67
313
 
68
314
  ## Development
69
315
 
70
- 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.
316
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
317
+
318
+ Generate an executable that you can use for testing:
319
+
320
+ ```bash
321
+ bundle exec rake mcp:generate_executable
322
+ ```
323
+
324
+ This will generate a `bin/dev` executable you can provide to MCP clients.
325
+
326
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
71
327
 
72
328
  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).
73
329
 
data/Rakefile CHANGED
@@ -7,4 +7,6 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "standard/rake"
9
9
 
10
+ Dir.glob("tasks/*.rake").each { |r| load r }
11
+
10
12
  task default: %i[spec standard]
@@ -0,0 +1,78 @@
1
+ module ModelContextProtocol
2
+ class Server::Configuration
3
+ # Raised when configured with invalid name.
4
+ class InvalidServerNameError < StandardError; end
5
+
6
+ # Raised when configured with invalid version.
7
+ class InvalidServerVersionError < StandardError; end
8
+
9
+ # Raised when configured with invalid registry.
10
+ class InvalidRegistryError < StandardError; end
11
+
12
+ # Raised when a required environment variable is not set
13
+ class MissingRequiredEnvironmentVariable < StandardError; end
14
+
15
+ attr_accessor :enable_log, :name, :registry, :version
16
+
17
+ def logging_enabled?
18
+ enable_log || false
19
+ end
20
+
21
+ def validate!
22
+ raise InvalidServerNameError unless valid_name?
23
+ raise InvalidRegistryError unless valid_registry?
24
+ raise InvalidServerVersionError unless valid_version?
25
+
26
+ validate_environment_variables!
27
+ end
28
+
29
+ def environment_variables
30
+ @environment_variables ||= {}
31
+ end
32
+
33
+ def environment_variable(key)
34
+ environment_variables[key.to_s.upcase] || ENV[key.to_s.upcase] || nil
35
+ end
36
+
37
+ def require_environment_variable(key)
38
+ required_environment_variables << key.to_s.upcase
39
+ end
40
+
41
+ # Programatically set an environment variable - useful if an alternative
42
+ # to environment variables is used for security purposes. Despite being
43
+ # more like 'configuration variables', these are called environment variables
44
+ # to align with the Model Context Protocol terminology.
45
+ #
46
+ # see: https://modelcontextprotocol.io/docs/tools/debugging#environment-variables
47
+ #
48
+ # @param key [String] The key to set the environment variable for
49
+ # @param value [String] The value to set the environment variable to
50
+ def set_environment_variable(key, value)
51
+ environment_variables[key.to_s.upcase] = value
52
+ end
53
+
54
+ private
55
+
56
+ def required_environment_variables
57
+ @required_environment_variables ||= []
58
+ end
59
+
60
+ def validate_environment_variables!
61
+ required_environment_variables.each do |key|
62
+ raise MissingRequiredEnvironmentVariable, "#{key} is not set" unless environment_variable(key)
63
+ end
64
+ end
65
+
66
+ def valid_name?
67
+ name&.is_a?(String)
68
+ end
69
+
70
+ def valid_registry?
71
+ registry&.is_a?(ModelContextProtocol::Server::Registry)
72
+ end
73
+
74
+ def valid_version?
75
+ version&.is_a?(String)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,72 @@
1
+ module ModelContextProtocol
2
+ class Server::Prompt
3
+ attr_reader :params, :description
4
+
5
+ def initialize(params)
6
+ validate!(params)
7
+ @description = self.class.description
8
+ @params = params
9
+ end
10
+
11
+ def call
12
+ raise NotImplementedError, "Subclasses must implement the call method"
13
+ end
14
+
15
+ Response = Data.define(:messages, :prompt) do
16
+ def serialized
17
+ {description: prompt.description, messages:}
18
+ end
19
+ end
20
+ private_constant :Response
21
+
22
+ private def respond_with(messages:)
23
+ Response[messages:, prompt: self]
24
+ end
25
+
26
+ private def validate!(params = {})
27
+ arguments = self.class.arguments || []
28
+ required_args = arguments.select { |arg| arg[:required] }.map { |arg| arg[:name] }
29
+ valid_arg_names = arguments.map { |arg| arg[:name] }
30
+
31
+ missing_args = required_args - params.keys
32
+ unless missing_args.empty?
33
+ missing_args_list = missing_args.join(", ")
34
+ raise ArgumentError, "Missing required arguments: #{missing_args_list}"
35
+ end
36
+
37
+ extra_args = params.keys - valid_arg_names
38
+ unless extra_args.empty?
39
+ extra_args_list = extra_args.join(", ")
40
+ raise ArgumentError, "Unexpected arguments: #{extra_args_list}"
41
+ end
42
+ end
43
+
44
+ class << self
45
+ attr_reader :name, :description, :arguments
46
+
47
+ def with_metadata(&block)
48
+ metadata = instance_eval(&block)
49
+
50
+ @name = metadata[:name]
51
+ @description = metadata[:description]
52
+ @arguments = metadata[:arguments]
53
+ end
54
+
55
+ def inherited(subclass)
56
+ subclass.instance_variable_set(:@name, @name)
57
+ subclass.instance_variable_set(:@description, @description)
58
+ subclass.instance_variable_set(:@arguments, @arguments)
59
+ end
60
+
61
+ def call(params)
62
+ new(params).call
63
+ rescue ArgumentError => error
64
+ raise ModelContextProtocol::Server::ParameterValidationError, error.message
65
+ end
66
+
67
+ def metadata
68
+ {name: @name, description: @description, arguments: @arguments}
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,102 @@
1
+ module ModelContextProtocol
2
+ class Server::Registry
3
+ attr_reader :prompts_options, :resources_options, :tools_options
4
+
5
+ def self.new(&block)
6
+ registry = allocate
7
+ registry.send(:initialize)
8
+ registry.instance_eval(&block) if block
9
+ registry
10
+ end
11
+
12
+ def initialize
13
+ @prompts = []
14
+ @resources = []
15
+ @tools = []
16
+ @prompts_options = {}
17
+ @resources_options = {}
18
+ @tools_options = {}
19
+ end
20
+
21
+ def prompts(options = {}, &block)
22
+ @prompts_options = options
23
+ instance_eval(&block) if block
24
+ end
25
+
26
+ def resources(options = {}, &block)
27
+ @resources_options = options
28
+ instance_eval(&block) if block
29
+ end
30
+
31
+ def tools(options = {}, &block)
32
+ @tools_options = options
33
+ instance_eval(&block) if block
34
+ end
35
+
36
+ def register(klass)
37
+ metadata = klass.metadata
38
+ entry = {klass: klass}.merge(metadata)
39
+
40
+ case klass.ancestors
41
+ when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Prompt) }
42
+ @prompts << entry
43
+ when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Resource) }
44
+ @resources << entry
45
+ when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Tool) }
46
+ @tools << entry
47
+ else
48
+ raise ArgumentError, "Unknown class type: #{klass}"
49
+ end
50
+ end
51
+
52
+ def find_prompt(name)
53
+ find_by_name(@prompts, name)
54
+ end
55
+
56
+ def find_resource(uri)
57
+ entry = @resources.find { |r| r[:uri] == uri }
58
+ entry ? entry[:klass] : nil
59
+ end
60
+
61
+ def find_tool(name)
62
+ find_by_name(@tools, name)
63
+ end
64
+
65
+ def prompts_data
66
+ PromptsData[prompts: @prompts.map { |entry| entry.except(:klass) }]
67
+ end
68
+
69
+ def resources_data
70
+ ResourcesData[resources: @resources.map { |entry| entry.except(:klass) }]
71
+ end
72
+
73
+ def tools_data
74
+ ToolsData[tools: @tools.map { |entry| entry.except(:klass) }]
75
+ end
76
+
77
+ private
78
+
79
+ PromptsData = Data.define(:prompts) do
80
+ def serialized
81
+ {prompts:}
82
+ end
83
+ end
84
+
85
+ ResourcesData = Data.define(:resources) do
86
+ def serialized
87
+ {resources:}
88
+ end
89
+ end
90
+
91
+ ToolsData = Data.define(:tools) do
92
+ def serialized
93
+ {tools:}
94
+ end
95
+ end
96
+
97
+ def find_by_name(collection, name)
98
+ entry = collection.find { |item| item[:name] == name }
99
+ entry ? entry[:klass] : nil
100
+ end
101
+ end
102
+ end