Skip to main content
Glama

myAI Memory Sync

by Jktfe
directServer.ts13.8 kB
#!/usr/bin/env node import { readFileSync, existsSync, writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { homedir } from 'node:os'; import { z } from 'zod'; import * as readline from 'node:readline'; // Get current directory const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // JSON-RPC schema for validation const jsonRpcSchema = z.object({ jsonrpc: z.literal('2.0'), id: z.union([z.string(), z.number()]).optional(), method: z.string().optional(), params: z.any().optional(), result: z.any().optional(), error: z.object({ code: z.number(), message: z.string(), data: z.any().optional(), }).optional(), }).refine(data => { // Either method (request) or result/error (response) must be present return (data.method !== undefined) || (data.result !== undefined) || (data.error !== undefined); }, { message: 'Invalid JSON-RPC message: must contain either method, result, or error', }); // Default config const DEFAULT_CONFIG = { email: '', apiKey: '', syncEnabled: true, autoSync: true, syncInterval: 3600, // 1 hour lastSync: 0, debug: false, }; // Template data const TEMPLATE = { name: 'Memory Template', description: 'Template for storing memories', sections: [ { name: 'User Information', description: 'Information about the user', fields: [ { name: 'name', type: 'string', description: 'The name of the user', required: true, }, { name: 'email', type: 'string', description: 'The email of the user', required: true, }, ], }, { name: 'Memory', description: 'The memory to store', fields: [ { name: 'title', type: 'string', description: 'The title of the memory', required: true, }, { name: 'content', type: 'string', description: 'The content of the memory', required: true, }, { name: 'tags', type: 'array', description: 'Tags associated with the memory', required: false, }, ], }, ], }; // Tool definitions const TOOLS = [ { name: 'get_template', description: 'Get the template for storing memories', parameters: { type: 'object', properties: {}, }, }, { name: 'get_section', description: 'Get a specific section of the template', parameters: { type: 'object', properties: { sectionName: { type: 'string', description: 'The name of the section to retrieve', }, }, }, }, { name: 'save_memory', description: 'Save a memory to the database', parameters: { type: 'object', properties: { memory: { type: 'object', description: 'The memory to save', }, }, }, }, { name: 'get_memories', description: 'Get memories from the database', parameters: { type: 'object', properties: { query: { type: 'string', description: 'The query to search for', }, limit: { type: 'number', description: 'The maximum number of memories to return', }, }, }, }, ]; /** * Direct MCP Server implementation that handles JSON-RPC messages directly */ export class DirectMcpServer { private config: typeof DEFAULT_CONFIG; private stdinReader: readline.Interface | null = null; private isStarted = false; constructor() { // Load or create config const configPath = join(homedir(), '.myai-memory-sync', 'config.json'); this.config = DEFAULT_CONFIG; try { if (existsSync(configPath)) { const configData = readFileSync(configPath, 'utf-8'); this.config = { ...DEFAULT_CONFIG, ...JSON.parse(configData) }; } else { // Create default config const configDir = dirname(configPath); if (!existsSync(configDir)) { const { mkdir } = require('node:fs/promises'); mkdir(configDir, { recursive: true }); } writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2)); console.error('Created sample config.json file'); } } catch (err) { console.error(`Error loading config: ${err instanceof Error ? err.message : String(err)}`); } console.error(`Using email for Claude Web syncer: ${this.config.email || 'not configured'}`); } /** * Start the server */ start(): void { if (this.isStarted) { console.error('Server already started'); return; } console.error('Starting direct MCP server'); // Create readline interface for stdin this.stdinReader = readline.createInterface({ input: process.stdin, terminal: false, }); // Set up message handler this.stdinReader.on('line', (line) => { try { // Skip empty lines if (!line.trim()) { return; } console.error(`Received raw input: ${line.substring(0, 100)}${line.length > 100 ? '...' : ''}`); try { // Parse the message const message = JSON.parse(line); // Validate the message const validationResult = jsonRpcSchema.safeParse(message); if (!validationResult.success) { console.error(`Received invalid JSON-RPC message: ${validationResult.error.message}`); // If it has an ID, send an error response if (message && typeof message === 'object' && 'id' in message) { this.sendResponse({ jsonrpc: '2.0', id: message.id, error: { code: -32600, message: 'Invalid Request: ' + validationResult.error.message } }); } return; } // Process the message this.processMessage(message); } catch (err) { console.error(`Error parsing message: ${err instanceof Error ? err.message : String(err)}`); // Try to send a parse error response try { const parsed = JSON.parse(line); if (parsed && typeof parsed === 'object' && 'id' in parsed) { this.sendResponse({ jsonrpc: '2.0', id: parsed.id, error: { code: -32700, message: 'Parse error' } }); } } catch { // If we can't parse it at all, we can't send a proper response console.error('Could not parse message to extract ID for error response'); } } } catch (err) { console.error(`Error processing message: ${err instanceof Error ? err.message : String(err)}`); } }); this.isStarted = true; console.error('Direct MCP server started'); } /** * Process a JSON-RPC message */ private processMessage(message: any): void { if (!message.method) { console.error('Message has no method'); return; } console.error(`Processing message method: ${message.method}`); switch (message.method) { case 'initialize': this.handleInitialize(message); break; case 'listTools': this.handleListTools(message); break; case 'get_template': this.handleGetTemplate(message); break; case 'get_section': this.handleGetSection(message); break; case 'save_memory': this.handleSaveMemory(message); break; case 'get_memories': this.handleGetMemories(message); break; default: this.sendResponse({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } }); break; } } /** * Handle initialize request */ private handleInitialize(message: any): void { console.error('Handling initialize request'); this.sendResponse({ jsonrpc: '2.0', id: message.id, result: { serverInfo: { name: 'myAI Memory Sync', version: '1.0.0', }, capabilities: { tools: {}, }, } }); } /** * Handle listTools request */ private handleListTools(message: any): void { console.error('Handling listTools request'); this.sendResponse({ jsonrpc: '2.0', id: message.id, result: { tools: TOOLS } }); } /** * Handle get_template request */ private handleGetTemplate(message: any): void { console.error('Handling get_template request'); this.sendResponse({ jsonrpc: '2.0', id: message.id, result: { template: TEMPLATE } }); } /** * Handle get_section request */ private handleGetSection(message: any): void { console.error('Handling get_section request'); const sectionName = message.params?.sectionName; if (!sectionName) { this.sendResponse({ jsonrpc: '2.0', id: message.id, error: { code: -32602, message: 'Invalid params: sectionName is required' } }); return; } // Find the section const section = TEMPLATE.sections.find(s => s.name === sectionName); if (!section) { this.sendResponse({ jsonrpc: '2.0', id: message.id, error: { code: -32602, message: `Invalid params: section "${sectionName}" not found` } }); return; } this.sendResponse({ jsonrpc: '2.0', id: message.id, result: { section } }); } /** * Handle save_memory request */ private handleSaveMemory(message: any): void { console.error('Handling save_memory request'); const memory = message.params?.memory; if (!memory) { this.sendResponse({ jsonrpc: '2.0', id: message.id, error: { code: -32602, message: 'Invalid params: memory is required' } }); return; } // Here you would save the memory to your database // For now, just log it console.error(`Saving memory: ${JSON.stringify(memory)}`); this.sendResponse({ jsonrpc: '2.0', id: message.id, result: { success: true, message: 'Memory saved successfully' } }); } /** * Handle get_memories request */ private handleGetMemories(message: any): void { console.error('Handling get_memories request'); const query = message.params?.query; const limit = message.params?.limit || 10; // Here you would query your database for memories // For now, just return some sample data this.sendResponse({ jsonrpc: '2.0', id: message.id, result: { memories: [ { id: '1', title: 'Sample Memory 1', content: 'This is a sample memory', tags: ['sample', 'memory'], createdAt: new Date().toISOString(), }, { id: '2', title: 'Sample Memory 2', content: 'This is another sample memory', tags: ['sample', 'memory'], createdAt: new Date().toISOString(), }, ] } }); } /** * Send a JSON-RPC response */ private sendResponse(response: any): void { try { // Validate the response const validationResult = jsonRpcSchema.safeParse(response); if (!validationResult.success) { console.error(`Invalid JSON-RPC response: ${validationResult.error.message}`); return; } // Convert to string const responseStr = JSON.stringify(response); // Debug log console.error(`Sending response: ${responseStr.substring(0, 100)}...`); // Write directly to stdout process.stdout.write(responseStr + '\n'); } catch (err) { console.error(`Error sending response: ${err instanceof Error ? err.message : String(err)}`); } } /** * Stop the server */ stop(): void { if (this.stdinReader) { this.stdinReader.close(); this.stdinReader = null; } this.isStarted = false; console.error('Direct MCP server stopped'); } } // Main function async function main() { try { console.error('Starting myAI Memory Sync MCP server'); // Create and start the server const server = new DirectMcpServer(); server.start(); console.error('MCP server ready'); // Keep the process alive process.stdin.resume(); // Handle SIGTERM gracefully process.on('SIGTERM', () => { console.error('Received SIGTERM, shutting down...'); server.stop(); process.exit(0); }); // Handle SIGINT gracefully process.on('SIGINT', () => { console.error('Received SIGINT, shutting down...'); server.stop(); process.exit(0); }); } catch (error) { console.error(`Error starting MCP server: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } } // Start the server if this is the main module if (import.meta.url === `file://${process.argv[1]}`) { main(); }

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/Jktfe/myAImemory-mcp'

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