Skip to content

Latest commit

 

History

History
1145 lines (909 loc) · 43.4 KB

File metadata and controls

1145 lines (909 loc) · 43.4 KB

Testing and Using BridgeService as an MCP Server

This document explains how to run BridgeService as an MCP server and verify it works, first using the built-in demo data, then from an external project that hosts its own Java library.

Table of Contents


MCP Configuration Reference

Variable Default Description
IBS.MCP.ENABLED false Enables the MCP endpoint at /mcp. Must be true for any MCP usage.
IBS.MCP.PRECHAIN JSON callContent fragment prepended to every java_call invocation. Used for server-wide setup such as shared authentication. Can also be supplied per-client via the ibs-prechain HTTP header (env var takes precedence).
IBS.MCP.REQUIRE_JAVADOC strict Controls which methods are exposed based on Javadoc quality. false = expose all public static methods. true = requires a non-empty Javadoc comment. strict (default) = requires a comment and a non-empty @param tag for every parameter.

See the relevant sections below for full configuration details and examples.


Testing with the built-in demo data

bridgeService-data is a module included in this repository that provides concrete Java classes used by the test suite. It is the quickest way to verify that the MCP endpoint is working correctly without any external dependencies.

Starting the server

Run the following command from the repository root. It starts IBS on port 8080 in demo mode (which compiles bridgeService-data into the classpath), enables the MCP endpoint, and points tool discovery at the testdata.one package:

mvn -pl integroBridgeService exec:java \
    -Dexec.args="test" \
    -Ddemo.project.mode=compile \
    -DIBS.MCP.ENABLED=true \
    -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.adobe.campaign.tests.bridge.testdata.one

To scan multiple packages, separate them with commas:

-DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.adobe.campaign.tests.bridge.testdata.one,com.adobe.campaign.tests.bridge.testdata.two

MCP handshake

Every MCP session begins with an initialize request. Send it once before any other call:

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "clientInfo": { "name": "my-client", "version": "1.0" },
      "capabilities": {}
    }
  }'

Expected response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo": { "name": "bridgeService", "version": "3.11.4" },
    "capabilities": { "tools": {} }
  }
}

Discovering tools

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

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.

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "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",
          "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": {} }
      }
    ]
  }
}

Calling tools

Single-method calls can be made directly by tool name. Multi-step chains go through java_call.

No-argument method:

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "java_call",
      "arguments": {
        "callContent": {
          "result": {
            "class": "com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods",
            "method": "methodReturningString",
            "args": []
          }
        }
      }
    }
  }'

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"returnValues\":{\"result\":\"_Success\"},\"callDurations\":{\"result\":2}}"
      }
    ],
    "isError": false
  }
}

Method with a String argument:

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tools/call",
    "params": {
      "name": "java_call",
      "arguments": {
        "callContent": {
          "result": {
            "class": "com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods",
            "method": "methodAcceptingStringArgument",
            "args": ["hello"]
          }
        }
      }
    }
  }'

Call chaining (get a country list, then pass it to another method):

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 7,
    "method": "tools/call",
    "params": {
      "name": "java_call",
      "arguments": {
        "callContent": {
          "countries": {
            "class": "com.adobe.campaign.tests.bridge.testdata.one.ClassWithLogger",
            "method": "getCountries",
            "args": []
          },
          "size": {
            "class": "com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods",
            "method": "methodAcceptingListArguments",
            "args": ["countries"]
          }
        }
      }
    }
  }'

How method discovery works

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
Loading

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.

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.

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

Individual tools are for discovery and stateless calls. Each tool call runs in a fresh isolated class loader. Complex Java objects (e.g. List<MimeMessage>) 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 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.


Call chaining best practice

Each java_call invocation runs inside a freshly isolated class loader context. Static variables set in one call are not visible to the next call — authentication state, cached connections, or any other static state established in one invocation will be gone by the time a second invocation starts.

Bundle related operations into a single java_call using call chaining: all entries in callContent share the same isolated context and execute in insertion order. The return value of an earlier step is referenced by key in the args of a later step:

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 10,
    "method": "tools/call",
    "params": {
      "name": "java_call",
      "arguments": {
        "callContent": {
          "step1": {
            "class": "com.example.Auth",
            "method": "login",
            "args": ["user", "password"]
          },
          "step2": {
            "class": "com.example.Resource",
            "method": "create",
            "args": ["step1"]
          }
        }
      }
    }
  }'

