mcp-rb 0.3.1 → 0.3.2

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: 7c5b2677ddf71a4e2597cae1bb3f5806308e33beb5c5a28032741788a1c0b7b6
4
+ data.tar.gz: 63c873245f9af796df2b60072167bac6a4810f6233a3f872e9295e52e1d42b38
5
5
  SHA512:
6
- metadata.gz: 3ed62e7606ff685a999c7f95118167e30d8ecb6a6ff35a260223b60fed2fc65599a79bd22424dfde3b0bfc4cd1b3bdf1cb46dcb6877c7720e8837021f456d8cf
7
- data.tar.gz: 5536f506e8e2c89a73fe34acc6edc3981b444672ca8cb3730e089eea22fa9194cee33e6afe26a2442175441487283828aa29103e78b39c8387714d2f1c6a5a82
6
+ metadata.gz: 9672f55327ee861ba8777447b5b04724d4ffac344a268245a8dc53f9aa5715a048af67f226d8856e2066b28268aff98e7ed5b82fcb8dd59a97710f6f4e4e3ae6
7
+ data.tar.gz: 2d82cf39dd04173518e7b2365513d07536661dbc80a0db72a38b759401808651ff186f2bb0910c24ac73ecbc613a709db66c4c89c7d9422d71d93b7fd0478665
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ 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.2] - 2025-03-07
9
+
10
+ ### Added
11
+ - Add support for Nested Arguments and Array Arguments: https://github.com/funwarioisii/mcp-rb/pull/6
12
+ - Add validation for nested arguments
13
+ - Support array type arguments
14
+
8
15
  ## [0.3.1] - 2025-03-05
9
16
 
10
17
  ### 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
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,91 @@ 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
+ next_cursor = (start + page_size < tools.length) ? (start + page_size).to_s : nil
116
+ {tools: paginated.map { |t| {name: t[:name], description: t[:description], inputSchema: t[:input_schema]} }, nextCursor: next_cursor}
86
117
  end
87
118
 
88
- def call_tool(name, **arguments)
119
+ # Calls a tool with the provided arguments
120
+ def call_tool(name, **args)
89
121
  tool = tools[name]
90
122
  raise ArgumentError, "Tool not found: #{name}" unless tool
91
123
 
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
124
+ validate_arguments(tool[:input_schema], args)
125
+ {content: [{type: "text", text: tool[:handler].call(args).to_s}], isError: false}
126
+ rescue => e
127
+ {content: [{type: "text", text: "Error: #{e.message}"}], isError: true}
115
128
  end
116
129
 
117
130
  private
118
131
 
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}"
132
+ def validate(schema, arg, path = "")
133
+ errors = []
134
+ type = schema[:type]
135
+
136
+ if type == :object
137
+ if !arg.is_a?(Hash)
138
+ errors << (path.empty? ? "Arguments must be a hash" : "Expected object for #{path}, got #{arg.class}")
139
+ else
140
+ schema[:required]&.each do |req|
141
+ unless arg.key?(req)
142
+ errors << (path.empty? ? "Missing required param :#{req}" : "Missing required param #{path}.#{req}")
143
+ end
144
+ end
145
+ schema[:properties].each do |key, subschema|
146
+ if arg.key?(key)
147
+ sub_path = path.empty? ? key : "#{path}.#{key}"
148
+ sub_errors = validate(subschema, arg[key], sub_path)
149
+ errors.concat(sub_errors)
150
+ end
151
+ end
152
+ end
153
+ elsif type == :array
154
+ if !arg.is_a?(Array)
155
+ errors << "Expected array for #{path}, got #{arg.class}"
156
+ else
157
+ arg.each_with_index do |item, index|
158
+ sub_path = "#{path}[#{index}]"
159
+ sub_errors = validate(schema[:items], item, sub_path)
160
+ errors.concat(sub_errors)
161
+ end
162
+ end
163
+ else
164
+ valid = case type
165
+ when :string then arg.is_a?(String)
166
+ when :integer then arg.is_a?(Integer)
167
+ when :number then arg.is_a?(Float)
168
+ when :boolean then arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
169
+ else false
170
+ end
171
+ unless valid
172
+ errors << "Expected #{type} for #{path}, got #{arg.class}"
125
173
  end
126
174
  end
175
+ errors
127
176
  end
128
177
 
129
- def format_tool(tool)
130
- {
131
- name: tool[:name],
132
- description: tool[:description],
133
- inputSchema: tool[:input_schema]
134
- }
178
+ def validate_arguments(schema, args)
179
+ errors = validate(schema, args, "")
180
+ unless errors.empty?
181
+ raise ArgumentError, errors.join("\n").to_s
182
+ end
135
183
  end
136
184
  end
137
185
  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.2"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - funwarioisii
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2025-03-04 00:00:00.000000000 Z
11
+ date: 2025-03-08 00:00:00.000000000 Z
11
12
  dependencies: []
12
13
  description: MCP-RB is a Ruby framework that provides a Sinatra-like DSL for implementing
13
14
  Model Context Protocol servers.
@@ -37,6 +38,7 @@ metadata:
37
38
  homepage_uri: https://github.com/funwarioisii/mcp-rb
38
39
  source_code_uri: https://github.com/funwarioisii/mcp-rb
39
40
  changelog_uri: https://github.com/funwarioisii/mcp-rb/blob/main/CHANGELOG.md
41
+ post_install_message:
40
42
  rdoc_options: []
41
43
  require_paths:
42
44
  - lib
@@ -51,7 +53,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
53
  - !ruby/object:Gem::Version
52
54
  version: '0'
53
55
  requirements: []
54
- rubygems_version: 3.6.3
56
+ rubygems_version: 3.4.1
57
+ signing_key:
55
58
  specification_version: 4
56
59
  summary: A lightweight Ruby framework for implementing MCP (Model Context Protocol)
57
60
  servers