@codervisor/devlog-mcp
Version:
MCP server for managing development logs and working notes
264 lines (263 loc) • 9.56 kB
JavaScript
/**
* 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);
}
}
}
}