mcp 0.2.0 → 0.3.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.
data/lib/mcp/server.rb CHANGED
@@ -31,11 +31,13 @@ module MCP
31
31
 
32
32
  include Instrumentation
33
33
 
34
- attr_accessor :name, :version, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
34
+ attr_accessor :name, :title, :version, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
35
35
 
36
36
  def initialize(
37
37
  name: "model_context_protocol",
38
+ title: nil,
38
39
  version: DEFAULT_VERSION,
40
+ instructions: nil,
39
41
  tools: [],
40
42
  prompts: [],
41
43
  resources: [],
@@ -46,7 +48,9 @@ module MCP
46
48
  transport: nil
47
49
  )
48
50
  @name = name
51
+ @title = title
49
52
  @version = version
53
+ @instructions = instructions
50
54
  @tools = tools.to_h { |t| [t.name_value, t] }
51
55
  @prompts = prompts.to_h { |p| [p.name_value, p] }
52
56
  @resources = resources
@@ -54,6 +58,9 @@ module MCP
54
58
  @resource_index = index_resources_by_uri(resources)
55
59
  @server_context = server_context
56
60
  @configuration = MCP.configuration.merge(configuration)
61
+
62
+ validate!
63
+
57
64
  @capabilities = capabilities || default_capabilities
58
65
 
59
66
  @handlers = {
@@ -66,6 +73,7 @@ module MCP
66
73
  Methods::PROMPTS_GET => method(:get_prompt),
67
74
  Methods::INITIALIZE => method(:init),
68
75
  Methods::PING => ->(_) { {} },
76
+ Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
69
77
 
70
78
  # No op handlers for currently unsupported methods
71
79
  Methods::RESOURCES_SUBSCRIBE => ->(_) {},
@@ -88,13 +96,15 @@ module MCP
88
96
  end
89
97
  end
90
98
 
91
- def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
92
- tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
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)
93
101
  @tools[tool.name_value] = tool
102
+
103
+ validate!
94
104
  end
95
105
 
96
- def define_prompt(name: nil, description: nil, arguments: [], &block)
97
- prompt = Prompt.define(name:, description:, arguments:, &block)
106
+ def define_prompt(name: nil, title: nil, description: nil, arguments: [], &block)
107
+ prompt = Prompt.define(name:, title:, description:, arguments:, &block)
98
108
  @prompts[prompt.name_value] = prompt
99
109
  end
100
110
 
@@ -160,6 +170,25 @@ module MCP
160
170
 
161
171
  private
162
172
 
173
+ def validate!
174
+ if @configuration.protocol_version == "2024-11-05"
175
+ if @instructions
176
+ message = "`instructions` supported by protocol version 2025-03-26 or higher"
177
+ raise ArgumentError, message
178
+ end
179
+
180
+ error_tool_names = @tools.each_with_object([]) do |(tool_name, tool), error_tool_names|
181
+ if tool.annotations
182
+ error_tool_names << tool_name
183
+ end
184
+ end
185
+ unless error_tool_names.empty?
186
+ message = "Error occurred in #{error_tool_names.join(", ")}. `annotations` are supported by protocol version 2025-03-26 or higher"
187
+ raise ArgumentError, message
188
+ end
189
+ end
190
+ end
191
+
163
192
  def handle_request(request, method)
164
193
  handler = @handlers[method]
165
194
  unless handler
@@ -209,8 +238,9 @@ module MCP
209
238
  def server_info
210
239
  @server_info ||= {
211
240
  name:,
241
+ title:,
212
242
  version:,
213
- }
243
+ }.compact
214
244
  end
215
245
 
216
246
  def init(request)
@@ -218,7 +248,8 @@ module MCP
218
248
  protocolVersion: configuration.protocol_version,
219
249
  capabilities: capabilities,
220
250
  serverInfo: server_info,
221
- }
251
+ instructions: instructions,
252
+ }.compact
222
253
  end
223
254
 
224
255
  def list_tools(request)
@@ -233,7 +264,7 @@ module MCP
233
264
  raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
234
265
  end
235
266
 
236
- arguments = request[:arguments]
267
+ arguments = request[:arguments] || {}
237
268
  add_instrumentation_data(tool_name:)
238
269
 
239
270
  if tool.input_schema&.missing_required_arguments?(arguments)
