mcp-rb 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ff3f38b8c9fec5aac8fce9d0906f956a77f481ac0477c273e9b677948d0a8cb
4
- data.tar.gz: 116def752685bfa0792c6d2b1dc6bd75e6b029cec03205fb2a67b00d84532cfc
3
+ metadata.gz: 89a4d1cb8ae8957cb8a1d3a867e93218215dc4224eb2e03ab81baf81c0a1c6ba
4
+ data.tar.gz: 5f16964ddf22038d610967d3ae8781fcf7a7f91b17d0b7b749a5c1d38251a967
5
5
  SHA512:
6
- metadata.gz: 3ed62e7606ff685a999c7f95118167e30d8ecb6a6ff35a260223b60fed2fc65599a79bd22424dfde3b0bfc4cd1b3bdf1cb46dcb6877c7720e8837021f456d8cf
7
- data.tar.gz: 5536f506e8e2c89a73fe34acc6edc3981b444672ca8cb3730e089eea22fa9194cee33e6afe26a2442175441487283828aa29103e78b39c8387714d2f1c6a5a82
6
+ metadata.gz: 9f743d921b897ae532ee48343ab3b318c0688e740a90b7b8a2e23fda1a3996669b5385ab49f59027c1c9d5333d57c65c54baa577c71970d6e5fa32e7ca209c75
7
+ data.tar.gz: 441afb473c07c345647a036ae778dd7e36dad5cab6ad3bbad010a97d1ecc863d28bca0b4f5c6898cc86750566f8e0aabe99c93409f3f93c43d68b194c3aea973
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.3] - 2025-03-12
9
+
10
+ ### Fixed
11
+ - Fix error response style: https://github.com/funwarioisii/mcp-rb/pull/12
12
+ - Follow the return style of `handle_*` methods
13
+ - Omit `nextCursor` key when no additional pages exist: https://github.com/funwarioisii/mcp-rb/pull/13
14
+
15
+ ## [0.3.2] - 2025-03-07
16
+
17
+ ### Added
18
+ - Add support for Nested Arguments and Array Arguments: https://github.com/funwarioisii/mcp-rb/pull/6
19
+ - Add validation for nested arguments
20
+ - Support array type arguments
21
+
8
22
  ## [0.3.1] - 2025-03-05
9
23
 
10
24
  ### Added
data/README.md CHANGED
@@ -43,6 +43,27 @@ tool "greet" do
43
43
  "Hello, #{args[:name]}!"
44
44
  end
45
45
  end
