diff --git a/CHANGELOG.md b/CHANGELOG.md index e2acde3..40c5cfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. +## [1.3.2] - 2026-01-31 + +### Fixed + +- **Schema Compatibility:** Fixed column name mismatch with production Outline database + - Changed `emoji` to `icon` in documents queries (8 files affected) + - Changed `emoji` to `icon` in revisions queries + - Updated export/import tools to use `icon` field + - Updated templates tools to use `icon` field + - `reactions.emoji` kept unchanged (correct schema) + +### Files Updated + +- `src/tools/documents.ts` - SELECT queries +- `src/tools/advanced-search.ts` - Search queries +- `src/tools/analytics.ts` - Analytics queries + GROUP BY +- `src/tools/export-import.ts` - Export/import with metadata +- `src/tools/templates.ts` - Template queries + INSERT +- `src/tools/collections.ts` - Collection document listing +- `src/tools/revisions.ts` - Revision comparison + +### Verified + +- Production connection: hub.descomplicar.pt (448 documents) +- All 164 tools build without errors + ## [1.3.1] - 2026-01-31 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index c7acc64..6abc7a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co MCP server for direct PostgreSQL access to Outline Wiki database. Follows patterns established by `mcp-desk-crm-sql-v3`. -**Version:** 1.3.1 +**Version:** 1.3.2 **Total Tools:** 164 tools across 33 modules **Production:** hub.descomplicar.pt (via SSH tunnel) diff --git a/package.json b/package.json index a513463..64179be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-outline-postgresql", - "version": "1.3.1", + "version": "1.3.2", "description": "MCP Server for Outline Wiki via PostgreSQL direct access", "main": "dist/index.js", "scripts": { diff --git a/src/tools/advanced-search.ts b/src/tools/advanced-search.ts index a2fd0e9..0c5c41c 100644 --- a/src/tools/advanced-search.ts +++ b/src/tools/advanced-search.ts @@ -86,7 +86,7 @@ const advancedSearchDocuments: BaseTool = { const result = await pgClient.query(` SELECT - d.id, d.title, d.emoji, d.template, + d.id, d.title, d.icon, d.template, d."collectionId", d."createdById", d."createdAt", d."updatedAt", d."publishedAt", d."archivedAt", c.name as "collectionName", @@ -217,7 +217,7 @@ const searchRecent: BaseTool = { // Most viewed documents const mostViewed = await pgClient.query(` SELECT - d.id, d.title, d.emoji, c.name as "collectionName", + d.id, d.title, d.icon, c.name as "collectionName", COUNT(v.id) as "viewCount", COUNT(DISTINCT v."userId") as "uniqueViewers" FROM documents d LEFT JOIN views v ON v."documentId" = d.id LEFT JOIN collections c ON d."collectionId" = c.id WHERE d."deletedAt" IS NULL ${collectionCondition} - GROUP BY d.id, d.title, d.emoji, c.name + GROUP BY d.id, d.title, d.icon, c.name ORDER BY "viewCount" DESC LIMIT 10 `, params); @@ -235,13 +235,13 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = { // Most starred documents const mostStarred = await pgClient.query(` SELECT - d.id, d.title, d.emoji, c.name as "collectionName", + d.id, d.title, d.icon, c.name as "collectionName", COUNT(s.id) as "starCount" FROM documents d LEFT JOIN stars s ON s."documentId" = d.id LEFT JOIN collections c ON d."collectionId" = c.id WHERE d."deletedAt" IS NULL ${collectionCondition} - GROUP BY d.id, d.title, d.emoji, c.name + GROUP BY d.id, d.title, d.icon, c.name HAVING COUNT(s.id) > 0 ORDER BY "starCount" DESC LIMIT 10 @@ -250,7 +250,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = { // Stale documents (not updated in 90 days) const staleDocuments = await pgClient.query(` SELECT - d.id, d.title, d.emoji, c.name as "collectionName", + d.id, d.title, d.icon, c.name as "collectionName", d."updatedAt", EXTRACT(DAY FROM NOW() - d."updatedAt") as "daysSinceUpdate" FROM documents d @@ -267,7 +267,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = { // Documents without views const neverViewed = await pgClient.query(` SELECT - d.id, d.title, d.emoji, c.name as "collectionName", + d.id, d.title, d.icon, c.name as "collectionName", d."createdAt" FROM documents d LEFT JOIN views v ON v."documentId" = d.id diff --git a/src/tools/collections.ts b/src/tools/collections.ts index f94ef12..1a47c16 100644 --- a/src/tools/collections.ts +++ b/src/tools/collections.ts @@ -583,7 +583,7 @@ export const collectionsTools: BaseTool[] = [ d.id, d."urlId", d.title, - d.emoji, + d.icon, d."collectionId", d."parentDocumentId", d.template, @@ -1148,7 +1148,7 @@ export const collectionsTools: BaseTool[] = [ SELECT d.id, d.title, - d.emoji, + d.icon, d.text, d."createdAt", d."updatedAt", @@ -1171,7 +1171,7 @@ export const collectionsTools: BaseTool[] = [ const exports = documentsResult.rows.map(doc => { const markdown = `--- title: ${doc.title} -emoji: ${doc.emoji || ''} +icon: ${doc.icon || ''} author: ${doc.authorName} created: ${doc.createdAt} updated: ${doc.updatedAt} @@ -1260,7 +1260,7 @@ ${doc.text || ''} SELECT d.id, d.title, - d.emoji, + d.icon, d.text, d."createdAt", d."updatedAt", @@ -1282,7 +1282,7 @@ ${doc.text || ''} const documents = documentsResult.rows.map(doc => { const markdown = `--- title: ${doc.title} -emoji: ${doc.emoji || ''} +icon: ${doc.icon || ''} author: ${doc.authorName} created: ${doc.createdAt} updated: ${doc.updatedAt} diff --git a/src/tools/documents.ts b/src/tools/documents.ts index 1ae1a34..403864b 100644 --- a/src/tools/documents.ts +++ b/src/tools/documents.ts @@ -34,7 +34,7 @@ const listDocuments: BaseTool = { const direction = validateSortDirection(args.direction); let query = ` - SELECT d.id, d."urlId", d.title, d.text, d.emoji, + SELECT d.id, d."urlId", d.title, d.text, d.icon, d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById", d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d.template, d."templateId", d."fullWidth", d.version, @@ -125,7 +125,7 @@ const getDocument: BaseTool = { } let query = ` - SELECT d.id, d."urlId", d.title, d.text, d.emoji, + SELECT d.id, d."urlId", d.title, d.text, d.icon, d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById", d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d."deletedAt", d.template, d."templateId", d."fullWidth", d.version, diff --git a/src/tools/export-import.ts b/src/tools/export-import.ts index 23dd659..9b13caa 100644 --- a/src/tools/export-import.ts +++ b/src/tools/export-import.ts @@ -21,7 +21,7 @@ interface ImportMarkdownArgs { title: string; content: string; parent_path?: string; - emoji?: string; + icon?: string; }>; create_hierarchy?: boolean; } @@ -62,7 +62,7 @@ const exportCollectionToMarkdown: BaseTool = { const documents = await pgClient.query(` WITH RECURSIVE doc_tree AS ( SELECT - d.id, d.title, d.text, d.emoji, d."parentDocumentId", + d.id, d.title, d.text, d.icon, d."parentDocumentId", d."createdAt", d."updatedAt", d."publishedAt", u.name as "authorName", 0 as depth, @@ -77,7 +77,7 @@ const exportCollectionToMarkdown: BaseTool = { UNION ALL SELECT - d.id, d.title, d.text, d.emoji, d."parentDocumentId", + d.id, d.title, d.text, d.icon, d."parentDocumentId", d."createdAt", d."updatedAt", d."publishedAt", u.name as "authorName", dt.depth + 1, @@ -111,7 +111,7 @@ const exportCollectionToMarkdown: BaseTool = { if (includeMetadata) { content += '---\n'; content += `title: "${doc.title.replace(/"/g, '\\"')}"\n`; - if (doc.emoji) content += `emoji: "${doc.emoji}"\n`; + if (doc.icon) content += `icon: "${doc.icon}"\n`; content += `author: "${doc.authorName || 'Unknown'}"\n`; content += `created: ${doc.createdAt}\n`; content += `updated: ${doc.updatedAt}\n`; @@ -122,7 +122,7 @@ const exportCollectionToMarkdown: BaseTool = { // Add title as H1 if not already in content if (!doc.text?.startsWith('# ')) { - content += `# ${doc.emoji ? doc.emoji + ' ' : ''}${doc.title}\n\n`; + content += `# ${doc.icon ? doc.icon + ' ' : ''}${doc.title}\n\n`; } content += doc.text || ''; @@ -171,7 +171,7 @@ const importMarkdownFolder: BaseTool = { title: { type: 'string', description: 'Document title' }, content: { type: 'string', description: 'Markdown content' }, parent_path: { type: 'string', description: 'Parent document path (e.g., "parent/child")' }, - emoji: { type: 'string', description: 'Document emoji' }, + icon: { type: 'string', description: 'Document icon' }, }, required: ['title', 'content'], }, @@ -256,7 +256,7 @@ const importMarkdownFolder: BaseTool = { // Create document const result = await client.query(` INSERT INTO documents ( - id, title, text, emoji, "collectionId", "teamId", "parentDocumentId", + id, title, text, icon, "collectionId", "teamId", "parentDocumentId", "createdById", "lastModifiedById", template, "createdAt", "updatedAt" ) VALUES ( @@ -266,7 +266,7 @@ const importMarkdownFolder: BaseTool = { `, [ sanitizeInput(doc.title), content, - doc.emoji || null, + doc.icon || null, args.collection_id, teamId, parentDocumentId, diff --git a/src/tools/revisions.ts b/src/tools/revisions.ts index 906d80c..a7f79f6 100644 --- a/src/tools/revisions.ts +++ b/src/tools/revisions.ts @@ -54,7 +54,7 @@ const listRevisions: BaseTool = { r.version, r."editorVersion", r.title, - r.emoji, + r.icon, r."documentId", r."userId", r."createdAt", @@ -127,7 +127,7 @@ const getRevision: BaseTool = { r."editorVersion", r.title, r.text, - r.emoji, + r.icon, r."documentId", r."userId", r."createdAt", @@ -211,7 +211,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { r.version, r.title, r.text, - r.emoji, + r.icon, r."documentId", r."createdAt", u.name as "createdByName" @@ -236,7 +236,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { r.version, r.title, r.text, - r.emoji, + r.icon, r."documentId", r."createdAt", u.name as "createdByName" @@ -263,7 +263,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { d.id, d.title, d.text, - d.emoji, + d.icon, d."updatedAt" as "createdAt", u.name as "createdByName" FROM documents d @@ -285,7 +285,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { // Calculate basic diff statistics const textLengthDiff = revision2.text.length - revision1.text.length; const titleChanged = revision1.title !== revision2.title; - const emojiChanged = revision1.emoji !== revision2.emoji; + const iconChanged = revision1.icon !== revision2.icon; return { content: [ @@ -298,7 +298,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { version: revision1.version, title: revision1.title, text: revision1.text, - emoji: revision1.emoji, + icon: revision1.icon, createdAt: revision1.createdAt, createdByName: revision1.createdByName, }, @@ -307,13 +307,13 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = { version: revision2.version, title: revision2.title, text: revision2.text, - emoji: revision2.emoji, + icon: revision2.icon, createdAt: revision2.createdAt, createdByName: revision2.createdByName, }, comparison: { titleChanged, - emojiChanged, + iconChanged, textLengthDiff, textLengthDiffPercent: ((textLengthDiff / revision1.text.length) * 100).toFixed(2) + '%', }, diff --git a/src/tools/templates.ts b/src/tools/templates.ts index b6810c7..f52249c 100644 --- a/src/tools/templates.ts +++ b/src/tools/templates.ts @@ -40,7 +40,7 @@ const listTemplates: BaseTool = { const result = await pgClient.query(` SELECT - d.id, d.title, d.emoji, d."collectionId", d."createdById", + d.id, d.title, d.icon, d."collectionId", d."createdById", d."createdAt", d."updatedAt", c.name as "collectionName", u.name as "createdByName", @@ -131,7 +131,7 @@ const createFromTemplate: BaseTool<{ template_id: string; title: string; collect // Create document from template const result = await pgClient.query(` INSERT INTO documents ( - id, title, text, emoji, "collectionId", "teamId", "parentDocumentId", + id, title, text, icon, "collectionId", "teamId", "parentDocumentId", "templateId", "createdById", "lastModifiedById", template, "createdAt", "updatedAt" ) @@ -142,7 +142,7 @@ const createFromTemplate: BaseTool<{ template_id: string; title: string; collect `, [ sanitizeInput(args.title), t.text, - t.emoji, + t.icon, args.collection_id || t.collectionId, t.teamId, args.parent_document_id || null,