fast-mcp 1.0.0 → 1.2.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: bedabe91d57ecb3800b625ad786389c7e842b73e5bcd9df621f8f31b401fe93f
4
- data.tar.gz: b8711a78f8f8928e5f66d94a1f271f9f6e6a5e101e0386eff81ed62e7153d2ae
3
+ metadata.gz: e9a99e9fd2c611e1645bfda1cd3d4c924f86b4284e1018e53d8f55c7612c6d48
4
+ data.tar.gz: bc9def81c86fb8db4ecdb32091209050a7d860c64d7204158ecfb98e27d01156
5
5
  SHA512:
6
- metadata.gz: 564cadd64c076e784bc0779a3f697d82d50eed14c77b1403f03abeee0935972103e68bf82bb94ec7cd3443b90ddfa75d739dd6a5a0bda32590e8b31d8a708869
7
- data.tar.gz: 731cc93d551842dfcec399d7d75d86c59d4a6af88d415925757a3ced8c5947dda7ae26eafc12b71423af496c6669a084cdbaabdc05cbbacef68476d39925991e
6
+ metadata.gz: 44ba180b84383e3f0cff990136551101cea90181cb384634abecf72ab96ae0316ab53cf9e05eb09626642f647dac8cf2039edc808339dd11c579cd14a0333d13
7
+ data.tar.gz: 03b61251ec444d33748a23c1a786f70fea8d54228017b2b1dec0eeae93cb1900afc9af8b19d3fb3c55bbfba26a8e52e2d5ddc266fd6a19f72de692b3ef880382
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.2.0] - UNRELEASED
9
+ ### Added
10
+ - Security enhancement: Bing only to localhost by default [#44 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/44)
11
+ - Prevent AuthenticatedRackMiddleware from blocking other rails routes[#35 @JulianPasquale](https://github.com/yjacquin/fast-mcp/pull/35)
12
+ - Stop Forcing reconnections after 30 pings [#42 @zoedsoupe](https://github.com/yjacquin/fast-mcp/pull/42)
13
+
14
+
15
+ ## [1.1.0] - 2025-04-13
16
+ ### Added
17
+ - Security enhancement: Added DNS rebinding protection by validating Origin headers [#32 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/32/files)
18
+ - Added configuration options for allowed origins in rack middleware [#32 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/32/files)
19
+ - Allow to change the SSE and Messages route [#23 @pedrofurtado](https://github.com/yjacquin/fast-mcp/pull/23)
20
+ - Fix invalid return value when processing notifications/initialized request [#31 @abMatGit](https://github.com/yjacquin/fast-mcp/pull/31)
21
+
22
+
8
23
  ## [1.0.0] - 2025-03-30
9
24
 
10
25
  ### Added
@@ -52,5 +67,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
52
67
  - Resource management with subscription capabilities
53
68
  - Binary resource support
54
69
  - Examples with STDIO Transport, HTTP & SSE, Rack app
55
- - Initialize lifecycle with capabilities
70
+ - Initialize lifecycle with capabilities
56
71
  - Comprehensive test suite with RSpec
data/README.md CHANGED
@@ -36,12 +36,11 @@ Fast MCP solves all these problems by providing a clean, Ruby-focused implementa
36
36
  ## 💎 What Makes FastMCP Great
37
37
  ```ruby
38
38
  # Define tools for AI models to use
39
- server = FastMcp::Server.new(name: 'recipe-ai', version: '1.0.0')
39
+ server = FastMcp::Server.new(name: 'popular-users', version: '1.0.0')
40
40
 
41
41
  # Define a tool by inheriting from FastMcp::Tool
42
42
  class CreateUserTool < FastMcp::Tool
43
- description "Find recipes based on ingredients"
44
-
43
+ description "Create a user"
45
44
  # These arguments will generate the needed JSON to be presented to the MCP Client
46
45
  # And they will be validated at run time.
47
46
  # The validation is based off Dry-Schema, with the addition of the description.
@@ -54,7 +53,7 @@ class CreateUserTool < FastMcp::Tool
54
53
  optional(:zipcode).filled(:string)
55
54
  end
56
55
  end
57
-
56
+
58
57
  def call(first_name:, age: nil, address: {})
59
58
  User.create!(first_name:, age:, address:)
60
59
  end
@@ -68,20 +67,20 @@ class PopularUsers < FastMcp::Resource
68
67
  uri "file://popular_users.json"
69
68
  resource_name "Popular Users"
70
69
  mime_type "application/json"
71
-
70
+
72
71
  def content
73
72
  JSON.generate(User.popular.limit(5).as_json)
74
73
  end
75
74
  end
76
75
 
77
76
  # Register the resource with the server
78
- server.register_resource(IngredientsResource)
77
+ server.register_resource(PopularUsers)
79
78
 
80
79
  # Accessing the resource through the server
81
- server.read_resource(IngredientsResource.uri)
80
+ server.read_resource(PopularUsers.uri)
82
81
 
83
82
  # Notify the resource content has been updated to clients
84
- server.notify_resource_updated(IngredientsResource.uri)
83
+ server.notify_resource_updated(PopularUsers.uri)
85
84
  ```
86
85
 
87
86
  ### 🚂 Fast Ruby on Rails implementation
@@ -99,9 +98,16 @@ FastMcp.mount_in_rails(
99
98
  Rails.application,
100
99
  name: Rails.application.class.module_parent_name.underscore.dasherize,
101
100
  version: '1.0.0',
102
- path_prefix: '/mcp' # This is the default path prefix
101
+ path_prefix: '/mcp', # This is the default path prefix
102
+ messages_route: 'messages', # This is the default route for the messages endpoint
103
+ sse_route: 'sse', # This is the default route for the SSE endpoint
104
+ # Add allowed origins below, it defaults to Rails.application.config.hosts
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']
103
109
  # authenticate: true, # Uncomment to enable authentication
104
- # auth_token: 'your-token', # Required if authenticate: true
110
+ # auth_token: 'your-token' # Required if authenticate: true
105
111
  ) do |server|
106
112
  Rails.application.config.after_initialize do
107
113
  # FastMcp will automatically discover and register:
@@ -136,11 +142,11 @@ These are automatically set up in Rails applications. You can use either naming
136
142
  # Using Rails-style naming:
137
143
  class MyTool < ActionTool::Base
138
144
  description "My awesome tool"
139
-
145
+
140
146
  arguments do
141
147
  required(:input).filled(:string)
142
148
  end
143
-
149
+
144
150
  def call(input:)
145
151
  # Your implementation
146
152
  end
@@ -182,12 +188,12 @@ server = FastMcp::Server.new(name: 'my-ai-server', version: '1.0.0')
182
188
  # Define a tool by inheriting from FastMcp::Tool
183
189
  class SummarizeTool < FastMcp::Tool
184
190
  description "Summarize a given text"
185
-
191
+
186
192
  arguments do
187
193
  required(:text).filled(:string).description("Text to summarize")
188
194
  optional(:max_length).filled(:integer).description("Maximum length of summary")
189
195
  end
190
-
196
+
191
197
  def call(text:, max_length: 100)
192
198
  # Your summarization logic here
193
199
  text.split('.').first(3).join('.') + '...'
@@ -203,7 +209,7 @@ class StatisticsResource < FastMcp::Resource
203
209
  resource_name "Usage Statistics"
204
210
  description "Current system statistics"
205
211
  mime_type "application/json"
206
-
212
+
207
213
  def content
208
214
  JSON.generate({
209
215
  users_online: 120,
@@ -307,6 +313,36 @@ Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
307
313
  - 📚 **Interactive Documentation**: Create AI-enhanced API documentation
308
314
  - 💬 **Chatbots and Assistants**: Build AI assistants with access to your app's data
309
315
 
316
+ ## 🔒 Security Features
317
+
318
+ Fast MCP includes built-in security features to protect your applications:
319
+
320
+ ### DNS Rebinding Protection
321
+
322
+ The HTTP/SSE transport validates the Origin header on all incoming connections to prevent DNS rebinding attacks, which could allow malicious websites to interact with local MCP servers.
323
+
324
+ ```ruby
325
+ # Configure allowed origins (defaults to ['localhost', '127.0.0.1'])
326
+ FastMcp.rack_middleware(app,
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'],
330
+ # other options...
331
+ )
332
+ ```
333
+
334
+ ### Authentication
335
+
336
+ Fast MCP supports token-based authentication for all connections:
337
+
338
+ ```ruby
339
+ # Enable authentication
340
+ FastMcp.authenticated_rack_middleware(app,
341
+ auth_token: 'your-secret-token',
342
+ # other options...
343
+ )
344
+ ```
345
+
310
346
  ## 📖 Documentation
311
347
 
312
348
  - [🚀 Getting Started Guide](docs/getting_started.md)
@@ -315,6 +351,7 @@ Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
315
351
  - [🌐 Sinatra Integration](docs/sinatra_integration.md)
316
352
  - [📚 Resources](docs/resources.md)
317
353
  - [🛠️ Tools](docs/tools.md)
354
+ - [🔒 Security](docs/security.md)
318
355
 
319
356
  ## 💻 Examples
320
357
 
data/lib/fast_mcp.rb CHANGED
@@ -36,7 +36,10 @@ module FastMcp
36
36
  # @option options [String] :name The name of the server
37
37
  # @option options [String] :version The version of the server
38
38
  # @option options [String] :path_prefix The path prefix for the MCP endpoints
39
+ # @option options [String] :messages_route The route for the messages endpoint
40
+ # @option options [String] :sse_route The route for the SSE endpoint
39
41
  # @option options [Logger] :logger The logger to use
42
+ # @option options [Array<String,Regexp>] :allowed_origins List of allowed origins for DNS rebinding protection
40
43
  # @yield [server] A block to configure the server
41
44
  # @yieldparam server [FastMcp::Server] The server to configure
42
45
  # @return [#call] The Rack middleware
@@ -63,6 +66,7 @@ module FastMcp
63
66
  # @option options [String] :name The name of the server
64
67
  # @option options [String] :version The version of the server
65
68
  # @option options [String] :auth_token The authentication token
69
+ # @option options [Array<String,Regexp>] :allowed_origins List of allowed origins for DNS rebinding protection
66
70
  # @yield [server] A block to configure the server
67
71
  # @yieldparam server [FastMcp::Server] The server to configure
68
72
  # @return [#call] The Rack middleware
@@ -118,9 +122,12 @@ module FastMcp
118
122
  # @option options [String] :name The name of the server
119
123
  # @option options [String] :version The version of the server
120
124
  # @option options [String] :path_prefix The path prefix for the MCP endpoints
125
+ # @option options [String] :messages_route The route for the messages endpoint
126
+ # @option options [String] :sse_route The route for the SSE endpoint
121
127
  # @option options [Logger] :logger The logger to use
122
128
  # @option options [Boolean] :authenticate Whether to use authentication
123
129
  # @option options [String] :auth_token The authentication token
130
+ # @option options [Array<String,Regexp>] :allowed_origins List of allowed origins for DNS rebinding protection
124
131
  # @yield [server] A block to configure the server
125
132
  # @yieldparam server [FastMcp::Server] The server to configure
126
133
  # @return [#call] The Rack middleware
@@ -130,9 +137,17 @@ module FastMcp
130
137
  version = options.delete(:version) || '1.0.0'
131
138
  logger = options[:logger] || Rails.logger
132
139
  path_prefix = options.delete(:path_prefix) || '/mcp'
140
+ messages_route = options.delete(:messages_route) || 'messages'
141
+ sse_route = options.delete(:sse_route) || 'sse'
133
142
  authenticate = options.delete(:authenticate) || false
143
+ allowed_origins = options[:allowed_origins] || default_rails_allowed_origins(app)
144
+ allowed_ips = options[:allowed_ips] || FastMcp::Transports::RackTransport::DEFAULT_ALLOWED_IPS
134
145
 
146
+ options[:localhost_only] = Rails.env.local? if options[:localhost_only].nil?
147
+ options[:allowed_ips] = allowed_ips
135
148
  options[:logger] = logger
149
+ options[:allowed_origins] = allowed_origins
150
+
136
151
  # Create or get the server
137
152
  self.server = FastMcp::Server.new(name: name, version: version, logger: logger)
138
153
  yield self.server if block_given?
@@ -145,7 +160,25 @@ module FastMcp
145
160
  end
146
161
 
147
162
  # Insert the middleware in the Rails middleware stack
148
- app.middleware.use self.server.transport_klass, self.server, options.merge(path_prefix: path_prefix)
163
+ app.middleware.use(
164
+ self.server.transport_klass,
165
+ self.server,
166
+ options.merge(path_prefix: path_prefix, messages_route: messages_route, sse_route: sse_route)
167
+ )
168
+ end
169
+
170
+ def self.default_rails_allowed_origins(rail_app)
171
+ hosts = rail_app.config.hosts
172
+
173
+ hosts.map do |host|
174
+ if host.is_a?(String) && host.start_with?('.')
175
+ # Convert .domain to domain and *.domain
176
+ host_without_dot = host[1..]
177
+ [host_without_dot, Regexp.new(".*\.#{host_without_dot}")] # rubocop:disable Style/RedundantStringEscape
178
+ else
179
+ host
180
+ end
181
+ end.flatten.compact
149
182
  end
150
183
 
151
184
  # Notify the server that a resource has been updated
@@ -18,7 +18,14 @@ FastMcp.mount_in_rails(
18
18
  Rails.application,
19
19
  name: Rails.application.class.module_parent_name.underscore.dasherize,
20
20
  version: '1.0.0',
21
- path_prefix: '/mcp' # This is the default path prefix
21
+ path_prefix: '/mcp', # This is the default path prefix
22
+ messages_route: 'messages', # This is the default route for the messages endpoint
23
+ sse_route: 'sse' # This is the default route for the SSE endpoint
24
+ # Add allowed origins below, it defaults to Rails.application.config.hosts
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']
22
29
  # authenticate: true, # Uncomment to enable authentication
23
30
  # auth_token: 'your-token', # Required if authenticate: true
24
31
  ) do |server|
data/lib/mcp/server.rb CHANGED
@@ -275,7 +275,7 @@ module FastMcp
275
275
  @client_initialized = true
276
276
  @logger.info('Client initialized, beginning normal operation')
277
277
 
278
- nil
278
+ send_result({}, nil)
279
279
  end
280
280
 
281
281
  # Handle tools/list request
@@ -14,9 +14,7 @@ module FastMcp
14
14
  @auth_enabled = !@auth_token.nil?
15
15
  end
16
16
 
17
- def call(env)
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',
@@ -9,15 +9,22 @@ module FastMcp
9
9
  module Transports
10
10
  # Rack middleware transport for MCP
11
11
  # This transport can be mounted in any Rack-compatible web framework
12
- class RackTransport < BaseTransport
12
+ class RackTransport < BaseTransport # rubocop:disable Metrics/ClassLength
13
13
  DEFAULT_PATH_PREFIX = '/mcp'
14
-
15
- attr_reader :app, :path_prefix, :sse_clients
14
+ DEFAULT_ALLOWED_ORIGINS = ['localhost', '127.0.0.1', '[::1]'].freeze
15
+ DEFAULT_ALLOWED_IPS = ['127.0.0.1', '::1'].freeze
16
+ attr_reader :app, :path_prefix, :sse_clients, :messages_route, :sse_route, :allowed_origins, :localhost_only,
17
+ :allowed_ips
16
18
 
17
19
  def initialize(app, server, options = {}, &_block)
18
20
  super(server, logger: options[:logger])
19
21
  @app = app
20
22
  @path_prefix = options[:path_prefix] || DEFAULT_PATH_PREFIX
23
+ @messages_route = options[:messages_route] || 'messages'
24
+ @sse_route = options[:sse_route] || 'sse'
25
+ @allowed_origins = options[:allowed_origins] || DEFAULT_ALLOWED_ORIGINS
26
+ @localhost_only = options.fetch(:localhost_only, true) # Default to localhost-only mode
27
+ @allowed_ips = options[:allowed_ips] || DEFAULT_ALLOWED_IPS
21
28
  @sse_clients = {}
22
29
  @running = false
23
30
  end
@@ -25,6 +32,7 @@ module FastMcp
25
32
  # Start the transport
26
33
  def start
27
34
  @logger.debug("Starting Rack transport with path prefix: #{@path_prefix}")
35
+ @logger.info("DNS rebinding protection enabled. Allowed origins: #{allowed_origins.join(', ')}")
28
36
  @running = true
29
37
  end
30
38
 
@@ -100,15 +108,86 @@ module FastMcp
100
108
 
101
109
  private
102
110
 
111
+ def validate_client_ip(request)
112
+ client_ip = request.ip
113
+
114
+ # Check if we're in localhost-only mode
115
+ if @localhost_only && !@allowed_ips.include?(client_ip)
116
+ @logger.warn("Blocked connection from non-localhost IP: #{client_ip}")
117
+ return false
118
+ end
119
+
120
+ true
121
+ end
122
+
123
+ # Validate the Origin header to prevent DNS rebinding attacks
124
+ def validate_origin(request, env)
125
+ origin = env['HTTP_ORIGIN']
126
+
127
+ # If no origin header is present, check the referer or host
128
+ origin = env['HTTP_REFERER'] || request.host if origin.nil? || origin.empty?
129
+
130
+ # Extract hostname from the origin
131
+ hostname = extract_hostname(origin)
132
+
133
+ # If we have a hostname and allowed_origins is not empty
134
+ if hostname && !allowed_origins.empty?
135
+ @logger.debug("Validating origin: #{hostname}")
136
+
137
+ # Check if the hostname matches any allowed origin
138
+ is_allowed = allowed_origins.any? do |allowed|
139
+ if allowed.is_a?(Regexp)
140
+ hostname.match?(allowed)
141
+ else
142
+ hostname == allowed
143
+ end
144
+ end
145
+
146
+ unless is_allowed
147
+ @logger.warn("Blocked request with origin: #{hostname}")
148
+ return false
149
+ end
150
+ end
151
+
152
+ true
153
+ end
154
+
155
+ # Extract hostname from a URL
156
+ def extract_hostname(url)
157
+ return nil if url.nil? || url.empty?
158
+
159
+ begin
160
+ # Check if the URL has a scheme, if not, add http:// as a prefix
161
+ has_scheme = url.match?(%r{^[a-zA-Z][a-zA-Z0-9+.-]*://})
162
+ parsing_url = has_scheme ? url : "http://#{url}"
163
+
164
+ uri = URI.parse(parsing_url)
165
+
166
+ # Return nil for invalid URLs where host is empty
167
+ return nil if uri.host.nil? || uri.host.empty?
168
+
169
+ uri.host
170
+ rescue URI::InvalidURIError
171
+ # If standard parsing fails, try to extract host with a regex for host:port format
172
+ url.split(':').first if url.match?(%r{^([^:/]+)(:\d+)?$})
173
+ end
174
+ end
175
+
103
176
  # Handle MCP-specific requests
104
177
  def handle_mcp_request(request, env)
178
+ # Validate client IP to ensure it's connecting from allowed sources
179
+ return forbidden_response('Forbidden: Remote IP not allowed') unless validate_client_ip(request)
180
+
181
+ # Validate Origin header to prevent DNS rebinding attacks
182
+ return forbidden_response('Forbidden: Origin validation failed') unless validate_origin(request, env)
183
+
105
184
  subpath = request.path[@path_prefix.length..]
106
185
  @logger.info("MCP request subpath: '#{subpath.inspect}'")
107
186
 
108
187
  case subpath
109
- when '/sse'
188
+ when "/#{@sse_route}"
110
189
  handle_sse_request(request, env)
111
- when '/messages'
190
+ when "/#{@messages_route}"
112
191
  handle_message_request(request)
113
192
  else
114
193
  @logger.info('Received unknown request')
@@ -117,6 +196,20 @@ module FastMcp
117
196
  end
118
197
  end
119
198
 
199
+ def forbidden_response(message)
200
+ [403, { 'Content-Type' => 'application/json' },
201
+ [JSON.generate(
202
+ {
203
+ jsonrpc: '2.0',
204
+ error: {
205
+ code: -32_600,
206
+ message: message
207
+ },
208
+ id: nil
209
+ }
210
+ )]]
211
+ end
212
+
120
213
  # Return a 404 endpoint not found response
121
214
  def endpoint_not_found_response
122
215
  [404, { 'Content-Type' => 'application/json' },
@@ -213,9 +306,6 @@ module FastMcp
213
306
  browser_type = detect_browser_type(user_agent)
214
307
  @logger.info("Client connection from: #{user_agent} (#{browser_type})")
215
308
 
216
- # Handle MCP inspector with fixed client ID
217
- @logger.info("MCP Inspector detected, using fixed client ID: #{client_id}") if mcp_inspector?(user_agent, env)
218
-
219
309
  # Handle reconnection
220
310
  if client_id && @sse_clients.key?(client_id)
221
311
  handle_client_reconnection(client_id, browser_type)
@@ -248,11 +338,6 @@ module FastMcp
248
338
  end
249
339
  end
250
340
 
251
- # Check if client is MCP inspector
252
- def mcp_inspector?(user_agent, env)
253
- user_agent.include?('mcp-inspector') || (env['mcp.client_name'] == 'mcp-inspector')
254
- end
255
-
256
341
  # Handle client reconnection
257
342
  def handle_client_reconnection(client_id, browser_type)
258
343
  @logger.info("Client #{client_id} is reconnecting (#{browser_type})")
@@ -300,7 +385,7 @@ module FastMcp
300
385
  io.write(": SSE connection established\n\n")
301
386
 
302
387
  # Send endpoint information as the first message
303
- endpoint = "#{@path_prefix}/messages"
388
+ endpoint = "#{@path_prefix}/#{@messages_route}"
304
389
  @logger.debug("Sending endpoint information to client #{client_id}: #{endpoint}")
305
390
  io.write("event: endpoint\ndata: #{endpoint}\n\n")
306
391
 
@@ -332,13 +417,11 @@ module FastMcp
332
417
  @logger.info("Starting keep-alive loop for SSE connection #{client_id}")
333
418
  ping_count = 0
334
419
  ping_interval = 1 # Send a ping every 1 second
335
- max_ping_count = 30 # Reset connection after 30 pings (about 30 seconds)
336
420
  @running = true
337
421
 
338
422
  while @running && !io.closed?
339
423
  begin
340
- ping_count = send_keep_alive_ping(io, client_id, ping_count, max_ping_count)
341
- break if ping_count >= max_ping_count
424
+ ping_count = send_keep_alive_ping(io, client_id, ping_count)
342
425
 
343
426
  sleep ping_interval
344
427
  rescue Errno::EPIPE, IOError => e
@@ -350,7 +433,7 @@ module FastMcp
350
433
  end
351
434
 
352
435
  # Send a keep-alive ping and return the updated ping count
353
- def send_keep_alive_ping(io, client_id, ping_count, max_ping_count)
436
+ def send_keep_alive_ping(io, client_id, ping_count)
354
437
  ping_count += 1
355
438
 
356
439
  # Send a comment before each ping to keep the connection alive
@@ -363,12 +446,6 @@ module FastMcp
363
446
  send_ping_event(io)
364
447
  end
365
448
 
366
- # If we've reached the max ping count, force a reconnection
367
- if ping_count >= max_ping_count
368
- @logger.debug("Reached max ping count (#{max_ping_count}) for client #{client_id}, forcing reconnection")
369
- send_reconnect_event(io)
370
- end
371
-
372
449
  ping_count
373
450
  end
374
451
 
@@ -377,18 +454,12 @@ module FastMcp
377
454
  ping_message = {
378
455
  jsonrpc: '2.0',
379
456
  method: 'ping',
380
- id: SecureRandom.uuid
457
+ id: rand(1_000_000)
381
458
  }
382
459
  io.write("event: ping\ndata: #{JSON.generate(ping_message)}\n\n")
383
460
  io.flush
384
461
  end
385
462
 
386
- # Send a reconnect event
387
- def send_reconnect_event(io)
388
- io.write("event: reconnect\ndata: {\"reason\":\"timeout prevention\"}\n\n")
389
- io.flush
390
- end
391
-
392
463
  # Clean up SSE connection
393
464
  def cleanup_sse_connection(client_id, io)
394
465
  @logger.info("Cleaning up SSE connection for client #{client_id}")
data/lib/mcp/version.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Version information
4
3
  module FastMcp
5
- VERSION = '1.0.0'
4
+ VERSION = '1.2.0'
6
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.0.0
4
+ version: 1.2.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-03-30 00:00:00.000000000 Z
11
+ date: 2025-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64