Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ mvn package

**Quality gates that must pass before merging:** unit tests pass, no coverage decrease, SonarCloud gate green, license headers present.

**Coverage rules (enforced by SonarCloud and codecov/patch in CI):**
- Overall line coverage must not decrease from the baseline on `main`.
- New/changed code (the PR patch) must reach **80% line coverage** — this is the `codecov/patch` check. If it fails, add targeted tests for the uncovered branches in your new code.
- JaCoCo reports are generated at `target/site/jacoco/` after `mvn test`. Check method-level branch coverage there before pushing.

## Module Structure

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,23 @@ public static String overLoadedMethod1Arg(int in_intArgument) {
return in_intArgument + SUCCESS_VAL;
}

// For testing MCP tool discovery of overloads with different arity.
// The 1-arg variant has no Javadoc (exercises the skip path for overloads).
public static String overLoadedMethodDifferentArity(String in_arg) {
return in_arg + SUCCESS_VAL;
}

/**
* Two-argument overload for testing MCP tool name disambiguation by arity.
*
* @param in_arg1 first string argument
* @param in_arg2 second string argument
* @return concatenation of both arguments with the success suffix
*/
public static String overLoadedMethodDifferentArity(String in_arg1, String in_arg2) {
return in_arg1 + in_arg2 + SUCCESS_VAL;
}

//For impossible Objects exception
public static String complexMethodAcceptor(Instantiable in_arg) {
return SUCCESS_VAL;
Expand Down
50 changes: 35 additions & 15 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ using the built-in demo data, then from an external project that hosts its own J
|---|---|---|
| `IBS.MCP.ENABLED` | `false` | Enables the MCP endpoint at `/mcp`. Must be `true` for any MCP usage. |
| `IBS.MCP.PRECHAIN` | — | JSON `callContent` fragment prepended to every `java_call` invocation. Used for server-wide setup such as shared authentication. Can also be supplied per-client via the `ibs-prechain` HTTP header (env var takes precedence). |
| `IBS.MCP.REQUIRE_JAVADOC` | `true` | When `true`, only methods with a non-empty Javadoc comment are included in the tool catalog. Methods without Javadoc are silently excluded from `tools/list`. |
| `IBS.MCP.REQUIRE_JAVADOC` | `strict` | Controls which methods are exposed based on Javadoc quality. `false` = expose all public static methods. `true` = requires a non-empty Javadoc comment. `strict` (default) = requires a comment **and** a non-empty `@param` tag for every parameter. |

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

Expand Down Expand Up @@ -558,7 +558,7 @@ Response:
"mcpConfig": {
"packagesConfigured": "com.example.services",
"prechainActive": true,
"javadocRequired": true
"javadocQualityGate": "strict"
},
"headers": {
"secretHeaderKeys": ["ibs-secret-login", "ibs-secret-pass", "ibs-secret-url"],
Expand All @@ -580,7 +580,7 @@ Response:
| `deploymentMode` | `TEST` or `PRODUCTION` |
| `mcpConfig.packagesConfigured` | Value of `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` |
| `mcpConfig.prechainActive` | Whether a prechain is configured (env var or header) |
| `mcpConfig.javadocRequired` | Whether `IBS.MCP.REQUIRE_JAVADOC` is enabled |
| `mcpConfig.javadocQualityGate` | Active value of `IBS.MCP.REQUIRE_JAVADOC` (`false`, `true`, or `strict`) |
| `headers.secretHeaderKeys` | Names of `ibs-secret-*` headers received (values suppressed) |
| `headers.envVarHeaders` | Decoded env-var headers (`ibs-env-*` prefix stripped, uppercased) |
| `headers.regularHeaderCount` | Count of headers that are neither secret nor env-var |
Expand Down Expand Up @@ -883,16 +883,36 @@ Without the dependency, tools are still fully functional; only the description q

### Javadoc quality gate

By default (`IBS.MCP.REQUIRE_JAVADOC=true`), BridgeService **only exposes methods that have a
non-empty Javadoc comment**. Methods without Javadoc are silently skipped at startup and will not
appear in `tools/list`.
BridgeService enforces a configurable documentation quality gate via `IBS.MCP.REQUIRE_JAVADOC`.
Methods that do not satisfy the active gate are silently skipped at startup and will not appear
in `tools/list`.

This is intentional. A method with no Javadoc would receive a generic fallback description such as
`"Calls com.example.MyClass.method()"`, which gives an AI agent no useful information about when
or why to call it. Exposing such tools increases the risk of accidental invocations.
The three levels are:

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

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

At startup, BridgeService logs the active gate level and its effect so you can confirm your
configuration is applied:
```
MCPRequestHandler ready: 12 individual tool(s) + java_call + ibs_diagnostics.
Javadoc quality gate: strict — only methods with Javadoc comment + @param for every parameter are exposed
```

**To use the previous default** (non-empty comment only):
```
IBS.MCP.REQUIRE_JAVADOC=true
```

**To expose all public static methods** (no documentation required):
```
IBS.MCP.REQUIRE_JAVADOC=false
```
Expand Down Expand Up @@ -1113,11 +1133,11 @@ tool invocation.
3. **What each parameter expects** — use `@param` tags; BridgeService uses them as argument descriptions in the tool schema.
4. **When to use it vs similar methods** — if overloads or related methods exist, say which scenario each is for.

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

**Good Javadoc pays compound interest.** A well-described method is discovered correctly the
first time, requires no follow-up prompting, and stays reliable as the AI session context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ public void activate(String in_value) {
"IBS.DESERIALIZATION.DATE.FORMAT", "NONE", false, "The date format to be used for deserialization."),
MCP_ENABLED("IBS.MCP.ENABLED", "false", false,
"When set to true, enables the MCP server endpoint at POST /mcp, exposing configured packages as tools."),
MCP_REQUIRE_JAVADOC("IBS.MCP.REQUIRE_JAVADOC", "true", false,
"When true (default), only methods with a non-empty Javadoc comment are exposed as MCP tools. "
+ "Methods without Javadoc are silently skipped. Set to false to expose all public static methods."),
MCP_REQUIRE_JAVADOC("IBS.MCP.REQUIRE_JAVADOC", "strict", false,
"Controls which methods are exposed as MCP tools based on Javadoc quality. "
+ "Accepted values: 'false' (expose all public static methods), "
+ "'true' (requires non-empty Javadoc comment), "
+ "'strict' (requires comment + non-empty @param for every parameter, default)."),
MCP_PRECHAIN("IBS.MCP.PRECHAIN", null, false,
"JSON callContent fragment prepended to every auto-discovered MCP tool invocation. "
+ "Entries execute in the same isolated context as the actual call, so call-chaining "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,13 @@
l_diagnosticsTool.put("inputSchema", DIAGNOSTICS_SCHEMA_MAP);
tools.add(l_diagnosticsTool);
this.toolList = Collections.unmodifiableList(tools);
log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics.",
discoveredToolCount);
String l_javadocExplained = ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("strict")

Check warning on line 140 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbgJq2l4d7xGgWe&open=AZ34NQbgJq2l4d7xGgWe&pullRequest=52
? "strict — only methods with Javadoc comment + @param for every parameter are exposed"
: ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("false")
? "false — all public static methods are exposed regardless of documentation"
: "true — only methods with a non-empty Javadoc comment are exposed";

Check warning on line 144 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbgJq2l4d7xGgWf&open=AZ34NQbgJq2l4d7xGgWf&pullRequest=52
log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics. "
+ "Javadoc quality gate: {}", discoveredToolCount, l_javadocExplained);
}