All entries in callContent execute in the same isolated context in insertion order. A prior call's return value is substituted by referencing its key as a string argument (e.g. "step1" in the args of step2).

The java_call tool description also makes this explicit, so AI agents reading the tool list will see the guidance directly.


Project-specific pre-call setup (IBS.MCP.PRECHAIN)

Some projects need one or more setup operations to run before every tool invocation — for example, an authentication step that establishes a session token in the class loader's static cache.

IBS.MCP.PRECHAIN addresses this at the server level. Set it to a JSON callContent fragment and BridgeService will prepend those calls to every java_call invocation, running them inside the same isolated context as the actual call. Pre-chain return values are stripped from the response before it is returned.

Configuration

IBS.MCP.PRECHAIN={"<key1>":{"class":"...","method":"...","args":[...]}, ...}

The value is a standard BridgeService callContent JSON object — the same format used in java_call payloads. Entries execute in insertion order, and call-chaining dependency resolution (referencing a prior entry's key in an args array) works as normal.

Alternatively, the same JSON can be supplied as the ibs-prechain HTTP header on the MCP server registration. The header is used when IBS.MCP.PRECHAIN is not set. This is useful for client-side configuration in MCP clients that support custom headers (e.g. Claude Code's .claude.json):

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "ibs-prechain": "{\"ibs_auth\":{\"class\":\"utils.CampaignUtils\",\"method\":\"setCurrentAuthenticationToLocal\",\"args\":[\"ibs-secret-url\",\"ibs-secret-login\",\"ibs-secret-pass\"]}}"
      }
    }
  }
}

Example: CampaignTests authentication

CampaignTests requires two steps before any operation:

  1. Fetch an auth token (ConnectionToken.fetchAuthFromIMSBearerToken)
  2. Store it as the current authentication (CampaignUtils.setCurrentAuthentication)

With IBS.MCP.PRECHAIN the auth is injected automatically into every java_call invocation:

IBS.MCP.PRECHAIN={"ibs_auth":{"class":"com.example.ConnectionToken","method":"fetchAuthFromIMSBearerToken","args":["ibs-secret-endpoint","ibs-secret-token"]},"ibs_set_auth":{"class":"com.example.CampaignUtils","method":"setCurrentAuthentication","args":["ibs_auth"]}}

Passing secrets securely

Sensitive values (tokens, endpoints) should be passed as HTTP headers in the MCP server registration, using the existing ibs-secret- prefix. BridgeService injects these headers into the class loader result cache at the start of every call, making them available for call-chaining dependency resolution — no extra code is required.

Register the server with credentials in the .mcp.json headers map:

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "ibs-secret-endpoint": "https://my-instance.campaign.adobe.com",
        "ibs-secret-token": "eyJ..."
      }
    }
  }
}

The prechain args reference those header names as plain strings:

IBS.MCP.PRECHAIN={"ibs_auth":{"class":"...","method":"fetchAuthFromIMSBearerToken","args":["ibs-secret-endpoint","ibs-secret-token"]}, ...}

BridgeService resolves the strings "ibs-secret-endpoint" and "ibs-secret-token" to the corresponding header values via the standard dependency mechanism — exactly as call chaining would resolve any prior result by key.

Secrets are always protected:

  • Headers with the ibs-secret- prefix are suppressed from all tool responses.
  • Pre-chain return values (e.g. ibs_auth, ibs_set_auth) are stripped from returnValues before the response is returned — only the actual tool result is visible to the caller.
  • The value of IBS.MCP.PRECHAIN is never written to logs at INFO or DEBUG level.

How prechain integrates with your call chain

The user's callContent steps execute after the prechain steps inside the same isolated context. User steps can reference prechain keys by name in their args and receive the return values by reference (same JVM heap). Prechain keys are stripped from returnValues and callDurations before the response is returned.

{
  "callContent": {
    "result": {
      "class": "com.example.Resource",
      "method": "create",
      "args": ["ibs_auth"]
    }
  }
}

Here "ibs_auth" is the key of a prechain step that returned an Authentication object. It is resolved at runtime — it is never passed as a literal string.

PRECHAIN is deployment-wide — not suitable for per-user auth

IBS.MCP.PRECHAIN is a server environment variable. It is the same for every user connecting to that deployment. This makes it the right mechanism for setup that is uniform across all callers — classloader configuration, plugin initialisation, or auth that uses a shared service account.

