feat: Initial release MCP Outline PostgreSQL v1.0.0
86 tools across 12 modules for direct PostgreSQL access to Outline Wiki: - Documents (19), Collections (14), Users (9), Groups (8) - Comments (6), Shares (5), Revisions (3), Events (3) - Attachments (5), File Operations (4), OAuth (8), Auth (2) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
370
src/tools/events.ts
Normal file
370
src/tools/events.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 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<EventArgs> = {
|
||||
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<ToolResponse> => {
|
||||
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<ToolResponse> => {
|
||||
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."isAdmin" as "actorIsAdmin",
|
||||
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<EventArgs> = {
|
||||
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<ToolResponse> => {
|
||||
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<any>[] = [
|
||||
listEvents,
|
||||
getEvent,
|
||||
getEventStats,
|
||||
];
|
||||
Reference in New Issue
Block a user