diff --git a/CLAUDE.md b/CLAUDE.md index 822ad20..50a73bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,11 @@ mvn package **Quality gates that must pass before merging:** unit tests pass, no coverage decrease, SonarCloud gate green, license headers present. +**Coverage rules (enforced by SonarCloud and codecov/patch in CI):** +- Overall line coverage must not decrease from the baseline on `main`. +- New/changed code (the PR patch) must reach **80% line coverage** — this is the `codecov/patch` check. If it fails, add targeted tests for the uncovered branches in your new code. +- JaCoCo reports are generated at `target/site/jacoco/` after `mvn test`. Check method-level branch coverage there before pushing. + ## Module Structure ``` diff --git a/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java b/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java index c2235b3..a95d082 100644 --- a/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java +++ b/bridgeService-data/src/main/java/com/adobe/campaign/tests/bridge/testdata/one/SimpleStaticMethods.java @@ -159,6 +159,23 @@ public static String overLoadedMethod1Arg(int in_intArgument) { return in_intArgument + SUCCESS_VAL; } + // For testing MCP tool discovery of overloads with different arity. + // The 1-arg variant has no Javadoc (exercises the skip path for overloads). + public static String overLoadedMethodDifferentArity(String in_arg) { + return in_arg + SUCCESS_VAL; + } + + /** + * Two-argument overload for testing MCP tool name disambiguation by arity. + * + * @param in_arg1 first string argument + * @param in_arg2 second string argument + * @return concatenation of both arguments with the success suffix + */ + public static String overLoadedMethodDifferentArity(String in_arg1, String in_arg2) { + return in_arg1 + in_arg2 + SUCCESS_VAL; + } + //For impossible Objects exception public static String complexMethodAcceptor(Instantiable in_arg) { return SUCCESS_VAL; diff --git a/docs/MCP.md b/docs/MCP.md index cc58426..e12f0a2 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -43,7 +43,7 @@ using the built-in demo data, then from an external project that hosts its own J |---|---|---| | `IBS.MCP.ENABLED` | `false` | Enables the MCP endpoint at `/mcp`. Must be `true` for any MCP usage. | | `IBS.MCP.PRECHAIN` | — | JSON `callContent` fragment prepended to every `java_call` invocation. Used for server-wide setup such as shared authentication. Can also be supplied per-client via the `ibs-prechain` HTTP header (env var takes precedence). | -| `IBS.MCP.REQUIRE_JAVADOC` | `true` | When `true`, only methods with a non-empty Javadoc comment are included in the tool catalog. Methods without Javadoc are silently excluded from `tools/list`. | +| `IBS.MCP.REQUIRE_JAVADOC` | `strict` | Controls which methods are exposed based on Javadoc quality. `false` = expose all public static methods. `true` = requires a non-empty Javadoc comment. `strict` (default) = requires a comment **and** a non-empty `@param` tag for every parameter. | See the relevant sections below for full configuration details and examples. @@ -558,7 +558,7 @@ Response: "mcpConfig": { "packagesConfigured": "com.example.services", "prechainActive": true, - "javadocRequired": true + "javadocQualityGate": "strict" }, "headers": { "secretHeaderKeys": ["ibs-secret-login", "ibs-secret-pass", "ibs-secret-url"], @@ -580,7 +580,7 @@ Response: | `deploymentMode` | `TEST` or `PRODUCTION` | | `mcpConfig.packagesConfigured` | Value of `IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES` | | `mcpConfig.prechainActive` | Whether a prechain is configured (env var or header) | -| `mcpConfig.javadocRequired` | Whether `IBS.MCP.REQUIRE_JAVADOC` is enabled | +| `mcpConfig.javadocQualityGate` | Active value of `IBS.MCP.REQUIRE_JAVADOC` (`false`, `true`, or `strict`) | | `headers.secretHeaderKeys` | Names of `ibs-secret-*` headers received (values suppressed) | | `headers.envVarHeaders` | Decoded env-var headers (`ibs-env-*` prefix stripped, uppercased) | | `headers.regularHeaderCount` | Count of headers that are neither secret nor env-var | @@ -883,16 +883,36 @@ Without the dependency, tools are still fully functional; only the description q ### Javadoc quality gate -By default (`IBS.MCP.REQUIRE_JAVADOC=true`), BridgeService **only exposes methods that have a -non-empty Javadoc comment**. Methods without Javadoc are silently skipped at startup and will not -appear in `tools/list`. +BridgeService enforces a configurable documentation quality gate via `IBS.MCP.REQUIRE_JAVADOC`. +Methods that do not satisfy the active gate are silently skipped at startup and will not appear +in `tools/list`. -This is intentional. A method with no Javadoc would receive a generic fallback description such as -`"Calls com.example.MyClass.method()"`, which gives an AI agent no useful information about when -or why to call it. Exposing such tools increases the risk of accidental invocations. +The three levels are: -**To opt out** (expose all public static methods regardless of documentation): +| Value | What is required to be exposed | +|---|---| +| `false` | Nothing — all public static methods are exposed regardless of documentation | +| `true` | A non-empty Javadoc comment on the method | +| `strict` *(default)* | A non-empty comment **and** a non-empty `@param` tag for every parameter | + +**Why `strict` is the default:** a method with parameters but no `@param` tags causes BridgeService +to fall back to the Java type name as the parameter description (e.g. `"description": "String"`). +This gives the AI agent almost no guidance on what to pass, which leads to incorrect calls and +wasted round-trips. `strict` prevents such tools from appearing in the catalog. + +At startup, BridgeService logs the active gate level and its effect so you can confirm your +configuration is applied: +``` +MCPRequestHandler ready: 12 individual tool(s) + java_call + ibs_diagnostics. +Javadoc quality gate: strict — only methods with Javadoc comment + @param for every parameter are exposed +``` + +**To use the previous default** (non-empty comment only): +``` +IBS.MCP.REQUIRE_JAVADOC=true +``` +**To expose all public static methods** (no documentation required): ``` IBS.MCP.REQUIRE_JAVADOC=false ``` @@ -1113,11 +1133,11 @@ tool invocation. 3. **What each parameter expects** — use `@param` tags; BridgeService uses them as argument descriptions in the tool schema. 4. **When to use it vs similar methods** — if overloads or related methods exist, say which scenario each is for. -**The quality gate enforces the minimum bar.** `IBS.MCP.REQUIRE_JAVADOC=true` (the default) -silently drops any method with no Javadoc from the catalog entirely — it will not appear in -`tools/list` and cannot be called via auto-discovery. Passing the gate (a non-empty comment) -is necessary but not sufficient: a one-word description passes the gate but still produces a -useless tool entry. +**The quality gate enforces the minimum bar.** `IBS.MCP.REQUIRE_JAVADOC=strict` (the default) +silently drops any method that lacks a comment or is missing `@param` tags — it will not appear in +`tools/list` and cannot be called via auto-discovery. Passing the gate is necessary but not +sufficient: a one-word description with perfunctory `@param` tags passes the gate but still +produces a low-quality tool entry. **Good Javadoc pays compound interest.** A well-described method is discovered correctly the first time, requires no follow-up prompting, and stays reliable as the AI session context diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java index 26919fc..f6a1bfb 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/ConfigValueHandlerIBS.java @@ -73,9 +73,11 @@ public void activate(String in_value) { "IBS.DESERIALIZATION.DATE.FORMAT", "NONE", false, "The date format to be used for deserialization."), MCP_ENABLED("IBS.MCP.ENABLED", "false", false, "When set to true, enables the MCP server endpoint at POST /mcp, exposing configured packages as tools."), - MCP_REQUIRE_JAVADOC("IBS.MCP.REQUIRE_JAVADOC", "true", false, - "When true (default), only methods with a non-empty Javadoc comment are exposed as MCP tools. " - + "Methods without Javadoc are silently skipped. Set to false to expose all public static methods."), + MCP_REQUIRE_JAVADOC("IBS.MCP.REQUIRE_JAVADOC", "strict", false, + "Controls which methods are exposed as MCP tools based on Javadoc quality. " + + "Accepted values: 'false' (expose all public static methods), " + + "'true' (requires non-empty Javadoc comment), " + + "'strict' (requires comment + non-empty @param for every parameter, default)."), MCP_PRECHAIN("IBS.MCP.PRECHAIN", null, false, "JSON callContent fragment prepended to every auto-discovered MCP tool invocation. " + "Entries execute in the same isolated context as the actual call, so call-chaining " diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java index 52815ae..dddc3ba 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPRequestHandler.java @@ -137,8 +137,13 @@ public MCPRequestHandler() { l_diagnosticsTool.put("inputSchema", DIAGNOSTICS_SCHEMA_MAP); tools.add(l_diagnosticsTool); this.toolList = Collections.unmodifiableList(tools); - log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics.", - discoveredToolCount); + String l_javadocExplained = ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("strict") + ? "strict — only methods with Javadoc comment + @param for every parameter are exposed" + : ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("false") + ? "false — all public static methods are exposed regardless of documentation" + : "true — only methods with a non-empty Javadoc comment are exposed"; + log.info("MCPRequestHandler ready: {} individual tool(s) + java_call + ibs_diagnostics. " + + "Javadoc quality gate: {}", discoveredToolCount, l_javadocExplained); } /** @@ -338,8 +343,8 @@ private String handleDiagnostics(Object id, Map headers) { ConfigValueHandlerIBS.STATIC_INTEGRITY_PACKAGES.fetchValue()); String prechain = ConfigValueHandlerIBS.MCP_PRECHAIN.fetchValue(); mcpConfig.put("prechainActive", prechain != null && !prechain.isBlank()); - mcpConfig.put("javadocRequired", - Boolean.parseBoolean(ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue())); + mcpConfig.put("javadocQualityGate", + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue()); diag.put("mcpConfig", mcpConfig); String secretPrefix = ConfigValueHandlerIBS.SECRETS_FILTER_PREFIX.fetchValue(); diff --git a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java index c95b760..ab79654 100644 --- a/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java +++ b/integroBridgeService/src/main/java/com/adobe/campaign/tests/bridge/service/MCPToolDiscovery.java @@ -40,10 +40,13 @@ public static class DiscoveryResult { public final List> tools; /** Maps each tool name to the Java Method it represents, used to build the catalog in the java_call description. */ public final Map methodRegistry; + /** Number of methods skipped by the active Javadoc quality gate. */ + public final int skippedCount; - public DiscoveryResult(List> tools, Map methodRegistry) { + public DiscoveryResult(List> tools, Map methodRegistry, int skippedCount) { this.tools = Collections.unmodifiableList(tools); this.methodRegistry = Collections.unmodifiableMap(methodRegistry); + this.skippedCount = skippedCount; } } @@ -57,11 +60,12 @@ public DiscoveryResult(List> tools, Map meth public static DiscoveryResult discoverTools(String packagesCsv) { List> tools = new ArrayList<>(); Map registry = new LinkedHashMap<>(); + int[] l_skipped = {0}; if (packagesCsv == null || packagesCsv.trim().isEmpty()) { log.warn("IBS.CLASSLOADER.STATIC.INTEGRITY.PACKAGES is not set — no tools will be discovered for MCP. " + "Set this property to enable tool discovery."); - return new DiscoveryResult(tools, registry); + return new DiscoveryResult(tools, registry, 0); } // Strip trailing dots that IBS uses as package separators (e.g. "com.example.") @@ -95,9 +99,8 @@ public static DiscoveryResult discoverTools(String packagesCsv) { if (overloads.size() == 1) { // Unique method name on this class — use simple tool name Method method = overloads.get(0); - if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true") && !hasJavadoc(method)) { - log.debug("Skipping {}.{} — no Javadoc (IBS.MCP.REQUIRE_JAVADOC=true)", - clazz.getSimpleName(), methodName); + if (shouldSkipForJavadoc(method, clazz.getSimpleName(), methodName)) { + l_skipped[0]++; } else { String toolName = clazz.getSimpleName() + "_" + methodName; registerTool(tools, registry, toolName, method); @@ -113,13 +116,12 @@ public static DiscoveryResult discoverTools(String packagesCsv) { + "use the java_call tool to invoke them directly.", clazz.getName(), methodName, countEntry.getKey()); } else { - Method method = countEntry.getValue().get(0); - if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true") && !hasJavadoc(method)) { - log.debug("Skipping {}.{} — no Javadoc (IBS.MCP.REQUIRE_JAVADOC=true)", - clazz.getSimpleName(), methodName); + Method lt_method = countEntry.getValue().get(0); + if (shouldSkipForJavadoc(lt_method, clazz.getSimpleName(), methodName)) { + l_skipped[0]++; } else { - String toolName = clazz.getSimpleName() + "_" + methodName + "_" + countEntry.getKey(); - registerTool(tools, registry, toolName, method); + String lt_toolName = clazz.getSimpleName() + "_" + methodName + "_" + countEntry.getKey(); + registerTool(tools, registry, lt_toolName, lt_method); } } } @@ -127,9 +129,9 @@ public static DiscoveryResult discoverTools(String packagesCsv) { } } - log.info("MCP tool discovery complete: {} tool(s) registered from {} class(es).", - tools.size(), allClasses.size()); - return new DiscoveryResult(tools, registry); + log.info("MCP tool discovery complete: {} tool(s) registered, {} skipped by quality gate ({}), from {} class(es).", + tools.size(), l_skipped[0], ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.fetchValue(), allClasses.size()); + return new DiscoveryResult(tools, registry, l_skipped[0]); } private static void registerTool(List> tools, Map registry, @@ -182,6 +184,42 @@ static boolean hasJavadoc(Method method) { } } + /** + * Returns true if the method has a non-empty Javadoc comment AND a non-empty + * {@code @param} tag for every parameter. Methods with no parameters pass if + * they have a non-empty comment. + */ + static boolean hasAdequateJavadoc(Method method) { + try { + MethodJavadoc l_javadoc = RuntimeJavadoc.getJavadoc(method); + if (l_javadoc == null || COMMENT_FORMATTER.format(l_javadoc.getComment()).isEmpty()) return false; + int l_paramCount = method.getParameterCount(); + if (l_paramCount == 0) return true; + List l_params = l_javadoc.getParams(); + if (l_params.size() < l_paramCount) return false; + return l_params.stream().allMatch(p -> !COMMENT_FORMATTER.format(p.getComment()).isEmpty()); + } catch (Exception e) { + return false; + } + } + + private static boolean shouldSkipForJavadoc(Method in_method, String in_clazz, String in_method_name) { + if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("strict")) { + if (!hasAdequateJavadoc(in_method)) { + log.debug("Skipping {}.{} — Javadoc quality gate (strict) not met: missing comment or @param", + in_clazz, in_method_name); + return true; + } + } else if (ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.is("true")) { + if (!hasJavadoc(in_method)) { + log.debug("Skipping {}.{} — Javadoc quality gate (true) not met: no Javadoc comment", + in_clazz, in_method_name); + return true; + } + } + return false; + } + /** * Builds a JSON Schema object describing the input parameters of a method. * Parameter names are generated as arg0, arg1, ... since Java reflection diff --git a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java index a58d439..96b88e2 100644 --- a/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java +++ b/integroBridgeService/src/test/java/com/adobe/campaign/tests/bridge/service/MCPBridgeServerTest.java @@ -8,14 +8,22 @@ */ package com.adobe.campaign.tests.bridge.service; +import com.adobe.campaign.tests.bridge.testdata.one.SimpleStaticMethods; +import com.github.therapi.runtimejavadoc.RuntimeJavadoc; import io.restassured.response.Response; import org.hamcrest.Matchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.testng.annotations.AfterGroups; import org.testng.annotations.BeforeGroups; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import io.javalin.Javalin; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + import static io.restassured.RestAssured.given; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -201,7 +209,7 @@ public void testDiagnosticsTool_mcpConfigReflectsCurrentState() { assertThat(text, containsString("packagesConfigured")); assertThat(text, containsString(TESTDATA_ONE_PACKAGE)); assertThat(text, containsString("\"prechainActive\":false")); - assertThat(text, containsString("\"javadocRequired\":true")); + assertThat(text, containsString("\"javadocQualityGate\"")); } @Test(groups = "MCP") @@ -1092,6 +1100,145 @@ public void testToolsList_constructorsNotExposedAsIndividualTools() { .body("result.tools.name", not(hasItem("Instantiable_Instantiable"))); } + // ---- hasAdequateJavadoc unit tests ---- + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_methodWithCommentAndAllParams_returnsTrue() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("methodAcceptingStringArgument", String.class); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(true)); + } + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_noArgMethodWithComment_returnsTrue() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("methodReturningString"); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(true)); + } + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_methodWithNoJavadoc_returnsFalse() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("overLoadedMethod1Arg", String.class); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(false)); + } + + @Test(groups = "MCP") + public void testHasAdequateJavadoc_runtimeJavadocThrows_returnsFalse() throws Exception { + Method l_method = SimpleStaticMethods.class.getMethod("methodAcceptingStringArgument", String.class); + try (MockedStatic l_mock = Mockito.mockStatic(RuntimeJavadoc.class)) { + l_mock.when(() -> RuntimeJavadoc.getJavadoc(l_method)) + .thenThrow(new RuntimeException("simulated Javadoc read failure")); + assertThat(MCPToolDiscovery.hasAdequateJavadoc(l_method), is(false)); + } + } + + // ---- Javadoc quality gate integration tests ---- + + @Test(groups = "MCP") + public void testDiscoverTools_strictMode_skippedCountIsPositive() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("strict"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + assertThat("skippedCount must be positive — test data contains undocumented methods", + l_result.skippedCount, greaterThan(0)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testDiscoverTools_strictMode_documentedMethodsIncluded() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("strict"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + List> l_tools = l_result.tools; + assertThat("Documented method with @param must appear under strict", + l_tools.stream().anyMatch(t -> "SimpleStaticMethods_methodAcceptingStringArgument".equals(t.get("name"))), + is(true)); + assertThat("Undocumented method must be absent under strict", + l_tools.stream().noneMatch(t -> ("SimpleStaticMethods_overLoadedMethod1Arg").equals(t.get("name"))), + is(true)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testDiscoverTools_trueMode_excludesUndocumentedIncludesDocumented() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("true"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + assertThat("Undocumented method must be absent under true mode", + l_result.tools.stream().noneMatch(t -> + "SimpleStaticMethods_methodThrowingLinkageError".equals(t.get("name"))), + is(true)); + assertThat("Documented method must appear under true mode", + l_result.tools.stream().anyMatch(t -> + "SimpleStaticMethods_methodAcceptingStringArgument".equals(t.get("name"))), + is(true)); + assertThat("Skip count must reflect filtered methods", + l_result.skippedCount, greaterThan(0)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testDiscoverTools_falseMode_undocumentedMethodsIncluded() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("false"); + try { + MCPToolDiscovery.DiscoveryResult l_result = + MCPToolDiscovery.discoverTools(TESTDATA_ONE_PACKAGE); + // methodThrowingLinkageError has no Javadoc — excluded under strict/true, included under false + assertThat("Undocumented method must appear when gate is disabled", + l_result.tools.stream().anyMatch(t -> + "SimpleStaticMethods_methodThrowingLinkageError".equals(t.get("name"))), + is(true)); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testMCPRequestHandler_falseMode_startupLogBranchCovered() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("false"); + try { + // Constructing MCPRequestHandler exercises the 'false' branch of the startup log ternary. + MCPRequestHandler l_handler = new MCPRequestHandler(); + assertThat(l_handler, notNullValue()); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testMCPRequestHandler_trueModeStartupLogBranchCovered() { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.activate("true"); + try { + // Constructing MCPRequestHandler exercises the 'true' branch of the startup log ternary. + MCPRequestHandler l_handler = new MCPRequestHandler(); + assertThat(l_handler, notNullValue()); + } finally { + ConfigValueHandlerIBS.MCP_REQUIRE_JAVADOC.reset(); + } + } + + @Test(groups = "MCP") + public void testToolsList_strictDefault_exposesFullyDocumentedMethods() { + // With the default (strict), all documented SimpleStaticMethods with @param should be present. + given() + .contentType(CONTENT_TYPE_JSON) + .body("{\"jsonrpc\":\"2.0\",\"id\":200,\"method\":\"tools/list\",\"params\":{}}") + .when() + .post(MCP_ENDPOINT) + .then() + .statusCode(200) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingStringArgument")) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingTwoArguments")) + .body("result.tools.name", hasItem("SimpleStaticMethods_methodAcceptingIntArgument")); + } + @Test(groups = "MCP") public void testJavaCall_constructorChain_instantiableThenStaticMethod() { // Constructors are not available as individual tools — use java_call to instantiate