Skip to main content
Glama
client.tsโ€ข12.4 kB
import axios, { type AxiosError, type AxiosInstance } from 'axios'; import { v4 as uuidv4 } from 'uuid'; import type { APIError, BuildableConfig, ClientOptions, CompleteTaskRequest, CompleteTaskResponse, CreateDiscussionRequest, DiscussionResponse, MCPResponse, NextTaskResponse, ProgressResponse, ProgressUpdate, ProjectContext, StartTaskOptions, StartTaskResponse, } from './types'; interface ConnectionData { ai_assistant_id: string; status: string; connected_at: string; last_activity_at: string; } export class BuildableMCPClient { private axios: AxiosInstance; private config: BuildableConfig; private options: ClientOptions; private aiAssistantId: string; constructor(config: BuildableConfig, options: ClientOptions = {}) { 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_${uuidv4().substring(0, 8)}`; // Create axios instance with authentication this.axios = axios.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: AxiosError) => { 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(): Promise<ProjectContext> { this.log('debug', 'Fetching project context...'); try { const response = await this.makeRequest<ProjectContext>( '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(): Promise<NextTaskResponse> { this.log('debug', 'Getting next recommended task...'); try { const response = await this.makeRequest<NextTaskResponse>( '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: string, options: StartTaskOptions = {} ): Promise<StartTaskResponse> { this.log('debug', `Starting task ${taskId}...`); try { const response = await this.makeRequest<StartTaskResponse>( '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: string, progress: ProgressUpdate ): Promise<ProgressResponse> { this.log( 'debug', `Updating progress for task ${taskId}: ${progress.progress}%` ); try { const response = await this.makeRequest<ProgressResponse>( '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: string, completion: CompleteTaskRequest ): Promise<CompleteTaskResponse> { this.log('debug', `Completing task ${taskId}...`); try { const response = await this.makeRequest<CompleteTaskResponse>( '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: CreateDiscussionRequest ): Promise<DiscussionResponse> { this.log('debug', `Creating discussion: "${discussion.topic}"`); try { const response = await this.makeRequest<DiscussionResponse>( '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(): Promise<{ status: string; timestamp: string }> { try { const response = await this.makeRequest<{ status: string; timestamp: string; }>('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(): Promise<void> { 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(): Promise<void> { 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(): Promise<{ status: string; connected_at: string; last_activity_at: string; }> { try { const response = await this.axios.get( `/projects/${this.config.projectId}/ai-connections` ); const connections = (response.data.connections || []) as ConnectionData[]; 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 private async makeRequest<T>( method: 'GET' | 'POST' | 'PUT' | 'DELETE', url: string, data?: Record<string, unknown> ): Promise<MCPResponse<T>> { 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; } } private async updateConnectionStatus( status: 'connected' | 'working' | 'disconnected', currentTaskId?: string ): Promise<void> { 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 ); } } private formatError(error: AxiosError): APIError { const apiError: APIError = { error: error.message || 'Unknown API error', timestamp: new Date().toISOString(), }; if (error.response?.data) { const responseData = error.response.data as Record<string, unknown>; apiError.error = (responseData.error as string) || (responseData.message as string) || apiError.error; apiError.code = responseData.code as string; apiError.details = responseData.details as Record<string, unknown>; } return apiError; } private log( // eslint-disable-next-line @typescript-eslint/no-unused-vars _level: 'debug' | 'info' | 'warn' | 'error', // eslint-disable-next-line @typescript-eslint/no-unused-vars _message: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars ..._args: unknown[] ): void { // 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; } } // Export default instance creator export function createBuildableClient( config: BuildableConfig, options?: ClientOptions ): BuildableMCPClient { return new BuildableMCPClient(config, options); } // Export for convenience export default BuildableMCPClient;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chunkydotdev/bldbl-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server