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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/ci.yml +22 -7
  4. data/.github/workflows/release.yml +34 -2
  5. data/.rubocop.yml +3 -0
  6. data/AGENTS.md +107 -0
  7. data/CHANGELOG.md +58 -0
  8. data/Gemfile +6 -4
  9. data/README.md +135 -39
  10. data/RELEASE.md +12 -0
  11. data/bin/generate-gh-pages.sh +119 -0
  12. data/dev.yml +1 -2
  13. data/docs/_config.yml +6 -0
  14. data/docs/index.md +7 -0
  15. data/docs/latest/index.html +19 -0
  16. data/examples/http_server.rb +0 -2
  17. data/examples/stdio_server.rb +0 -1
  18. data/examples/streamable_http_server.rb +0 -2
  19. data/lib/json_rpc_handler.rb +151 -0
  20. data/lib/mcp/client/http.rb +23 -7
  21. data/lib/mcp/client.rb +62 -5
  22. data/lib/mcp/configuration.rb +38 -14
  23. data/lib/mcp/content.rb +2 -3
  24. data/lib/mcp/icon.rb +22 -0
  25. data/lib/mcp/instrumentation.rb +1 -1
  26. data/lib/mcp/methods.rb +3 -0
  27. data/lib/mcp/prompt/argument.rb +9 -5
  28. data/lib/mcp/prompt/message.rb +1 -2
  29. data/lib/mcp/prompt/result.rb +1 -2
  30. data/lib/mcp/prompt.rb +32 -4
  31. data/lib/mcp/resource/contents.rb +1 -2
  32. data/lib/mcp/resource/embedded.rb +1 -2
  33. data/lib/mcp/resource.rb +4 -3
  34. data/lib/mcp/resource_template.rb +4 -3
  35. data/lib/mcp/server/transports/streamable_http_transport.rb +96 -18
  36. data/lib/mcp/server.rb +92 -26
  37. data/lib/mcp/string_utils.rb +3 -4
  38. data/lib/mcp/tool/annotations.rb +1 -1
  39. data/lib/mcp/tool/input_schema.rb +6 -52
  40. data/lib/mcp/tool/output_schema.rb +3 -51
  41. data/lib/mcp/tool/response.rb +5 -4
  42. data/lib/mcp/tool/schema.rb +65 -0
  43. data/lib/mcp/tool.rb +47 -8
  44. data/lib/mcp/version.rb +1 -1
  45. data/lib/mcp.rb +2 -0
  46. data/mcp.gemspec +5 -2
  47. metadata +16 -18
  48. 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
- require "json_rpc_handler"
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:, title:, description:, input_schema:, annotations:, &block)
101
- @tools[tool.name_value] = tool
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:, title:, description:, arguments:, &block)
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
- name:,
241
- title:,
242
- version:,
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 { |_, tool| tool.to_h }
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
- raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
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
- raise RequestHandlerError.new(
273
- "Missing required arguments: #{tool.input_schema.missing_required_arguments(arguments).join(", ")}",
274
- request,
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
- raise RequestHandlerError.new(e.message, request, error_type: :invalid_schema)
339
+
340
+ return error_tool_response(e.message)
285
341
  end
286
342
  end
287
343
 
288
- begin
289
- call_tool_with_args(tool, arguments)
290
- rescue => e
291
- raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
292
- end
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 { |_, prompt| prompt.to_h }
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
 
@@ -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.dup
20
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
21
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
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
@@ -19,7 +19,7 @@ module MCP
19
19
  idempotentHint: idempotent_hint,
20
20
  openWorldHint: open_world_hint,
21
21
  readOnlyHint: read_only_hint,
22
- title:,
22
+ title: title,
23
23
  }.compact
24
24
  end
25
25
  end
@@ -1,74 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json-schema"
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
- (required - arguments.keys.map(&:to_sym))
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 = JSON::Validator.fully_validate(to_h, arguments)
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
- require "json-schema"
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 = JSON::Validator.fully_validate(to_h, result)
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
@@ -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:, isError: error? }.compact
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
- properties = value[:properties] || value["properties"] || {}
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
- properties = value[:properties] || value["properties"] || {}
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
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
- spec.required_ruby_version = ">= 3.2.0"
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