Skip to content

Commit 2dec6db

Browse files
committed
Add VS Code GitHub Copilot integration docs for Keycloak OAuth
- Add "Use Keycloak OAuth MCP server with GitHub Copilot" section to README - Add step-by-step instructions for connecting VS Code to deployed MCP server - Include screenshots for authentication flow (allow access, sign-in, redirect) - Add Spanish translation of the new section to spanish/README.md - Configure VS Code redirect URIs in Keycloak realm for DCR support - Update infra and keycloak configs for VS Code OAuth redirect handling
1 parent 19f9b5b commit 2dec6db

17 files changed

Lines changed: 112 additions & 52 deletions

.vscode/mcp.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
"servers/basic_mcp_stdio.py"
2929
]
3030
},
31-
"my-mcp-server-e53825c5": {
32-
"url": "http://localhost:8000/mcp",
31+
"my-mcp-server-60626805": {
32+
"url": "https://mcproutes.whitetree-5dc0246f.eastus2.azurecontainerapps.io/mcp",
3333
"type": "http"
3434
}
3535
},

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,46 @@ This project supports deploying with OAuth 2.0 authentication using Keycloak as
393393

394394
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.
395395

396+
### Use Keycloak OAuth MCP server with GitHub Copilot
397+
398+
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.
399+
400+
To use the deployed MCP server with GitHub Copilot Chat:
401+
402+
1. To avoid conflicts, stop the MCP servers from `mcp.json` and disable the expense MCP servers in GitHub Copilot Chat tools.
403+
2. Select "MCP: Add Server" from the VS Code Command Palette
404+
3. Select "HTTP" as the server type
405+
4. Enter the URL of the MCP server from `azd env get-value MCP_SERVER_URL`
406+
5. You should see a Keycloak authentication screen open in your browser. Select "Allow access":
407+
408+
![Keycloak allow access screen](screenshots/kc-allow-1.jpg)
409+
410+
6. Sign in with a Keycloak user (e.g., `testuser` / `testpass` for the pre-configured demo user):
411+
412+
![Keycloak sign-in screen](screenshots/kc-signin-2.jpg)
413+
414+
7. After authentication, the browser will redirect back to VS Code:
415+
416+
![VS Code redirect after Keycloak sign-in](screenshots/kc-redirect-3.jpg)
417+
418+
8. Enable the MCP server in GitHub Copilot Chat tools:
419+
420+
![Select MCP tools in GitHub Copilot](screenshots/kc-select-tools-4.jpg)
421+
422+
9. Test it with an expense tracking query:
423+
424+
```text
425+
Log expense for 75 dollars of office supplies on my visa last Friday
426+
```
427+
428+
![Example GitHub Copilot Chat with Keycloak auth](screenshots/kc-chat-5.jpg)
429+
430+
10. Verify the expense was added by checking the Cosmos DB `user-expenses` container in the Azure Portal or by asking GitHub Copilot Chat:
431+
432+
```text
433+
Show me my expenses from last week
434+
```
435+
396436
### Known limitations (demo trade-offs)
397437
398438
| Item | Current | Production Recommendation | Why |

agents/agentframework_http.py

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
import asyncio
42
import logging
53
import os
@@ -13,24 +11,18 @@
1311
from rich import print
1412
from rich.logging import RichHandler
1513

16-
try:
17-
from keycloak_auth import get_auth_headers
18-
except ImportError:
19-
from agents.keycloak_auth import get_auth_headers
20-
2114
# Configure logging
2215
logging.basicConfig(level=logging.WARNING, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()])
2316
logger = logging.getLogger("agentframework_mcp_http")
17+
logger.setLevel(logging.INFO)
2418

25-
# Load environment variables
26-
load_dotenv(override=True)
27-
28-
# Constants
19+
# Configure constants and client based on environment
2920
RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true"
30-
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp/")
3121

32-
# Optional: Keycloak authentication (set KEYCLOAK_REALM_URL to enable)
33-
KEYCLOAK_REALM_URL = os.getenv("KEYCLOAK_REALM_URL")
22+
if not RUNNING_IN_PRODUCTION:
23+
load_dotenv(override=True)
24+
25+
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp/")
3426

3527
# Configure chat client based on API_HOST
3628
API_HOST = os.getenv("API_HOST", "github")
@@ -61,24 +53,9 @@
6153

