Skip to main content
Glama
README.md
# adf-mcp-server

Read-only MCP (Model Context Protocol) server for Azure Data Factory
monitoring and root-cause analysis, built for use from VS Code / Claude Code.

**Status: Step 1 (skeleton + health check).** No Azure connectivity yet -
that's added in Step 2 (auth) and Step 3 (ADF tools).

## Requirements

- Python 3.11+
- One of:
  - An Azure AD App Registration (Service Principal) with **Reader** role on
    the Data Factory resource(s), **or**
  - Your own Entra ID user account with **Reader** role on the same
    (see "Auth modes" below)

## Local setup

```bash
cd adf-mcp-server
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
cp .env.example .env
```

## Auth modes

This server supports two ways to authenticate to Azure, controlled by
`ADF_MCP_AUTH_MODE` in `.env`. Both go through the same `check_auth` /
`get_credential()` code path - nothing else in the codebase cares which
one is active.

### Service Principal (`service_principal`)

Uses an app registration + client secret. Good for shared/CI use where no
human needs to be present.

```bash
az ad sp create-for-rbac \
  --name "adf-mcp-server-reader" \
  --role "Reader" \
  --scopes "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RG_NAME>/providers/Microsoft.DataFactory/factories/<FACTORY_NAME>"
```
Map the output into `.env`: `AZURE_CLIENT_ID` (appId), `AZURE_CLIENT_SECRET`
(password), `AZURE_TENANT_ID` (tenant).

### Interactive Browser (`interactive_browser`)

Uses **your own Entra ID sign-in** - the same identity you use to log into
the Azure portal - via a browser popup. No app registration or secret to
manage. Good fit for solo local/VS Code use.

```bash
ADF_MCP_AUTH_MODE=interactive_browser
AZURE_TENANT_ID=<your tenant ID>   # recommended - see note below
AZURE_SUBSCRIPTION_ID=<your subscription ID>
```

**RBAC note:** in this mode, Azure checks *your own user account's*
permissions, not an app registration's. Your account (or a group you're
in) needs **Reader** on the target Data Factory - ask whoever manages
RBAC to run:
```bash
az role assignment create --assignee <your-email-or-object-id> --role "Reader" \
  --scope "/subscriptions/<SUB_ID>/resourceGroups/<RG>/providers/Microsoft.DataFactory/factories/<FACTORY>"
```

**Behavior:**
- The first time any tool needs a token, a browser window opens for you
  to sign in.
- After that, the token is cached **in memory** for the life of the
  server process - no repeat popups until it restarts.
