PR #115 · AIML-757 · PR 1 of 3

Migrate MCP Server to Gradle Modules

First of three PRs for AIML-757 — establishes the Gradle multi-module skeleton and Java 21 baseline before publishing (S3B) and standards alignment (S3C) follow

Branch: AIML-757-gradle-migration Base: main Files: 251 changed Lines: +2,580 / −1,160
1

Why This Migration Exists

Contrast is building a hosted MCP server that needs to share tool logic with the existing public local server — and Maven single-module can't produce a consumable library artifact.

The AIML-110 initiative requires extracting transport-neutral tool infrastructure into a shared core that both servers consume. Maven's single-module layout made this impossible. This PR creates the Gradle multi-module structure that unblocks all shared tool work.

Java 21 is a co-requisite, not a nice-to-have. The hosted server runs on Java 21 in the aiml-services monorepo. The public repo must compile at the same target so the shared core artifact is binary-compatible. This PR upgrades from Java 17 to 21 across CI, Docker, and the build toolchain in one pass.

This is a tracer bullet, not the final state The core module in this PR is intentionally minimal — this is the first of three AIML-757 PRs.

What's in core today:
  • Validation, hints, and filter helpers — that's it
What's not in core yet (and why):
  • 12+ production tools, BaseTool pipeline, ContrastApiClient, shared DTOs
  • Blocked by coupling to local SDK factories, SDK helpers, and Guava caches
This first PR proves the Gradle multi-module build works end-to-end (CI, release, Docker, tests). The next two PRs — S3B (publish & consume) and S3C (standards alignment) — complete the migration before later slices begin filling the module with production code.
AIML-757 is three PRs, not one This is PR 1 of 3 for AIML-757 (Slice 3 of the AIML-110 delivery plan). Each PR lands independently:
  • S3A (this PR) — Gradle multi-module skeleton, Java 21, CI, boundary test. Proves the build works.
  • S3B (next) — Add maven-publish, prove hosted server consumes core via composite-build substitution, validate classpath boundary.
  • S3C (final) — Align contributor docs (README, AGENTS.md), Dependabot, Makefile. Document composite-build local dev for cross-repo workflow.
Without the multi-module split, the hosted server team would need to fork or duplicate ~3,000 lines of tool infrastructure. Everything after AIML-757 — ContrastApiClient extraction (S4A), tool migration (S5+), BearerToken transport — depends on all three PRs being in place.

Module evolution: Maven → This PR → Final State

BEFORE (Maven) mcp-contrast (single module) Tools + SDK + Config + Hints + Validation Spring Boot + Spring AI + Contrast SDK pom.xml • Java 17 • Maven wrapper Cannot extract shared library This PR THIS PR (Intermediate) contrast-mcp-core Validation + Hints + Filters only spring-core + gson + slf4j contrast-mcp-stdio-app All 12 tools still here (SDK-coupled) BaseTool pipeline + SDK extensions Config + Boot + SDK + local wiring Baseline proven • Build works end-to-end Subsequent slices FINAL STATE (Target) contrast-mcp-core (published) 12 remote-eligible shared tools BaseTool + PaginatedTool + SingleTool pipeline ContrastApiClient interface + DTOs + results Validation + Hints + Filter helpers SDK extension data types (shared DTOs) Published to Artifactory via maven-publish contrast-mcp-stdio-app Local config + SdkApiClient + Boot + get_scan_results Local-only concerns • Boot JAR aiml-hosted-mcp-server (consumer) Tools stay in app because they still depend on local SDK factories After ContrastApiClient decouples tools from local SDK, they move to core

Package redistribution: what moved where (214 files)

