diff --git a/jcommon/mcp/mcp-memory/pom.xml b/jcommon/mcp/mcp-memory/pom.xml new file mode 100644 index 000000000..9234e221b --- /dev/null +++ b/jcommon/mcp/mcp-memory/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + run.mone + mcp + 1.6.1-jdk21-SNAPSHOT + + + mcp-memory + + + 17 + 17 + + + + + + + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.source} + ${maven.compiler.target} + true + UTF-8 + + ${project.basedir}/src/main/java + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.14 + + run.mone.mcp.memory.MemoryMcpBootstrap + app + + + + + repackage + + + + + + + + + + + \ No newline at end of file diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/MemoryMcpBootstrap.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/MemoryMcpBootstrap.java new file mode 100644 index 000000000..0f4c938ec --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/MemoryMcpBootstrap.java @@ -0,0 +1,13 @@ +package run.mone.mcp.memory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan("run.mone.mcp.memory") +public class MemoryMcpBootstrap { + public static void main(String[] args) { + SpringApplication.run(MemoryMcpBootstrap.class, args); + } +} \ No newline at end of file diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/config/McpStdioTransportConfig.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/config/McpStdioTransportConfig.java new file mode 100644 index 000000000..ec3e5ba9f --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/config/McpStdioTransportConfig.java @@ -0,0 +1,21 @@ +package run.mone.mcp.memory.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import run.mone.hive.mcp.server.transport.StdioServerTransport; + +@Configuration +@ConditionalOnProperty(name = "stdio.enabled", havingValue = "true") +class McpStdioTransportConfig { + /** + * stdio 通信 + * @param mapper + * @return + */ + @Bean + StdioServerTransport stdioServerTransport(ObjectMapper mapper) { + return new StdioServerTransport(mapper); + } +} diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/function/MemoryFunctions.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/function/MemoryFunctions.java new file mode 100644 index 000000000..13aaca558 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/function/MemoryFunctions.java @@ -0,0 +1,417 @@ +package run.mone.mcp.memory.function; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import run.mone.hive.mcp.spec.McpSchema; +import run.mone.hive.mcp.spec.McpSchema.CallToolResult; +import run.mone.mcp.memory.graph.KnowledgeGraphManager; +import run.mone.mcp.memory.graph.Entity; +import run.mone.mcp.memory.graph.KnowledgeGraph; +import run.mone.mcp.memory.graph.Relation; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Data +@Slf4j +public class MemoryFunctions { + + private static final KnowledgeGraphManager graphManager = new KnowledgeGraphManager(); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Data + public static class CreateEntitiesFunction implements Function, McpSchema.CallToolResult> { + private String name = "create_entities"; + + private String desc = "Create multiple new entities in the knowledge graph"; + + private String toolScheme = """ + { + "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"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List entities = parseObject(t.get("entities"), new TypeReference>() {}); + log.info("Creating entities: {}", entities); + try { + List newEntities = graphManager.createEntities(entities); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(objectMapper.writeValueAsString(newEntities))), false); + } catch (Throwable e) { + log.error("Failed to create entities", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to create entities")), true); + } + } + } + + @Data + public static class CreateRelationsFunction implements Function, McpSchema.CallToolResult> { + private String name = "create_relations"; + + private String desc = "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice"; + + private String toolScheme = """ + { + "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"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List relations = parseObject(t.get("relations"), new TypeReference>() {}); + log.info("Creating relations: {}", relations); + try { + List newRelations = graphManager.createRelations(relations); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(objectMapper.writeValueAsString(newRelations))), false); + } catch (Throwable e) { + log.error("Failed to create relations", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to create relations")), true); + } + } + } + + @Data + public static class AddObservationsFunction implements Function, McpSchema.CallToolResult> { + private String name = "add_observations"; + + private String desc = "Add new observations to existing entities in the knowledge graph"; + + private String toolScheme = """ + { + "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"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List observations = parseObject(t.get("observations"), new TypeReference>() {}); + log.info("Adding observations: {}", observations); + try { + List results = graphManager.addObservations(observations); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(objectMapper.writeValueAsString(results))), false); + } catch (Throwable e) { + log.error("Failed to add observations", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to add observations")), true); + } + } + } + + @Data + public static class DeleteEntitiesFunction implements Function, McpSchema.CallToolResult> { + private String name = "delete_entities"; + + private String desc = "Delete multiple entities and their associated relations from the knowledge graph"; + + private String toolScheme = """ + { + "type": "object", + "properties": { + "entityNames": { + "type": "array", + "items": { + "type": "string" + }, + "description": "An array of entity names to delete" + } + }, + "required": ["entityNames"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List entityNames = parseObject(t.get("entityNames"), new TypeReference>() {}); + log.info("Deleting entities: {}", entityNames); + try { + graphManager.deleteEntities(entityNames); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Entities deleted successfully")), false); + } catch (Throwable e) { + log.error("Failed to delete entities", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to delete entities")), true); + } + } + } + + @Data + public static class DeleteObservationsFunction implements Function, McpSchema.CallToolResult> { + private String name = "delete_observations"; + + private String desc = "Delete specific observations from entities in the knowledge graph"; + + private String toolScheme = """ + { + "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"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List deletions = parseObject(t.get("deletions"), new TypeReference>() {}); + log.info("Deleting observations: {}", deletions); + try { + graphManager.deleteObservations(deletions); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Observations deleted successfully")), false); + } catch (Throwable e) { + log.error("Failed to delete observations", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to delete observations")), true); + } + } + } + + @Data + public static class DeleteRelationsFunction implements Function, McpSchema.CallToolResult> { + private String name = "delete_relations"; + + private String desc = "Delete multiple relations from the knowledge graph"; + + private String toolScheme = """ + { + "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"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List relations = parseObject(t.get("relations"), new TypeReference>() {}); + log.info("Deleting relations: {}", relations); + try { + graphManager.deleteRelations(relations); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Relations deleted successfully")), false); + } catch (Throwable e) { + log.error("Failed to delete relations", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to delete relations")), true); + } + } + } + + @Data + public static class ReadGraphFunction implements Function, McpSchema.CallToolResult> { + private String name = "read_graph"; + + private String desc = "Read the entire knowledge graph"; + + private String toolScheme = """ + { + "type": "object", + "properties": {} + } + """; + + @Override + public CallToolResult apply(Map t) { + log.info("Reading graph"); + try { + KnowledgeGraph graph = graphManager.readGraph(); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(objectMapper.writeValueAsString(graph))), false); + } catch (Throwable e) { + log.error("Failed to read graph", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to read graph")), true); + } + } + } + + @Data + public static class SearchNodesFunction implements Function, McpSchema.CallToolResult> { + private String name = "search_nodes"; + + private String desc = "Search for nodes in the knowledge graph based on a query"; + + private String toolScheme = """ + { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query to match against entity names, types, and observation content" + } + }, + "required": ["query"] + } + """; + + @Override + public CallToolResult apply(Map t) { + String query = (String) t.get("query"); + log.info("Searching nodes with query: {}", query); + try { + KnowledgeGraph results = graphManager.searchNodes(query); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(objectMapper.writeValueAsString(results))), false); + } catch (Throwable e) { + log.error("Failed to search nodes", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to search nodes")), true); + } + } + } + + @Data + public static class OpenNodesFunction implements Function, McpSchema.CallToolResult> { + private String name = "open_nodes"; + + private String desc = "Open specific nodes in the knowledge graph by their names"; + + private String toolScheme = """ + { + "type": "object", + "properties": { + "names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "An array of entity names to retrieve" + } + }, + "required": ["names"] + } + """; + + @Override + public CallToolResult apply(Map t) { + List names = parseObject(t.get("names"), new TypeReference>() {}); + log.info("Opening nodes: {}", names); + try { + KnowledgeGraph results = graphManager.openNodes(names); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(objectMapper.writeValueAsString(results))), false); + } catch (Throwable e) { + log.error("Failed to open nodes", e); + return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("Failed to open nodes")), true); + } + } + } + + private static T parseObject(Object obj, TypeReference typeReference) { + try { + return objectMapper.readValue(objectMapper.writeValueAsString(obj), typeReference); + } catch (Exception e) { + log.error("Failed to parse JSON: {}", obj, e); + throw new RuntimeException("Failed to parse JSON", e); + } + } +} \ No newline at end of file diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/Entity.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/Entity.java new file mode 100644 index 000000000..d5a310ee7 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/Entity.java @@ -0,0 +1,22 @@ +package run.mone.mcp.memory.graph; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.ArrayList; + +@Data +@NoArgsConstructor +public class Entity { + private String type = "entity"; + private String name; + private String entityType; + private List observations; + + public Entity(String name, String entityType) { + this.name = name; + this.entityType = entityType; + this.observations = new ArrayList<>(); + } +} diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/KnowledgeGraph.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/KnowledgeGraph.java new file mode 100644 index 000000000..73d15aea8 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/KnowledgeGraph.java @@ -0,0 +1,11 @@ +package run.mone.mcp.memory.graph; + +import lombok.Data; +import java.util.List; +import java.util.ArrayList; + +@Data +public class KnowledgeGraph { + private List entities = new ArrayList<>(); + private List relations = new ArrayList<>(); +} \ No newline at end of file diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/KnowledgeGraphManager.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/KnowledgeGraphManager.java new file mode 100644 index 000000000..92b24ebf6 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/KnowledgeGraphManager.java @@ -0,0 +1,232 @@ +package run.mone.mcp.memory.graph; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class KnowledgeGraphManager { + private static final String MEMORY_FILE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "memory.jsonl"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private void ensureFileExists() throws IOException { + Path path = Paths.get(MEMORY_FILE_PATH); + if (!Files.exists(path)) { + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + } + + private KnowledgeGraph loadGraph() throws IOException { + ensureFileExists(); + List lines = Files.readAllLines(Paths.get(MEMORY_FILE_PATH)); + + KnowledgeGraph graph = new KnowledgeGraph(); + + for (String line : lines) { + if (line.trim().isEmpty()) continue; + + Map item = objectMapper.readValue(line, new TypeReference>() {}); + String type = (String) item.get("type"); + + if ("entity".equals(type)) { + Entity entity = objectMapper.convertValue(item, Entity.class); + graph.getEntities().add(entity); + } else if ("relation".equals(type)) { + Relation relation = objectMapper.convertValue(item, Relation.class); + graph.getRelations().add(relation); + } + } + return graph; + } + + private void saveGraph(KnowledgeGraph graph) throws IOException { + List lines = new ArrayList<>(); + + for (Entity entity : graph.getEntities()) { + Map item = new HashMap<>(); + item.put("type", "entity"); + item.put("name", entity.getName()); + item.put("entityType", entity.getEntityType()); + item.put("observations", entity.getObservations()); + lines.add(objectMapper.writeValueAsString(item)); + } + + for (Relation relation : graph.getRelations()) { + Map item = new HashMap<>(); + item.put("type", "relation"); + item.put("from", relation.getFrom()); + item.put("to", relation.getTo()); + item.put("relationType", relation.getRelationType()); + lines.add(objectMapper.writeValueAsString(item)); + } + + Files.write(Paths.get(MEMORY_FILE_PATH), lines); + } + + public List createEntities(List entities) throws IOException { + KnowledgeGraph graph = loadGraph(); + List newEntities = entities.stream() + .filter(e -> !graph.getEntities().contains(e.getName())) + .collect(Collectors.toList()); + + graph.getEntities().addAll(newEntities); + saveGraph(graph); + return newEntities; + } + + public List createRelations(List relations) throws IOException { + KnowledgeGraph graph = loadGraph(); + List newRelations = relations.stream() + .filter(r -> !graph.getRelations().stream() + .anyMatch(existing -> + existing.getFrom().equals(r.getFrom()) && + existing.getTo().equals(r.getTo()) && + existing.getRelationType().equals(r.getRelationType()))) + .collect(Collectors.toList()); + + graph.getRelations().addAll(newRelations); + saveGraph(graph); + return newRelations; + } + + public List addObservations(List requests) throws IOException { + KnowledgeGraph graph = loadGraph(); + List results = new ArrayList<>(); + + for (ObservationRequest request : requests) { + Entity entity = graph.getEntities().stream() + .filter(e -> e.getName().equals(request.getEntityName())) + .findFirst() + .orElse(null); + if (entity == null) { + throw new IllegalArgumentException("Entity not found: " + request.getEntityName()); + } + + List newObservations = request.getContents().stream() + .filter(content -> !entity.getObservations().contains(content)) + .collect(Collectors.toList()); + + entity.getObservations().addAll(newObservations); + results.add(new ObservationResult(request.getEntityName(), newObservations)); + } + + saveGraph(graph); + return results; + } + + public void deleteEntities(List entityNames) throws IOException { + KnowledgeGraph graph = loadGraph(); + graph.getEntities().removeIf(e -> entityNames.contains(e.getName())); + saveGraph(graph); + } + + public void deleteObservations(List deletions) throws IOException { + KnowledgeGraph graph = loadGraph(); + + for (ObservationDeletion deletion : deletions) { + Entity entity = graph.getEntities().stream() + .filter(e -> e.getName().equals(deletion.getEntityName())) + .findFirst() + .orElse(null); + if (entity != null) { + entity.getObservations().removeAll(deletion.getObservations()); + } + } + + saveGraph(graph); + } + + public void deleteRelations(List relations) throws IOException { + KnowledgeGraph graph = loadGraph(); + graph.getRelations().removeIf(r -> + relations.stream().anyMatch(delRelation -> + r.getFrom().equals(delRelation.getFrom()) && + r.getTo().equals(delRelation.getTo()) && + r.getRelationType().equals(delRelation.getRelationType()) + ) + ); + saveGraph(graph); + } + + public KnowledgeGraph readGraph() throws IOException { + return loadGraph(); + } + + public KnowledgeGraph searchNodes(String query) throws IOException { + KnowledgeGraph graph = loadGraph(); + String lowercaseQuery = query.toLowerCase(); + + List filteredEntities = graph.getEntities().stream() + .filter(e -> + e.getName().toLowerCase().contains(lowercaseQuery) || + e.getEntityType().toLowerCase().contains(lowercaseQuery) || + e.getObservations().stream() + .anyMatch(o -> o.toLowerCase().contains(lowercaseQuery))) + .collect(Collectors.toList()); + + List filteredRelations = graph.getRelations().stream() + .filter(r -> + filteredEntities.contains(r.getFrom()) && + filteredEntities.contains(r.getTo())) + .collect(Collectors.toList()); + + KnowledgeGraph filteredGraph = new KnowledgeGraph(); + filteredGraph.getEntities().addAll(filteredEntities); + filteredGraph.getRelations().addAll(filteredRelations); + + return filteredGraph; + } + + public KnowledgeGraph openNodes(List names) throws IOException { + KnowledgeGraph graph = loadGraph(); + + List filteredEntities = graph.getEntities().stream() + .filter(e -> names.contains(e.getName())) + .collect(Collectors.toList()); + + List filteredRelations = graph.getRelations().stream() + .filter(r -> + filteredEntities.contains(r.getFrom()) && + filteredEntities.contains(r.getTo())) + .collect(Collectors.toList()); + + KnowledgeGraph filteredGraph = new KnowledgeGraph(); + filteredGraph.getEntities().addAll(filteredEntities); + filteredGraph.getRelations().addAll(filteredRelations); + + return filteredGraph; + } + + // 辅助类 + @Data + public static class ObservationRequest { + private String entityName; + private List contents; + } + + @Data + public static class ObservationResult { + private String entityName; + private List addedObservations; + + public ObservationResult(String entityName, List addedObservations) { + this.entityName = entityName; + this.addedObservations = addedObservations; + } + } + + @Data + public static class ObservationDeletion { + private String entityName; + private List observations; + } +} + + diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/Relation.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/Relation.java new file mode 100644 index 000000000..1fa5c8531 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/graph/Relation.java @@ -0,0 +1,17 @@ +package run.mone.mcp.memory.graph; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Relation { + private String type = "relation"; + private String from; + private String to; + private String relationType; +} diff --git a/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/server/MemoryMcpServer.java b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/server/MemoryMcpServer.java new file mode 100644 index 000000000..e4a1baf64 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/java/run/mone/mcp/memory/server/MemoryMcpServer.java @@ -0,0 +1,103 @@ +package run.mone.mcp.memory.server; + +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import run.mone.hive.mcp.server.McpServer; +import run.mone.hive.mcp.server.McpSyncServer; +import run.mone.hive.mcp.spec.McpSchema.ServerCapabilities; +import run.mone.hive.mcp.spec.ServerMcpTransport; +import run.mone.mcp.memory.function.MemoryFunctions; +import run.mone.hive.mcp.server.McpServer.ToolRegistration; +import run.mone.hive.mcp.spec.McpSchema.Tool; + +@Component +public class MemoryMcpServer { + + private ServerMcpTransport transport; + private McpSyncServer syncServer; + + public MemoryMcpServer(ServerMcpTransport transport) { + this.transport = transport; + } + + public McpSyncServer start() { + McpSyncServer syncServer = McpServer.using(transport) + .serverInfo("memory_mcp", "0.0.1") + .capabilities(ServerCapabilities.builder() + .tools(true) + .logging() + .build()) + .sync(); + + MemoryFunctions.CreateEntitiesFunction function1 = new MemoryFunctions.CreateEntitiesFunction(); + var toolRegistration1 = new ToolRegistration( + new Tool(function1.getName(), function1.getDesc(), function1.getToolScheme()), function1 + ); + + MemoryFunctions.CreateRelationsFunction function2 = new MemoryFunctions.CreateRelationsFunction(); + var toolRegistration2 = new ToolRegistration( + new Tool(function2.getName(), function2.getDesc(), function2.getToolScheme()), function2 + ); + + MemoryFunctions.AddObservationsFunction function3 = new MemoryFunctions.AddObservationsFunction(); + var toolRegistration3 = new ToolRegistration( + new Tool(function3.getName(), function3.getDesc(), function3.getToolScheme()), function3 + ); + + MemoryFunctions.DeleteEntitiesFunction function4 = new MemoryFunctions.DeleteEntitiesFunction(); + var toolRegistration4 = new ToolRegistration( + new Tool(function4.getName(), function4.getDesc(), function4.getToolScheme()), function4 + ); + + MemoryFunctions.DeleteRelationsFunction function5 = new MemoryFunctions.DeleteRelationsFunction(); + var toolRegistration5 = new ToolRegistration( + new Tool(function5.getName(), function5.getDesc(), function5.getToolScheme()), function5 + ); + + MemoryFunctions.DeleteObservationsFunction function6 = new MemoryFunctions.DeleteObservationsFunction(); + var toolRegistration6 = new ToolRegistration( + new Tool(function6.getName(), function6.getDesc(), function6.getToolScheme()), function6 + ); + + MemoryFunctions.ReadGraphFunction function7 = new MemoryFunctions.ReadGraphFunction(); + var toolRegistration7 = new ToolRegistration( + new Tool(function7.getName(), function7.getDesc(), function7.getToolScheme()), function7 + ); + + MemoryFunctions.SearchNodesFunction function8 = new MemoryFunctions.SearchNodesFunction(); + var toolRegistration8 = new ToolRegistration( + new Tool(function8.getName(), function8.getDesc(), function8.getToolScheme()), function8 + ); + + MemoryFunctions.OpenNodesFunction function9 = new MemoryFunctions.OpenNodesFunction(); + var toolRegistration9 = new ToolRegistration( + new Tool(function9.getName(), function9.getDesc(), function9.getToolScheme()), function9 + ); + + syncServer.addTool(toolRegistration1); + syncServer.addTool(toolRegistration2); + syncServer.addTool(toolRegistration3); + syncServer.addTool(toolRegistration4); + syncServer.addTool(toolRegistration5); + syncServer.addTool(toolRegistration6); + syncServer.addTool(toolRegistration7); + syncServer.addTool(toolRegistration8); + syncServer.addTool(toolRegistration9); + return syncServer; + } + + @PostConstruct + public void init() { + this.syncServer = start(); + } + + @PreDestroy + public void stop() { + if (this.syncServer != null) { + this.syncServer.closeGracefully(); + } + } + +} diff --git a/jcommon/mcp/mcp-memory/src/main/resources/application.properties b/jcommon/mcp/mcp-memory/src/main/resources/application.properties new file mode 100644 index 000000000..33a709ec8 --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/main/resources/application.properties @@ -0,0 +1,2 @@ +stdio.enabled=true +spring.main.web-application-type=none \ No newline at end of file diff --git a/jcommon/mcp/mcp-memory/src/test/java/run/mone/mcp/memory/test/MemoryFunctionsTest.java b/jcommon/mcp/mcp-memory/src/test/java/run/mone/mcp/memory/test/MemoryFunctionsTest.java new file mode 100644 index 000000000..775ca7a7e --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/test/java/run/mone/mcp/memory/test/MemoryFunctionsTest.java @@ -0,0 +1,180 @@ +package run.mone.mcp.memory.test; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import run.mone.hive.mcp.spec.McpSchema; +import run.mone.mcp.memory.MemoryMcpBootstrap; +import run.mone.mcp.memory.function.MemoryFunctions; +import run.mone.mcp.memory.graph.Entity; +import run.mone.mcp.memory.graph.Relation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = MemoryMcpBootstrap.class) +@TestPropertySource(locations = "classpath:application-test.properties") +public class MemoryFunctionsTest { + + private MemoryFunctions.CreateEntitiesFunction createEntitiesFunction; + private MemoryFunctions.CreateRelationsFunction createRelationsFunction; + private MemoryFunctions.AddObservationsFunction addObservationsFunction; + private MemoryFunctions.DeleteEntitiesFunction deleteEntitiesFunction; + private MemoryFunctions.DeleteObservationsFunction deleteObservationsFunction; + private MemoryFunctions.DeleteRelationsFunction deleteRelationsFunction; + private MemoryFunctions.ReadGraphFunction readGraphFunction; + private MemoryFunctions.SearchNodesFunction searchNodesFunction; + private MemoryFunctions.OpenNodesFunction openNodesFunction; + + @BeforeEach + void setUp() { + createEntitiesFunction = new MemoryFunctions.CreateEntitiesFunction(); + createRelationsFunction = new MemoryFunctions.CreateRelationsFunction(); + addObservationsFunction = new MemoryFunctions.AddObservationsFunction(); + deleteEntitiesFunction = new MemoryFunctions.DeleteEntitiesFunction(); + deleteObservationsFunction = new MemoryFunctions.DeleteObservationsFunction(); + deleteRelationsFunction = new MemoryFunctions.DeleteRelationsFunction(); + readGraphFunction = new MemoryFunctions.ReadGraphFunction(); + searchNodesFunction = new MemoryFunctions.SearchNodesFunction(); + openNodesFunction = new MemoryFunctions.OpenNodesFunction(); + } + + @Test + void testCreateEntities() { + List entities = new ArrayList<>(); + Entity entity = new Entity(); + entity.setName("TestEntity"); + entity.setEntityType("TestType"); + entity.setObservations(List.of("Observation1", "Observation2")); + entities.add(entity); + + Map args = new HashMap<>(); + args.put("entities", entities); + + McpSchema.CallToolResult result = createEntitiesFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testCreateRelations() { + // First create some entities + testCreateEntities(); + + List relations = new ArrayList<>(); + Relation relation = new Relation(); + relation.setFrom("TestEntity"); + relation.setTo("TestEntity"); + relation.setRelationType("SELF_RELATION"); + relations.add(relation); + + Map args = new HashMap<>(); + args.put("relations", relations); + + McpSchema.CallToolResult result = createRelationsFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testAddObservations() { + // First create an entity + testCreateEntities(); + + Map args = new HashMap<>(); + List> observations = new ArrayList<>(); + Map observation = new HashMap<>(); + observation.put("entityName", "TestEntity"); + observation.put("contents", List.of("NewObservation1", "NewObservation2")); + observations.add(observation); + args.put("observations", observations); + + McpSchema.CallToolResult result = addObservationsFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testDeleteEntities() { + // First create an entity + testCreateEntities(); + + Map args = new HashMap<>(); + args.put("entityNames", List.of("TestEntity")); + + McpSchema.CallToolResult result = deleteEntitiesFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testDeleteObservations() { + // First create an entity with observations + testCreateEntities(); + + Map args = new HashMap<>(); + List> deletions = new ArrayList<>(); + Map deletion = new HashMap<>(); + deletion.put("entityName", "TestEntity"); + deletion.put("observations", List.of("Observation1")); + deletions.add(deletion); + args.put("deletions", deletions); + + McpSchema.CallToolResult result = deleteObservationsFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testDeleteRelations() { + // First create some relations + testCreateRelations(); + + List relations = new ArrayList<>(); + Relation relation = new Relation(); + relation.setFrom("TestEntity"); + relation.setTo("TestEntity"); + relation.setRelationType("SELF_RELATION"); + relations.add(relation); + + Map args = new HashMap<>(); + args.put("relations", relations); + + McpSchema.CallToolResult result = deleteRelationsFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testReadGraph() { + // First create some data + testCreateRelations(); + + Map args = new HashMap<>(); + McpSchema.CallToolResult result = readGraphFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testSearchNodes() { + // First create some data + testCreateEntities(); + + Map args = new HashMap<>(); + args.put("query", "TestEntity"); + + McpSchema.CallToolResult result = searchNodesFunction.apply(args); + assertFalse(result.isError()); + } + + @Test + void testOpenNodes() { + // First create some entities + testCreateEntities(); + + Map args = new HashMap<>(); + args.put("names", List.of("TestEntity")); + + McpSchema.CallToolResult result = openNodesFunction.apply(args); + assertFalse(result.isError()); + } +} diff --git a/jcommon/mcp/mcp-memory/src/test/resources/application-test.properties b/jcommon/mcp/mcp-memory/src/test/resources/application-test.properties new file mode 100644 index 000000000..2fe0226be --- /dev/null +++ b/jcommon/mcp/mcp-memory/src/test/resources/application-test.properties @@ -0,0 +1,6 @@ +# Test-specific configuration +spring.main.banner-mode=off + +# Logging +logging.level.root=WARN +logging.level.run.mone.mcp.playwright=DEBUG \ No newline at end of file diff --git a/jcommon/mcp/pom.xml b/jcommon/mcp/pom.xml index 07d7431ed..05e45dd25 100644 --- a/jcommon/mcp/pom.xml +++ b/jcommon/mcp/pom.xml @@ -22,6 +22,7 @@ mcp-playwright mcp-applescript mcp-multimodal + mcp-memory @@ -149,4 +150,4 @@ - \ No newline at end of file +