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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +1 -0
- data/lib/mcp/server.rb +11 -7
- data/lib/mcp/tool.rb +77 -79
- data/lib/mcp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50141727429ea7f39887b8a0f6ec2da8515cc3c35803a1c3e328287931bebcae
|
4
|
+
data.tar.gz: 518a481ba94b59f339b4e4957c54971866a76835e54833fd408efdb12e97c0e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
118
|
-
module
|
119
|
-
# Extract
|
120
|
-
def
|
121
|
-
|
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
|
-
|
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
|
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
|
-
|
168
|
+
extract_metadata_from_ast(rule.ast, metadata)
|
135
169
|
end
|
136
170
|
|
137
|
-
|
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
|
158
|
-
def
|
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,
|
162
|
-
process_set_node(ast,
|
163
|
-
process_and_node(ast,
|
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,
|
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
|
-
|
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,
|
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
|
-
|
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,
|
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
|
-
|
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,
|
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,
|
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],
|
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,
|
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,
|
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,
|
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
|
-
|
251
|
+
extract_metadata(and_node, metadata, nested_path)
|
235
252
|
end
|
236
253
|
end
|
237
254
|
|
238
|
-
# Extract
|
239
|
-
def
|
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
|
-
|
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
|
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
|
657
|
-
@
|
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
|
-
|
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
|
-
|
729
|
-
|
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
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.
|
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-
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|