/** * MCP Outline PostgreSQL - Templates Tools * Templates are documents with template=true * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { Pool } from 'pg'; import { BaseTool, ToolResponse, PaginationArgs } from '../types/tools.js'; import { validatePagination, isValidUUID, sanitizeInput } from '../utils/security.js'; interface TemplateListArgs extends PaginationArgs { collection_id?: string; } /** * templates.list - List templates */ const listTemplates: BaseTool = { name: 'outline_list_templates', description: 'List document templates. Templates are reusable document structures.', inputSchema: { type: 'object', properties: { collection_id: { type: 'string', description: 'Filter by collection ID (UUID)' }, 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[] = ['d.template = true', 'd."deletedAt" IS NULL']; const params: any[] = []; let idx = 1; if (args.collection_id) { if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id'); conditions.push(`d."collectionId" = $${idx++}`); params.push(args.collection_id); } const result = await pgClient.query(` SELECT d.id, d.title, d.icon, d."collectionId", d."createdById", d."createdAt", d."updatedAt", c.name as "collectionName", u.name as "createdByName", (SELECT COUNT(*) FROM documents WHERE "templateId" = d.id) as "usageCount" FROM documents d LEFT JOIN collections c ON d."collectionId" = c.id LEFT JOIN users u ON d."createdById" = u.id WHERE ${conditions.join(' AND ')} ORDER BY d."updatedAt" 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) }], }; }, }; /** * templates.info - Get template details */ const getTemplate: BaseTool<{ id: string }> = { name: 'outline_get_template', description: 'Get detailed information about a template including its content.', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template ID (UUID)' }, }, required: ['id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.id)) throw new Error('Invalid template ID'); const result = await pgClient.query(` SELECT d.*, c.name as "collectionName", u.name as "createdByName", (SELECT COUNT(*) FROM documents WHERE "templateId" = d.id) as "usageCount" FROM documents d LEFT JOIN collections c ON d."collectionId" = c.id LEFT JOIN users u ON d."createdById" = u.id WHERE d.id = $1 AND d.template = true AND d."deletedAt" IS NULL `, [args.id]); if (result.rows.length === 0) throw new Error('Template not found'); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0] }, null, 2) }], }; }, }; /** * templates.create - Create document from template */ const createFromTemplate: BaseTool<{ template_id: string; title: string; collection_id?: string; parent_document_id?: string }> = { name: 'outline_create_from_template', description: 'Create a new document from a template.', inputSchema: { type: 'object', properties: { template_id: { type: 'string', description: 'Template ID (UUID)' }, title: { type: 'string', description: 'Title for the new document' }, collection_id: { type: 'string', description: 'Collection ID (UUID, optional - uses template collection)' }, parent_document_id: { type: 'string', description: 'Parent document ID (UUID, optional)' }, }, required: ['template_id', 'title'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.template_id)) throw new Error('Invalid template_id'); if (args.collection_id && !isValidUUID(args.collection_id)) throw new Error('Invalid collection_id'); if (args.parent_document_id && !isValidUUID(args.parent_document_id)) throw new Error('Invalid parent_document_id'); // Get template const template = await pgClient.query( `SELECT * FROM documents WHERE id = $1 AND template = true AND "deletedAt" IS NULL`, [args.template_id] ); if (template.rows.length === 0) throw new Error('Template not found'); const t = template.rows[0]; // Get user const user = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`); const userId = user.rows.length > 0 ? user.rows[0].id : t.createdById; // Create document from template const result = await pgClient.query(` INSERT INTO documents ( id, title, text, icon, "collectionId", "teamId", "parentDocumentId", "templateId", "createdById", "lastModifiedById", template, "createdAt", "updatedAt" ) VALUES ( gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $8, false, NOW(), NOW() ) RETURNING * `, [ sanitizeInput(args.title), t.text, t.icon, args.collection_id || t.collectionId, t.teamId, args.parent_document_id || null, args.template_id, userId, ]); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Document created from template' }, null, 2) }], }; }, }; /** * templates.convert - Convert document to template */ const convertToTemplate: BaseTool<{ document_id: string }> = { name: 'outline_convert_to_template', description: 'Convert an existing document to a template.', inputSchema: { type: 'object', properties: { document_id: { type: 'string', description: 'Document ID to convert (UUID)' }, }, required: ['document_id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.document_id)) throw new Error('Invalid document_id'); const result = await pgClient.query(` UPDATE documents SET template = true, "updatedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL AND template = false RETURNING id, title, template `, [args.document_id]); if (result.rows.length === 0) throw new Error('Document not found or already a template'); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Converted to template' }, null, 2) }], }; }, }; /** * templates.unconvert - Convert template back to document */ const convertFromTemplate: BaseTool<{ template_id: string }> = { name: 'outline_convert_from_template', description: 'Convert a template back to a regular document.', inputSchema: { type: 'object', properties: { template_id: { type: 'string', description: 'Template ID to convert (UUID)' }, }, required: ['template_id'], }, handler: async (args, pgClient): Promise => { if (!isValidUUID(args.template_id)) throw new Error('Invalid template_id'); const result = await pgClient.query(` UPDATE documents SET template = false, "updatedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL AND template = true RETURNING id, title, template `, [args.template_id]); if (result.rows.length === 0) throw new Error('Template not found or already a document'); return { content: [{ type: 'text', text: JSON.stringify({ data: result.rows[0], message: 'Converted to document' }, null, 2) }], }; }, }; export const templatesTools: BaseTool[] = [ listTemplates, getTemplate, createFromTemplate, convertToTemplate, convertFromTemplate ];