Files
mcp-outline-postgresql/src/tools/imports-tools.ts
Emanuel Almeida 56f37892c0 fix: Schema compatibility - 8 column/table fixes found during testing
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>
2026-01-31 17:23:00 +00:00

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];