power-automate-mcp-server
# power-automate-mcp-server
[](https://github.com/sapientsai/power-automate-mcp-server/actions/workflows/node.js.yml)
[](https://www.npmjs.com/package/power-automate-mcp-server)
[](https://www.npmjs.com/package/power-automate-mcp-server)
[](https://opensource.org/licenses/MIT)
An MCP server that lets agents **inspect, operate, and author Microsoft Power Automate cloud
flows** from a CLI/agent context — list and inspect flows, debug runs, check connections and
owners, and (when explicitly enabled) enable/disable flows, cancel/resubmit runs, manage
owners, and **create/update/delete flows**.
Primarily a **management** surface — the Power Automate portal's visual designer remains the
better place to author complex flow logic — but `create_flow`/`update_flow`/`delete_flow` are
also available (write-gated) for programmatic authoring. Built on
[SomaMCP](https://github.com/sapientsai/SomaMCP) (telemetry, health/info/
dashboard, error classification) over FastMCP.
> ⚠️ **Unofficial API.** v1 targets `api.flow.microsoft.com` — the surface the Power Automate
> portal itself uses. Microsoft labels it _"isn't supported. Customers should instead use the
> Dataverse Web APIs."_ It is stable in practice and, unlike Dataverse, sees **all** flows
> (including personal "My Flows") and works on M365‑seeded entitlements (no Premium license).
> Every tool's description carries this disclaimer. A supported Dataverse backend is stubbed
> for the future (see [`src/backend/dataverse/README.md`](src/backend/dataverse/README.md)).
## Quick start
```bash
pnpm install
cp .env.example .env # set AZURE_CLIENT_ID (see "App registration" below)
pnpm build
pnpm dev:stdio # local agent over stdio (device-code sign-in to stderr)
```
On first use the server prints a device-code prompt to **stderr**; open
`https://microsoft.com/devicelogin`, enter the code, and sign in. The token is cached
(`TOKEN_CACHE_PATH`, mode 0600) and silently refreshed thereafter.
### Add to an MCP client (stdio)
```jsonc
{
"mcpServers": {
"power-automate": {
"command": "npx",
"args": ["-y", "power-automate-mcp-server", "--stdio"],
"env": { "AZURE_CLIENT_ID": "<your-app-registration-client-id>" },
},
},
}
```
**Developing on this repo?** It ships a project-scoped [`.mcp.json`](.mcp.json) (stdio, local
`dist/bin.js`). After `pnpm build`, export `AZURE_CLIENT_ID` and open the repo in Claude Code —
the `power-automate` server loads automatically (complete the device-code sign-in once; the
cached token is then reused).
## App registration
This server ships **no** default client id — you register your own (one‑time):
1. **Azure Portal → Microsoft Entra ID → App registrations → New registration.**
2. Name it (e.g. `power-automate-mcp`). Supported account types: **multitenant** (or
single‑tenant if you'll only ever use one org).
3. **Authentication → Add a platform → Mobile and desktop applications.** Add redirect URI
`http://localhost` (unused by device code, but required to register the platform). Set
**"Allow public client flows" = Yes**.
4. **API permissions → Add a permission.** You need a **delegated** permission for the Power
Automate / _Microsoft Flow Service_ API. If it isn't in the picker, see "Token audience"
below — this is the known friction point.
5. Copy the **Application (client) ID** → `AZURE_CLIENT_ID`.
For unattended `clientCredentials` mode instead: add a **client secret**, grant **application**
permissions with **admin consent**, and set `AZURE_AUTH_MODE=clientCredentials`,
`AZURE_TENANT_ID=<your tenant>`, `AZURE_CLIENT_SECRET=...`. Note app‑only has **limited Flow
reach** (it generally cannot see personal "My Flows").
### Verified working setup ⚠️
Confirmed against a real tenant. Interactive Flow auth needs **all four** of these — each one,
if missing, fails with a different cryptic `AADSTS…` (details in [`docs/api-notes.md`](docs/api-notes.md)):
1. **A specific tenant** — `AZURE_TENANT_ID=<your-tenant-GUID>`, **not `common`**. `common` + a
resource scope → `AADSTS50059` (and MSAL hides it as an empty device-code response).
2. **The _Microsoft Flow Service_ delegated permission** on the app (resource app id
`7df0a125-d3be-4c96-aa54-591f83ff541c`). Missing → `AADSTS650057`.
3. **Specific scopes, not `.default`**, when reusing an app that has other (incrementally
consented) permissions — `.default` validates the whole app and can fail with `AADSTS650051`.
A _dedicated_ app may use `.default`. Pin via `FLOW_SCOPES`.
4. **"Allow public client flows" = Yes** (`isFallbackPublicClient=true`). Off → token redemption
fails with `invalid_client`.
Verified `FLOW_SCOPES`:
`https://service.flow.microsoft.com/Flows.Read.All,https://service.flow.microsoft.com/Flows.Manage.All`
#### Configure an existing app via `az`
```bash
APP=<your-app-client-id>; FLOW=7df0a125-d3be-4c96-aa54-591f83ff541c
# Flows.Read.All + Flows.Manage.All (delegated), then tenant-wide consent:
az ad app permission add --id "$APP" --api "$FLOW" --api-permissions \
e45c5562-459d-4d1b-8148-83eb1b6dcf83=Scope 30b2d850-00c3-4802-b7ae-ece9af9de5c6=Scope
az ad app permission admin-consent --id "$APP"
# enable device-code (public client flows):
az ad app update --id "$APP" --set isFallbackPublicClient=true
```
> **Recommended:** a **dedicated** public-client app with only the Flow delegated permissions
> avoids the `.default`/shared-app pitfalls (#3) entirely — cleaner than reusing a Graph app.
## Tools
All tools are **read‑only by default**. Write tools are registered but **refuse** unless
`ENABLE_WRITE_OPS=true`.
### Read-only (always enabled)
| Tool | Parameters | Returns |
| ------------------- | ------------------------------------------------ | -------------------------------------------------------------------------- |
| `list_environments` | — | `{ id, name, displayName, location, isDefault }[]` |
| `list_flows` | `environment?`, `owner?` | `{ name, displayName, state, createdTime, lastModifiedTime, owner }[]` |
| `get_flow` | `environment?`, `flow` | full flow incl. `definition`, `connectionReferences`, trigger/action names |
| `list_flow_runs` | `environment?`, `flow`, `top?` (≤100), `status?` | `{ name, status, startTime, endTime, durationMs, triggerName, error }[]` |
| `get_flow_run` | `environment?`, `flow`, `run` | run detail + first‑failure + `raw` properties (debugging) |
| `list_connections` | `environment?` | `{ name, apiName, displayName, status, accountName, expiresAt }[]` |
| `list_flow_owners` | `environment?`, `flow` | `{ principalId, principalType, roleName, principalDisplayName }[]` |
### Write (require `ENABLE_WRITE_OPS=true`)
| Tool | Parameters |
| ------------------------------ | ---------------------------------------------------------------------------------------------- |
| `create_flow` | `environment?`, `displayName`, `definition`, `connectionReferences?`, `state?` |
| `update_flow` | `environment?`, `flow`, any of `displayName` / `definition` / `state` / `connectionReferences` |
| `delete_flow` | `environment?`, `flow`, `confirm` (must be `true`) |
| `enable_flow` / `disable_flow` | `environment?`, `flow` |
| `cancel_flow_run` | `environment?`, `flow`, `run` |
| `resubmit_flow_run` | `environment?`, `flow`, `run`, `trigger` |
| `add_flow_owner` | `environment?`, `flow`, `principalId`, `roleName` (`CanEdit`\|`CanView`) |
| `remove_flow_owner` | `environment?`, `flow`, `principalId` |
> **Authoring (`create_flow`/`update_flow`):** `definition` is the raw Logic Apps-style
> workflow JSON (see `get_flow` output as a template). The visual designer is better for
> complex logic; for edits, `get_flow` → modify the `definition` → pass it back to `update_flow`.
When `environment` is omitted, tools use `DEFAULT_ENVIRONMENT` if set, else the discovered
default environment (`isDefault: true`).
### Built-in (from SomaMCP)
- `info` MCP tool — server name, version, git SHA, capability counts.
- `report_feedback` — file API‑drift/bug reports as GitHub issues (`FEEDBACK_GITHUB_REPO`,
`GITHUB_TOKEN`).
- HTTP endpoints `/health`, `/health/detail`, `/info`, `/dashboard` (the detailed ones are
protected by `MCP_API_KEY` when set).
## Configuration
See [`.env.example`](.env.example) for the full list. Highlights: `AZURE_CLIENT_ID` (required),
`AZURE_TENANT_ID` (`common`), `AZURE_AUTH_MODE`, `TRANSPORT` (`stdio`\|`http`), `PORT`,
`ENABLE_WRITE_OPS`, `DEFAULT_ENVIRONMENT`, `MCP_API_KEY`, `TELEMETRY`, `TOKEN_CACHE_PATH`.
## Transports & deployment
| Scenario | Transport | Auth | Notes |
| ----------------------- | --------- | -------------------------------------- | ---------------------------------------------------------------------------- |
| Local agent | `stdio` | device-code | Primary. Full reach. `pnpm dev:stdio`. |
| Docker, single operator | `http` | device-code + **mounted token volume** | Auth once via `docker logs`; persists. Full reach. `docker compose up`. |
| Docker, unattended | `http` | `clientCredentials` | No human, but **no personal flows**; verify it can mint a Flow token at all. |
> [!WARNING]
> **The HTTP transport is single-operator.** Every caller shares one Power Automate
> identity: the server holds a single process-wide token (one device-code sign-in, or one
> app identity), and `MCP_API_KEY` is a shared static bearer that gates _access_, not
> _identity_. Do **not** expose the HTTP endpoint to multiple users expecting per-user
> separation — they would all see and mutate the same person's flows. For per-user
> isolation, run **stdio** (one process per user, how Claude Desktop already runs it) or
> wait for the v2 per-user OAuth path
> ([#9](https://github.com/sapientsai/power-automate-mcp-server/issues/9)).
> **v2:** per‑user browser OAuth over HTTP via FastMCP's `AzureProvider` + disk token cache
> (the upstream token surfaces on the session). Reachable through SomaMCP's `backendOptions`
> passthrough without a fork — not wired in v1.
```bash
# Docker (single-operator device-code with a persisted token volume)
AZURE_CLIENT_ID=... docker compose up --build
docker compose logs -f # grab the device code on first run
curl -s http://localhost:3333/health
```
## Development
```bash
pnpm validate # format + lint + typecheck + test + build
pnpm test # vitest (unit)
pnpm dev # http transport, watch
pnpm dev:stdio # stdio transport, watch
pnpm build # tsdown -> dist/
```
Integration tests that hit a real tenant live under `test/integration/` and run only with
`INTEGRATION=1` (see that folder's README). CI runs unit tests only.
## Troubleshooting
- **Device code never grants a token / "device-code sign-in failed for all scope candidates"**
→ the Flow audience isn't grantable to your app. See "Token audience" and `docs/api-notes.md`.
- **`auth error` on every call** → token cache stale; restart to re‑auth, or delete
`TOKEN_CACHE_PATH`.
- **`not found` on a known flow** → wrong environment; run `list_environments` / `list_flows`
first. The flow `name` is the GUID, not the display name.
- **`forbidden`** → the signed‑in user lacks permission on that flow.
- **Empty `list_flows`** in `clientCredentials` mode → app‑only can't see personal flows; use
`interactive`.
- **An endpoint 404/410s unexpectedly** → Microsoft may have moved the api‑version; check the
portal's network tab and pin a newer `api-version` (see `docs/api-notes.md`).
## License
MIT.
---
**Sponsored by <a href="https://sapientsai.com/"><img src="https://sapientsai.com/images/logo.svg" alt="SapientsAI" width="20" style="vertical-align: middle;"> SapientsAI</a>** — Building agentic AI for businesses
TDQS
Scored across 18 tools
Each tool targets a distinct resource and action: flows, runs, owners, connections, environments are all clearly separated. Even list_flow_runs vs get_flow_run are unambiguous (list vs single). No overlapping purposes or ambiguous boundaries.
Tools overwhelmingly follow a verb_noun snake_case pattern (list_flows, get_flow, enable_flow, add_flow_owner). The only deviation is 'info' (bare noun) and 'report_feedback' (verb_noun but not domain-related), which is a minor inconsistency rather than a chaotic mix.
At 18 tools, the set is slightly above the typically ideal 3-15 range, but each tool serves a distinct purpose in managing Power Automate flows, runs, owners, and connections. The count feels justified rather than bloated, though it is on the heavier side.
The flow lifecycle is well-covered (create, get, update, delete, enable, disable), plus runs (list, get, cancel, resubmit) and owner management (list, add, remove). Notable gaps include no manual flow trigger (run_flow) and no connection create/update, but these may be out of scope for a read/monitor-oriented server.