adf-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/LICENSE +21 -0
  3. package/README.md +276 -0
  4. package/index.js +1083 -0
  5. package/package.json +57 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,163 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0]
11
+
12
+ First stable release. The 0.x line was a series of breaking surface
13
+ expansions (auth modes, write tools, destructive tools); 1.0.0 marks the
14
+ point where the tool, env-var, and gating contracts are committed and
15
+ SemVer kicks in.
16
+
17
+ ### Added
18
+ - GitHub Actions **CI workflow** (`.github/workflows/ci.yml`) running
19
+ `npm run lint`, `npm run format:check`, `node --check index.js`, and
20
+ `npm audit --omit=dev` on every push and PR.
21
+ - GitHub Actions **release workflow** (`.github/workflows/release.yml`)
22
+ publishing to npm with provenance when a `v*` tag is pushed. Requires
23
+ a `NPM_TOKEN` repository secret.
24
+ - **`Dockerfile`** (multi-stage, alpine, non-root) for hosting the server
25
+ on Azure with managed identity. Companion `.dockerignore`.
26
+ - **`SECURITY.md`** with explicit disclosure process, in/out-of-scope
27
+ items, and a list of known design limitations.
28
+ - README sections: "Install from npm", "Docker / managed identity",
29
+ and a link to `SECURITY.md`.
30
+ - `package.json` gains `files`, `publishConfig`, and `keywords` for npm
31
+ discoverability and to keep the published tarball minimal.
32
+ - Plan-store capacity cap (`PLAN_STORE_MAX = 100`) — when full,
33
+ the oldest entry is evicted before insert. Prevents memory exhaustion
34
+ if a buggy client floods plans faster than the TTL sweep runs.
35
+
36
+ ### Changed
37
+ - Server version is now read from `package.json` at startup instead of
38
+ being hard-coded in `new McpServer({ version })`. Single source of truth.
39
+
40
+ ### Known limitations (intentional)
41
+ - No automated test suite — manual smoke tests only.
42
+ - `list_*` tools don't follow ARM `nextLink`; factories with hundreds of
43
+ pipelines/datasets/etc. will see only the first ARM page.
44
+ - `AZURE_CLIENT_ID` is overloaded across auth modes (CLI public client /
45
+ SP app reg / user-assigned MI). Documented in `.env.example`.
46
+ - Plan/apply tokens are in-memory only and do not survive a restart.
47
+
48
+ ## [0.4.0]
49
+
50
+ ### Added
51
+ - `ADF_MCP_ALLOW_DELETE=true` (requires `ADF_MCP_MODE=write`) enables 8 new
52
+ destructive tools — 4 `create_or_update_*` and 4 `delete_*` for pipelines,
53
+ triggers, linked services, and datasets. Off by default. Setting the flag
54
+ without write mode logs a warning and is ignored.
55
+ - **Plan/apply confirmation pattern** for every destructive tool. First call
56
+ (`dry_run: true`, default) returns a before/after diff plus a single-use
57
+ `confirm_token` with a 10-minute TTL. Second call (`dry_run: false` +
58
+ matching `confirm_token`) applies the change.
59
+ - **Optimistic concurrency** via ETag captured at plan time and passed as
60
+ `If-Match` on apply. If the resource changed between plan and apply, ARM
61
+ returns 412 and the server surfaces "Resource changed since the plan;
62
+ request a new plan."
63
+ - Token-store hygiene: tokens are bound to `(tool, target, payload-hash)`,
64
+ single-use, expire after 10 minutes, and are swept every minute.
65
+ - README "Destructive mode" section covering the plan/apply rationale, the
66
+ ETag concurrency story, and why `create_or_update_*` is bundled with
67
+ `delete_*` under the same flag.
68
+
69
+ ### Changed
70
+ - `armAt()` now accepts `extraHeaders` so callers can pass `If-Match`.
71
+ - Errors from ARM responses now expose `.status` so downstream code (e.g.
72
+ `fetchExistingOrNull`, 412 detection in apply) can branch on HTTP status.
73
+
74
+ ## [0.3.0]
75
+
76
+ ### Added
77
+ - Optional **write mode** opt-in via `ADF_MCP_MODE=write`. When unset (default)
78
+ the server is read-only — write tools are not registered at all, so an LLM
79
+ cannot call them regardless of prompt.
80
+ - Five write tools (registered only in write mode):
81
+ - `create_pipeline_run` — kick off a new run, optionally with parameters.
82
+ - `cancel_pipeline_run` — cancel an in-progress run; child runs too by default.
83
+ - `rerun_pipeline_run` — re-execute a previous run; defaults to resuming from
84
+ the failed activity.
85
+ - `start_trigger` / `stop_trigger` — toggle a trigger's runtime state.
86
+ - Audit logging to stderr for every write tool call. Each invocation emits
87
+ ATTEMPT + (SUCCESS | FAILURE) lines with timestamp, target, and caller
88
+ identity parsed from the Entra token (`upn`/`preferred_username`/`appid`/`oid`).
89
+ - Startup log line when write mode is enabled, so the operator can see in the
90
+ MCP server log whether mutations are possible.
91
+ - README "Write mode" section covering RBAC requirements, audit log format,
92
+ recommended SP pairing, and the separation from destructive ops (Stage 6).
93
+
94
+ ### Changed
95
+ - `armAt()` now builds URLs via the `URL` constructor and accepts an
96
+ `extraQuery` argument, enabling endpoints that need query params beyond
97
+ `api-version` (rerun's `referencePipelineRunId`, cancel's `isRecursive`).
98
+
99
+ ## [0.2.0]
100
+
101
+ ### Added
102
+ - Five new read tools:
103
+ - `get_pipeline_run` — direct lookup of a single run by ID.
104
+ - `list_linked_services` — linked services and their types.
105
+ - `list_datasets` — datasets and their linked-service references.
106
+ - `list_integration_runtimes` — IRs and their state (spot offline self-hosted
107
+ IRs).
108
+ - `list_factories` — discover other factories in the current subscription.
109
+ - Pagination on `query_pipeline_runs` via `continuation_token` input and
110
+ `continuationToken` in the response.
111
+ - Automatic retry with `Retry-After`-aware backoff on ARM HTTP 429 (up to 3
112
+ retries, capped at 60 s per attempt). Retries are logged to stderr.
113
+ - `safeTool` wrapper converting thrown errors into structured MCP tool errors
114
+ (`isError: true`) so the LLM can see and react to the message.
115
+ - Startup validation that `ADF_FACTORY_RESOURCE_ID` parses as a valid ARM ID
116
+ with a `/subscriptions/<id>` prefix.
117
+ - `ADF_AUTH_MODE` env var selecting from six credential types: `interactive`
118
+ (default), `device-code`, `cli`, `service-principal`, `managed-identity`,
119
+ `default`. Default preserves prior behavior.
120
+ - `AZURE_CLIENT_SECRET` env var (required only for `service-principal` mode).
121
+ - README "Authentication modes" section with per-mode requirements and notes.
122
+ - `.env.example` documenting supported environment variables.
123
+
124
+ ### Changed
125
+ - `query_activity_runs` truncates each activity's `input` and `output` to ~4 KB
126
+ by default to protect the LLM context window. Pass `full=true` to receive
127
+ the untruncated payloads. **Breaking** for callers that depended on the full
128
+ blob being returned by default.
129
+ - `.editorconfig` enforcing consistent indentation and line endings.
130
+ - `.gitattributes` enforcing LF line endings cross-platform.
131
+ - `ROADMAP.md` capturing the staged plan for upcoming work.
132
+ - README "Troubleshooting" section covering common auth and RBAC failures.
133
+ - ESLint (flat config) + Prettier with `lint`, `lint:fix`, `format`,
134
+ `format:check` npm scripts.
135
+ - `CONTRIBUTING.md` with branching, Conventional Commits, and PR conventions.
136
+ - `.github/` issue and pull request templates.
137
+
138
+ ### Changed
139
+ - `index.js` reformatted by Prettier to match the new project style (no
140
+ behavior changes).
141
+
142
+ ### Security
143
+ - Resolved 4 advisories (1 high, 3 moderate) in transitive dependencies of
144
+ `@modelcontextprotocol/sdk` via `npm audit fix`: `fast-uri`,
145
+ `hono`/`@hono/node-server`, `express-rate-limit`, `ip-address`. All affected
146
+ packages are HTTP-transport code paths that are not loaded at runtime by
147
+ this stdio-only server, so practical exposure was zero. Patch/minor bumps
148
+ only; no `package.json` changes.
149
+
150
+ ## [0.1.0] - 2026-04-27
151
+
152
+ ### Added
153
+ - Initial MCP server with read-only tools: `list_pipelines`, `get_pipeline`,
154
+ `query_pipeline_runs`, `query_activity_runs`, `list_triggers`.
155
+ - Interactive browser authentication via `@azure/identity`.
156
+ - MIT license and `package.json` metadata for distribution.
157
+
158
+ [Unreleased]: https://github.com/user-vik/adf-mcp-server/compare/v1.0.0...HEAD
159
+ [1.0.0]: https://github.com/user-vik/adf-mcp-server/compare/v0.4.0...v1.0.0
160
+ [0.4.0]: https://github.com/user-vik/adf-mcp-server/compare/v0.3.0...v0.4.0
161
+ [0.3.0]: https://github.com/user-vik/adf-mcp-server/compare/v0.2.0...v0.3.0
162
+ [0.2.0]: https://github.com/user-vik/adf-mcp-server/compare/v0.1.0...v0.2.0
163
+ [0.1.0]: https://github.com/user-vik/adf-mcp-server/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Perez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # adf-mcp-server
2
+
3
+ MCP server for classic Azure Data Factory (V2) pipeline troubleshooting and operations. Exposes ARM tools for pipelines, pipeline runs, activity runs, triggers, linked services, datasets, and integration runtimes over stdio. Supports six Entra auth modes — interactive browser, device code, Azure CLI session, service principal, managed identity, or auto-detect. **Read-only by default**, with two opt-in tiers: `ADF_MCP_MODE=write` for runtime ops (run/cancel/start/stop), plus `ADF_MCP_ALLOW_DELETE=true` for definition-changing ops (`create_or_update_*` and `delete_*`) gated by a plan/apply confirmation pattern.
4
+
5
+ ## What it does
6
+
7
+ Wraps the ADF REST API as MCP tools so an AI agent (Claude Code, Claude Desktop, Cursor, etc.) can investigate, run, and control a Data Factory. **Read-only by default** — write tools (kicking off pipeline runs, cancelling them, toggling triggers) are registered only when the operator sets `ADF_MCP_MODE=write` in the MCP client config. See **Write mode** below.
8
+
9
+ | Tool | Purpose |
10
+ | --------------------------- | ------------------------------------------------------------------------------------------------------ |
11
+ | `list_pipelines` | All pipelines in the factory + activity counts, parameters, folder |
12
+ | `get_pipeline` | Full JSON definition of a specific pipeline |
13
+ | `query_pipeline_runs` | Pipeline runs in a time window (default last 24h), filterable by pipeline/status, with pagination |
14
+ | `get_pipeline_run` | Full details for a single pipeline run by ID |
15
+ | `query_activity_runs` | Activity runs for a pipeline run — input/output truncated by default; pass `full=true` to opt out |
16
+ | `list_triggers` | All triggers + runtime state and recurrence |
17
+ | `list_linked_services` | Linked services (databases, storage, etc.) and their types |
18
+ | `list_datasets` | Datasets and the linked service each one belongs to |
19
+ | `list_integration_runtimes` | Integration runtimes and their state — useful for spotting offline self-hosted IRs |
20
+ | `list_factories` | All ADF v2 instances in the current subscription — discover other factories without their full ARM IDs |
21
+
22
+ **Write tools** — only registered when `ADF_MCP_MODE=write`:
23
+
24
+ | Tool | Purpose |
25
+ | --------------------- | ----------------------------------------------------------------------------------------------- |
26
+ | `create_pipeline_run` | Kick off a new run of a pipeline (optionally with parameters). Returns the new `runId`. |
27
+ | `cancel_pipeline_run` | Cancel an in-progress pipeline run. Cancels child runs too by default. |
28
+ | `rerun_pipeline_run` | Re-execute a previous run. Defaults to resuming from the failed activity (the common workflow). |
29
+ | `start_trigger` | Start a trigger so it begins firing on its schedule. |
30
+ | `stop_trigger` | Stop a trigger so it stops firing. |
31
+
32
+ **Destructive tools** — only registered when `ADF_MCP_MODE=write` AND `ADF_MCP_ALLOW_DELETE=true`. Every call uses a two-step plan/apply confirmation pattern (see **Destructive mode** below):
33
+
34
+ | Tool | Purpose |
35
+ | --------------------------------- | ----------------------------------------------------------------------- |
36
+ | `create_or_update_pipeline` | Create a new pipeline or overwrite an existing one. |
37
+ | `create_or_update_trigger` | Create or overwrite a trigger. New triggers start in Stopped state. |
38
+ | `create_or_update_linked_service` | Create or overwrite a linked service (database / storage connection). |
39
+ | `create_or_update_dataset` | Create or overwrite a dataset. |
40
+ | `delete_pipeline` | Delete a pipeline. |
41
+ | `delete_trigger` | Delete a trigger (must be stopped first via `stop_trigger`). |
42
+ | `delete_linked_service` | Delete a linked service. Datasets that reference it will start failing. |
43
+ | `delete_dataset` | Delete a dataset. Pipelines that reference it will start failing. |
44
+
45
+ ## Prerequisites
46
+
47
+ - **Node.js >= 20** on PATH (`node -v` to verify). The Node MSI install may be UAC-blocked on locked-down corp Windows boxes — ask IT if needed.
48
+ - **Git** to clone the repo.
49
+ - **Azure RBAC**: at minimum **Reader** role on the target Data Factory resource. Assigned in Azure Portal → the ADF resource → **Access control (IAM)** → Role assignments. Without this every tool call returns 403 even when auth succeeds.
50
+ - **An MCP-aware client** (Claude Code, Claude Desktop, Cursor, etc.) to wire it into.
51
+
52
+ ## Install
53
+
54
+ ### From npm (recommended)
55
+
56
+ ```sh
57
+ # One-off run via npx — no global install needed
58
+ npx adf-mcp-server
59
+
60
+ # Or install globally
61
+ npm install -g adf-mcp-server
62
+ ```
63
+
64
+ ### From source (for development or to pin to a commit)
65
+
66
+ ```sh
67
+ git clone https://github.com/user-vik/adf-mcp-server
68
+ cd adf-mcp-server
69
+ npm install
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ The server reads everything from environment variables — typically set inside your MCP client config rather than the shell.
75
+
76
+ | Variable | Required | Notes |
77
+ | ------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `ADF_FACTORY_RESOURCE_ID` | always | Full ARM resource ID, e.g. `/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.DataFactory/factories/<factory>` |
79
+ | `ADF_AUTH_MODE` | no | Auth credential to use. Defaults to `interactive`. See **Authentication modes** below. |
80
+ | `ADF_MCP_MODE` | no | `read` (default) or `write`. `write` registers run/cancel/start/stop tools. See **Write mode** below. |
81
+ | `ADF_MCP_ALLOW_DELETE` | no | When `true` AND `ADF_MCP_MODE=write`, also registers the 8 `create_or_update_*` and `delete_*` tools. See **Destructive mode** below. |
82
+ | `AZURE_TENANT_ID` | for `interactive` / `device-code` / `service-principal` | Entra tenant ID. |
83
+ | `AZURE_CLIENT_ID` | for `service-principal` | Optional for `interactive`/`device-code` (defaults to Azure CLI public client). For `managed-identity`, set only when targeting a user-assigned MI. |
84
+ | `AZURE_CLIENT_SECRET` | for `service-principal` | Treat as a secret. Never commit. |
85
+
86
+ ## Wiring into an MCP client
87
+
88
+ Add an entry to your client's MCP config. Example (Claude Code / Claude Desktop format):
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "ms-adf": {
94
+ "type": "stdio",
95
+ "command": "node",
96
+ "args": ["C:\\path\\to\\adf-mcp-server\\index.js"],
97
+ "env": {
98
+ "AZURE_TENANT_ID": "<your-tenant-id>",
99
+ "ADF_FACTORY_RESOURCE_ID": "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.DataFactory/factories/<factory>"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ Restart the MCP client after editing the config.
107
+
108
+ ## Authentication modes
109
+
110
+ Set `ADF_AUTH_MODE` to pick how the server obtains an Entra token. Default is `interactive`, which preserves prior behavior.
111
+
112
+ | Mode | Credential | Use case | Required env (beyond `ADF_FACTORY_RESOURCE_ID`) |
113
+ | ------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
114
+ | `interactive` _(default)_ | `InteractiveBrowserCredential` | Desktop devs — opens a browser tab on first call. | `AZURE_TENANT_ID` |
115
+ | `device-code` | `DeviceCodeCredential` | SSH / WSL / headless — prints a code + URL to stderr (the MCP client's server log). | `AZURE_TENANT_ID` |
116
+ | `cli` | `AzureCliCredential` | Devs already signed in via `az login`. Zero prompts. | _(none — uses CLI session)_ |
117
+ | `service-principal` | `ClientSecretCredential` | CI, shared servers, automation. | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` |
118
+ | `managed-identity` | `ManagedIdentityCredential` | MCP server hosted on an Azure VM, Container App, App Service, etc. | _(none — uses the host identity)_ |
119
+ | `default` | `DefaultAzureCredential` | Chain: env vars → managed identity → CLI → VS Code → interactive browser. Easiest "just works." | _(varies by what's available)_ |
120
+
121
+ On the first tool call, the chosen credential acquires a token at the `https://management.azure.com/.default` scope. Tokens are cached in memory for the lifetime of the process; subsequent calls reuse them.
122
+
123
+ For all user-flow modes (`interactive`, `device-code`, `cli`), the effective ARM permissions are _your own_ personal RBAC on the factory. For `service-principal` and `managed-identity`, they are the SP's or MI's RBAC — grant that identity at least **Reader** on the factory.
124
+
125
+ ### Mode-specific notes
126
+
127
+ - **`device-code`**: the message containing the verification URL and one-time code is written to **stderr**, which most MCP clients route to their server log rather than the chat. In Claude Code, view it via `/mcp` → server logs. The auth call blocks until you complete the flow in a browser.
128
+ - **`service-principal`**: `AZURE_CLIENT_ID` here is the SP's app registration, _not_ the Azure CLI public client default.
129
+ - **`managed-identity`**: omit `AZURE_CLIENT_ID` for a system-assigned MI; set it to the MI's client ID for a user-assigned MI.
130
+ - **`default`**: opaque when something fails. If `DefaultAzureCredential` errors with "no credential was found", switch to a specific mode to see which one is actually failing.
131
+
132
+ ## Write mode
133
+
134
+ By default the server is **read-only** — no MCP tool it exposes can mutate the factory. An LLM literally cannot call write operations because they aren't registered.
135
+
136
+ To enable mutations, set `ADF_MCP_MODE=write` in the MCP client's `env` block. The server logs `[adf-mcp] write mode enabled — pipeline run + trigger control tools are exposed` at startup so it's visible in the server log.
137
+
138
+ ### What write mode unlocks
139
+
140
+ `create_pipeline_run`, `cancel_pipeline_run`, `rerun_pipeline_run`, `start_trigger`, `stop_trigger`. See the tool table above.
141
+
142
+ ### What it does NOT unlock
143
+
144
+ Definition-changing operations — `create_or_update_*` and `delete_*` — are gated separately behind `ADF_MCP_ALLOW_DELETE=true` and use a two-step plan/apply confirmation. See **Destructive mode** below.
145
+
146
+ ### RBAC
147
+
148
+ `Reader` is no longer enough. Grant the identity used by `ADF_AUTH_MODE` at least **Data Factory Contributor** on the factory, or a narrower custom role that includes the action `Microsoft.DataFactory/factories/pipelineruns/*` and the trigger start/stop actions.
149
+
150
+ ### Audit log
151
+
152
+ Every write call is logged to **stderr** with a line like:
153
+
154
+ ```
155
+ [adf-mcp][AUDIT] 2026-05-19T15:42:01.123Z tool=create_pipeline_run target=pipeline=ETL_Daily caller=victor.perez@example.com status=ATTEMPT
156
+ [adf-mcp][AUDIT] 2026-05-19T15:42:01.987Z tool=create_pipeline_run target=pipeline=ETL_Daily caller=victor.perez@example.com status=SUCCESS
157
+ ```
158
+
159
+ The caller is parsed from the `upn` / `preferred_username` / `appid` / `oid` claims of the Entra access token. Forward your MCP client's server log somewhere durable if you need a long-term audit trail.
160
+
161
+ ### Recommended pairing for shared / CI deployments
162
+
163
+ `ADF_MCP_MODE=write` + `ADF_AUTH_MODE=service-principal`. The SP gets exactly the RBAC it needs, the audit log identifies it consistently, and individual users don't need factory-Contributor on their personal accounts.
164
+
165
+ ## Destructive mode
166
+
167
+ Setting `ADF_MCP_ALLOW_DELETE=true` in addition to `ADF_MCP_MODE=write` registers the 8 tools that mutate the factory's definition (`create_or_update_*` for pipelines, triggers, linked services, datasets, plus their `delete_*` counterparts). The server logs `[adf-mcp] destructive mode enabled` at startup so it's visible in the MCP server log. Setting `ADF_MCP_ALLOW_DELETE=true` without `ADF_MCP_MODE=write` logs a warning and is ignored.
168
+
169
+ ### Why "destructive" covers create_or_update too
170
+
171
+ `create_or_update_*` can silently overwrite an existing resource — the LLM might not realize it exists. Bundling it with `delete_*` under the same flag keeps "anything that changes the factory's definition" behind a single, conscious opt-in.
172
+
173
+ ### Plan/apply confirmation
174
+
175
+ Every destructive tool uses a two-step pattern that forces the LLM (and the human reading the chat) to look at the diff before applying it.
176
+
177
+ 1. **Plan step** — the LLM calls the tool with `dry_run: true` (the default). The server fetches the existing resource, returns a `before` / `after` diff, and issues a one-time `confirm_token` with a 10-minute TTL.
178
+
179
+ ```jsonc
180
+ {
181
+ "plan_type": "DRY_RUN",
182
+ "action": "create_or_update",
183
+ "target": "pipeline=ETL_Daily",
184
+ "before": {
185
+ "properties": {
186
+ /* current pipeline */
187
+ },
188
+ },
189
+ "after": {
190
+ "properties": {
191
+ /* proposed pipeline */
192
+ },
193
+ },
194
+ "confirm_token": "8c1f...e0a3",
195
+ "expires_at": "2026-05-19T15:52:00.000Z",
196
+ "hint": "To apply, call create_or_update_pipeline again with dry_run=false and confirm_token=\"8c1f...e0a3\".",
197
+ }
198
+ ```
199
+
200
+ 2. **Apply step** — the LLM calls the same tool again with `dry_run: false` and the `confirm_token` from step 1. The token is single-use, bound to the exact `(tool, target, payload)` triple, and tied to the resource's ETag at plan time. If anything changed since the plan was computed, ARM returns HTTP 412 Precondition Failed and the server surfaces "Resource changed since the plan; request a new plan."
201
+
202
+ ### Why tokens, not just `dry_run=false`?
203
+
204
+ Without the token, an LLM could skip the plan step entirely. Requiring a token means the LLM must have seen a plan in its own context (and surfaced it to you in the chat) before it can apply. The token store is in-memory; restarting the MCP server invalidates all pending plans.
205
+
206
+ ### RBAC for destructive mode
207
+
208
+ The identity needs **Data Factory Contributor** on the factory (same role as write mode — no additional permissions, because ADF doesn't model "can create but not delete" separately at the RBAC level).
209
+
210
+ ## Docker / managed identity
211
+
212
+ A `Dockerfile` is included for hosting the server on Azure (Container Apps, App Service, AKS, or a VM) with a managed identity — no client secrets in env vars.
213
+
214
+ ```sh
215
+ docker build -t adf-mcp-server .
216
+ ```
217
+
218
+ Typical deployment pattern on Azure Container Apps:
219
+
220
+ 1. Grant the Container App's system-assigned managed identity **Data Factory Contributor** on the target factory.
221
+ 2. Configure the container with:
222
+ ```
223
+ ADF_FACTORY_RESOURCE_ID=/subscriptions/.../factories/...
224
+ ADF_AUTH_MODE=managed-identity
225
+ ADF_MCP_MODE=write # if you want write tools
226
+ ADF_MCP_ALLOW_DELETE=true # if you want destructive tools
227
+ ```
228
+ 3. Use an `azureContainerAppsApi`-style transport from your MCP client (out of scope for this server — stdio MCP usually runs locally; this section is for centralized deployments that proxy stdio via a sidecar).
229
+
230
+ The image runs as a non-root user (`adf`, uid auto-assigned). Stdio is the only entrypoint; no ports are exposed.
231
+
232
+ ## Run standalone (for debugging)
233
+
234
+ ```sh
235
+ ADF_FACTORY_RESOURCE_ID=... AZURE_TENANT_ID=... node index.js
236
+ ```
237
+
238
+ The server speaks MCP over stdio, so running it directly will just block waiting for an MCP client to connect via stdin/stdout. Useful only to confirm it starts without crashing.
239
+
240
+ ## Troubleshooting
241
+
242
+ | Symptom | Likely cause | Fix |
243
+ | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
244
+ | `Missing required env vars` on startup | `ADF_FACTORY_RESOURCE_ID` or `AZURE_TENANT_ID` not set in the MCP client's `env` block | Add them to the client config and restart the client. |
245
+ | Tool call returns `403` from ARM | Your account lacks RBAC on the factory | Ask the resource owner to grant at least **Reader** on the Data Factory resource (Portal → ADF → Access control (IAM)). |
246
+ | Tool call returns `401` / token errors | Conditional Access or MFA blocked the silent token | Sign out of Azure CLI / browser sessions, then re-trigger any tool to force a fresh interactive sign-in. |
247
+ | Browser tab never opens on first call | Running over SSH / inside WSL / on a headless host | Switch to `ADF_AUTH_MODE=device-code` and read the code/URL from the MCP server's stderr log. |
248
+ | `Invalid ADF_AUTH_MODE` | Typo in the mode name | Use one of: `interactive`, `device-code`, `cli`, `service-principal`, `managed-identity`, `default`. |
249
+ | `ADF_AUTH_MODE=service-principal requires ...` | Missing `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, or `AZURE_CLIENT_SECRET` | Set all three in the MCP client's `env` block. |
250
+ | MCP client says "server failed to start" | Wrong path in `args`, or Node not on PATH for the client's user | Verify the path with `node "C:\\path\\to\\index.js"` from a fresh shell. On Windows, the MCP client may inherit a different PATH than your terminal. |
251
+ | Calls hang or time out | ARM is throttling (HTTP 429) and the server is auto-retrying with backoff | Check the MCP server's stderr log — each retry is logged. Up to 3 retries honoring `Retry-After`; on exhaustion, the call fails with the original 429. |
252
+ | Activity output is `{ _truncated: true, ... }` | Default 4 KB truncation kicked in to protect the LLM context window | Pass `full=true` to `query_activity_runs` for the untruncated payload. |
253
+ | LLM says "no tool to start a run / cancel" | Server is in read-only mode (default) | Set `ADF_MCP_MODE=write` in the MCP client's `env` block and restart the client. |
254
+ | Write tool returns `403 Authorization failed` | The identity has Reader but not Contributor on the factory | Grant **Data Factory Contributor** (or a narrower custom role with the relevant pipelineruns/triggers actions) to the user / SP / MI. |
255
+ | Apply call returns "Resource changed since the plan" | Someone (or something) modified the resource between your plan and apply step | Re-run with `dry_run=true` to fetch a fresh plan, then apply the new `confirm_token`. |
256
+ | Apply call returns "Invalid confirm_token" | Token expired (10-min TTL), already used, or server restarted | Re-run with `dry_run=true` to get a new token. |
257
+ | Apply call returns "confirm_token does not match" | The payload changed between plan and apply (e.g. the LLM edited the definition) | Re-run the plan step with the current payload to get a token bound to it. |
258
+ | `404` for a pipeline that exists | Wrong factory in `ADF_FACTORY_RESOURCE_ID` | Confirm the ARM ID matches the factory you expect (subscription, resource group, and name all match). |
259
+
260
+ For everything else, check the project [issues](https://github.com/user-vik/adf-mcp-server/issues).
261
+
262
+ ## Security
263
+
264
+ See [SECURITY.md](SECURITY.md) for the disclosure process, in/out-of-scope items, and known design limitations.
265
+
266
+ ## Roadmap
267
+
268
+ See [ROADMAP.md](ROADMAP.md) for the staged plan and what's intentionally deferred.
269
+
270
+ ## Changelog
271
+
272
+ See [CHANGELOG.md](CHANGELOG.md).
273
+
274
+ ## License
275
+
276
+ MIT — see [LICENSE](LICENSE).