BEFORE src/main/java/...contrast/ hints/ 4 files tool/validation/ 9 files tool/base/ 12 files 3 to core (FilterHelper, LoggingKeys, ToolParams) config/ local wiring tool/vulnerability/ SDK-coupled tool/application/ SDK-coupled tool/attack/ SDK-coupled tool/{library,coverage,sast}/ SDK-coupled sdkextension/ SDK data types result/ response types McpContrastApplication.java Boot entry + 90 test files, resources, properties AFTER contrast-mcp-core 16 src + 12 test files hints/ — HintGenerator, HintProvider, HintUtils, RuleHints tool/validation/ — ToolValidationContext, *Spec (7) tool/base/ — FilterHelper, LoggingKeys, ToolParams Transport-neutral • No SDK dependency contrast-mcp-stdio-app 105 src + 78 test files tool/base/ — BaseTool, PaginatedTool, SingleTool, ExecutionResult, WarningCollector, params (9) tool/vulnerability/ — SearchVulnerabilities, GetVulnerability, GetVulnerabilityRecommendation, ... tool/{application,attack,library,coverage,sast}/ — all 12 production tools + params sdkextension/ — SDK data models (ADR, Application, RouteCoverage, SCA, SessionMetadata) result/ — response types (AttackSummary, LibrarySummary, VulnerabilityDetail, ...) config/ — ContrastProperties, ContrastSDKFactory, SDKExtensionFactory, SdkApiClient McpContrastApplication.java — Spring Boot entry point SDK-coupled • Local credential wiring • Spring Boot Future: tool/base/ pipeline + 12 production tools migrate to core after ContrastApiClient decouples them (S4A+) Stays permanently: McpContrastApplication, config/, SdkApiClient, get_scan_results Moved to core (16 files) Moved to app (105 files) Split across both (tool/base/: 3 to core, 9 to app)
This PR — Tracer Bullet (S3A)
Gradle multi-module — two subprojects with centralized versions
Minimal core — validation, hints, filter helpers only (intermediate)
Java 21 — toolchain, CI, Docker all upgraded
Boundary enforcementCoreBoundaryTest blocks local-only deps
Zero behavior change — all existing tests pass, all renames
Remaining AIML-757 PRs (S3B & S3C)
S3B: maven-publish → hosted server consumes core via composite build
S3B: Classpath boundary proof — no stdio classes in hosted service
S3C: Align README, AGENTS.md, Dependabot with Gradle split
S3C: Document composite-build local dev for cross-repo workflow
After AIML-757: ContrastApiClient (S4A), tool migration (S5+)
2

Module Structure

Two modules, one clear boundary: core holds transport-neutral support types, the app holds everything that touches the Contrast SDK or Spring Boot. The settings.gradle defines the project topology. FAIL_ON_PROJECT_REPOS ensures no subproject can declare its own repository — all dependency resolution goes through the centralized mavenCentral() declaration.

settings.gradle NEW
+1pluginManagement {
+2    repositories {
+3        gradlePluginPortal()
+4        mavenCentral()
+5    }
+6}
+7
+8dependencyResolutionManagement {
+9    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+10    repositories {
+11        mavenCentral()
+12    }
+13}
+14
+15rootProject.name = 'mcp-contrast'
+16
+17include 'contrast-mcp-core'
+18include 'contrast-mcp-stdio-app'
    
FAIL_ON_PROJECT_REPOS prevents subprojects from declaring their own repositories, ensuring all dependency resolution is centralized. This prevents a future contributor from accidentally pulling from an unapproved source inside a subproject build.gradle.

All dependency versions live in a single gradle.properties file. No version string appears inline in any build.gradle. This satisfies the project's dependency policy (no dynamic versions) and makes version bumps atomic single-line changes — exactly what Dependabot needs to submit clean PRs.

gradle.properties NEW
+1version=1.0.1-SNAPSHOT
+2
+3springBootVersion=3.5.7
+4springAiVersion=1.1.4
+5spotlessGradlePluginVersion=7.0.4
+6googleJavaFormatVersion=1.26.0
+7guavaVersion=33.5.0-jre
+8contrastSdkVersion=3.7.0
+9commonsValidatorVersion=1.10.1
+10mockitoInlineVersion=5.2.0
+11assertjVersion=3.27.7
+12checkstyleVersion=13.4.1
+13lombokVersion=1.18.38
    
The release workflow reads and writes the version line via printVersion/setVersion tasks, replacing Maven's release:prepare plugin. Dependabot updates land as single-line changes to this file.
3

