fast_mcp_jwt_auth 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a40c2263c74d33678f1b189fd831d51e5ea5ebf4e498fb3334e93f3763997e99
4
+ data.tar.gz: ef51aae1fa0e79848a23f67cf4a77194d67dfd244594edcc9f4ffcd356069249
5
+ SHA512:
6
+ metadata.gz: 357905685ab8feab45e48cfa5dc0b2c302b8e795c82038b98046f1ae732edbd6e685787e3b3b149c3967c4e4f711fab040fb95c489aceb1491448e65db248a0f
7
+ data.tar.gz: df3d0ace55cf03367db55b0c0cb290319d12b20a015f3ac771b4e72e0cbea499159ae6c684535507eea103f3afedd5dd8adaa070315985e2d679ee6578644976
data/.DS_Store ADDED
Binary file
data/.mcp.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "mcpServers": {
3
+ "workvector-production": {
4
+ "type": "sse",
5
+ "name": "WorkVector Production",
6
+ "url": "https://workvector.com/mcp/sse",
7
+ "headers": {
8
+ "Authorization": "Bearer ${WORKVECTOR_TOKEN}"
9
+ }
10
+ },
11
+ "filesystem-project": {
12
+ "type": "stdio",
13
+ "name": "Filesystem",
14
+ "command": "npx",
15
+ "args": [
16
+ "-y",
17
+ "@modelcontextprotocol/server-filesystem",
18
+ "${PWD}"
19
+ ]
20
+ },
21
+ "llmmn-production": {
22
+ "type": "sse",
23
+ "name": "LLM Memory Notes Production",
24
+ "url": "https://llm-memory.com/mcp/sse",
25
+ "headers": {
26
+ "Authorization": "Bearer ${LLMMN_TOKEN}"
27
+ }
28
+ }
29
+ }
30
+ }
data/.mcp.json.example ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "mcpServers": {
3
+ "example-server": {
4
+ "type": "sse",
5
+ "name": "Example MCP Server",
6
+ "url": "https://example.com/mcp/sse",
7
+ "headers": {
8
+ "Authorization": "Bearer ${MCP_JWT_TOKEN}"
9
+ }
10
+ },
11
+ "workvector-production": {
12
+ "type": "sse",
13
+ "name": "WorkVector Production",
14
+ "url": "https://workvector.com/mcp/sse",
15
+ "headers": {
16
+ "Authorization": "Bearer ${WORKVECTOR_TOKEN}"
17
+ }
18
+ },
19
+ "filesystem-project": {
20
+ "type": "stdio",
21
+ "name": "Filesystem",
22
+ "command": "npx",
23
+ "args": [
24
+ "-y",
25
+ "@modelcontextprotocol/server-filesystem",
26
+ "${PWD}"
27
+ ]
28
+ }
29
+ }
30
+ }
data/.rubocop.yml ADDED
@@ -0,0 +1,34 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ plugins:
7
+ - rubocop-minitest
8
+
9
+ Layout/LineLength:
10
+ Max: 200
11
+
12
+ Style/StringLiterals:
13
+ EnforcedStyle: double_quotes
14
+
15
+ Style/StringLiteralsInInterpolation:
16
+ EnforcedStyle: double_quotes
17
+
18
+ # Relax some metrics for reasonable code
19
+ Metrics/ClassLength:
20
+ Max: 150
21
+
22
+ Metrics/MethodLength:
23
+ Max: 30
24
+
25
+ Metrics/AbcSize:
26
+ Max: 35
27
+
28
+ # Allow longer module for JWT authentication patch - it's a cohesive logical unit
29
+ Metrics/ModuleLength:
30
+ Max: 130
31
+
32
+ # Allow development dependencies in gemspec for gems
33
+ Gemspec/DevelopmentDependencies:
34
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.4.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-08-19
11
+
12
+ ### Added
13
+ - Initial release of FastMcp JWT Auth gem
14
+ - JWT token extraction from Authorization: Bearer headers
15
+ - Configurable JWT decoder callback for token decoding
16
+ - Configurable user finder callback for user lookup from decoded tokens
17
+ - Token validation with configurable callback (defaults to expiration check)
18
+ - Current user setter and resetter with configurable callbacks
19
+ - Automatic Rails integration via Railtie
20
+ - Lazy patch application for FastMcp::Transports::RackTransport
21
+ - Comprehensive test suite with mocked dependencies
22
+ - Error handling with graceful fallback to normal request processing
23
+ - Support for Rails 7.0+ and Ruby 3.1+
24
+
25
+ ### Changed
26
+ - N/A (initial release)
27
+
28
+ ### Deprecated
29
+ - N/A (initial release)
30
+
31
+ ### Removed
32
+ - N/A (initial release)
33
+
34
+ ### Fixed
35
+ - N/A (initial release)
36
+
37
+ ### Security
38
+ - N/A (initial release)
data/CLAUDE.md ADDED
@@ -0,0 +1,148 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code when working with the `fast_mcp_jwt_auth` gem.
4
+
5
+ ## Gem Overview
6
+
7
+ FastMCp Jwt Auth provides JWT authentication for FastMcp RackTransport, enabling secure user authentication in MCP requests.
8
+ It integrates seamlessly with Rails, allowing you to authenticate users using JWT tokens in a Rails application.
9
+
10
+ ## Code Conventions
11
+
12
+ ### Code Quality
13
+ - Max 200 chars/line (soft limit - prefer readability over strict compliance)
14
+ - breaking Ruby chain calls destroys the natural sentence flow and readability
15
+ - 14 lines/method, 110 lines/class
16
+ - Comments and tests in English
17
+ - KEEP CODE DRY (Don't Repeat Yourself)
18
+
19
+ ### Ruby/Rails Philosophy
20
+ - **DO IT RUBY WAY OR RAILS WAY** - it's not Python, Java or PHP!
21
+ - Strong use of Ruby metaprogramming techniques
22
+ - code line should look like human sentence (e.g. `3.times do` not `for i in 0..2 do` - Ruby syntax reads like English)
23
+ - keep code raising exceptions when it's programmer's fault - DO NOT validate method parameters, expect them to be correct! Only validate user input
24
+ - do not repeat name of parameter in method name (e.g. `def create_new_user_from_user(user)` should be `def create_new_user_from(user)`)
25
+ - do not use extra variable if used only once - saves memory and reduces GC pressure under high traffic (e.g. `user = User.find(params[:id]); user.update(...)` should be `User.find(params[:id]).update(...)`) - use `.tap do` for chaining when you need to use the object later
26
+ - use metaprogramming instead of case statements (e.g. `self.send(method_name, params)` instead of `case method_name; when "find_slot"...` - let Ruby handle method dispatch and NoMethodError)
27
+ - PREFER FUNCTIONAL STYLE: use flat_map, map, select over loops and temp variables (e.g. `items.flat_map(&:children).uniq` not `results = []; items.each { |i| results.concat(i.children) }; results.uniq`)
28
+ - USE PATTERN MATCHING: Ruby 3.0+ `case/in` for complex conditionals instead of if/elsif chains - more expressive and catches unhandled cases
29
+ - ONE CLEAR RESPONSIBILITY: each method should do one thing well - if method has "and" in description, split it (e.g. `normalize_and_search` → `normalize` + `search`)
30
+ - FOLLOW KISS PRINCIPLE: Keep It Simple, Stupid - avoid unnecessary complexity, use simple solutions first
31
+ - ALWAYS TEST YOUR CODE
32
+
33
+ ### Error Handling
34
+ - Use meaningful exception classes (not generic StandardError)
35
+ - Log errors with context using the configured logger
36
+ - Proper error propagation with fallback mechanisms
37
+ - Use `rescue_from` for common exceptions in Rails integration
38
+
39
+ ### Performance Considerations
40
+ - Use database connection pooling efficiently
41
+ - Avoid blocking operations in main threads
42
+ - Cache expensive operations
43
+ - Monitor thread lifecycle and cleanup
44
+
45
+ ### Thread Safety
46
+ - All operations must be thread-safe for cluster mode
47
+ - Use proper synchronization when accessing shared resources
48
+ - Handle thread lifecycle correctly (creation, monitoring, cleanup)
49
+ - Use connection checkout/checkin pattern for database operations
50
+
51
+ ### Gem Specific Guidelines
52
+
53
+ #### Configuration
54
+ - Use configuration object pattern for all settings
55
+ - Provide sensible defaults that work out of the box
56
+ - Make all components configurable but not required
57
+ - Support both programmatic and initializer-based configuration
58
+
59
+ #### Rails Integration
60
+ - Use Railtie for automatic Rails integration
61
+ - Hook into appropriate Rails lifecycle events
62
+ - Respect Rails conventions for logging and error handling
63
+ - Provide manual configuration options for non-Rails usage
64
+
65
+ #### Error Recovery
66
+ - Implement automatic retry with backoff for transient errors
67
+ - Provide fallback mechanisms when PubSub fails
68
+ - Log errors appropriately without flooding logs
69
+ - Handle connection failures gracefully
70
+
71
+ #### Testing
72
+ - Test all public interfaces
73
+ - Mock external dependencies (PostgreSQL, FastMcp)
74
+ - Test error conditions and edge cases
75
+ - Provide test helpers for gem users
76
+ - Test both Rails and non-Rails usage
77
+
78
+ ## Architecture
79
+
80
+ ### Components
81
+
82
+ 1. **FastMcpJwtAuth::Service** - Core JWT authentication service
83
+ - Handles JWT token generation and validation
84
+ - Integrates with FastMcp RackTransport for secure requests
85
+ 2. **FastMcpJwtAuth::Configuration** - Configuration management
86
+ - Manages settings like JWT secret, expiration, and algorithm
87
+ 3. **FastMcpJwtAuth::RackTransportPatch** - Monkey patch for FastMcp transport
88
+ - Overrides `send_message` to include JWT authentication
89
+ 4. **FastMcpJwtAuth::Railtie** - Rails integration and lifecycle management
90
+ - Automatically patches FastMcp::Transports::RackTransport during Rails initialization
91
+
92
+ ### Message Flow
93
+
94
+ 1. **MCP Request Received** - FastMcp RackTransport receives HTTP request with Authorization header
95
+ 2. **JWT Extraction** - Extract Bearer token from Authorization header (`HTTP_AUTHORIZATION`)
96
+ 3. **Token Decoding** - Use configured `jwt_decoder` callback to decode JWT token
97
+ 4. **Token Validation** - Validate token expiration and other claims using `token_validator` callback
98
+ 5. **User Lookup** - Find user from decoded token using `user_finder` callback
99
+ 6. **User Assignment** - Set current user in context using `current_user_setter` callback
100
+ 7. **Request Processing** - Continue with normal MCP request handling
101
+ 8. **Cleanup** - Clear current user context using `current_resetter` callback
102
+
103
+ ### Thread Management
104
+
105
+ The gem is designed to be thread-safe for use in Rails applications:
106
+
107
+ - **Request Isolation** - Each MCP request runs in its own thread context
108
+ - **Current User Context** - Uses thread-local storage via Rails `Current` class for user context
109
+ - **Monkey Patching Safety** - Patch is applied only once using thread-safe flag checking
110
+ - **No Shared State** - All operations are stateless except for configuration (immutable after initialization)
111
+ - **Callback Thread Safety** - User-provided callbacks (`jwt_decoder`, `user_finder`, etc.) must be thread-safe
112
+ - **Automatic Cleanup** - Current user context is always cleared after request processing (even on exceptions)
113
+
114
+ ## Dependencies
115
+
116
+ ### Runtime Dependencies
117
+ - **rails** (>= 7.0) - Required for Rails integration, Current class, and logger support
118
+
119
+ ### Development Dependencies
120
+ - **jwt** (~> 2.0) - Used in tests for JWT token generation and decoding examples
121
+ - **minitest** (~> 5.16) - Test framework
122
+ - **rubocop** (~> 1.21) - Ruby code style enforcement
123
+ - **rubocop-minitest** (~> 0.25) - Minitest-specific RuboCop rules
124
+ - **rubocop-rails** (~> 2.0) - Rails-specific RuboCop rules
125
+
126
+ ### External Dependencies
127
+ - **FastMcp** - The gem monkey patches `FastMcp::Transports::RackTransport` (not declared as dependency to avoid circular dependencies)
128
+ - **JWT Library** - Users must provide their own JWT decoder implementation (commonly `jwt` gem)
129
+
130
+ ## Development
131
+
132
+ ### Running Tests
133
+ ```bash
134
+ bundle exec rake test
135
+ ```
136
+
137
+ ### Linting
138
+ ```bash
139
+ bundle exec rubocop
140
+ ```
141
+
142
+ ### Console
143
+ ```bash
144
+ bundle exec rake console
145
+ ```
146
+
147
+ ## Project-Specific Info
148
+ - **LLM Memory identifier**: `fast_mcp_jwt_auth`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 josefchmel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # FastMcp JWT Auth
2
+
3
+ **JWT Authorization header authentication extension for [FastMcp](https://github.com/yjacquin/fast-mcp) RackTransport.**
4
+
5
+ This gem extends the [FastMcp](https://github.com/yjacquin/fast-mcp) gem to enable JWT-based user authentication via Authorization headers in Rails applications. It provides configurable callbacks for token decoding, user lookup, and validation.
6
+
7
+ ## Problem
8
+
9
+ FastMcp::Transports::RackTransport doesn't have built-in JWT authentication support. For integrating with external MCP clients that use JWT tokens for authentication, you need a way to:
10
+
11
+ 1. Extract JWT tokens from Authorization headers
12
+ 2. Decode and validate the tokens
13
+ 3. Find users based on token payload
14
+ 4. Set `Current.user` for the request duration
15
+
16
+ ## Solution
17
+
18
+ This gem provides a monkey patch for `FastMcp::Transports::RackTransport` that:
19
+
20
+ 1. Extracts JWT tokens from `Authorization: Bearer` headers
21
+ 2. Decodes tokens using configurable callbacks
22
+ 3. Validates token expiration and other claims
23
+ 4. Finds users using configurable lookup logic
24
+ 5. Sets `Current.user` for request duration
25
+ 6. Cleans up `Current` after request processing
26
+
27
+ ## Installation
28
+
29
+ **Prerequisites**: This gem requires the [fast-mcp](https://github.com/yjacquin/fast-mcp) gem to be installed first.
30
+
31
+ Add both gems to your application's Gemfile:
32
+
33
+ ```ruby
34
+ gem 'fast-mcp' # Required base gem
35
+ gem 'fast_mcp_jwt_auth', github: 'jchsoft/fast_mcp_jwt_auth' # This extension
36
+ ```
37
+
38
+ And then execute:
39
+
40
+ ```bash
41
+ bundle install
42
+ ```
43
+
44
+ **Note**: The `fast-mcp` gem provides the core MCP (Model Context Protocol) server functionality, while this gem extends it with JWT authentication support.
45
+
46
+ ## Usage
47
+
48
+ ### Automatic Integration
49
+
50
+ **No configuration needed for basic usage!** Just add the gem to your Gemfile and configure the callbacks.
51
+
52
+ The gem will:
53
+ - ✅ **Automatically patch** FastMcp::Transports::RackTransport during Rails initialization
54
+ - ✅ **Extract JWT tokens** from Authorization: Bearer headers automatically
55
+ - ✅ **Use Rails.logger** for logging (no configuration required)
56
+ - ✅ **Handle errors gracefully** with fallback to normal request processing
57
+
58
+ ### MCP Server Configuration
59
+
60
+ ⚠️ **IMPORTANT**: This gem enables JWT authentication for your Rails application when used with the `fast_mcp` gem. For MCP clients to authenticate with your Rails app, they need to send JWT tokens in the `Authorization: Bearer` header.
61
+
62
+ ### Client-Side MCP Configuration
63
+
64
+ When your Rails app is running as an MCP server (using `fast_mcp` gem and `fast_mcp_jwt_auth` gem), MCP clients need to be configured with proper authentication headers to connect to it.
65
+
66
+ For example create or update your `.mcp.json` configuration file:
67
+
68
+ ```bash
69
+ cp .mcp.json.example .mcp.json
70
+ ```
71
+
72
+ **Critical: The `headers` section with `Authorization: Bearer` is essential for JWT authentication:**
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "your-rails-app": {
78
+ "type": "sse",
79
+ "name": "Your Rails MCP Server",
80
+ "url": "https://your-rails-app.com/mcp/sse",
81
+ "headers": {
82
+ "Authorization": "Bearer ${JWT_TOKEN}"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Real Example - WorkVector Integration
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "workvector-production": {
95
+ "type": "sse",
96
+ "name": "WorkVector Production",
97
+ "url": "https://workvector.com/mcp/sse",
98
+ "headers": {
99
+ "Authorization": "Bearer ${WORKVECTOR_TOKEN}"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Why Headers are Critical
107
+
108
+ ❌ **This WON'T work** - missing authentication:
109
+ ```json
110
+ {
111
+ "mcpServers": {
112
+ "your-app": {
113
+ "type": "sse",
114
+ "url": "https://your-app.com/mcp/sse"
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ✅ **This WILL work** - includes JWT authentication header:
121
+ ```json
122
+ {
123
+ "mcpServers": {
124
+ "your-app": {
125
+ "type": "sse",
126
+ "url": "https://your-app.com/mcp/sse",
127
+ "headers": {
128
+ "Authorization": "Bearer ${JWT_TOKEN}"
129
+ }
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Environment Variables
136
+
137
+ Use environment variables for sensitive tokens in your `.mcp.json`:
138
+
139
+ - `${WORKVECTOR_TOKEN}` - Your WorkVector authentication token
140
+ - `${MCP_JWT_TOKEN}` - JWT token for other MCP servers
141
+ - `${PWD}` - Current working directory path
142
+
143
+ Set these in your environment or `.env` file:
144
+
145
+ ```bash
146
+ export WORKVECTOR_TOKEN="your_workvector_token_here"
147
+ export MCP_JWT_TOKEN="your_jwt_token_here"
148
+ ```
149
+
150
+ ### Security Best Practices
151
+
152
+ - ✅ **Never commit** `.mcp.json` to version control (it's in `.gitignore`)
153
+ - ✅ **Use environment variables** for tokens instead of hardcoding them
154
+ - ✅ **Keep tokens secure** and rotate them regularly
155
+ - ✅ **Use the example file** as a template for new environments
156
+
157
+ ## Configuration
158
+
159
+ Create an initializer to configure JWT decoding and user lookup:
160
+
161
+ ```ruby
162
+ # config/initializers/fast_mcp_jwt_auth.rb
163
+ FastMcpJwtAuth.configure do |config|
164
+ config.enabled = true
165
+
166
+ # JWT token decoding callback
167
+ config.jwt_decoder = ->(jwt_token) do
168
+ JWT.decode(jwt_token, Rails.application.credentials.secret_key_base, true, algorithm: 'HS256')[0]
169
+ end
170
+
171
+ # User lookup callback
172
+ config.user_finder = ->(decoded_token) do
173
+ User.find_by(authentication_token: decoded_token['authentication_token'])
174
+ end
175
+
176
+ # Optional: Token validation callback (defaults to expiration check)
177
+ config.token_validator = ->(decoded_token) do
178
+ decoded_token['exp'].nil? || decoded_token['exp'] >= Time.current.to_i
179
+ end
180
+
181
+ # Optional: Custom current user setter (defaults to Current.user=)
182
+ config.current_user_setter = ->(user) do
183
+ Current.user = user
184
+ end
185
+
186
+ # Optional: Custom context resetter (defaults to Current.reset)
187
+ config.current_resetter = -> do
188
+ Current.reset
189
+ end
190
+ end
191
+ ```
192
+
193
+ ### WorkVector Integration Example
194
+
195
+ For WorkVector-style JWT integration using JwtIdClaim:
196
+
197
+ ```ruby
198
+ # config/initializers/fast_mcp_jwt_auth.rb
199
+ FastMcpJwtAuth.configure do |config|
200
+ config.enabled = true
201
+
202
+ # Use JwtIdClaim for token decoding (WorkVector pattern)
203
+ config.jwt_decoder = ->(jwt_token) do
204
+ JwtIdClaim.decode(jwt_token)
205
+ end
206
+
207
+ # Find user by authentication_token from JWT payload
208
+ config.user_finder = ->(decoded_token) do
209
+ User.find_by(authentication_token: decoded_token[:authentication_token])
210
+ end
211
+ end
212
+ ```
213
+
214
+ ## Configuration Callbacks
215
+
216
+ The gem provides these configurable callbacks:
217
+
218
+ - **`jwt_decoder`**: Callback for JWT token decoding (required)
219
+ - **`user_finder`**: Callback for user lookup from decoded token (required)
220
+ - **`token_validator`**: Callback for token validation (optional, defaults to expiration check)
221
+ - **`current_user_setter`**: Callback for setting current user (optional, defaults to `Current.user=`)
222
+ - **`current_resetter`**: Callback for resetting current context (optional, defaults to `Current.reset`)
223
+
224
+ ## How It Works
225
+
226
+ 1. **Request Processing**: When FastMcp processes an MCP request, the patch intercepts it
227
+ 2. **Header Extraction**: Looks for `Authorization: Bearer <token>` header
228
+ 3. **Token Decoding**: Uses configured `jwt_decoder` callback to decode the JWT
229
+ 4. **Token Validation**: Validates token using `token_validator` callback
230
+ 5. **User Lookup**: Finds user using `user_finder` callback
231
+ 6. **Context Setting**: Sets current user using `current_user_setter` callback
232
+ 7. **Request Processing**: Continues with normal MCP request processing
233
+ 8. **Cleanup**: Resets current context using `current_resetter` callback
234
+
235
+ ## Error Handling
236
+
237
+ The gem handles errors gracefully:
238
+ - Invalid JWT tokens are logged as warnings but don't break request processing
239
+ - Missing or malformed Authorization headers are ignored silently
240
+ - Decoding errors fall back to normal request processing without authentication
241
+ - User lookup failures result in no authentication but normal request processing
242
+
243
+ ## Requirements
244
+
245
+ - Ruby >= 3.1.0
246
+ - Rails >= 7.0
247
+ - FastMcp gem
248
+
249
+ ## Development
250
+
251
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
252
+
253
+ To install this gem onto your local machine, run `bundle exec rake install`.
254
+
255
+ ## Testing
256
+
257
+ ```bash
258
+ rake test
259
+ rubocop
260
+ ```
261
+
262
+ ## Contributing
263
+
264
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jchsoft/fast_mcp_jwt_auth.
265
+
266
+ ## License
267
+
268
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpJwtAuth
4
+ # Configuration class for FastMcpJwtAuth gem settings
5
+ class Configuration
6
+ attr_accessor :enabled, :logger, :jwt_decoder, :user_finder, :token_validator,
7
+ :current_user_setter, :current_resetter
8
+
9
+ def initialize
10
+ @enabled = true
11
+ @logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
12
+ @jwt_decoder = nil
13
+ @user_finder = nil
14
+ @token_validator = lambda do |decoded_token|
15
+ decoded_token[:exp].nil? || decoded_token[:exp] >= Time.current.to_i
16
+ end
17
+ @current_user_setter = ->(user) { Current.user = user }
18
+ @current_resetter = -> { Current.reset }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Monkey patch for FastMcp::Transports::RackTransport
4
+ # Adds JWT authentication support via Authorization header
5
+
6
+ module FastMcpJwtAuth
7
+ # Lazy patch application - applies patch when FastMcp transport is first accessed
8
+ module RackTransportPatch
9
+ @patch_applied = false
10
+
11
+ def self.apply_patch!
12
+ return FastMcpJwtAuth.log_debug("RackTransport patch already applied, skipping") if @patch_applied
13
+ return FastMcpJwtAuth.log_debug("FastMcp::Transports::RackTransport not defined yet, skipping patch") unless defined?(FastMcp::Transports::RackTransport)
14
+ return FastMcpJwtAuth.log_debug("JWT authentication disabled, skipping patch") unless FastMcpJwtAuth.config.enabled
15
+
16
+ apply_patch_to_transport
17
+ end
18
+
19
+ def self.apply_patch_to_transport
20
+ FastMcpJwtAuth.log_info "Applying JWT authentication patch to FastMcp::Transports::RackTransport"
21
+ patch_transport_class
22
+ @patch_applied = true
23
+ FastMcpJwtAuth.log_info "JWT authentication patch applied successfully"
24
+ end
25
+
26
+ def self.patch_transport_class
27
+ FastMcp::Transports::RackTransport.prepend(JwtAuthenticationPatch)
28
+ end
29
+
30
+ def self.patch_applied?
31
+ @patch_applied
32
+ end
33
+
34
+ # The actual patch module that gets prepended
35
+ module JwtAuthenticationPatch
36
+ def handle_mcp_request(request, env)
37
+ authenticate_user_from_jwt(request)
38
+ super
39
+ ensure
40
+ clear_current_user
41
+ end
42
+
43
+ private
44
+
45
+ def authenticate_user_from_jwt(request)
46
+ extract_jwt_token(request)&.tap do |jwt_token|
47
+ FastMcpJwtAuth.log_debug "Extracted JWT token from Authorization header (length: #{jwt_token.length} chars)"
48
+ authenticate_user_with_token(jwt_token)
49
+ end
50
+ rescue StandardError => e
51
+ log_authentication_error(e)
52
+ end
53
+
54
+ def extract_jwt_token(request)
55
+ auth_header = request.env["HTTP_AUTHORIZATION"]
56
+ return log_and_return("No Authorization header found in request") unless auth_header
57
+ return log_and_return("Authorization header present but not Bearer token format: #{auth_header[0..20]}...") unless auth_header.start_with?("Bearer ")
58
+
59
+ auth_header.sub("Bearer ", "")
60
+ end
61
+
62
+ def log_and_return(message)
63
+ FastMcpJwtAuth.log_debug message
64
+ nil
65
+ end
66
+
67
+ def log_authentication_error(exception)
68
+ FastMcpJwtAuth.log_error "JWT token authentication failed with exception: #{exception.class.name} - #{exception.message}"
69
+ FastMcpJwtAuth.log_debug "JWT authentication error backtrace: #{exception.backtrace&.first(3)&.join("; ")}"
70
+ end
71
+
72
+ def authenticate_user_with_token(jwt_token)
73
+ return FastMcpJwtAuth.log_warn("JWT decoder not configured, skipping token authentication") unless FastMcpJwtAuth.config.jwt_decoder
74
+
75
+ decode_and_authenticate_token(jwt_token)
76
+ rescue StandardError => e
77
+ FastMcpJwtAuth.log_error "JWT token decoding failed: #{e.class.name} - #{e.message}"
78
+ end
79
+
80
+ def decode_and_authenticate_token(jwt_token)
81
+ FastMcpJwtAuth.log_debug "Attempting to decode JWT token"
82
+ return FastMcpJwtAuth.log_warn("JWT decoder returned nil - token may be invalid or malformed") unless (decoded_token = FastMcpJwtAuth.config.jwt_decoder.call(jwt_token))
83
+
84
+ FastMcpJwtAuth.log_debug "JWT token decoded successfully, checking validity"
85
+ return unless token_valid?(decoded_token)
86
+
87
+ FastMcpJwtAuth.log_debug "JWT token validation passed, looking up user"
88
+ authenticate_found_user(find_user_from_token(decoded_token))
89
+ end
90
+
91
+ def authenticate_found_user(user)
92
+ if user
93
+ FastMcpJwtAuth.log_debug "Setting current user context"
94
+ assign_current_user(user)
95
+ FastMcpJwtAuth.log_info "User authentication completed successfully"
96
+ else
97
+ FastMcpJwtAuth.log_warn "Authentication failed: no user found for token"
98
+ end
99
+ end
100
+
101
+ def token_valid?(decoded_token)
102
+ return log_debug_and_return_true?("No token validator configured, considering token valid") unless FastMcpJwtAuth.config.token_validator
103
+
104
+ validate_decoded_token(decoded_token)
105
+ rescue StandardError => e
106
+ FastMcpJwtAuth.log_error "Token validation failed with exception: #{e.class.name} - #{e.message}"
107
+ false
108
+ end
109
+
110
+ def validate_decoded_token(decoded_token)
111
+ FastMcpJwtAuth.log_debug "Running token validation"
112
+ FastMcpJwtAuth.config.token_validator.call(decoded_token).tap do |valid|
113
+ log_validation_result(valid)
114
+ end
115
+ end
116
+
117
+ def log_validation_result(valid)
118
+ if valid
119
+ FastMcpJwtAuth.log_debug "Token validation passed"
120
+ else
121
+ FastMcpJwtAuth.log_warn "Token validation failed - validator returned falsy value"
122
+ end
123
+ end
124
+
125
+ def log_debug_and_return_true?(message)
126
+ FastMcpJwtAuth.log_debug message
127
+ true
128
+ end
129
+
130
+ def find_user_from_token(decoded_token)
131
+ return FastMcpJwtAuth.log_warn("User finder not configured, cannot authenticate user") unless FastMcpJwtAuth.config.user_finder
132
+
133
+ lookup_user_from_decoded_token(decoded_token)
134
+ rescue StandardError => e
135
+ FastMcpJwtAuth.log_error "User lookup failed with exception: #{e.class.name} - #{e.message}"
136
+ nil
137
+ end
138
+
139
+ def lookup_user_from_decoded_token(decoded_token)
140
+ FastMcpJwtAuth.log_debug "Looking up user from decoded token"
141
+ FastMcpJwtAuth.config.user_finder.call(decoded_token).tap do |user|
142
+ log_user_lookup_result(user)
143
+ end
144
+ end
145
+
146
+ def log_user_lookup_result(user)
147
+ if user
148
+ FastMcpJwtAuth.log_debug "User found successfully: #{user}"
149
+ else
150
+ FastMcpJwtAuth.log_warn "User finder returned nil - user may not exist or be inactive"
151
+ end
152
+ end
153
+
154
+ def assign_current_user(user)
155
+ FastMcpJwtAuth.config.current_user_setter&.call(user)
156
+ end
157
+
158
+ def clear_current_user
159
+ FastMcpJwtAuth.config.current_resetter&.call
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ # NOTE: Patch is automatically applied by Railtie initializer when Rails loads
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpJwtAuth
4
+ # Rails integration for automatic FastMcpJwtAuth setup
5
+ class Railtie < Rails::Railtie
6
+ # Apply patch to FastMcp::Transports::RackTransport after all initializers are loaded
7
+ initializer "fast_mcp_jwt_auth.apply_patch", after: :load_config_initializers do
8
+ Rails.application.config.after_initialize do
9
+ FastMcpJwtAuth.config.enabled ? FastMcpJwtAuth::Railtie.apply_jwt_patch : FastMcpJwtAuth::Railtie.log_disabled_status
10
+ end
11
+ end
12
+
13
+ class << self
14
+ def apply_jwt_patch
15
+ FastMcpJwtAuth.log_debug "Attempting to apply RackTransport patch"
16
+ FastMcpJwtAuth::RackTransportPatch.apply_patch!
17
+ end
18
+
19
+ def log_disabled_status
20
+ FastMcpJwtAuth.log_info "JWT authentication disabled"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpJwtAuth
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fast_mcp_jwt_auth/version"
4
+ require_relative "fast_mcp_jwt_auth/configuration"
5
+
6
+ # JWT Authorization header authentication for FastMcp RackTransport.
7
+ # Enables FastMcp RackTransport to authenticate users via JWT tokens passed through Authorization headers
8
+ # with configurable callbacks for token decoding and user lookup.
9
+ module FastMcpJwtAuth
10
+ class Error < StandardError; end
11
+
12
+ class << self
13
+ attr_accessor :configuration
14
+ end
15
+
16
+ def self.configure
17
+ self.configuration ||= Configuration.new
18
+ yield(configuration)
19
+ end
20
+
21
+ def self.config
22
+ self.configuration ||= Configuration.new
23
+ end
24
+
25
+ # Simple logging helper - uses Rails.logger since this gem is Rails-specific
26
+ def self.logger
27
+ Rails.logger
28
+ end
29
+
30
+ # DRY logging with consistent prefix - Ruby metaprogramming way
31
+ %i[debug info warn error].each do |level|
32
+ define_singleton_method("log_#{level}") do |message|
33
+ logger&.public_send(level, "FastMcpJwtAuth: #{message}")
34
+ end
35
+ end
36
+ end
37
+
38
+ # Load patch after module is fully defined
39
+ require_relative "fast_mcp_jwt_auth/rack_transport_patch"
40
+ require_relative "fast_mcp_jwt_auth/railtie" if defined?(Rails)
@@ -0,0 +1,4 @@
1
+ module FastMcpJwtAuth
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_mcp_jwt_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - josefchmel
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-10-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: jwt
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.16'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.16'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.21'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.21'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop-minitest
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.25'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.25'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop-rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ description: Enables FastMcp RackTransport to authenticate users via JWT tokens passed
97
+ through Authorization headers with configurable callbacks for token decoding and
98
+ user lookup
99
+ email:
100
+ - chmel@jchsoft.cz
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".DS_Store"
106
+ - ".mcp.json"
107
+ - ".mcp.json.example"
108
+ - ".rubocop.yml"
109
+ - ".ruby-version"
110
+ - CHANGELOG.md
111
+ - CLAUDE.md
112
+ - LICENSE.txt
113
+ - README.md
114
+ - Rakefile
115
+ - lib/fast_mcp_jwt_auth.rb
116
+ - lib/fast_mcp_jwt_auth/configuration.rb
117
+ - lib/fast_mcp_jwt_auth/rack_transport_patch.rb
118
+ - lib/fast_mcp_jwt_auth/railtie.rb
119
+ - lib/fast_mcp_jwt_auth/version.rb
120
+ - sig/fast_mcp_jwt_auth.rbs
121
+ homepage: https://github.com/jchsoft/fast_mcp_jwt_auth
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ allowed_push_host: https://rubygems.org
126
+ homepage_uri: https://github.com/jchsoft/fast_mcp_jwt_auth
127
+ source_code_uri: https://github.com/jchsoft/fast_mcp_jwt_auth
128
+ changelog_uri: https://github.com/jchsoft/fast_mcp_jwt_auth/blob/main/CHANGELOG.md
129
+ rubygems_mfa_required: 'true'
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 3.1.0
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubygems_version: 3.6.2
145
+ specification_version: 4
146
+ summary: JWT Authorization header authentication for FastMcp RackTransport
147
+ test_files: []