Fixed issues discovered during comprehensive testing of 164 tools: - groups.ts: Remove non-existent description column - analytics.ts: Use group_permissions instead of collection_group_memberships - notifications.ts: Remove non-existent data column - imports-tools.ts: Remove non-existent type/documentCount/fileCount columns - emojis.ts: Graceful handling when emojis table doesn't exist - teams.ts: Remove passkeysEnabled/description/preferences columns - collections.ts: Use lastModifiedById instead of updatedById - revisions.ts: Use lastModifiedById instead of updatedById Tested 45+ tools against production (hub.descomplicar.pt) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
180 lines
5.8 KiB
TypeScript
180 lines
5.8 KiB
TypeScript
/**
|
|
* 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<ImportListArgs> = {
|
|
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<ToolResponse> => {
|
|
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<ToolResponse> => {
|
|
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<ToolResponse> => {
|
|
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<ToolResponse> => {
|
|
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<any>[] = [listImports, getImportStatus, createImport, cancelImport];
|