The Root Build

The root build.gradle is the single source of truth for shared configuration — Java toolchain, Spotless, Checkstyle, Lombok, and test dependencies are configured once and inherited by both modules. This eliminates the duplication that would otherwise creep in as modules are added. The root also provides two custom tasks for the release workflow: printVersion and setVersion.

build.gradle NEW
+1import org.gradle.api.plugins.quality.Checkstyle
+2
+3plugins {
+4    id 'base'
+5    id 'com.diffplug.spotless' version "${spotlessGradlePluginVersion}" apply false
+6    id 'org.springframework.boot' version "${springBootVersion}" apply false
+7}
    
Plugins are declared with apply false at the root so they're resolved once (pinning the version) but only applied in the subprojects that need them. Spring Boot is applied only in contrast-mcp-stdio-app.
build.gradle — subprojects block NEW
+45subprojects {
+46    apply plugin: 'java-library'
+47    apply plugin: 'checkstyle'
+48    apply plugin: 'com.diffplug.spotless'
+49
+50    java {
+51        toolchain {
+52            languageVersion = JavaLanguageVersion.of(21)
+53        }
+54    }
+55
+56    tasks.withType(JavaCompile).configureEach {
+57        options.encoding = 'UTF-8'
+58        options.release = 21
+59    }
    
Both toolchain and options.release are set to 21. The toolchain controls which JDK Gradle uses to compile. The release flag is a javac cross-compilation guard — it ensures the compiled bytecode only uses APIs available in Java 21, even if a newer JDK is on the path.
build.gradle — shared dependencies NEW
+87    dependencies {
+88        implementation enforcedPlatform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")
+89
+90        compileOnly "org.projectlombok:lombok:${lombokVersion}"
+91        annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
+92
+93        testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
+94        testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}"
+95        testImplementation 'org.junit.jupiter:junit-jupiter'
+96        testImplementation "org.assertj:assertj-core:${assertjVersion}"
+97        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+98    }
    
enforcedPlatform() is the key to BOM precedence — and it's resolving real conflicts today. Unlike a regular platform(), an enforced platform overrides any transitive version that disagrees. This build has concrete examples:

Jackson  — Spring AI wants 2.19.4, Boot pins 2.19.2
Reactor — Spring AI wants 3.7.16, Boot pins 3.7.12


Without enforcedPlatform(), the higher Spring AI versions would silently win at resolve time, producing a classpath that Boot's integration testing never validated. The enforced BOM ensures we run on the exact dependency set that Spring Boot was tested against.
Note The positiveIntProperty helper (lines 14–25) validates integer Gradle properties like testMaxParallelForks and integrationTestMaxParallelForks. It exists because Gradle properties are untyped strings — without validation, a typo like -PtestMaxParallelForks=abc would produce a confusing NumberFormatException deep inside a task configuration.
4

The Core Module

The core module's build file is intentionally minimal right now — this is an intermediate state, not the final boundary.

Today it has three dependencies and only validation/hints/filter helpers:

The production tools, BaseTool pipeline, ContrastApiClient, and shared DTOs aren't here yet — they still depend directly on local SDK factories and SDK helpers that must be decoupled first. Once ContrastApiClient is introduced (S4A, after AIML-757), tools migrate here incrementally.

contrast-mcp-core/build.gradle NEW
+1dependencies {
+2    api 'org.springframework:spring-core'
+3    api 'com.google.code.gson:gson'
+4
+5    implementation 'org.slf4j:slf4j-api'
+6}
    
api vs implementation matters here. spring-core and gson use api because the core module's public types expose these in their signatures (e.g., StringUtils in validation, Gson types in hints). slf4j-api uses implementation because it's an internal detail — consumers shouldn't see it on their compile classpath.

No version numbers appear here because the root build.gradle applies Spring Boot's enforcedPlatform to all subprojects. The Boot BOM manages spring-core, gson, and slf4j-api versions. This is how the core module gets version alignment for free without importing Boot itself.

This module will grow substantially The eventual contents of contrast-mcp-core go far beyond what's here today.