- By default, the token cache is also **persisted** (encrypted, via your
  OS's keychain/credential manager) so restarting the server usually
  doesn't prompt again either. Set `ADF_MCP_AZURE_USE_PERSISTENT_TOKEN_CACHE=false`
  in `.env` to disable this and always use an in-memory-only cache.
- Setting `AZURE_TENANT_ID` is recommended: without it, sign-in uses the
  multi-tenant "organizations" endpoint, which can prompt you to pick a
  tenant if your account belongs to more than one (e.g. a personal
  Microsoft account plus a work Entra ID tenant).

## Running the server

```bash
python -m adf_mcp.server
# or, after `pip install -e .`:
adf-mcp-server
```

The server communicates over **stdio** - running it directly in a terminal
will look like it hangs; that's expected, it's waiting for an MCP client
(VS Code extension, Claude Code, `mcp dev`, etc.) to connect via stdin/stdout.

### Configuring in VS Code

Point your MCP-capable extension's server config at:

```json
{
  "command": "python",
  "args": ["-m", "adf_mcp.server"],
  "cwd": "/absolute/path/to/adf-mcp-server"
}
```

Once connected:

1. Call `health_check` - should return `{"status": "ok", ...}` without
   touching Azure at all.
2. Call `check_auth` - this makes one real call to Azure AD to acquire an
   ARM token. Success looks like:
   ```json
   {"authenticated": true, "auth_mode": "service_principal", "token_expires_on": 1735000000}
   ```
   Failure returns a structured (not stack-trace) explanation, e.g. missing
   env vars or an invalid secret - see Troubleshooting below.
3. Call `list_factories` - this makes a real call to Azure Data Factory.
   Returns each factory's `resource_group`, which every other tool below
   needs as an input:
   ```json
   {"factories": [{"name": "shell-prod-adf", "resource_group": "rg-shell-prod", "location": "eastus"}]}
   ```

## Available tools (Step 3)

All tools are **read-only** - none of them can create, modify, trigger, or
delete anything in Azure Data Factory.

| Tool | Required args | Notes |
|---|---|---|
| `health_check` | — | No Azure calls |
| `check_auth` | — | Verifies the Service Principal only |
| `list_factories` | — | Start here - returns `resource_group` for each factory |
| `get_factory` | `resource_group`, `factory_name` | |
| `list_pipelines` | `resource_group`, `factory_name` | Lightweight: name + activity count/names |
| `get_pipeline` | `resource_group`, `factory_name`, `pipeline_name` | Full activity list for one pipeline |
| `list_pipeline_runs` | `resource_group`, `factory_name` | `start_time`/`end_time` optional (default: last 24h), plus optional `pipeline_name`/`status` filters. Messages truncated to 500 chars. |
| `get_pipeline_run` | `resource_group`, `factory_name`, `run_id` | Full, untruncated run detail - get `run_id` from `list_pipeline_runs` first |

Example RCA flow for an agent: `list_factories` → `list_pipeline_runs(status="Failed")`
→ `get_failed_activity_details(run_id=...)` for the error breakdown directly.

## Available tools (Step 4 additions)

| Tool | Required args | Notes |
|---|---|---|
| `list_activity_runs` | `resource_group`, `factory_name`, `run_id` | Full activity list for a run; `start_time`/`end_time` optional (default: last 7 days) |
| `get_failed_activity_details` | `resource_group`, `factory_name`, `run_id` | **The RCA tool** - only failed activities, with `error_code`/`message`/`failure_type` already extracted |
| `list_triggers` | `resource_group`, `factory_name` | All triggers + current runtime state (Started/Stopped) |
| `get_trigger_status` | `resource_group`, `factory_name`, `trigger_name` | One trigger's runtime state - catches "pipeline never ran because its trigger was stopped" |
| `list_trigger_runs` | `resource_group`, `factory_name` | `trigger_name` optional (omit for all triggers); default window last 7 days; optional `status` filter |
| `get_pipeline_run_tree` | `resource_group`, `factory_name`, `run_id` | **Master/child RCA tool** - recursively walks every Execute Pipeline activity to its child run, returning the full nested tree plus a flattened `failures` list across every level. `max_depth` (default 5) caps recursion. |

Full RCA flow for a failed pipeline: `list_pipeline_runs(status="Failed")` →
`get_failed_activity_details(run_id=...)` for the error, and separately
`get_trigger_status(trigger_name=...)` to rule out "it never even fired."

For a **master pipeline with child pipelines** (Execute Pipeline
activities), use `get_pipeline_run_tree(run_id=<master's run_id>)` instead
of chaining `get_failed_activity_details` manually level by level - it
walks the whole tree in one call and tells you exactly which child
pipeline (and which activity inside it) actually failed, however deep.

## Running tests

```bash
pip install -e ".[dev]" pytest-asyncio
pytest -v
```

## Project layout

See `src/adf_mcp/` - `server.py` (MCP transport), `config.py` (settings),
`logging_config.py` (structured logging). Domain logic and Azure
connectivity are added under `src/adf_mcp/domain/` from Step 3 onward.

## Troubleshooting

- **Client shows "server disconnected" immediately**: check `python -m
  adf_mcp.server` runs cleanly on its own first - a startup exception will
  kill the process before the client ever connects.
- **Client can't parse responses / garbled output**: something wrote to
  stdout other than the MCP protocol itself (e.g. a stray `print()`). All
  logging in this project goes to stderr for exactly this reason.
- **`check_auth` returns "Missing required Service Principal setting(s)"**:
  one of `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` is
  empty in `.env`. Note these three do NOT use the `ADF_MCP_` prefix.
- **`check_auth` returns "Azure authentication failed"**: usually an
  expired/rotated client secret, a disabled App Registration, or a
  tenant ID typo. Re-verify with `az ad sp show --id <AZURE_CLIENT_ID>`.
- **`ClientAuthenticationError: AADSTS7000215`**: invalid client secret -
  regenerate it in the App Registration and update `.env`.
- **A tool returns `{"error": "AZURE_SUBSCRIPTION_ID is not set..."}`**:
  add `AZURE_SUBSCRIPTION_ID` to `.env` - required for every ADF tool
  (not `check_auth`, which only needs tenant/client/secret).
- **A tool returns `{"error": "Azure API error (403): ..."}`**: the
  Service Principal lacks Reader access to that factory/resource group -
  re-check the `az ad sp create-for-rbac --role Reader --scopes ...`
  assignment from setup.
- **A tool returns `{"error": "Azure API error (404): ..."}`**: check the
  `resource_group`/`factory_name`/`pipeline_name` spelling - these are
  case-sensitive and must match exactly what `list_factories`/
  `list_pipelines` returned.
- **`get_failed_activity_details` returns an empty list but you know the
  pipeline failed**: the failure may be at the pipeline level (e.g. an
  invalid parameter) rather than any single activity - check the parent
  run's own `message` via `get_pipeline_run` instead.
- **A pipeline "just didn't run" with no failed runs at all**: check
  `get_trigger_status` for its trigger - `runtime_state: "Stopped"` means
  the trigger was disabled and never fired, which won't show up as a
  failed run because no run was ever created.
- **`check_auth` opens a browser but sign-in never completes / times out**:
  `interactive_browser` mode waits 5 minutes by default. If you're on VS
  Code Remote/SSH or in a container without a reachable local browser,
  this mode may not work at all - fall back to `service_principal` in
  that environment.
- **Browser popup appears every single time you restart the server**:
  persistence may have silently failed and fallen back to in-memory-only
  (check server logs for "Persistent token cache unavailable"). This is
  common on headless Linux without a keyring daemon running - either
  install/enable one (e.g. `gnome-keyring` or `kwallet`), or accept the
  repeat prompts as the tradeoff of that environment.
- **`check_auth` succeeds but every ADF tool returns a 403**: in
  `interactive_browser` mode this means *your own account* lacks Reader
  on the factory (not an app registration) - see the RBAC note above.
- **`get_pipeline_run_tree` shows a child's activities as an empty list,
  but you know it has activities**: the child run may have started
  outside the shared `start_time`/`end_time` window (default: last 7
  days) - widen the window explicitly if a master pipeline runs for an
  unusually long time relative to its children.
- **A deeply nested child pipeline is missing from the tree**: check
  `truncated: true` on its parent node - `max_depth` (default 5) was
  reached. Re-run with a higher `max_depth` if your pipelines nest that
  deep.

TDQS

A3.6/5.0

Scored across 13 tools

Disambiguation5/5

Each tool targets a distinct layer of the ADF workflow: health/auth bootstrap, factory discovery, pipeline inspection, run monitoring, activity diagnostics, and trigger management. Even the closest pair, list_activity_runs and get_failed_activity_details, is clearly separated by scope and purpose.

Naming Consistency4/5

The naming is overwhelmingly consistent with list_/get_ + resource noun in snake_case, and the run-related tools follow a clear pattern. health_check is a minor convention break compared to check_auth, but this is a small deviation in an otherwise predictable set.

Tool Count5/5

13 tools is well-scoped for an ADF server: each tool maps to a distinct object hierarchy or bootstrap step without unnecessary redundancy. The count is substantial enough to cover real workflows but not bloated.

Completeness4/5

The set covers the read-only factory, pipeline, run, activity, and trigger diagnostics lifecycle well, including failure RCA. It lacks any management/remediation operations such as stopping or starting triggers, canceling runs, or rerunning pipelines, so it is slightly incomplete for full operational control.

Maintenance

ActivityMaintained
ResponsivenessNo issues