重构了memory.ts,增加了文件写入锁,解决了并行写入导致记忆文件错误的问题; (#4671)

优化了memory.json文件的加载过程,只加载一次,其它涉及图谱的操作均在内存中完成,提高效率;
注意新引入了async-mutex软件包,需要yarn install安装。
This commit is contained in:
robot-AI 2025-04-11 22:03:57 +08:00 committed by GitHub
parent 04333535dd
commit 7c39116351
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 516 additions and 366 deletions

View File

@ -73,6 +73,7 @@
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"async-mutex": "^0.5.0",
"color": "^5.0.0", "color": "^5.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"docx": "^9.0.2", "docx": "^9.0.2",

View File

@ -1,15 +1,14 @@
// port https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts
import { getConfigDir } from '@main/utils/file' import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { Mutex } from 'async-mutex' // 引入 Mutex
// Define memory file path using environment variable with fallback // Define memory file path
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json') const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
// We are storing our memory using entities, relations, and observations in a graph structure // Interfaces remain the same
interface Entity { interface Entity {
name: string name: string
entityType: string entityType: string
@ -22,6 +21,7 @@ interface Relation {
relationType: string relationType: string
} }
// Structure for storing the graph in memory and in the file
interface KnowledgeGraph { interface KnowledgeGraph {
entities: Entity[] entities: Entity[]
relations: Relation[] relations: Relation[]
@ -30,200 +30,304 @@ interface KnowledgeGraph {
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager { class KnowledgeGraphManager {
private memoryPath: string private memoryPath: string
private entities: Map<string, Entity> // Use Map for efficient entity lookup
private relations: Set<string> // Store stringified relations for easy Set operations
private fileMutex: Mutex // Mutex for file writing
constructor(memoryPath: string) { private constructor(memoryPath: string) {
this.memoryPath = memoryPath this.memoryPath = memoryPath
this.ensureMemoryPathExists() this.entities = new Map<string, Entity>()
this.relations = new Set<string>()
this.fileMutex = new Mutex()
} }
private async ensureMemoryPathExists(): Promise<void> { // Static async factory method for initialization
public static async create(memoryPath: string): Promise<KnowledgeGraphManager> {
const manager = new KnowledgeGraphManager(memoryPath)
await manager._ensureMemoryPathExists()
await manager._loadGraphFromDisk()
return manager
}
private async _ensureMemoryPathExists(): Promise<void> {
try { try {
// Ensure the directory exists
const directory = path.dirname(this.memoryPath) const directory = path.dirname(this.memoryPath)
await fs.mkdir(directory, { recursive: true }) await fs.mkdir(directory, { recursive: true })
// Check if the file exists, if not create an empty one
try { try {
await fs.access(this.memoryPath) await fs.access(this.memoryPath)
} catch (error) { } catch (error) {
// File doesn't exist, create an empty file // File doesn't exist, create an empty file with initial structure
await fs.writeFile(this.memoryPath, '') await fs.writeFile(this.memoryPath, JSON.stringify({ entities: [], relations: [] }, null, 2))
} }
} catch (error) { } catch (error) {
console.error('Failed to create memory path:', error) console.error('Failed to ensure memory path exists:', error)
// Propagate the error or handle it more gracefully depending on requirements
throw new McpError(ErrorCode.InternalError, `Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`)
} }
} }
private async loadGraph(): Promise<KnowledgeGraph> { // Load graph from disk into memory (called once during initialization)
private async _loadGraphFromDisk(): Promise<void> {
try { try {
const data = await fs.readFile(this.memoryPath, 'utf-8') const data = await fs.readFile(this.memoryPath, 'utf-8')
const lines = data.split('\n').filter((line) => line.trim() !== '') // Handle empty file case
return lines.reduce( if (data.trim() === '') {
(graph: KnowledgeGraph, line) => { this.entities = new Map()
const item = JSON.parse(line) this.relations = new Set()
if (item.type === 'entity') graph.entities.push(item as Entity) // Optionally write the initial empty structure back
if (item.type === 'relation') graph.relations.push(item as Relation) await this._persistGraph()
return graph return
}, }
{ entities: [], relations: [] } const graph: KnowledgeGraph = JSON.parse(data)
) this.entities.clear()
this.relations.clear()
graph.entities.forEach(entity => this.entities.set(entity.name, entity))
graph.relations.forEach(relation => this.relations.add(this._serializeRelation(relation)))
} catch (error) { } catch (error) {
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
return { entities: [], relations: [] } // File doesn't exist (should have been created by _ensureMemoryPathExists, but handle defensively)
this.entities = new Map()
this.relations = new Set()
await this._persistGraph() // Create the file with empty structure
} else if (error instanceof SyntaxError) {
console.error('Failed to parse memory.json, initializing with empty graph:', error)
// If JSON is invalid, start fresh and overwrite the corrupted file
this.entities = new Map()
this.relations = new Set()
await this._persistGraph()
} else {
console.error('Failed to load knowledge graph from disk:', error)
throw new McpError(ErrorCode.InternalError, `Failed to load graph: ${error instanceof Error ? error.message : String(error)}`)
} }
throw error
} }
} }
private async saveGraph(graph: KnowledgeGraph): Promise<void> { // Persist the current in-memory graph to disk using a mutex
const lines = [ private async _persistGraph(): Promise<void> {
...graph.entities.map((e) => JSON.stringify({ type: 'entity', ...e })), const release = await this.fileMutex.acquire()
...graph.relations.map((r) => JSON.stringify({ type: 'relation', ...r })) try {
] const graphData: KnowledgeGraph = {
await fs.writeFile(this.memoryPath, lines.join('\n')) entities: Array.from(this.entities.values()),
relations: Array.from(this.relations).map(rStr => this._deserializeRelation(rStr))
}
await fs.writeFile(this.memoryPath, JSON.stringify(graphData, null, 2))
} catch (error) {
console.error('Failed to save knowledge graph:', error)
// Decide how to handle write errors - potentially retry or notify
throw new McpError(ErrorCode.InternalError, `Failed to save graph: ${error instanceof Error ? error.message : String(error)}`)
} finally {
release()
}
}
// Helper to consistently serialize relations for Set storage
private _serializeRelation(relation: Relation): string {
// Simple serialization, ensure order doesn't matter if properties are consistent
return JSON.stringify({ from: relation.from, to: relation.to, relationType: relation.relationType })
}
// Helper to deserialize relations from Set storage
private _deserializeRelation(relationStr: string): Relation {
return JSON.parse(relationStr) as Relation
} }
async createEntities(entities: Entity[]): Promise<Entity[]> { async createEntities(entities: Entity[]): Promise<Entity[]> {
const graph = await this.loadGraph() const newEntities: Entity[] = []
const newEntities = entities.filter((e) => !graph.entities.some((existingEntity) => existingEntity.name === e.name)) entities.forEach(entity => {
graph.entities.push(...newEntities) if (!this.entities.has(entity.name)) {
await this.saveGraph(graph) // Ensure observations is always an array
const newEntity = { ...entity, observations: Array.isArray(entity.observations) ? entity.observations : [] };
this.entities.set(entity.name, newEntity)
newEntities.push(newEntity)
}
})
if (newEntities.length > 0) {
await this._persistGraph()
}
return newEntities return newEntities
} }
async createRelations(relations: Relation[]): Promise<Relation[]> { async createRelations(relations: Relation[]): Promise<Relation[]> {
const graph = await this.loadGraph() const newRelations: Relation[] = []
const newRelations = relations.filter( relations.forEach(relation => {
(r) => // Ensure related entities exist before creating a relation
!graph.relations.some( if (!this.entities.has(relation.from) || !this.entities.has(relation.to)) {
(existingRelation) => console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`)
existingRelation.from === r.from && return; // Skip this relation
existingRelation.to === r.to && }
existingRelation.relationType === r.relationType const relationStr = this._serializeRelation(relation)
) if (!this.relations.has(relationStr)) {
) this.relations.add(relationStr)
graph.relations.push(...newRelations) newRelations.push(relation)
await this.saveGraph(graph) }
})
if (newRelations.length > 0) {
await this._persistGraph()
}
return newRelations return newRelations
} }
async addObservations( async addObservations(
observations: { entityName: string; contents: string[] }[] observations: { entityName: string; contents: string[] }[]
): Promise<{ entityName: string; addedObservations: string[] }[]> { ): Promise<{ entityName: string; addedObservations: string[] }[]> {
const graph = await this.loadGraph() const results: { entityName: string; addedObservations: string[] }[] = []
const results = observations.map((o) => { let changed = false
const entity = graph.entities.find((e) => e.name === o.entityName) observations.forEach(o => {
const entity = this.entities.get(o.entityName)
if (!entity) { if (!entity) {
throw new Error(`Entity with name ${o.entityName} not found`) // Option 1: Throw error
throw new McpError(ErrorCode.InvalidParams, `Entity with name ${o.entityName} not found`)
// Option 2: Skip and warn
// console.warn(`Entity with name ${o.entityName} not found when adding observations. Skipping.`);
// return;
} }
const newObservations = o.contents.filter((content) => !entity.observations.includes(content)) // Ensure observations array exists
if (!Array.isArray(entity.observations)) {
entity.observations = [];
}
const newObservations = o.contents.filter(content => !entity.observations.includes(content))
if (newObservations.length > 0) {
entity.observations.push(...newObservations) entity.observations.push(...newObservations)
return { entityName: o.entityName, addedObservations: newObservations } results.push({ entityName: o.entityName, addedObservations: newObservations })
changed = true
} else {
// Still include in results even if nothing was added, to confirm processing
results.push({ entityName: o.entityName, addedObservations: [] })
}
}) })
await this.saveGraph(graph) if (changed) {
await this._persistGraph()
}
return results return results
} }
async deleteEntities(entityNames: string[]): Promise<void> { async deleteEntities(entityNames: string[]): Promise<void> {
const graph = await this.loadGraph() let changed = false
graph.entities = graph.entities.filter((e) => !entityNames.includes(e.name)) const namesToDelete = new Set(entityNames)
graph.relations = graph.relations.filter((r) => !entityNames.includes(r.from) && !entityNames.includes(r.to))
await this.saveGraph(graph) // Delete entities
namesToDelete.forEach(name => {
if (this.entities.delete(name)) {
changed = true
}
})
// Delete relations involving deleted entities
const relationsToDelete = new Set<string>()
this.relations.forEach(relStr => {
const rel = this._deserializeRelation(relStr)
if (namesToDelete.has(rel.from) || namesToDelete.has(rel.to)) {
relationsToDelete.add(relStr)
}
})
relationsToDelete.forEach(relStr => {
if (this.relations.delete(relStr)) {
changed = true
}
})
if (changed) {
await this._persistGraph()
}
} }
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> { async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
const graph = await this.loadGraph() let changed = false
deletions.forEach((d) => { deletions.forEach(d => {
const entity = graph.entities.find((e) => e.name === d.entityName) const entity = this.entities.get(d.entityName)
if (entity) { if (entity && Array.isArray(entity.observations)) {
entity.observations = entity.observations.filter((o) => !d.observations.includes(o)) const initialLength = entity.observations.length
const observationsToDelete = new Set(d.observations)
entity.observations = entity.observations.filter(o => !observationsToDelete.has(o))
if (entity.observations.length !== initialLength) {
changed = true
}
} }
}) })
await this.saveGraph(graph) if (changed) {
await this._persistGraph()
}
} }
async deleteRelations(relations: Relation[]): Promise<void> { async deleteRelations(relations: Relation[]): Promise<void> {
const graph = await this.loadGraph() let changed = false
graph.relations = graph.relations.filter( relations.forEach(rel => {
(r) => const relStr = this._serializeRelation(rel)
!relations.some( if (this.relations.delete(relStr)) {
(delRelation) => changed = true
r.from === delRelation.from && r.to === delRelation.to && r.relationType === delRelation.relationType }
) })
) if (changed) {
await this.saveGraph(graph) await this._persistGraph()
}
} }
// Read the current state from memory
async readGraph(): Promise<KnowledgeGraph> { async readGraph(): Promise<KnowledgeGraph> {
return this.loadGraph() // Return a deep copy to prevent external modification of the internal state
return JSON.parse(JSON.stringify({
entities: Array.from(this.entities.values()),
relations: Array.from(this.relations).map(rStr => this._deserializeRelation(rStr))
}));
} }
// Very basic search function // Search operates on the in-memory graph
async searchNodes(query: string): Promise<KnowledgeGraph> { async searchNodes(query: string): Promise<KnowledgeGraph> {
const graph = await this.loadGraph() const lowerCaseQuery = query.toLowerCase()
const filteredEntities = Array.from(this.entities.values()).filter(
// Filter entities e =>
const filteredEntities = graph.entities.filter( e.name.toLowerCase().includes(lowerCaseQuery) ||
(e) => e.entityType.toLowerCase().includes(lowerCaseQuery) ||
e.name.toLowerCase().includes(query.toLowerCase()) || (Array.isArray(e.observations) && e.observations.some(o => o.toLowerCase().includes(lowerCaseQuery)))
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
e.observations.some((o) => o.toLowerCase().includes(query.toLowerCase()))
) )
// Create a Set of filtered entity names for quick lookup const filteredEntityNames = new Set(filteredEntities.map(e => e.name))
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
// Filter relations to only include those between filtered entities const filteredRelations = Array.from(this.relations)
const filteredRelations = graph.relations.filter( .map(rStr => this._deserializeRelation(rStr))
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) .filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to))
)
const filteredGraph: KnowledgeGraph = { return {
entities: filteredEntities, entities: filteredEntities,
relations: filteredRelations relations: filteredRelations
} }
return filteredGraph
} }
// Open operates on the in-memory graph
async openNodes(names: string[]): Promise<KnowledgeGraph> { async openNodes(names: string[]): Promise<KnowledgeGraph> {
const graph = await this.loadGraph() const nameSet = new Set(names);
const filteredEntities = Array.from(this.entities.values()).filter(e => nameSet.has(e.name));
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter entities const filteredRelations = Array.from(this.relations)
const filteredEntities = graph.entities.filter((e) => names.includes(e.name)) .map(rStr => this._deserializeRelation(rStr))
.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
// Create a Set of filtered entity names for quick lookup return {
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
)
const filteredGraph: KnowledgeGraph = {
entities: filteredEntities, entities: filteredEntities,
relations: filteredRelations relations: filteredRelations
} };
return filteredGraph
} }
} }
class MemoryServer { class MemoryServer {
public server: Server public server: Server
private knowledgeGraphManager: KnowledgeGraphManager // Hold the manager instance, initialized asynchronously
private knowledgeGraphManager: KnowledgeGraphManager | null = null;
private initializationPromise: Promise<void>; // To track initialization
constructor(envPath: string = '') { constructor(envPath: string = '') {
const memoryPath = envPath const memoryPath = envPath
? path.isAbsolute(envPath) ? path.isAbsolute(envPath)
? envPath ? envPath
: path.join(path.dirname(fileURLToPath(import.meta.url)), envPath) : path.resolve(envPath) // Use path.resolve for relative paths based on CWD
: defaultMemoryPath : defaultMemoryPath
this.knowledgeGraphManager = new KnowledgeGraphManager(memoryPath)
this.server = new Server( this.server = new Server(
{ {
name: 'memory-server', name: 'memory-server',
version: '1.0.0' version: '1.1.0' // Incremented version for changes
}, },
{ {
capabilities: { capabilities: {
@ -231,17 +335,54 @@ class MemoryServer {
} }
} }
) )
this.initialize() // Start initialization, but don't block constructor
this.initializationPromise = this._initializeManager(memoryPath);
this.setupRequestHandlers(); // Setup handlers immediately
} }
initialize() { // Private async method to handle manager initialization
// The server instance and tools exposed to Claude private async _initializeManager(memoryPath: string): Promise<void> {
try {
this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath);
console.log("KnowledgeGraphManager initialized successfully.");
} catch (error) {
console.error("Failed to initialize KnowledgeGraphManager:", error);
// Server might be unusable, consider how to handle this state
// Maybe set a flag and return errors for all tool calls?
this.knowledgeGraphManager = null; // Ensure it's null if init fails
}
}
// Ensures the manager is initialized before handling tool calls
private async _getManager(): Promise<KnowledgeGraphManager> {
await this.initializationPromise; // Wait for initialization to complete
if (!this.knowledgeGraphManager) {
throw new McpError(ErrorCode.InternalError, "Memory server failed to initialize. Cannot process requests.");
}
return this.knowledgeGraphManager;
}
// Setup handlers (can be called from constructor)
setupRequestHandlers() {
// ListTools remains largely the same, descriptions might be updated if needed
this.server.setRequestHandler(ListToolsRequestSchema, async () => { this.server.setRequestHandler(ListToolsRequestSchema, async () => {
// Ensure manager is ready before listing tools that depend on it
// Although ListTools itself doesn't *call* the manager, it implies the
// manager is ready to handle calls for those tools.
try {
await this._getManager(); // Wait for initialization before confirming tools are available
} catch (error) {
// If manager failed to init, maybe return an empty tool list or throw?
console.error("Cannot list tools, manager initialization failed:", error);
return { tools: [] }; // Return empty list if server is not ready
}
return { return {
tools: [ tools: [
{ {
name: 'create_entities', name: 'create_entities',
description: 'Create multiple new entities in the knowledge graph', description: 'Create multiple new entities in the knowledge graph. Skips existing entities.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -255,10 +396,11 @@ class MemoryServer {
observations: { observations: {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'An array of observation contents associated with the entity' description: 'An array of observation contents associated with the entity',
default: [] // Add default empty array
} }
}, },
required: ['name', 'entityType', 'observations'] required: ['name', 'entityType'] // Observations are optional now on creation
} }
} }
}, },
@ -267,8 +409,7 @@ class MemoryServer {
}, },
{ {
name: 'create_relations', name: 'create_relations',
description: description: 'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.',
'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -290,7 +431,7 @@ class MemoryServer {
}, },
{ {
name: 'add_observations', name: 'add_observations',
description: 'Add new observations to existing entities in the knowledge graph', description: 'Add new observations to existing entities. Skips duplicate observations.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -315,7 +456,7 @@ class MemoryServer {
}, },
{ {
name: 'delete_entities', name: 'delete_entities',
description: 'Delete multiple entities and their associated relations from the knowledge graph', description: 'Delete multiple entities and their associated relations.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -330,7 +471,7 @@ class MemoryServer {
}, },
{ {
name: 'delete_observations', name: 'delete_observations',
description: 'Delete specific observations from entities in the knowledge graph', description: 'Delete specific observations from entities.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -355,7 +496,7 @@ class MemoryServer {
}, },
{ {
name: 'delete_relations', name: 'delete_relations',
description: 'Delete multiple relations from the knowledge graph', description: 'Delete multiple specific relations.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -378,7 +519,7 @@ class MemoryServer {
}, },
{ {
name: 'read_graph', name: 'read_graph',
description: 'Read the entire knowledge graph', description: 'Read the entire knowledge graph from memory.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: {} properties: {}
@ -386,7 +527,7 @@ class MemoryServer {
}, },
{ {
name: 'search_nodes', name: 'search_nodes',
description: 'Search for nodes in the knowledge graph based on a query', description: 'Search nodes (entities and relations) in memory based on a query.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -400,7 +541,7 @@ class MemoryServer {
}, },
{ {
name: 'open_nodes', name: 'open_nodes',
description: 'Open specific nodes in the knowledge graph by their names', description: 'Retrieve specific entities and their connecting relations from memory by name.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@ -417,90 +558,88 @@ class MemoryServer {
} }
}) })
// CallTool handler needs to await the manager and the async methods
this.server.setRequestHandler(CallToolRequestSchema, async (request) => { this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const manager = await this._getManager(); // Ensure manager is ready
const { name, arguments: args } = request.params const { name, arguments: args } = request.params
if (!args) { if (!args) {
throw new Error(`No arguments provided for tool: ${name}`) // Use McpError for standard errors
throw new McpError(ErrorCode.InvalidParams, `No arguments provided for tool: ${name}`)
} }
try {
switch (name) { switch (name) {
case 'create_entities': case 'create_entities':
return { // Validate args structure if necessary, though SDK might do basic validation
content: [ if (!args.entities || !Array.isArray(args.entities)) {
{ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'entities' array is required.`);
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.createEntities(args.entities as Entity[]),
null,
2
)
} }
] return {
content: [{ type: 'text', text: JSON.stringify(await manager.createEntities(args.entities as Entity[]), null, 2) }]
} }
case 'create_relations': case 'create_relations':
return { if (!args.relations || !Array.isArray(args.relations)) {
content: [ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'relations' array is required.`);
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.createRelations(args.relations as Relation[]),
null,
2
)
} }
] return {
content: [{ type: 'text', text: JSON.stringify(await manager.createRelations(args.relations as Relation[]), null, 2) }]
} }
case 'add_observations': case 'add_observations':
return { if (!args.observations || !Array.isArray(args.observations)) {
content: [ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'observations' array is required.`);
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.addObservations(
args.observations as { entityName: string; contents: string[] }[]
),
null,
2
)
} }
] return {
content: [{ type: 'text', text: JSON.stringify(await manager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }]
} }
case 'delete_entities': case 'delete_entities':
await this.knowledgeGraphManager.deleteEntities(args.entityNames as string[]) if (!args.entityNames || !Array.isArray(args.entityNames)) {
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'entityNames' array is required.`);
}
await manager.deleteEntities(args.entityNames as string[])
return { content: [{ type: 'text', text: 'Entities deleted successfully' }] } return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
case 'delete_observations': case 'delete_observations':
await this.knowledgeGraphManager.deleteObservations( if (!args.deletions || !Array.isArray(args.deletions)) {
args.deletions as { entityName: string; observations: string[] }[] throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'deletions' array is required.`);
) }
await manager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[])
return { content: [{ type: 'text', text: 'Observations deleted successfully' }] } return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
case 'delete_relations': case 'delete_relations':
await this.knowledgeGraphManager.deleteRelations(args.relations as Relation[]) if (!args.relations || !Array.isArray(args.relations)) {
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'relations' array is required.`);
}
await manager.deleteRelations(args.relations as Relation[])
return { content: [{ type: 'text', text: 'Relations deleted successfully' }] } return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
case 'read_graph': case 'read_graph':
// No arguments expected or needed for read_graph based on original schema
return { return {
content: [{ type: 'text', text: JSON.stringify(await this.knowledgeGraphManager.readGraph(), null, 2) }] content: [{ type: 'text', text: JSON.stringify(await manager.readGraph(), null, 2) }]
} }
case 'search_nodes': case 'search_nodes':
return { if (typeof args.query !== 'string') {
content: [ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'query' string is required.`);
{
type: 'text',
text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2)
} }
] return {
content: [{ type: 'text', text: JSON.stringify(await manager.searchNodes(args.query as string), null, 2) }]
} }
case 'open_nodes': case 'open_nodes':
return { if (!args.names || !Array.isArray(args.names)) {
content: [ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'names' array is required.`);
{
type: 'text',
text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2)
} }
] return {
content: [{ type: 'text', text: JSON.stringify(await manager.openNodes(args.names as string[]), null, 2) }]
} }
default: default:
throw new Error(`Unknown tool: ${name}`) throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
}
} catch (error) {
// Catch errors from manager methods (like entity not found) or other issues
if (error instanceof McpError) {
throw error; // Re-throw McpErrors directly
}
console.error(`Error executing tool ${name}:`, error);
// Throw a generic internal error for unexpected issues
throw new McpError(ErrorCode.InternalError, `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`);
} }
}) })
} }

View File

@ -3942,6 +3942,7 @@ __metadata:
analytics: "npm:^0.8.16" analytics: "npm:^0.8.16"
antd: "npm:^5.22.5" antd: "npm:^5.22.5"
applescript: "npm:^1.0.0" applescript: "npm:^1.0.0"
async-mutex: "npm:^0.5.0"
axios: "npm:^1.7.3" axios: "npm:^1.7.3"
babel-plugin-styled-components: "npm:^2.1.4" babel-plugin-styled-components: "npm:^2.1.4"
browser-image-compression: "npm:^2.0.2" browser-image-compression: "npm:^2.0.2"
@ -4477,6 +4478,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"async-mutex@npm:^0.5.0":
version: 0.5.0
resolution: "async-mutex@npm:0.5.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/9096e6ad6b674c894d8ddd5aa4c512b09bb05931b8746ebd634952b05685608b2b0820ed5c406e6569919ff5fe237ab3c491e6f2887d6da6b6ba906db3ee9c32
languageName: node
linkType: hard
"async@npm:^3.2.3": "async@npm:^3.2.3":
version: 3.2.6 version: 3.2.6
resolution: "async@npm:3.2.6" resolution: "async@npm:3.2.6"