actionmcp 0.52.1 → 0.52.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba6892aa3c28876f79be1ab3b365b2ed70b449e6f55781de9ea565e3f26d4f8e
4
- data.tar.gz: ba861b9e0ec4dcdfcf743a70e8fed177a98a169c86c56f9f5d7b50c4bf7bf7b6
3
+ metadata.gz: a11c68a5511cb3cafd2f90ae24aa31e2099f41c8b4ee3d357719574f598752d4
4
+ data.tar.gz: 145398e069f03f6bd2697a16efca476d47983ccdcf84f3730f9ea0b0223eec03
5
5
  SHA512:
6
- metadata.gz: de7d1b0f9db37da9cc7be60ec42aac243ffb521cf878ba4c537269c0afeabdf22c5a2431b6eb5a1a00e79d574bdddbd444d63a1230543fb364b31d8d98ef36da
7
- data.tar.gz: eafee3ce33017cfde2175f655caf5cc0e292b37a70427d36acf474e21503abea3ea71b124430c69cb8c29168f8993cac385f5f227c05863482a65e9b8f323e6e
6
+ metadata.gz: 9bb02d28a5b99c70bde877ab7e481619c26d55f68b47fbebc40e384400fc445c4683e04e35ac6bc9f837028bb5af9ec84928662fb8fd93e80e3a7bcec446e6ad
7
+ data.tar.gz: 78c11ff2bc200f7a482ffefc516b2ab2ad487399b37685d3fe49cd907de6b1e6a3aafd891ea584b3b2387c4567c4d58c1ed9db5d6aaca35b065ac613f505c4d8
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ # ActiveRecord-backed session store (default for production)
6
+ class ActiveRecordSessionStore
7
+ include SessionStore
8
+
9
+ def create_session(session_id = nil, attributes = {})
10
+ session = ActionMCP::Session.new(attributes)
11
+ session.id = session_id if session_id
12
+ session.save!
13
+ session
14
+ end
15
+
16
+ def load_session(session_id)
17
+ ActionMCP::Session.find_by(id: session_id)
18
+ end
19
+
20
+ def save_session(session)
21
+ session.save! if session.is_a?(ActionMCP::Session)
22
+ end
23
+
24
+ def delete_session(session_id)
25
+ ActionMCP::Session.find_by(id: session_id)&.destroy
26
+ end
27
+
28
+ def session_exists?(session_id)
29
+ ActionMCP::Session.exists?(id: session_id)
30
+ end
31
+
32
+ def find_sessions(criteria = {})
33
+ scope = ActionMCP::Session.all
34
+
35
+ scope = scope.where(status: criteria[:status]) if criteria[:status]
36
+ scope = scope.where(role: criteria[:role]) if criteria[:role]
37
+
38
+ scope
39
+ end
40
+
41
+ def cleanup_expired_sessions(older_than: 24.hours.ago)
42
+ ActionMCP::Session.where("updated_at < ?", older_than).destroy_all
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,423 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module Server
5
+ # Memory-based session object that mimics ActiveRecord Session
6
+ class MemorySession
7
+ attr_accessor :id, :status, :initialized, :role, :messages_count,
8
+ :sse_event_counter, :protocol_version, :client_info,
9
+ :client_capabilities, :server_info, :server_capabilities,
10
+ :tool_registry, :prompt_registry, :resource_registry,
11
+ :created_at, :updated_at, :ended_at, :last_event_id,
12
+ :session_data
13
+
14
+ def initialize(attributes = {}, store = nil)
15
+ @store = store
16
+ @messages = Concurrent::Array.new
17
+ @subscriptions = Concurrent::Array.new
18
+ @resources = Concurrent::Array.new
19
+ @sse_events = Concurrent::Array.new
20
+ @sse_counter = Concurrent::AtomicFixnum.new(0)
21
+ @message_counter = Concurrent::AtomicFixnum.new(0)
22
+ @new_record = true
23
+
24
+ attributes.each do |key, value|
25
+ send("#{key}=", value) if respond_to?("#{key}=")
26
+ end
27
+ end
28
+
29
+ # Mimic ActiveRecord interface
30
+ def new_record?
31
+ @new_record
32
+ end
33
+
34
+ def persisted?
35
+ !@new_record
36
+ end
37
+
38
+ def save
39
+ self.updated_at = Time.current
40
+ @store.save_session(self) if @store
41
+ @new_record = false
42
+ true
43
+ end
44
+
45
+ def save!
46
+ save
47
+ end
48
+
49
+ def update(attributes)
50
+ attributes.each do |key, value|
51
+ send("#{key}=", value) if respond_to?("#{key}=")
52
+ end
53
+ save
54
+ end
55
+
56
+ def update!(attributes)
57
+ update(attributes)
58
+ end
59
+
60
+ def destroy
61
+ @store.delete_session(id) if @store
62
+ end
63
+
64
+ def reload
65
+ self
66
+ end
67
+
68
+ def initialized?
69
+ initialized
70
+ end
71
+
72
+ def initialize!
73
+ return false if initialized?
74
+
75
+ self.initialized = true
76
+ self.status = "initialized"
77
+ save
78
+ end
79
+
80
+ def close!
81
+ self.status = "closed"
82
+ self.ended_at = Time.current
83
+ save
84
+ end
85
+
86
+ # Message management
87
+ def write(data)
88
+ @messages << {
89
+ data: data,
90
+ direction: role == "server" ? "client" : "server",
91
+ created_at: Time.current
92
+ }
93
+ @message_counter.increment
94
+ self.messages_count = @message_counter.value
95
+ end
96
+
97
+ def read(data)
98
+ @messages << {
99
+ data: data,
100
+ direction: role,
101
+ created_at: Time.current
102
+ }
103
+ @message_counter.increment
104
+ self.messages_count = @message_counter.value
105
+ end
106
+
107
+ def messages
108
+ MessageCollection.new(@messages)
109
+ end
110
+
111
+ def subscriptions
112
+ SubscriptionCollection.new(@subscriptions)
113
+ end
114
+
115
+ def resources
116
+ ResourceCollection.new(@resources)
117
+ end
118
+
119
+ def sse_events
120
+ SSEEventCollection.new(@sse_events)
121
+ end
122
+
123
+ # SSE event management
124
+ def increment_sse_counter!
125
+ new_value = @sse_counter.increment
126
+ self.sse_event_counter = new_value
127
+ save
128
+ new_value
129
+ end
130
+
131
+ def store_sse_event(event_id, data, max_events = 100)
132
+ event = { event_id: event_id, data: data, created_at: Time.current }
133
+ @sse_events << event
134
+
135
+ # Maintain cache limit
136
+ while @sse_events.size > max_events
137
+ @sse_events.shift
138
+ end
139
+
140
+ event
141
+ end
142
+
143
+ def get_sse_events_after(last_event_id, limit = 50)
144
+ @sse_events.select { |e| e[:event_id] > last_event_id }
145
+ .first(limit)
146
+ end
147
+
148
+ def cleanup_old_sse_events(max_age = 15.minutes)
149
+ cutoff_time = Time.current - max_age
150
+ @sse_events.delete_if { |e| e[:created_at] < cutoff_time }
151
+ end
152
+
153
+ # Adapter methods
154
+ def adapter
155
+ ActionMCP::Server.server.pubsub
156
+ end
157
+
158
+ def session_key
159
+ "action_mcp:session:#{id}"
160
+ end
161
+
162
+ # Capability methods
163
+ def server_capabilities_payload
164
+ {
165
+ protocolVersion: ActionMCP::PROTOCOL_VERSION,
166
+ serverInfo: server_info,
167
+ capabilities: server_capabilities
168
+ }
169
+ end
170
+
171
+ def set_protocol_version(version)
172
+ version = ActionMCP::PROTOCOL_VERSION if ActionMCP.configuration.vibed_ignore_version
173
+ self.protocol_version = version
174
+ save
175
+ end
176
+
177
+ def store_client_info(info)
178
+ self.client_info = info
179
+ end
180
+
181
+ def store_client_capabilities(capabilities)
182
+ self.client_capabilities = capabilities
183
+ end
184
+
185
+ # Subscription management
186
+ def resource_subscribe(uri)
187
+ unless @subscriptions.any? { |s| s[:uri] == uri }
188
+ @subscriptions << { uri: uri, created_at: Time.current }
189
+ end
190
+ end
191
+
192
+ def resource_unsubscribe(uri)
193
+ @subscriptions.delete_if { |s| s[:uri] == uri }
194
+ end
195
+
196
+ # Progress notification
197
+ def send_progress_notification(progressToken:, progress:, total: nil, message: nil)
198
+ handler = ActionMCP::Server::TransportHandler.new(self)
199
+ handler.send_progress_notification(
200
+ progressToken: progressToken,
201
+ progress: progress,
202
+ total: total,
203
+ message: message
204
+ )
205
+ end
206
+
207
+ # Registry management methods
208
+ def register_tool(tool_class_or_name)
209
+ tool_name = normalize_name(tool_class_or_name, :tool)
210
+ return false unless tool_exists?(tool_name)
211
+
212
+ self.tool_registry ||= []
213
+ unless self.tool_registry.include?(tool_name)
214
+ self.tool_registry << tool_name
215
+ save!
216
+ send_tools_list_changed_notification
217
+ end
218
+ true
219
+ end
220
+
221
+ def unregister_tool(tool_class_or_name)
222
+ tool_name = normalize_name(tool_class_or_name, :tool)
223
+ self.tool_registry ||= []
224
+
225
+ return unless self.tool_registry.delete(tool_name)
226
+
227
+ save!
228
+ send_tools_list_changed_notification
229
+ end
230
+
231
+ def register_prompt(prompt_class_or_name)
232
+ prompt_name = normalize_name(prompt_class_or_name, :prompt)
233
+ return false unless prompt_exists?(prompt_name)
234
+
235
+ self.prompt_registry ||= []
236
+ unless self.prompt_registry.include?(prompt_name)
237
+ self.prompt_registry << prompt_name
238
+ save!
239
+ send_prompts_list_changed_notification
240
+ end
241
+ true
242
+ end
243
+
244
+ def unregister_prompt(prompt_class_or_name)
245
+ prompt_name = normalize_name(prompt_class_or_name, :prompt)
246
+ self.prompt_registry ||= []
247
+
248
+ return unless self.prompt_registry.delete(prompt_name)
249
+
250
+ save!
251
+ send_prompts_list_changed_notification
252
+ end
253
+
254
+ def register_resource_template(template_class_or_name)
255
+ template_name = normalize_name(template_class_or_name, :resource_template)
256
+ return false unless resource_template_exists?(template_name)
257
+
258
+ self.resource_registry ||= []
259
+ unless self.resource_registry.include?(template_name)
260
+ self.resource_registry << template_name
261
+ save!
262
+ send_resources_list_changed_notification
263
+ end
264
+ true
265
+ end
266
+
267
+ def unregister_resource_template(template_class_or_name)
268
+ template_name = normalize_name(template_class_or_name, :resource_template)
269
+ self.resource_registry ||= []
270
+
271
+ return unless self.resource_registry.delete(template_name)
272
+
273
+ save!
274
+ send_resources_list_changed_notification
275
+ end
276
+
277
+ def registered_tools
278
+ (self.tool_registry || []).filter_map do |tool_name|
279
+ ActionMCP::ToolsRegistry.find(tool_name)
280
+ rescue StandardError
281
+ nil
282
+ end
283
+ end
284
+
285
+ def registered_prompts
286
+ (self.prompt_registry || []).filter_map do |prompt_name|
287
+ ActionMCP::PromptsRegistry.find(prompt_name)
288
+ rescue StandardError
289
+ nil
290
+ end
291
+ end
292
+
293
+ def registered_resource_templates
294
+ (self.resource_registry || []).filter_map do |template_name|
295
+ ActionMCP::ResourceTemplatesRegistry.find(template_name)
296
+ rescue StandardError
297
+ nil
298
+ end
299
+ end
300
+
301
+ private
302
+
303
+ def normalize_name(class_or_name, type)
304
+ case class_or_name
305
+ when String
306
+ class_or_name
307
+ when Class
308
+ case type
309
+ when :tool
310
+ class_or_name.tool_name
311
+ when :prompt
312
+ class_or_name.prompt_name
313
+ when :resource_template
314
+ class_or_name.capability_name
315
+ end
316
+ else
317
+ raise ArgumentError, "Expected String or Class, got #{class_or_name.class}"
318
+ end
319
+ end
320
+
321
+ def tool_exists?(tool_name)
322
+ ActionMCP::ToolsRegistry.find(tool_name)
323
+ true
324
+ rescue ActionMCP::RegistryBase::NotFound
325
+ false
326
+ end
327
+
328
+ def prompt_exists?(prompt_name)
329
+ ActionMCP::PromptsRegistry.find(prompt_name)
330
+ true
331
+ rescue ActionMCP::RegistryBase::NotFound
332
+ false
333
+ end
334
+
335
+ def resource_template_exists?(template_name)
336
+ ActionMCP::ResourceTemplatesRegistry.find(template_name)
337
+ true
338
+ rescue ActionMCP::RegistryBase::NotFound
339
+ false
340
+ end
341
+
342
+ def send_tools_list_changed_notification
343
+ # Only send if server capabilities allow it
344
+ return unless server_capabilities.dig("tools", "listChanged")
345
+
346
+ write(JSON_RPC::Notification.new(method: "notifications/tools/list_changed"))
347
+ end
348
+
349
+ def send_prompts_list_changed_notification
350
+ return unless server_capabilities.dig("prompts", "listChanged")
351
+
352
+ write(JSON_RPC::Notification.new(method: "notifications/prompts/list_changed"))
353
+ end
354
+
355
+ def send_resources_list_changed_notification
356
+ return unless server_capabilities.dig("resources", "listChanged")
357
+
358
+ write(JSON_RPC::Notification.new(method: "notifications/resources/list_changed"))
359
+ end
360
+
361
+ public
362
+
363
+ # Simple collection classes to mimic ActiveRecord associations
364
+ class MessageCollection < Array
365
+ def create!(attributes)
366
+ self << attributes
367
+ attributes
368
+ end
369
+
370
+ def order(field)
371
+ # Simple ordering implementation
372
+ sort_by { |msg| msg[field] || msg[field.to_s] }
373
+ end
374
+ end
375
+
376
+ class SubscriptionCollection < Array
377
+ def find_or_create_by(attributes)
378
+ existing = find { |s| s[:uri] == attributes[:uri] }
379
+ return existing if existing
380
+
381
+ subscription = attributes.merge(created_at: Time.current)
382
+ self << subscription
383
+ subscription
384
+ end
385
+
386
+ def find_by(attributes)
387
+ find { |s| s[:uri] == attributes[:uri] }
388
+ end
389
+ end
390
+
391
+ class ResourceCollection < Array
392
+ end
393
+
394
+ class SSEEventCollection < Array
395
+ def create!(attributes)
396
+ self << attributes
397
+ attributes
398
+ end
399
+
400
+ def count
401
+ size
402
+ end
403
+
404
+ def where(condition, value)
405
+ # Simple implementation for "event_id > ?" condition
406
+ select { |e| e[:event_id] > value }
407
+ end
408
+
409
+ def order(field)
410
+ sort_by { |e| e[field.is_a?(Hash) ? field.keys.first : field] }
411
+ end
412
+
413
+ def limit(n)
414
+ first(n)
415
+ end
416
+
417
+ def delete_all
418
+ clear
419
+ end
420
+ end
421
+ end
422
+ end
423
+ end