@mseep/mcp-neo4j-memory-server
Version:
MCP Memory Server with Neo4j backend for AI knowledge graph storage
785 lines (778 loc) • 22.6 kB
JavaScript
// 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);
});