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