/** * MCP Outline PostgreSQL - Pins 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 PinListArgs extends PaginationArgs { collection_id?: string; team_id?: string; } interface PinCreateArgs { document_id: string; collection_id?: string; } interface PinDeleteArgs { id: string; } /** * pins.list - List pinned documents */ const listPins: BaseTool = { name: 'outline_pins_list', description: 'List pinned documents. Pins highlight important documents at the top of collections or home.', inputSchema: { type: 'object', properties: { collection_id: { type: 'string', description: 'Filter by collection ID (UUID)', }, team_id: { type: 'string', description: 'Filter by team ID (UUID)', }, limit: { type: 'number', description: 'Maximum results (default: 25, max: 100)', }, offset: { type: 'number', description: 'Results to skip (default: 0)', }, }, }, handler: async (args, pgClient): Promise => { const { limit, offset } = validatePagination(args.limit, args.offset); const conditions: string[] = []; const params: any[] = []; let paramIndex = 1; if (args.collection_id) { if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id format'); conditions.push(`p."collectionId" = $${paramIndex++}`); params.push(args.collection_id); } if (args.team_id) { if (!isValidUUID(args.team_id)) throw new Error('Invalid team_id format'); conditions.push(`p."teamId" = $${paramIndex++}`); params.push(args.team_id); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const result = await pgClient.query( ` SELECT p.id, p."documentId", p."collectionId", p."teamId", p."createdById", p.index, p."createdAt", d.title as "documentTitle", c.name as "collectionName", u.name as "createdByName" FROM pins p LEFT JOIN documents d ON p."documentId" = d.id LEFT JOIN collections c ON p."collectionId" = c.id LEFT JOIN users u ON p."createdById" = u.id ${whereClause} ORDER BY p.index ASC NULLS LAST, p."createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex} `, [...params, limit, offset] ); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows, pagination: { limit, offset, total: result.rows.length } }, null, 2), }], }; }, }; /** * pins.create - Pin a document */ const createPin: BaseTool = { name: 'outline_pins_create', description: 'Pin a document to highlight it at the top of a collection or home.', inputSchema: { type: 'object', properties: { document_id: { type: 'string', description: 'Document ID to pin (UUID)', }, collection_id: { type: 'string', description: 'Collection ID to pin to (UUID, optional - pins to home if not specified)', }, }, required: ['document_id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.document_id)) throw new Error('Invalid document_id format'); if (args.collection_id && !isValidUUID(args.collection_id)) throw new Error('Invalid collection_id format'); // Get document to find team const docResult = await pgClient.query( `SELECT id, "teamId" FROM documents WHERE id = $1 AND "deletedAt" IS NULL`, [args.document_id] ); if (docResult.rows.length === 0) { throw new Error('Document not found'); } const teamId = docResult.rows[0].teamId; // Get admin user for createdById const userResult = await pgClient.query( `SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1` ); if (userResult.rows.length === 0) { throw new Error('No admin user found'); } // Check for existing pin const existing = await pgClient.query( `SELECT id FROM pins WHERE "documentId" = $1 AND ("collectionId" = $2 OR ($2 IS NULL AND "collectionId" IS NULL))`, [args.document_id, args.collection_id || null] ); if (existing.rows.length > 0) { throw new Error('Document is already pinned'); } const result = await pgClient.query( ` INSERT INTO pins (id, "documentId", "collectionId", "teamId", "createdById", "createdAt", "updatedAt") VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW()) RETURNING * `, [args.document_id, args.collection_id || null, teamId, userResult.rows[0].id] ); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Pin created successfully' }, null, 2), }], }; }, }; /** * pins.delete - Remove a pin */ const deletePin: BaseTool = { name: 'outline_pins_delete', description: 'Remove a pin from a document.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Pin ID to delete (UUID)', }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.id)) throw new Error('Invalid id format'); const result = await pgClient.query( `DELETE FROM pins WHERE id = $1 RETURNING id, "documentId", "collectionId"`, [args.id] ); if (result.rows.length === 0) { throw new Error('Pin not found'); } return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Pin deleted successfully' }, null, 2), }], }; }, }; export const pinsTools: BaseTool[] = [listPins, createPin, deletePin];