Skip to content

Commit d6bf401

Browse files
robfrankCNE Pierre FICHEPOIL
andauthored
Improve MCP server: init instructions and helpful database error (#3966)
Co-authored-by: CNE Pierre FICHEPOIL <pierre-1.fichepoil@gendarmerie.interieur.gouv.fr>
1 parent 830e38e commit d6bf401

6 files changed

Lines changed: 103 additions & 13 deletions

File tree

server/src/main/java/com/arcadedb/server/mcp/MCPHttpHandler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ private ExecutionResponse handleInitialize(final Object id) {
125125
capabilities.put("tools", new JSONObject().put("listChanged", false));
126126
result.put("capabilities", capabilities);
127127

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

server/src/main/java/com/arcadedb/server/mcp/tools/ExecuteCommandTool.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri
7272
final String command = args.getString("command");
7373
final int limit = args.getInt("limit", DEFAULT_LIMIT);
7474

75-
if (!user.canAccessToDatabase(databaseName))
76-
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
77-
78-
final Database database = server.getDatabase(databaseName);
75+
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);
7976

8077
// Analyze once for both permission checking and execution (avoids double parsing)
8178
final QueryEngine engine = database.getQueryEngine(language);

server/src/main/java/com/arcadedb/server/mcp/tools/GetSchemaTool.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ public static JSONObject execute(final ArcadeDBServer server, final ServerSecuri
5757

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

60-
if (!user.canAccessToDatabase(databaseName))
61-
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
62-
63-
final Database database = server.getDatabase(databaseName);
60+
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);
6461

6562
final Schema schema = database.getSchema();
6663
final JSONArray types = new JSONArray();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.server.mcp.tools;
20+
21+
import java.util.Set;
22+
import java.util.TreeSet;
23+
24+
import com.arcadedb.server.ArcadeDBServer;
25+
import com.arcadedb.server.ServerDatabase;
26+
import com.arcadedb.server.security.ServerSecurityUser;
27+
28+
public class MCPToolUtils {
29+
30+
private MCPToolUtils() {
31+
}
32+
33+
/**
34+
* Resolves a database by name, throwing an {@link IllegalArgumentException} with the list of databases
35+
* accessible to the user when the requested database does not exist — so the LLM can self-correct
36+
* without a separate list_databases round-trip.
37+
*/
38+
public static ServerDatabase resolveDatabase(final ArcadeDBServer server, final ServerSecurityUser user,
39+
final String databaseName) {
40+
if (!server.existsDatabase(databaseName)) {
41+
final Set<String> installed = new TreeSet<>(server.getDatabaseNames());
42+
installed.removeIf(db -> !user.canAccessToDatabase(db));
43+
throw new IllegalArgumentException(
44+
"Database '" + databaseName + "' does not exist. Available databases: " + installed
45+
+ ". Use one of these names or call list_databases to refresh the list.");
46+
}
47+
if (!user.canAccessToDatabase(databaseName))
48+
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
49+
return server.getDatabase(databaseName);
50+
}
51+
}

server/src/main/java/com/arcadedb/server/mcp/tools/QueryTool.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020

2121
import com.arcadedb.database.Database;
2222
import com.arcadedb.query.QueryEngine;
23-
import com.arcadedb.server.mcp.MCPConfiguration;
2423
import com.arcadedb.query.sql.executor.Result;
2524
import com.arcadedb.query.sql.executor.ResultSet;
2625
import com.arcadedb.serializer.JsonSerializer;
2726
import com.arcadedb.serializer.json.JSONArray;
2827
import com.arcadedb.serializer.json.JSONObject;
2928
import com.arcadedb.server.ArcadeDBServer;
29+
import com.arcadedb.server.mcp.MCPConfiguration;
3030
import com.arcadedb.server.security.ServerSecurityUser;
3131

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

76-
if (!user.canAccessToDatabase(databaseName))
77-
throw new SecurityException("User '" + user.getName() + "' is not authorized to access database '" + databaseName + "'");
78-
79-
final Database database = server.getDatabase(databaseName);
76+
final Database database = MCPToolUtils.resolveDatabase(server, user, databaseName);
8077

8178
// Verify the query is actually read-only using semantic analysis
8279
final QueryEngine engine = database.getQueryEngine(language);

server/src/test/java/com/arcadedb/server/mcp/MCPServerPluginTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,46 @@ void apiTokenUserAllowedByWildcard() throws Exception {
790790
}
791791
}
792792

793+
@Test
794+
void queryUnknownDatabaseReturnsAvailableList() throws Exception {
795+
final JSONObject response = callTool("query", new JSONObject()
796+
.put("database", "nonexistent_db")
797+
.put("language", "cypher")
798+
.put("query", "RETURN 1"));
799+
800+
assertThat(response.getBoolean("isError", false)).isTrue();
801+
final String errorText = response.getJSONArray("content").getJSONObject(0).getString("text");
802+
assertThat(errorText).contains("nonexistent_db");
803+
assertThat(errorText).containsIgnoringCase("available databases");
804+
assertThat(errorText).contains("graph");
805+
}
806+
807+
@Test
808+
void executeCommandUnknownDatabaseReturnsAvailableList() throws Exception {
809+
final JSONObject response = callTool("execute_command", new JSONObject()
810+
.put("database", "nonexistent_db")
811+
.put("language", "cypher")
812+
.put("command", "CREATE (n:Test) RETURN n"));
813+
814+
assertThat(response.getBoolean("isError", false)).isTrue();
815+
final String errorText = response.getJSONArray("content").getJSONObject(0).getString("text");
816+
assertThat(errorText).contains("nonexistent_db");
817+
assertThat(errorText).containsIgnoringCase("available databases");
818+
assertThat(errorText).contains("graph");
819+
}
820+
821+
@Test
822+
void getSchemaUnknownDatabaseReturnsAvailableList() throws Exception {
823+
final JSONObject response = callTool("get_schema", new JSONObject()
824+
.put("database", "nonexistent_db"));
825+
826+
assertThat(response.getBoolean("isError", false)).isTrue();
827+
final String errorText = response.getJSONArray("content").getJSONObject(0).getString("text");
828+
assertThat(errorText).contains("nonexistent_db");
829+
assertThat(errorText).containsIgnoringCase("available databases");
830+
assertThat(errorText).contains("graph");
831+
}
832+
793833
// ---- Helper methods ----
794834

795835
private JSONObject mcpRequest(final JSONObject request) throws Exception {

0 commit comments

Comments
 (0)