重构了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",
"@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",

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 { 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<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.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 {
// 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<KnowledgeGraph> {
// Load graph from disk into memory (called once during initialization)
private async _loadGraphFromDisk(): Promise<void> {
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<void> {
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<void> {
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<Entity[]> {
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<Relation[]> {
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;
}
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)
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
}
async deleteEntities(entityNames: string[]): Promise<void> {
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<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> {
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<void> {
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<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> {
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<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 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 = {
return {
entities: filteredEntities,
relations: filteredRelations
}
return filteredGraph
};
}
}
class MemoryServer {
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 = '') {
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,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() {
// The server instance and tools exposed to Claude
// Private async method to handle manager initialization
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 () => {
// 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',
description: 'Create multiple new entities in the knowledge graph. Skips existing entities.',
inputSchema: {
type: 'object',
properties: {
@ -255,10 +396,11 @@ class MemoryServer {
observations: {
type: 'array',
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',
description:
'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
description: 'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.',
inputSchema: {
type: 'object',
properties: {
@ -290,7 +431,7 @@ class MemoryServer {
},
{
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: {
type: 'object',
properties: {
@ -315,7 +456,7 @@ class MemoryServer {
},
{
name: 'delete_entities',
description: 'Delete multiple entities and their associated relations from the knowledge graph',
description: 'Delete multiple entities and their associated relations.',
inputSchema: {
type: 'object',
properties: {
@ -330,7 +471,7 @@ class MemoryServer {
},
{
name: 'delete_observations',
description: 'Delete specific observations from entities in the knowledge graph',
description: 'Delete specific observations from entities.',
inputSchema: {
type: 'object',
properties: {
@ -355,7 +496,7 @@ class MemoryServer {
},
{
name: 'delete_relations',
description: 'Delete multiple relations from the knowledge graph',
description: 'Delete multiple specific relations.',
inputSchema: {
type: 'object',
properties: {
@ -378,7 +519,7 @@ class MemoryServer {
},
{
name: 'read_graph',
description: 'Read the entire knowledge graph',
description: 'Read the entire knowledge graph from memory.',
inputSchema: {
type: 'object',
properties: {}
@ -386,7 +527,7 @@ class MemoryServer {
},
{
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: {
type: 'object',
properties: {
@ -400,7 +541,7 @@ class MemoryServer {
},
{
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: {
type: 'object',
properties: {
@ -417,90 +558,88 @@ class MemoryServer {
}
})
// 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}`)
}
try {
switch (name) {
case 'create_entities':
return {
content: [
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.createEntities(args.entities as Entity[]),
null,
2
)
// 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':
return {
content: [
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.createRelations(args.relations as Relation[]),
null,
2
)
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':
return {
content: [
{
type: 'text',
text: JSON.stringify(
await this.knowledgeGraphManager.addObservations(
args.observations as { entityName: string; contents: string[] }[]
),
null,
2
)
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':
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' }] }
case 'delete_observations':
await this.knowledgeGraphManager.deleteObservations(
args.deletions as { entityName: string; observations: string[] }[]
)
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':
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' }] }
case 'read_graph':
// No arguments expected or needed for read_graph based on original schema
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':
return {
content: [
{
type: 'text',
text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2)
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':
return {
content: [
{
type: 'text',
text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2)
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 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"
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"