Skip to main content
Glama
JanneMattila

Azure Databricks Jobs MCP Server

by JanneMattila

Azure Databricks Jobs MCP Server

CAUTION

This is asample 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:

  • Users, including GitHub Copilot in VS Code, call Databricks as the signed-in user through the On-Behalf-Of (OBO) flow. Their Databricks permissions apply.

  • Machines (service principals or managed identities) call without an 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.

Tools

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.

Related MCP server: Databricks MCP Server

How authentication works

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.

Delegated users (OBO)

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)
  1. Copilot reads this server's Protected Resource Metadata at /.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.

  2. Copilot performs the OAuth flow directly with Entra and receives an access token with audience = this app's API (api://<client_id>).

  3. Copilot calls the MCP endpoint with that token. The server validates the JWT locally against Entra's published JWKS (signature, issuer, audience, and required scope).

  4. Each tool takes the validated inbound token and runs the OBO exchange (MSAL) for scope 2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default (the Azure Databricks resource).

  5. The resulting token is used as Authorization: Bearer against the Jobs API.

Machines (app-only)

SP / managed identity --Entra token for MCP API--> MCP server
  MCP server --client credentials--> Entra --Databricks token--> Jobs API
                                                     (acts as MCP server SP)
  1. The caller obtains an app-only token for this MCP API, with assigned app roles in the roles claim, and sends it as a bearer token to /mcp.

  2. The server validates the JWT against Entra's JWKS, issuer, and accepted 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.

  3. A caller is treated as a machine when idtyp is app, or when it has roles but no delegated scp/scope. Each tool checks its specific machine role before acquiring a Databricks token or calling the Jobs API.

  4. For an authorized machine caller, MSAL acquires a Databricks token through 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.

Server credential: client secret or managed identity

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:

  • Client secret — set AZURE_CLIENT_SECRET. Simplest for local development.

  • Managed identity (recommended on Azure) — set 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.

Alternative: calling Databricks directly with managed identity

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.

Prerequisites

  • Python 3.12+ and uv

  • An Azure Databricks workspace, with access granted to users for OBO and/or the MCP server's app-registration service principal for machine calls

  • An Entra app registration (see below)

Entra app registration requirements

This server assumes the app registration already exists. Configure it as follows.

  1. Expose an API

    • Application ID URI: keep the default api://<client_id>.

    • Add a scope, e.g. jobs (matches MCP_REQUIRED_SCOPES).

  2. Certificates & secrets → create a client secret (store it in .env), or use a managed identity when deployed (see below). Both outbound authentication paths reuse this credential.

  3. For delegated user access, API permissions → add AzureDatabricks → user_impersonation (Delegated), then Grant admin consent. This permission lets the OBO exchange succeed.

Delegated client access to the API

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.

Machine client access to the API

  1. On the MCP server's app registration, create enabled App roles with Allowed member types = Applications (allowedMemberTypes: ["Application"]):

    • Value Jobs.Read for the read tools.

    • Value Jobs.Run for run_now.

    • If you customize the role values, set the matching MCP_MACHINE_READ_ROLE / MCP_MACHINE_RUN_ROLE environment variables.

  2. Grant roles to the calling principal:

    • For an app-registration caller, add the MCP API's Application permissions under the caller's API permissions, select the appropriate roles, and Grant admin consent.

    • For a managed-identity caller, an administrator assigns the roles to its 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.

    • Assign only Jobs.Read for read-only access; assign both roles for reading and triggering runs. Azure resource IAM roles are not these Entra app roles.

  3. Provision the MCP server's app-registration service principal (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.

Granting Databricks access for app-only authentication

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:

  • Server application (client) ID: <mcp-server-client-id>.

  • Workspace: 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:

  1. Open Settings > Identity and access.

  2. Next to Service principals, select Manage, then Add service principal.

  3. If the principal already exists in the Databricks account, select it and add it to this workspace. Otherwise, select Add new:

    • Management: Microsoft Entra ID managed.

    • Application ID: the server's AZURE_CLIENT_ID.

    • Name: a recognizable name, such as Azure Databricks MCP Server.

  4. Confirm the service principal is Active and has the Workspace access 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.

  • Jobs returned: workspace access and job permissions are working.

  • Successful but empty job list: check Can View on a known job in the configured workspace.

  • Databricks API error 403: User not authorized: verify the server's exact 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.

