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 +4 -4
- data/.solargraph.yml +13 -0
- data/CHANGELOG.md +26 -1
- data/README.md +275 -19
- data/Rakefile +2 -0
- data/lib/model_context_protocol/server/configuration.rb +78 -0
- data/lib/model_context_protocol/server/prompt.rb +72 -0
- data/lib/model_context_protocol/server/registry.rb +102 -0
- data/lib/model_context_protocol/server/resource.rb +67 -0
- data/lib/model_context_protocol/server/router.rb +25 -106
- data/lib/model_context_protocol/server/stdio_transport.rb +65 -0
- data/lib/model_context_protocol/server/tool.rb +107 -0
- data/lib/model_context_protocol/server.rb +89 -55
- data/lib/model_context_protocol/version.rb +1 -1
- data/tasks/mcp.rake +62 -0
- data/tasks/templates/dev.erb +33 -0
- metadata +26 -8
- data/lib/model_context_protocol/server/router/base_map.rb +0 -22
- data/lib/model_context_protocol/server/router/prompts_map.rb +0 -17
- data/lib/model_context_protocol/server/router/protocol_map.rb +0 -0
- data/lib/model_context_protocol/server/router/resources_map.rb +0 -17
- data/lib/model_context_protocol/server/router/tools_map.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa9e49b895a1502fe4887f7e2089c24b69b980d9ac7c27754f97c52ad1541c7b
|
4
|
+
data.tar.gz: 92e343e66c69cd1e5ae9f9d8adb83567303a2aa5e48021b1dc964c172128426d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bcbfcb95e17f7e0deaa368f7e2b3dc7915be124b4c98b2b86b8e94c446fe18bd7d9e8c7dbfb6e4301edf6b1ad28e491701fec85bbd31dcd2d79d8b91a3f0c8c1
|
7
|
+
data.tar.gz: 284e545287ecacb83d42c7a5f72dbe9b0f5ab359d50c9d0cfcde1eb2e00d9c330e9cce3b6bcb15e68fb48963e8778888ca3dbdf1fab5ac0a0d27d36f1ca4e6bd
|
data/.solargraph.yml
ADDED
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.
|
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 `
|
22
|
+
Include `model_context_protocol` in your project.
|
8
23
|
|
9
24
|
```ruby
|
10
|
-
require '
|
25
|
+
require 'model_context_protocol'
|
11
26
|
```
|
12
27
|
|
13
|
-
|
28
|
+
### Building an MCP Server
|
14
29
|
|
15
|
-
Build a simple MCP server by
|
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 = "
|
20
|
-
config.router = router
|
34
|
+
config.name = "MCP Development Server"
|
21
35
|
config.version = "1.0.0"
|
22
36
|
config.enable_log = true
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
@@ -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
|