Skip to content

Commit b663e4e

Browse files
committed
[WIP] refactor to configuration classes
tests stuff
1 parent 98646ab commit b663e4e

20 files changed

+273
-513
lines changed

lib/mcp/prompt.rb

Lines changed: 19 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,36 @@
22
# frozen_string_literal: true
33

44
module MCP
5-
class Prompt
6-
class << self
7-
NOT_SET = Object.new
8-
9-
attr_reader :description_value
10-
attr_reader :arguments_value
5+
module Prompt
6+
attr_reader :name, :description, :arguments, :to_h
117

12-
def template(args, server_context:)
13-
raise NotImplementedError, "Subclasses must implement template"
14-
end
8+
class << self
9+
def define(...) = new(...)
10+
private :new
1511

16-
def to_h
17-
{ name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
18-
end
12+
private
1913

2014
def inherited(subclass)
2115
super
22-
subclass.instance_variable_set(:@name_value, nil)
23-
subclass.instance_variable_set(:@description_value, nil)
24-
subclass.instance_variable_set(:@arguments_value, nil)
25-
end
26-
27-
def prompt_name(value = NOT_SET)
28-
if value == NOT_SET
29-
@name_value
30-
else
31-
@name_value = value
32-
end
33-
end
34-
35-
def name_value
36-
@name_value || StringUtils.handle_from_class_name(name)
37-
end
38-
39-
def description(value = NOT_SET)
40-
if value == NOT_SET
41-
@description_value
42-
else
43-
@description_value = value
44-
end
45-
end
46-
47-
def arguments(value = NOT_SET)
48-
if value == NOT_SET
49-
@arguments_value
50-
else
51-
@arguments_value = value
52-
end
16+
raise TypeError, "#{self} should no longer be subclassed. Use #{self}.define factory method instead."
5317
end
18+
end
5419

55-
def define(name: nil, description: nil, arguments: [], &block)
56-
Class.new(self) do
57-
prompt_name name
58-
description description
59-
arguments arguments
60-
define_singleton_method(:template) do |args, server_context:|
61-
instance_exec(args, server_context:, &block)
62-
end
63-
end
64-
end
20+
def initialize(name:, description:, arguments:, &block)
21+
arguments = arguments.map { |arg| Hash === arg ? Argument.new(**arg) : arg }
6522

66-
def validate_arguments!(args)
67-
missing = required_args - args.keys
68-
return if missing.empty?
23+
@name = name
24+
@description = description
25+
@arguments = arguments
26+
@block = block
6927

70-
raise MCP::Server::RequestHandlerError.new(
71-
"Missing required arguments: #{missing.join(", ")}", nil, error_type: :missing_required_arguments
72-
)
73-
end
28+
@to_h = { name:, description:, arguments: arguments.map(&:to_h) }.compact.freeze
7429

75-
private
30+
freeze
31+
end
7632

77-
def required_args
78-
arguments_value.filter_map { |arg| arg.name.to_sym if arg.required }
79-
end
33+
def call(args, server_context:)
34+
@block.call(args, server_context:)
8035
end
8136
end
8237
end

lib/mcp/prompt/argument.rb

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22
# frozen_string_literal: true
33

44
module MCP
5-
class Prompt
5+
module Prompt
66
class Argument
7-
attr_reader :name, :description, :required, :arguments
7+
attr_reader :name, :description, :required, :to_h
88

99
def initialize(name:, description: nil, required: false)
1010
@name = name
1111
@description = description
1212
@required = required
13-
@arguments = arguments
14-
end
1513

16-
def to_h
17-
{ name:, description:, required: }.compact
14+
@to_h = {
15+
name:,
16+
description:,
17+
required:,
18+
}.compact.freeze
19+
20+
freeze
1821
end
1922
end
2023
end

lib/mcp/prompt/message.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# frozen_string_literal: true
33

44
module MCP
5-
class Prompt
5+
module Prompt
66
class Message
77
attr_reader :role, :content
88

lib/mcp/prompt/result.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# frozen_string_literal: true
33

44
module MCP
5-
class Prompt
5+
module Prompt
66
class Result
77
attr_reader :description, :messages
88

lib/mcp/resource/contents.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,25 @@ class TextContents < Contents
2020
attr_reader :text
2121

2222
def initialize(text:, uri:, mime_type:)
23-
super(uri: uri, mime_type: mime_type)
23+
super(uri:, mime_type:)
2424
@text = text
2525
end
2626

2727
def to_h
28-
super.merge(text: text)
28+
super.merge(text:)
2929
end
3030
end
3131

3232
class BlobContents < Contents
3333
attr_reader :data
3434

3535
def initialize(data:, uri:, mime_type:)
36-
super(uri: uri, mime_type: mime_type)
36+
super(uri:, mime_type:)
3737
@data = data
3838
end
3939

4040
def to_h
41-
super.merge(data: data)
41+
super.merge(data:)
4242
end
4343
end
4444
end

lib/mcp/resource/embedded.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Embedded
77
attr_reader :resource, :annotations
88

99
def initialize(resource:, annotations: nil)
10+
@resource = resource
1011
@annotations = annotations
1112
end
1213

lib/mcp/resource_template.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@
33

44
module MCP
55
class ResourceTemplate
6-
attr_reader :uri_template, :name, :description, :mime_type
6+
attr_reader :uri_template, :name, :description, :mime_type, :to_h
77

88
def initialize(uri_template:, name:, description: nil, mime_type: nil)
99
@uri_template = uri_template
1010
@name = name
1111
@description = description
1212
@mime_type = mime_type
13-
end
1413

15-
def to_h
16-
{
14+
@to_h = {
1715
uriTemplate: @uri_template,
1816
name: @name,
1917
description: @description,
2018
mimeType: @mime_type,
21-
}.compact
19+
}.compact.freeze
20+
21+
freeze
2222
end
2323
end
2424
end

lib/mcp/server.rb

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ class Server
1111

1212
class RequestHandlerError < StandardError
1313
attr_reader :error_type
14-
attr_reader :original_error
1514

16-
def initialize(message, request, error_type: :internal_error, original_error: nil)
15+
def initialize(message, request, error_type: :internal_error)
1716
super(message)
1817
@request = request
1918
@error_type = error_type
20-
@original_error = original_error
2119
end
2220
end
2321

@@ -39,8 +37,8 @@ def initialize(
3937
)
4038
@name = name
4139
@version = version
42-
@tools = tools.to_h { |t| [t.name_value, t] }
43-
@prompts = prompts.to_h { |p| [p.name_value, p] }
40+
@tools = tools.to_h { |t| [t.name, t] }
41+
@prompts = prompts.to_h { |p| [p.name, p] }
4442
@resources = resources
4543
@resource_templates = resource_templates
4644
@resource_index = index_resources_by_uri(resources)
@@ -88,12 +86,12 @@ def handle_json(request)
8886

8987
def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
9088
tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
91-
@tools[tool.name_value] = tool
89+
@tools[tool.name] = tool
9290
end
9391

9492
def define_prompt(name: nil, description: nil, arguments: [], &block)
9593
prompt = Prompt.define(name:, description:, arguments:, &block)
96-
@prompts[prompt.name_value] = prompt
94+
@prompts[prompt.name] = prompt
9795
end
9896

9997
def resources_list_handler(&block)
@@ -156,14 +154,14 @@ def handle_request(request, method)
156154
@handlers[method].call(params)
157155
end
158156
rescue => e
159-
report_exception(e, { request: request })
157+
report_exception(e, { request: })
160158
if e.is_a?(RequestHandlerError)
161159
add_instrumentation_data(error: e.error_type)
162160
raise e
163161
end
164162

165163
add_instrumentation_data(error: :internal_error)
166-
raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
164+
raise RequestHandlerError.new("Internal error handling #{method} request", request)
167165
end
168166
}
169167
end
@@ -201,28 +199,24 @@ def call_tool(request)
201199
arguments = request[:arguments]
202200
add_instrumentation_data(tool_name:)
203201

