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

Reply via email to