UNPKG

@codervisor/devlog-mcp

Version:

MCP server for managing development logs and working notes

424 lines (423 loc) โ€ข 15.9 kB
/** * MCP Adapter using HTTP API client * Simplified version that only uses devlog operations through the web API */ import { DevlogApiClient } from '../api/devlog-api-client.js'; /** * MCP Adapter that communicates through HTTP API instead of direct core access */ export class MCPApiAdapter { apiClient; currentProjectId; initialized = false; constructor(config) { this.apiClient = new DevlogApiClient(config.apiClient); this.currentProjectId = config.defaultProjectId || 0; if (this.currentProjectId) { this.apiClient.setCurrentProject(this.currentProjectId); } } /** * Initialize the adapter and test connection */ async initialize() { if (this.initialized) return; try { // Test connection to the API await this.apiClient.healthCheck(); console.log('โœ… MCP API adapter initialized successfully'); this.initialized = true; } catch (error) { console.error('โŒ Failed to initialize MCP API adapter:', error); throw new Error(`Failed to connect to devlog API: ${error instanceof Error ? error.message : String(error)}`); } } /** * Set the current project ID */ setCurrentProjectId(projectId) { this.currentProjectId = projectId; this.apiClient.setCurrentProject(projectId); } /** * Get the current project ID */ getCurrentProjectId() { return this.currentProjectId; } /** * Get the underlying API client (for project tools) */ get manager() { return this.apiClient; } // Devlog Operations async createDevlog(args) { await this.ensureInitialized(); try { const createRequest = { title: args.title, type: args.type, description: args.description, priority: args.priority, businessContext: args.businessContext, technicalContext: args.technicalContext, acceptanceCriteria: args.acceptanceCriteria, }; const entry = await this.apiClient.createDevlog(createRequest); return { content: [ { type: 'text', text: `Created devlog entry: ${entry.id}\nTitle: ${entry.title}\nType: ${entry.type}\nPriority: ${entry.priority}\nStatus: ${entry.status}`, }, ], }; } catch (error) { return this.handleError('Failed to create devlog', error); } } async updateDevlog(args) { await this.ensureInitialized(); try { const updateRequest = { status: args.status, priority: args.priority, businessContext: args.businessContext, technicalContext: args.technicalContext, acceptanceCriteria: args.acceptanceCriteria, }; const entry = await this.apiClient.updateDevlog(args.id, updateRequest); return { content: [ { type: 'text', text: `Updated devlog entry: ${entry.id}\nTitle: ${entry.title}\nStatus: ${entry.status}\nLast Updated: ${entry.updatedAt}`, }, ], }; } catch (error) { return this.handleError('Failed to update devlog', error); } } async getDevlog(args) { await this.ensureInitialized(); try { const entry = await this.apiClient.getDevlog(args.id); return { content: [ { type: 'text', text: JSON.stringify(entry, null, 2), }, ], }; } catch (error) { return this.handleError(`Failed to get devlog ${args.id}`, error); } } async listDevlogs(args = {}) { await this.ensureInitialized(); try { const filter = { status: args.status ? [args.status] : undefined, type: args.type ? [args.type] : undefined, priority: args.priority ? [args.priority] : undefined, archived: args.archived, pagination: args.page || args.limit || args.sortBy ? { page: args.page, limit: args.limit, sortBy: args.sortBy, sortOrder: args.sortOrder, } : undefined, }; const result = await this.apiClient.listDevlogs(filter); const entries = result.items; if (entries.length === 0) { return { content: [ { type: 'text', text: 'No devlog entries found matching the criteria.', }, ], }; } const summary = entries .map((entry) => `- [${entry.status}] ${entry.title} (${entry.type}, ${entry.priority}) - ${entry.id}`) .join('\n'); let resultText = `Found ${entries.length} devlog entries`; if (result.pagination) { resultText += ` (page ${result.pagination.page} of ${result.pagination.totalPages}, ${result.pagination.total} total)`; } resultText += `:\n\n${summary}`; return { content: [ { type: 'text', text: resultText, }, ], }; } catch (error) { return this.handleError('Failed to list devlogs', error); } } async searchDevlogs(args) { await this.ensureInitialized(); try { const filter = { status: args.status ? [args.status] : undefined, type: args.type ? [args.type] : undefined, priority: args.priority ? [args.priority] : undefined, archived: args.archived, }; const result = await this.apiClient.searchDevlogs(args.query, filter); const entries = result.items; if (entries.length === 0) { return { content: [ { type: 'text', text: `No devlog entries found matching query: "${args.query}"`, }, ], }; } const summary = entries .map((entry) => `- [${entry.status}] ${entry.title} (${entry.type}, ${entry.priority}) - ${entry.id}`) .join('\n'); return { content: [ { type: 'text', text: `Found ${entries.length} devlog entries matching "${args.query}":\n\n${summary}`, }, ], }; } catch (error) { return this.handleError('Failed to search devlogs', error); } } async addDevlogNote(args) { await this.ensureInitialized(); try { const entry = await this.apiClient.addDevlogNote(args.id, args.note, args.category, args.files, args.codeChanges); return { content: [ { type: 'text', text: `Added ${args.category || 'progress'} note to devlog '${entry.id}':\n${args.note}\n\nTotal notes: ${entry.notes?.length || 0}`, }, ], }; } catch (error) { return this.handleError('Failed to add devlog note', error); } } async updateDevlogWithNote(args) { await this.ensureInitialized(); try { // First update the devlog fields if provided if (args.status || args.priority) { const updateRequest = { status: args.status, priority: args.priority, }; await this.apiClient.updateDevlog(args.id, updateRequest); } // Then add the note const entry = await this.apiClient.addDevlogNote(args.id, args.note, args.category, args.files, args.codeChanges); return { content: [ { type: 'text', text: `Updated devlog '${entry.id}' and added ${args.category || 'progress'} note:\n${args.note}\n\nStatus: ${entry.status}\nTotal notes: ${entry.notes?.length || 0}`, }, ], }; } catch (error) { return this.handleError('Failed to update devlog with note', error); } } async completeDevlog(args) { await this.ensureInitialized(); try { // Update status to done const updateRequest = { status: 'done', // Note: closedAt is not part of UpdateDevlogRequest interface // This should be handled by the API implementation }; const entry = await this.apiClient.updateDevlog(args.id, updateRequest); // Add completion note if provided (skip for now since notes API doesn't exist) // TODO: Re-enable when notes API is implemented // if (args.summary) { // await this.apiClient.addDevlogNote(args.id, `Completed: ${args.summary}`, 'progress'); // } return { content: [ { type: 'text', text: `Completed devlog '${entry.title}' (ID: ${entry.id})${args.summary ? ` with summary: ${args.summary}` : ''}`, }, ], }; } catch (error) { return this.handleError('Failed to complete devlog', error); } } async closeDevlog(args) { await this.ensureInitialized(); try { // Update status to cancelled const updateRequest = { status: 'cancelled', // Note: closedAt is not part of UpdateDevlogRequest interface // This should be handled by the API implementation }; const entry = await this.apiClient.updateDevlog(args.id, updateRequest); // Add closure note if provided (skip for now since notes API doesn't exist) // TODO: Re-enable when notes API is implemented // if (args.reason) { // await this.apiClient.addDevlogNote(args.id, `Closed: ${args.reason}`, 'feedback'); // } return { content: [ { type: 'text', text: `Closed devlog '${entry.id}': ${entry.title}\nStatus: ${entry.status}\nReason: ${args.reason || 'None provided'}`, }, ], }; } catch (error) { return this.handleError('Failed to close devlog', error); } } async archiveDevlog(args) { await this.ensureInitialized(); try { const entry = await this.apiClient.archiveDevlog(args.id); return { content: [ { type: 'text', text: `Archived devlog '${entry.id}': ${entry.title}`, }, ], }; } catch (error) { return this.handleError('Failed to archive devlog', error); } } async unarchiveDevlog(args) { await this.ensureInitialized(); try { const entry = await this.apiClient.unarchiveDevlog(args.id); return { content: [ { type: 'text', text: `Unarchived devlog '${entry.id}': ${entry.title}`, }, ], }; } catch (error) { return this.handleError('Failed to unarchive devlog', error); } } async discoverRelatedDevlogs(args) { await this.ensureInitialized(); try { // Use search to find potentially related entries const searchTerms = [args.workDescription, ...(args.keywords || []), args.scope || ''] .filter(Boolean) .join(' '); const result = await this.apiClient.searchDevlogs(searchTerms); const entries = result.items; if (entries.length === 0) { return { content: [ { type: 'text', text: `No related devlog entries found for:\nWork: ${args.workDescription}\nType: ${args.workType}\n\nโœ… Safe to create a new devlog entry - no overlapping work detected.`, }, ], }; } const analysis = entries .slice(0, 10) .map((entry) => { const statusEmoji = { new: '๐Ÿ†•', 'in-progress': '๐Ÿ”„', blocked: '๐Ÿšซ', 'in-review': '๐Ÿ‘€', testing: '๐Ÿงช', done: 'โœ…', cancelled: '๐Ÿ“ฆ', }; return (`${statusEmoji[entry.status] || '๐Ÿ“„'} **${entry.title}** (${entry.type})\n` + ` ID: ${entry.id}\n` + ` Status: ${entry.status} | Priority: ${entry.priority}\n` + ` Description: ${entry.description.substring(0, 150)}${entry.description.length > 150 ? '...' : ''}\n` + ` Last Updated: ${new Date(entry.updatedAt).toLocaleDateString()}\n`); }) .join('\n'); return { content: [ { type: 'text', text: `## Discovery Analysis for: "${args.workDescription}"\n\n` + `**Search Parameters:**\n` + `- Type: ${args.workType}\n` + `- Keywords: ${args.keywords?.join(', ') || 'None'}\n` + `- Scope: ${args.scope || 'Not specified'}\n\n` + `**Found ${entries.length} potentially related entries:**\n\n${analysis}\n\n` + `โš ๏ธ RECOMMENDATION: Review related entries before creating new work to avoid duplication.`, }, ], }; } catch (error) { return this.handleError('Failed to discover related devlogs', error); } } async dispose() { this.initialized = false; } // Helper methods async ensureInitialized() { if (!this.initialized) { await this.initialize(); } } handleError(message, error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`${message}:`, errorMessage); return { content: [ { type: 'text', text: `${message}: ${errorMessage}`, }, ], isError: true, }; } }