46
+
47
+ # Define a tool with nested arguments
48
+ tool "greet_full_name" do
49
+ description "Greet someone by their full name"
50
+ argument :person, required: true, description: "Person to greet" do
51
+ argument :first_name, String, required: false, description: "First name"
52
+ argument :last_name, String, required: false, description: "Last name"
53
+ end
54
+ call do |args|
55
+ "Hello, First: #{args[:person][:first_name]} Last: #{args[:person][:last_name]}!"
56
+ end
57
+ end
58
+
59
+ # Define a tool with an Array argument
60
+ tool "group_greeting" do
61
+ description "Greet multiple people at once"
62
+ argument :people, Array, required: true, items: String, description: "People to greet"
63
+ call do |args|
64
+ args[:people].map { |person| "Hello, #{person}!" }.join(", ")
65
+ end
66
+ end
46
67
  ```
47
68
 
48
69
  ## Supported specifications
@@ -79,7 +79,7 @@ module MCP
79
79
  {
80
80
  resources: paginated.map { |r| format_resource(r) },
81
81
  nextCursor: next_cursor
82
- }
82
+ }.compact
83
83
  end
84
84
 
85
85
  def read_resource(uri)
@@ -124,7 +124,7 @@ module MCP
124
124
  {
125
125
  resourceTemplates: paginated.map { |t| format_resource_template(t) },
126
126
  nextCursor: next_cursor
127
- }
127
+ }.compact
128
128
  end
129
129
 
130
130
  private
data/lib/mcp/app/tool.rb CHANGED
@@ -7,31 +7,83 @@ module MCP
7
7
  @tools ||= {}
8
8
  end
9
9
 
10
+ # Builds schemas for arguments, supporting simple types, nested objects, and arrays
11
+ class SchemaBuilder
12
+ def initialize
13
+ @schema = nil
14
+ @properties = {}
15
+ @required = []
16
+ end
17
+
18
+ def argument(name, type = nil, required: false, description: "", items: nil, &block)
19
+ if type == Array
20
+ if block_given?
21
+ sub_builder = SchemaBuilder.new
22
+ sub_builder.instance_eval(&block)
23
+ item_schema = sub_builder.to_schema
24
+ elsif items
25
+ item_schema = {type: ruby_type_to_schema_type(items)}
26
+ else
27
+ raise ArgumentError, "Must provide items or a block for array type"
28
+ end
29
+ @properties[name] = {type: :array, description: description, items: item_schema}
30
+ elsif block_given?
31
+ raise ArgumentError, "Type not allowed with block for objects" if type
32
+ sub_builder = SchemaBuilder.new
33
+ sub_builder.instance_eval(&block)
34
+ @properties[name] = sub_builder.to_schema.merge(description: description)
35
+ else
36
+ raise ArgumentError, "Type required for simple arguments" if type.nil?
37
+ @properties[name] = {type: ruby_type_to_schema_type(type), description: description}
38
+ end
39
+ @required << name if required
40
+ end
41
+
42
+ def type(t)
43
+ @schema = {type: ruby_type_to_schema_type(t)}
44
+ end
45
+
46
+ def to_schema
47
+ @schema || {type: :object, properties: @properties, required: @required}
48
+ end
49
+
50
+ private
51
+
52
+ def ruby_type_to_schema_type(type)
53
+ if type == String
54
+ :string
55
+ elsif type == Integer
56
+ :integer
57
+ elsif type == Float
58
+ :number
59
+ elsif type == TrueClass || type == FalseClass
60
+ :boolean
61
+ elsif type == Array
62
+ :array
63
+ else
64
+ raise ArgumentError, "Unsupported type: #{type}"
65
+ end
66
+ end
67
+ end
68
+
69
+ # Constructs tool definitions with enhanced schema support
10
70
  class ToolBuilder
11
- attr_reader :name, :description, :arguments, :handler
71
+ attr_reader :name, :arguments, :handler
12
72
 
13
73
  def initialize(name)
14
74
  raise ArgumentError, "Tool name cannot be nil or empty" if name.nil? || name.empty?
15
75
  @name = name
16
76
  @description = ""
17
- @arguments = {}
18
- @required_arguments = []
77
+ @schema_builder = SchemaBuilder.new
19
78
  @handler = nil
20
79
  end
21
80
 
22
- # standard:disable Lint/DuplicateMethods
23
81
  def description(text = nil)
24
- return @description if text.nil?
25
- @description = text
82
+ text ? @description = text : @description
26
83
  end
27
- # standard:enable Lint/DuplicateMethods
28
84
 
29
- def argument(name, type, required: false, description: "")
30
- @arguments[name] = {
31
- type: ruby_type_to_schema_type(type),
32
- description: description
33
- }
34
- @required_arguments << name if required
85
+ def argument(*args, **kwargs, &block)
86
+ @schema_builder.argument(*args, **kwargs, &block)
35
87
  end
36
88
 
37
89
  def call(&block)
@@ -43,95 +95,92 @@ module MCP
43
95
  {
44
96
  name: @name,
45
97
  description: @description,
46
- input_schema: {
47
- type: :object,
48
- properties: @arguments,
49
- required: @required_arguments
50
- },
98
+ input_schema: @schema_builder.to_schema,
51
99
  handler: @handler
52
100
  }
53
101
  end
54
-
55
- private
56
-
57
- def ruby_type_to_schema_type(type)
58
- case type.to_s
59
- when "String" then :string
60
- when "Integer" then :integer
61
- when "Float" then :number
62
- when "TrueClass", "FalseClass", "Boolean" then :boolean
63
- else :object
64
- end
65
- end
66
102
  end
67
103
 
104
+ # Registers a tool with the given name and block
68
105
  def register_tool(name, &block)
69
106
  builder = ToolBuilder.new(name)
70
107
  builder.instance_eval(&block)
71
- tool_hash = builder.to_tool_hash
72
- tools[name] = tool_hash
73
- tool_hash
108
+ tools[name] = builder.to_tool_hash
74
109
  end
75
110
 
111
+ # Lists tools with pagination
76
112
  def list_tools(cursor: nil, page_size: 10)
77
- tool_values = tools.values
78
- start_index = cursor ? cursor.to_i : 0
79
- paginated = tool_values[start_index, page_size]
80
- next_cursor = (start_index + page_size < tool_values.length) ? (start_index + page_size).to_s : nil
81
-
82
- {
83
- tools: paginated.map { |t| format_tool(t) },
84
- nextCursor: next_cursor
85
- }
113
+ start = cursor ? cursor.to_i : 0
114
+ paginated = tools.values[start, page_size]
115
+
116
+ next_cursor = (start + page_size < tools.length) ? (start + page_size).to_s : nil
117
+ {tools: paginated.map { |t| {name: t[:name], description: t[:description], inputSchema: t[:input_schema]} }, nextCursor: next_cursor}.compact
86
118
  end
87
119
 
88
- def call_tool(name, **arguments)
120
+ # Calls a tool with the provided arguments
121
+ def call_tool(name, **args)
89
122
  tool = tools[name]
90
123
  raise ArgumentError, "Tool not found: #{name}" unless tool
91
124
 
92
- begin
93
- validate_arguments(tool[:input_schema], arguments)
94
- result = tool[:handler].call(arguments)
95
- {
96
- content: [
97
- {
98
- type: "text",
99
- text: result.to_s
100
- }
101
- ],
102
- isError: false
103
- }
104
- rescue => e
105
- {
106
- content: [
107
- {
108
- type: "text",
109
- text: "Error: #{e.message}"
110
- }
111
- ],
112
- isError: true
113
- }
114
- end
125
+ validate_arguments(tool[:input_schema], args)
126
+ {content: [{type: "text", text: tool[:handler].call(args).to_s}], isError: false}
127
+ rescue => e
128
+ {content: [{type: "text", text: "Error: #{e.message}"}], isError: true}
115
129
  end
116
130
 
117
131
  private
118
132
 
119
- def validate_arguments(schema, arguments)
120
- return unless schema[:required]
121
-
122
- schema[:required].each do |required_arg|
123
- unless arguments.key?(required_arg)
124
- raise ArgumentError, "missing keyword: :#{required_arg}"
133
+ def validate(schema, arg, path = "")
134
+ errors = []
135
+ type = schema[:type]
136
+
137
+ if type == :object
138
+ if !arg.is_a?(Hash)
139
+ errors << (path.empty? ? "Arguments must be a hash" : "Expected object for #{path}, got #{arg.class}")
140
+ else
141
+ schema[:required]&.each do |req|
142
+ unless arg.key?(req)
143
+ errors << (path.empty? ? "Missing required param :#{req}" : "Missing required param #{path}.#{req}")
144
+ end
145
+ end
146
+ schema[:properties].each do |key, subschema|
147
+ if arg.key?(key)
148
+ sub_path = path.empty? ? key : "#{path}.#{key}"
149
+ sub_errors = validate(subschema, arg[key], sub_path)
150
+ errors.concat(sub_errors)
151
+ end
152
+ end
153
+ end
154
+ elsif type == :array
155
+ if !arg.is_a?(Array)
156
+ errors << "Expected array for #{path}, got #{arg.class}"
157
+ else
158
+ arg.each_with_index do |item, index|
159
+ sub_path = "#{path}[#{index}]"
160
+ sub_errors = validate(schema[:items], item, sub_path)
161
+ errors.concat(sub_errors)
162
+ end
163
+ end
164
+ else
165
+ valid = case type
166
+ when :string then arg.is_a?(String)
167
+ when :integer then arg.is_a?(Integer)
168
+ when :number then arg.is_a?(Float)
169
+ when :boolean then arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
170
+ else false
171
+ end
172
+ unless valid
173
+ errors << "Expected #{type} for #{path}, got #{arg.class}"
125
174
  end
126
175
  end
176
+ errors
127
177
  end
128
178
 
129
- def format_tool(tool)
130
- {
131
- name: tool[:name],
132
- description: tool[:description],
133
- inputSchema: tool[:input_schema]
134
- }
179
+ def validate_arguments(schema, args)
180
+ errors = validate(schema, args, "")
181
+ unless errors.empty?
182
+ raise ArgumentError, errors.join("\n").to_s
183
+ end
135
184
  end
136
185
  end
137
186
  end
data/lib/mcp/server.rb CHANGED
@@ -169,7 +169,11 @@ module MCP
169
169
  arguments = request.dig(:params, :arguments)
170
170
  begin
171
171
  result = @app.call_tool(name, **arguments.transform_keys(&:to_sym))
172
- success_response(request[:id], result)
172
+ if result[:isError]
173
+ error_response(request[:id], Constants::ErrorCodes::INVALID_REQUEST, result[:content].first[:text])
174
+ else
175
+ success_response(request[:id], result)
176
+ end
173
177
  rescue ArgumentError => e
174
178
  error_response(request[:id], Constants::ErrorCodes::INVALID_REQUEST, e.message)
175
179
  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.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - funwarioisii
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-04 00:00:00.000000000 Z
10
+ date: 2025-03-12 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: MCP-RB is a Ruby framework that provides a Sinatra-like DSL for implementing
13
13
  Model Context Protocol servers.
@@ -44,14 +44,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 3.0.0
47
+ version: 3.3.0
48
48
  required_rubygems_version: !ruby/object:Gem::Requirement
49
49
  requirements:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
52
  version: '0'
53
53
  requirements: []
54
- rubygems_version: 3.6.3
54
+ rubygems_version: 3.6.2
55
55
  specification_version: 4
56
56
  summary: A lightweight Ruby framework for implementing MCP (Model Context Protocol)
57
57
  servers