UNPKG

sia-vision-mcp-server

Version:

Enhanced v2 MCP server with improved error handling, validation, and comprehensive tool schemas for SIA.Vision storytelling platform

174 lines 7.45 kB
const MCP_PROTOCOL_VERSION = process.env.MCP_PROTOCOL_VERSION ?? '2024-11-05'; import axios from 'axios'; export class MCPError extends Error { code; details; constructor(code, message, details) { super(message); this.code = code; this.details = details; this.name = 'MCPError'; } } export class FirebaseClient { baseUrl; apiKey; client; retryCount = 0; maxRetries = 3; constructor(baseUrl, apiKey) { this.baseUrl = baseUrl; this.apiKey = apiKey; // Validate API key format if (!apiKey || !apiKey.startsWith('sia_')) { throw new MCPError('INVALID_API_KEY', 'API key must start with "sia_". Get your key at https://sia.vision/dashboard'); } this.client = axios.create({ baseURL: baseUrl, timeout: 30000, headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey, 'MCP-Protocol-Version': MCP_PROTOCOL_VERSION, 'User-Agent': 'sia-vision-mcp-server/1.4.0', }, }); // Add request interceptor for logging this.client.interceptors.request.use((config) => { if (process.env.DEBUG === '1') { console.error(`🔧 ${config.method?.toUpperCase()} ${config.url}`); } return config; }); // Add response interceptor for error handling this.client.interceptors.response.use((response) => response, (error) => { if (process.env.DEBUG === '1') { console.error(`❌ Request failed: ${error.message}`); } return Promise.reject(error); }); } async listTools() { try { const { data } = await this.client.get('/mcpTools'); // Validate response structure if (!data || !Array.isArray(data.tools)) { throw new MCPError('INVALID_RESPONSE', 'Invalid tools list response from server', { received: data }); } return data; } catch (error) { throw this.handleError(error, 'listTools'); } } async executeTool(toolName, args) { // Validate inputs if (!toolName || typeof toolName !== 'string') { throw new MCPError('INVALID_TOOL_NAME', 'Tool name must be a non-empty string', { toolName }); } try { const response = await this.retryRequest(async () => { return await this.client.post('/mcpExecute', { tool: toolName, arguments: args || {}, }); }); if (response.data.success === false) { throw new MCPError('TOOL_EXECUTION_FAILED', response.data.error || `Tool "${toolName}" execution failed`, { toolName, args, response: response.data }); } // Prefer MCP-shaped response passthrough if present if (response.data && Array.isArray(response.data.content)) { if (response.data.isError) { const msg = response.data.content.find((c) => c?.type === 'text')?.text || 'Tool execution failed'; throw new MCPError('TOOL_ERROR', msg, { toolName, args }); } return { content: response.data.content, isError: false }; } // Fallbacks: some endpoints return { data } or plain body if (response.data && response.data.data !== undefined) { return response.data.data; } return response.data; } catch (error) { throw this.handleError(error, 'executeTool', { toolName, args }); } } async healthCheck() { try { const response = await this.client.get('/mcpHealth', { timeout: 5000 }); return response.status === 200; } catch { return false; } } async retryRequest(requestFn) { let lastError; for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { return await requestFn(); } catch (error) { lastError = error; // Don't retry on authentication or validation errors if (axios.isAxiosError(error)) { const status = error.response?.status; if (status === 401 || status === 403 || status === 400) { throw error; } } // Don't retry on the last attempt if (attempt === this.maxRetries) { throw error; } // Exponential backoff: 1s, 2s, 4s const delay = Math.pow(2, attempt) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); if (process.env.DEBUG === '1') { console.error(`🔄 Retrying request (attempt ${attempt + 1}/${this.maxRetries})`); } } } throw lastError; } handleError(error, operation, context) { if (error instanceof MCPError) { return error; } if (axios.isAxiosError(error)) { const status = error.response?.status; const data = error.response?.data; switch (status) { case 401: return new MCPError('UNAUTHORIZED', 'Invalid API key. Generate a new one at https://sia.vision/dashboard', { operation, context }); case 403: return new MCPError('FORBIDDEN', 'Insufficient permissions for this operation', { operation, context }); case 404: return new MCPError('NOT_FOUND', 'Requested resource not found', { operation, context }); case 429: return new MCPError('RATE_LIMITED', 'Rate limit exceeded. Please wait before retrying', { operation, context }); case 500: return new MCPError('SERVER_ERROR', 'Internal server error. Please try again later', { operation, context }); default: if (data?.error) { return new MCPError('API_ERROR', data.error, { operation, context, status }); } } // Network or timeout errors if (error.code === 'ECONNREFUSED') { return new MCPError('CONNECTION_REFUSED', 'Cannot connect to SIA Vision services. Please check your internet connection', { operation, context }); } if (error.code === 'ENOTFOUND') { return new MCPError('DNS_ERROR', 'Cannot resolve SIA Vision services. Please check your internet connection', { operation, context }); } if (error.code === 'ETIMEDOUT') { return new MCPError('TIMEOUT', 'Request timed out. Please try again', { operation, context }); } return new MCPError('NETWORK_ERROR', error.message || 'Network error occurred', { operation, context, code: error.code }); } // Unknown error return new MCPError('UNKNOWN_ERROR', error?.message || 'An unknown error occurred', { operation, context, error }); } } //# sourceMappingURL=firebase-client.js.map