What will move into core (after AIML-757):
  • All 12 remote-eligible shared tools
  • BaseTool / PaginatedTool / SingleTool / CursorPaginatedTool pipeline
  • ContrastApiClient interface
  • Shared DTOs and result types
Why they can't move yet: they import ContrastSDKFactory, SDKHelper, Guava caches, and other local implementation details. S4A introduces the ContrastApiClient abstraction that breaks this coupling.

What stays permanently in contrast-mcp-stdio-app:
  • McpContrastApplication and local credential/config wiring
  • SdkApiClient and SDK helper/cache implementations
  • The deprecated get_scan_results
5

The App Module

The app module is where the interesting dependency management decisions live — particularly how Spring Boot's BOM and Spring AI's BOM coexist without stepping on each other. This module also defines a custom integrationTest task that solves the credential forwarding problem that plagued the Maven build.

contrast-mcp-stdio-app/build.gradle — dependencies NEW
+7dependencies {
+8    implementation project(':contrast-mcp-core')
+9    // Keep Spring Boot's enforced dependency management authoritative; Spring AI supplies only its own coordinates.
+10    implementation platform("org.springframework.ai:spring-ai-bom:${springAiVersion}")
+11
+12    implementation 'com.google.code.gson:gson'
+13    implementation "com.google.guava:guava:${guavaVersion}"
+14    implementation "com.contrastsecurity:contrast-sdk-java:${contrastSdkVersion}"
+15    implementation "commons-validator:commons-validator:${commonsValidatorVersion}"
+16    implementation 'org.springframework.ai:spring-ai-starter-mcp-server'
+17
+18    testImplementation 'org.springframework.boot:spring-boot-starter-test'
+19    testImplementation "org.mockito:mockito-inline:${mockitoInlineVersion}"
+20}
    
Line 10 is the BOM precedence decision. Spring AI's BOM is imported as a regular platform() — not enforcedPlatform(). The root already applies Spring Boot's BOM as enforced. The practical effect: when both BOMs manage the same dependency (Jackson, Reactor, etc.), Boot's version wins. Spring AI's BOM only contributes versions for coordinates it uniquely owns, like spring-ai-starter-mcp-server. If someone changed this to enforcedPlatform(), Spring AI would override Boot's pinned versions — producing an untested dependency combination.

The integration test task reproduces Maven Failsafe's dual credential forwarding in Gradle's idiom. Maven's <systemPropertyVariables> and <environmentVariables> blocks both forwarded Contrast credentials to the forked test JVM. Gradle's Test task doesn't inherit the parent process's environment by default, so the same dual forwarding must be wired explicitly via systemProperty and environment calls.

contrast-mcp-stdio-app/build.gradle — integrationTest task NEW
+48test {
+49    exclude '**/*IT.class'
+50    exclude '**/*IntegrationTest.class'
+51}
+52
+53tasks.register('integrationTest', Test) {
+54    description = 'Runs integration tests.'
+55    group = 'verification'
+56    testClassesDirs = sourceSets.test.output.classesDirs
+57    classpath = sourceSets.test.runtimeClasspath
+58    maxParallelForks = rootProject.ext.positiveIntProperty(
+59        'integrationTestMaxParallelForks',
+60        defaultIntegrationTestMaxParallelForks)
+61    shouldRunAfter test
+62    include '**/*IT.class'
+63    include '**/*IntegrationTest.class'
+64    contrastIntegrationTestCredentialEnvVars.each { credentialEnvVar ->
+65        def credentialValue = System.getenv(credentialEnvVar)
+66        if (credentialValue) {
+67            systemProperty credentialEnvVar, credentialValue
+68            environment credentialEnvVar, credentialValue
+69        }
+70    }
+71}
    
Lines 67–68 replicate what Maven Failsafe did via XML config. Gradle's Test task forks a clean JVM that doesn't inherit environment variables. systemProperty makes credentials available via System.getProperty(); environment makes them available via System.getenv(). Both paths are needed because Spring Boot's @Value resolution and @EnabledIfEnvironmentVariable test guards use different lookup mechanisms.
Design note The default parallelism for integration tests is min(5, availableProcessors / 2). This balances speed against the Contrast API rate limits that integration tests hit. Unit tests default to 1 fork (safe for tests that share in-memory state).
6

