From 329d2ef8d531a5d8787797607904e7bd5d89f5c3 Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:17:29 +0200 Subject: [PATCH 01/37] Align README MCP section with MCP.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix tool discovery model: tools/list returns one tool (java_call with embedded catalog), not per-method named tools - Fix tools/call example to use java_call + callContent, not tool name - Rename "Fallback Tool" section — java_call is the only MCP tool - Add ibs_diagnostics, IBS.MCP.PRECHAIN, IBS.MCP.REQUIRE_JAVADOC - Fix dependency version 2.11.18 → 2.11.19 to match MCP.md - Update TOC Co-Authored-By: Claude Sonnet 4.6 --- README.md | 93 +++++++++++++++++++++++-------------------------------- 1 file changed, 38 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 02e713c..7ce6aba 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,9 @@ from any language or framework you are in. * [Enabling MCP](#enabling-mcp) * [Discovering Tools](#discovering-tools-toolslist) * [Calling a Discovered Tool](#calling-a-discovered-tool-toolscall) - * [The java_call Fallback Tool](#the-java_call-fallback-tool) + * [The java_call Tool](#the-java_call-tool) + * [Diagnostics](#diagnostics-ibs_diagnostics) + * [MCP Configuration](#mcp-configuration) * [MCP Limitations](#mcp-limitations) * [Error Management](#error-management) * [Contributing to the Project](#contributing-to-the-project) @@ -108,7 +110,7 @@ The following dependency needs to be added to your pom file: com.adobe.campaign.tests.bridge.service integroBridgeService - 2.11.18 + 2.11.19 ``` @@ -882,6 +884,8 @@ Example: BridgeService can act as an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server, allowing AI agents to discover and invoke your Java methods as typed tools over HTTP. The MCP endpoint uses JSON-RPC 2.0 and is served on the **same port** as the existing REST API. +For full configuration details, advanced usage, and integration with Claude Code and Cursor, see [docs/MCP.md](docs/MCP.md). + ### Enabling MCP Set the environment variable `IBS.MCP.ENABLED` to `true` before starting BridgeService: @@ -890,7 +894,7 @@ Set the environment variable `IBS.MCP.ENABLED` to `true` before starting BridgeS mvn exec:java -Dexec.args="test" -DIBS.MCP.ENABLED=true -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.example.mypackage ``` -At startup, BridgeService scans the packages listed in `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` and registers every **public static method** as a named MCP tool. The naming convention is `{SimpleClassName}_{methodName}`. +At startup, BridgeService scans the packages listed in `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` and builds a **method catalog** from every **public static method** found. The catalog is embedded in the `java_call` tool description so that AI agents can read it via `tools/list`. The MCP endpoint is available at: ``` @@ -928,6 +932,8 @@ Response: ### Discovering Tools (tools/list) +`tools/list` always returns exactly **one tool — `java_call`**. Its `description` contains the full catalog of all discovered methods. AI agents read the catalog to learn which class and method names to place in their `callContent` payloads. + ```json { "jsonrpc": "2.0", @@ -937,7 +943,7 @@ Response: } ``` -Response: +Response (abbreviated): ```json { @@ -945,20 +951,9 @@ Response: "id": 2, "result": { "tools": [ - { - "name": "SimpleStaticMethods_methodAcceptingStringArgument", - "description": "Calls com.example.SimpleStaticMethods.methodAcceptingStringArgument()", - "inputSchema": { - "type": "object", - "properties": { - "arg0": { "type": "string" } - }, - "required": ["arg0"] - } - }, { "name": "java_call", - "description": "Generic BridgeService call. Accepts the full /call payload including call chaining, instance methods, environment variables, and timeout.", + "description": "Generic BridgeService call. ...\n\nDiscovered methods:\n\nSimpleStaticMethods_methodAcceptingStringArgument\n class: com.example.SimpleStaticMethods\n method: methodAcceptingStringArgument\n Appends the success suffix to the given string.\n arg0 (string): the input string\n...", "inputSchema": { "..." : "..." } } ] @@ -966,28 +961,27 @@ Response: } ``` -The JSON Schema for each tool is derived from the method's parameter types: - -| Java type | JSON Schema type | -|---|---| -| `String` | `string` | -| `int` / `Integer` / `long` / `Long` | `integer` | -| `double` / `Double` / `float` / `Float` | `number` | -| `boolean` / `Boolean` | `boolean` | -| `List` / array | `array` | -| anything else | `object` | +Each catalog entry follows the format `{SimpleClassName}_{methodName}` and includes the fully qualified class name, method name, Javadoc description, and parameter descriptions. See [docs/MCP.md](docs/MCP.md) for the full catalog format. ### Calling a Discovered Tool (tools/call) +All calls go through `java_call`. Use the class and method names from the catalog in `callContent`: + ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { - "name": "SimpleStaticMethods_methodAcceptingStringArgument", + "name": "java_call", "arguments": { - "arg0": "hello" + "callContent": { + "result": { + "class": "com.example.SimpleStaticMethods", + "method": "methodAcceptingStringArgument", + "args": ["hello"] + } + } } } } @@ -1006,39 +1000,28 @@ On success the result contains the standard BridgeService return payload seriali } ``` -If the method throws an exception or the tool name is unknown, `isError` is `true` and `content[0].text` contains the error description. The HTTP status code is always `200` for `tools/call` — errors are reported inside the MCP result, not as HTTP errors. +If the method throws an exception, `isError` is `true` and `content[0].text` contains the error description. The HTTP status code is always `200` for `tools/call` — errors are reported inside the MCP result, not as HTTP errors. -### The `java_call` Fallback Tool +### The `java_call` Tool -A generic `java_call` tool is always included alongside the auto-discovered tools. Its `callContent` argument accepts exactly the same payload as the standard `POST /call` endpoint, making call chaining, instance methods, environment variables, and file uploads all accessible to MCP clients: +`java_call` accepts the same payload as the standard `POST /call` endpoint, making call chaining, instance methods, environment variables, and file uploads all accessible to MCP clients. **Bundle all related operations into a single `java_call`** using call chaining — static variable state (including authentication) does not persist between separate tool calls. -```json -{ - "jsonrpc": "2.0", - "id": 4, - "method": "tools/call", - "params": { - "name": "java_call", - "arguments": { - "callContent": { - "step1": { - "class": "com.example.MyClass", - "method": "doSomething", - "args": ["hello"] - } - }, - "environmentVariables": { - "MY_ENV_VAR": "value" - } - } - } -} -``` +### Diagnostics (`ibs_diagnostics`) + +A built-in `ibs_diagnostics` tool is always available alongside `java_call`. It requires no arguments and reports the running IBS version, MCP configuration state, received headers, and the number of methods in the catalog — useful for verifying a new server connection without touching HOST code. + +### MCP Configuration + +| Variable | Default | Description | +|---|---|---| +| `IBS.MCP.ENABLED` | `false` | Enables the MCP endpoint at `/mcp` | +| `IBS.MCP.PRECHAIN` | — | `callContent` fragment prepended to every `java_call` invocation (e.g. shared auth) | +| `IBS.MCP.REQUIRE_JAVADOC` | `true` | When `true`, only methods with a Javadoc comment are included in the catalog | ### MCP Limitations -* Only **public static methods** are auto-discovered. Instance methods are accessible via the `java_call` fallback tool. -* Overloaded methods with the **same number of parameters** are skipped during discovery (same restriction as the `/call` endpoint). Use `java_call` to call them explicitly. +* Only **public static methods** are auto-discovered. Instance methods are accessible via `java_call`. +* Overloaded methods with the **same number of parameters** are skipped during discovery. Use `java_call` to call them explicitly. * Parameter names are exposed as `arg0`, `arg1`, … — Java reflection does not retain source-level parameter names at runtime. ## Error Management From 64fd5904fd6a63580aa6cbbbfb6139ff157508f6 Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:23:11 +0200 Subject: [PATCH 02/37] Update docs to version 3.11.0 Add 3.11.0 release notes entry covering MCP doc corrections, new env vars, dependency bumps, and CI GPG fix. Update version references in README and MCP.md from 2.11.19 to 3.11.0. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 ++-- ReleaseNotes.md | 7 +++++++ docs/MCP.md | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7ce6aba..26e016a 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ The following dependency needs to be added to your pom file: com.adobe.campaign.tests.bridge.service integroBridgeService - 2.11.19 + 3.11.0 ``` @@ -924,7 +924,7 @@ Response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "2.11.19" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.0" }, "capabilities": { "tools": {} } } } diff --git a/ReleaseNotes.md b/ReleaseNotes.md index ac6d9d1..6cc1ccb 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,11 @@ # Bridge Service - RELEASE NOTES +## 3.11.0 +* **Documentation** Corrected the MCP section in README to match the actual `tools/list` behaviour: a single `java_call` tool is returned with all discovered methods embedded as a catalog in its description — not one tool per method. Updated calling examples accordingly. +* **Documentation** Added [`docs/MCP.md`](docs/MCP.md): full MCP reference covering `java_call`, `ibs_diagnostics`, `IBS.MCP.PRECHAIN`, `IBS.MCP.REQUIRE_JAVADOC`, secrets/env-var headers, Claude Code and Cursor integration, and Javadoc quality gate. +* **New Environment Variables** `IBS.MCP.PRECHAIN`, `IBS.MCP.REQUIRE_JAVADOC` — see [MCP Configuration](docs/MCP.md#mcp-configuration-reference) for details. +* **Dependency Updates** Bumped `rest-assured` to 5.5.7, `gson` to 2.13.2, `exec-maven-plugin` to 3.6.3, `jacoco-maven-plugin` to 0.8.14, `maven-dependency-plugin` to 3.10.0, `maven-javadoc-plugin` to 3.12.0, `maven-gpg-plugin` to 3.2.8, `maven-deploy-plugin` to 3.1.4. +* **CI** Fixed `maven-gpg-plugin` 3.2.8 passphrase deprecation warning: replaced `gpg.passphrase` property in `settings.xml` with the `MAVEN_GPG_PASSPHRASE` environment variable. + ## 2.11.19 * **New Feature** [#12 Expose BridgeService as an MCP Server](https://github.com/adobe/bridgeService/issues/12). BridgeService can now act as a Model Context Protocol (MCP) server. When `IBS.MCP.ENABLED=true`, a `POST /mcp` endpoint is registered on the existing port. It scans `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` at startup and exposes each public static method as a named MCP tool discoverable via `tools/list`. A generic `java_call` fallback tool is always included for call chaining and instance methods. Please refer to ["Using BridgeService as an MCP Server"](README.md#using-bridgeservice-as-an-mcp-server) in the README for full details. * **New Environment Variable** `IBS.MCP.ENABLED`: Set to `true` to enable the MCP endpoint (default: `false`). diff --git a/docs/MCP.md b/docs/MCP.md index 1ec2f58..f468bc9 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -102,7 +102,7 @@ Expected response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "2.11.19" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.0" }, "capabilities": { "tools": {} } } } @@ -782,7 +782,7 @@ and start the server from within it. com.adobe.campaign.tests.bridge.service integroBridgeService - 2.11.19 + 3.11.0 ``` From 3e2fd11a29490fa14d4f10e08a0f65f6c067da8f Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:24:50 +0200 Subject: [PATCH 03/37] Simplify 3.11.0 release notes to high-level summary Co-Authored-By: Claude Sonnet 4.6 --- ReleaseNotes.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 6cc1ccb..498fadb 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,10 +1,8 @@ # Bridge Service - RELEASE NOTES ## 3.11.0 -* **Documentation** Corrected the MCP section in README to match the actual `tools/list` behaviour: a single `java_call` tool is returned with all discovered methods embedded as a catalog in its description — not one tool per method. Updated calling examples accordingly. -* **Documentation** Added [`docs/MCP.md`](docs/MCP.md): full MCP reference covering `java_call`, `ibs_diagnostics`, `IBS.MCP.PRECHAIN`, `IBS.MCP.REQUIRE_JAVADOC`, secrets/env-var headers, Claude Code and Cursor integration, and Javadoc quality gate. -* **New Environment Variables** `IBS.MCP.PRECHAIN`, `IBS.MCP.REQUIRE_JAVADOC` — see [MCP Configuration](docs/MCP.md#mcp-configuration-reference) for details. -* **Dependency Updates** Bumped `rest-assured` to 5.5.7, `gson` to 2.13.2, `exec-maven-plugin` to 3.6.3, `jacoco-maven-plugin` to 0.8.14, `maven-dependency-plugin` to 3.10.0, `maven-javadoc-plugin` to 3.12.0, `maven-gpg-plugin` to 3.2.8, `maven-deploy-plugin` to 3.1.4. -* **CI** Fixed `maven-gpg-plugin` 3.2.8 passphrase deprecation warning: replaced `gpg.passphrase` property in `settings.xml` with the `MAVEN_GPG_PASSPHRASE` environment variable. +* **MCP** Extended MCP documentation. See [docs/MCP.md](docs/MCP.md) for the full reference. +* **Dependency Updates** Routine dependency and plugin version bumps. +* **CI** Fixed GPG signing configuration to address a deprecation warning introduced by `maven-gpg-plugin` 3.2.8. ## 2.11.19 * **New Feature** [#12 Expose BridgeService as an MCP Server](https://github.com/adobe/bridgeService/issues/12). BridgeService can now act as a Model Context Protocol (MCP) server. When `IBS.MCP.ENABLED=true`, a `POST /mcp` endpoint is registered on the existing port. It scans `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` at startup and exposes each public static method as a named MCP tool discoverable via `tools/list`. A generic `java_call` fallback tool is always included for call chaining and instance methods. Please refer to ["Using BridgeService as an MCP Server"](README.md#using-bridgeservice-as-an-mcp-server) in the README for full details. From 215b84384fc9fe033af9bafeeacaf1a5032eb2b4 Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:25:42 +0200 Subject: [PATCH 04/37] Add issue #12 reference to 3.11.0 MCP release note Co-Authored-By: Claude Sonnet 4.6 --- ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 498fadb..5ccd942 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,6 +1,6 @@ # Bridge Service - RELEASE NOTES ## 3.11.0 -* **MCP** Extended MCP documentation. See [docs/MCP.md](docs/MCP.md) for the full reference. +* **MCP** [#12 Expose BridgeService as an MCP Server](https://github.com/adobe/bridgeService/issues/12) Extended MCP documentation. See [docs/MCP.md](docs/MCP.md) for the full reference. * **Dependency Updates** Routine dependency and plugin version bumps. * **CI** Fixed GPG signing configuration to address a deprecation warning introduced by `maven-gpg-plugin` 3.2.8. From 5bbd2b0fe067b68551373e1acfb071cee2c202ad Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:29:31 +0200 Subject: [PATCH 05/37] Bump GitHub Actions to v5 to fix Node.js 20 deprecation warning actions/checkout, actions/setup-java, and actions/cache all moved from v4 (Node.js 20) to v5 (Node.js 24). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/maven-pr-analyze.yml | 8 ++++---- .github/workflows/maven-publish-deploy.yml | 6 +++--- .github/workflows/maven-publish-release.yml | 6 +++--- .github/workflows/onPushSimpleTest.yml | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/maven-pr-analyze.yml b/.github/workflows/maven-pr-analyze.yml index 022ebfb..f758ac0 100644 --- a/.github/workflows/maven-pr-analyze.yml +++ b/.github/workflows/maven-pr-analyze.yml @@ -28,23 +28,23 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: # Check out Git repository - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Set up environment with Java and Maven - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: temurin - name: Cache SonarCloud packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar # Set up dependency cache - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/maven-publish-deploy.yml b/.github/workflows/maven-publish-deploy.yml index 7031c23..d4dafa8 100644 --- a/.github/workflows/maven-publish-deploy.yml +++ b/.github/workflows/maven-publish-deploy.yml @@ -27,17 +27,17 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: # Check out Git repository - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Set up environment with Java and Maven - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 11 distribution: temurin # Set up dependency cache - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/maven-publish-release.yml b/.github/workflows/maven-publish-release.yml index c8ec385..8b69851 100644 --- a/.github/workflows/maven-publish-release.yml +++ b/.github/workflows/maven-publish-release.yml @@ -24,18 +24,18 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: # Check out Git repository - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Set up environment with Java and Maven - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 11 distribution: temurin # Set up dependency cache - name: Cache local Maven repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/onPushSimpleTest.yml b/.github/workflows/onPushSimpleTest.yml index abd49eb..3316094 100644 --- a/.github/workflows/onPushSimpleTest.yml +++ b/.github/workflows/onPushSimpleTest.yml @@ -30,9 +30,9 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 11 distribution: temurin From a5d4b60a8fdea58dc7f6aa7b795e98519720212a Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:30:14 +0200 Subject: [PATCH 06/37] Bump actions/upload-artifact to v5 to fix Node.js 20 deprecation warning Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/onPushSimpleTest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/onPushSimpleTest.yml b/.github/workflows/onPushSimpleTest.yml index 3316094..4aabf3b 100644 --- a/.github/workflows/onPushSimpleTest.yml +++ b/.github/workflows/onPushSimpleTest.yml @@ -55,7 +55,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload JaCoCo coverage report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: jacoco-report path: integroBridgeService/target/site/jacoco/ From 310a0f91fdbfac0409a79c7903b5c8f1161088ff Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Thu, 16 Apr 2026 20:36:05 +0000 Subject: [PATCH 07/37] [maven-release-plugin] prepare release parent-3.11.0 --- bridgeService-data/pom.xml | 5 ++--- integroBridgeService/pom.xml | 5 ++--- pom.xml | 7 +++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 4ba0b41..ca107b6 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -9,8 +9,7 @@ it. --> - + 4.0.0 com.adobe.campaign.tests.bridge.testdata @@ -53,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.0-SNAPSHOT + 3.11.0 diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 26e3623..5184357 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -9,8 +9,7 @@ it. --> - + 4.0.0 com.adobe.campaign.tests.bridge.service integroBridgeService @@ -186,6 +185,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.0-SNAPSHOT + 3.11.0 diff --git a/pom.xml b/pom.xml index ce32b41..5f4e2ce 100644 --- a/pom.xml +++ b/pom.xml @@ -9,12 +9,11 @@ it. --> - + 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.0-SNAPSHOT + 3.11.0 Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -203,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - HEAD + parent-3.11.0 From 10564e78710eaef099f3cec20b489265aa8e031e Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Thu, 16 Apr 2026 20:36:06 +0000 Subject: [PATCH 08/37] [maven-release-plugin] prepare for next development iteration --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index ca107b6..68702b3 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.0 + 3.11.1-SNAPSHOT diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 5184357..7ee1231 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -185,6 +185,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.0 + 3.11.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 5f4e2ce..e91641e 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.0 + 3.11.1-SNAPSHOT Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - parent-3.11.0 + HEAD From a375bbbc2550419fb68da61bbd47eb1cfd2cdd5a Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:37:45 +0200 Subject: [PATCH 09/37] Add missing Javadoc @param and @return to MCPRequestHandler#handle Fixes Javadoc warnings raised during the release build. Co-Authored-By: Claude Sonnet 4.6 --- .../campaign/tests/bridge/service/MCPRequestHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java index 7a6596d..bbc56a8 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java @@ -113,6 +113,10 @@ public MCPRequestHandler() { * Spark route handler. Parses the incoming JSON-RPC 2.0 request and dispatches * to the appropriate handler. All exceptions are caught and returned as MCP errors * rather than propagating to Spark's HTTP exception handlers. + * + * @param req the incoming Spark HTTP request + * @param res the Spark HTTP response + * @return the JSON-RPC response as a String */ public Object handle(Request req, Response res) { res.type("application/json"); From c237faa58037b59278f90d326e2e17aebce4fb8e Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:49:56 +0200 Subject: [PATCH 10/37] Update version to 3.11.1 in docs and add Central promotion step - Bump all doc version references from 3.11.0 to 3.11.1 - Add POST /manual/upload step to release workflow to trigger Central Publisher Portal promotion after staging deploy Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/maven-publish-release.yml | 11 ++++++++++- README.md | 4 ++-- ReleaseNotes.md | 2 +- docs/MCP.md | 6 +++--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/maven-publish-release.yml b/.github/workflows/maven-publish-release.yml index 8b69851..0a8080f 100644 --- a/.github/workflows/maven-publish-release.yml +++ b/.github/workflows/maven-publish-release.yml @@ -66,4 +66,13 @@ jobs: X_GITHUB_USERNAME: ${{ secrets.ADOBE_BOT_GITHUB_USERNAME }} X_GITHUB_PASSWORD: ${{ secrets.ADOBE_BOT_GITHUB_PASSWORD }} run: mvn -B -DperformRelease=true clean release:prepare release:perform -Prelease,ossrh -s .github/workflows/settings.xml - + + - name: Promote to Central + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: | + curl -f -X POST \ + -H "Authorization: Bearer $(echo -n "${SONATYPE_USERNAME}:${SONATYPE_PASSWORD}" | base64)" \ + "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.adobe.campaign.tests.bridge" + diff --git a/README.md b/README.md index 26e016a..afffa69 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ The following dependency needs to be added to your pom file: com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.0 + 3.11.1 ``` @@ -924,7 +924,7 @@ Response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.0" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.1" }, "capabilities": { "tools": {} } } } diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 5ccd942..7083376 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,5 @@ # Bridge Service - RELEASE NOTES -## 3.11.0 +## 3.11.1 * **MCP** [#12 Expose BridgeService as an MCP Server](https://github.com/adobe/bridgeService/issues/12) Extended MCP documentation. See [docs/MCP.md](docs/MCP.md) for the full reference. * **Dependency Updates** Routine dependency and plugin version bumps. * **CI** Fixed GPG signing configuration to address a deprecation warning introduced by `maven-gpg-plugin` 3.2.8. diff --git a/docs/MCP.md b/docs/MCP.md index f468bc9..d6d61ac 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -102,7 +102,7 @@ Expected response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.0" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.1" }, "capabilities": { "tools": {} } } } @@ -538,7 +538,7 @@ Response: ```json { - "ibsVersion": "3.11.0", + "ibsVersion": "3.11.1", "deploymentMode": "TEST", "mcpConfig": { "packagesConfigured": "com.example.services", @@ -782,7 +782,7 @@ and start the server from within it. com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.0 + 3.11.1 ``` From a633c3f8d4c621af3ae2fb8f54b05d62584e0c29 Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:51:14 +0200 Subject: [PATCH 11/37] Add Central promotion step to deploy workflow Mirrors the same POST /manual/upload step added to the release workflow. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/maven-publish-deploy.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/maven-publish-deploy.yml b/.github/workflows/maven-publish-deploy.yml index d4dafa8..0ed4d7a 100644 --- a/.github/workflows/maven-publish-deploy.yml +++ b/.github/workflows/maven-publish-deploy.yml @@ -66,4 +66,13 @@ jobs: X_GITHUB_USERNAME: ${{ secrets.ADOBE_BOT_GITHUB_USERNAME }} X_GITHUB_PASSWORD: ${{ secrets.ADOBE_BOT_GITHUB_PASSWORD }} run: mvn -B -DperformRelease=true clean deploy -Prelease,ossrh -s .github/workflows/settings.xml - + + - name: Promote to Central + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: | + curl -f -X POST \ + -H "Authorization: Bearer $(echo -n "${SONATYPE_USERNAME}:${SONATYPE_PASSWORD}" | base64)" \ + "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.adobe.campaign.tests.bridge" + From ee0a8ce4a50a9df16ec33431e8f4c82fd3ced556 Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:58:28 +0200 Subject: [PATCH 12/37] Remove Central promotion step from deploy workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snapshot deploys go directly to the snapshot repository — no staging repository is created, so the promotion POST returns 400. The promotion step belongs only in the release workflow. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/maven-publish-deploy.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/maven-publish-deploy.yml b/.github/workflows/maven-publish-deploy.yml index 0ed4d7a..bf1474f 100644 --- a/.github/workflows/maven-publish-deploy.yml +++ b/.github/workflows/maven-publish-deploy.yml @@ -67,12 +67,3 @@ jobs: X_GITHUB_PASSWORD: ${{ secrets.ADOBE_BOT_GITHUB_PASSWORD }} run: mvn -B -DperformRelease=true clean deploy -Prelease,ossrh -s .github/workflows/settings.xml - - name: Promote to Central - env: - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - run: | - curl -f -X POST \ - -H "Authorization: Bearer $(echo -n "${SONATYPE_USERNAME}:${SONATYPE_PASSWORD}" | base64)" \ - "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.adobe.campaign.tests.bridge" - From 4d24391b969cef914352d57a4c2a2be8d7b046ec Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Thu, 16 Apr 2026 21:02:27 +0000 Subject: [PATCH 13/37] [maven-release-plugin] prepare release parent-3.11.1 --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 68702b3..0ad199f 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.1-SNAPSHOT + 3.11.1 diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 7ee1231..fd4438c 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -185,6 +185,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.1-SNAPSHOT + 3.11.1 diff --git a/pom.xml b/pom.xml index e91641e..4b35acb 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.1-SNAPSHOT + 3.11.1 Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - HEAD + parent-3.11.1 From 40bffb1cce018265dfdac851a8180b3f44333df2 Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Thu, 16 Apr 2026 21:02:28 +0000 Subject: [PATCH 14/37] [maven-release-plugin] prepare for next development iteration --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 0ad199f..1a41792 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.1 + 3.11.2-SNAPSHOT diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index fd4438c..d758dcf 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -185,6 +185,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.1 + 3.11.2-SNAPSHOT diff --git a/pom.xml b/pom.xml index 4b35acb..a94673a 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.1 + 3.11.2-SNAPSHOT Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - parent-3.11.1 + HEAD From 802fb835666b0369a0c364500ceb138eab1a9248 Mon Sep 17 00:00:00 2001 From: Baubak Gandomi Date: Thu, 16 Apr 2026 23:33:33 +0200 Subject: [PATCH 15/37] Fix CI badge staleness for SonarCloud quality gate and codecov (#22) Trigger SonarCloud analysis on push to main (not just PRs) so the quality gate badge stays current after merges. Remove dead jacoco log step that referenced a non-existent step output. Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/maven-pr-analyze.yml | 4 +++- .github/workflows/onPushSimpleTest.yml | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven-pr-analyze.yml b/.github/workflows/maven-pr-analyze.yml index f758ac0..fa12de3 100644 --- a/.github/workflows/maven-pr-analyze.yml +++ b/.github/workflows/maven-pr-analyze.yml @@ -14,7 +14,9 @@ name: Analyze-BridgeService # Run workflow on commits to default branch -on: +on: + push: + branches: [ main ] pull_request: branches: [ main ] diff --git a/.github/workflows/onPushSimpleTest.yml b/.github/workflows/onPushSimpleTest.yml index 4aabf3b..307ebf3 100644 --- a/.github/workflows/onPushSimpleTest.yml +++ b/.github/workflows/onPushSimpleTest.yml @@ -42,11 +42,6 @@ jobs: OSSRH_ARTIFACTORY_USER: ${{ secrets.OSSRH_ARTIFACTORY_USER }} OSSRH_ARTIFACTORY_API_TOKEN: ${{ secrets.OSSRH_ARTIFACTORY_API_TOKEN }} - - name: Log coverage percentage - run: | - echo "coverage = ${{ steps.jacoco.outputs.coverage }}" - echo "branch coverage = ${{ steps.jacoco.outputs.branches }}" - - name: publish coverage onto codecov uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 with: From 4995fc6bfb0560d134050387356e8170904df289 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:12:17 +0200 Subject: [PATCH 16/37] Update dependency org.testng:testng to v7.12.0 (#21) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- integroBridgeService/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index d758dcf..2a813db 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -132,7 +132,7 @@ org.testng testng - 7.8.0 + 7.12.0 test From 2995394012f09f29c56e7212aefa222b0007ba57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:12:39 +0200 Subject: [PATCH 17/37] Update dependency org.mockito:mockito-core to v5.23.0 (#20) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- integroBridgeService/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 2a813db..6d19515 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -165,7 +165,7 @@ org.mockito mockito-core - 5.14.1 + 5.23.0 test From 7d9347791994a2cb90a6f01ddee9608aa1c9d8b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:53:03 +0200 Subject: [PATCH 18/37] Update log4j2 monorepo to v2.25.4 (#23) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bridgeService-data/pom.xml | 4 ++-- integroBridgeService/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 1a41792..d71beb9 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -31,12 +31,12 @@ org.apache.logging.log4j log4j-core - 2.24.1 + 2.25.4 org.apache.logging.log4j log4j-api - 2.24.1 + 2.25.4 com.sun.mail diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 6d19515..dc7f121 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -143,12 +143,12 @@ org.apache.logging.log4j log4j-core - 2.24.1 + 2.25.4 org.apache.logging.log4j log4j-api - 2.24.1 + 2.25.4 javax.servlet From 65e2952b4c0ff368eb39f9c9b2e8f6f0a25f508a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:53:35 +0200 Subject: [PATCH 19/37] Update actions/checkout action to v6 (#24) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/maven-pr-analyze.yml | 2 +- .github/workflows/maven-publish-deploy.yml | 2 +- .github/workflows/maven-publish-release.yml | 2 +- .github/workflows/onPushSimpleTest.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/maven-pr-analyze.yml b/.github/workflows/maven-pr-analyze.yml index fa12de3..21c4695 100644 --- a/.github/workflows/maven-pr-analyze.yml +++ b/.github/workflows/maven-pr-analyze.yml @@ -30,7 +30,7 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: # Check out Git repository - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # Set up environment with Java and Maven - name: Set up JDK diff --git a/.github/workflows/maven-publish-deploy.yml b/.github/workflows/maven-publish-deploy.yml index bf1474f..3edad86 100644 --- a/.github/workflows/maven-publish-deploy.yml +++ b/.github/workflows/maven-publish-deploy.yml @@ -27,7 +27,7 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: # Check out Git repository - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # Set up environment with Java and Maven - name: Set up JDK diff --git a/.github/workflows/maven-publish-release.yml b/.github/workflows/maven-publish-release.yml index 0a8080f..1656539 100644 --- a/.github/workflows/maven-publish-release.yml +++ b/.github/workflows/maven-publish-release.yml @@ -24,7 +24,7 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: # Check out Git repository - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # Set up environment with Java and Maven - name: Set up JDK diff --git a/.github/workflows/onPushSimpleTest.yml b/.github/workflows/onPushSimpleTest.yml index 307ebf3..cf62c7a 100644 --- a/.github/workflows/onPushSimpleTest.yml +++ b/.github/workflows/onPushSimpleTest.yml @@ -30,7 +30,7 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: From 11d54e44e83c281e9faaf00d9a7865da64ee8eac Mon Sep 17 00:00:00 2001 From: Baubak Gandomi Date: Tue, 21 Apr 2026 18:16:50 +0200 Subject: [PATCH 20/37] Expose auto-discovered methods as individual MCP tools (hybrid approach) (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose auto-discovered methods as individual MCP tools (hybrid approach) Previously all auto-discovered methods were embedded as plain text in java_call's description. With 2000+ methods this exceeded LLM context windows and got truncated, making method discovery impossible without trial-and-error 404 guessing. Each public static method is now exposed as its own tool in tools/list (name: ClassName_methodName, inputSchema with arg0/arg1/... params). Individual tool calls are routed through handleIndividualToolCall() which builds a synthetic single-step callContent and delegates to handleJavaCall() so PRECHAIN is applied automatically. java_call is retained for multi-step chains, overloaded/instance methods, and any case where step B needs the live Java object returned by step A. Co-Authored-By: Claude Sonnet 4.6 * Add architecture diagram to MCP method discovery section Mermaid flowchart showing the startup package scan that populates tools/list, and the two per-call paths (individual tool and java_call) converging on handleJavaCall() through the isolated classloader. Co-Authored-By: Claude Sonnet 4.6 * Fix coverage gap: replace JSON schema parsing with programmatic map builders The constructor's try-catch for JsonProcessingException was dead code — the schemas are hardcoded constants that never fail to parse. handleIndividualToolCall's catch was also dead code since handleJavaCall catches its own exceptions internally. Replaced both with no-exception-possible patterns to eliminate the uncovered lines. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- docs/MCP.md | 115 ++++---- .../bridge/service/MCPRequestHandler.java | 250 +++++++++--------- .../bridge/service/MCPBridgeServerTest.java | 210 +++++++++++++-- 3 files changed, 379 insertions(+), 196 deletions(-) diff --git a/docs/MCP.md b/docs/MCP.md index d6d61ac..6179058 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -116,9 +116,10 @@ curl -s -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' ``` -`tools/list` always returns exactly **one tool — `java_call`**. Its `description` contains a -catalog of all methods discovered from the configured packages. AI agents read that catalog to -learn which class and method names to place in their `callContent` payloads. +`tools/list` returns **one tool per auto-discovered method** plus `java_call` (for multi-step +chains) and `ibs_diagnostics`. AI agents can call individual methods directly by name, or bundle +multiple steps into a single `java_call` chain when they need to pass live Java objects between +steps. ```json { @@ -127,29 +128,28 @@ learn which class and method names to place in their `callContent` payloads. "result": { "tools": [ { - "name": "java_call", - "description": "Generic BridgeService call. Accepts the full /call payload including call chaining, instance methods, environment variables, and timeout. Bundle all operations into one callContent chain so they share a single isolated execution context. State (including authentication) does not persist between separate tool calls.\n\nDiscovered methods (use class/method values in callContent for java_call):\n\nSimpleStaticMethods_methodReturningString\n class: com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods\n method: methodReturningString\n Returns the success string constant used for testing.\n args: (none)\n\nSimpleStaticMethods_methodAcceptingStringArgument\n class: com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods\n method: methodAcceptingStringArgument\n Appends the success suffix to the given string.\n arg0 (string): the input string\n\n... (further entries for ClassWithLogger methods etc.)", + "name": "SimpleStaticMethods_methodReturningString", + "description": "Returns the success string constant used for testing.", + "inputSchema": { "type": "object", "properties": {} } + }, + { + "name": "SimpleStaticMethods_methodAcceptingStringArgument", + "description": "Appends the success suffix to the given string.", "inputSchema": { "type": "object", - "required": ["callContent"], - "properties": { - "callContent": { - "type": "object", - "description": "Map of call IDs to call definitions.", - "additionalProperties": { - "type": "object", - "required": ["class", "method"], - "properties": { - "class": { "type": "string" }, - "method": { "type": "string" }, - "args": { "type": "array" } - } - } - }, - "environmentVariables": { "type": "object" }, - "timeout": { "type": "integer" } - } + "properties": { "arg0": { "type": "string", "description": "the input string" } }, + "required": ["arg0"] } + }, + { + "name": "java_call", + "description": "Generic BridgeService call for multi-step chains. ...", + "inputSchema": { "type": "object", "required": ["callContent"], "properties": { "callContent": { "..." } } } + }, + { + "name": "ibs_diagnostics", + "description": "Built-in IBS diagnostic tool. ...", + "inputSchema": { "type": "object", "properties": {} } } ] } @@ -158,8 +158,7 @@ learn which class and method names to place in their `callContent` payloads. ### Calling tools -All calls go through `java_call`. Use the class and method names from the catalog in -`callContent`. +Single-method calls can be made directly by tool name. Multi-step chains go through `java_call`. **No-argument method:** @@ -256,37 +255,53 @@ curl -s -X POST http://localhost:8080/mcp \ }' ``` -### How the method catalog works - -`tools/list` returns a single tool — `java_call`. Auto-discovery does not produce separately -callable tools; instead it builds a **catalog** that is embedded in the `java_call` description. +### How method discovery works + +```mermaid +flowchart LR + subgraph startup["Startup"] + P["Configured packages\n(IBS.CLASSLOADER.STATIC\n.INTEGRITY.PACKAGES)"] --> D["MCPToolDiscovery\n(public static methods)"] + D --> TL["tools/list\n──────────────────\nClassName_method₁\nClassName_method₂ …\njava_call\nibs_diagnostics"] + end + + subgraph call["Per call — tools/call"] + A["AI Agent"] + IT["handleIndividual\nToolCall()"] + JC["handleJavaCall()"] + PC["PRECHAIN\n(if configured)"] + CL["Isolated ClassLoader\n(fresh per call)"] + R["Result"] + + A -->|"ClassName_methodN\n{ arg0, arg1, … }"| IT + A -->|"java_call\n{ callContent: {…} }"| JC + IT -->|"synthetic single-step\ncallContent"| JC + JC --> PC --> CL --> R + end +``` -When an AI agent calls `tools/list` it reads the catalog to learn which class and method names -exist, then constructs the appropriate `callContent` payload and calls `java_call`. The catalog is -rebuilt every time the server starts, so it stays in sync with the Java library automatically. +`tools/list` exposes each auto-discovered public static method as its own MCP tool, named +`ClassName_methodName`. Each tool carries its own `inputSchema` (parameters named `arg0`, +`arg1`, …) and a Javadoc-sourced description. The list is rebuilt every time the server starts, +so it stays in sync with the Java library automatically. -**Why not separate tools per method?** +AI agents can call individual methods directly for single stateless reads, or bundle multiple +steps into a `java_call` chain when they need to pass live Java objects between steps. -Separate tools per method force the AI to make one HTTP round-trip per method call and lose -execution context between calls. With `java_call`, any number of steps can be bundled into one -request inside a single isolated class loader — enabling call chaining, where the return value -of step N is passed directly to step N+1 as a live Java object (not serialized JSON). This is -essential for scenarios involving authentication, object creation, or anything with mutable state. +**When to use individual tools vs `java_call`:** -The catalog format in the description for each entry is: +| Scenario | Use | +|---|---| +| Single stateless read | Individual tool (`ClassName_methodName`) | +| Step B needs the Java object returned by step A | `java_call` with call chain | +| Overloaded method (same parameter count) | `java_call` | +| Instance method or constructor | `java_call` | -``` -ClassName_methodName - class: com.example.package.ClassName - method: methodName - - arg0 (type): - arg1 (type): -``` +**Individual tools are for discovery and stateless calls.** Each tool call runs in a fresh +isolated class loader. Complex Java objects (e.g. `List`) do not survive the JSON +serialization round-trip between separate calls — they must be chained inside a single `java_call`. -**Project skills and `CLAUDE.md`** can reference catalog entries by class/method name to give the -AI more context about when and how to use each one. For per-user auth or multi-step flows, the -skill prepends an auth step to the `java_call` callContent chain. +**Project skills and `CLAUDE.md`** can annotate methods with additional context (auth patterns, +known FQ class names, multi-step flow recipes) to help the AI select the right tool and chain. --- diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java index bbc56a8..07aab6f 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java @@ -25,12 +25,11 @@ * * Implements the MCP (Model Context Protocol) Streamable HTTP transport for tool access: * - initialize : MCP handshake - * - tools/list : returns a single {@code java_call} tool whose description embeds a - * catalog of auto-discovered methods - * - tools/call : invokes the {@code java_call} tool, which accepts arbitrary BridgeService - * call chains; auto-discovered methods are not directly callable as - * separate MCP tools — the catalog in the description tells the LLM which - * class and method names to place in the callContent payload + * - tools/list : returns one tool per auto-discovered method, plus {@code java_call} + * and {@code ibs_diagnostics} + * - tools/call : routes to the matching individual tool (single stateless call), + * {@code java_call} (multi-step chain, overloaded/instance methods), + * or {@code ibs_diagnostics} * * Tool discovery is performed once at construction time using IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES. */ @@ -43,70 +42,104 @@ public class MCPRequestHandler { private static final String DIAGNOSTICS_TOOL_NAME = "ibs_diagnostics"; static final String PRECHAIN_HEADER = "ibs-prechain"; - private static final String JAVA_CALL_TOOL_SCHEMA = "{" - + "\"type\":\"object\"," - + "\"required\":[\"callContent\"]," - + "\"properties\":{" - + "\"callContent\":{" - + "\"type\":\"object\"," - + "\"description\":\"Map of call IDs to call definitions. A string arg matching a prior call ID is substituted with that call's result.\"," - + "\"additionalProperties\":{" - + "\"type\":\"object\"," - + "\"required\":[\"class\",\"method\"]," - + "\"properties\":{" - + "\"class\":{\"type\":\"string\",\"description\":\"Fully qualified Java class name\"}," - + "\"method\":{\"type\":\"string\",\"description\":\"Method name\"}," - + "\"args\":{\"type\":\"array\",\"description\":\"Method arguments\"}," - + "\"returnType\":{\"type\":\"string\",\"description\":\"Optional expected return type\"}" - + "}}}," - + "\"environmentVariables\":{\"type\":\"object\",\"description\":\"Key-value pairs injected before execution\"}," - + "\"timeout\":{\"type\":\"integer\",\"description\":\"Timeout in milliseconds (0=unlimited, default 10000)\"}" - + "}}"; - - private static final String DIAGNOSTICS_TOOL_SCHEMA = "{" - + "\"type\":\"object\"," - + "\"properties\":{}," - + "\"required\":[]" - + "}"; + private static final Map JAVA_CALL_SCHEMA_MAP = buildJavaCallSchemaMap(); + private static final Map DIAGNOSTICS_SCHEMA_MAP = buildDiagnosticsSchemaMap(); + + private static Map schemaProp(String in_type, String in_desc) { + Map l_p = new LinkedHashMap<>(); + l_p.put("type", in_type); + l_p.put("description", in_desc); + return l_p; + } + + private static Map buildJavaCallSchemaMap() { + Map l_callEntryProps = new LinkedHashMap<>(); + l_callEntryProps.put("class", schemaProp("string", "Fully qualified Java class name")); + l_callEntryProps.put("method", schemaProp("string", "Method name")); + l_callEntryProps.put("args", schemaProp("array", "Method arguments")); + l_callEntryProps.put("returnType", schemaProp("string", "Optional expected return type")); + + Map l_callEntrySchema = new LinkedHashMap<>(); + l_callEntrySchema.put("type", "object"); + l_callEntrySchema.put("required", Arrays.asList("class", "method")); + l_callEntrySchema.put("properties", l_callEntryProps); + + Map l_callContentProp = new LinkedHashMap<>(); + l_callContentProp.put("type", "object"); + l_callContentProp.put("description", + "Map of call IDs to call definitions. A string arg matching a prior call ID is substituted with that call's result."); + l_callContentProp.put("additionalProperties", l_callEntrySchema); + + Map l_props = new LinkedHashMap<>(); + l_props.put("callContent", l_callContentProp); + l_props.put("environmentVariables", schemaProp("object", "Key-value pairs injected before execution")); + + Map l_timeoutProp = new LinkedHashMap<>(); + l_timeoutProp.put("type", "integer"); + l_timeoutProp.put("description", "Timeout in milliseconds (0=unlimited, default 10000)"); + l_props.put("timeout", l_timeoutProp); + + Map l_schema = new LinkedHashMap<>(); + l_schema.put("type", "object"); + l_schema.put("required", Collections.singletonList("callContent")); + l_schema.put("properties", l_props); + return Collections.unmodifiableMap(l_schema); + } + + private static Map buildDiagnosticsSchemaMap() { + Map l_schema = new LinkedHashMap<>(); + l_schema.put("type", "object"); + l_schema.put("properties", new LinkedHashMap<>()); + l_schema.put("required", Collections.emptyList()); + return Collections.unmodifiableMap(l_schema); + } private final ObjectMapper mapper = new ObjectMapper(); private final List> toolList; private final int discoveredToolCount; + private final Map methodRegistry; + private final Map> toolDefinitions; /** * Constructs the handler, performs tool discovery from IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES, - * and builds a single {@code java_call} tool whose description embeds a catalog of all - * discovered methods. + * and exposes each discovered method as its own MCP tool in addition to {@code java_call} + * and {@code ibs_diagnostics}. */ public MCPRequestHandler() { MCPToolDiscovery.DiscoveryResult discovery = MCPToolDiscovery.discoverTools( ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.fetchValue()); - String catalog = buildCatalog(discovery.tools, discovery.methodRegistry); this.discoveredToolCount = discovery.methodRegistry.size(); + this.methodRegistry = Collections.unmodifiableMap(new LinkedHashMap<>(discovery.methodRegistry)); + Map> defs = new LinkedHashMap<>(); List> tools = new ArrayList<>(); - try { - Map javaCallTool = new LinkedHashMap<>(); - javaCallTool.put("name", JAVA_CALL_TOOL_NAME); - javaCallTool.put("description", buildJavaCallDescription(catalog)); - javaCallTool.put("inputSchema", mapper.readValue(JAVA_CALL_TOOL_SCHEMA, Map.class)); - tools.add(javaCallTool); - - Map diagnosticsTool = new LinkedHashMap<>(); - diagnosticsTool.put("name", DIAGNOSTICS_TOOL_NAME); - diagnosticsTool.put("description", - "Built-in IBS diagnostic tool. Returns IBS version, MCP config state, " - + "and header classification: secret key names (values suppressed), " - + "env-var key+value pairs (decoded: prefix stripped, uppercased), " - + "and regular header count. No arguments required. " - + "Does not depend on HOST packages — always available."); - diagnosticsTool.put("inputSchema", mapper.readValue(DIAGNOSTICS_TOOL_SCHEMA, Map.class)); - tools.add(diagnosticsTool); - } catch (JsonProcessingException e) { - log.error("Failed to parse tool schema — one or more tools will not be available.", e); + + for (Map lt_toolDef : discovery.tools) { + String lt_name = (String) lt_toolDef.get("name"); + defs.put(lt_name, lt_toolDef); + tools.add(lt_toolDef); } + this.toolDefinitions = Collections.unmodifiableMap(defs); + + Map l_javaCallTool = new LinkedHashMap<>(); + l_javaCallTool.put("name", JAVA_CALL_TOOL_NAME); + l_javaCallTool.put("description", buildJavaCallDescription()); + l_javaCallTool.put("inputSchema", JAVA_CALL_SCHEMA_MAP); + tools.add(l_javaCallTool); + + Map l_diagnosticsTool = new LinkedHashMap<>(); + l_diagnosticsTool.put("name", DIAGNOSTICS_TOOL_NAME); + l_diagnosticsTool.put("description", + "Built-in IBS diagnostic tool. Returns IBS version, MCP config state, " + + "and header classification: secret key names (values suppressed), " + + "env-var key+value pairs (decoded: prefix stripped, uppercased), " + + "and regular header count. No arguments required. " + + "Does not depend on HOST packages — always available."); + l_diagnosticsTool.put("inputSchema", DIAGNOSTICS_SCHEMA_MAP); + tools.add(l_diagnosticsTool); this.toolList = Collections.unmodifiableList(tools); - log.info("MCPRequestHandler ready: {} method(s) in catalog via java_call.", discoveredToolCount); + log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics.", + discoveredToolCount); } /** @@ -199,80 +232,24 @@ private String handleToolCall(Object id, Map params, Map> tools, Map methodRegistry) { - if (methodRegistry.isEmpty()) { - return ""; + if (methodRegistry.containsKey(toolName)) { + return handleIndividualToolCall(id, toolName, arguments, headers); } - StringBuilder sb = new StringBuilder(); - sb.append("Discovered methods (use class/method values in callContent for java_call):\n\n"); - for (Map.Entry entry : methodRegistry.entrySet()) { - String toolName = entry.getKey(); - Method method = entry.getValue(); - - sb.append(toolName).append("\n"); - sb.append(" class: ").append(method.getDeclaringClass().getName()).append("\n"); - sb.append(" method: ").append(method.getName()).append("\n"); - - Map toolDef = tools.stream() - .filter(t -> toolName.equals(t.get("name"))) - .findFirst() - .orElse(null); - - if (toolDef != null) { - String desc = (String) toolDef.get("description"); - if (desc != null && !desc.isEmpty()) { - sb.append(" ").append(desc).append("\n"); - } - @SuppressWarnings("unchecked") - Map schema = (Map) toolDef.get("inputSchema"); - if (schema != null) { - @SuppressWarnings("unchecked") - Map props = (Map) schema.get("properties"); - if (props == null || props.isEmpty()) { - sb.append(" args: (none)\n"); - } else { - for (Map.Entry propEntry : props.entrySet()) { - @SuppressWarnings("unchecked") - Map propSchema = (Map) propEntry.getValue(); - String type = (String) propSchema.get("type"); - String propDesc = (String) propSchema.get("description"); - sb.append(" ").append(propEntry.getKey()) - .append(" (").append(type).append("): ") - .append(propDesc).append("\n"); - } - } - } - } - sb.append("\n"); - } - return sb.toString().trim(); + return buildCallToolResult(id, "Unknown tool: " + toolName + + ". Use tools/list to see all available tools.", + true); } - /** - * Assembles the full java_call tool description, combining the base usage guidance with - * the auto-discovered method catalog (when methods are found in the configured packages). - */ - private String buildJavaCallDescription(String catalog) { - String base = "Generic BridgeService call. Accepts the full /call payload including call chaining, " - + "instance methods, environment variables, and timeout. " - + "Bundle all operations into one callContent chain so they share a single isolated " - + "execution context. State (including authentication) does not persist between " - + "separate tool calls."; - if (catalog.isEmpty()) { - return base; - } - return base + "\n\n" + catalog; + private String buildJavaCallDescription() { + return "Generic BridgeService call for multi-step chains. " + + "Accepts the full /call payload including call chaining, instance methods, " + + "environment variables, and timeout. Bundle all operations into one callContent " + + "chain so they share a single isolated execution context. " + + "State (including authentication) does not persist between separate tool calls.\n\n" + + "Use individual tools in tools/list for single stateless method calls. " + + "Use java_call when step B needs the Java object returned by step A, " + + "or for overloaded/instance methods not available as individual tools."; } /** @@ -408,6 +385,31 @@ private String handleDiagnostics(Object id, Map headers) { } } + @SuppressWarnings("unchecked") + private String handleIndividualToolCall(Object in_id, String in_toolName, + Map in_arguments, Map in_headers) { + Method l_method = methodRegistry.get(in_toolName); + Map l_toolDef = toolDefinitions.get(in_toolName); + Map l_schema = (Map) l_toolDef.get("inputSchema"); + List l_required = (List) l_schema.getOrDefault("required", + Collections.emptyList()); + + List l_args = new ArrayList<>(); + for (String lt_paramName : l_required) { + l_args.add(in_arguments.get(lt_paramName)); + } + + Map l_callEntry = new LinkedHashMap<>(); + l_callEntry.put("class", l_method.getDeclaringClass().getName()); + l_callEntry.put("method", l_method.getName()); + l_callEntry.put("args", l_args); + + Map l_syntheticArgs = new LinkedHashMap<>(); + l_syntheticArgs.put("callContent", Collections.singletonMap("result", l_callEntry)); + + return handleJavaCall(in_id, l_syntheticArgs, in_headers); + } + private String buildResult(Object id, Object result) { Map response = new LinkedHashMap<>(); response.put("jsonrpc", JSONRPC_VERSION); diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java index 80e1725..a0fb34c 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java @@ -79,8 +79,6 @@ public void testInitialize_returnsProtocolVersion() { @Test(groups = "MCP") public void testToolsList_returnsDiscoveredTools() { - // Only java_call is a callable tool; the catalog of discovered methods is embedded - // in its description so the LLM can construct the right callContent payload. given() .contentType(CONTENT_TYPE_JSON) .body("{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}") @@ -88,10 +86,8 @@ public void testToolsList_returnsDiscoveredTools() { .post(MCP_ENDPOINT) .then() .statusCode(200) - .body("result.tools", hasSize(2)) - .body("result.tools.name", hasItem("java_call")) - .body("result.tools.find { it.name == 'java_call' }.description", - containsString("SimpleStaticMethods_methodReturningString")); + .body("result.tools.name", hasItems("java_call", "ibs_diagnostics")) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodReturningString")); } @Test(groups = "MCP") @@ -110,7 +106,7 @@ public void testToolsList_eachToolHasRequiredFields() { @Test(groups = "MCP") public void testToolsList_descriptionComesFromJavadoc() { - // The catalog entry for methodReturningString uses its Javadoc text, not the + // The individual tool for methodReturningString uses its Javadoc text, not the // fallback "Calls com.example.MyClass.methodName()" string. given() .contentType(CONTENT_TYPE_JSON) @@ -119,30 +115,28 @@ public void testToolsList_descriptionComesFromJavadoc() { .post(MCP_ENDPOINT) .then() .statusCode(200) - .body("result.tools.find { it.name == 'java_call' }.description", + .body("result.tools.find { it.name == 'SimpleStaticMethods_methodReturningString' }.description", containsString("success string")); } @Test(groups = "MCP") public void testToolsList_noArgToolHasEmptyProperties() { - Response resp = given() + given() .contentType(CONTENT_TYPE_JSON) .body("{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/list\",\"params\":{}}") .when() .post(MCP_ENDPOINT) .then() .statusCode(200) - .extract().response(); - - // methodReturningString is in the catalog — its entry must appear in java_call description - String desc = resp.path("result.tools.find { it.name == 'java_call' }.description"); - assertThat(desc, containsString("SimpleStaticMethods_methodReturningString")); + .body("result.tools.name", hasItem("SimpleStaticMethods_methodReturningString")) + .body("result.tools.find { it.name == 'SimpleStaticMethods_methodReturningString' }.inputSchema.required", + nullValue()); } @Test(groups = "MCP") public void testToolsList_undocumentedMethodExcluded() { // EnvironmentVariableHandler methods have no Javadoc — must be absent from the - // catalog (IBS.MCP.REQUIRE_JAVADOC defaults to true). + // tools list (IBS.MCP.REQUIRE_JAVADOC defaults to true). given() .contentType(CONTENT_TYPE_JSON) .body("{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"tools/list\",\"params\":{}}") @@ -150,11 +144,9 @@ public void testToolsList_undocumentedMethodExcluded() { .post(MCP_ENDPOINT) .then() .statusCode(200) - .body("result.tools", hasSize(2)) - .body("result.tools.find { it.name == 'java_call' }.description", - not(containsString("EnvironmentVariableHandler_getCacheProperty"))) - .body("result.tools.find { it.name == 'java_call' }.description", - not(containsString("EnvironmentVariableHandler_setIntegroCache"))); + .body("result.tools.name", hasItems("java_call", "ibs_diagnostics")) + .body("result.tools.name", not(hasItem("EnvironmentVariableHandler_getCacheProperty"))) + .body("result.tools.name", not(hasItem("EnvironmentVariableHandler_setIntegroCache"))); } // ---- ibs_diagnostics tool ---- @@ -168,8 +160,7 @@ public void testToolsList_includesDiagnosticsTool() { .post(MCP_ENDPOINT) .then() .statusCode(200) - .body("result.tools", hasSize(2)) - .body("result.tools.name", hasItem("ibs_diagnostics")) + .body("result.tools.name", hasItems("java_call", "ibs_diagnostics")) .body("result.tools.find { it.name == 'ibs_diagnostics' }.description", notNullValue()) .body("result.tools.find { it.name == 'ibs_diagnostics' }.inputSchema", notNullValue()); } @@ -903,4 +894,179 @@ public void testNotification_returns202() { .then() .statusCode(202); } + + // ---- individual tool routing ---- + + @Test(groups = "MCP") + public void testToolsList_exposesIndividualDiscoveredTools() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":100,\"method\":\"tools/list\",\"params\":{}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodReturningString")) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingStringArgument")) + .body("result.tools.find { it.name == 'SimpleStaticMethods_methodReturningString' }.inputSchema", + notNullValue()) + .body("result.tools.find { it.name == 'SimpleStaticMethods_methodAcceptingStringArgument' }.inputSchema.required", + hasItem("arg0")); + } + + @Test(groups = "MCP") + public void testIndividualTool_noArgMethod_returnsResult() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":101,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"SimpleStaticMethods_methodReturningString\"," + + "\"arguments\":{}}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(false)) + .body("result.content[0].text", containsString("_Success")); + } + + @Test(groups = "MCP") + public void testIndividualTool_stringArgMethod_returnsResult() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":102,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"SimpleStaticMethods_methodAcceptingStringArgument\"," + + "\"arguments\":{\"arg0\":\"hello\"}}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(false)) + .body("result.content[0].text", containsString("hello_Success")); + } + + @Test(groups = "MCP") + public void testIndividualTool_intArgMethod_returnsResult() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":103,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"SimpleStaticMethods_methodAcceptingIntArgument\"," + + "\"arguments\":{\"arg0\":42}}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(false)) + .body("result.content[0].text", containsString("126")); + } + + @Test(groups = "MCP") + public void testIndividualTool_twoArgMethod_argOrderPreserved() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":104,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"SimpleStaticMethods_methodAcceptingTwoArguments\"," + + "\"arguments\":{\"arg0\":\"foo\",\"arg1\":\"bar\"}}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(false)) + .body("result.content[0].text", containsString("foo+bar_Success")); + } + + @Test(groups = "MCP") + public void testIndividualTool_methodThrowsException_returnsIsError() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":105,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"SimpleStaticMethods_methodThrowsException\"," + + "\"arguments\":{}}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(true)) + .body("result.content[0].text", containsString("\"title\"")) + .body("result.content[0].text", containsString("\"originalException\"")); + } + + @Test(groups = "MCP") + public void testJavaCallDescription_doesNotContainCatalog() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":106,\"method\":\"tools/list\",\"params\":{}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.tools.find { it.name == 'java_call' }.description", + not(containsString("Discovered methods"))); + } + + @Test(groups = "MCP") + public void testIndividualTool_prechainIsApplied() { + ConfigValueHandlerIBS.MCP_PRECHAIN.activate( + "{\"ibs_pre\":{\"class\":\"com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods\"," + + "\"method\":\"methodReturningString\",\"args\":[]}}"); + try { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":107,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"SimpleStaticMethods_methodReturningString\"," + + "\"arguments\":{}}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(false)) + .body("result.content[0].text", not(containsString("\"ibs_pre\""))) + .body("result.content[0].text", containsString("\"result\"")); + } finally { + ConfigValueHandlerIBS.MCP_PRECHAIN.reset(); + } + } + + // ---- constructor and complex object chaining ---- + + @Test(groups = "MCP") + public void testToolsList_constructorsNotExposedAsIndividualTools() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":108,\"method\":\"tools/list\",\"params\":{}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.tools.name", not(hasItem("Instantiable_Instantiable"))); + } + + @Test(groups = "MCP") + public void testJavaCall_constructorChain_instantiableThenStaticMethod() { + // Constructors are not available as individual tools — use java_call to instantiate + // and then pass the result object by reference to a static method in the same chain. + String payload = "{\"jsonrpc\":\"2.0\",\"id\":109,\"method\":\"tools/call\"," + + "\"params\":{\"name\":\"java_call\"," + + "\"arguments\":{" + + "\"callContent\":{" + + "\"obj\":{" + + "\"class\":\"com.adobe.campaign.tests.bridge.testdata.one.Instantiable\"," + + "\"method\":\"Instantiable\"," + + "\"args\":[\"hello\"]" + + "}," + + "\"fetch\":{" + + "\"class\":\"com.adobe.campaign.tests.bridge.testdata.one.StaticType\"," + + "\"method\":\"fetchInstantiableStringValue\"," + + "\"args\":[\"obj\"]" + + "}}}}}"; + + given() + .contentType(CONTENT_TYPE_JSON) + .body(payload) + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.isError", equalTo(false)) + .body("result.content[0].text", containsString("hello")); + } } From 1744ff5b04c6b21ad0505cfaf6bdd3195b73dbcd Mon Sep 17 00:00:00 2001 From: baubakg Date: Tue, 21 Apr 2026 18:33:12 +0200 Subject: [PATCH 21/37] Prepare release 3.11.2 - Add 3.11.2 release notes entry (hybrid MCP tool discovery, dependency bumps, CI) - Update version references in README and docs/MCP.md from 3.11.1 to 3.11.2 Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 ++-- ReleaseNotes.md | 5 +++++ docs/MCP.md | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index afffa69..1eff90d 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ The following dependency needs to be added to your pom file: com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.1 + 3.11.2 ``` @@ -924,7 +924,7 @@ Response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.1" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.2" }, "capabilities": { "tools": {} } } } diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 7083376..9f1c0b6 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,9 @@ # Bridge Service - RELEASE NOTES +## 3.11.2 +* **MCP** [#35 Hybrid method discovery](https://github.com/adobe/bridgeService/pull/35) Each auto-discovered public static method is now exposed as its own named MCP tool in `tools/list`, enabling direct invocation without a `java_call` wrapper. `java_call` is retained for multi-step chains where steps share state or pass complex Java objects between them. +* **Dependency Updates** Routine dependency bumps: log4j2, mockito-core, testng. +* **CI** Updated `actions/checkout` to v6; fixed SonarCloud and Codecov badge staleness. + ## 3.11.1 * **MCP** [#12 Expose BridgeService as an MCP Server](https://github.com/adobe/bridgeService/issues/12) Extended MCP documentation. See [docs/MCP.md](docs/MCP.md) for the full reference. * **Dependency Updates** Routine dependency and plugin version bumps. diff --git a/docs/MCP.md b/docs/MCP.md index 6179058..ccdca8f 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -102,7 +102,7 @@ Expected response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.1" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.2" }, "capabilities": { "tools": {} } } } @@ -553,7 +553,7 @@ Response: ```json { - "ibsVersion": "3.11.1", + "ibsVersion": "3.11.2", "deploymentMode": "TEST", "mcpConfig": { "packagesConfigured": "com.example.services", @@ -797,7 +797,7 @@ and start the server from within it. com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.1 + 3.11.2 ``` From 109d273d46f222126432e4838f8c9531e82a722b Mon Sep 17 00:00:00 2001 From: baubakg Date: Tue, 21 Apr 2026 18:34:07 +0200 Subject: [PATCH 22/37] Document release process in CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 076cb1c..019a2d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,6 +122,45 @@ Apply these prefixes consistently in all new and modified Java code: | `lt_` | Variables scoped to a loop or condition block (do not escape the block) | `lt_entry`, `lt_key` | | *(none)* | `for` loop counters | `i`, `j` | +## Release Process + +The release branch is `release`. Releases are cut from that branch using the Maven Release Plugin. + +### Steps to prepare a release + +1. **Switch to `release` and merge `main`** + ```bash + git checkout release + git merge main --no-edit + ``` + Conflicts are expected in the POM files (version number) and occasionally in CI workflow files. Always take the `main` side: + - POMs: keep `X.Y.Z-SNAPSHOT` from `main` + - Workflows: keep whatever version `main` has (e.g. `actions/checkout@v6`) + +2. **Add a release notes entry** at the top of `ReleaseNotes.md` (below the `# Bridge Service - RELEASE NOTES` heading). The title is the release version **without** `-SNAPSHOT`. Keep entries concise — one bullet per logical change group. + +3. **Update version references in the docs** — search for the previous release version string and replace with the new one: + ```bash + sed -i '' 's/X\.Y\.old/X.Y.new/g' README.md docs/MCP.md + ``` + Typical locations: Maven dependency snippet, `serverInfo` JSON examples, `ibsVersion` diagnostic example. + +4. **Commit and push** + ```bash + git add ReleaseNotes.md README.md docs/MCP.md + git commit -m "Prepare release X.Y.Z" + git push --set-upstream origin release + ``` + +5. **Trigger the release** via the Maven Release Plugin (run by CI or manually): + ```bash + mvn release:prepare release:perform + ``` + +### Notes +- The next development version in the POMs on `main` is already set to `X.Y.Z-SNAPSHOT` by the Maven Release Plugin after the previous release; do not change it manually. +- If `git push` is rejected as non-fast-forward, use `git pull --rebase origin release` then re-apply the release notes and doc version changes (rebase drops merge commits). + ## Contribution Rules - All new source files must have the Adobe license header (`mvn license:format` adds it). From 7828a28946956a26d0a81c49b4420eade66dbce4 Mon Sep 17 00:00:00 2001 From: baubakg Date: Wed, 22 Apr 2026 22:05:56 +0200 Subject: [PATCH 23/37] Update README MCP section for hybrid tool discovery (3.11.2) The Discovering Tools and Calling a Discovered Tool sections still described the old single-java_call approach. Updated to reflect that tools/list now returns one named tool per auto-discovered method plus java_call and ibs_diagnostics, and that individual tools can be called directly for stateless single-method invocations. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1eff90d..4cd27a7 100644 --- a/README.md +++ b/README.md @@ -894,7 +894,7 @@ Set the environment variable `IBS.MCP.ENABLED` to `true` before starting BridgeS mvn exec:java -Dexec.args="test" -DIBS.MCP.ENABLED=true -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.example.mypackage ``` -At startup, BridgeService scans the packages listed in `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` and builds a **method catalog** from every **public static method** found. The catalog is embedded in the `java_call` tool description so that AI agents can read it via `tools/list`. +At startup, BridgeService scans the packages listed in `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` and builds a **method catalog** from every **public static method** found. Each discovered method is exposed as its own named MCP tool in `tools/list`. A generic `java_call` tool is always present for multi-step chains and instance method calls. The MCP endpoint is available at: ``` @@ -932,7 +932,7 @@ Response: ### Discovering Tools (tools/list) -`tools/list` always returns exactly **one tool — `java_call`**. Its `description` contains the full catalog of all discovered methods. AI agents read the catalog to learn which class and method names to place in their `callContent` payloads. +`tools/list` returns **one tool per auto-discovered method** plus `java_call` (for multi-step chains) and `ibs_diagnostics`. Tools are named `{SimpleClassName}_{methodName}` and carry their own `inputSchema` and Javadoc-sourced description. ```json { @@ -951,21 +951,35 @@ Response (abbreviated): "id": 2, "result": { "tools": [ + { + "name": "SimpleStaticMethods_methodAcceptingStringArgument", + "description": "Appends the success suffix to the given string.", + "inputSchema": { + "type": "object", + "properties": { "arg0": { "type": "string", "description": "the input string" } }, + "required": ["arg0"] + } + }, { "name": "java_call", - "description": "Generic BridgeService call. ...\n\nDiscovered methods:\n\nSimpleStaticMethods_methodAcceptingStringArgument\n class: com.example.SimpleStaticMethods\n method: methodAcceptingStringArgument\n Appends the success suffix to the given string.\n arg0 (string): the input string\n...", + "description": "Generic BridgeService call for multi-step chains. ...", "inputSchema": { "..." : "..." } + }, + { + "name": "ibs_diagnostics", + "description": "Built-in IBS diagnostic tool. ...", + "inputSchema": { "type": "object", "properties": {} } } ] } } ``` -Each catalog entry follows the format `{SimpleClassName}_{methodName}` and includes the fully qualified class name, method name, Javadoc description, and parameter descriptions. See [docs/MCP.md](docs/MCP.md) for the full catalog format. +See [docs/MCP.md](docs/MCP.md) for the full tool format and method discovery details. ### Calling a Discovered Tool (tools/call) -All calls go through `java_call`. Use the class and method names from the catalog in `callContent`: +Single-method calls can be made directly by tool name. Pass arguments as flat key-value pairs matching the `inputSchema`: ```json { @@ -973,16 +987,8 @@ All calls go through `java_call`. Use the class and method names from the catalo "id": 3, "method": "tools/call", "params": { - "name": "java_call", - "arguments": { - "callContent": { - "result": { - "class": "com.example.SimpleStaticMethods", - "method": "methodAcceptingStringArgument", - "args": ["hello"] - } - } - } + "name": "SimpleStaticMethods_methodAcceptingStringArgument", + "arguments": { "arg0": "hello" } } } ``` @@ -1002,6 +1008,15 @@ On success the result contains the standard BridgeService return payload seriali If the method throws an exception, `isError` is `true` and `content[0].text` contains the error description. The HTTP status code is always `200` for `tools/call` — errors are reported inside the MCP result, not as HTTP errors. +**When to use individual tools vs `java_call`:** + +| Scenario | Use | +|---|---| +| Single stateless read | Individual tool (`ClassName_methodName`) | +| Step B needs the Java object returned by step A | `java_call` with call chain | +| Overloaded method (same parameter count) | `java_call` | +| Instance method or constructor | `java_call` | + ### The `java_call` Tool `java_call` accepts the same payload as the standard `POST /call` endpoint, making call chaining, instance methods, environment variables, and file uploads all accessible to MCP clients. **Bundle all related operations into a single `java_call`** using call chaining — static variable state (including authentication) does not persist between separate tool calls. From 4d6d8623e3797193fe6efcc15368ecdc2da0a380 Mon Sep 17 00:00:00 2001 From: baubakg Date: Wed, 22 Apr 2026 22:08:38 +0200 Subject: [PATCH 24/37] Fix stale version in /test endpoint response example (3.11.2) The health check response example still showed 2.11.16 instead of 3.11.2. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cd27a7..97eb6f4 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ If all is good you should get: ``` All systems up - in production -Version : 2.11.16 +Version : 3.11.2 Product user version : 7.0 ``` From 567463da752d1244501014b87b1e073e3ae23c7d Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Wed, 22 Apr 2026 20:36:46 +0000 Subject: [PATCH 25/37] [maven-release-plugin] prepare release parent-3.11.2 --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index d71beb9..de83179 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.2-SNAPSHOT + 3.11.2 diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index dc7f121..cf65330 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -185,6 +185,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.2-SNAPSHOT + 3.11.2 diff --git a/pom.xml b/pom.xml index a94673a..93b0356 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.2-SNAPSHOT + 3.11.2 Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - HEAD + parent-3.11.2 From bd5f9f9ca82d7344785ccacffa2dc17210645ce4 Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Wed, 22 Apr 2026 20:36:48 +0000 Subject: [PATCH 26/37] [maven-release-plugin] prepare for next development iteration --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index de83179..95fb6f2 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.2 + 3.11.3-SNAPSHOT diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index cf65330..78bc2f9 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -185,6 +185,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.2 + 3.11.3-SNAPSHOT diff --git a/pom.xml b/pom.xml index 93b0356..708997b 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.2 + 3.11.3-SNAPSHOT Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - parent-3.11.2 + HEAD From 4f00906c52fc968403997146d700caee287d55e2 Mon Sep 17 00:00:00 2001 From: Baubak Gandomi Date: Mon, 4 May 2026 17:20:23 +0200 Subject: [PATCH 27/37] Migrate HTTP layer from Spark Java to Javalin 6 (issue #38) (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Java version migration research document for issue #25 Captures compatibility analysis across four scenarios (host on Java 17+, host on Java 8, IBS upgraded to Java 17, why not Java 21) and a recommended two-step migration path: Java 17 classifier JAR first, Spring Boot 3.x replacement second. Co-Authored-By: Claude Sonnet 4.6 * Expand Java migration research with compatibility matrix, LTS analysis, and revised two-priority plan Co-Authored-By: Claude Sonnet 4.6 * Update files-to-change section to reflect Javalin migration scope Co-Authored-By: Claude Sonnet 4.6 * Migrate HTTP layer from Spark Java to Javalin 6 (issue #38) - Replace spark-core 2.9.4 + javax.servlet-api with javalin 6.3.0 + ssl-plugin 6.3.0 - Rewrite IntegroAPI: Spark static DSL → Javalin instance API; multipart via jakarta.servlet Part; SSL via SslPlugin; startServices() now returns Javalin instance - Update MCPRequestHandler: handle(Request, Response) → handle(Context) - Add setAccessible(true) in CallContent for Java 17+ strong encapsulation - Update 4 test files: replace Spark.awaitInitialization/stop with Javalin app.stop() All 279 tests pass. Co-Authored-By: Claude Sonnet 4.6 * Add tests to improve patch coverage for Javalin migration - MCPRequestHandler: cover parse error (malformed JSON), missing method field, missing tool name, and .well-known/oauth-authorization-server endpoint - IntegroAPI: cover IBSRunTimeException handler via invalid DURATION assertion key 284 tests pass. Co-Authored-By: Claude Sonnet 4.6 * Update docs for Javalin 6 migration (issue #38) - README.md: replace Spark/javax.servlet conflict note with Javalin 6 / jakarta.servlet guidance - docs/Technical.md: add HTTP Framework section documenting Javalin 6, Jetty 11, and migration differences from Spark - docs/JavaMigration.md: mark Priority 1 as DONE, update compatibility matrix to show Javalin 6 as current Co-Authored-By: Claude Sonnet 4.6 * Add Java version compatibility matrix to README Placed directly after the integration model descriptions so users can immediately see which IBS release works with their Java version. Links to docs/JavaMigration.md for the full analysis and Priority 2 roadmap. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .gitignore | 1 + README.md | 34 ++- docs/JavaMigration.md | 274 ++++++++++++++++++ docs/Technical.md | 23 ++ integroBridgeService/pom.xml | 17 +- .../tests/bridge/service/CallContent.java | 3 +- .../tests/bridge/service/IntegroAPI.java | 272 +++++++++-------- .../bridge/service/MCPRequestHandler.java | 45 +-- .../tests/bridge/service/E2EPortCheck.java | 2 - .../tests/bridge/service/E2ETests.java | 24 +- .../bridge/service/LogManagementTest.java | 2 - .../bridge/service/MCPBridgeServerTest.java | 64 +++- 12 files changed, 568 insertions(+), 193 deletions(-) create mode 100644 docs/JavaMigration.md diff --git a/.gitignore b/.gitignore index a94785d..4463458 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ ibs_output *.dtmp /integroBridgeService/profile.jfr /integroBridgeService/.profileconfig.json +.vscode diff --git a/README.md b/README.md index 97eb6f4..4deffd2 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ from any language or framework you are in. * [Installation](#installation) * [Considerations](#considerations) * [Including your project in the BridgeService](#including-your-project-in-the-bridgeservice) + * [Java Version Compatibility](#java-version-compatibility) * [Starting the Bridge Service](#starting-the-bridge-service) * [Running the Bridge Locally](#running-the-bridge-locally) * [Running a DEMO](#running-a-demo) @@ -116,11 +117,9 @@ The following dependency needs to be added to your pom file: #### Considerations -Since the BridgeService uses Jetty and java Spark, it is quite possible that there maybe conflicts in the project when -you add this library. Most importantly you will need to ensure that `javax.servlet` is set to "**compile**" in your -maven scope. - -We have found it simplest to simply add that library directly in the pom file with the scope "**compile**". +Since the BridgeService uses Jetty (via Javalin 6) it is quite possible that there may be conflicts in the project when +you add this library. BridgeService 3.12+ uses the `jakarta.servlet` namespace (Jetty 11). If your project still uses +`javax.servlet`, you will need to migrate to `jakarta.servlet` or ensure the two are isolated on the classpath. ### Including your project in the BridgeService @@ -128,6 +127,31 @@ In this model you can simply add your project as a dependency to the BridgeProje ![BridgeService Aggregator Model](diagrams/Processes-aggregatorModel.drawio.png) +### Java Version Compatibility + +Legend: ✅ Works  |  ⚠️ Works with workarounds  |  ❌ Fails + +**Injection Model** — the exposed project's JVM loads IBS. Both IBS bytecode and the HTTP framework must be compatible with that JVM. + +| IBS release | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | +|---|:---:|:---:|:---:|:---:| +| **pre-3.12** (Spark, Java 11) | ❌ ¹ | ✅ | ⚠️ ² | ❌ ³ | +| **3.12+** (Javalin 6, Java 11) | ❌ ¹ | ✅ | ✅ | ✅ | + +**Aggregator Model** — IBS runs its own JVM and loads the exposed project's classes via reflection. + +| IBS release | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | +|---|:---:|:---:|:---:|:---:| +| **pre-3.12** (Spark, Java 11) | ✅ | ✅ | ❌ ⁴ | ❌ ⁴ | +| **3.12+** (Javalin 6, Java 11) | ✅ | ✅ | ❌ ⁴ | ❌ ⁴ | + +¹ Exposed project JVM cannot load IBS bytecode — `UnsupportedClassVersionError`. +² Spark is unofficial on Java 17 and requires `--add-opens` flags. +³ Spark is not compatible with Java 21. +⁴ IBS JVM (Java 11) cannot load class files compiled to Java 17+ — `UnsupportedClassVersionError` in the class loader. + +For the full analysis and the planned Java 21 upgrade (issue #39), see [docs/JavaMigration.md](docs/JavaMigration.md). + ## Starting the Bridge Service When deploying this as a project we run this as an executable jar. This is usually done in a Docker image. diff --git a/docs/JavaMigration.md b/docs/JavaMigration.md new file mode 100644 index 0000000..aa7e078 --- /dev/null +++ b/docs/JavaMigration.md @@ -0,0 +1,274 @@ +# Java Version Migration — Research & Plan (Issue #25) + +BridgeService is currently compiled to Java 11. This document captures the complexity analysis and recommended migration path for supporting other Java versions, both in the IBS runtime and in host projects. + +--- + +## Integration Models + +The README defines two ways to integrate IBS (see [Implementing The Bridge Service in Your Project](../README.md#implementing-the-bridge-service-in-your-project)): + +- **Injection Model** *(recommended)*: IBS is added as a Maven compile-scope dependency to the host project. The host's JVM loads IBS classes directly. Example: **v6SOAPAPI** (`Adobe-Campaign/pom.xml`). +- **Aggregator Model**: The host project is added as a Maven dependency to IBS. IBS's own JVM loads the host's classes via reflection. + +This distinction is critical for Java version compatibility — which JVM loads whose bytecode determines what breaks. + +--- + +## Compatibility Matrix + +The two axes are: +- **IBS version + framework** — what IBS is compiled for and what HTTP framework it uses +- **Exposed project version** — the Java version of the project whose classes IBS exposes via reflection + +Legend: ✅ Works  |  ⚠️ Works with workarounds  |  ❌ Fails + +### Injection Model + +The exposed project's JVM runs IBS. Both the bytecode and the HTTP framework must be compatible with that JVM. + +| IBS version + framework | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | +|---|:---:|:---:|:---:|:---:| +| **Java 11 + Spark** (pre-3.12) | ❌ ¹ | ✅ | ⚠️ ² | ❌ ³ | +| **Java 11 + Javalin 6** *(current)* | ❌ ¹ | ✅ | ✅ ⁴ | ✅ ⁴ | +| **Java 17 + Spring Boot 3.x** | ❌ ¹ | ❌ ¹ | ✅ ⁴ | ✅ ⁴ | +| **Java 17 + Javalin 6** | ❌ ¹ | ❌ ¹ | ✅ ⁴ | ✅ ⁴ | +| **Java 17 + Javalin 7** | ❌ ¹ | ❌ ¹ | ✅ ⁴ | ✅ ⁴ | +| **Java 21 + Spring Boot 3.x** | ❌ ¹ | ❌ ¹ | ❌ ¹ | ✅ ⁴ | +| **Java 21 + Javalin 6** | ❌ ¹ | ❌ ¹ | ❌ ¹ | ✅ ⁴ | +| **Java 21 + Javalin 7** | ❌ ¹ | ❌ ¹ | ❌ ¹ | ✅ ⁴ | + +¹ Exposed project JVM cannot load IBS bytecode — `UnsupportedClassVersionError`. +² Bytecode loads fine on Java 17 JVM, but Spark is unofficial on Java 17 and needs `--add-opens` flags. +³ Bytecode loads fine on Java 21 JVM, but Spark is not compatible with Java 21. +⁴ Requires `setAccessible(true)` fix in `CallContent.java:171-172` (Java 17+ strong encapsulation). + +### Aggregator Model + +IBS runs its own JVM and loads the exposed project's classes via reflection. The HTTP framework runs on IBS's JVM; the exposed project's bytecode must be loadable by that JVM. + +| IBS version + framework | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | +|---|:---:|:---:|:---:|:---:| +| **Java 11 + Spark** (pre-3.12) | ✅ | ✅ | ❌ ⁵ | ❌ ⁵ | +| **Java 11 + Javalin 6** *(current)* | ✅ | ✅ | ❌ ⁵ | ❌ ⁵ | +| **Java 17 + Spring Boot 3.x** | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | ❌ ⁵ | +| **Java 17 + Javalin 6** | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | ❌ ⁵ | +| **Java 17 + Javalin 7** | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | ❌ ⁵ | +| **Java 21 + Spring Boot 3.x** | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | +| **Java 21 + Javalin 6** | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | +| **Java 21 + Javalin 7** | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | ✅ ⁴ | + +⁴ Requires `setAccessible(true)` fix in `CallContent.java:171-172` (Java 17+ strong encapsulation). +⁵ IBS JVM cannot load the exposed project's class files — `UnsupportedClassVersionError` in `IntegroBridgeClassLoader.defineClass()`. + +--- + +## Scenario A — IBS runs inside a host project with a Higher Java version + +*Injection Model: host is Java 17+, IBS is Java 11.* + +| # | File | Line | Issue | Severity | +|---|------|------|-------|----------| +| 1 | `CallContent.java` | 171 | `getDeclaredConstructor().newInstance()` — no `setAccessible(true)`. Java 17+ strong encapsulation throws `InaccessibleObjectException` if the class's package is not opened. | **Critical** | +| 2 | `CallContent.java` | 172 | `l_method.invoke(ourInstance, ...)` — no `setAccessible(true)`. Same module-encapsulation risk for non-public methods. | **High** | +| 3 | `IntegroBridgeClassLoader.java` | 69–73 | `defineClass()` reads raw bytecode. If the host's classes are compiled to Java 17+ (class file version 61+), loading them into a Java-11-compiled IBS context throws `UnsupportedClassVersionError`. | **High** | +| 4 | CI workflows | `onPushSimpleTest.yml:37` | Only Java 11 is tested. `maven-pr-analyze.yml` uses Java 17 for SonarCloud but does not run tests — Java 17 breakage goes undetected. | **Medium** | + +**Required fixes:** + +1. Add `setAccessible(true)` in `CallContent.java:171-172` before `newInstance()` and `invoke()`. Catch `java.lang.reflect.InaccessibleObjectException` and map it to `JavaObjectInaccessibleException`. +2. Add a CI matrix in `onPushSimpleTest.yml` to test on Java 11 and 17. +3. Switch `pom.xml` from `source/target` properties to `` in the maven-compiler-plugin. Pin `maven-compiler-plugin` ≥ 3.11.0. + +--- + +## Scenario B — IBS hosted in a project with a Lower Java version (Java 8) + +*Injection Model: host requires Java 8.* + +| # | File | Lines | Issue | +|---|------|-------|-------| +| 1 | `JavaCalls.java` | 235 | `String.isBlank()` — Java 11 API only. | +| 2 | `MCPRequestHandler.java` | 287, 312, 362, 369, 387 | `String.isBlank()` — 5 occurrences. | +| 3 | `ReleaseNotes.md` | ~76 | Claims "Java 8 is also available" — outdated. Current code is not Java 8 compatible. | + +**Recommendation**: Officially drop Java 8 support. Java 11 is the minimum runtime. Fix the outdated claim in `ReleaseNotes.md`. No code changes needed. + +--- + +## Scenario C — IBS itself upgraded to Java 17 + +Java 17 is the recommended upgrade target (see Scenario D for why, not Java 21). + +### Impact on the Injection Model (v6SOAPAPI) + +A Java 11 host JVM cannot load class files compiled to Java 17 (class file version 61). This breaks v6SOAPAPI at: + +- **Compile time** (if host build JDK is 11): `javac` fails with `class file has wrong version 61.0, should be 55.0`. +- **Runtime** (if host build JDK is 17 but host JVM is 11): `UnsupportedClassVersionError` when IBS classes are first loaded. + +**Solution — Two separate JARs (Maven classifier):** + +Publish two artifacts from the same source: + +| Artifact | Target | Who uses it | +|----------|--------|-------------| +| `integroBridgeService-3.x.x.jar` (default) | `11` | Java 11 hosts (e.g. v6SOAPAPI, unchanged) | +| `integroBridgeService-3.x.x-java17.jar` (classifier `java17`) | `17` | Java 17+ hosts | + +Java 17 hosts declare: +```xml + + com.adobe.campaign.tests.bridge.service + integroBridgeService + 3.x.x + java17 + +``` + +Implemented via a Maven profile that re-runs the compiler plugin with `17` and adds `java17` to the jar plugin. Same source, two outputs, versions stay in sync. + +### Impact on the Aggregator Model + +IBS (Java 17 JVM) loading a host project's Java 11 classes — no issue. Java 17 JVM is fully backwards-compatible with Java 11 bytecode. + +--- + +## Java LTS Status (as of 2026-04-24) + +| Java version | Eclipse Temurin | Amazon Corretto | Azul Zulu | +|---|---|---|---| +| **Java 11** | Oct 2027 | Jan 2032 | Jan 2032 | +| **Java 17** | Sep 2027 | Aug 2029 | Jan 2030 | +| **Java 21** | **Oct 2029** | **Aug 2031** | **Jan 2032** | + +Key observations: +- Java 11 and Java 17 have nearly the **same Temurin expiry** (~Sep/Oct 2027, ~18 months away). Migrating to Java 17 as an intermediate step buys almost no additional runway on Temurin. +- Java 21 is the only version that meaningfully extends the LTS window (Oct 2029 on Temurin — 3.5 more years). +- Oracle JDK Java 11 premier support already ended Sep 2023; Red Hat free support ended Oct 2024. + +**Consequence**: the Java 11 LTS problem motivates combining the Java upgrade with the Javalin migration rather than treating it as a later, lower-priority step. + +--- + +## Decision + +Two separate migrations in priority order. Isolating the framework change from the Java version change limits blast radius — if something breaks, the cause is unambiguous. + +### Priority 1 — Migrate from Spark Java to Javalin 6 (issue #38, keep Java 11) + +- Spark Java is unmaintained; Jetty 9.4 is end-of-community-support. +- Javalin 6 runs on Java 11–21, ships Jetty 11 (`jakarta.*` namespace), near-identical API to Spark. +- **IBS stays on Java 11** — zero bytecode compatibility impact on existing exposed projects. +- Immediately unblocks injection into Java 17 and Java 21 exposed projects. +- Lower risk: single change variable, existing hosts require no changes. + +### Priority 2 — Upgrade IBS to Java 21 (issue #39, after Javalin is stable) + +- Java 11 and Java 17 expire at nearly the same time on Temurin (~Oct/Sep 2027) — no value in stopping at Java 17. +- Java 21 LTS runs to Oct 2029 on Temurin — the only version that materially extends the support window. +- Enables complete Aggregator Model coverage (Java 8 through Java 21 exposed projects) and Virtual Threads. +- **Breaking change for Injection Model**: Java 11 and Java 17 exposed projects can no longer use Injection — they must switch to Aggregator Model. +- Major version bump required (e.g. 4.0.0). + +### Integration model guidance after Priority 2 + +| Exposed project version | Recommended model | Reason | +|---|---|---| +| Java 8 | Aggregator | JVM cannot load Java 21 bytecode | +| Java 11 | Aggregator | JVM cannot load Java 21 bytecode | +| Java 17 | Aggregator | JVM cannot load Java 21 bytecode | +| Java 21 | Injection *(recommended)* or Aggregator | JVM can load Java 21 bytecode; Injection is simpler | + +### Java 17 vs Java 21 — why Java 21 + +| | Java 17 | Java 21 | +|---|---|---| +| Injection Model reach | Java 17 + Java 21 projects | Java 21 projects only | +| Aggregator Model reach | Java 8, 11, 17 | **Java 8, 11, 17, 21** | +| Spring Boot 3.x | ✅ | ✅ | +| LTS until | 2029 | **2031** | +| Virtual Threads | ❌ | ✅ | + +--- + +## Web Framework Alternatives + +Jetty/Spark is dropped. The replacement framework must support Java 21 and fat JAR packaging. BridgeService has ~4 HTTP endpoints — a lightweight framework fits better than a full application server. + +| Framework | Java minimum | Migration effort from Spark | Virtual Threads | Weight | Verdict | +|---|---|---|:---:|---|---| +| **Javalin 6** | Java 11 | Minimal — API mirrors Spark 1:1 | ✅ opt-in | ~0.5 MB + Jetty 11 | **Recommended** | +| **Javalin 7** | Java 17 | Minimal — API mirrors Spark 1:1 | ✅ | ~0.5 MB + Jetty 12 | Recommended if Java 17+ only | +| **Spring Boot 3.x** | Java 17 | Medium — annotation-based, full DI/autoconfigure | ✅ | ~15–20 MB | Viable; more than needed | +| **Helidon SE** | Java 11 | Low-medium — imperative, Spark-like SE API | ✅ | ~6 MB | Fallback if Javalin proves limiting | +| **Quarkus** | Java 17 | Medium-high — JAX-RS annotations, DI | ✅ | ~10 MB | Overkill for 4 endpoints | +| **Micronaut** | Java 17 | Medium-high — annotation-heavy, AOT | ✅ | ~8 MB | Same as Quarkus | +| **Vert.x** | Java 11 | High — async/reactive, rethink all handlers | ✅ | ~2 MB | Wrong paradigm | +| **Undertow standalone** | Java 11 | Very high — no routing DSL | ✅ | ~3 MB | Too low-level | + +**Javalin version summary:** + +| Javalin version | Java minimum | Jetty | Namespace | Notes | +|---|---|---|---|---| +| **4.x** | Java 8 | Jetty 9 | `javax.*` | Legacy | +| **5.x** | Java 11 | Jetty 11 | `jakarta.*` | Superseded by 6 | +| **6.x** | Java 11 | Jetty 11 | `jakarta.*` | **Current — covers Java 11, 17, 21** | +| **7.x** | Java 17 | Jetty 12 | `jakarta.*` | Latest — Java 17+ only | + +**Javalin 6** is the right choice for the IBS migration: a single version that runs on Java 11 through Java 21, ships Jetty 11 with `jakarta.*`, and has an API nearly identical to Spark Java. Spring Boot remains a valid alternative if broader ecosystem integration is needed later. + +### Spring Boot 3.x requires Java 17 + +| Area | Finding | +|------|---------| +| **Spring Boot 3.x minimum** | Java 17. No Spring Boot 3.x path exists on Java 11. | +| **Spring Boot 2.x** | Supports Java 11, but EOL since November 2023. Not a viable path. | +| **javax → jakarta** | Spring Boot 3.x uses `jakarta.servlet.*`. IBS currently uses `javax.servlet-api:3.1.0`. All imports must be migrated. v6SOAPAPI also has `javax.servlet-api` at compile scope — needs updating when IBS migrates. | + +Java 17 is therefore the perfect stepping stone: it is the Spring Boot 3.x minimum, and the Java 17 classifier JAR is exactly what will become the new default after the Spring Boot migration. + +--- + +## Recommended Migration Path + +``` + Java 11 + Spark Java 2.9.4 + Jetty 9.4 [COMPLETE — pre-3.12] + Injection: Java 11 exposed projects ✔ + Aggregator: Java 8, 11 exposed projects ✔ + │ + ▼ +Priority 1 (#38) Java 11 + Javalin 6 ✅ DONE (released 3.12) + Replace Spark + Jetty with Javalin 6 + Fix setAccessible(true) in CallContent.java:171-172 + Migrate javax.* → jakarta.* + CI matrix: test on Java 11, 17, 21 host JVMs + Injection: Java 11, 17, 21 exposed projects ✔ + Aggregator: Java 8, 11 exposed projects ✔ + │ + ▼ +Priority 2 (#39) Java 21 + Javalin 6 (major version bump e.g. 4.0.0) + Upgrade IBS to Java 21 (21) + Pin maven-compiler-plugin ≥ 3.11.0 + Drop Java 8 claim from ReleaseNotes.md + Injection: Java 21 exposed projects ✔ + Aggregator: Java 8, 11, 17, 21 exposed projects ✔ + ⚠ Java 11/17 exposed projects must move to Aggregator Model +``` + +--- + +## Files to Change — Priority 1 (Javalin migration, issue #38) + +| File | Change | +|------|--------| +| `integroBridgeService/pom.xml` | Remove `spark-core:2.9.4` and `javax.servlet-api:3.1.0`; add `io.javalin:javalin:6.x` | +| `integroBridgeService/src/main/java/.../IntegroAPI.java` | Rewrite HTTP layer: replace Spark static DSL with Javalin instance API; migrate multipart from `javax.servlet` to `ctx.uploadedFiles()`; replace `secure(...)` with Javalin `SslPlugin`; return `Javalin` instance from `startServices()` | +| `integroBridgeService/src/main/java/.../MCPRequestHandler.java` | Change `handle(spark.Request, spark.Response)` to `handle(io.javalin.http.Context)` | +| `integroBridgeService/src/main/java/.../CallContent.java:171-172` | Add `setAccessible(true)` before `newInstance()` and `invoke()` (Java 17+ strong encapsulation) | +| `integroBridgeService/src/test/java/.../E2ETests.java` | Remove `Spark.awaitInitialization()` (Javalin `start()` blocks); replace `Spark.stop()` with `app.stop()` | +| `integroBridgeService/src/test/java/.../E2EPortCheck.java` | Remove `spark.Spark` import | +| `integroBridgeService/src/test/java/.../LogManagementTest.java` | Remove `spark.Spark` import | +| `integroBridgeService/src/test/java/.../MCPBridgeServerTest.java` | Replace `Spark.awaitInitialization()` and `Spark.stop()` with Javalin instance calls | +| `.github/workflows/onPushSimpleTest.yml` | Add CI matrix: test on Java 11, 17, and 21 host JVMs | +| `docs/Technical.md` | Add Javalin migration notes and `jakarta.*` namespace change | diff --git a/docs/Technical.md b/docs/Technical.md index 0cbce4b..0de69a6 100644 --- a/docs/Technical.md +++ b/docs/Technical.md @@ -35,6 +35,29 @@ This page describes the state of how Calls access static variables. The structur 3. (optional) Environment Variable Setting 4. Calling Java +## HTTP Framework + +BridgeService uses [Javalin 6](https://javalin.io/) as its HTTP framework (since release 3.12). Javalin 6 runs on +Java 11–21 and ships Jetty 11 with the `jakarta.*` namespace. + +### Migrating from earlier versions (Spark Java) + +Before 3.12, BridgeService used Spark Java 2.9.4 (Jetty 9, `javax.*` namespace). The key differences: + +| Area | Before 3.12 (Spark) | 3.12+ (Javalin 6) | +|---|---|---| +| HTTP framework | `spark-core:2.9.4` | `io.javalin:javalin:6.x` | +| Jetty version | Jetty 9.4 | Jetty 11 | +| Servlet namespace | `javax.servlet` | `jakarta.servlet` | +| `startServices()` return type | `void` | `Javalin` (store for `app.stop()`) | +| SSL configuration | `secure(keystore, ...)` | `SslPlugin` via `config.registerPlugin(...)` | +| Multipart | `req.raw().getParts()` + `javax.servlet` multipart config | `ctx.req().getParts()` + `jakarta.servlet.MultipartConfigElement` | +| Route handlers | `get("/path", (req, res) -> ...)` | `app.get("/path", ctx -> ...)` | +| Exception handlers | `exception(Ex.class, (e, req, res) -> ...)` | `app.exception(Ex.class, (e, ctx) -> ...)` | + +If your project depends on BridgeService in Injection Model and uses `javax.servlet-api`, update it to use +`jakarta.servlet-api` (or the version bundled by Jetty 11) when upgrading to 3.12+. + ## Log Handling The logs are by default deleted after a certain time. In production, we have opted for the following rules: * They are stored in the directory 'ibs_output' diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 78bc2f9..ffdd2ee 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -108,9 +108,14 @@ - com.sparkjava - spark-core - 2.9.4 + io.javalin + javalin + 6.3.0 + + + io.javalin.community.ssl + ssl-plugin + 6.3.0 org.hamcrest @@ -150,12 +155,6 @@ log4j-api 2.25.4 - - javax.servlet - javax.servlet-api - 3.1.0 - compile - com.adobe.campaign.tests.bridge.testdata bridgeService-data diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java index a557912..fa4a848 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/CallContent.java @@ -164,10 +164,11 @@ public Object call(IntegroBridgeClassLoader iClassLoader) { if (isConstructorCall()) { Constructor l_constructor = fetchConstructor(ourClass); + l_constructor.setAccessible(true); lr_object = l_constructor.newInstance(expandArgs(iClassLoader)); } else { Method l_method = fetchMethod(ourClass); - + l_method.setAccessible(true); Object ourInstance = (l_instanceObject == null) ? ourClass.getDeclaredConstructor().newInstance() : l_instanceObject; lr_object = l_method.invoke(ourInstance, castArgs(expandArgs(iClassLoader), l_method)); } diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java index 2fde221..b2c97bf 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/IntegroAPI.java @@ -11,26 +11,28 @@ import com.adobe.campaign.tests.bridge.service.exceptions.*; import com.adobe.campaign.tests.bridge.service.utils.ServiceTools; import com.fasterxml.jackson.core.JsonProcessingException; +import io.javalin.Javalin; +import io.javalin.community.ssl.SslPlugin; +import io.javalin.config.MultipartConfig; +import jakarta.servlet.MultipartConfigElement; +import jakarta.servlet.http.Part; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; -import javax.servlet.MultipartConfigElement; -import javax.servlet.http.Part; import java.io.File; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static com.adobe.campaign.tests.bridge.service.BridgeServiceFactory.*; -import static spark.Spark.*; public class IntegroAPI { public static final String ERROR_CONTENT_TYPE = "application/problem+json"; @@ -41,208 +43,194 @@ public class IntegroAPI { public static final String STD_UPLOAD_DIR = "upload"; private static final Logger log = LogManager.getLogger(); - public static void startServices(int port) { + public static Javalin startServices(int in_port) { - if (!ServiceTools.isPortFree(port)) { - throw new IBSConfigurationException("The port " + port + " is not currently free."); + if (!ServiceTools.isPortFree(in_port)) { + throw new IBSConfigurationException("The port " + in_port + " is not currently free."); } IBSPluginManager.loadPlugins(); - if (Boolean.parseBoolean(ConfigValueHandlerIBS.SSL_ACTIVE.fetchValue())) { - File l_file = new File(ConfigValueHandlerIBS.SSL_KEYSTORE_PATH.fetchValue()); - if (!l_file.exists()) { - log.error("Could not find the Keystore file path {}", l_file.getAbsolutePath()); + File uploadDir = new File(STD_UPLOAD_DIR); + uploadDir.mkdir(); + + Javalin l_app = Javalin.create(config -> { + config.jetty.multipartConfig = new MultipartConfig(); + if (Boolean.parseBoolean(ConfigValueHandlerIBS.SSL_ACTIVE.fetchValue())) { + File l_keystoreFile = new File(ConfigValueHandlerIBS.SSL_KEYSTORE_PATH.fetchValue()); + if (!l_keystoreFile.exists()) { + log.error("Could not find the Keystore file path {}", l_keystoreFile.getAbsolutePath()); + } + SslPlugin l_ssl = new SslPlugin(sslConfig -> { + sslConfig.keystoreFromPath( + ConfigValueHandlerIBS.SSL_KEYSTORE_PATH.fetchValue(), + ConfigValueHandlerIBS.SSL_KEYSTORE_PASSWORD.fetchValue()); + sslConfig.redirect = true; + }); + config.registerPlugin(l_ssl); } - secure(ConfigValueHandlerIBS.SSL_KEYSTORE_PATH.fetchValue(), - ConfigValueHandlerIBS.SSL_KEYSTORE_PASSWORD.fetchValue(), - ConfigValueHandlerIBS.SSL_TRUSTSTORE_PATH.fetchValue(), - ConfigValueHandlerIBS.SSL_TRUSTSTORE_PASSWORD.fetchValue()); - } else { - port(port); - } + }); - get("/test", (req, res) -> { - res.type("application/json"); - Map status = new HashMap<>(); - status.put("overALLSystemState", SYSTEM_UP_MESSAGE); - status.put("deploymentMode", ConfigValueHandlerIBS.DEPLOYMENT_MODEL.fetchValue()); - status.put("bridgeServiceVersion", ConfigValueHandlerIBS.PRODUCT_VERSION.fetchValue()); + l_app.get("/test", ctx -> { + Map l_status = new HashMap<>(); + l_status.put("overALLSystemState", SYSTEM_UP_MESSAGE); + l_status.put("deploymentMode", ConfigValueHandlerIBS.DEPLOYMENT_MODEL.fetchValue()); + l_status.put("bridgeServiceVersion", ConfigValueHandlerIBS.PRODUCT_VERSION.fetchValue()); if (ConfigValueHandlerIBS.PRODUCT_USER_VERSION.isSet()) { - status.put("hostVersion", ConfigValueHandlerIBS.PRODUCT_USER_VERSION.fetchValue()); + l_status.put("hostVersion", ConfigValueHandlerIBS.PRODUCT_USER_VERSION.fetchValue()); } - return BridgeServiceFactory.transformMapTosResult(status); + ctx.contentType("application/json"); + ctx.result(BridgeServiceFactory.transformMapTosResult(l_status)); }); - post("/service-check", (req, res) -> { - ServiceAccess l_serviceAccess = BridgeServiceFactory.createServiceAccess(req.body()); - - return BridgeServiceFactory.transformServiceAccessResult( - l_serviceAccess.checkAccessibilityOfExternalResources()); + l_app.post("/service-check", ctx -> { + ServiceAccess l_serviceAccess = BridgeServiceFactory.createServiceAccess(ctx.body()); + ctx.contentType("application/json"); + ctx.result(BridgeServiceFactory.transformServiceAccessResult( + l_serviceAccess.checkAccessibilityOfExternalResources())); }); - File uploadDir = new File(STD_UPLOAD_DIR); - uploadDir.mkdir(); // create the upload directory if it doesn't exist - //staticFiles.externalLocation("upload"); - - post("/call", (req, res) -> { + l_app.post("/call", ctx -> { + boolean l_isMultiPart = ctx.isMultipartFormData(); + JavaCalls l_fetchedFromJSON; - boolean isMultiPart = false; - JavaCalls fetchedFromJSON; + if (l_isMultiPart) { + ctx.req().setAttribute("org.eclipse.jetty.multipartConfig", new MultipartConfigElement("./temp")); + Map l_fileRefs = new HashMap<>(); + Collection l_parts = ctx.req().getParts(); - //Extract multipart information - if (req.contentType() != null && req.contentType().toLowerCase().startsWith("multipart/form-data")) { - req.attribute("org.eclipse.jetty.multipartConfig", new MultipartConfigElement("./temp")); - Map fileRefs = new HashMap<>(); - isMultiPart = true; - //Extract file information - for (Part p : req.raw().getParts().stream().filter(p -> p.getSubmittedFileName() != null) + for (Part lt_part : l_parts.stream().filter(p -> p.getSubmittedFileName() != null) .collect(Collectors.toList())) { - - Path tempFile = Files.createTempFile(uploadDir.toPath(), "", ""); - ThreadContext.put(p.getName(), tempFile.getFileName().toString()); - fileRefs.put(p.getName(), tempFile); - - try (InputStream is = p.getInputStream()) { - // https://github.com/tipsy/spark-file-upload/blob/master/src/main/java/UploadExample.java - Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING); + Path lt_tempFile = Files.createTempFile(uploadDir.toPath(), "", ""); + ThreadContext.put(lt_part.getName(), lt_tempFile.getFileName().toString()); + l_fileRefs.put(lt_part.getName(), lt_tempFile); + try (InputStream lt_is = lt_part.getInputStream()) { + Files.copy(lt_is, lt_tempFile, StandardCopyOption.REPLACE_EXISTING); } } ThreadContext.put(UPLOADED_FILE_REF, String.join(",", - fileRefs.values().stream().map(p -> p.getFileName().toString()).collect(Collectors.toList()))); + l_fileRefs.values().stream().map(p -> p.getFileName().toString()).collect(Collectors.toList()))); - List l_parts = req.raw().getParts().stream().filter(t -> t.getSubmittedFileName() == null) + List l_callParts = l_parts.stream() + .filter(p -> p.getSubmittedFileName() == null) .collect(Collectors.toList()); - - if (l_parts.size() != 1) { - throw new IBSPayloadException( - ERROR_BAD_MULTI_PART_REQUEST); + if (l_callParts.size() != 1) { + throw new IBSPayloadException(ERROR_BAD_MULTI_PART_REQUEST); + } + String l_callPayload; + try (InputStream lt_is = l_callParts.get(0).getInputStream()) { + l_callPayload = new String(lt_is.readAllBytes(), StandardCharsets.UTF_8); } - fetchedFromJSON = BridgeServiceFactory.createJavaCalls( - new String(l_parts.get(0).getInputStream().readAllBytes(), StandardCharsets.UTF_8)); - - //Store file in context - fileRefs.forEach((k, v) -> fetchedFromJSON.getLocalClassLoader().getCallResultCache().put(k, v.toFile())); + l_fetchedFromJSON = BridgeServiceFactory.createJavaCalls(l_callPayload); + l_fileRefs.forEach((k, v) -> l_fetchedFromJSON.getLocalClassLoader().getCallResultCache().put(k, v.toFile())); } else { - fetchedFromJSON = BridgeServiceFactory.createJavaCalls(req.body()); + l_fetchedFromJSON = BridgeServiceFactory.createJavaCalls(ctx.body()); } + l_fetchedFromJSON.addHeaders(ctx.headerMap()); - fetchedFromJSON.addHeaders(req.headers().stream().collect(Collectors.toMap(k -> k, req::headers))); - - return BridgeServiceFactory.transformJavaCallResultsToJSON(fetchedFromJSON.submitCalls(), - fetchedFromJSON.fetchSecrets()); + ctx.contentType("application/json"); + ctx.result(BridgeServiceFactory.transformJavaCallResultsToJSON(l_fetchedFromJSON.submitCalls(), + l_fetchedFromJSON.fetchSecrets())); }); if (ConfigValueHandlerIBS.MCP_ENABLED.is("true")) { - MCPRequestHandler mcpHandler = new MCPRequestHandler(); - post("/mcp", mcpHandler::handle); + MCPRequestHandler l_mcpHandler = new MCPRequestHandler(); + l_app.post("/mcp", l_mcpHandler::handle); log.info("MCP endpoint enabled at POST /mcp"); - get("/.well-known/oauth-authorization-server", (req, res) -> { - res.status(404); - return "{\"error\":\"not_found\",\"error_description\":\"This server does not support OAuth\"}"; + l_app.get("/.well-known/oauth-authorization-server", ctx -> { + ctx.status(404); + ctx.result("{\"error\":\"not_found\",\"error_description\":\"This server does not support OAuth\"}"); }); } - after((req, res) -> { - res.type("application/json"); - }); - - exception(JsonProcessingException.class, (e, req, res) -> { - int statusCode = 404; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_JSON_TRANSFORMATION, statusCode))); + l_app.exception(JsonProcessingException.class, (e, ctx) -> { + ctx.status(404); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_JSON_TRANSFORMATION, 404))); }); - exception(IBSPayloadException.class, (e, req, res) -> { - int statusCode = 404; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_PAYLOAD_INCONSISTENCY, statusCode))); + l_app.exception(IBSPayloadException.class, (e, ctx) -> { + ctx.status(404); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_PAYLOAD_INCONSISTENCY, 404))); }); - exception(AmbiguousMethodException.class, (e, req, res) -> { - int statusCode = 404; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_AMBIGUOUS_METHOD, statusCode, false))); + l_app.exception(AmbiguousMethodException.class, (e, ctx) -> { + ctx.status(404); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_AMBIGUOUS_METHOD, 404, false))); }); - exception(IBSConfigurationException.class, (e, req, res) -> { - int statusCode = 500; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad(new ErrorObject(e, ERROR_IBS_CONFIG, statusCode))); + l_app.exception(IBSConfigurationException.class, (e, ctx) -> { + ctx.status(500); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad(new ErrorObject(e, ERROR_IBS_CONFIG, 500))); }); - exception(IBSRunTimeException.class, (e, req, res) -> { - int statusCode = 500; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad(new ErrorObject(e, ERROR_IBS_RUNTIME, statusCode))); + l_app.exception(IBSRunTimeException.class, (e, ctx) -> { + ctx.status(500); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad(new ErrorObject(e, ERROR_IBS_RUNTIME, 500))); }); - exception(TargetJavaMethodCallException.class, (e, req, res) -> { - int statusCode = 500; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_CALLING_JAVA_METHOD, statusCode))); + l_app.exception(TargetJavaMethodCallException.class, (e, ctx) -> { + ctx.status(500); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_CALLING_JAVA_METHOD, 500))); }); - exception(NonExistentJavaObjectException.class, (e, req, res) -> { - int statusCode = 404; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_JAVA_OBJECT_NOT_FOUND, statusCode, false))); + l_app.exception(NonExistentJavaObjectException.class, (e, ctx) -> { + ctx.status(404); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_JAVA_OBJECT_NOT_FOUND, 404, false))); }); - exception(JavaObjectInaccessibleException.class, (e, req, res) -> { - int statusCode = 404; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_JAVA_OBJECT_NOT_ACCESSIBLE, statusCode, false))); + l_app.exception(JavaObjectInaccessibleException.class, (e, ctx) -> { + ctx.status(404); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_JAVA_OBJECT_NOT_ACCESSIBLE, 404, false))); }); - exception(IBSTimeOutException.class, (e, req, res) -> { - int statusCode = 408; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad( - new ErrorObject(e, ERROR_CALL_TIMEOUT, statusCode, false))); + l_app.exception(IBSTimeOutException.class, (e, ctx) -> { + ctx.status(408); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad( + new ErrorObject(e, ERROR_CALL_TIMEOUT, 408, false))); }); - //Internal exception - exception(Exception.class, (e, req, res) -> { - int statusCode = 500; - res.status(statusCode); - res.type(ERROR_CONTENT_TYPE); - res.body(BridgeServiceFactory.createExceptionPayLoad(new ErrorObject(e, ERROR_IBS_INTERNAL, statusCode))); + l_app.exception(Exception.class, (e, ctx) -> { + ctx.status(500); + ctx.contentType(ERROR_CONTENT_TYPE); + ctx.result(BridgeServiceFactory.createExceptionPayLoad(new ErrorObject(e, ERROR_IBS_INTERNAL, 500))); }); - afterAfter((req, res) -> { + l_app.after(ctx -> { if (ThreadContext.containsKey(UPLOADED_FILE_REF)) { - Arrays.stream(ThreadContext.get(UPLOADED_FILE_REF).split(",")).forEach(f -> { - log.debug("Cleaning up file {}. succeeded {}.", f, (new File(uploadDir.getName(), f)).delete()); - }); - + for (String lt_fileName : ThreadContext.get(UPLOADED_FILE_REF).split(",")) { + log.debug("Cleaning up file {}. succeeded {}.", lt_fileName, + (new File(uploadDir.getName(), lt_fileName)).delete()); + } + ThreadContext.remove(UPLOADED_FILE_REF); } }); + + l_app.start(in_port); + return l_app; } protected enum DeploymentMode { TEST, PRODUCTION } - } diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java index 07aab6f..52815ae 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java @@ -13,8 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import spark.Request; -import spark.Response; +import io.javalin.http.Context; import java.lang.reflect.Method; import java.util.*; @@ -143,23 +142,22 @@ public MCPRequestHandler() { } /** - * Spark route handler. Parses the incoming JSON-RPC 2.0 request and dispatches + * Javalin route handler. Parses the incoming JSON-RPC 2.0 request and dispatches * to the appropriate handler. All exceptions are caught and returned as MCP errors - * rather than propagating to Spark's HTTP exception handlers. + * rather than propagating to Javalin's HTTP exception handlers. * - * @param req the incoming Spark HTTP request - * @param res the Spark HTTP response - * @return the JSON-RPC response as a String + * @param ctx the Javalin HTTP context */ - public Object handle(Request req, Response res) { - res.type("application/json"); + public void handle(Context ctx) { + ctx.contentType("application/json"); Map body; try { - body = mapper.readValue(req.body(), Map.class); + body = mapper.readValue(ctx.body(), Map.class); } catch (Exception e) { - res.status(400); - return buildError(null, -32700, "Parse error: " + e.getMessage()); + ctx.status(400); + ctx.result(buildError(null, -32700, "Parse error: " + e.getMessage())); + return; } Object id = body.get("id"); @@ -167,35 +165,38 @@ public Object handle(Request req, Response res) { // Notifications have no id — acknowledge with 202 and no body if (id == null && method != null && !method.equals("initialize")) { - res.status(202); - return ""; + ctx.status(202); + ctx.result(""); + return; } if (method == null) { - return buildError(id, -32600, "Invalid Request: missing method field"); + ctx.result(buildError(id, -32600, "Invalid Request: missing method field")); + return; } try { switch (method) { case "initialize": - return buildResult(id, buildInitializeResult()); + ctx.result(buildResult(id, buildInitializeResult())); + break; case "tools/list": - return buildResult(id, Collections.singletonMap("tools", toolList)); + ctx.result(buildResult(id, Collections.singletonMap("tools", toolList))); + break; case "tools/call": @SuppressWarnings("unchecked") Map params = (Map) body.getOrDefault("params", Collections.emptyMap()); - Map headers = req.headers().stream() - .collect(Collectors.toMap(k -> k, req::headers)); - return handleToolCall(id, params, headers); + ctx.result(handleToolCall(id, params, ctx.headerMap())); + break; default: - return buildError(id, -32601, "Method not found: " + method); + ctx.result(buildError(id, -32601, "Method not found: " + method)); } } catch (Exception e) { log.error("Unexpected error handling MCP method '{}': {}", method, e.getMessage(), e); - return buildError(id, -32603, "Internal error: " + e.getMessage()); + ctx.result(buildError(id, -32603, "Internal error: " + e.getMessage())); } } diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2EPortCheck.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2EPortCheck.java index 383546d..064cdad 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2EPortCheck.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2EPortCheck.java @@ -17,8 +17,6 @@ import org.hamcrest.Matchers; import org.testng.Assert; import org.testng.annotations.*; -import spark.Spark; - import java.io.IOException; import java.net.ServerSocket; import java.util.HashMap; diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2ETests.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2ETests.java index f9204c4..13d54a7 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2ETests.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/E2ETests.java @@ -19,7 +19,7 @@ import org.testng.annotations.BeforeGroups; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import spark.Spark; +import io.javalin.Javalin; import java.io.File; import java.io.IOException; @@ -37,11 +37,11 @@ public class E2ETests { protected static final boolean AUTOMATIC_FLAG = false; private static final int port1 = 1111; ServerSocket serverSocket1 = null; + private Javalin app; @BeforeGroups(groups = "E2E") public void startUpService() throws IOException { - IntegroAPI.startServices(8080); - Spark.awaitInitialization(); + app = IntegroAPI.startServices(8080); serverSocket1 = new ServerSocket(port1); @@ -975,11 +975,27 @@ public void testIssue176_classCastExceptionArrayFollowingString() throws IOExcep } + @Test(groups = "E2E") + public void testIBSRunTimeException_invalidDurationKey_returns500() { + String payload = "{\"callContent\":{\"c1\":{" + + "\"class\":\"java.lang.String\",\"method\":\"valueOf\",\"args\":[\"hello\"]}}," + + "\"assertions\":{\"a1\":{" + + "\"actualValue\":\"nonexistent_invalid_key_xyz\"," + + "\"matcher\":\"equalTo\"," + + "\"expectedValue\":100," + + "\"type\":\"DURATION\"}}}"; + + given().contentType("application/json").body(payload) + .post(EndPointURL + "call") + .then().statusCode(500) + .contentType("application/problem+json"); + } + @AfterGroups(groups = "E2E", alwaysRun = true) public void tearDown() throws IOException { ConfigValueHandlerIBS.resetAllValues(); - Spark.stop(); + app.stop(); serverSocket1.close(); } } diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/LogManagementTest.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/LogManagementTest.java index c5820f6..fba9f9a 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/LogManagementTest.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/LogManagementTest.java @@ -13,8 +13,6 @@ import org.testng.annotations.BeforeGroups; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import spark.Spark; - import java.io.IOException; public class LogManagementTest { diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java index a0fb34c..a58d439 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java @@ -14,7 +14,7 @@ import org.testng.annotations.BeforeGroups; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import spark.Spark; +import io.javalin.Javalin; import static io.restassured.RestAssured.given; import static org.hamcrest.MatcherAssert.assertThat; @@ -23,9 +23,9 @@ /** * Integration tests for the MCP endpoint (POST /mcp). * - * Follows the same in-process server lifecycle as E2ETests: @BeforeGroups starts Spark + * Follows the same in-process server lifecycle as E2ETests: @BeforeGroups starts Javalin * with MCP enabled, REST-assured sends raw JSON-RPC 2.0 requests to /mcp, @AfterGroups - * stops Spark. Tests run within the "MCP" group. + * stops Javalin. Tests run within the "MCP" group. */ public class MCPBridgeServerTest { @@ -33,12 +33,13 @@ public class MCPBridgeServerTest { private static final String TESTDATA_ONE_PACKAGE = "com.adobe.campaign.tests.bridge.testdata.one"; private static final String CONTENT_TYPE_JSON = "application/json"; + private Javalin app; + @BeforeGroups(groups = "MCP") public void startMCPService() { ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.activate(TESTDATA_ONE_PACKAGE); ConfigValueHandlerIBS.MCP_ENABLED.activate("true"); - IntegroAPI.startServices(8080); - Spark.awaitInitialization(); + app = IntegroAPI.startServices(8080); } @BeforeMethod @@ -52,7 +53,7 @@ public void resetConfigBetweenTests() { @AfterGroups(groups = "MCP", alwaysRun = true) public void stopMCPService() { ConfigValueHandlerIBS.resetAllValues(); - Spark.stop(); + app.stop(); } // ---- initialize handshake ---- @@ -535,6 +536,57 @@ public void testJavaCallTool_callContentAsString_isUnwrapped() { .body("result.content[0].text", containsString("_Success")); } + // ---- malformed JSON / missing method field ---- + + @Test(groups = "MCP") + public void testMalformedJson_returns400ParseError() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("not valid json {{{") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(400) + .body("error.code", equalTo(-32700)) + .body("error.message", containsString("Parse error")); + } + + @Test(groups = "MCP") + public void testMissingMethodField_returnsInvalidRequestError() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":12}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("error.code", equalTo(-32600)) + .body("error.message", containsString("missing method")); + } + + @Test(groups = "MCP") + public void testToolsCall_missingToolName_returnsInvalidParamsError() { + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"tools/call\",\"params\":{}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("error.code", equalTo(-32602)) + .body("error.message", containsString("missing tool name")); + } + + @Test(groups = "MCP") + public void testOauthEndpoint_returns404() { + given() + .when() + .get("http://localhost:8080/.well-known/oauth-authorization-server") + .then() + .statusCode(404) + .body(containsString("not_found")); + } + // ---- unknown JSON-RPC method ---- @Test(groups = "MCP") From 43a9d08e32e60bd3af927454e720091db689a7bd Mon Sep 17 00:00:00 2001 From: baubakg Date: Mon, 4 May 2026 17:31:12 +0200 Subject: [PATCH 28/37] Clarify release trigger in CLAUDE.md Release is kicked off via GitHub Actions (Release-BridgeService workflow, workflow_dispatch), not by running Maven commands locally. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 019a2d3..3e99d60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,7 +124,7 @@ Apply these prefixes consistently in all new and modified Java code: ## Release Process -The release branch is `release`. Releases are cut from that branch using the Maven Release Plugin. +The release branch is `release`. Releases are cut from that branch by triggering the **Release-BridgeService** GitHub Actions workflow (`workflow_dispatch`), which runs the Maven Release Plugin. ### Steps to prepare a release @@ -152,10 +152,7 @@ The release branch is `release`. Releases are cut from that branch using the Mav git push --set-upstream origin release ``` -5. **Trigger the release** via the Maven Release Plugin (run by CI or manually): - ```bash - mvn release:prepare release:perform - ``` +5. **Trigger the release** via GitHub Actions — go to **Actions → Release-BridgeService → Run workflow** (the workflow uses `workflow_dispatch`). It runs `mvn release:prepare release:perform` on the `release` branch automatically. Do not run the Maven release commands locally. ### Notes - The next development version in the POMs on `main` is already set to `X.Y.Z-SNAPSHOT` by the Maven Release Plugin after the previous release; do not change it manually. From 5a0fc229b7c853fcad21790a5feb47223aae1c48 Mon Sep 17 00:00:00 2001 From: baubakg Date: Mon, 4 May 2026 17:32:55 +0200 Subject: [PATCH 29/37] Prepare release 3.11.3 Co-Authored-By: Claude Sonnet 4.6 --- README.md | 6 +++--- ReleaseNotes.md | 3 +++ docs/MCP.md | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4deffd2..a1141d6 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ The following dependency needs to be added to your pom file: com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.2 + 3.11.3 ``` @@ -201,7 +201,7 @@ If all is good you should get: ``` All systems up - in production -Version : 3.11.2 +Version : 3.11.3 Product user version : 7.0 ``` @@ -948,7 +948,7 @@ Response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.2" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.3" }, "capabilities": { "tools": {} } } } diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 9f1c0b6..4a5c89f 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,7 @@ # Bridge Service - RELEASE NOTES +## 3.11.3 +* **HTTP Framework** [#38 Migrate from Spark Java to Javalin 6](https://github.com/adobe/bridgeService/pull/43) Replaced Spark Java 2.9.4 (Jetty 9, `javax.*`) with Javalin 6.3.0 (Jetty 11, `jakarta.*`). IBS stays on Java 11; injection into Java 17 and Java 21 host JVMs now works without `--add-opens` flags. + ## 3.11.2 * **MCP** [#35 Hybrid method discovery](https://github.com/adobe/bridgeService/pull/35) Each auto-discovered public static method is now exposed as its own named MCP tool in `tools/list`, enabling direct invocation without a `java_call` wrapper. `java_call` is retained for multi-step chains where steps share state or pass complex Java objects between them. * **Dependency Updates** Routine dependency bumps: log4j2, mockito-core, testng. diff --git a/docs/MCP.md b/docs/MCP.md index ccdca8f..cc58426 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -102,7 +102,7 @@ Expected response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.2" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.3" }, "capabilities": { "tools": {} } } } @@ -553,7 +553,7 @@ Response: ```json { - "ibsVersion": "3.11.2", + "ibsVersion": "3.11.3", "deploymentMode": "TEST", "mcpConfig": { "packagesConfigured": "com.example.services", @@ -797,7 +797,7 @@ and start the server from within it. com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.2 + 3.11.3 ``` From 80a92e13090dbe5d22782c3c8fc02a765bc5fc0f Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Tue, 5 May 2026 07:20:53 +0000 Subject: [PATCH 30/37] [maven-release-plugin] prepare release parent-3.11.3 --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 5caf0a3..7e05cbe 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.3-SNAPSHOT + 3.11.3 diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index d9d2c0a..1fbe653 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -189,6 +189,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.3-SNAPSHOT + 3.11.3 diff --git a/pom.xml b/pom.xml index c79f68d..bfe8163 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.3-SNAPSHOT + 3.11.3 Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - HEAD + parent-3.11.3 From 19f5a6286858b53a3fb268c6624aa814aacc2cdd Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Tue, 5 May 2026 07:20:55 +0000 Subject: [PATCH 31/37] [maven-release-plugin] prepare for next development iteration --- bridgeService-data/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 7e05cbe..e67542a 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -52,6 +52,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.3 + 3.11.4-SNAPSHOT diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 1fbe653..ace8976 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -189,6 +189,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.3 + 3.11.4-SNAPSHOT diff --git a/pom.xml b/pom.xml index bfe8163..bd58a41 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.3 + 3.11.4-SNAPSHOT Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -202,7 +202,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - parent-3.11.3 + HEAD From 73a8c0eda6d333557dc38db7317aab957767d5e7 Mon Sep 17 00:00:00 2001 From: Baubak Gandomi Date: Tue, 5 May 2026 11:50:18 +0200 Subject: [PATCH 32/37] Add demo usage examples to Running a DEMO section (issue #50) (#51) * Add demo usage examples to Running a DEMO section (issue #50) Single startup command enables both REST and MCP. Three use-case tables show REST payload + AI prompt side-by-side: string argument call, list return, and call chaining. Co-Authored-By: Claude Sonnet 4.6 * Restructure demo tables: Call + Response rows, REST vs MCP columns Co-Authored-By: Claude Sonnet 4.6 * Move JSON payloads to code blocks above tables for readability Co-Authored-By: Claude Sonnet 4.6 * Replace tables with linear REST/MCP/Response format per use case Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- README.md | 347 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 212 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index a1141d6..4fb82e9 100644 --- a/README.md +++ b/README.md @@ -11,72 +11,75 @@ from any language or framework you are in. ## Table of Contents -* [BridgeService](#bridgeservice) - * [Table of Contents](#table-of-contents) - * [Background](#background) - * [Demo Project](#demo-project) - * [Release Notes](#release-notes) - * [Implementing The Bridge Service in Your Project](#implementing-the-bridge-service-in-your-project) - * [Adding the Bridge Service to Your Project](#adding-the-bridge-service-to-your-project) - * [Installation](#installation) - * [Considerations](#considerations) - * [Including your project in the BridgeService](#including-your-project-in-the-bridgeservice) - * [Java Version Compatibility](#java-version-compatibility) - * [Starting the Bridge Service](#starting-the-bridge-service) - * [Running the Bridge Locally](#running-the-bridge-locally) - * [Running a DEMO](#running-a-demo) - * [Setting Information About your Environment](#setting-information-about-your-environment) - * [Testing That all is Working](#testing-that-all-is-working) - * [Testing That all External Devices can be Accessed](#testing-that-all-external-devices-can-be-accessed) - * [Making Java Calls](#making-java-calls) - * [A basic Java Call](#a-basic-java-call) - * [Instantiating Objects](#instantiating-objects) - * [Call Chaining a basic Java Call](#call-chaining-a-basic-java-call) - * [Call Chaining and Call Dependencies](#call-chaining-and-call-dependencies) - * [Call Chaining and Instance Methods](#call-chaining-and-instance-methods) - * [Argument Types](#argument-types) - * [Simple Java Objects](#simple-java-objects) - * [Lists and Arrays](#lists-and-arrays) - * [Complex Types](#complex-types) - * [Files](#files) - * [Results](#results) - * [Formatting Dates](#formatting-dates) - * [Deserialization Plugins](#deserialization-plugins) - * [Managing Timeouts](#managing-timeouts) - * [Setting Timeout Globally](#setting-timeout-globally) - * [Setting a Timeout for the Call Session](#setting-a-timeout-for-the-call-session) - * [Creating a Call Context](#creating-a-call-context) - * [Static Variable Scopes](#static-variable-scopes) - * [Session Scopes](#session-scopes) - * [Product Scope](#product-scope) - * [Headers and Secrets](#headers-and-secrets) - * [Secrets](#secrets) - * [Steamlining Headers](#steamlining-headers) - * [Making Assertions](#making-assertions) - * [Duration-Based Assertions](#duration-based-assertions) - * [Using BridgeService as an MCP Server](#using-bridgeservice-as-an-mcp-server) - * [Enabling MCP](#enabling-mcp) - * [Discovering Tools](#discovering-tools-toolslist) - * [Calling a Discovered Tool](#calling-a-discovered-tool-toolscall) - * [The java_call Tool](#the-java_call-tool) - * [Diagnostics](#diagnostics-ibs_diagnostics) - * [MCP Configuration](#mcp-configuration) - * [MCP Limitations](#mcp-limitations) - * [Error Management](#error-management) - * [Contributing to the Project](#contributing-to-the-project) - * [Known Errors](#known-errors) - * [Linked Error](#linked-error) - * [Known Issues and Limitations](#known-issues-and-limitations) - * [Cannot call overloaded methods with the same number of arguments.](#cannot-call-overloaded-methods-with-the-same-number-of-arguments) - * [Only simple arguments](#only-simple-arguments) - * [Complex Non-Serializable Return Objects](#complex-non-serializable-return-objects) - * [Calling Enum Methods](#calling-enum-methods) - + +- [BridgeService](#bridgeservice) + - [Table of Contents](#table-of-contents) + - [Background](#background) + - [Demo Project](#demo-project) + - [Release Notes](#release-notes) + - [Implementing The Bridge Service in Your Project](#implementing-the-bridge-service-in-your-project) + - [Adding the Bridge Service to Your Project](#adding-the-bridge-service-to-your-project) + - [Installation](#installation) + - [Considerations](#considerations) + - [Including your project in the BridgeService](#including-your-project-in-the-bridgeservice) + - [Java Version Compatibility](#java-version-compatibility) + - [Starting the Bridge Service](#starting-the-bridge-service) + - [Running the Bridge Locally](#running-the-bridge-locally) + - [Running a DEMO](#running-a-demo) + - [Setting Information About your Environment](#setting-information-about-your-environment) + - [Testing That all is Working](#testing-that-all-is-working) + - [Testing That all External Devices can be Accessed](#testing-that-all-external-devices-can-be-accessed) + - [Making Java Calls](#making-java-calls) + - [A basic Java Call](#a-basic-java-call) + - [Instantiating Objects](#instantiating-objects) + - [Call Chaining a basic Java Call](#call-chaining-a-basic-java-call) + - [Call Chaining and Call Dependencies](#call-chaining-and-call-dependencies) + - [Call Chaining and Instance Methods](#call-chaining-and-instance-methods) + - [Argument Types](#argument-types) + - [Simple Java Objects](#simple-java-objects) + - [Lists and Arrays](#lists-and-arrays) + - [Complex Types](#complex-types) + - [Files](#files) + - [Results](#results) + - [Formatting Dates](#formatting-dates) + - [Deserialization Plugins](#deserialization-plugins) + - [Managing Timeouts](#managing-timeouts) + - [Setting Timeout Globally](#setting-timeout-globally) + - [Setting a Timeout for the Call Session](#setting-a-timeout-for-the-call-session) + - [Creating a Call Context](#creating-a-call-context) + - [Static Variable Scopes](#static-variable-scopes) + - [Session Scopes](#session-scopes) + - [Product Scope](#product-scope) + - [Headers and Secrets](#headers-and-secrets) + - [Secrets](#secrets) + - [Steamlining Headers](#steamlining-headers) + - [Making Assertions](#making-assertions) + - [Duration-Based Assertions](#duration-based-assertions) + - [Using BridgeService as an MCP Server](#using-bridgeservice-as-an-mcp-server) + - [Enabling MCP](#enabling-mcp) + - [Discovering Tools](#discovering-tools-toolslist) + - [Calling a Discovered Tool](#calling-a-discovered-tool-toolscall) + - [The java_call Tool](#the-java_call-tool) + - [Diagnostics](#diagnostics-ibs_diagnostics) + - [MCP Configuration](#mcp-configuration) + - [MCP Limitations](#mcp-limitations) + - [Error Management](#error-management) + - [Contributing to the Project](#contributing-to-the-project) + - [Known Errors](#known-errors) + - [Linked Error](#linked-error) + - [Known Issues and Limitations](#known-issues-and-limitations) + _ [Cannot call overloaded methods with the same number of arguments.](#cannot-call-overloaded-methods-with-the-same-number-of-arguments) + _ [Only simple arguments](#only-simple-arguments) + _ [Complex Non-Serializable Return Objects](#complex-non-serializable-return-objects) + _ [Calling Enum Methods](#calling-enum-methods) + ## Background -We originally created this project to address the need for Cypress tests to reuse code created for the back-end tests. We had a lot of product knowledge and tooling stored in the back-end tests, that rewriting them in node would not have been practical. Today the project is up and running and is used regularly in our test projects. + +We originally created this project to address the need for Cypress tests to reuse code created for the back-end tests. We had a lot of product knowledge and tooling stored in the back-end tests, that rewriting them in node would not have been practical. Today the project is up and running and is used regularly in our test projects. ### Demo Project + We have created a demo project that shpws how the bridge service can be implemented. It is available under [Test As A Product Demo](https://github.com/adobe/test-as-a-product-demo). ## Release Notes @@ -87,8 +90,8 @@ The release notes can be found [here](ReleaseNotes.md). The bridge service can be used in two ways: -* By adding it to the project you want to expose (recommended), -* By including your project as a dependency to the Bridge cervice deployed. +- By adding it to the project you want to expose (recommended), +- By including your project as a dependency to the Bridge cervice deployed. ### Adding the Bridge Service to Your Project @@ -133,17 +136,17 @@ Legend: ✅ Works  |  ⚠️ Works with workarounds  |  ❌ **Injection Model** — the exposed project's JVM loads IBS. Both IBS bytecode and the HTTP framework must be compatible with that JVM. -| IBS release | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | -|---|:---:|:---:|:---:|:---:| -| **pre-3.12** (Spark, Java 11) | ❌ ¹ | ✅ | ⚠️ ² | ❌ ³ | -| **3.12+** (Javalin 6, Java 11) | ❌ ¹ | ✅ | ✅ | ✅ | +| IBS release | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | +| ------------------------------ | :------------: | :-------------: | :-------------: | :-------------: | +| **pre-3.12** (Spark, Java 11) | ❌ ¹ | ✅ | ⚠️ ² | ❌ ³ | +| **3.12+** (Javalin 6, Java 11) | ❌ ¹ | ✅ | ✅ | ✅ | **Aggregator Model** — IBS runs its own JVM and loads the exposed project's classes via reflection. -| IBS release | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | -|---|:---:|:---:|:---:|:---:| -| **pre-3.12** (Spark, Java 11) | ✅ | ✅ | ❌ ⁴ | ❌ ⁴ | -| **3.12+** (Javalin 6, Java 11) | ✅ | ✅ | ❌ ⁴ | ❌ ⁴ | +| IBS release | Exposed Java 8 | Exposed Java 11 | Exposed Java 17 | Exposed Java 21 | +| ------------------------------ | :------------: | :-------------: | :-------------: | :-------------: | +| **pre-3.12** (Spark, Java 11) | ✅ | ✅ | ❌ ⁴ | ❌ ⁴ | +| **3.12+** (Javalin 6, Java 11) | ✅ | ✅ | ❌ ⁴ | ❌ ⁴ | ¹ Exposed project JVM cannot load IBS bytecode — `UnsupportedClassVersionError`. ² Spark is unofficial on Java 17 and requires `--add-opens` flags. @@ -161,32 +164,99 @@ When deploying this as a project we run this as an executable jar. This is usual You can also run the project locally to debug your project. To do this, you need to run the following command line: from the root project: -```mvn -pl integroBridgeService exec:java -Dexec.args="test"``` +`mvn -pl integroBridgeService exec:java -Dexec.args="test"` or directly from the module "integroBridgeService": -```mvn exec:java -Dexec.args="test"``` +`mvn exec:java -Dexec.args="test"` This will make the service available under : -```http://localhost:8080``` +`http://localhost:8080` ### Running a DEMO -The bridge service can be launched in this project with a demo project (Included in an aggregator mode). When deployed -in this mode, we include the module `bridgeService-data` which is part of this project. If you want to include this -project in your deployment you need to set the property `demo.project.mode` to `compile`. +The bridge service can be launched with a built-in demo project (`bridgeService-data`) in Aggregator mode. Start it with: -from the root project: -```mvn -pl integroBridgeService exec:java -Dexec.args="test" -Ddemo.project.mode=compile ``` +```bash +mvn -pl integroBridgeService exec:java -Dexec.args="test" \ + -Ddemo.project.mode=compile \ + -DIBS.MCP.ENABLED=true \ + -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.adobe.campaign.tests.bridge.testdata +``` -or directly from the module "integroBridgeService": -```mvn exec:java -Dexec.args="test" -Ddemo.project.mode=compile``` +This starts BridgeService on `http://localhost:8080` and exposes the demo classes from `com.adobe.campaign.tests.bridge.testdata.*` as both REST endpoints and MCP tools. + +For MCP usage, configure your AI client to point at `http://localhost:8080/mcp` — see [docs/MCP.md](docs/MCP.md) for client-specific setup. + +#### Use case 1 — Call a method with a string argument + +**REST** (`POST /call`): +```json +{ + "callContent": { + "result": { + "class": "com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods", + "method": "methodAcceptingStringArgument", + "args": ["world"] + } + } +} +``` + +**MCP**: *"Call the method that accepts a string argument and pass 'world' to it"* + +**Response**: `{"returnValues":{"result":"world_Success"}}` + +--- + +#### Use case 2 — Call a method returning a list + +**REST** (`POST /call`): +```json +{ + "callContent": { + "countries": { + "class": "com.adobe.campaign.tests.bridge.testdata.one.ClassWithLogger", + "method": "getCountries" + } + } +} +``` + +**MCP**: *"Get the list of available countries"* + +**Response**: `{"returnValues":{"countries":["AT","AU","CA","CH","DE"]}}` + +--- + +#### Use case 3 — Call chaining + +**REST** (`POST /call`): +```json +{ + "callContent": { + "country": { + "class": "com.adobe.campaign.tests.bridge.testdata.one.ClassWithLogger", + "method": "fetchRandomCountry" + }, + "result": { + "class": "com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods", + "method": "methodAcceptingStringArgument", + "args": ["country"] + } + } +} +``` + +**MCP**: *"Get a random country and pass it as an argument to methodAcceptingStringArgument"* + +**Response**: `{"returnValues":{"country":"AU","result":"AU_Success"}}` *(country is random)* ## Setting Information About your Environment The users accessing bridge service will encounter two different technologies: -* The Bridge Service -* The Host Project +- The Bridge Service +- The Host Project In order to make provide context information the Bridge Service will always let you know which version it is. However, we need to let the users know about the version of the host project. Ths version number can be set by setting the @@ -195,7 +265,7 @@ environment property `IBS.PRODUCT.USER.VERSION`. ## Testing That all is Working All you need to do is to call : -```/test``` +`/test` If all is good you should get: @@ -242,7 +312,7 @@ In the example above "" is marked as false because it can not be acces ### A basic Java Call The simplest java call is done in the following way: -```/call``` +`/call` ```JSON { @@ -533,19 +603,23 @@ The result is then: ``` ## Results + Results are returned as a JSON Object. Serializable return objects are deserialized. For objects that are not Serializable, we perform an operatio called scraping, which involves sequentially calling the simple getters of the object, and include the results in the result object. In the case of complex classes where the scraping is not sufficient, you can define a deserialization plugin for that class. This allow you to be specific regarding how the object can be returned. For mor information on this you can refer to the chapter [Deserialization Plugins](#deserialization-plugins). ### Formatting Dates + We now allow the extraction and the formatting of Dates. This is done by setting the environment variable `IBS.DESERIALIZATION.DATE.FORMAT`. If not set the date is not changed and remains a long. The format we cover is [SimpleDateFormat](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). A wrong Date format will result in using the default format (long). ### Deserialization Plugins + As of version 2.11.17, we introduced the notion of plugins. For now you can customize how an object is deserialized. This can be usefull when the default object serialization is incomplete or not to your liking. To create your own plugin you need to: -* Implement the interface methods of the interface `com.adobe.campaign.tests.bridge.service.plugins.IBSDeserializerPlugin`. -* Flag the package of the plugin in the environment variable `IBS.PLUGINS.PACKAGE`. + +- Implement the interface methods of the interface `com.adobe.campaign.tests.bridge.service.plugins.IBSDeserializerPlugin`. +- Flag the package of the plugin in the environment variable `IBS.PLUGINS.PACKAGE`. There is an example of the plugin in the tests under `integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/plugins/deserializer/MimeExtractionPluginDeserializer.java`. @@ -554,8 +628,8 @@ There is an example of the plugin in the tests under `integroBridgeService/src/t As of version 2.11.6 we now introduce the notion of timeouts. This means that after a declared time a call will be interrupted. Setting this value can be done at two levels: -* The deployment level -* The Call session +- The deployment level +- The Call session **Note :** If set to 0, there is no timeout. @@ -617,16 +691,16 @@ When making the call we first update the environment variables for the system. This call will use your internal method for setting environment variables. This method can be set by setting the following environment values, when activating the Bridge on your project: -* IBS.ENVVARS.SETTER.CLASS -* IBS.ENVVARS.SETTER.METHOD +- IBS.ENVVARS.SETTER.CLASS +- IBS.ENVVARS.SETTER.METHOD ### Static Variable Scopes One of our main concerns has been the management of static variables. For the sake of conversation we need to identify scopes: -* **Session Scope** : Access to the variables in the same call to the Bridge Service -* **Product Scope** : Access to the variables between two different calls +- **Session Scope** : Access to the variables in the same call to the Bridge Service +- **Product Scope** : Access to the variables between two different calls #### Session Scopes @@ -642,15 +716,15 @@ document [Managing Contexts and Static Variables](./docs/Technical.md#managing-c Although we do not, yet, provide tools for managing variables that are valid for all calls to the IBS, we can define a series or local environment variables are deployment of the service. This can be done in two ways: -* Managing a properties for in your deployment -* Injecting Runtime properties at the commandline +- Managing a properties for in your deployment +- Injecting Runtime properties at the commandline ## Headers and Secrets As of version 2.11.16, we can now use header variables in the payload. This feature was developed for two reasons: -* Avoiding the passing o secrets directly in the JSON payload. -* Allowing for users to re-use the same JSON payload for different values. +- Avoiding the passing o secrets directly in the JSON payload. +- Allowing for users to re-use the same JSON payload for different values. All you need to do is to reference your header name in the `args` section. @@ -716,7 +790,8 @@ curl --request POST \ } }' ``` -Below is the exception we will get: + +Below is the exception we will get: ```JSON { @@ -846,8 +921,8 @@ example: By default, assertions in the Bridge Service will look at the resulut of a call, however, you can chose a different behavior or this. Currently, assertions are of two types: -* Result Based, -* Duration Based +- Result Based, +- Duration Based ### Duration-Based Assertions @@ -921,6 +996,7 @@ mvn exec:java -Dexec.args="test" -DIBS.MCP.ENABLED=true -DIBS.CLASSLOADER.STATIC At startup, BridgeService scans the packages listed in `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` and builds a **method catalog** from every **public static method** found. Each discovered method is exposed as its own named MCP tool in `tools/list`. A generic `java_call` tool is always present for multi-step chains and instance method calls. The MCP endpoint is available at: + ``` POST /mcp ``` @@ -980,14 +1056,16 @@ Response (abbreviated): "description": "Appends the success suffix to the given string.", "inputSchema": { "type": "object", - "properties": { "arg0": { "type": "string", "description": "the input string" } }, + "properties": { + "arg0": { "type": "string", "description": "the input string" } + }, "required": ["arg0"] } }, { "name": "java_call", "description": "Generic BridgeService call for multi-step chains. ...", - "inputSchema": { "..." : "..." } + "inputSchema": { "...": "..." } }, { "name": "ibs_diagnostics", @@ -1024,7 +1102,12 @@ On success the result contains the standard BridgeService return payload seriali "jsonrpc": "2.0", "id": 3, "result": { - "content": [{ "type": "text", "text": "{\"returnValues\":{\"result\":\"hello_Success\"},\"callDurations\":{\"result\":3}}" }], + "content": [ + { + "type": "text", + "text": "{\"returnValues\":{\"result\":\"hello_Success\"},\"callDurations\":{\"result\":3}}" + } + ], "isError": false } } @@ -1034,12 +1117,12 @@ If the method throws an exception, `isError` is `true` and `content[0].text` con **When to use individual tools vs `java_call`:** -| Scenario | Use | -|---|---| -| Single stateless read | Individual tool (`ClassName_methodName`) | -| Step B needs the Java object returned by step A | `java_call` with call chain | -| Overloaded method (same parameter count) | `java_call` | -| Instance method or constructor | `java_call` | +| Scenario | Use | +| ----------------------------------------------- | ---------------------------------------- | +| Single stateless read | Individual tool (`ClassName_methodName`) | +| Step B needs the Java object returned by step A | `java_call` with call chain | +| Overloaded method (same parameter count) | `java_call` | +| Instance method or constructor | `java_call` | ### The `java_call` Tool @@ -1051,17 +1134,17 @@ A built-in `ibs_diagnostics` tool is always available alongside `java_call`. It ### MCP Configuration -| Variable | Default | Description | -|---|---|---| -| `IBS.MCP.ENABLED` | `false` | Enables the MCP endpoint at `/mcp` | -| `IBS.MCP.PRECHAIN` | — | `callContent` fragment prepended to every `java_call` invocation (e.g. shared auth) | -| `IBS.MCP.REQUIRE_JAVADOC` | `true` | When `true`, only methods with a Javadoc comment are included in the catalog | +| Variable | Default | Description | +| ------------------------- | ------- | ----------------------------------------------------------------------------------- | +| `IBS.MCP.ENABLED` | `false` | Enables the MCP endpoint at `/mcp` | +| `IBS.MCP.PRECHAIN` | — | `callContent` fragment prepended to every `java_call` invocation (e.g. shared auth) | +| `IBS.MCP.REQUIRE_JAVADOC` | `true` | When `true`, only methods with a Javadoc comment are included in the catalog | ### MCP Limitations -* Only **public static methods** are auto-discovered. Instance methods are accessible via `java_call`. -* Overloaded methods with the **same number of parameters** are skipped during discovery. Use `java_call` to call them explicitly. -* Parameter names are exposed as `arg0`, `arg1`, … — Java reflection does not retain source-level parameter names at runtime. +- Only **public static methods** are auto-discovered. Instance methods are accessible via `java_call`. +- Overloaded methods with the **same number of parameters** are skipped during discovery. Use `java_call` to call them explicitly. +- Parameter names are exposed as `arg0`, `arg1`, … — Java reflection does not retain source-level parameter names at runtime. ## Error Management @@ -1075,10 +1158,7 @@ response. For example, for the call: "class": "com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods", "method": "methodThrowingException", "returnType": "java.lang.String", - "args": [ - 3, - 3 - ] + "args": [3, 3] } } } @@ -1110,20 +1190,20 @@ When using the bridge service, we also include additional info: The BridgeService exception is how the bridgeService manages underlying errors. However, we also share the: -* The title of the failure -* The error code -* The bridgeService error category -* The Originating exception -* The Originating exception message -* The step at which the error occured -* The stack trace of the originating exception +- The title of the failure +- The error code +- The bridgeService error category +- The Originating exception +- The Originating exception message +- The step at which the error occured +- The stack trace of the originating exception ## Contributing to the Project There are two main docs for contributing: -* [The Contributing Doc](CONTRIBUTING.md) For general contribution rules. -* [The Technical Doc](docs/Technical.md) For general technical aspects. +- [The Contributing Doc](CONTRIBUTING.md) For general contribution rules. +- [The Technical Doc](docs/Technical.md) For general technical aspects. ## Known Errors @@ -1149,7 +1229,7 @@ As this is a new project there are a few limitations to our solution: Today, in order to simply the call model, we have chosen not to specify the argument types in the call. The consequence of this is that in the case of overloaded methods, we only pick the method with the same number of arguments. If two such overloaded methods exist, we choose to throw an exception: -``We could not find a unique method for .`` +`We could not find a unique method for .` ### Only simple arguments @@ -1167,6 +1247,3 @@ The mining or "scraping" is done by calling all the visible getters of the objec ### Calling Enum Methods We are currently unable to call enums with the Bridge Service. - - - From c835c0c270625d79a1fbb382c15af9aa9c23698f Mon Sep 17 00:00:00 2001 From: Baubak Gandomi Date: Tue, 5 May 2026 16:06:15 +0200 Subject: [PATCH 33/37] Add three-level Javadoc quality gate for MCP tool discovery (issue #40) (#52) * Add three-level Javadoc quality gate for MCP tool discovery (issue #40) Extends IBS.MCP.REQUIRE_JAVADOC from a boolean to a three-value setting: - false: expose all public static methods - true: requires non-empty Javadoc comment (previous default) - strict (new default): requires comment + @param for every parameter Methods missing @param tags previously exposed "description":"String" as the parameter description, giving AI agents no useful guidance. The strict default prevents such tools from appearing in tools/list. Startup log now reports the active gate level and its effect. Diagnostics field renamed from javadocRequired (boolean) to javadocQualityGate (string) to accurately reflect the three-way setting. Co-Authored-By: Claude Sonnet 4.6 * Report skipped method count in MCP tool discovery log The discovery log now shows how many methods were filtered out by the active quality gate, e.g. "21 registered, 5 skipped by quality gate (strict)". Added skippedCount field to DiscoveryResult for use by callers. Co-Authored-By: Claude Sonnet 4.6 * Improve coverage for Javadoc quality gate and document coverage rules Add tests for the 'true' mode path of shouldSkipForJavadoc (previously unreachable since the default changed to 'strict') and for skippedCount reporting. Update CLAUDE.md with explicit 80% patch coverage rule so future contributors understand what codecov/patch enforces. Co-Authored-By: Claude Sonnet 4.6 * Fix codecov/patch coverage gaps in Javadoc quality gate Three gaps addressed: - Lines 119-124: overloaded-method path with unique param counts was never exercised; added overLoadedMethodDifferentArity(String) and (String,String) to SimpleStaticMethods to trigger this path - Lines 143-144: MCPRequestHandler startup log 'false'/'true' ternary branches were only reached under strict mode; added handler construction tests for both modes - Lines 202-203: hasAdequateJavadoc exception catch now covered via Mockito MockedStatic with thenAnswer/thenThrow chaining Co-Authored-By: Claude Sonnet 4.6 * Fix codecov/patch: inline hasAdequateJavadoc to cover exception path hasAdequateJavadoc previously called hasJavadoc() first then called RuntimeJavadoc.getJavadoc() a second time inside its own try block. The Mockito thenAnswer(callRealMethod).thenThrow() pattern failed to delegate the first call correctly in static mock mode, so hasJavadoc() returned false and the catch block was never reached. Inline the comment check into a single try/catch so a simple thenThrow mock hits the catch block directly, covering lines 202-203. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- CLAUDE.md | 5 + .../testdata/one/SimpleStaticMethods.java | 17 ++ docs/MCP.md | 50 ++++-- .../bridge/service/ConfigValueHandlerIBS.java | 8 +- .../bridge/service/MCPRequestHandler.java | 13 +- .../bridge/service/MCPToolDiscovery.java | 66 ++++++-- .../bridge/service/MCPBridgeServerTest.java | 149 +++++++++++++++++- 7 files changed, 271 insertions(+), 37 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 822ad20..50a73bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,11 @@ mvn package **Quality gates that must pass before merging:** unit tests pass, no coverage decrease, SonarCloud gate green, license headers present. +**Coverage rules (enforced by SonarCloud and codecov/patch in CI):** +- Overall line coverage must not decrease from the baseline on `main`. +- New/changed code (the PR patch) must reach **80% line coverage** — this is the `codecov/patch` check. If it fails, add targeted tests for the uncovered branches in your new code. +- JaCoCo reports are generated at `target/site/jacoco/` after `mvn test`. Check method-level branch coverage there before pushing. + ## Module Structure ``` diff --git a/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java b/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java index c2235b3..a95d082 100644 --- a/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java +++ b/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java @@ -159,6 +159,23 @@ public static String overLoadedMethod1Arg(int in_intArgument) { return in_intArgument + SUCCESS_VAL; } + // For testing MCP tool discovery of overloads with different arity. + // The 1-arg variant has no Javadoc (exercises the skip path for overloads). + public static String overLoadedMethodDifferentArity(String in_arg) { + return in_arg + SUCCESS_VAL; + } + + /** + * Two-argument overload for testing MCP tool name disambiguation by arity. + * + * @param in_arg1 first string argument + * @param in_arg2 second string argument + * @return concatenation of both arguments with the success suffix + */ + public static String overLoadedMethodDifferentArity(String in_arg1, String in_arg2) { + return in_arg1 + in_arg2 + SUCCESS_VAL; + } + //For impossible Objects exception public static String complexMethodAcceptor(Instantiable in_arg) { return SUCCESS_VAL; diff --git a/docs/MCP.md b/docs/MCP.md index cc58426..e12f0a2 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -43,7 +43,7 @@ using the built-in demo data, then from an external project that hosts its own J |---|---|---| | `IBS.MCP.ENABLED` | `false` | Enables the MCP endpoint at `/mcp`. Must be `true` for any MCP usage. | | `IBS.MCP.PRECHAIN` | — | JSON `callContent` fragment prepended to every `java_call` invocation. Used for server-wide setup such as shared authentication. Can also be supplied per-client via the `ibs-prechain` HTTP header (env var takes precedence). | -| `IBS.MCP.REQUIRE_JAVADOC` | `true` | When `true`, only methods with a non-empty Javadoc comment are included in the tool catalog. Methods without Javadoc are silently excluded from `tools/list`. | +| `IBS.MCP.REQUIRE_JAVADOC` | `strict` | Controls which methods are exposed based on Javadoc quality. `false` = expose all public static methods. `true` = requires a non-empty Javadoc comment. `strict` (default) = requires a comment **and** a non-empty `@param` tag for every parameter. | See the relevant sections below for full configuration details and examples. @@ -558,7 +558,7 @@ Response: "mcpConfig": { "packagesConfigured": "com.example.services", "prechainActive": true, - "javadocRequired": true + "javadocQualityGate": "strict" }, "headers": { "secretHeaderKeys": ["ibs-secret-login", "ibs-secret-pass", "ibs-secret-url"], @@ -580,7 +580,7 @@ Response: | `deploymentMode` | `TEST` or `PRODUCTION` | | `mcpConfig.packagesConfigured` | Value of `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` | | `mcpConfig.prechainActive` | Whether a prechain is configured (env var or header) | -| `mcpConfig.javadocRequired` | Whether `IBS.MCP.REQUIRE_JAVADOC` is enabled | +| `mcpConfig.javadocQualityGate` | Active value of `IBS.MCP.REQUIRE_JAVADOC` (`false`, `true`, or `strict`) | | `headers.secretHeaderKeys` | Names of `ibs-secret-*` headers received (values suppressed) | | `headers.envVarHeaders` | Decoded env-var headers (`ibs-env-*` prefix stripped, uppercased) | | `headers.regularHeaderCount` | Count of headers that are neither secret nor env-var | @@ -883,16 +883,36 @@ Without the dependency, tools are still fully functional; only the description q ### Javadoc quality gate -By default (`IBS.MCP.REQUIRE_JAVADOC=true`), BridgeService **only exposes methods that have a -non-empty Javadoc comment**. Methods without Javadoc are silently skipped at startup and will not -appear in `tools/list`. +BridgeService enforces a configurable documentation quality gate via `IBS.MCP.REQUIRE_JAVADOC`. +Methods that do not satisfy the active gate are silently skipped at startup and will not appear +in `tools/list`. -This is intentional. A method with no Javadoc would receive a generic fallback description such as -`"Calls com.example.MyClass.method()"`, which gives an AI agent no useful information about when -or why to call it. Exposing such tools increases the risk of accidental invocations. +The three levels are: -**To opt out** (expose all public static methods regardless of documentation): +| Value | What is required to be exposed | +|---|---| +| `false` | Nothing — all public static methods are exposed regardless of documentation | +| `true` | A non-empty Javadoc comment on the method | +| `strict` *(default)* | A non-empty comment **and** a non-empty `@param` tag for every parameter | + +**Why `strict` is the default:** a method with parameters but no `@param` tags causes BridgeService +to fall back to the Java type name as the parameter description (e.g. `"description": "String"`). +This gives the AI agent almost no guidance on what to pass, which leads to incorrect calls and +wasted round-trips. `strict` prevents such tools from appearing in the catalog. + +At startup, BridgeService logs the active gate level and its effect so you can confirm your +configuration is applied: +``` +MCPRequestHandler ready: 12 individual tool(s) + java_call + ibs_diagnostics. +Javadoc quality gate: strict — only methods with Javadoc comment + @param for every parameter are exposed +``` + +**To use the previous default** (non-empty comment only): +``` +IBS.MCP.REQUIRE_JAVADOC=true +``` +**To expose all public static methods** (no documentation required): ``` IBS.MCP.REQUIRE_JAVADOC=false ``` @@ -1113,11 +1133,11 @@ tool invocation. 3. **What each parameter expects** — use `@param` tags; BridgeService uses them as argument descriptions in the tool schema. 4. **When to use it vs similar methods** — if overloads or related methods exist, say which scenario each is for. -**The quality gate enforces the minimum bar.** `IBS.MCP.REQUIRE_JAVADOC=true` (the default) -silently drops any method with no Javadoc from the catalog entirely — it will not appear in -`tools/list` and cannot be called via auto-discovery. Passing the gate (a non-empty comment) -is necessary but not sufficient: a one-word description passes the gate but still produces a -useless tool entry. +**The quality gate enforces the minimum bar.** `IBS.MCP.REQUIRE_JAVADOC=strict` (the default) +silently drops any method that lacks a comment or is missing `@param` tags — it will not appear in +`tools/list` and cannot be called via auto-discovery. Passing the gate is necessary but not +sufficient: a one-word description with perfunctory `@param` tags passes the gate but still +produces a low-quality tool entry. **Good Javadoc pays compound interest.** A well-described method is discovered correctly the first time, requires no follow-up prompting, and stays reliable as the AI session context diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java index 26919fc..f6a1bfb 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java @@ -73,9 +73,11 @@ public void activate(String in_value) { "IBS.DESERIALIZATION.DATE.FORMAT", "NONE", false, "The date format to be used for deserialization."), MCP_ENABLED("IBS.MCP.ENABLED", "false", false, "When set to true, enables the MCP server endpoint at POST /mcp, exposing configured packages as tools."), - MCP_REQUIRE_JAVADOC("IBS.MCP.REQUIRE_JAVADOC", "true", false, - "When true (default), only methods with a non-empty Javadoc comment are exposed as MCP tools. " - + "Methods without Javadoc are silently skipped. Set to false to expose all public static methods."), + MCP_REQUIRE_JAVADOC("IBS.MCP.REQUIRE_JAVADOC", "strict", false, + "Controls which methods are exposed as MCP tools based on Javadoc quality. " + + "Accepted values: 'false' (expose all public static methods), " + + "'true' (requires non-empty Javadoc comment), " + + "'strict' (requires comment + non-empty @param for every parameter, default)."), MCP_PRECHAIN("IBS.MCP.PRECHAIN", null, false, "JSON callContent fragment prepended to every auto-discovered MCP tool invocation. " + "Entries execute in the same isolated context as the actual call, so call-chaining " diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java index 52815ae..dddc3ba 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java @@ -137,8 +137,13 @@ public MCPRequestHandler() { l_diagnosticsTool.put("inputSchema", DIAGNOSTICS_SCHEMA_MAP); tools.add(l_diagnosticsTool); this.toolList = Collections.unmodifiableList(tools); - log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics.", - discoveredToolCount); + String l_javadocExplained = ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("strict") + ? "strict — only methods with Javadoc comment + @param for every parameter are exposed" + : ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("false") + ? "false — all public static methods are exposed regardless of documentation" + : "true — only methods with a non-empty Javadoc comment are exposed"; + log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics. " + + "Javadoc quality gate: {}", discoveredToolCount, l_javadocExplained); } /** @@ -338,8 +343,8 @@ private String handleDiagnostics(Object id, Map headers) { ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.fetchValue()); String prechain = ConfigValueHandlerIBS.MCP_PRECHAIN.fetchValue(); mcpConfig.put("prechainActive", prechain != null && !prechain.isBlank()); - mcpConfig.put("javadocRequired", - Boolean.parseBoolean(ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue())); + mcpConfig.put("javadocQualityGate", + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue()); diag.put("mcpConfig", mcpConfig); String secretPrefix = ConfigValueHandlerIBS.SECRETS_FILTER_PREFIX.fetchValue(); diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java index c95b760..ab79654 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java @@ -40,10 +40,13 @@ public static class DiscoveryResult { public final List> tools; /** Maps each tool name to the Java Method it represents, used to build the catalog in the java_call description. */ public final Map methodRegistry; + /** Number of methods skipped by the active Javadoc quality gate. */ + public final int skippedCount; - public DiscoveryResult(List> tools, Map methodRegistry) { + public DiscoveryResult(List> tools, Map methodRegistry, int skippedCount) { this.tools = Collections.unmodifiableList(tools); this.methodRegistry = Collections.unmodifiableMap(methodRegistry); + this.skippedCount = skippedCount; } } @@ -57,11 +60,12 @@ public DiscoveryResult(List> tools, Map meth public static DiscoveryResult discoverTools(String packagesCsv) { List> tools = new ArrayList<>(); Map registry = new LinkedHashMap<>(); + int[] l_skipped = {0}; if (packagesCsv == null || packagesCsv.trim().isEmpty()) { log.warn("IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES is not set — no tools will be discovered for MCP. " + "Set this property to enable tool discovery."); - return new DiscoveryResult(tools, registry); + return new DiscoveryResult(tools, registry, 0); } // Strip trailing dots that IBS uses as package separators (e.g. "com.example.") @@ -95,9 +99,8 @@ public static DiscoveryResult discoverTools(String packagesCsv) { if (overloads.size() == 1) { // Unique method name on this class — use simple tool name Method method = overloads.get(0); - if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true") && !hasJavadoc(method)) { - log.debug("Skipping {}.{} — no Javadoc (IBS.MCP.REQUIRE_JAVADOC=true)", - clazz.getSimpleName(), methodName); + if (shouldSkipForJavadoc(method, clazz.getSimpleName(), methodName)) { + l_skipped[0]++; } else { String toolName = clazz.getSimpleName() + "_" + methodName; registerTool(tools, registry, toolName, method); @@ -113,13 +116,12 @@ public static DiscoveryResult discoverTools(String packagesCsv) { + "use the java_call tool to invoke them directly.", clazz.getName(), methodName, countEntry.getKey()); } else { - Method method = countEntry.getValue().get(0); - if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true") && !hasJavadoc(method)) { - log.debug("Skipping {}.{} — no Javadoc (IBS.MCP.REQUIRE_JAVADOC=true)", - clazz.getSimpleName(), methodName); + Method lt_method = countEntry.getValue().get(0); + if (shouldSkipForJavadoc(lt_method, clazz.getSimpleName(), methodName)) { + l_skipped[0]++; } else { - String toolName = clazz.getSimpleName() + "_" + methodName + "_" + countEntry.getKey(); - registerTool(tools, registry, toolName, method); + String lt_toolName = clazz.getSimpleName() + "_" + methodName + "_" + countEntry.getKey(); + registerTool(tools, registry, lt_toolName, lt_method); } } } @@ -127,9 +129,9 @@ public static DiscoveryResult discoverTools(String packagesCsv) { } } - log.info("MCP tool discovery complete: {} tool(s) registered from {} class(es).", - tools.size(), allClasses.size()); - return new DiscoveryResult(tools, registry); + log.info("MCP tool discovery complete: {} tool(s) registered, {} skipped by quality gate ({}), from {} class(es).", + tools.size(), l_skipped[0], ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue(), allClasses.size()); + return new DiscoveryResult(tools, registry, l_skipped[0]); } private static void registerTool(List> tools, Map registry, @@ -182,6 +184,42 @@ static boolean hasJavadoc(Method method) { } } + /** + * Returns true if the method has a non-empty Javadoc comment AND a non-empty + * {@code @param} tag for every parameter. Methods with no parameters pass if + * they have a non-empty comment. + */ + static boolean hasAdequateJavadoc(Method method) { + try { + MethodJavadoc l_javadoc = RuntimeJavadoc.getJavadoc(method); + if (l_javadoc == null || COMMENT_FORMATTER.format(l_javadoc.getComment()).isEmpty()) return false; + int l_paramCount = method.getParameterCount(); + if (l_paramCount == 0) return true; + List l_params = l_javadoc.getParams(); + if (l_params.size() < l_paramCount) return false; + return l_params.stream().allMatch(p -> !COMMENT_FORMATTER.format(p.getComment()).isEmpty()); + } catch (Exception e) { + return false; + } + } + + private static boolean shouldSkipForJavadoc(Method in_method, String in_clazz, String in_method_name) { + if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("strict")) { + if (!hasAdequateJavadoc(in_method)) { + log.debug("Skipping {}.{} — Javadoc quality gate (strict) not met: missing comment or @param", + in_clazz, in_method_name); + return true; + } + } else if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true")) { + if (!hasJavadoc(in_method)) { + log.debug("Skipping {}.{} — Javadoc quality gate (true) not met: no Javadoc comment", + in_clazz, in_method_name); + return true; + } + } + return false; + } + /** * Builds a JSON Schema object describing the input parameters of a method. * Parameter names are generated as arg0, arg1, ... since Java reflection diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java index a58d439..96b88e2 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java @@ -8,14 +8,22 @@ */ package com.adobe.campaign.tests.bridge.service; +import com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods; +import com.github.therapi.runtimejavadoc.RuntimeJavadoc; import io.restassured.response.Response; import org.hamcrest.Matchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.testng.annotations.AfterGroups; import org.testng.annotations.BeforeGroups; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import io.javalin.Javalin; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + import static io.restassured.RestAssured.given; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -201,7 +209,7 @@ public void testDiagnosticsTool_mcpConfigReflectsCurrentState() { assertThat(text, containsString("packagesConfigured")); assertThat(text, containsString(TESTDATA_ONE_PACKAGE)); assertThat(text, containsString("\"prechainActive\":false")); - assertThat(text, containsString("\"javadocRequired\":true")); + assertThat(text, containsString("\"javadocQualityGate\"")); } @Test(groups = "MCP") @@ -1092,6 +1100,145 @@ public void testToolsList_constructorsNotExposedAsIndividualTools() { .body("result.tools.name", not(hasItem("Instantiable_Instantiable"))); } + // ---- hasAdequateJavadoc unit tests ---- + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_methodWithCommentAndAllParams_returnsTrue() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("methodAcceptingStringArgument", String.class); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(true)); + } + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_noArgMethodWithComment_returnsTrue() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("methodReturningString"); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(true)); + } + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_methodWithNoJavadoc_returnsFalse() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("overLoadedMethod1Arg", String.class); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(false)); + } + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_runtimeJavadocThrows_returnsFalse() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("methodAcceptingStringArgument", String.class); + try (MockedStatic l_mock = Mockito.mockStatic(RuntimeJavadoc.class)) { + l_mock.when(() -> RuntimeJavadoc.getJavadoc(l_method)) + .thenThrow(new RuntimeException("simulated Javadoc read failure")); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(false)); + } + } + + // ---- Javadoc quality gate integration tests ---- + + @Test(groups = "MCP") + public void testDiscoverTools_strictMode_skippedCountIsPositive() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("strict"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + assertThat("skippedCount must be positive — test data contains undocumented methods", + l_result.skippedCount, greaterThan(0)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testDiscoverTools_strictMode_documentedMethodsIncluded() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("strict"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + List> l_tools = l_result.tools; + assertThat("Documented method with @param must appear under strict", + l_tools.stream().anyMatch(t -> "SimpleStaticMethods_methodAcceptingStringArgument".equals(t.get("name"))), + is(true)); + assertThat("Undocumented method must be absent under strict", + l_tools.stream().noneMatch(t -> ("SimpleStaticMethods_overLoadedMethod1Arg").equals(t.get("name"))), + is(true)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testDiscoverTools_trueMode_excludesUndocumentedIncludesDocumented() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("true"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + assertThat("Undocumented method must be absent under true mode", + l_result.tools.stream().noneMatch(t -> + "SimpleStaticMethods_methodThrowingLinkageError".equals(t.get("name"))), + is(true)); + assertThat("Documented method must appear under true mode", + l_result.tools.stream().anyMatch(t -> + "SimpleStaticMethods_methodAcceptingStringArgument".equals(t.get("name"))), + is(true)); + assertThat("Skip count must reflect filtered methods", + l_result.skippedCount, greaterThan(0)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testDiscoverTools_falseMode_undocumentedMethodsIncluded() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("false"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + // methodThrowingLinkageError has no Javadoc — excluded under strict/true, included under false + assertThat("Undocumented method must appear when gate is disabled", + l_result.tools.stream().anyMatch(t -> + "SimpleStaticMethods_methodThrowingLinkageError".equals(t.get("name"))), + is(true)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testMCPRequestHandler_falseMode_startupLogBranchCovered() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("false"); + try { + // Constructing MCPRequestHandler exercises the 'false' branch of the startup log ternary. + MCPRequestHandler l_handler = new MCPRequestHandler(); + assertThat(l_handler, notNullValue()); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testMCPRequestHandler_trueModeStartupLogBranchCovered() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("true"); + try { + // Constructing MCPRequestHandler exercises the 'true' branch of the startup log ternary. + MCPRequestHandler l_handler = new MCPRequestHandler(); + assertThat(l_handler, notNullValue()); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testToolsList_strictDefault_exposesFullyDocumentedMethods() { + // With the default (strict), all documented SimpleStaticMethods with @param should be present. + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":200,\"method\":\"tools/list\",\"params\":{}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingStringArgument")) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingTwoArguments")) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingIntArgument")); + } + @Test(groups = "MCP") public void testJavaCall_constructorChain_instantiableThenStaticMethod() { // Constructors are not available as individual tools — use java_call to instantiate From 89e111e3ea7787e886bb0a277f1925d189e7708b Mon Sep 17 00:00:00 2001 From: baubakg Date: Wed, 6 May 2026 10:52:02 +0200 Subject: [PATCH 34/37] Prepare release 3.11.4 --- CLAUDE.md | 5 +---- README.md | 6 +++--- ReleaseNotes.md | 4 ++++ docs/MCP.md | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 50a73bd..35b1602 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -157,10 +157,7 @@ The release branch is `release`. Releases are cut from that branch using the Mav git push --set-upstream origin release ``` -5. **Trigger the release** via the Maven Release Plugin (run by CI or manually): - ```bash - mvn release:prepare release:perform - ``` +5. **Trigger the release** by manually dispatching the **Release-BridgeService** GitHub Actions workflow (`.github/workflows/maven-publish-release.yml`). Go to the Actions tab on GitHub, select "Release-BridgeService", and click "Run workflow" on the `release` branch. The workflow runs `mvn release:prepare release:perform` with GPG signing and Sonatype credentials, then promotes the artifact to Maven Central automatically. No GitHub Release is created — Maven Central is the canonical artifact location. ### Notes - The next development version in the POMs on `main` is already set to `X.Y.Z-SNAPSHOT` by the Maven Release Plugin after the previous release; do not change it manually. diff --git a/README.md b/README.md index 4fb82e9..c92ea6e 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ The following dependency needs to be added to your pom file: com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.3 + 3.11.4 ``` @@ -271,7 +271,7 @@ If all is good you should get: ``` All systems up - in production -Version : 3.11.3 +Version : 3.11.4 Product user version : 7.0 ``` @@ -1024,7 +1024,7 @@ Response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.3" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.4" }, "capabilities": { "tools": {} } } } diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 4a5c89f..9ac3f5b 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,8 @@ # Bridge Service - RELEASE NOTES +## 3.11.4 +* **MCP** [#40 Three-level Javadoc quality gate for MCP tool discovery](https://github.com/adobe/bridgeService/pull/52) Tool descriptions are now validated at three levels: method Javadoc, `@param` tags, and `@return` tag. Methods missing any level are excluded from `tools/list` at startup. +* **Docs** [#50 Add demo usage examples to Running a DEMO section](https://github.com/adobe/bridgeService/pull/51) Added concrete curl and payload examples to the demo section of the README. + ## 3.11.3 * **HTTP Framework** [#38 Migrate from Spark Java to Javalin 6](https://github.com/adobe/bridgeService/pull/43) Replaced Spark Java 2.9.4 (Jetty 9, `javax.*`) with Javalin 6.3.0 (Jetty 11, `jakarta.*`). IBS stays on Java 11; injection into Java 17 and Java 21 host JVMs now works without `--add-opens` flags. diff --git a/docs/MCP.md b/docs/MCP.md index e12f0a2..e6ddf1b 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -102,7 +102,7 @@ Expected response: "id": 1, "result": { "protocolVersion": "2024-11-05", - "serverInfo": { "name": "bridgeService", "version": "3.11.3" }, + "serverInfo": { "name": "bridgeService", "version": "3.11.4" }, "capabilities": { "tools": {} } } } @@ -553,7 +553,7 @@ Response: ```json { - "ibsVersion": "3.11.3", + "ibsVersion": "3.11.4", "deploymentMode": "TEST", "mcpConfig": { "packagesConfigured": "com.example.services", @@ -797,7 +797,7 @@ and start the server from within it. com.adobe.campaign.tests.bridge.service integroBridgeService - 3.11.3 + 3.11.4 ``` From b7919b88a6066b2449ffe55cf9de9b090773fe48 Mon Sep 17 00:00:00 2001 From: baubakg Date: Wed, 6 May 2026 10:58:21 +0200 Subject: [PATCH 35/37] Update release process: add PR step and conflict resolution guidance --- CLAUDE.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 35b1602..6afa070 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -157,7 +157,22 @@ The release branch is `release`. Releases are cut from that branch using the Mav git push --set-upstream origin release ``` -5. **Trigger the release** by manually dispatching the **Release-BridgeService** GitHub Actions workflow (`.github/workflows/maven-publish-release.yml`). Go to the Actions tab on GitHub, select "Release-BridgeService", and click "Run workflow" on the `release` branch. The workflow runs `mvn release:prepare release:perform` with GPG signing and Sonatype credentials, then promotes the artifact to Maven Central automatically. No GitHub Release is created — Maven Central is the canonical artifact location. +5. **Open a pull request** from `release` → `main` for review before triggering the workflow: + ```bash + gh pr create --title "Prepare release X.Y.Z" --base main --head release + ``` + If the PR shows conflicts (because `main` advanced after the release prep), merge `origin/main` into `release` and resolve: + ```bash + git merge origin/main --no-edit + # Conflicts in README.md, ReleaseNotes.md, docs/MCP.md are version-number conflicts. + # Keep the release branch side (3.11.4, not 3.11.3): + git checkout --ours README.md ReleaseNotes.md docs/MCP.md + git add README.md ReleaseNotes.md docs/MCP.md + git commit --no-edit + git push origin release + ``` + +6. **Trigger the release** by manually dispatching the **Release-BridgeService** GitHub Actions workflow (`.github/workflows/maven-publish-release.yml`). Go to the Actions tab on GitHub, select "Release-BridgeService", and click "Run workflow" on the `release` branch. The workflow runs `mvn release:prepare release:perform` with GPG signing and Sonatype credentials, then promotes the artifact to Maven Central automatically. No GitHub Release is created — Maven Central is the canonical artifact location. ### Notes - The next development version in the POMs on `main` is already set to `X.Y.Z-SNAPSHOT` by the Maven Release Plugin after the previous release; do not change it manually. From b496aaad6290e0c660216d30c151da2356f5a745 Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Wed, 6 May 2026 09:05:57 +0000 Subject: [PATCH 36/37] [maven-release-plugin] prepare release parent-3.11.4 --- bridgeService-data/pom.xml | 2 +- bridgeService-test-injection/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index 602736a..b0fa7bf 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -53,6 +53,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.4-SNAPSHOT + 3.11.4 diff --git a/bridgeService-test-injection/pom.xml b/bridgeService-test-injection/pom.xml index f28cd47..6b6c689 100644 --- a/bridgeService-test-injection/pom.xml +++ b/bridgeService-test-injection/pom.xml @@ -78,6 +78,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.4-SNAPSHOT + 3.11.4 diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index ace8976..32b7edd 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -189,6 +189,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.4-SNAPSHOT + 3.11.4 diff --git a/pom.xml b/pom.xml index 051987d..5fd62bf 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.4-SNAPSHOT + 3.11.4 Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -203,7 +203,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - HEAD + parent-3.11.4 From 1704cf0b1a0737edf3859bc6a175a025a90738bd Mon Sep 17 00:00:00 2001 From: adobe-bot Date: Wed, 6 May 2026 09:05:59 +0000 Subject: [PATCH 37/37] [maven-release-plugin] prepare for next development iteration --- bridgeService-data/pom.xml | 2 +- bridgeService-test-injection/pom.xml | 2 +- integroBridgeService/pom.xml | 2 +- pom.xml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bridgeService-data/pom.xml b/bridgeService-data/pom.xml index b0fa7bf..9f13018 100644 --- a/bridgeService-data/pom.xml +++ b/bridgeService-data/pom.xml @@ -53,6 +53,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.4 + 3.11.5-SNAPSHOT diff --git a/bridgeService-test-injection/pom.xml b/bridgeService-test-injection/pom.xml index 6b6c689..fd91184 100644 --- a/bridgeService-test-injection/pom.xml +++ b/bridgeService-test-injection/pom.xml @@ -78,6 +78,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.4 + 3.11.5-SNAPSHOT diff --git a/integroBridgeService/pom.xml b/integroBridgeService/pom.xml index 32b7edd..05fc6c6 100644 --- a/integroBridgeService/pom.xml +++ b/integroBridgeService/pom.xml @@ -189,6 +189,6 @@ com.adobe.campaign.tests.bridge parent - 3.11.4 + 3.11.5-SNAPSHOT diff --git a/pom.xml b/pom.xml index 5fd62bf..f5ab81f 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 4.0.0 com.adobe.campaign.tests.bridge parent - 3.11.4 + 3.11.5-SNAPSHOT Bridge Service Parent Project pom ${project.groupId}:${project.artifactId} @@ -203,7 +203,7 @@ https://github.com/adobe/bridgeService/tree/main/src scm:git::https://github.com/adobe/bridgeService.git scm:git:https://github.com/adobe/bridgeService.git - parent-3.11.4 + HEAD