From 4d5c77eada01f53c4cc0ac7c4c6eb18f110cd41e Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Wed, 17 Dec 2025 13:27:11 +0100 Subject: [PATCH 01/17] Enabling keep alive and adding validation to ucp --- .../src/main/java/com/oracle/database/mcptoolkit/Utils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java index 47db6578..d61c7a1c 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java @@ -223,6 +223,8 @@ private static DataSource createDataSource(String url, String user, char[] passw pds.setConnectionProperty("remarksReporting", "true"); pds.setConnectionProperty("oracle.jdbc.vectorDefaultGetObjectType", "double[]"); pds.setConnectionProperty("oracle.jdbc.jsonDefaultGetObjectType", "java.lang.String"); + pds.setConnectionProperty("oracle.net.keepAlive", "true"); + pds.setValidateConnectionOnBorrow(true); return pds; } From a68b36784a117b3f84254a9f2002d0285e5c6f8c Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Wed, 14 Jan 2026 17:08:09 +0100 Subject: [PATCH 02/17] Adding admin tools ("create/edit tools" tool and "list tools" tool) + supporting change config file at runtime --- .../mcptoolkit/OracleDatabaseMCPToolkit.java | 87 +++++- .../com/oracle/database/mcptoolkit/Utils.java | 181 +++++++++---- .../database/mcptoolkit/tools/AdminTools.java | 254 ++++++++++++++++++ .../mcptoolkit/tools/ToolSchemas.java | 41 +++ 4 files changed, 513 insertions(+), 50 deletions(-) create mode 100644 src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java index df1eeb6d..38f25f4d 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java @@ -28,6 +28,13 @@ import org.apache.tomcat.util.net.SSLHostConfigCertificate; import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; import java.util.logging.Logger; import static com.oracle.database.mcptoolkit.Utils.installExternalExtensionsFromDir; @@ -40,6 +47,8 @@ public class OracleDatabaseMCPToolkit { private static final Logger LOG = Logger.getLogger(OracleDatabaseMCPToolkit.class.getName()); static ServerConfig config; + private static volatile McpSyncServer serverInstance; + static { config = Utils.loadConfig(); @@ -52,10 +61,10 @@ public static void main(String[] args) { switch (LoadedConstants.TRANSPORT_KIND) { case "http" -> { - server = startHttpServer(); + serverInstance = startHttpServer(); } case "stdio" -> { - server = McpServer + serverInstance = McpServer .sync(new StdioServerTransportProvider(new ObjectMapper())) .serverInfo("oracle-db-mcp-toolkit", "1.0.0") .capabilities(McpSchema.ServerCapabilities.builder() @@ -68,7 +77,19 @@ public static void main(String[] args) { default -> throw new IllegalArgumentException( "Unsupported transport: " + LoadedConstants.TRANSPORT_KIND + " (expected 'stdio' or 'http')"); } - Utils.addSyncToolSpecifications(server, config); + Utils.addSyncToolSpecifications(serverInstance, config); + +// if (LoadedConstants.CONFIG_FILE != null) { +// Thread watcher = new Thread(() -> { +// watchConfigFile(LoadedConstants.CONFIG_FILE); +// }, "config-file-watcher"); +// watcher.setDaemon(true); +// watcher.start(); +// } + + Thread pollingThread = new Thread(() -> pollConfigFile(LoadedConstants.CONFIG_FILE), "config-file-poller"); + pollingThread.setDaemon(true); + pollingThread.start(); } private OracleDatabaseMCPToolkit() { @@ -192,5 +213,65 @@ private static void enableHttps(Tomcat tomcat, String keystorePath, String keyst } } + public static ServerConfig getConfig() { + return config; + } + + private static void watchConfigFile(String filePath) { + Path configPath = Paths.get(filePath); + try (WatchService watcher = FileSystems.getDefault().newWatchService()) { + Path dir = configPath.getParent(); + if (dir == null) dir = Paths.get("."); + dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY); + + while (true) { + WatchKey key = watcher.take(); // block until events + for (WatchEvent event : key.pollEvents()) { + Path changed = ((WatchEvent)event).context(); + LOG.info(()->"[DEBUG] Watch event: " + event.kind() + ", file: " + changed); + LOG.info(()->"[DEBUG] Looking for file: " + configPath.getFileName()); + if (changed.endsWith(configPath.getFileName())) { + LOG.info(()->"[DEBUG] Detected relevant config file event: " + event.kind()); + reloadConfigAndResetTools(); + } + } + key.reset(); + } + } catch (Exception e) { + System.err.println("[oracle-db-mcp-toolkit] Config file watcher failed: " + e); + } + } + + private static void reloadConfigAndResetTools() { + try { + LOG.info(()->"[DEBUG] Reloading config..."); + ServerConfig newConfig = Utils.loadConfig(); + LOG.info(()->"[DEBUG] Old custom tool names: " + Utils.customToolNames); + LOG.info(()->"[DEBUG] New config tool names: " + newConfig.tools.keySet()); + config = newConfig; // update reference + Utils.reloadCustomTools(serverInstance, newConfig); + LOG.info(()->"[DEBUG] Reloaded config and refreshed tools."); + } catch (Exception e) { + System.err.println("[oracle-db-mcp-toolkit] Failed to reload config: " + e); + } + } + // For now, we rely on this instead of the nio watcher logic (for container sake) + private static void pollConfigFile(String filePath) { + File configFile = new File(filePath); + long lastModified = configFile.lastModified(); + while (true) { + long nowModified = configFile.lastModified(); + if (nowModified != lastModified && nowModified != 0) { + lastModified = nowModified; + reloadConfigAndResetTools(); + } + try { + Thread.sleep(2000); // Check every 2 seconds + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } } diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java index d61c7a1c..4a57599e 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java @@ -12,6 +12,7 @@ import com.oracle.database.mcptoolkit.config.DataSourceConfig; import com.oracle.database.mcptoolkit.config.ToolConfig; import com.oracle.database.mcptoolkit.config.ToolParameterConfig; +import com.oracle.database.mcptoolkit.tools.AdminTools; import com.oracle.database.mcptoolkit.tools.ExplainAndExecutePlanTool; import com.oracle.database.mcptoolkit.tools.LogAnalyzerTools; import com.oracle.database.mcptoolkit.tools.SimilaritySearchTool; @@ -39,10 +40,12 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -64,15 +67,24 @@ public class Utils { private static final Map dataSources = new ConcurrentHashMap<>(); private static volatile DataSource defaultDataSource; + public static final Set customToolNames = new HashSet<>(); + private static final Map customToolSignatures = new ConcurrentHashMap<>(); + /** *

* Returns the list of all available tools for this server. *

*/ static void addSyncToolSpecifications(McpSyncServer server, ServerConfig config) { + // Clear custom tool names at startup in case of restart + synchronized (customToolNames) { + customToolNames.clear(); + customToolSignatures.clear(); + } + List specs = LogAnalyzerTools.getTools(); for (McpServerFeatures.SyncToolSpecification spec : specs) { - String toolName = spec.tool().name(); // e.g. "get-stats", "get-queries" + String toolName = spec.tool().name(); if (isToolEnabled(config, toolName)) { server.addTool(spec); } @@ -88,48 +100,122 @@ static void addSyncToolSpecifications(McpSyncServer server, ServerConfig config) server.addTool(ExplainAndExecutePlanTool.getExplainAndExecutePlanTool(config)); } + // admin tools + if (isToolEnabled(config, "list-tools")) { + server.addTool(AdminTools.getListToolsTool(config)); + } + if (isToolEnabled(config, "edit-tools")) { + server.addTool(AdminTools.getEditToolsTool(config)); + } + // ---------- Dynamically Added Tools ---------- - for (Map.Entry entry : config.tools.entrySet()) { - ToolConfig tc = entry.getValue(); - server.addTool( - McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name(tc.name) - .title(tc.name) - .description(tc.description) - .inputSchema(tc.buildInputSchemaJson()) - .build() - ) - .callHandler((exchange, callReq) -> - tryCall(() -> { - try (Connection c = openConnection(config, tc.dataSource)) { - PreparedStatement ps = c.prepareStatement(tc.statement); - int paramIdx = 1; - if (tc.parameters != null) { - for (ToolParameterConfig param : tc.parameters) { - Object argVal = callReq.arguments().get(param.name); - ps.setObject(paramIdx++, argVal); - } - } - if (tc.statement.trim().toLowerCase().startsWith("select")) { - ResultSet rs = ps.executeQuery(); - List> rows = rsToList(rs); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("rows", rows, "rowCount", rows.size())) - .addTextContent(new ObjectMapper().writeValueAsString(rows)) - .build(); - } else { - int n = ps.executeUpdate(); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("updateCount", n)) - .addTextContent("{\"updateCount\":" + n + "}") - .build(); - } - } - }) - ) - .build() - ); + if (config.tools != null) { + for (Map.Entry entry : config.tools.entrySet()) { + ToolConfig tc = entry.getValue(); + server.addTool(makeSyncToolSpecification(tc, config)); + String sig = computeSignature(tc); + synchronized (customToolNames) { + customToolNames.add(tc.name); + customToolSignatures.put(tc.name, sig); + } + } + } + } + + private static String computeSignature(ToolConfig tc) { + StringBuilder sb = new StringBuilder(); + sb.append(nullToEmpty(tc.name)).append('|') + .append(nullToEmpty(tc.dataSource)).append('|') + .append(nullToEmpty(tc.description)).append('|') + .append(nullToEmpty(tc.statement)).append('|'); + if (tc.parameters != null) { + for (ToolParameterConfig p : tc.parameters) { + if (p == null) continue; + sb.append(nullToEmpty(p.name)).append(':') + .append(nullToEmpty(p.type)).append(':') + .append(nullToEmpty(p.description)).append(':') + .append(p.required).append(';'); + } + } + return sb.toString(); + } + + private static String nullToEmpty(String s) { return s == null ? "" : s; } + + private static McpServerFeatures.SyncToolSpecification makeSyncToolSpecification(ToolConfig tc, ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name(tc.name) + .title(tc.name) + .description(tc.description) + .inputSchema(tc.buildInputSchemaJson()) + .build() + ) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, tc.dataSource)) { + PreparedStatement ps = c.prepareStatement(tc.statement); + int paramIdx = 1; + if (tc.parameters != null) { + for (ToolParameterConfig param : tc.parameters) { + Object argVal = callReq.arguments().get(param.name); + ps.setObject(paramIdx++, argVal); + } + } + if (tc.statement.trim().toLowerCase().startsWith("select")) { + ResultSet rs = ps.executeQuery(); + List> rows = rsToList(rs); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("rows", rows, "rowCount", rows.size())) + .addTextContent(new ObjectMapper().writeValueAsString(rows)) + .build(); + } else { + int n = ps.executeUpdate(); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("updateCount", n)) + .addTextContent("{\"updateCount\":" + n + "}") + .build(); + } + } + })) + .build(); + } + + /** + * Incrementally apply YAML tool changes: add new tools, update changed ones, and do NOT remove tools + * that are not present in the new config. + */ + public static void reloadCustomTools(McpSyncServer server, ServerConfig config) { + LOG.info(() -> "[DEBUG] serverInstance identity: " + System.identityHashCode(server)); + synchronized (customToolNames) { + if (config.tools != null) { + for (Map.Entry entry : config.tools.entrySet()) { + String name = entry.getKey(); + ToolConfig tc = entry.getValue(); + String newSig = computeSignature(tc); + + if (!customToolNames.contains(name)) { + LOG.info(() -> "[DEBUG] Adding new tool: " + name); + server.addTool(makeSyncToolSpecification(tc, config)); + customToolNames.add(name); + customToolSignatures.put(name, newSig); + } else { + String oldSig = customToolSignatures.get(name); + if (oldSig == null || !oldSig.equals(newSig)) { + try { + LOG.info(() -> "[DEBUG] Updating changed tool: " + name); + server.removeTool(name); + } catch (Exception ex) { + LOG.info(() -> "[DEBUG] Failed to remove tool for update: " + name + ". Exception: " + ex); + } + server.addTool(makeSyncToolSpecification(tc, config)); + customToolSignatures.put(name, newSig); + } else { + LOG.info(() -> "[DEBUG] Tool unchanged (skipped): " + name); + } + } + } + } + // Intentionally do not remove tools absent from the new config } } @@ -163,13 +249,15 @@ static ServerConfig loadConfig() { if (yamlConfig == null) { config = ServerConfig.fromSystemProperties(); } else { - String defaultSourceKey = yamlConfig.dataSources!=null?yamlConfig.dataSources.keySet().stream().findFirst().orElse(null):null; + String defaultSourceKey = yamlConfig.dataSources != null + ? yamlConfig.dataSources.keySet().stream().findFirst().orElse(null) + : null; config = ServerConfig.fromSystemPropertiesAndYaml(yamlConfig, defaultSourceKey); } return config; } - /** +/** * Acquires a JDBC connection from the active data source. * * @param cfg server configuration @@ -189,7 +277,7 @@ public static Connection openConnection(ServerConfig cfg, String sourceName) thr * @param sourceName the name of the source; if null, uses the default source * @return a {@link DataSource} for the specified source * @throws SQLException if creation or configuration fails - */ + */ private static DataSource getOrCreateDataSource(ServerConfig cfg, String sourceName) throws SQLException { if (sourceName == null || sourceName.equals(ServerConfig.defaultSourceName)) { if (defaultDataSource != null) return defaultDataSource; @@ -377,5 +465,4 @@ private static boolean isToolEnabled(ServerConfig config, String toolName) { String key = toolName.toLowerCase(Locale.ROOT); return config.toolsFilter.contains(key); } - -} \ No newline at end of file +} diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java new file mode 100644 index 00000000..78824c06 --- /dev/null +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java @@ -0,0 +1,254 @@ +/* + ** Oracle Database MCP Toolkit version 1.0.0 + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.database.mcptoolkit.tools; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oracle.database.mcptoolkit.LoadedConstants; +import com.oracle.database.mcptoolkit.ServerConfig; +import com.oracle.database.mcptoolkit.OracleDatabaseMCPToolkit; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import org.yaml.snakeyaml.Yaml; + +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Admin/maintenance tools: + * - list-tools: list all available tools with descriptions + * - edit-tools: upsert a YAML-defined tool in the config file and rely on runtime reload + */ +public final class AdminTools { + private AdminTools() {} + + public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("list-tools") + .title("List Tools") + .description("List all available tools with their descriptions.") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> { + try { + // Always use the latest runtime config to avoid requiring a rebuild + ServerConfig current = OracleDatabaseMCPToolkit.getConfig() != null ? OracleDatabaseMCPToolkit.getConfig() : config; + + List> tools = new ArrayList<>(); + + // 1) Built-in log analyzer tools (respect toolsFilter) + for (McpServerFeatures.SyncToolSpecification spec : LogAnalyzerTools.getTools()) { + String name = spec.tool().name(); + if (isEnabled(current, name)) { + tools.add(Map.of( + "name", name, + "title", spec.tool().title(), + "description", spec.tool().description() + )); + } + } + + // 2) Built-in database tools (respect toolsFilter) + if (isEnabled(current, "similarity_search")) { + var t = SimilaritySearchTool.getSymilaritySearchTool(current).tool(); + tools.add(Map.of( + "name", t.name(), + "title", t.title(), + "description", t.description() + )); + } + if (isEnabled(current, "explain_plan")) { + var t = ExplainAndExecutePlanTool.getExplainAndExecutePlanTool(current).tool(); + tools.add(Map.of( + "name", t.name(), + "title", t.title(), + "description", t.description() + )); + } + + // 3) Custom YAML tools (always listed if present in config) + if (current.tools != null) { + for (Map.Entry e : current.tools.entrySet()) { + var tc = e.getValue(); + tools.add(Map.of( + "name", tc.name, + "title", tc.name, + "description", tc.description + )); + } + } + + // 4) Admin tools themselves if enabled + if (isEnabled(current, "list-tools")) { + tools.add(Map.of( + "name", "list-tools", + "title", "List Tools", + "description", "List all available tools with their descriptions." + )); + } + if (isEnabled(current, "edit-tools")) { + tools.add(Map.of( + "name", "edit-tools", + "title", "Edit/Add Tools", + "description", "Create or update YAML-defined tools and trigger live reload." + )); + } + + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("tools", tools)) + .addTextContent(new ObjectMapper().writeValueAsString(tools)) + .build(); + } catch (Exception e) { + return McpSchema.CallToolResult.builder() + .addTextContent("Unexpected: " + e.getMessage()) + .isError(true) + .build(); + } + }) + .build(); + } + + public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("edit-tools") + .title("Edit/Add Tools") + .description("Create or update YAML-defined tools in the config file. Changes are auto-reloaded.") + .inputSchema(ToolSchemas.EDIT_TOOL_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> { + try { + final var cfgPath = LoadedConstants.CONFIG_FILE; + if (cfgPath == null || cfgPath.isBlank()) { + return McpSchema.CallToolResult.builder() + .addTextContent("Config file path (configFile) is not set. Cannot edit tools.") + .isError(true) + .build(); + } + + String name = asString(callReq.arguments().get("name")); + if (name == null || name.isBlank()) { + return McpSchema.CallToolResult.builder() + .addTextContent("'name' is required") + .isError(true) + .build(); + } + + Path path = Paths.get(cfgPath); + Yaml yaml = new Yaml(); + + // Load existing YAML into a mutable map + Map root; + if (Files.exists(path)) { + try (Reader r = Files.newBufferedReader(path)) { + Object loaded = yaml.load(r); + if (loaded instanceof Map) { + //noinspection unchecked + root = new LinkedHashMap<>((Map) loaded); + } else { + root = new LinkedHashMap<>(); + } + } + } else { + root = new LinkedHashMap<>(); + } + + // Ensure 'tools' map exists + Map tools = getOrCreateMap(root, "tools"); + + // Upsert tool entry + Map t = getOrCreateMap(tools, name); + + // Optional fields + putIfPresent(t, callReq.arguments(), "description"); + putIfPresent(t, callReq.arguments(), "dataSource"); + putIfPresent(t, callReq.arguments(), "statement"); + + // parameters: array of objects + Object paramsObj = callReq.arguments().get("parameters"); + if (paramsObj instanceof List list) { + List> params = new ArrayList<>(); + for (Object o : list) { + if (o instanceof Map m) { + Map p = new LinkedHashMap<>(); + copyIfPresent(m, p, "name"); + copyIfPresent(m, p, "type"); + copyIfPresent(m, p, "description"); + copyIfPresent(m, p, "required"); + params.add(p); + } + } + t.put("parameters", params); + } + + tools.put(name, t); + root.put("tools", tools); + + // Write back YAML + try (Writer w = Files.newBufferedWriter(path)) { + yaml.dump(root, w); + } + + // The config poller will pick up the change and hot-reload within ~2 seconds. + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "status", "ok", + "message", "Tool '" + name + "' upserted into YAML. Reload will occur shortly.", + "configFile", cfgPath + )) + .addTextContent("{\"status\":\"ok\",\"name\":\"" + name + "\"}") + .build(); + } catch (Exception e) { + return McpSchema.CallToolResult.builder() + .addTextContent("Unexpected: " + e.getMessage()) + .isError(true) + .build(); + } + }) + .build(); + } + + private static boolean isEnabled(ServerConfig config, String toolName) { + if (config.toolsFilter == null) return true; + String key = toolName.toLowerCase(Locale.ROOT); + return config.toolsFilter.contains(key); + } + + private static String asString(Object v) { + return v == null ? null : String.valueOf(v); + } + + @SuppressWarnings("unchecked") + private static Map getOrCreateMap(Map parent, String key) { + Object o = parent.get(key); + if (o instanceof Map m) { + return new LinkedHashMap<>((Map) m); + } + Map created = new LinkedHashMap<>(); + parent.put(key, created); + return created; + } + + private static void putIfPresent(Map target, Map source, String key) { + Object v = source.get(key); + if (v != null) target.put(key, v); + } + + private static void copyIfPresent(Map src, Map dst, String key) { + Object v = src.get(key); + if (v != null) dst.put(key, v); + } +} diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java index 0ef145dd..ddae3043 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java @@ -156,4 +156,45 @@ public class ToolSchemas { }, "required": ["sql"] }"""; + + /** + * JSON schema for admin tools that take no input. + */ + static final String NO_INPUT_SCHEMA = """ + { + "type": "object", + "properties": {} + } + """; + + /** + * JSON schema for editing or adding a dynamic tool in the YAML config. + * The operation is an upsert keyed by the tool name. Only provided fields are updated. + */ + static final String EDIT_TOOL_SCHEMA = """ + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Tool name (YAML key). Required for upsert." }, + "description": { "type": "string", "description": "Human-friendly description of the tool" }, + "dataSource": { "type": "string", "description": "Reference key from dataSources to use for this tool" }, + "statement": { "type": "string", "description": "SQL statement to execute (SELECT or DML)" }, + "parameters": { + "type": "array", + "description": "Optional parameter list for the tool", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Parameter name" }, + "type": { "type": "string", "description": "JSON schema type (e.g., string, number, integer, boolean)" }, + "description": { "type": "string", "description": "Parameter description" }, + "required": { "type": "boolean", "description": "Whether this parameter is required" } + }, + "required": ["name", "type"] + } + } + }, + "required": ["name"] + } + """; } From 0a6be452f208a4048dbc9d4107fa3e7c3eb24923 Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Tue, 20 Jan 2026 10:37:58 +0100 Subject: [PATCH 03/17] Supporting deleting tools and updating readme --- src/oracle-db-mcp-toolkit/README.md | 51 ++++++++++++++++++- .../mcptoolkit/OracleDatabaseMCPToolkit.java | 8 +++ .../com/oracle/database/mcptoolkit/Utils.java | 31 +++++++++-- .../database/mcptoolkit/tools/AdminTools.java | 45 ++++++++++++++++ .../mcptoolkit/tools/ToolSchemas.java | 4 +- 5 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/oracle-db-mcp-toolkit/README.md b/src/oracle-db-mcp-toolkit/README.md index eecc654f..f0879dbe 100644 --- a/src/oracle-db-mcp-toolkit/README.md +++ b/src/oracle-db-mcp-toolkit/README.md @@ -7,6 +7,7 @@ Oracle Database MCP Toolkit is a Model Context Protocol (MCP) server that lets y * Use built-in tools: * Analyze Oracle JDBC thin client logs and RDBMS/SQLNet trace files. * **database-powered tools**, including **vector similarity search** and **SQL execution plan analysis**. + * **admin tools** for runtime discovery and configuration: list available tools and live-edit YAML-defined tools with hot reload. * Deploy locally or remotely - optionally as a container - with support for TLS and OAuth2 ![MCP Toolkit Architecture Diagram](./images/MCPToolkitArchitectureDiagram.svg) @@ -146,6 +147,44 @@ These tools operate on RDBMS/SQLNet trace files: * `planText`: DBMS_XPLAN output. * `llmPrompt`: A structured prompt for an LLM to explain + tune the plan. +### 3.5. Admin and Runtime Configuration Tools + +These tools help you discover what's enabled and manage YAML-defined tools at runtime. They are part of the `admin` toolset (enable via `-Dtools=admin` or include `list-tools,edit-tools`). + +- `list-tools`: List all available tools with their descriptions. + - Inputs: none + - Returns: `tools` array with `{ name, title, description }` for built-ins (honoring `-Dtools` filter) and any YAML-defined tools. + +- `edit-tools`: Create, update, or remove a YAML-defined tool. Changes are auto-reloaded by the server. + - Inputs (subset; see schema in code): + - `name` (string, required): Tool name/YAML key + - `remove` (boolean, optional): If true, delete the tool + - `description` (string, optional) + - `dataSource` (string, optional): Key from `dataSources:` + - `statement` (string, optional): SQL (SELECT or DML) + - `parameters` (array, optional): Items of `{ name, type, description, required }` + - Requirements and behavior: + - Requires `-DconfigFile` to be set to a writable YAML file; otherwise the tool will return an error. + - On upsert/remove, the YAML is written and the server hot-reloads the configuration shortly after. + + Example (upsert a tool): + ```jsonc + { + "name": "hotels-by-rating", + "description": "List hotels with a minimum rating", + "dataSource": "prod-db", + "statement": "SELECT * FROM hotels WHERE rating >= :minRating ORDER BY rating DESC", + "parameters": [ + { "name": "minRating", "type": "number", "description": "Minimum rating", "required": true } + ] + } + ``` + + Example (remove a tool): + ```jsonc + { "name": "hotels-by-rating", "remove": true } + ``` + --- ## 4. Installation @@ -184,7 +223,7 @@ This is the mode used by tools like Claude Desktop, where the client directly la "args": [ "-Ddb.url=jdbc:oracle:thin:@your-host:1521/your-service", "-Ddb.user=your_user", - "-Ddb.password=your_password" + "-Ddb.password=your_password", "-Dtools=get-jdbc-stats,get-jdbc-queries", "-Dojdbc.ext.dir=/path/to/extra-jars", "-jar", @@ -196,6 +235,8 @@ This is the mode used by tools like Claude Desktop, where the client directly la ``` If you don’t set `-Dtransport`, the server runs in stdio mode by default. +To enable admin tools in this mode, include them in `-Dtools` (e.g., `-Dtools=admin,explain`). + #### 4.3.2. HTTP mode In HTTP mode, you run the server as a standalone HTTP service and point an MCP client to it. @@ -214,6 +255,8 @@ java \ ``` This exposes the MCP endpoint at: `http://localhost:45450/mcp`. +If you plan to use `edit-tools`, ensure `-DconfigFile` points to a writable YAML file on the server. + ### 4.4. Enabling HTTPS (SSL/TLS) To enable HTTPS (SSL/TLS), specify your certificate keystore path and password using the `-DcertificatePath` and `-DcertificatePassword` options. Only PKCS12 (`.p12` or `.pfx`) keystore files are supported. @@ -439,7 +482,7 @@ Ultimately, the token must be included in the http request header (e.g. `Authori configFile No - Path to a YAML file defining datasources and tools. + Path to a YAML file defining datasources and tools. Required if you intend to use the edit-tools admin tool to persist changes. /opt/mcp/config.yaml @@ -531,6 +574,10 @@ podman run --rm \ ``` This exposes the MCP endpoint at: http://[your-ip-address]:45450/mcp or https://[your-ip-address]:45451/mcp +If you plan to use the `edit-tools` admin tool inside the container, mount a writable config file and set `-DconfigFile` accordingly, for example: +- Mount: `-v /absolute/path/config.yaml:/config/config.yaml:Z` +- Set: `-DconfigFile=/config/config.yaml` + You can then configure Cline or Claude Desktop as described in the Using HTTP from Cline / Claude Desktop sections above. If you need extra JDBC / security jars (e.g. `oraclepki`, wallets, centralized config, or providers that fetch full diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java index 38f25f4d..94501bc2 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java @@ -217,6 +217,14 @@ public static ServerConfig getConfig() { return config; } + /** + * Exposes the running McpSyncServer instance for admin operations (e.g., removing tools at runtime). + */ + public static McpSyncServer getServer() { + return serverInstance; + } + + private static void watchConfigFile(String filePath) { Path configPath = Paths.get(filePath); try (WatchService watcher = FileSystems.getDefault().newWatchService()) { diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java index 4a57599e..ce4e22a3 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java @@ -50,6 +50,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; +import java.lang.reflect.Field; /** * Utility class for managing Oracle database connections and @@ -112,6 +113,7 @@ static void addSyncToolSpecifications(McpSyncServer server, ServerConfig config) if (config.tools != null) { for (Map.Entry entry : config.tools.entrySet()) { ToolConfig tc = entry.getValue(); + if (!isToolEnabled(config, tc.name)) continue; server.addTool(makeSyncToolSpecification(tc, config)); String sig = computeSignature(tc); synchronized (customToolNames) { @@ -142,6 +144,25 @@ private static String computeSignature(ToolConfig tc) { private static String nullToEmpty(String s) { return s == null ? "" : s; } + /** + * Unregister a custom tool from local in-memory registries so runtime removal stays consistent. + */ + public static void unregisterCustomToolLocally(String name) { + if (name == null) return; + synchronized (customToolNames) { + customToolNames.remove(name); + try { + // Remove signature if present; safe even if missing + Field f = Utils.class.getDeclaredField("customToolSignatures"); + f.setAccessible(true); + @SuppressWarnings("unchecked") + Map sigs = (java.util.Map) f.get(null); + sigs.remove(name); + } catch (Throwable ignored) {} + } + } + + private static McpServerFeatures.SyncToolSpecification makeSyncToolSpecification(ToolConfig tc, ServerConfig config) { return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() @@ -185,16 +206,17 @@ private static McpServerFeatures.SyncToolSpecification makeSyncToolSpecification * that are not present in the new config. */ public static void reloadCustomTools(McpSyncServer server, ServerConfig config) { - LOG.info(() -> "[DEBUG] serverInstance identity: " + System.identityHashCode(server)); synchronized (customToolNames) { if (config.tools != null) { for (Map.Entry entry : config.tools.entrySet()) { String name = entry.getKey(); ToolConfig tc = entry.getValue(); + if (!isToolEnabled(config, name)) { + continue; + } String newSig = computeSignature(tc); if (!customToolNames.contains(name)) { - LOG.info(() -> "[DEBUG] Adding new tool: " + name); server.addTool(makeSyncToolSpecification(tc, config)); customToolNames.add(name); customToolSignatures.put(name, newSig); @@ -202,15 +224,14 @@ public static void reloadCustomTools(McpSyncServer server, ServerConfig config) String oldSig = customToolSignatures.get(name); if (oldSig == null || !oldSig.equals(newSig)) { try { - LOG.info(() -> "[DEBUG] Updating changed tool: " + name); server.removeTool(name); } catch (Exception ex) { - LOG.info(() -> "[DEBUG] Failed to remove tool for update: " + name + ". Exception: " + ex); + LOG.info(() -> "Failed to remove tool for update: " + name + ". Exception: " + ex); } server.addTool(makeSyncToolSpecification(tc, config)); customToolSignatures.put(name, newSig); } else { - LOG.info(() -> "[DEBUG] Tool unchanged (skipped): " + name); + LOG.info(() -> "Tool unchanged (skipped): " + name); } } } diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java index 78824c06..30bd7e6c 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.oracle.database.mcptoolkit.LoadedConstants; import com.oracle.database.mcptoolkit.ServerConfig; +import com.oracle.database.mcptoolkit.Utils; import com.oracle.database.mcptoolkit.OracleDatabaseMCPToolkit; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.spec.McpSchema; @@ -169,6 +170,42 @@ public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerCon // Ensure 'tools' map exists Map tools = getOrCreateMap(root, "tools"); + // Remove if requested + Object removeFlag = callReq.arguments().get("remove"); + boolean remove = (removeFlag instanceof Boolean b && b) || + (removeFlag != null && "true".equalsIgnoreCase(String.valueOf(removeFlag))); + if (remove) { + if (tools.remove(name) != null) { + root.put("tools", tools); + try (Writer w = Files.newBufferedWriter(path)) { + yaml.dump(root, w); + } + // Try to remove the tool from the running server as well + try { + OracleDatabaseMCPToolkit.getServer().removeTool(name); + Utils.unregisterCustomToolLocally(name); + } catch (Exception ignored) {} + + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "status", "ok", + "message", "Tool '" + name + "' removed from YAML. Reload will occur shortly.", + "configFile", cfgPath + )) + .addTextContent("{\"status\":\"ok\",\"removed\":\"" + name + "\"}") + .build(); + } else { + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "status", "noop", + "message", "Tool '" + name + "' not found; nothing to remove.", + "configFile", cfgPath + )) + .addTextContent("{\"status\":\"noop\",\"missing\":\"" + name + "\"}") + .build(); + } + } + // Upsert tool entry Map t = getOrCreateMap(tools, name); @@ -202,6 +239,13 @@ public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerCon yaml.dump(root, w); } + // Try to remove stale instance at runtime (best-effort hot update); server will re-add on reload + try { + var srv = OracleDatabaseMCPToolkit.getServer(); + if (srv != null) srv.removeTool(name); + } catch (Exception ignored) {} + + // The config poller will pick up the change and hot-reload within ~2 seconds. return McpSchema.CallToolResult.builder() .structuredContent(Map.of( @@ -212,6 +256,7 @@ public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerCon .addTextContent("{\"status\":\"ok\",\"name\":\"" + name + "\"}") .build(); } catch (Exception e) { + return McpSchema.CallToolResult.builder() .addTextContent("Unexpected: " + e.getMessage()) .isError(true) diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java index ddae3043..3d2a6b76 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java @@ -175,7 +175,8 @@ public class ToolSchemas { { "type": "object", "properties": { - "name": { "type": "string", "description": "Tool name (YAML key). Required for upsert." }, + "name": { "type": "string", "description": "Tool name (YAML key). Required for upsert or delete." }, + "remove": { "type": "boolean", "description": "If true, remove this tool from the YAML config. Other fields are ignored." }, "description": { "type": "string", "description": "Human-friendly description of the tool" }, "dataSource": { "type": "string", "description": "Reference key from dataSources to use for this tool" }, "statement": { "type": "string", "description": "SQL statement to execute (SELECT or DML)" }, @@ -197,4 +198,5 @@ public class ToolSchemas { "required": ["name"] } """; + } From be87b405efddcace5c415cb8eb49262aee31c8a7 Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Tue, 20 Jan 2026 10:40:33 +0100 Subject: [PATCH 04/17] Supporting toolsets and custom toolsets --- src/oracle-db-mcp-toolkit/README.md | 27 +++++- .../database/mcptoolkit/ServerConfig.java | 82 ++++++++++++++++--- .../mcptoolkit/config/ConfigRoot.java | 8 ++ 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/oracle-db-mcp-toolkit/README.md b/src/oracle-db-mcp-toolkit/README.md index f0879dbe..83175532 100644 --- a/src/oracle-db-mcp-toolkit/README.md +++ b/src/oracle-db-mcp-toolkit/README.md @@ -76,12 +76,23 @@ tools: description: Hotel name to search for. required: false statement: SELECT * FROM hotels WHERE name LIKE '%' || :name || '%' + +# Optional toolsets combining custom tools +toolsets: + reporting: [hotels-by-name] ``` To enable YAML configuration, launch the server with: ```bash java -DconfigFile=/path/to/config.yaml -jar .jar ``` +Toolsets can be enabled from `-Dtools` alongside individual tools. For example: +- `-Dtools=reporting` enables all tools in the `reporting` toolset +- `-Dtools=reporting,explain` enables your `reporting` set plus the built-in `explain` toolset (see below) +- `-Dtools=*` or omit `-Dtools` to enable everything + +> Tip: You can also manage YAML-defined tools at runtime using the `edit-tools` admin tool; see section 3.5. + --- ## 3. Built-in Tools @@ -424,12 +435,20 @@ Ultimately, the token must be included in the http request header (e.g. `Authori tools (aka -Dtools) No - Comma-separated allow-list of tool names to enable. - Use * or all to enable everything. - If omitted, all tools are enabled by default. + Comma-separated allow-list of tool or toolset names to enable (case-insensitive).
+ You can pass individual tools (e.g. get-jdbc-stats) or any of the following built-in toolsets: +
    +
  • log_analyzer — all JDBC log and RDBMS/SQLNet analysis tools
  • +
  • explainexplain_plan
  • +
  • similaritysimilarity_search
  • +
  • admin — server admin tools (list-tools, edit-tools)
  • +
+ You can also define your own YAML toolsets: and reference them here. + Use * or all to enable everything. If omitted, all tools are enabled by default. - get-jdbc-stats,get-jdbc-queries + log_analyzer or reporting,explain + ojdbc.ext.dir No diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java index 98d91cd3..b06305d6 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java @@ -64,6 +64,23 @@ private ServerConfig( "similarity_search", "explain_plan" ); + /** Built-in toolsets covering predefined tools. Lowercase keys and members. */ + private static final Map> BUILTIN_TOOLSETS = Map.of( + "log_analyzer", Set.of( + "get-jdbc-stats", + "get-jdbc-queries", + "get-jdbc-errors", + "list-log-files-from-directory", + "jdbc-log-comparison", + "get-jdbc-connection-events", + "get-rdbms-errors", + "get-rdbms-packet-dumps" + ), + "explain", Set.of("explain_plan"), + "similarity", Set.of("similarity_search"), + "admin", Set.of("list-tools", "edit-tools") + ); + /** * Builds a {@link ServerConfig} from JVM system properties (i.e., {@code -Dkey=value}), @@ -83,8 +100,7 @@ private ServerConfig( * @throws IllegalStateException if required properties are missing from both system properties and YAML config */ public static ServerConfig fromSystemPropertiesAndYaml(ConfigRoot configRoot, String defaultSourceKey) { - Set tools = parseToolsProp(LoadedConstants.TOOLS); - boolean needDb = wantsAnyDbTools(tools); + Set rawTools = parseToolsProp(LoadedConstants.TOOLS); String dbUrl = LoadedConstants.DB_URL; String dbUser = LoadedConstants.DB_USER; @@ -98,7 +114,10 @@ public static ServerConfig fromSystemPropertiesAndYaml(ConfigRoot configRoot, St entry.getValue().name = entry.getKey(); } } - configRoot.substituteEnvVars(); + if (configRoot != null) configRoot.substituteEnvVars(); + + Set expandedTools = expandToolsFilter(rawTools, configRoot); + boolean allLoadedConstantsPresent = dbUrl != null && !dbUrl.isBlank() && dbUser != null && !dbUser.isBlank() @@ -112,6 +131,8 @@ public static ServerConfig fromSystemPropertiesAndYaml(ConfigRoot configRoot, St defaultSourceName = defaultSourceKey; } + boolean needDb = wantsAnyDbToolsExpanded(expandedTools); + if (needDb && (dbUrl == null || dbUrl.isBlank())) { throw new IllegalStateException("Missing required db.url in both system properties and YAML config"); } @@ -121,7 +142,7 @@ public static ServerConfig fromSystemPropertiesAndYaml(ConfigRoot configRoot, St if (needDb && (dbPass == null || dbPass.length == 0)) { throw new IllegalStateException("Missing required db.password in both system properties and YAML config"); } - return new ServerConfig(dbUrl, dbUser, dbPass, tools, sources, toolsMap); + return new ServerConfig(dbUrl, dbUser, dbPass, expandedTools, sources, toolsMap); } /** @@ -132,8 +153,9 @@ public static ServerConfig fromSystemPropertiesAndYaml(ConfigRoot configRoot, St * @throws IllegalStateException if {@code db.url} is missing or blank */ static ServerConfig fromSystemProperties() { - Set tools = parseToolsProp(LoadedConstants.TOOLS); - boolean needDb = wantsAnyDbTools(tools); + Set raw = parseToolsProp(LoadedConstants.TOOLS); + Set expanded = expandToolsFilter(raw, null); + boolean needDb = wantsAnyDbToolsExpanded(expanded); String dbUrl = LoadedConstants.DB_URL; if (needDb && (dbUrl == null || dbUrl.isBlank())) { @@ -144,7 +166,7 @@ static ServerConfig fromSystemProperties() { dbUrl, LoadedConstants.DB_USER, LoadedConstants.DB_PASSWORD, - tools, + expanded, new HashMap<>(), new HashMap<>() ); @@ -174,9 +196,49 @@ private static Set parseToolsProp(String prop) { return s; } - private static boolean wantsAnyDbTools(Set toolsFilter) { - if (toolsFilter == null) return true; // null == all tools enabled - for (String t : toolsFilter) { + /** + * Expands the raw -Dtools filter to concrete tool names by resolving YAML and built-in toolsets. + * Returns null to mean "all tools" if the input is null. + */ + private static Set expandToolsFilter(Set raw, ConfigRoot configRoot) { + if (raw == null) return null; // all tools enabled + Map> yamlSets = (configRoot != null) ? configRoot.toolsets : null; + + Set out = new LinkedHashSet<>(); + for (String name : raw) { + String k = name == null ? null : name.trim().toLowerCase(Locale.ROOT); + if (k == null || k.isEmpty()) continue; + + // YAML toolset match + if (yamlSets != null && yamlSets.containsKey(k)) { + List members = yamlSets.get(k); + if (members != null) { + for (String m : members) { + if (m != null) { + String mm = m.trim().toLowerCase(Locale.ROOT); + if (!mm.isEmpty()) out.add(mm); + } + } + } + continue; + } + + // Built-in toolset match + Set builtin = BUILTIN_TOOLSETS.get(k); + if (builtin != null) { + out.addAll(builtin); + continue; + } + + // Fallback to explicit tool name + out.add(k); + } + return out; + } + + private static boolean wantsAnyDbToolsExpanded(Set expandedFilter) { + if (expandedFilter == null) return true; // all enabled + for (String t : expandedFilter) { if (DB_TOOLS.contains(t)) return true; } return false; diff --git a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java index 38fcca68..53bfbf28 100644 --- a/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java +++ b/src/oracle-db-mcp-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java @@ -7,6 +7,7 @@ package com.oracle.database.mcptoolkit.config; +import java.util.List; import java.util.Map; /** @@ -15,6 +16,13 @@ public class ConfigRoot { public Map dataSources; public Map tools; + /** + * Optional named toolsets allowing users to group custom tools and enable them with -Dtools. + * Example YAML: + * toolsets: + * reporting: [top_customers, sales_by_region] + */ + public Map> toolsets; /** * Substitutes environment variables in the source and tool configurations. From 6c183423f4daa0cab5f62bacb0f017c600c590df Mon Sep 17 00:00:00 2001 From: Mouhsin Elmajdouby Date: Tue, 20 Jan 2026 16:26:23 +0100 Subject: [PATCH 05/17] add database tools to admin toolset and standardize tool naming --- src/oracle-db-mcp-java-toolkit/README.md | 65 ++- .../database/mcptoolkit/ServerConfig.java | 27 +- .../com/oracle/database/mcptoolkit/Utils.java | 69 ++- .../database/mcptoolkit/tools/AdminTools.java | 476 +++++++++++++++++- .../tools/ExplainAndExecutePlanTool.java | 36 +- .../tools/SimilaritySearchTool.java | 41 +- .../database/mcptoolkit/tools/SqlQueries.java | 50 ++ .../mcptoolkit/tools/ToolSchemas.java | 27 + 8 files changed, 684 insertions(+), 107 deletions(-) create mode 100644 src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SqlQueries.java diff --git a/src/oracle-db-mcp-java-toolkit/README.md b/src/oracle-db-mcp-java-toolkit/README.md index f6257f13..a1c26ac3 100644 --- a/src/oracle-db-mcp-java-toolkit/README.md +++ b/src/oracle-db-mcp-java-toolkit/README.md @@ -7,6 +7,7 @@ Oracle Database MCP Toolkit is a Model Context Protocol (MCP) server that lets y * Define your own custom tools via a simple YAML configuration file. * Use built-in tools: * Analyze Oracle JDBC thin client logs and RDBMS/SQLNet trace files. + * Database tools for SQL execution, table management, transactions, and performance monitoring. * Database-powered tools, including vector similarity search and SQL execution plan analysis. * Admin tools for runtime discovery and configuration: list available tools and live-edit YAML-defined tools with hot reload. * Deploy locally or remotely - optionally as a container - with support for TLS and OAuth2 @@ -98,13 +99,41 @@ Toolsets can be enabled from `-Dtools` alongside individual tools. For example: - `-Dtools=reporting,explain` enables your `reporting` set plus the built-in `explain` toolset (see below) - `-Dtools=*` or omit `-Dtools` to enable everything -> Tip: You can also manage YAML-defined tools at runtime using the `edit-tools` admin tool; see section 3.5. +> Tip: You can also manage YAML-defined tools at runtime using the `edit-tools` admin tool; see section 3.9. --- ## 3. Built-in Tools -### 3.1. Oracle JDBC Log Analysis +### 3.1. Database Operations +These tools provide direct SQL execution capabilities: + +- **`read-query`**: Execute SELECT-only queries and return results as JSON. +- **`write-query`**: Execute DML/DDL operations (INSERT, UPDATE, DELETE, CREATE, etc.) with autocommit. + +### 3.2. Table Management +These tools help you manage database tables: + +- **`create-table`**: Create a table using full CREATE TABLE statements +- **`delete-table`**: Drop an existing table by name +- **`list-tables`**: List all tables and synonyms in the current schema +- **`describe-table`**: Get detailed column information for any table + +### 3.3. Transaction Management +These tools provide fine-grained control over database transactions: + +- **`start-transaction`**: Begin a new JDBC transaction and get a transaction ID +- **`resume-transaction`**: Verify if a transaction ID is still active +- **`commit-transaction`**: Commit and close a transaction +- **`rollback-transaction`**: Rollback and close a transaction + +### 3.4. Database Monitoring +These tools help monitor database health and performance: + +- **`db-ping`**: Connectivity + timings (connect/round-trip) + Database metadata +- **`db-metrics-range`**: Retrieve Oracle performance metrics from V$SYSSTAT + +### 3.5. Oracle JDBC Log Analysis These tools operate on Oracle JDBC thin client logs: @@ -115,16 +144,16 @@ These tools operate on Oracle JDBC thin client logs: * **`list-log-files-from-directory`**: List all visible files from a specified directory, which helps the user analyze multiple files with one prompt. * **`jdbc-log-comparison`**: Compares two log files for performance metrics, errors, and network information. -### 3.2. RDBMS/SQLNet Trace Analysis: +### 3.6. RDBMS/SQLNet Trace Analysis: These tools operate on RDBMS/SQLNet trace files: * **`get-rdbms-errors`**: Extracts errors from RDBMS/SQLNet trace files. * **`get-rdbms-packet-dumps`**: Extracts packet dumps for a specific connection ID. -### 3.3. Vector Similarity Search +### 3.7. Vector Similarity Search -* **`similarity_search`**: Perform semantic similarity search using Oracle’s vector features (`VECTOR_EMBEDDING`, `VECTOR_DISTANCE`). +* **`similarity-search`**: Perform semantic similarity search using Oracle’s vector features (`VECTOR_EMBEDDING`, `VECTOR_DISTANCE`). **Inputs:** @@ -140,9 +169,9 @@ These tools operate on RDBMS/SQLNet trace files: * JSON array of similar rows with scores and truncated snippets. -### 3.4. SQL Execution Plan Analysis +### 3.8. SQL Execution Plan Analysis -* **`explain_plan`**: Generate Oracle execution plans and receive a pre-formatted LLM prompt for tuning and explanation. +* **`explain-plan`**: Generate Oracle execution plans and receive a pre-formatted LLM prompt for tuning and explanation. **Modes:** @@ -165,9 +194,15 @@ These tools operate on RDBMS/SQLNet trace files: * `planText`: DBMS_XPLAN output. * `llmPrompt`: A structured prompt for an LLM to explain + tune the plan. -### 3.5. Admin and Runtime Configuration Tools +### 3.9. Admin, Runtime Configuration and Database Tools + +These tools help you discover what's enabled and manage YAML-defined tools at runtime, and perform core database operations. +They are part of the `admin` toolset (enable via `-Dtools=admin` or include individual tool names). + +_Note: The admin toolset includes both the admin tools listed below AND all database operation tools described in +sections 3.1-3.4 (Database Operations, Table Management, Transaction Management, and Database Monitoring)._ -These tools help you discover what's enabled and manage YAML-defined tools at runtime. They are part of the `admin` toolset (enable via `-Dtools=admin` or include `list-tools,edit-tools`). +#### Admin Tools: - `list-tools`: List all available tools with their descriptions. - Inputs: none @@ -451,17 +486,17 @@ Ultimately, the token must be included in the http request header (e.g. `Authori No Comma-separated allow-list of tool or toolset names to enable (case-insensitive).
- You can pass individual tools (e.g. get-jdbc-stats) or any of the following built-in toolsets: + You can pass individual tools (e.g. get-jdbc-stats, read-query) or any of the following built-in toolsets:
    -
  • log_analyzer — all JDBC log and RDBMS/SQLNet analysis tools
  • -
  • explainexplain_plan
  • -
  • similaritysimilarity_search
  • -
  • admin — server admin tools (list-tools, edit-tools)
  • +
  • log_analyzer — all JDBC log and RDBMS/SQLNet analysis tools (get-jdbc-stats, get-jdbc-queries, get-jdbc-errors, list-log-files-from-directory, jdbc-log-comparison, get-jdbc-connection-events, get-rdbms-errors, get-rdbms-packet-dumps)
  • +
  • explainexplain-plan
  • +
  • similaritysimilarity-search
  • +
  • admin — server admin and database tools (list-tools, edit-tools, read-query, write-query, create-table, delete-table, list-tables, describe-table, start-transaction, resume-transaction, commit-transaction, rollback-transaction, db-ping, db-metrics-range)
You can also define your own YAML toolsets: and reference them here. Use * or all to enable everything. If omitted, all tools are enabled by default. - log_analyzer or reporting,explain + admin,log_analyzer or reporting,explain ojdbc.ext.dir diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java index b06305d6..647581f3 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java @@ -11,12 +11,7 @@ import com.oracle.database.mcptoolkit.config.DataSourceConfig; import com.oracle.database.mcptoolkit.config.ToolConfig; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Immutable server configuration loaded from system properties. @@ -30,7 +25,9 @@ *
    *
  • {@code db.user}
  • *
  • {@code db.password}
  • - *
  • {@code tools} — comma-separated allow-list of tool names; {@code *} or {@code all} enables all.
  • + *
  • {@code tools} — comma-separated allow-list of tool names or toolset + * names; (e.g., {@code log_analyzer}, {@code admin}, {@code explain}, {@code similarity}); + * {@code *} or {@code all} enables all.
  • *
* *

Use {@link #fromSystemProperties()} to create an instance with validation and defaults.

@@ -61,7 +58,10 @@ private ServerConfig( } private static final Set DB_TOOLS = Set.of( - "similarity_search", "explain_plan" + "similarity_search", "explain_plan", + "read-query", "write-query", "create-table", "delete-table", + "list-tables", "describe-table", "start-transaction", "resume-transaction", + "commit-transaction", "rollback-transaction", "db-ping", "db-metrics-range" ); /** Built-in toolsets covering predefined tools. Lowercase keys and members. */ @@ -76,9 +76,14 @@ private ServerConfig( "get-rdbms-errors", "get-rdbms-packet-dumps" ), - "explain", Set.of("explain_plan"), - "similarity", Set.of("similarity_search"), - "admin", Set.of("list-tools", "edit-tools") + "explain", Set.of("explain-plan"), + "similarity", Set.of("similarity-search"), + "admin", Set.of( + "list-tools", "edit-tools", + "read-query", "write-query", "create-table", "delete-table", + "list-tables", "describe-table", "start-transaction", "resume-transaction", + "commit-transaction", "rollback-transaction", "db-ping", "db-metrics-range" + ) ); diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java index ce4e22a3..4b075a10 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java @@ -49,6 +49,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import java.util.stream.Stream; import java.lang.reflect.Field; @@ -64,6 +65,7 @@ */ public class Utils { private static final Logger LOG = Logger.getLogger(Utils.class.getName()); + private static final Pattern SAFE_IDENT = Pattern.compile("[A-Za-z0-9_$.#]+"); private static final Map dataSources = new ConcurrentHashMap<>(); private static volatile DataSource defaultDataSource; @@ -91,22 +93,23 @@ static void addSyncToolSpecifications(McpSyncServer server, ServerConfig config) } } - // similarity_search - if (isToolEnabled(config, "similarity_search")) { + // similarity-search + if (isToolEnabled(config, "similarity-search")) { server.addTool(SimilaritySearchTool.getSymilaritySearchTool(config)); } - // explain_plan - if (isToolEnabled(config, "explain_plan")) { + // explain-plan + if (isToolEnabled(config, "explain-plan")) { server.addTool(ExplainAndExecutePlanTool.getExplainAndExecutePlanTool(config)); } // admin tools - if (isToolEnabled(config, "list-tools")) { - server.addTool(AdminTools.getListToolsTool(config)); - } - if (isToolEnabled(config, "edit-tools")) { - server.addTool(AdminTools.getEditToolsTool(config)); + List adminSpecs = AdminTools.getTools(config); + for (McpServerFeatures.SyncToolSpecification spec : adminSpecs) { + String toolName = spec.tool().name(); + if (isToolEnabled(config, toolName)) { + server.addTool(spec); + } } // ---------- Dynamically Added Tools ---------- @@ -439,7 +442,7 @@ static void installExternalExtensionsFromDir() { * @return list of rows with column:value mapping * @throws SQLException if reading from ResultSet fails */ - static List> rsToList(ResultSet rs) throws SQLException { + public static List> rsToList(ResultSet rs) throws SQLException { List> out = new ArrayList<>(); ResultSetMetaData md = rs.getMetaData(); int cols = md.getColumnCount(); @@ -486,4 +489,50 @@ private static boolean isToolEnabled(ServerConfig config, String toolName) { String key = toolName.toLowerCase(Locale.ROOT); return config.toolsFilter.contains(key); } + + /** + * Checks if the provided SQL looks like a SELECT. + * + * @param sql the SQL string + * @return true if it begins with "SELECT" (case-insensitive) + */ + public static boolean looksSelect(String sql) { + return sql != null && sql.trim().regionMatches(true, 0, "SELECT", 0, 6); + } + + /** + * DDL detector (CREATE/ALTER/DROP/TRUNCATE/RENAME/COMMENT/GRANT/REVOKE). + * Used to block DDL inside user-managed transactions. + */ + public static boolean isDdl(String sql) { + if (sql == null) return false; + String s = sql.trim().toUpperCase(); + return s.startsWith("CREATE ") + || s.startsWith("ALTER ") + || s.startsWith("DROP ") + || s.startsWith("TRUNCATE ") + || s.startsWith("RENAME ") + || s.startsWith("COMMENT ") + || s.startsWith("GRANT ") + || s.startsWith("REVOKE "); + } + + /** + * Escapes and quotes a potentially unsafe identifier for SQL use. + * + * @param ident identifier to quote + * @return a quoted or validated identifier + */ + public static String quoteIdent(String ident) { + if (ident == null) throw new IllegalArgumentException("identifier is null"); + String s = ident.trim(); + if (!SAFE_IDENT.matcher(s).matches()) { + return "\"" + s.replace("\"", "\"\"") + "\""; + } + return s; + } + + + + } diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java index 30bd7e6c..e1edc55e 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java @@ -21,20 +21,70 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static com.oracle.database.mcptoolkit.Utils.*; /** * Admin/maintenance tools: * - list-tools: list all available tools with descriptions * - edit-tools: upsert a YAML-defined tool in the config file and rely on runtime reload + * - read-query: execute SELECT queries and return JSON results + * - write-query: execute DML/DDL statements + * - create-table: create table. + * - delete-table: drop table by name + * - list-tables: list all tables and synonyms in the current schema + * - describe-table: get column details for a specific table + * - start-transaction: begin a new JDBC transaction + * - resume-transaction: verify a transaction ID is active + * - commit-transaction: commit and close a transaction + * - rollback-transaction: rollback and close a transaction + * - db-ping: check database connectivity and latency + * - db-metrics-range: retrieve Oracle performance metrics from V$SYSSTAT */ public final class AdminTools { + + // Transaction store (txId -> Connection) + private static final Map TX = new ConcurrentHashMap<>(); + + private AdminTools() {} + /** + * Returns all admin tool specifications. + */ + public static List getTools(ServerConfig config) { + List tools = new ArrayList<>(); + + tools.add(getListToolsTool(config)); + tools.add(getEditToolsTool(config)); + tools.add(getReadQueryTool(config)); + tools.add(getWriteQueryTool(config)); + tools.add(getCreateTableTool(config)); + tools.add(getDeleteTableTool(config)); + tools.add(getListTablesTool(config)); + tools.add(getDescribeTableTool(config)); + tools.add(getStartTransactionTool(config)); + tools.add(getResumeTransactionTool()); + tools.add(getCommitTransactionTool()); + tools.add(getRollbackTransactionTool()); + tools.add(getDbPingTool(config)); + tools.add(getDbMetricsTool(config)); + + // Add shutdown hook to clean up transactions + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + TX.values().forEach(conn -> { + try { if (!conn.getAutoCommit()) conn.rollback(); } catch (Exception ignored) {} + try { conn.close(); } catch (Exception ignored) {} + }); + TX.clear(); + })); + + return tools; + } + public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerConfig config) { return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() @@ -266,6 +316,385 @@ public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerCon .build(); } + private static McpServerFeatures.SyncToolSpecification getReadQueryTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("read-query") + .title("Read Query") + .description("Run a SELECT and return rows as JSON. (Optionally accepts txId to run inside an open transaction.)") + .inputSchema(ToolSchemas.SQL_ONLY) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (ConnLease lease = acquireConnection(config, callReq.arguments().get("txId"))) { + Connection c = lease.c; + String sql = String.valueOf(callReq.arguments().get("sql")); + if (!looksSelect(sql)) { + return new McpSchema.CallToolResult("Only SELECT is allowed", true); + } + var rows = rsToList(c.createStatement().executeQuery(sql)); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("rows", rows, "rowCount", rows.size())) + .addTextContent(new ObjectMapper().writeValueAsString(rows)) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getWriteQueryTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("write-query") + .title("Write Query") + .description("Execute DML (inside a transaction if txId is provided) or DML/DDL in autocommit mode.") + .inputSchema(ToolSchemas.SQL_ONLY) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (ConnLease lease = acquireConnection(config, callReq.arguments().get("txId"))) { + Connection c = lease.c; + String sql = String.valueOf(callReq.arguments().get("sql")); + boolean inTx = !lease.closeOnExit; + if (inTx && isDdl(sql)) { + return new McpSchema.CallToolResult( + "DDL is not allowed inside a transaction. Run this statement without txId.", true); + } + int n = execUpdate(c, sql); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("updateCount", n)) + .addTextContent("{\"updateCount\":" + n + "}") + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getCreateTableTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("create-table") + .title("Create Table") + .description("Create a table from a full CREATE TABLE statement.") + .inputSchema(ToolSchemas.SQL_ONLY) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + String sql = String.valueOf(callReq.arguments().get("sql")); + execUpdate(c, sql); + return new McpSchema.CallToolResult("OK", false); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDeleteTableTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("delete-table") + .title("Drop Table") + .description("Drop a table by name.") + .inputSchema(ToolSchemas.DROP_OR_DESCRIBE_TABLE) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + String table = String.valueOf(callReq.arguments().get("table")); + int updateCount = execUpdate(c, "DROP TABLE " + quoteIdent(table)); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("updateCount", updateCount, "table", table)) + .addTextContent("OK") + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getListTablesTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("list-tables") + .title("List Tables & Synonyms") + .description("List TABLE and SYNONYM in the current schema via DatabaseMetaData (includes comments).") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + DatabaseMetaData md = c.getMetaData(); + String schema = Optional.ofNullable(c.getSchema()) + .orElseGet(() -> { + try { + return md.getUserName(); + } catch (SQLException e) { + return null; + } + }); + if (schema != null) + schema = schema.toUpperCase(Locale.ROOT); + List> tables = new ArrayList<>(); + List> synonyms = new ArrayList<>(); + try (ResultSet rs = md.getTables(null, schema, "%", new String[]{"TABLE", "SYNONYM"})) { + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("owner", rs.getString("TABLE_SCHEM")); + row.put("name", rs.getString("TABLE_NAME")); + row.put("kind", rs.getString("TABLE_TYPE")); + row.put("comment", rs.getString("REMARKS")); + String kind = String.valueOf(row.get("kind")); + if ("SYNONYM".equalsIgnoreCase(kind)) { + synonyms.add(row); + } else { + tables.add(row); + } + } + } + Map payload = Map.of( + "schema", schema, + "counts", Map.of("tables", tables.size(), "synonyms", synonyms.size()), + "tables", tables, + "synonyms", synonyms + ); + return McpSchema.CallToolResult.builder() + .structuredContent(payload) + .addTextContent(new ObjectMapper().writeValueAsString(payload)) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDescribeTableTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("describe-table") + .title("Describe Table") + .description("Describe columns via DatabaseMetaData. Returns COLUMN_ID, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COMMENTS.") + .inputSchema(ToolSchemas.DROP_OR_DESCRIBE_TABLE) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + String table = String.valueOf(callReq.arguments().get("table")); + if (table == null || table.isBlank()) { + return new McpSchema.CallToolResult("Parameter 'table' is required", true); + } + DatabaseMetaData md = c.getMetaData(); + String schema = Optional.ofNullable(c.getSchema()) + .orElseGet(() -> { + try { + return md.getUserName(); + } catch (SQLException e) { + return null; + } + }); + if (schema != null) + schema = schema.toUpperCase(Locale.ROOT); + String tableName = table.toUpperCase(Locale.ROOT); + List> rows = new ArrayList<>(); + try (ResultSet rs = md.getColumns(null, schema, tableName, "%")) { + while (rs.next()) { + int ordinal = rs.getInt("ORDINAL_POSITION"); + String colName = rs.getString("COLUMN_NAME"); + String typeName = rs.getString("TYPE_NAME"); + int colSize = rs.getInt("COLUMN_SIZE"); + int precision = rs.getInt("COLUMN_SIZE"); + int scale = rs.getInt("DECIMAL_DIGITS"); + int nullableFlag = rs.getInt("NULLABLE"); + String remarks = rs.getString("REMARKS"); + String nullableYN = (nullableFlag == DatabaseMetaData.columnNullable) ? "Y" : "N"; + if (remarks == null) + remarks = ""; + Map row = new LinkedHashMap<>(); + row.put("COLUMN_ID", ordinal); + row.put("COLUMN_NAME", colName); + row.put("DATA_TYPE", typeName); + row.put("DATA_LENGTH", colSize); + row.put("DATA_PRECISION", (scale >= 0 && precision > 0) ? precision : null); + row.put("DATA_SCALE", (scale >= 0) ? scale : null); + row.put("NULLABLE", nullableYN); + row.put("COMMENTS", remarks); + rows.add(row); + } + } + rows.sort(Comparator.comparingInt(m -> ((Number)m.get("COLUMN_ID")).intValue())); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("columns", rows)) + .addTextContent(new ObjectMapper().writeValueAsString(rows)) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getStartTransactionTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("start-transaction") + .title("Start Transaction") + .description("Start a JDBC transaction (autoCommit=false). Returns txId.") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + Connection c = openConnection(config, null); + c.setAutoCommit(false); + String txId = UUID.randomUUID().toString(); + TX.put(txId, c); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("txId", txId)) + .addTextContent("{\"txId\":\"" + txId + "\"}") + .build(); + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getResumeTransactionTool() { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("resume-transaction") + .title("Resume Transaction") + .description("Verify a txId is active (no-op). Returns ok.") + .inputSchema(ToolSchemas.TX_ID) + .build()) + .callHandler((exchange, callReq) -> { + String txId = String.valueOf(callReq.arguments().get("txId")); + if (TX.containsKey(txId)) { + return new McpSchema.CallToolResult("{\"ok\":true}", false); + } + return new McpSchema.CallToolResult("Unknown txId", true); + }) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getCommitTransactionTool() { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("commit-transaction") + .title("Commit Transaction") + .description("Commit and close a txId.") + .inputSchema(ToolSchemas.TX_ID) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + String txId = String.valueOf(callReq.arguments().get("txId")); + Connection c = TX.remove(txId); + if (c == null) + return new McpSchema.CallToolResult("Unknown txId", true); + try (c) { + c.commit(); + return new McpSchema.CallToolResult("{\"ok\":true}", false); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getRollbackTransactionTool() { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("rollback-transaction") + .title("Rollback Transaction") + .description("Rollback and close a txId.") + .inputSchema(ToolSchemas.TX_ID) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + String txId = String.valueOf(callReq.arguments().get("txId")); + Connection c = TX.remove(txId); + if (c == null) + return new McpSchema.CallToolResult("Unknown txId", true); + try (c) { + c.rollback(); + return new McpSchema.CallToolResult("{\"ok\":true}", false); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDbPingTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("db-ping") + .title("DB Ping") + .description("Checks connectivity and round-trip latency; returns user, schema, DB name, and version.") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + long connStart = System.nanoTime(); + try (Connection c = openConnection(config, null)) { + long connectMs = (System.nanoTime() - connStart) / 1_000_000L; + long rtStart = System.nanoTime(); + String user = null, schema = null, dbName = null, dbTime = null; + try (PreparedStatement ps = c.prepareStatement(SqlQueries.DB_PING_QUERY); + ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + user = rs.getString(1); + schema = rs.getString(2); + dbName = rs.getString(3); + dbTime = rs.getString(4); + } + } + long roundTripMs = (System.nanoTime() - rtStart) / 1_000_000L; + DatabaseMetaData md = c.getMetaData(); + Map sc = new LinkedHashMap<>(); + sc.put("ok", true); + sc.put("connectLatencyMs", connectMs); + sc.put("roundTripMs", roundTripMs); + sc.put("dbProduct", md.getDatabaseProductName()); + sc.put("dbVersion", md.getDatabaseProductVersion()); + sc.put("user", user); + sc.put("schema", schema); + sc.put("dbName", dbName); + sc.put("dbTime", dbTime); + String text = String.format(""" + OK — connection verified + connectLatencyMs: %d + roundTripMs: %d + dbProduct: %s + dbVersion: %s + user: %s + schema: %s + dbName: %s + dbTime: %s + """, connectMs, roundTripMs, md.getDatabaseProductName(), + md.getDatabaseProductVersion(), user, schema, dbName, dbTime + ); + return McpSchema.CallToolResult.builder() + .structuredContent(sc) + .addTextContent(text) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDbMetricsTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("db-metrics-range") + .title("DB Metrics") + .description("Returns Oracle DB metrics from V$SYSSTAT (cumulative counters only).") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null); + Statement st = c.createStatement(); + ResultSet rs = st.executeQuery(SqlQueries.DB_METRICS_RANGE)) { + var rows = rsToList(rs); + Map metrics = new LinkedHashMap<>(); + StringBuilder text = new StringBuilder( + "Live metrics — V$SYSSTAT (cumulative counters; rates not available):\n" + ); + for (Map r : rows) { + String name = String.valueOf(r.get("NAME")); + Object value = r.get("VALUE"); + metrics.put(name, Map.of("value", value, "unit", "")); + text.append("- ").append(name).append(": ").append(value).append("\n"); + } + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "source", "V$SYSSTAT", + "note", "Cumulative since instance startup; these are totals, not per-second rates.", + "metrics", metrics)) + .addTextContent(text.toString().trim()) + .build(); + } + })) + .build(); + } + private static boolean isEnabled(ServerConfig config, String toolName) { if (config.toolsFilter == null) return true; String key = toolName.toLowerCase(Locale.ROOT); @@ -296,4 +725,41 @@ private static void copyIfPresent(Map src, Map dst, String Object v = src.get(key); if (v != null) dst.put(key, v); } + + private static int execUpdate(Connection c, String sql) throws SQLException { + try (Statement st = c.createStatement()) { + return st.executeUpdate(sql); + } + } + + private static class ConnLease implements AutoCloseable { + final Connection c; + final boolean closeOnExit; + + ConnLease(Connection c, boolean closeOnExit) { + this.c = c; + this.closeOnExit = closeOnExit; + } + + @Override + public void close() { + if (closeOnExit) { + try { c.close(); } catch (Exception ignored) {} + } + } + } + + private static ConnLease acquireConnection(ServerConfig config, Object txIdArg) throws SQLException { + String txId = (txIdArg == null) ? null : String.valueOf(txIdArg); + if (txId == null || txId.isBlank()) { + Connection c = openConnection(config, null); + try { + c.setAutoCommit(true); + } catch (Exception ignored) {} + return new ConnLease(c, true); + } + Connection c = TX.get(txId); + if (c == null) throw new SQLException("Unknown txId"); + return new ConnLease(c, false); + } } diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java index 42ad06fa..f0def8bc 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java @@ -24,8 +24,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static com.oracle.database.mcptoolkit.Utils.openConnection; -import static com.oracle.database.mcptoolkit.Utils.tryCall; +import static com.oracle.database.mcptoolkit.Utils.*; /** * Provides functionality for explaining and executing Oracle SQL plans. @@ -34,18 +33,18 @@ */ public class ExplainAndExecutePlanTool { /** - * Returns a tool specification for the "explain_plan" tool. + * Returns a tool specification for the "explain-plan" tool. * This tool generates an Oracle execution plan for the provided SQL and * produces an accompanying LLM prompt to explain and tune the plan. * * @param config Server configuration - * @return Tool specification for the "explain_plan" tool + * @return Tool specification for the "explain-plan" tool */ public static McpServerFeatures.SyncToolSpecification getExplainAndExecutePlanTool(ServerConfig config) { return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() - .name("explain_plan") + .name("explain-plan") .title("Explain Plan (static or dynamic)") .description(""" Returns an Oracle execution plan for the provided SQL. @@ -214,16 +213,6 @@ private static String readXplan(Connection c, boolean dynamic, String xplanOptio return sb.toString().trim(); } - /** - * Checks if the provided SQL looks like a SELECT. - * - * @param sql the SQL string - * @return true if it begins with "SELECT" (case-insensitive) - */ - static boolean looksSelect(String sql) { - return sql != null && sql.trim().regionMatches(true, 0, "SELECT", 0, 6); - } - private static final Pattern DML_VERB = Pattern.compile("^\\s*(?:--.*?$|/\\*.*?\\*/\\s*)*(UPDATE|DELETE|INSERT|MERGE)\\b", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); @@ -246,23 +235,6 @@ static String injectGatherStatsHintAfterVerb(String sql) { static final Pattern FIRST_WORD = Pattern.compile("^\\s*([A-Za-z0-9_]+)"); - /** - * DDL detector (CREATE/ALTER/DROP/TRUNCATE/RENAME/COMMENT/GRANT/REVOKE). - * Used to block DDL inside user-managed transactions. - */ - static boolean isDdl(String sql) { - if (sql == null) return false; - String s = sql.trim().toUpperCase(); - return s.startsWith("CREATE ") - || s.startsWith("ALTER ") - || s.startsWith("DROP ") - || s.startsWith("TRUNCATE ") - || s.startsWith("RENAME ") - || s.startsWith("COMMENT ") - || s.startsWith("GRANT ") - || s.startsWith("REVOKE "); - } - record ExplainResult(String planText) {} /** diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java index 6f453aa3..33e8beca 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java @@ -19,34 +19,23 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; -import static com.oracle.database.mcptoolkit.Utils.openConnection; -import static com.oracle.database.mcptoolkit.Utils.tryCall; -import static com.oracle.database.mcptoolkit.Utils.getOrDefault; +import static com.oracle.database.mcptoolkit.Utils.*; +import static com.oracle.database.mcptoolkit.tools.ToolSchemas.SIMILARITY_SEARCH; /** * Provides a tool for performing similarity searches using vector embeddings. *

- * This class is responsible for handling the "similarity_search" tool, which allows users to - * search for similar text based on a given query. + * This class is responsible for handling the "similarity-search" tool, which + * allows users to search for similar text based on a given query. */ public class SimilaritySearchTool { - private static final Pattern SAFE_IDENT = Pattern.compile("[A-Za-z0-9_$.#]+"); - private static final String DEFAULT_VECTOR_TABLE = "profile_oracle"; private static final String DEFAULT_VECTOR_DATA_COLUMN = "text"; private static final String DEFAULT_VECTOR_EMBEDDING_COLUMN = "embedding"; private static final String DEFAULT_VECTOR_MODEL_NAME = "doc_model"; private static final int DEFAULT_VECTOR_TEXT_FETCH_LIMIT = 4000; - private static final String SIMILARITY_SEARCH = """ - SELECT dbms_lob.substr(%s, %s, 1) AS text - FROM %s - ORDER BY VECTOR_DISTANCE(%s, - TO_VECTOR(VECTOR_EMBEDDING(%s USING ? AS data))) - FETCH FIRST ? ROWS ONLY - """; /** * Returns a tool specification for the "similarity_search" tool. @@ -60,10 +49,10 @@ public static McpServerFeatures.SyncToolSpecification getSymilaritySearchTool(Se return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() - .name("similarity_search") + .name("similarity-search") .title("Similarity Search") .description("Semantic vector similarity over a table with (text, embedding) columns") - .inputSchema(ToolSchemas.SIMILARITY_SEARCH) + .inputSchema(SIMILARITY_SEARCH) .build()) .callHandler((exchange, callReq) -> tryCall(() -> { try (Connection c = openConnection(config, null)) { @@ -133,7 +122,7 @@ private static List runSimilaritySearch(Connection c, String question, int topK) throws SQLException { String sql = String.format( - SIMILARITY_SEARCH, + SqlQueries.SIMILARITY_SEARCH_QUERY, quoteIdent(dataColumn), textFetchLimit, quoteIdent(table), embeddingColumn, modelName ); @@ -150,20 +139,4 @@ private static List runSimilaritySearch(Connection c, return result; } - - /** - * Escapes and quotes a potentially unsafe identifier for SQL use. - * - * @param ident identifier to quote - * @return a quoted or validated identifier - */ - static String quoteIdent(String ident) { - if (ident == null) throw new IllegalArgumentException("identifier is null"); - String s = ident.trim(); - if (!SAFE_IDENT.matcher(s).matches()) { - return "\"" + s.replace("\"", "\"\"") + "\""; - } - return s; - } - } diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SqlQueries.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SqlQueries.java new file mode 100644 index 00000000..44e9c9d6 --- /dev/null +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SqlQueries.java @@ -0,0 +1,50 @@ +/* + ** Oracle Database MCP Toolkit version 1.0.0 + ** + ** Copyright (c) 2025 Oracle and/or its affiliates. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.database.mcptoolkit.tools; + +/** + * Centralized SQL queries used by JDBC admin and other database tools. + */ +final class SqlQueries { + + /** Query template for vector similarity search. **/ + public static final String SIMILARITY_SEARCH_QUERY = """ + SELECT dbms_lob.substr(%s, %s, 1) AS text + FROM %s + ORDER BY VECTOR_DISTANCE(%s, + TO_VECTOR(VECTOR_EMBEDDING(%s USING ? AS data))) + FETCH FIRST ? ROWS ONLY + """; + + /** Query for fetching session user, schema, DB name, and time. */ + public static final String DB_PING_QUERY = """ + SELECT SYS_CONTEXT('USERENV','SESSION_USER') AS u, + SYS_CONTEXT('USERENV','CURRENT_SCHEMA') AS s, + SYS_CONTEXT('USERENV','DB_NAME') AS d, + TO_CHAR(SYSDATE,'YYYY-MM-DD HH24:MI:SS') AS t + FROM dual + """; + + /** Query for fetching selected metrics. */ + public static final String DB_METRICS_RANGE = """ + SELECT name, value + FROM v$sysstat + WHERE name IN ( + 'session logical reads', + 'db block gets', + 'consistent gets', + 'physical reads', + 'physical writes', + 'redo size' + ) + """; + + private SqlQueries() { + // Utility class + } +} \ No newline at end of file diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java index 3d2a6b76..95a9cc00 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ToolSchemas.java @@ -33,6 +33,33 @@ public class ToolSchemas { "required":["sql"] }"""; + /** + * JSON schema for DROP/DESCRIBE table operations. + */ + static final String DROP_OR_DESCRIBE_TABLE = """ + { + "type":"object", + "properties": + { + "table": + { + "type":"string" + }}, + "required":["table"] + }"""; + + /** + * JSON schema for transaction ID operations. + */ + static final String TX_ID = """ + { + "type":"object", + "properties": + {"txId": + {"type":"string"}}, + "required":["txId"] + }"""; + /** * JSON schema for file path operations. *

From e2f849368c562472259004afb50502915aa5aadf Mon Sep 17 00:00:00 2001 From: Mouhsin Elmajdouby Date: Wed, 21 Jan 2026 15:16:11 +0100 Subject: [PATCH 06/17] enhance README --- src/oracle-db-mcp-java-toolkit/README.md | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/oracle-db-mcp-java-toolkit/README.md b/src/oracle-db-mcp-java-toolkit/README.md index a1c26ac3..41e5a561 100644 --- a/src/oracle-db-mcp-java-toolkit/README.md +++ b/src/oracle-db-mcp-java-toolkit/README.md @@ -105,6 +105,56 @@ Toolsets can be enabled from `-Dtools` alongside individual tools. For example: ## 3. Built-in Tools +### Built-in Toolsets Overview +The server provides four built-in toolsets that can be enabled via `-Dtools`: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ToolsetDescriptionTools Included
adminDatabase operations and server admin + read-query, write-query, create-table, delete-table, list-tables, + describe-table, start-transaction, resume-transaction, + commit-transaction, rollback-transaction, db-ping, + db-metrics-range, list-tools, edit-tools +
log_analyzerJDBC and RDBMS log analysis + get-jdbc-stats, get-jdbc-queries, get-jdbc-errors, + get-jdbc-connection-events, list-log-files-from-directory, + jdbc-log-comparison, get-rdbms-errors, get-rdbms-packet-dumps +
similarityVector similarity searchsimilarity-search
explainSQL execution plan analysisexplain-plan
+ +**Common Configurations:** +- `-Dtools=admin` - Database operations only +- `-Dtools=log_analyzer` - Log analysis only (no database required) +- `-Dtools=admin,log_analyzer` - Database + log analysis +- `-Dtools=*` - All tools (default if omitted) + ### 3.1. Database Operations These tools provide direct SQL execution capabilities: From 015ff5bb64db5a64915c645015ed0c5ca83bf3b7 Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Wed, 21 Jan 2026 17:32:57 +0100 Subject: [PATCH 07/17] Changing log analyzer toolset name --- src/oracle-db-mcp-java-toolkit/README.md | 10 +++++----- .../com/oracle/database/mcptoolkit/ServerConfig.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/oracle-db-mcp-java-toolkit/README.md b/src/oracle-db-mcp-java-toolkit/README.md index 41e5a561..953ea2f9 100644 --- a/src/oracle-db-mcp-java-toolkit/README.md +++ b/src/oracle-db-mcp-java-toolkit/README.md @@ -128,7 +128,7 @@ The server provides four built-in toolsets that can be enabled via `-Dtools`: - log_analyzer + log-analyzer JDBC and RDBMS log analysis get-jdbc-stats, get-jdbc-queries, get-jdbc-errors, @@ -151,8 +151,8 @@ The server provides four built-in toolsets that can be enabled via `-Dtools`: **Common Configurations:** - `-Dtools=admin` - Database operations only -- `-Dtools=log_analyzer` - Log analysis only (no database required) -- `-Dtools=admin,log_analyzer` - Database + log analysis +- `-Dtools=log-analyzer` - Log analysis only (no database required) +- `-Dtools=admin,log-analyzer` - Database + log analysis - `-Dtools=*` - All tools (default if omitted) ### 3.1. Database Operations @@ -538,7 +538,7 @@ Ultimately, the token must be included in the http request header (e.g. `Authori Comma-separated allow-list of tool or toolset names to enable (case-insensitive).
You can pass individual tools (e.g. get-jdbc-stats, read-query) or any of the following built-in toolsets:

    -
  • log_analyzer — all JDBC log and RDBMS/SQLNet analysis tools (get-jdbc-stats, get-jdbc-queries, get-jdbc-errors, list-log-files-from-directory, jdbc-log-comparison, get-jdbc-connection-events, get-rdbms-errors, get-rdbms-packet-dumps)
  • +
  • log-analyzer — all JDBC log and RDBMS/SQLNet analysis tools (get-jdbc-stats, get-jdbc-queries, get-jdbc-errors, list-log-files-from-directory, jdbc-log-comparison, get-jdbc-connection-events, get-rdbms-errors, get-rdbms-packet-dumps)
  • explainexplain-plan
  • similaritysimilarity-search
  • admin — server admin and database tools (list-tools, edit-tools, read-query, write-query, create-table, delete-table, list-tables, describe-table, start-transaction, resume-transaction, commit-transaction, rollback-transaction, db-ping, db-metrics-range)
  • @@ -546,7 +546,7 @@ Ultimately, the token must be included in the http request header (e.g. `Authori You can also define your own YAML toolsets: and reference them here. Use * or all to enable everything. If omitted, all tools are enabled by default. - admin,log_analyzer or reporting,explain + admin,log-analyzer or reporting,explain ojdbc.ext.dir diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java index 647581f3..ce56f459 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java @@ -66,7 +66,7 @@ private ServerConfig( /** Built-in toolsets covering predefined tools. Lowercase keys and members. */ private static final Map> BUILTIN_TOOLSETS = Map.of( - "log_analyzer", Set.of( + "log-analyzer", Set.of( "get-jdbc-stats", "get-jdbc-queries", "get-jdbc-errors", From aa966e5bd3fc3558b27dbc37a15eede16427fe30 Mon Sep 17 00:00:00 2001 From: Mouhsin Elmajdouby Date: Mon, 26 Jan 2026 16:39:01 +0100 Subject: [PATCH 08/17] Introduce database-operator, rag, and mcp-admin toolsets and update docs --- src/oracle-db-mcp-java-toolkit/README.md | 61 +- .../database/mcptoolkit/ServerConfig.java | 20 +- .../com/oracle/database/mcptoolkit/Utils.java | 31 +- .../database/mcptoolkit/tools/AdminTools.java | 765 ------------------ .../tools/DatabaseOperatorTools.java | 713 ++++++++++++++++ .../tools/ExplainAndExecutePlanTool.java | 268 ------ .../mcptoolkit/tools/McpAdminTools.java | 315 ++++++++ ...imilaritySearchTool.java => RagTools.java} | 112 +-- 8 files changed, 1150 insertions(+), 1135 deletions(-) delete mode 100644 src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java create mode 100644 src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java delete mode 100644 src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java create mode 100644 src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java rename src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/{SimilaritySearchTool.java => RagTools.java} (56%) diff --git a/src/oracle-db-mcp-java-toolkit/README.md b/src/oracle-db-mcp-java-toolkit/README.md index 953ea2f9..1eb4c4ab 100644 --- a/src/oracle-db-mcp-java-toolkit/README.md +++ b/src/oracle-db-mcp-java-toolkit/README.md @@ -7,8 +7,8 @@ Oracle Database MCP Toolkit is a Model Context Protocol (MCP) server that lets y * Define your own custom tools via a simple YAML configuration file. * Use built-in tools: * Analyze Oracle JDBC thin client logs and RDBMS/SQLNet trace files. - * Database tools for SQL execution, table management, transactions, and performance monitoring. - * Database-powered tools, including vector similarity search and SQL execution plan analysis. + * Database tools for SQL execution, table management, transactions, performance monitoring and execution plan analysis. + * Database-powered tools, including vector similarity search (RAG). * Admin tools for runtime discovery and configuration: list available tools and live-edit YAML-defined tools with hot reload. * Deploy locally or remotely - optionally as a container - with support for TLS and OAuth2 @@ -27,7 +27,7 @@ This provides a flexible and declarative way to extend the server without modify A YAML file may define: * **datasources:** — Database configuration info: - * `url`: This the JDBC URL used by the MCP server to connect to the database using the JDBC driver. + * `url`: This is the JDBC URL used by the MCP server to connect to the database using the JDBC driver. * `user`: The username to use for the database connection. * `password`: The password to use for the database connection. * `host` (optional): The hostname or IP address of the database server. @@ -118,13 +118,10 @@ The server provides four built-in toolsets that can be enabled via `-Dtools`: - admin - Database operations and server admin + mcp-admin + Server discovery and runtime configuration - read-query, write-query, create-table, delete-table, list-tables, - describe-table, start-transaction, resume-transaction, - commit-transaction, rollback-transaction, db-ping, - db-metrics-range, list-tools, edit-tools + list-tools, edit-tools @@ -137,22 +134,31 @@ The server provides four built-in toolsets that can be enabled via `-Dtools`: - similarity - Vector similarity search - similarity-search + database-operator + Database operations, transactions, monitoring, and execution plans + + read-query, write-query, create-table, delete-table, + list-tables, describe-table, start-transaction, resume-transaction, + commit-transaction, rollback-transaction, db-ping, + db-metrics-range, explain-plan + - explain - SQL execution plan analysis - explain-plan + rag + Vector similarity search + similarity-search +_Note: Each tool belongs to exactly one built-in toolset. Enabling a toolset enables all tools listed for that toolset._ + **Common Configurations:** -- `-Dtools=admin` - Database operations only +- `-Dtools=mcp-admin` - Admin and runtime configuration tools - `-Dtools=log-analyzer` - Log analysis only (no database required) -- `-Dtools=admin,log-analyzer` - Database + log analysis +- `-Dtools=database-operator` - Database operations and SQL execution +- `-Dtools=rag` – Vector similarity search +- `-Dtools=mcp-admin,log-analyzer` - Admin + log analysis - `-Dtools=*` - All tools (default if omitted) ### 3.1. Database Operations @@ -201,7 +207,7 @@ These tools operate on RDBMS/SQLNet trace files: * **`get-rdbms-errors`**: Extracts errors from RDBMS/SQLNet trace files. * **`get-rdbms-packet-dumps`**: Extracts packet dumps for a specific connection ID. -### 3.7. Vector Similarity Search +### 3.7. Vector Similarity Search (RAG) * **`similarity-search`**: Perform semantic similarity search using Oracle’s vector features (`VECTOR_EMBEDDING`, `VECTOR_DISTANCE`). @@ -244,15 +250,14 @@ These tools operate on RDBMS/SQLNet trace files: * `planText`: DBMS_XPLAN output. * `llmPrompt`: A structured prompt for an LLM to explain + tune the plan. -### 3.9. Admin, Runtime Configuration and Database Tools +### 3.9. Admin and Runtime Configuration Tools -These tools help you discover what's enabled and manage YAML-defined tools at runtime, and perform core database operations. -They are part of the `admin` toolset (enable via `-Dtools=admin` or include individual tool names). +These tools help you discover what's enabled and manage YAML-defined tools at runtime. +They are part of the `mcp-admin` toolset (enable via `-Dtools=mcp-admin` or include individual tool names). -_Note: The admin toolset includes both the admin tools listed below AND all database operation tools described in -sections 3.1-3.4 (Database Operations, Table Management, Transaction Management, and Database Monitoring)._ +_Note: The `mcp-admin` toolset is focused on server discovery and runtime configuration only._ -#### Admin Tools: +#### MCP Admin Tools: - `list-tools`: List all available tools with their descriptions. - Inputs: none @@ -538,15 +543,15 @@ Ultimately, the token must be included in the http request header (e.g. `Authori Comma-separated allow-list of tool or toolset names to enable (case-insensitive).
    You can pass individual tools (e.g. get-jdbc-stats, read-query) or any of the following built-in toolsets:
      +
    • mcp-admin — server discovery and runtime configuration tools (list-tools, edit-tools)
    • +
    • database-operator — database operations, transactions, monitoring, and execution plans (read-query, write-query, create-table, delete-table, list-tables, describe-table, start-transaction, resume-transaction, commit-transaction, rollback-transaction, db-ping, db-metrics-range, explain-plan).
    • log-analyzer — all JDBC log and RDBMS/SQLNet analysis tools (get-jdbc-stats, get-jdbc-queries, get-jdbc-errors, list-log-files-from-directory, jdbc-log-comparison, get-jdbc-connection-events, get-rdbms-errors, get-rdbms-packet-dumps)
    • -
    • explainexplain-plan
    • -
    • similaritysimilarity-search
    • -
    • admin — server admin and database tools (list-tools, edit-tools, read-query, write-query, create-table, delete-table, list-tables, describe-table, start-transaction, resume-transaction, commit-transaction, rollback-transaction, db-ping, db-metrics-range)
    • +
    • rag — similarity-search
    You can also define your own YAML toolsets: and reference them here. Use * or all to enable everything. If omitted, all tools are enabled by default. - admin,log-analyzer or reporting,explain + mcp-admin,log-analyzer or reporting ojdbc.ext.dir diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java index ce56f459..4a84a23d 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java @@ -26,8 +26,8 @@ *
  • {@code db.user}
  • *
  • {@code db.password}
  • *
  • {@code tools} — comma-separated allow-list of tool names or toolset - * names; (e.g., {@code log_analyzer}, {@code admin}, {@code explain}, {@code similarity}); - * {@code *} or {@code all} enables all.
  • + * names; (e.g., {@code log_analyzer}, {@code admin}, {@code explain}, + * {@code rag}); {@code *} or {@code all} enables all. *
* *

Use {@link #fromSystemProperties()} to create an instance with validation and defaults.

@@ -58,7 +58,7 @@ private ServerConfig( } private static final Set DB_TOOLS = Set.of( - "similarity_search", "explain_plan", + "similarity-search", "explain-plan", "read-query", "write-query", "create-table", "delete-table", "list-tables", "describe-table", "start-transaction", "resume-transaction", "commit-transaction", "rollback-transaction", "db-ping", "db-metrics-range" @@ -76,14 +76,14 @@ private ServerConfig( "get-rdbms-errors", "get-rdbms-packet-dumps" ), - "explain", Set.of("explain-plan"), - "similarity", Set.of("similarity-search"), - "admin", Set.of( - "list-tools", "edit-tools", + "rag", Set.of("similarity-search"), + "database-operator", Set.of( "read-query", "write-query", "create-table", "delete-table", "list-tables", "describe-table", "start-transaction", "resume-transaction", - "commit-transaction", "rollback-transaction", "db-ping", "db-metrics-range" - ) + "commit-transaction", "rollback-transaction", "db-ping", "db-metrics-range", + "explain-plan" + ), + "mcp-admin", Set.of("list-tools", "edit-tools") ); @@ -184,7 +184,7 @@ static ServerConfig fromSystemProperties() { * “enable every tool” and returns {@code null}. * * Examples: - * "similarity_search,explain_plan" -> ["similarity_search","explain_plan"] + * "similarity-search,explain-plan" -> ["similarity-search","explain-plan"] * "*" or "all" or "" -> null (treat as all tools enabled) * * @param prop comma-separated tool names diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java index 4b075a10..85f717f6 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/Utils.java @@ -12,10 +12,7 @@ import com.oracle.database.mcptoolkit.config.DataSourceConfig; import com.oracle.database.mcptoolkit.config.ToolConfig; import com.oracle.database.mcptoolkit.config.ToolParameterConfig; -import com.oracle.database.mcptoolkit.tools.AdminTools; -import com.oracle.database.mcptoolkit.tools.ExplainAndExecutePlanTool; -import com.oracle.database.mcptoolkit.tools.LogAnalyzerTools; -import com.oracle.database.mcptoolkit.tools.SimilaritySearchTool; +import com.oracle.database.mcptoolkit.tools.*; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; @@ -93,19 +90,27 @@ static void addSyncToolSpecifications(McpSyncServer server, ServerConfig config) } } - // similarity-search - if (isToolEnabled(config, "similarity-search")) { - server.addTool(SimilaritySearchTool.getSymilaritySearchTool(config)); + // RAG tools + List ragSpecs = RagTools.getTools(config); + for (McpServerFeatures.SyncToolSpecification spec : ragSpecs) { + String toolName = spec.tool().name(); + if (isToolEnabled(config, toolName)) { + server.addTool(spec); + } } - // explain-plan - if (isToolEnabled(config, "explain-plan")) { - server.addTool(ExplainAndExecutePlanTool.getExplainAndExecutePlanTool(config)); + // Database Operator tools + List dbOperatorSpecs = DatabaseOperatorTools.getTools(config); + for (McpServerFeatures.SyncToolSpecification spec : dbOperatorSpecs) { + String toolName = spec.tool().name(); + if (isToolEnabled(config, toolName)) { + server.addTool(spec); + } } - // admin tools - List adminSpecs = AdminTools.getTools(config); - for (McpServerFeatures.SyncToolSpecification spec : adminSpecs) { + // MCP Admin tools + List mcpAdminSpecs = McpAdminTools.getTools(config); + for (McpServerFeatures.SyncToolSpecification spec : mcpAdminSpecs) { String toolName = spec.tool().name(); if (isToolEnabled(config, toolName)) { server.addTool(spec); diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java deleted file mode 100644 index e1edc55e..00000000 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/AdminTools.java +++ /dev/null @@ -1,765 +0,0 @@ -/* - ** Oracle Database MCP Toolkit version 1.0.0 - ** - ** Copyright (c) 2025 Oracle and/or its affiliates. - ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - */ - -package com.oracle.database.mcptoolkit.tools; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.oracle.database.mcptoolkit.LoadedConstants; -import com.oracle.database.mcptoolkit.ServerConfig; -import com.oracle.database.mcptoolkit.Utils; -import com.oracle.database.mcptoolkit.OracleDatabaseMCPToolkit; -import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.spec.McpSchema; -import org.yaml.snakeyaml.Yaml; - -import java.io.Reader; -import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.sql.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -import static com.oracle.database.mcptoolkit.Utils.*; - -/** - * Admin/maintenance tools: - * - list-tools: list all available tools with descriptions - * - edit-tools: upsert a YAML-defined tool in the config file and rely on runtime reload - * - read-query: execute SELECT queries and return JSON results - * - write-query: execute DML/DDL statements - * - create-table: create table. - * - delete-table: drop table by name - * - list-tables: list all tables and synonyms in the current schema - * - describe-table: get column details for a specific table - * - start-transaction: begin a new JDBC transaction - * - resume-transaction: verify a transaction ID is active - * - commit-transaction: commit and close a transaction - * - rollback-transaction: rollback and close a transaction - * - db-ping: check database connectivity and latency - * - db-metrics-range: retrieve Oracle performance metrics from V$SYSSTAT - */ -public final class AdminTools { - - // Transaction store (txId -> Connection) - private static final Map TX = new ConcurrentHashMap<>(); - - - private AdminTools() {} - - /** - * Returns all admin tool specifications. - */ - public static List getTools(ServerConfig config) { - List tools = new ArrayList<>(); - - tools.add(getListToolsTool(config)); - tools.add(getEditToolsTool(config)); - tools.add(getReadQueryTool(config)); - tools.add(getWriteQueryTool(config)); - tools.add(getCreateTableTool(config)); - tools.add(getDeleteTableTool(config)); - tools.add(getListTablesTool(config)); - tools.add(getDescribeTableTool(config)); - tools.add(getStartTransactionTool(config)); - tools.add(getResumeTransactionTool()); - tools.add(getCommitTransactionTool()); - tools.add(getRollbackTransactionTool()); - tools.add(getDbPingTool(config)); - tools.add(getDbMetricsTool(config)); - - // Add shutdown hook to clean up transactions - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - TX.values().forEach(conn -> { - try { if (!conn.getAutoCommit()) conn.rollback(); } catch (Exception ignored) {} - try { conn.close(); } catch (Exception ignored) {} - }); - TX.clear(); - })); - - return tools; - } - - public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("list-tools") - .title("List Tools") - .description("List all available tools with their descriptions.") - .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) - .build()) - .callHandler((exchange, callReq) -> { - try { - // Always use the latest runtime config to avoid requiring a rebuild - ServerConfig current = OracleDatabaseMCPToolkit.getConfig() != null ? OracleDatabaseMCPToolkit.getConfig() : config; - - List> tools = new ArrayList<>(); - - // 1) Built-in log analyzer tools (respect toolsFilter) - for (McpServerFeatures.SyncToolSpecification spec : LogAnalyzerTools.getTools()) { - String name = spec.tool().name(); - if (isEnabled(current, name)) { - tools.add(Map.of( - "name", name, - "title", spec.tool().title(), - "description", spec.tool().description() - )); - } - } - - // 2) Built-in database tools (respect toolsFilter) - if (isEnabled(current, "similarity_search")) { - var t = SimilaritySearchTool.getSymilaritySearchTool(current).tool(); - tools.add(Map.of( - "name", t.name(), - "title", t.title(), - "description", t.description() - )); - } - if (isEnabled(current, "explain_plan")) { - var t = ExplainAndExecutePlanTool.getExplainAndExecutePlanTool(current).tool(); - tools.add(Map.of( - "name", t.name(), - "title", t.title(), - "description", t.description() - )); - } - - // 3) Custom YAML tools (always listed if present in config) - if (current.tools != null) { - for (Map.Entry e : current.tools.entrySet()) { - var tc = e.getValue(); - tools.add(Map.of( - "name", tc.name, - "title", tc.name, - "description", tc.description - )); - } - } - - // 4) Admin tools themselves if enabled - if (isEnabled(current, "list-tools")) { - tools.add(Map.of( - "name", "list-tools", - "title", "List Tools", - "description", "List all available tools with their descriptions." - )); - } - if (isEnabled(current, "edit-tools")) { - tools.add(Map.of( - "name", "edit-tools", - "title", "Edit/Add Tools", - "description", "Create or update YAML-defined tools and trigger live reload." - )); - } - - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("tools", tools)) - .addTextContent(new ObjectMapper().writeValueAsString(tools)) - .build(); - } catch (Exception e) { - return McpSchema.CallToolResult.builder() - .addTextContent("Unexpected: " + e.getMessage()) - .isError(true) - .build(); - } - }) - .build(); - } - - public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("edit-tools") - .title("Edit/Add Tools") - .description("Create or update YAML-defined tools in the config file. Changes are auto-reloaded.") - .inputSchema(ToolSchemas.EDIT_TOOL_SCHEMA) - .build()) - .callHandler((exchange, callReq) -> { - try { - final var cfgPath = LoadedConstants.CONFIG_FILE; - if (cfgPath == null || cfgPath.isBlank()) { - return McpSchema.CallToolResult.builder() - .addTextContent("Config file path (configFile) is not set. Cannot edit tools.") - .isError(true) - .build(); - } - - String name = asString(callReq.arguments().get("name")); - if (name == null || name.isBlank()) { - return McpSchema.CallToolResult.builder() - .addTextContent("'name' is required") - .isError(true) - .build(); - } - - Path path = Paths.get(cfgPath); - Yaml yaml = new Yaml(); - - // Load existing YAML into a mutable map - Map root; - if (Files.exists(path)) { - try (Reader r = Files.newBufferedReader(path)) { - Object loaded = yaml.load(r); - if (loaded instanceof Map) { - //noinspection unchecked - root = new LinkedHashMap<>((Map) loaded); - } else { - root = new LinkedHashMap<>(); - } - } - } else { - root = new LinkedHashMap<>(); - } - - // Ensure 'tools' map exists - Map tools = getOrCreateMap(root, "tools"); - - // Remove if requested - Object removeFlag = callReq.arguments().get("remove"); - boolean remove = (removeFlag instanceof Boolean b && b) || - (removeFlag != null && "true".equalsIgnoreCase(String.valueOf(removeFlag))); - if (remove) { - if (tools.remove(name) != null) { - root.put("tools", tools); - try (Writer w = Files.newBufferedWriter(path)) { - yaml.dump(root, w); - } - // Try to remove the tool from the running server as well - try { - OracleDatabaseMCPToolkit.getServer().removeTool(name); - Utils.unregisterCustomToolLocally(name); - } catch (Exception ignored) {} - - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of( - "status", "ok", - "message", "Tool '" + name + "' removed from YAML. Reload will occur shortly.", - "configFile", cfgPath - )) - .addTextContent("{\"status\":\"ok\",\"removed\":\"" + name + "\"}") - .build(); - } else { - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of( - "status", "noop", - "message", "Tool '" + name + "' not found; nothing to remove.", - "configFile", cfgPath - )) - .addTextContent("{\"status\":\"noop\",\"missing\":\"" + name + "\"}") - .build(); - } - } - - // Upsert tool entry - Map t = getOrCreateMap(tools, name); - - // Optional fields - putIfPresent(t, callReq.arguments(), "description"); - putIfPresent(t, callReq.arguments(), "dataSource"); - putIfPresent(t, callReq.arguments(), "statement"); - - // parameters: array of objects - Object paramsObj = callReq.arguments().get("parameters"); - if (paramsObj instanceof List list) { - List> params = new ArrayList<>(); - for (Object o : list) { - if (o instanceof Map m) { - Map p = new LinkedHashMap<>(); - copyIfPresent(m, p, "name"); - copyIfPresent(m, p, "type"); - copyIfPresent(m, p, "description"); - copyIfPresent(m, p, "required"); - params.add(p); - } - } - t.put("parameters", params); - } - - tools.put(name, t); - root.put("tools", tools); - - // Write back YAML - try (Writer w = Files.newBufferedWriter(path)) { - yaml.dump(root, w); - } - - // Try to remove stale instance at runtime (best-effort hot update); server will re-add on reload - try { - var srv = OracleDatabaseMCPToolkit.getServer(); - if (srv != null) srv.removeTool(name); - } catch (Exception ignored) {} - - - // The config poller will pick up the change and hot-reload within ~2 seconds. - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of( - "status", "ok", - "message", "Tool '" + name + "' upserted into YAML. Reload will occur shortly.", - "configFile", cfgPath - )) - .addTextContent("{\"status\":\"ok\",\"name\":\"" + name + "\"}") - .build(); - } catch (Exception e) { - - return McpSchema.CallToolResult.builder() - .addTextContent("Unexpected: " + e.getMessage()) - .isError(true) - .build(); - } - }) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getReadQueryTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("read-query") - .title("Read Query") - .description("Run a SELECT and return rows as JSON. (Optionally accepts txId to run inside an open transaction.)") - .inputSchema(ToolSchemas.SQL_ONLY) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (ConnLease lease = acquireConnection(config, callReq.arguments().get("txId"))) { - Connection c = lease.c; - String sql = String.valueOf(callReq.arguments().get("sql")); - if (!looksSelect(sql)) { - return new McpSchema.CallToolResult("Only SELECT is allowed", true); - } - var rows = rsToList(c.createStatement().executeQuery(sql)); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("rows", rows, "rowCount", rows.size())) - .addTextContent(new ObjectMapper().writeValueAsString(rows)) - .build(); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getWriteQueryTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("write-query") - .title("Write Query") - .description("Execute DML (inside a transaction if txId is provided) or DML/DDL in autocommit mode.") - .inputSchema(ToolSchemas.SQL_ONLY) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (ConnLease lease = acquireConnection(config, callReq.arguments().get("txId"))) { - Connection c = lease.c; - String sql = String.valueOf(callReq.arguments().get("sql")); - boolean inTx = !lease.closeOnExit; - if (inTx && isDdl(sql)) { - return new McpSchema.CallToolResult( - "DDL is not allowed inside a transaction. Run this statement without txId.", true); - } - int n = execUpdate(c, sql); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("updateCount", n)) - .addTextContent("{\"updateCount\":" + n + "}") - .build(); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getCreateTableTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("create-table") - .title("Create Table") - .description("Create a table from a full CREATE TABLE statement.") - .inputSchema(ToolSchemas.SQL_ONLY) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null)) { - String sql = String.valueOf(callReq.arguments().get("sql")); - execUpdate(c, sql); - return new McpSchema.CallToolResult("OK", false); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getDeleteTableTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("delete-table") - .title("Drop Table") - .description("Drop a table by name.") - .inputSchema(ToolSchemas.DROP_OR_DESCRIBE_TABLE) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null)) { - String table = String.valueOf(callReq.arguments().get("table")); - int updateCount = execUpdate(c, "DROP TABLE " + quoteIdent(table)); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("updateCount", updateCount, "table", table)) - .addTextContent("OK") - .build(); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getListTablesTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("list-tables") - .title("List Tables & Synonyms") - .description("List TABLE and SYNONYM in the current schema via DatabaseMetaData (includes comments).") - .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null)) { - DatabaseMetaData md = c.getMetaData(); - String schema = Optional.ofNullable(c.getSchema()) - .orElseGet(() -> { - try { - return md.getUserName(); - } catch (SQLException e) { - return null; - } - }); - if (schema != null) - schema = schema.toUpperCase(Locale.ROOT); - List> tables = new ArrayList<>(); - List> synonyms = new ArrayList<>(); - try (ResultSet rs = md.getTables(null, schema, "%", new String[]{"TABLE", "SYNONYM"})) { - while (rs.next()) { - Map row = new LinkedHashMap<>(); - row.put("owner", rs.getString("TABLE_SCHEM")); - row.put("name", rs.getString("TABLE_NAME")); - row.put("kind", rs.getString("TABLE_TYPE")); - row.put("comment", rs.getString("REMARKS")); - String kind = String.valueOf(row.get("kind")); - if ("SYNONYM".equalsIgnoreCase(kind)) { - synonyms.add(row); - } else { - tables.add(row); - } - } - } - Map payload = Map.of( - "schema", schema, - "counts", Map.of("tables", tables.size(), "synonyms", synonyms.size()), - "tables", tables, - "synonyms", synonyms - ); - return McpSchema.CallToolResult.builder() - .structuredContent(payload) - .addTextContent(new ObjectMapper().writeValueAsString(payload)) - .build(); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getDescribeTableTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("describe-table") - .title("Describe Table") - .description("Describe columns via DatabaseMetaData. Returns COLUMN_ID, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COMMENTS.") - .inputSchema(ToolSchemas.DROP_OR_DESCRIBE_TABLE) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null)) { - String table = String.valueOf(callReq.arguments().get("table")); - if (table == null || table.isBlank()) { - return new McpSchema.CallToolResult("Parameter 'table' is required", true); - } - DatabaseMetaData md = c.getMetaData(); - String schema = Optional.ofNullable(c.getSchema()) - .orElseGet(() -> { - try { - return md.getUserName(); - } catch (SQLException e) { - return null; - } - }); - if (schema != null) - schema = schema.toUpperCase(Locale.ROOT); - String tableName = table.toUpperCase(Locale.ROOT); - List> rows = new ArrayList<>(); - try (ResultSet rs = md.getColumns(null, schema, tableName, "%")) { - while (rs.next()) { - int ordinal = rs.getInt("ORDINAL_POSITION"); - String colName = rs.getString("COLUMN_NAME"); - String typeName = rs.getString("TYPE_NAME"); - int colSize = rs.getInt("COLUMN_SIZE"); - int precision = rs.getInt("COLUMN_SIZE"); - int scale = rs.getInt("DECIMAL_DIGITS"); - int nullableFlag = rs.getInt("NULLABLE"); - String remarks = rs.getString("REMARKS"); - String nullableYN = (nullableFlag == DatabaseMetaData.columnNullable) ? "Y" : "N"; - if (remarks == null) - remarks = ""; - Map row = new LinkedHashMap<>(); - row.put("COLUMN_ID", ordinal); - row.put("COLUMN_NAME", colName); - row.put("DATA_TYPE", typeName); - row.put("DATA_LENGTH", colSize); - row.put("DATA_PRECISION", (scale >= 0 && precision > 0) ? precision : null); - row.put("DATA_SCALE", (scale >= 0) ? scale : null); - row.put("NULLABLE", nullableYN); - row.put("COMMENTS", remarks); - rows.add(row); - } - } - rows.sort(Comparator.comparingInt(m -> ((Number)m.get("COLUMN_ID")).intValue())); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("columns", rows)) - .addTextContent(new ObjectMapper().writeValueAsString(rows)) - .build(); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getStartTransactionTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("start-transaction") - .title("Start Transaction") - .description("Start a JDBC transaction (autoCommit=false). Returns txId.") - .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - Connection c = openConnection(config, null); - c.setAutoCommit(false); - String txId = UUID.randomUUID().toString(); - TX.put(txId, c); - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("txId", txId)) - .addTextContent("{\"txId\":\"" + txId + "\"}") - .build(); - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getResumeTransactionTool() { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("resume-transaction") - .title("Resume Transaction") - .description("Verify a txId is active (no-op). Returns ok.") - .inputSchema(ToolSchemas.TX_ID) - .build()) - .callHandler((exchange, callReq) -> { - String txId = String.valueOf(callReq.arguments().get("txId")); - if (TX.containsKey(txId)) { - return new McpSchema.CallToolResult("{\"ok\":true}", false); - } - return new McpSchema.CallToolResult("Unknown txId", true); - }) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getCommitTransactionTool() { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("commit-transaction") - .title("Commit Transaction") - .description("Commit and close a txId.") - .inputSchema(ToolSchemas.TX_ID) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - String txId = String.valueOf(callReq.arguments().get("txId")); - Connection c = TX.remove(txId); - if (c == null) - return new McpSchema.CallToolResult("Unknown txId", true); - try (c) { - c.commit(); - return new McpSchema.CallToolResult("{\"ok\":true}", false); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getRollbackTransactionTool() { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("rollback-transaction") - .title("Rollback Transaction") - .description("Rollback and close a txId.") - .inputSchema(ToolSchemas.TX_ID) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - String txId = String.valueOf(callReq.arguments().get("txId")); - Connection c = TX.remove(txId); - if (c == null) - return new McpSchema.CallToolResult("Unknown txId", true); - try (c) { - c.rollback(); - return new McpSchema.CallToolResult("{\"ok\":true}", false); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getDbPingTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("db-ping") - .title("DB Ping") - .description("Checks connectivity and round-trip latency; returns user, schema, DB name, and version.") - .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - long connStart = System.nanoTime(); - try (Connection c = openConnection(config, null)) { - long connectMs = (System.nanoTime() - connStart) / 1_000_000L; - long rtStart = System.nanoTime(); - String user = null, schema = null, dbName = null, dbTime = null; - try (PreparedStatement ps = c.prepareStatement(SqlQueries.DB_PING_QUERY); - ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - user = rs.getString(1); - schema = rs.getString(2); - dbName = rs.getString(3); - dbTime = rs.getString(4); - } - } - long roundTripMs = (System.nanoTime() - rtStart) / 1_000_000L; - DatabaseMetaData md = c.getMetaData(); - Map sc = new LinkedHashMap<>(); - sc.put("ok", true); - sc.put("connectLatencyMs", connectMs); - sc.put("roundTripMs", roundTripMs); - sc.put("dbProduct", md.getDatabaseProductName()); - sc.put("dbVersion", md.getDatabaseProductVersion()); - sc.put("user", user); - sc.put("schema", schema); - sc.put("dbName", dbName); - sc.put("dbTime", dbTime); - String text = String.format(""" - OK — connection verified - connectLatencyMs: %d - roundTripMs: %d - dbProduct: %s - dbVersion: %s - user: %s - schema: %s - dbName: %s - dbTime: %s - """, connectMs, roundTripMs, md.getDatabaseProductName(), - md.getDatabaseProductVersion(), user, schema, dbName, dbTime - ); - return McpSchema.CallToolResult.builder() - .structuredContent(sc) - .addTextContent(text) - .build(); - } - })) - .build(); - } - - private static McpServerFeatures.SyncToolSpecification getDbMetricsTool(ServerConfig config) { - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("db-metrics-range") - .title("DB Metrics") - .description("Returns Oracle DB metrics from V$SYSSTAT (cumulative counters only).") - .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null); - Statement st = c.createStatement(); - ResultSet rs = st.executeQuery(SqlQueries.DB_METRICS_RANGE)) { - var rows = rsToList(rs); - Map metrics = new LinkedHashMap<>(); - StringBuilder text = new StringBuilder( - "Live metrics — V$SYSSTAT (cumulative counters; rates not available):\n" - ); - for (Map r : rows) { - String name = String.valueOf(r.get("NAME")); - Object value = r.get("VALUE"); - metrics.put(name, Map.of("value", value, "unit", "")); - text.append("- ").append(name).append(": ").append(value).append("\n"); - } - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of( - "source", "V$SYSSTAT", - "note", "Cumulative since instance startup; these are totals, not per-second rates.", - "metrics", metrics)) - .addTextContent(text.toString().trim()) - .build(); - } - })) - .build(); - } - - private static boolean isEnabled(ServerConfig config, String toolName) { - if (config.toolsFilter == null) return true; - String key = toolName.toLowerCase(Locale.ROOT); - return config.toolsFilter.contains(key); - } - - private static String asString(Object v) { - return v == null ? null : String.valueOf(v); - } - - @SuppressWarnings("unchecked") - private static Map getOrCreateMap(Map parent, String key) { - Object o = parent.get(key); - if (o instanceof Map m) { - return new LinkedHashMap<>((Map) m); - } - Map created = new LinkedHashMap<>(); - parent.put(key, created); - return created; - } - - private static void putIfPresent(Map target, Map source, String key) { - Object v = source.get(key); - if (v != null) target.put(key, v); - } - - private static void copyIfPresent(Map src, Map dst, String key) { - Object v = src.get(key); - if (v != null) dst.put(key, v); - } - - private static int execUpdate(Connection c, String sql) throws SQLException { - try (Statement st = c.createStatement()) { - return st.executeUpdate(sql); - } - } - - private static class ConnLease implements AutoCloseable { - final Connection c; - final boolean closeOnExit; - - ConnLease(Connection c, boolean closeOnExit) { - this.c = c; - this.closeOnExit = closeOnExit; - } - - @Override - public void close() { - if (closeOnExit) { - try { c.close(); } catch (Exception ignored) {} - } - } - } - - private static ConnLease acquireConnection(ServerConfig config, Object txIdArg) throws SQLException { - String txId = (txIdArg == null) ? null : String.valueOf(txIdArg); - if (txId == null || txId.isBlank()) { - Connection c = openConnection(config, null); - try { - c.setAutoCommit(true); - } catch (Exception ignored) {} - return new ConnLease(c, true); - } - Connection c = TX.get(txId); - if (c == null) throw new SQLException("Unknown txId"); - return new ConnLease(c, false); - } -} diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java new file mode 100644 index 00000000..bdcdfea5 --- /dev/null +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java @@ -0,0 +1,713 @@ +/* + ** Oracle Database MCP Toolkit version 1.0.0 + ** + ** Copyright (c) 2026 Oracle and/or its affiliates. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.database.mcptoolkit.tools; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oracle.database.mcptoolkit.ServerConfig; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.oracle.database.mcptoolkit.Utils.*; + +/** + * Database operator tools: + * - read-query: execute SELECT queries and return JSON results + * - write-query: execute DML/DDL statements + * - create-table: create table. + * - delete-table: drop table by name + * - list-tables: list all tables and synonyms in the current schema + * - describe-table: get column details for a specific table + * - start-transaction: begin a new JDBC transaction + * - resume-transaction: verify a transaction ID is active + * - commit-transaction: commit and close a transaction + * - rollback-transaction: rollback and close a transaction + * - db-ping: check database connectivity and latency + * - db-metrics-range: retrieve Oracle performance metrics from V$SYSSTAT + * - explain-plan: generate and explain Oracle SQL execution plans (static or dynamic) + */ +public final class DatabaseOperatorTools { + + // Transaction store (txId -> Connection) + private static final Map TX = new ConcurrentHashMap<>(); + + private DatabaseOperatorTools() {} + + /** + * Returns all database operator tool specifications. + */ + public static List getTools(ServerConfig config) { + List tools = new ArrayList<>(); + + tools.add(getReadQueryTool(config)); + tools.add(getWriteQueryTool(config)); + tools.add(getCreateTableTool(config)); + tools.add(getDeleteTableTool(config)); + tools.add(getListTablesTool(config)); + tools.add(getDescribeTableTool(config)); + tools.add(getStartTransactionTool(config)); + tools.add(getResumeTransactionTool()); + tools.add(getCommitTransactionTool()); + tools.add(getRollbackTransactionTool()); + tools.add(getDbPingTool(config)); + tools.add(getDbMetricsTool(config)); + tools.add(getExplainAndExecutePlanTool(config)); + + // Add shutdown hook to clean up transactions + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + TX.values().forEach(conn -> { + try { if (!conn.getAutoCommit()) conn.rollback(); } catch (Exception ignored) {} + try { conn.close(); } catch (Exception ignored) {} + }); + TX.clear(); + })); + + return tools; + } + + private static McpServerFeatures.SyncToolSpecification getReadQueryTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("read-query") + .title("Read Query") + .description("Run a SELECT and return rows as JSON. (Optionally accepts txId to run inside an open transaction.)") + .inputSchema(ToolSchemas.SQL_ONLY) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (DatabaseOperatorTools.ConnLease lease = acquireConnection(config, callReq.arguments().get("txId"))) { + Connection c = lease.c; + String sql = String.valueOf(callReq.arguments().get("sql")); + if (!looksSelect(sql)) { + return new McpSchema.CallToolResult("Only SELECT is allowed", true); + } + var rows = rsToList(c.createStatement().executeQuery(sql)); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("rows", rows, "rowCount", rows.size())) + .addTextContent(new ObjectMapper().writeValueAsString(rows)) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getWriteQueryTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("write-query") + .title("Write Query") + .description("Execute DML (inside a transaction if txId is provided) or DML/DDL in autocommit mode.") + .inputSchema(ToolSchemas.SQL_ONLY) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (DatabaseOperatorTools.ConnLease lease = acquireConnection(config, callReq.arguments().get("txId"))) { + Connection c = lease.c; + String sql = String.valueOf(callReq.arguments().get("sql")); + boolean inTx = !lease.closeOnExit; + if (inTx && isDdl(sql)) { + return new McpSchema.CallToolResult( + "DDL is not allowed inside a transaction. Run this statement without txId.", true); + } + int n = execUpdate(c, sql); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("updateCount", n)) + .addTextContent("{\"updateCount\":" + n + "}") + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getCreateTableTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("create-table") + .title("Create Table") + .description("Create a table from a full CREATE TABLE statement.") + .inputSchema(ToolSchemas.SQL_ONLY) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + String sql = String.valueOf(callReq.arguments().get("sql")); + execUpdate(c, sql); + return new McpSchema.CallToolResult("OK", false); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDeleteTableTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("delete-table") + .title("Drop Table") + .description("Drop a table by name.") + .inputSchema(ToolSchemas.DROP_OR_DESCRIBE_TABLE) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + String table = String.valueOf(callReq.arguments().get("table")); + int updateCount = execUpdate(c, "DROP TABLE " + quoteIdent(table)); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("updateCount", updateCount, "table", table)) + .addTextContent("OK") + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getListTablesTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("list-tables") + .title("List Tables & Synonyms") + .description("List TABLE and SYNONYM in the current schema via DatabaseMetaData (includes comments).") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + DatabaseMetaData md = c.getMetaData(); + String schema = Optional.ofNullable(c.getSchema()) + .orElseGet(() -> { + try { + return md.getUserName(); + } catch (SQLException e) { + return null; + } + }); + if (schema != null) + schema = schema.toUpperCase(Locale.ROOT); + List> tables = new ArrayList<>(); + List> synonyms = new ArrayList<>(); + try (ResultSet rs = md.getTables(null, schema, "%", new String[]{"TABLE", "SYNONYM"})) { + while (rs.next()) { + Map row = new LinkedHashMap<>(); + row.put("owner", rs.getString("TABLE_SCHEM")); + row.put("name", rs.getString("TABLE_NAME")); + row.put("kind", rs.getString("TABLE_TYPE")); + row.put("comment", rs.getString("REMARKS")); + String kind = String.valueOf(row.get("kind")); + if ("SYNONYM".equalsIgnoreCase(kind)) { + synonyms.add(row); + } else { + tables.add(row); + } + } + } + Map payload = Map.of( + "schema", schema, + "counts", Map.of("tables", tables.size(), "synonyms", synonyms.size()), + "tables", tables, + "synonyms", synonyms + ); + return McpSchema.CallToolResult.builder() + .structuredContent(payload) + .addTextContent(new ObjectMapper().writeValueAsString(payload)) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDescribeTableTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("describe-table") + .title("Describe Table") + .description("Describe columns via DatabaseMetaData. Returns COLUMN_ID, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COMMENTS.") + .inputSchema(ToolSchemas.DROP_OR_DESCRIBE_TABLE) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + String table = String.valueOf(callReq.arguments().get("table")); + if (table == null || table.isBlank()) { + return new McpSchema.CallToolResult("Parameter 'table' is required", true); + } + DatabaseMetaData md = c.getMetaData(); + String schema = Optional.ofNullable(c.getSchema()) + .orElseGet(() -> { + try { + return md.getUserName(); + } catch (SQLException e) { + return null; + } + }); + if (schema != null) + schema = schema.toUpperCase(Locale.ROOT); + String tableName = table.toUpperCase(Locale.ROOT); + List> rows = new ArrayList<>(); + try (ResultSet rs = md.getColumns(null, schema, tableName, "%")) { + while (rs.next()) { + int ordinal = rs.getInt("ORDINAL_POSITION"); + String colName = rs.getString("COLUMN_NAME"); + String typeName = rs.getString("TYPE_NAME"); + int colSize = rs.getInt("COLUMN_SIZE"); + int precision = rs.getInt("COLUMN_SIZE"); + int scale = rs.getInt("DECIMAL_DIGITS"); + int nullableFlag = rs.getInt("NULLABLE"); + String remarks = rs.getString("REMARKS"); + String nullableYN = (nullableFlag == DatabaseMetaData.columnNullable) ? "Y" : "N"; + if (remarks == null) + remarks = ""; + Map row = new LinkedHashMap<>(); + row.put("COLUMN_ID", ordinal); + row.put("COLUMN_NAME", colName); + row.put("DATA_TYPE", typeName); + row.put("DATA_LENGTH", colSize); + row.put("DATA_PRECISION", (scale >= 0 && precision > 0) ? precision : null); + row.put("DATA_SCALE", (scale >= 0) ? scale : null); + row.put("NULLABLE", nullableYN); + row.put("COMMENTS", remarks); + rows.add(row); + } + } + rows.sort(Comparator.comparingInt(m -> ((Number)m.get("COLUMN_ID")).intValue())); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("columns", rows)) + .addTextContent(new ObjectMapper().writeValueAsString(rows)) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getStartTransactionTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("start-transaction") + .title("Start Transaction") + .description("Start a JDBC transaction (autoCommit=false). Returns txId.") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + Connection c = openConnection(config, null); + c.setAutoCommit(false); + String txId = UUID.randomUUID().toString(); + TX.put(txId, c); + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("txId", txId)) + .addTextContent("{\"txId\":\"" + txId + "\"}") + .build(); + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getResumeTransactionTool() { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("resume-transaction") + .title("Resume Transaction") + .description("Verify a txId is active (no-op). Returns ok.") + .inputSchema(ToolSchemas.TX_ID) + .build()) + .callHandler((exchange, callReq) -> { + String txId = String.valueOf(callReq.arguments().get("txId")); + if (TX.containsKey(txId)) { + return new McpSchema.CallToolResult("{\"ok\":true}", false); + } + return new McpSchema.CallToolResult("Unknown txId", true); + }) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getCommitTransactionTool() { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("commit-transaction") + .title("Commit Transaction") + .description("Commit and close a txId.") + .inputSchema(ToolSchemas.TX_ID) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + String txId = String.valueOf(callReq.arguments().get("txId")); + Connection c = TX.remove(txId); + if (c == null) + return new McpSchema.CallToolResult("Unknown txId", true); + try (c) { + c.commit(); + return new McpSchema.CallToolResult("{\"ok\":true}", false); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getRollbackTransactionTool() { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("rollback-transaction") + .title("Rollback Transaction") + .description("Rollback and close a txId.") + .inputSchema(ToolSchemas.TX_ID) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + String txId = String.valueOf(callReq.arguments().get("txId")); + Connection c = TX.remove(txId); + if (c == null) + return new McpSchema.CallToolResult("Unknown txId", true); + try (c) { + c.rollback(); + return new McpSchema.CallToolResult("{\"ok\":true}", false); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDbPingTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("db-ping") + .title("DB Ping") + .description("Checks connectivity and round-trip latency; returns user, schema, DB name, and version.") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + long connStart = System.nanoTime(); + try (Connection c = openConnection(config, null)) { + long connectMs = (System.nanoTime() - connStart) / 1_000_000L; + long rtStart = System.nanoTime(); + String user = null, schema = null, dbName = null, dbTime = null; + try (PreparedStatement ps = c.prepareStatement(SqlQueries.DB_PING_QUERY); + ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + user = rs.getString(1); + schema = rs.getString(2); + dbName = rs.getString(3); + dbTime = rs.getString(4); + } + } + long roundTripMs = (System.nanoTime() - rtStart) / 1_000_000L; + DatabaseMetaData md = c.getMetaData(); + Map sc = new LinkedHashMap<>(); + sc.put("ok", true); + sc.put("connectLatencyMs", connectMs); + sc.put("roundTripMs", roundTripMs); + sc.put("dbProduct", md.getDatabaseProductName()); + sc.put("dbVersion", md.getDatabaseProductVersion()); + sc.put("user", user); + sc.put("schema", schema); + sc.put("dbName", dbName); + sc.put("dbTime", dbTime); + String text = String.format(""" + OK — connection verified + connectLatencyMs: %d + roundTripMs: %d + dbProduct: %s + dbVersion: %s + user: %s + schema: %s + dbName: %s + dbTime: %s + """, connectMs, roundTripMs, md.getDatabaseProductName(), + md.getDatabaseProductVersion(), user, schema, dbName, dbTime + ); + return McpSchema.CallToolResult.builder() + .structuredContent(sc) + .addTextContent(text) + .build(); + } + })) + .build(); + } + + private static McpServerFeatures.SyncToolSpecification getDbMetricsTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("db-metrics-range") + .title("DB Metrics") + .description("Returns Oracle DB metrics from V$SYSSTAT (cumulative counters only).") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null); + Statement st = c.createStatement(); + ResultSet rs = st.executeQuery(SqlQueries.DB_METRICS_RANGE)) { + var rows = rsToList(rs); + Map metrics = new LinkedHashMap<>(); + StringBuilder text = new StringBuilder( + "Live metrics — V$SYSSTAT (cumulative counters; rates not available):\n" + ); + for (Map r : rows) { + String name = String.valueOf(r.get("NAME")); + Object value = r.get("VALUE"); + metrics.put(name, Map.of("value", value, "unit", "")); + text.append("- ").append(name).append(": ").append(value).append("\n"); + } + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "source", "V$SYSSTAT", + "note", "Cumulative since instance startup; these are totals, not per-second rates.", + "metrics", metrics)) + .addTextContent(text.toString().trim()) + .build(); + } + })) + .build(); + } + + public static McpServerFeatures.SyncToolSpecification getExplainAndExecutePlanTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("explain-plan") + .title("Explain Plan (static or dynamic)") + .description(""" + Returns an Oracle execution plan for the provided SQL. + mode: "static" (EXPLAIN PLAN) or "dynamic" (DISPLAY_CURSOR of the last execution in this session). + Response includes: planText (DBMS_XPLAN output) and llmPrompt (ready-to-use for the LLM). + + You are an Oracle SQL performance expert. Explain the execution plan to the user in clear language and then provide prioritized, practical tuning advice. + + Instructions: + 1) Summarize how the query executes (major steps, joins, access paths). + 2) Point out potential bottlenecks (scans, sorts, joins, TEMP/PGA, cardinality mismatches if present). + 3) Give the top 3–5 tuning ideas with rationale (indexes, predicates, rewrites, stats/histograms, hints if appropriate). + 4) Mention any trade-offs or risks. + + Note to model: + If the sql is a dml operation and it was actually executed No permanent data changes were committed. When explaining the plan, mention this statement will be rolled back + """) + .inputSchema(ToolSchemas.EXPLAIN_PLAN) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + final String sql = String.valueOf(callReq.arguments().get("sql")); + if (sql == null || sql.isBlank()) { + return new McpSchema.CallToolResult("Parameter 'sql' is required", true); + } + final String mode = String.valueOf(callReq.arguments().getOrDefault("mode", "static")) + .toLowerCase(Locale.ROOT); + Boolean executeArg = null; + Object exObj = callReq.arguments().get("execute"); + if (exObj != null) executeArg = Boolean.parseBoolean(String.valueOf(exObj)); + + Integer maxRows = null; + try { + Object mr = callReq.arguments().get("maxRows"); + if (mr != null) maxRows = Integer.parseInt(String.valueOf(mr)); + } catch (Exception ignored) {} + final String xplanOptions = Optional.ofNullable(callReq.arguments().get("xplanOptions")) + .map(Object::toString).orElse(null); + var res = getExplainPlan( + c, + sql, + "dynamic".equals(mode), + maxRows, + executeArg, + xplanOptions + ); + Map payload = new LinkedHashMap<>(); + payload.put("mode", mode); + payload.put("sql", sql); + payload.put("planText", res.planText()); + + return McpSchema.CallToolResult.builder() + .structuredContent(payload) + .addTextContent(res.planText()) + .build(); + } + })) + .build(); + } + + /** + * Returns an execution plan (static or dynamic) for the given SQL and also produces + * an accompanying LLM prompt to explain and tune the plan. + * - static → EXPLAIN PLAN (no execution, estimated plan only) + * - dynamic → DISPLAY_CURSOR (requires a real cursor; may lightly execute the SQL) + * + * @param c JDBC connection + * @param sql SQL to analyze + * @param dynamic true = dynamic plan, false = static plan + * @param maxRows limit when lightly executing SELECT (default = 1) + * @param execute whether to execute or just parse (null = auto per SQL type) + * @param xplanOptions DBMS_XPLAN formatting options + */ + static ExplainResult getExplainPlan( + Connection c, + String sql, + boolean dynamic, + Integer maxRows, + Boolean execute, + String xplanOptions + ) throws Exception { + + if (!dynamic) { + try (Statement st = c.createStatement()) { + st.executeUpdate("EXPLAIN PLAN FOR " + sql); + } + String planText = readXplan(c, false, xplanOptions); + return new ExplainResult(planText); + } + + // dynamic mode → prepare or execute depending on flags + runQueryLightweight(c, sql, maxRows, execute); + + String planText = readXplan(c, true, xplanOptions); + return new ExplainResult(planText); + } + + /** + * Prepare or execute a statement lightly so a cursor exists for DISPLAY_CURSOR. + * Handles SELECT, DML, and DDL safely. + * + * @param c open connection + * @param sql SQL text + * @param maxRows optional limit (applies to SELECT only) + * @param execute whether to actually execute (null = smart default) + */ + private static void runQueryLightweight(Connection c, String sql, Integer maxRows, Boolean execute) + throws SQLException { + + boolean isSelect = looksSelect(sql); + boolean doExecute = (execute != null) ? execute : isSelect; // smart default + + if (!doExecute) { + // just parse (safe) + try (PreparedStatement ps = c.prepareStatement(sql)) { /* parse only */ } + return; + } + + if (isSelect) { + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setMaxRows((maxRows != null && maxRows > 0) ? maxRows : 1); + ps.setFetchSize(1); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { /* first row only */ } + } + } + return; + } + + // DML/DDL: execute inside rollback-safe transaction (to minimise side effects) + boolean prevAutoCommit = c.getAutoCommit(); + try { + c.setAutoCommit(false); + + String execSql = injectGatherStatsHintAfterVerb(sql); + try (PreparedStatement ps = c.prepareStatement(execSql)) { + ps.execute(); + } + + c.rollback(); + } finally { + c.setAutoCommit(prevAutoCommit); + } + + } + + /** Read DBMS_XPLAN for either EXPLAIN PLAN or last cursor. */ + private static String readXplan(Connection c, boolean dynamic, String xplanOptions) throws SQLException { + final String opts = (xplanOptions == null || xplanOptions.isBlank()) + ? (dynamic ? "ALLSTATS LAST +PEEKED_BINDS +OUTLINE +PROJECTION" + : "BASIC +OUTLINE +PROJECTION +ALIAS") + : xplanOptions; + final String q = dynamic + ? ("SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, '" + opts + "'))") + : ("SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, '" + opts + "'))"); + + StringBuilder sb = new StringBuilder(); + try (Statement st = c.createStatement(); ResultSet rs = st.executeQuery(q)) { + while (rs.next()) sb.append(Objects.toString(rs.getString(1), "")).append('\n'); + } + return sb.toString().trim(); + } + + private static final Pattern DML_VERB = + Pattern.compile("^\\s*(?:--.*?$|/\\*.*?\\*/\\s*)*(UPDATE|DELETE|INSERT|MERGE)\\b", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); + + /** Injects "/*+ gather_plan_statistics +*\/" immediately after the first DML verb. + * - Preserves leading whitespace and comments + * - No-op if the SQL already contains the hint (case-insensitive) + * - Skips if not a DML statement (e.g., SELECT/BEGIN/DECLARE/ALTER/CREATE) + */ + static String injectGatherStatsHintAfterVerb(String sql) { + if (sql == null) return null; + String s = sql.trim(); + if (s.toLowerCase(Locale.ROOT).contains("gather_plan_statistics")) return sql; + String head = s.length() >= 16 ? s.substring(0, 16).toLowerCase(Locale.ROOT) : s.toLowerCase(Locale.ROOT); + if (head.startsWith("begin") || head.startsWith("declare") || isDdl(head)) { + return sql; + } + return injectAfterMatch(sql, DML_VERB, "/*+ gather_plan_statistics */", "gather_plan_statistics"); + } + + /** + * Injects a given string after the first match group found by the pattern, + * unless the SQL already contains the skip string (case-insensitive). + * + * @param sql SQL statement to operate on + * @param pattern Regex pattern with a capturing group for insertion point + * @param injection Text to inject + * @param skipIfContains Injection is skipped if this substring (case-insensitive) is present + * @return Modified SQL with the injection, or original if no changes made + */ + private static String injectAfterMatch( + String sql, Pattern pattern, String injection, String skipIfContains + ) { + if (sql == null) return null; + String s = sql.trim(); + if (s.toLowerCase(Locale.ROOT).contains(skipIfContains.toLowerCase(Locale.ROOT))) + return sql; + Matcher m = pattern.matcher(sql); + if (!m.find()) return sql; + int start = m.start(1), end = m.end(1); + String word = sql.substring(start, end); + StringBuilder out = new StringBuilder(sql.length() + injection.length() + 4); + out.append(sql, 0, start) + .append(word) + .append(" ").append(injection) + .append(sql.substring(end)); + return out.toString(); + } + + private record ExplainResult(String planText) {} + + private static int execUpdate(Connection c, String sql) throws SQLException { + try (Statement st = c.createStatement()) { + return st.executeUpdate(sql); + } + } + + private static class ConnLease implements AutoCloseable { + final Connection c; + final boolean closeOnExit; + + ConnLease(Connection c, boolean closeOnExit) { + this.c = c; + this.closeOnExit = closeOnExit; + } + + @Override + public void close() { + if (closeOnExit) { + try { c.close(); } catch (Exception ignored) {} + } + } + } + + private static DatabaseOperatorTools.ConnLease acquireConnection(ServerConfig config, + Object txIdArg) throws SQLException { + String txId = (txIdArg == null) ? null : String.valueOf(txIdArg); + if (txId == null || txId.isBlank()) { + Connection c = openConnection(config, null); + try { + c.setAutoCommit(true); + } catch (Exception ignored) {} + return new DatabaseOperatorTools.ConnLease(c, true); + } + Connection c = TX.get(txId); + if (c == null) throw new SQLException("Unknown txId"); + return new DatabaseOperatorTools.ConnLease(c, false); + } + +} diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java deleted file mode 100644 index f0def8bc..00000000 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/ExplainAndExecutePlanTool.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - ** Oracle Database MCP Toolkit version 1.0.0 - ** - ** Copyright (c) 2025 Oracle and/or its affiliates. - ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - */ - -package com.oracle.database.mcptoolkit.tools; - -import com.oracle.database.mcptoolkit.ServerConfig; -import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.spec.McpSchema; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static com.oracle.database.mcptoolkit.Utils.*; - -/** - * Provides functionality for explaining and executing Oracle SQL plans. - * This class contains methods to generate execution plans for SQL queries and - * to explain these plans in a human-readable format. - */ -public class ExplainAndExecutePlanTool { - /** - * Returns a tool specification for the "explain-plan" tool. - * This tool generates an Oracle execution plan for the provided SQL and - * produces an accompanying LLM prompt to explain and tune the plan. - * - * @param config Server configuration - * @return Tool specification for the "explain-plan" tool - */ - public static McpServerFeatures.SyncToolSpecification getExplainAndExecutePlanTool(ServerConfig config) { - return - McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("explain-plan") - .title("Explain Plan (static or dynamic)") - .description(""" - Returns an Oracle execution plan for the provided SQL. - mode: "static" (EXPLAIN PLAN) or "dynamic" (DISPLAY_CURSOR of the last execution in this session). - Response includes: planText (DBMS_XPLAN output) and llmPrompt (ready-to-use for the LLM). - - You are an Oracle SQL performance expert. Explain the execution plan to the user in clear language and then provide prioritized, practical tuning advice. - - Instructions: - 1) Summarize how the query executes (major steps, joins, access paths). - 2) Point out potential bottlenecks (scans, sorts, joins, TEMP/PGA, cardinality mismatches if present). - 3) Give the top 3–5 tuning ideas with rationale (indexes, predicates, rewrites, stats/histograms, hints if appropriate). - 4) Mention any trade-offs or risks. - - Note to model: - If the sql is a dml operation and it was actually executed No permanent data changes were committed. When explaining the plan, mention this statement will be rolled back - """) - .inputSchema(ToolSchemas.EXPLAIN_PLAN) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null)) { - final String sql = String.valueOf(callReq.arguments().get("sql")); - if (sql == null || sql.isBlank()) { - return new McpSchema.CallToolResult("Parameter 'sql' is required", true); - } - final String mode = String.valueOf(callReq.arguments().getOrDefault("mode", "static")) - .toLowerCase(Locale.ROOT); - - Boolean executeArg = null; - Object exObj = callReq.arguments().get("execute"); - if (exObj != null) executeArg = Boolean.parseBoolean(String.valueOf(exObj)); - - Integer maxRows = null; - try { - Object mr = callReq.arguments().get("maxRows"); - if (mr != null) maxRows = Integer.parseInt(String.valueOf(mr)); - } catch (Exception ignored) {} - - final String xplanOptions = Optional.ofNullable(callReq.arguments().get("xplanOptions")) - .map(Object::toString).orElse(null); - - var res = getExplainPlan( - c, - sql, - "dynamic".equals(mode), - maxRows, - executeArg, - xplanOptions - ); - Map payload = new LinkedHashMap<>(); - payload.put("mode", mode); - payload.put("sql", sql); - payload.put("planText", res.planText()); - - return McpSchema.CallToolResult.builder() - .structuredContent(payload) - .addTextContent(res.planText()) - .build(); - } - })) - .build(); - } - - - /** - * Returns an execution plan (static or dynamic) for the given SQL and also produces - * an accompanying LLM prompt to explain and tune the plan. - * - static → EXPLAIN PLAN (no execution, estimated plan only) - * - dynamic → DISPLAY_CURSOR (requires a real cursor; may lightly execute the SQL) - * - * @param c JDBC connection - * @param sql SQL to analyze - * @param dynamic true = dynamic plan, false = static plan - * @param maxRows limit when lightly executing SELECT (default = 1) - * @param execute whether to execute or just parse (null = auto per SQL type) - * @param xplanOptions DBMS_XPLAN formatting options - */ - static ExplainResult getExplainPlan( - Connection c, - String sql, - boolean dynamic, - Integer maxRows, - Boolean execute, - String xplanOptions - ) throws Exception { - - if (!dynamic) { - try (Statement st = c.createStatement()) { - st.executeUpdate("EXPLAIN PLAN FOR " + sql); - } - String planText = readXplan(c, false, xplanOptions); - return new ExplainResult(planText); - } - - // dynamic mode → prepare or execute depending on flags - runQueryLightweight(c, sql, maxRows, execute); - - String planText = readXplan(c, true, xplanOptions); - return new ExplainResult(planText); - } - - - /** - * Prepare or execute a statement lightly so a cursor exists for DISPLAY_CURSOR. - * Handles SELECT, DML, and DDL safely. - * - * @param c open connection - * @param sql SQL text - * @param maxRows optional limit (applies to SELECT only) - * @param execute whether to actually execute (null = smart default) - */ - private static void runQueryLightweight(Connection c, String sql, Integer maxRows, Boolean execute) - throws SQLException { - - boolean isSelect = looksSelect(sql); - boolean doExecute = (execute != null) ? execute : isSelect; // smart default - - if (!doExecute) { - // just parse (safe) - try (PreparedStatement ps = c.prepareStatement(sql)) { /* parse only */ } - return; - } - - if (isSelect) { - try (PreparedStatement ps = c.prepareStatement(sql)) { - ps.setMaxRows((maxRows != null && maxRows > 0) ? maxRows : 1); - ps.setFetchSize(1); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { /* first row only */ } - } - } - return; - } - - // DML/DDL: execute inside rollback-safe transaction (to minimise side effects) - boolean prevAutoCommit = c.getAutoCommit(); - try { - c.setAutoCommit(false); - - String execSql = injectGatherStatsHintAfterVerb(sql); - try (PreparedStatement ps = c.prepareStatement(execSql)) { - ps.execute(); - } - - c.rollback(); - } finally { - c.setAutoCommit(prevAutoCommit); - } - - } - - /** Read DBMS_XPLAN for either EXPLAIN PLAN or last cursor. */ - private static String readXplan(Connection c, boolean dynamic, String xplanOptions) throws SQLException { - final String opts = (xplanOptions == null || xplanOptions.isBlank()) - ? (dynamic ? "ALLSTATS LAST +PEEKED_BINDS +OUTLINE +PROJECTION" - : "BASIC +OUTLINE +PROJECTION +ALIAS") - : xplanOptions; - final String q = dynamic - ? ("SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, '" + opts + "'))") - : ("SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, '" + opts + "'))"); - - StringBuilder sb = new StringBuilder(); - try (Statement st = c.createStatement(); ResultSet rs = st.executeQuery(q)) { - while (rs.next()) sb.append(Objects.toString(rs.getString(1), "")).append('\n'); - } - return sb.toString().trim(); - } - - private static final Pattern DML_VERB = - Pattern.compile("^\\s*(?:--.*?$|/\\*.*?\\*/\\s*)*(UPDATE|DELETE|INSERT|MERGE)\\b", - Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); - - /** Injects "/*+ gather_plan_statistics +*\/" immediately after the first DML verb. - * - Preserves leading whitespace and comments - * - No-op if the SQL already contains the hint (case-insensitive) - * - Skips if not a DML statement (e.g., SELECT/BEGIN/DECLARE/ALTER/CREATE) - */ - static String injectGatherStatsHintAfterVerb(String sql) { - if (sql == null) return null; - String s = sql.trim(); - if (s.toLowerCase(Locale.ROOT).contains("gather_plan_statistics")) return sql; - String head = s.length() >= 16 ? s.substring(0, 16).toLowerCase(Locale.ROOT) : s.toLowerCase(Locale.ROOT); - if (head.startsWith("begin") || head.startsWith("declare") || isDdl(head)) { - return sql; - } - return injectAfterMatch(sql, DML_VERB, "/*+ gather_plan_statistics */", "gather_plan_statistics"); - } - - static final Pattern FIRST_WORD = Pattern.compile("^\\s*([A-Za-z0-9_]+)"); - - record ExplainResult(String planText) {} - - /** - * Injects a given string after the first match group found by the pattern, - * unless the SQL already contains the skip string (case-insensitive). - * - * @param sql SQL statement to operate on - * @param pattern Regex pattern with a capturing group for insertion point - * @param injection Text to inject - * @param skipIfContains Injection is skipped if this substring (case-insensitive) is present - * @return Modified SQL with the injection, or original if no changes made - */ - private static String injectAfterMatch( - String sql, Pattern pattern, String injection, String skipIfContains - ) { - if (sql == null) return null; - String s = sql.trim(); - if (s.toLowerCase(Locale.ROOT).contains(skipIfContains.toLowerCase(Locale.ROOT))) - return sql; - Matcher m = pattern.matcher(sql); - if (!m.find()) return sql; - int start = m.start(1), end = m.end(1); - String word = sql.substring(start, end); - StringBuilder out = new StringBuilder(sql.length() + injection.length() + 4); - out.append(sql, 0, start) - .append(word) - .append(" ").append(injection) - .append(sql.substring(end)); - return out.toString(); - } -} diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java new file mode 100644 index 00000000..0aa726f3 --- /dev/null +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java @@ -0,0 +1,315 @@ +/* + ** Oracle Database MCP Toolkit version 1.0.0 + ** + ** Copyright (c) 2026 Oracle and/or its affiliates. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.database.mcptoolkit.tools; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oracle.database.mcptoolkit.LoadedConstants; +import com.oracle.database.mcptoolkit.OracleDatabaseMCPToolkit; +import com.oracle.database.mcptoolkit.ServerConfig; +import com.oracle.database.mcptoolkit.Utils; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import org.yaml.snakeyaml.Yaml; + +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +/** + * Admin/maintenance tools: + * - list-tools: list all available tools with descriptions + * - edit-tools: upsert a YAML-defined tool in the config file and rely on runtime reload + */ +public class McpAdminTools { + + private McpAdminTools() {} + + /** + * Returns all MCP admin tool specifications. + */ + public static List getTools(ServerConfig config) { + List tools = new ArrayList<>(); + + tools.add(getListToolsTool(config)); + tools.add(getEditToolsTool(config)); + + return tools; + } + + public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("list-tools") + .title("List Tools") + .description("List all available tools with their descriptions.") + .inputSchema(ToolSchemas.NO_INPUT_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> { + try { + // Always use the latest runtime config to avoid requiring a rebuild + ServerConfig current = OracleDatabaseMCPToolkit.getConfig() != null ? OracleDatabaseMCPToolkit.getConfig() : config; + + List> tools = new ArrayList<>(); + + // 1) Built-in log analyzer tools (respect toolsFilter) + for (McpServerFeatures.SyncToolSpecification spec : LogAnalyzerTools.getTools()) { + String name = spec.tool().name(); + if (isEnabled(current, name)) { + tools.add(Map.of( + "name", name, + "title", spec.tool().title(), + "description", spec.tool().description() + )); + } + } + + // 2) RAG tools (respect toolsFilter) + for (McpServerFeatures.SyncToolSpecification spec : RagTools.getTools(current)) { + String name = spec.tool().name(); + if (isEnabled(current, name)) { + tools.add(Map.of( + "name", name, + "title", spec.tool().title(), + "description", spec.tool().description() + )); + } + } + + // 3) Database operator tools (respect toolsFilter) + for (McpServerFeatures.SyncToolSpecification spec : DatabaseOperatorTools.getTools(current)) { + String name = spec.tool().name(); + if (isEnabled(current, name)) { + tools.add(Map.of( + "name", name, + "title", spec.tool().title(), + "description", spec.tool().description() + )); + } + } + + // 3) Custom YAML tools (always listed if present in config) + if (current.tools != null) { + for (Map.Entry e : current.tools.entrySet()) { + var tc = e.getValue(); + tools.add(Map.of( + "name", tc.name, + "title", tc.name, + "description", tc.description + )); + } + } + + // 4) Admin tools themselves if enabled + if (isEnabled(current, "list-tools")) { + tools.add(Map.of( + "name", "list-tools", + "title", "List Tools", + "description", "List all available tools with their descriptions." + )); + } + if (isEnabled(current, "edit-tools")) { + tools.add(Map.of( + "name", "edit-tools", + "title", "Edit/Add Tools", + "description", "Create or update YAML-defined tools and trigger live reload." + )); + } + + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("tools", tools)) + .addTextContent(new ObjectMapper().writeValueAsString(tools)) + .build(); + } catch (Exception e) { + return McpSchema.CallToolResult.builder() + .addTextContent("Unexpected: " + e.getMessage()) + .isError(true) + .build(); + } + }) + .build(); + } + + public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerConfig config) { + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("edit-tools") + .title("Edit/Add Tools") + .description("Create or update YAML-defined tools in the config file. Changes are auto-reloaded.") + .inputSchema(ToolSchemas.EDIT_TOOL_SCHEMA) + .build()) + .callHandler((exchange, callReq) -> { + try { + final var cfgPath = LoadedConstants.CONFIG_FILE; + if (cfgPath == null || cfgPath.isBlank()) { + return McpSchema.CallToolResult.builder() + .addTextContent("Config file path (configFile) is not set. Cannot edit tools.") + .isError(true) + .build(); + } + + String name = asString(callReq.arguments().get("name")); + if (name == null || name.isBlank()) { + return McpSchema.CallToolResult.builder() + .addTextContent("'name' is required") + .isError(true) + .build(); + } + + Path path = Paths.get(cfgPath); + Yaml yaml = new Yaml(); + + // Load existing YAML into a mutable map + Map root; + if (Files.exists(path)) { + try (Reader r = Files.newBufferedReader(path)) { + Object loaded = yaml.load(r); + if (loaded instanceof Map) { + //noinspection unchecked + root = new LinkedHashMap<>((Map) loaded); + } else { + root = new LinkedHashMap<>(); + } + } + } else { + root = new LinkedHashMap<>(); + } + + // Ensure 'tools' map exists + Map tools = getOrCreateMap(root, "tools"); + + // Remove if requested + Object removeFlag = callReq.arguments().get("remove"); + boolean remove = (removeFlag instanceof Boolean b && b) || + (removeFlag != null && "true".equalsIgnoreCase(String.valueOf(removeFlag))); + if (remove) { + if (tools.remove(name) != null) { + root.put("tools", tools); + try (Writer w = Files.newBufferedWriter(path)) { + yaml.dump(root, w); + } + // Try to remove the tool from the running server as well + try { + OracleDatabaseMCPToolkit.getServer().removeTool(name); + Utils.unregisterCustomToolLocally(name); + } catch (Exception ignored) {} + + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "status", "ok", + "message", "Tool '" + name + "' removed from YAML. Reload will occur shortly.", + "configFile", cfgPath + )) + .addTextContent("{\"status\":\"ok\",\"removed\":\"" + name + "\"}") + .build(); + } else { + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "status", "noop", + "message", "Tool '" + name + "' not found; nothing to remove.", + "configFile", cfgPath + )) + .addTextContent("{\"status\":\"noop\",\"missing\":\"" + name + "\"}") + .build(); + } + } + + // Upsert tool entry + Map t = getOrCreateMap(tools, name); + + // Optional fields + putIfPresent(t, callReq.arguments(), "description"); + putIfPresent(t, callReq.arguments(), "dataSource"); + putIfPresent(t, callReq.arguments(), "statement"); + + // parameters: array of objects + Object paramsObj = callReq.arguments().get("parameters"); + if (paramsObj instanceof List list) { + List> params = new ArrayList<>(); + for (Object o : list) { + if (o instanceof Map m) { + Map p = new LinkedHashMap<>(); + copyIfPresent(m, p, "name"); + copyIfPresent(m, p, "type"); + copyIfPresent(m, p, "description"); + copyIfPresent(m, p, "required"); + params.add(p); + } + } + t.put("parameters", params); + } + + tools.put(name, t); + root.put("tools", tools); + + // Write back YAML + try (Writer w = Files.newBufferedWriter(path)) { + yaml.dump(root, w); + } + + // Try to remove stale instance at runtime (best-effort hot update); server will re-add on reload + try { + var srv = OracleDatabaseMCPToolkit.getServer(); + if (srv != null) srv.removeTool(name); + } catch (Exception ignored) {} + + + // The config poller will pick up the change and hot-reload within ~2 seconds. + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of( + "status", "ok", + "message", "Tool '" + name + "' upserted into YAML. Reload will occur shortly.", + "configFile", cfgPath + )) + .addTextContent("{\"status\":\"ok\",\"name\":\"" + name + "\"}") + .build(); + } catch (Exception e) { + + return McpSchema.CallToolResult.builder() + .addTextContent("Unexpected: " + e.getMessage()) + .isError(true) + .build(); + } + }) + .build(); + } + + private static boolean isEnabled(ServerConfig config, String toolName) { + if (config.toolsFilter == null) return true; + String key = toolName.toLowerCase(Locale.ROOT); + return config.toolsFilter.contains(key); + } + + private static String asString(Object v) { + return v == null ? null : String.valueOf(v); + } + + @SuppressWarnings("unchecked") + private static Map getOrCreateMap(Map parent, String key) { + Object o = parent.get(key); + if (o instanceof Map m) { + return new LinkedHashMap<>((Map) m); + } + Map created = new LinkedHashMap<>(); + parent.put(key, created); + return created; + } + + private static void putIfPresent(Map target, Map source, String key) { + Object v = source.get(key); + if (v != null) target.put(key, v); + } + + private static void copyIfPresent(Map src, Map dst, String key) { + Object v = src.get(key); + if (v != null) dst.put(key, v); + } + +} diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java similarity index 56% rename from src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java rename to src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java index 33e8beca..34e228a4 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/SimilaritySearchTool.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java @@ -24,12 +24,12 @@ import static com.oracle.database.mcptoolkit.tools.ToolSchemas.SIMILARITY_SEARCH; /** - * Provides a tool for performing similarity searches using vector embeddings. + * RAG (Retrieval-Augmented Generation) tools. *

- * This class is responsible for handling the "similarity-search" tool, which - * allows users to search for similar text based on a given query. + * This class provides tools for RAG applications, including similarity search + * using vector embeddings. */ -public class SimilaritySearchTool { +public class RagTools { private static final String DEFAULT_VECTOR_TABLE = "profile_oracle"; private static final String DEFAULT_VECTOR_DATA_COLUMN = "text"; @@ -37,6 +37,17 @@ public class SimilaritySearchTool { private static final String DEFAULT_VECTOR_MODEL_NAME = "doc_model"; private static final int DEFAULT_VECTOR_TEXT_FETCH_LIMIT = 4000; + private RagTools() {} + + /** + * Returns all RAG tool specifications. + */ + public static List getTools(ServerConfig config) { + List tools = new ArrayList<>(); + tools.add(getSymilaritySearchTool(config)); + return tools; + } + /** * Returns a tool specification for the "similarity_search" tool. *

@@ -46,53 +57,52 @@ public class SimilaritySearchTool { * @return tool specification */ public static McpServerFeatures.SyncToolSpecification getSymilaritySearchTool(ServerConfig config) { - - return McpServerFeatures.SyncToolSpecification.builder() - .tool(McpSchema.Tool.builder() - .name("similarity-search") - .title("Similarity Search") - .description("Semantic vector similarity over a table with (text, embedding) columns") - .inputSchema(SIMILARITY_SEARCH) - .build()) - .callHandler((exchange, callReq) -> tryCall(() -> { - try (Connection c = openConnection(config, null)) { - Map arguments = callReq.arguments(); - String question = String.valueOf(arguments.get("question")); - if (question == null || question.isBlank()) { - return new McpSchema.CallToolResult("Question must be non-blank", true); - } - int topK; - try { - topK = Integer.parseInt(String.valueOf(arguments.getOrDefault("topK", 5))); - } catch (NumberFormatException e) { - topK = 5; - } - topK = Math.max(1, Math.min(100, topK)); - - String table = getOrDefault(arguments.get("table"), DEFAULT_VECTOR_TABLE); - String dataColumn = getOrDefault(arguments.get("dataColumn"), DEFAULT_VECTOR_DATA_COLUMN); - String embeddingColumn = getOrDefault(arguments.get("embeddingColumn"), DEFAULT_VECTOR_EMBEDDING_COLUMN); - String modelName = getOrDefault(arguments.get("modelName"), DEFAULT_VECTOR_MODEL_NAME); - - int textFetchLimit = DEFAULT_VECTOR_TEXT_FETCH_LIMIT; - Object limitArg = arguments.get("textFetchLimit"); - if (limitArg != null) { - try { - textFetchLimit = Math.max(1, Integer.parseInt(String.valueOf(limitArg))); - } - catch (NumberFormatException ignored) {} - } - - List results = runSimilaritySearch( - c, table, dataColumn, embeddingColumn, modelName, textFetchLimit, question, topK); - - return McpSchema.CallToolResult.builder() - .structuredContent(Map.of("rows", results)) - .addTextContent(new JsonMapper().writeValueAsString(results)) - .build(); - } - })) - .build(); + return McpServerFeatures.SyncToolSpecification.builder() + .tool(McpSchema.Tool.builder() + .name("similarity-search") + .title("Similarity Search") + .description("Semantic vector similarity over a table with (text, embedding) columns") + .inputSchema(SIMILARITY_SEARCH) + .build()) + .callHandler((exchange, callReq) -> tryCall(() -> { + try (Connection c = openConnection(config, null)) { + Map arguments = callReq.arguments(); + String question = String.valueOf(arguments.get("question")); + if (question == null || question.isBlank()) { + return new McpSchema.CallToolResult("Question must be non-blank", true); + } + int topK; + try { + topK = Integer.parseInt(String.valueOf(arguments.getOrDefault("topK", 5))); + } catch (NumberFormatException e) { + topK = 5; + } + topK = Math.max(1, Math.min(100, topK)); + + String table = getOrDefault(arguments.get("table"), DEFAULT_VECTOR_TABLE); + String dataColumn = getOrDefault(arguments.get("dataColumn"), DEFAULT_VECTOR_DATA_COLUMN); + String embeddingColumn = getOrDefault(arguments.get("embeddingColumn"), DEFAULT_VECTOR_EMBEDDING_COLUMN); + String modelName = getOrDefault(arguments.get("modelName"), DEFAULT_VECTOR_MODEL_NAME); + + int textFetchLimit = DEFAULT_VECTOR_TEXT_FETCH_LIMIT; + Object limitArg = arguments.get("textFetchLimit"); + if (limitArg != null) { + try { + textFetchLimit = Math.max(1, Integer.parseInt(String.valueOf(limitArg))); + } + catch (NumberFormatException ignored) {} + } + + List results = runSimilaritySearch( + c, table, dataColumn, embeddingColumn, modelName, textFetchLimit, question, topK); + + return McpSchema.CallToolResult.builder() + .structuredContent(Map.of("rows", results)) + .addTextContent(new JsonMapper().writeValueAsString(results)) + .build(); + } + })) + .build(); } From 1f5325cc2610992ce79682f5c15fd43517fffb20 Mon Sep 17 00:00:00 2001 From: Mouhsin Elmajdouby Date: Tue, 27 Jan 2026 13:42:34 +0100 Subject: [PATCH 09/17] use consistent tool listing pattern for MCP admin tools --- .../mcptoolkit/tools/McpAdminTools.java | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java index 0aa726f3..f5f4a6f8 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java @@ -107,20 +107,15 @@ public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerCon } } - // 4) Admin tools themselves if enabled - if (isEnabled(current, "list-tools")) { - tools.add(Map.of( - "name", "list-tools", - "title", "List Tools", - "description", "List all available tools with their descriptions." - )); - } - if (isEnabled(current, "edit-tools")) { - tools.add(Map.of( - "name", "edit-tools", - "title", "Edit/Add Tools", - "description", "Create or update YAML-defined tools and trigger live reload." - )); + for (McpServerFeatures.SyncToolSpecification spec : McpAdminTools.getTools(current)) { + String name = spec.tool().name(); + if (isEnabled(current, name)) { + tools.add(Map.of( + "name", name, + "title", spec.tool().title(), + "description", spec.tool().description() + )); + } } return McpSchema.CallToolResult.builder() From a04b4b64bd21d69358976b12238e501bb113e032 Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Wed, 28 Jan 2026 09:46:06 +0100 Subject: [PATCH 10/17] Making DEMO.md about to build it yourself --- src/oracle-db-mcp-toolkit/DEMO.md | 84 +++++++++++++++++-------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/oracle-db-mcp-toolkit/DEMO.md b/src/oracle-db-mcp-toolkit/DEMO.md index 0dd7c0db..47a27891 100644 --- a/src/oracle-db-mcp-toolkit/DEMO.md +++ b/src/oracle-db-mcp-toolkit/DEMO.md @@ -2,8 +2,9 @@ ## 1.Overview -To test the capabilities of the Oracle Database MCP Toolkit, a demo instance of the MCP server is made available via - with the following tools activated: +This document demonstrates how to try the Oracle Database MCP Toolkit by running your own instance of the MCP server. + +The demo focuses on the following tools: JDBC log analysis tools: @@ -14,12 +15,12 @@ JDBC log analysis tools: - **`list-log-files-from-directory`**: List all visible files from a specified directory, which helps the user analyze multiple files with one prompt. - **`jdbc-log-comparison`**: Compares two log files for performance metrics, errors, and network information. -RDBMS/SQLNet trace analysis Tools: +RDBMS/SQLNet trace analysis tools: - **`get-rdbms-errors`**: Extracts errors from RDBMS/SQLNet trace files. - **`get-rdbms-packet-dumps`**: Extracts packet dumps for a specific connection ID. -Custom tools (created using YAML configuration file): +Custom tools (via YAML configuration): - **`hotels-by-name`**: Return the details of a hotel given its name. The details include the capacity, rating and address. This tool is created using the following YAML configuration file: @@ -42,33 +43,54 @@ tools: statement: SELECT * FROM hotels WHERE name LIKE '%' || :name || '%' ``` -Where `${db_url}`, `${user}` and `${password}`are environment variables. +Where `${db_url}`, `${user}` and `${password}` are environment variables. ## 2. Requirements -An MCP Client that support Streamable HTTP transport mode is needed, such as MCP Inspector, Cline or Claude Desktop. +- An MCP client that supports the Streamable HTTP transport (e.g., MCP Inspector, Cline, Claude Desktop). Stdio is also + supported by the server; see the README for details. +- A running MCP Toolkit server. **Note**: If you're using Claude Desktop, you also need [mcp-remote](https://www.npmjs.com/package/mcp-remote). -## 3. Setup +## 3. Start a local MCP Toolkit server (HTTP example) -The deployed instance uses `streamableHttp` transport protocol and a runtime generated `Authorization` token. +You can run the server over HTTPS with authentication enabled. The token can be supplied via the +`ORACLE_DB_TOOLKIT_AUTH_TOKEN` environment variable or, if not set, it will be generated and printed to the logs (see README §4.4). -Use the following token `3e297077-f01e-4045-a9d0-2a71e97e6dfa`. +Example: -### MCP Inspector +```bash +java \ + -Dtransport=http \ + -Dhttps.port=45450 \ + -DcertificatePath=/path/to/your-certificate.p12 \ + -DcertificatePassword=yourPassword \ + -DenableAuthentication=true \ + -Dtools=get-jdbc-stats,get-jdbc-queries,get-jdbc-errors,jdbc-log-comparison,get-rdbms-errors,get-rdbms-packet-dumps \ + -jar /oracle-db-mcp-toolkit-1.0.0.jar +``` + +This exposes the MCP endpoint at: `https://localhost:45450/mcp`. + +When connecting from a client, include the token in the `Authorization` header as `Bearer YOUR_TOKEN`. + +For additional deployment modes (including stdio and Docker/Podman) and OAuth2 configuration, see the project README. + +## 4. Connect your MCP client -To use MCP Inspector as an MCP client, specify `streamableHttp`as transport type, `https://mcptoolkit.orcl.dev:45453/mcp` as the URL, _Via Proxy_ as Connection Type, -for Authentication, add a `Authorization` custom header with `Bearer 3e297077-f01e-4045-a9d0-2a71e97e6dfa` as value. -the final configuration should look as shown below: +### MCP Inspector -MCP Inspector config screenshot +Configure MCP Inspector with: -After checking the configuration, click the *Connect* button, and the available tools will be shown in the main section: +- Transport: `streamableHttp` +- URL: `https://localhost:45450/mcp` +- Connection Type: Via Proxy +- Authentication: Add a custom header `Authorization: Bearer YOUR_TOKEN` -MCP Inspector tools screenshot +After saving, click Connect and the available tools will be listed. -_Note :_ The filePath should be provided as a URL. +_Note:_ For log analysis tools, provide `filePath` values as URLs where applicable. ### Cline @@ -82,22 +104,14 @@ Add or merge this configuration into `cline_mcp_settings.json`: "disabled": false, "timeout": 60, "type": "streamableHttp", - "url": "https://mcptoolkit.orcl.dev:45453/mcp", - "headers": { - "Authorization": "Bearer 3e297077-f01e-4045-a9d0-2a71e97e6dfa" - } + "url": "https://localhost:45450/mcp", + "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` -After saving the configuration file, the available tools will be shown in the *Configure* Tab of *MCP Servers* settings: - -Cline tools screenshot - -Here's an example of a prompt that trigger the `get-jdbc-queries` tool: - -Cline prompt example screenshot +After saving, the tools will appear in the MCP Servers settings. You can then invoke tools like `get-jdbc-queries` by prompt. ### Claude Desktop @@ -112,22 +126,14 @@ Below is an example of `claude_desktop_config.json` file: "args": [ "-y", "mcp-remote", - "https://mcptoolkit.orcl.dev:45453/mcp", + "https://localhost:45450/mcp", "--header", "Authorization:${DEMO_TOKEN}" ], - "env": { - "DEMO_TOKEN": "Bearer 3e297077-f01e-4045-a9d0-2a71e97e6dfa" - } + "env": { "DEMO_TOKEN": "Bearer YOUR_TOKEN" } } } } ``` -Upon saving the configuration file an opening Claude Desktop, you'll be to see the tools in the *Connectors* section: - -Claude Desktop tools screenshot - -Here's the result of the same prompt used to know what queries were executed : - -Claude Desktop prompt example screenshot \ No newline at end of file +Upon saving, open Claude Desktop and you should see the tools in the Connectors section. \ No newline at end of file From bda6cbbb2264625fd3b76da4de7dbb5551a9be3a Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Thu, 29 Jan 2026 13:52:32 +0100 Subject: [PATCH 11/17] Container deployment in DEMO.md --- src/oracle-db-mcp-java-toolkit/DEMO.md | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/oracle-db-mcp-java-toolkit/DEMO.md b/src/oracle-db-mcp-java-toolkit/DEMO.md index 516fec73..e7d0e109 100644 --- a/src/oracle-db-mcp-java-toolkit/DEMO.md +++ b/src/oracle-db-mcp-java-toolkit/DEMO.md @@ -75,6 +75,37 @@ When connecting from a client, include the token in the `Authorization` header a For additional deployment modes (including stdio and Docker/Podman) and OAuth2 configuration, see the project README. +### Run in a container (Podman) + +The repository contains a Dockerfile you can use to build and run the server in a container. + +1) Build the image (from the repo root): + +```bash +podman build -t oracle-db-mcp-toolkit:1.0.0 . +``` + +2) Run the container with HTTPS and token auth (adjust paths and secrets for your environment): + +```bash +podman run --name with_token_auth -d \ + -p 45453:45453 \ + -v /home/opc/jetty.p12:/app/jetty.p12:ro,z \ + -v /home/opc/custom_tools/custom_tools.yaml:/app/custom_tools.yaml:ro,z \ + -e JAVA_TOOL_OPTIONS="-Dtransport=http -Dhttps.port=45453 -DcertificatePath=/app/jetty.p12 -DcertificatePassword=CERTIF_PASSWORD -DenableAuthentication=true -Dtools=mcp-admin,log-analyzer,rag -DconfigFile=/app/custom_tools.yaml" \ + -e ORACLE_DB_TOOLKIT_AUTH_TOKEN="3e297077-f01e-4045-a9d0-2a71e97e6dfa" \ + oracle-db-mcp-toolkit:1.0.0 +``` + +After the container starts, the MCP endpoint is available at: + + https://localhost:45453/mcp + +Notes: + +- The :z volume flag is commonly required on SELinux-enabled hosts (Podman). It is harmless elsewhere. +- For Docker, you can use the same command but omit :z on volume mounts. + ## 4. Connect your MCP client ### MCP Inspector From a553eb0e53c5a4ac94ebc50e36d8973c2dbccc49 Mon Sep 17 00:00:00 2001 From: Ayoub Aarrasse Date: Wed, 4 Feb 2026 10:40:58 +0100 Subject: [PATCH 12/17] Priority to built-in tools --- .../database/mcptoolkit/ServerConfig.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java index 4a84a23d..b75765ff 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/ServerConfig.java @@ -12,6 +12,8 @@ import com.oracle.database.mcptoolkit.config.ToolConfig; import java.util.*; +import java.util.logging.Logger; +import java.util.logging.Level; /** * Immutable server configuration loaded from system properties. @@ -33,6 +35,7 @@ *

Use {@link #fromSystemProperties()} to create an instance with validation and defaults.

*/ public final class ServerConfig { + private static final Logger LOG = Logger.getLogger(ServerConfig.class.getName()); public final String dbUrl; public final String dbUser; public final char[] dbPassword; @@ -214,7 +217,17 @@ private static Set expandToolsFilter(Set raw, ConfigRoot configR String k = name == null ? null : name.trim().toLowerCase(Locale.ROOT); if (k == null || k.isEmpty()) continue; - // YAML toolset match + // Built-in toolset match first. If a YAML toolset has the same name, prefer built-in and log an error. + Set builtin = BUILTIN_TOOLSETS.get(k); + if (builtin != null) { + if (yamlSets != null && yamlSets.containsKey(k)) { + LOG.log(Level.SEVERE, () -> "Custom toolset '" + k + "' conflicts with built-in toolset; ignoring custom definition and using built-in."); + } + out.addAll(builtin); + continue; + } + + // YAML toolset match (only if not a built-in name) if (yamlSets != null && yamlSets.containsKey(k)) { List members = yamlSets.get(k); if (members != null) { @@ -228,13 +241,6 @@ private static Set expandToolsFilter(Set raw, ConfigRoot configR continue; } - // Built-in toolset match - Set builtin = BUILTIN_TOOLSETS.get(k); - if (builtin != null) { - out.addAll(builtin); - continue; - } - // Fallback to explicit tool name out.add(k); } From da41b0835121dcf670d61d8ecd0fb22b2d540584 Mon Sep 17 00:00:00 2001 From: Youssef Erradi Date: Wed, 4 Feb 2026 13:13:03 +0100 Subject: [PATCH 13/17] Update MAVEN_VERSION value to 3.9.12 --- src/oracle-db-mcp-java-toolkit/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oracle-db-mcp-java-toolkit/Dockerfile b/src/oracle-db-mcp-java-toolkit/Dockerfile index 2034a156..5e212faf 100644 --- a/src/oracle-db-mcp-java-toolkit/Dockerfile +++ b/src/oracle-db-mcp-java-toolkit/Dockerfile @@ -1,7 +1,7 @@ # ---------- 1) Build stage ---------- FROM container-registry.oracle.com/java/openjdk:17 AS builder -ARG MAVEN_VERSION=3.9.11 +ARG MAVEN_VERSION=3.9.12 ARG MAVEN_BASE_URL=https://dlcdn.apache.org/maven/maven-3 RUN curl -fsSL \ From eb88406fc59080f791e9cfa3a7ad005cb013ab21 Mon Sep 17 00:00:00 2001 From: Youssef Erradi Date: Wed, 4 Feb 2026 16:18:34 +0100 Subject: [PATCH 14/17] Enable keepAliveInterval once every 60 seconds --- .../database/mcptoolkit/OracleDatabaseMCPToolkit.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java index b69a679d..53c0ce77 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java @@ -28,6 +28,7 @@ import org.apache.tomcat.util.net.SSLHostConfigCertificate; import java.io.File; +import java.time.Duration; import java.util.concurrent.TimeUnit; import java.nio.file.FileSystems; import java.nio.file.Path; @@ -103,10 +104,11 @@ private OracleDatabaseMCPToolkit() { private static McpSyncServer startHttpServer() { try { HttpServletStreamableServerTransportProvider transport = - HttpServletStreamableServerTransportProvider.builder() - .objectMapper(new ObjectMapper()) - .mcpEndpoint("/mcp") - .build(); + HttpServletStreamableServerTransportProvider.builder() + .objectMapper(new ObjectMapper()) + .keepAliveInterval(Duration.ofSeconds(60)) + .mcpEndpoint("/mcp") + .build(); McpSyncServer server = McpServer .sync(transport) From cac52772347214c44c1d2522985c99e2628d307a Mon Sep 17 00:00:00 2001 From: Youssef Erradi Date: Thu, 5 Feb 2026 10:39:07 +0100 Subject: [PATCH 15/17] Change the MAVEN_BASE_URL arg to the archive.apache.org domain --- src/oracle-db-mcp-java-toolkit/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oracle-db-mcp-java-toolkit/Dockerfile b/src/oracle-db-mcp-java-toolkit/Dockerfile index 5e212faf..3871967e 100644 --- a/src/oracle-db-mcp-java-toolkit/Dockerfile +++ b/src/oracle-db-mcp-java-toolkit/Dockerfile @@ -2,7 +2,7 @@ FROM container-registry.oracle.com/java/openjdk:17 AS builder ARG MAVEN_VERSION=3.9.12 -ARG MAVEN_BASE_URL=https://dlcdn.apache.org/maven/maven-3 +ARG MAVEN_BASE_URL=https://archive.apache.org/dist/maven/maven-3 RUN curl -fsSL \ ${MAVEN_BASE_URL}/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \ From 303ddb899d6701a1a195129775dedfdb2759d2ee Mon Sep 17 00:00:00 2001 From: Youssef Erradi Date: Thu, 5 Feb 2026 16:10:10 +0100 Subject: [PATCH 16/17] Remove unused code and add missing JavaDoc --- .../mcptoolkit/OracleDatabaseMCPToolkit.java | 54 +++------------ .../mcptoolkit/config/ConfigRoot.java | 2 + .../mcptoolkit/config/ToolConfig.java | 11 +++ .../tools/DatabaseOperatorTools.java | 58 ++++++++++++---- .../mcptoolkit/tools/McpAdminTools.java | 69 +++++++++++++++++-- .../database/mcptoolkit/tools/RagTools.java | 30 ++++++-- 6 files changed, 152 insertions(+), 72 deletions(-) diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java index 53c0ce77..07ba5d26 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/OracleDatabaseMCPToolkit.java @@ -59,8 +59,6 @@ public class OracleDatabaseMCPToolkit { public static void main(String[] args) { installExternalExtensionsFromDir(); - McpSyncServer server; - switch (LoadedConstants.TRANSPORT_KIND) { case "http" -> { serverInstance = startHttpServer(); @@ -81,15 +79,7 @@ public static void main(String[] args) { } Utils.addSyncToolSpecifications(serverInstance, config); -// if (LoadedConstants.CONFIG_FILE != null) { -// Thread watcher = new Thread(() -> { -// watchConfigFile(LoadedConstants.CONFIG_FILE); -// }, "config-file-watcher"); -// watcher.setDaemon(true); -// watcher.start(); -// } - - Thread pollingThread = new Thread(() -> pollConfigFile(LoadedConstants.CONFIG_FILE), "config-file-poller"); + Thread pollingThread = new Thread(() -> pollConfigFile(), "config-file-poller"); pollingThread.setDaemon(true); pollingThread.start(); } @@ -151,7 +141,7 @@ private static McpSyncServer startHttpServer() { if (LoadedConstants.HTTPS_PORT == null || LoadedConstants.KEYSTORE_PATH == null || LoadedConstants.KEYSTORE_PASSWORD == null) throw new RuntimeException("SSL setup failed: HTTPS port, Keystore path or password not specified"); - enableHttps(tomcat, LoadedConstants.KEYSTORE_PATH, LoadedConstants.KEYSTORE_PASSWORD); + enableHttps(tomcat); tomcat.start(); @@ -166,11 +156,9 @@ private static McpSyncServer startHttpServer() { * Configures and enables HTTPS on the provided Tomcat server using the specified keystore. * * @param tomcat the Tomcat server instance to configure - * @param keystorePath the file path to the PKCS12 keystore containing the SSL certificate - * @param keystorePassword the password for the keystore * @throws RuntimeException if the HTTPS connector or SSL configuration fails */ - private static void enableHttps(Tomcat tomcat, String keystorePath, String keystorePassword) { + private static void enableHttps(Tomcat tomcat) { try { // Create HTTPS connector Connector https = new Connector("org.apache.coyote.http11.Http11NioProtocol"); @@ -188,8 +176,8 @@ private static void enableHttps(Tomcat tomcat, String keystorePath, String keyst SSLHostConfigCertificate.Type.RSA ); - cert.setCertificateKeystoreFile(keystorePath); - cert.setCertificateKeystorePassword(keystorePassword); + cert.setCertificateKeystoreFile(LoadedConstants.KEYSTORE_PATH); + cert.setCertificateKeystorePassword(LoadedConstants.KEYSTORE_PASSWORD); cert.setCertificateKeystoreType("PKCS12"); // Attach certificate to SSL config @@ -217,32 +205,6 @@ public static McpSyncServer getServer() { return serverInstance; } - - private static void watchConfigFile(String filePath) { - Path configPath = Paths.get(filePath); - try (WatchService watcher = FileSystems.getDefault().newWatchService()) { - Path dir = configPath.getParent(); - if (dir == null) dir = Paths.get("."); - dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY); - - while (true) { - WatchKey key = watcher.take(); // block until events - for (WatchEvent event : key.pollEvents()) { - Path changed = ((WatchEvent)event).context(); - LOG.info(()->"[DEBUG] Watch event: " + event.kind() + ", file: " + changed); - LOG.info(()->"[DEBUG] Looking for file: " + configPath.getFileName()); - if (changed.endsWith(configPath.getFileName())) { - LOG.info(()->"[DEBUG] Detected relevant config file event: " + event.kind()); - reloadConfigAndResetTools(); - } - } - key.reset(); - } - } catch (Exception e) { - System.err.println("[oracle-db-mcp-toolkit] Config file watcher failed: " + e); - } - } - private static void reloadConfigAndResetTools() { try { LOG.info(()->"[DEBUG] Reloading config..."); @@ -257,9 +219,9 @@ private static void reloadConfigAndResetTools() { } } - // For now, we rely on this instead of the nio watcher logic (for container sake) - private static void pollConfigFile(String filePath) { - File configFile = new File(filePath); + // For now, we rely on this instead of the nio watcher logic (for container’s sake) + private static void pollConfigFile() { + File configFile = new File(LoadedConstants.CONFIG_FILE); long lastModified = configFile.lastModified(); while (true) { long nowModified = configFile.lastModified(); diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java index 53bfbf28..3c4985d8 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ConfigRoot.java @@ -19,8 +19,10 @@ public class ConfigRoot { /** * Optional named toolsets allowing users to group custom tools and enable them with -Dtools. * Example YAML: + *
    * toolsets:
    *   reporting: [top_customers, sales_by_region]
+   * 
*/ public Map> toolsets; diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ToolConfig.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ToolConfig.java index 2e949e9f..658fcae3 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ToolConfig.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/config/ToolConfig.java @@ -61,6 +61,17 @@ public void substituteEnvVars() { } } + /** + * Builds a JSON representation of the input schema for this tool configuration. + *

+ * The input schema is constructed based on the tool's parameter configurations. + * Each parameter is represented as a property in the schema, with its type and description. + * Required parameters are listed in the "required" section of the schema. + *

+ * The resulting JSON string can be used to validate input data for the tool. + * + * @return a JSON string representing the input schema for this tool configuration + */ public String buildInputSchemaJson() { ObjectNode schema = JsonNodeFactory.instance.objectNode(); schema.put("type", "object"); diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java index bdcdfea5..7ca19edb 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/DatabaseOperatorTools.java @@ -21,20 +21,24 @@ import static com.oracle.database.mcptoolkit.Utils.*; /** - * Database operator tools: - * - read-query: execute SELECT queries and return JSON results - * - write-query: execute DML/DDL statements - * - create-table: create table. - * - delete-table: drop table by name - * - list-tables: list all tables and synonyms in the current schema - * - describe-table: get column details for a specific table - * - start-transaction: begin a new JDBC transaction - * - resume-transaction: verify a transaction ID is active - * - commit-transaction: commit and close a transaction - * - rollback-transaction: rollback and close a transaction - * - db-ping: check database connectivity and latency - * - db-metrics-range: retrieve Oracle performance metrics from V$SYSSTAT - * - explain-plan: generate and explain Oracle SQL execution plans (static or dynamic) + * Provides a set of database operator tools for various database operations. + * + *

The available tools are:

+ *
    + *
  • read-query: Execute SELECT queries and return JSON results.
  • + *
  • write-query: Execute DML/DDL statements.
  • + *
  • create-table: Create table.
  • + *
  • delete-table: Drop table by name.
  • + *
  • list-tables: List all tables and synonyms in the current schema.
  • + *
  • describe-table: Get column details for a specific table.
  • + *
  • start-transaction: Begin a new JDBC transaction.
  • + *
  • resume-transaction: Verify a transaction ID is active.
  • + *
  • commit-transaction: Commit and close a transaction.
  • + *
  • rollback-transaction: Rollback and close a transaction.
  • + *
  • db-ping: Check database connectivity and latency.
  • + *
  • db-metrics-range: Retrieve Oracle performance metrics from V$SYSSTAT.
  • + *
  • explain-plan: Generate and explain Oracle SQL execution plans (static or dynamic).
  • + *
*/ public final class DatabaseOperatorTools { @@ -44,7 +48,31 @@ public final class DatabaseOperatorTools { private DatabaseOperatorTools() {} /** - * Returns all database operator tool specifications. + * Returns a list of database operator tool specifications based on the provided server configuration. + * The returned tools are used for various database operations such as executing queries, managing transactions, + * and retrieving database metrics. + * + *

The tools returned include:

+ *
    + *
  • read-query: Execute SELECT queries and return JSON results.
  • + *
  • write-query: Execute DML/DDL statements.
  • + *
  • create-table: Create table.
  • + *
  • delete-table: Drop table by name.
  • + *
  • list-tables: List all tables and synonyms in the current schema.
  • + *
  • describe-table: Get column details for a specific table.
  • + *
  • start-transaction: Begin a new JDBC transaction.
  • + *
  • resume-transaction: Verify a transaction ID is active.
  • + *
  • commit-transaction: Commit and close a transaction.
  • + *
  • rollback-transaction: Rollback and close a transaction.
  • + *
  • db-ping: Check database connectivity and latency.
  • + *
  • db-metrics-range: Retrieve Oracle performance metrics from V$SYSSTAT.
  • + *
  • explain-plan: Generate and explain Oracle SQL execution plans (static or dynamic).
  • + *
+ * + *

A shutdown hook is added to clean up any active transactions when the JVM shuts down.

+ * + * @param config the server configuration to use for database connections + * @return a list of tool specifications for database operations */ public static List getTools(ServerConfig config) { List tools = new ArrayList<>(); diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java index f5f4a6f8..cd39a7b5 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/McpAdminTools.java @@ -24,26 +24,52 @@ import java.util.*; /** - * Admin/maintenance tools: - * - list-tools: list all available tools with descriptions - * - edit-tools: upsert a YAML-defined tool in the config file and rely on runtime reload + * Provides a set of admin/maintenance tools for various operations. + * + *

The available tools are:

+ *
    + *
  • list-tools: List all available tools with descriptions.
  • + *
  • edit-tools: Upsert a YAML-defined tool in the config file and rely on runtime reload.
  • + *
*/ public class McpAdminTools { private McpAdminTools() {} /** - * Returns all MCP admin tool specifications. + * Returns a list of all MCP admin tool specifications, including "list-tools" and "edit-tools". + * The returned tools are filtered based on the configuration provided. + * + * @param config the server configuration used to filter the tools + * @return a list of MCP admin tool specifications */ public static List getTools(ServerConfig config) { List tools = new ArrayList<>(); tools.add(getListToolsTool(config)); - tools.add(getEditToolsTool(config)); + tools.add(getEditToolsTool()); return tools; } + /** + * Returns a tool specification for the "list-tools" tool, which lists all available tools + * with their descriptions. The tool respects the tools filter configuration. + * + *

The tool returns a list of tools, including:

+ *
    + *
  • Built-in log analyzer tools
  • + *
  • RAG tools
  • + *
  • Database operator tools
  • + *
  • Custom YAML tools
  • + *
  • MCP admin tools
  • + *
+ * + *

The tool's output is filtered based on the {@link ServerConfig#toolsFilter} configuration.

+ * + * @param config the server configuration used to filter the tools + * @return a tool specification for the "list-tools" tool + */ public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerConfig config) { return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() @@ -132,7 +158,38 @@ public static McpServerFeatures.SyncToolSpecification getListToolsTool(ServerCon .build(); } - public static McpServerFeatures.SyncToolSpecification getEditToolsTool(ServerConfig config) { + /** + * Returns a tool specification for the "edit-tools" tool, which creates or updates + * YAML-defined tools in the configuration file. The changes are auto-reloaded. + * + *

The tool accepts the following input parameters:

+ *
    + *
  • name: The name of the tool to create or update (required).
  • + *
  • remove: A boolean flag indicating whether to remove the tool (optional).
  • + *
  • description: A description of the tool (optional).
  • + *
  • dataSource: The data source for the tool (optional).
  • + *
  • statement: The SQL statement for the tool (optional).
  • + *
  • parameters: A list of parameter objects for the tool (optional). + * Each parameter object can have the following properties: + *
      + *
    • name: The name of the parameter.
    • + *
    • type: The data type of the parameter.
    • + *
    • description: A description of the parameter.
    • + *
    • required: A boolean indicating whether the parameter is required.
    • + *
    + *
  • + *
+ * + *

The tool returns a result with the following properties:

+ *
    + *
  • status: The status of the operation (e.g., "ok", "noop", "error").
  • + *
  • message: A human-readable message describing the result.
  • + *
  • configFile: The path to the configuration file.
  • + *
+ * + * @return a tool specification for the "edit-tools" tool + */ + public static McpServerFeatures.SyncToolSpecification getEditToolsTool() { return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() .name("edit-tools") diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java index 34e228a4..91ca2ef8 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java @@ -40,11 +40,17 @@ public class RagTools { private RagTools() {} /** - * Returns all RAG tool specifications. + * Returns a list of all RAG tool specifications based on the provided server configuration. + *

+ * The returned list includes tool specifications for RAG applications, such as similarity search + * using vector embeddings. The tools are filtered based on the configuration settings. + * + * @param config the server configuration to use for determining which tools to include + * @return a list of tool specifications for RAG tools */ public static List getTools(ServerConfig config) { List tools = new ArrayList<>(); - tools.add(getSymilaritySearchTool(config)); + tools.add(getSimilaritySearchTool(config)); return tools; } @@ -52,11 +58,25 @@ public static List getTools(ServerConfi * Returns a tool specification for the "similarity_search" tool. *

* This tool allows users to perform similarity searches using vector embeddings. + * The tool's behavior is configured based on the provided server configuration. + *

+ * The tool accepts the following input arguments: + *

    + *
  • {@code question}: the natural-language query text (required, non-blank)
  • + *
  • {@code topK}: the maximum number of rows to return (optional, default=5, clamped to [1, 100])
  • + *
  • {@code table}: the table name containing text + embedding columns (optional, default="profile_oracle")
  • + *
  • {@code dataColumn}: the column holding the text/CLOB to return (optional, default="text")
  • + *
  • {@code embeddingColumn}: the vector column used by the similarity function (optional, default="embedding")
  • + *
  • {@code modelName}: the database vector model used to embed the question (optional, default="doc_model")
  • + *
  • {@code textFetchLimit}: the substring length to return from the text column (optional, default=4000)
  • + *
+ *

+ * The tool returns a list of text snippets ranked by similarity, along with a structured content map containing the results. * - * @param config server configuration - * @return tool specification + * @param config the server configuration to use for determining the tool's behavior + * @return a tool specification for the "similarity_search" tool */ - public static McpServerFeatures.SyncToolSpecification getSymilaritySearchTool(ServerConfig config) { + public static McpServerFeatures.SyncToolSpecification getSimilaritySearchTool(ServerConfig config) { return McpServerFeatures.SyncToolSpecification.builder() .tool(McpSchema.Tool.builder() .name("similarity-search") From f3590e9fa62fdccfae69443db4c2bd780bd3510a Mon Sep 17 00:00:00 2001 From: Youssef Erradi Date: Thu, 5 Feb 2026 23:31:40 +0100 Subject: [PATCH 17/17] fix similarity search tool name in the Javadoc --- .../java/com/oracle/database/mcptoolkit/tools/RagTools.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java index 91ca2ef8..9c615dea 100644 --- a/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java +++ b/src/oracle-db-mcp-java-toolkit/src/main/java/com/oracle/database/mcptoolkit/tools/RagTools.java @@ -55,7 +55,7 @@ public static List getTools(ServerConfi } /** - * Returns a tool specification for the "similarity_search" tool. + * Returns a tool specification for the {@code similarity-search} tool. *

* This tool allows users to perform similarity searches using vector embeddings. * The tool's behavior is configured based on the provided server configuration. @@ -74,7 +74,7 @@ public static List getTools(ServerConfi * The tool returns a list of text snippets ranked by similarity, along with a structured content map containing the results. * * @param config the server configuration to use for determining the tool's behavior - * @return a tool specification for the "similarity_search" tool + * @return a tool specification for the {@code similarity-search} tool */ public static McpServerFeatures.SyncToolSpecification getSimilaritySearchTool(ServerConfig config) { return McpServerFeatures.SyncToolSpecification.builder()