Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"0.0.0.0:5678",
"servers/basic_mcp_stdio.py"
]
},
}

Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is trailing whitespace on this line after the closing brace. Consider removing it to maintain consistent formatting.

Suggested change

Copilot uses AI. Check for mistakes.
},
"inputs": []
}
}
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,23 +432,46 @@ This project supports deploying with OAuth 2.0 authentication using Keycloak as

Login with `admin` and your configured password.

### Testing with the agent

1. Generate the local environment file (automatically created after `azd up`):
### Use Keycloak OAuth MCP server with GitHub Copilot

```bash
./infra/write_env.sh
```
The Keycloak deployment supports Dynamic Client Registration (DCR), which allows VS Code to automatically register as an OAuth client. VS Code redirect URIs are pre-configured in the Keycloak realm.

To use the deployed MCP server with GitHub Copilot Chat:

1. To avoid conflicts, stop the MCP servers from `mcp.json` and disable the expense MCP servers in GitHub Copilot Chat tools.
2. Select "MCP: Add Server" from the VS Code Command Palette
3. Select "HTTP" as the server type
4. Enter the URL of the MCP server from `azd env get-value MCP_SERVER_URL`
5. You should see a Keycloak authentication screen open in your browser. Select "Allow access":

This creates `.env` with `KEYCLOAK_REALM_URL`, `MCP_SERVER_URL`, and Azure OpenAI settings.
![Keycloak allow access screen](screenshots/kc-allow-1.jpg)

2. Run the agent:
6. Sign in with a Keycloak user (e.g., `testuser` / `testpass` for the pre-configured demo user):

