diff --git a/package.json b/package.json index f7f007ec..c5e391c4 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@types/react-infinite-scroll-component": "^5.0.0", "@xyflow/react": "^12.4.4", "adm-zip": "^0.5.16", + "async-mutex": "^0.5.0", "color": "^5.0.0", "diff": "^7.0.0", "docx": "^9.0.2", diff --git a/src/main/mcpServers/memory.ts b/src/main/mcpServers/memory.ts index cee7e15d..9b4d2d4c 100644 --- a/src/main/mcpServers/memory.ts +++ b/src/main/mcpServers/memory.ts @@ -1,15 +1,14 @@ -// port https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts import { getConfigDir } from '@main/utils/file' 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 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') -// We are storing our memory using entities, relations, and observations in a graph structure +// Interfaces remain the same interface Entity { name: string entityType: string @@ -22,6 +21,7 @@ interface Relation { relationType: string } +// Structure for storing the graph in memory and in the file interface KnowledgeGraph { entities: Entity[] relations: Relation[] @@ -30,200 +30,304 @@ interface KnowledgeGraph { // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph class KnowledgeGraphManager { private memoryPath: string + private entities: Map // Use Map for efficient entity lookup + private relations: Set // 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.ensureMemoryPathExists() + this.entities = new Map() + this.relations = new Set() + this.fileMutex = new Mutex() } - private async ensureMemoryPathExists(): Promise { + // Static async factory method for initialization + public static async create(memoryPath: string): Promise { + const manager = new KnowledgeGraphManager(memoryPath) + await manager._ensureMemoryPathExists() + await manager._loadGraphFromDisk() + return manager + } + + private async _ensureMemoryPathExists(): Promise { try { - // Ensure the directory exists const directory = path.dirname(this.memoryPath) await fs.mkdir(directory, { recursive: true }) - - // Check if the file exists, if not create an empty one try { await fs.access(this.memoryPath) } catch (error) { - // File doesn't exist, create an empty file - await fs.writeFile(this.memoryPath, '') + // File doesn't exist, create an empty file with initial structure + await fs.writeFile(this.memoryPath, JSON.stringify({ entities: [], relations: [] }, null, 2)) } } 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 { + // Load graph from disk into memory (called once during initialization) + private async _loadGraphFromDisk(): Promise { try { const data = await fs.readFile(this.memoryPath, 'utf-8') - const lines = data.split('\n').filter((line) => line.trim() !== '') - return lines.reduce( - (graph: KnowledgeGraph, line) => { - const item = JSON.parse(line) - if (item.type === 'entity') graph.entities.push(item as Entity) - if (item.type === 'relation') graph.relations.push(item as Relation) - return graph - }, - { entities: [], relations: [] } - ) + // Handle empty file case + if (data.trim() === '') { + this.entities = new Map() + this.relations = new Set() + // Optionally write the initial empty structure back + await this._persistGraph() + return + } + 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) { 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 { - const lines = [ - ...graph.entities.map((e) => JSON.stringify({ type: 'entity', ...e })), - ...graph.relations.map((r) => JSON.stringify({ type: 'relation', ...r })) - ] - await fs.writeFile(this.memoryPath, lines.join('\n')) + // Persist the current in-memory graph to disk using a mutex + private async _persistGraph(): Promise { + const release = await this.fileMutex.acquire() + try { + const graphData: KnowledgeGraph = { + 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 { - const graph = await this.loadGraph() - const newEntities = entities.filter((e) => !graph.entities.some((existingEntity) => existingEntity.name === e.name)) - graph.entities.push(...newEntities) - await this.saveGraph(graph) + const newEntities: Entity[] = [] + entities.forEach(entity => { + if (!this.entities.has(entity.name)) { + // 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 } async createRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph() - const newRelations = relations.filter( - (r) => - !graph.relations.some( - (existingRelation) => - existingRelation.from === r.from && - existingRelation.to === r.to && - existingRelation.relationType === r.relationType - ) - ) - graph.relations.push(...newRelations) - await this.saveGraph(graph) + const newRelations: Relation[] = [] + relations.forEach(relation => { + // Ensure related entities exist before creating a relation + if (!this.entities.has(relation.from) || !this.entities.has(relation.to)) { + console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`) + return; // Skip this relation + } + const relationStr = this._serializeRelation(relation) + if (!this.relations.has(relationStr)) { + this.relations.add(relationStr) + newRelations.push(relation) + } + }) + if (newRelations.length > 0) { + await this._persistGraph() + } return newRelations } async addObservations( observations: { entityName: string; contents: string[] }[] ): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph() - const results = observations.map((o) => { - const entity = graph.entities.find((e) => e.name === o.entityName) + const results: { entityName: string; addedObservations: string[] }[] = [] + let changed = false + observations.forEach(o => { + const entity = this.entities.get(o.entityName) 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; + } + // 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) + 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: [] }) } - const newObservations = o.contents.filter((content) => !entity.observations.includes(content)) - entity.observations.push(...newObservations) - return { entityName: o.entityName, addedObservations: newObservations } }) - await this.saveGraph(graph) + if (changed) { + await this._persistGraph() + } return results } async deleteEntities(entityNames: string[]): Promise { - const graph = await this.loadGraph() - graph.entities = graph.entities.filter((e) => !entityNames.includes(e.name)) - graph.relations = graph.relations.filter((r) => !entityNames.includes(r.from) && !entityNames.includes(r.to)) - await this.saveGraph(graph) + let changed = false + const namesToDelete = new Set(entityNames) + + // Delete entities + namesToDelete.forEach(name => { + if (this.entities.delete(name)) { + changed = true + } + }) + + // Delete relations involving deleted entities + const relationsToDelete = new Set() + 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 { - const graph = await this.loadGraph() - deletions.forEach((d) => { - const entity = graph.entities.find((e) => e.name === d.entityName) - if (entity) { - entity.observations = entity.observations.filter((o) => !d.observations.includes(o)) + let changed = false + deletions.forEach(d => { + const entity = this.entities.get(d.entityName) + if (entity && Array.isArray(entity.observations)) { + 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 { - const graph = await this.loadGraph() - graph.relations = graph.relations.filter( - (r) => - !relations.some( - (delRelation) => - r.from === delRelation.from && r.to === delRelation.to && r.relationType === delRelation.relationType - ) - ) - await this.saveGraph(graph) + let changed = false + relations.forEach(rel => { + const relStr = this._serializeRelation(rel) + if (this.relations.delete(relStr)) { + changed = true + } + }) + if (changed) { + await this._persistGraph() + } } + // Read the current state from memory async readGraph(): Promise { - 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 { - const graph = await this.loadGraph() - - // Filter entities - const filteredEntities = graph.entities.filter( - (e) => - e.name.toLowerCase().includes(query.toLowerCase()) || - e.entityType.toLowerCase().includes(query.toLowerCase()) || - e.observations.some((o) => o.toLowerCase().includes(query.toLowerCase())) + const lowerCaseQuery = query.toLowerCase() + const filteredEntities = Array.from(this.entities.values()).filter( + e => + e.name.toLowerCase().includes(lowerCaseQuery) || + e.entityType.toLowerCase().includes(lowerCaseQuery) || + (Array.isArray(e.observations) && e.observations.some(o => o.toLowerCase().includes(lowerCaseQuery))) ) - // 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 = graph.relations.filter( - (r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to) - ) + const filteredRelations = Array.from(this.relations) + .map(rStr => this._deserializeRelation(rStr)) + .filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)) - const filteredGraph: KnowledgeGraph = { + return { entities: filteredEntities, relations: filteredRelations } - - return filteredGraph } + // Open operates on the in-memory graph async openNodes(names: string[]): Promise { - 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 filteredEntities = graph.entities.filter((e) => names.includes(e.name)) + const filteredRelations = Array.from(this.relations) + .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 - 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, - relations: filteredRelations - } - - return filteredGraph + return { + entities: filteredEntities, + relations: filteredRelations + }; } } class MemoryServer { public server: Server - private knowledgeGraphManager: KnowledgeGraphManager + // Hold the manager instance, initialized asynchronously + private knowledgeGraphManager: KnowledgeGraphManager | null = null; + private initializationPromise: Promise; // To track initialization constructor(envPath: string = '') { const memoryPath = envPath ? path.isAbsolute(envPath) ? envPath - : path.join(path.dirname(fileURLToPath(import.meta.url)), envPath) + : path.resolve(envPath) // Use path.resolve for relative paths based on CWD : defaultMemoryPath - this.knowledgeGraphManager = new KnowledgeGraphManager(memoryPath) + this.server = new Server( { name: 'memory-server', - version: '1.0.0' + version: '1.1.0' // Incremented version for changes }, { capabilities: { @@ -231,276 +335,311 @@ class MemoryServer { } } ) - this.initialize() + // Start initialization, but don't block constructor + this.initializationPromise = this._initializeManager(memoryPath); + this.setupRequestHandlers(); // Setup handlers immediately } - initialize() { - // The server instance and tools exposed to Claude + // Private async method to handle manager initialization + private async _initializeManager(memoryPath: string): Promise { + 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 { + 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 () => { + // 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 { tools: [ - { - name: 'create_entities', - description: 'Create multiple new entities in the knowledge graph', - inputSchema: { - type: 'object', - properties: { - entities: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string', description: 'The name of the entity' }, - entityType: { type: 'string', description: 'The type of the entity' }, - observations: { - type: 'array', - items: { type: 'string' }, - description: 'An array of observation contents associated with the entity' - } - }, - required: ['name', 'entityType', 'observations'] + { + name: 'create_entities', + description: 'Create multiple new entities in the knowledge graph. Skips existing entities.', + inputSchema: { + type: 'object', + properties: { + entities: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'The name of the entity' }, + entityType: { type: 'string', description: 'The type of the entity' }, + observations: { + type: 'array', + items: { type: 'string' }, + description: 'An array of observation contents associated with the entity', + default: [] // Add default empty array + } + }, + required: ['name', 'entityType'] // Observations are optional now on creation + } + } + }, + required: ['entities'] + } + }, + { + name: 'create_relations', + description: 'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.', + inputSchema: { + type: 'object', + properties: { + relations: { + type: 'array', + items: { + type: 'object', + properties: { + from: { type: 'string', description: 'The name of the entity where the relation starts' }, + to: { type: 'string', description: 'The name of the entity where the relation ends' }, + relationType: { type: 'string', description: 'The type of the relation' } + }, + required: ['from', 'to', 'relationType'] + } } - } - }, - required: ['entities'] - } - }, - { - name: 'create_relations', - description: - 'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice', - inputSchema: { - type: 'object', - properties: { - relations: { - type: 'array', - items: { - type: 'object', - properties: { - from: { type: 'string', description: 'The name of the entity where the relation starts' }, - to: { type: 'string', description: 'The name of the entity where the relation ends' }, - relationType: { type: 'string', description: 'The type of the relation' } - }, - required: ['from', 'to', 'relationType'] + }, + required: ['relations'] + } + }, + { + name: 'add_observations', + description: 'Add new observations to existing entities. Skips duplicate observations.', + inputSchema: { + type: 'object', + properties: { + observations: { + type: 'array', + items: { + type: 'object', + properties: { + entityName: { type: 'string', description: 'The name of the entity to add the observations to' }, + contents: { + type: 'array', + items: { type: 'string' }, + description: 'An array of observation contents to add' + } + }, + required: ['entityName', 'contents'] + } } - } - }, - required: ['relations'] - } - }, - { - name: '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: 'The name of the entity to add the observations to' }, - contents: { - type: 'array', - items: { type: 'string' }, - description: 'An array of observation contents to add' - } - }, - required: ['entityName', 'contents'] + }, + required: ['observations'] + } + }, + { + name: 'delete_entities', + description: 'Delete multiple entities and their associated relations.', + inputSchema: { + type: 'object', + properties: { + entityNames: { + type: 'array', + items: { type: 'string' }, + description: 'An array of entity names to delete' } - } - }, - required: ['observations'] - } - }, - { - name: 'delete_entities', - description: 'Delete multiple entities and their associated relations from the knowledge graph', - inputSchema: { - type: 'object', - properties: { - entityNames: { - type: 'array', - items: { type: 'string' }, - description: 'An array of entity names to delete' - } - }, - required: ['entityNames'] - } - }, - { - name: '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: 'The name of the entity containing the observations' }, - observations: { - type: 'array', - items: { type: 'string' }, - description: 'An array of observations to delete' - } - }, - required: ['entityName', 'observations'] + }, + required: ['entityNames'] + } + }, + { + name: 'delete_observations', + description: 'Delete specific observations from entities.', + inputSchema: { + type: 'object', + properties: { + deletions: { + type: 'array', + items: { + type: 'object', + properties: { + entityName: { type: 'string', description: 'The name of the entity containing the observations' }, + observations: { + type: 'array', + items: { type: 'string' }, + description: 'An array of observations to delete' + } + }, + required: ['entityName', 'observations'] + } } - } - }, - required: ['deletions'] - } - }, - { - name: 'delete_relations', - description: 'Delete multiple relations from the knowledge graph', - inputSchema: { - type: 'object', - properties: { - relations: { - type: 'array', - items: { - type: 'object', - properties: { - from: { type: 'string', description: 'The name of the entity where the relation starts' }, - to: { type: 'string', description: 'The name of the entity where the relation ends' }, - relationType: { type: 'string', description: 'The type of the relation' } + }, + required: ['deletions'] + } + }, + { + name: 'delete_relations', + description: 'Delete multiple specific relations.', + inputSchema: { + type: 'object', + properties: { + relations: { + type: 'array', + items: { + type: 'object', + properties: { + from: { type: 'string', description: 'The name of the entity where the relation starts' }, + to: { type: 'string', description: 'The name of the entity where the relation ends' }, + relationType: { type: 'string', description: 'The type of the relation' } + }, + required: ['from', 'to', 'relationType'] }, - required: ['from', 'to', 'relationType'] - }, - description: 'An array of relations to delete' - } - }, - required: ['relations'] - } - }, - { - name: 'read_graph', - description: 'Read the entire knowledge graph', - inputSchema: { - type: 'object', - properties: {} - } - }, - { - name: 'search_nodes', - description: 'Search for nodes in the knowledge graph based on a query', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The search query to match against entity names, types, and observation content' - } - }, - required: ['query'] - } - }, - { - name: 'open_nodes', - description: 'Open specific nodes in the knowledge graph by their names', - inputSchema: { - type: 'object', - properties: { - names: { - type: 'array', - items: { type: 'string' }, - description: 'An array of entity names to retrieve' - } - }, - required: ['names'] - } - } - ] + description: 'An array of relations to delete' + } + }, + required: ['relations'] + } + }, + { + name: 'read_graph', + description: 'Read the entire knowledge graph from memory.', + inputSchema: { + type: 'object', + properties: {} + } + }, + { + name: 'search_nodes', + description: 'Search nodes (entities and relations) in memory based on a query.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The search query to match against entity names, types, and observation content' + } + }, + required: ['query'] + } + }, + { + name: 'open_nodes', + description: 'Retrieve specific entities and their connecting relations from memory by name.', + inputSchema: { + type: 'object', + properties: { + names: { + type: 'array', + items: { type: 'string' }, + description: 'An array of entity names to retrieve' + } + }, + required: ['names'] + } + } + ] } }) + // CallTool handler needs to await the manager and the async methods this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const manager = await this._getManager(); // Ensure manager is ready const { name, arguments: args } = request.params 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}`) } - switch (name) { - case 'create_entities': - return { - content: [ - { - type: 'text', - text: JSON.stringify( - await this.knowledgeGraphManager.createEntities(args.entities as Entity[]), - null, - 2 - ) - } - ] + try { + switch (name) { + case 'create_entities': + // Validate args structure if necessary, though SDK might do basic validation + if (!args.entities || !Array.isArray(args.entities)) { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'entities' array is required.`); + } + return { + content: [{ type: 'text', text: JSON.stringify(await manager.createEntities(args.entities as Entity[]), null, 2) }] + } + case 'create_relations': + if (!args.relations || !Array.isArray(args.relations)) { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'relations' array is required.`); + } + return { + content: [{ type: 'text', text: JSON.stringify(await manager.createRelations(args.relations as Relation[]), null, 2) }] + } + case 'add_observations': + if (!args.observations || !Array.isArray(args.observations)) { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'observations' array is required.`); + } + return { + content: [{ type: 'text', text: JSON.stringify(await manager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] + } + case 'delete_entities': + 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' }] } + case 'delete_observations': + if (!args.deletions || !Array.isArray(args.deletions)) { + 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' }] } + case 'delete_relations': + 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' }] } + case 'read_graph': + // No arguments expected or needed for read_graph based on original schema + return { + content: [{ type: 'text', text: JSON.stringify(await manager.readGraph(), null, 2) }] + } + case 'search_nodes': + if (typeof args.query !== 'string') { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'query' string is required.`); + } + return { + content: [{ type: 'text', text: JSON.stringify(await manager.searchNodes(args.query as string), null, 2) }] + } + case 'open_nodes': + if (!args.names || !Array.isArray(args.names)) { + throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'names' array is required.`); + } + return { + content: [{ type: 'text', text: JSON.stringify(await manager.openNodes(args.names as string[]), null, 2) }] + } + default: + 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 } - case 'create_relations': - return { - content: [ - { - type: 'text', - text: JSON.stringify( - await this.knowledgeGraphManager.createRelations(args.relations as Relation[]), - null, - 2 - ) - } - ] - } - case 'add_observations': - return { - content: [ - { - type: 'text', - text: JSON.stringify( - await this.knowledgeGraphManager.addObservations( - args.observations as { entityName: string; contents: string[] }[] - ), - null, - 2 - ) - } - ] - } - case 'delete_entities': - await this.knowledgeGraphManager.deleteEntities(args.entityNames as string[]) - return { content: [{ type: 'text', text: 'Entities deleted successfully' }] } - case 'delete_observations': - await this.knowledgeGraphManager.deleteObservations( - args.deletions as { entityName: string; observations: string[] }[] - ) - return { content: [{ type: 'text', text: 'Observations deleted successfully' }] } - case 'delete_relations': - await this.knowledgeGraphManager.deleteRelations(args.relations as Relation[]) - return { content: [{ type: 'text', text: 'Relations deleted successfully' }] } - case 'read_graph': - return { - content: [{ type: 'text', text: JSON.stringify(await this.knowledgeGraphManager.readGraph(), null, 2) }] - } - case 'search_nodes': - return { - content: [ - { - type: 'text', - text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2) - } - ] - } - case 'open_nodes': - return { - content: [ - { - type: 'text', - text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2) - } - ] - } - default: - throw new Error(`Unknown tool: ${name}`) + 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)}`); } }) } diff --git a/yarn.lock b/yarn.lock index 45976d17..03f1f521 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3942,6 +3942,7 @@ __metadata: analytics: "npm:^0.8.16" antd: "npm:^5.22.5" applescript: "npm:^1.0.0" + async-mutex: "npm:^0.5.0" axios: "npm:^1.7.3" babel-plugin-styled-components: "npm:^2.1.4" browser-image-compression: "npm:^2.0.2" @@ -4477,6 +4478,15 @@ __metadata: languageName: node 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": version: 3.2.6 resolution: "async@npm:3.2.6"