The Boundary Guard

CoreBoundaryTest guards the boundary by blocking local-only dependencies — but deliberately does not block shared tool abstractions that belong in core's future.

What the test forbids (permanently local-only concerns):

What the test does not forbid:

contrast-mcp-core/src/test/java/.../CoreBoundaryTest.java NEW
+29class CoreBoundaryTest {
+30
+31  private static final Path CORE_MAIN = Path.of("src/main/java");
+32  private static final List<Path> REQUIRED_SUPPORT_TYPES =
+33      List.of(
+34          sourcePath("com/contrast/.../tool/base/FilterHelper.java"),
+35          sourcePath("com/contrast/.../tool/base/LoggingKeys.java"),
+36          sourcePath("com/contrast/.../tool/base/ToolParams.java"),
+37          sourcePath("com/contrast/.../tool/validation/ToolValidationContext.java"),
+38          sourcePath("com/contrast/.../hints/HintGenerator.java"));
    
This list represents the current minimum, not the final contract. It will grow significantly as tools are decoupled from local SDK factories and migrated into core. Today it's 5 support types; the final state includes 12+ tools, the BaseTool pipeline, ContrastApiClient, and shared DTOs. If any of these files get accidentally removed from core, the test fails immediately.
CoreBoundaryTest.java — prohibited patterns NEW
+50  private static final List<String> LOCAL_ONLY_TEXT =
+51      List.of(
+52          "com.contrast.labs.ai.mcp.contrast.config",
+53          "com.contrastsecurity",
+54          "org.springframework.ai",
+55          "spring-ai",
+56          "@Configuration",
+57          "ContrastSDKFactory",
+58          "SDKExtensionFactory",
+59          "SdkApiClient",
+60          "SDKHelper",
+61          "McpContrastApplication",
+62          "com.google.common.cache",
+63          "get_scan_results",
+64          "sarif",
+65          "SARIF");
    
Most of these are permanently forbidden patterns — local credential wiring, config classes, Guava caches, SARIF code, and direct Contrast SDK imports must never enter core. One exception to watch: org.springframework.ai is forbidden today because core has no Spring AI dependency, but when @Tool-annotated classes migrate to core in S5+, this pattern will need to be narrowed (e.g., allow @Tool but still block MCP server internals). That's a deliberate future refinement, not an oversight. BaseTool, PaginatedTool, and shared tool names are deliberately absent from this list — they belong in core's future.

The test uses two complementary strategies: structural and textual. The first test method checks file presence (required types exist) and absence (local-only types aren't here). The second walks every .java file and does substring matching against the prohibited patterns. Together they form a fast, zero-dependency boundary check that runs on every ./gradlew test.

Why the test is shaped this way The boundary test blocks local-only concerns, not shared tool categories.

An earlier iteration explicitly forbade BaseTool, SingleTool, PaginatedTool, and tool implementation names from appearing in core. That was wrong — it would have encoded the temporary S3A minimal state as a permanent rule, blocking the very migrations that are the whole point of this split.

The corrected test asks one question: does this file reference local SDK factories, credential wiring, or SARIF code? If a class can be decoupled from those concerns, it's welcome in core.
7

CI & Release Pipeline

The CI workflow consolidates what were separate Maven steps into a single Gradle invocation, and adds wrapper validation as a supply chain security measure. The release workflow replaces Maven's release:prepare plugin with explicit setVersion/printVersion tasks that read and write gradle.properties.

.github/workflows/build.yml MODIFIED
- name: Java CI with Maven
+1name: Java CI with Gradle
  ...
+23    - name: Validate Gradle wrapper
+24      uses: gradle/actions/wrapper-validation@0b6dd653...  # v4
  ...
-         java-version: '17'
+30        java-version: '21'
  ...
-         cache: maven
+32        cache: gradle
-     - name: Build with Maven
-       run: ./mvnw clean package -DskipTests
-     - name: Run tests
-       run: ./mvnw test
+33    - name: Run checks, tests, and build
+34      run: ./gradlew spotlessCheck checkstyleMain checkstyleTest test :contrast-mcp-stdio-app:bootJar --no-daemon
    
The wrapper validation step is a supply chain security guard. It verifies the gradle-wrapper.jar checksum against known Gradle distributions, preventing an attacker from slipping a malicious wrapper JAR into the repo. The single consolidated ./gradlew invocation replaces two separate Maven commands, running formatting checks, Checkstyle, tests, and the boot JAR build in one pass.
gradle-release.yml — version management (excerpt) RENAMED
-           CURRENT=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)
+51          CURRENT=$(./gradlew -q printVersion --no-daemon)
            ...
-           ./mvnw release:prepare \
-             -DreleaseVersion=${{ steps.versions.outputs.release }} \
-             -DdevelopmentVersion=${{ steps.versions.outputs.next }} \
-             -Dtag=v${{ steps.versions.outputs.release }} \
-             -DpushChanges=false \
-             -B
+69          ./gradlew setVersion -PnewVersion=${{ steps.versions.outputs.release }} --no-daemon
+70          ./gradlew clean spotlessCheck check :contrast-mcp-stdio-app:integrationTest :contrast-mcp-stdio-app:bootJar --no-daemon
+71          git add gradle.properties
+72          git commit -m "[gradle-release] prepare release v${{ steps.versions.outputs.release }}"
+73          git tag v${{ steps.versions.outputs.release }}
+74          ./gradlew setVersion -PnewVersion=${{ steps.versions.outputs.next }} --no-daemon
+75          git add gradle.properties
+76          git commit -m "[gradle-release] prepare next development iteration"
    
The release prepare step now runs the full verification suite at the release version before tagging. Maven's release:prepare was a black box. The Gradle equivalent is explicit: set version, verify everything (including integration tests), commit, tag, advance to next snapshot, commit. Each step is visible and auditable.
8

Developer Experience

The Makefile, Docker build, and test output formatting all got updated to speak Gradle instead of Maven — but the developer-facing interface (make check-test, make verify, etc.) is unchanged. A developer who never reads this PR will notice nothing different about their workflow.

Makefile — Maven-to-Gradle command substitution Standard Pattern Makefile (87 lines) Mechanical: mvn → ./gradlew
Every $(MVN) reference becomes $(GRADLE) (defaulting to ./gradlew). Target names and the make help output are unchanged. The verify target now runs test :contrast-mcp-stdio-app:integrationTest instead of mvn verify.
run_silent.sh — token-optimization helper rewritten for Gradle Standard Pattern hack/run_silent.sh Dev tooling, not production code
This script reduces token consumption when AI agents run tests by suppressing verbose Gradle output and emitting only a compact summary (test count, failures, elapsed time). Maven printed test counts inline (Tests run: 45, Failures: 0); Gradle doesn't. The rewrite parses JUnit XML reports from build/test-results via a Perl one-liner, using a marker-file timestamp to exclude stale results from prior runs.
Dockerfile — Java 21 Alpine base images Standard Pattern Dockerfile (24 lines) Pattern: multi-stage build
Builder stage: maven:3.9-eclipse-temurin-17eclipse-temurin:21-jdk-alpine (with digest pin). Runtime stage: eclipse-temurin:25-jre-alpineeclipse-temurin:21-jre-alpine. The runtime "downgrade" from JRE 25 to 21 is intentional — the old builder compiled Java 17 bytecode that happened to run on a JRE 25 runtime; now builder and runtime are aligned at 21. Build command: mvn clean package./gradlew :contrast-mcp-stdio-app:bootJar. JAR path: target/contrast-mcp-stdio-app/build/libs/. A .dockerignore was added to exclude .git, .gradle, and build/ directories from the build context.
Documentation updates — Maven→Gradle references across 9 files Standard Pattern CLAUDE.md, RELEASING.md, INTEGRATION_TESTS.md, MCP_STANDARDS.md, SECURITY.md, WORKFLOW.md, README.md, 6 install guides Mechanical text substitution
All Maven command references updated to Gradle equivalents. RELEASING.md was substantially rewritten to document the new gradle-release.yml workflow. Install guides update the JAR path from target/mcp-contrast-*.jar to contrast-mcp-stdio-app/build/libs/mcp-contrast-*.jar. dependabot.yml changed package-ecosystem from maven to gradle.
Source file renames — 214 files moved to module paths Standard Pattern 214 renamed files (202 identical, 12 with Javadoc edits) Pure renames (R100 in git)
All source files under src/ were moved to either contrast-mcp-core/src/ or contrast-mcp-stdio-app/src/. 202 of 214 renames are R100 (100% identical). The remaining 12 have minor Javadoc edits updating mvn verify references to Gradle commands — no logic changes. The only package that split across both modules is tool/base/: 3 transport-neutral helpers (FilterHelper, LoggingKeys, ToolParams) went to core; the 9 SDK-coupled pipeline classes (BaseTool, PaginatedTool, SingleTool, etc.) stayed in the app module.
Deleted Maven artifacts — pom.xml, mvnw, .mvn/ Standard Pattern pom.xml (274 lines), mvnw, mvnw.cmd, .mvn/wrapper/ Replaced by Gradle equivalents
The Maven POM, wrapper scripts, and wrapper properties are deleted. Their Gradle equivalents are build.gradle, settings.gradle, gradle.properties, gradlew, gradlew.bat, and gradle/wrapper/*. The Gradle wrapper JAR is added (43KB binary).
9

What’s Next

This is PR 1 of 3 for AIML-757. Two more PRs follow to complete the Gradle migration:

Only after all three merge do later slices (S4A, S5+) begin decoupling and migrating tools.

Suggested review order: settings.gradlegradle.properties → root build.gradle → module build files → CoreBoundaryTest. CI workflows are worth a look. Everything else is mechanical.

SliceScopeWhat Happens to Core
S3A Gradle SkeletonPR 1 of 3 Core gets validation, hints, filter helpers — proves the build works
S3B Publish & Consume maven-publish added; hosted server consumes core via composite build; classpath boundary proven
S3C Standards & Docs README, AGENTS.md, Dependabot, Makefile aligned with Gradle split; composite-build dev documented
After AIML-757 completes Once all three S3 PRs have merged, later slices begin to fill the core module with production code:
  • S4A (AIML-758) — introduces ContrastApiClient and the auth strategy hook, decoupling tools from local SDK factories
  • S5+ (AIML-759) — migrates 12 shared tools, the BaseTool pipeline, and DTOs into core. App shrinks to: local config, SdkApiClient, get_scan_results
FilePurposeLinesNovelty
settings.gradle Module topology + centralized repo config +18 Novel
gradle.properties Centralized dependency versions +13 Novel
build.gradle Shared Java 21 toolchain, Spotless, Checkstyle, Lombok, BOM +106 Novel
contrast-mcp-core/build.gradle Core module: spring-core + gson + slf4j +6 Novel
contrast-mcp-stdio-app/build.gradle App module: Boot, SDK, Spring AI, integration tests +71 Novel
CoreBoundaryTest.java Boundary enforcement: required types + prohibited patterns +114 Novel
.github/workflows/build.yml CI: wrapper validation, Java 21, consolidated Gradle step +10/−8 Pattern
.github/workflows/gradle-release.yml Release: setVersion/printVersion, explicit prepare steps +16/−9 Pattern
hack/run_silent.sh Token-optimization helper rewritten for Gradle test reports +120/−80 Pattern
Makefile mvn → ./gradlew command substitution +13/−13 Standard
Dockerfile Java 21 Alpine images, Gradle build command +4/−4 Standard
.dockerignore Exclude .git, .gradle, build/ from Docker context +5 Standard
checkstyle.xml Added suppressions file config property +3 Standard
CLAUDE.md + 8 doc files Maven→Gradle command updates across all docs +44/−44 Standard
~200 .java files Pure renames into module src/ paths (R100) Standard
pom.xml + Maven wrapper Deleted (replaced by Gradle equivalents) −701 Standard