/** * MCP Outline PostgreSQL - Collections Tools * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { BaseTool, ToolResponse } from '../types/tools.js'; import { validatePagination, isValidUUID } from '../utils/security.js'; export const collectionsTools: BaseTool[] = [ // 1. LIST COLLECTIONS { name: 'list_collections', description: 'List all collections in the team. Supports pagination and filtering by teamId.', inputSchema: { type: 'object', properties: { teamId: { type: 'string', description: 'Filter collections by team ID (UUID format)', }, offset: { type: 'number', description: 'Number of records to skip (default: 0)', default: 0, }, limit: { type: 'number', description: 'Maximum number of records to return (default: 25, max: 100)', default: 25, }, }, }, handler: async (args: { teamId?: string; offset?: number; limit?: number }, pool: Pool): Promise => { try { const { offset = 0, limit = 25, teamId } = args; validatePagination(offset, limit); let query = ` SELECT c.id, c."urlId", c.name, c.description, c.icon, c.color, c.index, c.permission, c."maintainerApprovalRequired", c."documentStructure", c.sharing, c.sort, c."teamId", c."createdById", c."createdAt", c."updatedAt", c."deletedAt", c."archivedAt", u.name as "createdByName", u.email as "createdByEmail", (SELECT COUNT(*) FROM documents WHERE "collectionId" = c.id AND "deletedAt" IS NULL) as "documentCount", (SELECT COUNT(*) FROM collection_users WHERE "collectionId" = c.id) as "memberCount" FROM collections c LEFT JOIN users u ON c."createdById" = u.id WHERE c."deletedAt" IS NULL `; const params: any[] = []; if (teamId) { if (!isValidUUID(teamId)) { return { content: [{ type: 'text', text: 'Invalid teamId format. Must be a valid UUID.' }], isError: true, }; } params.push(teamId); query += ` AND c."teamId" = $${params.length}`; } query += ` ORDER BY c.index ASC, c."createdAt" DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2} `; params.push(limit, offset); const result = await pool.query(query, params); // Get total count let countQuery = 'SELECT COUNT(*) FROM collections WHERE "deletedAt" IS NULL'; const countParams: any[] = []; if (teamId) { countParams.push(teamId); countQuery += ` AND "teamId" = $1`; } const countResult = await pool.query(countQuery, countParams); const totalCount = parseInt(countResult.rows[0].count, 10); return { content: [ { type: 'text', text: JSON.stringify({ collections: result.rows, pagination: { total: totalCount, offset, limit, hasMore: offset + limit < totalCount, }, }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error listing collections: ${error.message}` }], isError: true, }; } }, }, // 2. GET COLLECTION { name: 'get_collection', description: 'Get detailed information about a specific collection by ID or urlId.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Collection ID (UUID) or urlId', }, }, required: ['id'], }, handler: async (args: { id: string }, pool: Pool): Promise => { try { const { id } = args; const query = ` SELECT c.id, c."urlId", c.name, c.description, c.icon, c.color, c.index, c.permission, c."maintainerApprovalRequired", c."documentStructure", c.sharing, c.sort, c."teamId", c."createdById", c."createdAt", c."updatedAt", c."deletedAt", c."archivedAt", u.name as "createdByName", u.email as "createdByEmail", t.name as "teamName", (SELECT COUNT(*) FROM documents WHERE "collectionId" = c.id AND "deletedAt" IS NULL) as "documentCount", (SELECT COUNT(*) FROM collection_users WHERE "collectionId" = c.id) as "memberCount", (SELECT COUNT(*) FROM collection_groups WHERE "collectionId" = c.id) as "groupCount" FROM collections c LEFT JOIN users u ON c."createdById" = u.id LEFT JOIN teams t ON c."teamId" = t.id WHERE (c.id = $1 OR c."urlId" = $1) AND c."deletedAt" IS NULL `; const result = await pool.query(query, [id]); if (result.rows.length === 0) { return { content: [{ type: 'text', text: 'Collection not found or has been deleted.' }], isError: true, }; } return { content: [{ type: 'text', text: JSON.stringify(result.rows[0], null, 2) }], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error getting collection: ${error.message}` }], isError: true, }; } }, }, // 3. CREATE COLLECTION { name: 'create_collection', description: 'Create a new collection in a team.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Collection name', }, teamId: { type: 'string', description: 'Team ID (UUID)', }, createdById: { type: 'string', description: 'User ID creating the collection (UUID)', }, description: { type: 'string', description: 'Collection description', }, icon: { type: 'string', description: 'Collection icon (emoji or icon name)', }, color: { type: 'string', description: 'Collection color (hex format)', }, permission: { type: 'string', description: 'Default permission level (read, read_write)', enum: ['read', 'read_write'], default: 'read_write', }, sharing: { type: 'boolean', description: 'Allow public sharing', default: true, }, index: { type: 'string', description: 'Custom index for sorting', }, }, required: ['name', 'teamId', 'createdById'], }, handler: async (args: { name: string; teamId: string; createdById: string; description?: string; icon?: string; color?: string; permission?: string; sharing?: boolean; index?: string; }, pool: Pool): Promise => { try { const { name, teamId, createdById, description, icon, color, permission = 'read_write', sharing = true, index } = args; // Validate UUIDs if (!isValidUUID(teamId) || !isValidUUID(createdById)) { return { content: [{ type: 'text', text: 'Invalid teamId or createdById format. Must be valid UUIDs.' }], isError: true, }; } // Generate urlId from name const urlId = name.toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .substring(0, 50); const query = ` INSERT INTO collections ( name, "urlId", "teamId", "createdById", description, icon, color, permission, sharing, index, "createdAt", "updatedAt" ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) RETURNING id, "urlId", name, description, icon, color, index, permission, sharing, "teamId", "createdById", "createdAt", "updatedAt" `; const result = await pool.query(query, [ name, urlId, teamId, createdById, description || null, icon || null, color || null, permission, sharing, index || null, ]); return { content: [ { type: 'text', text: JSON.stringify({ message: 'Collection created successfully', collection: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error creating collection: ${error.message}` }], isError: true, }; } }, }, // 4. UPDATE COLLECTION { name: 'update_collection', description: 'Update an existing collection.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Collection ID (UUID)', }, name: { type: 'string', description: 'New collection name', }, description: { type: 'string', description: 'New description', }, icon: { type: 'string', description: 'New icon', }, color: { type: 'string', description: 'New color (hex format)', }, permission: { type: 'string', description: 'New default permission', enum: ['read', 'read_write'], }, sharing: { type: 'boolean', description: 'New sharing setting', }, index: { type: 'string', description: 'New index for sorting', }, }, required: ['id'], }, handler: async (args: { id: string; name?: string; description?: string; icon?: string; color?: string; permission?: string; sharing?: boolean; index?: string; }, pool: Pool): Promise => { try { const { id, name, description, icon, color, permission, sharing, index } = args; if (!isValidUUID(id)) { return { content: [{ type: 'text', text: 'Invalid collection ID format. Must be a valid UUID.' }], isError: true, }; } // Build dynamic UPDATE query const updates: string[] = []; const values: any[] = []; let paramCount = 1; if (name !== undefined) { updates.push(`name = $${paramCount}`); values.push(name); paramCount++; // Update urlId if name changes const urlId = name.toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .substring(0, 50); updates.push(`"urlId" = $${paramCount}`); values.push(urlId); paramCount++; } if (description !== undefined) { updates.push(`description = $${paramCount}`); values.push(description); paramCount++; } if (icon !== undefined) { updates.push(`icon = $${paramCount}`); values.push(icon); paramCount++; } if (color !== undefined) { updates.push(`color = $${paramCount}`); values.push(color); paramCount++; } if (permission !== undefined) { updates.push(`permission = $${paramCount}`); values.push(permission); paramCount++; } if (sharing !== undefined) { updates.push(`sharing = $${paramCount}`); values.push(sharing); paramCount++; } if (index !== undefined) { updates.push(`index = $${paramCount}`); values.push(index); paramCount++; } if (updates.length === 0) { return { content: [{ type: 'text', text: 'No fields to update provided.' }], isError: true, }; } updates.push(`"updatedAt" = NOW()`); values.push(id); const query = ` UPDATE collections SET ${updates.join(', ')} WHERE id = $${paramCount} AND "deletedAt" IS NULL RETURNING id, "urlId", name, description, icon, color, index, permission, sharing, "teamId", "createdById", "createdAt", "updatedAt" `; const result = await pool.query(query, values); if (result.rows.length === 0) { return { content: [{ type: 'text', text: 'Collection not found or has been deleted.' }], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Collection updated successfully', collection: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error updating collection: ${error.message}` }], isError: true, }; } }, }, // 5. DELETE COLLECTION { name: 'delete_collection', description: 'Soft delete a collection (sets deletedAt timestamp).', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Collection ID (UUID)', }, }, required: ['id'], }, handler: async (args: { id: string }, pool: Pool): Promise => { try { const { id } = args; if (!isValidUUID(id)) { return { content: [{ type: 'text', text: 'Invalid collection ID format. Must be a valid UUID.' }], isError: true, }; } const query = ` UPDATE collections SET "deletedAt" = NOW(), "updatedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL RETURNING id, name, "deletedAt" `; const result = await pool.query(query, [id]); if (result.rows.length === 0) { return { content: [{ type: 'text', text: 'Collection not found or already deleted.' }], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Collection deleted successfully', collection: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error deleting collection: ${error.message}` }], isError: true, }; } }, }, // 6. LIST COLLECTION DOCUMENTS { name: 'list_collection_documents', description: 'List all documents in a collection with pagination.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, offset: { type: 'number', description: 'Number of records to skip (default: 0)', default: 0, }, limit: { type: 'number', description: 'Maximum number of records to return (default: 25, max: 100)', default: 25, }, }, required: ['collectionId'], }, handler: async (args: { collectionId: string; offset?: number; limit?: number }, pool: Pool): Promise => { try { const { collectionId, offset = 0, limit = 25 } = args; if (!isValidUUID(collectionId)) { return { content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], isError: true, }; } validatePagination(offset, limit); const query = ` SELECT d.id, d."urlId", d.title, d.icon, d."collectionId", d."parentDocumentId", d.template, d.fullWidth, d."createdById", d."lastModifiedById", d."createdAt", d."updatedAt", d."publishedAt", d."archivedAt", d."deletedAt", creator.name as "createdByName", creator.email as "createdByEmail", updater.name as "updatedByName", updater.email as "updatedByEmail" FROM documents d LEFT JOIN users creator ON d."createdById" = creator.id LEFT JOIN users updater ON d."lastModifiedById" = updater.id WHERE d."collectionId" = $1 AND d."deletedAt" IS NULL ORDER BY d."updatedAt" DESC LIMIT $2 OFFSET $3 `; const result = await pool.query(query, [collectionId, limit, offset]); // Get total count const countQuery = 'SELECT COUNT(*) FROM documents WHERE "collectionId" = $1 AND "deletedAt" IS NULL'; const countResult = await pool.query(countQuery, [collectionId]); const totalCount = parseInt(countResult.rows[0].count, 10); return { content: [ { type: 'text', text: JSON.stringify({ documents: result.rows, pagination: { total: totalCount, offset, limit, hasMore: offset + limit < totalCount, }, }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error listing collection documents: ${error.message}` }], isError: true, }; } }, }, // 7. ADD USER TO COLLECTION { name: 'add_user_to_collection', description: 'Add a user to a collection with specific permissions.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, userId: { type: 'string', description: 'User ID (UUID)', }, permission: { type: 'string', description: 'Permission level for the user', enum: ['read', 'read_write', 'maintain'], default: 'read_write', }, createdById: { type: 'string', description: 'User ID adding the member (UUID)', }, }, required: ['collectionId', 'userId', 'createdById'], }, handler: async (args: { collectionId: string; userId: string; permission?: string; createdById: string; }, pool: Pool): Promise => { try { const { collectionId, userId, permission = 'read_write', createdById } = args; if (!isValidUUID(collectionId) || !isValidUUID(userId) || !isValidUUID(createdById)) { return { content: [{ type: 'text', text: 'Invalid UUID format for collectionId, userId, or createdById.' }], isError: true, }; } const query = ` INSERT INTO collection_users ("collectionId", "userId", permission, "createdById", "createdAt", "updatedAt") VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT ("collectionId", "userId") DO UPDATE SET permission = $3, "updatedAt" = NOW() RETURNING id, "collectionId", "userId", permission, "createdById", "createdAt", "updatedAt" `; const result = await pool.query(query, [collectionId, userId, permission, createdById]); return { content: [ { type: 'text', text: JSON.stringify({ message: 'User added to collection successfully', membership: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error adding user to collection: ${error.message}` }], isError: true, }; } }, }, // 8. REMOVE USER FROM COLLECTION { name: 'remove_user_from_collection', description: 'Remove a user from a collection.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, userId: { type: 'string', description: 'User ID (UUID)', }, }, required: ['collectionId', 'userId'], }, handler: async (args: { collectionId: string; userId: string }, pool: Pool): Promise => { try { const { collectionId, userId } = args; if (!isValidUUID(collectionId) || !isValidUUID(userId)) { return { content: [{ type: 'text', text: 'Invalid UUID format for collectionId or userId.' }], isError: true, }; } const query = ` DELETE FROM collection_users WHERE "collectionId" = $1 AND "userId" = $2 RETURNING id, "collectionId", "userId" `; const result = await pool.query(query, [collectionId, userId]); if (result.rows.length === 0) { return { content: [{ type: 'text', text: 'User membership not found in collection.' }], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify({ message: 'User removed from collection successfully', membership: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error removing user from collection: ${error.message}` }], isError: true, }; } }, }, // 9. LIST COLLECTION MEMBERSHIPS { name: 'list_collection_memberships', description: 'List all user memberships for a collection.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, offset: { type: 'number', description: 'Number of records to skip (default: 0)', default: 0, }, limit: { type: 'number', description: 'Maximum number of records to return (default: 25, max: 100)', default: 25, }, }, required: ['collectionId'], }, handler: async (args: { collectionId: string; offset?: number; limit?: number }, pool: Pool): Promise => { try { const { collectionId, offset = 0, limit = 25 } = args; if (!isValidUUID(collectionId)) { return { content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], isError: true, }; } validatePagination(offset, limit); const query = ` SELECT cu.id, cu."collectionId", cu."userId", cu.permission, cu."createdById", cu."createdAt", cu."updatedAt", u.name as "userName", u.email as "userEmail", creator.name as "addedByName" FROM collection_users cu LEFT JOIN users u ON cu."userId" = u.id LEFT JOIN users creator ON cu."createdById" = creator.id WHERE cu."collectionId" = $1 ORDER BY cu."createdAt" DESC LIMIT $2 OFFSET $3 `; const result = await pool.query(query, [collectionId, limit, offset]); // Get total count const countQuery = 'SELECT COUNT(*) FROM collection_users WHERE "collectionId" = $1'; const countResult = await pool.query(countQuery, [collectionId]); const totalCount = parseInt(countResult.rows[0].count, 10); return { content: [ { type: 'text', text: JSON.stringify({ memberships: result.rows, pagination: { total: totalCount, offset, limit, hasMore: offset + limit < totalCount, }, }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error listing collection memberships: ${error.message}` }], isError: true, }; } }, }, // 10. ADD GROUP TO COLLECTION { name: 'add_group_to_collection', description: 'Add a group to a collection with specific permissions.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, groupId: { type: 'string', description: 'Group ID (UUID)', }, permission: { type: 'string', description: 'Permission level for the group', enum: ['read', 'read_write', 'maintain'], default: 'read_write', }, createdById: { type: 'string', description: 'User ID adding the group (UUID)', }, }, required: ['collectionId', 'groupId', 'createdById'], }, handler: async (args: { collectionId: string; groupId: string; permission?: string; createdById: string; }, pool: Pool): Promise => { try { const { collectionId, groupId, permission = 'read_write', createdById } = args; if (!isValidUUID(collectionId) || !isValidUUID(groupId) || !isValidUUID(createdById)) { return { content: [{ type: 'text', text: 'Invalid UUID format for collectionId, groupId, or createdById.' }], isError: true, }; } const query = ` INSERT INTO collection_groups ("collectionId", "groupId", permission, "createdById", "createdAt", "updatedAt") VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT ("collectionId", "groupId") DO UPDATE SET permission = $3, "updatedAt" = NOW() RETURNING id, "collectionId", "groupId", permission, "createdById", "createdAt", "updatedAt" `; const result = await pool.query(query, [collectionId, groupId, permission, createdById]); return { content: [ { type: 'text', text: JSON.stringify({ message: 'Group added to collection successfully', membership: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error adding group to collection: ${error.message}` }], isError: true, }; } }, }, // 11. REMOVE GROUP FROM COLLECTION { name: 'remove_group_from_collection', description: 'Remove a group from a collection.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, groupId: { type: 'string', description: 'Group ID (UUID)', }, }, required: ['collectionId', 'groupId'], }, handler: async (args: { collectionId: string; groupId: string }, pool: Pool): Promise => { try { const { collectionId, groupId } = args; if (!isValidUUID(collectionId) || !isValidUUID(groupId)) { return { content: [{ type: 'text', text: 'Invalid UUID format for collectionId or groupId.' }], isError: true, }; } const query = ` DELETE FROM collection_groups WHERE "collectionId" = $1 AND "groupId" = $2 RETURNING id, "collectionId", "groupId" `; const result = await pool.query(query, [collectionId, groupId]); if (result.rows.length === 0) { return { content: [{ type: 'text', text: 'Group membership not found in collection.' }], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Group removed from collection successfully', membership: result.rows[0], }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error removing group from collection: ${error.message}` }], isError: true, }; } }, }, // 12. LIST COLLECTION GROUP MEMBERSHIPS { name: 'list_collection_group_memberships', description: 'List all group memberships for a collection.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, offset: { type: 'number', description: 'Number of records to skip (default: 0)', default: 0, }, limit: { type: 'number', description: 'Maximum number of records to return (default: 25, max: 100)', default: 25, }, }, required: ['collectionId'], }, handler: async (args: { collectionId: string; offset?: number; limit?: number }, pool: Pool): Promise => { try { const { collectionId, offset = 0, limit = 25 } = args; if (!isValidUUID(collectionId)) { return { content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], isError: true, }; } validatePagination(offset, limit); const query = ` SELECT cg.id, cg."collectionId", cg."groupId", cg.permission, cg."createdById", cg."createdAt", cg."updatedAt", g.name as "groupName", creator.name as "addedByName" FROM collection_groups cg LEFT JOIN groups g ON cg."groupId" = g.id LEFT JOIN users creator ON cg."createdById" = creator.id WHERE cg."collectionId" = $1 ORDER BY cg."createdAt" DESC LIMIT $2 OFFSET $3 `; const result = await pool.query(query, [collectionId, limit, offset]); // Get total count const countQuery = 'SELECT COUNT(*) FROM collection_groups WHERE "collectionId" = $1'; const countResult = await pool.query(countQuery, [collectionId]); const totalCount = parseInt(countResult.rows[0].count, 10); return { content: [ { type: 'text', text: JSON.stringify({ memberships: result.rows, pagination: { total: totalCount, offset, limit, hasMore: offset + limit < totalCount, }, }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error listing collection group memberships: ${error.message}` }], isError: true, }; } }, }, // 13. EXPORT COLLECTION { name: 'export_collection', description: 'Export all documents from a collection as markdown files.', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'Collection ID (UUID)', }, includeArchived: { type: 'boolean', description: 'Include archived documents', default: false, }, }, required: ['collectionId'], }, handler: async (args: { collectionId: string; includeArchived?: boolean }, pool: Pool): Promise => { try { const { collectionId, includeArchived = false } = args; if (!isValidUUID(collectionId)) { return { content: [{ type: 'text', text: 'Invalid collectionId format. Must be a valid UUID.' }], isError: true, }; } // Get collection info const collectionQuery = ` SELECT id, name, description FROM collections WHERE id = $1 AND "deletedAt" IS NULL `; const collectionResult = await pool.query(collectionQuery, [collectionId]); if (collectionResult.rows.length === 0) { return { content: [{ type: 'text', text: 'Collection not found or has been deleted.' }], isError: true, }; } const collection = collectionResult.rows[0]; // Get documents let documentsQuery = ` SELECT d.id, d.title, d.icon, d.text, d."createdAt", d."updatedAt", d."publishedAt", u.name as "authorName" FROM documents d LEFT JOIN users u ON d."createdById" = u.id WHERE d."collectionId" = $1 AND d."deletedAt" IS NULL `; if (!includeArchived) { documentsQuery += ` AND d."archivedAt" IS NULL`; } documentsQuery += ` ORDER BY d."createdAt" ASC`; const documentsResult = await pool.query(documentsQuery, [collectionId]); // Format export const exports = documentsResult.rows.map(doc => { const markdown = `--- title: ${doc.title} icon: ${doc.icon || ''} author: ${doc.authorName} created: ${doc.createdAt} updated: ${doc.updatedAt} published: ${doc.publishedAt || 'Not published'} --- ${doc.text || ''} `; return { id: doc.id, title: doc.title, filename: `${doc.title.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.md`, markdown, }; }); return { content: [ { type: 'text', text: JSON.stringify({ collection: { id: collection.id, name: collection.name, description: collection.description, }, documentCount: exports.length, documents: exports, }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error exporting collection: ${error.message}` }], isError: true, }; } }, }, // 14. EXPORT ALL COLLECTIONS { name: 'export_all_collections', description: 'Export all collections and their documents from a team.', inputSchema: { type: 'object', properties: { teamId: { type: 'string', description: 'Team ID (UUID)', }, includeArchived: { type: 'boolean', description: 'Include archived documents', default: false, }, }, required: ['teamId'], }, handler: async (args: { teamId: string; includeArchived?: boolean }, pool: Pool): Promise => { try { const { teamId, includeArchived = false } = args; if (!isValidUUID(teamId)) { return { content: [{ type: 'text', text: 'Invalid teamId format. Must be a valid UUID.' }], isError: true, }; } // Get all collections const collectionsQuery = ` SELECT id, name, description FROM collections WHERE "teamId" = $1 AND "deletedAt" IS NULL ORDER BY index ASC, "createdAt" ASC `; const collectionsResult = await pool.query(collectionsQuery, [teamId]); const exports = []; for (const collection of collectionsResult.rows) { // Get documents for each collection let documentsQuery = ` SELECT d.id, d.title, d.icon, d.text, d."createdAt", d."updatedAt", d."publishedAt", u.name as "authorName" FROM documents d LEFT JOIN users u ON d."createdById" = u.id WHERE d."collectionId" = $1 AND d."deletedAt" IS NULL `; if (!includeArchived) { documentsQuery += ` AND d."archivedAt" IS NULL`; } documentsQuery += ` ORDER BY d."createdAt" ASC`; const documentsResult = await pool.query(documentsQuery, [collection.id]); const documents = documentsResult.rows.map(doc => { const markdown = `--- title: ${doc.title} icon: ${doc.icon || ''} author: ${doc.authorName} created: ${doc.createdAt} updated: ${doc.updatedAt} published: ${doc.publishedAt || 'Not published'} --- ${doc.text || ''} `; return { id: doc.id, title: doc.title, filename: `${doc.title.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.md`, markdown, }; }); exports.push({ collection: { id: collection.id, name: collection.name, description: collection.description, }, documentCount: documents.length, documents, }); } return { content: [ { type: 'text', text: JSON.stringify({ teamId, collectionCount: exports.length, totalDocuments: exports.reduce((sum, col) => sum + col.documentCount, 0), collections: exports, }, null, 2), }, ], }; } catch (error: any) { return { content: [{ type: 'text', text: `Error exporting all collections: ${error.message}` }], isError: true, }; } }, }, ];