```bash
uv run agents/agentframework_http.py
![Keycloak sign-in screen](screenshots/kc-signin-2.jpg)

7. After authentication, the browser will redirect back to VS Code:

![VS Code redirect after Keycloak sign-in](screenshots/kc-redirect-3.jpg)

8. Enable the MCP server in GitHub Copilot Chat tools:

![Select MCP tools in GitHub Copilot](screenshots/kc-select-tools-4.jpg)

9. Test it with an expense tracking query:

```text
Log expense for 75 dollars of office supplies on my visa last Friday
```

The agent automatically detects `KEYCLOAK_REALM_URL` in the environment and authenticates via DCR + client credentials. On success, it will add an expense and print the result.
![Example GitHub Copilot Chat with Keycloak auth](screenshots/kc-chat-5.jpg)

10. Verify the expense was added by checking the Cosmos DB `user-expenses` container in the Azure Portal or by asking GitHub Copilot Chat:

```text
Show me my expenses from last week
```

### Known limitations (demo trade-offs)

Expand Down
120 changes: 0 additions & 120 deletions agents/keycloak_auth.py

This file was deleted.

11 changes: 2 additions & 9 deletions infra/http-routes.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,14 @@ resource httpRouteConfig 'Microsoft.App/managedEnvironments/httpRouteConfigs@202
parent: containerEnv
properties: {
rules: [
// Route /auth/* to Keycloak (strip /auth prefix since Keycloak serves at root)
// Using pathSeparatedPrefix ensures /auth doesn't match /authentication
{
description: 'Keycloak Authentication Server'
routes: [
{
match: {
pathSeparatedPrefix: '/auth'
}
action: {
prefixRewrite: '/'
}
action: {}
}
]
targets: [
Expand All @@ -49,17 +45,14 @@ resource httpRouteConfig 'Microsoft.App/managedEnvironments/httpRouteConfigs@202
}
]
}
// Route everything else to MCP server (catch-all)
{
description: 'MCP Expenses Server'
routes: [
{
match: {
prefix: '/'
}
action: {
prefixRewrite: '/'
}
action: {}
}
]
targets: [
Expand Down
10 changes: 5 additions & 5 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,7 @@ module server 'server.bicep' = {
openTelemetryPlatform: openTelemetryPlatform
exists: serverExists
// Keycloak authentication configuration (only when enabled)
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/realms/${keycloakRealmName}' : ''
keycloakTokenIssuer: useKeycloak ? '${keycloakMcpServerBaseUrl}/realms/${keycloakRealmName}' : ''
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/auth/realms/${keycloakRealmName}' : ''
keycloakMcpServerBaseUrl: useKeycloak ? keycloakMcpServerBaseUrl : ''
keycloakMcpServerAudience: keycloakMcpServerAudience
// Azure/Entra ID OAuth Proxy authentication configuration (only when enabled)
Expand All @@ -810,7 +809,7 @@ module agent 'agent.bicep' = {
openAiDeploymentName: openAiDeploymentName
openAiEndpoint: openAi.outputs.endpoint
mcpServerUrl: useKeycloak ? 'https://mcproutes.${containerApps.outputs.defaultDomain}/mcp' : '${server.outputs.uri}/mcp'
keycloakRealmUrl: useKeycloak ? '${keycloak.outputs.uri}/realms/${keycloakRealmName}' : ''
keycloakRealmUrl: useKeycloak ? '${keycloak.outputs.uri}/auth/realms/${keycloakRealmName}' : ''
exists: agentExists
}
}
Expand Down Expand Up @@ -946,9 +945,10 @@ output KEYCLOAK_MCP_SERVER_BASE_URL string = useKeycloak ? keycloakMcpServerBase

// Keycloak and MCP Server routing outputs (only populated when mcpAuthProvider is keycloak)
output KEYCLOAK_REALM_URL string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/realms/${keycloakRealmName}' : ''
output KEYCLOAK_ADMIN_CONSOLE string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/admin' : ''
output KEYCLOAK_ADMIN_CONSOLE string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/admin/master/console' : ''
output KEYCLOAK_DIRECT_URL string = keycloak.outputs.uri
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output KEYCLOAK_DIRECT_URL is not conditional on useKeycloak, which means it will attempt to access keycloak.outputs.uri even when Keycloak is not deployed. This will cause a deployment error when mcpAuthProvider is not set to "keycloak". Consider making this conditional like the other Keycloak outputs: output KEYCLOAK_DIRECT_URL string = useKeycloak ? keycloak.outputs.uri : ''

Suggested change
output KEYCLOAK_DIRECT_URL string = keycloak.outputs.uri
output KEYCLOAK_DIRECT_URL string = useKeycloak ? keycloak.outputs.uri : ''

Copilot uses AI. Check for mistakes.
output KEYCLOAK_TOKEN_ISSUER string = useKeycloak ? '${keycloakMcpServerBaseUrl}/realms/${keycloakRealmName}' : ''
output KEYCLOAK_TOKEN_ISSUER string = useKeycloak ? '${keycloakMcpServerBaseUrl}/auth/realms/${keycloakRealmName}' : ''
output KEYCLOAK_AGENT_REALM_URL string = useKeycloak ? '${keycloak!.outputs.uri}/auth/realms/${keycloakRealmName}' : ''

// Auth provider for env scripts
output MCP_AUTH_PROVIDER string = mcpAuthProvider
Expand Down
5 changes: 0 additions & 5 deletions infra/server.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ param applicationInsightsConnectionString string = ''
])
param openTelemetryPlatform string = 'appinsights'
param keycloakRealmUrl string = ''
param keycloakTokenIssuer string = ''
param keycloakMcpServerAudience string = 'mcp-server'
param keycloakMcpServerBaseUrl string = ''
param entraProxyClientId string = ''
Expand Down Expand Up @@ -112,10 +111,6 @@ var keycloakEnv = !empty(keycloakRealmUrl) ? [
name: 'KEYCLOAK_REALM_URL'
value: keycloakRealmUrl
}
{
name: 'KEYCLOAK_TOKEN_ISSUER'
value: !empty(keycloakTokenIssuer) ? keycloakTokenIssuer : keycloakRealmUrl
}
{
name: 'KEYCLOAK_MCP_SERVER_AUDIENCE'
value: keycloakMcpServerAudience
Expand Down
1 change: 1 addition & 0 deletions infra/write_env.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ $KEYCLOAK_REALM_URL = Get-AzdValue KEYCLOAK_REALM_URL
if ($KEYCLOAK_REALM_URL -and $KEYCLOAK_REALM_URL -ne "") {
Add-Content -Path $ENV_FILE_PATH -Value "KEYCLOAK_REALM_URL=$KEYCLOAK_REALM_URL"
Write-EnvIfSet KEYCLOAK_TOKEN_ISSUER
Write-EnvIfSet KEYCLOAK_AGENT_REALM_URL
}

# Entra proxy env vars (only if ENTRA_PROXY_AZURE_CLIENT_ID is set)
Expand Down
1 change: 1 addition & 0 deletions infra/write_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ KEYCLOAK_REALM_URL=$(get_azd_value KEYCLOAK_REALM_URL)
if [ -n "$KEYCLOAK_REALM_URL" ]; then
echo "KEYCLOAK_REALM_URL=${KEYCLOAK_REALM_URL}" >> "$ENV_FILE_PATH"
write_env_if_set KEYCLOAK_TOKEN_ISSUER
write_env_if_set KEYCLOAK_AGENT_REALM_URL
fi

# Entra proxy env vars (only if ENTRA_PROXY_AZURE_CLIENT_ID is set)
Expand Down
4 changes: 3 additions & 1 deletion keycloak/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
# Start in dev mode with H2 database (still uses pre-built themes)
# --proxy-headers=xforwarded tells Keycloak it's behind a reverse proxy that sets X-Forwarded-* headers
# --hostname-strict=false allows dynamic hostname resolution from proxy headers
# --http-relative-path=/auth sets the base path so Keycloak serves all content under /auth/*
# --import-realm imports the MCP realm on startup
CMD ["start-dev", "--http-port=8080", "--proxy-headers=xforwarded", "--hostname-strict=false", "--import-realm"]
# --import-strategy=overwrite-existing ensures realm.json changes are applied even if the realm exists
CMD ["start-dev", "--http-port=8080", "--proxy-headers=xforwarded", "--hostname-strict=false", "--http-relative-path=/auth", "--import-realm", "--import-strategy=overwrite-existing"]
Loading