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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +7 -4
- data/README.md +330 -47
- data/examples/http_client.rb +13 -13
- data/examples/http_server.rb +14 -11
- data/examples/stdio_server.rb +4 -3
- data/examples/streamable_http_client.rb +10 -12
- data/examples/streamable_http_server.rb +32 -31
- data/lib/mcp/client/http.rb +88 -0
- data/lib/mcp/client/tool.rb +16 -0
- data/lib/mcp/client.rb +88 -0
- data/lib/mcp/configuration.rb +6 -1
- data/lib/mcp/prompt.rb +13 -2
- data/lib/mcp/resource.rb +8 -6
- data/lib/mcp/resource_template.rb +8 -6
- data/lib/mcp/server/transports/streamable_http_transport.rb +13 -1
- data/lib/mcp/server.rb +41 -12
- data/lib/mcp/tool/annotations.rb +4 -4
- data/lib/mcp/tool/input_schema.rb +6 -1
- data/lib/mcp/tool/output_schema.rb +66 -0
- data/lib/mcp/tool/response.rb +15 -4
- data/lib/mcp/tool.rb +37 -6
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +4 -0
- metadata +6 -2
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
|
-
|
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
|
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
|
data/lib/mcp/tool/annotations.rb
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
module MCP
|
4
4
|
class Tool
|
5
5
|
class Annotations
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :destructive_hint, :idempotent_hint, :open_world_hint, :read_only_hint, :title
|
7
7
|
|
8
|
-
def initialize(
|
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"
|
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
|
data/lib/mcp/tool/response.rb
CHANGED
@@ -3,15 +3,26 @@
|
|
3
3
|
module MCP
|
4
4
|
class Tool
|
5
5
|
class Response
|
6
|
-
|
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
|
-
@
|
17
|
+
@error = error
|
18
|
+
end
|
19
|
+
|
20
|
+
def error?
|
21
|
+
!!@error
|
11
22
|
end
|
12
23
|
|
13
24
|
def to_h
|
14
|
-
{ content:, isError:
|
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
|
-
|
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
|
-
|
23
|
-
|
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
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.
|
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.
|
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: []
|