It is the wrong mechanism for per-user auth. On a shared IBS deployment two testers may need to connect to different Campaign instances, use different auth methods (local session vs IMS bearer), or supply different credentials. A single PRECHAIN value cannot satisfy both.

Per-user auth belongs in the project skill — a CLAUDE.md or a ~/.claude/skills/<project>.md file that each user maintains locally. The skill instructs the AI to open every java_call chain with the auth step appropriate for that user:

## BridgeService MCP usage
Always start every java_call chain with your auth step:

    "ibs_auth": {
      "class": "utils.CampaignUtils",
      "method": "setCurrentAuthenticationToLocal",
      "args": ["https://my-instance.campaign.adobe.com", "myuser", "mypassword"]
    }

User A's skill might call setCurrentAuthenticationToLocal; User B's might call fetchAuthFromIMSBearerToken. Both connect to the same IBS server with no server-side changes.

Credentials in skills should reference ibs-secret-* headers (configured in the user's .mcp.json) rather than be written in plain text:

    "args": ["ibs-secret-endpoint", "ibs-secret-token"]

Summary: what belongs where

Concern Right place Why
Per-user auth credentials and method User's skill / CLAUDE.md Differs across callers — cannot be a server-side default
Shared service-account auth IBS.MCP.PRECHAIN Truly uniform across all users
Classloader / plugin setup IBS.MCP.PRECHAIN Deployment-wide, same for everyone
Class/method names and call patterns Skill / CLAUDE.md LLM guidance, not execution

Consumer project guidance

Any project registering BridgeService as an MCP server that requires setup before every tool call should:

  1. Configure IBS.MCP.PRECHAIN with the required setup steps.
  2. Pass credentials as ibs-secret-* headers in the MCP server registration.
  3. Document the pattern in the project's own CLAUDE.md so AI agents understand they do not need to perform auth themselves — it is handled transparently by the server:
## BridgeService MCP usage
- Auth is pre-configured via IBS.MCP.PRECHAIN; do not include auth calls in your tool payloads.
- For multi-step scenarios use `java_call` with call chaining (single `callContent` payload).
  State does not persist between separate tool calls.

Built-in diagnostics tool (ibs_diagnostics)

BridgeService exposes a built-in ibs_diagnostics tool alongside java_call. It requires no arguments and has no dependency on the HOST project — it is always available regardless of whether tool discovery succeeds.

Call it via tools/call:

curl -s -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": { "name": "ibs_diagnostics", "arguments": {} }
  }'

Response:

{
  "ibsVersion": "3.11.4",
  "deploymentMode": "TEST",
  "mcpConfig": {
    "packagesConfigured": "com.example.services",
    "prechainActive": true,
    "javadocQualityGate": "strict"
  },
  "headers": {
    "secretHeaderKeys": ["ibs-secret-login", "ibs-secret-pass", "ibs-secret-url"],
    "envVarHeaders": {
      "AC.UITEST.HOST": "my-instance.example.com",
      "AC.UITEST.LANGUAGE": "en_US"
    },
    "regularHeaderCount": 3
  },
  "discoveredToolCount": 142
}

Fields:

Field Description
ibsVersion Running BridgeService version
deploymentMode TEST or PRODUCTION
mcpConfig.packagesConfigured Value of IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES
mcpConfig.prechainActive Whether a prechain is configured (env var or header)
mcpConfig.javadocQualityGate Active value of IBS.MCP.REQUIRE_JAVADOC (false, true, or strict)
headers.secretHeaderKeys Names of ibs-secret-* headers received (values suppressed)
headers.envVarHeaders Decoded env-var headers (ibs-env-* prefix stripped, uppercased)
headers.regularHeaderCount Count of headers that are neither secret nor env-var
discoveredToolCount Number of methods in the java_call catalog

Use ibs_diagnostics as the first step when connecting a new HOSTSERVICE — it confirms connectivity, verifies that all ibs-secret-* and ibs-env-* headers are reaching the server, and shows whether PRECHAIN and Javadoc requirements are active, all without touching HOST code.


Passing secrets and environment variables in MCP client config

BridgeService reads two special header prefixes from every MCP request. Both are configured in the headers block of the MCP server registration in your client config file (.claude.json, .cursor/mcp.json, etc.).

Secrets (ibs-secret-*)

