diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4c957e08..84695da0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,6 +28,10 @@ We use Spotless for code formatting and style enforcement. CI enforces `spotless
- Include unit tests for new features
- Keep methods focused and concise
+### Null safety
+
+Every package is `@NullMarked` via `package-info.java`. Methods, parameters, and return types are non-null by default — mark legitimate null surfaces with `@Nullable` (`org.jspecify.annotations.Nullable`). [NullAway](https://github.com/uber/NullAway) enforces the contract on `compileJava`; new code that violates it fails the build. Reference: [JSpecify](https://jspecify.dev/).
+
## Testing
To keep this document concise, please see the Development Guide for all testing workflows and tips:
@@ -137,15 +141,23 @@ For implementation details and examples, see the Development Guide:
- Dependencies and version catalogs: dev-docs/DEVELOPMENT.md#build-system
- Documentation practices: dev-docs/DEVELOPMENT.md#modifying-configuration
+## Security Setup (HTTP Mode)
+
+For OAuth2 configuration with supported providers:
+
+- [Auth0 Setup Guide](docs/security/auth0.md)
+- [Keycloak Setup Guide](docs/security/keycloak.md)
+
## Questions or Need Help?
-- Open an issue for bugs or feature requests
-- Start a discussion for questions or ideas
-- Check existing issues and discussions first
+- **Slack:** [`#solr-mcp`](https://the-asf.slack.com/archives/C09TVG3BM1P) in the `the-asf` workspace
+- **Issues:** [GitHub Issues](https://github.com/apache/solr-mcp/issues) for bugs or feature requests
+- **Discussions:** [GitHub Discussions](https://github.com/apache/solr-mcp/discussions) for questions or ideas
+- **Mailing lists:** Shared with Apache Solr — see [mailing lists](https://solr.apache.org/community.html#mailing-lists-chat)
## Code of Conduct
-Be respectful, inclusive, and professional. We're all here to build something great together.
+As an Apache project, we follow the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct).
## License
diff --git a/README.md b/README.md
index 60b63569..3b8027a8 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,24 @@
# Solr MCP Server
-A Spring AI Model Context Protocol (MCP) server that provides tools for interacting with Apache Solr. Enables AI assistants like Claude to search, index, and manage Solr collections through the MCP protocol.
+Search, index, and manage [Apache Solr](https://solr.apache.org/) collections using **natural language** — no need to hand-craft Solr queries, build filter expressions, or memorize the admin API.
+
+Instead of writing:
+
+```
+q=title:"star wars" AND genre_s:"sci-fi"&fq=year_i:[2000 TO *]&facet=true&facet.field=genre_s&sort=score desc&rows=10
+```
+
+Just ask your AI assistant:
+
+> *"Find sci-fi movies with 'star wars' in the title released after 2000, show me the genre breakdown, and sort by relevance."*
+
+This Spring AI [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/) server exposes Solr operations as tools that any MCP-compatible AI client (Claude Desktop, Claude Code, VS Code/Copilot, Cursor, JetBrains) can invoke.
+
+**[Website](https://solr.apache.org/mcp)** ·
+**[Quick Start](https://solr.apache.org/mcp/quick-start.html)** ·
+**[Client Setup](https://solr.apache.org/mcp/clients/claude-desktop.html)** ·
+**[Features](https://solr.apache.org/mcp/features.html)**
## What's inside
@@ -13,11 +30,17 @@ A Spring AI Model Context Protocol (MCP) server that provides tools for interact
- 🔧 Inspect schema
- 🔌 Transports: STDIO (Claude Desktop) and HTTP (MCP Inspector)
- 🔐 OAuth2 security with Auth0 (HTTP mode only)
-- 🐳 Docker images built with Jib
+- 🐳 Docker images built locally with Jib (JVM) and Paketo (native)
## Get started (users)
+> **Note:** Published container images are not yet available on a public registry. The `docker run` examples below use a **locally built** image — build it first with `./gradlew jibDockerBuild` (produces `solr-mcp:latest`), or use the JAR path instead. See [Building Docker images](#building-docker-images).
+
- Prerequisites: Java 25+, Docker (and Docker Compose), Git
+- Clone the repo:
+ ```bash
+ git clone https://github.com/apache/solr-mcp.git && cd solr-mcp
+ ```
- Start Solr with sample data:
```bash
docker compose up -d
@@ -35,7 +58,7 @@ A Spring AI Model Context Protocol (MCP) server that provides tools for interact
```
- Docker:
```bash
- docker run -i --rm ghcr.io/apache/solr-mcp:latest
+ docker run -i --rm solr-mcp:latest
```
- **HTTP mode**:
- Gradle:
@@ -48,10 +71,10 @@ A Spring AI Model Context Protocol (MCP) server that provides tools for interact
```
- Docker:
```bash
- docker run -p 8080:8080 --rm -e PROFILES=http ghcr.io/apache/solr-mcp:latest
+ docker run -p 8080:8080 --rm -e PROFILES=http solr-mcp:latest
```
-For more options (custom SOLR_URL, Linux host networking) see the Deployment Guide: docs/DEPLOYMENT.md
+For more options (custom SOLR_URL, Linux host networking) see the [Deployment Guide](dev-docs/DEPLOYMENT.md).
### Claude Desktop
@@ -65,7 +88,7 @@ Using Docker:
"mcpServers": {
"solr-mcp": {
"command": "docker",
- "args": ["run", "-i", "--rm", "ghcr.io/apache/solr-mcp:latest"],
+ "args": ["run", "-i", "--rm", "solr-mcp:latest"],
"env": {
"SOLR_URL": "http://localhost:8983/solr/"
}
@@ -107,7 +130,7 @@ Using Docker:
"-p",
"8080:8080",
"--rm",
- "ghcr.io/apache/solr-mcp:latest"
+ "solr-mcp:latest"
],
"env": {
"PROFILES": "http",
@@ -158,7 +181,7 @@ PROFILES=http java -jar build/libs/solr-mcp-1.0.0-SNAPSHOT.jar
Running via Docker:
```bash
-docker run -p 8080:8080 --rm -e PROFILES=http ghcr.io/apache/solr-mcp:latest
+docker run -p 8080:8080 --rm -e PROFILES=http solr-mcp:latest
```
Then add to your `claude_desktop_config.json`:
@@ -187,7 +210,7 @@ Add Solr MCP to [Claude Code](https://docs.anthropic.com/en/docs/claude-code) us
Using Docker (CLI):
```bash
-claude mcp add --transport stdio solr-mcp -- docker run -i --rm ghcr.io/apache/solr-mcp:latest
+claude mcp add --transport stdio solr-mcp -- docker run -i --rm solr-mcp:latest
```
Using JAR (CLI):
@@ -204,7 +227,7 @@ Using Docker:
"solr-mcp": {
"type": "stdio",
"command": "docker",
- "args": ["run", "-i", "--rm", "ghcr.io/apache/solr-mcp:latest"],
+ "args": ["run", "-i", "--rm", "solr-mcp:latest"],
"env": {
"SOLR_URL": "http://localhost:8983/solr/"
}
@@ -240,7 +263,7 @@ PROFILES=http ./gradlew bootRun
PROFILES=http java -jar build/libs/solr-mcp-1.0.0-SNAPSHOT.jar
# Docker
-docker run -p 8080:8080 --rm -e PROFILES=http ghcr.io/apache/solr-mcp:latest
+docker run -p 8080:8080 --rm -e PROFILES=http solr-mcp:latest
```
Then add to Claude Code:
@@ -260,6 +283,139 @@ Or add to `.mcp.json`:
}
```
+### Other clients
+
+
+VS Code / GitHub Copilot
+
+Create `.vscode/mcp.json` in your project root:
+
+**STDIO mode** (JAR):
+
+```json
+{
+ "servers": {
+ "solr-mcp": {
+ "type": "stdio",
+ "command": "java",
+ "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
+ "env": { "SOLR_URL": "http://localhost:8983/solr/" }
+ }
+ }
+}
+```
+
+**HTTP mode:**
+
+```json
+{
+ "servers": {
+ "solr-mcp": {
+ "type": "sse",
+ "url": "http://localhost:8080/mcp"
+ }
+ }
+}
+```
+
+After adding the configuration, Solr MCP tools are available in GitHub Copilot Chat (Agent mode).
+
+
+
+
+Cursor
+
+Create `.cursor/mcp.json` in your project root:
+
+**STDIO mode** (JAR):
+
+```json
+{
+ "mcpServers": {
+ "solr-mcp": {
+ "command": "java",
+ "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
+ "env": { "SOLR_URL": "http://localhost:8983/solr/" }
+ }
+ }
+}
+```
+
+**HTTP mode:**
+
+```json
+{
+ "mcpServers": {
+ "solr-mcp": {
+ "url": "http://localhost:8080/mcp"
+ }
+ }
+}
+```
+
+Or use **Cursor Settings > Features > MCP Servers > Add New MCP Server**.
+
+
+
+
+JetBrains IDEs
+
+Create `.junie/mcp.json` in your project root:
+
+**STDIO mode** (JAR):
+
+```json
+{
+ "mcpServers": {
+ "solr-mcp": {
+ "command": "java",
+ "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
+ "env": { "SOLR_URL": "http://localhost:8983/solr/" }
+ }
+ }
+}
+```
+
+**HTTP mode:**
+
+```json
+{
+ "mcpServers": {
+ "solr-mcp": {
+ "url": "http://localhost:8080/mcp"
+ }
+ }
+}
+```
+
+Or use **Settings > Tools > AI Assistant > MCP Servers > Add**.
+
+
+
+
+MCP Inspector
+
+```bash
+npx @modelcontextprotocol/inspector
+```
+
+**HTTP:** connect to `http://localhost:8080/mcp`
+
+**STDIO:** command `java`, arguments `-jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar`
+
+
+
+## Configuration
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `SOLR_URL` | Solr base URL | `http://localhost:8983/solr/` |
+| `PROFILES` | Transport mode: `stdio` or `http` | `stdio` |
+| `SECURITY_ENABLED` | Enable OAuth2 authentication (HTTP only) | `false` |
+| `OAUTH2_ISSUER_URI` | OAuth2 issuer URL (Auth0, Keycloak, Okta) | — |
+| `OTEL_SAMPLING_PROBABILITY` | Tracing sampling rate (0.0–1.0) | `1.0` |
+| `OTEL_TRACES_URL` | OTLP collector endpoint | `http://localhost:4317` |
+
## Security (OAuth2)
The Solr MCP server supports OAuth2 authentication when running in HTTP mode, providing secure access control for your
@@ -274,7 +430,7 @@ MCP tools.
### Quick Setup
-1. **Configure Auth0** (see detailed guide: [security-docs/AUTH0_SETUP.md](security-docs/AUTH0_SETUP.md))
+1. **Configure Auth0** (see detailed guide: [docs/security/auth0.md](docs/security/auth0.md))
- Create an Auth0 Application (Machine to Machine)
- Create an Auth0 API with your audience identifier
- Note your Domain, Client ID, Client Secret, and Audience
@@ -304,7 +460,7 @@ MCP tools.
http://localhost:8080/mcp
```
-For complete setup instructions, see [security-docs/AUTH0_SETUP.md](security-docs/AUTH0_SETUP.md)
+For complete setup instructions, see [docs/security/auth0.md](docs/security/auth0.md)
## Available MCP tools
@@ -339,6 +495,8 @@ For complete setup instructions, see [security-docs/AUTH0_SETUP.md](security-doc
| `add-fields` | Add one or more fields to a Solr collection schema (additive only; existing fields cannot be modified) |
| `get-schema` | Retrieve schema information for a collection |
+Every tool advertises MCP behavior hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so clients can build sensible approval UX — `search` and the metadata tools are read-only, indexing is destructive but idempotent, schema modification is additive.
+
## Available MCP Resources
MCP Resources provide a way to expose data that can be read by MCP clients. The Solr MCP Server provides the following resources:
@@ -356,6 +514,19 @@ The `solr://{collection}/schema` resource supports autocompletion for the `{coll

+## Available MCP Prompts
+
+Slash-command-style workflow templates. The framework wraps each prompt's return value as a single user-role `PromptMessage` directing the LLM through a canonical Solr workflow.
+
+| Prompt | Arguments | Purpose |
+|---|---|---|
+| `explore-collections` | — | Read-only walkthrough: list collections and characterise each by stats and health |
+| `setup-collection` | `name`, `purpose` (optional) | Validate a name, pick configset / shards / replication factor, create the collection, verify it |
+| `view-schema` | `collection` | Read-only schema walkthrough |
+| `design-schema` | `collection`, `datasetDescription`, `sampleDocument` (optional) | Inspect schema, choose field types, apply additive schema changes |
+| `index-data` | `collection`, `format` (`json` / `csv` / `xml`), `sample` (optional) | Verify the target schema, pick the right indexing tool, confirm the result |
+| `search-collection` | `collection`, `question` | Translate a natural-language question into a Solr query |
+
## Screenshots
- Claude Desktop (STDIO):
@@ -472,12 +643,7 @@ From a running HTTP-mode server:
curl -s http://localhost:8080/actuator/sbom/application > application.cdx.json
```
-From the local build (no server required):
-
-```bash
-./gradlew cyclonedxBom
-cat build/reports/application.cdx.json
-```
+To generate the SBOM locally from source, see [dev-docs/DEVELOPMENT.md#generating-the-sbom-locally](dev-docs/DEVELOPMENT.md#generating-the-sbom-locally).
### Scan the SBOM
@@ -493,7 +659,8 @@ Both tools natively consume CycloneDX 1.6 and report CVEs against the listed com
## Documentation
-- [Auth0 Setup (OAuth2 configuration)](security-docs/AUTH0_SETUP.md)
+- Security model: [STDIO transport](docs/security/stdio.md) · [HTTP transport](docs/security/http.md)
+- OAuth2 provider setup: [Auth0](docs/security/auth0.md) · [Keycloak](docs/security/keycloak.md)
- [GraalVM native image spec](docs/specs/graalvm-native-image.md)
## Contributing
@@ -502,10 +669,13 @@ We welcome contributions!
- Start here: [CONTRIBUTING.md](CONTRIBUTING.md)
-## Support
+## Community
-- Issues: https://github.com/apache/solr-mcp/issues
-- Discussions: https://github.com/apache/solr-mcp/discussions
+- **Website:** https://solr.apache.org/mcp
+- **Slack:** [`#solr-mcp`](https://the-asf.slack.com/archives/C09TVG3BM1P) in the `the-asf` workspace
+- **Mailing lists:** Shared with Apache Solr — see [mailing lists](https://solr.apache.org/community.html#mailing-lists-chat)
+- **Issues:** https://github.com/apache/solr-mcp/issues
+- **Discussions:** https://github.com/apache/solr-mcp/discussions
## License
diff --git a/TESTING_DISTRIBUTED_TRACING.md b/TESTING_DISTRIBUTED_TRACING.md
deleted file mode 100644
index 07af83b6..00000000
--- a/TESTING_DISTRIBUTED_TRACING.md
+++ /dev/null
@@ -1,174 +0,0 @@
-# Distributed Tracing Test Implementation - Complete ✅
-
-## Summary
-
-Successfully implemented comprehensive distributed tracing tests for Spring Boot 3.5 using SimpleTracer from micrometer-tracing-test. All distributed tracing unit tests are passing.
-
-## Test Results
-
-### DistributedTracingTest ✅
-**Status:** All 6 tests passing
-**Execution time:** ~6 seconds
-**Coverage:**
-- ✅ `shouldCreateSpanForSearchServiceMethod()` - Verifies spans are created for @Observed methods
-- ✅ `shouldIncludeSpanAttributes()` - Verifies span attributes/tags are set
-- ✅ `shouldCreateSpanHierarchy()` - Verifies span creation
-- ✅ `shouldSetCorrectSpanKind()` - Verifies span kinds
-- ✅ `shouldIncludeServiceNameInResource()` - Verifies service name in spans
-- ✅ `shouldRecordSpanDuration()` - Verifies span timing (start/end timestamps)
-
-## Key Implementation Details
-
-### 1. Test Configuration: OpenTelemetryTestConfiguration.java
-
-```java
-@TestConfiguration
-public class OpenTelemetryTestConfiguration {
- @Bean
- @Primary
- public SimpleTracer simpleTracer() {
- return new SimpleTracer();
- }
-}
-```
-
-**How it works:**
-- Provides SimpleTracer as @Primary bean to replace OpenTelemetry tracer
-- Spring Boot's observability auto-configuration connects this to the ObservationRegistry
-- No external infrastructure required for testing
-
-### 2. Test Approach
-
-**Spring Boot 3.5 Observability Stack:**
-```
-@Observed annotation → Micrometer Observation API → Micrometer Tracing → SimpleTracer
-```
-
-**Key API differences:**
-- Method: `tracer.getSpans()` (not `getFinishedSpans()`)
-- Return type: `Deque` (not `List`)
-- Span name format: `"search-service#search"` (kebab-case: `class-name#method-name`)
-
-### 3. Dependencies Added
-
-**Main dependencies** (build.gradle.kts):
-```kotlin
-implementation("io.micrometer:micrometer-tracing-bridge-otel")
-implementation("org.springframework.boot:spring-boot-starter-aop")
-```
-
-**Test dependencies** (libs.versions.toml):
-```kotlin
-micrometer-tracing-test = { module = "io.micrometer:micrometer-tracing-test" }
-awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" }
-```
-
-### 4. Test Properties
-
-```properties
-# Disable OTLP export in tests - we're using SimpleTracer instead
-management.otlp.tracing.endpoint=
-management.opentelemetry.logging.export.otlp.enabled=false
-
-# Ensure 100% sampling for tests
-management.tracing.sampling.probability=1.0
-
-# Enable @Observed annotation support
-management.observations.annotations.enabled=true
-```
-
-## Known Issues
-
-### OtlpExportIntegrationTest ⚠️
-**Status:** Disabled
-**Reason:** Jetty HTTP client ClassNotFoundException with LgtmStackContainer
-**Impact:** Low - core distributed tracing functionality is fully tested
-
-The testcontainers-grafana module requires `org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP` which is not properly resolved with the current Jetty BOM configuration. This integration test can be addressed separately or replaced with an alternative approach.
-
-**Workaround options:**
-1. Use a different HTTP client library (Apache HttpClient, OkHttp)
-2. Upgrade to testcontainers-grafana version that doesn't require Jetty
-3. Test OTLP export manually with LGTM Stack container
-4. Use different testing approach (MockWebServer, WireMock)
-
-## Files Modified
-
-### Test Files
-- `src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java` - 6 comprehensive tests
-- `src/test/java/org/apache/solr/mcp/server/observability/OpenTelemetryTestConfiguration.java` - SimpleTracer configuration
-- `src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java` - Disabled (Jetty issue)
-- `src/test/java/org/apache/solr/mcp/server/observability/LgtmAssertions.java` - LGTM Stack query helpers (ready for use)
-- `src/test/java/org/apache/solr/mcp/server/observability/TraceAssertions.java` - Span assertion utilities
-
-### Configuration Files
-- `build.gradle.kts` - Added micrometer-tracing-bridge-otel and spring-boot-starter-aop
-- `gradle/libs.versions.toml` - Added test dependencies (micrometer-tracing-test, awaitility, Jetty modules)
-
-### Main Code
-- `src/main/java/org/apache/solr/mcp/server/search/SearchService.java` - Already has @Observed annotation (no changes needed)
-
-## How to Run Tests
-
-```bash
-# Run distributed tracing tests only
-./gradlew test --tests "org.apache.solr.mcp.server.observability.DistributedTracingTest"
-
-# Run all tests
-./gradlew build
-
-# Run with verbose output
-./gradlew test --tests "*.DistributedTracingTest" --info
-```
-
-## Example Span Output
-
-From test execution, SimpleTracer captures spans like:
-```java
-SimpleSpan{
- name='search-service#search',
- tags={method=search, class=org.apache.solr.mcp.server.search.SearchService},
- startMillis=1770309759979,
- endMillis=1770309759988,
- traceId='72a53a4517951631',
- spanId='72a53a4517951631'
-}
-```
-
-## Spring Boot 3 vs Spring Boot 4 Differences
-
-| Aspect | Spring Boot 3.5 | Spring Boot 4 |
-|--------|----------------|---------------|
-| **Tracing API** | Micrometer Observation → Micrometer Tracing → OpenTelemetry | Direct OpenTelemetry integration |
-| **Test Approach** | SimpleTracer from micrometer-tracing-test | InMemorySpanExporter from opentelemetry-sdk-testing |
-| **Span Retrieval** | `tracer.getSpans()` | `spanExporter.getFinishedSpanItems()` |
-| **Span Type** | `SimpleSpan` (Micrometer) | `SpanData` (OpenTelemetry) |
-| **Bridge Dependency** | `micrometer-tracing-bridge-otel` required | Not required |
-| **AspectJ Starter** | `spring-boot-starter-aop` | `spring-boot-starter-aspectj` |
-
-## Next Steps (Optional)
-
-1. ✅ Core distributed tracing tests - **COMPLETE**
-2. ⚠️ LGTM Stack integration test - Jetty issue (optional to fix)
-3. 📝 Consider adding more span attribute assertions
-4. 📝 Consider testing span parent-child relationships explicitly
-5. 📝 Consider adding tests for error scenarios (exceptions in @Observed methods)
-
-## References
-
-- [Micrometer Tracing Testing Documentation](https://docs.micrometer.io/tracing/reference/testing.html)
-- [Spring Boot 3 Observability](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.micrometer-tracing)
-- [SimpleTracer API](https://github.com/micrometer-metrics/tracing/blob/main/micrometer-tracing-tests/micrometer-tracing-test/src/main/java/io/micrometer/tracing/test/simple/SimpleTracer.java)
-- [Observability With Spring Boot | Baeldung](https://www.baeldung.com/spring-boot-3-observability)
-
-## Success Criteria Met ✅
-
-- [x] Comprehensive distributed tracing test suite implemented
-- [x] Tests adapted from Spring Boot 4 implementation (PR #23)
-- [x] All unit tests passing (6/6 DistributedTracingTest)
-- [x] No regressions (full build successful)
-- [x] Spring Boot 3.5 architecture properly used (Micrometer Observation API)
-- [x] SimpleTracer successfully capturing spans from @Observed annotations
-- [x] Test documentation complete
-
-**Result:** Distributed tracing testing for Spring Boot 3.5 is fully functional and ready for use. ✅
diff --git a/dev-docs/DEVELOPMENT.md b/dev-docs/DEVELOPMENT.md
index a9624c18..6f5f28d4 100644
--- a/dev-docs/DEVELOPMENT.md
+++ b/dev-docs/DEVELOPMENT.md
@@ -37,6 +37,37 @@ The build produces an executable JAR in `build/libs/`:
- `solr-mcp-1.0.0-SNAPSHOT.jar` — Spring Boot executable (fat) JAR
+### Publishing to Maven Local
+
+To install the project artifacts to your local Maven repository for testing or local development:
+
+```bash
+./gradlew publishToMavenLocal
+```
+
+This publishes the following artifacts to `~/.m2/repository/org/apache/solr/solr-mcp/{version}/`:
+
+- `solr-mcp-{version}.jar` - Main application JAR
+- `solr-mcp-{version}-sources.jar` - Source code for IDE navigation
+- `solr-mcp-{version}-javadoc.jar` - API documentation
+- `solr-mcp-{version}.pom` - Maven POM with dependencies
+
+This is useful when:
+- Testing the library locally before publishing to a remote repository
+- Sharing artifacts between local projects during development
+- Verifying the published POM and artifact structure
+
+### Generating the SBOM locally
+
+The build produces a [CycloneDX](https://cyclonedx.org/) 1.6 Software Bill of Materials. The Spring Boot Gradle plugin also embeds it in the bootJar (and therefore every Docker image) at `META-INF/sbom/application.cdx.json`. To generate it from source without running the server:
+
+```bash
+./gradlew cyclonedxBom
+cat build/reports/application.cdx.json
+```
+
+For consuming and scanning the SBOM (HTTP endpoint, GitHub Release attachment, Trivy/Grype), see the [Supply chain & SBOM](../README.md#supply-chain--sbom) section of the README.
+
## Running Locally
### Start Solr
@@ -142,6 +173,43 @@ npx @modelcontextprotocol/inspector
Then open the browser URL provided (typically http://localhost:6274) and connect to http://localhost:8080/mcp
+### Distributed Tracing Tests
+
+`DistributedTracingTest` verifies that spans are produced for `@Observed` methods (e.g.
+`SearchService#search`) without requiring any external tracing infrastructure.
+
+```bash
+./gradlew test --tests "org.apache.solr.mcp.server.observability.DistributedTracingTest"
+```
+
+**How it works.** Spring Boot 3.5's observability stack is
+`@Observed annotation → Micrometer Observation API → Micrometer Tracing → tracer`. The test
+swaps in a `SimpleTracer` (from `micrometer-tracing-test`) as a `@Primary` bean via
+`OpenTelemetryTestConfiguration`, so spans are captured in-memory. Spans are retrieved with
+`tracer.getSpans()` (returns `Deque`) and named in kebab-case as
+`class-name#method-name` (e.g. `search-service#search`). Test properties disable OTLP export,
+force `management.tracing.sampling.probability=1.0`, and set
+`management.observations.annotations.enabled=true`.
+
+**Known issue — `OtlpExportIntegrationTest` is disabled.** The end-to-end OTLP export test
+(via `LgtmStackContainer`/`testcontainers-grafana`) fails with a Jetty
+`ClassNotFoundException` for `org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP`
+under the current Jetty BOM. Core tracing is fully covered by `DistributedTracingTest`, so the
+impact is low; fixing it would mean swapping the HTTP client (Apache HttpClient/OkHttp) or
+upgrading `testcontainers-grafana`.
+
+**Spring Boot 3.5 vs 4 differences** (relevant if/when we upgrade — SB4 drops the Micrometer
+bridge for direct OpenTelemetry):
+
+| Aspect | Spring Boot 3.5 | Spring Boot 4 |
+|--------|-----------------|---------------|
+| Tracing API | Micrometer Observation → Micrometer Tracing → OpenTelemetry | Direct OpenTelemetry integration |
+| Test approach | `SimpleTracer` (`micrometer-tracing-test`) | `InMemorySpanExporter` (`opentelemetry-sdk-testing`) |
+| Span retrieval | `tracer.getSpans()` | `spanExporter.getFinishedSpanItems()` |
+| Span type | `SimpleSpan` (Micrometer) | `SpanData` (OpenTelemetry) |
+| Bridge dependency | `micrometer-tracing-bridge-otel` required | not required |
+| AspectJ starter | `spring-boot-starter-aop` | `spring-boot-starter-aspectj` |
+
## Code Quality
### Spotless Formatting
@@ -206,6 +274,69 @@ export DOCKER_EXECUTABLE=/custom/path/to/docker
./gradlew jibDockerBuild
```
+### Native Image (GraalVM)
+
+Native compilation is opt-in behind the `-Pnative` Gradle property. It trades a
+slower, RAM-hungry build for sub-second startup, much lower RSS, and a smaller,
+JRE-free image — most valuable for the local STDIO use case where Claude Desktop
+launches a fresh container per session. See the [Image × Mode matrix](../AGENTS.md)
+for which image serves which transport.
+
+**Prerequisites.** `nativeCompile`/`nativeTest` need a GraalVM JDK on `PATH` or
+`JAVA_HOME` (the plugin does not auto-provision a toolchain). Install locally via
+SDKMAN:
+
+```bash
+sdk install java 25.0.2-graalce
+```
+
+or download from . CI provisions it with
+`graalvm/setup-graalvm`.
+
+**Build and test.**
+
+```bash
+# Compile a host-OS native binary
+./gradlew nativeCompile -Pnative
+
+# Run the test suite as a native image (slow — not part of ./gradlew build)
+./gradlew nativeTest -Pnative
+
+# Build a native Docker image via Paketo buildpacks (compiles inside a Linux
+# builder container, so it works on any host OS — no cross-compilation)
+./gradlew bootBuildImage -Pnative # stdio binary
+./gradlew bootBuildImage -Pnative -Pprofile=http # http binary
+```
+
+`nativeTest` is intentionally excluded from `./gradlew build` (an image compile
+per run is slow); it runs in the dedicated `native.yml` CI job instead.
+
+**Adding a reflection hint.** GraalVM's closed-world analysis can't see
+reflective access, so when `nativeTest` fails with a missing-class/method or
+resource error:
+
+1. Add a targeted hint to a `RuntimeHintsRegistrar` (we centralize these in
+ `SolrNativeHints.java`, registered via `@ImportRuntimeHints`) rather than
+ scattering `@Reflective` annotations — the rules stay reviewable in one place.
+2. Only if static analysis of the failures is too noisy, fall back to the
+ tracing agent (`-agentlib:native-image-agent`); commit its output under
+ `src/main/resources/META-INF/native-image/`.
+
+**Known gotchas.**
+
+- **Memory:** `nativeCompile` commonly needs 4–8 GB RAM. Ensure local/CI runners
+ have headroom.
+- **First Paketo build is large:** `bootBuildImage` downloads a ~1 GB builder on
+ first run; CI caching mitigates this.
+- **OpenTelemetry build-time init:** the pinned OTel instrumentation BOM lacks
+ native metadata, so the build adds `--initialize-at-build-time` for four OTel
+ packages (see `SolrNativeHints`/`build.gradle.kts`). Do **not** add
+ `io.opentelemetry.instrumentation.spring` — it contains CGLIB proxies that
+ cannot be build-time initialized. Bumping the OTel BOM to 2.26.1 currently
+ fails at AOT time (`io.opentelemetry.common.ComponentLoader` not found) because
+ it outpaces the OTel SDK that Spring Boot 3.5.x manages; revisit when Spring
+ Boot aligns its managed OTel version.
+
## IDE Setup
### IntelliJ IDEA
diff --git a/security-docs/AUTH0_SETUP.md b/docs/security/auth0.md
similarity index 100%
rename from security-docs/AUTH0_SETUP.md
rename to docs/security/auth0.md
diff --git a/docs/security/http.md b/docs/security/http.md
index 7c823243..6add6df7 100644
--- a/docs/security/http.md
+++ b/docs/security/http.md
@@ -172,5 +172,6 @@ exists for browser-based tooling.
## Related documents
- [STDIO transport security model](./stdio.md)
+- OAuth2 provider setup: [Auth0](./auth0.md) · [Keycloak](./keycloak.md)
- [GraalVM native image spec](../specs/graalvm-native-image.md)
- [Logging architecture in `CLAUDE.md`](../../CLAUDE.md#logging-architecture)
diff --git a/security-docs/keycloak.md b/docs/security/keycloak.md
similarity index 100%
rename from security-docs/keycloak.md
rename to docs/security/keycloak.md
diff --git a/docs/security/stdio.md b/docs/security/stdio.md
index 74b35658..af49a0a7 100644
--- a/docs/security/stdio.md
+++ b/docs/security/stdio.md
@@ -93,6 +93,7 @@ that launched the process. No code changes are required for STDIO security.
## Related documents
-- HTTP transport security model — *planned, will live at `docs/security/http.md`*
+- [HTTP transport security model](./http.md)
+- OAuth2 provider setup (HTTP mode): [Auth0](./auth0.md) · [Keycloak](./keycloak.md)
- [GraalVM native image spec](../specs/graalvm-native-image.md)
- [Logging architecture in `CLAUDE.md`](../../CLAUDE.md#logging-architecture)