diff --git a/README.md b/README.md index 2420857..03d4978 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,37 @@ HTTP `500` uses `INTERNAL_ERROR`. `DDExternalLicenseService` is also implemented by the LM controller. Transport resources belong to the client proxy and are released through `RestClient.close(client)`; the shared service interface has no lifecycle methods. + +## MCP function API + +`apis/com.dbeaver.datadam.mcp.api` exports `DDMcpFunctionService` for desktop tool discovery and execution. +`getFunctions()` returns descriptors with `id`, display `name`, optional `description` and `List parameters`. +Each parameter has `name`, `type` (`STRING`, `INTEGER`, `NUMBER`, `BOOLEAN`), optional `description`, `required`, +optional `defaultValue` and optional `validValues`. Defaults and allowed values are strings interpreted according to `type`. +The MCP server builds each tool's JSON Schema `inputSchema` from these parameters. +`callFunction(DDMcpFunctionCall)` accepts `functionId` and `Map arguments`, and returns a JSON value +and an `error` flag; execution errors use `error=true`. +The MCP server maps object results to `structuredContent` and the flag to `isError`. +Function IDs are unique and match `[a-zA-Z0-9_.-]{1,128}`; display names are nonblank and parameter names are nonblank and unique. +Results use `JsonNull.INSTANCE` for JSON null, never a Java null. + +Register the desktop implementation on its ready Remote DAM RPC endpoint: + +```java +var codec = new DDRemoteDamGsonRpcCodec(new GsonBuilder() + .serializeNulls() + .setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL) + .create()); +var registration = rpc.register(DDMcpFunctionService.SERVICE_ID, DDMcpFunctionService.class, implementation, codec); +``` + +The shared topic is `dbeaver.mcp.functions`. Retain the registration until the implementation is unavailable, +then close it. Both peers must use the renamed MCP contract; Java argument type names are part of RPC operation IDs. +Configure `serializeNulls()` and `ToNumberPolicy.BIG_DECIMAL` on both peers to preserve explicit null entries +and numeric precision in argument maps, including nested maps and lists. Gson's default `Object` number handling uses `Double`. + +The desktop exposes only functions allowed for `MCP`/`ALL`, rechecks permissions and arguments on every call, +and enforces enabled connections and result limits. Database credentials remain on the desktop. +Remote DAM transports thrown exceptions as RPC diagnostics, not as reconstructed `DDMcpFunctionException` objects. +MCP returns a generic error for remote handler failures without exposing diagnostic stack traces. +Timeout or interruption does not guarantee remote cancellation or SQL rollback; calls are never replayed automatically. diff --git a/apis/com.dbeaver.datadam.mcp.api/META-INF/MANIFEST.MF b/apis/com.dbeaver.datadam.mcp.api/META-INF/MANIFEST.MF new file mode 100644 index 0000000..53bea63 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: DBeaver Corp +Bundle-Name: DataDam MCP API +Bundle-SymbolicName: com.dbeaver.datadam.mcp.api +Bundle-Version: 0.0.17.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Export-Package: com.dbeaver.datadam.mcp.api.exception, + com.dbeaver.datadam.mcp.api.model, + com.dbeaver.datadam.mcp.api.service +Bundle-ActivationPolicy: lazy +Require-Bundle: org.jkiss.utils +Import-Package: com.google.gson +Automatic-Module-Name: com.dbeaver.datadam.mcp.api diff --git a/apis/com.dbeaver.datadam.mcp.api/build.properties b/apis/com.dbeaver.datadam.mcp.api/build.properties new file mode 100644 index 0000000..f0239d7 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java +output.. = target/classes/ +bin.includes = ., + META-INF/ diff --git a/apis/com.dbeaver.datadam.mcp.api/pom.xml b/apis/com.dbeaver.datadam.mcp.api/pom.xml new file mode 100644 index 0000000..63a4bb6 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.dbeaver.datadam + api + 0.0.17-SNAPSHOT + ../pom.xml + + + com.dbeaver.datadam.mcp.api + eclipse-plugin + DataDam MCP API + + + + com.dbeaver.common + org.jkiss.utils + + + com.google.code.gson + gson + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/test/java + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + test + + test + + + + + + + + diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java new file mode 100644 index 0000000..4c5a0e9 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.exception; + +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionErrorCode; +import com.google.gson.JsonObject; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +public class DDMcpFunctionException extends Exception { + private final DDMcpFunctionErrorCode code; + @Nullable + private final JsonObject data; + + public DDMcpFunctionException(@NotNull DDMcpFunctionErrorCode code, @NotNull String message, @Nullable JsonObject data) { + super(message); + this.code = code; + this.data = data; + } + + @NotNull + public DDMcpFunctionErrorCode getCode() { + return code; + } + + @Nullable + public JsonObject getData() { + return data; + } +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java new file mode 100644 index 0000000..b7f6ff3 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.model; + +import org.jkiss.code.NotNull; + +import java.util.Map; + +public record DDMcpFunctionCall(@NotNull String functionId, @NotNull Map arguments) { +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java new file mode 100644 index 0000000..49aeb06 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java @@ -0,0 +1,30 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.model; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +import java.util.List; + +public record DDMcpFunctionDescriptor( + @NotNull String id, + @NotNull String name, + @Nullable String description, + @NotNull List parameters +) { +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java new file mode 100644 index 0000000..1e40bb2 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.model; + +public enum DDMcpFunctionErrorCode { + FUNCTION_NOT_FOUND, + INVALID_ARGUMENTS, + ACCESS_DENIED, + INTERNAL_ERROR +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionParameter.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionParameter.java new file mode 100644 index 0000000..af70c7b --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionParameter.java @@ -0,0 +1,33 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.model; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +import java.util.List; + +public record DDMcpFunctionParameter( + @NotNull String name, + @NotNull Type type, + @Nullable String description, + boolean required, + @Nullable String defaultValue, + @Nullable List validValues +) { + public enum Type { STRING, INTEGER, NUMBER, BOOLEAN } +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java new file mode 100644 index 0000000..4d2e013 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java @@ -0,0 +1,23 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.model; + +import com.google.gson.JsonElement; +import org.jkiss.code.NotNull; + +public record DDMcpFunctionResult(@NotNull JsonElement value, boolean error) { +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java new file mode 100644 index 0000000..84eceb8 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api.service; + +import com.dbeaver.datadam.mcp.api.exception.DDMcpFunctionException; +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionCall; +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionDescriptor; +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionResult; +import org.jkiss.code.NotNull; + +import java.util.List; + +/** + * Remotely available MCP functions of one authenticated desktop. + */ +public interface DDMcpFunctionService { + String SERVICE_ID = "dbeaver.mcp.functions"; + + @NotNull + List getFunctions() throws DDMcpFunctionException; + + @NotNull + DDMcpFunctionResult callFunction(@NotNull DDMcpFunctionCall call) throws DDMcpFunctionException; +} diff --git a/apis/com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java b/apis/com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java new file mode 100644 index 0000000..bfbca55 --- /dev/null +++ b/apis/com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java @@ -0,0 +1,74 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package com.dbeaver.datadam.mcp.api; + +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionCall; +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionDescriptor; +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionParameter; +import com.dbeaver.datadam.mcp.api.model.DDMcpFunctionResult; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; +import com.google.gson.ToNumberPolicy; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +class DDMcpFunctionContractTest { + private final Gson gson = new GsonBuilder().serializeNulls().setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL).create(); + + @Test + void preservesTypedParametersAndOptionalDescription() { + var descriptor = new DDMcpFunctionDescriptor("database_query", "Query", null, List.of( + new DDMcpFunctionParameter("query", DDMcpFunctionParameter.Type.STRING, "SQL query", true, null, null), + new DDMcpFunctionParameter("limit", DDMcpFunctionParameter.Type.INTEGER, null, false, "100", List.of("10", "100")), + new DDMcpFunctionParameter("ratio", DDMcpFunctionParameter.Type.NUMBER, null, false, "0.5", null), + new DDMcpFunctionParameter("readOnly", DDMcpFunctionParameter.Type.BOOLEAN, null, false, "true", null) + )); + Assertions.assertEquals(descriptor, gson.fromJson(gson.toJson(descriptor), DDMcpFunctionDescriptor.class)); + } + + @Test + void preservesMapArgumentsWithoutLosingNullsOrNumericPrecision() { + Map arguments = new LinkedHashMap<>(); + arguments.put("id", new BigInteger("9007199254740993")); + arguments.put("amount", new BigDecimal("0.1234567890123456789")); + arguments.put("value", null); + arguments.put("nested", Map.of("items", Arrays.asList(null, true, "text", new BigInteger("9223372036854775808")))); + var call = new DDMcpFunctionCall("database_query", arguments); + var decoded = gson.fromJson(gson.toJson(call), DDMcpFunctionCall.class); + Assertions.assertEquals(call.functionId(), decoded.functionId()); + Assertions.assertEquals(new BigDecimal("9007199254740993"), decoded.arguments().get("id")); + Assertions.assertEquals(new BigDecimal("0.1234567890123456789"), decoded.arguments().get("amount")); + Assertions.assertTrue(decoded.arguments().containsKey("value")); + Assertions.assertNull(decoded.arguments().get("value")); + Map nested = Assertions.assertInstanceOf(Map.class, decoded.arguments().get("nested")); + Assertions.assertEquals(Arrays.asList(null, true, "text", new BigDecimal("9223372036854775808")), nested.get("items")); + } + + @Test + void preservesExecutionFailureAndStructuredResult() { + var result = new DDMcpFunctionResult(JsonParser.parseString("{\"message\":\"Permission denied\",\"rows\":[]}"), true); + Assertions.assertEquals(result, gson.fromJson(gson.toJson(result), DDMcpFunctionResult.class)); + } +} diff --git a/apis/pom.xml b/apis/pom.xml index 4b60748..a379241 100644 --- a/apis/pom.xml +++ b/apis/pom.xml @@ -19,6 +19,7 @@ DataDam API + com.dbeaver.datadam.mcp.api com.dbeaver.datadam.dm.api com.dbeaver.datadam.dam.remote.api com.dbeaver.datadam.share.api