UNPKG

@shirokuma-library/mcp-knowledge-base

Version:

Shirokuma MCP Server for comprehensive knowledge management including issues, plans, documents, and work sessions. All stored data is structured for AI processing, not human readability.

184 lines (183 loc) 8.41 kB
import { z } from 'zod'; export class HandlerPatterns { static createCrudHandlers(handler, entityName, operations, schemas, formatters) { return { handleList: handler.wrapHandler(`list ${entityName}s`, z.object({}), async () => { handler.ensureDatabase(); const items = await operations.getAll(); if (items.length === 0) { return handler.createResponse(`## ${entityName}s\n\nNo ${entityName.toLowerCase()}s found.`); } if (formatters?.list) { return handler.createResponse(formatters.list(items)); } const markdown = [ `## ${entityName}s`, '', `Found ${items.length} ${entityName.toLowerCase()}${items.length === 1 ? '' : 's'}:`, '', ...items.map(item => `- **${item.title}** (ID: ${item.id})`) ].join('\n'); return handler.createResponse(markdown); }), handleGetById: handler.wrapHandler(`get ${entityName}`, z.object({ id: z.union([z.string(), z.number()]) }), async (args) => { handler.ensureDatabase(); const item = await operations.getById(args.id); if (!item) { return handler.createErrorResponse(`${entityName} with ID ${args.id} not found`); } if (formatters?.detail) { return handler.createResponse(formatters.detail(item)); } return handler.createResponse(handler.formatJson(item)); }), handleCreate: handler.wrapHandler(`create ${entityName}`, schemas.create, async (args) => { handler.ensureDatabase(); const created = await operations.create(args); return handler.createResponse(`## ${entityName} Created\n\n` + `Successfully created ${entityName.toLowerCase()} "${created.title}" with ID ${created.id}`); }), handleUpdate: handler.wrapHandler(`update ${entityName}`, schemas.update, async (args) => { handler.ensureDatabase(); const updated = await operations.update(args.id, args); if (!updated) { return handler.createErrorResponse(`${entityName} with ID ${args.id} not found`); } return handler.createResponse(`## ${entityName} Updated\n\n` + `Successfully updated ${entityName.toLowerCase()} "${updated.title}"`); }), handleDelete: handler.wrapHandler(`delete ${entityName}`, schemas.delete, async (args) => { handler.ensureDatabase(); const deleted = await operations.delete(args.id); if (!deleted) { return handler.createErrorResponse(`${entityName} with ID ${args.id} not found or cannot be deleted`); } return handler.createResponse(`## ${entityName} Deleted\n\n` + `Successfully deleted ${entityName.toLowerCase()} with ID ${args.id}`); }) }; } static createSearchHandler(handler, entityName, searchOperation, schema, formatter) { return handler.wrapHandler(`search ${entityName}s`, schema, async (args) => { handler.ensureDatabase(); const results = await searchOperation(args.query, args); if (results.length === 0) { return handler.createResponse(`## Search Results\n\nNo ${entityName.toLowerCase()}s found matching your search.`); } if (formatter) { return handler.createResponse(formatter(results)); } const markdown = [ '## Search Results', '', `Found ${results.length} ${entityName.toLowerCase()}${results.length === 1 ? '' : 's'}:`, '', ...results.map(item => `- **${item.title}** (ID: ${item.id})`) ].join('\n'); return handler.createResponse(markdown); }); } static createTagSearchHandler(handler, entityName, searchByTag, formatter) { return handler.wrapHandler(`search ${entityName}s by tag`, z.object({ tag: z.string() }), async (args) => { handler.ensureDatabase(); const results = await searchByTag(args.tag); if (results.length === 0) { return handler.createResponse(`## ${entityName}s with tag "${args.tag}"\n\n` + `No ${entityName.toLowerCase()}s found with this tag.`); } return handler.createResponse(formatter(results)); }); } static createBatchHandler(handler, operationName, batchOperation, schema, formatter) { return handler.wrapHandler(operationName, schema, async (args) => { handler.ensureDatabase(); if (!args.items || args.items.length === 0) { return handler.createErrorResponse('No items provided for batch operation'); } const results = await batchOperation(args.items); return handler.createResponse(formatter(results)); }); } static createDateRangeHandler(handler, entityName, operation, formatter) { return handler.wrapHandler(`get ${entityName}s by date`, z.object({ start_date: z.string().optional(), end_date: z.string().optional() }).refine(data => { if (data.start_date && data.end_date) { return new Date(data.start_date) <= new Date(data.end_date); } return true; }, { message: 'Start date must be before end date' }), async (args) => { handler.ensureDatabase(); const items = await operation(args.start_date, args.end_date); if (items.length === 0) { const dateRange = this.formatDateRange(args.start_date, args.end_date); return handler.createResponse(`## ${entityName}s ${dateRange}\n\n` + `No ${entityName.toLowerCase()}s found in this date range.`); } return handler.createResponse(formatter(items)); }); } static formatDateRange(startDate, endDate) { if (startDate && endDate) { return `from ${startDate} to ${endDate}`; } else if (startDate) { return `from ${startDate}`; } else if (endDate) { return `until ${endDate}`; } else { return ''; } } } export class ResponsePatterns { static formatAsTable(items, columns, title) { const lines = []; if (title) { lines.push(`## ${title}`, ''); } const headers = columns.map(col => col.header); lines.push(`| ${headers.join(' | ')} |`); const separators = columns.map(() => '---'); lines.push(`| ${separators.join(' | ')} |`); for (const item of items) { const values = columns.map(col => { const value = item[col.key]; return col.formatter ? col.formatter(value) : String(value ?? ''); }); lines.push(`| ${values.join(' | ')} |`); } return lines.join('\n'); } static formatGroupedList(items, groupBy, itemFormatter, title) { const groups = new Map(); for (const item of items) { const key = item[groupBy]; if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(item); } const lines = []; if (title) { lines.push(`## ${title}`, ''); } for (const [groupKey, groupItems] of groups) { lines.push(`### ${groupKey}`, ''); lines.push(...groupItems.map(itemFormatter)); lines.push(''); } return lines.join('\n'); } static formatStats(stats, title = 'Statistics') { const lines = [ `## ${title}`, '', ...Object.entries(stats).map(([key, value]) => `- **${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:** ${value}`) ]; return lines.join('\n'); } }