/**
Expand Down Expand Up @@ -338,8 +343,8 @@
ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.fetchValue());
String prechain = ConfigValueHandlerIBS.MCP_PRECHAIN.fetchValue();
mcpConfig.put("prechainActive", prechain != null && !prechain.isBlank());
mcpConfig.put("javadocRequired",
Boolean.parseBoolean(ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue()));
mcpConfig.put("javadocQualityGate",
ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue());
diag.put("mcpConfig", mcpConfig);

String secretPrefix = ConfigValueHandlerIBS.SECRETS_FILTER_PREFIX.fetchValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@
public final List<Map<String, Object>> tools;
/** Maps each tool name to the Java Method it represents, used to build the catalog in the java_call description. */
public final Map<String, Method> methodRegistry;
/** Number of methods skipped by the active Javadoc quality gate. */
public final int skippedCount;

public DiscoveryResult(List<Map<String, Object>> tools, Map<String, Method> methodRegistry) {
public DiscoveryResult(List<Map<String, Object>> tools, Map<String, Method> methodRegistry, int skippedCount) {
this.tools = Collections.unmodifiableList(tools);
this.methodRegistry = Collections.unmodifiableMap(methodRegistry);
this.skippedCount = skippedCount;
}
}

Expand All @@ -57,11 +60,12 @@
public static DiscoveryResult discoverTools(String packagesCsv) {
List<Map<String, Object>> tools = new ArrayList<>();
Map<String, Method> registry = new LinkedHashMap<>();
int[] l_skipped = {0};

Check warning on line 63 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34OtWjR2iCaMNMh7Li&open=AZ34OtWjR2iCaMNMh7Li&pullRequest=52

if (packagesCsv == null || packagesCsv.trim().isEmpty()) {
log.warn("IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES is not set — no tools will be discovered for MCP. "
+ "Set this property to enable tool discovery.");
return new DiscoveryResult(tools, registry);
return new DiscoveryResult(tools, registry, 0);
}

// Strip trailing dots that IBS uses as package separators (e.g. "com.example.")
Expand Down Expand Up @@ -95,9 +99,8 @@
if (overloads.size() == 1) {
// Unique method name on this class — use simple tool name
Method method = overloads.get(0);
if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true") && !hasJavadoc(method)) {
log.debug("Skipping {}.{} — no Javadoc (IBS.MCP.REQUIRE_JAVADOC=true)",
clazz.getSimpleName(), methodName);
if (shouldSkipForJavadoc(method, clazz.getSimpleName(), methodName)) {
l_skipped[0]++;
} else {
String toolName = clazz.getSimpleName() + "_" + methodName;
registerTool(tools, registry, toolName, method);
Expand All @@ -113,23 +116,22 @@
+ "use the java_call tool to invoke them directly.",
clazz.getName(), methodName, countEntry.getKey());
} else {
Method method = countEntry.getValue().get(0);
if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true") && !hasJavadoc(method)) {
log.debug("Skipping {}.{} — no Javadoc (IBS.MCP.REQUIRE_JAVADOC=true)",
clazz.getSimpleName(), methodName);
Method lt_method = countEntry.getValue().get(0);

Check warning on line 119 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWV&open=AZ34NQbLJq2l4d7xGgWV&pullRequest=52
if (shouldSkipForJavadoc(lt_method, clazz.getSimpleName(), methodName)) {
l_skipped[0]++;
} else {
String toolName = clazz.getSimpleName() + "_" + methodName + "_" + countEntry.getKey();
registerTool(tools, registry, toolName, method);
String lt_toolName = clazz.getSimpleName() + "_" + methodName + "_" + countEntry.getKey();

Check warning on line 123 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWW&open=AZ34NQbLJq2l4d7xGgWW&pullRequest=52
registerTool(tools, registry, lt_toolName, lt_method);
}
}
}
}
}
}

