UNPKG

@mseep/mcp-neo4j-memory-server

Version:

MCP Memory Server with Neo4j backend for AI knowledge graph storage

785 lines (778 loc) 22.6 kB
// src/index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z as z2 } from "zod"; // src/logger.ts var ConsoleLogger = class { level = "info" /* INFO */; setLevel(level) { this.level = level; } debug(message, payload) { if (this.shouldLog("debug" /* DEBUG */)) { console.debug(message, payload); } } info(message, payload) { if (this.shouldLog("info" /* INFO */)) { console.info(message, payload); } } warn(message, payload) { if (this.shouldLog("warning" /* WARN */)) { console.warn(message, payload); } } error(message, payload) { if (this.shouldLog("error" /* ERROR */)) { console.error(message, payload); } } shouldLog(messageLevel) { const levels = [ "debug" /* DEBUG */, "info" /* INFO */, "warning" /* WARN */, "error" /* ERROR */ ]; return levels.indexOf(messageLevel) >= levels.indexOf(this.level); } }; // src/manager.ts import Fuse from "fuse.js"; import neo4j from "neo4j-driver"; // src/utils.ts var extractError = (error) => { if (error instanceof Error) { return { message: error.message }; } else { return { message: "Unknown error" }; } }; // src/manager.ts var Neo4jKnowledgeGraphManager = class { driver = null; fuse; initialized = false; logger; uri; user; password; database; /** * 构造函数 * @param configResolver 配置解析器函数 * @param logger 可选的日志记录器 */ constructor(configResolver, logger2) { const config = configResolver(); this.uri = config.uri; this.user = config.user; this.password = config.password; this.database = config.database; this.logger = logger2 || new ConsoleLogger(); this.fuse = new Fuse([], { keys: ["name", "entityType", "observations"], includeScore: true, threshold: 0.4 // 搜索严格度(越接近0越严格) }); } /** * 获取会话 * @returns Neo4j会话 */ async getSession() { if (!this.driver) { await this.initialize(); } return this.driver.session({ database: this.database }); } /** * 初始化数据库 */ async initialize() { if (this.initialized) return; try { if (!this.driver) { this.driver = neo4j.driver( this.uri, neo4j.auth.basic(this.user, this.password), { maxConnectionLifetime: 3 * 60 * 60 * 1e3 } // 3小时 ); } const session = await this.getSession(); try { await session.run(` CREATE CONSTRAINT entity_name_unique IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE `); await session.run(` CREATE INDEX entity_type_index IF NOT EXISTS FOR (e:Entity) ON (e.entityType) `); await session.run(` CREATE FULLTEXT INDEX entity_fulltext IF NOT EXISTS FOR (e:Entity) ON EACH [e.name, e.entityType] `); const entities = await this.getAllEntities(); this.fuse.setCollection(entities); this.initialized = true; } finally { await session.close(); } } catch (error) { this.logger.error("Failed to initialize database", extractError(error)); throw error; } } /** * 获取所有实体 * @returns 所有实体数组 */ async getAllEntities() { const session = await this.getSession(); try { const result = await session.run(` MATCH (e:Entity) OPTIONAL MATCH (e)-[r:HAS_OBSERVATION]->(o) RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations `); const entities = result.records.map((record) => { return { name: record.get("name"), entityType: record.get("entityType"), observations: record.get("observations").filter(Boolean) }; }); return entities; } catch (error) { this.logger.error("Error getting all entities", extractError(error)); return []; } finally { await session.close(); } } /** * 创建实体 * @param entities 要创建的实体数组 * @returns 创建的实体数组 */ async createEntities(entities) { if (entities.length === 0) return []; const session = await this.getSession(); try { const createdEntities = []; const tx = session.beginTransaction(); try { const existingEntitiesResult = await tx.run( "MATCH (e:Entity) RETURN e.name AS name" ); const existingNames = new Set( existingEntitiesResult.records.map((record) => record.get("name")) ); const newEntities = entities.filter( (entity) => !existingNames.has(entity.name) ); for (const entity of newEntities) { await tx.run( ` CREATE (e:Entity {name: $name, entityType: $entityType}) WITH e UNWIND $observations AS observation CREATE (o:Observation {content: observation}) CREATE (e)-[:HAS_OBSERVATION]->(o) RETURN e `, { name: entity.name, entityType: entity.entityType, observations: entity.observations } ); createdEntities.push(entity); } await tx.commit(); const allEntities = await this.getAllEntities(); this.fuse.setCollection(allEntities); return createdEntities; } catch (error) { await tx.rollback(); this.logger.error("Error creating entities", extractError(error)); throw error; } } finally { await session.close(); } } /** * 创建关系 * @param relations 要创建的关系数组 * @returns 创建的关系数组 */ async createRelations(relations) { if (relations.length === 0) return []; const session = await this.getSession(); try { const tx = session.beginTransaction(); try { const entityNamesResult = await tx.run( "MATCH (e:Entity) RETURN e.name AS name" ); const entityNames = new Set( entityNamesResult.records.map((record) => record.get("name")) ); const validRelations = relations.filter( (relation) => entityNames.has(relation.from) && entityNames.has(relation.to) ); const existingRelationsResult = await tx.run(` MATCH (from:Entity)-[r]->(to:Entity) RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType `); const existingRelations = existingRelationsResult.records.map((record) => { return { from: record.get("fromName"), to: record.get("toName"), relationType: record.get("relationType") }; }); const newRelations = validRelations.filter( (newRel) => !existingRelations.some( (existingRel) => existingRel.from === newRel.from && existingRel.to === newRel.to && existingRel.relationType === newRel.relationType ) ); for (const relation of newRelations) { await tx.run( ` MATCH (from:Entity {name: $fromName}) MATCH (to:Entity {name: $toName}) CREATE (from)-[r:${relation.relationType}]->(to) RETURN r `, { fromName: relation.from, toName: relation.to } ); } await tx.commit(); return newRelations; } catch (error) { await tx.rollback(); this.logger.error("Error creating relations", extractError(error)); throw error; } } finally { await session.close(); } } /** * 添加观察 * @param observations 要添加的观察数组 * @returns 添加的观察数组 */ async addObservations(observations) { if (observations.length === 0) return []; const session = await this.getSession(); try { const addedObservations = []; const tx = session.beginTransaction(); try { for (const observation of observations) { const entityResult = await tx.run( "MATCH (e:Entity {name: $name}) RETURN e", { name: observation.entityName } ); if (entityResult.records.length > 0) { const existingObservationsResult = await tx.run( ` MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation) RETURN o.content AS content `, { name: observation.entityName } ); const existingObservations = new Set( existingObservationsResult.records.map((record) => record.get("content")) ); const newContents = observation.contents.filter( (content) => !existingObservations.has(content) ); if (newContents.length > 0) { await tx.run( ` MATCH (e:Entity {name: $name}) UNWIND $contents AS content CREATE (o:Observation {content: content}) CREATE (e)-[:HAS_OBSERVATION]->(o) `, { name: observation.entityName, contents: newContents } ); addedObservations.push({ entityName: observation.entityName, contents: newContents }); } } } await tx.commit(); const allEntities = await this.getAllEntities(); this.fuse.setCollection(allEntities); return addedObservations; } catch (error) { await tx.rollback(); this.logger.error("Error adding observations", extractError(error)); throw error; } } finally { await session.close(); } } /** * 删除实体 * @param entityNames 要删除的实体名称数组 */ async deleteEntities(entityNames) { if (entityNames.length === 0) return; const session = await this.getSession(); try { const tx = session.beginTransaction(); try { await tx.run( ` UNWIND $names AS name MATCH (e:Entity {name: name}) OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation) DETACH DELETE e, o `, { names: entityNames } ); await tx.commit(); const allEntities = await this.getAllEntities(); this.fuse.setCollection(allEntities); } catch (error) { await tx.rollback(); this.logger.error("Error deleting entities", extractError(error)); throw error; } } finally { await session.close(); } } /** * 删除观察 * @param deletions 要删除的观察数组 */ async deleteObservations(deletions) { if (deletions.length === 0) return; const session = await this.getSession(); try { const tx = session.beginTransaction(); try { for (const deletion of deletions) { if (deletion.contents.length > 0) { await tx.run( ` MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation) WHERE o.content IN $contents DETACH DELETE o `, { name: deletion.entityName, contents: deletion.contents } ); } } await tx.commit(); const allEntities = await this.getAllEntities(); this.fuse.setCollection(allEntities); } catch (error) { await tx.rollback(); this.logger.error("Error deleting observations", extractError(error)); throw error; } } finally { await session.close(); } } /** * 删除关系 * @param relations 要删除的关系数组 */ async deleteRelations(relations) { if (relations.length === 0) return; const session = await this.getSession(); try { const tx = session.beginTransaction(); try { for (const relation of relations) { await tx.run( ` MATCH (from:Entity {name: $fromName})-[r:${relation.relationType}]->(to:Entity {name: $toName}) DELETE r `, { fromName: relation.from, toName: relation.to } ); } await tx.commit(); } catch (error) { await tx.rollback(); this.logger.error("Error deleting relations", extractError(error)); throw error; } } finally { await session.close(); } } /** * 搜索节点 * @param query 搜索查询 * @returns 包含匹配实体和关系的知识图谱 */ async searchNodes(query) { if (!query || query.trim() === "") { return { entities: [], relations: [] }; } const session = await this.getSession(); try { const searchResult = await session.run( ` CALL db.index.fulltext.queryNodes("entity_fulltext", $query) YIELD node, score RETURN node `, { query } ); const allEntities = await this.getAllEntities(); this.fuse.setCollection(allEntities); const fuseResults = this.fuse.search(query); const uniqueEntities = /* @__PURE__ */ new Map(); for (const record of searchResult.records) { const node = record.get("node"); const name = node.properties.name; if (!uniqueEntities.has(name)) { const entity = allEntities.find((e) => e.name === name); if (entity) { uniqueEntities.set(name, entity); } } } for (const result of fuseResults) { if (!uniqueEntities.has(result.item.name)) { uniqueEntities.set(result.item.name, result.item); } } const entities = Array.from(uniqueEntities.values()); const entityNames = entities.map((entity) => entity.name); if (entityNames.length === 0) { return { entities: [], relations: [] }; } const relationsResult = await session.run( ` MATCH (from:Entity)-[r]->(to:Entity) WHERE from.name IN $names OR to.name IN $names RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType `, { names: entityNames } ); const relations = relationsResult.records.map((record) => { return { from: record.get("fromName"), to: record.get("toName"), relationType: record.get("relationType") }; }); return { entities, relations }; } catch (error) { this.logger.error("Error searching nodes", extractError(error)); return { entities: [], relations: [] }; } finally { await session.close(); } } /** * 打开节点 * @param names 要打开的节点名称数组 * @returns 包含匹配实体和关系的知识图谱 */ async openNodes(names) { if (names.length === 0) { return { entities: [], relations: [] }; } const session = await this.getSession(); try { const entitiesResult = await session.run( ` MATCH (e:Entity) WHERE e.name IN $names OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation) RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations `, { names } ); const entities = entitiesResult.records.map((record) => { return { name: record.get("name"), entityType: record.get("entityType"), observations: record.get("observations").filter(Boolean) }; }); const entityNames = entities.map((entity) => entity.name); if (entityNames.length > 0) { const relationsResult = await session.run( ` MATCH (from:Entity)-[r]->(to:Entity) WHERE from.name IN $names OR to.name IN $names RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType `, { names: entityNames } ); const relations = relationsResult.records.map((record) => { return { from: record.get("fromName"), to: record.get("toName"), relationType: record.get("relationType") }; }); return { entities, relations }; } else { return { entities, relations: [] }; } } catch (error) { this.logger.error("Error opening nodes", extractError(error)); return { entities: [], relations: [] }; } finally { await session.close(); } } /** * 关闭连接 */ async close() { if (this.driver) { await this.driver.close(); this.driver = null; } } }; // src/types.ts import { z } from "zod"; var EntityObject = z.object({ name: z.string().describe("The name of the entity"), entityType: z.string().describe("The type of the entity"), observations: z.array(z.string()).describe("An array of observation contents associated with the entity") }); var RelationObject = z.object({ from: z.string().describe("The name of the entity where the relation starts"), to: z.string().describe("The name of the entity where the relation ends"), relationType: z.string().describe("The type of the relation") }); var ObservationObject = z.object({ entityName: z.string().describe("The name of the entity to add the observations to"), contents: z.array(z.string()).describe("An array of observation contents to add") }); // src/index.ts var server = new McpServer({ name: "neo4j-memory-server", version: "1.0.0" }); var logger = new ConsoleLogger(); logger.setLevel("error" /* ERROR */); var knowledgeGraphManager = new Neo4jKnowledgeGraphManager( /** * 根据环境变量获取Neo4j配置 * @returns Neo4j配置 */ () => { return { uri: process.env.NEO4J_URI || "bolt://localhost:7687", user: process.env.NEO4J_USER || "neo4j", password: process.env.NEO4J_PASSWORD || "password", database: process.env.NEO4J_DATABASE || "neo4j" }; }, logger ); server.tool( "create_entities", "Create multiple new entities in the knowledge graph", { entities: z2.array(EntityObject) }, async ({ entities }) => ({ content: [ { type: "text", text: JSON.stringify( await knowledgeGraphManager.createEntities(entities), null, 2 ) } ] }) ); server.tool( "create_relations", "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", { relations: z2.array(RelationObject) }, async ({ relations }) => ({ content: [ { type: "text", text: JSON.stringify( await knowledgeGraphManager.createRelations(relations), null, 2 ) } ] }) ); server.tool( "add_observations", "Add new observations to existing entities in the knowledge graph", { observations: z2.array(ObservationObject) }, async ({ observations }) => ({ content: [ { type: "text", text: JSON.stringify( await knowledgeGraphManager.addObservations(observations), null, 2 ) } ] }) ); server.tool( "delete_entities", "Delete multiple entities and their associated relations from the knowledge graph", { entityNames: z2.array(z2.string()).describe("An array of entity names to delete") }, async ({ entityNames }) => { await knowledgeGraphManager.deleteEntities(entityNames); return { content: [{ type: "text", text: "Entities deleted successfully" }] }; } ); server.tool( "delete_observations", "Delete specific observations from entities in the knowledge graph", { deletions: z2.array( z2.object({ entityName: z2.string().describe("The name of the entity containing the observations"), contents: z2.array(z2.string()).describe("An array of observations to delete") }) ) }, async ({ deletions }) => { await knowledgeGraphManager.deleteObservations(deletions); return { content: [{ type: "text", text: "Observations deleted successfully" }] }; } ); server.tool( "delete_relations", "Delete multiple relations from the knowledge graph", { relations: z2.array( z2.object({ from: z2.string().describe("The name of the entity where the relation starts"), to: z2.string().describe("The name of the entity where the relation ends"), relationType: z2.string().describe("The type of the relation") }) ).describe("An array of relations to delete") }, async ({ relations }) => { await knowledgeGraphManager.deleteRelations(relations); return { content: [{ type: "text", text: "Relations deleted successfully" }] }; } ); server.tool( "search_nodes", "Search for nodes in the knowledge graph based on a query", { query: z2.string().describe( "The search query to match against entity names, types, and observation content" ) }, async ({ query }) => ({ content: [ { type: "text", text: JSON.stringify( await knowledgeGraphManager.searchNodes(query), null, 2 ) } ] }) ); server.tool( "open_nodes", "Open specific nodes in the knowledge graph by their names", { names: z2.array(z2.string()).describe("An array of entity names to retrieve") }, async ({ names }) => ({ content: [ { type: "text", text: JSON.stringify( await knowledgeGraphManager.openNodes(names), null, 2 ) } ] }) ); var main = async () => { try { await knowledgeGraphManager.initialize(); const transport = new StdioServerTransport(); await server.connect(transport); logger.info("Neo4j Knowledge Graph MCP Server running on stdio"); } catch (error) { logger.error("Failed to start server:", extractError(error)); process.exit(1); } }; main().catch((error) => { logger.error("Error during server startup:", extractError(error)); process.exit(1); });