Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ private ExecutionResponse handleInitialize(final Object id) {
capabilities.put("tools", new JSONObject().put("listChanged", false));
result.put("capabilities", capabilities);

result.put("instructions",
"You are connected to an ArcadeDB multi-model database server. Follow these rules:\n"
+ "1. ALWAYS call list_databases first when you do not know the target database name. Never guess it.\n"
+ "2. Prefer Cypher (language: 'cypher') for graph queries unless SQL is explicitly requested.\n"
+ "3. Use the 'query' tool for read-only operations (SELECT, MATCH, RETURN) and 'execute_command' for writes (CREATE, INSERT, UPDATE, DELETE, MERGE).\n"
+ "4. Call get_schema before writing queries against an unfamiliar database to understand its types and properties.\n"
+ "5. If a query returns no results, verify the type/property names with get_schema before concluding the data does not exist.");

return jsonRpcResult(id, result);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri
final String command = args.getString("command");
final int limit = args.getInt("limit", DEFAULT_LIMIT);

if (!user.canAccessToDatabase(databaseName))
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");

final Database database = server.getDatabase(databaseName);
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);

// Analyze once for both permission checking and execution (avoids double parsing)
final QueryEngine engine = database.getQueryEngine(language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri

final String databaseName = args.getString("database");

if (!user.canAccessToDatabase(databaseName))
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");

final Database database = server.getDatabase(databaseName);
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);

final Schema schema = database.getSchema();
final JSONArray types = new JSONArray();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.server.mcp.tools;

import java.util.Set;
import java.util.TreeSet;

import com.arcadedb.server.ArcadeDBServer;
import com.arcadedb.server.ServerDatabase;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The utility should use the Database interface instead of the ServerDatabase implementation. This aligns with the return type of ArcadeDBServer.getDatabase() and the expectations of the calling tools.

Suggested change
import com.arcadedb.server.ServerDatabase;
import com.arcadedb.database.Database;

import com.arcadedb.server.security.ServerSecurityUser;

public class MCPToolUtils {

private MCPToolUtils() {
}

/**
* Resolves a database by name, throwing an {@link IllegalArgumentException} with the list of databases
* accessible to the user when the requested database does not exist — so the LLM can self-correct
* without a separate list_databases round-trip.
*/
public static ServerDatabase resolveDatabase(final ArcadeDBServer server, final ServerSecurityUser user,
final String databaseName) {
Comment on lines +38 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The return type must be changed to Database. Since ArcadeDBServer.getDatabase() returns the Database interface, returning it as ServerDatabase without an explicit cast will cause a compilation error. Furthermore, all tools calling this method (QueryTool, ExecuteCommandTool, GetSchemaTool) already expect the Database interface.

Suggested change
public static ServerDatabase resolveDatabase(final ArcadeDBServer server, final ServerSecurityUser user,
final String databaseName) {
public static Database resolveDatabase(final ArcadeDBServer server, final ServerSecurityUser user,
final String databaseName) {

if (!server.existsDatabase(databaseName)) {
final Set<String> installed = new TreeSet<>(server.getDatabaseNames());
installed.removeIf(db -> !user.canAccessToDatabase(db));
throw new IllegalArgumentException(
"Database '" + databaseName + "' does not exist. Available databases: " + installed
+ ". Use one of these names or call list_databases to refresh the list.");
}
if (!user.canAccessToDatabase(databaseName))
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
return server.getDatabase(databaseName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@

import com.arcadedb.database.Database;
import com.arcadedb.query.QueryEngine;
import com.arcadedb.server.mcp.MCPConfiguration;
import com.arcadedb.query.sql.executor.Result;
import com.arcadedb.query.sql.executor.ResultSet;
import com.arcadedb.serializer.JsonSerializer;
import com.arcadedb.serializer.json.JSONArray;
import com.arcadedb.serializer.json.JSONObject;
import com.arcadedb.server.ArcadeDBServer;
import com.arcadedb.server.mcp.MCPConfiguration;
import com.arcadedb.server.security.ServerSecurityUser;

import java.util.Collections;
Expand Down Expand Up @@ -73,10 +73,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri
final String query = args.getString("query");
final int limit = args.getInt("limit", DEFAULT_LIMIT);

if (!user.canAccessToDatabase(databaseName))
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");

final Database database = server.getDatabase(databaseName);
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);

// Verify the query is actually read-only using semantic analysis
final QueryEngine engine = database.getQueryEngine(language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,46 @@ void apiTokenUserAllowedByWildcard() throws Exception {
}
}

@Test
void queryUnknownDatabaseReturnsAvailableList() throws Exception {
final JSONObject response = callTool("query", new JSONObject()
.put("database", "nonexistent_db")
.put("language", "cypher")
.put("query", "RETURN 1"));

assertThat(response.getBoolean("isError", false)).isTrue();
final String errorText = response.getJSONArray("content").getJSONObject(0).getString("text");
assertThat(errorText).contains("nonexistent_db");
assertThat(errorText).containsIgnoringCase("available databases");
assertThat(errorText).contains("graph");
}

@Test
void executeCommandUnknownDatabaseReturnsAvailableList() throws Exception {
final JSONObject response = callTool("execute_command", new JSONObject()
.put("database", "nonexistent_db")
.put("language", "cypher")
.put("command", "CREATE (n:Test) RETURN n"));

assertThat(response.getBoolean("isError", false)).isTrue();
final String errorText = response.getJSONArray("content").getJSONObject(0).getString("text");
assertThat(errorText).contains("nonexistent_db");
assertThat(errorText).containsIgnoringCase("available databases");
assertThat(errorText).contains("graph");
}

@Test
void getSchemaUnknownDatabaseReturnsAvailableList() throws Exception {
final JSONObject response = callTool("get_schema", new JSONObject()
.put("database", "nonexistent_db"));

assertThat(response.getBoolean("isError", false)).isTrue();
final String errorText = response.getJSONArray("content").getJSONObject(0).getString("text");
assertThat(errorText).contains("nonexistent_db");
assertThat(errorText).containsIgnoringCase("available databases");
assertThat(errorText).contains("graph");
}

// ---- Helper methods ----

private JSONObject mcpRequest(final JSONObject request) throws Exception {
Expand Down
Loading