Skip to main content
Glama

Claude Slack

schema.sqlโ€ข22.8 kB
-- Claude-Slack v3 Database Schema -- Unified channel system with permission controls -- Phase 2 (v3.0.0): Permission System & DM Unification -- Enable foreign keys PRAGMA foreign_keys = ON; -- Enable WAL mode for better concurrency PRAGMA journal_mode = WAL; -- ============================================================================ -- Core Tables -- ============================================================================ -- Projects table (track all known projects) CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, -- hashed project path (32 chars) path TEXT UNIQUE NOT NULL, -- absolute path to project root name TEXT, -- human-readable project name created_at REAL DEFAULT (strftime('%s', 'now')), last_active REAL, metadata JSON -- project-specific settings ); -- Agents table (registry with DM policies) CREATE TABLE IF NOT EXISTS agents ( name TEXT NOT NULL, -- agent name from frontmatter project_id TEXT, -- project this agent belongs to (NULL for global) description TEXT, status TEXT DEFAULT 'offline', -- online/offline/busy current_project_id TEXT, -- currently active project (for context) -- DM permission settings dm_policy TEXT DEFAULT 'open' CHECK (dm_policy IN ('open', 'restricted', 'closed')), -- open: anyone can DM, restricted: only allowed list, closed: no DMs discoverable TEXT DEFAULT 'public' CHECK (discoverable IN ('public', 'project', 'private')), -- public: visible to all, project: visible in linked projects, private: not discoverable last_active REAL, created_at REAL DEFAULT (strftime('%s', 'now')), metadata JSON, -- Additional agent info PRIMARY KEY (name, project_id), FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (current_project_id) REFERENCES projects(id) ); -- Unified Channels table (channels AND DMs) CREATE TABLE IF NOT EXISTS channels ( id TEXT PRIMARY KEY, -- Format: {scope}:{name} or dm:{agent1}:{agent2} channel_type TEXT NOT NULL CHECK (channel_type IN ('channel', 'direct')), access_type TEXT NOT NULL CHECK (access_type IN ('open', 'members', 'private')), -- open: anyone can join, members: invite-only, private: fixed membership (DMs) project_id TEXT, -- NULL for global channels/DMs scope TEXT NOT NULL, -- 'global' or 'project' name TEXT NOT NULL, -- Channel name or DM identifier description TEXT, -- Pre-allocated Phase 1 fields (v3.1.0) topic_required BOOLEAN DEFAULT FALSE, default_topic TEXT DEFAULT 'general', channel_metadata JSON, created_by TEXT, -- Agent name that created the channel created_by_project_id TEXT, -- Agent's project_id created_at REAL DEFAULT (strftime('%s', 'now')), is_default BOOLEAN DEFAULT FALSE, -- Auto-subscribe new agents is_archived BOOLEAN DEFAULT FALSE, -- For agent notes channels (special case) owner_agent_name TEXT, -- For agent-notes channels owner_agent_project_id TEXT, -- For agent-notes channels FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (created_by, created_by_project_id) REFERENCES agents(name, project_id), FOREIGN KEY (owner_agent_name, owner_agent_project_id) REFERENCES agents(name, project_id), UNIQUE(project_id, name) -- Ensures unique channel names per scope ); -- Unified Channel Members table (ALL agent-channel relationships) -- Replaces both subscriptions and channel_members in the unified model CREATE TABLE IF NOT EXISTS channel_members ( channel_id TEXT NOT NULL, agent_name TEXT NOT NULL, agent_project_id TEXT, -- NULL for global agents -- How they joined (unified model) invited_by TEXT DEFAULT 'self', -- 'self' for self-joined, 'system' for DMs, or inviter's name joined_at REAL DEFAULT (strftime('%s', 'now')), source TEXT DEFAULT 'manual', -- 'frontmatter', 'manual', 'default', 'system' -- Capabilities (not roles!) can_leave BOOLEAN DEFAULT TRUE, -- FALSE only for DMs and system channels can_send BOOLEAN DEFAULT TRUE, can_invite BOOLEAN DEFAULT FALSE, can_manage BOOLEAN DEFAULT FALSE, -- User preferences (unified across all channel types) is_muted BOOLEAN DEFAULT FALSE, notification_preference TEXT DEFAULT 'all', -- Read tracking (Phase 1 fields) last_read_at REAL, last_read_message_id INTEGER, -- Default provisioning tracking is_from_default BOOLEAN DEFAULT FALSE, -- Was this membership from is_default=true? opted_out BOOLEAN DEFAULT FALSE, -- Has user explicitly opted out? opted_out_at REAL, -- When did they opt out? PRIMARY KEY (channel_id, agent_name, agent_project_id), FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, FOREIGN KEY (agent_name, agent_project_id) REFERENCES agents(name, project_id) ); -- DM Permissions table (allow/block lists for DMs) CREATE TABLE IF NOT EXISTS dm_permissions ( agent_name TEXT NOT NULL, agent_project_id TEXT, other_agent_name TEXT NOT NULL, other_agent_project_id TEXT, permission TEXT NOT NULL CHECK (permission IN ('allow', 'block')), reason TEXT, created_at REAL DEFAULT (strftime('%s', 'now')), PRIMARY KEY (agent_name, agent_project_id, other_agent_name, other_agent_project_id), FOREIGN KEY (agent_name, agent_project_id) REFERENCES agents(name, project_id), FOREIGN KEY (other_agent_name, other_agent_project_id) REFERENCES agents(name, project_id) ); -- Messages table (unified for channels and DMs) CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id TEXT NOT NULL, -- Always references a channel (including DM channels) sender_id TEXT NOT NULL, -- Agent name sender_project_id TEXT, -- Agent's project_id content TEXT NOT NULL, -- Pre-allocated Phase 1 fields (v3.1.0) topic_name TEXT, -- NULL initially, required in v3.1.0 ai_metadata JSON, confidence REAL, model_version TEXT, intent_type TEXT, thread_id TEXT, -- For threading timestamp REAL DEFAULT (strftime('%s', 'now')), -- Unix timestamp (seconds since epoch) is_edited BOOLEAN DEFAULT FALSE, edited_at REAL, metadata JSON, -- priority, references, etc. FOREIGN KEY (channel_id) REFERENCES channels(id), FOREIGN KEY (sender_id, sender_project_id) REFERENCES agents(name, project_id) ); -- Pre-created for Phase 1 (v3.1.0) - Agent Message State CREATE TABLE IF NOT EXISTS agent_message_state ( agent_name TEXT NOT NULL, agent_project_id TEXT, message_id INTEGER NOT NULL, channel_id TEXT, -- Denormalized for performance -- Relationship and action types (for v3.1.0) relationship_type TEXT DEFAULT 'subscriber', action_type TEXT, -- 'review', 'action', 'fyi' (for mentions) status TEXT DEFAULT 'unread', created_at REAL DEFAULT (strftime('%s', 'now')), read_at REAL, done_at REAL, PRIMARY KEY (agent_name, agent_project_id, message_id), FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, FOREIGN KEY (agent_name, agent_project_id) REFERENCES agents(name, project_id) ); -- Note: Subscriptions table removed in unified membership model -- All channel access now goes through channel_members table -- "Subscriptions" are just memberships where invited_by='self' -- Project Links table (for cross-project communication) CREATE TABLE IF NOT EXISTS project_links ( project_a_id TEXT NOT NULL, project_b_id TEXT NOT NULL, link_type TEXT DEFAULT 'bidirectional', -- 'bidirectional', 'a_to_b', 'b_to_a' enabled BOOLEAN DEFAULT TRUE, created_at REAL DEFAULT (strftime('%s', 'now')), created_by TEXT, metadata JSON, PRIMARY KEY (project_a_id, project_b_id), FOREIGN KEY (project_a_id) REFERENCES projects(id), FOREIGN KEY (project_b_id) REFERENCES projects(id), CHECK (project_a_id < project_b_id) -- Ensure consistent ordering ); -- Sessions table for tracking Claude session contexts CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, project_id TEXT, project_path TEXT, project_name TEXT, transcript_path TEXT, scope TEXT NOT NULL DEFAULT 'global', updated_at REAL DEFAULT (strftime('%s', 'now')), metadata JSON, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL ); -- Tool calls table for deduplication CREATE TABLE IF NOT EXISTS tool_calls ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, tool_inputs_hash TEXT NOT NULL, tool_inputs JSON, called_at REAL DEFAULT (strftime('%s', 'now')), FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE ); -- Configuration Sync History table (tracks config reconciliation) CREATE TABLE IF NOT EXISTS config_sync_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, config_hash TEXT NOT NULL, -- Hash of the config file for change detection config_snapshot JSON NOT NULL, -- Full snapshot of config at time of sync applied_at REAL DEFAULT (strftime('%s', 'now')), scope TEXT, -- 'global', 'project', or 'all' project_id TEXT, -- Project ID if project-scoped actions_taken JSON, -- List of actions performed success BOOLEAN DEFAULT TRUE, error_message TEXT, FOREIGN KEY (project_id) REFERENCES projects(id) ); -- ============================================================================ -- Permission Views (Core of Phase 2) -- ============================================================================ -- View: Channels accessible to each agent CREATE VIEW IF NOT EXISTS agent_channels AS WITH accessible_channels AS ( SELECT a.name as agent_name, a.project_id as agent_project_id, c.id as channel_id, c.channel_type, c.access_type, c.scope, c.name as channel_name, c.description, c.project_id as channel_project_id, c.is_archived, -- Unified model: ALL channel access through channel_members EXISTS ( SELECT 1 FROM channel_members cm WHERE cm.channel_id = c.id AND cm.agent_name = a.name AND cm.agent_project_id IS NOT DISTINCT FROM a.project_id AND NOT cm.opted_out -- Respect opt-outs ) as has_access FROM agents a CROSS JOIN channels c ) SELECT * FROM accessible_channels WHERE has_access = 1; -- View: DM access permissions CREATE VIEW IF NOT EXISTS dm_access AS SELECT a1.name as agent1_name, a1.project_id as agent1_project_id, a2.name as agent2_name, a2.project_id as agent2_project_id, CASE -- Check if either agent blocks the other WHEN EXISTS ( SELECT 1 FROM dm_permissions dp WHERE ( (dp.agent_name = a1.name AND dp.agent_project_id IS NOT DISTINCT FROM a1.project_id AND dp.other_agent_name = a2.name AND dp.other_agent_project_id IS NOT DISTINCT FROM a2.project_id AND dp.permission = 'block') OR (dp.agent_name = a2.name AND dp.agent_project_id IS NOT DISTINCT FROM a2.project_id AND dp.other_agent_name = a1.name AND dp.other_agent_project_id IS NOT DISTINCT FROM a1.project_id AND dp.permission = 'block') ) ) THEN 0 -- Check DM policies WHEN a1.dm_policy = 'closed' OR a2.dm_policy = 'closed' THEN 0 WHEN a1.dm_policy = 'restricted' AND NOT EXISTS ( SELECT 1 FROM dm_permissions dp WHERE dp.agent_name = a1.name AND dp.agent_project_id IS NOT DISTINCT FROM a1.project_id AND dp.other_agent_name = a2.name AND dp.other_agent_project_id IS NOT DISTINCT FROM a2.project_id AND dp.permission = 'allow' ) THEN 0 WHEN a2.dm_policy = 'restricted' AND NOT EXISTS ( SELECT 1 FROM dm_permissions dp WHERE dp.agent_name = a2.name AND dp.agent_project_id IS NOT DISTINCT FROM a2.project_id AND dp.other_agent_name = a1.name AND dp.other_agent_project_id IS NOT DISTINCT FROM a1.project_id AND dp.permission = 'allow' ) THEN 0 ELSE 1 END as can_dm FROM agents a1 CROSS JOIN agents a2 WHERE a1.name != a2.name OR a1.project_id != a2.project_id; -- View: Agent discovery - who can discover whom for DMs CREATE VIEW IF NOT EXISTS agent_discovery AS WITH project_relationships AS ( -- Get all project relationships (bidirectional) SELECT project_a_id as project1, project_b_id as project2 FROM project_links WHERE enabled = TRUE UNION SELECT project_b_id as project1, project_a_id as project2 FROM project_links WHERE enabled = TRUE ) SELECT a1.name as discovering_agent, a1.project_id as discovering_project_id, a2.name as discoverable_agent, a2.project_id as discoverable_project_id, a2.description as discoverable_description, a2.status as discoverable_status, a2.dm_policy as dm_policy, a2.discoverable as discoverable_setting, p2.name as discoverable_project_name, -- Can this agent be discovered? CASE -- Can't discover yourself WHEN a1.name = a2.name AND a1.project_id IS NOT DISTINCT FROM a2.project_id THEN 0 -- Public agents are always discoverable WHEN a2.discoverable = 'public' THEN 1 -- Private agents are never discoverable (except already have DM channel) WHEN a2.discoverable = 'private' THEN 0 -- Project-scoped agents WHEN a2.discoverable = 'project' THEN CASE -- Same project WHEN a1.project_id = a2.project_id AND a1.project_id IS NOT NULL THEN 1 -- Global agent can discover project agents WHEN a1.project_id IS NULL THEN 1 -- Linked projects WHEN EXISTS ( SELECT 1 FROM project_relationships pr WHERE pr.project1 = a1.project_id AND pr.project2 = a2.project_id ) THEN 1 ELSE 0 END ELSE 0 END as can_discover, -- Can actually DM? (for UI hints) CASE WHEN a2.dm_policy = 'closed' THEN 'unavailable' WHEN a2.dm_policy = 'restricted' THEN CASE WHEN EXISTS ( SELECT 1 FROM dm_permissions dp WHERE dp.agent_name = a2.name AND dp.agent_project_id IS NOT DISTINCT FROM a2.project_id AND dp.other_agent_name = a1.name AND dp.other_agent_project_id IS NOT DISTINCT FROM a1.project_id AND dp.permission = 'allow' ) THEN 'available' ELSE 'requires_permission' END WHEN a2.dm_policy = 'open' THEN CASE WHEN EXISTS ( SELECT 1 FROM dm_permissions dp WHERE dp.agent_name = a2.name AND dp.agent_project_id IS NOT DISTINCT FROM a2.project_id AND dp.other_agent_name = a1.name AND dp.other_agent_project_id IS NOT DISTINCT FROM a1.project_id AND dp.permission = 'block' ) THEN 'blocked' ELSE 'available' END ELSE 'unknown' END as dm_availability, -- Does a DM channel already exist? CASE WHEN EXISTS ( SELECT 1 FROM channels c JOIN channel_members cm1 ON c.id = cm1.channel_id JOIN channel_members cm2 ON c.id = cm2.channel_id WHERE c.channel_type = 'direct' AND cm1.agent_name = a1.name AND cm1.agent_project_id IS NOT DISTINCT FROM a1.project_id AND cm2.agent_name = a2.name AND cm2.agent_project_id IS NOT DISTINCT FROM a2.project_id ) THEN 1 ELSE 0 END as has_existing_dm FROM agents a1 CROSS JOIN agents a2 LEFT JOIN projects p2 ON a2.project_id = p2.id; -- View: Shared channels between projects CREATE VIEW IF NOT EXISTS shared_channels AS SELECT DISTINCT c.id as channel_id, c.name as channel_name, c.scope, p1.id as project1_id, p1.name as project1_name, p2.id as project2_id, p2.name as project2_name FROM channels c JOIN channel_members cm1 ON cm1.channel_id = c.id JOIN channel_members cm2 ON cm2.channel_id = c.id LEFT JOIN projects p1 ON cm1.agent_project_id = p1.id LEFT JOIN projects p2 ON cm2.agent_project_id = p2.id WHERE cm1.agent_project_id != cm2.agent_project_id OR (cm1.agent_project_id IS NULL AND cm2.agent_project_id IS NOT NULL) OR (cm1.agent_project_id IS NOT NULL AND cm2.agent_project_id IS NULL); -- ============================================================================ -- Indexes for Performance -- ============================================================================ CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(channel_type); CREATE INDEX IF NOT EXISTS idx_channels_access ON channels(access_type); CREATE INDEX IF NOT EXISTS idx_channels_scope ON channels(scope); CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id); CREATE INDEX IF NOT EXISTS idx_channels_dm ON channels(channel_type) WHERE channel_type = 'direct'; CREATE INDEX IF NOT EXISTS idx_channel_members_agent ON channel_members(agent_name, agent_project_id); CREATE INDEX IF NOT EXISTS idx_channel_members_channel ON channel_members(channel_id); CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id, sender_project_id); CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp DESC); -- Note: subscription indexes removed - unified model uses channel_members only CREATE INDEX IF NOT EXISTS idx_dm_permissions_agent ON dm_permissions(agent_name, agent_project_id); CREATE INDEX IF NOT EXISTS idx_dm_permissions_other ON dm_permissions(other_agent_name, other_agent_project_id); CREATE INDEX IF NOT EXISTS idx_agent_message_state_agent ON agent_message_state(agent_name, agent_project_id, status); CREATE INDEX IF NOT EXISTS idx_agent_message_state_message ON agent_message_state(message_id); CREATE INDEX IF NOT EXISTS idx_project_links_a ON project_links(project_a_id); CREATE INDEX IF NOT EXISTS idx_project_links_b ON project_links(project_b_id); CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); CREATE INDEX IF NOT EXISTS idx_tool_calls_lookup ON tool_calls(tool_name, tool_inputs_hash, called_at DESC); CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id); -- ============================================================================ -- Full-text Search -- ============================================================================ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( content, content=messages, content_rowid=id ); -- Triggers to keep FTS in sync CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content); END; CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE rowid = old.id; END; CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN UPDATE messages_fts SET content = new.content WHERE rowid = new.id; END; -- ============================================================================ -- Indexes for Performance -- ============================================================================ -- Indexes for unified membership model CREATE INDEX IF NOT EXISTS idx_channel_members_defaults ON channel_members(is_from_default, opted_out); CREATE INDEX IF NOT EXISTS idx_channel_members_invited_by ON channel_members(invited_by, source); CREATE INDEX IF NOT EXISTS idx_channels_defaults ON channels(is_default, scope, is_archived); CREATE INDEX IF NOT EXISTS idx_config_sync_history_scope ON config_sync_history(scope, project_id, applied_at); -- ============================================================================ -- Cleanup Triggers -- ============================================================================ -- Update session timestamp on access -- This creates infinite loop. Removing. Handled in application logic. -- CREATE TRIGGER IF NOT EXISTS update_session_timestamp -- AFTER UPDATE ON sessions -- BEGIN -- UPDATE sessions -- SET updated_at = strftime('%s', 'now') -- WHERE id = NEW.id; -- END; -- Cleanup old sessions (older than 24 hours) CREATE TRIGGER IF NOT EXISTS cleanup_old_sessions AFTER INSERT ON sessions BEGIN DELETE FROM sessions WHERE updated_at < (strftime('%s', 'now') - 86400); -- 24 hours = 86400 seconds END; -- Cleanup old tool calls (older than 10 minutes) CREATE TRIGGER IF NOT EXISTS cleanup_old_tool_calls AFTER INSERT ON tool_calls BEGIN DELETE FROM tool_calls WHERE called_at < (strftime('%s', 'now') - 600); -- 10 minutes = 600 seconds END; -- ============================================================================ -- Helper Functions (as views for SQLite) -- ============================================================================ -- View: Get DM channel ID for two agents CREATE VIEW IF NOT EXISTS dm_channel_lookup AS SELECT CASE WHEN a1.name < a2.name OR (a1.name = a2.name AND a1.project_id < a2.project_id) THEN 'dm:' || a1.name || COALESCE(':' || a1.project_id, '') || ':' || a2.name || COALESCE(':' || a2.project_id, '') ELSE 'dm:' || a2.name || COALESCE(':' || a2.project_id, '') || ':' || a1.name || COALESCE(':' || a1.project_id, '') END as channel_id, a1.name as agent1_name, a1.project_id as agent1_project_id, a2.name as agent2_name, a2.project_id as agent2_project_id FROM agents a1 CROSS JOIN agents a2 WHERE a1.name != a2.name OR a1.project_id != a2.project_id;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/theo-nash/claude-slack'

If you have feedback or need assistance with the MCP directory API, please join our Discord server