log.info("MCP tool discovery complete: {} tool(s) registered from {} class(es).",
tools.size(), allClasses.size());
return new DiscoveryResult(tools, registry);
log.info("MCP tool discovery complete: {} tool(s) registered, {} skipped by quality gate ({}), from {} class(es).",
tools.size(), l_skipped[0], ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue(), allClasses.size());

Check warning on line 133 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Invoke method(s) only conditionally.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34OtWjR2iCaMNMh7Lj&open=AZ34OtWjR2iCaMNMh7Lj&pullRequest=52
return new DiscoveryResult(tools, registry, l_skipped[0]);
}

private static void registerTool(List<Map<String, Object>> tools, Map<String, Method> registry,
Expand Down Expand Up @@ -182,6 +184,42 @@
}
}

/**
* Returns true if the method has a non-empty Javadoc comment AND a non-empty
* {@code @param} tag for every parameter. Methods with no parameters pass if
* they have a non-empty comment.
*/
static boolean hasAdequateJavadoc(Method method) {
try {
MethodJavadoc l_javadoc = RuntimeJavadoc.getJavadoc(method);

Check warning on line 194 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWY&open=AZ34NQbLJq2l4d7xGgWY&pullRequest=52
if (l_javadoc == null || COMMENT_FORMATTER.format(l_javadoc.getComment()).isEmpty()) return false;
int l_paramCount = method.getParameterCount();

Check warning on line 196 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWX&open=AZ34NQbLJq2l4d7xGgWX&pullRequest=52
if (l_paramCount == 0) return true;
List<ParamJavadoc> l_params = l_javadoc.getParams();

Check warning on line 198 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWZ&open=AZ34NQbLJq2l4d7xGgWZ&pullRequest=52
if (l_params.size() < l_paramCount) return false;
return l_params.stream().allMatch(p -> !COMMENT_FORMATTER.format(p.getComment()).isEmpty());
} catch (Exception e) {
return false;
}
}

private static boolean shouldSkipForJavadoc(Method in_method, String in_clazz, String in_method_name) {

Check warning on line 206 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWa&open=AZ34NQbLJq2l4d7xGgWa&pullRequest=52

Check warning on line 206 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWb&open=AZ34NQbLJq2l4d7xGgWb&pullRequest=52

Check warning on line 206 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWc&open=AZ34NQbLJq2l4d7xGgWc&pullRequest=52
if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("strict")) {
if (!hasAdequateJavadoc(in_method)) {
log.debug("Skipping {}.{} — Javadoc quality gate (strict) not met: missing comment or @param",
in_clazz, in_method_name);
return true;
}
} else if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true")) {
if (!hasJavadoc(in_method)) {

Check warning on line 214 in integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this if statement with the enclosing one.

See more on https://sonarcloud.io/project/issues?id=adobe_bridgeService&issues=AZ34NQbLJq2l4d7xGgWd&open=AZ34NQbLJq2l4d7xGgWd&pullRequest=52
log.debug("Skipping {}.{} — Javadoc quality gate (true) not met: no Javadoc comment",
in_clazz, in_method_name);
return true;
}
}
return false;
}

/**
* Builds a JSON Schema object describing the input parameters of a method.
* Parameter names are generated as arg0, arg1, ... since Java reflection
Expand Down
Loading
Loading