@@ -307,14 +338,12 @@ module MCP
307
338
 
308
339
  def accepts_server_context?(method_object)
309
340
  parameters = method_object.parameters
310
- accepts_server_context = parameters.any? { |_type, name| name == :server_context }
311
- has_kwargs = parameters.any? { |type, _| type == :keyrest }
312
341
 
313
- accepts_server_context || has_kwargs
342
+ parameters.any? { |type, name| type == :keyrest || name == :server_context }
314
343
  end
315
344
 
316
345
  def call_tool_with_args(tool, arguments)
317
- args = arguments.transform_keys(&:to_sym)
346
+ args = arguments&.transform_keys(&:to_sym) || {}
318
347
 
319
348
  if accepts_server_context?(tool.method(:call))
320
349
  tool.call(**args, server_context: server_context).to_h
@@ -3,9 +3,9 @@
3
3
  module MCP
4
4
  class Tool
5
5
  class Annotations
6
- attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
6
+ attr_reader :destructive_hint, :idempotent_hint, :open_world_hint, :read_only_hint, :title
7
7
 
8
- def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
8
+ def initialize(destructive_hint: true, idempotent_hint: false, open_world_hint: true, read_only_hint: false, title: nil)
9
9
  @title = title
10
10
  @read_only_hint = read_only_hint
11
11
  @destructive_hint = destructive_hint
@@ -15,11 +15,11 @@ module MCP
15
15
 
16
16
  def to_h
17
17
  {
18
- title:,
19
- readOnlyHint: read_only_hint,
20
18
  destructiveHint: destructive_hint,
21
19
  idempotentHint: idempotent_hint,
22
20
  openWorldHint: open_world_hint,
21
+ readOnlyHint: read_only_hint,
22
+ title:,
23
23
  }.compact
24
24
  end
25
25
  end
@@ -15,8 +15,13 @@ module MCP
15
15
  validate_schema!
16
16
  end
17
17
 
18
+ def ==(other)
19
+ other.is_a?(InputSchema) && properties == other.properties && required == other.required
20
+ end
21
+
18
22
  def to_h
19
- { type: "object", properties: }.tap do |hsh|
23
+ { type: "object" }.tap do |hsh|
24
+ hsh[:properties] = properties if properties.any?
20
25
  hsh[:required] = required if required.any?
21
26
  end
22
27
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json-schema"
4
+
5
+ module MCP
6
+ class Tool
7
+ class OutputSchema
8
+ class ValidationError < StandardError; end
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
+ def validate_result(result)
30
+ errors = JSON::Validator.fully_validate(to_h, result)
31
+ if errors.any?
32
+ raise ValidationError, "Invalid result: #{errors.join(", ")}"
33
+ end
34
+ 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
+ end
65
+ end
66
+ end
@@ -3,15 +3,26 @@
3
3
  module MCP
4
4
  class Tool
5
5
  class Response
6
- attr_reader :content, :is_error
6
+ NOT_GIVEN = Object.new.freeze
7
+
8
+ attr_reader :content
9
+
10
+ def initialize(content, deprecated_error = NOT_GIVEN, error: false)
11
+ if deprecated_error != NOT_GIVEN
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
+ error = deprecated_error
14
+ end
7
15
 
8
- def initialize(content, is_error = false)
9
16
  @content = content
10
- @is_error = is_error
17
+ @error = error
18
+ end
19
+
20
+ def error?
21
+ !!@error
11
22
  end
12
23
 
13
24
  def to_h
14
- { content:, isError: is_error }.compact
25
+ { content:, isError: error? }.compact
15
26
  end
16
27
  end
17
28
  end
data/lib/mcp/tool.rb CHANGED
@@ -5,8 +5,8 @@ module MCP
5
5
  class << self
6
6
  NOT_SET = Object.new
7
7
 
8
+ attr_reader :title_value
8
9
  attr_reader :description_value
9
- attr_reader :input_schema_value
10
10
  attr_reader :annotations_value
11
11
 
12
12
  def call(*args, server_context: nil)
@@ -14,20 +14,23 @@ module MCP
14
14
  end
15
15
 
16
16
  def to_h
