Azure Databricks Jobs Mcp Server — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Azure Databricks Jobs Mcp Server (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
[!CAUTION] This is a sample implementation provided for educational purposes. It is not intended for production use without proper security review, testing, and hardening. Use at your own risk.
A Python MCP server that exposes a limited set of Azure Databricks Jobs capabilities. It authenticates callers with Microsoft Entra ID and supports two authentication paths:
signed-in user through the On-Behalf-Of (OBO)** flow. Their Databricks permissions apply.
interactive user. The server uses client credentials to call Databricks as the MCP server's app-registration service principal.
This enables unattended clients to inspect job status, retrieve outputs, and optionally trigger runs. Scheduling, polling, and analysis remain the client's responsibility.
Built with FastMCP and managed with uv.
| Tool | Databricks endpoint | Description | Required machine app role |
|---|---|---|---|
list_jobs | GET /api/2.2/jobs/list | List jobs (helps discover job IDs). | Jobs.Read |
list_runs | GET /api/2.2/jobs/runs/list | List job runs, filter by job / state. | Jobs.Read |
get_run | GET /api/2.2/jobs/runs/get | Get details/status of a run. | Jobs.Read |
get_run_output | GET /api/2.2/jobs/runs/get-output | Get a task run's output. | Jobs.Read |
run_now | POST /api/2.2/jobs/run-now | Trigger a new run of a job. | Jobs.Run |
Only these jobs-related tools are exposed — no other Databricks capabilities. The role names above are defaults and can be configured. Jobs.Run alone permits run_now; it does not grant read access. Assign both roles when a machine must trigger runs and inspect their results. Delegated users do not need these app roles; their required API scopes and Databricks permissions apply instead.
This server is an OAuth 2.0 protected resource (RFC 9728). It does not run any login UI of its own — it validates the bearer tokens it receives and tells clients which authorization server (Entra) issues them.
VS Code Copilot --OAuth--> Microsoft Entra ID --token--> Copilot --Bearer--> MCP server --OBO--> Databricks
(user signs in directly) (issues access token) (validates JWT) (acts as user)/.well-known/oauth-protected-resource/mcp (RFC 9728 §3.1 appends the /mcp resource path after /.well-known/oauth-protected-resource), which names Entra as the authorization server and the scope to request.
with audience = this app's API (api://<client_id>).
against Entra's published JWKS (signature, issuer, audience, and required scope).
scope 2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default (the Azure Databricks resource).
Authorization: Bearer against the Jobs API.SP / managed identity --Entra token for MCP API--> MCP server
MCP server --client credentials--> Entra --Databricks token--> Jobs API
(acts as MCP server SP)roles in the roles claim, and sends it as a bearer token to /mcp.
audiences (api://<client_id> or <client_id>). Its central gate accepts tokens carrying all configured delegated scopes or at least one recognized app role. Tokens satisfying neither condition are rejected.
idtyp is app, or when it has rolesbut no delegated scp/scope. Each tool checks its specific machine role before acquiring a Databricks token or calling the Jobs API.
client credentials using the server's app registration and existing credential. MSAL caches the token internally. App-only tokens never use OBO, and the inbound token is never forwarded to Databricks.
There is no feature flag for machine access and no role is assigned by default. Control access through app-role assignments and the server principal's Databricks permissions.
Shared Databricks identity: all machine callers act as the MCP server's app-registration service principal, not as the calling SP or managed identity. Databricks audit logs therefore attribute these requests to the MCP server SP. Grant that principal only the job permissions it needs. Per-machine-caller Databricks identities are not supported.
Token validation needs no secret — the server only fetches Entra's public JWKS. In this implementation, both the On-Behalf-Of exchange and client-credentials token acquisition use the app registration as a confidential client, so the server must prove that app's identity. There are two ways to do that:
AZURE_CLIENT_SECRET. Simplest for localdevelopment.
AZURE_USE_MANAGED_IDENTITY=true.The app authenticates with a federated identity credential: a managed identity mints a short‑lived assertion for the api://AzureADTokenExchange audience, which Entra trusts in place of a secret. No secret is stored or rotated. See Deploying to Azure Container Apps.
Why both an app registration and a managed identity? In this implementation, the managed identity replaces the client secret, not the app registration — they do different jobs: - The app registration exposes the API/scopes and machine app roles, and holds the delegated `user_impersonation` permission that makes the OBO ("act as the user") exchange possible. It also identifies the shared SP used for machine calls to Databricks. Managed identities cannot expose this API or perform OBO themselves. - The managed identity is used as a secretless way to prove the app registration's identity during either outbound flow (via the federated credential).
>
So you can drop the secret, but not the app registration in this implementation. A machine caller's managed identity is separate from the identity used to authenticate the server's outbound token requests.
Azure Databricks also supports direct managed-identity authentication. For a machine-only outbound request, the server could obtain a Databricks-audience token using its own managed identity and call the Jobs API as that identity. This connection does not require a separate app registration, client secret, or federated identity credential.
The managed identity must be assigned to the Databricks workspace and granted the required job permissions. A managed identity is backed by an Entra service principal, and Databricks treats it as a service principal; no separate app-registration service principal is needed for this outbound connection. See Authenticate with Azure managed identities.
| Outbound machine flow | Identity Databricks sees | Federated identity credential |
|---|---|---|
| Current managed-identity mode: identity authenticates the app registration | MCP app-registration SP | Required |
| Alternative: identity obtains a Databricks token directly | Server's managed identity | Not required |
These outbound choices are separate from caller-to-MCP authentication. This server's app registration also exposes the MCP API's scopes and app roles and identifies its accepted token audience. It supports the delegated user/OBO path as well. Using direct managed identity for machine calls would not remove those inbound API and OBO requirements.
Direct managed identity for Databricks is not currently implemented here. AZURE_USE_MANAGED_IDENTITY=true selects federated authentication of the app registration, not direct Databricks access. Adopting the alternative would require changing the outbound machine token acquisition and granting Databricks permissions to the managed identity instead of relying on the app-registration SP's grants. It would still use one shared outbound identity for all machine callers, not preserve each calling machine's identity.
MCP server's app-registration service principal for machine calls
This server assumes the app registration already exists. Configure it as follows.
api://<client_id>.jobs (matches MCP_REQUIRED_SCOPES)..env),or use a managed identity when deployed (see below). Both outbound authentication paths reuse this credential.
then Grant admin consent. This permission lets the OBO exchange succeed.
Because clients authenticate directly with Entra (which has no Dynamic Client Registration), the MCP client needs to be a client Entra recognizes and must be allowed to request the api://<client_id>/jobs scope. In VS Code, Copilot performs the Entra sign-in when you first use a tool and requests the scope advertised in the Protected Resource Metadata. Ensure user (or admin) consent is granted for the client to access this API's jobs scope in your tenant.
Allowed member types = Applications (allowedMemberTypes: ["Application"]):
Jobs.Read for the read tools.Jobs.Run for run_now.MCP_MACHINE_READ_ROLE / MCP_MACHINE_RUN_ROLE environment variables.
under the caller's API permissions, select the appropriate roles, and Grant admin consent.
service principal through Microsoft Graph appRoleAssignments. The assignment's resource is the MCP API's enterprise application/service principal, not the Databricks service principal. See Microsoft's managed-identity app-role assignment guide.
Jobs.Read for read-only access; assign both roles for readingand triggering runs. Azure resource IAM roles are not these Entra app roles.
(AZURE_CLIENT_ID) in the Databricks workspace. Grant Can View on the relevant jobs and Can Manage Run only on jobs it should be able to trigger. Provisioning the caller's identity or the server's federated managed identity instead does not grant the outbound MCP app principal access. Follow the Databricks access steps below.
App-only access has two separate authorization boundaries:
Caller service principal --MCP API app role--> MCP server
MCP server service principal --workspace access + job permissions--> Databricks| Identity | Required access |
|---|---|
Caller used by scripts/auth-app-only.ps1 (-ClientId) | MCP API application permission Jobs.Read, with admin consent; also Jobs.Run if triggering jobs |
MCP server app-registration service principal (AZURE_CLIENT_ID) | Membership in the target Databricks workspace and permissions on the relevant jobs |
Server's federated managed identity (AZURE_MANAGED_IDENTITY_CLIENT_ID) | Authenticates the MCP server app registration through its federated credential; it is not the Databricks caller in this implementation |
#### 1. Identify the server application and workspace
Use the deployed server's `AZURE_CLIENT_ID` and `DATABRICKS_HOST`, not the test caller's client ID:
<mcp-server-client-id>.https://<databricks-workspace-host>.Substitute your deployment's settings. Databricks needs the application/client ID, not the Entra app-registration object ID or enterprise application object ID.
#### 2. Add the server service principal to the workspace
Sign in to the target Databricks workspace as a workspace administrator:
it to this workspace. Otherwise, select Add new:
AZURE_CLIENT_ID.Azure Databricks MCP Server.entitlement.
If using the Databricks account console instead, add or select the Microsoft Entra ID managed principal under User management > Service principals, then assign it through Workspaces > target workspace > Permissions > Add permissions. Creating the principal at account level alone does not grant workspace access.
See Manage service principals for the workspace and account-console procedures.
#### 3. Grant permissions on the relevant jobs
As a job owner or administrator, open Jobs & Pipelines, select a job, open Permissions, and add the server service principal:
| MCP operation | Databricks job permission |
|---|---|
list_jobs, list_runs, get_run, get_run_output | Can View |
run_now | Can Manage Run |
Start with Can View on one known job for the read-only test, then grant access to additional jobs as needed. Do not grant workspace administrator, job ownership, or unrestricted cluster creation just to read existing jobs. You do not need to change the job's Run as identity: permission to invoke an existing job is separate from the identity used to execute its tasks.
Azure portal IAM roles such as Contributor and the caller's MCP Jobs.Read permission do not replace these Databricks permissions. Likewise, the delegated AzureDatabricks/user_impersonation API permission used for OBO does not grant the server service principal workspace or job access.
See Databricks job ACLs.
#### 4. Verify the app-only path
Run the PowerShell test again using the same caller credentials. These access changes normally do not require redeploying the Container App; allow a short time for propagation.
configured workspace.
application ID, active status, workspace assignment, and Workspace access entitlement, then check job permissions.
MCP initialization and tools/list do not call Databricks. Seeing the tool names only confirms access to the MCP server; a successful list_jobs invocation tests the outbound client-credentials path.
Use the caller's credentials with the tenant's Entra v2 token endpoint (https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token), the client_credentials grant, and scope `api://<mcp-server-client-id>/.default`. A managed-identity caller uses its Azure identity token API for that same MCP API resource (or the /.default scope when using a scope-based SDK).
Configure the MCP client to attach the resulting token as Authorization: Bearer <access-token> when connecting to /mcp, and to refresh it before expiry. Do not send a token for Databricks, Microsoft Graph, or api://AzureADTokenExchange to the MCP endpoint. The latter audience is used only for the server's federated credential.
The token must have the configured tenant's v2 issuer, an accepted MCP API audience, and the assigned roles. Configure the MCP API app registration's api.requestedAccessTokenVersion to 2 to request v2 access tokens; using the v2 token endpoint alone does not determine the access-token version.
Protected Resource Metadata continues to advertise delegated scopes such as api://<client_id>/jobs; it does not assign machine roles or replace this onboarding. There is no Dynamic Client Registration or automatic machine login provided by this server.
auth-app-only.ps1 signs in as a calling service principal, requests a token for the MCP API, completes the MCP initialization handshake, lists the available tools, and calls `list_jobs` with a limit of five. It does not start Databricks jobs or change Azure resources or role assignments.
Prerequisites:
Az.Accounts 2.19.0 or newer.application permission granted with admin consent, as described above.
app-registration service principal must have Databricks workspace/job access.
# Use the CALLER's application/client ID, not the MCP server's app ID.
$authParameters = @{
ClientId = '<caller-application-id>'
TenantId = '<tenant-id>'
McpServerClientId = '<mcp-server-client-id>'
McpEndpoint = 'https://<container-app-fqdn>/mcp'
}
# The script prompts for the caller's secret as a SecureString.
.\scripts\auth-app-only.ps1 @authParameters
# Optionally provide a SecureString obtained from a prompt or secret store.
$secret = Read-Host 'Caller client secret' -AsSecureString
.\scripts\auth-app-only.ps1 @authParameters -ClientSecret $secret -Limit 10-ClientId, -TenantId, -McpServerClientId, and -McpEndpoint are required. There are no environment-specific defaults; replace the placeholders with your own values. The token audience is `api://<McpServerClientId>`, not Databricks or ARM. The caller needs Entra app-role permissions, not an Azure subscription IAM role.
Authentication runs in a separate PowerShell job with Az context autosave disabled, leaving your existing CLI and PowerShell logins unchanged. Secrets and tokens are not printed or written to files. Both JSON and SSE MCP responses are supported, including session IDs and the negotiated protocol version. The script attempts to close the MCP session on exit.
Success prints the tool result, which can contain live job metadata; handle it accordingly. HTTP authentication failures, JSON-RPC errors, and MCP tool errors terminate the script. An empty jobs list can be a successful call. A successful handshake alone does not establish that the server-to-Databricks client-credentials flow works; the list_jobs call tests that path.
The mocked tests do not sign in or contact Azure and require Pester 5:
Invoke-Pester -Path .\tests\auth-app-only.Tests.ps1# 1. Install dependencies
uv sync
# 2. Create your local config
cp .env.example .env # then fill in the values
# 3. Run the server (Streamable HTTP)
uv run databricks-jobs-mcpThe server listens on http://127.0.0.1:8000/mcp by default.
.env)| Variable | Required | Description |
|---|---|---|
AZURE_TENANT_ID | yes | Entra tenant (directory) ID. |
AZURE_CLIENT_ID | yes | App registration (client) ID. |
AZURE_CLIENT_SECRET | conditional | App registration client secret. Required unless AZURE_USE_MANAGED_IDENTITY=true. |
AZURE_USE_MANAGED_IDENTITY | no (false) | Authenticate both outbound token flows with a managed identity federated credential instead of a secret. |
AZURE_MANAGED_IDENTITY_CLIENT_ID | no | Client ID of the user-assigned managed identity. Omit for the system-assigned identity. |
MCP_BASE_URL | no (http://localhost:8000) | Public base URL; published as the resource identifier in Protected Resource Metadata. On Azure Container Apps it is auto-derived by combining the injected CONTAINER_APP_NAME and CONTAINER_APP_ENV_DNS_SUFFIX when left unset. |
MCP_HOST | no (127.0.0.1) | Bind interface. |
MCP_PORT | no (8000) | Bind port. |
MCP_REQUIRED_SCOPES | no (jobs) | Required delegated API scope name(s), comma separated. All must be present for scope-based acceptance; machine callers use app roles instead. |
MCP_MACHINE_READ_ROLE | no (Jobs.Read) | App-role value required for machine callers of list_jobs, list_runs, get_run, and get_run_output. Must match the Entra role value. |
MCP_MACHINE_RUN_ROLE | no (Jobs.Run) | App-role value required for machine callers of run_now. Does not imply read access. Must match the Entra role value. |
DATABRICKS_HOST | yes | Workspace URL, e.g. https://adb-xxxx.azuredatabricks.net. |
DATABRICKS_API_VERSION | no (2.2) | Jobs API version (use 2.1 only if needed). |
LOG_LEVEL | no (INFO) | Logging verbosity (DEBUG, INFO, WARNING, ERROR). Set to DEBUG to log why a token is rejected (issuer/audience mismatch, missing scopes/roles, signature/JWKS failures). |
Secrets live only in .env (gitignored).
A repo-level .mcp.json is included:
{
"mcpServers": {
"databricks-jobs": { "type": "http", "url": "http://localhost:8000/mcp" }
}
}Start the server (uv run databricks-jobs-mcp), then open the repo in VS Code with Copilot. On first use of a tool, Copilot opens the Entra sign-in flow; afterwards the jobs tools are available and run as you.
A Dockerfile is included. It builds a slim, multi-stage image with uv, runs as a non-root user, and binds the HTTP transport to 0.0.0.0:8000 (via MCP_HOST / MCP_PORT) so it works behind an ingress.
# Build
docker build -t databricks-jobs-mcp:latest .
# Run locally (the container listens on 8000)
docker run --rm -p 8000:8000 --env-file .env databricks-jobs-mcp:latestRequires the Azure CLI with the containerapp extension and Docker. Replace the placeholder values below.The app authenticates to Entra with a managed identity — no client secret is stored or rotated. It uses a federated identity credential (FIC): a user-assigned managed identity mints a short-lived assertion that Entra trusts in place of a secret. This credential supports both outbound flows. The deployment commands below do not create app roles, assign caller permissions, or provision the app-registration SP in Databricks; complete machine onboarding separately when enabling machine callers.
# 0. Variables
RG="<resource-group-name>"
LOCATION="<azure-region>"
ACR="<unique-registry-name>" # must be globally unique
ENV="<container-app-environment-name>"
APP="<container-app-name>"
UAMI="<managed-identity-name>"
CLIENT_ID=<client-id> # App REGISTRATION client ID (= AZURE_CLIENT_ID), NOT the managed identity
TENANT_ID=<tenant-id> # Entra tenant (directory) ID
# 1. Resource group + container registry
az group create -n $RG -l $LOCATION
az acr create -n $ACR -g $RG --sku Basic
# 2. Build & push the image (uses the included Dockerfile)
az acr build -r $ACR -t $APP:latest .
# 3. Container Apps environment
az containerapp env create -n $ENV -g $RG -l $LOCATION
# 4. Create a user-assigned managed identity and read its IDs
az identity create -n $UAMI -g $RG -l $LOCATION
MI_CLIENT_ID=$(az identity show -n $UAMI -g $RG --query clientId -o tsv)
MI_PRINCIPAL_ID=$(az identity show -n $UAMI -g $RG --query principalId -o tsv)
MI_RESOURCE_ID=$(az identity show -n $UAMI -g $RG --query id -o tsv)
# Let the managed identity pull images from the registry (no ACR admin user)
az role assignment create \
--assignee-object-id $MI_PRINCIPAL_ID --assignee-principal-type ServicePrincipal \
--role AcrPull \
--scope $(az acr show -n $ACR -g $RG --query id -o tsv)
# 5. Configure the app registration to trust the managed identity (FIC).
# --id is the app REGISTRATION (= AZURE_CLIENT_ID); the subject is the
# managed identity's principal ID, and the audience is fixed.
az ad app federated-credential create \
--id $CLIENT_ID \
--parameters '{
"name": "databricks-mcp-mi",
"issuer": "https://login.microsoftonline.com/'$TENANT_ID'/v2.0",
"subject": "'$MI_PRINCIPAL_ID'",
"audiences": ["api://AzureADTokenExchange"]
}'
# 6. Deploy the app with the managed identity assigned (no client secret)
az containerapp create \
-n $APP -g $RG --environment $ENV \
--image $ACR.azurecr.io/$APP:latest \
--registry-server $ACR.azurecr.io \
--registry-identity $MI_RESOURCE_ID \
--target-port 8000 --ingress external \
--min-replicas 1 \
--user-assigned $MI_RESOURCE_ID \
--env-vars \
AZURE_TENANT_ID=$TENANT_ID \
AZURE_CLIENT_ID=$CLIENT_ID \
AZURE_USE_MANAGED_IDENTITY=true \
AZURE_MANAGED_IDENTITY_CLIENT_ID=$MI_CLIENT_ID \
DATABRICKS_HOST=https://adb-xxxx.azuredatabricks.net
# 7. Fetch the app's FQDN
az containerapp show -n $APP -g $RG --query properties.configuration.ingress.fqdn -o tsvThe same, in PowerShell:
# 0. Variables
$RG = "<resource-group-name>"
$LOCATION = "<azure-region>"
$ACR = "<unique-registry-name>" # must be globally unique
$ENVNAME = "<container-app-environment-name>"
$APP = "<container-app-name>"
$UAMI = "<managed-identity-name>"
$TENANT_ID = "<tenant-id>" # Entra tenant (directory) ID
$CLIENT_ID = "<client-id>" # App REGISTRATION client ID (= AZURE_CLIENT_ID), NOT the managed identity
# 1. Resource group + container registry
az group create -n $RG -l $LOCATION
az acr create -n $ACR -g $RG --sku Basic
# 2. Build & push the image (uses the included Dockerfile)
az acr build -r $ACR -t "$($APP):latest" .
# 3. Container Apps environment
az containerapp env create -n $ENVNAME -g $RG -l $LOCATION
# 4. Create a user-assigned managed identity and read its IDs
az identity create -n $UAMI -g $RG -l $LOCATION
$MI_CLIENT_ID = az identity show -n $UAMI -g $RG --query clientId -o tsv
$MI_PRINCIPAL_ID = az identity show -n $UAMI -g $RG --query principalId -o tsv
$MI_RESOURCE_ID = az identity show -n $UAMI -g $RG --query id -o tsv
$ACR_ID = az acr show -n $ACR -g $RG --query id -o tsv
# Let the managed identity pull images from the registry (no ACR admin user)
az role assignment create `
--assignee-object-id $MI_PRINCIPAL_ID --assignee-principal-type ServicePrincipal `
--role AcrPull `
--scope $ACR_ID
# 5. Configure the app registration to trust the managed identity (FIC).
# The audience is fixed; the subject is the managed identity's principal ID.
@{
name = "databricks-mcp-mi"
issuer = "https://login.microsoftonline.com/$TENANT_ID/v2.0"
subject = $MI_PRINCIPAL_ID
audiences = @("api://AzureADTokenExchange")
} | ConvertTo-Json | Set-Content -Path fic.json -Encoding utf8
az ad app federated-credential create --id $CLIENT_ID --parameters '@fic.json'
# 6. Deploy the app with the managed identity assigned (no client secret)
az containerapp create `
-n $APP -g $RG --environment $ENVNAME `
--image "$($ACR).azurecr.io/$($APP):latest" `
--registry-server "$($ACR).azurecr.io" `
--registry-identity $MI_RESOURCE_ID `
--target-port 8000 --ingress external `
--min-replicas 1 `
--user-assigned $MI_RESOURCE_ID `
--env-vars `
AZURE_TENANT_ID=$TENANT_ID `
AZURE_CLIENT_ID=$CLIENT_ID `
AZURE_USE_MANAGED_IDENTITY=true `
AZURE_MANAGED_IDENTITY_CLIENT_ID=$MI_CLIENT_ID `
DATABRICKS_HOST=https://adb-xxxx.azuredatabricks.net
# 7. Fetch the app's FQDN
az containerapp show -n $APP -g $RG --query properties.configuration.ingress.fqdn -o tsvThe app's public FQDN is shown after containerapp create (or via az containerapp show -n $APP -g $RG --query properties.configuration.ingress.fqdn -o tsv).
After deploying:
MCP_BASE_URL is auto-derived from the stable Container Apps FQDN(CONTAINER_APP_NAME + CONTAINER_APP_ENV_DNS_SUFFIX), so you don't need to set it. (To override the URL, e.g. a custom domain, set MCP_BASE_URL explicitly.)
AZURE_MANAGED_IDENTITY_CLIENT_ID selects the user-assigned identity. Omit it onlyif you use the Container App's system-assigned identity (and set the FIC subject to that identity's principal ID instead).
extra configuration.
AzureDatabricks → `user_impersonation` permission, which powers the OBO exchange.
in Databricks, and callers need the MCP API's app roles. The federated managed identity does not become the Databricks caller.
https://<container-app-fqdn>/.well-known/oauth-protected-resource/mcpis the Protected Resource Metadata endpoint (RFC 9728). Copilot uses it to learn this server's resource identifier, the authorization server (Entra) to obtain tokens from, and the scope to request.
Here is an example of the metadata document returned by the deployed server:
{
"resource": "https://<container-app-fqdn>/mcp",
"authorization_servers": [
"https://login.microsoftonline.com/<tenant-id>/v2.0"
],
"scopes_supported": [
"api://<client-id>/jobs"
],
"bearer_methods_supported": [
"header"
],
"resource_name": "Azure Databricks Jobs"
}To use that in Copilot, you can use the following .mcp.json:
{
"mcpServers": {
"databricks-jobs-azure": {
"type": "http",
"url": "https://<container-app-fqdn>/mcp"
}
}
}this server is intentionally HTTP and jobs-only, with user OBO and machine client-credentials authentication.
invalid_token)A 401 Unauthorized with {"error": "invalid_token"} means the server rejected the bearer token during JWT validation or the central scope/role gate — it never reached a tool, so neither outbound token flow is involved yet. By default the logs only show the generic Auth error returned: invalid_token. Set `LOG_LEVEL=DEBUG` to make FastMCP's JWTVerifier log the precise reason:
WARNING, e.g.Bearer token rejected ... audience mismatch (got ..., expected ...).
DEBUG, e.g.Token validation failed: JWT signature/format invalid.
server at DEBUG, with the required and received scope/role values.
When the base JWT verifier rejects a token, the server also logs (at DEBUG) a decoded — but unverified — summary of the offending token so you can see the actual claims without capturing the bearer token by hand:
Rejected bearer token: header={alg=..., kid=..., typ=..., nonce=...} \
claims={iss=..., aud=..., appid/azp=..., ver=..., scp=..., exp=...}The server also logs (at DEBUG) the exact values it validates against at startup:
Auth config: base_url=... issuer=... audiences=[...] required_scopes=[...] jwks_uri=...`JWT signature/format invalid` with a successful JWKS fetch? The token reached signature verification but failed. TheRejected bearer token:line tells you why: - `nonce=PRESENT` → the client obtained a nonce-protected Microsoft token (issued when the token'saudis a Microsoft first-party resource, not this API). These are intentionally not validatable by third parties. The client must request a token for this API (api://<client_id>/jobsfor users, orapi://<client_id>/.defaultfor client credentials), not for Microsoft Graph or another resource. - `not a well-formed JWS` → the client sent an opaque/garbled token, not a JWT.
Enable it on the deployed Container App (this restarts the revision):
az containerapp update -n $APP -g $RG --set-env-vars LOG_LEVEL=DEBUG
# then stream the logs and reproduce from Foundry
az containerapp logs show -n $APP -g $RG --followCommon causes when calling from Microsoft Foundry:
aud is not this app(api://<client_id> / <client_id>). For delegated clients, the metadata at /.well-known/oauth-protected-resource/mcp must advertise this server's resource and the api://<client_id>/jobs scope so the client requests a token for this API. For machine clients, configure the MCP API resource as described in Obtaining a machine token.
(https://login.microsoftonline.com/<tenant-id>/v2.0).
MCP_REQUIRED_SCOPES (default jobs). An app-only caller instead needs at least one recognized role in roles (Jobs.Read or Jobs.Run by default).
MCP_BASE_URL does not match the public FQDN, clients mayrequest a token for the wrong resource. On Container Apps it is auto-derived; override only for custom domains.
tool call with Caller is missing the required app role '...'. For example, Jobs.Read does not allow run_now, and Jobs.Run does not allow read tools. This is a tool authorization error, not an inbound 401 invalid_token. No Databricks request is made for that call.
its roles. Managed-identity token caching can delay visibility of assignments.
app-registration credential or federated credential, not just the caller's credentials. The machine path does not use OBO.
the signed-in user for OBO, or the MCP app-registration SP for machine calls. An MCP role assignment does not itself grant Databricks workspace/job access. For app-only 403: User not authorized errors, follow Granting Databricks access for app-only authentication.
Turn debug logging back off once diagnosed:
az containerapp update -n $APP -g $RG --set-env-vars LOG_LEVEL=INFOThese are recommended regression checks, not a record of completed verification. The repository currently has no automated authentication tests.
tools still use OBO and the user's Databricks permissions.
required scopes nor recognized roles are rejected.
idtyp=app and roles-only tokens, and delegateddetection for scope-only tokens.
Jobs.Read only, verify all four read tools succeed and run_now isblocked before any Databricks call.
Jobs.Run only, verify run_now is authorized but read tools are blocked.Use a designated test job: this check triggers a real run.
attributes machine requests to the MCP app-registration SP.
(client secret and managed-identity federation) in the intended environment.
Copilot summarizing a failed job run (list_runs / get_run):
Copilot showing details of a failed job run
Copilot explaining why the run failed (get_run_output):
Copilot explaining the run output and the failing SQL query
Foundry showing the job run output in a notebook (get_run_output):
Foundry showing the run output
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.