重构了memory.ts,增加了文件写入锁,解决了并行写入导致记忆文件错误的问题; (#4671)
优化了memory.json文件的加载过程,只加载一次,其它涉及图谱的操作均在内存中完成,提高效率; 注意新引入了async-mutex软件包,需要yarn install安装。
This commit is contained in:
parent
04333535dd
commit
7c39116351
@ -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",
|
||||
|
||||
@ -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)}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
10
yarn.lock
10
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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user