feat: Add 52 new tools bringing total to 160
New modules (11): - teams.ts (5 tools): Team/workspace management - integrations.ts (6 tools): External integrations (Slack, embeds) - notifications.ts (4 tools): User notification management - subscriptions.ts (4 tools): Document subscription management - templates.ts (5 tools): Document template management - imports-tools.ts (4 tools): Import job management - emojis.ts (3 tools): Custom emoji management - user-permissions.ts (3 tools): Permission management - bulk-operations.ts (6 tools): Batch operations - advanced-search.ts (6 tools): Faceted search, recent, orphaned, duplicates - analytics.ts (6 tools): Usage statistics and insights Updated: - src/index.ts: Import and register all new tools - src/tools/index.ts: Export all new modules - CHANGELOG.md: Version 1.2.0 entry - CLAUDE.md: Updated tool count to 160 - CONTINUE.md: Updated state documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
223
src/tools/templates.ts
Normal file
223
src/tools/templates.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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<TemplateListArgs> = {
|
||||
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<ToolResponse> => {
|
||||
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.emoji, 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<ToolResponse> => {
|
||||
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<ToolResponse> => {
|
||||
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, emoji, "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.emoji,
|
||||
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<ToolResponse> => {
|
||||
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<ToolResponse> => {
|
||||
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<any>[] = [
|
||||
listTemplates, getTemplate, createFromTemplate, convertToTemplate, convertFromTemplate
|
||||
];
|
||||
Reference in New Issue
Block a user