fast-mcp 1.0.0 → 1.1.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: dde8305abcf47ffc314fc95df26467f737957e1b97eecae8c2c4a88b34c76017
4
+ data.tar.gz: 22715faf289cd3f45090480f59585220c3bb904b2a15c9669c5b93700a85e104
5
5
  SHA512:
6
- metadata.gz: 564cadd64c076e784bc0779a3f697d82d50eed14c77b1403f03abeee0935972103e68bf82bb94ec7cd3443b90ddfa75d739dd6a5a0bda32590e8b31d8a708869
7
- data.tar.gz: 731cc93d551842dfcec399d7d75d86c59d4a6af88d415925757a3ced8c5947dda7ae26eafc12b71423af496c6669a084cdbaabdc05cbbacef68476d39925991e
6
+ metadata.gz: eacade9a05c17a5032bbe2ed13e68bf094e7d018f272163a70f2457971c60ed79672488ab177fe3926dc9aa51cbf738ed2e3ba70bd9ab4c309599e593659ab97
7
+ data.tar.gz: 390cb3617e29219da28df5c49ff7da06c36b09d341068b5bc4c20883b64dd61f33c67e0542e03f368bf686129f1338ea5af3157bac28439c824520095250adfd
data/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ 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.1.0] - 2025-04-13
9
+ ### Added
10
+ - Security enhancement: Added DNS rebinding protection by validating Origin headers [#32 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/32/files)
11
+ - Added configuration options for allowed origins in rack middleware [#32 @yjacquin](https://github.com/yjacquin/fast-mcp/pull/32/files)
12
+ - Allow to change the SSE and Messages route [#23 @pedrofurtado](https://github.com/yjacquin/fast-mcp/pull/23)
13
+ - Fix invalid return value when processing notifications/initialized request [#31 @abMatGit](https://github.com/yjacquin/fast-mcp/pull/31)
14
+
15
+
8
16
  ## [1.0.0] - 2025-03-30
9
17
 
10
18
  ### Added
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,13 @@ 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/],
103
106
  # authenticate: true, # Uncomment to enable authentication
104
- # auth_token: 'your-token', # Required if authenticate: true
107
+ # auth_token: 'your-token' # Required if authenticate: true
105
108
  ) do |server|
106
109
  Rails.application.config.after_initialize do
107
110
  # FastMcp will automatically discover and register:
@@ -136,11 +139,11 @@ These are automatically set up in Rails applications. You can use either naming
136
139
  # Using Rails-style naming:
137
140
  class MyTool < ActionTool::Base
138
141
  description "My awesome tool"
139
-
142
+
140
143
  arguments do
141
144
  required(:input).filled(:string)
142
145
  end
143
-
146
+
144
147
  def call(input:)
145
148
  # Your implementation
146
149
  end
@@ -182,12 +185,12 @@ server = FastMcp::Server.new(name: 'my-ai-server', version: '1.0.0')
182
185
  # Define a tool by inheriting from FastMcp::Tool
183
186
  class SummarizeTool < FastMcp::Tool
184
187
  description "Summarize a given text"
185
-
188
+
186
189
  arguments do
187
190
  required(:text).filled(:string).description("Text to summarize")
188
191
  optional(:max_length).filled(:integer).description("Maximum length of summary")
189
192
  end
190
-
193
+
191
194
  def call(text:, max_length: 100)
192
195
  # Your summarization logic here
193
196
  text.split('.').first(3).join('.') + '...'
@@ -203,7 +206,7 @@ class StatisticsResource < FastMcp::Resource
203
206
  resource_name "Usage Statistics"
204
207
  description "Current system statistics"
205
208
  mime_type "application/json"
206
-
209
+
207
210
  def content
208
211
  JSON.generate({
209
212
  users_online: 120,
@@ -307,6 +310,34 @@ Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
307
310
  - 📚 **Interactive Documentation**: Create AI-enhanced API documentation
308
311
  - 💬 **Chatbots and Assistants**: Build AI assistants with access to your app's data
309
312
 
313
+ ## 🔒 Security Features
314
+
315
+ Fast MCP includes built-in security features to protect your applications:
316
+
317
+ ### DNS Rebinding Protection
318
+
319
+ 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.
320
+
321
+ ```ruby
322
+ # Configure allowed origins (defaults to ['localhost', '127.0.0.1'])
323
+ FastMcp.rack_middleware(app,
324
+ allowed_origins: ['localhost', '127.0.0.1', 'your-domain.com', /.*\.your-domain\.com/],
325
+ # other options...
326
+ )
327
+ ```
328
+
329
+ ### Authentication
330
+
331
+ Fast MCP supports token-based authentication for all connections:
332
+
333
+ ```ruby
334
+ # Enable authentication
335
+ FastMcp.authenticated_rack_middleware(app,
336
+ auth_token: 'your-secret-token',
337
+ # other options...
338
+ )
339
+ ```
340
+
310
341
  ## 📖 Documentation
311
342
 
312
343
  - [🚀 Getting Started Guide](docs/getting_started.md)
@@ -315,6 +346,7 @@ Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
315
346
  - [🌐 Sinatra Integration](docs/sinatra_integration.md)
316
347
  - [📚 Resources](docs/resources.md)
317
348
  - [🛠️ Tools](docs/tools.md)
349
+ - [🔒 Security](docs/security.md)
318
350
 
319
351
  ## 💻 Examples
320
352
 
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,14 @@ 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)
134
144
 
135
145
  options[:logger] = logger
146
+ options[:allowed_origins] = allowed_origins
147
+
136
148
  # Create or get the server
137
149
  self.server = FastMcp::Server.new(name: name, version: version, logger: logger)
138
150
  yield self.server if block_given?
@@ -145,7 +157,25 @@ module FastMcp
145
157
  end
146
158
 
147
159
  # Insert the middleware in the Rails middleware stack
148
- app.middleware.use self.server.transport_klass, self.server, options.merge(path_prefix: path_prefix)
160
+ app.middleware.use(
161
+ self.server.transport_klass,
162
+ self.server,
163
+ options.merge(path_prefix: path_prefix, messages_route: messages_route, sse_route: sse_route)
164
+ )
165
+ end
166
+
167
+ def self.default_rails_allowed_origins(rail_app)
168
+ hosts = rail_app.config.hosts
169
+
170
+ hosts.map do |host|
171
+ if host.is_a?(String) && host.start_with?('.')
172
+ # Convert .domain to domain and *.domain
173
+ host_without_dot = host[1..]
174
+ [host_without_dot, Regexp.new(".*\.#{host_without_dot}")] # rubocop:disable Style/RedundantStringEscape
175
+ else
176
+ host
177
+ end
178
+ end.flatten.compact
149
179
  end
150
180
 
151
181
  # Notify the server that a resource has been updated
@@ -18,7 +18,11 @@ 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', 'example.com', /.*\.example\.com/],
22
26
  # authenticate: true, # Uncomment to enable authentication
23
27
  # auth_token: 'your-token', # Required if authenticate: true
24
28
  ) 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
@@ -9,15 +9,19 @@ 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
+ DEFAULT_ALLOWED_ORIGINS = ['localhost', '127.0.0.1'].freeze
14
15
 
15
- attr_reader :app, :path_prefix, :sse_clients
16
+ attr_reader :app, :path_prefix, :sse_clients, :messages_route, :sse_route, :allowed_origins
16
17
 
17
18
  def initialize(app, server, options = {}, &_block)
18
19
  super(server, logger: options[:logger])
19
20
  @app = app
20
21
  @path_prefix = options[:path_prefix] || DEFAULT_PATH_PREFIX
22
+ @messages_route = options[:messages_route] || 'messages'
23
+ @sse_route = options[:sse_route] || 'sse'
24
+ @allowed_origins = options[:allowed_origins] || DEFAULT_ALLOWED_ORIGINS
21
25
  @sse_clients = {}
22
26
  @running = false
23
27
  end
@@ -25,6 +29,7 @@ module FastMcp
25
29
  # Start the transport
26
30
  def start
27
31
  @logger.debug("Starting Rack transport with path prefix: #{@path_prefix}")
32
+ @logger.info("DNS rebinding protection enabled. Allowed origins: #{allowed_origins.join(', ')}")
28
33
  @running = true
29
34
  end
30
35
 
@@ -100,15 +105,83 @@ module FastMcp
100
105
 
101
106
  private
102
107
 
108
+ # Validate the Origin header to prevent DNS rebinding attacks
109
+ def validate_origin(request, env)
110
+ origin = env['HTTP_ORIGIN']
111
+
112
+ # If no origin header is present, check the referer or host
113
+ origin = env['HTTP_REFERER'] || request.host if origin.nil? || origin.empty?
114
+
115
+ # Extract hostname from the origin
116
+ hostname = extract_hostname(origin)
117
+
118
+ # If we have a hostname and allowed_origins is not empty
119
+ if hostname && !allowed_origins.empty?
120
+ @logger.info("Validating origin: #{hostname}")
121
+
122
+ # Check if the hostname matches any allowed origin
123
+ is_allowed = allowed_origins.any? do |allowed|
124
+ if allowed.is_a?(Regexp)
125
+ hostname.match?(allowed)
126
+ else
127
+ hostname == allowed
128
+ end
129
+ end
130
+
131
+ unless is_allowed
132
+ @logger.warn("Blocked request with origin: #{hostname}")
133
+ return false
134
+ end
135
+ end
136
+
137
+ true
138
+ end
139
+
140
+ # Extract hostname from a URL
141
+ def extract_hostname(url)
142
+ return nil if url.nil? || url.empty?
143
+
144
+ begin
145
+ # Check if the URL has a scheme, if not, add http:// as a prefix
146
+ has_scheme = url.match?(%r{^[a-zA-Z][a-zA-Z0-9+.-]*://})
147
+ parsing_url = has_scheme ? url : "http://#{url}"
148
+
149
+ uri = URI.parse(parsing_url)
150
+
151
+ # Return nil for invalid URLs where host is empty
152
+ return nil if uri.host.nil? || uri.host.empty?
153
+
154
+ uri.host
155
+ rescue URI::InvalidURIError
156
+ # If standard parsing fails, try to extract host with a regex for host:port format
157
+ url.split(':').first if url.match?(%r{^([^:/]+)(:\d+)?$})
158
+ end
159
+ end
160
+
103
161
  # Handle MCP-specific requests
104
162
  def handle_mcp_request(request, env)
163
+ # 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
177
+
105
178
  subpath = request.path[@path_prefix.length..]
106
179
  @logger.info("MCP request subpath: '#{subpath.inspect}'")
107
180
 
108
181
  case subpath
109
- when '/sse'
182
+ when "/#{@sse_route}"
110
183
  handle_sse_request(request, env)
111
- when '/messages'
184
+ when "/#{@messages_route}"
112
185
  handle_message_request(request)
113
186
  else
114
187
  @logger.info('Received unknown request')
@@ -300,7 +373,7 @@ module FastMcp
300
373
  io.write(": SSE connection established\n\n")
301
374
 
302
375
  # Send endpoint information as the first message
303
- endpoint = "#{@path_prefix}/messages"
376
+ endpoint = "#{@path_prefix}/#{@messages_route}"
304
377
  @logger.debug("Sending endpoint information to client #{client_id}: #{endpoint}")
305
378
  io.write("event: endpoint\ndata: #{endpoint}\n\n")
306
379
 
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.1.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.1.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-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64