6254

6355
# --- Main Agent Logic ---
64-
65-
6656
async def http_mcp_example() -> None:
67-
"""
68-
Demonstrate MCP integration with the Expenses MCP server.
69-
70-
If KEYCLOAK_REALM_URL is set, authenticates via OAuth (DCR + client credentials).
71-
Otherwise, connects without authentication.
72-
"""
73-
# Get auth headers if Keycloak is configured
74-
headers = await get_auth_headers(KEYCLOAK_REALM_URL, client_name_prefix="agentframework")
75-
if headers:
76-
logger.info(f"🔐 Auth enabled - connecting to {MCP_SERVER_URL} with Bearer token")
77-
else:
78-
logger.info(f"📡 No auth - connecting to {MCP_SERVER_URL}")
79-
8057
async with (
81-
MCPStreamableHTTPTool(name="Expenses MCP Server", url=MCP_SERVER_URL, headers=headers) as mcp_server,
58+
MCPStreamableHTTPTool(name="Expenses MCP Server", url=MCP_SERVER_URL) as mcp_server,
8259
ChatAgent(
8360
chat_client=client,
8461
name="Expenses Agent",
@@ -96,4 +73,4 @@ async def http_mcp_example() -> None:
9673

9774

9875
if __name__ == "__main__":
99-
asyncio.run(http_mcp_example())
76+
asyncio.run(http_mcp_example())

agents/keycloak_auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async def register_client_via_dcr(keycloak_realm_url: str, client_name_prefix: s
2727
2828
Args:
2929
keycloak_realm_url: The Keycloak realm URL (e.g., http://localhost:8080/realms/myrealm)
30+
Make sure to use direct container URL, not route.
3031
client_name_prefix: Prefix for the generated client name
3132
3233
Returns:

infra/http-routes.bicep

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ resource httpRouteConfig 'Microsoft.App/managedEnvironments/httpRouteConfigs@202
2929
parent: containerEnv
3030
properties: {
3131
rules: [
32-
// Route /auth/* to Keycloak (strip /auth prefix since Keycloak serves at root)
32+
// Route /auth/* to Keycloak (Keycloak is configured with --http-relative-path=/auth)
3333
// Using pathSeparatedPrefix ensures /auth doesn't match /authentication
3434
{
3535
description: 'Keycloak Authentication Server'
@@ -38,9 +38,7 @@ resource httpRouteConfig 'Microsoft.App/managedEnvironments/httpRouteConfigs@202
3838
match: {
3939
pathSeparatedPrefix: '/auth'
4040
}
41-
action: {
42-
prefixRewrite: '/'
43-
}
41+
action: {}
4442
}
4543
]
4644
targets: [

infra/main.bicep

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -749,8 +749,8 @@ module server 'server.bicep' = {
749749
applicationInsightsConnectionString: useMonitoring ? applicationInsights!.outputs.connectionString : ''
750750
exists: serverExists
751751
// Keycloak authentication configuration (only when enabled)
752-
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/realms/${keycloakRealmName}' : ''
753-
keycloakTokenIssuer: useKeycloak ? '${keycloakMcpServerBaseUrl}/realms/${keycloakRealmName}' : ''
752+
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/auth/realms/${keycloakRealmName}' : ''
753+
keycloakTokenIssuer: useKeycloak ? '${keycloakMcpServerBaseUrl}/auth/realms/${keycloakRealmName}' : ''
754754
keycloakMcpServerBaseUrl: useKeycloak ? keycloakMcpServerBaseUrl : ''
755755
keycloakMcpServerAudience: keycloakMcpServerAudience
756756
// Azure/Entra ID OAuth Proxy authentication configuration (only when enabled)
@@ -776,7 +776,7 @@ module agent 'agent.bicep' = {
776776
openAiDeploymentName: openAiDeploymentName
777777
openAiEndpoint: openAi.outputs.endpoint
778778
mcpServerUrl: useKeycloak ? 'https://mcproutes.${containerApps.outputs.defaultDomain}/mcp' : '${server.outputs.uri}/mcp'
779-
keycloakRealmUrl: useKeycloak ? '${keycloak.outputs.uri}/realms/${keycloakRealmName}' : ''
779+
keycloakRealmUrl: useKeycloak ? '${keycloak.outputs.uri}/auth/realms/${keycloakRealmName}' : ''
780780
exists: agentExists
781781
}
782782
}
@@ -912,9 +912,10 @@ output KEYCLOAK_MCP_SERVER_BASE_URL string = useKeycloak ? keycloakMcpServerBase
912912

913913
// Keycloak and MCP Server routing outputs (only populated when mcpAuthProvider is keycloak)
914914
output KEYCLOAK_REALM_URL string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/realms/${keycloakRealmName}' : ''
915-
output KEYCLOAK_ADMIN_CONSOLE string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/admin' : ''
915+
output KEYCLOAK_ADMIN_CONSOLE string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/admin/master/console' : ''
916916
output KEYCLOAK_DIRECT_URL string = keycloak.outputs.uri
917-
output KEYCLOAK_TOKEN_ISSUER string = useKeycloak ? '${keycloakMcpServerBaseUrl}/realms/${keycloakRealmName}' : ''
917+
output KEYCLOAK_TOKEN_ISSUER string = useKeycloak ? '${keycloakMcpServerBaseUrl}/auth/realms/${keycloakRealmName}' : ''
918+
output KEYCLOAK_AGENT_REALM_URL string = useKeycloak ? '${keycloak!.outputs.uri}/auth/realms/${keycloakRealmName}' : ''
918919

919920
// Auth provider for env scripts
920921
output MCP_AUTH_PROVIDER string = mcpAuthProvider

infra/write_env.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ $KEYCLOAK_REALM_URL = Get-AzdValue KEYCLOAK_REALM_URL
4747
if ($KEYCLOAK_REALM_URL -and $KEYCLOAK_REALM_URL -ne "") {
4848
Add-Content -Path $ENV_FILE_PATH -Value "KEYCLOAK_REALM_URL=$KEYCLOAK_REALM_URL"
4949
Write-EnvIfSet KEYCLOAK_TOKEN_ISSUER
50+
Write-EnvIfSet KEYCLOAK_AGENT_REALM_URL
5051
}
5152

5253
# Entra proxy env vars (only if ENTRA_PROXY_AZURE_CLIENT_ID is set)

infra/write_env.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ KEYCLOAK_REALM_URL=$(get_azd_value KEYCLOAK_REALM_URL)
5353
if [ -n "$KEYCLOAK_REALM_URL" ]; then
5454
echo "KEYCLOAK_REALM_URL=${KEYCLOAK_REALM_URL}" >> "$ENV_FILE_PATH"
5555
write_env_if_set KEYCLOAK_TOKEN_ISSUER
56+
write_env_if_set KEYCLOAK_AGENT_REALM_URL
5657
fi
5758

5859
# Entra proxy env vars (only if ENTRA_PROXY_AZURE_CLIENT_ID is set)

keycloak/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
2222
# Start in dev mode with H2 database (still uses pre-built themes)
2323
# --proxy-headers=xforwarded tells Keycloak it's behind a reverse proxy that sets X-Forwarded-* headers
2424
# --hostname-strict=false allows dynamic hostname resolution from proxy headers
25+
# --http-relative-path=/auth sets the base path so Keycloak serves all content under /auth/*
2526
# --import-realm imports the MCP realm on startup
26-
CMD ["start-dev", "--http-port=8080", "--proxy-headers=xforwarded", "--hostname-strict=false", "--import-realm"]
27+
CMD ["start-dev", "--http-port=8080", "--proxy-headers=xforwarded", "--hostname-strict=false", "--http-relative-path=/auth", "--import-realm"]

keycloak/realm.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@
6060
"host-sending-registration-request-must-match": ["false"],
6161
"client-uris-must-match": ["true"],
6262
"trusted-hosts": [
63-
"localhost",
64-
"127.0.0.1",
65-
"172.17.0.1",
66-
"host.docker.internal",
67-
"vscode.dev",
68-
"insiders.vscode.dev",
69-
"code.visualstudio.com"
70-
]
63+
"localhost",
64+
"127.0.0.1",
65+
"172.17.0.1",
66+
"host.docker.internal",
67+
"vscode.dev",
68+
"insiders.vscode.dev",
69+
"code.visualstudio.com"
70+
]
7171
}
7272
},
7373
{

0 commit comments

Comments
 (0)