MATLAB MCP Server

  • src
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { createClient, SupabaseClient } from '@supabase/supabase-js'; import * as fs from 'fs'; import * as path from 'path'; import { homedir, tmpdir } from 'os'; // Set up debug mode const DEBUG = process.env.DEBUG === 'true'; // Set up debug logging const debugLog = (message: string) => { if (!DEBUG) return; try { // Use os.tmpdir() for platform-independent temporary directory const logPath = path.join(tmpdir(), 'mcp-brain-debug.log'); fs.appendFileSync(logPath, `${new Date().toISOString()} - ${message}\n`); } catch (err) { if (DEBUG) { console.error('Failed to write to debug log:', err); } } }; interface Entity { name: string; entity_type: string; observations: string[]; created_at?: string; updated_at?: string; } interface Relation { from: string; to: string; relation_type: string; created_at?: string; updated_at?: string; } class BrainServer { private server: Server; private supabase: SupabaseClient; constructor() { // Ensure environment variables are defined const SUPABASE_URL = process.env.SUPABASE_URL; const SUPABASE_KEY = process.env.SUPABASE_KEY; if (!SUPABASE_URL || !SUPABASE_KEY) { throw new Error('SUPABASE_URL and SUPABASE_KEY environment variables are required'); } this.server = new Server( { name: 'mcp-brain', version: '0.1.0', description: 'A cognitive memory system that helps the model remember and reason about past interactions', }, { capabilities: { tools: {}, }, } ); // Initialize Supabase client this.supabase = createClient(SUPABASE_URL, SUPABASE_KEY); debugLog('Initialized Supabase client'); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private async initialize() { // Initialize database tables await this.initDatabase(); debugLog('Initialized database tables'); // Connect server const transport = new StdioServerTransport(); await this.server.connect(transport); debugLog('Brain MCP server running on stdio'); } private async initDatabase() { try { // Verify database connection and table access const { error } = await this.supabase .from('locks') .select('*') .limit(1); if (error?.code === '42P01') { throw new Error( 'Database tables not found. Please run the SQL migration script from sql/01_mcp_brain_schema.sql in your Supabase project.' ); } else if (error) { throw error; } debugLog('Database connection verified'); } catch (error) { console.error('Database initialization failed:', error); throw error; } } private async withLock<T>(operation: () => Promise<T>): Promise<T> { const lockId = 'memory_lock'; const maxRetries = 20; // Increased from 10 const retryDelay = 2000; // Increased from 1 second const lockTimeout = 60000; // Increased from 30 seconds for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Always try to clear stale locks first const { data: locks, error: selectError } = await this.supabase .from('locks') .select('*') .eq('id', lockId); if (selectError) { debugLog(`Error checking locks: ${selectError.message}`); throw selectError; } // Clear any stale or invalid locks for (const lock of locks || []) { try { const acquiredAt = new Date(lock.acquired_at).getTime(); const now = Date.now(); if (now - acquiredAt > lockTimeout) { debugLog(`Clearing stale lock from ${lock.locked_by}`); await this.supabase .from('locks') .delete() .eq('id', lock.id); } } catch (e) { debugLog(`Error processing lock ${lock.id}: ${e}`); // Try to delete invalid lock await this.supabase .from('locks') .delete() .eq('id', lock.id); } } // Try to acquire lock with unique ID const lockerId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`; const { error: lockError } = await this.supabase .from('locks') .insert({ id: lockId, acquired_at: new Date().toISOString(), locked_by: lockerId }); if (!lockError) { try { // Execute operation while holding lock return await operation(); } finally { // Release lock if we still own it const { data: currentLock } = await this.supabase .from('locks') .select('*') .eq('id', lockId) .eq('locked_by', lockerId) .single(); if (currentLock) { await this.supabase .from('locks') .delete() .eq('id', lockId) .eq('locked_by', lockerId); } } } else if (lockError.code === '23505') { // Unique violation debugLog(`Lock is held, attempt ${attempt + 1}/${maxRetries}`); } else { debugLog(`Lock error: ${lockError.message}`); throw lockError; } // Lock is held, wait and retry await new Promise(resolve => setTimeout(resolve, retryDelay)); } catch (error) { debugLog(`Lock error: ${error}`); throw error; } } throw new Error('Failed to acquire lock after maximum retries'); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'read_graph', description: 'Read the brain\'s entire memory graph to recall past interactions and knowledge', inputSchema: { type: 'object', properties: {}, }, }, { name: 'create_entities', description: 'Store new memories as entities in the brain\'s knowledge graph', inputSchema: { type: 'object', properties: { entities: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, entity_type: { type: 'string' }, observations: { type: 'array', items: { type: 'string' } }, }, required: ['name', 'entity_type', 'observations'], }, }, }, required: ['entities'], }, }, { name: 'create_relations', description: 'Create connections between memories in the brain\'s knowledge graph', inputSchema: { type: 'object', properties: { relations: { type: 'array', items: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' }, relation_type: { type: 'string' }, }, required: ['from', 'to', 'relation_type'], }, }, }, required: ['relations'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name } = request.params; switch (name) { case 'read_graph': { return await this.withLock(async () => { const { data: entities, error: entitiesError } = await this.supabase .from('entities') .select('*'); if (entitiesError) throw entitiesError; const { data: relations, error: relationsError } = await this.supabase .from('relations') .select('*'); if (relationsError) throw relationsError; return { content: [{ type: 'text', text: JSON.stringify({ entities, relations }, null, 2) }], }; }); } case 'create_entities': { const { entities } = request.params.arguments as { entities: Entity[] }; return await this.withLock(async () => { const { data, error } = await this.supabase .from('entities') .insert(entities) .select(); if (error) throw error; return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], }; }); } case 'create_relations': { const { relations } = request.params.arguments as { relations: Relation[] }; return await this.withLock(async () => { const { data, error } = await this.supabase .from('relations') .insert(relations) .select(); if (error) throw error; return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], }; }); } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error: unknown) { console.error('[Tool Error]', error); const message = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true, }; } }); } async run() { await this.initialize(); } } const server = new BrainServer(); server.run().catch(error => { console.error('Failed to start server:', error); process.exit(1); });