/** * MCP Outline PostgreSQL - Events Tools * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { BaseTool, ToolResponse, EventArgs } from '../types/tools.js'; import { validatePagination, isValidUUID } from '../utils/security.js'; /** * events.list - List events with optional filters */ const listEvents: BaseTool = { name: 'outline_events_list', description: 'List system events with optional filtering by name, actor, document, collection, or date range. Useful for audit logs and activity tracking.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Filter by event name (e.g., "documents.create", "users.signin")', }, actor_id: { type: 'string', description: 'Filter by actor user ID (UUID)', }, document_id: { type: 'string', description: 'Filter by document ID (UUID)', }, collection_id: { type: 'string', description: 'Filter by collection ID (UUID)', }, date_from: { type: 'string', description: 'Filter events from this date (ISO 8601 format)', }, date_to: { type: 'string', description: 'Filter events until this date (ISO 8601 format)', }, audit_log: { type: 'boolean', description: 'Filter only audit log worthy events (default: false)', }, limit: { type: 'number', description: 'Maximum number of results (default: 25, max: 100)', }, offset: { type: 'number', description: 'Number of results to skip (default: 0)', }, }, }, handler: async (args, pgClient): Promise => { const { limit, offset } = validatePagination(args.limit, args.offset); const conditions: string[] = []; const params: any[] = []; let paramIndex = 1; if (args.name) { conditions.push(`e.name = $${paramIndex++}`); params.push(args.name); } if (args.actor_id) { if (!isValidUUID(args.actor_id)) { throw new Error('Invalid actor_id format'); } conditions.push(`e."actorId" = $${paramIndex++}`); params.push(args.actor_id); } if (args.document_id) { if (!isValidUUID(args.document_id)) { throw new Error('Invalid document_id format'); } conditions.push(`e."documentId" = $${paramIndex++}`); params.push(args.document_id); } if (args.collection_id) { if (!isValidUUID(args.collection_id)) { throw new Error('Invalid collection_id format'); } conditions.push(`e."collectionId" = $${paramIndex++}`); params.push(args.collection_id); } if (args.date_from) { conditions.push(`e."createdAt" >= $${paramIndex++}`); params.push(args.date_from); } if (args.date_to) { conditions.push(`e."createdAt" <= $${paramIndex++}`); params.push(args.date_to); } // Audit log filter - common audit events if (args.audit_log) { conditions.push(`e.name IN ( 'users.create', 'users.update', 'users.delete', 'users.signin', 'documents.create', 'documents.update', 'documents.delete', 'documents.publish', 'collections.create', 'collections.update', 'collections.delete', 'groups.create', 'groups.update', 'groups.delete', 'shares.create', 'shares.revoke' )`); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const query = ` SELECT e.id, e.name, e."modelId", e."actorId", e."userId", e."collectionId", e."documentId", e."teamId", e.ip, e.data, e."createdAt", actor.name as "actorName", actor.email as "actorEmail", u.name as "userName", c.name as "collectionName", d.title as "documentTitle" FROM events e LEFT JOIN users actor ON e."actorId" = actor.id LEFT JOIN users u ON e."userId" = u.id LEFT JOIN collections c ON e."collectionId" = c.id LEFT JOIN documents d ON e."documentId" = d.id ${whereClause} ORDER BY e."createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex} `; params.push(limit, offset); const result = await pgClient.query(query, params); return { content: [ { type: 'text', text: JSON.stringify( { data: result.rows, pagination: { limit, offset, total: result.rows.length, }, }, null, 2 ), }, ], }; }, }; /** * events.info - Get detailed information about a specific event */ const getEvent: BaseTool<{ id: string }> = { name: 'outline_events_info', description: 'Get detailed information about a specific event by ID, including all associated metadata.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Event ID (UUID)', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.id)) { throw new Error('Invalid event ID format'); } const query = ` SELECT e.id, e.name, e."modelId", e."actorId", e."userId", e."collectionId", e."documentId", e."teamId", e.ip, e.data, e."createdAt", actor.name as "actorName", actor.email as "actorEmail", actor.role as "actorRole", u.name as "userName", u.email as "userEmail", c.name as "collectionName", d.title as "documentTitle", t.name as "teamName" FROM events e LEFT JOIN users actor ON e."actorId" = actor.id LEFT JOIN users u ON e."userId" = u.id LEFT JOIN collections c ON e."collectionId" = c.id LEFT JOIN documents d ON e."documentId" = d.id LEFT JOIN teams t ON e."teamId" = t.id WHERE e.id = $1 `; const result = await pgClient.query(query, [args.id]); if (result.rows.length === 0) { throw new Error('Event not found'); } return { content: [ { type: 'text', text: JSON.stringify( { data: result.rows[0], }, null, 2 ), }, ], }; }, }; /** * events.stats - Get event statistics and summaries */ const getEventStats: BaseTool = { name: 'outline_events_stats', description: 'Get statistical analysis of events. Provides event counts by type, top actors, and activity trends.', inputSchema: { type: 'object', properties: { date_from: { type: 'string', description: 'Statistics from this date (ISO 8601 format)', }, date_to: { type: 'string', description: 'Statistics until this date (ISO 8601 format)', }, collection_id: { type: 'string', description: 'Filter statistics by collection ID (UUID)', }, }, }, handler: async (args, pgClient): Promise => { const conditions: string[] = []; const params: any[] = []; let paramIndex = 1; if (args.date_from) { conditions.push(`e."createdAt" >= $${paramIndex++}`); params.push(args.date_from); } if (args.date_to) { conditions.push(`e."createdAt" <= $${paramIndex++}`); params.push(args.date_to); } if (args.collection_id) { if (!isValidUUID(args.collection_id)) { throw new Error('Invalid collection_id format'); } conditions.push(`e."collectionId" = $${paramIndex++}`); params.push(args.collection_id); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Event counts by type const eventsByTypeQuery = await pgClient.query( `SELECT e.name, COUNT(*) as count FROM events e ${whereClause} GROUP BY e.name ORDER BY count DESC LIMIT 20`, params ); // Top actors const topActorsQuery = await pgClient.query( `SELECT e."actorId", u.name as "actorName", u.email as "actorEmail", COUNT(*) as "eventCount" FROM events e LEFT JOIN users u ON e."actorId" = u.id ${whereClause} GROUP BY e."actorId", u.name, u.email ORDER BY "eventCount" DESC LIMIT 10`, params ); // Activity by day (last 30 days or filtered range) const activityByDayQuery = await pgClient.query( `SELECT DATE(e."createdAt") as date, COUNT(*) as "eventCount" FROM events e ${whereClause} GROUP BY DATE(e."createdAt") ORDER BY date DESC LIMIT 30`, params ); // Total statistics const totalsQuery = await pgClient.query( `SELECT COUNT(*) as "totalEvents", COUNT(DISTINCT e."actorId") as "uniqueActors", COUNT(DISTINCT e."documentId") as "affectedDocuments", COUNT(DISTINCT e."collectionId") as "affectedCollections" FROM events e ${whereClause}`, params ); return { content: [ { type: 'text', text: JSON.stringify( { totals: totalsQuery.rows[0], eventsByType: eventsByTypeQuery.rows, topActors: topActorsQuery.rows, activityByDay: activityByDayQuery.rows, }, null, 2 ), }, ], }; }, }; // Export all event tools export const eventsTools: BaseTool[] = [ listEvents, getEvent, getEventStats, ];