fast-mcp 1.3.2 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 247f36763da6fd88d9c9e454d5d63eaeb351d08a72de8c2e5dc65919d9e2a9f1
4
- data.tar.gz: 846d3427bb33357cfdc6c5387d153d393449792455beab413e36eeb8d0f1bf1d
3
+ metadata.gz: 50141727429ea7f39887b8a0f6ec2da8515cc3c35803a1c3e328287931bebcae
4
+ data.tar.gz: 518a481ba94b59f339b4e4957c54971866a76835e54833fd408efdb12e97c0e8
5
5
  SHA512:
6
- metadata.gz: 8917675ff31b47cd67ddc5171a0c1e853cca9ba8a5853a7a9314ab5b794c9868c7c48b2db17dbd2edb580e81e9b12de8a0318151239babc2a5971cc2d4b10d72
7
- data.tar.gz: 926a58cda786415804f88bb7d2cbabe91fe90b6ae84964be9452070f0ac4de7a29705eedd69af79c641c4e4086865b5dda3438108f1cb068457b1b9c2930472e
6
+ metadata.gz: 76f1f6136bc4f1041021f5a3522f230ae1f044bb19e2e0d8f0871249aac222740b3907229db4b7d482812396f03d492cd6ea414206008f3737361e4346cbb5ca
7
+ data.tar.gz: 1ac14395b264d76b826feb2612a0ceae18933c8b37695ba8e5c673c6c5b29045f241617a2196409267f1c4c58b1fef0b3ff9865d92fae759306a6b17e2795db2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ 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
+ ## [1.4.0] - 2025-05-10
9
+ ### Added
10
+ - Conditionnally hidden properties for tool calls (#70) [#70 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/70)
11
+ - Metadata to tool call results (#69) [#69 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/69)
12
+ - Link to official Discord Server in README.md
13
+
8
14
  ## [1.3.2] - 2025-05-08
9
15
  ### Changed
10
16
  - Logs are now less verbose by default [#64 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/64)
data/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  <a href="https://github.com/yjacquin/fast-mcp/workflows/CI/badge.svg"><img src="https://github.com/yjacquin/fast-mcp/workflows/CI/badge.svg" alt="CI Status" /></a>
11
11
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
12
12
  <a href="code_of_conduct.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant" /></a>
13
+ <a href="https://discord.gg/ayu8ZKvz"><img src = "https://dcbadge.limes.pink/api/server/https://discord.gg/ayu8ZKvz?style=flat" alt="Discord invite link" /></a>
13
14
  </p>
14
15
 
15
16
  ## 🌟 Interface your Servers with LLMs in minutes !
data/lib/mcp/server.rb CHANGED
@@ -304,10 +304,10 @@ module FastMcp
304
304
  begin
305
305
  # Convert string keys to symbols for Ruby
306
306
  symbolized_args = symbolize_keys(arguments)
307
- result = tool.new.call_with_schema_validation!(**symbolized_args)
307
+ result, metadata = tool.new.call_with_schema_validation!(**symbolized_args)
308
308
 
309
309
  # Format and send the result
310
- send_formatted_result(result, id)
310
+ send_formatted_result(result, id, metadata)
311
311
  rescue FastMcp::Tool::InvalidArgumentsError => e
312
312
  @logger.error("Invalid arguments for tool #{tool_name}: #{e.message}")
313
313
  send_error_result(e.message, id)
@@ -318,18 +318,18 @@ module FastMcp
318
318
  end
319
319
 
320
320
  # Format and send successful result
321
- def send_formatted_result(result, id)
321
+ def send_formatted_result(result, id, metadata)
322
322
  # Check if the result is already in the expected format
323
323
  if result.is_a?(Hash) && result.key?(:content)
324
- # Result is already in the correct format
325
- send_result(result, id)
324
+ send_result(result, id, metadata: metadata)
326
325
  else
327
326
  # Format the result according to the MCP specification
328
327
  formatted_result = {
329
328
  content: [{ type: 'text', text: result.to_s }],
330
329
  isError: false
331
330
  }
332
- send_result(formatted_result, id)
331
+
332
+ send_result(formatted_result, id, metadata: metadata)
333
333
  end
334
334
  end
335
335
 
@@ -340,6 +340,7 @@ module FastMcp
340
340
  content: [{ type: 'text', text: "Error: #{message}" }],
341
341
  isError: true
342
342
  }
343
+
343
344
  send_result(error_result, id)
344
345
  end
345
346
 
@@ -408,7 +409,9 @@ module FastMcp
408
409
  end
409
410
 
410
411
  # Send a JSON-RPC result response
411
- def send_result(result, id)
412
+ def send_result(result, id, metadata: {})
413
+ result[:_meta] = metadata if metadata.is_a?(Hash) && !metadata.empty?
414
+
412
415
  response = {
413
416
  jsonrpc: '2.0',
414
417
  id: id,
@@ -429,6 +432,7 @@ module FastMcp
429
432
  },
430
433
  id: id
431
434
  }
435
+
432
436
  send_response(response)
433
437
  end
434
438
 
data/lib/mcp/tool.rb CHANGED
@@ -11,6 +11,14 @@ module Dry
11
11
  def description(text)
12
12
  key_name = name.to_sym
13
13
  schema_dsl.meta(key_name, :description, text)
14
+
15
+ self
16
+ end
17
+
18
+ def hidden(hidden = true) # rubocop:disable Style/OptionalBooleanParameter
19
+ key_name = name.to_sym
20
+ schema_dsl.meta(key_name, :hidden, hidden)
21
+
14
22
  self
15
23
  end
16
24
  end
@@ -20,6 +28,14 @@ module Dry
20
28
  def description(text)
21
29
  key_name = name.to_sym
22
30
  schema_dsl.meta(key_name, :description, text)
31
+
32
+ self
33
+ end
34
+
35
+ def hidden(hidden = true) # rubocop:disable Style/OptionalBooleanParameter
36
+ key_name = name.to_sym
37
+ schema_dsl.meta(key_name, :hidden, hidden)
38
+
23
39
  self
24
40
  end
25
41
  end
@@ -29,6 +45,14 @@ module Dry
29
45
  def description(text)
30
46
  key_name = name.to_sym
31
47
  schema_dsl.meta(key_name, :description, text)
48
+
49
+ self
50
+ end
51
+
52
+ def hidden(hidden = true) # rubocop:disable Style/OptionalBooleanParameter
53
+ key_name = name.to_sym
54
+ schema_dsl.meta(key_name, :hidden, hidden)
55
+
32
56
  self
33
57
  end
34
58
  end
@@ -102,6 +126,12 @@ module FastMcp
102
126
  end
103
127
  end
104
128
 
129
+ def initialize
130
+ @_meta = {}
131
+ end
132
+
133
+ attr_accessor :_meta
134
+
105
135
  def notify_resource_updated(uri)
106
136
  self.class.server.notify_resource_updated(uri)
107
137
  end
@@ -110,106 +140,93 @@ module FastMcp
110
140
  arg_validation = self.class.input_schema.call(args)
111
141
  raise InvalidArgumentsError, arg_validation.errors.to_h.to_json if arg_validation.errors.any?
112
142
 
113
- call(**args)
143
+ # When calling the tool, its metadata can be altered to be returned in response.
144
+ # We return the altered metadata with the tool's result
145
+ [call(**args), _meta]
114
146
  end
115
147
  end
116
148
 
117
- # Module for handling schema descriptions
118
- module SchemaDescriptionExtractor
119
- # Extract descriptions from a schema
120
- def extract_descriptions_from_schema(schema)
121
- descriptions = {}
149
+ # Module for handling schema metadata
150
+ module SchemaMetadataExtractor
151
+ # Extract metadata from a schema
152
+ def extract_metadata_from_schema(schema)
153
+ # a deeply-assignable hash, the default value of a key is {}
154
+ metadata = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
122
155
 
123
156
  # Extract descriptions from the top-level schema
124
157
  if schema.respond_to?(:schema_dsl) && schema.schema_dsl.respond_to?(:meta_data)
125
158
  schema.schema_dsl.meta_data.each do |key, meta|
126
- descriptions[key.to_s] = meta[:description] if meta[:description]
159
+ metadata[key.to_s][:description] = meta[:description] if meta[:description]
160
+ metadata[key.to_s][:hidden] = meta[:hidden]
127
161
  end
128
162
  end
129
163
 
130
- # Extract descriptions from nested schemas using AST
164
+ # Extract metadata from nested schemas using AST
131
165
  schema.rules.each_value do |rule|
132
166
  next unless rule.respond_to?(:ast)
133
167
 
134
- extract_descriptions_from_ast(rule.ast, descriptions)
168
+ extract_metadata_from_ast(rule.ast, metadata)
135
169
  end
136
170
 
137
- # Special case for the nested properties test
138
- handle_special_case_for_person(schema, descriptions)
139
-
140
- descriptions
141
- end
142
-
143
- # Handle special case for person schema in tests
144
- def handle_special_case_for_person(schema, descriptions)
145
- return unless schema.rules.key?(:person) &&
146
- schema.rules[:person].respond_to?(:rule) &&
147
- schema.rules[:person].rule.is_a?(Dry::Logic::Operations::And)
148
-
149
- # Check if this is the test schema with person.first_name and person.last_name
150
- person_rule = schema.rules[:person]
151
- return unless person_rule.rule.rules.any? { |r| r.is_a?(Dry::Logic::Operations::Set) }
152
-
153
- descriptions['person.first_name'] = 'First name of the person'
154
- descriptions['person.last_name'] = 'Last name of the person'
171
+ metadata
155
172
  end
156
173
 
157
- # Extract descriptions from AST
158
- def extract_descriptions_from_ast(ast, descriptions, parent_key = nil)
174
+ # Extract metadata from AST
175
+ def extract_metadata_from_ast(ast, metadata, parent_key = nil)
159
176
  return unless ast.is_a?(Array)
160
177
 
161
- process_key_node(ast, descriptions, parent_key) if ast[0] == :key
162
- process_set_node(ast, descriptions, parent_key) if ast[0] == :set
163
- process_and_node(ast, descriptions, parent_key) if ast[0] == :and
178
+ process_key_node(ast, metadata, parent_key) if ast[0] == :key
179
+ process_set_node(ast, metadata, parent_key) if ast[0] == :set
180
+ process_and_node(ast, metadata, parent_key) if ast[0] == :and
164
181
  end
165
182
 
166
183
  # Process a key node in the AST
167
- def process_key_node(ast, descriptions, parent_key)
184
+ def process_key_node(ast, metadata, parent_key)
168
185
  return unless ast[1].is_a?(Array) && ast[1].size >= 2
169
186
 
170
187
  key = ast[1][0]
171
188
  full_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
172
189
 
173
190
  # Process nested AST
174
- extract_descriptions_from_ast(ast[1][1], descriptions, full_key) if ast[1][1].is_a?(Array)
191
+ extract_metadata_from_ast(ast[1][1], metadata, full_key) if ast[1][1].is_a?(Array)
175
192
  end
176
193
 
177
194
  # Process a set node in the AST
178
- def process_set_node(ast, descriptions, parent_key)
195
+ def process_set_node(ast, metadata, parent_key)
179
196
  return unless ast[1].is_a?(Array)
180
197
 
181
198
  ast[1].each do |set_node|
182
- extract_descriptions_from_ast(set_node, descriptions, parent_key)
199
+ extract_metadata_from_ast(set_node, metadata, parent_key)
183
200
  end
184
201
  end
185
202
 
186
203
  # Process an and node in the AST
187
- def process_and_node(ast, descriptions, parent_key)
204
+ def process_and_node(ast, metadata, parent_key)
188
205
  return unless ast[1].is_a?(Array)
189
206
 
190
207
  # Process each child node
191
208
  ast[1].each do |and_node|
192
- extract_descriptions_from_ast(and_node, descriptions, parent_key)
209
+ extract_metadata_from_ast(and_node, metadata, parent_key)
193
210
  end
194
211
 
195
212
  # Process nested properties
196
- process_nested_properties(ast, descriptions, parent_key)
213
+ process_nested_properties(ast, metadata, parent_key)
197
214
  end
198
215
 
199
216
  # Process nested properties in an and node
200
- def process_nested_properties(ast, descriptions, parent_key)
217
+ def process_nested_properties(ast, metadata, parent_key)
201
218
  ast[1].each do |node|
202
219
  next unless node[0] == :key && node[1].is_a?(Array) && node[1][1].is_a?(Array) && node[1][1][0] == :and
203
220
 
204
221
  key_name = node[1][0]
205
222
  nested_key = parent_key ? "#{parent_key}.#{key_name}" : key_name.to_s
206
223
 
207
- process_nested_schema_ast(node[1][1], descriptions, nested_key)
224
+ process_nested_schema_ast(node[1][1], metadata, nested_key)
208
225
  end
209
226
  end
210
227
 
211
228
  # Process a nested schema
212
- def process_nested_schema_ast(ast, descriptions, nested_key)
229
+ def process_nested_schema_ast(ast, metadata, nested_key)
213
230
  return unless ast[1].is_a?(Array)
214
231
 
215
232
  ast[1].each do |subnode|
@@ -218,31 +235,31 @@ module FastMcp
218
235
  subnode[1].each do |set_node|
219
236
  next unless set_node[0] == :and && set_node[1].is_a?(Array)
220
237
 
221
- process_nested_keys(set_node, descriptions, nested_key)
238
+ process_nested_keys(set_node, metadata, nested_key)
222
239
  end
223
240
  end
224
241
  end
225
242
 
226
243
  # Process nested keys in a schema
227
- def process_nested_keys(set_node, descriptions, nested_key)
244
+ def process_nested_keys(set_node, metadata, nested_key)
228
245
  set_node[1].each do |and_node|
229
246
  next unless and_node[0] == :key && and_node[1].is_a?(Array) && and_node[1].size >= 2
230
247
 
231
248
  nested_field = and_node[1][0]
232
249
  nested_path = "#{nested_key}.#{nested_field}"
233
250
 
234
- extract_meta_description(and_node, descriptions, nested_path)
251
+ extract_metadata(and_node, metadata, nested_path)
235
252
  end
236
253
  end
237
254
 
238
- # Extract meta description from a node
239
- def extract_meta_description(and_node, descriptions, nested_path)
255
+ # Extract metadata from a node
256
+ def extract_metadata(and_node, metadata, nested_path)
240
257
  return unless and_node[1][1].is_a?(Array) && and_node[1][1][1].is_a?(Array)
241
258
 
242
259
  and_node[1][1][1].each do |meta_node|
243
260
  next unless meta_node[0] == :meta && meta_node[1].is_a?(Hash) && meta_node[1][:description]
244
261
 
245
- descriptions[nested_path] = meta_node[1][:description]
262
+ metadata[nested_path] = meta_node[1][:description]
246
263
  end
247
264
  end
248
265
  end
@@ -251,9 +268,7 @@ module FastMcp
251
268
  module RuleTypeDetector
252
269
  # Check if a rule is for a hash type
253
270
  def hash_type?(rule)
254
- return true if direct_hash_predicate?(rule)
255
- return true if nested_hash_predicate?(rule)
256
- return true if special_case_hash?(rule)
271
+ return true if direct_hash_predicate?(rule) || nested_hash_predicate?(rule)
257
272
 
258
273
  false
259
274
  end
@@ -279,28 +294,6 @@ module FastMcp
279
294
  false
280
295
  end
281
296
 
282
- # Check for special case hash
283
- def special_case_hash?(rule)
284
- # Special case for schema_compiler_spec.rb tests
285
- return true if rule.respond_to?(:path) && [:metadata, :user].include?(rule.path)
286
-
287
- # Special case for person hash in the test
288
- return false unless rule.respond_to?(:ast)
289
-
290
- ast = rule.ast
291
- return false unless ast[0] == :and && ast[1].is_a?(Array)
292
-
293
- ast[1].each do |node|
294
- next unless node[0] == :key && node[1].is_a?(Array) && node[1][1].is_a?(Array) && node[1][1][0] == :and
295
-
296
- node[1][1][1].each do |subnode|
297
- return true if subnode[0] == :predicate && subnode[1].is_a?(Array) && subnode[1][0] == :hash?
298
- end
299
- end
300
-
301
- false
302
- end
303
-
304
297
  # Check if a rule is for an array type
305
298
  def array_type?(rule)
306
299
  rule.is_a?(Dry::Logic::Operations::And) &&
@@ -625,7 +618,7 @@ module FastMcp
625
618
 
626
619
  # SchemaCompiler class for converting Dry::Schema to JSON Schema
627
620
  class SchemaCompiler
628
- include SchemaDescriptionExtractor
621
+ include SchemaMetadataExtractor
629
622
  include RuleTypeDetector
630
623
  include PredicateHandler
631
624
  include BasicTypePredicateHandler
@@ -653,8 +646,8 @@ module FastMcp
653
646
  # Store the schema for later use
654
647
  @schema = schema
655
648
 
656
- # Extract descriptions from the schema
657
- @descriptions = extract_descriptions_from_schema(schema)
649
+ # Extract metadata from the schema
650
+ @metadata = extract_metadata_from_schema(schema)
658
651
 
659
652
  # Process each rule in the schema
660
653
  schema.rules.each do |key, rule|
@@ -668,6 +661,9 @@ module FastMcp
668
661
  end
669
662
 
670
663
  def process_rule(key, rule)
664
+ # Skip if this property is hidden
665
+ return if @metadata.dig(key.to_s, :hidden) == true
666
+
671
667
  # Initialize property if it doesn't exist
672
668
  @json_schema[:properties][key] ||= {}
673
669
 
@@ -678,7 +674,8 @@ module FastMcp
678
674
  extract_predicates(rule, key)
679
675
 
680
676
  # Add description if available
681
- @json_schema[:properties][key][:description] = @descriptions[key.to_s] if @descriptions.key?(key.to_s)
677
+ description = @metadata.dig(key.to_s, :description)
678
+ @json_schema[:properties][key][:description] = description unless description && description.empty?
682
679
 
683
680
  # Check if this is a hash type
684
681
  is_hash = hash_type?(rule)
@@ -725,8 +722,9 @@ module FastMcp
725
722
 
726
723
  # Add description if available for nested property
727
724
  nested_key_path = "#{key}.#{nested_key}"
728
- if @descriptions.key?(nested_key_path)
729
- @json_schema[:properties][key][:properties][nested_key][:description] = @descriptions[nested_key_path]
725
+ description = @metadata.dig(nested_key_path, :description)
726
+ unless description && description.empty?
727
+ @json_schema[:properties][key][:properties][nested_key][:description] = description
730
728
  end
731
729
 
732
730
  # Special case for the test with person.first_name and person.last_name
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastMcp
4
- VERSION = '1.3.2'
4
+ VERSION = '1.4.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yorick Jacquin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-09 00:00:00.000000000 Z
11
+ date: 2025-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64