UNPKG

sia-vision-mcp-server

Version:

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

248 lines 8.8 kB
#!/usr/bin/env node /** * SIA.Vision MCP Server * * Official MCP server for the SIA.Vision storytelling platform. * Enables AI agents like Claude to create and manage storyworlds, * characters, scenes, and narrative content. * * Usage: * npx @sia-vision/mcp-server * * Environment Variables: * SIA_API_KEY - Your SIA.Vision API key (required) * SIA_MCP_BASE_URL - SIA Firebase Functions base URL (optional) * DEBUG - Enable debug logging (optional) */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { FirebaseClient } from './services/firebase-client.js'; // Use minimal v2-only core tools for local fallback import { v2CoreTools } from './tools/v2-core-tools.js'; import { allResources, resourceContent } from './resources/index.js'; import { allPrompts, promptContent } from './prompts/index.js'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const API_KEY = process.env.SIA_API_KEY; const BASE_URL = process.env.SIA_MCP_BASE_URL || 'https://us-central1-sia-vision.cloudfunctions.net'; const DEBUG = process.env.DEBUG === '1'; // Validate configuration if (!API_KEY) { console.error('❌ Error: SIA_API_KEY environment variable is required'); console.error(''); console.error('Get your API key at: https://sia.vision'); console.error('Then set: export SIA_API_KEY="your-api-key-here"'); process.exit(1); } const server = new Server({ name: 'sia-vision', version: '1.0.0', }, { capabilities: { tools: {}, resources: { listChanged: true }, prompts: { listChanged: true }, }, }); // Initialize Firebase client const firebaseClient = new FirebaseClient(BASE_URL, API_KEY); // Combine all tools (local fallback). Remote list from Functions is authoritative. const allTools = [ ...v2CoreTools ]; let listedToolsCache = allTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, })); // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { try { // Prefer authoritative tools list from Functions MCP endpoint const remote = await firebaseClient.listTools(); if (!remote || !Array.isArray(remote.tools)) { throw new Error('Invalid tools payload'); } listedToolsCache = remote.tools; if (DEBUG) { console.error(`🛠️ Loaded ${listedToolsCache.length} remote tools`); } return { tools: listedToolsCache }; } catch (err) { if (DEBUG) { const msg = err instanceof Error ? err.message : String(err); console.error(`⚠️ Falling back to local tool list (${msg})`); } const fallback = allTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, })); listedToolsCache = fallback; return { tools: fallback }; } }); // Tool execution handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (DEBUG) { console.error(`🔧 Executing tool: ${name}`); console.error(`📝 Arguments:`, JSON.stringify(args, null, 2)); } try { // Validate the tool exists based on the last-known authoritative list const toolExists = listedToolsCache.some(t => t.name === name); if (!toolExists) { throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } // Execute the tool via Firebase Functions const result = await firebaseClient.executeTool(name, args); if (DEBUG) { console.error(`✅ Tool ${name} completed successfully`); } return { content: [ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), }, ], }; } catch (error) { if (DEBUG) { console.error(`❌ Tool ${name} failed:`, error); } const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error executing ${name}: ${errorMessage}`, }, ], isError: true, }; } }); // List resources handler server.setRequestHandler(ListResourcesRequestSchema, async () => { if (DEBUG) { console.error(`📚 Listing ${allResources.length} available resources`); } return { resources: allResources, }; }); // Read resource handler server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; if (DEBUG) { console.error(`📖 Reading resource: ${uri}`); } try { const content = resourceContent[uri]; if (!content) { throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${uri}`); } return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(content, null, 2), }, ], }; } catch (error) { if (DEBUG) { console.error(`❌ Resource ${uri} failed:`, error); } throw new McpError(ErrorCode.InternalError, `Error reading resource ${uri}: ${error instanceof Error ? error.message : String(error)}`); } }); // List prompts handler server.setRequestHandler(ListPromptsRequestSchema, async () => { if (DEBUG) { console.error(`💭 Listing ${allPrompts.length} available prompts`); } return { prompts: allPrompts, }; }); // Get prompt handler server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (DEBUG) { console.error(`💡 Getting prompt: ${name}`); console.error(`📝 Arguments:`, JSON.stringify(args, null, 2)); } try { const prompt = allPrompts.find(p => p.name === name); if (!prompt) { throw new McpError(ErrorCode.InvalidRequest, `Prompt not found: ${name}`); } const baseContent = promptContent[name] || `You are helping with ${name.replace(/-/g, ' ')}.`; // Build context-aware prompt content let contextualContent = baseContent; if (args) { contextualContent += '\n\n**Context:**\n'; Object.entries(args).forEach(([key, value]) => { contextualContent += `- ${key.replace(/_/g, ' ')}: ${value}\n`; }); } return { description: prompt.description, messages: [ { role: 'user', content: { type: 'text', text: contextualContent, }, }, ], }; } catch (error) { if (DEBUG) { console.error(`❌ Prompt ${name} failed:`, error); } throw new McpError(ErrorCode.InternalError, `Error getting prompt ${name}: ${error instanceof Error ? error.message : String(error)}`); } }); // Start the server async function main() { const transport = new StdioServerTransport(); if (DEBUG) { console.error('🚀 Starting SIA.Vision MCP Server'); console.error(`🔗 Base URL: ${BASE_URL}`); console.error(`🔑 API Key: ${API_KEY.substring(0, 12)}...`); console.error(`📋 Tools available: ${allTools.length}`); console.error(`📚 Resources available: ${allResources.length}`); console.error(`💭 Prompts available: ${allPrompts.length}`); } await server.connect(transport); if (DEBUG) { console.error('✅ SIA.Vision MCP Server is ready!'); } } // Error handling process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); // Start the server main().catch((error) => { console.error('Failed to start MCP server:', error); process.exit(1); }); //# sourceMappingURL=index.js.map