Headers prefixed with ibs-secret- are injected into BridgeService's arg-resolution cache. They can be referenced by name as plain strings in java_call args arrays — BridgeService substitutes the header name with its value at runtime. Secret values are never included in tool responses or logs.

Use them for credentials that your Java methods need as arguments (URLs, usernames, passwords, tokens):

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "ibs-secret-url":   "https://my-instance.example.com/nl/jsp/soaprouter.jsp",
        "ibs-secret-login": "admin",
        "ibs-secret-pass":  "mypassword"
      }
    }
  }
}

A java_call step then references them by header name:

{
  "callContent": {
    "auth": {
      "class": "utils.CampaignUtils",
      "method": "setCurrentAuthenticationToLocal",
      "args": ["ibs-secret-url", "ibs-secret-login", "ibs-secret-pass"]
    }
  }
}

Environment variables (ibs-env-*)

Headers prefixed with ibs-env- are injected as environment variables into the Java execution context — equivalent to supplying them in the environmentVariables JSON node of a /call payload. The prefix is stripped and the remainder uppercased to form the variable name.

Use them for configuration values your Java methods read from the environment (hostnames, ports, locale, feature flags):

{
  "headers": {
    "ibs-env-AC.UITEST.HOST":     "my-instance.example.com",
    "ibs-env-AC.UITEST.LANGUAGE": "en_US",
    "ibs-env-AC.UITEST.MAILING.PORT": "143"
  }
}

The ibs-env- prefix is stripped before injection. The header ibs-env-AC.UITEST.HOST becomes the environment variable AC.UITEST.HOST — not IBS-ENV-AC.UITEST.HOST. Your Java code must read the name without the prefix. This is a common source of confusion: the header name and the variable name are different.

Key difference

Header prefix Where the value lands Referenceable in args?
ibs-secret-* Arg-resolution cache Yes — use the full header name as the arg string
ibs-env-* Java env / IntegroCache No — passing the key as an arg sends it as a literal string

Full registration example

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "ibs-secret-url":                  "https://my-instance.example.com/nl/jsp/soaprouter.jsp",
        "ibs-secret-login":                "admin",
        "ibs-secret-pass":                 "mypassword",
        "ibs-env-AC.UITEST.HOST":          "my-instance.example.com",
        "ibs-env-AC.UITEST.LANGUAGE":      "en_US",
        "ibs-env-AC.UITEST.MAILING.PORT":  "143",
        "ibs-prechain": "{\"ibs_auth\":{\"class\":\"utils.CampaignUtils\",\"method\":\"setCurrentAuthenticationToLocal\",\"args\":[\"ibs-secret-url\",\"ibs-secret-login\",\"ibs-secret-pass\"]}}"
      }
    }
  }
}

Use ibs_diagnostics to verify that all headers are reaching the server correctly — it reports secret key names, decoded env-var key/value pairs, and regular header count without exposing secret values.


Passing environment variables via headers (ibs-env-*)

Some Java methods depend on environment variables that must be set before execution — for example, a hostname, a port, or a locale that changes per deployment. Both the /call and /mcp endpoints accept these as HTTP headers with the ibs-env- prefix, as an alternative to the environmentVariables JSON node.

BridgeService reads every request header whose name begins with ibs-env-, strips the prefix, uppercases the remainder, and injects the key/value pair as an environment variable into the JavaCalls execution context — exactly as if it had been provided in the environmentVariables node of a /call payload. Header-supplied variables are merged with any variables already in the payload; payload variables take precedence for the same key.

This works for the REST /call endpoint, auto-discovered MCP tools, and the java_call fallback.

Configuring env vars in .claude.json

Add env vars to the headers block of the MCP server registration, using the ibs-env- prefix:

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "ibs-secret-login": "admin",
        "ibs-secret-pass": "mypassword",
        "ibs-env-AC.UITEST.HOST": "accintg-ci93.rd.campaign.adobe.com",
        "ibs-env-AC.UITEST.LANGUAGE": "en_US",
        "ibs-env-AC.UITEST.MAILING.PORT": "143",
        "ibs-env-AC.UITEST.MAILING.HOST": "mail.example.com"
      }
    }
  }
}

At runtime the server extracts these headers and injects:

Environment variable Value
AC.UITEST.HOST accintg-ci93.rd.campaign.adobe.com
AC.UITEST.LANGUAGE en_US
AC.UITEST.MAILING.PORT 143
AC.UITEST.MAILING.HOST mail.example.com

