fast-mcp 1.1.0 → 1.3.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 +16 -1
- data/README.md +6 -1
- data/lib/fast_mcp.rb +3 -0
- data/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb +4 -1
- data/lib/mcp/server.rb +1 -1
- data/lib/mcp/transports/authenticated_rack_transport.rb +2 -3
- data/lib/mcp/transports/rack_transport.rb +70 -73
- data/lib/mcp/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 688719b076ec2db4860e3e8a0d40d5417b485cdafbaebb777975d680600c1b9b
|
4
|
+
data.tar.gz: e367624636eecd848a48fd89eff2fd0860b7b1468287d63da77e529d76e96f6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c861b5abf1a982e435b5954075ebaeeece64559a79bae67e36a7d91313a05eba45d5bd61924b87eddc2f6a12f63e69187e3821c4856329e0b2f48b68834bec0f
|
7
|
+
data.tar.gz: 53b8d45da7985600f6b4bb8c131159c769c7458ad2b94471cbfbcd8497154f21c50061fba8a6ac3768bb54c21375035d3fd9f18bc79c008504e54e087017553e
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,21 @@ 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.3.0] - 2025-04-28
|
9
|
+
### Added
|
10
|
+
- Added automatic forwarding of query params from to the messages endpoint [@yjacquin](https://github.com/yjacquin/fast-mcp/commit/011d968ac982d0b0084f7753dcac5789f66339ee)
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
- Declare rack as an explicit dependency [#49 @subelsky](https://github.com/yjacquin/fast-mcp/pull/49)
|
14
|
+
- Fix notifications/initialized response [#51 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/51)
|
15
|
+
|
16
|
+
## [1.2.0] - 2025-04-21
|
17
|
+
### Added
|
18
|
+
- Security enhancement: Bing only to localhost by default [#44 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/44)
|
19
|
+
- Prevent AuthenticatedRackMiddleware from blocking other rails routes[#35 @JulianPasquale](https://github.com/yjacquin/fast-mcp/pull/35)
|
20
|
+
- Stop Forcing reconnections after 30 pings [#42 @zoedsoupe](https://github.com/yjacquin/fast-mcp/pull/42)
|
21
|
+
|
22
|
+
|
8
23
|
## [1.1.0] - 2025-04-13
|
9
24
|
### Added
|
10
25
|
- Security enhancement: Added DNS rebinding protection by validating Origin headers [#32 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/32/files)
|
@@ -60,5 +75,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
60
75
|
- Resource management with subscription capabilities
|
61
76
|
- Binary resource support
|
62
77
|
- Examples with STDIO Transport, HTTP & SSE, Rack app
|
63
|
-
- Initialize lifecycle with capabilities
|
78
|
+
- Initialize lifecycle with capabilities
|
64
79
|
- Comprehensive test suite with RSpec
|
data/README.md
CHANGED
@@ -103,6 +103,9 @@ FastMcp.mount_in_rails(
|
|
103
103
|
sse_route: 'sse', # This is the default route for the SSE endpoint
|
104
104
|
# Add allowed origins below, it defaults to Rails.application.config.hosts
|
105
105
|
# allowed_origins: ['localhost', '127.0.0.1', 'example.com', /.*\.example\.com/],
|
106
|
+
# localhost_only: true, # Set to false to allow connections from other hosts
|
107
|
+
# whitelist specific ips to if you want to run on localhost and allow connections from other IPs
|
108
|
+
# allowed_ips: ['127.0.0.1', '::1']
|
106
109
|
# authenticate: true, # Uncomment to enable authentication
|
107
110
|
# auth_token: 'your-token' # Required if authenticate: true
|
108
111
|
) do |server|
|
@@ -320,8 +323,10 @@ The HTTP/SSE transport validates the Origin header on all incoming connections t
|
|
320
323
|
|
321
324
|
```ruby
|
322
325
|
# Configure allowed origins (defaults to ['localhost', '127.0.0.1'])
|
323
|
-
FastMcp.rack_middleware(app,
|
326
|
+
FastMcp.rack_middleware(app,
|
324
327
|
allowed_origins: ['localhost', '127.0.0.1', 'your-domain.com', /.*\.your-domain\.com/],
|
328
|
+
localhost_only: false,
|
329
|
+
allowed_ips: ['192.168.1.1', '10.0.0.1'],
|
325
330
|
# other options...
|
326
331
|
)
|
327
332
|
```
|
data/lib/fast_mcp.rb
CHANGED
@@ -141,7 +141,10 @@ module FastMcp
|
|
141
141
|
sse_route = options.delete(:sse_route) || 'sse'
|
142
142
|
authenticate = options.delete(:authenticate) || false
|
143
143
|
allowed_origins = options[:allowed_origins] || default_rails_allowed_origins(app)
|
144
|
+
allowed_ips = options[:allowed_ips] || FastMcp::Transports::RackTransport::DEFAULT_ALLOWED_IPS
|
144
145
|
|
146
|
+
options[:localhost_only] = Rails.env.local? if options[:localhost_only].nil?
|
147
|
+
options[:allowed_ips] = allowed_ips
|
145
148
|
options[:logger] = logger
|
146
149
|
options[:allowed_origins] = allowed_origins
|
147
150
|
|
@@ -22,7 +22,10 @@ FastMcp.mount_in_rails(
|
|
22
22
|
messages_route: 'messages', # This is the default route for the messages endpoint
|
23
23
|
sse_route: 'sse' # This is the default route for the SSE endpoint
|
24
24
|
# Add allowed origins below, it defaults to Rails.application.config.hosts
|
25
|
-
# allowed_origins: ['localhost', '127.0.0.1', 'example.com', /.*\.example\.com/],
|
25
|
+
# allowed_origins: ['localhost', '127.0.0.1', '[::1]', 'example.com', /.*\.example\.com/],
|
26
|
+
# localhost_only: true, # Set to false to allow connections from other hosts
|
27
|
+
# whitelist specific ips to if you want to run on localhost and allow connections from other IPs
|
28
|
+
# allowed_ips: ['127.0.0.1', '::1']
|
26
29
|
# authenticate: true, # Uncomment to enable authentication
|
27
30
|
# auth_token: 'your-token', # Required if authenticate: true
|
28
31
|
) do |server|
|
data/lib/mcp/server.rb
CHANGED
@@ -14,9 +14,7 @@ module FastMcp
|
|
14
14
|
@auth_enabled = !@auth_token.nil?
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
request = Rack::Request.new(env)
|
19
|
-
|
17
|
+
def handle_mcp_request(request, env)
|
20
18
|
if auth_enabled? && !exempt_from_auth?(request.path)
|
21
19
|
auth_header = request.env["HTTP_#{@auth_header_name.upcase.gsub('-', '_')}"]
|
22
20
|
token = auth_header&.gsub('Bearer ', '')
|
@@ -42,6 +40,7 @@ module FastMcp
|
|
42
40
|
end
|
43
41
|
|
44
42
|
def unauthorized_response(request)
|
43
|
+
@logger.error('Unauthorized request: Invalid or missing authentication token')
|
45
44
|
body = JSON.generate(
|
46
45
|
{
|
47
46
|
jsonrpc: '2.0',
|
@@ -11,9 +11,25 @@ module FastMcp
|
|
11
11
|
# This transport can be mounted in any Rack-compatible web framework
|
12
12
|
class RackTransport < BaseTransport # rubocop:disable Metrics/ClassLength
|
13
13
|
DEFAULT_PATH_PREFIX = '/mcp'
|
14
|
-
DEFAULT_ALLOWED_ORIGINS = ['localhost', '127.0.0.1'].freeze
|
15
|
-
|
16
|
-
|
14
|
+
DEFAULT_ALLOWED_ORIGINS = ['localhost', '127.0.0.1', '[::1]'].freeze
|
15
|
+
DEFAULT_ALLOWED_IPS = ['127.0.0.1', '::1'].freeze
|
16
|
+
|
17
|
+
SSE_HEADERS = {
|
18
|
+
'Content-Type' => 'text/event-stream',
|
19
|
+
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
20
|
+
'Connection' => 'keep-alive',
|
21
|
+
'X-Accel-Buffering' => 'no', # For Nginx
|
22
|
+
'Access-Control-Allow-Origin' => '*', # Allow CORS
|
23
|
+
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
|
24
|
+
'Access-Control-Allow-Headers' => 'Content-Type',
|
25
|
+
'Access-Control-Max-Age' => '86400', # 24 hours
|
26
|
+
'Keep-Alive' => 'timeout=600', # 10 minutes timeout
|
27
|
+
'Pragma' => 'no-cache',
|
28
|
+
'Expires' => '0'
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
attr_reader :app, :path_prefix, :sse_clients, :messages_route, :sse_route, :allowed_origins, :localhost_only,
|
32
|
+
:allowed_ips
|
17
33
|
|
18
34
|
def initialize(app, server, options = {}, &_block)
|
19
35
|
super(server, logger: options[:logger])
|
@@ -22,6 +38,8 @@ module FastMcp
|
|
22
38
|
@messages_route = options[:messages_route] || 'messages'
|
23
39
|
@sse_route = options[:sse_route] || 'sse'
|
24
40
|
@allowed_origins = options[:allowed_origins] || DEFAULT_ALLOWED_ORIGINS
|
41
|
+
@localhost_only = options.fetch(:localhost_only, true) # Default to localhost-only mode
|
42
|
+
@allowed_ips = options[:allowed_ips] || DEFAULT_ALLOWED_IPS
|
25
43
|
@sse_clients = {}
|
26
44
|
@running = false
|
27
45
|
end
|
@@ -105,6 +123,18 @@ module FastMcp
|
|
105
123
|
|
106
124
|
private
|
107
125
|
|
126
|
+
def validate_client_ip(request)
|
127
|
+
client_ip = request.ip
|
128
|
+
|
129
|
+
# Check if we're in localhost-only mode
|
130
|
+
if @localhost_only && !@allowed_ips.include?(client_ip)
|
131
|
+
@logger.warn("Blocked connection from non-localhost IP: #{client_ip}")
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
|
135
|
+
true
|
136
|
+
end
|
137
|
+
|
108
138
|
# Validate the Origin header to prevent DNS rebinding attacks
|
109
139
|
def validate_origin(request, env)
|
110
140
|
origin = env['HTTP_ORIGIN']
|
@@ -117,7 +147,7 @@ module FastMcp
|
|
117
147
|
|
118
148
|
# If we have a hostname and allowed_origins is not empty
|
119
149
|
if hostname && !allowed_origins.empty?
|
120
|
-
@logger.
|
150
|
+
@logger.debug("Validating origin: #{hostname}")
|
121
151
|
|
122
152
|
# Check if the hostname matches any allowed origin
|
123
153
|
is_allowed = allowed_origins.any? do |allowed|
|
@@ -160,20 +190,11 @@ module FastMcp
|
|
160
190
|
|
161
191
|
# Handle MCP-specific requests
|
162
192
|
def handle_mcp_request(request, env)
|
193
|
+
# Validate client IP to ensure it's connecting from allowed sources
|
194
|
+
return forbidden_response('Forbidden: Remote IP not allowed') unless validate_client_ip(request)
|
195
|
+
|
163
196
|
# Validate Origin header to prevent DNS rebinding attacks
|
164
|
-
unless validate_origin(request, env)
|
165
|
-
return [403, { 'Content-Type' => 'application/json' },
|
166
|
-
[JSON.generate(
|
167
|
-
{
|
168
|
-
jsonrpc: '2.0',
|
169
|
-
error: {
|
170
|
-
code: -32_600,
|
171
|
-
message: 'Forbidden: Origin validation failed'
|
172
|
-
},
|
173
|
-
id: nil
|
174
|
-
}
|
175
|
-
)]]
|
176
|
-
end
|
197
|
+
return forbidden_response('Forbidden: Origin validation failed') unless validate_origin(request, env)
|
177
198
|
|
178
199
|
subpath = request.path[@path_prefix.length..]
|
179
200
|
@logger.info("MCP request subpath: '#{subpath.inspect}'")
|
@@ -190,6 +211,20 @@ module FastMcp
|
|
190
211
|
end
|
191
212
|
end
|
192
213
|
|
214
|
+
def forbidden_response(message)
|
215
|
+
[403, { 'Content-Type' => 'application/json' },
|
216
|
+
[JSON.generate(
|
217
|
+
{
|
218
|
+
jsonrpc: '2.0',
|
219
|
+
error: {
|
220
|
+
code: -32_600,
|
221
|
+
message: message
|
222
|
+
},
|
223
|
+
id: nil
|
224
|
+
}
|
225
|
+
)]]
|
226
|
+
end
|
227
|
+
|
193
228
|
# Return a 404 endpoint not found response
|
194
229
|
def endpoint_not_found_response
|
195
230
|
[404, { 'Content-Type' => 'application/json' },
|
@@ -212,24 +247,21 @@ module FastMcp
|
|
212
247
|
|
213
248
|
return method_not_allowed_response unless request.get?
|
214
249
|
|
215
|
-
# Set up SSE headers
|
216
|
-
headers = setup_sse_headers
|
217
|
-
|
218
250
|
# Handle streaming based on the framework
|
219
|
-
handle_streaming(env
|
251
|
+
handle_streaming(env)
|
220
252
|
end
|
221
253
|
|
222
254
|
# Handle streaming based on the framework
|
223
|
-
def handle_streaming(env
|
255
|
+
def handle_streaming(env)
|
224
256
|
@logger.info("Handling streaming for env: #{env['HTTP_USER_AGENT']}")
|
225
257
|
if env['rack.hijack']
|
226
258
|
# Rack hijacking (e.g., Puma)
|
227
259
|
@logger.info('Handling rack hijack SSE')
|
228
|
-
handle_rack_hijack_sse(env
|
260
|
+
handle_rack_hijack_sse(env)
|
229
261
|
elsif rails_live_streaming?(env)
|
230
262
|
# Rails ActionController::Live
|
231
263
|
@logger.info('Handling rails live streaming SSE')
|
232
|
-
handle_rails_sse(env
|
264
|
+
handle_rails_sse(env)
|
233
265
|
else
|
234
266
|
# Fallback for servers that don't support streaming
|
235
267
|
@logger.info('Falling back to default SSE')
|
@@ -244,23 +276,6 @@ module FastMcp
|
|
244
276
|
env['action_controller.instance'].response.respond_to?(:stream)
|
245
277
|
end
|
246
278
|
|
247
|
-
# Set up headers for SSE connection
|
248
|
-
def setup_sse_headers
|
249
|
-
{
|
250
|
-
'Content-Type' => 'text/event-stream',
|
251
|
-
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
252
|
-
'Connection' => 'keep-alive',
|
253
|
-
'X-Accel-Buffering' => 'no', # For Nginx
|
254
|
-
'Access-Control-Allow-Origin' => '*', # Allow CORS
|
255
|
-
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
|
256
|
-
'Access-Control-Allow-Headers' => 'Content-Type',
|
257
|
-
'Access-Control-Max-Age' => '86400', # 24 hours
|
258
|
-
'Keep-Alive' => 'timeout=600', # 10 minutes timeout
|
259
|
-
'Pragma' => 'no-cache',
|
260
|
-
'Expires' => '0'
|
261
|
-
}
|
262
|
-
end
|
263
|
-
|
264
279
|
# Set up CORS headers for preflight requests
|
265
280
|
def setup_cors_headers
|
266
281
|
{
|
@@ -286,9 +301,6 @@ module FastMcp
|
|
286
301
|
browser_type = detect_browser_type(user_agent)
|
287
302
|
@logger.info("Client connection from: #{user_agent} (#{browser_type})")
|
288
303
|
|
289
|
-
# Handle MCP inspector with fixed client ID
|
290
|
-
@logger.info("MCP Inspector detected, using fixed client ID: #{client_id}") if mcp_inspector?(user_agent, env)
|
291
|
-
|
292
304
|
# Handle reconnection
|
293
305
|
if client_id && @sse_clients.key?(client_id)
|
294
306
|
handle_client_reconnection(client_id, browser_type)
|
@@ -321,11 +333,6 @@ module FastMcp
|
|
321
333
|
end
|
322
334
|
end
|
323
335
|
|
324
|
-
# Check if client is MCP inspector
|
325
|
-
def mcp_inspector?(user_agent, env)
|
326
|
-
user_agent.include?('mcp-inspector') || (env['mcp.client_name'] == 'mcp-inspector')
|
327
|
-
end
|
328
|
-
|
329
336
|
# Handle client reconnection
|
330
337
|
def handle_client_reconnection(client_id, browser_type)
|
331
338
|
@logger.info("Client #{client_id} is reconnecting (#{browser_type})")
|
@@ -342,7 +349,7 @@ module FastMcp
|
|
342
349
|
end
|
343
350
|
|
344
351
|
# Handle SSE with Rack hijacking (e.g., Puma)
|
345
|
-
def handle_rack_hijack_sse(env
|
352
|
+
def handle_rack_hijack_sse(env)
|
346
353
|
client_id = extract_client_id(env)
|
347
354
|
@logger.debug("Setting up Rack hijack SSE connection for client #{client_id}")
|
348
355
|
|
@@ -350,7 +357,7 @@ module FastMcp
|
|
350
357
|
io = env['rack.hijack_io']
|
351
358
|
@logger.debug("Obtained hijack IO for client #{client_id}")
|
352
359
|
|
353
|
-
setup_sse_connection(client_id, io,
|
360
|
+
setup_sse_connection(client_id, io, env)
|
354
361
|
start_keep_alive_thread(client_id, io)
|
355
362
|
|
356
363
|
# Return async response
|
@@ -358,11 +365,11 @@ module FastMcp
|
|
358
365
|
end
|
359
366
|
|
360
367
|
# Set up the SSE connection
|
361
|
-
def setup_sse_connection(client_id, io,
|
368
|
+
def setup_sse_connection(client_id, io, env)
|
362
369
|
# Send headers
|
363
370
|
@logger.debug("Sending HTTP headers for SSE connection #{client_id}")
|
364
371
|
io.write("HTTP/1.1 200 OK\r\n")
|
365
|
-
|
372
|
+
SSE_HEADERS.each { |k, v| io.write("#{k}: #{v}\r\n") }
|
366
373
|
io.write("\r\n")
|
367
374
|
io.flush
|
368
375
|
|
@@ -372,8 +379,12 @@ module FastMcp
|
|
372
379
|
# Send an initial comment to keep the connection alive
|
373
380
|
io.write(": SSE connection established\n\n")
|
374
381
|
|
375
|
-
#
|
382
|
+
# Extract query parameters from the request
|
383
|
+
query_string = env['QUERY_STRING']
|
384
|
+
|
385
|
+
# Send endpoint information as the first message with query parameters
|
376
386
|
endpoint = "#{@path_prefix}/#{@messages_route}"
|
387
|
+
endpoint += "?#{query_string}" if query_string
|
377
388
|
@logger.debug("Sending endpoint information to client #{client_id}: #{endpoint}")
|
378
389
|
io.write("event: endpoint\ndata: #{endpoint}\n\n")
|
379
390
|
|
@@ -405,13 +416,11 @@ module FastMcp
|
|
405
416
|
@logger.info("Starting keep-alive loop for SSE connection #{client_id}")
|
406
417
|
ping_count = 0
|
407
418
|
ping_interval = 1 # Send a ping every 1 second
|
408
|
-
max_ping_count = 30 # Reset connection after 30 pings (about 30 seconds)
|
409
419
|
@running = true
|
410
420
|
|
411
421
|
while @running && !io.closed?
|
412
422
|
begin
|
413
|
-
ping_count = send_keep_alive_ping(io, client_id, ping_count
|
414
|
-
break if ping_count >= max_ping_count
|
423
|
+
ping_count = send_keep_alive_ping(io, client_id, ping_count)
|
415
424
|
|
416
425
|
sleep ping_interval
|
417
426
|
rescue Errno::EPIPE, IOError => e
|
@@ -423,7 +432,7 @@ module FastMcp
|
|
423
432
|
end
|
424
433
|
|
425
434
|
# Send a keep-alive ping and return the updated ping count
|
426
|
-
def send_keep_alive_ping(io, client_id, ping_count
|
435
|
+
def send_keep_alive_ping(io, client_id, ping_count)
|
427
436
|
ping_count += 1
|
428
437
|
|
429
438
|
# Send a comment before each ping to keep the connection alive
|
@@ -436,12 +445,6 @@ module FastMcp
|
|
436
445
|
send_ping_event(io)
|
437
446
|
end
|
438
447
|
|
439
|
-
# If we've reached the max ping count, force a reconnection
|
440
|
-
if ping_count >= max_ping_count
|
441
|
-
@logger.debug("Reached max ping count (#{max_ping_count}) for client #{client_id}, forcing reconnection")
|
442
|
-
send_reconnect_event(io)
|
443
|
-
end
|
444
|
-
|
445
448
|
ping_count
|
446
449
|
end
|
447
450
|
|
@@ -450,18 +453,12 @@ module FastMcp
|
|
450
453
|
ping_message = {
|
451
454
|
jsonrpc: '2.0',
|
452
455
|
method: 'ping',
|
453
|
-
id:
|
456
|
+
id: rand(1_000_000)
|
454
457
|
}
|
455
458
|
io.write("event: ping\ndata: #{JSON.generate(ping_message)}\n\n")
|
456
459
|
io.flush
|
457
460
|
end
|
458
461
|
|
459
|
-
# Send a reconnect event
|
460
|
-
def send_reconnect_event(io)
|
461
|
-
io.write("event: reconnect\ndata: {\"reason\":\"timeout prevention\"}\n\n")
|
462
|
-
io.flush
|
463
|
-
end
|
464
|
-
|
465
462
|
# Clean up SSE connection
|
466
463
|
def cleanup_sse_connection(client_id, io)
|
467
464
|
@logger.info("Cleaning up SSE connection for client #{client_id}")
|
@@ -475,7 +472,7 @@ module FastMcp
|
|
475
472
|
end
|
476
473
|
|
477
474
|
# Handle SSE with Rails ActionController::Live
|
478
|
-
def handle_rails_sse(env
|
475
|
+
def handle_rails_sse(env)
|
479
476
|
client_id = extract_client_id(env)
|
480
477
|
controller = env['action_controller.instance']
|
481
478
|
stream = controller.response.stream
|
@@ -484,7 +481,7 @@ module FastMcp
|
|
484
481
|
register_sse_client(client_id, stream)
|
485
482
|
|
486
483
|
# The controller will handle the streaming
|
487
|
-
[200,
|
484
|
+
[200, SSE_HEADERS, []]
|
488
485
|
end
|
489
486
|
|
490
487
|
# Handle message POST request
|
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.3.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-04-
|
11
|
+
date: 2025-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.1'
|
69
83
|
description: A flexible and powerful implementation of the MCP with multiple approaches
|
70
84
|
for defining tools.
|
71
85
|
email:
|