UNPKG

onerios-mcp-server

Version:

OneriosMCP server providing memory, backlog management, file operations, and utility functions for enhanced AI assistant capabilities

560 lines (559 loc) 24 kB
"use strict"; /** * Memory Module - Knowledge Graph-based persistent memory for MCP server * * This module provides persistent memory functionality using a knowledge graph structure * with entities, relations, and observations. Based on the official MCP memory server * implementation with adaptations for our modular architecture. * * @see https://github.com/modelcontextprotocol/servers/tree/main/src/memory */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getTools = getTools; exports.handleTool = handleTool; const zod_1 = require("zod"); const fs_1 = require("fs"); const path_1 = require("path"); // ===== ZOD VALIDATION SCHEMAS ===== const EntitySchema = zod_1.z.object({ name: zod_1.z.string().min(1, 'Entity name cannot be empty'), entityType: zod_1.z.string().min(1, 'Entity type cannot be empty'), observations: zod_1.z.array(zod_1.z.string()).default([]) }); const RelationSchema = zod_1.z.object({ from: zod_1.z.string().min(1, 'Relation source cannot be empty'), to: zod_1.z.string().min(1, 'Relation target cannot be empty'), relationType: zod_1.z.string().min(1, 'Relation type cannot be empty') }); const CreateEntitiesSchema = zod_1.z.object({ entities: zod_1.z.array(EntitySchema).min(1, 'Must provide at least one entity') }); const CreateRelationsSchema = zod_1.z.object({ relations: zod_1.z.array(RelationSchema).min(1, 'Must provide at least one relation') }); const AddObservationsSchema = zod_1.z.object({ observations: zod_1.z.array(zod_1.z.object({ entityName: zod_1.z.string().min(1, 'Entity name cannot be empty'), contents: zod_1.z.array(zod_1.z.string().min(1, 'Observation content cannot be empty')).min(1, 'Must provide at least one observation') })).min(1, 'Must provide at least one observation entry') }); const DeleteEntitiesSchema = zod_1.z.object({ entityNames: zod_1.z.array(zod_1.z.string().min(1, 'Entity name cannot be empty')).min(1, 'Must provide at least one entity name') }); const DeleteObservationsSchema = zod_1.z.object({ deletions: zod_1.z.array(zod_1.z.object({ entityName: zod_1.z.string().min(1, 'Entity name cannot be empty'), observations: zod_1.z.array(zod_1.z.string().min(1, 'Observation cannot be empty')).min(1, 'Must provide at least one observation') })).min(1, 'Must provide at least one deletion entry') }); const DeleteRelationsSchema = zod_1.z.object({ relations: zod_1.z.array(RelationSchema).min(1, 'Must provide at least one relation') }); const SearchNodesSchema = zod_1.z.object({ query: zod_1.z.string().min(1, 'Search query cannot be empty') }); const OpenNodesSchema = zod_1.z.object({ names: zod_1.z.array(zod_1.z.string().min(1, 'Entity name cannot be empty')).min(1, 'Must provide at least one entity name') }); // ===== STORAGE CONFIGURATION ===== // Determine memory file path with environment variable support const defaultMemoryPath = (0, path_1.resolve)(__dirname, '..', '..', 'memory.json'); function getMemoryFilePath() { return process.env.MEMORY_FILE_PATH ? (0, path_1.isAbsolute)(process.env.MEMORY_FILE_PATH) ? process.env.MEMORY_FILE_PATH : (0, path_1.resolve)(__dirname, '..', '..', process.env.MEMORY_FILE_PATH) : defaultMemoryPath; } // ===== KNOWLEDGE GRAPH MANAGER ===== /** * KnowledgeGraphManager handles all operations on the knowledge graph * including persistence, CRUD operations, and search functionality */ class KnowledgeGraphManager { constructor() { this.graphCache = null; this.lastModified = 0; } /** * Load the knowledge graph from persistent storage * Uses NDJSON format where each line is a JSON object with type field */ async loadGraph() { try { // Check if we need to reload from disk const memoryFilePath = getMemoryFilePath(); const stats = await fs_1.promises.stat(memoryFilePath); if (this.graphCache && stats.mtime.getTime() <= this.lastModified) { return this.graphCache; } const data = await fs_1.promises.readFile(memoryFilePath, 'utf-8'); const lines = data.split('\n').filter(line => line.trim() !== ''); const graph = { entities: [], relations: [] }; for (const line of lines) { try { const item = JSON.parse(line); if (item.type === 'entity') { const { type, ...entity } = item; graph.entities.push(entity); } else if (item.type === 'relation') { const { type, ...relation } = item; graph.relations.push(relation); } } catch (parseError) { console.error('Failed to parse memory file line:', line, parseError); // Continue processing other lines } } this.graphCache = graph; this.lastModified = stats.mtime.getTime(); return graph; } catch (error) { // Handle file not found - return empty graph if (error?.code === 'ENOENT') { const emptyGraph = { entities: [], relations: [] }; this.graphCache = emptyGraph; this.lastModified = Date.now(); return emptyGraph; } throw new Error(`Failed to load knowledge graph: ${error instanceof Error ? error.message : String(error)}`); } } /** * Save the knowledge graph to persistent storage * Uses atomic write with temporary file to prevent corruption */ async saveGraph(graph) { try { const lines = [ ...graph.entities.map(entity => JSON.stringify({ type: 'entity', ...entity })), ...graph.relations.map(relation => JSON.stringify({ type: 'relation', ...relation })) ]; const content = lines.join('\n'); const memoryFilePath = getMemoryFilePath(); const tempPath = `${memoryFilePath}.tmp`; // Atomic write: write to temp file then rename await fs_1.promises.writeFile(tempPath, content, 'utf-8'); await fs_1.promises.rename(tempPath, memoryFilePath); // Update cache this.graphCache = graph; this.lastModified = Date.now(); } catch (error) { throw new Error(`Failed to save knowledge graph: ${error instanceof Error ? error.message : String(error)}`); } } /** * Create new entities in the knowledge graph * Filters out entities with names that already exist */ async createEntities(entities) { const graph = await this.loadGraph(); const existingNames = new Set(graph.entities.map(e => e.name)); const newEntities = entities.filter(entity => !existingNames.has(entity.name)); graph.entities.push(...newEntities); await this.saveGraph(graph); return newEntities; } /** * Create new relations between entities * Filters out relations that already exist */ async createRelations(relations) { const graph = await this.loadGraph(); const newRelations = relations.filter(relation => !graph.relations.some(existing => existing.from === relation.from && existing.to === relation.to && existing.relationType === relation.relationType)); graph.relations.push(...newRelations); await this.saveGraph(graph); return newRelations; } /** * Add observations to existing entities * Returns the observations that were actually added (excludes duplicates) */ async addObservations(observations) { const graph = await this.loadGraph(); const results = observations.map(obs => { const entity = graph.entities.find(e => e.name === obs.entityName); if (!entity) { throw new Error(`Entity with name '${obs.entityName}' not found`); } const newObservations = obs.contents.filter(content => !entity.observations.includes(content)); entity.observations.push(...newObservations); return { entityName: obs.entityName, addedObservations: newObservations }; }); await this.saveGraph(graph); return results; } /** * Delete entities and cascade delete their relations */ async deleteEntities(entityNames) { const graph = await this.loadGraph(); const nameSet = new Set(entityNames); // Remove entities graph.entities = graph.entities.filter(entity => !nameSet.has(entity.name)); // Cascade delete relations graph.relations = graph.relations.filter(relation => !nameSet.has(relation.from) && !nameSet.has(relation.to)); await this.saveGraph(graph); } /** * Delete specific observations from entities */ async deleteObservations(deletions) { const graph = await this.loadGraph(); for (const deletion of deletions) { const entity = graph.entities.find(e => e.name === deletion.entityName); if (entity) { entity.observations = entity.observations.filter(obs => !deletion.observations.includes(obs)); } } await this.saveGraph(graph); } /** * Delete specific relations from the graph */ async deleteRelations(relations) { const graph = await this.loadGraph(); graph.relations = graph.relations.filter(existing => !relations.some(deleteRel => existing.from === deleteRel.from && existing.to === deleteRel.to && existing.relationType === deleteRel.relationType)); await this.saveGraph(graph); } /** * Read the entire knowledge graph */ async readGraph() { return this.loadGraph(); } /** * Search for nodes based on query string * Searches entity names, types, and observation content */ async searchNodes(query) { const graph = await this.loadGraph(); const queryLower = query.toLowerCase(); // Find matching entities const matchingEntities = graph.entities.filter(entity => entity.name.toLowerCase().includes(queryLower) || entity.entityType.toLowerCase().includes(queryLower) || entity.observations.some(obs => obs.toLowerCase().includes(queryLower))); // Get names of matching entities for relation filtering const matchingEntityNames = new Set(matchingEntities.map(e => e.name)); // Find relations that involve any matching entity (either as from or to) const matchingRelations = graph.relations.filter(relation => matchingEntityNames.has(relation.from) || matchingEntityNames.has(relation.to)); return { entities: matchingEntities, relations: matchingRelations }; } /** * Open specific nodes by their names * Returns the requested entities and relations between them */ async openNodes(names) { const graph = await this.loadGraph(); const nameSet = new Set(names); // Find requested entities const requestedEntities = graph.entities.filter(entity => nameSet.has(entity.name)); // Find relations between requested entities const relatedRelations = graph.relations.filter(relation => nameSet.has(relation.from) && nameSet.has(relation.to)); return { entities: requestedEntities, relations: relatedRelations }; } } // Create singleton instance const knowledgeGraphManager = new KnowledgeGraphManager(); // ===== MCP TOOLS EXPORT ===== /** * Get all memory-related MCP tools */ function getTools() { return [ { name: 'memory_create_entities', description: 'Create multiple new entities in the knowledge graph memory', inputSchema: { type: 'object', properties: { entities: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Unique identifier for the entity' }, entityType: { type: 'string', description: 'Type/classification of the entity (e.g., person, organization, event)' }, observations: { type: 'array', items: { type: 'string' }, description: 'Array of discrete facts/observations about this entity' } }, required: ['name', 'entityType', 'observations'] }, description: 'Array of entities to create' } }, required: ['entities'] } }, { name: 'memory_create_relations', description: 'Create multiple new relations between entities in the knowledge graph', inputSchema: { type: 'object', properties: { relations: { type: 'array', items: { type: 'object', properties: { from: { type: 'string', description: 'Source entity name' }, to: { type: 'string', description: 'Target entity name' }, relationType: { type: 'string', description: 'Type of relationship in active voice (e.g., works_at, located_in)' } }, required: ['from', 'to', 'relationType'] }, description: 'Array of relations to create' } }, required: ['relations'] } }, { name: 'memory_add_observations', description: 'Add new observations to existing entities in the knowledge graph', inputSchema: { type: 'object', properties: { observations: { type: 'array', items: { type: 'object', properties: { entityName: { type: 'string', description: 'Name of the entity to add observations to' }, contents: { type: 'array', items: { type: 'string' }, description: 'Array of observation contents to add' } }, required: ['entityName', 'contents'] }, description: 'Array of observation sets to add' } }, required: ['observations'] } }, { name: 'memory_delete_entities', description: 'Delete entities and their associated relations from the knowledge graph', inputSchema: { type: 'object', properties: { entityNames: { type: 'array', items: { type: 'string' }, description: 'Array of entity names to delete' } }, required: ['entityNames'] } }, { name: 'memory_delete_observations', description: 'Delete specific observations from entities in the knowledge graph', inputSchema: { type: 'object', properties: { deletions: { type: 'array', items: { type: 'object', properties: { entityName: { type: 'string', description: 'Name of the entity to remove observations from' }, observations: { type: 'array', items: { type: 'string' }, description: 'Array of observations to remove' } }, required: ['entityName', 'observations'] }, description: 'Array of observation deletions to perform' } }, required: ['deletions'] } }, { name: 'memory_delete_relations', description: 'Delete specific relations from the knowledge graph', inputSchema: { type: 'object', properties: { relations: { type: 'array', items: { type: 'object', properties: { from: { type: 'string', description: 'Source entity name' }, to: { type: 'string', description: 'Target entity name' }, relationType: { type: 'string', description: 'Type of relationship' } }, required: ['from', 'to', 'relationType'] }, description: 'Array of relations to delete' } }, required: ['relations'] } }, { name: 'memory_read_graph', description: 'Read the entire knowledge graph from memory', inputSchema: { type: 'object', properties: {} } }, { name: 'memory_search_nodes', description: 'Search for nodes in the knowledge graph based on a query', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query to match against entity names, types, and observations' } }, required: ['query'] } }, { name: 'memory_open_nodes', description: 'Open specific nodes in the knowledge graph by their names', inputSchema: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' }, description: 'Array of entity names to retrieve' } }, required: ['names'] } } ]; } /** * Handle memory tool invocations */ async function handleTool(name, args) { try { switch (name) { case 'memory_create_entities': { const validated = CreateEntitiesSchema.parse(args); const createdEntities = await knowledgeGraphManager.createEntities(validated.entities); return { content: [{ type: 'text', text: `Created ${createdEntities.length} entities:\n${JSON.stringify(createdEntities, null, 2)}` }] }; } case 'memory_create_relations': { const validated = CreateRelationsSchema.parse(args); const createdRelations = await knowledgeGraphManager.createRelations(validated.relations); return { content: [{ type: 'text', text: `Created ${createdRelations.length} relations:\n${JSON.stringify(createdRelations, null, 2)}` }] }; } case 'memory_add_observations': { const validated = AddObservationsSchema.parse(args); const results = await knowledgeGraphManager.addObservations(validated.observations); return { content: [{ type: 'text', text: `Added observations:\n${JSON.stringify(results, null, 2)}` }] }; } case 'memory_delete_entities': { const validated = DeleteEntitiesSchema.parse(args); await knowledgeGraphManager.deleteEntities(validated.entityNames); return { content: [{ type: 'text', text: `Successfully deleted ${validated.entityNames.length} entities: ${validated.entityNames.join(', ')}` }] }; } case 'memory_delete_observations': { const validated = DeleteObservationsSchema.parse(args); await knowledgeGraphManager.deleteObservations(validated.deletions); return { content: [{ type: 'text', text: `Successfully deleted observations from ${validated.deletions.length} entities` }] }; } case 'memory_delete_relations': { const validated = DeleteRelationsSchema.parse(args); await knowledgeGraphManager.deleteRelations(validated.relations); return { content: [{ type: 'text', text: `Successfully deleted ${validated.relations.length} relations` }] }; } case 'memory_read_graph': { const graph = await knowledgeGraphManager.readGraph(); return { content: [{ type: 'text', text: `Knowledge Graph:\n${JSON.stringify(graph, null, 2)}` }] }; } case 'memory_search_nodes': { const validated = SearchNodesSchema.parse(args); const results = await knowledgeGraphManager.searchNodes(validated.query); return { content: [{ type: 'text', text: `Search results for "${validated.query}":\n${JSON.stringify(results, null, 2)}` }] }; } case 'memory_open_nodes': { const validated = OpenNodesSchema.parse(args); const results = await knowledgeGraphManager.openNodes(validated.names); return { content: [{ type: 'text', text: `Nodes "${validated.names.join(', ')}":\n${JSON.stringify(results, null, 2)}` }] }; } default: throw new Error(`Unknown memory tool: ${name}`); } } catch (error) { if (error instanceof zod_1.z.ZodError) { throw new Error(`Invalid arguments: ${error.errors.map(e => e.message).join(', ')}`); } throw error; } }