204-
if tool.input_schema&.missing_required_arguments?(arguments)
205-
add_instrumentation_data(error: :missing_required_arguments)
206-
raise RequestHandlerError.new(
207-
"Missing required arguments: #{tool.input_schema.missing_required_arguments(arguments).join(", ")}",
208-
request,
209-
error_type: :missing_required_arguments,
210-
)
211-
end
202+
validate_tool_arguments!(tool, arguments, request)
212203

213204
begin
214-
call_params = tool_call_parameters(tool)
215-
216-
if call_params.include?(:server_context)
217-
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
218-
else
219-
tool.call(**arguments.transform_keys(&:to_sym)).to_h
220-
end
221-
rescue => e
222-
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
205+
tool.call(arguments.transform_keys(&:to_sym), server_context:).to_h
206+
rescue
207+
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request)
223208
end
224209
end
225210

211+
def validate_tool_arguments!(tool, arguments, request)
212+
input_schema = tool.input_schema
213+
return unless input_schema
214+
215+
missing_arguments = input_schema.required - arguments.keys.map(&:to_sym)
216+
217+
missing_required_arguments!(missing_arguments, request) unless missing_arguments.empty?
218+
end
219+
226220
def list_prompts(request)
227221
add_instrumentation_data(method: Methods::PROMPTS_LIST)
228222
@prompts.map { |_, prompt| prompt.to_h }
@@ -240,9 +234,31 @@ def get_prompt(request)
240234
add_instrumentation_data(prompt_name:)
241235