Prefix configuration

The default prefix is ibs-env-. It can be changed via:

IBS.ENV.HEADER.PREFIX=my-custom-prefix-

Set it to blank to disable the feature entirely:

IBS.ENV.HEADER.PREFIX=

Interaction with IBS.MCP.PRECHAIN

Env vars injected from ibs-env-* headers are populated into JavaCalls.environmentVariables inside addHeaders(), which is called before submitCalls(). Pre-chain steps that depend on env vars (e.g., a hostname resolution step) will therefore see them.


Methods intentionally excluded from the catalog

Some methods in SimpleStaticMethods are not included in the auto-discovery catalog:

Method Reason excluded
overLoadedMethod1Arg(String) and overLoadedMethod1Arg(int) Both have one parameter — ambiguous, cannot be disambiguated by parameter count
methodAcceptingFile(File) File parameters require multi-part upload, not representable as a JSON arg
Any instance method Only public static methods are discovered

These methods are still fully accessible via java_call — simply specify the class and method name directly in the callContent payload.


Exposing your own project as MCP tools

There are two ways to deploy IBS with your project, matching the two models described in the main README.

Injection model — adding IBS to your project

This is the recommended approach. You add integroBridgeService as a dependency to your project and start the server from within it.

1. Add the dependency to your pom.xml:

<dependency>
    <groupId>com.adobe.campaign.tests.bridge.service</groupId>
    <artifactId>integroBridgeService</artifactId>
    <version>3.11.4</version>
</dependency>

2. Start the service with MCP enabled and your package(s) listed:

mvn compile exec:java \
    -Dexec.mainClass=MainContainer \
    -Dexec.args="test" \
    -DIBS.MCP.ENABLED=true \
    -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.example.myproject.services

All public static methods found in com.example.myproject.services (and its sub-packages) are immediately available as MCP tools.

Aggregator model — adding your project to IBS

In this model you clone (or fork) the BridgeService repository, add your project as a Maven dependency inside integroBridgeService/pom.xml, and build a fat JAR or Docker image that bundles everything together.

<!-- inside integroBridgeService/pom.xml -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>myproject</artifactId>
    <version>1.0.0</version>
</dependency>

Then build and start:

mvn clean package
java -jar integroBridgeService/target/integroBridgeService-*.jar test \
    -DIBS.MCP.ENABLED=true \
    -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.example.myproject.services

Configuring tool discovery

The environment variable IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES controls which packages are scanned at startup. It accepts a comma-separated list of package prefixes:

IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.example.services,com.example.utils

Only public static methods in classes directly under those packages (and sub-packages) are registered as tools. The same variable also drives the IBS class loader isolation, so every class your methods transitively depend on must be reachable under one of the listed prefixes — or in the system classpath.

Surfacing Javadoc as tool descriptions

By default, tool descriptions fall back to a generated string ("Calls com.example.MyClass.methodName()"). To have the actual Javadoc comment appear as the tool description in tools/list, add the therapi-runtime-javadoc-scribe annotation processor to your project's pom.xml:

<dependency>
    <groupId>com.github.therapi</groupId>
    <artifactId>therapi-runtime-javadoc-scribe</artifactId>
    <version>0.15.0</version>
    <scope>provided</scope>
</dependency>

No extra Maven goals are required. The annotation processor runs automatically during the compile phase, which is already part of your existing mvn clean package. It embeds the Javadoc comments as resource files inside your compiled JAR (e.g. javadoc/com/example/EmailService.json).

At startup, MCPToolDiscovery reads those embedded resources via RuntimeJavadoc.getJavadoc(method). As long as your JAR is on BridgeService's classpath when the server starts, the descriptions are picked up transparently — no configuration needed beyond the dependency above.

@param Javadoc tags are also read and used as the parameter descriptions in the tool's inputSchema, helping the AI understand what each argument expects.

Without the dependency, tools are still fully functional; only the description quality is reduced.

Javadoc quality gate

BridgeService enforces a configurable documentation quality gate via IBS.MCP.REQUIRE_JAVADOC. Methods that do not satisfy the active gate are silently skipped at startup and will not appear in tools/list.

The three levels are:

Value What is required to be exposed
false Nothing — all public static methods are exposed regardless of documentation
true A non-empty Javadoc comment on the method
strict (default) A non-empty comment and a non-empty @param tag for every parameter

