Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
29 changes: 27 additions & 2 deletions docs/security/keycloak.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 64 additions & 0 deletions keycloak/solr-mcp-realm.json
Original file line number Diff line number Diff line change
@@ -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": "test@example.com",
"firstName": "Test",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [ { "type": "password", "value": "testpassword", "temporary": false } ]
}
]
}
2 changes: 2 additions & 0 deletions src/main/resources/application-http.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down