Skip to content

Commit d6663a3

Browse files
committed
feat: use DCR dynamically and add multi-stage Dockerfile build
- Update langchainv1_keycloak.py to use Dynamic Client Registration instead of hardcoded client credentials - Add register_client_via_dcr() to create clients at runtime - Remove TEST_CLIENT_ID and TEST_CLIENT_SECRET constants - Update Dockerfile.keycloak to multi-stage build with kc.sh build for consistent theme cache hashes across replicas
1 parent a0b7d12 commit d6663a3

File tree

2 files changed

+53
-18
lines changed

2 files changed

+53
-18
lines changed

agents/langchainv1_keycloak.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
LangChain agent that connects to Keycloak-protected MCP server.
33
44
This script demonstrates:
5-
1. Getting an OAuth token from Keycloak via client_credentials grant
6-
2. Connecting to the MCP server with Bearer token authentication
7-
3. Using MCP tools through LangChain
5+
1. Dynamic Client Registration (DCR) with Keycloak
6+
2. Getting an OAuth token using the registered client
7+
3. Connecting to the MCP server with Bearer token authentication
8+
4. Using MCP tools through LangChain
89
910
Usage:
1011
python agents/langchainv1_keycloak.py
@@ -42,10 +43,6 @@
4243
"https://mcp-gps-key-n7pc5ej-kc.ashymeadow-ae27942e.eastus2.azurecontainerapps.io/realms/mcp"
4344
)
4445

45-
# Test client credentials (from DCR)
46-
CLIENT_ID = os.getenv("TEST_CLIENT_ID", "4f061e11-d30c-4978-bb2e-2164ce26cc51")
47-
CLIENT_SECRET = os.getenv("TEST_CLIENT_SECRET", "UKZLK1eEga9FfZTFsqTHXK70ubq0PAJu")
48-
4946
# Configure language model based on API_HOST
5047
API_HOST = os.getenv("API_HOST", "github")
5148

@@ -74,19 +71,47 @@
7471
base_model = ChatOpenAI(model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"))
7572

7673

77-
async def get_keycloak_token() -> str:
74+
async def register_client_via_dcr() -> tuple[str, str]:
75+
"""Register a new client dynamically using Keycloak's DCR endpoint."""
76+
dcr_url = f"{KEYCLOAK_REALM_URL}/clients-registrations/openid-connect"
77+
78+
logger.info("📝 Registering client via DCR...")
79+
80+
async with httpx.AsyncClient() as client:
81+
response = await client.post(
82+
dcr_url,
83+
json={
84+
"client_name": f"langchain-agent-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
85+
"grant_types": ["client_credentials"],
86+
"token_endpoint_auth_method": "client_secret_basic",
87+
},
88+
headers={"Content-Type": "application/json"},
89+
)
90+
91+
if response.status_code not in (200, 201):
92+
raise Exception(f"DCR failed: {response.status_code} - {response.text}")
93+
94+
data = response.json()
95+
client_id = data["client_id"]
96+
client_secret = data["client_secret"]
97+
98+
logger.info(f"✅ Registered client: {client_id[:20]}...")
99+
return client_id, client_secret
100+
101+
102+
async def get_keycloak_token(client_id: str, client_secret: str) -> str:
78103
"""Get an access token from Keycloak using client_credentials grant."""
79104
token_url = f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/token"
80105

81-
logger.info(f"🔑 Getting access token from Keycloak...")
106+
logger.info("🔑 Getting access token from Keycloak...")
82107

83108
async with httpx.AsyncClient() as client:
84109
response = await client.post(
85110
token_url,
86111
data={
87112
"grant_type": "client_credentials",
88-
"client_id": CLIENT_ID,
89-
"client_secret": CLIENT_SECRET,
113+
"client_id": client_id,
114+
"client_secret": client_secret,
90115
},
91116
headers={"Content-Type": "application/x-www-form-urlencoded"},
92117
)
@@ -106,8 +131,9 @@ async def run_agent() -> None:
106131
"""
107132
Run the agent to process expense-related queries using authenticated MCP tools.
108133
"""
109-
# First, get OAuth token from Keycloak
110-
access_token = await get_keycloak_token()
134+
# Register client via DCR and get token
135+
client_id, client_secret = await register_client_via_dcr()
136+
access_token = await get_keycloak_token(client_id, client_secret)
111137

112138
logger.info(f"📡 Connecting to MCP server: {MCP_SERVER_URL}")
113139

@@ -155,11 +181,11 @@ async def main():
155181
print("=" * 60)
156182
print("LangChain Agent with Keycloak-Protected MCP Server")
157183
print("=" * 60)
158-
print(f"\nConfiguration:")
184+
print("\nConfiguration:")
159185
print(f" MCP Server: {MCP_SERVER_URL}")
160186
print(f" Keycloak: {KEYCLOAK_REALM_URL}")
161-
print(f" Client ID: {CLIENT_ID[:20]}...")
162187
print(f" LLM Host: {API_HOST}")
188+
print(" Auth: Dynamic Client Registration (DCR)")
163189
print()
164190

165191
await run_agent()

infra/Dockerfile.keycloak

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
FROM quay.io/keycloak/keycloak:26.0
1+
FROM quay.io/keycloak/keycloak:26.0 AS builder
22

3-
# Copy the MCP realm configuration for import on startup
3+
# Copy the MCP realm configuration for import
44
COPY infra/keycloak-realm.json /opt/keycloak/data/import/mcp-realm.json
55

6+
# Build Keycloak to pre-compile themes (fixes cache hash mismatch across replicas)
7+
RUN /opt/keycloak/bin/kc.sh build
8+
9+
# Production image with pre-built themes
10+
FROM quay.io/keycloak/keycloak:26.0
11+
12+
# Copy built Keycloak with consistent theme cache hashes
13+
COPY --from=builder /opt/keycloak/ /opt/keycloak/
14+
615
# Expose port 8080
716
EXPOSE 8080
817

918
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
1019

11-
# Start in dev mode with H2 database
20+
# Start in dev mode with H2 database (still uses pre-built themes)
1221
# --proxy-headers=xforwarded tells Keycloak it's behind a reverse proxy that sets X-Forwarded-* headers
1322
# --hostname-strict=false allows dynamic hostname resolution from proxy headers
1423
# --import-realm imports the MCP realm on startup

0 commit comments

Comments
 (0)