mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
491 lines
16 KiB
TypeScript
491 lines
16 KiB
TypeScript
// 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 { promises as fs } from 'fs'
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
// Define memory file path using environment variable with fallback
|
|
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
|
|
|
|
// We are storing our memory using entities, relations, and observations in a graph structure
|
|
interface Entity {
|
|
name: string
|
|
entityType: string
|
|
observations: string[]
|
|
}
|
|
|
|
interface Relation {
|
|
from: string
|
|
to: string
|
|
relationType: string
|
|
}
|
|
|
|
interface KnowledgeGraph {
|
|
entities: Entity[]
|
|
relations: Relation[]
|
|
}
|
|
|
|
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
|
|
class KnowledgeGraphManager {
|
|
private memoryPath: string
|
|
|
|
constructor(memoryPath: string) {
|
|
this.memoryPath = memoryPath
|
|
}
|
|
|
|
private async loadGraph(): Promise<KnowledgeGraph> {
|
|
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: [] }
|
|
)
|
|
} catch (error) {
|
|
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
|
return { entities: [], relations: [] }
|
|
}
|
|
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'))
|
|
}
|
|
|
|
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)
|
|
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)
|
|
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)
|
|
if (!entity) {
|
|
throw new Error(`Entity with name ${o.entityName} not found`)
|
|
}
|
|
const newObservations = o.contents.filter((content) => !entity.observations.includes(content))
|
|
entity.observations.push(...newObservations)
|
|
return { entityName: o.entityName, addedObservations: newObservations }
|
|
})
|
|
await this.saveGraph(graph)
|
|
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)
|
|
}
|
|
|
|
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))
|
|
}
|
|
})
|
|
await this.saveGraph(graph)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
async readGraph(): Promise<KnowledgeGraph> {
|
|
return this.loadGraph()
|
|
}
|
|
|
|
// Very basic search function
|
|
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()))
|
|
)
|
|
|
|
// Create a Set of filtered entity names for quick lookup
|
|
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
|
|
|
|
// Filter relations to only include those between filtered entities
|
|
const filteredRelations = graph.relations.filter(
|
|
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
|
|
)
|
|
|
|
const filteredGraph: KnowledgeGraph = {
|
|
entities: filteredEntities,
|
|
relations: filteredRelations
|
|
}
|
|
|
|
return filteredGraph
|
|
}
|
|
|
|
async openNodes(names: string[]): Promise<KnowledgeGraph> {
|
|
const graph = await this.loadGraph()
|
|
|
|
// Filter entities
|
|
const filteredEntities = graph.entities.filter((e) => names.includes(e.name))
|
|
|
|
// Create a Set of filtered entity names for quick lookup
|
|
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
|
|
|
|
// Filter relations to only include those between filtered entities
|
|
const filteredRelations = graph.relations.filter(
|
|
(r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
|
|
)
|
|
|
|
const filteredGraph: KnowledgeGraph = {
|
|
entities: filteredEntities,
|
|
relations: filteredRelations
|
|
}
|
|
|
|
return filteredGraph
|
|
}
|
|
}
|
|
|
|
class MemoryServer {
|
|
public server: Server
|
|
private knowledgeGraphManager: KnowledgeGraphManager
|
|
|
|
constructor(envPath: string = '') {
|
|
const memoryPath = envPath
|
|
? path.isAbsolute(envPath)
|
|
? envPath
|
|
: path.join(path.dirname(fileURLToPath(import.meta.url)), envPath)
|
|
: defaultMemoryPath
|
|
this.knowledgeGraphManager = new KnowledgeGraphManager(memoryPath)
|
|
this.server = new Server(
|
|
{
|
|
name: 'memory-server',
|
|
version: '1.0.0'
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {}
|
|
}
|
|
}
|
|
)
|
|
this.initialize()
|
|
}
|
|
|
|
initialize() {
|
|
// The server instance and tools exposed to Claude
|
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
return {
|
|
tools: [
|
|
{
|
|
name: 'create_entities',
|
|
description: 'Create multiple new entities in the knowledge graph',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
entities: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string', description: 'The name of the entity' },
|
|
entityType: { type: 'string', description: 'The type of the entity' },
|
|
observations: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'An array of observation contents associated with the entity'
|
|
}
|
|
},
|
|
required: ['name', 'entityType', 'observations']
|
|
}
|
|
}
|
|
},
|
|
required: ['entities']
|
|
}
|
|
},
|
|
{
|
|
name: 'create_relations',
|
|
description:
|
|
'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
relations: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
from: { type: 'string', description: 'The name of the entity where the relation starts' },
|
|
to: { type: 'string', description: 'The name of the entity where the relation ends' },
|
|
relationType: { type: 'string', description: 'The type of the relation' }
|
|
},
|
|
required: ['from', 'to', 'relationType']
|
|
}
|
|
}
|
|
},
|
|
required: ['relations']
|
|
}
|
|
},
|
|
{
|
|
name: 'add_observations',
|
|
description: 'Add new observations to existing entities in the knowledge graph',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
observations: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
entityName: { type: 'string', description: 'The name of the entity to add the observations to' },
|
|
contents: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'An array of observation contents to add'
|
|
}
|
|
},
|
|
required: ['entityName', 'contents']
|
|
}
|
|
}
|
|
},
|
|
required: ['observations']
|
|
}
|
|
},
|
|
{
|
|
name: 'delete_entities',
|
|
description: 'Delete multiple entities and their associated relations from the knowledge graph',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
entityNames: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'An array of entity names to delete'
|
|
}
|
|
},
|
|
required: ['entityNames']
|
|
}
|
|
},
|
|
{
|
|
name: 'delete_observations',
|
|
description: 'Delete specific observations from entities in the knowledge graph',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
deletions: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
entityName: { type: 'string', description: 'The name of the entity containing the observations' },
|
|
observations: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'An array of observations to delete'
|
|
}
|
|
},
|
|
required: ['entityName', 'observations']
|
|
}
|
|
}
|
|
},
|
|
required: ['deletions']
|
|
}
|
|
},
|
|
{
|
|
name: 'delete_relations',
|
|
description: 'Delete multiple relations from the knowledge graph',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
relations: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
from: { type: 'string', description: 'The name of the entity where the relation starts' },
|
|
to: { type: 'string', description: 'The name of the entity where the relation ends' },
|
|
relationType: { type: 'string', description: 'The type of the relation' }
|
|
},
|
|
required: ['from', 'to', 'relationType']
|
|
},
|
|
description: 'An array of relations to delete'
|
|
}
|
|
},
|
|
required: ['relations']
|
|
}
|
|
},
|
|
{
|
|
name: 'read_graph',
|
|
description: 'Read the entire knowledge graph',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {}
|
|
}
|
|
},
|
|
{
|
|
name: 'search_nodes',
|
|
description: 'Search for nodes in the knowledge graph based on a query',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
description: 'The search query to match against entity names, types, and observation content'
|
|
}
|
|
},
|
|
required: ['query']
|
|
}
|
|
},
|
|
{
|
|
name: 'open_nodes',
|
|
description: 'Open specific nodes in the knowledge graph by their names',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
names: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'An array of entity names to retrieve'
|
|
}
|
|
},
|
|
required: ['names']
|
|
}
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params
|
|
|
|
if (!args) {
|
|
throw new Error(`No arguments provided for tool: ${name}`)
|
|
}
|
|
|
|
switch (name) {
|
|
case 'create_entities':
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(
|
|
await this.knowledgeGraphManager.createEntities(args.entities as Entity[]),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
]
|
|
}
|
|
case 'create_relations':
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(
|
|
await this.knowledgeGraphManager.createRelations(args.relations as Relation[]),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
]
|
|
}
|
|
case 'add_observations':
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(
|
|
await this.knowledgeGraphManager.addObservations(
|
|
args.observations as { entityName: string; contents: string[] }[]
|
|
),
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
]
|
|
}
|
|
case 'delete_entities':
|
|
await this.knowledgeGraphManager.deleteEntities(args.entityNames as string[])
|
|
return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
|
|
case 'delete_observations':
|
|
await this.knowledgeGraphManager.deleteObservations(
|
|
args.deletions as { entityName: string; observations: string[] }[]
|
|
)
|
|
return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
|
|
case 'delete_relations':
|
|
await this.knowledgeGraphManager.deleteRelations(args.relations as Relation[])
|
|
return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
|
|
case 'read_graph':
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(await this.knowledgeGraphManager.readGraph(), null, 2) }]
|
|
}
|
|
case 'search_nodes':
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(await this.knowledgeGraphManager.searchNodes(args.query as string), null, 2)
|
|
}
|
|
]
|
|
}
|
|
case 'open_nodes':
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(await this.knowledgeGraphManager.openNodes(args.names as string[]), null, 2)
|
|
}
|
|
]
|
|
}
|
|
default:
|
|
throw new Error(`Unknown tool: ${name}`)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
export default MemoryServer
|