Obtaining a machine token

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.

Testing the live server-to-server flow with PowerShell

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:

  • PowerShell 7 and Az.Accounts 2.19.0 or newer.

  • A caller app registration with a client secret and the MCP API's Jobs.Read application permission granted with admin consent, as described above.

  • The deployed server must support machine authentication, and its own 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

Setup

# 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-mcp

The server listens on http://127.0.0.1:8000/mcp by default.

Configuration (.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).

Using from VS Code Copilot

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.

Container image

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:latest

Deploying to Azure Container Apps

Requires 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 tsv

The 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 tsv

The 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.)

  • No AZURE_CLIENT_SECRET is needed. Both outbound token flows use the FIC instead.

  • AZURE_MANAGED_IDENTITY_CLIENT_ID selects the user-assigned identity. Omit it only if you use the Container App's system-assigned identity (and set the FIC subject to that identity's principal ID instead).

  • Token validation is stateless, so the app scales to multiple replicas without extra configuration.

  • For delegated users, the app registration still needs the AzureDatabricks → user_impersonation permission, which powers the OBO exchange.

  • For machine callers, the app-registration SP needs workspace/job permissions 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/mcp is 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"
    }
  }
}

Notes

  • Inspired by the (stdio-based) databricks-solutions/ai-dev-kit; this server is intentionally HTTP and jobs-only, with user OBO and machine client-credentials authentication.

Troubleshooting authentication (401 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:

  • Issuer / audience mismatch is logged at WARNING, e.g. Bearer token rejected ... audience mismatch (got ..., expected ...).

  • Signature / JWKS / format failures are logged at DEBUG, e.g. Token validation failed: JWT signature/format invalid.

  • Neither all required scopes nor any known app role is logged by this 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. The Rejected bearer token: line tells you why:

  • nonce=PRESENT → the client obtained a nonce-protected Microsoft token (issued when the token's aud is 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>/jobs for users, or api://<client_id>/.default for 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 --follow

Common causes when calling from Microsoft Foundry:

  • Audience mismatch — Foundry obtained a token whose 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.

  • Issuer mismatch — the token must be a v2.0 token (https://login.microsoftonline.com/<tenant-id>/v2.0).

  • Missing scopes/roles — a delegated client normally needs all configured 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).

  • Wrong base URL — if MCP_BASE_URL does not match the public FQDN, clients may request a token for the wrong resource. On Container Apps it is auto-derived; override only for custom domains.

Tool authorization and outbound failures

  • Missing tool role — an app-only token can pass the inbound gate but fail a 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.

  • Role assignments recently changed — obtain a fresh caller token and check its roles. Managed-identity token caching can delay visibility of assignments.

  • Client-credentials token acquisition failed — check the MCP server's app-registration credential or federated credential, not just the caller's credentials. The machine path does not use OBO.

  • Databricks denies access — verify the identity used by the selected path: 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=INFO

Authentication verification checklist

These are recommended regression checks, not a record of completed verification. The repository currently has no automated authentication tests.

  • Verify delegated tokens with all required scopes are accepted and all five tools still use OBO and the user's Databricks permissions.

  • Verify app-only tokens with a known role are accepted, and tokens with neither required scopes nor recognized roles are rejected.

  • Verify machine detection for idtyp=app and roles-only tokens, and delegated detection for scope-only tokens.

  • With Jobs.Read only, verify all four read tools succeed and run_now is blocked before any Databricks call.

  • With Jobs.Run only, verify run_now is authorized but read tools are blocked. Use a designated test job: this check triggers a real run.

  • With both roles, verify reading and triggering runs work, and Databricks attributes machine requests to the MCP app-registration SP.

  • Verify configured custom role names and both server credential options (client secret and managed-identity federation) in the intended environment.

Example responses

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

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants like Claude to interact with Databricks workspaces through secure OAuth authentication. Supports custom prompts, tools for cluster management, SQL execution, and job operations via the Databricks SDK.
    19
    -
  • F
    license
    Not graded
    quality
    Not graded
    maintenance
    Enables AI assistants like Claude to interact with Databricks workspaces through secure OAuth authentication. Supports custom prompts, tools for cluster management, SQL execution, and job operations via the Databricks SDK.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants like Claude to interact with Databricks workspaces through secure OAuth authentication. Supports custom prompts, tools for cluster management, SQL execution, and job operations via the Databricks SDK.
    -