Update mcp tool universe
This commit was merged in pull request #1.
This commit is contained in:
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
.kotlin
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
79
pom.xml
Normal file
79
pom.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>de.shahondin1624</groupId>
|
||||
<artifactId>knowledge-graph</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<neo4j.version>2026.01.4</neo4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j</artifactId>
|
||||
<version>${neo4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-bolt</artifactId>
|
||||
<version>${neo4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.mcp-java</groupId>
|
||||
<artifactId>mcp-server-lib</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>de.shahondin1624.knowledgegraph.Main</mainClass>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,135 @@
|
||||
package de.shahondin1624.knowledgegraph;
|
||||
|
||||
import org.neo4j.configuration.GraphDatabaseSettings;
|
||||
import org.neo4j.configuration.connectors.BoltConnector;
|
||||
import org.neo4j.configuration.helpers.SocketAddress;
|
||||
import org.neo4j.io.ByteUnit;
|
||||
import org.neo4j.dbms.api.DatabaseManagementService;
|
||||
import org.neo4j.dbms.api.DatabaseManagementServiceBuilder;
|
||||
import org.neo4j.graphdb.GraphDatabaseService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.*;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME;
|
||||
|
||||
public class DatabaseLauncher {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseLauncher.class);
|
||||
private final Path databaseDirectory;
|
||||
private GraphDatabaseService graphDb;
|
||||
|
||||
public DatabaseLauncher() {
|
||||
this(Paths.get(System.getProperty("user.home"), ".graphdb"));
|
||||
}
|
||||
|
||||
public DatabaseLauncher(final Path databaseDirectory) {
|
||||
this.databaseDirectory = databaseDirectory;
|
||||
}
|
||||
|
||||
public void start() throws IOException, URISyntaxException {
|
||||
logger.info("Starting database from directory: {}", databaseDirectory);
|
||||
if (!Files.exists(databaseDirectory)) {
|
||||
logger.info("Database directory does not exist, extracting template...");
|
||||
extractDatabaseTemplate();
|
||||
}
|
||||
spinUpDatabase();
|
||||
}
|
||||
|
||||
private void extractDatabaseTemplate() throws IOException, URISyntaxException {
|
||||
final URL resourceUrl = getClass().getResource("/graphdb-template");
|
||||
if (resourceUrl == null) {
|
||||
logger.warn("No database template found at /graphdb-template, creating empty directory");
|
||||
// If no template is provided, just create the directory
|
||||
Files.createDirectories(databaseDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
URI resourceUri = resourceUrl.toURI();
|
||||
logger.debug("Extracting database template from URI: {}", resourceUri);
|
||||
|
||||
if ("jar".equals(resourceUri.getScheme())) {
|
||||
try (final FileSystem fileSystem = FileSystems.newFileSystem(resourceUri, Collections.emptyMap())) {
|
||||
final Path source = fileSystem.getPath("/graphdb-template");
|
||||
copyDirectory(source, databaseDirectory);
|
||||
}
|
||||
} else {
|
||||
Path source = Paths.get(resourceUri);
|
||||
copyDirectory(source, databaseDirectory);
|
||||
}
|
||||
logger.info("Database template extracted successfully to {}", databaseDirectory);
|
||||
}
|
||||
|
||||
private void copyDirectory(final Path source, final Path target) throws IOException {
|
||||
try (final Stream<Path> stream = Files.walk(source)) {
|
||||
stream.forEach(path -> {
|
||||
try {
|
||||
String relativePathStr = source.relativize(path).toString();
|
||||
Path destination = target.resolve(relativePathStr);
|
||||
if (Files.isDirectory(path)) {
|
||||
if (!Files.exists(destination)) {
|
||||
Files.createDirectories(destination);
|
||||
}
|
||||
} else {
|
||||
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to copy database template", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void spinUpDatabase() {
|
||||
logger.info("Spinning up Neo4j database...");
|
||||
final Path configFile = databaseDirectory.resolve("neo4j.conf");
|
||||
final DatabaseManagementServiceBuilder builder = new DatabaseManagementServiceBuilder(databaseDirectory);
|
||||
|
||||
if (Files.exists(configFile)) {
|
||||
logger.info("Loading database configuration from {}", configFile);
|
||||
builder.loadPropertiesFromFile(configFile);
|
||||
} else {
|
||||
logger.info("No configuration file found, using programmatic defaults");
|
||||
configureProgrammatically(builder);
|
||||
}
|
||||
|
||||
final DatabaseManagementService managementService = builder.build();
|
||||
graphDb = managementService.database(DEFAULT_DATABASE_NAME);
|
||||
GraphDatabaseProvider.setGraphDb(graphDb);
|
||||
registerShutdownHook(managementService);
|
||||
logger.info("Neo4j database '{}' is ready", DEFAULT_DATABASE_NAME);
|
||||
}
|
||||
|
||||
private void configureProgrammatically(final DatabaseManagementServiceBuilder builder) {
|
||||
builder.setConfig(GraphDatabaseSettings.pagecache_memory, ByteUnit.mebiBytes(512))
|
||||
.setConfig(GraphDatabaseSettings.transaction_timeout, Duration.ofSeconds(60))
|
||||
.setConfig(GraphDatabaseSettings.preallocate_logical_logs, true)
|
||||
.setConfig(BoltConnector.enabled, true)
|
||||
.setConfig(BoltConnector.listen_address, new SocketAddress("0.0.0.0", 7687))
|
||||
.setConfig(BoltConnector.encryption_level, BoltConnector.EncryptionLevel.DISABLED)
|
||||
.setConfig(GraphDatabaseSettings.auth_enabled, false);
|
||||
}
|
||||
|
||||
private void registerShutdownHook(final DatabaseManagementService managementService) {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
logger.info("Shutting down Neo4j database...");
|
||||
managementService.shutdown();
|
||||
logger.info("Neo4j database shut down successfully");
|
||||
}));
|
||||
}
|
||||
|
||||
public GraphDatabaseService getGraphDb() {
|
||||
return graphDb;
|
||||
}
|
||||
|
||||
public Path getDatabaseDirectory() {
|
||||
return databaseDirectory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.shahondin1624.knowledgegraph;
|
||||
|
||||
import org.neo4j.graphdb.GraphDatabaseService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Static provider for the GraphDatabaseService to be accessed by MCP tools.
|
||||
*/
|
||||
public class GraphDatabaseProvider {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GraphDatabaseProvider.class);
|
||||
private static GraphDatabaseService graphDb;
|
||||
|
||||
public static synchronized void setGraphDb(final GraphDatabaseService db) {
|
||||
logger.info("Registering GraphDatabaseService with provider");
|
||||
graphDb = db;
|
||||
}
|
||||
|
||||
public static synchronized GraphDatabaseService getGraphDb() {
|
||||
if (graphDb == null) {
|
||||
throw new IllegalStateException("GraphDatabaseService has not been initialized yet.");
|
||||
}
|
||||
return graphDb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import mcp.util.Result;
|
||||
import org.neo4j.graphdb.Label;
|
||||
import org.neo4j.graphdb.Node;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for creating a new node with optional labels.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class CreateNodeTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CreateNodeTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "create_node";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "Create Node";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Creates a new node with the specified labels.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder()
|
||||
.addProperty("labels", "array", "Optional list of labels for the node")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isIdempotent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
List<String> labelNames = (List<String>) arguments.get("labels");
|
||||
logger.debug("Creating node with labels: {}", labelNames);
|
||||
|
||||
final Result<String, Exception> result = getStringExceptionResult(labelNames);
|
||||
|
||||
if (result.isOk()) {
|
||||
String elementId = result.unwrap();
|
||||
logger.info("Successfully created node with ID: {}", elementId);
|
||||
return success("Node created with Element ID: " + elementId);
|
||||
} else {
|
||||
logger.error("Failed to create node: {}", result.err().unwrap().getMessage());
|
||||
return error("Failed to create node: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Result<String, Exception> getStringExceptionResult(List<String> labelNames) {
|
||||
DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
return dbHelper.wrapInTx(tx -> {
|
||||
Node node;
|
||||
if (labelNames == null || labelNames.isEmpty()) {
|
||||
node = tx.createNode();
|
||||
} else {
|
||||
Label[] labels = labelNames.stream()
|
||||
.map(Label::label)
|
||||
.toArray(Label[]::new);
|
||||
node = tx.createNode(labels);
|
||||
}
|
||||
return node.getElementId();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import org.neo4j.graphdb.Result;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for executing Cypher queries.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class ExecuteCypherTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExecuteCypherTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "execute_cypher";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "Execute Cypher Query";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Executes a Cypher query on the Neo4j database and returns the result as a list of maps.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder()
|
||||
.addProperty("query", "string", "The Cypher query to execute")
|
||||
.addProperty("parameters", "object", "Optional query parameters")
|
||||
.required("query")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isReadOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isIdempotent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
final String query = (String) arguments.get("query");
|
||||
final Map<String, Object> parameters = (Map<String, Object>) arguments.get("parameters");
|
||||
logger.debug("Executing Cypher query: {} with parameters: {}", query, parameters);
|
||||
|
||||
final DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
mcp.util.Result<List<Map<String, Object>>, Exception> result = dbHelper.wrapInTx(tx -> {
|
||||
Result neoResult = tx.execute(query, parameters != null ? parameters : Map.of());
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
while (neoResult.hasNext()) {
|
||||
rows.add(neoResult.next());
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
List<Map<String, Object>> rows = result.unwrap();
|
||||
logger.info("Successfully executed Cypher query, returned {} rows", rows.size());
|
||||
return success(rows.toString());
|
||||
} else {
|
||||
logger.error("Cypher query execution failed: {}", result.err().unwrap().getMessage());
|
||||
return error("Query execution failed: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import org.neo4j.graphdb.Label;
|
||||
import org.neo4j.graphdb.Node;
|
||||
import org.neo4j.graphdb.ResourceIterator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for finding nodes by label and property.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class FindNodesTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(FindNodesTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "find_nodes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "Find Nodes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Finds nodes by label and a property key-value pair.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder()
|
||||
.addProperty("label", "string", "The label of the nodes to find")
|
||||
.addProperty("key", "string", "The property key to filter by")
|
||||
.addProperty("value", "string", "The property value to filter by")
|
||||
.required("label", "key", "value")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
String labelName = (String) arguments.get("label");
|
||||
String key = (String) arguments.get("key");
|
||||
Object value = arguments.get("value");
|
||||
logger.debug("Finding nodes with label: {}, key: {}, value: {}", labelName, key, value);
|
||||
|
||||
DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
mcp.util.Result<List<Map<String, Object>>, Exception> result = dbHelper.wrapInTx(tx -> {
|
||||
List<Map<String, Object>> nodes = new ArrayList<>();
|
||||
try (ResourceIterator<Node> it = tx.findNodes(Label.label(labelName), key, value)) {
|
||||
while (it.hasNext()) {
|
||||
Node node = it.next();
|
||||
nodes.add(Map.of(
|
||||
"elementId", node.getElementId(),
|
||||
"labels", StreamSupportLabels(node.getLabels()),
|
||||
"properties", node.getAllProperties()
|
||||
));
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
List<Map<String, Object>> nodes = result.unwrap();
|
||||
logger.info("Successfully found {} nodes", nodes.size());
|
||||
return success(nodes.toString());
|
||||
} else {
|
||||
logger.error("Failed to find nodes: {}", result.err().unwrap().getMessage());
|
||||
return error("Failed to find nodes: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> StreamSupportLabels(Iterable<Label> labels) {
|
||||
List<String> labelNames = new ArrayList<>();
|
||||
for (Label label : labels) {
|
||||
labelNames.add(label.name());
|
||||
}
|
||||
return labelNames;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import org.neo4j.graphdb.Relationship;
|
||||
import org.neo4j.graphdb.RelationshipType;
|
||||
import org.neo4j.graphdb.ResourceIterator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for finding relationships by type and property.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class FindRelationshipsTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(FindRelationshipsTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "find_relationships";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "Find Relationships";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Finds relationships by type and a property key-value pair.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder()
|
||||
.addProperty("type", "string", "The type of the relationships to find")
|
||||
.addProperty("key", "string", "The property key to filter by")
|
||||
.addProperty("value", "string", "The property value to filter by")
|
||||
.required("type", "key", "value")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
String typeName = (String) arguments.get("type");
|
||||
String key = (String) arguments.get("key");
|
||||
Object value = arguments.get("value");
|
||||
logger.debug("Finding relationships with type: {}, key: {}, value: {}", typeName, key, value);
|
||||
|
||||
DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
mcp.util.Result<List<Map<String, Object>>, Exception> result = dbHelper.wrapInTx(tx -> {
|
||||
List<Map<String, Object>> relationships = new ArrayList<>();
|
||||
try (ResourceIterator<Relationship> it = tx.findRelationships(RelationshipType.withName(typeName), key, value)) {
|
||||
while (it.hasNext()) {
|
||||
Relationship rel = it.next();
|
||||
relationships.add(Map.of(
|
||||
"elementId", rel.getElementId(),
|
||||
"type", rel.getType().name(),
|
||||
"startNodeId", rel.getStartNode().getElementId(),
|
||||
"endNodeId", rel.getEndNode().getElementId(),
|
||||
"properties", rel.getAllProperties()
|
||||
));
|
||||
}
|
||||
}
|
||||
return relationships;
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
List<Map<String, Object>> rels = result.unwrap();
|
||||
logger.info("Successfully found {} relationships", rels.size());
|
||||
return success(rels.toString());
|
||||
} else {
|
||||
logger.error("Failed to find relationships: {}", result.err().unwrap().getMessage());
|
||||
return error("Failed to find relationships: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import org.neo4j.graphdb.Label;
|
||||
import org.neo4j.graphdb.Node;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for getting a node by its Element ID.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class GetNodeTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetNodeTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "get_node";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "Get Node";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Gets a node's labels and properties by its Element ID.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder()
|
||||
.addProperty("elementId", "string", "The Element ID of the node to retrieve")
|
||||
.required("elementId")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
String elementId = (String) arguments.get("elementId");
|
||||
logger.debug("Retrieving node with ID: {}", elementId);
|
||||
|
||||
DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
mcp.util.Result<Map<String, Object>, Exception> result = dbHelper.wrapInTx(tx -> {
|
||||
Node node = tx.getNodeByElementId(elementId);
|
||||
if (node == null) {
|
||||
throw new RuntimeException("Node not found with ID: " + elementId);
|
||||
}
|
||||
return Map.of(
|
||||
"elementId", node.getElementId(),
|
||||
"labels", StreamSupportLabels(node.getLabels()),
|
||||
"properties", node.getAllProperties()
|
||||
);
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
logger.info("Successfully retrieved node: {}", elementId);
|
||||
return success(result.unwrap().toString());
|
||||
} else {
|
||||
logger.error("Failed to get node {}: {}", elementId, result.err().unwrap().getMessage());
|
||||
return error("Failed to get node: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> StreamSupportLabels(Iterable<Label> labels) {
|
||||
List<String> labelNames = new ArrayList<>();
|
||||
for (Label label : labels) {
|
||||
labelNames.add(label.name());
|
||||
}
|
||||
return labelNames;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import org.neo4j.graphdb.Label;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for listing all labels in use in the database.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class ListLabelsTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ListLabelsTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "list_labels";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "List Labels";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Returns a list of all labels currently in use in the database.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
logger.debug("Listing all labels in use");
|
||||
DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
mcp.util.Result<List<String>, Exception> result = dbHelper.wrapInTx(tx -> {
|
||||
List<String> labels = new ArrayList<>();
|
||||
for (Label label : tx.getAllLabelsInUse()) {
|
||||
labels.add(label.name());
|
||||
}
|
||||
return labels;
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
List<String> labels = result.unwrap();
|
||||
logger.info("Successfully listed {} labels", labels.size());
|
||||
return success(labels.toString());
|
||||
} else {
|
||||
logger.error("Failed to list labels: {}", result.err().unwrap().getMessage());
|
||||
return error("Failed to list labels: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.shahondin1624.knowledgegraph.tooling;
|
||||
|
||||
import de.shahondin1624.knowledgegraph.GraphDatabaseProvider;
|
||||
import de.shahondin1624.knowledgegraph.util.DbHelper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import mcp.tools.DefaultMcpTool;
|
||||
import mcp.tools.McpValidatedTool;
|
||||
import mcp.tools.helper.SchemaBuilder;
|
||||
import org.neo4j.graphdb.RelationshipType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool for listing all relationship types in use in the database.
|
||||
*/
|
||||
@DefaultMcpTool
|
||||
public class ListRelationshipTypesTool extends McpValidatedTool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ListRelationshipTypesTool.class);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "list_relationship_types";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return "List Relationship Types";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Returns a list of all relationship types currently in use in the database.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.JsonSchema inputSchema() {
|
||||
return new SchemaBuilder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public McpSchema.CallToolResult callValidated(McpSchema.CallToolRequest request, Map<String, Object> arguments) throws Exception {
|
||||
logger.debug("Listing all relationship types in use");
|
||||
DbHelper dbHelper = new DbHelper(GraphDatabaseProvider.getGraphDb());
|
||||
mcp.util.Result<List<String>, Exception> result = dbHelper.wrapInTx(tx -> {
|
||||
List<String> types = new ArrayList<>();
|
||||
for (RelationshipType type : tx.getAllRelationshipTypesInUse()) {
|
||||
types.add(type.name());
|
||||
}
|
||||
return types;
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
List<String> types = result.unwrap();
|
||||
logger.info("Successfully listed {} relationship types", types.size());
|
||||
return success(types.toString());
|
||||
} else {
|
||||
logger.error("Failed to list relationship types: {}", result.err().unwrap().getMessage());
|
||||
return error("Failed to list relationship types: " + result.err().unwrap().getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.shahondin1624.knowledgegraph.util;
|
||||
|
||||
import mcp.util.Result;
|
||||
import org.neo4j.graphdb.GraphDatabaseService;
|
||||
import org.neo4j.graphdb.Transaction;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DbHelper {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DbHelper.class);
|
||||
private final GraphDatabaseService graphDb;
|
||||
|
||||
public DbHelper(GraphDatabaseService graphDb) {
|
||||
this.graphDb = graphDb;
|
||||
}
|
||||
|
||||
public Transaction beginTx() {
|
||||
return graphDb.beginTx();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E, T extends Throwable> Result<E, T> wrapInTx(TransactionContextWithReturn<E> action) {
|
||||
try (final TransactionWrapper tx = new TransactionWrapper(beginTx())) {
|
||||
final E result = action.execute(tx);
|
||||
tx.commit();
|
||||
return Result.Ok(result);
|
||||
} catch (final Exception e) {
|
||||
logger.error("Transaction failed (with return): {}", e.getMessage(), e);
|
||||
return Result.Err((T) e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Throwable> Result<Void, T> wrapInTx(TransactionContextWithoutReturn action) {
|
||||
try (final TransactionWrapper tx = new TransactionWrapper(beginTx())) {
|
||||
action.execute(tx);
|
||||
tx.commit();
|
||||
return Result.Ok(null);
|
||||
} catch (final Exception e) {
|
||||
logger.error("Transaction failed (without return): {}", e.getMessage(), e);
|
||||
return Result.Err((T) e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.shahondin1624.knowledgegraph.util;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TransactionContextWithReturn<E> {
|
||||
E execute(final TransactionWrapper tx) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.shahondin1624.knowledgegraph.util;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TransactionContextWithoutReturn {
|
||||
void execute(final TransactionWrapper tx) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package de.shahondin1624.knowledgegraph.util;
|
||||
|
||||
import org.neo4j.graphdb.*;
|
||||
import org.neo4j.graphdb.schema.Schema;
|
||||
import org.neo4j.graphdb.traversal.BidirectionalTraversalDescription;
|
||||
import org.neo4j.graphdb.traversal.TraversalDescription;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class TransactionWrapper implements AutoCloseable {
|
||||
private final Transaction tx;
|
||||
|
||||
public TransactionWrapper(final Transaction tx) {
|
||||
this.tx = tx;
|
||||
}
|
||||
|
||||
public Node createNode() {
|
||||
return tx.createNode();
|
||||
}
|
||||
|
||||
public Node createNode(Label... labels) {
|
||||
return tx.createNode(labels);
|
||||
}
|
||||
|
||||
public Node getNodeByElementId(String s) {
|
||||
return tx.getNodeByElementId(s);
|
||||
}
|
||||
|
||||
public Relationship getRelationshipByElementId(String s) {
|
||||
return tx.getRelationshipByElementId(s);
|
||||
}
|
||||
|
||||
public BidirectionalTraversalDescription bidirectionalTraversalDescription() {
|
||||
return tx.bidirectionalTraversalDescription();
|
||||
}
|
||||
|
||||
public TraversalDescription traversalDescription() {
|
||||
return tx.traversalDescription();
|
||||
}
|
||||
|
||||
public Result execute(String s) throws QueryExecutionException {
|
||||
return tx.execute(s);
|
||||
}
|
||||
|
||||
public Result execute(String s, Map<String, Object> map) throws QueryExecutionException {
|
||||
return tx.execute(s, map);
|
||||
}
|
||||
|
||||
public Iterable<Label> getAllLabelsInUse() {
|
||||
return tx.getAllLabelsInUse();
|
||||
}
|
||||
|
||||
public Iterable<RelationshipType> getAllRelationshipTypesInUse() {
|
||||
return tx.getAllRelationshipTypesInUse();
|
||||
}
|
||||
|
||||
public Iterable<Label> getAllLabels() {
|
||||
return tx.getAllLabels();
|
||||
}
|
||||
|
||||
public Iterable<RelationshipType> getAllRelationshipTypes() {
|
||||
return tx.getAllRelationshipTypes();
|
||||
}
|
||||
|
||||
public Iterable<String> getAllPropertyKeys() {
|
||||
return tx.getAllPropertyKeys();
|
||||
}
|
||||
|
||||
public ResourceIterator<Node> findNodes(Label label, String s, String s1, StringSearchMode stringSearchMode) {
|
||||
return tx.findNodes(label, s, s1, stringSearchMode);
|
||||
}
|
||||
|
||||
public ResourceIterator<Node> findNodes(Label label, Map<String, Object> map) {
|
||||
return tx.findNodes(label, map);
|
||||
}
|
||||
|
||||
public ResourceIterator<Node> findNodes(Label label, String s, Object o, String s1, Object o1, String s2, Object o2) {
|
||||
return tx.findNodes(label, s, o, s1, o1, s2, o2);
|
||||
}
|
||||
|
||||
public ResourceIterator<Node> findNodes(Label label, String s, Object o, String s1, Object o1) {
|
||||
return tx.findNodes(label, s, o, s1, o1);
|
||||
}
|
||||
|
||||
public Node findNode(Label label, String s, Object o) {
|
||||
return tx.findNode(label, s, o);
|
||||
}
|
||||
|
||||
public ResourceIterator<Node> findNodes(Label label, String s, Object o) {
|
||||
return tx.findNodes(label, s, o);
|
||||
}
|
||||
|
||||
public ResourceIterator<Node> findNodes(Label label) {
|
||||
return tx.findNodes(label);
|
||||
}
|
||||
|
||||
public ResourceIterator<Relationship> findRelationships(RelationshipType relationshipType, String s, String s1, StringSearchMode stringSearchMode) {
|
||||
return tx.findRelationships(relationshipType, s, s1, stringSearchMode);
|
||||
}
|
||||
|
||||
public ResourceIterator<Relationship> findRelationships(RelationshipType relationshipType, Map<String, Object> map) {
|
||||
return tx.findRelationships(relationshipType, map);
|
||||
}
|
||||
|
||||
public ResourceIterator<Relationship> findRelationships(RelationshipType relationshipType, String s, Object o, String s1, Object o1, String s2, Object o2) {
|
||||
return tx.findRelationships(relationshipType, s, o, s1, o1, s2, o2);
|
||||
}
|
||||
|
||||
public ResourceIterator<Relationship> findRelationships(RelationshipType relationshipType, String s, Object o, String s1, Object o1) {
|
||||
return tx.findRelationships(relationshipType, s, o, s1, o1);
|
||||
}
|
||||
|
||||
public Relationship findRelationship(RelationshipType relationshipType, String s, Object o) {
|
||||
return tx.findRelationship(relationshipType, s, o);
|
||||
}
|
||||
|
||||
public ResourceIterator<Relationship> findRelationships(RelationshipType relationshipType, String s, Object o) {
|
||||
return tx.findRelationships(relationshipType, s, o);
|
||||
}
|
||||
|
||||
public ResourceIterator<Relationship> findRelationships(RelationshipType relationshipType) {
|
||||
return tx.findRelationships(relationshipType);
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
tx.terminate();
|
||||
}
|
||||
|
||||
public ResourceIterable<Node> getAllNodes() {
|
||||
return tx.getAllNodes();
|
||||
}
|
||||
|
||||
public ResourceIterable<Relationship> getAllRelationships() {
|
||||
return tx.getAllRelationships();
|
||||
}
|
||||
|
||||
public Lock acquireWriteLock(Entity entity) {
|
||||
return tx.acquireWriteLock(entity);
|
||||
}
|
||||
|
||||
public Lock acquireReadLock(Entity entity) {
|
||||
return tx.acquireReadLock(entity);
|
||||
}
|
||||
|
||||
public Schema schema() {
|
||||
return tx.schema();
|
||||
}
|
||||
|
||||
public void commit() {
|
||||
tx.commit();
|
||||
}
|
||||
|
||||
public void rollback() {
|
||||
tx.rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
tx.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user