fast_mcp_pubsub 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db696202b8d3b7dae7c2231fcb132d6dba086b20e578eac993aac24bb92d820b
4
+ data.tar.gz: 67f0d7bf55c87fcc35a1f9a9cc2686fef93329ff958c4377e823027670724aea
5
+ SHA512:
6
+ metadata.gz: 50b8171fab26238c6177e7d450eef9218adc02b76bb1e6434e65a80ef4c0ff998e43f2c3e538fccadde7097ac8928ff0b20c129aba73d36ec8d2a128a213ab2d
7
+ data.tar.gz: d386411918a12d71f91cb2b9a40a2b3aaa6bf11d527bb637c231a3bc5c9dc76e53814d91a222d418c8dea02ced23ad41aff72bb5db94d48f1982083e65000fca
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/.rubocop.yml ADDED
@@ -0,0 +1,40 @@
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 development dependencies in gemspec for gems
29
+ Gemspec/DevelopmentDependencies:
30
+ Enabled: false
31
+
32
+ Style/Documentation:
33
+ Enabled: false
34
+
35
+ # Allow longer blocks for configuration and setup
36
+ Metrics/BlockLength:
37
+ Exclude:
38
+ - "test/**/*"
39
+ - "*.gemspec"
40
+ - "lib/fast_mcp_pubsub/railtie.rb" # Debug logging temporarily increases block length
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.4.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
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
+ ## [1.0.0] - 2025-08-19
9
+
10
+ ### Added
11
+ - Initial implementation of PostgreSQL NOTIFY/LISTEN clustering for FastMcp RackTransport
12
+ - FastMcpPubsub::Service for message broadcasting and listening
13
+ - FastMcpPubsub::Configuration for configurable settings (enabled, channel_name, auto_start, connection_pool_size)
14
+ - RackTransport monkey patch for cluster mode message distribution
15
+ - Rails Railtie for automatic initialization and Rails integration
16
+ - Puma cluster mode integration with automatic worker hooks
17
+ - Payload size validation (7800 bytes limit) with fallback error responses
18
+ - Thread-safe listener management with automatic restart on errors
19
+ - Comprehensive logging via FastMcpPubsub.logger (Rails.logger)
20
+ - Connection pooling for database operations
21
+ - Automatic patch application during Rails initialization
22
+ - Method redefinition protection to avoid warnings
23
+ - Full test coverage (18 tests, 33 assertions)
24
+ - RuboCop compliance (0 offenses)
25
+
26
+ ### Implementation Details
27
+ - **Automatic Integration**: No manual configuration required - just add to Gemfile
28
+ - **Smart Timing**: Patch applied after Rails initializers load via `after: :load_config_initializers`
29
+ - **Dual Mode Support**: Works in both single-worker and cluster mode
30
+ - **Clean Logging**: Simplified logging without complex conditional checks
31
+ - **Warning-Free**: Eliminated method redefinition warnings using proper mocking patterns
32
+ - **Robust Error Handling**: Fallback to local delivery if PostgreSQL NOTIFY fails
33
+ - **Rails-Specific**: Designed for Rails applications with ActiveRecord and PostgreSQL
34
+
35
+ ### Technical Architecture
36
+ - **Patch Strategy**: Monkey patches `FastMcp::Transports::RackTransport#send_message`
37
+ - **Broadcasting**: Uses PostgreSQL NOTIFY/LISTEN for inter-worker communication
38
+ - **Listener Management**: Dedicated thread per worker with automatic lifecycle management
39
+ - **Configuration**: Simple configuration object with sensible defaults
40
+ - **Integration**: Rails Railtie for seamless Rails integration
data/CLAUDE.md ADDED
@@ -0,0 +1,108 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code when working with the `fast_mcp_pubsub` gem.
4
+
5
+ ## Gem Overview
6
+
7
+ FastMcp PubSub provides PostgreSQL NOTIFY/LISTEN clustering support for FastMcp RackTransport, enabling message broadcasting across multiple Puma workers in cluster mode.
8
+
9
+ ## Code Conventions
10
+
11
+ ### Code Quality
12
+ - Max 200 chars/line (soft limit - prefer readability over strict compliance)
13
+ - breaking Ruby chain calls destroys the natural sentence flow and readability
14
+ - 14 lines/method, 110 lines/class
15
+ - Comments and tests in English
16
+ - KEEP CODE DRY (Don't Repeat Yourself)
17
+
18
+ ### Error Handling
19
+ - Use meaningful exception classes (not generic StandardError)
20
+ - Log errors with context using the configured logger
21
+ - Proper error propagation with fallback mechanisms
22
+ - Use `rescue_from` for common exceptions in Rails integration
23
+
24
+ ### Performance Considerations
25
+ - Use database connection pooling efficiently
26
+ - Avoid blocking operations in main threads
27
+ - Cache expensive operations
28
+ - Monitor thread lifecycle and cleanup
29
+
30
+ ### Thread Safety
31
+ - All operations must be thread-safe for cluster mode
32
+ - Use proper synchronization when accessing shared resources
33
+ - Handle thread lifecycle correctly (creation, monitoring, cleanup)
34
+ - Use connection checkout/checkin pattern for database operations
35
+
36
+ ### Gem Specific Guidelines
37
+
38
+ #### Configuration
39
+ - Use configuration object pattern for all settings
40
+ - Provide sensible defaults that work out of the box
41
+ - Make all components configurable but not required
42
+ - Support both programmatic and initializer-based configuration
43
+
44
+ #### Rails Integration
45
+ - Use Railtie for automatic Rails integration
46
+ - Hook into appropriate Rails lifecycle events
47
+ - Respect Rails conventions for logging and error handling
48
+ - Provide manual configuration options for non-Rails usage
49
+
50
+ #### Error Recovery
51
+ - Implement automatic retry with backoff for transient errors
52
+ - Provide fallback mechanisms when PubSub fails
53
+ - Log errors appropriately without flooding logs
54
+ - Handle connection failures gracefully
55
+
56
+ #### Testing
57
+ - Test all public interfaces
58
+ - Mock external dependencies (PostgreSQL, FastMcp)
59
+ - Test error conditions and edge cases
60
+ - Provide test helpers for gem users
61
+ - Test both Rails and non-Rails usage
62
+
63
+ ## Architecture
64
+
65
+ ### Components
66
+
67
+ 1. **FastMcpPubsub::Service** - Core PostgreSQL NOTIFY/LISTEN service
68
+ 2. **FastMcpPubsub::Configuration** - Configuration management
69
+ 3. **FastMcpPubsub::RackTransportPatch** - Monkey patch for FastMcp transport
70
+ 4. **FastMcpPubsub::Railtie** - Rails integration and lifecycle management
71
+
72
+ ### Message Flow
73
+
74
+ 1. `RackTransport#send_message` → `FastMcpPubsub::Service.broadcast`
75
+ 2. `Service.broadcast` → PostgreSQL NOTIFY
76
+ 3. Each worker's listener thread receives NOTIFY
77
+ 4. Listener calls `RackTransport#send_local_message` for local clients
78
+
79
+ ### Thread Management
80
+
81
+ - One listener thread per worker process
82
+ - Thread cleanup on process exit
83
+ - Automatic restart on listener errors
84
+ - Connection pooling for database operations
85
+
86
+ ## Dependencies
87
+
88
+ - **Rails** (>= 7.0) - Core framework integration
89
+ - **PostgreSQL** (via pg gem >= 1.0) - Database NOTIFY/LISTEN
90
+ - **ActiveRecord** - Connection pooling and database access
91
+ - **FastMcp** - The transport being patched (development/test dependency)
92
+
93
+ ## Development
94
+
95
+ ### Running Tests
96
+ ```bash
97
+ bundle exec rake test
98
+ ```
99
+
100
+ ### Linting
101
+ ```bash
102
+ bundle exec rubocop
103
+ ```
104
+
105
+ ### Console
106
+ ```bash
107
+ bundle exec rake console
108
+ ```
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,168 @@
1
+ # FastMcp PubSub
2
+
3
+ **Multi-worker cluster support extension for [fast-mcp](https://github.com/yjacquin/fast-mcp) gem.**
4
+
5
+ This gem extends the [FastMcp](https://github.com/yjacquin/fast-mcp) gem to work with multiple Puma workers by adding PostgreSQL NOTIFY/LISTEN clustering support for `FastMcp::Transports::RackTransport` in Rails applications.
6
+
7
+ ## Problem
8
+
9
+ FastMcp::Transports::RackTransport stores SSE clients in an in-memory hash `@sse_clients`. In cluster mode (multiple Puma workers), messages don't reach between workers because each has its own memory space.
10
+
11
+ ## Solution
12
+
13
+ This gem provides PostgreSQL NOTIFY/LISTEN system for broadcasting messages between workers:
14
+
15
+ 1. `send_message` → PostgreSQL NOTIFY
16
+ 2. Listener thread in each worker → PostgreSQL LISTEN
17
+ 3. On notification → `send_local_message` to local clients
18
+
19
+ ## Installation
20
+
21
+ **Prerequisites**: This gem requires the [fast-mcp](https://github.com/yjacquin/fast-mcp) gem to be installed first.
22
+
23
+ Add both gems to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'fast-mcp', '~> 1.5.0' # Required base gem
27
+ gem 'fast_mcp_pubsub' # This extension
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ **Note**: The `fast-mcp` gem provides the core MCP (Model Context Protocol) server functionality, while this gem extends it with multi-worker support.
37
+
38
+ ## Usage
39
+
40
+ ### Automatic Integration
41
+
42
+ **No configuration needed!** Just add the gem to your Gemfile and it works automatically.
43
+
44
+ The gem will:
45
+ - ✅ **Automatically patch** FastMcp::Transports::RackTransport during Rails initialization
46
+ - ✅ **Start listener** automatically when Rails server starts
47
+ - ✅ **Use Rails.logger** for logging (no configuration required)
48
+ - ✅ **Work in both** single-worker and cluster mode
49
+
50
+ ### Optional Configuration
51
+
52
+ If you need custom settings:
53
+
54
+ ```ruby
55
+ # config/initializers/fast_mcp_pubsub.rb (optional)
56
+ FastMcpPubsub.configure do |config|
57
+ config.enabled = Rails.env.production? # Enable only in production
58
+ config.channel_name = 'my_custom_channel' # Custom PostgreSQL NOTIFY channel
59
+ config.auto_start = true # Start listener automatically (default: true)
60
+ config.connection_pool_size = 10 # Database connection pool size
61
+ end
62
+ ```
63
+
64
+ ### Puma Cluster Mode
65
+
66
+ For cluster mode (multiple workers), you need to manually start the listener in each worker process:
67
+
68
+ ```ruby
69
+ # config/puma/production.rb
70
+ workers ENV.fetch("WEB_CONCURRENCY") { 2 }
71
+ preload_app!
72
+
73
+ on_worker_boot do
74
+ Rails.logger.info "MCP Transport: Starting PubSub listener for cluster mode worker #{Process.pid}"
75
+
76
+ # Start FastMcpPubsub listener in each worker
77
+ FastMcpPubsub::Service.start_listener
78
+
79
+ # Your other worker boot code...
80
+ end
81
+ ```
82
+
83
+ **Why manual setup is required:**
84
+ - 🔧 **Master process** automatically detects cluster mode and skips listener startup
85
+ - 👷 **Worker processes** need explicit listener startup in `on_worker_boot` hook
86
+ - 📡 **Each worker** gets its own listener thread for receiving broadcasts
87
+ - 🔄 **Automatic cleanup** happens on worker shutdown
88
+
89
+ ### Manual Control
90
+
91
+ ```ruby
92
+ # Manually start/stop listener
93
+ FastMcpPubsub::Service.start_listener
94
+ FastMcpPubsub::Service.stop_listener
95
+
96
+ # Check listener status
97
+ FastMcpPubsub::Service.listener_thread&.alive?
98
+ ```
99
+
100
+ ## How It Works
101
+
102
+ 1. **Patches FastMcp::Transports::RackTransport**: Overrides `send_message` method
103
+ 2. **Broadcasts via PostgreSQL**: Uses `NOTIFY channel, payload`
104
+ 3. **Listener threads**: Each worker has a dedicated listener thread
105
+ 4. **Local delivery**: Messages are delivered to local SSE clients in each worker
106
+
107
+ ## Configuration Options
108
+
109
+ | Option | Default | Description |
110
+ |--------|---------|-------------|
111
+ | `enabled` | `true` | Enable/disable PubSub functionality |
112
+ | `channel_name` | `'mcp_broadcast'` | PostgreSQL NOTIFY channel name |
113
+ | `auto_start` | `true` | Start listener automatically |
114
+ | `connection_pool_size` | `5` | Database connection pool size |
115
+
116
+ **Note**: Logging is handled automatically via `FastMcpPubsub.logger` which returns `Rails.logger`.
117
+
118
+ ## Error Handling
119
+
120
+ - **Payload size limit**: 7800 bytes (PostgreSQL NOTIFY limit)
121
+ - **Fallback mechanism**: Falls back to local delivery if PubSub fails
122
+ - **Automatic restart**: Listener restarts on connection errors
123
+ - **Graceful shutdown**: Proper cleanup on process exit
124
+
125
+ ## Requirements
126
+
127
+ **This gem is an extension for Rails applications using FastMcp and requires:**
128
+
129
+ - **[FastMcp gem](https://github.com/yjacquin/fast-mcp)** ~> 1.5.0 (the base MCP server this gem extends)
130
+ - **Rails 7.0+** (required for Railtie integration)
131
+ - **PostgreSQL database** (for NOTIFY/LISTEN functionality)
132
+ - **Puma web server** in cluster mode (multi-worker setup)
133
+
134
+ **Important Notes**:
135
+ - This gem will not work in standalone Ruby applications or non-Rails frameworks, as it relies heavily on Rails infrastructure (ActiveRecord, Railtie, Rails.logger, etc.)
136
+ - **PostgreSQL is mandatory** - this gem will NOT work with MySQL, SQLite, or other databases as it requires PostgreSQL's NOTIFY/LISTEN functionality. Support for other databases would require significant additional development.
137
+
138
+ ## Thread Safety
139
+
140
+ All operations are thread-safe and designed for multi-worker environments:
141
+
142
+ - Connection pooling for database operations
143
+ - Proper thread lifecycle management
144
+ - Automatic cleanup on process termination
145
+
146
+ ## Development
147
+
148
+ After checking out the repo, run:
149
+
150
+ ```bash
151
+ bin/setup # Install dependencies
152
+ rake test # Run tests
153
+ bin/console # Interactive prompt
154
+ ```
155
+
156
+ To install locally:
157
+
158
+ ```bash
159
+ bundle exec rake install
160
+ ```
161
+
162
+ ## Contributing
163
+
164
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jchsoft/fast_mcp_pubsub.
165
+
166
+ ## License
167
+
168
+ 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/lib/.DS_Store ADDED
Binary file
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpPubsub
4
+ # Configuration class for FastMcpPubsub gem settings
5
+ class Configuration
6
+ attr_accessor :enabled, :channel_name, :auto_start, :connection_pool_size
7
+
8
+ def initialize
9
+ @enabled = true
10
+ @channel_name = "mcp_broadcast"
11
+ @auto_start = true
12
+ @connection_pool_size = 5
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Monkey patch for FastMcp::Transports::RackTransport
4
+ # Adds PostgreSQL PubSub support for cluster mode
5
+
6
+ module FastMcpPubsub
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
+ if @patch_applied
13
+ FastMcpPubsub.logger.debug "FastMcpPubsub: RackTransport patch already applied, skipping"
14
+ return
15
+ end
16
+
17
+ unless defined?(FastMcp::Transports::RackTransport)
18
+ FastMcpPubsub.logger.debug "FastMcpPubsub: FastMcp::Transports::RackTransport not defined yet, skipping patch"
19
+ return
20
+ end
21
+
22
+ FastMcpPubsub.logger.info "FastMcpPubsub: Patching FastMcp::Transports::RackTransport for PostgreSQL PubSub support"
23
+
24
+ patch_transport_class
25
+ @patch_applied = true
26
+ FastMcpPubsub.logger.info "FastMcpPubsub: RackTransport patch applied successfully"
27
+ end
28
+
29
+ def self.patch_transport_class
30
+ add_basic_methods
31
+ add_send_message_override
32
+ add_fallback_method
33
+ end
34
+
35
+ def self.add_basic_methods
36
+ FastMcp::Transports::RackTransport.class_eval do
37
+ alias_method :send_local_message, :send_message unless method_defined?(:send_local_message)
38
+ define_method(:running?) { @running } unless method_defined?(:running?)
39
+ end
40
+ end
41
+
42
+ def self.add_send_message_override
43
+ FastMcp::Transports::RackTransport.class_eval do
44
+ return if method_defined?(:send_message_with_pubsub)
45
+
46
+ alias_method :send_message_original, :send_message if method_defined?(:send_message)
47
+
48
+ define_method(:send_message) do |message|
49
+ FastMcpPubsub.config.enabled ? broadcast_with_fallback(message) : send_local_message(message)
50
+ end
51
+
52
+ alias_method :send_message_with_pubsub, :send_message
53
+ end
54
+ end
55
+
56
+ def self.add_fallback_method
57
+ FastMcp::Transports::RackTransport.class_eval do
58
+ return if method_defined?(:broadcast_with_fallback)
59
+
60
+ define_method(:broadcast_with_fallback) do |message|
61
+ FastMcpPubsub.logger.debug "RackTransport: Broadcasting message via PostgreSQL PubSub"
62
+ FastMcpPubsub::Service.broadcast(message)
63
+ rescue StandardError => e
64
+ FastMcpPubsub.logger.error "RackTransport: Error broadcasting message: #{e.message}"
65
+ send_local_message(message)
66
+ end
67
+ end
68
+ end
69
+
70
+ def self.patch_applied?
71
+ @patch_applied
72
+ end
73
+ end
74
+ end
75
+
76
+ # NOTE: Patch is automatically applied by Railtie initializer when Rails loads
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpPubsub
4
+ # Rails integration for automatic FastMcpPubsub setup and Puma cluster mode hooks
5
+ class Railtie < Rails::Railtie
6
+ # Start listener when Rails is ready
7
+ initializer "fast_mcp_pubsub.start_listener" do |app|
8
+ railtie = self
9
+ app.config.to_prepare do
10
+ # Non-cluster mode initialization (rails server)
11
+ # Only start if we're in a web server environment
12
+ Rails.logger.info "FastMcpPubsub: Checking listener startup conditions - cluster_mode: #{railtie.send(:cluster_mode?)}, should_start: #{railtie.send(:should_start_listener?)}"
13
+
14
+ if railtie.send(:should_start_listener?)
15
+ Rails.logger.info "FastMcpPubsub: Starting listener for non-cluster mode"
16
+ FastMcpPubsub::Service.start_listener
17
+ railtie.instance_variable_set(:@listener_started, true)
18
+ else
19
+ Rails.logger.info "FastMcpPubsub: Not starting listener in master process (cluster mode detected or conditions not met)"
20
+ end
21
+ end
22
+ end
23
+
24
+ # Apply patch to FastMcp::Transports::RackTransport after all initializers are loaded
25
+ initializer "fast_mcp_pubsub.apply_patch", after: :load_config_initializers do
26
+ FastMcpPubsub.logger.debug "FastMcpPubsub: Attempting to apply RackTransport patch"
27
+ FastMcpPubsub::RackTransportPatch.apply_patch!
28
+ end
29
+
30
+ # NOTE: For cluster mode, add FastMcpPubsub::Service.start_listener to your
31
+ # on_worker_boot hook in config/puma/production.rb
32
+
33
+ private
34
+
35
+ def should_start_listener?
36
+ web_server_environment? &&
37
+ !cluster_mode? &&
38
+ FastMcpPubsub.config.enabled &&
39
+ FastMcpPubsub.config.auto_start &&
40
+ !instance_variable_get(:@listener_started)
41
+ end
42
+
43
+ def web_server_environment?
44
+ defined?(Rails::Server) || defined?(Puma) || ENV["MCP_SERVER_AUTO_START"] == "true"
45
+ end
46
+
47
+ def cluster_mode?
48
+ # Check if Puma is running in cluster mode (multiple workers)
49
+ defined?(Puma.cli_config) &&
50
+ Puma.cli_config&.options&.[](:workers).to_i > 1
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpPubsub
4
+ # Core PostgreSQL NOTIFY/LISTEN service for broadcasting MCP messages across Puma workers
5
+ class Service
6
+ MAX_PAYLOAD_SIZE = 7800 # PostgreSQL NOTIFY limit is 8000 bytes, leave some margin
7
+
8
+ class << self
9
+ attr_reader :listener_thread
10
+
11
+ def broadcast(message)
12
+ payload = message.to_json
13
+
14
+ payload_too_large?(payload) ? send_error_response(message, payload) : send_payload(payload)
15
+ rescue StandardError => e
16
+ FastMcpPubsub.logger.error "FastMcpPubsub: Error broadcasting message: #{e.message}"
17
+ raise
18
+ end
19
+
20
+ def start_listener
21
+ unless FastMcpPubsub.config.enabled
22
+ FastMcpPubsub.logger.info "FastMcpPubsub: Not starting listener - disabled in config for PID #{Process.pid}"
23
+ return
24
+ end
25
+
26
+ if @listener_thread&.alive?
27
+ FastMcpPubsub.logger.info "FastMcpPubsub: Listener already running for PID #{Process.pid}"
28
+ return
29
+ end
30
+
31
+ FastMcpPubsub.logger.info "FastMcpPubsub: Starting listener thread for PID #{Process.pid}"
32
+
33
+ @listener_thread = Thread.new do
34
+ Thread.current.name = "fast-mcp-pubsub-listener"
35
+ listen_loop
36
+ end
37
+
38
+ # Register shutdown hook
39
+ at_exit { stop_listener }
40
+ end
41
+
42
+ def stop_listener
43
+ return unless @listener_thread&.alive?
44
+
45
+ FastMcpPubsub.logger.info "FastMcpPubsub: Stopping listener thread for PID #{Process.pid}"
46
+ @listener_thread.kill
47
+ @listener_thread.join(5) # Wait max 5 seconds
48
+ @listener_thread = nil
49
+ end
50
+
51
+ private
52
+
53
+ def send_payload(payload)
54
+ channel = FastMcpPubsub.config.channel_name
55
+ FastMcpPubsub.logger.debug "FastMcpPubsub: Broadcasting message to #{channel}: #{payload.bytesize} bytes"
56
+
57
+ ActiveRecord::Base.connection.execute(
58
+ "NOTIFY #{channel}, #{ActiveRecord::Base.connection.quote(payload)}"
59
+ )
60
+ end
61
+
62
+ def payload_too_large?(payload)
63
+ payload.bytesize > MAX_PAYLOAD_SIZE
64
+ end
65
+
66
+ def send_error_response(message, payload)
67
+ FastMcpPubsub.logger.error "FastMcpPubsub: Payload too large (#{payload.bytesize} bytes > #{MAX_PAYLOAD_SIZE} bytes)"
68
+
69
+ error_message = {
70
+ jsonrpc: "2.0",
71
+ id: message[:id],
72
+ error: {
73
+ code: -32_001,
74
+ message: "Response too large for PostgreSQL NOTIFY. Try requesting smaller page size."
75
+ }
76
+ }
77
+
78
+ send_payload(error_message.to_json)
79
+ end
80
+
81
+ def listen_loop
82
+ channel = FastMcpPubsub.config.channel_name
83
+
84
+ begin
85
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
86
+ raw_conn = conn.raw_connection
87
+
88
+ FastMcpPubsub.logger.info "FastMcpPubsub: Listening on #{channel} for PID #{Process.pid}"
89
+ raw_conn.async_exec("LISTEN #{channel}")
90
+
91
+ begin
92
+ loop do
93
+ raw_conn.wait_for_notify do |channel, pid, payload|
94
+ handle_notification(channel, pid, payload)
95
+ end
96
+ end
97
+ ensure
98
+ begin
99
+ raw_conn.async_exec("UNLISTEN #{channel}")
100
+ rescue StandardError => e
101
+ FastMcpPubsub.logger.error "FastMcpPubsub: Error during UNLISTEN: #{e.message}"
102
+ end
103
+ end
104
+ end
105
+ rescue StandardError => e
106
+ FastMcpPubsub.logger.error "FastMcpPubsub: Listener error: #{e.message}"
107
+ FastMcpPubsub.logger.error e.backtrace.join("\n")
108
+
109
+ # Restart after error
110
+ sleep 1
111
+ retry
112
+ end
113
+ end
114
+
115
+ def handle_notification(_channel, pid, payload)
116
+ FastMcpPubsub.logger.debug "FastMcpPubsub: Received notification from PID #{pid}: #{payload}"
117
+
118
+ begin
119
+ message = JSON.parse(payload)
120
+
121
+ # Find active RackTransport instances and send to local clients
122
+ if defined?(FastMcp::Transports::RackTransport)
123
+ transports = transport_instances
124
+ FastMcpPubsub.logger.debug "FastMcpPubsub: Found #{transports.size} transport instances"
125
+
126
+ transports.each do |transport|
127
+ FastMcpPubsub.logger.debug "FastMcpPubsub: Sending message to transport #{transport.object_id}"
128
+ transport.send_local_message(message)
129
+ end
130
+ end
131
+ rescue JSON::ParserError => e
132
+ FastMcpPubsub.logger.error "FastMcpPubsub: Invalid JSON payload: #{e.message}"
133
+ rescue StandardError => e
134
+ FastMcpPubsub.logger.error "FastMcpPubsub: Error handling notification: #{e.message}"
135
+ end
136
+ end
137
+
138
+ def transport_instances
139
+ # Find all RackTransport instances - don't filter by running? since it's not reliably implemented
140
+ ObjectSpace.each_object(FastMcp::Transports::RackTransport).to_a
141
+ rescue StandardError
142
+ []
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastMcpPubsub
4
+ VERSION = "1.1.0"
5
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fast_mcp_pubsub/version"
4
+ require_relative "fast_mcp_pubsub/configuration"
5
+ require_relative "fast_mcp_pubsub/service"
6
+
7
+ # PostgreSQL NOTIFY/LISTEN clustering support for FastMcp RackTransport.
8
+ # Enables FastMcp RackTransport to work in cluster mode by broadcasting messages
9
+ # via PostgreSQL NOTIFY/LISTEN across multiple Puma workers.
10
+ module FastMcpPubsub
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def self.configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration)
20
+ end
21
+
22
+ def self.config
23
+ self.configuration ||= Configuration.new
24
+ end
25
+
26
+ # Simple logging helper - uses Rails.logger since this gem is Rails-specific
27
+ def self.logger
28
+ Rails.logger
29
+ end
30
+ end
31
+
32
+ # Load patch after module is fully defined
33
+ require_relative "fast_mcp_pubsub/rack_transport_patch"
34
+ require_relative "fast_mcp_pubsub/railtie" if defined?(Rails)
@@ -0,0 +1,4 @@
1
+ module FastMcpPubsub
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_mcp_pubsub
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.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: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: pg
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.16'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.16'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.21'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.21'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop-minitest
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.25'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.25'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop-rails
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
110
+ description: Enables FastMcp RackTransport to work in cluster mode by broadcasting
111
+ messages via PostgreSQL NOTIFY/LISTEN across multiple Puma workers
112
+ email:
113
+ - chmel@jchsoft.cz
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".mcp.json"
119
+ - ".rubocop.yml"
120
+ - ".ruby-version"
121
+ - CHANGELOG.md
122
+ - CLAUDE.md
123
+ - LICENSE.txt
124
+ - README.md
125
+ - Rakefile
126
+ - lib/.DS_Store
127
+ - lib/fast_mcp_pubsub.rb
128
+ - lib/fast_mcp_pubsub/configuration.rb
129
+ - lib/fast_mcp_pubsub/rack_transport_patch.rb
130
+ - lib/fast_mcp_pubsub/railtie.rb
131
+ - lib/fast_mcp_pubsub/service.rb
132
+ - lib/fast_mcp_pubsub/version.rb
133
+ - sig/fast_mcp_pubsub.rbs
134
+ homepage: https://github.com/jchsoft/fast_mcp_pubsub
135
+ licenses:
136
+ - MIT
137
+ metadata:
138
+ allowed_push_host: https://rubygems.org
139
+ homepage_uri: https://github.com/jchsoft/fast_mcp_pubsub
140
+ source_code_uri: https://github.com/jchsoft/fast_mcp_pubsub
141
+ changelog_uri: https://github.com/jchsoft/fast_mcp_pubsub/blob/main/CHANGELOG.md
142
+ rubygems_mfa_required: 'true'
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 3.1.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.6.2
158
+ specification_version: 4
159
+ summary: PostgreSQL NOTIFY/LISTEN clustering support for FastMcp RackTransport
160
+ test_files: []