重构了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",
|
"@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",
|
||||||
|
|||||||
@ -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)}`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user