From 329d2ef8d531a5d8787797607904e7bd5d89f5c3 Mon Sep 17 00:00:00 2001 From: baubakg Date: Thu, 16 Apr 2026 22:17:29 +0200 Subject: [PATCH 01/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] [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/31] [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/31] 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/31] 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/31] 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/31] 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/31] [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/31] [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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] [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/31] [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/31] 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/31] 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/31] 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/31] [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/31] [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