solid_mcp 0.0.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +140 -0
- data/README.md +303 -14
- data/app/models/solid_mcp/message.rb +25 -0
- data/app/models/solid_mcp/record.rb +10 -0
- data/bin/rails +15 -0
- data/bin/test +8 -0
- data/db/migrate/20250624000001_create_solid_mcp_messages.rb +28 -0
- data/lib/generators/solid_mcp/install/install_generator.rb +28 -0
- data/lib/generators/solid_mcp/install/templates/create_solid_mcp_messages.rb.erb +26 -0
- data/lib/generators/solid_mcp/install/templates/solid_mcp.rb +20 -0
- data/lib/solid_mcp/cleanup_job.rb +12 -0
- data/lib/solid_mcp/configuration.rb +37 -0
- data/lib/solid_mcp/engine.rb +35 -0
- data/lib/solid_mcp/logger.rb +35 -0
- data/lib/solid_mcp/message_writer.rb +156 -0
- data/lib/solid_mcp/pub_sub.rb +58 -0
- data/lib/solid_mcp/subscriber.rb +95 -0
- data/lib/solid_mcp/test_pub_sub.rb +41 -0
- data/lib/solid_mcp/version.rb +2 -2
- data/lib/solid_mcp.rb +33 -2
- data/release-please-config.json +8 -0
- data/sig/solid_mcp.rbs +1 -1
- data/solid_mcp.gemspec +9 -4
- metadata +86 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3aa4d7472deba4836dde19ecd8f77ddf711078e1e67653e217234e68db7e8aba
|
4
|
+
data.tar.gz: 980e352ee18764b440a56cad8c8201e9158288ba617812181b72fc04af2c1405
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec42aab4269af5a30ae4bf9378cdbbbd0e3e00fc52fdf000ecd05367d520f59dc275e4d4ec536d887508e8fbf93c3de915aeed505a8a31d95439fa3e61f0dd4b
|
7
|
+
data.tar.gz: a9804075acc655457b4e857d154278ef3125706478048ec822ce42a8da29e335777017666831ef3b9793cc4bbf7add35d90fb2ae8ff6e74914f78e716ba5cccd
|
@@ -0,0 +1 @@
|
|
1
|
+
{".":"0.2.1"}
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.1](https://github.com/seuros/solid_mcp/compare/solid_mcp/v0.2.0...solid_mcp/v0.2.1) (2025-07-01)
|
4
|
+
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
* add issues permission to release workflow ([dd6d90d](https://github.com/seuros/solid_mcp/commit/dd6d90d5d3e87987c80215401bc422abf02da1b0))
|
9
|
+
* add issues permission to release workflow ([1105f92](https://github.com/seuros/solid_mcp/commit/1105f926d1266f107dad80839c1ae7bd178f8bd0))
|
10
|
+
|
11
|
+
## [0.2.0](https://github.com/seuros/solid_mcp/compare/solid_mcp-v0.1.0...solid_mcp/v0.2.0) (2025-07-01)
|
12
|
+
|
13
|
+
|
14
|
+
### Features
|
15
|
+
|
16
|
+
* initial implementation of SolidMCP ([6123b7a](https://github.com/seuros/solid_mcp/commit/6123b7a04a7a726892f04da9e33147ce2bfcfeb5))
|
17
|
+
|
3
18
|
## [0.1.0] - 2025-05-20
|
4
19
|
|
5
20
|
- Initial release
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
solid_mcp (0.2.1)
|
5
|
+
activejob (>= 8.0)
|
6
|
+
activerecord (>= 8.0)
|
7
|
+
concurrent-ruby (~> 1.0)
|
8
|
+
railties (>= 8.0)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
actionpack (8.0.2)
|
14
|
+
actionview (= 8.0.2)
|
15
|
+
activesupport (= 8.0.2)
|
16
|
+
nokogiri (>= 1.8.5)
|
17
|
+
rack (>= 2.2.4)
|
18
|
+
rack-session (>= 1.0.1)
|
19
|
+
rack-test (>= 0.6.3)
|
20
|
+
rails-dom-testing (~> 2.2)
|
21
|
+
rails-html-sanitizer (~> 1.6)
|
22
|
+
useragent (~> 0.16)
|
23
|
+
actionview (8.0.2)
|
24
|
+
activesupport (= 8.0.2)
|
25
|
+
builder (~> 3.1)
|
26
|
+
erubi (~> 1.11)
|
27
|
+
rails-dom-testing (~> 2.2)
|
28
|
+
rails-html-sanitizer (~> 1.6)
|
29
|
+
activejob (8.0.2)
|
30
|
+
activesupport (= 8.0.2)
|
31
|
+
globalid (>= 0.3.6)
|
32
|
+
activemodel (8.0.2)
|
33
|
+
activesupport (= 8.0.2)
|
34
|
+
activerecord (8.0.2)
|
35
|
+
activemodel (= 8.0.2)
|
36
|
+
activesupport (= 8.0.2)
|
37
|
+
timeout (>= 0.4.0)
|
38
|
+
activesupport (8.0.2)
|
39
|
+
base64
|
40
|
+
benchmark (>= 0.3)
|
41
|
+
bigdecimal
|
42
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
43
|
+
connection_pool (>= 2.2.5)
|
44
|
+
drb
|
45
|
+
i18n (>= 1.6, < 2)
|
46
|
+
logger (>= 1.4.2)
|
47
|
+
minitest (>= 5.1)
|
48
|
+
securerandom (>= 0.3)
|
49
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
50
|
+
uri (>= 0.13.1)
|
51
|
+
base64 (0.2.0)
|
52
|
+
benchmark (0.4.0)
|
53
|
+
bigdecimal (3.1.9)
|
54
|
+
builder (3.3.0)
|
55
|
+
concurrent-ruby (1.3.5)
|
56
|
+
connection_pool (2.5.3)
|
57
|
+
crass (1.0.6)
|
58
|
+
date (3.4.1)
|
59
|
+
drb (2.2.1)
|
60
|
+
erb (5.0.1)
|
61
|
+
erubi (1.13.1)
|
62
|
+
globalid (1.2.1)
|
63
|
+
activesupport (>= 6.1)
|
64
|
+
i18n (1.14.7)
|
65
|
+
concurrent-ruby (~> 1.0)
|
66
|
+
io-console (0.8.0)
|
67
|
+
irb (1.15.2)
|
68
|
+
pp (>= 0.6.0)
|
69
|
+
rdoc (>= 4.0.0)
|
70
|
+
reline (>= 0.4.2)
|
71
|
+
logger (1.7.0)
|
72
|
+
loofah (2.24.1)
|
73
|
+
crass (~> 1.0.2)
|
74
|
+
nokogiri (>= 1.12.0)
|
75
|
+
minitest (5.25.5)
|
76
|
+
nokogiri (1.18.8-arm64-darwin)
|
77
|
+
racc (~> 1.4)
|
78
|
+
nokogiri (1.18.8-x86_64-linux-gnu)
|
79
|
+
racc (~> 1.4)
|
80
|
+
pp (0.6.2)
|
81
|
+
prettyprint
|
82
|
+
prettyprint (0.2.0)
|
83
|
+
psych (5.2.6)
|
84
|
+
date
|
85
|
+
stringio
|
86
|
+
racc (1.8.1)
|
87
|
+
rack (3.1.15)
|
88
|
+
rack-session (2.1.1)
|
89
|
+
base64 (>= 0.1.0)
|
90
|
+
rack (>= 3.0.0)
|
91
|
+
rack-test (2.2.0)
|
92
|
+
rack (>= 1.3)
|
93
|
+
rackup (2.2.1)
|
94
|
+
rack (>= 3)
|
95
|
+
rails-dom-testing (2.2.0)
|
96
|
+
activesupport (>= 5.0.0)
|
97
|
+
minitest
|
98
|
+
nokogiri (>= 1.6)
|
99
|
+
rails-html-sanitizer (1.6.2)
|
100
|
+
loofah (~> 2.21)
|
101
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
102
|
+
railties (8.0.2)
|
103
|
+
actionpack (= 8.0.2)
|
104
|
+
activesupport (= 8.0.2)
|
105
|
+
irb (~> 1.13)
|
106
|
+
rackup (>= 1.0.0)
|
107
|
+
rake (>= 12.2)
|
108
|
+
thor (~> 1.0, >= 1.2.2)
|
109
|
+
zeitwerk (~> 2.6)
|
110
|
+
rake (13.2.1)
|
111
|
+
rdoc (6.14.0)
|
112
|
+
erb
|
113
|
+
psych (>= 4.0.0)
|
114
|
+
reline (0.6.1)
|
115
|
+
io-console (~> 0.5)
|
116
|
+
securerandom (0.4.1)
|
117
|
+
sqlite3 (2.7.0-arm64-darwin)
|
118
|
+
sqlite3 (2.7.0-x86_64-linux-gnu)
|
119
|
+
stringio (3.1.7)
|
120
|
+
thor (1.3.2)
|
121
|
+
timeout (0.4.3)
|
122
|
+
tzinfo (2.0.6)
|
123
|
+
concurrent-ruby (~> 1.0)
|
124
|
+
uri (1.0.3)
|
125
|
+
useragent (0.16.11)
|
126
|
+
zeitwerk (2.7.3)
|
127
|
+
|
128
|
+
PLATFORMS
|
129
|
+
arm64-darwin-24
|
130
|
+
x86_64-linux
|
131
|
+
|
132
|
+
DEPENDENCIES
|
133
|
+
irb
|
134
|
+
minitest (~> 5.16)
|
135
|
+
rake (~> 13.0)
|
136
|
+
solid_mcp!
|
137
|
+
sqlite3 (~> 2.0)
|
138
|
+
|
139
|
+
BUNDLED WITH
|
140
|
+
2.6.9
|
data/README.md
CHANGED
@@ -1,39 +1,328 @@
|
|
1
|
-
#
|
1
|
+
# SolidMCP
|
2
2
|
|
3
|
-
|
3
|
+
SolidMCP is a high-performance, database-backed pub/sub engine specifically designed for ActionMCP (Model Context Protocol for Rails). It provides reliable message delivery for MCP's Server-Sent Events (SSE) with support for SQLite, PostgreSQL, and MySQL.
|
4
4
|
|
5
|
-
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Database-agnostic**: Works with SQLite, PostgreSQL, and MySQL
|
8
|
+
- **Session-based routing**: Optimized for MCP's point-to-point messaging pattern
|
9
|
+
- **Batched writes**: Handles SQLite's single-writer limitation efficiently
|
10
|
+
- **Automatic cleanup**: Configurable retention periods for delivered/undelivered messages
|
11
|
+
- **Thread-safe**: Dedicated writer thread with in-memory queuing
|
12
|
+
- **SSE resumability**: Supports reconnection with last-event-id
|
13
|
+
- **Rails Engine**: Seamless integration with Rails applications
|
14
|
+
- **Multiple backends**: Database backend by default, Redis backend coming soon
|
15
|
+
|
16
|
+
## Requirements
|
17
|
+
|
18
|
+
- Ruby 3.0+
|
19
|
+
- Rails 8.0+
|
20
|
+
- ActiveRecord 8.0+
|
21
|
+
- SQLite, PostgreSQL, or MySQL database
|
6
22
|
|
7
23
|
## Installation
|
8
24
|
|
9
|
-
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem 'solid_mcp'
|
29
|
+
```
|
10
30
|
|
11
|
-
|
31
|
+
And then execute:
|
12
32
|
|
13
33
|
```bash
|
14
|
-
bundle
|
34
|
+
bundle install
|
15
35
|
```
|
16
36
|
|
17
|
-
|
37
|
+
Run the installation generator:
|
18
38
|
|
19
39
|
```bash
|
20
|
-
|
40
|
+
bin/rails generate solid_mcp:install
|
41
|
+
bin/rails db:migrate
|
42
|
+
```
|
43
|
+
|
44
|
+
This will:
|
45
|
+
- Create a migration for the `solid_mcp_messages` table
|
46
|
+
- Create an initializer with default configuration
|
47
|
+
|
48
|
+
## Configuration
|
49
|
+
|
50
|
+
Configure SolidMCP in your Rails application:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# config/initializers/solid_mcp.rb
|
54
|
+
SolidMcp.configure do |config|
|
55
|
+
# Number of messages to write in a single batch
|
56
|
+
config.batch_size = 200
|
57
|
+
|
58
|
+
# Seconds between batch flushes
|
59
|
+
config.flush_interval = 0.05
|
60
|
+
|
61
|
+
# Polling interval for checking new messages
|
62
|
+
config.polling_interval = 0.1
|
63
|
+
|
64
|
+
# Maximum time to wait for messages before timeout
|
65
|
+
config.max_wait_time = 30
|
66
|
+
|
67
|
+
# How long to keep delivered messages
|
68
|
+
config.delivered_retention = 1.hour
|
69
|
+
|
70
|
+
# How long to keep undelivered messages
|
71
|
+
config.undelivered_retention = 24.hours
|
72
|
+
end
|
21
73
|
```
|
22
74
|
|
23
|
-
## Usage
|
75
|
+
## Usage with ActionMCP
|
76
|
+
|
77
|
+
In your `config/mcp.yml`:
|
78
|
+
|
79
|
+
```yaml
|
80
|
+
production:
|
81
|
+
adapter: solid_mcp
|
82
|
+
polling_interval: 0.5.seconds
|
83
|
+
batch_size: 200
|
84
|
+
flush_interval: 0.05
|
85
|
+
```
|
86
|
+
|
87
|
+
## Architecture
|
88
|
+
|
89
|
+
SolidMCP is implemented as a Rails Engine with the following components:
|
90
|
+
|
91
|
+
### Core Components
|
92
|
+
|
93
|
+
1. **SolidMCP::MessageWriter**: Singleton that handles batched writes to the database
|
94
|
+
- Non-blocking enqueue operation
|
95
|
+
- Dedicated writer thread per Rails process
|
96
|
+
- Automatic batching and flushing
|
97
|
+
- Graceful shutdown with pending message delivery
|
98
|
+
|
99
|
+
2. **SolidMCP::PubSub**: Main interface for publishing and subscribing to messages
|
100
|
+
- Session-based subscriptions (not channel-based)
|
101
|
+
- Automatic listener management per session
|
102
|
+
- Thread-safe operations
|
103
|
+
|
104
|
+
3. **SolidMCP::Subscriber**: Handles polling for new messages
|
105
|
+
- Efficient database queries using indexes
|
106
|
+
- Automatic message delivery tracking
|
107
|
+
- Configurable polling intervals
|
24
108
|
|
25
|
-
|
109
|
+
4. **SolidMCP::Message**: ActiveRecord model for message storage
|
110
|
+
- Optimized indexes for polling and cleanup
|
111
|
+
- Scopes for message filtering
|
112
|
+
- Built-in cleanup methods
|
113
|
+
|
114
|
+
### Message Flow
|
115
|
+
|
116
|
+
1. Publisher calls `broadcast(session_id, event_type, data)`
|
117
|
+
2. MessageWriter queues the message in memory
|
118
|
+
3. Writer thread batches messages and writes to database
|
119
|
+
4. Subscriber polls for new messages for its session
|
120
|
+
5. Messages are marked as delivered after successful processing
|
121
|
+
|
122
|
+
## Database Schema
|
123
|
+
|
124
|
+
The gem creates a `solid_mcp_messages` table:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
create_table :solid_mcp_messages do |t|
|
128
|
+
t.string :session_id, null: false, limit: 36 # MCP session identifier
|
129
|
+
t.string :event_type, null: false, limit: 50 # SSE event type
|
130
|
+
t.text :data # Message payload (usually JSON)
|
131
|
+
t.datetime :created_at, null: false # Message creation time
|
132
|
+
t.datetime :delivered_at # Delivery timestamp
|
133
|
+
|
134
|
+
t.index [:session_id, :id], name: 'idx_solid_mcp_messages_on_session_and_id'
|
135
|
+
t.index [:delivered_at, :created_at], name: 'idx_solid_mcp_messages_on_delivered_and_created'
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
## Performance Considerations
|
140
|
+
|
141
|
+
### SQLite
|
142
|
+
- Single writer thread prevents "database is locked" errors
|
143
|
+
- Batching reduces write frequency
|
144
|
+
- Consider WAL mode for better concurrency
|
145
|
+
|
146
|
+
### PostgreSQL/MySQL
|
147
|
+
- Benefits from batching to reduce transaction overhead
|
148
|
+
- Can handle multiple writers but single writer is maintained for consistency
|
149
|
+
- Consider partitioning for high-volume applications
|
150
|
+
|
151
|
+
## Maintenance
|
152
|
+
|
153
|
+
### Automatic Cleanup
|
154
|
+
|
155
|
+
Old messages are automatically cleaned up based on retention settings:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
# Run periodically (e.g., with whenever gem or solid_queue)
|
159
|
+
SolidMCP::CleanupJob.perform_later
|
160
|
+
|
161
|
+
# Or directly:
|
162
|
+
SolidMCP::Message.cleanup
|
163
|
+
```
|
164
|
+
|
165
|
+
### Manual Cleanup
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# Clean up delivered messages older than 1 hour
|
169
|
+
SolidMCP::Message.old_delivered(1.hour).delete_all
|
170
|
+
|
171
|
+
# Clean up undelivered messages older than 24 hours
|
172
|
+
SolidMCP::Message.old_undelivered(24.hours).delete_all
|
173
|
+
```
|
174
|
+
|
175
|
+
### Monitoring
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# Check message queue size
|
179
|
+
SolidMCP::Message.undelivered.count
|
180
|
+
|
181
|
+
# Check messages for a specific session
|
182
|
+
SolidMCP::Message.for_session(session_id).count
|
183
|
+
|
184
|
+
# Find stuck messages
|
185
|
+
SolidMCP::Message.undelivered.where('created_at < ?', 1.hour.ago)
|
186
|
+
```
|
187
|
+
|
188
|
+
## Testing
|
189
|
+
|
190
|
+
The gem includes a test implementation for use in test environments:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
# In test environment, SolidMCP::PubSub automatically uses TestPubSub
|
194
|
+
# which provides immediate delivery without database persistence
|
195
|
+
```
|
196
|
+
|
197
|
+
Run the test suite:
|
198
|
+
|
199
|
+
```bash
|
200
|
+
bundle exec rake test
|
201
|
+
```
|
202
|
+
|
203
|
+
### Testing in Your Application
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# test/test_helper.rb
|
207
|
+
class ActiveSupport::TestCase
|
208
|
+
setup do
|
209
|
+
SolidMCP::Message.delete_all
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# In your tests
|
214
|
+
test "broadcasts message to session" do
|
215
|
+
pubsub = SolidMCP::PubSub.new
|
216
|
+
messages = []
|
217
|
+
|
218
|
+
pubsub.subscribe("test-session") do |msg|
|
219
|
+
messages << msg
|
220
|
+
end
|
221
|
+
|
222
|
+
pubsub.broadcast("test-session", "test_event", { data: "test" })
|
223
|
+
|
224
|
+
assert_equal 1, messages.size
|
225
|
+
assert_equal "test_event", messages.first[:event_type]
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
## SSE Integration
|
230
|
+
|
231
|
+
SolidMCP is designed to work seamlessly with Server-Sent Events:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
# In your SSE controller
|
235
|
+
def sse_endpoint
|
236
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
237
|
+
|
238
|
+
pubsub = SolidMCP::PubSub.new
|
239
|
+
last_event_id = request.headers['Last-Event-ID']
|
240
|
+
|
241
|
+
# Resume from last event if reconnecting
|
242
|
+
if last_event_id
|
243
|
+
missed_messages = SolidMCP::Message
|
244
|
+
.for_session(session_id)
|
245
|
+
.after_id(last_event_id)
|
246
|
+
.undelivered
|
247
|
+
|
248
|
+
missed_messages.each do |msg|
|
249
|
+
response.stream.write "id: #{msg.id}\n"
|
250
|
+
response.stream.write "event: #{msg.event_type}\n"
|
251
|
+
response.stream.write "data: #{msg.data}\n\n"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Subscribe to new messages
|
256
|
+
pubsub.subscribe(session_id) do |message|
|
257
|
+
response.stream.write "id: #{message[:id]}\n"
|
258
|
+
response.stream.write "event: #{message[:event_type]}\n"
|
259
|
+
response.stream.write "data: #{message[:data]}\n\n"
|
260
|
+
end
|
261
|
+
ensure
|
262
|
+
pubsub&.unsubscribe(session_id)
|
263
|
+
response.stream.close
|
264
|
+
end
|
265
|
+
```
|
26
266
|
|
27
267
|
## Development
|
28
268
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
269
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
270
|
+
|
271
|
+
### Running Tests
|
272
|
+
|
273
|
+
```bash
|
274
|
+
# Run all tests
|
275
|
+
bundle exec rake test
|
276
|
+
|
277
|
+
# Run specific test file
|
278
|
+
bundle exec ruby test/solid_mcp/message_test.rb
|
279
|
+
```
|
280
|
+
|
281
|
+
## Roadmap
|
282
|
+
|
283
|
+
### Redis Backend (Coming Soon)
|
284
|
+
|
285
|
+
Future versions will support Redis as an alternative backend:
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
# config/initializers/solid_mcp.rb
|
289
|
+
SolidMCP.configure do |config|
|
290
|
+
config.backend = :redis
|
291
|
+
config.redis_url = ENV['REDIS_URL']
|
292
|
+
end
|
293
|
+
```
|
30
294
|
|
31
|
-
|
295
|
+
This will provide:
|
296
|
+
- Lower latency for high-traffic applications
|
297
|
+
- Pub/Sub without polling
|
298
|
+
- Automatic expiration of old messages
|
299
|
+
- Better horizontal scaling
|
300
|
+
|
301
|
+
## Comparison with Other Solutions
|
302
|
+
|
303
|
+
| Feature | SolidMCP | ActionCable + Redis | Custom Polling |
|
304
|
+
|---------|----------|-------------------|----------------|
|
305
|
+
| No Redis Required | ✅ | ❌ | ✅ |
|
306
|
+
| SSE Resumability | ✅ | ❌ | Manual |
|
307
|
+
| Horizontal Scaling | ✅ (with DB) | ✅ | ❌ |
|
308
|
+
| Message Persistence | ✅ | ❌ | Manual |
|
309
|
+
| Batch Writing | ✅ | N/A | ❌ |
|
310
|
+
| SQLite Support | ✅ | ❌ | ✅ |
|
32
311
|
|
33
312
|
## Contributing
|
34
313
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
314
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/seuros/solid_mcp.
|
315
|
+
|
316
|
+
### Development Setup
|
317
|
+
|
318
|
+
1. Fork the repository
|
319
|
+
2. Clone your fork
|
320
|
+
3. Install dependencies: `bundle install`
|
321
|
+
4. Create a feature branch: `git checkout -b my-feature`
|
322
|
+
5. Make your changes and add tests
|
323
|
+
6. Run tests: `bundle exec rake test`
|
324
|
+
7. Push to your fork and submit a pull request
|
36
325
|
|
37
326
|
## License
|
38
327
|
|
39
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
328
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidMCP
|
4
|
+
class Message < Record
|
5
|
+
self.table_name = "solid_mcp_messages"
|
6
|
+
|
7
|
+
scope :for_session, ->(session_id) { where(session_id: session_id) }
|
8
|
+
scope :undelivered, -> { where(delivered_at: nil) }
|
9
|
+
scope :delivered, -> { where.not(delivered_at: nil) }
|
10
|
+
scope :after_id, ->(id) { where("id > ?", id) }
|
11
|
+
scope :old_delivered, ->(age) { delivered.where("delivered_at < ?", age.ago) }
|
12
|
+
scope :old_undelivered, ->(age) { undelivered.where("created_at < ?", age.ago) }
|
13
|
+
|
14
|
+
# Mark messages as delivered
|
15
|
+
def self.mark_delivered(ids)
|
16
|
+
where(id: ids).update_all(delivered_at: Time.current)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Cleanup old messages
|
20
|
+
def self.cleanup(delivered_retention: 1.hour, undelivered_retention: 24.hours)
|
21
|
+
old_delivered(delivered_retention).delete_all
|
22
|
+
old_undelivered(undelivered_retention).delete_all
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/bin/rails
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
5
|
+
# installed from the root of your application.
|
6
|
+
|
7
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
8
|
+
APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
|
9
|
+
|
10
|
+
# Set up gems listed in the Gemfile.
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
12
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
13
|
+
|
14
|
+
require 'rails/all'
|
15
|
+
require 'rails/engine/commands'
|
data/bin/test
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateSolidMCPMessages < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :solid_mcp_messages do |t|
|
6
|
+
# Session this message belongs to
|
7
|
+
t.string :session_id, null: false, limit: 36
|
8
|
+
|
9
|
+
# Type of event (e.g., 'message', 'ping', 'connection_closed')
|
10
|
+
t.string :event_type, null: false, limit: 50
|
11
|
+
|
12
|
+
# The actual data payload
|
13
|
+
t.text :data
|
14
|
+
|
15
|
+
# Timestamp when message was created
|
16
|
+
t.datetime :created_at, null: false
|
17
|
+
|
18
|
+
# Timestamp when message was delivered
|
19
|
+
t.datetime :delivered_at
|
20
|
+
|
21
|
+
# Composite index for efficient polling
|
22
|
+
t.index [:session_id, :id], name: 'idx_solid_mcp_messages_on_session_and_id'
|
23
|
+
|
24
|
+
# Index for cleanup
|
25
|
+
t.index [:delivered_at, :created_at], name: 'idx_solid_mcp_messages_on_delivered_and_created'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
module SolidMCP
|
7
|
+
module Generators
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
include ActiveRecord::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
def create_migration_file
|
14
|
+
migration_template "create_solid_mcp_messages.rb.erb", "db/migrate/create_solid_mcp_messages.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_initializer
|
18
|
+
template "solid_mcp.rb", "config/initializers/solid_mcp.rb"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def migration_version
|
24
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CreateSolidMCPMessages < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :solid_mcp_messages do |t|
|
4
|
+
# Session this message belongs to
|
5
|
+
t.string :session_id, null: false, limit: 36
|
6
|
+
|
7
|
+
# Type of event (e.g., 'message', 'ping', 'connection_closed')
|
8
|
+
t.string :event_type, null: false, limit: 50
|
9
|
+
|
10
|
+
# The actual data payload
|
11
|
+
t.text :data
|
12
|
+
|
13
|
+
# Timestamp when message was created
|
14
|
+
t.datetime :created_at, null: false
|
15
|
+
|
16
|
+
# Timestamp when message was delivered
|
17
|
+
t.datetime :delivered_at
|
18
|
+
|
19
|
+
# Composite index for efficient polling
|
20
|
+
t.index [:session_id, :id], name: 'idx_solid_mcp_messages_on_session_and_id'
|
21
|
+
|
22
|
+
# Index for cleanup
|
23
|
+
t.index [:delivered_at, :created_at], name: 'idx_solid_mcp_messages_on_delivered_and_created'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Initialize SolidMCP message writer
|
4
|
+
Rails.application.config.to_prepare do
|
5
|
+
# Ensure the writer thread is started
|
6
|
+
SolidMCP::MessageWriter.instance
|
7
|
+
end
|
8
|
+
|
9
|
+
# Gracefully shutdown on exit
|
10
|
+
at_exit do
|
11
|
+
SolidMCP::MessageWriter.instance.shutdown if defined?(SolidMCP::MessageWriter)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Configure SolidMCP
|
15
|
+
SolidMCP.configure do |config|
|
16
|
+
config.batch_size = 200
|
17
|
+
config.flush_interval = 0.05
|
18
|
+
config.delivered_retention = 1.hour
|
19
|
+
config.undelivered_retention = 24.hours
|
20
|
+
end
|