fr3kc0d3
Version: 
Personal AI Infrastructure (PAI) - Complete implementation of Daniel Miessler's PAI vision with 4-layer context enforcement, FOBs system, flow orchestration, and Kai digital assistant
571 lines (517 loc) • 18.7 kB
JavaScript
/**
 * fr3kc0de UniMemory MCP Server
 * Unified persistent memory and learning system for crypto trading insights
 */
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import path from 'path';
import fs from 'fs/promises'; // For initial file-based fallback/migration
import Redis from 'ioredis';
import { QdrantClient } from '@qdrant/qdrant-js';
import neo4j from 'neo4j-driver';
import { Pool } from 'pg';
class Fr3kc0deUniMemoryServer {
  constructor() {
    this.server = new Server(
      {
        name: 'fr3kc0de-unimemory',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );
    // Configuration (will be externalized later)
    this.config = {
      redis: { enabled: true, host: 'localhost', port: 6379, db: 0 },
      qdrant: { enabled: true, url: 'http://localhost:6333' },
      neo4j: { enabled: true, uri: 'bolt://localhost:7687', user: 'neo4j', password: 'fr3kc0de123' },
      timescaledb: { enabled: true, host: 'localhost', port: 5432, user: 'fr3kc0de', password: 'fr3kc0de123', database: 'unimemory' },
    };
    if (this.config.redis.enabled) {
      this.redisClient = new Redis({
        host: this.config.redis.host,
        port: this.config.redis.port,
        db: this.config.redis.db,
      });
      this.redisClient.on('connect', () => console.log('Redis connected!'));
      this.redisClient.on('error', (err) => console.error('Redis error:', err));
    } else {
      this.redisClient = null;
    }
    if (this.config.qdrant.enabled) {
      this.qdrantClient = new QdrantClient({ host: this.config.qdrant.url.split('//')[1] });
      this.qdrantCollectionName = 'fr3kc0de_memories';
      this.initializeQdrantCollection();
    } else {
      this.qdrantClient = null;
    }
    if (this.config.neo4j.enabled) {
      this.neo4jDriver = neo4j.driver(
        this.config.neo4j.uri,
        neo4j.auth.basic(this.config.neo4j.user, this.config.neo4j.password)
      );
      this.neo4jDriver.verifyConnectivity()
        .then(() => console.log('Neo4j connected!'))
        .catch((error) => console.error('Neo4j connection error:', error));
    } else {
      this.neo4jDriver = null;
    }
    if (this.config.timescaledb.enabled) {
      this.timescalePool = new Pool(this.config.timescaledb);
      this.timescalePool.on('connect', () => console.log('TimescaleDB connected!'));
      this.timescalePool.on('error', (err) => console.error('TimescaleDB error:', err));
      this.initializeTimescaleDB();
    } else {
      this.timescalePool = null;
    }
    // Placeholder for embedding generation
    this.generateEmbedding = async (text) => {
      // In a real scenario, this would call an embedding model (e.g., OpenAI, local LLM)
      // For now, return a dummy vector
      const dummyVector = Array(1536).fill(0).map((_, i) => Math.random()); // Example: 1536-dim vector
      return dummyVector;
    }
  }
  async initializeQdrantCollection() {
    if (!this.qdrantClient) return;
    try {
      const { collections } = await this.qdrantClient.getCollections();
      const collectionExists = collections.some(c => c.name === this.qdrantCollectionName);
      if (!collectionExists) {
        await this.qdrantClient.createCollection(this.qdrantCollectionName, {
          vectors: { size: 1536, distance: 'Cosine' }, // Assuming 1536-dim embeddings
        });
        console.log(`Qdrant collection '${this.qdrantCollectionName}' created.`);
      } else {
        console.log(`Qdrant collection '${this.qdrantCollectionName}' already exists.`);
      }
    } catch (error) {
      console.error('Error initializing Qdrant collection:', error);
    }
  }
  async initializeTimescaleDB() {
    if (!this.timescalePool) return;
    const client = await this.timescalePool.connect();
    try {
      const createTableQuery = `
        CREATE TABLE IF NOT EXISTS memory_events (
          time TIMESTAMPTZ NOT NULL,
          memory_id TEXT NOT NULL,
          event_type TEXT NOT NULL,
          memory_type TEXT,
          tags TEXT[],
          content_length INTEGER
        );
      `;
      await client.query(createTableQuery);
      const createHypertableQuery = `
        SELECT create_hypertable('memory_events', 'time', if_not_exists => TRUE);
      `;
      await client.query(createHypertableQuery);
      console.log('TimescaleDB hypertable "memory_events" is ready.');
    } catch (error) {
      console.error('Error initializing TimescaleDB:', error);
    } finally {
      client.release();
    }
  }
  setupToolHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'mcp__fr3kc0de_unimemory__remember',
          description: 'Store important trading insights, strategies, and market observations across multiple memory layers',
          inputSchema: {
            type: 'object',
            properties: {
              content: {
                type: 'string',
                description: 'The trading insight or information to remember',
              },
              context: {
                type: 'string', 
                description: 'Context about when/why this is being stored',
              },
              tags: {
                type: 'array',
                items: { type: 'string' },
                description: 'Tags to categorize the memory',
              },
              type: {
                type: 'string',
                description: 'Type of memory (e.g., "trading_insight", "event", "structured_fact")',
                default: 'trading_insight'
              }
            },
            required: ['content'],
          },
        },
        {
          name: 'mcp__fr3kc0de_unimemory__recall',
          description: 'Search and retrieve stored trading memories and insights from unified memory layers',
          inputSchema: {
            type: 'object',
            properties: {
              query: {
                type: 'string',
                description: 'Search query to find relevant memories',
              },
              tags: {
                type: 'array',
                items: { type: 'string' },
                description: 'Filter by specific tags',
              },
              limit: {
                type: 'number',
                description: 'Maximum number of results to return',
                default: 10,
              },
              memory_types: {
                type: 'array',
                items: { type: 'string' },
                description: 'Filter by specific memory types (e.g., "trading_insight", "event")',
              }
            },
          },
        },
        {
          name: 'mcp__fr3kc0de_unimemory__list_memories',
          description: 'List recent trading memories with optional filtering from unified memory layers',
          inputSchema: {
            type: 'object',
            properties: {
              limit: {
                type: 'number',
                description: 'Maximum number of memories to return',
                default: 20,
              },
              tags: {
                type: 'array',
                items: { type: 'string' },
                description: 'Filter by specific tags',
              },
              memory_types: {
                type: 'array',
                items: { type: 'string' },
                description: 'Filter by specific memory types',
              }
            },
          },
        },
      ],
    }));
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      try {
        switch (name) {
          case 'mcp__fr3kc0de_unimemory__remember':
            return await this.remember(args.content, args.context, args.tags, args.type);
          case 'mcp__fr3kc0de_unimemory__recall':
            return await this.recall(args.query, args.tags, args.limit, args.memory_types);
          case 'mcp__fr3kc0de_unimemory__list_memories':
            return await this.listMemories(args.limit, args.tags, args.memory_types);
          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: `Error: ${error.message}`,
            },
          ],
        };
      }
    });
  }
  // --- Unified Memory Interface Methods ---
  async remember(content, context = '', tags = [], type = 'trading_insight') {
    const newMemory = {
      id: Date.now().toString(), // Simple ID for now
      content,
      context,
      tags: tags || [],
      type,
      created_at: new Date().toISOString(),
    };
    const operations = [];
    // Redis Operation
    if (this.redisClient) {
      operations.push((async () => {
        const redisKey = `unimemory:${newMemory.id}`;
        await this.redisClient.setex(redisKey, 3600, JSON.stringify(newMemory)); // Store for 1 hour
        console.log(`Stored memory ${newMemory.id} in Redis.`);
      })());
    }
    // Qdrant Operation
    if (this.qdrantClient) {
      operations.push((async () => {
        try {
          const embedding = await this.generateEmbedding(newMemory.content);
          await this.qdrantClient.upsert(this.qdrantCollectionName, {
            wait: true,
            points: [{ id: newMemory.id, vector: embedding, payload: newMemory }],
          });
          console.log(`Stored memory ${newMemory.id} in Qdrant.`);
        } catch (error) {
          console.error(`Error storing memory ${newMemory.id} in Qdrant:`, error);
        }
      })());
    }
    // Neo4j Operation
    if (this.neo4jDriver) {
      operations.push((async () => {
        const session = this.neo4jDriver.session();
        try {
          const cypher = `
            CREATE (m:Memory {id: $id, content: $content, context: $context, type: $type, created_at: $created_at})
            WITH m
            UNWIND $tags AS tagName
            MERGE (t:Tag {name: tagName})
            MERGE (m)-[:TAGGED_AS]->(t)
            RETURN m
          `;
          await session.run(cypher, newMemory);
          console.log(`Stored memory ${newMemory.id} in Neo4j.`);
        } catch (error) {
          console.error(`Error storing memory ${newMemory.id} in Neo4j:`, error);
        } finally {
          await session.close();
        }
      })());
    }
    // TimescaleDB Operation
    if (this.timescalePool) {
      operations.push((async () => {
        try {
          const insertQuery = `
            INSERT INTO memory_events(time, memory_id, event_type, memory_type, tags, content_length)
            VALUES($1, $2, $3, $4, $5, $6)
          `;
          const values = [
            newMemory.created_at,
            newMemory.id,
            'remember',
            newMemory.type,
            newMemory.tags,
            newMemory.content.length
          ];
          await this.timescalePool.query(insertQuery, values);
          console.log(`Logged 'remember' event for memory ${newMemory.id} in TimescaleDB.`);
        } catch (error) {
          console.error(`Error logging event for memory ${newMemory.id} in TimescaleDB:`, error);
        }
      })());
    }
    await Promise.all(operations);
    return {
      content: [{ 
        type: 'text',
        text: `✅ UniMemory: Received memory of type "${type}" with content: "${content.substring(0, 50)}..."`,
      }],
    };
  }
  async recall(query = '', tags = [], limit = 10, memory_types = []) {
    const allMemories = new Map();
    const sources = [];
    if (this.redisClient && !query && !tags.length && !memory_types.length) {
      sources.push(this._recallFromRedis(limit));
    }
    if (this.qdrantClient && query) {
      sources.push(this._recallFromQdrant(query, limit));
    }
    if (this.neo4jDriver && (query || tags.length > 0 || memory_types.length > 0)) {
      sources.push(this._recallFromNeo4j(query, tags, memory_types, limit));
    }
    const results = await Promise.all(sources);
    for (const result of results) {
      for (const memory of result) {
        if (!allMemories.has(memory.id)) {
          allMemories.set(memory.id, memory);
        }
      }
    }
    const recalledMemories = Array.from(allMemories.values())
      .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
      .slice(0, limit);
    if (recalledMemories.length > 0) {
      const summary = recalledMemories.map(mem => `[${mem.source}] ${mem.content.substring(0, 70)}...`).join('\n');
      return {
        content: [{ 
          type: 'text',
          text: `🧠 UniMemory: Recalled ${recalledMemories.length} memories:\n${summary}`,
        }],
      };
    }
    return {
      content: [{ 
        type: 'text',
        text: `🧠 UniMemory: No memories found for query "${query}"`,
      }],
    };
  }
  async listMemories(limit = 20, tags = [], memory_types = []) {
    const allMemories = new Map();
    const sources = [];
    if (this.redisClient) {
      sources.push(this._listFromRedis(limit, tags, memory_types));
    }
    if (this.qdrantClient) {
      sources.push(this._listFromQdrant(limit, tags, memory_types));
    }
    if (this.neo4jDriver) {
      sources.push(this._listFromNeo4j(limit, tags, memory_types));
    }
    const results = await Promise.all(sources);
    for (const result of results) {
      for (const memory of result) {
        if (!allMemories.has(memory.id)) {
          allMemories.set(memory.id, memory);
        }
      }
    }
    const listedMemories = Array.from(allMemories.values())
      .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
      .slice(0, limit);
    if (listedMemories.length > 0) {
      const summary = listedMemories.map((mem, index) => `${index + 1}. [${mem.source}] ${mem.content.substring(0, 70)}...`).join('\n');
      return {
        content: [{ 
          type: 'text',
          text: `📚 UniMemory: Listed ${listedMemories.length} memories:\n${summary}`,
        }],
      };
    }
    return {
      content: [{ 
        type: 'text',
        text: `📚 UniMemory: No memories found.`,
      }],
    };
  }
  // --- Private Helper Methods for Data Retrieval ---
  async _recallFromRedis(limit) {
    const keys = await this.redisClient.keys('unimemory:*');
    const memories = [];
    for (const key of keys) {
      const memory = JSON.parse(await this.redisClient.get(key));
      memories.push({ ...memory, source: 'Redis' });
    }
    return memories.slice(0, limit);
  }
  async _recallFromQdrant(query, limit) {
    try {
      const queryEmbedding = await this.generateEmbedding(query);
      const searchResult = await this.qdrantClient.search(this.qdrantCollectionName, {
        vector: queryEmbedding,
        limit: limit,
        with_payload: true,
      });
      return searchResult.map(hit => ({ ...hit.payload, source: 'Qdrant', score: hit.score }));
    } catch (error) {
      console.error('Error searching Qdrant:', error);
      return [];
    }
  }
  async _recallFromNeo4j(query, tags, memory_types, limit) {
    const session = this.neo4jDriver.session();
    try {
      let cypher = `MATCH (m:Memory)`;
      let whereClauses = [];
      let params = { limit };
      if (query) {
        whereClauses.push(`m.content CONTAINS $query OR m.context CONTAINS $query`);
        params.query = query;
      }
      if (tags.length > 0) {
        cypher += `-[:TAGGED_AS]->(t:Tag)`;
        whereClauses.push(`t.name IN $tags`);
        params.tags = tags;
      }
      if (memory_types.length > 0) {
        whereClauses.push(`m.type IN $memory_types`);
        params.memory_types = memory_types;
      }
      if (whereClauses.length > 0) {
        cypher += ` WHERE ` + whereClauses.join(` AND `);
      }
      cypher += ` RETURN m ORDER BY m.created_at DESC LIMIT $limit`;
      const result = await session.run(cypher, params);
      return result.records.map(record => ({ ...record.get('m').properties, source: 'Neo4j' }));
    } catch (error) {
      console.error('Error searching Neo4j:', error);
      return [];
    } finally {
      await session.close();
    }
  }
  async _listFromRedis(limit, tags, memory_types) {
    const keys = await this.redisClient.keys('unimemory:*');
    let memories = [];
    for (const key of keys) {
      const memory = JSON.parse(await this.redisClient.get(key));
      if ((!tags || tags.length === 0 || tags.some(tag => memory.tags.includes(tag))) &&
          (!memory_types || memory_types.length === 0 || memory_types.includes(memory.type))) {
        memories.push({ ...memory, source: 'Redis' });
      }
    }
    return memories.slice(0, limit);
  }
  async _listFromQdrant(limit, tags, memory_types) {
    try {
      const { points } = await this.qdrantClient.scroll(this.qdrantCollectionName, {
        limit: limit,
        with_payload: true,
      });
      return points.map(point => ({ ...point.payload, source: 'Qdrant' }));
    } catch (error) {
      console.error('Error listing from Qdrant:', error);
      return [];
    }
  }
  async _listFromNeo4j(limit, tags, memory_types) {
    const session = this.neo4jDriver.session();
    try {
      let cypher = `MATCH (m:Memory)`;
      let whereClauses = [];
      let params = { limit };
      if (tags.length > 0) {
        cypher += `-[:TAGGED_AS]->(t:Tag)`;
        whereClauses.push(`t.name IN $tags`);
        params.tags = tags;
      }
      if (memory_types.length > 0) {
        whereClauses.push(`m.type IN $memory_types`);
        params.memory_types = memory_types;
      }
      if (whereClauses.length > 0) {
        cypher += ` WHERE ` + whereClauses.join(` AND `);
      }
      cypher += ` RETURN m ORDER BY m.created_at DESC LIMIT $limit`;
      const result = await session.run(cypher, params);
      return result.records.map(record => ({ ...record.get('m').properties, source: 'Neo4j' }));
    } catch (error) {
      console.error('Error listing from Neo4j:', error);
      return [];
    } finally {
      await session.close();
    }
  }
  // --- Server Run Method ---
  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.log("fr3kc0de UniMemory Server started.");
  }
}
const server = new Fr3kc0deUniMemoryServer();
server.run().catch(console.error);