17
- result = {
17
+ {
18
18
  name: name_value,
19
+ title: title_value,
19
20
  description: description_value,
20
21
  inputSchema: input_schema_value.to_h,
21
- }
22
- result[:annotations] = annotations_value.to_h if annotations_value
23
- result
22
+ outputSchema: @output_schema_value&.to_h,
23
+ annotations: annotations_value&.to_h,
24
+ }.compact
24
25
  end
25
26
 
26
27
  def inherited(subclass)
27
28
  super
28
29
  subclass.instance_variable_set(:@name_value, nil)
30
+ subclass.instance_variable_set(:@title_value, nil)
29
31
  subclass.instance_variable_set(:@description_value, nil)
30
32
  subclass.instance_variable_set(:@input_schema_value, nil)
33
+ subclass.instance_variable_set(:@output_schema_value, nil)
31
34
  subclass.instance_variable_set(:@annotations_value, nil)
32
35
  end
33
36
 
@@ -43,6 +46,20 @@ module MCP
43
46
  @name_value || StringUtils.handle_from_class_name(name)
44
47
  end
45
48
 
49
+ def input_schema_value
50
+ @input_schema_value || InputSchema.new
51
+ end
52
+
53
+ attr_reader :output_schema_value
54
+
55
+ def title(value = NOT_SET)
56
+ if value == NOT_SET
57
+ @title_value
58
+ else
59
+ @title_value = value
60
+ end
61
+ end
62
+
46
63
  def description(value = NOT_SET)
47
64
  if value == NOT_SET
48
65
  @description_value
@@ -63,6 +80,18 @@ module MCP
63
80
  end
64
81
  end
65
82
 
83
+ def output_schema(value = NOT_SET)
84
+ if value == NOT_SET
85
+ output_schema_value
86
+ 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:)
90
+ elsif value.is_a?(OutputSchema)
91
+ @output_schema_value = value
92
+ end
93
+ end
94
+
66
95
  def annotations(hash = NOT_SET)
67
96
  if hash == NOT_SET
68
97
  @annotations_value
@@ -71,11 +100,13 @@ module MCP
71
100
  end
72
101
  end
73
102
 
74
- def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
103
+ def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, annotations: nil, &block)
75
104
  Class.new(self) do
76
105
  tool_name name
106
+ title title
77
107
  description description
78
108
  input_schema input_schema
109
+ output_schema output_schema
79
110
  self.annotations(annotations) if annotations
80
111
  define_singleton_method(:call, &block) if block
81
112
  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.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/mcp.rb CHANGED
@@ -18,10 +18,14 @@ require_relative "mcp/server/transports/stdio_transport"
18
18
  require_relative "mcp/string_utils"
19
19
  require_relative "mcp/tool"
20
20
  require_relative "mcp/tool/input_schema"
21
+ require_relative "mcp/tool/output_schema"
21
22
  require_relative "mcp/tool/response"
22
23
  require_relative "mcp/tool/annotations"
23
24
  require_relative "mcp/transport"
24
25
  require_relative "mcp/version"
26
+ require_relative "mcp/client"
27
+ require_relative "mcp/client/http"
28
+ require_relative "mcp/client/tool"
25
29
 
26
30
  module MCP
27
31
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -67,6 +67,9 @@ files:
67
67
  - examples/streamable_http_client.rb
68
68
  - examples/streamable_http_server.rb
69
69
  - lib/mcp.rb
70
+ - lib/mcp/client.rb
71
+ - lib/mcp/client/http.rb
72
+ - lib/mcp/client/tool.rb
70
73
  - lib/mcp/configuration.rb
71
74
  - lib/mcp/content.rb
72
75
  - lib/mcp/instrumentation.rb
@@ -87,6 +90,7 @@ files:
87
90
  - lib/mcp/tool.rb
88
91
  - lib/mcp/tool/annotations.rb
89
92
  - lib/mcp/tool/input_schema.rb
93
+ - lib/mcp/tool/output_schema.rb
90
94
  - lib/mcp/tool/response.rb
91
95
  - lib/mcp/transport.rb
92
96
  - lib/mcp/transports/stdio.rb
@@ -113,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
117
  - !ruby/object:Gem::Version
114
118
  version: '0'
115
119
  requirements: []
116
- rubygems_version: 3.6.7
120
+ rubygems_version: 3.6.9
117
121
  specification_version: 4
118
122
  summary: The official Ruby SDK for Model Context Protocol servers and clients
119
123
  test_files: []