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 804fb91a feat(compose): provision Keycloak with the realm the http
profile needs (#192)
804fb91a is described below
commit 804fb91ac829d7e2ebce460a1dac893cdff9a59e
Author: Aditya Parikh <[email protected]>
AuthorDate: Tue Oct 6 14:29:33 2026 -0400
feat(compose): provision Keycloak with the realm the http profile needs
(#192)
* feat(compose): provision Keycloak with the realm the http profile needs
The http profile authenticates against Keycloak, but nothing started one. A
developer had to run the container by hand and then script a realm, a client
and an audience mapper before the server would boot — and the mapper is
easy to
miss, because skipping it produces a token that is issued normally and then
refused with 401.
compose.yaml now defines a keycloak service that imports
keycloak/solr-mcp-realm.json, so the realm, both clients and the mapper
exist
before the server asks for a token. The import covers what the Quick Start
created by hand: solr-mcp-service (confidential, service accounts) for
machine-to-machine callers, solr-mcp-client (public) for MCP Inspector, and
testuser. The credentials in it are development credentials, committed on
purpose; a real deployment provisions its own.
The healthcheck is load-bearing rather than decoration. The server resolves
the
issuer while building its JWT decoder and fails to boot if the realm is not
yet
answering, which is the ordering constraint keycloak.md warns about;
declaring a
healthcheck makes Spring Boot's compose support wait for the container
instead.
Keycloak's image ships neither curl nor wget, so the probe goes through
bash's
/dev/tcp against the management port.
Verified by running the service from this compose file: the container
reaches
healthy, the realm resolves, and both grants return tokens carrying
aud http://localhost:8080/mcp — client_credentials for the service client
and
the password grant for the imported test user.
Signed-off-by: Aditya Parikh <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_011nUD34DFfoJeyQRTquPy7a
* feat(compose): gate Keycloak behind the http compose profile
The service comment said it was started only by the http profile, but it
had no profiles key, so a plain docker compose up -d for STDIO users pulled
Keycloak, bound 8180 and waited on its healthcheck. The service now sits in
the http compose profile, which application-http.properties activates via
spring.docker.compose.profiles.active. The Quick Start no longer says
bootRun alone is enough while also requiring OAUTH2_ISSUER_URI, and it tells
compose users to reuse solr-mcp-service instead of creating a client by
hand.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CiUHyyXLTo9ATdgg8eRFZJ
Signed-off-by: Aditya Parikh <[email protected]>
* refactor(compose): adopt upstream Keycloak container health check
recommendation
The Keycloak container image (UBI 9 Minimal) intentionally ships without
curl or wget, and health endpoints are exposed on the dedicated management
port 9000. Switch the healthcheck probe to Keycloak's official Containerfile
HEALTHCHECK pattern
(https://www.keycloak.org/observability/health#_healthcheck)
using bash socket redirection.
Signed-off-by: Aditya Parikh <[email protected]>
Co-authored-by: Junie <[email protected]>
---------
Signed-off-by: Aditya Parikh <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Junie <[email protected]>
---
compose.yaml | 39 ++++++++++++++++
docs/security/keycloak.md | 29 +++++++++++-
keycloak/solr-mcp-realm.json | 64 ++++++++++++++++++++++++++
src/main/resources/application-http.properties | 2 +
4 files changed, 132 insertions(+), 2 deletions(-)
diff --git a/compose.yaml b/compose.yaml
index 75c59201..3b5b6cc8 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -57,6 +57,45 @@ services:
# Prevent Spring Boot auto-configuration from trying to manage this
service
org.springframework.boot.ignore: "true"
+ #
=============================================================================
+ # Keycloak - the identity provider the http profile authenticates against
+ #
=============================================================================
+ # Gated behind the `http` compose profile so a plain `docker compose up -d`
(STDIO users) does not
+ # start it; the http Spring profile activates it via
spring.docker.compose.profiles.active. The realm is
+ # imported from keycloak/solr-mcp-realm.json, so the clients and —
critically — the audience
+ # protocol mapper exist before the server ever asks for a token.
+ #
+ # The mapper is what makes tokens acceptable here. Keycloak does not honour
the RFC 8707
+ # `resource=` parameter, so without it a token is issued normally and then
rejected by
+ # validateAudienceClaim(true) with a 401. See docs/security/keycloak.md.
+ #
+ # The healthcheck is not decoration: the server resolves the issuer at
startup and fails to boot
+ # if the realm is not yet answering, so Spring Boot must wait for this
container to be healthy
+ # before the application context starts.
+ # Keycloak 26's minimal image (UBI 9) intentionally omits curl and wget, and
health endpoints are
+ # exposed on the dedicated management port 9000. This probe follows
Keycloak's official Containerfile
+ # HEALTHCHECK recommendation
(https://www.keycloak.org/observability/health#_healthcheck) using
+ # bash's built-in /dev/tcp socket redirection.
+ keycloak:
+ profiles: [ http ]
+ image: quay.io/keycloak/keycloak:26.0
+ ports:
+ - "8180:8080"
+ networks: [ search ]
+ environment:
+ KC_BOOTSTRAP_ADMIN_USERNAME: admin
+ KC_BOOTSTRAP_ADMIN_PASSWORD: admin
+ KC_HEALTH_ENABLED: "true"
+ command: [ "start-dev", "--import-realm" ]
+ volumes:
+ - ./keycloak:/opt/keycloak/data/import:ro
+ healthcheck:
+ test: [ "CMD-SHELL", "{ printf 'HEAD /health/ready HTTP/1.0\\r\\n\\r\\n'
>&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000" ]
+ interval: 5s
+ timeout: 5s
+ retries: 30
+ start_period: 10s
+
volumes:
data:
diff --git a/docs/security/keycloak.md b/docs/security/keycloak.md
index 3befbf2c..36e76a82 100644
--- a/docs/security/keycloak.md
+++ b/docs/security/keycloak.md
@@ -77,11 +77,36 @@ The Quick Start below is ordered to satisfy both.
You do **not** need to start Solr yourself. `application-http.properties` sets
`spring.docker.compose.enabled=true`, so `./gradlew bootRun` starts the Solr,
-ZooKeeper and LGTM containers from `compose.yaml` before the application
-context comes up.
+ZooKeeper, LGTM and Keycloak containers from `compose.yaml` before the
+application context comes up.
## Quick Start
+> **The `http` profile now brings its own Keycloak.** `compose.yaml` defines a
`keycloak` service
+> that imports `keycloak/solr-mcp-realm.json` on startup, so the realm, both
clients and the
+> audience mapper exist before the server asks for a token — and because the
service declares a
+> healthcheck, Spring Boot waits for it rather than failing on an unresolvable
issuer. Point the
+> server at the imported realm and start it; Spring Boot's Docker Compose
support brings Keycloak up
+> (it sits behind the `http` compose profile, so start it by hand with
+> `docker compose --profile http up -d` if you are not using `bootRun`):
+>
+> ```bash
+> export PROFILES=http
+> export OAUTH2_ISSUER_URI=http://localhost:8180/realms/solr-mcp
+> ./gradlew bootRun
+> ```
+>
+> The imported realm provides `solr-mcp-service` (confidential, service
accounts, secret
+> `dev-only-not-a-secret`) for machine-to-machine callers, `solr-mcp-client`
(public) for MCP
+> Inspector, and `testuser` / `testpassword`. These are development
credentials committed on
+> purpose; a real deployment provisions its own. With this realm, skip the
client-creation steps
+> below and use `solr-mcp-service` / `dev-only-not-a-secret` wherever a
confidential client is needed.
+>
+> The manual walkthrough below remains the reference for what that import
contains, and for setting
+> the same thing up against an existing Keycloak. It binds its own container
to the same port,
+> 8180, so use one or the other: if you follow it, start the server with
+> `SPRING_DOCKER_COMPOSE_ENABLED=false` so Boot does not bring up a second
Keycloak.
+
This block is runnable end to end — copy the whole thing. It waits for
Keycloak, creates the realm, client, audience mapper and test user, verifies
the realm resolves, and only then starts the server.
diff --git a/keycloak/solr-mcp-realm.json b/keycloak/solr-mcp-realm.json
new file mode 100644
index 00000000..3fb0cd4b
--- /dev/null
+++ b/keycloak/solr-mcp-realm.json
@@ -0,0 +1,64 @@
+{
+ "realm": "solr-mcp",
+ "enabled": true,
+ "displayName": "Solr MCP (development)",
+ "clients": [
+ {
+ "clientId": "solr-mcp-service",
+ "name": "Machine-to-machine caller",
+ "description": "Confidential client for services that call the MCP
server on their own behalf.",
+ "enabled": true,
+ "publicClient": false,
+ "serviceAccountsEnabled": true,
+ "standardFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "secret": "dev-only-not-a-secret",
+ "protocolMappers": [
+ {
+ "name": "mcp-audience",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-mapper",
+ "config": {
+ "included.custom.audience": "http://localhost:8080/mcp",
+ "access.token.claim": "true",
+ "id.token.claim": "false",
+ "introspection.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "clientId": "solr-mcp-client",
+ "name": "MCP Inspector and browser clients",
+ "enabled": true,
+ "publicClient": true,
+ "directAccessGrantsEnabled": true,
+ "redirectUris": [ "http://localhost:6274/*", "http://localhost:*" ],
+ "webOrigins": [ "http://localhost:6274" ],
+ "protocolMappers": [
+ {
+ "name": "mcp-audience",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-mapper",
+ "config": {
+ "included.custom.audience": "http://localhost:8080/mcp",
+ "access.token.claim": "true",
+ "id.token.claim": "false",
+ "introspection.token.claim": "true"
+ }
+ }
+ ]
+ }
+ ],
+ "users": [
+ {
+ "username": "testuser",
+ "email": "[email protected]",
+ "firstName": "Test",
+ "lastName": "User",
+ "enabled": true,
+ "emailVerified": true,
+ "credentials": [ { "type": "password", "value": "testpassword",
"temporary": false } ]
+ }
+ ]
+}
diff --git a/src/main/resources/application-http.properties
b/src/main/resources/application-http.properties
index 77578a3c..d6a2cd18 100644
--- a/src/main/resources/application-http.properties
+++ b/src/main/resources/application-http.properties
@@ -21,6 +21,8 @@ spring.ai.mcp.server.stdio=false
# Docker Compose integration: automatically start Solr and other services on
bootRun.
# Spring Boot detects compose.yaml and starts declared services before the
application context.
spring.docker.compose.enabled=true
+# Keycloak sits behind the `http` compose profile so STDIO users' `docker
compose up -d` skips it.
+spring.docker.compose.profiles.active=http
# OAuth2 Security Configuration
# Configure the issuer URI for your OAuth2 authorization server
# For Auth0: https://<your-auth0-domain>/.well-known/openid-configuration