From 9424e1338c40fd86f52a476e7574abacd063d754 Mon Sep 17 00:00:00 2001 From: Tsimafei Labanovich Date: Wed, 21 Oct 2026 14:54:57 +0200 Subject: [PATCH 1/5] dbeaver/pro#11120 Add shared AI function service contract --- README.md | 91 +++++++++++++++++++ .../META-INF/MANIFEST.MF | 14 +++ .../build.properties | 4 + apis/com.dbeaver.datadam.ai.api/pom.xml | 59 ++++++++++++ .../api/exception/DDAiFunctionException.java | 44 +++++++++ .../ai/api/model/DDAiFunctionCall.java | 23 +++++ .../ai/api/model/DDAiFunctionDescriptor.java | 29 ++++++ .../ai/api/model/DDAiFunctionErrorCode.java | 24 +++++ .../ai/api/model/DDAiFunctionResult.java | 23 +++++ .../ai/api/service/DDAiFunctionService.java | 36 ++++++++ .../ai/api/DDAiFunctionContractTest.java | 57 ++++++++++++ apis/pom.xml | 1 + 12 files changed, 405 insertions(+) create mode 100644 apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF create mode 100644 apis/com.dbeaver.datadam.ai.api/build.properties create mode 100644 apis/com.dbeaver.datadam.ai.api/pom.xml create mode 100644 apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java create mode 100644 apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java create mode 100644 apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java create mode 100644 apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java create mode 100644 apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java create mode 100644 apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java create mode 100644 apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java diff --git a/README.md b/README.md index d3ecb3e..49e7357 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,94 @@ 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. + +## AI function API + +`apis/com.dbeaver.datadam.ai.api` is the shared contract for remotely available desktop AI functions. +It is an OSGi bundle usable from DBeaver and a Maven dependency usable from DataDam Account. +It depends on shared annotations and Gson, not on Spring, desktop UI or the DBeaver AI registry. + +The desktop implements `com.dbeaver.datadam.ai.api.service.DDAiFunctionService`: + +```java +List getFunctions() throws DDAiFunctionException; +DDAiFunctionResult callFunction(DDAiFunctionCall call) throws DDAiFunctionException; +``` + +Both methods are synchronous. The transport supplies a proxy bound to an authorized desktop, +dispatches the call and correlates its response. User/account identity, desktop/connection IDs, +request IDs, topics, deadlines and WebSocket envelopes are not service arguments. Implementations +must not construct JSON-RPC replies or manage transport listeners. + +### DTOs + +| DTO | Fields | Meaning | +| --- | --- | --- | +| `DDAiFunctionDescriptor` | `id`, `name`, nullable `description`, `inputSchema` | Stable callable ID, display name, AI description and full JSON Schema object for arguments. | +| `DDAiFunctionCall` | `functionId`, `arguments` | The exact descriptor ID and an object containing named arguments. Use an empty object when there are none. | +| `DDAiFunctionResult` | `value`, `error` | JSON result value and an execution-failure flag. | +| `DDAiFunctionErrorCode` | `FUNCTION_NOT_FOUND`, `INVALID_ARGUMENTS`, `ACCESS_DENIED`, `INTERNAL_ERROR` | Service-level errors represented by `DDAiFunctionException`. | + +Function IDs must be unique within a desktop and use 1–128 ASCII letters, digits, underscores, +dots or hyphens. Use the registry's full function ID (including its toolbox prefix) where applicable; +do not use the human-readable name or legacy ID as the callable identifier. Names must be nonblank. +`inputSchema` must declare `"type":"object"` and can contain nested schemas, required properties, +enums and defaults. There is no separate parameter-description DTO: the JSON Schema preserves +these definitions without duplicating the schema language. `getFunctions()` returns the complete +list; this version does not paginate. + +Example descriptor and invocation payloads (not transport envelopes): + +```json +{ + "id": "database_listTableNames", + "name": "List tables", + "description": "List tables in a selected schema", + "inputSchema": { + "type": "object", + "properties": {"schema": {"type": "string"}}, + "required": ["schema"] + } +} +``` + +```json +{"functionId":"database_listTableNames","arguments":{"schema":"public"}} +``` + +`value` supports strings, objects, arrays, numbers, booleans and explicit JSON null (`JsonNull.INSTANCE`); +it must not be a Java null. For example, `{"value":{"tables":["orders"]},"error":false}` is a structured +success, and `{"value":"SQL execution failed","error":true}` is a tool execution failure. +Convert desktop-specific objects to JSON values; never transfer callbacks, UI objects or Java throwables. + +`DDAiFunctionException(code, message, data)` represents rejection of the service call, such as an unknown +function or invalid arguments. `data` is an optional JSON object. The transport must preserve these +explicit error fields rather than serializing Java exception internals. Messages and data are exposed +to the AI caller. Database execution failures are returned as `DDAiFunctionResult` with `error=true`. + +JSON serialization must preserve explicit nulls and numeric precision. When using Gson, use +`new GsonBuilder().serializeNulls().create()` and retain `JsonObject`/`JsonElement` values instead of +round-tripping through `Map` with double-valued numbers. Omit unknown optional fields +or ignore them when reading; never silently drop argument fields whose value is JSON null. + +### Desktop responsibilities + +Return only functions with an allowed remote purpose (`MCP` or `ALL` in the current DBeaver registry), +subject to local permissions and context. Recheck eligibility and arguments in `callFunction()`; +a caller can request an ID without first fetching the list. Do not use a denylist of UI functions. +Only explicitly enabled database connections may be accessed. Keep existing AI restrictions and +result limits. The API carries neither database credentials nor connection configurations. + +### Account integration and outstanding transport work + +Account maps descriptor `id` to MCP tool `name`, display `name` to `title`, and forwards the schema. +String results become text directly; other JSON values become JSON text. Object values are also +returned as MCP `structuredContent`; `error` maps to `isError`. Service exceptions map to JSON-RPC +errors: unknown function/invalid arguments → `-32602`, access denied → `-32001`, internal error → `-32603`. + +Account's `DDMcpClientProvider` is the integration seam for the future External DAM service proxy. +The desktop team can implement this shared interface and test against stubs before that proxy is ready. +There is no production External DAM adapter yet. The transport wire format, routing identity and +remote cancellation API still need agreement. A thread interruption or an MCP timeout does not by +itself guarantee cancellation or rollback of a remote SQL operation. Never automatically replay SQL +when delivery or execution outcome is unknown. diff --git a/apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF b/apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF new file mode 100644 index 0000000..7e608cd --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: DBeaver Corp +Bundle-Name: DataDam AI API +Bundle-SymbolicName: com.dbeaver.datadam.ai.api +Bundle-Version: 0.0.17.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Export-Package: com.dbeaver.datadam.ai.api.exception, + com.dbeaver.datadam.ai.api.model, + com.dbeaver.datadam.ai.api.service +Bundle-ActivationPolicy: lazy +Require-Bundle: org.jkiss.utils +Import-Package: com.google.gson +Automatic-Module-Name: com.dbeaver.datadam.ai.api diff --git a/apis/com.dbeaver.datadam.ai.api/build.properties b/apis/com.dbeaver.datadam.ai.api/build.properties new file mode 100644 index 0000000..f0239d7 --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.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.ai.api/pom.xml b/apis/com.dbeaver.datadam.ai.api/pom.xml new file mode 100644 index 0000000..285cb03 --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.dbeaver.datadam + api + 0.0.17-SNAPSHOT + ../pom.xml + + + com.dbeaver.datadam.ai.api + eclipse-plugin + DataDam AI 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.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java new file mode 100644 index 0000000..4fe3408 --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.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.ai.api.exception; + +import com.dbeaver.datadam.ai.api.model.DDAiFunctionErrorCode; +import com.google.gson.JsonObject; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +public class DDAiFunctionException extends Exception { + private final DDAiFunctionErrorCode code; + @Nullable + private final JsonObject data; + + public DDAiFunctionException(@NotNull DDAiFunctionErrorCode code, @NotNull String message, @Nullable JsonObject data) { + super(message); + this.code = code; + this.data = data; + } + + @NotNull + public DDAiFunctionErrorCode getCode() { + return code; + } + + @Nullable + public JsonObject getData() { + return data; + } +} diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java new file mode 100644 index 0000000..ac058d9 --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.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.ai.api.model; + +import com.google.gson.JsonObject; +import org.jkiss.code.NotNull; + +public record DDAiFunctionCall(@NotNull String functionId, @NotNull JsonObject arguments) { +} diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java new file mode 100644 index 0000000..3febe1a --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java @@ -0,0 +1,29 @@ +/* + * 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.ai.api.model; + +import com.google.gson.JsonObject; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +public record DDAiFunctionDescriptor( + @NotNull String id, + @NotNull String name, + @Nullable String description, + @NotNull JsonObject inputSchema +) { +} diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java new file mode 100644 index 0000000..c3f556a --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.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.ai.api.model; + +public enum DDAiFunctionErrorCode { + FUNCTION_NOT_FOUND, + INVALID_ARGUMENTS, + ACCESS_DENIED, + INTERNAL_ERROR +} diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java new file mode 100644 index 0000000..2004a90 --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.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.ai.api.model; + +import com.google.gson.JsonElement; +import org.jkiss.code.NotNull; + +public record DDAiFunctionResult(@NotNull JsonElement value, boolean error) { +} diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java new file mode 100644 index 0000000..def04d6 --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java @@ -0,0 +1,36 @@ +/* + * 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.ai.api.service; + +import com.dbeaver.datadam.ai.api.exception.DDAiFunctionException; +import com.dbeaver.datadam.ai.api.model.DDAiFunctionCall; +import com.dbeaver.datadam.ai.api.model.DDAiFunctionDescriptor; +import com.dbeaver.datadam.ai.api.model.DDAiFunctionResult; +import org.jkiss.code.NotNull; + +import java.util.List; + +/** + * Remotely available AI functions of one authenticated desktop. + */ +public interface DDAiFunctionService { + @NotNull + List getFunctions() throws DDAiFunctionException; + + @NotNull + DDAiFunctionResult callFunction(@NotNull DDAiFunctionCall call) throws DDAiFunctionException; +} diff --git a/apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java b/apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java new file mode 100644 index 0000000..759f9ef --- /dev/null +++ b/apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java @@ -0,0 +1,57 @@ +/* + * 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.ai.api; + +import com.dbeaver.datadam.ai.api.model.DDAiFunctionCall; +import com.dbeaver.datadam.ai.api.model.DDAiFunctionDescriptor; +import com.dbeaver.datadam.ai.api.model.DDAiFunctionResult; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DDAiFunctionContractTest { + private final Gson gson = new GsonBuilder().serializeNulls().create(); + + @Test + void preservesNestedSchemaAndOptionalDescription() { + var schema = JsonParser.parseString(""" + {"type":"object","properties":{"filter":{"type":"object","properties":{"ids":{"type":"array", + "items":{"type":"integer"}}}}},"required":["filter"],"additionalProperties":false} + """).getAsJsonObject(); + var descriptor = new DDAiFunctionDescriptor("database_query", "Query", null, schema); + Assertions.assertEquals(descriptor, gson.fromJson(gson.toJson(descriptor), DDAiFunctionDescriptor.class)); + } + + @Test + void preservesNumericArgumentsWithoutDoubleConversion() { + var arguments = JsonParser.parseString("{\"id\":9007199254740993,\"amount\":0.1234567890123456789,\"value\":null}") + .getAsJsonObject(); + var call = new DDAiFunctionCall("database_query", arguments); + var decoded = gson.fromJson(gson.toJson(call), DDAiFunctionCall.class); + Assertions.assertEquals("9007199254740993", decoded.arguments().get("id").getAsString()); + Assertions.assertEquals("0.1234567890123456789", decoded.arguments().get("amount").getAsString()); + Assertions.assertTrue(decoded.arguments().get("value").isJsonNull()); + } + + @Test + void preservesExecutionFailureAndStructuredResult() { + var result = new DDAiFunctionResult(JsonParser.parseString("{\"message\":\"Permission denied\",\"rows\":[]}"), true); + Assertions.assertEquals(result, gson.fromJson(gson.toJson(result), DDAiFunctionResult.class)); + } +} diff --git a/apis/pom.xml b/apis/pom.xml index d559603..e05fcc5 100644 --- a/apis/pom.xml +++ b/apis/pom.xml @@ -19,6 +19,7 @@ DataDam API + com.dbeaver.datadam.ai.api com.dbeaver.datadam.dm.api com.dbeaver.datadam.share.api com.dbeaver.datadam.lm.api From a3a1f027466023655edaa25c279c119b537eaada Mon Sep 17 00:00:00 2001 From: Tsimafei Labanovich Date: Tue, 27 Oct 2026 14:32:59 +0100 Subject: [PATCH 2/5] dbeaver/pro#11120 Rename desktop function contract to MCP --- README.md | 101 ++++-------------- .../META-INF/MANIFEST.MF | 14 --- .../META-INF/MANIFEST.MF | 14 +++ .../build.properties | 0 .../pom.xml | 4 +- .../exception/DDMcpFunctionException.java} | 12 +-- .../mcp/api/model/DDMcpFunctionCall.java} | 4 +- .../api/model/DDMcpFunctionDescriptor.java} | 4 +- .../api/model/DDMcpFunctionErrorCode.java} | 4 +- .../mcp/api/model/DDMcpFunctionResult.java} | 4 +- .../api/service/DDMcpFunctionService.java} | 20 ++-- .../mcp/api/DDMcpFunctionContractTest.java} | 22 ++-- apis/pom.xml | 2 +- 13 files changed, 71 insertions(+), 134 deletions(-) delete mode 100644 apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF create mode 100644 apis/com.dbeaver.datadam.mcp.api/META-INF/MANIFEST.MF rename apis/{com.dbeaver.datadam.ai.api => com.dbeaver.datadam.mcp.api}/build.properties (100%) rename apis/{com.dbeaver.datadam.ai.api => com.dbeaver.datadam.mcp.api}/pom.xml (95%) rename apis/{com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java => com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java} (71%) rename apis/{com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java => com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java} (83%) rename apis/{com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java => com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java} (91%) rename apis/{com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java => com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java} (90%) rename apis/{com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java => com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java} (85%) rename apis/{com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java => com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java} (53%) rename apis/{com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java => com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java} (73%) diff --git a/README.md b/README.md index 4caf42f..515d9cc 100644 --- a/README.md +++ b/README.md @@ -140,93 +140,28 @@ HTTP `500` uses `INTERNAL_ERROR`. resources belong to the client proxy and are released through `RestClient.close(client)`; the shared service interface has no lifecycle methods. -## AI function API +## MCP function API -`apis/com.dbeaver.datadam.ai.api` is the shared contract for remotely available desktop AI functions. -It is an OSGi bundle usable from DBeaver and a Maven dependency usable from DataDam Account. -It depends on shared annotations and Gson, not on Spring, desktop UI or the DBeaver AI registry. +`apis/com.dbeaver.datadam.mcp.api` exports `DDMcpFunctionService` for desktop tool discovery and execution. +`getFunctions()` returns descriptors with `id`, display `name`, optional `description` and JSON Schema `inputSchema`. +`callFunction(DDMcpFunctionCall)` returns a JSON value and an `error` flag; execution errors use `error=true`. +The MCP server maps descriptors to tools, object results to `structuredContent`, and the flag to `isError`. +Function IDs are unique and match `[a-zA-Z0-9_.-]{1,128}`; names are nonblank and schemas declare `"type":"object"`. +Results use `JsonNull.INSTANCE` for JSON null, never a Java null. -The desktop implements `com.dbeaver.datadam.ai.api.service.DDAiFunctionService`: +Register the desktop implementation on its ready Remote DAM RPC endpoint: ```java -List getFunctions() throws DDAiFunctionException; -DDAiFunctionResult callFunction(DDAiFunctionCall call) throws DDAiFunctionException; +var codec = new DDRemoteDamGsonRpcCodec(new GsonBuilder().serializeNulls().create()); +var registration = rpc.register(DDMcpFunctionService.SERVICE_ID, DDMcpFunctionService.class, implementation, codec); ``` -Both methods are synchronous. The transport supplies a proxy bound to an authorized desktop, -dispatches the call and correlates its response. User/account identity, desktop/connection IDs, -request IDs, topics, deadlines and WebSocket envelopes are not service arguments. Implementations -must not construct JSON-RPC replies or manage transport listeners. - -### DTOs - -| DTO | Fields | Meaning | -| --- | --- | --- | -| `DDAiFunctionDescriptor` | `id`, `name`, nullable `description`, `inputSchema` | Stable callable ID, display name, AI description and full JSON Schema object for arguments. | -| `DDAiFunctionCall` | `functionId`, `arguments` | The exact descriptor ID and an object containing named arguments. Use an empty object when there are none. | -| `DDAiFunctionResult` | `value`, `error` | JSON result value and an execution-failure flag. | -| `DDAiFunctionErrorCode` | `FUNCTION_NOT_FOUND`, `INVALID_ARGUMENTS`, `ACCESS_DENIED`, `INTERNAL_ERROR` | Service-level errors represented by `DDAiFunctionException`. | - -Function IDs must be unique within a desktop and use 1–128 ASCII letters, digits, underscores, -dots or hyphens. Use the registry's full function ID (including its toolbox prefix) where applicable; -do not use the human-readable name or legacy ID as the callable identifier. Names must be nonblank. -`inputSchema` must declare `"type":"object"` and can contain nested schemas, required properties, -enums and defaults. There is no separate parameter-description DTO: the JSON Schema preserves -these definitions without duplicating the schema language. `getFunctions()` returns the complete -list; this version does not paginate. - -Example descriptor and invocation payloads (not transport envelopes): - -```json -{ - "id": "database_listTableNames", - "name": "List tables", - "description": "List tables in a selected schema", - "inputSchema": { - "type": "object", - "properties": {"schema": {"type": "string"}}, - "required": ["schema"] - } -} -``` - -```json -{"functionId":"database_listTableNames","arguments":{"schema":"public"}} -``` +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. +Use `JsonObject`/`JsonElement` and `serializeNulls()` on both peers to preserve explicit nulls and numeric precision. -`value` supports strings, objects, arrays, numbers, booleans and explicit JSON null (`JsonNull.INSTANCE`); -it must not be a Java null. For example, `{"value":{"tables":["orders"]},"error":false}` is a structured -success, and `{"value":"SQL execution failed","error":true}` is a tool execution failure. -Convert desktop-specific objects to JSON values; never transfer callbacks, UI objects or Java throwables. - -`DDAiFunctionException(code, message, data)` represents rejection of the service call, such as an unknown -function or invalid arguments. `data` is an optional JSON object. The transport must preserve these -explicit error fields rather than serializing Java exception internals. Messages and data are exposed -to the AI caller. Database execution failures are returned as `DDAiFunctionResult` with `error=true`. - -JSON serialization must preserve explicit nulls and numeric precision. When using Gson, use -`new GsonBuilder().serializeNulls().create()` and retain `JsonObject`/`JsonElement` values instead of -round-tripping through `Map` with double-valued numbers. Omit unknown optional fields -or ignore them when reading; never silently drop argument fields whose value is JSON null. - -### Desktop responsibilities - -Return only functions with an allowed remote purpose (`MCP` or `ALL` in the current DBeaver registry), -subject to local permissions and context. Recheck eligibility and arguments in `callFunction()`; -a caller can request an ID without first fetching the list. Do not use a denylist of UI functions. -Only explicitly enabled database connections may be accessed. Keep existing AI restrictions and -result limits. The API carries neither database credentials nor connection configurations. - -### Account integration and outstanding transport work - -Account maps descriptor `id` to MCP tool `name`, display `name` to `title`, and forwards the schema. -String results become text directly; other JSON values become JSON text. Object values are also -returned as MCP `structuredContent`; `error` maps to `isError`. Service exceptions map to JSON-RPC -errors: unknown function/invalid arguments → `-32602`, access denied → `-32001`, internal error → `-32603`. - -Account's `DDMcpClientProvider` is the integration seam for the future External DAM service proxy. -The desktop team can implement this shared interface and test against stubs before that proxy is ready. -There is no production External DAM adapter yet. The transport wire format, routing identity and -remote cancellation API still need agreement. A thread interruption or an MCP timeout does not by -itself guarantee cancellation or rollback of a remote SQL operation. Never automatically replay SQL -when delivery or execution outcome is unknown. +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.ai.api/META-INF/MANIFEST.MF b/apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF deleted file mode 100644 index 7e608cd..0000000 --- a/apis/com.dbeaver.datadam.ai.api/META-INF/MANIFEST.MF +++ /dev/null @@ -1,14 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Vendor: DBeaver Corp -Bundle-Name: DataDam AI API -Bundle-SymbolicName: com.dbeaver.datadam.ai.api -Bundle-Version: 0.0.17.qualifier -Bundle-RequiredExecutionEnvironment: JavaSE-21 -Export-Package: com.dbeaver.datadam.ai.api.exception, - com.dbeaver.datadam.ai.api.model, - com.dbeaver.datadam.ai.api.service -Bundle-ActivationPolicy: lazy -Require-Bundle: org.jkiss.utils -Import-Package: com.google.gson -Automatic-Module-Name: com.dbeaver.datadam.ai.api 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.ai.api/build.properties b/apis/com.dbeaver.datadam.mcp.api/build.properties similarity index 100% rename from apis/com.dbeaver.datadam.ai.api/build.properties rename to apis/com.dbeaver.datadam.mcp.api/build.properties diff --git a/apis/com.dbeaver.datadam.ai.api/pom.xml b/apis/com.dbeaver.datadam.mcp.api/pom.xml similarity index 95% rename from apis/com.dbeaver.datadam.ai.api/pom.xml rename to apis/com.dbeaver.datadam.mcp.api/pom.xml index 285cb03..63a4bb6 100644 --- a/apis/com.dbeaver.datadam.ai.api/pom.xml +++ b/apis/com.dbeaver.datadam.mcp.api/pom.xml @@ -10,9 +10,9 @@ ../pom.xml - com.dbeaver.datadam.ai.api + com.dbeaver.datadam.mcp.api eclipse-plugin - DataDam AI API + DataDam MCP API diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java similarity index 71% rename from apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java rename to apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java index 4fe3408..4c5a0e9 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/exception/DDAiFunctionException.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/exception/DDMcpFunctionException.java @@ -14,26 +14,26 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api.exception; +package com.dbeaver.datadam.mcp.api.exception; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionErrorCode; +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 DDAiFunctionException extends Exception { - private final DDAiFunctionErrorCode code; +public class DDMcpFunctionException extends Exception { + private final DDMcpFunctionErrorCode code; @Nullable private final JsonObject data; - public DDAiFunctionException(@NotNull DDAiFunctionErrorCode code, @NotNull String message, @Nullable JsonObject data) { + public DDMcpFunctionException(@NotNull DDMcpFunctionErrorCode code, @NotNull String message, @Nullable JsonObject data) { super(message); this.code = code; this.data = data; } @NotNull - public DDAiFunctionErrorCode getCode() { + public DDMcpFunctionErrorCode getCode() { return code; } diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java similarity index 83% rename from apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java rename to apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java index ac058d9..4ad9b51 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionCall.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionCall.java @@ -14,10 +14,10 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api.model; +package com.dbeaver.datadam.mcp.api.model; import com.google.gson.JsonObject; import org.jkiss.code.NotNull; -public record DDAiFunctionCall(@NotNull String functionId, @NotNull JsonObject arguments) { +public record DDMcpFunctionCall(@NotNull String functionId, @NotNull JsonObject arguments) { } diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java similarity index 91% rename from apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java rename to apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java index 3febe1a..086fbea 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionDescriptor.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionDescriptor.java @@ -14,13 +14,13 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api.model; +package com.dbeaver.datadam.mcp.api.model; import com.google.gson.JsonObject; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; -public record DDAiFunctionDescriptor( +public record DDMcpFunctionDescriptor( @NotNull String id, @NotNull String name, @Nullable String description, diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java similarity index 90% rename from apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java rename to apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java index c3f556a..1e40bb2 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionErrorCode.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionErrorCode.java @@ -14,9 +14,9 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api.model; +package com.dbeaver.datadam.mcp.api.model; -public enum DDAiFunctionErrorCode { +public enum DDMcpFunctionErrorCode { FUNCTION_NOT_FOUND, INVALID_ARGUMENTS, ACCESS_DENIED, diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java similarity index 85% rename from apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java rename to apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java index 2004a90..4d2e013 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/model/DDAiFunctionResult.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionResult.java @@ -14,10 +14,10 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api.model; +package com.dbeaver.datadam.mcp.api.model; import com.google.gson.JsonElement; import org.jkiss.code.NotNull; -public record DDAiFunctionResult(@NotNull JsonElement value, boolean error) { +public record DDMcpFunctionResult(@NotNull JsonElement value, boolean error) { } diff --git a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java similarity index 53% rename from apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java rename to apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java index def04d6..84eceb8 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/main/java/com/dbeaver/datadam/ai/api/service/DDAiFunctionService.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/service/DDMcpFunctionService.java @@ -14,23 +14,25 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api.service; +package com.dbeaver.datadam.mcp.api.service; -import com.dbeaver.datadam.ai.api.exception.DDAiFunctionException; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionCall; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionDescriptor; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionResult; +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 AI functions of one authenticated desktop. + * Remotely available MCP functions of one authenticated desktop. */ -public interface DDAiFunctionService { +public interface DDMcpFunctionService { + String SERVICE_ID = "dbeaver.mcp.functions"; + @NotNull - List getFunctions() throws DDAiFunctionException; + List getFunctions() throws DDMcpFunctionException; @NotNull - DDAiFunctionResult callFunction(@NotNull DDAiFunctionCall call) throws DDAiFunctionException; + DDMcpFunctionResult callFunction(@NotNull DDMcpFunctionCall call) throws DDMcpFunctionException; } diff --git a/apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java b/apis/com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java similarity index 73% rename from apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java rename to apis/com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java index 759f9ef..6c0cd07 100644 --- a/apis/com.dbeaver.datadam.ai.api/src/test/java/com/dbeaver/datadam/ai/api/DDAiFunctionContractTest.java +++ b/apis/com.dbeaver.datadam.mcp.api/src/test/java/com/dbeaver/datadam/mcp/api/DDMcpFunctionContractTest.java @@ -14,18 +14,18 @@ * is strictly forbidden unless prior written permission is obtained * from DBeaver Corp. */ -package com.dbeaver.datadam.ai.api; +package com.dbeaver.datadam.mcp.api; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionCall; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionDescriptor; -import com.dbeaver.datadam.ai.api.model.DDAiFunctionResult; +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 com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParser; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -class DDAiFunctionContractTest { +class DDMcpFunctionContractTest { private final Gson gson = new GsonBuilder().serializeNulls().create(); @Test @@ -34,16 +34,16 @@ void preservesNestedSchemaAndOptionalDescription() { {"type":"object","properties":{"filter":{"type":"object","properties":{"ids":{"type":"array", "items":{"type":"integer"}}}}},"required":["filter"],"additionalProperties":false} """).getAsJsonObject(); - var descriptor = new DDAiFunctionDescriptor("database_query", "Query", null, schema); - Assertions.assertEquals(descriptor, gson.fromJson(gson.toJson(descriptor), DDAiFunctionDescriptor.class)); + var descriptor = new DDMcpFunctionDescriptor("database_query", "Query", null, schema); + Assertions.assertEquals(descriptor, gson.fromJson(gson.toJson(descriptor), DDMcpFunctionDescriptor.class)); } @Test void preservesNumericArgumentsWithoutDoubleConversion() { var arguments = JsonParser.parseString("{\"id\":9007199254740993,\"amount\":0.1234567890123456789,\"value\":null}") .getAsJsonObject(); - var call = new DDAiFunctionCall("database_query", arguments); - var decoded = gson.fromJson(gson.toJson(call), DDAiFunctionCall.class); + var call = new DDMcpFunctionCall("database_query", arguments); + var decoded = gson.fromJson(gson.toJson(call), DDMcpFunctionCall.class); Assertions.assertEquals("9007199254740993", decoded.arguments().get("id").getAsString()); Assertions.assertEquals("0.1234567890123456789", decoded.arguments().get("amount").getAsString()); Assertions.assertTrue(decoded.arguments().get("value").isJsonNull()); @@ -51,7 +51,7 @@ void preservesNumericArgumentsWithoutDoubleConversion() { @Test void preservesExecutionFailureAndStructuredResult() { - var result = new DDAiFunctionResult(JsonParser.parseString("{\"message\":\"Permission denied\",\"rows\":[]}"), true); - Assertions.assertEquals(result, gson.fromJson(gson.toJson(result), DDAiFunctionResult.class)); + 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 c32b8ea..a379241 100644 --- a/apis/pom.xml +++ b/apis/pom.xml @@ -19,7 +19,7 @@ DataDam API - com.dbeaver.datadam.ai.api + com.dbeaver.datadam.mcp.api com.dbeaver.datadam.dm.api com.dbeaver.datadam.dam.remote.api com.dbeaver.datadam.share.api From 490374dc42a55a6f5df1061f08d5d0f7b0427bc4 Mon Sep 17 00:00:00 2001 From: Tsimafei Labanovich Date: Tue, 27 Oct 2026 15:38:50 +0100 Subject: [PATCH 3/5] dbeaver/pro#11120 Describe MCP parameters with typed Java models --- .../api/model/DDMcpFunctionDescriptor.java | 5 +-- .../mcp/api/model/DDMcpFunctionParameter.java | 33 +++++++++++++++++++ .../mcp/api/DDMcpFunctionContractTest.java | 16 +++++---- 3 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 apis/com.dbeaver.datadam.mcp.api/src/main/java/com/dbeaver/datadam/mcp/api/model/DDMcpFunctionParameter.java 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 index 086fbea..49aeb06 100644 --- 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 @@ -16,14 +16,15 @@ */ package com.dbeaver.datadam.mcp.api.model; -import com.google.gson.JsonObject; 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 JsonObject inputSchema + @NotNull List parameters ) { } 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/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 index 6c0cd07..87402ab 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -25,16 +26,19 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.List; + class DDMcpFunctionContractTest { private final Gson gson = new GsonBuilder().serializeNulls().create(); @Test - void preservesNestedSchemaAndOptionalDescription() { - var schema = JsonParser.parseString(""" - {"type":"object","properties":{"filter":{"type":"object","properties":{"ids":{"type":"array", - "items":{"type":"integer"}}}}},"required":["filter"],"additionalProperties":false} - """).getAsJsonObject(); - var descriptor = new DDMcpFunctionDescriptor("database_query", "Query", null, schema); + 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)); } From 577e1a3332c2b900c2df489c0b48563a06364177 Mon Sep 17 00:00:00 2001 From: Tsimafei Labanovich Date: Tue, 27 Oct 2026 15:51:47 +0100 Subject: [PATCH 4/5] dbeaver/pro#11120 Use maps for MCP function arguments --- .../mcp/api/model/DDMcpFunctionCall.java | 5 ++-- .../mcp/api/DDMcpFunctionContractTest.java | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) 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 index 4ad9b51..b7f6ff3 100644 --- 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 @@ -16,8 +16,9 @@ */ package com.dbeaver.datadam.mcp.api.model; -import com.google.gson.JsonObject; import org.jkiss.code.NotNull; -public record DDMcpFunctionCall(@NotNull String functionId, @NotNull JsonObject arguments) { +import java.util.Map; + +public record DDMcpFunctionCall(@NotNull String functionId, @NotNull Map arguments) { } 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 index 87402ab..bfbca55 100644 --- 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 @@ -23,13 +23,19 @@ 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().create(); + private final Gson gson = new GsonBuilder().serializeNulls().setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL).create(); @Test void preservesTypedParametersAndOptionalDescription() { @@ -43,14 +49,21 @@ void preservesTypedParametersAndOptionalDescription() { } @Test - void preservesNumericArgumentsWithoutDoubleConversion() { - var arguments = JsonParser.parseString("{\"id\":9007199254740993,\"amount\":0.1234567890123456789,\"value\":null}") - .getAsJsonObject(); + 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("9007199254740993", decoded.arguments().get("id").getAsString()); - Assertions.assertEquals("0.1234567890123456789", decoded.arguments().get("amount").getAsString()); - Assertions.assertTrue(decoded.arguments().get("value").isJsonNull()); + 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 From dd459d58678b8994ebbedb4390defbc25937ef86 Mon Sep 17 00:00:00 2001 From: Tsimafei Labanovich Date: Tue, 27 Oct 2026 16:09:52 +0100 Subject: [PATCH 5/5] dbeaver/pro#11120 Align MCP documentation with typed parameters and map arguments --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 515d9cc..03d4978 100644 --- a/README.md +++ b/README.md @@ -143,22 +143,30 @@ 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 JSON Schema `inputSchema`. -`callFunction(DDMcpFunctionCall)` returns a JSON value and an `error` flag; execution errors use `error=true`. -The MCP server maps descriptors to tools, object results to `structuredContent`, and the flag to `isError`. -Function IDs are unique and match `[a-zA-Z0-9_.-]{1,128}`; names are nonblank and schemas declare `"type":"object"`. +`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().create()); +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. -Use `JsonObject`/`JsonElement` and `serializeNulls()` on both peers to preserve explicit nulls and numeric precision. +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.