mcp 0.3.0 → 0.5.0
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/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +22 -7
- data/.github/workflows/release.yml +34 -2
- data/.rubocop.yml +3 -0
- data/AGENTS.md +107 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +6 -4
- data/README.md +135 -39
- data/RELEASE.md +12 -0
- data/bin/generate-gh-pages.sh +119 -0
- data/dev.yml +1 -2
- data/docs/_config.yml +6 -0
- data/docs/index.md +7 -0
- data/docs/latest/index.html +19 -0
- data/examples/http_server.rb +0 -2
- data/examples/stdio_server.rb +0 -1
- data/examples/streamable_http_server.rb +0 -2
- data/lib/json_rpc_handler.rb +151 -0
- data/lib/mcp/client/http.rb +23 -7
- data/lib/mcp/client.rb +62 -5
- data/lib/mcp/configuration.rb +38 -14
- data/lib/mcp/content.rb +2 -3
- data/lib/mcp/icon.rb +22 -0
- data/lib/mcp/instrumentation.rb +1 -1
- data/lib/mcp/methods.rb +3 -0
- data/lib/mcp/prompt/argument.rb +9 -5
- data/lib/mcp/prompt/message.rb +1 -2
- data/lib/mcp/prompt/result.rb +1 -2
- data/lib/mcp/prompt.rb +32 -4
- data/lib/mcp/resource/contents.rb +1 -2
- data/lib/mcp/resource/embedded.rb +1 -2
- data/lib/mcp/resource.rb +4 -3
- data/lib/mcp/resource_template.rb +4 -3
- data/lib/mcp/server/transports/streamable_http_transport.rb +96 -18
- data/lib/mcp/server.rb +92 -26
- data/lib/mcp/string_utils.rb +3 -4
- data/lib/mcp/tool/annotations.rb +1 -1
- data/lib/mcp/tool/input_schema.rb +6 -52
- data/lib/mcp/tool/output_schema.rb +3 -51
- data/lib/mcp/tool/response.rb +5 -4
- data/lib/mcp/tool/schema.rb +65 -0
- data/lib/mcp/tool.rb +47 -8
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- data/mcp.gemspec +5 -2
- metadata +16 -18
- data/.cursor/rules/release-changelogs.mdc +0 -17
data/lib/mcp/server.rb
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "../json_rpc_handler"
|
|
4
4
|
require_relative "instrumentation"
|
|
5
5
|
require_relative "methods"
|
|
6
6
|
|
|
7
7
|
module MCP
|
|
8
|
+
class ToolNotUnique < StandardError
|
|
9
|
+
def initialize(duplicated_tool_names)
|
|
10
|
+
super(<<~MESSAGE)
|
|
11
|
+
Tool names should be unique. Use `tool_name` to assign unique names to:
|
|
12
|
+
#{duplicated_tool_names.join(", ")}
|
|
13
|
+
MESSAGE
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
8
17
|
class Server
|
|
9
18
|
DEFAULT_VERSION = "0.1.0"
|
|
10
19
|
|
|
@@ -31,12 +40,15 @@ module MCP
|
|
|
31
40
|
|
|
32
41
|
include Instrumentation
|
|
33
42
|
|
|
34
|
-
attr_accessor :name, :title, :version, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
|
|
43
|
+
attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
|
|
35
44
|
|
|
36
45
|
def initialize(
|
|
46
|
+
description: nil,
|
|
47
|
+
icons: [],
|
|
37
48
|
name: "model_context_protocol",
|
|
38
49
|
title: nil,
|
|
39
50
|
version: DEFAULT_VERSION,
|
|
51
|
+
website_url: nil,
|
|
40
52
|
instructions: nil,
|
|
41
53
|
tools: [],
|
|
42
54
|
prompts: [],
|
|
@@ -47,10 +59,14 @@ module MCP
|
|
|
47
59
|
capabilities: nil,
|
|
48
60
|
transport: nil
|
|
49
61
|
)
|
|
62
|
+
@description = description
|
|
63
|
+
@icons = icons
|
|
50
64
|
@name = name
|
|
51
65
|
@title = title
|
|
52
66
|
@version = version
|
|
67
|
+
@website_url = website_url
|
|
53
68
|
@instructions = instructions
|
|
69
|
+
@tool_names = tools.map(&:name_value)
|
|
54
70
|
@tools = tools.to_h { |t| [t.name_value, t] }
|
|
55
71
|
@prompts = prompts.to_h { |p| [p.name_value, p] }
|
|
56
72
|
@resources = resources
|
|
@@ -80,6 +96,7 @@ module MCP
|
|
|
80
96
|
Methods::RESOURCES_UNSUBSCRIBE => ->(_) {},
|
|
81
97
|
Methods::COMPLETION_COMPLETE => ->(_) {},
|
|
82
98
|
Methods::LOGGING_SET_LEVEL => ->(_) {},
|
|
99
|
+
Methods::ELICITATION_CREATE => ->(_) {},
|
|
83
100
|
}
|
|
84
101
|
@transport = transport
|
|
85
102
|
end
|
|
@@ -96,16 +113,21 @@ module MCP
|
|
|
96
113
|
end
|
|
97
114
|
end
|
|
98
115
|
|
|
99
|
-
def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, &block)
|
|
100
|
-
tool = Tool.define(name
|
|
101
|
-
|
|
116
|
+
def define_tool(name: nil, title: nil, description: nil, input_schema: nil, annotations: nil, meta: nil, &block)
|
|
117
|
+
tool = Tool.define(name: name, title: title, description: description, input_schema: input_schema, annotations: annotations, meta: meta, &block)
|
|
118
|
+
tool_name = tool.name_value
|
|
119
|
+
|
|
120
|
+
@tool_names << tool_name
|
|
121
|
+
@tools[tool_name] = tool
|
|
102
122
|
|
|
103
123
|
validate!
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
def define_prompt(name: nil, title: nil, description: nil, arguments: [], &block)
|
|
107
|
-
prompt = Prompt.define(name
|
|
127
|
+
prompt = Prompt.define(name: name, title: title, description: description, arguments: arguments, &block)
|
|
108
128
|
@prompts[prompt.name_value] = prompt
|
|
129
|
+
|
|
130
|
+
validate!
|
|
109
131
|
end
|
|
110
132
|
|
|
111
133
|
def define_custom_method(method_name:, &block)
|
|
@@ -171,6 +193,30 @@ module MCP
|
|
|
171
193
|
private
|
|
172
194
|
|
|
173
195
|
def validate!
|
|
196
|
+
validate_tool_name!
|
|
197
|
+
|
|
198
|
+
# NOTE: The draft protocol version is the next version after 2025-11-25.
|
|
199
|
+
if @configuration.protocol_version <= "2025-06-18"
|
|
200
|
+
if server_info.key?(:description)
|
|
201
|
+
message = "Error occurred in server_info. `description` is not supported in protocol version 2025-06-18 or earlier"
|
|
202
|
+
raise ArgumentError, message
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if @configuration.protocol_version <= "2025-03-26"
|
|
207
|
+
if server_info.key?(:title) || server_info.key?(:websiteUrl)
|
|
208
|
+
message = "Error occurred in server_info. `title` or `website_url` are not supported in protocol version 2025-03-26 or earlier"
|
|
209
|
+
raise ArgumentError, message
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
primitive_titles = [@tools.values, @prompts.values, @resources, @resource_templates].flatten.map(&:title)
|
|
213
|
+
|
|
214
|
+
if primitive_titles.any?
|
|
215
|
+
message = "Error occurred in #{primitive_titles.join(", ")}. `title` is not supported in protocol version 2025-03-26 or earlier"
|
|
216
|
+
raise ArgumentError, message
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
174
220
|
if @configuration.protocol_version == "2024-11-05"
|
|
175
221
|
if @instructions
|
|
176
222
|
message = "`instructions` supported by protocol version 2025-03-26 or higher"
|
|
@@ -189,6 +235,12 @@ module MCP
|
|
|
189
235
|
end
|
|
190
236
|
end
|
|
191
237
|
|
|
238
|
+
def validate_tool_name!
|
|
239
|
+
duplicated_tool_names = @tool_names.tally.filter_map { |name, count| name if count >= 2 }
|
|
240
|
+
|
|
241
|
+
raise ToolNotUnique, duplicated_tool_names unless duplicated_tool_names.empty?
|
|
242
|
+
end
|
|
243
|
+
|
|
192
244
|
def handle_request(request, method)
|
|
193
245
|
handler = @handlers[method]
|
|
194
246
|
unless handler
|
|
@@ -237,9 +289,12 @@ module MCP
|
|
|
237
289
|
|
|
238
290
|
def server_info
|
|
239
291
|
@server_info ||= {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
292
|
+
description: description,
|
|
293
|
+
icons: icons,
|
|
294
|
+
name: name,
|
|
295
|
+
title: title,
|
|
296
|
+
version: version,
|
|
297
|
+
websiteUrl: website_url,
|
|
243
298
|
}.compact
|
|
244
299
|
end
|
|
245
300
|
|
|
@@ -253,27 +308,27 @@ module MCP
|
|
|
253
308
|
end
|
|
254
309
|
|
|
255
310
|
def list_tools(request)
|
|
256
|
-
@tools.map
|
|
311
|
+
@tools.values.map(&:to_h)
|
|
257
312
|
end
|
|
258
313
|
|
|
259
314
|
def call_tool(request)
|
|
260
315
|
tool_name = request[:name]
|
|
316
|
+
|
|
261
317
|
tool = tools[tool_name]
|
|
262
318
|
unless tool
|
|
263
|
-
add_instrumentation_data(error: :tool_not_found)
|
|
264
|
-
|
|
319
|
+
add_instrumentation_data(tool_name: tool_name, error: :tool_not_found)
|
|
320
|
+
|
|
321
|
+
return error_tool_response("Tool not found: #{tool_name}")
|
|
265
322
|
end
|
|
266
323
|
|
|
267
324
|
arguments = request[:arguments] || {}
|
|
268
|
-
add_instrumentation_data(tool_name:)
|
|
325
|
+
add_instrumentation_data(tool_name: tool_name)
|
|
269
326
|
|
|
270
327
|
if tool.input_schema&.missing_required_arguments?(arguments)
|
|
271
328
|
add_instrumentation_data(error: :missing_required_arguments)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
error_type: :missing_required_arguments,
|
|
276
|
-
)
|
|
329
|
+
|
|
330
|
+
missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
|
|
331
|
+
return error_tool_response("Missing required arguments: #{missing}")
|
|
277
332
|
end
|
|
278
333
|
|
|
279
334
|
if configuration.validate_tool_call_arguments && tool.input_schema
|
|
@@ -281,19 +336,20 @@ module MCP
|
|
|
281
336
|
tool.input_schema.validate_arguments(arguments)
|
|
282
337
|
rescue Tool::InputSchema::ValidationError => e
|
|
283
338
|
add_instrumentation_data(error: :invalid_schema)
|
|
284
|
-
|
|
339
|
+
|
|
340
|
+
return error_tool_response(e.message)
|
|
285
341
|
end
|
|
286
342
|
end
|
|
287
343
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
344
|
+
call_tool_with_args(tool, arguments)
|
|
345
|
+
rescue => e
|
|
346
|
+
report_exception(e, request: request)
|
|
347
|
+
|
|
348
|
+
error_tool_response("Internal error calling tool #{tool_name}: #{e.message}")
|
|
293
349
|
end
|
|
294
350
|
|
|
295
351
|
def list_prompts(request)
|
|
296
|
-
@prompts.map
|
|
352
|
+
@prompts.values.map(&:to_h)
|
|
297
353
|
end
|
|
298
354
|
|
|
299
355
|
def get_prompt(request)
|
|
@@ -304,7 +360,7 @@ module MCP
|
|
|
304
360
|
raise RequestHandlerError.new("Prompt not found #{prompt_name}", request, error_type: :prompt_not_found)
|
|
305
361
|
end
|
|
306
362
|
|
|
307
|
-
add_instrumentation_data(prompt_name:)
|
|
363
|
+
add_instrumentation_data(prompt_name: prompt_name)
|
|
308
364
|
|
|
309
365
|
prompt_args = request[:arguments]
|
|
310
366
|
prompt.validate_arguments!(prompt_args)
|
|
@@ -336,6 +392,16 @@ module MCP
|
|
|
336
392
|
end
|
|
337
393
|
end
|
|
338
394
|
|
|
395
|
+
def error_tool_response(text)
|
|
396
|
+
Tool::Response.new(
|
|
397
|
+
[{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: text,
|
|
400
|
+
}],
|
|
401
|
+
error: true,
|
|
402
|
+
).to_h
|
|
403
|
+
end
|
|
404
|
+
|
|
339
405
|
def accepts_server_context?(method_object)
|
|
340
406
|
parameters = method_object.parameters
|
|
341
407
|
|
data/lib/mcp/string_utils.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
@@ -16,9 +15,9 @@ module MCP
|
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
def underscore(camel_cased_word)
|
|
19
|
-
camel_cased_word
|
|
20
|
-
.gsub(/([A-Z]
|
|
21
|
-
.gsub(/([a-z\d])([A-Z])/,
|
|
18
|
+
camel_cased_word
|
|
19
|
+
.gsub(/(?<=[A-Z])(?=[A-Z][a-z])/, "_")
|
|
20
|
+
.gsub(/(?<=[a-z\d])(?=[A-Z])/, "_")
|
|
22
21
|
.tr("-", "_")
|
|
23
22
|
.downcase
|
|
24
23
|
end
|
data/lib/mcp/tool/annotations.rb
CHANGED
|
@@ -1,74 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "schema"
|
|
4
4
|
|
|
5
5
|
module MCP
|
|
6
6
|
class Tool
|
|
7
|
-
class InputSchema
|
|
7
|
+
class InputSchema < Schema
|
|
8
8
|
class ValidationError < StandardError; end
|
|
9
9
|
|
|
10
|
-
attr_reader :properties, :required
|
|
11
|
-
|
|
12
|
-
def initialize(properties: {}, required: [])
|
|
13
|
-
@properties = properties
|
|
14
|
-
@required = required.map(&:to_sym)
|
|
15
|
-
validate_schema!
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def ==(other)
|
|
19
|
-
other.is_a?(InputSchema) && properties == other.properties && required == other.required
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def to_h
|
|
23
|
-
{ type: "object" }.tap do |hsh|
|
|
24
|
-
hsh[:properties] = properties if properties.any?
|
|
25
|
-
hsh[:required] = required if required.any?
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
10
|
def missing_required_arguments?(arguments)
|
|
30
11
|
missing_required_arguments(arguments).any?
|
|
31
12
|
end
|
|
32
13
|
|
|
33
14
|
def missing_required_arguments(arguments)
|
|
34
|
-
|
|
15
|
+
return [] unless schema[:required].is_a?(Array)
|
|
16
|
+
|
|
17
|
+
(schema[:required] - arguments.keys.map(&:to_s))
|
|
35
18
|
end
|
|
36
19
|
|
|
37
20
|
def validate_arguments(arguments)
|
|
38
|
-
errors =
|
|
21
|
+
errors = fully_validate(arguments)
|
|
39
22
|
if errors.any?
|
|
40
23
|
raise ValidationError, "Invalid arguments: #{errors.join(", ")}"
|
|
41
24
|
end
|
|
42
25
|
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def validate_schema!
|
|
47
|
-
check_for_refs!
|
|
48
|
-
schema = to_h
|
|
49
|
-
schema_reader = JSON::Schema::Reader.new(
|
|
50
|
-
accept_uri: false,
|
|
51
|
-
accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
|
|
52
|
-
)
|
|
53
|
-
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
|
|
54
|
-
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
|
|
55
|
-
if errors.any?
|
|
56
|
-
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def check_for_refs!(obj = properties)
|
|
61
|
-
case obj
|
|
62
|
-
when Hash
|
|
63
|
-
if obj.key?("$ref") || obj.key?(:$ref)
|
|
64
|
-
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool input schemas"
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
obj.each_value { |value| check_for_refs!(value) }
|
|
68
|
-
when Array
|
|
69
|
-
obj.each { |item| check_for_refs!(item) }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
26
|
end
|
|
73
27
|
end
|
|
74
28
|
end
|
|
@@ -1,66 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "schema"
|
|
4
4
|
|
|
5
5
|
module MCP
|
|
6
6
|
class Tool
|
|
7
|
-
class OutputSchema
|
|
7
|
+
class OutputSchema < Schema
|
|
8
8
|
class ValidationError < StandardError; end
|
|
9
9
|
|
|
10
|
-
attr_reader :properties, :required
|
|
11
|
-
|
|
12
|
-
def initialize(properties: {}, required: [])
|
|
13
|
-
@properties = properties
|
|
14
|
-
@required = required.map(&:to_sym)
|
|
15
|
-
validate_schema!
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def ==(other)
|
|
19
|
-
other.is_a?(OutputSchema) && properties == other.properties && required == other.required
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def to_h
|
|
23
|
-
{ type: "object" }.tap do |hsh|
|
|
24
|
-
hsh[:properties] = properties if properties.any?
|
|
25
|
-
hsh[:required] = required if required.any?
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
10
|
def validate_result(result)
|
|
30
|
-
errors =
|
|
11
|
+
errors = fully_validate(result)
|
|
31
12
|
if errors.any?
|
|
32
13
|
raise ValidationError, "Invalid result: #{errors.join(", ")}"
|
|
33
14
|
end
|
|
34
15
|
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def validate_schema!
|
|
39
|
-
check_for_refs!
|
|
40
|
-
schema = to_h
|
|
41
|
-
schema_reader = JSON::Schema::Reader.new(
|
|
42
|
-
accept_uri: false,
|
|
43
|
-
accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
|
|
44
|
-
)
|
|
45
|
-
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
|
|
46
|
-
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
|
|
47
|
-
if errors.any?
|
|
48
|
-
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def check_for_refs!(obj = properties)
|
|
53
|
-
case obj
|
|
54
|
-
when Hash
|
|
55
|
-
if obj.key?("$ref") || obj.key?(:$ref)
|
|
56
|
-
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
obj.each_value { |value| check_for_refs!(value) }
|
|
60
|
-
when Array
|
|
61
|
-
obj.each { |item| check_for_refs!(item) }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
16
|
end
|
|
65
17
|
end
|
|
66
18
|
end
|
data/lib/mcp/tool/response.rb
CHANGED
|
@@ -5,16 +5,17 @@ module MCP
|
|
|
5
5
|
class Response
|
|
6
6
|
NOT_GIVEN = Object.new.freeze
|
|
7
7
|
|
|
8
|
-
attr_reader :content
|
|
8
|
+
attr_reader :content, :structured_content
|
|
9
9
|
|
|
10
|
-
def initialize(content, deprecated_error = NOT_GIVEN, error: false)
|
|
10
|
+
def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil)
|
|
11
11
|
if deprecated_error != NOT_GIVEN
|
|
12
12
|
warn("Passing `error` with the 2nd argument of `Response.new` is deprecated. Use keyword argument like `Response.new(content, error: error)` instead.", uplevel: 1)
|
|
13
13
|
error = deprecated_error
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
@content = content
|
|
16
|
+
@content = content || []
|
|
17
17
|
@error = error
|
|
18
|
+
@structured_content = structured_content
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def error?
|
|
@@ -22,7 +23,7 @@ module MCP
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def to_h
|
|
25
|
-
{ content
|
|
26
|
+
{ content: content, isError: error?, structuredContent: @structured_content }.compact
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json-schema"
|
|
4
|
+
|
|
5
|
+
module MCP
|
|
6
|
+
class Tool
|
|
7
|
+
class Schema
|
|
8
|
+
attr_reader :schema
|
|
9
|
+
|
|
10
|
+
def initialize(schema = {})
|
|
11
|
+
@schema = deep_transform_keys(JSON.parse(JSON.dump(schema)), &:to_sym)
|
|
12
|
+
@schema[:type] ||= "object"
|
|
13
|
+
validate_schema!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ==(other)
|
|
17
|
+
other.is_a?(self.class) && schema == other.schema
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
@schema
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def fully_validate(data)
|
|
27
|
+
JSON::Validator.fully_validate(to_h, data)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deep_transform_keys(schema, &block)
|
|
31
|
+
case schema
|
|
32
|
+
when Hash
|
|
33
|
+
schema.each_with_object({}) do |(key, value), result|
|
|
34
|
+
if key.casecmp?("$ref")
|
|
35
|
+
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool schemas"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result[yield(key)] = deep_transform_keys(value, &block)
|
|
39
|
+
end
|
|
40
|
+
when Array
|
|
41
|
+
schema.map { |e| deep_transform_keys(e, &block) }
|
|
42
|
+
else
|
|
43
|
+
schema
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_schema!
|
|
48
|
+
schema = to_h
|
|
49
|
+
gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
|
|
50
|
+
schema_reader = JSON::Schema::Reader.new(
|
|
51
|
+
accept_uri: false,
|
|
52
|
+
accept_file: ->(path) { File.realpath(path.to_s).start_with?(gem_path) },
|
|
53
|
+
)
|
|
54
|
+
metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
|
|
55
|
+
# Converts metaschema to a file URI for cross-platform compatibility
|
|
56
|
+
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
|
|
57
|
+
metaschema = metaschema_uri.to_s
|
|
58
|
+
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
|
|
59
|
+
if errors.any?
|
|
60
|
+
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/mcp/tool.rb
CHANGED
|
@@ -4,10 +4,13 @@ module MCP
|
|
|
4
4
|
class Tool
|
|
5
5
|
class << self
|
|
6
6
|
NOT_SET = Object.new
|
|
7
|
+
MAX_LENGTH_OF_NAME = 128
|
|
7
8
|
|
|
8
9
|
attr_reader :title_value
|
|
9
10
|
attr_reader :description_value
|
|
11
|
+
attr_reader :icons_value
|
|
10
12
|
attr_reader :annotations_value
|
|
13
|
+
attr_reader :meta_value
|
|
11
14
|
|
|
12
15
|
def call(*args, server_context: nil)
|
|
13
16
|
raise NotImplementedError, "Subclasses must implement call"
|
|
@@ -18,9 +21,11 @@ module MCP
|
|
|
18
21
|
name: name_value,
|
|
19
22
|
title: title_value,
|
|
20
23
|
description: description_value,
|
|
24
|
+
icons: icons&.map(&:to_h),
|
|
21
25
|
inputSchema: input_schema_value.to_h,
|
|
22
26
|
outputSchema: @output_schema_value&.to_h,
|
|
23
27
|
annotations: annotations_value&.to_h,
|
|
28
|
+
_meta: meta_value,
|
|
24
29
|
}.compact
|
|
25
30
|
end
|
|
26
31
|
|
|
@@ -29,9 +34,11 @@ module MCP
|
|
|
29
34
|
subclass.instance_variable_set(:@name_value, nil)
|
|
30
35
|
subclass.instance_variable_set(:@title_value, nil)
|
|
31
36
|
subclass.instance_variable_set(:@description_value, nil)
|
|
37
|
+
subclass.instance_variable_set(:@icons_value, nil)
|
|
32
38
|
subclass.instance_variable_set(:@input_schema_value, nil)
|
|
33
39
|
subclass.instance_variable_set(:@output_schema_value, nil)
|
|
34
40
|
subclass.instance_variable_set(:@annotations_value, nil)
|
|
41
|
+
subclass.instance_variable_set(:@meta_value, nil)
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
def tool_name(value = NOT_SET)
|
|
@@ -39,11 +46,13 @@ module MCP
|
|
|
39
46
|
name_value
|
|
40
47
|
else
|
|
41
48
|
@name_value = value
|
|
49
|
+
|
|
50
|
+
validate!
|
|
42
51
|
end
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
def name_value
|
|
46
|
-
@name_value || StringUtils.handle_from_class_name(name)
|
|
55
|
+
@name_value || (name.nil? ? nil : StringUtils.handle_from_class_name(name))
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
def input_schema_value
|
|
@@ -68,13 +77,19 @@ module MCP
|
|
|
68
77
|
end
|
|
69
78
|
end
|
|
70
79
|
|
|
80
|
+
def icons(value = NOT_SET)
|
|
81
|
+
if value == NOT_SET
|
|
82
|
+
@icons_value
|
|
83
|
+
else
|
|
84
|
+
@icons_value = value
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
71
88
|
def input_schema(value = NOT_SET)
|
|
72
89
|
if value == NOT_SET
|
|
73
90
|
input_schema_value
|
|
74
91
|
elsif value.is_a?(Hash)
|
|
75
|
-
|
|
76
|
-
required = value[:required] || value["required"] || []
|
|
77
|
-
@input_schema_value = InputSchema.new(properties:, required:)
|
|
92
|
+
@input_schema_value = InputSchema.new(value)
|
|
78
93
|
elsif value.is_a?(InputSchema)
|
|
79
94
|
@input_schema_value = value
|
|
80
95
|
end
|
|
@@ -84,14 +99,20 @@ module MCP
|
|
|
84
99
|
if value == NOT_SET
|
|
85
100
|
output_schema_value
|
|
86
101
|
elsif value.is_a?(Hash)
|
|
87
|
-
|
|
88
|
-
required = value[:required] || value["required"] || []
|
|
89
|
-
@output_schema_value = OutputSchema.new(properties:, required:)
|
|
102
|
+
@output_schema_value = OutputSchema.new(value)
|
|
90
103
|
elsif value.is_a?(OutputSchema)
|
|
91
104
|
@output_schema_value = value
|
|
92
105
|
end
|
|
93
106
|
end
|
|
94
107
|
|
|
108
|
+
def meta(value = NOT_SET)
|
|
109
|
+
if value == NOT_SET
|
|
110
|
+
@meta_value
|
|
111
|
+
else
|
|
112
|
+
@meta_value = value
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
95
116
|
def annotations(hash = NOT_SET)
|
|
96
117
|
if hash == NOT_SET
|
|
97
118
|
@annotations_value
|
|
@@ -100,15 +121,33 @@ module MCP
|
|
|
100
121
|
end
|
|
101
122
|
end
|
|
102
123
|
|
|
103
|
-
def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, annotations: nil, &block)
|
|
124
|
+
def define(name: nil, title: nil, description: nil, icons: [], input_schema: nil, output_schema: nil, meta: nil, annotations: nil, &block)
|
|
104
125
|
Class.new(self) do
|
|
105
126
|
tool_name name
|
|
106
127
|
title title
|
|
107
128
|
description description
|
|
129
|
+
icons icons
|
|
108
130
|
input_schema input_schema
|
|
131
|
+
meta meta
|
|
109
132
|
output_schema output_schema
|
|
110
133
|
self.annotations(annotations) if annotations
|
|
111
134
|
define_singleton_method(:call, &block) if block
|
|
135
|
+
end.tap(&:validate!)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# It complies with the following tool name specification:
|
|
139
|
+
# https://modelcontextprotocol.io/specification/latest/server/tools#tool-names
|
|
140
|
+
def validate!
|
|
141
|
+
return true unless tool_name
|
|
142
|
+
|
|
143
|
+
if tool_name.empty? || tool_name.length > MAX_LENGTH_OF_NAME
|
|
144
|
+
raise ArgumentError, "Tool names should be between 1 and 128 characters in length (inclusive)."
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
unless tool_name.match?(/\A[A-Za-z\d_\-\.]+\z/)
|
|
148
|
+
raise ArgumentError, <<~MESSAGE
|
|
149
|
+
Tool names only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.).
|
|
150
|
+
MESSAGE
|
|
112
151
|
end
|
|
113
152
|
end
|
|
114
153
|
end
|
data/lib/mcp/version.rb
CHANGED
data/lib/mcp.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "json_rpc_handler"
|
|
3
4
|
require_relative "mcp/configuration"
|
|
4
5
|
require_relative "mcp/content"
|
|
6
|
+
require_relative "mcp/icon"
|
|
5
7
|
require_relative "mcp/instrumentation"
|
|
6
8
|
require_relative "mcp/methods"
|
|
7
9
|
require_relative "mcp/prompt"
|
data/mcp.gemspec
CHANGED
|
@@ -13,11 +13,15 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
spec.homepage = "https://github.com/modelcontextprotocol/ruby-sdk"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Since this library is used by a broad range of users, it does not align its support policy with Ruby's EOL.
|
|
17
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
17
18
|
|
|
18
19
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v#{spec.version}"
|
|
19
21
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
22
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
23
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
24
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/mcp"
|
|
21
25
|
|
|
22
26
|
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
|
23
27
|
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
@@ -27,6 +31,5 @@ Gem::Specification.new do |spec|
|
|
|
27
31
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
28
32
|
spec.require_paths = ["lib"]
|
|
29
33
|
|
|
30
|
-
spec.add_dependency("json_rpc_handler", "~> 0.1")
|
|
31
34
|
spec.add_dependency("json-schema", ">= 4.1")
|
|
32
35
|
end
|