Class: CompletionKit::McpSession

Inherits:
ApplicationRecord show all
Defined in:
app/models/completion_kit/mcp_session.rb

Overview

MCP session marker — one row per active client session, kept in the database so sessions survive Puma restarts, deploys, and Rails.cache eviction.

Two things to know about why every query goes through ‘unscoped`:

  1. CompletionKit::ApplicationRecord applies the host app’s tenant_scope as a default_scope. MCP sessions are per-CONNECTION, not per-tenant — the table has no organization_id column. Letting the tenant_scope into a session lookup turns “is this session live?” into either a SQL error (no such column) or a false negative (‘WHERE 1=0`), which surfaces to the client as a spurious “Session not initialized” right after init.

  2. ‘active?` slides expires_at forward when a session is more than halfway through its TTL, so an MCP connection that keeps making calls stays alive instead of expiring on the original 1-hour wall clock.

Expired rows are opportunistically pruned on every new session start, so the table stays bounded by recent activity.

Constant Summary collapse

SESSION_TTL =
1.hour

Constants inherited from ApplicationRecord

ApplicationRecord::TenantScopedUniquenessValidator

Class Method Summary collapse

Class Method Details

.active?(session_id) ⇒ Boolean

Returns:

  • (Boolean)


30
31
32
33
34
35
36
37
38
# File 'app/models/completion_kit/mcp_session.rb', line 30

def self.active?(session_id)
  return false if session_id.blank?

  row = unscoped.where(session_id: session_id).where("expires_at > ?", Time.current).first
  return false unless row

  slide_expiry(row)
  true
end

.destroy_session(session_id) ⇒ Object



40
41
42
# File 'app/models/completion_kit/mcp_session.rb', line 40

def self.destroy_session(session_id)
  unscoped.where(session_id: session_id).delete_all
end

.prune_expired!Object



44
45
46
# File 'app/models/completion_kit/mcp_session.rb', line 44

def self.prune_expired!
  unscoped.where("expires_at < ?", Time.current).delete_all
end

.slide_expiry(row) ⇒ Object



48
49
50
51
52
# File 'app/models/completion_kit/mcp_session.rb', line 48

def self.slide_expiry(row)
  half_ttl_from_now = (SESSION_TTL / 2).from_now
  return if row.expires_at > half_ttl_from_now
  row.update_column(:expires_at, SESSION_TTL.from_now)
end

.start!Object



25
26
27
28
# File 'app/models/completion_kit/mcp_session.rb', line 25

def self.start!
  prune_expired!
  unscoped.create!(session_id: SecureRandom.uuid, expires_at: SESSION_TTL.from_now).session_id
end