Why strict is the default: a method with parameters but no @param tags causes BridgeService to fall back to the Java type name as the parameter description (e.g. "description": "String"). This gives the AI agent almost no guidance on what to pass, which leads to incorrect calls and wasted round-trips. strict prevents such tools from appearing in the catalog.

At startup, BridgeService logs the active gate level and its effect so you can confirm your configuration is applied:

MCPRequestHandler ready: 12 individual tool(s) + java_call + ibs_diagnostics.
Javadoc quality gate: strict — only methods with Javadoc comment + @param for every parameter are exposed

To use the previous default (non-empty comment only):

IBS.MCP.REQUIRE_JAVADOC=true

To expose all public static methods (no documentation required):

IBS.MCP.REQUIRE_JAVADOC=false

Writing good Javadoc for MCP tools goes beyond just satisfying the gate. Descriptions should make the testing or domain purpose self-evident so an AI agent can distinguish your tools from others in a multi-server session:

Weak Better
/** Returns a list of countries. */ /** Returns the fixed list of ISO 3166-1 country codes (AT, AU, CA, CH, DE) used as test fixtures for campaign validation. */
/** Sends an email. */ /** Sends a test email via the configured SMTP mock and returns the delivery receipt ID. */
/** Gets the cache value. */ /** Returns the value stored under the given key in the in-process integro test-execution cache. */

Include @param tags for each argument — BridgeService uses them to populate the parameter descriptions in the tool's inputSchema.

What tools will be generated

Given a project with the following class (with therapi-runtime-javadoc-scribe on the classpath):

package com.example.myproject.services;

public class EmailService {
    /** Sends an email to the given recipient with the specified subject. */
    public static String sendEmail(String recipient, String subject) { ... }
    /** Returns the list of message subjects in the given account's inbox. */
    public static List<String> listInbox(String account) { ... }
    /** Deletes all messages in the given account's inbox. */
    public static void purgeInbox(String account) { ... }
    public String getStatus() { ... }  // instance method — excluded
}

IBS would embed the following catalog entries in the java_call description, with descriptions sourced from Javadoc:

Catalog entry Description Args
EmailService_sendEmail Sends an email to the given recipient with the specified subject. arg0: string, arg1: string
EmailService_listInbox Returns the list of message subjects in the given account's inbox. arg0: string
EmailService_purgeInbox Deletes all messages in the given account's inbox. arg0: string

Without therapi-runtime-javadoc-scribe, the descriptions would fall back to "Calls com.example.myproject.services.EmailService.sendEmail()" etc.

getStatus() is excluded because it is an instance method.

An AI agent calling tools/list reads the catalog, then invokes methods via java_call by placing the listed class and method values in a callContent entry. For instance methods, overloaded methods, or call chaining across multiple steps, the same java_call payload handles all cases — see the Making Java Calls section of the main README.


Connecting to Claude Code

Claude Code supports MCP servers over HTTP natively. Once BridgeService is running with MCP enabled, you can register it as a named MCP server and Claude Code will automatically call tools/list at startup and make the tools available during your session.

Naming your MCP server

BridgeService is generic infrastructure that can be deployed for many different projects. The name you give it at registration time becomes the namespace for all its tools in MCP clients — Claude Code, for example, exposes tools as mcp__<serverName>__ClassName_method.

Always name the server after your project, not "bridgeService". This makes tools self-contextualising in a multi-server session:

Registration name Tool name seen by agent
bridgeService (generic, avoid) mcp__bridgeService__EmailService_sendEmail
CampaignTests (project-specific) mcp__CampaignTests__EmailService_sendEmail
EmailAutomation (feature-specific) mcp__EmailAutomation__EmailService_sendEmail

The name is set entirely on the client side when you register the server — no BridgeService configuration is needed. See Register the MCP server for the exact command.

Start BridgeService

Using the demo data (quickest way to try it):

mvn -pl integroBridgeService exec:java \
    -Dexec.args="test" \
    -Ddemo.project.mode=compile \
    -DIBS.MCP.ENABLED=true \
    -DIBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES=com.adobe.campaign.tests.bridge.testdata.one

For your own project, start BridgeService with your packages configured instead (see Exposing your own project as MCP tools).

Register the MCP server

