mcp 0.1.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/.cursor/rules/release-changelogs.mdc +11 -24
- data/.github/workflows/release.yml +25 -0
- data/.rubocop.yml +5 -3
- data/CHANGELOG.md +57 -0
- data/Gemfile +16 -6
- data/README.md +439 -61
- data/examples/README.md +197 -0
- data/examples/http_client.rb +184 -0
- data/examples/http_server.rb +171 -0
- data/examples/stdio_server.rb +6 -6
- data/examples/streamable_http_client.rb +203 -0
- data/examples/streamable_http_server.rb +173 -0
- 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 +22 -3
- data/lib/mcp/methods.rb +55 -33
- data/lib/mcp/prompt.rb +15 -4
- data/lib/mcp/resource.rb +8 -6
- data/lib/mcp/resource_template.rb +8 -6
- data/lib/mcp/server/capabilities.rb +96 -0
- data/lib/mcp/server/transports/stdio_transport.rb +57 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +301 -0
- data/lib/mcp/server.rb +116 -52
- data/lib/mcp/tool/annotations.rb +4 -4
- data/lib/mcp/tool/input_schema.rb +49 -1
- data/lib/mcp/tool/output_schema.rb +66 -0
- data/lib/mcp/tool/response.rb +15 -4
- data/lib/mcp/tool.rb +38 -7
- data/lib/mcp/transport.rb +16 -4
- data/lib/mcp/transports/stdio.rb +8 -28
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +20 -12
- data/mcp.gemspec +1 -2
- metadata +21 -24
data/lib/mcp/server.rb
CHANGED
@@ -20,24 +20,37 @@ module MCP
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
class MethodAlreadyDefinedError < StandardError
|
24
|
+
attr_reader :method_name
|
25
|
+
|
26
|
+
def initialize(method_name)
|
27
|
+
super("Method #{method_name} already defined")
|
28
|
+
@method_name = method_name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
23
32
|
include Instrumentation
|
24
33
|
|
25
|
-
|
26
|
-
attr_accessor :name, :version, :tools, :prompts, :resources, :server_context, :configuration
|
34
|
+
attr_accessor :name, :title, :version, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
|
27
35
|
|
28
36
|
def initialize(
|
29
37
|
name: "model_context_protocol",
|
38
|
+
title: nil,
|
30
39
|
version: DEFAULT_VERSION,
|
40
|
+
instructions: nil,
|
31
41
|
tools: [],
|
32
42
|
prompts: [],
|
33
43
|
resources: [],
|
34
44
|
resource_templates: [],
|
35
45
|
server_context: nil,
|
36
46
|
configuration: nil,
|
37
|
-
capabilities: nil
|
47
|
+
capabilities: nil,
|
48
|
+
transport: nil
|
38
49
|
)
|
39
50
|
@name = name
|
51
|
+
@title = title
|
40
52
|
@version = version
|
53
|
+
@instructions = instructions
|
41
54
|
@tools = tools.to_h { |t| [t.name_value, t] }
|
42
55
|
@prompts = prompts.to_h { |p| [p.name_value, p] }
|
43
56
|
@resources = resources
|
@@ -46,6 +59,10 @@ module MCP
|
|
46
59
|
@server_context = server_context
|
47
60
|
@configuration = MCP.configuration.merge(configuration)
|
48
61
|
|
62
|
+
validate!
|
63
|
+
|
64
|
+
@capabilities = capabilities || default_capabilities
|
65
|
+
|
49
66
|
@handlers = {
|
50
67
|
Methods::RESOURCES_LIST => method(:list_resources),
|
51
68
|
Methods::RESOURCES_READ => method(:read_resource_no_content),
|
@@ -56,6 +73,7 @@ module MCP
|
|
56
73
|
Methods::PROMPTS_GET => method(:get_prompt),
|
57
74
|
Methods::INITIALIZE => method(:init),
|
58
75
|
Methods::PING => ->(_) { {} },
|
76
|
+
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
|
59
77
|
|
60
78
|
# No op handlers for currently unsupported methods
|
61
79
|
Methods::RESOURCES_SUBSCRIBE => ->(_) {},
|
@@ -63,10 +81,7 @@ module MCP
|
|
63
81
|
Methods::COMPLETION_COMPLETE => ->(_) {},
|
64
82
|
Methods::LOGGING_SET_LEVEL => ->(_) {},
|
65
83
|
}
|
66
|
-
|
67
|
-
|
68
|
-
def capabilities
|
69
|
-
@capabilities ||= determine_capabilities
|
84
|
+
@transport = transport
|
70
85
|
end
|
71
86
|
|
72
87
|
def handle(request)
|
@@ -81,16 +96,50 @@ module MCP
|
|
81
96
|
end
|
82
97
|
end
|
83
98
|
|
84
|
-
def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
|
85
|
-
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)
|
86
101
|
@tools[tool.name_value] = tool
|
102
|
+
|
103
|
+
validate!
|
87
104
|
end
|
88
105
|
|
89
|
-
def define_prompt(name: nil, description: nil, arguments: [], &block)
|
90
|
-
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)
|
91
108
|
@prompts[prompt.name_value] = prompt
|
92
109
|
end
|
93
110
|
|
111
|
+
def define_custom_method(method_name:, &block)
|
112
|
+
if @handlers.key?(method_name)
|
113
|
+
raise MethodAlreadyDefinedError, method_name
|
114
|
+
end
|
115
|
+
|
116
|
+
@handlers[method_name] = block
|
117
|
+
end
|
118
|
+
|
119
|
+
def notify_tools_list_changed
|
120
|
+
return unless @transport
|
121
|
+
|
122
|
+
@transport.send_notification(Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED)
|
123
|
+
rescue => e
|
124
|
+
report_exception(e, { notification: "tools_list_changed" })
|
125
|
+
end
|
126
|
+
|
127
|
+
def notify_prompts_list_changed
|
128
|
+
return unless @transport
|
129
|
+
|
130
|
+
@transport.send_notification(Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED)
|
131
|
+
rescue => e
|
132
|
+
report_exception(e, { notification: "prompts_list_changed" })
|
133
|
+
end
|
134
|
+
|
135
|
+
def notify_resources_list_changed
|
136
|
+
return unless @transport
|
137
|
+
|
138
|
+
@transport.send_notification(Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED)
|
139
|
+
rescue => e
|
140
|
+
report_exception(e, { notification: "resources_list_changed" })
|
141
|
+
end
|
142
|
+
|
94
143
|
def resources_list_handler(&block)
|
95
144
|
@handlers[Methods::RESOURCES_LIST] = block
|
96
145
|
end
|
@@ -121,6 +170,25 @@ module MCP
|
|
121
170
|
|
122
171
|
private
|
123
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
|
+
|
124
192
|
def handle_request(request, method)
|
125
193
|
handler = @handlers[method]
|
126
194
|
unless handler
|
@@ -159,41 +227,36 @@ module MCP
|
|
159
227
|
}
|
160
228
|
end
|
161
229
|
|
162
|
-
def
|
163
|
-
defines_prompts = @prompts.any? || @handlers[Methods::PROMPTS_LIST] != method(:list_prompts)
|
164
|
-
defines_tools = @tools.any? || @handlers[Methods::TOOLS_LIST] != method(:list_tools)
|
165
|
-
defines_resources = @resources.any? || @handlers[Methods::RESOURCES_LIST] != method(:list_resources)
|
166
|
-
defines_resource_templates = @resource_templates.any? || @handlers[Methods::RESOURCES_TEMPLATES_LIST] != method(:list_resource_templates)
|
230
|
+
def default_capabilities
|
167
231
|
{
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
}
|
232
|
+
tools: { listChanged: true },
|
233
|
+
prompts: { listChanged: true },
|
234
|
+
resources: { listChanged: true },
|
235
|
+
}
|
172
236
|
end
|
173
237
|
|
174
238
|
def server_info
|
175
239
|
@server_info ||= {
|
176
240
|
name:,
|
241
|
+
title:,
|
177
242
|
version:,
|
178
|
-
}
|
243
|
+
}.compact
|
179
244
|
end
|
180
245
|
|
181
246
|
def init(request)
|
182
|
-
add_instrumentation_data(method: Methods::INITIALIZE)
|
183
247
|
{
|
184
248
|
protocolVersion: configuration.protocol_version,
|
185
249
|
capabilities: capabilities,
|
186
250
|
serverInfo: server_info,
|
187
|
-
|
251
|
+
instructions: instructions,
|
252
|
+
}.compact
|
188
253
|
end
|
189
254
|
|
190
255
|
def list_tools(request)
|
191
|
-
add_instrumentation_data(method: Methods::TOOLS_LIST)
|
192
256
|
@tools.map { |_, tool| tool.to_h }
|
193
257
|
end
|
194
258
|
|
195
259
|
def call_tool(request)
|
196
|
-
add_instrumentation_data(method: Methods::TOOLS_CALL)
|
197
260
|
tool_name = request[:name]
|
198
261
|
tool = tools[tool_name]
|
199
262
|
unless tool
|
@@ -201,7 +264,7 @@ module MCP
|
|
201
264
|
raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
|
202
265
|
end
|
203
266
|
|
204
|
-
arguments = request[:arguments]
|
267
|
+
arguments = request[:arguments] || {}
|
205
268
|
add_instrumentation_data(tool_name:)
|
206
269
|
|
207
270
|
if tool.input_schema&.missing_required_arguments?(arguments)
|
@@ -213,26 +276,27 @@ module MCP
|
|
213
276
|
)
|
214
277
|
end
|
215
278
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
tool.call(**arguments.transform_keys(&:to_sym)).to_h
|
279
|
+
if configuration.validate_tool_call_arguments && tool.input_schema
|
280
|
+
begin
|
281
|
+
tool.input_schema.validate_arguments(arguments)
|
282
|
+
rescue Tool::InputSchema::ValidationError => e
|
283
|
+
add_instrumentation_data(error: :invalid_schema)
|
284
|
+
raise RequestHandlerError.new(e.message, request, error_type: :invalid_schema)
|
223
285
|
end
|
286
|
+
end
|
287
|
+
|
288
|
+
begin
|
289
|
+
call_tool_with_args(tool, arguments)
|
224
290
|
rescue => e
|
225
291
|
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
|
226
292
|
end
|
227
293
|
end
|
228
294
|
|
229
295
|
def list_prompts(request)
|
230
|
-
add_instrumentation_data(method: Methods::PROMPTS_LIST)
|
231
296
|
@prompts.map { |_, prompt| prompt.to_h }
|
232
297
|
end
|
233
298
|
|
234
299
|
def get_prompt(request)
|
235
|
-
add_instrumentation_data(method: Methods::PROMPTS_GET)
|
236
300
|
prompt_name = request[:name]
|
237
301
|
prompt = @prompts[prompt_name]
|
238
302
|
unless prompt
|
@@ -245,25 +309,20 @@ module MCP
|
|
245
309
|
prompt_args = request[:arguments]
|
246
310
|
prompt.validate_arguments!(prompt_args)
|
247
311
|
|
248
|
-
prompt
|
312
|
+
call_prompt_template_with_args(prompt, prompt_args)
|
249
313
|
end
|
250
314
|
|
251
315
|
def list_resources(request)
|
252
|
-
add_instrumentation_data(method: Methods::RESOURCES_LIST)
|
253
|
-
|
254
316
|
@resources.map(&:to_h)
|
255
317
|
end
|
256
318
|
|
257
319
|
# Server implementation should set `resources_read_handler` to override no-op default
|
258
320
|
def read_resource_no_content(request)
|
259
|
-
add_instrumentation_data(method: Methods::RESOURCES_READ)
|
260
321
|
add_instrumentation_data(resource_uri: request[:uri])
|
261
322
|
[]
|
262
323
|
end
|
263
324
|
|
264
325
|
def list_resource_templates(request)
|
265
|
-
add_instrumentation_data(method: Methods::RESOURCES_TEMPLATES_LIST)
|
266
|
-
|
267
326
|
@resource_templates.map(&:to_h)
|
268
327
|
end
|
269
328
|
|
@@ -277,22 +336,27 @@ module MCP
|
|
277
336
|
end
|
278
337
|
end
|
279
338
|
|
280
|
-
def
|
281
|
-
|
282
|
-
|
339
|
+
def accepts_server_context?(method_object)
|
340
|
+
parameters = method_object.parameters
|
341
|
+
|
342
|
+
parameters.any? { |type, name| type == :keyrest || name == :server_context }
|
283
343
|
end
|
284
344
|
|
285
|
-
def
|
286
|
-
|
345
|
+
def call_tool_with_args(tool, arguments)
|
346
|
+
args = arguments&.transform_keys(&:to_sym) || {}
|
287
347
|
|
288
|
-
if
|
289
|
-
|
348
|
+
if accepts_server_context?(tool.method(:call))
|
349
|
+
tool.call(**args, server_context: server_context).to_h
|
350
|
+
else
|
351
|
+
tool.call(**args).to_h
|
352
|
+
end
|
353
|
+
end
|
290
354
|
|
291
|
-
|
292
|
-
|
293
|
-
|
355
|
+
def call_prompt_template_with_args(prompt, args)
|
356
|
+
if accepts_server_context?(prompt.method(:template))
|
357
|
+
prompt.template(args, server_context: server_context).to_h
|
294
358
|
else
|
295
|
-
|
359
|
+
prompt.template(args).to_h
|
296
360
|
end
|
297
361
|
end
|
298
362
|
end
|
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
|
@@ -1,17 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "json-schema"
|
4
|
+
|
3
5
|
module MCP
|
4
6
|
class Tool
|
5
7
|
class InputSchema
|
8
|
+
class ValidationError < StandardError; end
|
9
|
+
|
6
10
|
attr_reader :properties, :required
|
7
11
|
|
8
12
|
def initialize(properties: {}, required: [])
|
9
13
|
@properties = properties
|
10
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
|
11
20
|
end
|
12
21
|
|
13
22
|
def to_h
|
14
|
-
{ type: "object"
|
23
|
+
{ type: "object" }.tap do |hsh|
|
24
|
+
hsh[:properties] = properties if properties.any?
|
25
|
+
hsh[:required] = required if required.any?
|
26
|
+
end
|
15
27
|
end
|
16
28
|
|
17
29
|
def missing_required_arguments?(arguments)
|
@@ -21,6 +33,42 @@ module MCP
|
|
21
33
|
def missing_required_arguments(arguments)
|
22
34
|
(required - arguments.keys.map(&:to_sym))
|
23
35
|
end
|
36
|
+
|
37
|
+
def validate_arguments(arguments)
|
38
|
+
errors = JSON::Validator.fully_validate(to_h, arguments)
|
39
|
+
if errors.any?
|
40
|
+
raise ValidationError, "Invalid arguments: #{errors.join(", ")}"
|
41
|
+
end
|
42
|
+
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
|
24
72
|
end
|
25
73
|
end
|
26
74
|
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,29 +5,32 @@ 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
|
-
def call(*args, server_context:)
|
12
|
+
def call(*args, server_context: nil)
|
13
13
|
raise NotImplementedError, "Subclasses must implement call"
|
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/transport.rb
CHANGED
@@ -2,32 +2,44 @@
|
|
2
2
|
|
3
3
|
module MCP
|
4
4
|
class Transport
|
5
|
+
# Initialize the transport with the server instance
|
5
6
|
def initialize(server)
|
6
7
|
@server = server
|
7
8
|
end
|
8
9
|
|
10
|
+
# Send a response to the client
|
9
11
|
def send_response(response)
|
10
12
|
raise NotImplementedError, "Subclasses must implement send_response"
|
11
13
|
end
|
12
14
|
|
15
|
+
# Open the transport connection
|
13
16
|
def open
|
14
17
|
raise NotImplementedError, "Subclasses must implement open"
|
15
18
|
end
|
16
19
|
|
20
|
+
# Close the transport connection
|
17
21
|
def close
|
18
22
|
raise NotImplementedError, "Subclasses must implement close"
|
19
23
|
end
|
20
24
|
|
21
|
-
|
25
|
+
# Handle a JSON request
|
26
|
+
# Returns a response that should be sent back to the client
|
27
|
+
def handle_json_request(request)
|
28
|
+
response = @server.handle_json(request)
|
29
|
+
send_response(response) if response
|
30
|
+
end
|
22
31
|
|
32
|
+
# Handle an incoming request
|
33
|
+
# Returns a response that should be sent back to the client
|
23
34
|
def handle_request(request)
|
24
35
|
response = @server.handle(request)
|
25
36
|
send_response(response) if response
|
26
37
|
end
|
27
38
|
|
28
|
-
|
29
|
-
|
30
|
-
|
39
|
+
# Send a notification to the client
|
40
|
+
# Returns true if the notification was sent successfully
|
41
|
+
def send_notification(method, params = nil)
|
42
|
+
raise NotImplementedError, "Subclasses must implement send_notification"
|
31
43
|
end
|
32
44
|
end
|
33
45
|
end
|
data/lib/mcp/transports/stdio.rb
CHANGED
@@ -1,35 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../
|
4
|
-
|
3
|
+
require_relative "../server/transports/stdio_transport"
|
4
|
+
|
5
|
+
warn <<~MESSAGE, uplevel: 3
|
6
|
+
Use `require "mcp/server/transports/stdio_transport"` instead of `require "mcp/transports/stdio"`.
|
7
|
+
Also use `MCP::Server::Transports::StdioTransport` instead of `MCP::Transports::StdioTransport`.
|
8
|
+
This API is deprecated and will be removed in a future release.
|
9
|
+
MESSAGE
|
5
10
|
|
6
11
|
module MCP
|
7
12
|
module Transports
|
8
|
-
|
9
|
-
def initialize(server)
|
10
|
-
@server = server
|
11
|
-
@open = false
|
12
|
-
$stdin.set_encoding("UTF-8")
|
13
|
-
$stdout.set_encoding("UTF-8")
|
14
|
-
super
|
15
|
-
end
|
16
|
-
|
17
|
-
def open
|
18
|
-
@open = true
|
19
|
-
while @open && (line = $stdin.gets)
|
20
|
-
handle_json_request(line.strip)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def close
|
25
|
-
@open = false
|
26
|
-
end
|
27
|
-
|
28
|
-
def send_response(message)
|
29
|
-
json_message = message.is_a?(String) ? message : JSON.generate(message)
|
30
|
-
$stdout.puts(json_message)
|
31
|
-
$stdout.flush
|
32
|
-
end
|
33
|
-
end
|
13
|
+
StdioTransport = Server::Transports::StdioTransport
|
34
14
|
end
|
35
15
|
end
|