UNPKG

@chunkydotdev/bldbl-mcp

Version:

Official MCP client for Buildable - AI-powered development platform that makes any project buildable

315 lines 12.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BuildableMCPClient = void 0; exports.createBuildableClient = createBuildableClient; const axios_1 = __importDefault(require("axios")); const uuid_1 = require("uuid"); class BuildableMCPClient { constructor(config, options = {}) { this.config = config; this.options = { retryAttempts: 3, retryDelay: 1000, enableRealTimeUpdates: false, logLevel: 'info', ...options, }; // Generate or use provided AI assistant ID this.aiAssistantId = config.aiAssistantId || `ai_${(0, uuid_1.v4)().substring(0, 8)}`; // Create axios instance with authentication this.axios = axios_1.default.create({ baseURL: config.apiUrl, timeout: config.timeout || 30000, headers: { Authorization: `Bearer ${config.apiKey}`, 'X-AI-Assistant-ID': this.aiAssistantId, 'Content-Type': 'application/json', 'User-Agent': '@bldbl/mcp/1.0.0', }, }); // Setup response interceptor for error handling this.axios.interceptors.response.use((response) => response, (error) => { this.log('error', 'API request failed:', error.message); return Promise.reject(this.formatError(error)); }); this.log('info', `Buildable MCP Client initialized for project ${config.projectId}`); } /** * Get complete project context including plan, tasks, and recent activity */ async getProjectContext() { this.log('debug', 'Fetching project context...'); try { const response = await this.makeRequest('GET', `/projects/${this.config.projectId}/context`); this.log('info', 'Successfully retrieved project context'); return response.data; } catch (error) { this.log('error', 'Failed to get project context:', error); throw error; } } /** * Get the next recommended task to work on */ async getNextTask() { this.log('debug', 'Getting next recommended task...'); try { const response = await this.makeRequest('GET', `/projects/${this.config.projectId}/next-task`); if (response.data?.task) { this.log('info', `Next task: "${response.data.task.title}"`); } else { this.log('info', 'No tasks available:', response.data?.message); } return response.data; } catch (error) { this.log('error', 'Failed to get next task:', error); throw error; } } /** * Start working on a specific task */ async startTask(taskId, options = {}) { this.log('debug', `Starting task ${taskId}...`); try { const response = await this.makeRequest('POST', `/tasks/${taskId}/start`, { ai_assistant_id: this.aiAssistantId, estimated_time_minutes: options.estimated_duration, notes: options.notes, approach: options.approach, }); this.log('info', `Successfully started task ${taskId}`); // Update connection status to 'working' await this.updateConnectionStatus('working', taskId); return response.data; } catch (error) { this.log('error', `Failed to start task ${taskId}:`, error); throw error; } } /** * Update progress on the current task */ async updateProgress(taskId, progress) { this.log('debug', `Updating progress for task ${taskId}: ${progress.progress}%`); try { const response = await this.makeRequest('POST', `/tasks/${taskId}/progress`, { completion_percentage: progress.progress, files_created: progress.files_modified, files_modified: progress.files_modified, notes: progress.notes, blockers: progress.challenges, time_spent_minutes: progress.time_spent, current_step: progress.current_step, completed_steps: progress.completed_steps, }); this.log('info', `Progress updated: ${progress.progress}% complete`); // Update connection activity await this.updateConnectionStatus('working', taskId); return response.data; } catch (error) { this.log('error', `Failed to update progress for task ${taskId}:`, error); throw error; } } /** * Complete a task */ async completeTask(taskId, completion) { this.log('debug', `Completing task ${taskId}...`); try { const response = await this.makeRequest('POST', `/tasks/${taskId}/complete`, { files_created: completion.files_modified, files_modified: completion.files_modified, completion_notes: completion.completion_notes, time_spent_minutes: completion.time_spent, verification_evidence: completion.testing_completed ? 'Tests passed' : undefined, }); this.log('info', `Successfully completed task ${taskId}`); // Update connection status back to 'connected' await this.updateConnectionStatus('connected'); return response.data; } catch (error) { this.log('error', `Failed to complete task ${taskId}:`, error); throw error; } } /** * Create a discussion/question for human input */ async createDiscussion(discussion) { this.log('debug', `Creating discussion: "${discussion.topic}"`); try { const response = await this.makeRequest('POST', `/projects/${this.config.projectId}/discuss`, { type: 'question', title: discussion.topic, message: discussion.message, context: { task_id: discussion.context?.current_task_id, relevant_files: discussion.context?.related_files, specific_challenge: discussion.context?.specific_challenge, urgency: discussion.context?.urgency || 'medium', }, urgency: discussion.context?.urgency || 'medium', requires_human_response: true, created_by: this.aiAssistantId, }); this.log('info', `Discussion created: ${response.data?.discussion_id}`); return response.data; } catch (error) { this.log('error', 'Failed to create discussion:', error); throw error; } } /** * Check health/connectivity with Buildable API */ async healthCheck() { try { const response = await this.makeRequest('GET', '/health'); this.log('debug', 'Health check passed'); return response.data; } catch (error) { this.log('error', 'Health check failed:', error); throw error; } } /** * Connect to Buildable (create AI connection record) */ async connect() { this.log('info', 'Connecting to Buildable...'); try { await this.updateConnectionStatus('connected'); this.log('info', 'Successfully connected'); } catch (error) { this.log('warn', 'Failed to update connection status:', error); // Don't throw - connection status is non-critical } } /** * Disconnect from Buildable (cleanup) */ async disconnect() { this.log('info', 'Disconnecting from Buildable...'); try { await this.updateConnectionStatus('disconnected'); this.log('info', 'Successfully disconnected'); } catch (error) { this.log('warn', 'Failed to update disconnect status:', error); // Don't throw - disconnection should always succeed } } /** * Get current AI assistant connection status */ async getConnectionStatus() { try { const response = await this.axios.get(`/projects/${this.config.projectId}/ai-connections`); const connections = (response.data.connections || []); const myConnection = connections.find((conn) => conn.ai_assistant_id === this.aiAssistantId); return (myConnection || { status: 'disconnected', connected_at: '', last_activity_at: '', }); } catch (error) { this.log('warn', 'Failed to get connection status:', error); return { status: 'unknown', connected_at: '', last_activity_at: '' }; } } // Private helper methods async makeRequest(method, url, data) { const startTime = Date.now(); try { const response = await this.axios.request({ method, url, data, }); const duration = Date.now() - startTime; this.log('debug', `${method} ${url} completed in ${duration}ms`); return { success: true, data: response.data, timestamp: new Date().toISOString(), }; } catch (error) { const duration = Date.now() - startTime; this.log('error', `${method} ${url} failed after ${duration}ms`); throw error; } } async updateConnectionStatus(status, currentTaskId) { try { // This endpoint doesn't exist yet, but it's for internal connection tracking await this.axios.post('/internal/ai-connections', { ai_assistant_id: this.aiAssistantId, status, current_task_id: currentTaskId, metadata: { client_version: '1.0.0', capabilities: ['task_management', 'progress_tracking', 'discussions'], last_activity: new Date().toISOString(), }, }); } catch (error) { // Connection status updates are non-critical this.log('debug', 'Connection status update failed (non-critical):', error); } } formatError(error) { const apiError = { error: error.message || 'Unknown API error', timestamp: new Date().toISOString(), }; if (error.response?.data) { const responseData = error.response.data; apiError.error = responseData.error || responseData.message || apiError.error; apiError.code = responseData.code; apiError.details = responseData.details; } return apiError; } log( // eslint-disable-next-line @typescript-eslint/no-unused-vars _level, // eslint-disable-next-line @typescript-eslint/no-unused-vars _message, // eslint-disable-next-line @typescript-eslint/no-unused-vars ..._args) { // Disable all console output in MCP mode to prevent JSON-RPC pollution // MCP servers should not output to stdout/stderr as it interferes with the protocol return; } } exports.BuildableMCPClient = BuildableMCPClient; // Export default instance creator function createBuildableClient(config, options) { return new BuildableMCPClient(config, options); } // Export for convenience exports.default = BuildableMCPClient; //# sourceMappingURL=client.js.map