In a separate terminal, register the running BridgeService instance as an MCP server in Claude Code. Replace CampaignTests with the name of your project (see Naming your MCP server). Use --scope user to make it available globally across all projects, or omit it to register it only for the current project:

# Register globally (available in all Claude Code sessions)
claude mcp add --transport http CampaignTests http://localhost:8080/mcp --scope user

# Register for the current project only
claude mcp add --transport http CampaignTests http://localhost:8080/mcp

This writes an entry to ~/.claude/mcp.json (global) or .mcp.json in the project root (project-scoped). The resulting config entry looks like:

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp"
    }
  }
}

If BridgeService is deployed remotely (Docker/K8s), replace http://localhost:8080 with the actual deployment URL.

Verify the connection

List all registered MCP servers and their status:

claude mcp list

You should see bridgeService listed as connected. You can also check inside an interactive Claude Code session with the /mcp slash command.

Once connected, Claude Code will discover the java_call tool and its method catalog via tools/list at the start of each session. You can then ask Claude to call your Java methods — for example:

"Use java_call to call SimpleStaticMethods.methodAcceptingStringArgument with the argument hello"

To remove the server registration when you no longer need it:

claude mcp remove bridgeService

Connecting from Cursor

Cursor reads MCP server configuration from ~/.cursor/mcp.json (global) or .cursor/mcp.json in the project root. The format is identical to Claude Code's config. Add an entry manually:

{
  "mcpServers": {
    "CampaignTests": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "ibs-secret-login": "admin",
        "ibs-secret-pass": "mypassword",
        "ibs-secret-url": "https://my-instance.example.com/nl/jsp/soaprouter.jsp",
        "ibs-prechain": "{\"ibs_auth\":{\"class\":\"utils.CampaignUtils\",\"method\":\"setCurrentAuthenticationToLocal\",\"args\":[\"ibs-secret-url\",\"ibs-secret-login\",\"ibs-secret-pass\"]}}"
      }
    }
  }
}

Restart Cursor after editing the file. Cursor will call tools/list at session start and the java_call catalog will be available to the AI.

Other MCP clients

Any MCP client that supports the 2024-11-05 protocol version over HTTP can connect to BridgeService. The registration format varies by client but the required values are always the same:

Field Value
Transport HTTP (stateless JSON-RPC over POST)
URL http://<host>:8080/mcp
Headers ibs-secret-* for credentials, ibs-env-* for env vars, ibs-prechain for per-client prechain

Consult your client's documentation for where to place the config file and how to pass custom HTTP headers.


Best Practices

Javadoc is your tool description — garbage in, garbage out

The AI agent sees exactly what you write in Javadoc. Nothing more, nothing less.

BridgeService embeds Javadoc comments into the java_call catalog at startup. When an AI reads tools/list, the catalog is its only source of truth about what each method does, what its parameters mean, and when to call it. A vague or missing description produces a vague or wrong tool invocation.

Treat every Javadoc comment as a prompt you are writing for the AI.

Weak Why it fails Better
/** Creates a recipient. */ No context — the AI cannot tell when or why /** Creates a randomly generated test recipient in the nms:recipient schema and returns its internal ID. */
/** @param auth the auth */ Circular — adds no information /** @param auth Authentication object returned by setCurrentAuthenticationToLocal */
/** Sends email. */ Too generic — ambiguous in a multi-tool session /** Sends the prepared delivery to all recipients in its target list and returns the delivery log ID. */
No @param tags AI has to guess argument purpose and order One @param per argument, describing what value is expected

What to include in every exposed method's Javadoc:

  1. What the method does — in domain terms, not implementation terms.
  2. What it returns — the type and meaning of the return value.
  3. What each parameter expects — use @param tags; BridgeService uses them as argument descriptions in the tool schema.
  4. When to use it vs similar methods — if overloads or related methods exist, say which scenario each is for.

The quality gate enforces the minimum bar. IBS.MCP.REQUIRE_JAVADOC=strict (the default) silently drops any method that lacks a comment or is missing @param tags — it will not appear in tools/list and cannot be called via auto-discovery. Passing the gate is necessary but not sufficient: a one-word description with perfunctory @param tags passes the gate but still produces a low-quality tool entry.

Good Javadoc pays compound interest. A well-described method is discovered correctly the first time, requires no follow-up prompting, and stays reliable as the AI session context grows. Poor descriptions lead to incorrect calls, wasted round-trips, and subtle bugs that are hard to trace back to a missing @param.