-
Notifications
You must be signed in to change notification settings - Fork 36
Add Entra OAuth proxy (via FastMCP AzureAuthProvider) and automated Entra app registration #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| @@ -0,0 +1,219 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These files were based off the same files in azure-search-openai-demo.
I was hoping that I could use Bicep instead, but I think that'd require a FastMCP change, as they accept a client secret, whereas Bicep can also set up MI-as-FIC.
| "keycloakMcpServerAudience": { | ||
| "value": "${KEYCLOAK_MCP_SERVER_AUDIENCE=mcp-server}" | ||
| }, | ||
| "useFastMcpAuth": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do wonder if we should instead have one variable, MCP_AUTH_PROVIDER, default to None, optionally keycloak or entra.
|
|
||
|
|
||
| @mcp.tool | ||
| async def get_user_expenses(ctx: Context): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed to a tool as Copilot really does not want to use resources!
| except Exception as e: | ||
| logger.error(f"Error reading expenses: {str(e)}") | ||
| return f"Error: Unable to retrieve expense data - {str(e)}" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed prompt as well, doesnt seem necessary for this demonstration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds comprehensive support for Microsoft Entra ID (Azure AD) OAuth authentication to the MCP server via FastMCP's AzureAuthProvider, alongside automating Azure app registration through pre- and post-provision hooks. The implementation introduces a new authenticated server variant (auth_mcp.py), a custom Cosmos DB storage adapter for OAuth client persistence, and extensive infrastructure updates to support both Entra and existing Keycloak authentication options.
Key Changes:
- Automated Azure/Entra ID app registration with pre-provision hooks that create app registrations and post-provision hooks that update redirect URIs with deployed URLs
- New
auth_mcp.pyserver implementation supporting both Entra OAuth and Keycloak authentication with user-scoped data storage - CosmosDB-backed OAuth client storage via custom
CosmosDBStoreimplementing the py-key-value AsyncKeyValue protocol - Infrastructure updates enabling dynamic MCP entry point selection based on authentication configuration
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Updated dependencies for FastMCP 2.13.3, MCP 1.22.0, msgraph-sdk, dotenv-azd, and related OAuth packages |
| servers/deployed_mcp.py | Removed Keycloak authentication logic; simplified to basic MCP server without auth |
| servers/auth_mcp.py | New authenticated MCP server supporting both Entra OAuth and Keycloak with user-scoped expense tracking |
| servers/cosmosdb_store.py | New Cosmos DB implementation of AsyncKeyValue protocol for OAuth client storage |
| servers/Dockerfile | Updated CMD to use environment variable for dynamic entry point selection (deployed vs auth) |
| pyproject.toml | Added msgraph-sdk and dotenv-azd dependencies; bumped fastmcp version requirement |
| infra/main.bicep | Added useFastMcpAuth parameter, entraProxyClientId/Secret parameters, and new Cosmos containers |
| infra/server.bicep | Added Entra OAuth environment variables and secrets; dynamic entry point selection logic |
| infra/main.parameters.json | Added parameter mappings for USE_FASTMCP_AUTH and Entra client credentials |
| infra/fastmcp_auth_init.py | Pre-provision script to create/update Entra app registration with OAuth configuration |
| infra/fastmcp_auth_update.py | Post-provision script to update app registration with deployed server URL |
| infra/auth_init.sh/ps1 | Shell hooks to invoke fastmcp_auth_init.py during pre-provision |
| infra/auth_update.sh/ps1 | Shell hooks to invoke fastmcp_auth_update.py during post-provision |
| infra/write_env.sh/ps1 | Updated to write new Entra auth environment variables to .env file |
| azure.yaml | Wired up preprovision and postprovision hooks for both POSIX and Windows |
| README.md | Added comprehensive Entra OAuth deployment documentation and troubleshooting guidance |
| AGENTS.md | New developer instructions for adding azd environment variables |
| .vscode/mcp.json | Fixed JSON formatting (trailing comma) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 1. **infra/main.parameters.json**: Add the new parameter mapping from azd env variable to Bicep parameter | ||
| - Use format `${ENV_VAR_NAME}` for required values | ||
| - Use format `${ENV_VAR_NAME=default}` for optional values with defaults | ||
| - Example: `"useEntraProxy": { "value": "${USE_ENTRA_PROXY=false}" }` |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable name in the AGENTS.md example doesn't match the actual parameter name used in the PR. The example shows useEntraProxy but the actual code uses useFastMcpAuth (see infra/main.parameters.json line 53-54 and infra/main.bicep line 72).
Update the example to use the actual parameter name:
"useFastMcpAuth": { "value": "${USE_FASTMCP_AUTH=false}" }
| - Example: `"useEntraProxy": { "value": "${USE_ENTRA_PROXY=false}" }` | |
| - Example: `"useFastMcpAuth": { "value": "${USE_FASTMCP_AUTH=false}" }` |
| { | ||
| name: 'AZURE_TENANT_ID' | ||
| value: tenantId | ||
| } |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MCP_SERVER_URL environment variable is missing from the container configuration. The auth_mcp.py file reads this variable at line 73 when RUNNING_IN_PRODUCTION is true, but it's not set in the Bicep template.
Add this to the appropriate env array in server.bicep:
var entraProxyEnv = !empty(entraProxyClientId) ? [
{
name: 'MCP_SERVER_URL'
value: entraProxyBaseUrl
}
// ... existing entries
] : []Or add it to baseEnv if it should always be available.
| } | |
| } | |
| { | |
| name: 'MCP_SERVER_URL' | |
| value: entraProxyBaseUrl | |
| } |
| ENTRYPOINT ["uvicorn", "deployed_mcp:app", "--host", "0.0.0.0", "--port", "8000"] | ||
| ENV MCP_ENTRY=deployed | ||
|
|
||
| CMD ["uvicorn", "${MCP_ENTRY}_mcp:app", "--host", "0.0.0.0", "--port", "8000"] |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Dockerfile CMD uses shell variable substitution ${MCP_ENTRY}_mcp:app, but this won't work as expected with the CMD exec form (JSON array). Shell variable expansion only works with the shell form of CMD.
The current command will literally pass the string "${MCP_ENTRY}_mcp:app" to uvicorn without expanding the variable.
Consider using the shell form instead:
CMD uvicorn ${MCP_ENTRY}_mcp:app --host 0.0.0.0 --port 8000Or use ENTRYPOINT with shell form:
ENTRYPOINT sh -c "uvicorn ${MCP_ENTRY}_mcp:app --host 0.0.0.0 --port 8000"| CMD ["uvicorn", "${MCP_ENTRY}_mcp:app", "--host", "0.0.0.0", "--port", "8000"] | |
| CMD uvicorn ${MCP_ENTRY}_mcp:app --host 0.0.0.0 --port 8000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also encountered this error when deploying, what copilot suggests works
|
|
||
| ```bash | ||
| # Run the MCP server | ||
| cd servers && uvicorn deployed_mcp:app --host 0.0.0.0 --port 8000 |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation refers to running uvicorn deployed_mcp:app for local testing with OAuth, but when OAuth is enabled, the code actually uses auth_mcp.py (not deployed_mcp.py).
This should be:
cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000The deployed infrastructure correctly selects the entry point based on auth configuration, but the documentation should reflect this.
| cd servers && uvicorn deployed_mcp:app --host 0.0.0.0 --port 8000 | |
| cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000 |
| |----------|-------------| | ||
| | `FASTMCP_AUTH_AZURE_CLIENT_ID` | The App Registration's client ID | | ||
| | `FASTMCP_AUTH_AZURE_CLIENT_SECRET` | The App Registration's client secret | | ||
| | `FASTMCP_AUTH_AZURE_TENANT_ID` | Your Azure tenant ID | |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The environment variable name is inconsistent. The table lists FASTMCP_AUTH_AZURE_TENANT_ID, but the actual code uses AZURE_TENANT_ID (without the FASTMCP_AUTH prefix).
Looking at the code in auth_mcp.py line 90 and server.bicep line 109-110, the variable is named AZURE_TENANT_ID.
Update the documentation to use AZURE_TENANT_ID instead of FASTMCP_AUTH_AZURE_TENANT_ID.
| | `FASTMCP_AUTH_AZURE_TENANT_ID` | Your Azure tenant ID | | |
| | `AZURE_TENANT_ID` | Your Azure tenant ID | |
| logfire.configure(service_name="expenses-mcp", send_to_logfire=True) | ||
|
|
||
| # Configure Cosmos DB client | ||
| azure_credential = ChainedTokenCredential(ManagedIdentityCredential(), DefaultAzureCredential()) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ChainedTokenCredential tries ManagedIdentityCredential() first without specifying a client ID. In production, the code sets AZURE_CLIENT_ID in the environment (see server.bicep line 44-46), but the ManagedIdentityCredential here doesn't use it.
For consistency with the deployed_mcp.py approach (lines 52-56), consider using the client ID when available:
if RUNNING_IN_PRODUCTION and os.getenv("AZURE_CLIENT_ID"):
azure_credential = ManagedIdentityCredential(client_id=os.environ["AZURE_CLIENT_ID"])
else:
azure_credential = ChainedTokenCredential(ManagedIdentityCredential(), DefaultAzureCredential())This ensures the managed identity is used correctly in production.
| azure_credential = ChainedTokenCredential(ManagedIdentityCredential(), DefaultAzureCredential()) | |
| if RUNNING_IN_PRODUCTION and os.getenv("AZURE_CLIENT_ID"): | |
| azure_credential = ManagedIdentityCredential(client_id=os.environ["AZURE_CLIENT_ID"]) | |
| else: | |
| azure_credential = ChainedTokenCredential(ManagedIdentityCredential(), DefaultAzureCredential()) |
| // Azure/Entra ID OAuth Proxy environment variables (only added when configured) | ||
| var entraProxyEnv = !empty(entraProxyClientId) ? [ | ||
| { | ||
| name: 'FASTMCP_AUTH_AZURE_CLIENT_ID' | ||
| value: entraProxyClientId | ||
| } | ||
| { | ||
| name: 'FASTMCP_AUTH_AZURE_CLIENT_SECRET' | ||
| secretRef: 'entra-proxy-client-secret' | ||
| } | ||
| { | ||
| name: 'FASTMCP_AUTH_AZURE_BASE_URL' | ||
| value: entraProxyBaseUrl | ||
| } | ||
| { | ||
| name: 'AZURE_TENANT_ID' | ||
| value: tenantId | ||
| } | ||
| ] : [] |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The environment variables USE_FASTMCP_AUTH and USE_KEYCLOAK are missing from the Bicep configuration. The auth_mcp.py file checks these variables (lines 77 and 98) to determine which authentication mode to use, but they are not set in the container environment.
Add these environment variables to the appropriate env arrays in server.bicep:
var entraProxyEnv = !empty(entraProxyClientId) ? [
{
name: 'USE_FASTMCP_AUTH'
value: 'true'
}
// ... existing entries
] : []
var keycloakEnv = !empty(keycloakRealmUrl) ? [
{
name: 'USE_KEYCLOAK'
value: 'true'
}
// ... existing entries
] : []| - `http://localhost:5173/oauth/callback` (VS Code extension localhost) | ||
| - `http://localhost:5174/oauth/callback` (VS Code extension localhost alt port) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The redirect URIs listed in the documentation don't match what's actually configured in the code. The code in fastmcp_auth_init.py sets up:
https://vscode.dev/redirecthttp://127.0.0.1:{33418-33427}(10 ports)
But the documentation mentions:
http://localhost:5173/oauth/callbackhttp://localhost:5174/oauth/callbackhttps://vscode.dev/redirect
Update the documentation to accurately reflect the redirect URIs that are actually configured, or update the code to match the documentation.
| - `http://localhost:5173/oauth/callback` (VS Code extension localhost) | |
| - `http://localhost:5174/oauth/callback` (VS Code extension localhost alt port) | |
| - `http://127.0.0.1:{33418-33427}` (VS Code extension local authentication, 10 ports) |
|
|
||
| def update_azd_env(name: str, val: str) -> None: | ||
| """Update an Azure Developer CLI environment variable.""" | ||
| subprocess.run(f'azd env set {name} "{val}"', shell=True) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using shell=True with subprocess and string formatting creates a command injection security vulnerability. If name or val contain shell metacharacters, they could execute arbitrary commands.
Use subprocess with a list of arguments instead:
subprocess.run(['azd', 'env', 'set', name, val], check=True)This is safer and avoids shell injection risks.
| subprocess.run(f'azd env set {name} "{val}"', shell=True) | |
| subprocess.run(['azd', 'env', 'set', name, val], check=True) |
| for app in apps.value: | ||
| if app.app_id == app_id: | ||
| return app.id | ||
| except Exception: |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| exit 0 | ||
| fi | ||
|
|
||
| echo "Setting up Azure/Entra ID app registration for FastMCP OAuth Proxy..." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hit a KeyError: 'AZURE_TENANT_ID' during azd up here. I think it is because the variable isn't exported to pre-provision hooks.
I manually added:
AZURE_TENANT_ID=$(azd env get-value AZURE_TENANT_ID 2>/dev/null)
if [ $? -ne 0 ]; then AZURE_TENANT_ID=""; fi
export AZURE_TENANT_ID
if [ -z "$AZURE_TENANT_ID" ]; then
# Fallback: Extract tenant ID from azd auth token
token_json=$(azd auth token --output json 2>/dev/null)
if [ -n "$token_json" ]; then
AZURE_TENANT_ID=$(python -c "import sys, json, base64; ... print(json.loads(base64.b64decode(payload).decode('utf-8'))['tid'])" <<< "$token_json")
export AZURE_TENANT_ID
fi
fi
and it worked.
This pull request introduces support for deploying the MCP server with Microsoft Entra ID (Azure AD) OAuth authentication, alongside infrastructure and documentation updates. The main changes add pre- and post-provision hooks to automate Azure app registration, update Bicep templates and environment handling for OAuth, and provide comprehensive documentation for using the new authentication option.
Entra OAuth Proxy Integration
infra/auth_init.sh,infra/auth_init.ps1) and post-provision (infra/auth_update.sh,infra/auth_update.ps1) hooks to automate creation and configuration of Azure/Entra ID app registrations for FastMCP OAuth Proxy. These scripts call new Python scripts to handle Azure resource setup. [1] [2] [3] [4]infra/fastmcp_auth_init.py(pre-provision): creates or updates the Entra ID app registration, sets up redirect URIs (including for VS Code), exposes an API scope, generates a client secret, and stores credentials in azd environment variables.infra/fastmcp_auth_update.py(post-provision): updates the app registration with the actual deployed server URL as an OAuth redirect URI after deployment..vscode/mcp.jsonto fix minor JSON formatting.Infrastructure and Bicep Updates
infra/main.bicepfor enabling Entra OAuth Proxy (useFastMcpAuth), and for passing the app registration client ID and secret. Also added new CosmosDB containers for OAuth client and user storage. [1] [2] [3]azure.yamlto wire up the new pre- and post-provision hooks for both POSIX and Windows environments.Documentation
README.mdwith detailed instructions for deploying with Entra OAuth Proxy, including deployment steps, environment variables, local testing, VS Code integration, and troubleshooting. [1] [2]AGENTS.mdinstructions for properly adding new azd environment variables, including Bicep and script updates.These changes collectively make it much easier to deploy the MCP server with secure Microsoft Entra ID authentication, automate the required Azure setup, and provide clear guidance for developers.