242236
prompt_args = request[:arguments]
243-
prompt.validate_arguments!(prompt_args)
237+
validate_prompt_arguments!(prompt, prompt_args, request)
238+
239+
prompt.call(prompt_args, server_context:).to_h
240+
end
241+
242+
def validate_prompt_arguments!(prompt, provided_arguments, request)
243+
missing_arguments = prompt.arguments.filter_map do |configured_argument|
244+
next unless configured_argument.required
245+
246+
key = configured_argument.name
247+
next if provided_arguments.key?(key.to_s) || provided_arguments.key?(key.to_sym)
244248

245-
prompt.template(prompt_args, server_context:).to_h
249+
key
250+
end
251+
252+
missing_required_arguments!(missing_arguments, request) unless missing_arguments.empty?
253+
end
254+
255+
def missing_required_arguments!(missing_arguments, request)
256+
add_instrumentation_data(error: :missing_required_arguments)
257+
raise RequestHandlerError.new(
258+
"Missing required arguments: #{missing_arguments.join(", ")}",
259+
request,
260+
error_type: :missing_required_arguments,
261+
)
246262
end
247263

248264
def list_resources(request)
@@ -273,24 +289,5 @@ def index_resources_by_uri(resources)
273289
hash[resource.uri] = resource
274290
end
275291
end
276-
277-
def tool_call_parameters(tool)
278-
method_def = tool_call_method_def(tool)
279-
method_def.parameters.flatten
280-
end
281-
282-
def tool_call_method_def(tool)
283-
method = tool.method(:call)
284-
285-
if defined?(T::Utils) && T::Utils.respond_to?(:signature_for_method)
286-
sorbet_typed_method_definition = T::Utils.signature_for_method(method)&.method
287-
288-
# Return the Sorbet typed method definition if it exists, otherwise fallback to original method
289-
# definition if Sorbet is defined but not used by this tool.
290-
sorbet_typed_method_definition || method
291-
else
292-
method
293-
end
294-
end
295292
end
296293
end

lib/mcp/server/transports/stdio_transport.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ class StdioTransport < Transport
1010
STATUS_INTERRUPTED = Signal.list["INT"] + 128
1111

1212
def initialize(server)
13-
@server = server
13+
super
1414
@open = false
1515
$stdin.set_encoding("UTF-8")
1616
$stdout.set_encoding("UTF-8")
17-
super
1817
end
1918

2019
def open

0 commit comments

Comments
 (0)