Skip to main content
Glama

MCP Memory Service

mcp-client.js11 kB
/** * MCP Client for Memory Hook Integration * Provides MCP protocol communication for memory operations */ const { spawn } = require('child_process'); const { EventEmitter } = require('events'); const fs = require('fs'); const path = require('path'); class MCPClient extends EventEmitter { constructor(serverCommand, options = {}) { super(); this.serverCommand = serverCommand; this.serverWorkingDir = options.workingDir || process.cwd(); this.connectionTimeout = options.connectionTimeout || 5000; this.toolCallTimeout = options.toolCallTimeout || 10000; this.serverProcess = null; this.messageId = 0; this.pendingRequests = new Map(); this.connected = false; this.buffer = ''; // Load environment variables from .env file this.loadEnvironment(); } /** * Load environment variables from .env file */ loadEnvironment() { const envPath = path.join(this.serverWorkingDir, '.env'); try { if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf8'); const lines = envContent.split('\n'); for (const line of lines) { const match = line.match(/^([^#\s][^=]*?)=(.*)$/); if (match) { const [, key, value] = match; if (!process.env[key]) { process.env[key] = value.replace(/^["']|["']$/g, ''); } } } } } catch (error) { // Ignore .env loading errors } } /** * Start MCP server and establish connection */ async connect() { return new Promise((resolve, reject) => { try { // Start MCP server process this.serverProcess = spawn(this.serverCommand[0], this.serverCommand.slice(1), { cwd: this.serverWorkingDir, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } }); // Handle server output this.serverProcess.stdout.on('data', (data) => { this.buffer += data.toString(); this.processMessages(); }); this.serverProcess.stderr.on('data', (data) => { const error = data.toString(); // Only emit critical errors, ignore warnings and debug info if (error.includes('FATAL') || error.includes('ExceptionGroup')) { this.emit('error', new Error(`Server error: ${error.substring(0, 200)}...`)); } }); this.serverProcess.on('error', (error) => { if (!this.connected) { reject(new Error(`Server process error: ${error.message}`)); } }); this.serverProcess.on('exit', (code) => { this.connected = false; if (code !== 0 && !this.connected) { reject(new Error(`Server failed to start (exit code ${code})`)); } }); // Initialize MCP connection this.sendInitialize() .then(() => { this.connected = true; resolve(); }) .catch(reject); // Connection timeout setTimeout(() => { if (!this.connected) { reject(new Error('Connection timeout')); } }, this.connectionTimeout); } catch (error) { reject(error); } }); } /** * Process incoming messages from server */ processMessages() { const lines = this.buffer.split('\n'); this.buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const message = JSON.parse(line); this.handleMessage(message); } catch (error) { // Ignore malformed messages } } } } /** * Handle incoming MCP messages */ handleMessage(message) { if (message.id && this.pendingRequests.has(message.id)) { const { resolve, reject } = this.pendingRequests.get(message.id); this.pendingRequests.delete(message.id); if (message.error) { reject(new Error(message.error.message || 'MCP Error')); } else { resolve(message.result); } } } /** * Send MCP initialize request */ async sendInitialize() { const message = { jsonrpc: '2.0', id: ++this.messageId, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, clientInfo: { name: 'claude-hooks-mcp-client', version: '1.0.0' } } }; return this.sendMessage(message); } /** * Send message to MCP server */ async sendMessage(message) { return new Promise((resolve, reject) => { if (!this.serverProcess || this.serverProcess.exitCode !== null) { reject(new Error('Server not running')); return; } if (message.id) { this.pendingRequests.set(message.id, { resolve, reject }); // Timeout for this specific request setTimeout(() => { if (this.pendingRequests.has(message.id)) { this.pendingRequests.delete(message.id); reject(new Error('Request timeout')); } }, this.toolCallTimeout); } try { const messageStr = JSON.stringify(message) + '\n'; this.serverProcess.stdin.write(messageStr); if (!message.id) { resolve(); // No response expected } } catch (error) { if (message.id && this.pendingRequests.has(message.id)) { this.pendingRequests.delete(message.id); } reject(error); } }); } /** * Call a tool via MCP */ async callTool(toolName, args = {}) { const message = { jsonrpc: '2.0', id: ++this.messageId, method: 'tools/call', params: { name: toolName, arguments: args } }; return this.sendMessage(message); } /** * Get memory service health status */ async getHealthStatus() { try { const result = await this.callTool('check_database_health'); return { success: true, data: result.content ? this.parseToolResponse(result.content) : result }; } catch (error) { return { success: false, error: error.message, fallback: true }; } } /** * Query memories using semantic search */ async queryMemories(query, limit = 10) { try { const result = await this.callTool('retrieve_memory', { query: query, n_results: limit }); return this.parseToolResponse(result.content); } catch (error) { console.warn('[MCP Client] Memory query error:', error.message); return []; } } /** * Query memories by time range */ async queryMemoriesByTime(timeQuery, limit = 10) { try { const result = await this.callTool('recall_memory', { query: timeQuery, n_results: limit }); return this.parseToolResponse(result.content); } catch (error) { console.warn('[MCP Client] Time-based memory query error:', error.message); return []; } } /** * Parse tool response content */ parseToolResponse(content) { if (!content) return []; // Handle array of content objects if (Array.isArray(content)) { const textContent = content.find(c => c.type === 'text')?.text || ''; return this.parseMemoryResults(textContent); } // Handle direct text content if (typeof content === 'string') { return this.parseMemoryResults(content); } // Handle object with text property if (content.text) { return this.parseMemoryResults(content.text); } return []; } /** * Parse memory results from text response */ parseMemoryResults(textData) { try { // Handle Python dict format conversion to JSON let cleanText = textData .replace(/'/g, '"') .replace(/True/g, 'true') .replace(/False/g, 'false') .replace(/None/g, 'null'); const parsed = JSON.parse(cleanText); return parsed.results || parsed.memories || parsed || []; } catch (error) { // Try to extract JSON from text const jsonMatch = textData.match(/\{[\s\S]*\}/); if (jsonMatch) { try { const extracted = JSON.parse(jsonMatch[0]); return extracted.results || extracted.memories || [extracted]; } catch {} } console.warn('[MCP Client] Could not parse memory results:', error.message); return []; } } /** * Disconnect and cleanup */ async disconnect() { this.connected = false; // Clear pending requests for (const [id, { reject }] of this.pendingRequests) { reject(new Error('Connection closed')); } this.pendingRequests.clear(); // Terminate server process if (this.serverProcess && this.serverProcess.exitCode === null) { this.serverProcess.kill('SIGTERM'); // Force kill if doesn't exit gracefully setTimeout(() => { if (this.serverProcess && this.serverProcess.exitCode === null) { this.serverProcess.kill('SIGKILL'); } }, 2000); } } } module.exports = { MCPClient };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/doobidoo/mcp-memory-service'

If you have feedback or need assistance with the MCP directory API, please join our Discord server