This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git
The following commit(s) were added to refs/heads/main by this push:
new 518bc17 feat(build): RAT license-header enforcement as a buildSrc
convention plugin (stacked on #138) (#150)
518bc17 is described below
commit 518bc17a8a1f4c1c6a3f1881901d8d8892b6a86c
Author: Aditya Parikh <[email protected]>
AuthorDate: Mon Jun 15 12:19:20 2026 -0400
feat(build): RAT license-header enforcement as a buildSrc convention plugin
(stacked on #138) (#150)
* docs: add Apache LICENSE and NOTICE files
Add the top-level Apache License 2.0 text and NOTICE file required by
ASF release policy, and bundle them into the META-INF directory of every
JAR produced by the build (main, bootJar, sources, javadoc).
See https://www.apache.org/legal/release-policy.html#licensing-documentation
* docs(spec): add SBOM generation design
Captures decisions made during brainstorming: CycloneDX over SPDX,
embed-in-bootJar via Spring Boot's native CycloneDX integration, full
build + Docker + Release coverage, no cosign attestation in this PR.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(plan): add SBOM generation implementation plan
Step-by-step bite-sized tasks covering: version catalog, Gradle plugin
wiring, actuator endpoint enablement, focused HTTP integration test,
CI workflow uploads, README + CLAUDE.md docs, final verification.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* chore(deps): add CycloneDX Gradle plugin 1.10.0 to version catalog
Plugin will be applied in the next commit. Adding the catalog entry
first keeps build.gradle.kts changes reviewable in isolation.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* feat(build): generate and embed CycloneDX SBOM
Apply org.cyclonedx.bom Gradle plugin 2.4.1. Spring Boot 3.5's
CycloneDxPluginAction auto-wires bootJar to embed the generated SBOM at
META-INF/sbom/application.cdx.json, so every distribution (JAR, Jib JVM
image, both Paketo native images) ships the embedded SBOM via bootJar
packaging — no per-image wiring.
Plugin version note: 1.10.0 breaks against Gradle 9.4 with
UnsupportedOperationException (ImmutableCollection.removeAll). 2.4.1 is
the latest v1.x-compatible class layout (CycloneDxPlugin /
CycloneDxTask) that Spring Boot's auto-integration recognizes; v3.x
renamed the classes (CyclonedxPlugin) and is incompatible until Spring
Boot adopts the new shape.
projectType is set explicitly to Component.Type.APPLICATION because
v2.4.1 changed the property from Property<String> to
Property<Component.Type>; Spring Boot's `.convention("application")`
would store a raw String and break the task at execution time.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* feat(actuator): enable /actuator/sbom endpoint explicitly
`sbom` was already in management.endpoints.web.exposure.include; this
makes the endpoint enablement explicit so the file conveys intent
without relying on Spring Boot defaults.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(spec): drop integration-test scope, document plugin-version decisions
- Drop the planned SbomEndpointIntegrationTest: /actuator/sbom is stock
Spring Boot functionality; our only project-specific addition is two
property lines. The build itself fails if cyclonedxBom breaks
(Spring Boot's bootJar auto-depends on it).
- Update plugin version note to 2.4.1 and explain why both 1.10.0 (Gradle
9.4 bug) and 3.x (Spring Boot class-name change) are unsuitable.
- CycloneDX schema 1.6 (plugin default) replaces the originally-noted 1.5.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(spec): drop stale 1.10.0 version reference
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(spec): inline the plugin-version constraints explanation
Earlier edit lost the detail by accident. Restored as part of the Tool
choice section so the spec stands on its own.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* ci: upload CycloneDX SBOM as workflow artifact
Mirrors the existing JAR/test-results/coverage upload pattern. Retains
the SBOM for 30 days (vs the standard 7) since supply-chain
investigations often happen well after a build.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* ci(release): strict SBOM generation + upload + release attachment
The existing Generate SBOM step swallowed errors with `|| echo "..."`,
masking failures now that the plugin is wired. Removes the fallback,
uploads the SBOM as a 90-day workflow artifact, and attaches it to the
v<version> GitHub Release when one exists (graceful fallback otherwise
since the source release of record lives at dist.apache.org, not GitHub).
RELEASE_VERSION is already validated by validate-release; routing it
through an env var instead of inline ${{ }} interpolation is
defence-in-depth against actions-injection.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(readme): document SBOM location, retrieval, and scanning
New 'Supply chain & SBOM' section covers all four distribution
channels (embedded in JAR/image, /actuator/sbom endpoint, GitHub
Release asset, CI workflow artifact) and shows trivy/grype usage.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* refactor(build): drop unnecessary cyclonedxBom configuration
Spring Boot 3.5.14's CycloneDxPluginAction already sets outputName,
outputFormat, projectType, and wires bootJar embedding — matching what
Spring Initializr generates for the same dependency set. Verified that
applying the plugin alone produces a valid CycloneDX 1.6 SBOM at
META-INF/sbom/application.cdx.json inside the bootJar with
component type=application.
The earlier projectType override + includeConfigs/skipConfigs were
defensive but unnecessary; let the framework defaults work.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(agents): note SBOM generation in commands + architecture
CLAUDE.md symlinks to AGENTS.md; edit lands on the real file.
Records the cyclonedxBom command and how the SBOM flows through
bootJar → actuator → Docker images, so future agents have the
mental model when working on related code.
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* style: apply spotless
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* feat(build): derive binary-release LICENSE/NOTICE from the SBOM
The base LICENSE/NOTICE are correct for the source release, but the binary
release (the Spring Boot fat bootJar) bundles third-party bytecode and so
per
https://infra.apache.org/licensing-howto.html must additionally enumerate
each
bundled dependency's license and lift bundled ASF dependencies' NOTICE
snippets.
Stacks on the CycloneDX SBOM work and reuses it as the source of dependency
license data:
- generateBinaryLicense: base Apache-2.0 + an appendix listing every
productionRuntimeClasspath dependency with a link to its license, read
from the
bundled SBOM (META-INF/sbom/application.cdx.json). The SBOM resolves a
license
for every component, including Gradle-module-metadata-only ASF artifacts
(solr-solrj/solr-api) that POM-only scanners miss, so no per-dependency
list is
hand-maintained. It also gates the build: a bundled module missing from
the SBOM,
or carrying a license not in config/license-policy.json, fails the build.
- generateBinaryNotice: base NOTICE + the META-INF/NOTICE files lifted
verbatim and
de-duplicated from the bundled jars (the Shade
ApacheNoticeResourceTransformer
approach), so ASF dependency notices stay current automatically.
config/license-policy.json holds the allowedLicenses set plus overrides
(group:name -> SPDX id) correcting the few components CycloneDX mislabels
(mcp-server-security -> Apache-2.0; ANTLR ST4/antlr-runtime ->
BSD-3-Clause).
Source-form jars keep the base LICENSE/NOTICE.
Verified: ./gradlew build green; fat jar META-INF/LICENSE lists 158 deps
(incl. SolrJ) and META-INF/NOTICE aggregates 21 upstream notices.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* refactor(build): extract LICENSE/NOTICE generation to a buildSrc plugin
Move the inline LICENSE/NOTICE logic out of the root build.gradle.kts into a
buildSrc convention plugin (org.apache.solr.mcp.license-notice) backed by
two
typed tasks:
- GenerateBinaryLicense / GenerateBinaryNotice are proper DefaultTask types
with
@InputFile/@InputFiles/@OutputFile, so they're incremental and (being
real .kt
files) free of the kts-script-compiler limitations that forced the
previous
Pair-based workarounds — the logic now reads as plain Kotlin with data
classes.
- The root build.gradle.kts drops ~250 lines and three imports, and just
applies
`id("org.apache.solr.mcp.license-notice")`.
Behaviour is unchanged: the bootJar still bundles a LICENSE with the
SBOM-derived
158-dependency appendix (incl. SolrJ) and a NOTICE aggregating 21 upstream
notices; source-form jars keep the base files; `check` still runs the gate.
The tasks now live in buildSrc, so they can be unit-tested with Gradle
TestKit.
Verified: ./gradlew build green; fat-jar META-INF/LICENSE and NOTICE
identical
to the pre-refactor output.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* test(build): unit-test the LICENSE/NOTICE buildSrc tasks
Add ProjectBuilder-based tests for the two convention-plugin tasks (now
possible
since they live in buildSrc as typed tasks). Covers the correctness-critical
behaviour without needing the full spring-boot + cyclonedx stack:
- generateBinaryLicense: appendix lists bundled deps with SPDX links,
applies a
policy override to correct a mislabelled SBOM license, and preserves the
base
LICENSE text; the gate fails on a disallowed license and on a bundled
coordinate
absent from the SBOM.
- generateBinaryNotice: aggregates bundled META-INF/NOTICE files verbatim,
de-duplicates identical notices, attributes each to its module, and emits
just
the project NOTICE when no dependency notices exist.
buildSrc's test task runs as part of `./gradlew build`, so these are
enforced on
every build.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(build): explain the LICENSE/NOTICE tasks in comments and AGENTS.md
Add step-by-step comments to GenerateBinaryLicense/GenerateBinaryNotice
walking
through what each phase does (load policy, index the SBOM, resolve+gate each
shipped dependency, write the file; and notice matching/de-dup/attribution).
Expand the AGENTS.md "Release LICENSE / NOTICE" section with where the
tasks are
unit-tested and a short runbook for what to do when the license gate fails
(add an override for an SBOM mislabel, or allow a genuinely new license)
instead
of silencing it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* refactor(build): drop license-policy.json; disclose SBOM licenses verbatim
apache/solr has no license allow-list (it uses a per-dependency licenses/
folder,
which JanHoy said not to replicate), and the binary LICENSE is a
disclosure, not a
license policy. Remove config/license-policy.json and the allow-list gate +
override corrections it powered.
generateBinaryLicense now lists each shipped dependency with the license the
CycloneDX SBOM reports, verbatim — so a few imprecise-but-permissive
upstream
labels appear as-is (mcp-server-security: Apache-1.0; ANTLR: BSD-4-Clause /
BSD
licence). The appendix preamble says licenses are as-reported and links
each one.
The remaining gate is completeness only: fail if a bundled dependency is
absent
from the SBOM, so nothing is silently omitted from the LICENSE. Tests
updated to
assert verbatim SBOM labels and SBOM name/URL handling.
Verified: ./gradlew build green; fat-jar LICENSE still lists 158 deps and
NOTICE
aggregates 21 upstream notices.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(build): point the LICENSE appendix to the bundled SBOM
Add a line to the appendix preamble noting the machine-readable bill of
materials (component versions, hashes, licenses) is bundled at
META-INF/sbom/application.cdx.json — the inline appendix stays the
human-readable disclosure, with the SBOM offered for tooling.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs: document where/when the binary LICENSE & NOTICE are available
Add a 'where / when they appear' note to the Release LICENSE / NOTICE
section:
both binary files are regenerated on every build (tasks run ahead of
bootJar and
in check), land at META-INF/LICENSE and META-INF/NOTICE in the fat jar and
thus
in every published Docker image, and are also written to
build/generated/license/
for local viewing; source-form jars carry the repo-root base files.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(build): explain the buildSrc LICENSE/NOTICE plugin for non-Gradle
readers
Reviewers who don't work with Gradle had no easy way into buildSrc. Add:
- buildSrc/README.md: what buildSrc is, a short glossary of the Gradle
concepts
the code uses (Task, @TaskAction, the input/output annotations,
Property/Provider
types, convention plugin, productionRuntimeClasspath), and the end-to-end
flow.
- KDoc on GenerateBinaryLicense / GenerateBinaryNotice: a "for readers new
to
Gradle" orientation on each class plus a note on every annotated property
explaining what the input/output annotation does (up-to-date checking,
ordering).
- A note on the convention plugin header explaining precompiled script
plugins,
and a comment on buildSrc/build.gradle.kts explaining what it builds.
Documentation only; no behaviour change. ./gradlew :buildSrc:test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* docs(build): comment the convention plugin body for non-Gradle readers
Add plain-language inline comments through the plugin body explaining the
parts
that are opaque without Gradle background: what a 'configuration' is and why
productionRuntimeClasspath equals 'what ships', how the lazy provider chains
(flatMap/map over resolvedArtifacts) derive the coordinate list and the
jar-name->coordinate map, what tasks.register/.set wiring does, and how
metaInf
from(...) plus dependsOn bundle the generated files into the bootJar while
the
source-form jars keep the base files. Comments only; code unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
* Dont double load the root files
* feat(build): enforce Apache license headers via RAT convention plugin
Add Apache RAT (Release Audit Tool) header enforcement as an
`org.apache.solr.mcp.rat` buildSrc convention plugin, stacked on the
license-notice plugin from #138. RAT is wired into `check`, so `./gradlew
build` audits that every scanned file carries an ASF header (report at
build/reports/rat/index.html).
The .gitignore-to-RAT-glob translation lives in a pure, unit-tested
`RatExcludes` helper rather than inline in build.gradle.kts. Moving it to
buildSrc fixes two gitignore-semantics gaps from the inline approach:
interior-slash patterns (e.g. src/generated) are now root-anchored instead of
matched at any depth, and the negation/anchoring rules are documented and
tested.
Local developer-tooling dirs (.claude worktrees, .kotlin caches) are
excluded so contributors don't hit spurious audit failures. ASF headers are
added to the three application*.properties and libs.versions.toml so they pass
the audit.
Supersedes the inline approach in #149. Stacked on #138. Fixes #141.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
---------
Signed-off-by: Aditya Parikh <[email protected]>
Signed-off-by: adityamparikh <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Eric Pugh <[email protected]>
---
build.gradle.kts | 3 +
buildSrc/README.md | 17 +++-
buildSrc/build.gradle.kts | 9 +++
.../main/kotlin/org.apache.solr.mcp.rat.gradle.kts | 93 ++++++++++++++++++++++
.../org/apache/solr/mcp/build/RatExcludes.kt | 81 +++++++++++++++++++
.../org/apache/solr/mcp/build/RatExcludesTest.kt | 65 +++++++++++++++
gradle/libs.versions.toml | 16 ++++
src/main/resources/application-http.properties | 16 ++++
src/main/resources/application-stdio.properties | 16 ++++
src/main/resources/application.properties | 16 ++++
10 files changed, 331 insertions(+), 1 deletion(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index 572bc76..c9052fb 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -32,6 +32,9 @@ plugins {
// Listed after spring-boot + cyclonedx so productionRuntimeClasspath and
// cyclonedxBom exist when it wires its tasks. See buildSrc/.
id("org.apache.solr.mcp.license-notice")
+ // Enforces Apache license headers via Apache RAT (buildSrc convention
plugin).
+ // Wires `rat` into `check`, so `./gradlew build` audits headers. See
buildSrc/.
+ id("org.apache.solr.mcp.rat")
}
// GraalVM Native Image (Opt-In)
diff --git a/buildSrc/README.md b/buildSrc/README.md
index 4d9bdff..f05cc5a 100644
--- a/buildSrc/README.md
+++ b/buildSrc/README.md
@@ -15,6 +15,18 @@
limitations under the License.
-->
+# buildSrc — project build logic
+
+This directory holds the project's custom build logic, written in Kotlin.
Today that is
+two ASF-compliance concerns:
+
+- assembling the **binary-release `LICENSE` and `NOTICE`** files bundled
inside the
+ executable JAR (the end-user view of *what* these contain lives on the
+ [Licensing & Notices](https://solr.apache.org/mcp/licensing.html) docs
page); and
+- enforcing **Apache license headers** on source files via Apache RAT.
+
+If you don't work with Gradle day-to-day, this README explains what each piece
is and how
+they fit together.
# buildSrc — generating the binary LICENSE & NOTICE
This directory holds the build logic that assembles the **binary-release
`LICENSE`
@@ -39,7 +51,10 @@ small. (Think of it as a tiny library that only this
project's build uses.)
| `src/main/kotlin/.../GenerateBinaryNotice.kt` | A custom Gradle **task**
that writes the binary `NOTICE` (our `NOTICE` + the `NOTICE` files of bundled
dependencies). |
| `src/main/kotlin/org.apache.solr.mcp.license-notice.gradle.kts` | A
**convention plugin** that creates the two tasks above and wires them into the
build. |
| `src/test/kotlin/.../LicenseNoticeTasksTest.kt` | Unit tests for the two
tasks. |
-| `build.gradle.kts` | Builds `buildSrc` itself (enables Kotlin + the test
dependencies). |
+| `src/main/kotlin/.../RatExcludes.kt` | Pure helper that translates
`.gitignore` entries into Apache RAT (Ant-style) exclude globs. |
+| `src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts` | A **convention
plugin** that applies Apache RAT and configures its excludes
(`.gitignore`-derived + an explicit list). |
+| `src/test/kotlin/.../RatExcludesTest.kt` | Unit tests for the gitignore→glob
translation. |
+| `build.gradle.kts` | Builds `buildSrc` itself (enables Kotlin + the RAT
plugin + the test dependencies). |
## Gradle concepts, for Java developers
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 40fb7c8..d1296d7 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -24,9 +24,18 @@ plugins {
repositories {
mavenCentral()
+ // Hosts the Apache RAT plugin marker below — it is not published to Maven
Central.
+ gradlePluginPortal()
}
dependencies {
+ // Makes the Apache RAT plugin (id `org.nosphere.apache.rat`) available to
the
+ // `org.apache.solr.mcp.rat` convention plugin's `plugins {}` block. The
version lives
+ // here (mirroring how the junit dep below is pinned) since buildSrc does
not read the
+ // root project's version catalog. Latest release as of writing; bump in
lockstep with
+ // the plugin's upstream releases.
+
implementation("org.nosphere.apache.rat:org.nosphere.apache.rat.gradle.plugin:0.8.1")
+
// Only used by the task unit tests under src/test (the main code needs no
extra deps;
// the Gradle API is provided by the kotlin-dsl plugin).
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
diff --git a/buildSrc/src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts
b/buildSrc/src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts
new file mode 100644
index 0000000..e9ea949
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Convention plugin: Apache RAT (Release Audit Tool) license-header
enforcement.
+//
+// For readers new to Gradle: this `.gradle.kts` file under buildSrc is a
"precompiled
+// script plugin". Gradle compiles it into a plugin whose id is the file name
+// (`org.apache.solr.mcp.rat`); the root build applies it with one line,
+// `id("org.apache.solr.mcp.rat")`. The `plugins {}` block below applies the
third-party
+// RAT plugin (made available to buildSrc by the `org.nosphere.apache.rat...`
dependency
+// in buildSrc/build.gradle.kts), and the body configures its `rat` task.
+//
+// RAT verifies that every scanned file carries an Apache license header. The
plugin wires
+// its `rat` task into `check`, so a plain `./gradlew build` runs it; the
report lands at
+// build/reports/rat/index.html.
+//
+// Exclusions come from two sources so patterns are not duplicated:
+// 1. .gitignore — reused as the single source of truth for everything git
already
+// ignores (build output, .gradle, IDE dirs, *.iml, out/, bin/, .vscode,
etc.).
+// RatExcludes (in this same buildSrc) translates each entry into a RAT
(Ant) glob;
+// see that class for the gitignore→glob mapping rules.
+// 2. The explicit list below — only *tracked* files that RAT would scan but
that
+// legitimately carry no Apache header (binaries, data without a comment
syntax,
+// docs, tool/infra config, and LICENSE/NOTICE themselves).
+
+import org.apache.solr.mcp.build.RatExcludes
+import org.nosphere.apache.rat.RatTask
+
+plugins {
+ id("org.nosphere.apache.rat")
+}
+
+tasks.withType<RatTask>().configureEach {
+ val gitignore = rootProject.file(".gitignore")
+ if (gitignore.exists()) {
+ excludes.addAll(RatExcludes.fromGitignore(gitignore.readLines()))
+ }
+
+ excludes.addAll(
+ listOf(
+ // Gradle wrapper (ships under its own license) + on-disk OS cruft
+ "gradlew",
+ "gradlew.bat",
+ "gradle/wrapper/**",
+ "**/.DS_Store",
+ // Tracked dotfiles that take no header
+ ".run/**",
+ ".gitignore",
+ ".gitattributes",
+ ".editorconfig",
+ ".tool-versions",
+ ".env.example",
+ // License/notice files themselves (no header by definition)
+ "LICENSE",
+ "NOTICE",
+ // ASF infra metadata and tool config (no header by convention)
+ ".asf.yaml",
+ "config/**",
+ // Local developer tooling not tracked by git — Claude Code
worktrees/settings
+ // and the Kotlin compiler cache (analogous to the gitignored
.idea/.gradle).
+ // Present only on some local checkouts, never in CI.
+ ".claude/**",
+ "**/.kotlin/**",
+ // Tabular data — no comment syntax to hold a header
+ "**/*.csv",
+ // Documentation (markdown carries no license header)
+ "**/*.md",
+ "docs/**",
+ "dev-docs/**",
+ "security-docs/**",
+ // Binary assets
+ "images/**",
+ "**/*.png",
+ // Data / generated content (JSON cannot hold comments)
+ "mydata/**",
+ "**/*.json",
+ ),
+ )
+}
diff --git a/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/RatExcludes.kt
b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/RatExcludes.kt
new file mode 100644
index 0000000..f23d948
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/RatExcludes.kt
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.build
+
+/**
+ * Translates `.gitignore` lines into Apache RAT (Ant-style) exclude globs.
+ *
+ * RAT scans every file under the project directory; we reuse `.gitignore` as
the single
+ * source of truth for build output, IDE folders, and other untracked cruft so
those
+ * patterns are not duplicated in the build script. The RAT plugin's own
`excludeFile`
+ * does **not** interpret `.gitignore` path semantics, so we translate each
entry here.
+ *
+ * This is pure, Gradle-free logic precisely so it can be unit-tested
+ * ([RatExcludesTest]); the gitignore-to-Ant-glob mapping has enough edge cases
+ * (anchoring, directory markers, negation) to be worth pinning down with
tests.
+ *
+ * ### Mapping rules (a practical subset of gitignore semantics)
+ *
+ * Throughout, "globstar" means the two-asterisk Ant wildcard that matches any
number of
+ * path segments (written here without the trailing slash to keep this comment
valid).
+ *
+ * - Blank lines and `#` comments are skipped.
+ * - **Negation (`!foo`) entries are skipped.** Git uses them to *re-include*
a path, but
+ * RAT excludes have no re-inclusion mechanism. Skipping (rather than
excluding) is the
+ * safe choice: at worst RAT still scans a file git would ignore. The
opposite — a
+ * bare `build/` excluding a re-included `!build/keep.txt` — over-excludes
that one file
+ * from header checking, which we accept as a rare, low-risk gap.
+ * - A **trailing slash** (directory marker) is stripped; the entry is still
emitted in
+ * both bare and directory-contents (globstar-suffixed) forms.
+ * - **Anchoring** follows git: an entry with a leading slash, or with an
interior slash
+ * (a separator that is not just the trailing one), is anchored to the repo
root and is
+ * emitted as-is (leading slash removed). An entry with no separator — or
only a
+ * trailing slash — matches at any depth and is prefixed with a leading
globstar segment.
+ * An entry that already begins with a globstar segment is left untouched.
(The original
+ * inline implementation prefixed *every* non-leading-slash entry with a
globstar, which
+ * wrongly turned a root-anchored `foo/bar` into an any-depth match; this
distinguishes
+ * the two.)
+ *
+ * Each surviving entry yields two globs — the path itself and its
directory-contents form
+ * — so an ignored directory and everything under it are both pruned.
+ */
+object RatExcludes {
+
+ fun fromGitignore(lines: List<String>): List<String> =
+ lines.asSequence()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .filterNot { it.startsWith("#") }
+ .filterNot { it.startsWith("!") }
+ .map { it.trimEnd('/') }
+ .filter { it.isNotEmpty() }
+ .map(::toGlob)
+ .flatMap { sequenceOf(it, "$it/**") }
+ .distinct()
+ .toList()
+
+ /** Maps one normalized (trimmed, no trailing slash) gitignore entry to
one Ant glob. */
+ private fun toGlob(entry: String): String {
+ val anchored = entry.startsWith("/")
+ val body = entry.removePrefix("/")
+ return when {
+ body.startsWith("**/") -> body // already any-depth
+ anchored || body.contains("/") -> body // root-anchored (leading
or interior slash)
+ else -> "**/$body" // no separator → match at any depth
+ }
+ }
+}
diff --git
a/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/RatExcludesTest.kt
b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/RatExcludesTest.kt
new file mode 100644
index 0000000..92b747c
--- /dev/null
+++ b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/RatExcludesTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.build
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+class RatExcludesTest {
+
+ @Test
+ fun `each entry yields both the path and its directory-contents glob`() {
+ assertEquals(listOf("**/build", "**/build/**"),
RatExcludes.fromGitignore(listOf("build/")))
+ }
+
+ @Test
+ fun `a no-separator entry matches at any depth`() {
+ // ".gradle" can appear at any level, so it must be prefixed with **/.
+
assertTrue(RatExcludes.fromGitignore(listOf(".gradle")).contains("**/.gradle"))
+
assertTrue(RatExcludes.fromGitignore(listOf("*.iml")).contains("**/*.iml"))
+ }
+
+ @Test
+ fun `a leading-slash entry is anchored to the repo root`() {
+ // "/build" means the root build dir only — no **/ prefix, leading
slash stripped.
+ assertEquals(listOf("build", "build/**"),
RatExcludes.fromGitignore(listOf("/build")))
+ }
+
+ @Test
+ fun `an interior-slash entry is root-anchored, not any-depth`() {
+ // git anchors "src/generated" to the root; it must NOT become
**/src/generated.
+ val globs = RatExcludes.fromGitignore(listOf("src/generated"))
+ assertEquals(listOf("src/generated", "src/generated/**"), globs)
+ }
+
+ @Test
+ fun `an entry already starting with double-star is left untouched`() {
+ assertEquals(listOf("**/*.log", "**/*.log/**"),
RatExcludes.fromGitignore(listOf("**/*.log")))
+ }
+
+ @Test
+ fun `blank lines, comments, and negations are skipped`() {
+ val globs = RatExcludes.fromGitignore(listOf("", " ", "# a comment",
"!keep.txt"))
+ assertTrue(globs.isEmpty(), "expected no globs but got $globs")
+ }
+
+ @Test
+ fun `duplicate entries are collapsed`() {
+ assertEquals(listOf("**/target", "**/target/**"),
RatExcludes.fromGitignore(listOf("target", "target/")))
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6fc2187..473ff9f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
[versions]
# Build plugins
spring-boot = "3.5.14"
diff --git a/src/main/resources/application-http.properties
b/src/main/resources/application-http.properties
index c1b9759..77578a3 100644
--- a/src/main/resources/application-http.properties
+++ b/src/main/resources/application-http.properties
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
spring.main.web-application-type=servlet
spring.ai.mcp.server.type=sync
spring.ai.mcp.server.protocol=stateless
diff --git a/src/main/resources/application-stdio.properties
b/src/main/resources/application-stdio.properties
index b8864df..37f848b 100644
--- a/src/main/resources/application-stdio.properties
+++ b/src/main/resources/application-stdio.properties
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
spring.main.web-application-type=none
# NOTE: You must disable the banner and the console logging
# to allow the STDIO transport to work !!!
diff --git a/src/main/resources/application.properties
b/src/main/resources/application.properties
index 1592a77..c2038f5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,3 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
spring.application.name=solr-mcp
spring.profiles.active=${PROFILES:stdio}
spring.ai.mcp.server.instructions=This server provides tools to interact with
Apache Solr using Model Context Protocol (MCP) over STDIO and/or HTTP.