/** * MCP Outline PostgreSQL - Imports Tools * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { BaseTool, ToolResponse, PaginationArgs } from '../types/tools.js'; import { validatePagination, isValidUUID } from '../utils/security.js'; interface ImportListArgs extends PaginationArgs { team_id?: string; state?: string; } /** * imports.list - List imports */ const listImports: BaseTool = { name: 'outline_list_imports', description: 'List document import jobs.', inputSchema: { type: 'object', properties: { team_id: { type: 'string', description: 'Filter by team ID (UUID)' }, state: { type: 'string', description: 'Filter by state (pending, processing, completed, failed)' }, limit: { type: 'number', description: 'Max results (default: 25)' }, offset: { type: 'number', description: 'Skip results (default: 0)' }, }, }, handler: async (args, pgClient): Promise => { const { limit, offset } = validatePagination(args.limit, args.offset); const conditions: string[] = []; const params: any[] = []; let idx = 1; if (args.team_id) { if (!isValidUUID(args.team_id)) throw new Error('Invalid team_id'); conditions.push(`i."teamId" = $${idx++}`); params.push(args.team_id); } if (args.state) { conditions.push(`i.state = $${idx++}`); params.push(args.state); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await pgClient.query(` SELECT i.id, i.state, i."teamId", i."createdById", i."createdAt", i."updatedAt", u.name as "createdByName", t.name as "teamName" FROM imports i LEFT JOIN users u ON i."createdById" = u.id LEFT JOIN teams t ON i."teamId" = t.id ${whereClause} ORDER BY i."createdAt" DESC LIMIT $${idx++} OFFSET $${idx} `, [...params, limit, offset]); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows, pagination: { limit, offset, total: result.rows.length } }, null, 2) }], }; }, }; /** * imports.status - Get import status */ const getImportStatus: BaseTool<{ id: string }> = { name: 'outline_get_import_status', description: 'Get detailed status of an import job.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Import ID (UUID)' }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.id)) throw new Error('Invalid import ID'); const result = await pgClient.query(` SELECT i.*, u.name as "createdByName" FROM imports i LEFT JOIN users u ON i."createdById" = u.id WHERE i.id = $1 `, [args.id]); if (result.rows.length === 0) throw new Error('Import not found'); // Get import tasks const tasks = await pgClient.query(` SELECT id, state, "documentId", "createdAt" FROM import_tasks WHERE "importId" = $1 ORDER BY "createdAt" DESC LIMIT 50 `, [args.id]); return { content: [{ type: 'text', text: JSON.stringify({ import: result.rows[0], tasks: tasks.rows, taskCount: tasks.rows.length, }, null, 2) }], }; }, }; /** * imports.create - Create import job */ const createImport: BaseTool<{ type: string; collection_id?: string }> = { name: 'outline_create_import', description: 'Create a new import job. Note: This creates the job record, actual file upload handled separately.', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Import type (e.g., "notion", "confluence", "markdown")' }, collection_id: { type: 'string', description: 'Target collection ID (UUID, optional)' }, }, required: ['type'], }, handler: async (args, pgClient): Promise => { if (args.collection_id && !isValidUUID(args.collection_id)) throw new Error('Invalid collection_id'); const team = await pgClient.query(`SELECT id FROM teams WHERE "deletedAt" IS NULL LIMIT 1`); if (team.rows.length === 0) throw new Error('No team found'); const user = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`); if (user.rows.length === 0) throw new Error('No admin user found'); const result = await pgClient.query(` INSERT INTO imports (id, type, state, "teamId", "createdById", "documentCount", "fileCount", "createdAt", "updatedAt") VALUES (gen_random_uuid(), $1, 'pending', $2, $3, 0, 0, NOW(), NOW()) RETURNING * `, [args.type, team.rows[0].id, user.rows[0].id]); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Import job created' }, null, 2) }], }; }, }; /** * imports.cancel - Cancel import job */ const cancelImport: BaseTool<{ id: string }> = { name: 'outline_cancel_import', description: 'Cancel a pending or processing import job.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Import ID (UUID)' }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.id)) throw new Error('Invalid import ID'); const result = await pgClient.query(` UPDATE imports SET state = 'cancelled', "updatedAt" = NOW() WHERE id = $1 AND state IN ('pending', 'processing') RETURNING id, state, type `, [args.id]); if (result.rows.length === 0) throw new Error('Import not found or cannot be cancelled'); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Import cancelled' }, null, 2) }], }; }, }; export const importsTools: BaseTool[] = [listImports, getImportStatus, createImport, cancelImport];