model-context-protocol-rb 0.3.0 → 0.3.2
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/CHANGELOG.md +24 -1
- data/README.md +252 -46
- data/Rakefile +2 -0
- data/lib/model_context_protocol/server/completion.rb +45 -0
- data/lib/model_context_protocol/server/configuration.rb +40 -0
- data/lib/model_context_protocol/server/prompt.rb +73 -13
- data/lib/model_context_protocol/server/registry.rb +22 -0
- data/lib/model_context_protocol/server/resource.rb +49 -13
- data/lib/model_context_protocol/server/resource_template.rb +93 -0
- data/lib/model_context_protocol/server/router.rb +16 -2
- data/lib/model_context_protocol/server/tool.rb +57 -14
- data/lib/model_context_protocol/server.rb +40 -3
- data/lib/model_context_protocol/version.rb +1 -1
- data/lib/model_context_protocol.rb +1 -1
- data/tasks/mcp.rake +62 -0
- data/tasks/templates/dev.erb +37 -0
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99ee11ebae0c9c984a15954b386e89489f6715a1d371b288ece0438d066436d7
|
4
|
+
data.tar.gz: cdf41e8742941e9a0a41c714127d02f5a189d901a95a35e6f61a16f1f7b8a704
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aad64bd6e42b2ecea2b742bb23b902ea1bfae3e77cffc5009b0a83712df7efac417b680bb460649bb73fff31d2d554d33a17d6fe96adb35f3cc9d0addbbcf9be
|
7
|
+
data.tar.gz: 6fa834a1c2bf4494748474dc26a29bede650b1a1913c7c543a7c195fd24fe99eda5f1610c40a63067f454b50f669b1fb47030e106fa6ba80cbde522bccfb9518
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.3.2] - 2025-05-10
|
4
|
+
|
5
|
+
- Added resource template support.
|
6
|
+
- Added completion support for prompts and resources.
|
7
|
+
- Improved metadata definition for prompts, resources, and tools using simple DSL.
|
8
|
+
|
9
|
+
## [0.3.1] - 2025-04-04
|
10
|
+
|
11
|
+
- Added support for environment variables to MCP servers (thanks @hmk):
|
12
|
+
- `require_environment_variable` method to specify required environment variables
|
13
|
+
- `set_environment_variable` method to programmatically set environment variables
|
14
|
+
- Environment variables accessible within tool/prompt/resource handlers
|
15
|
+
- Added `respond_with` helper methods to simplify response creation:
|
16
|
+
- For tools: text, image, resource, and error responses
|
17
|
+
- For prompts: formatted message responses
|
18
|
+
- For resources: text and binary responses
|
19
|
+
- Improved development tooling:
|
20
|
+
- Generated executable now loads all test classes
|
21
|
+
- Fixed test support classes for better compatibility with MCP inspector
|
22
|
+
- Organized test tools, prompts, and resources in dedicated directories
|
23
|
+
|
3
24
|
## [0.3.0] - 2025-03-11
|
4
25
|
|
5
26
|
- (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.
|
@@ -16,7 +37,9 @@
|
|
16
37
|
|
17
38
|
- Initial release
|
18
39
|
|
19
|
-
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.
|
40
|
+
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.2...HEAD
|
41
|
+
[0.3.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.1...v0.3.2
|
42
|
+
[0.3.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.0...v0.3.1
|
20
43
|
[0.3.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.2.0...v0.3.0
|
21
44
|
[0.2.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.1.0...v0.2.0
|
22
45
|
[0.1.0]: https://github.com/dickdavis/model-context-protocol-rb/releases/tag/v0.1.0
|
data/README.md
CHANGED
@@ -8,32 +8,37 @@ You are welcome to contribute.
|
|
8
8
|
|
9
9
|
TODO's:
|
10
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
11
|
* [Pagination](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/pagination/)
|
14
12
|
* [Prompt list changed notifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#list-changed-notification)
|
15
13
|
* [Resource list changed notifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#list-changed-notification)
|
16
14
|
* [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
15
|
* [Tool list changed notifications](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#list-changed-notification)
|
19
16
|
|
20
17
|
## Usage
|
21
18
|
|
22
|
-
Include `
|
19
|
+
Include `model_context_protocol` in your project.
|
23
20
|
|
24
21
|
```ruby
|
25
|
-
require '
|
22
|
+
require 'model_context_protocol'
|
26
23
|
```
|
27
24
|
|
28
25
|
### Building an MCP Server
|
29
26
|
|
30
|
-
Build a simple MCP server by
|
27
|
+
Build a simple MCP server by registering your prompts, resources, resource templates, and tools. Then, configure and run the server.
|
31
28
|
|
32
29
|
```ruby
|
33
30
|
server = ModelContextProtocol::Server.new do |config|
|
34
31
|
config.name = "MCP Development Server"
|
35
32
|
config.version = "1.0.0"
|
36
33
|
config.enable_log = true
|
34
|
+
|
35
|
+
# Environment Variables - https://modelcontextprotocol.io/docs/tools/debugging#environment-variables
|
36
|
+
# Require specific environment variables to be set
|
37
|
+
config.require_environment_variable("API_KEY")
|
38
|
+
|
39
|
+
# Set environment variables programmatically
|
40
|
+
config.set_environment_variable("DEBUG_MODE", "true")
|
41
|
+
|
37
42
|
config.registry = ModelContextProtocol::Server::Registry.new do
|
38
43
|
prompts list_changed: true do
|
39
44
|
register TestPrompt
|
@@ -43,6 +48,10 @@ server = ModelContextProtocol::Server.new do |config|
|
|
43
48
|
register TestResource
|
44
49
|
end
|
45
50
|
|
51
|
+
resource_templates do
|
52
|
+
register TestResourceTemplate
|
53
|
+
end
|
54
|
+
|
46
55
|
tools list_changed: true do
|
47
56
|
register TestTool
|
48
57
|
end
|
@@ -54,29 +63,34 @@ server.start
|
|
54
63
|
|
55
64
|
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.
|
56
65
|
|
57
|
-
####
|
66
|
+
#### Prompts
|
67
|
+
|
68
|
+
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.
|
69
|
+
|
70
|
+
Define any arguments using the `with_argument` block. You can mark an argument as required, and you can optionally provide the class name of a service object that provides completions. See [Completions](#completions) for more information.
|
71
|
+
|
72
|
+
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.
|
58
73
|
|
59
|
-
|
74
|
+
This is an example prompt that returns a properly formatted response:
|
60
75
|
|
61
76
|
```ruby
|
62
77
|
class TestPrompt < ModelContextProtocol::Server::Prompt
|
63
78
|
with_metadata do
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
}
|
79
|
+
name "test_prompt"
|
80
|
+
description "A test prompt"
|
81
|
+
end
|
82
|
+
|
83
|
+
with_argument do
|
84
|
+
name "message"
|
85
|
+
description "The thing to do"
|
86
|
+
required true
|
87
|
+
completion TestCompletion
|
88
|
+
end
|
89
|
+
|
90
|
+
with_argument do
|
91
|
+
name "other"
|
92
|
+
description "Another thing to do"
|
93
|
+
required false
|
80
94
|
end
|
81
95
|
|
82
96
|
def call
|
@@ -90,56 +104,238 @@ class TestPrompt < ModelContextProtocol::Server::Prompt
|
|
90
104
|
}
|
91
105
|
]
|
92
106
|
|
93
|
-
|
107
|
+
respond_with messages: messages
|
94
108
|
end
|
95
109
|
end
|
96
110
|
```
|
97
111
|
|
98
|
-
####
|
112
|
+
#### Resources
|
99
113
|
|
100
|
-
The `Resource` class
|
114
|
+
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.
|
115
|
+
|
116
|
+
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.
|
117
|
+
|
118
|
+
This is an example resource that returns a text response:
|
101
119
|
|
102
120
|
```ruby
|
103
121
|
class TestResource < ModelContextProtocol::Server::Resource
|
104
122
|
with_metadata do
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
123
|
+
name "Test Resource"
|
124
|
+
description "A test resource"
|
125
|
+
mime_type "text/plain"
|
126
|
+
uri "resource://test-resource"
|
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
|
+
name "Project Logo"
|
141
|
+
description "The logo for the project"
|
142
|
+
mime_type "image/jpeg"
|
143
|
+
uri "resource://project-logo"
|
144
|
+
end
|
145
|
+
|
146
|
+
def call
|
147
|
+
# In a real implementation, we would retrieve the binary resource
|
148
|
+
data = "dGVzdA=="
|
149
|
+
respond_with :binary, blob: data
|
111
150
|
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
#### Resource Templates
|
112
155
|
|
156
|
+
The `ModelContextProtocol::Server::ResourceTemplate` base class allows subclasses to define a resource template that the MCP client can use. Define the [appropriate metadata](https://modelcontextprotocol.io/specification/2024-11-05/server/resources#resource-templates) in the `with_metadata` block.
|
157
|
+
|
158
|
+
This is an example resource template that provides a completion for a parameter of the URI template:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class TestResourceTemplateCompletion < ModelContextProtocol::Server::Completion
|
113
162
|
def call
|
114
|
-
|
163
|
+
hints = {
|
164
|
+
"name" => ["test-resource", "project-logo"]
|
165
|
+
}
|
166
|
+
values = hints[argument_name].grep(/#{argument_value}/)
|
167
|
+
|
168
|
+
respond_with values:
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class TestResourceTemplate < ModelContextProtocol::Server::ResourceTemplate
|
173
|
+
with_metadata do
|
174
|
+
name "Test Resource Template"
|
175
|
+
description "A test resource template"
|
176
|
+
mime_type "text/plain"
|
177
|
+
uri_template "resource://{name}" do
|
178
|
+
completion :name, TestResourceTemplateCompletion
|
179
|
+
end
|
115
180
|
end
|
116
181
|
end
|
117
182
|
```
|
118
183
|
|
119
|
-
####
|
184
|
+
#### Tools
|
185
|
+
|
186
|
+
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.
|
120
187
|
|
121
|
-
|
188
|
+
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.
|
189
|
+
|
190
|
+
This is an example tool that returns a text response:
|
122
191
|
|
123
192
|
```ruby
|
124
|
-
class
|
193
|
+
class TestToolWithTextResponse < ModelContextProtocol::Server::Tool
|
125
194
|
with_metadata do
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
195
|
+
name "double"
|
196
|
+
description "Doubles the provided number"
|
197
|
+
input_schema do
|
198
|
+
{
|
130
199
|
type: "object",
|
131
200
|
properties: {
|
132
|
-
|
201
|
+
number: {
|
133
202
|
type: "string"
|
134
203
|
}
|
135
204
|
},
|
136
|
-
required: ["
|
205
|
+
required: ["number"]
|
137
206
|
}
|
138
|
-
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def call
|
211
|
+
number = params["number"].to_i
|
212
|
+
result = number * 2
|
213
|
+
respond_with :text, text: "#{number} doubled is #{result}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
This is an example of a tool that returns an image:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
class TestToolWithImageResponse < ModelContextProtocol::Server::Tool
|
222
|
+
with_metadata do
|
223
|
+
name "custom-chart-generator"
|
224
|
+
description "Generates a chart in various formats"
|
225
|
+
input_schema do
|
226
|
+
{
|
227
|
+
type: "object",
|
228
|
+
properties: {
|
229
|
+
chart_type: {
|
230
|
+
type: "string",
|
231
|
+
description: "Type of chart (pie, bar, line)"
|
232
|
+
},
|
233
|
+
format: {
|
234
|
+
type: "string",
|
235
|
+
description: "Image format (jpg, svg, etc)"
|
236
|
+
}
|
237
|
+
},
|
238
|
+
required: ["chart_type", "format"]
|
239
|
+
}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def call
|
244
|
+
# Map format to mime type
|
245
|
+
mime_type = case params["format"].downcase
|
246
|
+
when "svg"
|
247
|
+
"image/svg+xml"
|
248
|
+
when "jpg", "jpeg"
|
249
|
+
"image/jpeg"
|
250
|
+
else
|
251
|
+
"image/png"
|
252
|
+
end
|
253
|
+
|
254
|
+
# In a real implementation, we would generate an actual chart
|
255
|
+
# This is a small valid base64 encoded string (represents "test")
|
256
|
+
chart_data = "dGVzdA=="
|
257
|
+
respond_with :image, data: chart_data, mime_type:
|
258
|
+
end
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
If you don't provide a mime type, it will default to `image/png`.
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
class TestToolWithImageResponseDefaultMimeType < ModelContextProtocol::Server::Tool
|
266
|
+
with_metadata do
|
267
|
+
name "other-custom-chart-generator"
|
268
|
+
description "Generates a chart"
|
269
|
+
input_schema do
|
270
|
+
{
|
271
|
+
type: "object",
|
272
|
+
properties: {
|
273
|
+
chart_type: {
|
274
|
+
type: "string",
|
275
|
+
description: "Type of chart (pie, bar, line)"
|
276
|
+
}
|
277
|
+
},
|
278
|
+
required: ["chart_type"]
|
279
|
+
}
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def call
|
284
|
+
# In a real implementation, we would generate an actual chart
|
285
|
+
# This is a small valid base64 encoded string (represents "test")
|
286
|
+
chart_data = "dGVzdA=="
|
287
|
+
respond_with :image, data: chart_data
|
288
|
+
end
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
This is an example of a tool that returns a resource response:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
class TestToolWithResourceResponse < ModelContextProtocol::Server::Tool
|
296
|
+
with_metadata do
|
297
|
+
name "document-finder"
|
298
|
+
description "Finds a the document with the given title"
|
299
|
+
input_schema do
|
300
|
+
{
|
301
|
+
type: "object",
|
302
|
+
properties: {
|
303
|
+
title: {
|
304
|
+
type: "string",
|
305
|
+
description: "The title of the document"
|
306
|
+
}
|
307
|
+
},
|
308
|
+
required: ["title"]
|
309
|
+
}
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def call
|
314
|
+
title = params["title"].downcase
|
315
|
+
# In a real implementation, we would do a lookup to get the document data
|
316
|
+
document = "richtextdata"
|
317
|
+
respond_with :resource, uri: "resource://document/#{title}", text: document, mime_type: "application/rtf"
|
139
318
|
end
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
### Completions
|
323
|
+
|
324
|
+
The `ModelContextProtocol::Server::Completion` base class allows subclasses to define a completion that the MCP client can use to obtain hints or suggestions for arguments to prompts and resources.
|
140
325
|
|
326
|
+
implement the `call` method to build your completion. Use the `respond_with` instance method to ensure your completion responds with appropriately formatted response data.
|
327
|
+
|
328
|
+
This is an example completion that returns an array of values in the response:
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
class TestCompletion < ModelContextProtocol::Server::Completion
|
141
332
|
def call
|
142
|
-
|
333
|
+
hints = {
|
334
|
+
"message" => ["hello", "world", "foo", "bar"]
|
335
|
+
}
|
336
|
+
values = hints[argument_name].grep(/#{argument_value}/)
|
337
|
+
|
338
|
+
respond_with values:
|
143
339
|
end
|
144
340
|
end
|
145
341
|
```
|
@@ -166,7 +362,17 @@ gem install model-context-protocol-rb
|
|
166
362
|
|
167
363
|
## Development
|
168
364
|
|
169
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
365
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
366
|
+
|
367
|
+
Generate an executable that you can use for testing:
|
368
|
+
|
369
|
+
```bash
|
370
|
+
bundle exec rake mcp:generate_executable
|
371
|
+
```
|
372
|
+
|
373
|
+
This will generate a `bin/dev` executable you can provide to MCP clients.
|
374
|
+
|
375
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
170
376
|
|
171
377
|
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).
|
172
378
|
|
data/Rakefile
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
module ModelContextProtocol
|
2
|
+
class Server::Completion
|
3
|
+
attr_reader :argument_name, :argument_value
|
4
|
+
|
5
|
+
def initialize(argument_name, argument_value)
|
6
|
+
@argument_name = argument_name
|
7
|
+
@argument_value = argument_value
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
raise NotImplementedError, "Subclasses must implement the call method"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.call(...)
|
15
|
+
new(...).call
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
Response = Data.define(:values, :total, :hasMore) do
|
21
|
+
def serialized
|
22
|
+
{completion: {values:, total:, hasMore:}}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_with(values:)
|
27
|
+
values_to_return = values.take(100)
|
28
|
+
total = values.size
|
29
|
+
has_more = values_to_return.size != total
|
30
|
+
Response[values:, total:, hasMore: has_more]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Server::NullCompletion
|
35
|
+
Response = Data.define(:values, :total, :hasMore) do
|
36
|
+
def serialized
|
37
|
+
{completion: {values:, total:, hasMore:}}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.call(_argument_name, _argument_value)
|
42
|
+
Response[values: [], total: 0, hasMore: false]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -9,6 +9,9 @@ module ModelContextProtocol
|
|
9
9
|
# Raised when configured with invalid registry.
|
10
10
|
class InvalidRegistryError < StandardError; end
|
11
11
|
|
12
|
+
# Raised when a required environment variable is not set
|
13
|
+
class MissingRequiredEnvironmentVariable < StandardError; end
|
14
|
+
|
12
15
|
attr_accessor :enable_log, :name, :registry, :version
|
13
16
|
|
14
17
|
def logging_enabled?
|
@@ -19,10 +22,47 @@ module ModelContextProtocol
|
|
19
22
|
raise InvalidServerNameError unless valid_name?
|
20
23
|
raise InvalidRegistryError unless valid_registry?
|
21
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
|
22
52
|
end
|
23
53
|
|
24
54
|
private
|
25
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
|
+
|
26
66
|
def valid_name?
|
27
67
|
name&.is_a?(String)
|
28
68
|
end
|
@@ -1,16 +1,9 @@
|
|
1
1
|
module ModelContextProtocol
|
2
2
|
class Server::Prompt
|
3
|
-
attr_reader :params
|
4
|
-
|
5
|
-
Response = Data.define(:messages, :prompt) do
|
6
|
-
def serialized
|
7
|
-
{description: prompt.description, messages:}
|
8
|
-
end
|
9
|
-
end
|
3
|
+
attr_reader :params
|
10
4
|
|
11
5
|
def initialize(params)
|
12
6
|
validate!(params)
|
13
|
-
@description = self.class.description
|
14
7
|
@params = params
|
15
8
|
end
|
16
9
|
|
@@ -18,6 +11,17 @@ module ModelContextProtocol
|
|
18
11
|
raise NotImplementedError, "Subclasses must implement the call method"
|
19
12
|
end
|
20
13
|
|
14
|
+
Response = Data.define(:messages, :description) do
|
15
|
+
def serialized
|
16
|
+
{description:, messages:}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
private_constant :Response
|
20
|
+
|
21
|
+
private def respond_with(messages:)
|
22
|
+
Response[messages:, description: self.class.description]
|
23
|
+
end
|
24
|
+
|
21
25
|
private def validate!(params = {})
|
22
26
|
arguments = self.class.arguments || []
|
23
27
|
required_args = arguments.select { |arg| arg[:required] }.map { |arg| arg[:name] }
|
@@ -40,17 +44,33 @@ module ModelContextProtocol
|
|
40
44
|
attr_reader :name, :description, :arguments
|
41
45
|
|
42
46
|
def with_metadata(&block)
|
43
|
-
|
47
|
+
@arguments ||= []
|
48
|
+
|
49
|
+
metadata_dsl = MetadataDSL.new
|
50
|
+
metadata_dsl.instance_eval(&block)
|
51
|
+
|
52
|
+
@name = metadata_dsl.name
|
53
|
+
@description = metadata_dsl.description
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_argument(&block)
|
57
|
+
@arguments ||= []
|
44
58
|
|
45
|
-
|
46
|
-
|
47
|
-
|
59
|
+
argument_dsl = ArgumentDSL.new
|
60
|
+
argument_dsl.instance_eval(&block)
|
61
|
+
|
62
|
+
@arguments << {
|
63
|
+
name: argument_dsl.name,
|
64
|
+
description: argument_dsl.description,
|
65
|
+
required: argument_dsl.required,
|
66
|
+
completion: argument_dsl.completion
|
67
|
+
}
|
48
68
|
end
|
49
69
|
|
50
70
|
def inherited(subclass)
|
51
71
|
subclass.instance_variable_set(:@name, @name)
|
52
72
|
subclass.instance_variable_set(:@description, @description)
|
53
|
-
subclass.instance_variable_set(:@arguments, @arguments)
|
73
|
+
subclass.instance_variable_set(:@arguments, @arguments&.dup)
|
54
74
|
end
|
55
75
|
|
56
76
|
def call(params)
|
@@ -62,6 +82,46 @@ module ModelContextProtocol
|
|
62
82
|
def metadata
|
63
83
|
{name: @name, description: @description, arguments: @arguments}
|
64
84
|
end
|
85
|
+
|
86
|
+
def complete_for(arg_name, value)
|
87
|
+
arg = @arguments&.find { |a| a[:name] == arg_name.to_s }
|
88
|
+
completion = (arg && arg[:completion]) ? arg[:completion] : ModelContextProtocol::Server::NullCompletion
|
89
|
+
completion.call(arg_name.to_s, value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class MetadataDSL
|
94
|
+
def name(value = nil)
|
95
|
+
@name = value if value
|
96
|
+
@name
|
97
|
+
end
|
98
|
+
|
99
|
+
def description(value = nil)
|
100
|
+
@description = value if value
|
101
|
+
@description
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class ArgumentDSL
|
106
|
+
def name(value = nil)
|
107
|
+
@name = value if value
|
108
|
+
@name
|
109
|
+
end
|
110
|
+
|
111
|
+
def description(value = nil)
|
112
|
+
@description = value if value
|
113
|
+
@description
|
114
|
+
end
|
115
|
+
|
116
|
+
def required(value = nil)
|
117
|
+
@required = value unless value.nil?
|
118
|
+
@required
|
119
|
+
end
|
120
|
+
|
121
|
+
def completion(klass = nil)
|
122
|
+
@completion = klass unless klass.nil?
|
123
|
+
@completion
|
124
|
+
end
|
65
125
|
end
|
66
126
|
end
|
67
127
|
end
|
@@ -12,6 +12,7 @@ module ModelContextProtocol
|
|
12
12
|
def initialize
|
13
13
|
@prompts = []
|
14
14
|
@resources = []
|
15
|
+
@resource_templates = []
|
15
16
|
@tools = []
|
16
17
|
@prompts_options = {}
|
17
18
|
@resources_options = {}
|
@@ -28,6 +29,10 @@ module ModelContextProtocol
|
|
28
29
|
instance_eval(&block) if block
|
29
30
|
end
|
30
31
|
|
32
|
+
def resource_templates(&block)
|
33
|
+
instance_eval(&block) if block
|
34
|
+
end
|
35
|
+
|
31
36
|
def tools(options = {}, &block)
|
32
37
|
@tools_options = options
|
33
38
|
instance_eval(&block) if block
|
@@ -42,6 +47,8 @@ module ModelContextProtocol
|
|
42
47
|
@prompts << entry
|
43
48
|
when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Resource) }
|
44
49
|
@resources << entry
|
50
|
+
when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::ResourceTemplate) }
|
51
|
+
@resource_templates << entry
|
45
52
|
when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Tool) }
|
46
53
|
@tools << entry
|
47
54
|
else
|
@@ -58,6 +65,11 @@ module ModelContextProtocol
|
|
58
65
|
entry ? entry[:klass] : nil
|
59
66
|
end
|
60
67
|
|
68
|
+
def find_resource_template(uri)
|
69
|
+
entry = @resource_templates.find { |r| uri == r[:uriTemplate] }
|
70
|
+
entry ? entry[:klass] : nil
|
71
|
+
end
|
72
|
+
|
61
73
|
def find_tool(name)
|
62
74
|
find_by_name(@tools, name)
|
63
75
|
end
|
@@ -70,6 +82,10 @@ module ModelContextProtocol
|
|
70
82
|
ResourcesData[resources: @resources.map { |entry| entry.except(:klass) }]
|
71
83
|
end
|
72
84
|
|
85
|
+
def resource_templates_data
|
86
|
+
ResourceTemplatesData[resource_templates: @resource_templates.map { |entry| entry.except(:klass, :completions) }]
|
87
|
+
end
|
88
|
+
|
73
89
|
def tools_data
|
74
90
|
ToolsData[tools: @tools.map { |entry| entry.except(:klass) }]
|
75
91
|
end
|
@@ -88,6 +104,12 @@ module ModelContextProtocol
|
|
88
104
|
end
|
89
105
|
end
|
90
106
|
|
107
|
+
ResourceTemplatesData = Data.define(:resource_templates) do
|
108
|
+
def serialized
|
109
|
+
{resourceTemplates: resource_templates}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
91
113
|
ToolsData = Data.define(:tools) do
|
92
114
|
def serialized
|
93
115
|
{tools:}
|
@@ -2,37 +2,51 @@ module ModelContextProtocol
|
|
2
2
|
class Server::Resource
|
3
3
|
attr_reader :mime_type, :uri
|
4
4
|
|
5
|
+
def initialize
|
6
|
+
@mime_type = self.class.mime_type
|
7
|
+
@uri = self.class.uri
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
raise NotImplementedError, "Subclasses must implement the call method"
|
12
|
+
end
|
13
|
+
|
5
14
|
TextResponse = Data.define(:resource, :text) do
|
6
15
|
def serialized
|
7
16
|
{contents: [{mimeType: resource.mime_type, text:, uri: resource.uri}]}
|
8
17
|
end
|
9
18
|
end
|
19
|
+
private_constant :TextResponse
|
10
20
|
|
11
21
|
BinaryResponse = Data.define(:blob, :resource) do
|
12
22
|
def serialized
|
13
23
|
{contents: [{blob:, mimeType: resource.mime_type, uri: resource.uri}]}
|
14
24
|
end
|
15
25
|
end
|
26
|
+
private_constant :BinaryResponse
|
16
27
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
private def respond_with(type, **options)
|
29
|
+
case [type, options]
|
30
|
+
in [:text, {text:}]
|
31
|
+
TextResponse[resource: self, text:]
|
32
|
+
in [:binary, {blob:}]
|
33
|
+
BinaryResponse[blob:, resource: self]
|
34
|
+
else
|
35
|
+
raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{type}, #{options}"
|
36
|
+
end
|
24
37
|
end
|
25
38
|
|
26
39
|
class << self
|
27
40
|
attr_reader :name, :description, :mime_type, :uri
|
28
41
|
|
29
42
|
def with_metadata(&block)
|
30
|
-
|
43
|
+
metadata_dsl = MetadataDSL.new
|
44
|
+
metadata_dsl.instance_eval(&block)
|
31
45
|
|
32
|
-
@name =
|
33
|
-
@description =
|
34
|
-
@mime_type =
|
35
|
-
@uri =
|
46
|
+
@name = metadata_dsl.name
|
47
|
+
@description = metadata_dsl.description
|
48
|
+
@mime_type = metadata_dsl.mime_type
|
49
|
+
@uri = metadata_dsl.uri
|
36
50
|
end
|
37
51
|
|
38
52
|
def inherited(subclass)
|
@@ -47,7 +61,29 @@ module ModelContextProtocol
|
|
47
61
|
end
|
48
62
|
|
49
63
|
def metadata
|
50
|
-
{name: @name, description: @description,
|
64
|
+
{name: @name, description: @description, mimeType: @mime_type, uri: @uri}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class MetadataDSL
|
69
|
+
def name(value = nil)
|
70
|
+
@name = value if value
|
71
|
+
@name
|
72
|
+
end
|
73
|
+
|
74
|
+
def description(value = nil)
|
75
|
+
@description = value if value
|
76
|
+
@description
|
77
|
+
end
|
78
|
+
|
79
|
+
def mime_type(value = nil)
|
80
|
+
@mime_type = value if value
|
81
|
+
@mime_type
|
82
|
+
end
|
83
|
+
|
84
|
+
def uri(value = nil)
|
85
|
+
@uri = value if value
|
86
|
+
@uri
|
51
87
|
end
|
52
88
|
end
|
53
89
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module ModelContextProtocol
|
2
|
+
class Server::ResourceTemplate
|
3
|
+
class << self
|
4
|
+
attr_reader :name, :description, :mime_type, :uri_template, :completions
|
5
|
+
|
6
|
+
def with_metadata(&block)
|
7
|
+
metadata_dsl = MetadataDSL.new
|
8
|
+
metadata_dsl.instance_eval(&block)
|
9
|
+
|
10
|
+
@name = metadata_dsl.name
|
11
|
+
@description = metadata_dsl.description
|
12
|
+
@mime_type = metadata_dsl.mime_type
|
13
|
+
@uri_template = metadata_dsl.uri_template
|
14
|
+
@completions = metadata_dsl.completions
|
15
|
+
end
|
16
|
+
|
17
|
+
def inherited(subclass)
|
18
|
+
subclass.instance_variable_set(:@name, @name)
|
19
|
+
subclass.instance_variable_set(:@description, @description)
|
20
|
+
subclass.instance_variable_set(:@mime_type, @mime_type)
|
21
|
+
subclass.instance_variable_set(:@uri_template, @uri_template)
|
22
|
+
subclass.instance_variable_set(:@completions, @completions&.dup)
|
23
|
+
end
|
24
|
+
|
25
|
+
def complete_for(param_name, value)
|
26
|
+
completion = if @completions && @completions[param_name.to_s]
|
27
|
+
@completions[param_name.to_s]
|
28
|
+
else
|
29
|
+
ModelContextProtocol::Server::NullCompletion
|
30
|
+
end
|
31
|
+
|
32
|
+
completion.call(param_name.to_s, value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def metadata
|
36
|
+
{
|
37
|
+
name: @name,
|
38
|
+
description: @description,
|
39
|
+
mimeType: @mime_type,
|
40
|
+
uriTemplate: @uri_template,
|
41
|
+
completions: @completions&.transform_keys(&:to_s)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class MetadataDSL
|
47
|
+
attr_reader :completions
|
48
|
+
|
49
|
+
def initialize
|
50
|
+
@completions = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def name(value = nil)
|
54
|
+
@name = value if value
|
55
|
+
@name
|
56
|
+
end
|
57
|
+
|
58
|
+
def description(value = nil)
|
59
|
+
@description = value if value
|
60
|
+
@description
|
61
|
+
end
|
62
|
+
|
63
|
+
def mime_type(value = nil)
|
64
|
+
@mime_type = value if value
|
65
|
+
@mime_type
|
66
|
+
end
|
67
|
+
|
68
|
+
def uri_template(value = nil, &block)
|
69
|
+
@uri_template = value if value
|
70
|
+
|
71
|
+
if block_given?
|
72
|
+
completion_dsl = CompletionDSL.new
|
73
|
+
completion_dsl.instance_eval(&block)
|
74
|
+
@completions = completion_dsl.completions
|
75
|
+
end
|
76
|
+
|
77
|
+
@uri_template
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class CompletionDSL
|
82
|
+
attr_reader :completions
|
83
|
+
|
84
|
+
def initialize
|
85
|
+
@completions = {}
|
86
|
+
end
|
87
|
+
|
88
|
+
def completion(param_name, completion_class)
|
89
|
+
@completions[param_name.to_s] = completion_class
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -3,8 +3,9 @@ module ModelContextProtocol
|
|
3
3
|
# Raised when an invalid method is provided.
|
4
4
|
class MethodNotFoundError < StandardError; end
|
5
5
|
|
6
|
-
def initialize
|
6
|
+
def initialize(configuration: nil)
|
7
7
|
@handlers = {}
|
8
|
+
@configuration = configuration
|
8
9
|
end
|
9
10
|
|
10
11
|
def map(method, &handler)
|
@@ -16,7 +17,20 @@ module ModelContextProtocol
|
|
16
17
|
handler = @handlers[method]
|
17
18
|
raise MethodNotFoundError, "Method not found: #{method}" unless handler
|
18
19
|
|
19
|
-
|
20
|
+
with_environment(@configuration&.environment_variables) do
|
21
|
+
handler.call(message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def with_environment(vars)
|
28
|
+
original = ENV.to_h
|
29
|
+
vars&.each { |key, value| ENV[key] = value }
|
30
|
+
yield
|
31
|
+
ensure
|
32
|
+
ENV.clear
|
33
|
+
original.each { |key, value| ENV[key] = value }
|
20
34
|
end
|
21
35
|
end
|
22
36
|
end
|
@@ -4,11 +4,21 @@ module ModelContextProtocol
|
|
4
4
|
class Server::Tool
|
5
5
|
attr_reader :params
|
6
6
|
|
7
|
+
def initialize(params)
|
8
|
+
validate!(params)
|
9
|
+
@params = params
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
raise NotImplementedError, "Subclasses must implement the call method"
|
14
|
+
end
|
15
|
+
|
7
16
|
TextResponse = Data.define(:text) do
|
8
17
|
def serialized
|
9
18
|
{content: [{type: "text", text:}], isError: false}
|
10
19
|
end
|
11
20
|
end
|
21
|
+
private_constant :TextResponse
|
12
22
|
|
13
23
|
ImageResponse = Data.define(:data, :mime_type) do
|
14
24
|
def initialize(data:, mime_type: "image/png")
|
@@ -19,6 +29,7 @@ module ModelContextProtocol
|
|
19
29
|
{content: [{type: "image", data:, mimeType: mime_type}], isError: false}
|
20
30
|
end
|
21
31
|
end
|
32
|
+
private_constant :ImageResponse
|
22
33
|
|
23
34
|
ResourceResponse = Data.define(:uri, :text, :mime_type) do
|
24
35
|
def initialize(uri:, text:, mime_type: "text/plain")
|
@@ -29,20 +40,32 @@ module ModelContextProtocol
|
|
29
40
|
{content: [{type: "resource", resource: {uri:, mimeType: mime_type, text:}}], isError: false}
|
30
41
|
end
|
31
42
|
end
|
43
|
+
private_constant :ResourceResponse
|
32
44
|
|
33
45
|
ToolErrorResponse = Data.define(:text) do
|
34
46
|
def serialized
|
35
47
|
{content: [{type: "text", text:}], isError: true}
|
36
48
|
end
|
37
49
|
end
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
50
|
+
private_constant :ToolErrorResponse
|
51
|
+
|
52
|
+
private def respond_with(type, **options)
|
53
|
+
case [type, options]
|
54
|
+
in [:text, {text:}]
|
55
|
+
TextResponse[text:]
|
56
|
+
in [:image, {data:, mime_type:}]
|
57
|
+
ImageResponse[data:, mime_type:]
|
58
|
+
in [:image, {data:}]
|
59
|
+
ImageResponse[data:]
|
60
|
+
in [:resource, {mime_type:, text:, uri:}]
|
61
|
+
ResourceResponse[mime_type:, text:, uri:]
|
62
|
+
in [:resource, {text:, uri:}]
|
63
|
+
ResourceResponse[text:, uri:]
|
64
|
+
in [:error, {text:}]
|
65
|
+
ToolErrorResponse[text:]
|
66
|
+
else
|
67
|
+
raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{type}, #{options}"
|
68
|
+
end
|
46
69
|
end
|
47
70
|
|
48
71
|
private def validate!(params)
|
@@ -53,11 +76,12 @@ module ModelContextProtocol
|
|
53
76
|
attr_reader :name, :description, :input_schema
|
54
77
|
|
55
78
|
def with_metadata(&block)
|
56
|
-
|
79
|
+
metadata_dsl = MetadataDSL.new
|
80
|
+
metadata_dsl.instance_eval(&block)
|
57
81
|
|
58
|
-
@name =
|
59
|
-
@description =
|
60
|
-
@input_schema =
|
82
|
+
@name = metadata_dsl.name
|
83
|
+
@description = metadata_dsl.description
|
84
|
+
@input_schema = metadata_dsl.input_schema
|
61
85
|
end
|
62
86
|
|
63
87
|
def inherited(subclass)
|
@@ -68,8 +92,10 @@ module ModelContextProtocol
|
|
68
92
|
|
69
93
|
def call(params)
|
70
94
|
new(params).call
|
71
|
-
rescue JSON::Schema::ValidationError =>
|
72
|
-
raise ModelContextProtocol::Server::ParameterValidationError,
|
95
|
+
rescue JSON::Schema::ValidationError => validation_error
|
96
|
+
raise ModelContextProtocol::Server::ParameterValidationError, validation_error.message
|
97
|
+
rescue ModelContextProtocol::Server::ResponseArgumentsError => response_arguments_error
|
98
|
+
raise response_arguments_error
|
73
99
|
rescue => error
|
74
100
|
ToolErrorResponse[text: error.message]
|
75
101
|
end
|
@@ -78,5 +104,22 @@ module ModelContextProtocol
|
|
78
104
|
{name: @name, description: @description, inputSchema: @input_schema}
|
79
105
|
end
|
80
106
|
end
|
107
|
+
|
108
|
+
class MetadataDSL
|
109
|
+
def name(value = nil)
|
110
|
+
@name = value if value
|
111
|
+
@name
|
112
|
+
end
|
113
|
+
|
114
|
+
def description(value = nil)
|
115
|
+
@description = value if value
|
116
|
+
@description
|
117
|
+
end
|
118
|
+
|
119
|
+
def input_schema(&block)
|
120
|
+
@input_schema = instance_eval(&block) if block_given?
|
121
|
+
@input_schema
|
122
|
+
end
|
123
|
+
end
|
81
124
|
end
|
82
125
|
end
|
@@ -2,6 +2,9 @@ require "logger"
|
|
2
2
|
|
3
3
|
module ModelContextProtocol
|
4
4
|
class Server
|
5
|
+
# Raised when invalid response arguments are provided.
|
6
|
+
class ResponseArgumentsError < StandardError; end
|
7
|
+
|
5
8
|
# Raised when invalid parameters are provided.
|
6
9
|
class ParameterValidationError < StandardError; end
|
7
10
|
|
@@ -10,7 +13,7 @@ module ModelContextProtocol
|
|
10
13
|
def initialize
|
11
14
|
@configuration = Configuration.new
|
12
15
|
yield(@configuration) if block_given?
|
13
|
-
@router = Router.new
|
16
|
+
@router = Router.new(configuration:)
|
14
17
|
map_handlers
|
15
18
|
end
|
16
19
|
|
@@ -57,12 +60,45 @@ module ModelContextProtocol
|
|
57
60
|
PingResponse[]
|
58
61
|
end
|
59
62
|
|
63
|
+
router.map("completion/complete") do |message|
|
64
|
+
type = message["params"]["ref"]["type"]
|
65
|
+
|
66
|
+
completion_source = case type
|
67
|
+
when "ref/prompt"
|
68
|
+
name = message["params"]["ref"]["name"]
|
69
|
+
configuration.registry.find_prompt(name)
|
70
|
+
when "ref/resource"
|
71
|
+
uri = message["params"]["ref"]["uri"]
|
72
|
+
configuration.registry.find_resource_template(uri)
|
73
|
+
else
|
74
|
+
raise ModelContextProtocol::Server::ParameterValidationError, "ref/type invalid"
|
75
|
+
end
|
76
|
+
|
77
|
+
arg_name, arg_value = message["params"]["argument"].values_at("name", "value")
|
78
|
+
|
79
|
+
if completion_source
|
80
|
+
completion_source.complete_for(arg_name, arg_value)
|
81
|
+
else
|
82
|
+
ModelContextProtocol::Server::NullCompletion.call(arg_name, arg_value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
60
86
|
router.map("resources/list") do
|
61
87
|
configuration.registry.resources_data
|
62
88
|
end
|
63
89
|
|
64
90
|
router.map("resources/read") do |message|
|
65
|
-
|
91
|
+
uri = message["params"]["uri"]
|
92
|
+
resource = configuration.registry.find_resource(uri)
|
93
|
+
unless resource
|
94
|
+
raise ModelContextProtocol::Server::ParameterValidationError, "resource not found for #{uri}"
|
95
|
+
end
|
96
|
+
|
97
|
+
resource.call
|
98
|
+
end
|
99
|
+
|
100
|
+
router.map("resources/templates/list") do |message|
|
101
|
+
configuration.registry.resource_templates_data
|
66
102
|
end
|
67
103
|
|
68
104
|
router.map("prompts/list") do
|
@@ -84,6 +120,7 @@ module ModelContextProtocol
|
|
84
120
|
|
85
121
|
def build_capabilities
|
86
122
|
{}.tap do |capabilities|
|
123
|
+
capabilities[:completions] = {}
|
87
124
|
capabilities[:logging] = {} if configuration.logging_enabled?
|
88
125
|
|
89
126
|
registry = configuration.registry
|
@@ -91,7 +128,7 @@ module ModelContextProtocol
|
|
91
128
|
if registry.prompts_options.any? && !registry.instance_variable_get(:@prompts).empty?
|
92
129
|
capabilities[:prompts] = {
|
93
130
|
listChanged: registry.prompts_options[:list_changed]
|
94
|
-
}.compact
|
131
|
+
}.except(:completions).compact
|
95
132
|
end
|
96
133
|
|
97
134
|
if registry.resources_options.any? && !registry.instance_variable_get(:@resources).empty?
|
data/tasks/mcp.rake
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
namespace :mcp do
|
4
|
+
desc "Generate the development server executable with the correct Ruby path"
|
5
|
+
task :generate_executable do
|
6
|
+
destination_path = "bin/dev"
|
7
|
+
template_path = File.expand_path("templates/dev.erb", __dir__)
|
8
|
+
|
9
|
+
# Create directory if it doesn't exist
|
10
|
+
FileUtils.mkdir_p(File.dirname(destination_path))
|
11
|
+
|
12
|
+
# Get the Ruby path
|
13
|
+
ruby_path = detect_ruby_path
|
14
|
+
|
15
|
+
# Read and process the template
|
16
|
+
template = File.read(template_path)
|
17
|
+
content = template.gsub("<%= @ruby_path %>", ruby_path)
|
18
|
+
|
19
|
+
# Write the executable
|
20
|
+
File.write(destination_path, content)
|
21
|
+
|
22
|
+
# Set permissions
|
23
|
+
FileUtils.chmod(0o755, destination_path)
|
24
|
+
|
25
|
+
# Show success message
|
26
|
+
puts "\nCreated executable at: #{File.expand_path(destination_path)}"
|
27
|
+
puts "Using Ruby path: #{ruby_path}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def detect_ruby_path
|
31
|
+
# Get Ruby version from project config
|
32
|
+
ruby_version = get_project_ruby_version
|
33
|
+
|
34
|
+
if ruby_version && ruby_version.strip != ""
|
35
|
+
# Find the absolute path to the Ruby executable via ASDF
|
36
|
+
asdf_ruby_path = `asdf where ruby #{ruby_version}`.strip
|
37
|
+
|
38
|
+
if asdf_ruby_path && !asdf_ruby_path.empty? && File.directory?(asdf_ruby_path)
|
39
|
+
return File.join(asdf_ruby_path, "bin", "ruby")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Fallback to current Ruby
|
44
|
+
`which ruby`.strip
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_project_ruby_version
|
48
|
+
# Try ASDF first
|
49
|
+
if File.exist?(".tool-versions")
|
50
|
+
content = File.read(".tool-versions")
|
51
|
+
ruby_line = content.lines.find { |line| line.start_with?("ruby ") }
|
52
|
+
return ruby_line.split[1].strip if ruby_line
|
53
|
+
end
|
54
|
+
|
55
|
+
# Try .ruby-version file
|
56
|
+
if File.exist?(".ruby-version")
|
57
|
+
return File.read(".ruby-version").strip
|
58
|
+
end
|
59
|
+
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env <%= @ruby_path %>
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require_relative "../lib/model_context_protocol"
|
5
|
+
|
6
|
+
Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
|
7
|
+
|
8
|
+
server = ModelContextProtocol::Server.new do |config|
|
9
|
+
config.name = "MCP Development Server"
|
10
|
+
config.version = "1.0.0"
|
11
|
+
config.enable_log = true
|
12
|
+
config.registry = ModelContextProtocol::Server::Registry.new do
|
13
|
+
prompts list_changed: true do
|
14
|
+
register TestPrompt
|
15
|
+
end
|
16
|
+
|
17
|
+
resources list_changed: true, subscribe: true do
|
18
|
+
register TestResource
|
19
|
+
register TestBinaryResource
|
20
|
+
end
|
21
|
+
|
22
|
+
resource_templates do
|
23
|
+
register TestResourceTemplate
|
24
|
+
end
|
25
|
+
|
26
|
+
tools list_changed: true do
|
27
|
+
register TestToolWithTextResponse
|
28
|
+
register TestToolWithImageResponse
|
29
|
+
register TestToolWithImageResponseDefaultMimeType
|
30
|
+
register TestToolWithResourceResponse
|
31
|
+
register TestToolWithResourceResponseDefaultMimeType
|
32
|
+
register TestToolWithToolErrorResponse
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
server.start
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: model-context-protocol-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dick Davis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json-schema
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: addressable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.8'
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- dick@hey.com
|
@@ -42,14 +56,18 @@ files:
|
|
42
56
|
- Rakefile
|
43
57
|
- lib/model_context_protocol.rb
|
44
58
|
- lib/model_context_protocol/server.rb
|
59
|
+
- lib/model_context_protocol/server/completion.rb
|
45
60
|
- lib/model_context_protocol/server/configuration.rb
|
46
61
|
- lib/model_context_protocol/server/prompt.rb
|
47
62
|
- lib/model_context_protocol/server/registry.rb
|
48
63
|
- lib/model_context_protocol/server/resource.rb
|
64
|
+
- lib/model_context_protocol/server/resource_template.rb
|
49
65
|
- lib/model_context_protocol/server/router.rb
|
50
66
|
- lib/model_context_protocol/server/stdio_transport.rb
|
51
67
|
- lib/model_context_protocol/server/tool.rb
|
52
68
|
- lib/model_context_protocol/version.rb
|
69
|
+
- tasks/mcp.rake
|
70
|
+
- tasks/templates/dev.erb
|
53
71
|
homepage: https://github.com/dickdavis/model-context-protocol-rb
|
54
72
|
licenses:
|
55
73
|
- MIT
|