UNPKG

@codervisor/devlog-mcp

Version:

MCP server for managing development logs and working notes

264 lines (263 loc) 9.56 kB
/** * HTTP API client for devlog operations * Provides project-aware interface to @codervisor/devlog-web API endpoints */ /** * Custom error class for API client errors */ export class DevlogApiClientError extends Error { statusCode; response; constructor(message, statusCode, response) { super(message); this.statusCode = statusCode; this.response = response; this.name = 'DevlogApiClientError'; } } /** * HTTP API client for devlog operations */ export class DevlogApiClient { baseUrl; timeout; retries; currentProjectId = null; constructor(config) { this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash this.timeout = config.timeout || 30000; this.retries = config.retries || 3; } /** * Set the current project ID for all subsequent requests */ setCurrentProject(projectId) { this.currentProjectId = projectId; } /** * Get the current project ID */ getCurrentProjectId() { return this.currentProjectId; } /** * Make HTTP request with retry logic */ async makeRequest(endpoint, options = {}, attempt = 1) { const url = `${this.baseUrl}${endpoint}`; const requestOptions = { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, signal: AbortSignal.timeout(this.timeout), }; try { const response = await fetch(url, requestOptions); if (!response.ok) { let errorText = ''; try { errorText = await response.text(); } catch { errorText = response.statusText; } // Try to parse JSON error response let errorData = errorText; try { const parsed = JSON.parse(errorText); errorData = parsed.error?.message || parsed.message || errorText; } catch { // Keep original text if not JSON } throw new DevlogApiClientError(`HTTP ${response.status}: ${errorData}`, response.status, errorText); } return response; } catch (error) { if (attempt < this.retries && !(error instanceof DevlogApiClientError)) { console.warn(`Request failed (attempt ${attempt}/${this.retries}), retrying...`); await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); return this.makeRequest(endpoint, options, attempt + 1); } throw error; } } /** * Unwrap standardized API response */ unwrapApiResponse(response) { // Handle standardized API response format with success/data wrapper (projects API) if (response && typeof response === 'object' && response.success === true && 'data' in response) { return response.data; } // Handle paginated response format (devlogs list API) if (response && typeof response === 'object' && 'items' in response && 'pagination' in response) { return response; } // Handle direct response (individual devlog, etc.) return response; } /** * GET request helper */ async get(endpoint) { const response = await this.makeRequest(endpoint, { method: 'GET' }); return response.json(); } /** * POST request helper */ async post(endpoint, data) { const response = await this.makeRequest(endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }); return response.json(); } /** * PUT request helper */ async put(endpoint, data) { const response = await this.makeRequest(endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }); return response.json(); } /** * DELETE request helper */ async delete(endpoint) { const response = await this.makeRequest(endpoint, { method: 'DELETE' }); return response.json(); } /** * Get the project-aware endpoint prefix */ getProjectEndpoint() { const projectId = this.currentProjectId || 'default'; return `/api/projects/${projectId}`; } // Project Management async listProjects() { const response = await this.get('/api/projects'); return this.unwrapApiResponse(response); } async getProject(projectId) { const id = projectId || this.currentProjectId || 0; const response = await this.get(`/api/projects/${id}`); return this.unwrapApiResponse(response); } async createProject(data) { const response = await this.post('/api/projects', data); return this.unwrapApiResponse(response); } // Devlog Operations async createDevlog(data) { const response = await this.post(`${this.getProjectEndpoint()}/devlogs`, data); return this.unwrapApiResponse(response); } async getDevlog(id) { const response = await this.get(`${this.getProjectEndpoint()}/devlogs/${id}`); return this.unwrapApiResponse(response); } async updateDevlog(id, data) { const response = await this.put(`${this.getProjectEndpoint()}/devlogs/${id}`, data); return this.unwrapApiResponse(response); } async deleteDevlog(id) { const response = await this.delete(`${this.getProjectEndpoint()}/devlogs/${id}`); return this.unwrapApiResponse(response); } async listDevlogs(filter) { const params = new URLSearchParams(); if (filter) { if (filter.status?.length) params.append('status', filter.status.join(',')); if (filter.type?.length) params.append('type', filter.type.join(',')); if (filter.priority?.length) params.append('priority', filter.priority.join(',')); if (filter.archived !== undefined) params.append('archived', String(filter.archived)); if (filter.pagination?.page) params.append('page', String(filter.pagination.page)); if (filter.pagination?.limit) params.append('limit', String(filter.pagination.limit)); if (filter.pagination?.sortBy) params.append('sortBy', filter.pagination.sortBy); if (filter.pagination?.sortOrder) params.append('sortOrder', filter.pagination.sortOrder); } const query = params.toString() ? `?${params.toString()}` : ''; const response = await this.get(`${this.getProjectEndpoint()}/devlogs${query}`); return this.unwrapApiResponse(response); } async searchDevlogs(query, filter) { const params = new URLSearchParams({ search: query }); if (filter) { if (filter.status?.length) params.append('status', filter.status.join(',')); if (filter.type?.length) params.append('type', filter.type.join(',')); if (filter.priority?.length) params.append('priority', filter.priority.join(',')); if (filter.archived !== undefined) params.append('archived', String(filter.archived)); } const response = await this.get(`${this.getProjectEndpoint()}/devlogs?${params.toString()}`); return this.unwrapApiResponse(response); } async addDevlogNote(devlogId, note, category, files, codeChanges) { const response = await this.post(`${this.getProjectEndpoint()}/devlogs/${devlogId}/notes`, { note, category, files, codeChanges, }); return this.unwrapApiResponse(response); } async archiveDevlog(id) { const response = await this.put(`${this.getProjectEndpoint()}/devlogs/${id}/archive`, {}); return this.unwrapApiResponse(response); } async unarchiveDevlog(id) { const response = await this.put(`${this.getProjectEndpoint()}/devlogs/${id}/unarchive`, {}); return this.unwrapApiResponse(response); } // Health check async healthCheck() { try { const response = await this.get('/api/health'); const result = this.unwrapApiResponse(response); // Validate the health check response if (!result || typeof result !== 'object' || !result.status) { throw new Error('Invalid health check response format'); } return result; } catch (error) { // If health endpoint doesn't exist, try a basic endpoint console.warn('Health endpoint failed, trying projects endpoint as backup...'); try { await this.get('/api/projects'); return { status: 'ok', timestamp: new Date().toISOString(), }; } catch (backupError) { throw new DevlogApiClientError(`Health check failed: ${error instanceof Error ? error.message : String(error)}`, 0, error); } } } }