diff --git a/build.gradle.kts b/build.gradle.kts index 572bc760..c9052fbd 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 4d9bdff9..f05cc5ae 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 40fb7c8b..d1296d79 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 00000000..e9ea9495 --- /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().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 00000000..f23d948b --- /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): List = + 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 00000000..92b747cc --- /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 6fc21873..473ff9ff 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 c1b97597..77578a3c 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 b8864dfa..37f848be 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 1592a770..c2038f5c 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.