mcp 0.2.0 → 0.4.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 +2 -2
- data/.github/workflows/release.yml +1 -1
- data/.rubocop.yml +3 -0
- data/AGENTS.md +119 -0
- data/CHANGELOG.md +56 -0
- data/Gemfile +7 -4
- data/README.md +393 -50
- data/examples/http_client.rb +13 -13
- data/examples/http_server.rb +14 -12
- data/examples/stdio_server.rb +4 -4
- data/examples/streamable_http_client.rb +10 -12
- data/examples/streamable_http_server.rb +32 -32
- data/lib/mcp/client/http.rb +88 -0
- data/lib/mcp/client/tool.rb +16 -0
- data/lib/mcp/client.rb +116 -0
- data/lib/mcp/configuration.rb +31 -4
- data/lib/mcp/content.rb +0 -1
- data/lib/mcp/prompt/argument.rb +9 -5
- data/lib/mcp/prompt/message.rb +0 -1
- data/lib/mcp/prompt/result.rb +0 -1
- data/lib/mcp/prompt.rb +30 -3
- data/lib/mcp/resource/contents.rb +0 -1
- data/lib/mcp/resource/embedded.rb +0 -1
- data/lib/mcp/resource.rb +8 -7
- data/lib/mcp/resource_template.rb +8 -7
- data/lib/mcp/server/transports/streamable_http_transport.rb +21 -1
- data/lib/mcp/server.rb +43 -14
- data/lib/mcp/string_utils.rb +0 -1
- data/lib/mcp/tool/annotations.rb +4 -4
- data/lib/mcp/tool/input_schema.rb +10 -2
- data/lib/mcp/tool/output_schema.rb +69 -0
- data/lib/mcp/tool/response.rb +17 -5
- data/lib/mcp/tool.rb +47 -6
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +4 -0
- metadata +8 -3
- data/.cursor/rules/release-changelogs.mdc +0 -17
data/lib/mcp/prompt.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module MCP
|
@@ -6,22 +5,32 @@ module MCP
|
|
6
5
|
class << self
|
7
6
|
NOT_SET = Object.new
|
8
7
|
|
8
|
+
attr_reader :title_value
|
9
9
|
attr_reader :description_value
|
10
10
|
attr_reader :arguments_value
|
11
|
+
attr_reader :meta_value
|
11
12
|
|
12
13
|
def template(args, server_context: nil)
|
13
14
|
raise NotImplementedError, "Subclasses must implement template"
|
14
15
|
end
|
15
16
|
|
16
17
|
def to_h
|
17
|
-
{
|
18
|
+
{
|
19
|
+
name: name_value,
|
20
|
+
title: title_value,
|
21
|
+
description: description_value,
|
22
|
+
arguments: arguments_value&.map(&:to_h),
|
23
|
+
_meta: meta_value,
|
24
|
+
}.compact
|
18
25
|
end
|
19
26
|
|
20
27
|
def inherited(subclass)
|
21
28
|
super
|
22
29
|
subclass.instance_variable_set(:@name_value, nil)
|
30
|
+
subclass.instance_variable_set(:@title_value, nil)
|
23
31
|
subclass.instance_variable_set(:@description_value, nil)
|
24
32
|
subclass.instance_variable_set(:@arguments_value, nil)
|
33
|
+
subclass.instance_variable_set(:@meta_value, nil)
|
25
34
|
end
|
26
35
|
|
27
36
|
def prompt_name(value = NOT_SET)
|
@@ -36,6 +45,14 @@ module MCP
|
|
36
45
|
@name_value || StringUtils.handle_from_class_name(name)
|
37
46
|
end
|
38
47
|
|
48
|
+
def title(value = NOT_SET)
|
49
|
+
if value == NOT_SET
|
50
|
+
@title_value
|
51
|
+
else
|
52
|
+
@title_value = value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
39
56
|
def description(value = NOT_SET)
|
40
57
|
if value == NOT_SET
|
41
58
|
@description_value
|
@@ -52,14 +69,24 @@ module MCP
|
|
52
69
|
end
|
53
70
|
end
|
54
71
|
|
55
|
-
def
|
72
|
+
def meta(value = NOT_SET)
|
73
|
+
if value == NOT_SET
|
74
|
+
@meta_value
|
75
|
+
else
|
76
|
+
@meta_value = value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def define(name: nil, title: nil, description: nil, arguments: [], meta: nil, &block)
|
56
81
|
Class.new(self) do
|
57
82
|
prompt_name name
|
83
|
+
title title
|
58
84
|
description description
|
59
85
|
arguments arguments
|
60
86
|
define_singleton_method(:template) do |args, server_context: nil|
|
61
87
|
instance_exec(args, server_context:, &block)
|
62
88
|
end
|
89
|
+
meta meta
|
63
90
|
end
|
64
91
|
end
|
65
92
|
|
data/lib/mcp/resource.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module MCP
|
5
4
|
class Resource
|
6
|
-
attr_reader :uri, :name, :description, :mime_type
|
5
|
+
attr_reader :uri, :name, :title, :description, :mime_type
|
7
6
|
|
8
|
-
def initialize(uri:, name:, description: nil, mime_type: nil)
|
7
|
+
def initialize(uri:, name:, title: nil, description: nil, mime_type: nil)
|
9
8
|
@uri = uri
|
10
9
|
@name = name
|
10
|
+
@title = title
|
11
11
|
@description = description
|
12
12
|
@mime_type = mime_type
|
13
13
|
end
|
14
14
|
|
15
15
|
def to_h
|
16
16
|
{
|
17
|
-
uri:
|
18
|
-
name:
|
19
|
-
|
20
|
-
|
17
|
+
uri: uri,
|
18
|
+
name: name,
|
19
|
+
title: title,
|
20
|
+
description: description,
|
21
|
+
mimeType: mime_type,
|
21
22
|
}.compact
|
22
23
|
end
|
23
24
|
end
|
@@ -1,23 +1,24 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module MCP
|
5
4
|
class ResourceTemplate
|
6
|
-
attr_reader :uri_template, :name, :description, :mime_type
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :mime_type
|
7
6
|
|
8
|
-
def initialize(uri_template:, name:, description: nil, mime_type: nil)
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil)
|
9
8
|
@uri_template = uri_template
|
10
9
|
@name = name
|
10
|
+
@title = title
|
11
11
|
@description = description
|
12
12
|
@mime_type = mime_type
|
13
13
|
end
|
14
14
|
|
15
15
|
def to_h
|
16
16
|
{
|
17
|
-
uriTemplate:
|
18
|
-
name:
|
19
|
-
|
20
|
-
|
17
|
+
uriTemplate: uri_template,
|
18
|
+
name: name,
|
19
|
+
title: title,
|
20
|
+
description: description,
|
21
|
+
mimeType: mime_type,
|
21
22
|
}.compact
|
22
23
|
end
|
23
24
|
end
|
@@ -34,7 +34,13 @@ module MCP
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
def send_notification(
|
37
|
+
def send_notification(method, params = nil, session_id: nil)
|
38
|
+
notification = {
|
39
|
+
jsonrpc: "2.0",
|
40
|
+
method:,
|
41
|
+
}
|
42
|
+
notification[:params] = params if params
|
43
|
+
|
38
44
|
@mutex.synchronize do
|
39
45
|
if session_id
|
40
46
|
# Send to specific session
|
@@ -102,6 +108,8 @@ module MCP
|
|
102
108
|
|
103
109
|
if body["method"] == "initialize"
|
104
110
|
handle_initialization(body_string, body)
|
111
|
+
elsif notification?(body) || response?(body)
|
112
|
+
handle_accepted
|
105
113
|
else
|
106
114
|
handle_regular_request(body_string, session_id)
|
107
115
|
end
|
@@ -160,6 +168,14 @@ module MCP
|
|
160
168
|
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
|
161
169
|
end
|
162
170
|
|
171
|
+
def notification?(body)
|
172
|
+
!body["id"] && !!body["method"]
|
173
|
+
end
|
174
|
+
|
175
|
+
def response?(body)
|
176
|
+
!!body["id"] && !body["method"]
|
177
|
+
end
|
178
|
+
|
163
179
|
def handle_initialization(body_string, body)
|
164
180
|
session_id = SecureRandom.uuid
|
165
181
|
|
@@ -179,6 +195,10 @@ module MCP
|
|
179
195
|
[200, headers, [response]]
|
180
196
|
end
|
181
197
|
|
198
|
+
def handle_accepted
|
199
|
+
[202, {}, []]
|
200
|
+
end
|
201
|
+
|
182
202
|
def handle_regular_request(body_string, session_id)
|
183
203
|
# If session ID is provided, but not in the sessions hash, return an error
|
184
204
|
if session_id && !@sessions.key?(session_id)
|
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, meta: nil, &block)
|
100
|
+
tool = Tool.define(name:, title:, description:, input_schema:, annotations:, meta:, &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,11 +248,12 @@ 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)
|
225
|
-
@tools.map
|
256
|
+
@tools.values.map(&:to_h)
|
226
257
|
end
|
227
258
|
|
228
259
|
def call_tool(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)
|
@@ -262,7 +293,7 @@ module MCP
|
|
262
293
|
end
|
263
294
|
|
264
295
|
def list_prompts(request)
|
265
|
-
@prompts.map
|
296
|
+
@prompts.values.map(&:to_h)
|
266
297
|
end
|
267
298
|
|
268
299
|
def get_prompt(request)
|
@@ -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/string_utils.rb
CHANGED
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
|
@@ -45,7 +50,10 @@ module MCP
|
|
45
50
|
accept_uri: false,
|
46
51
|
accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
|
47
52
|
)
|
48
|
-
|
53
|
+
metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
|
54
|
+
# Converts metaschema to a file URI for cross-platform compatibility
|
55
|
+
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
|
56
|
+
metaschema = metaschema_uri.to_s
|
49
57
|
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
|
50
58
|
if errors.any?
|
51
59
|
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
@@ -0,0 +1,69 @@
|
|
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 :schema
|
11
|
+
|
12
|
+
def initialize(schema = {})
|
13
|
+
@schema = deep_transform_keys(JSON.parse(JSON.dump(schema)), &:to_sym)
|
14
|
+
@schema[:type] ||= "object"
|
15
|
+
validate_schema!
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
other.is_a?(OutputSchema) && schema == other.schema
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
@schema
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate_result(result)
|
27
|
+
errors = JSON::Validator.fully_validate(to_h, result)
|
28
|
+
if errors.any?
|
29
|
+
raise ValidationError, "Invalid result: #{errors.join(", ")}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def deep_transform_keys(schema, &block)
|
36
|
+
case schema
|
37
|
+
when Hash
|
38
|
+
schema.each_with_object({}) do |(key, value), result|
|
39
|
+
if key.casecmp?("$ref")
|
40
|
+
raise ArgumentError, "Invalid JSON Schema: $ref is not allowed in tool output schemas"
|
41
|
+
end
|
42
|
+
|
43
|
+
result[yield(key)] = deep_transform_keys(value, &block)
|
44
|
+
end
|
45
|
+
when Array
|
46
|
+
schema.map { |e| deep_transform_keys(e, &block) }
|
47
|
+
else
|
48
|
+
schema
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_schema!
|
53
|
+
schema = to_h
|
54
|
+
schema_reader = JSON::Schema::Reader.new(
|
55
|
+
accept_uri: false,
|
56
|
+
accept_file: ->(path) { path.to_s.start_with?(Gem.loaded_specs["json-schema"].full_gem_path) },
|
57
|
+
)
|
58
|
+
metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
|
59
|
+
# Converts metaschema to a file URI for cross-platform compatibility
|
60
|
+
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
|
61
|
+
metaschema = metaschema_uri.to_s
|
62
|
+
errors = JSON::Validator.fully_validate(metaschema, schema, schema_reader: schema_reader)
|
63
|
+
if errors.any?
|
64
|
+
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/mcp/tool/response.rb
CHANGED
@@ -3,15 +3,27 @@
|
|
3
3
|
module MCP
|
4
4
|
class Tool
|
5
5
|
class Response
|
6
|
-
|
6
|
+
NOT_GIVEN = Object.new.freeze
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
attr_reader :content, :structured_content
|
9
|
+
|
10
|
+
def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil)
|
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
|
15
|
+
|
16
|
+
@content = content || []
|
17
|
+
@error = error
|
18
|
+
@structured_content = structured_content
|
19
|
+
end
|
20
|
+
|
21
|
+
def error?
|
22
|
+
!!@error
|
11
23
|
end
|
12
24
|
|
13
25
|
def to_h
|
14
|
-
{ content:, isError:
|
26
|
+
{ content:, isError: error?, structuredContent: @structured_content }.compact
|
15
27
|
end
|
16
28
|
end
|
17
29
|
end
|
data/lib/mcp/tool.rb
CHANGED
@@ -5,30 +5,36 @@ 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
|
+
attr_reader :meta_value
|
11
12
|
|
12
13
|
def call(*args, server_context: nil)
|
13
14
|
raise NotImplementedError, "Subclasses must implement call"
|
14
15
|
end
|
15
16
|
|
16
17
|
def to_h
|
17
|
-
|
18
|
+
{
|
18
19
|
name: name_value,
|
20
|
+
title: title_value,
|
19
21
|
description: description_value,
|
20
22
|
inputSchema: input_schema_value.to_h,
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
outputSchema: @output_schema_value&.to_h,
|
24
|
+
annotations: annotations_value&.to_h,
|
25
|
+
_meta: meta_value,
|
26
|
+
}.compact
|
24
27
|
end
|
25
28
|
|
26
29
|
def inherited(subclass)
|
27
30
|
super
|
28
31
|
subclass.instance_variable_set(:@name_value, nil)
|
32
|
+
subclass.instance_variable_set(:@title_value, nil)
|
29
33
|
subclass.instance_variable_set(:@description_value, nil)
|
30
34
|
subclass.instance_variable_set(:@input_schema_value, nil)
|
35
|
+
subclass.instance_variable_set(:@output_schema_value, nil)
|
31
36
|
subclass.instance_variable_set(:@annotations_value, nil)
|
37
|
+
subclass.instance_variable_set(:@meta_value, nil)
|
32
38
|
end
|
33
39
|
|
34
40
|
def tool_name(value = NOT_SET)
|
@@ -43,6 +49,20 @@ module MCP
|
|
43
49
|
@name_value || StringUtils.handle_from_class_name(name)
|
44
50
|
end
|
45
51
|
|
52
|
+
def input_schema_value
|
53
|
+
@input_schema_value || InputSchema.new
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :output_schema_value
|
57
|
+
|
58
|
+
def title(value = NOT_SET)
|
59
|
+
if value == NOT_SET
|
60
|
+
@title_value
|
61
|
+
else
|
62
|
+
@title_value = value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
46
66
|
def description(value = NOT_SET)
|
47
67
|
if value == NOT_SET
|
48
68
|
@description_value
|
@@ -63,6 +83,24 @@ module MCP
|
|
63
83
|
end
|
64
84
|
end
|
65
85
|
|
86
|
+
def output_schema(value = NOT_SET)
|
87
|
+
if value == NOT_SET
|
88
|
+
output_schema_value
|
89
|
+
elsif value.is_a?(Hash)
|
90
|
+
@output_schema_value = OutputSchema.new(value)
|
91
|
+
elsif value.is_a?(OutputSchema)
|
92
|
+
@output_schema_value = value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def meta(value = NOT_SET)
|
97
|
+
if value == NOT_SET
|
98
|
+
@meta_value
|
99
|
+
else
|
100
|
+
@meta_value = value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
66
104
|
def annotations(hash = NOT_SET)
|
67
105
|
if hash == NOT_SET
|
68
106
|
@annotations_value
|
@@ -71,11 +109,14 @@ module MCP
|
|
71
109
|
end
|
72
110
|
end
|
73
111
|
|
74
|
-
def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
|
112
|
+
def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, meta: nil, annotations: nil, &block)
|
75
113
|
Class.new(self) do
|
76
114
|
tool_name name
|
115
|
+
title title
|
77
116
|
description description
|
78
117
|
input_schema input_schema
|
118
|
+
meta meta
|
119
|
+
output_schema output_schema
|
79
120
|
self.annotations(annotations) if annotations
|
80
121
|
define_singleton_method(:call, &block) if block
|
81
122
|
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
|