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
JavaScript
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