fxray
Official# fxray
<!-- mcp-name: io.github.S-CurveLabs/fxray -->
An X-ray for Power Fx: an MCP server that wraps the Power Platform CLI (`pac`) so Claude, GitHub Copilot (VS Code
agent mode) or any MCP client can inspect and work with **Power Apps solutions, canvas app source and Dataverse**:
list and export solutions, read canvas apps as text (screens, controls, Power Fx formulas, data sources), read
table / column / relationship metadata, run read-only FetchXML, diff two solution versions, run the solution
checker, and (gated, dry-run by default) pack, import and publish.
| Area | Tools |
|---|---|
| Setup & environments | `pac_status` `whoami` `list_environments` |
| Solutions (live) | `list_solutions` `solution_components` `list_canvas_apps` `export_solution` `solution_check` |
| Solutions (offline) | `unpack_solution` `solution_components` `diff_solutions` |
| Canvas app source (offline) | `canvas_overview` `list_controls` `get_control` `search_formulas` `canvas_data_sources` `app_checker_results` |
| Dataverse metadata | `list_tables` `describe_table` `list_relationships` |
| Data, **read-only** | `fetch_query` (FetchXML with a row cap) |
| Writes, **dry-run by default** | `pack_solution` `import_solution` `publish_customizations` |
## Safety model
- **Environment allowlist.** Live tools only reach environments listed in `allowed_environments`. Every call
passes `--environment` explicitly, so pac's currently selected org is never used implicitly, and `whoami` and
every write re-check that the org pac actually connected to is allowlisted. **An empty allowlist (or no config
file) disables every live tool**; offline tools still work.
- **Read-only by default.** `pack_solution`, `import_solution` and `publish_customizations` default to
`dry_run=true` and return a plan plus the exact pac command. Executing needs `allow_writes = true` in the config
**and** `dry_run=false` on the call. Have the assistant show you the plan first.
- **Imports** default to `mode="update"` (never deletes). `upgrade` (managed only) is explicit and flagged as
deleting components missing from the zip. Blockers (managed/unmanaged mismatch, managed downgrade, empty
`DatabaseReferences`) stop execution. Before importing over an unmanaged solution the current one is exported
to `out/backups/<env>/` and the result carries an undo recipe; managed targets cannot be exported, which the
plan says.
- **Local output** (exports, unpacks, packs, checker results) goes under `output_dir`; `protected_paths` are never
written to.
- FetchXML is read-only by nature; it is parsed and validated (well-formed, no DOCTYPE, one entity) before it
reaches pac.
- No secrets in the config: pac keeps its own auth profiles. pac's signed checker-results URL is not returned.
## Install
```powershell
pip install fxray # or run it without installing: uvx fxray
```
Needs Python 3.11+ and the Power Platform CLI:
```powershell
dotnet tool install --global Microsoft.PowerApps.CLI.Tool # needs the .NET SDK
pac auth create --environment https://yourorg.crm.dynamics.com/ # a person, in a terminal (browser sign-in)
```
fxray finds pac on `PATH`, then at `~\.dotnet\tools\pac.exe` (the dotnet global-tool folder); the Windows MSI
install's `pac.cmd` shim is resolved to the `pac.exe` it wraps. MCP clients often start servers with a shorter
PATH than your terminal, so if pac works in a terminal but not in fxray, set `pac_path` to the full path.
Tested on Windows with pac 2.9.3. pac is a .NET tool and runs on macOS/Linux too; fxray should, but that is untested.
Interactive sign-in is impossible from an MCP server: when pac's token expires the tools say so and stop, and a
person runs `pac auth create` (or `pac auth select`) again in a terminal.
## Configure
Copy [`fxray.example.toml`](https://github.com/S-CurveLabs/fxray/blob/main/fxray.example.toml) to `fxray.toml`
and list the environments fxray may reach. Lookup order (first file found wins):
1. `$FXRAY_CONFIG` (an explicit path; an error if it does not exist)
2. `$FXRAY_WORKSPACE\fxray.toml`
3. `.\fxray.toml` (the folder the MCP client starts the server in)
4. `%LOCALAPPDATA%\fxray\fxray.toml` (Windows; `~/.config/fxray/fxray.toml` elsewhere; override the folder with `$FXRAY_HOME`)
The per-user file is the simplest choice: then `FXRAY_CONFIG` is not needed at all. The file is re-read on every
call (no restart needed).
| Setting | Default | Meaning |
|---|---|---|
| `pac_path` | auto | full path to pac; unset = `PATH`, then `~\.dotnet\tools` |
| `allowed_environments` | `[]` | URLs or environment ids. **The only environments any tool reaches.** Empty = live tools refuse |
| `default_environment` | first allowlisted | used when a tool gets no `environment` |
| `allow_writes` | `false` | write tools only plan unless this is true **and** the call passes `dry_run=false` |
| `output_dir` | `out` (next to the config) | exports, unpacked folders, packs, checker output, import backups |
| `protected_paths` | `[]` | folders no tool may write into (e.g. a schema-as-code repo whose files are generated) |
| `max_rows` | 200 | `fetch_query` row cap (ceiling 5000) |
| `timeout_seconds` / `long_timeout_seconds` | 120 / 1800 | pac timeouts (quick calls / export, import, check) |
```toml
allowed_environments = ["https://contoso-dev.crm.dynamics.com/"]
allow_writes = false
output_dir = "out"
```
## Register
**Claude Code:**
```powershell
claude mcp add --scope user fxray -- uvx fxray
# or, with the config somewhere other than the per-user folder:
claude mcp add --scope user fxray -e FXRAY_CONFIG=C:\path\to\fxray.toml -- uvx fxray
```
**VS Code / Copilot.** Add the server to the **user-level** `%APPDATA%\Code\User\mcp.json` so it works from every
window (a workspace `.vscode/mcp.json` only loads once you trust that workspace's MCP servers):
```json
{
"servers": {
"fxray": {
"type": "stdio",
"command": "uvx",
"args": ["fxray"],
"env": { "FXRAY_CONFIG": "C:\\path\\to\\fxray.toml" }
}
}
}
```
Drop the `env` block if the config is in the per-user folder. Then ask the assistant to run `pac_status`: it shows
which config file was used, which pac was found and its version, and pac's auth profiles (each flagged as
allowlisted or not).
## Tools
`source` = a solution `.zip`, an unpacked solution folder (`pac solution unpack` / `export_solution`), a canvas
`.msapp`, or a `pac canvas unpack --layout SourceCode` folder (`Src/*.pa.yaml` + `.msapr`). Offline tools need no pac.
| Tool | Kind | What it does |
|---|---|---|
| `pac_status` | read | config, pac version, auth profiles (flags whether each profile's env is allowlisted) |
| `whoami` | live read | `pac env who` for an allowlisted env; verifies the connected org is allowlisted |
| `list_environments` | live read | environments the account sees, marked allowed / not allowed |
| `list_solutions` | live read | name, version, managed, publisher (managed system solutions hidden by default) |
| `solution_components` | live / offline | live: every component row with resolved names (tables, apps, flows, env vars, connection refs); offline: manifest, publisher prefix, root components, missing dependencies |
| `list_canvas_apps` | live / offline | `pac canvas list`, or the apps inside a solution with their AppVersion stamps |
| `export_solution` | local output | export (read-side on the env) to a timestamped folder under `out/exports/`, unpacked by default |
| `unpack_solution` | local output | `pac solution unpack` into `out/unpacked/` |
| `solution_check` | local output | `pac solution check` (cloud checker), results summarized by rule and severity |
| `diff_solutions` | offline | two versions: manifest, tables/columns/choices, relationships, canvas screens/controls/formula diffs/data sources, flows/env vars, changed files |
| `canvas_overview` | offline | screens (Studio order), control counts and types, data sources, app checker counts |
| `list_controls` | offline | control tree, filter by screen / control type |
| `get_control` | offline | one control's type, parent, children and Power Fx formulas |
| `search_formulas` | offline | substring or regex over every formula; `code_only` ignores strings and comments |
| `canvas_data_sources` | offline | each data source + the formulas using it; unreferenced ones, meta.xml `DatabaseReferences`, schema-cache drift |
| `app_checker_results` | offline | Studio's saved App Checker results (SARIF inside the msapp) |
| `list_tables` | offline / live | tables in a solution or a canvas app's cached schema; live = names only |
| `describe_table` | offline | columns (type, required, length, lookup targets, choice values), primary id/name, relationships |
| `list_relationships` | offline | 1:N (with lookup column, cascade delete) and N:N |
| `fetch_query` | live read | read-only FetchXML with a row cap |
| `pack_solution` | write (local) | `pac solution pack`; dry-run plan with canvas warnings |
| `import_solution` | write | plan: verified target org, installed vs package version/managed, install/update/upgrade, blockers, warnings, backup, exact command |
| `publish_customizations` | write | `pac solution publish` |
Typical flow: `list_solutions` → `export_solution` (zip + unpacked folder) → read offline with `source=` that
folder: `solution_components`, `describe_table`, `canvas_overview`, `search_formulas`, `canvas_data_sources` →
change the source → `pack_solution` → `import_solution` (dry run, then for real) → `publish_customizations`.
## Traps it checks for
Canvas apps kept as source (a generator or hand-maintained `.pa.yaml`, packed with `pac solution pack` and
imported) fail in ways Studio does not show:
- **Studio prunes unreferenced data sources** on the next save/publish: `canvas_data_sources` flags data sources
no formula uses (a Power Fx lexer skips strings and comments, so `"Saved to Orders"` is not a reference).
- **Empty `DatabaseReferences` in meta.xml** breaks every write after import (reads work, `Patch` fails): critical
in `canvas_data_sources`, a blocker in `import_solution`.
- **The msapp caches table schemas.** New columns are invisible to the *published* player until the cache is
refreshed; Studio validates against live metadata so it looks fine. On a solution source,
`canvas_data_sources` lists solution columns missing from the app's cached `TableDefinition` and the formulas
that use them. It also lists tables used in formulas that have no cache entry.
- **Dataverse keeps the newer canvas app on import** (AppVersion): `import_solution` warns when the environment
copy was modified on or after the package's AppVersion day, because the import would silently keep it.
- **Import as update, not upgrade**: upgrade deletes components missing from the zip.
- **`.pa.yaml` formulas containing `": "` must be `|-` block scalars**: a YAML parse error names this cause.
- Generated `.pa.yaml` in a schema-as-code repo should never be hand-edited (change the generator):
fxray has no tool that writes canvas source, and `protected_paths` keeps its output out of such a folder.
## pac quirks handled (2.9.3)
- `--json` works only for `env who`, `env list`, `solution list`; `auth list` and `canvas list` print tables with
multi-word headers (`Created by`, `Environment Url`), parsed by known column names.
- `env fetch --xml "<fetch ...>"` crashes pac (XmlException) when the query has quotes: queries go through `--xmlFile`.
- `env fetch` rejects `top`, ignores `count`/`page`/`order` and pages through **every** record: fxray stops
reading at the row cap (kills pac) and sorts client-side when the result is complete.
- `env fetch` pads each column to its widest *value*, so header names and values can run together
(`ismanagedsolutionid`); the header is split using the attribute names from the query.
- `env fetch` omits null attributes and shifts the rest of the row left. Every row is verified (the last column
must be the primary key GUID); if any row is shifted the query is re-run one attribute at a time and joined on
the key.
- Values are pac's formatted strings (choice labels, lookup names, local dates, `10,840`).
- The `solutioncomponent` type label is blank for newer component types (connection references); they are
resolved by id.
- Setting the environment variable `PP_TOOLS_AUTOMATION_AGENT` makes pac crash at startup (DI error); fxray
passes the environment through untouched, so do not set it.
- stdin is closed, so a pac waiting for an interactive sign-in times out with an explanation instead of hanging.
## Known limits
- No live column metadata: pac has no metadata command and the `attribute` table is too thin through FetchXML.
`describe_table` reads a solution export or a canvas app's cached schema; `list_tables` without a source gives
live table names only.
- `fetch_query` returns formatted strings, not typed values; link-entity columns cannot use the null fallback.
- The AppVersion check compares days (pac reports the environment app's modified date only).
- Canvas source must be the current `.pa.yaml` format (the retired `.fx.yaml` layout is refused with instructions).
- `solution_check` uploads the zip to Microsoft's checker service and takes about a minute.
## Layout
`src/fxray/` — pure `fx` (Power Fx lexer) / `fetchxml` / `pacparse` (pac output parsers) / `metadata` / `canvas` /
`solution` / `diff` → `pac` (subprocess runner behind a small interface, faked in tests) → `live` + `writes`
(plans, gates, backups) → `server.py` (the tools).
## Development
```powershell
git clone https://github.com/S-CurveLabs/fxray; cd fxray
python -m venv .venv
.venv\Scripts\pip install -e .[dev]
.venv\Scripts\pytest
```
Everything runs against synthetic solutions and a scripted fake pac. To exercise the real pac read path, sign pac
in, point `FXRAY_CONFIG` at a config that allowlists a development environment, and run
`set FXRAY_LIVE=1` then `pytest -m live` (reads only).
## License
MIT
TDQS
Scored across 23 tools
Each tool targets a distinct resource and action granularity: environment status, solution lifecycle, canvas app analysis, and Dataverse metadata are cleanly separated. Even similarly named tools like list_controls/get_control and list_tables/describe_table occupy clearly different levels of detail.
The majority of tools follow a verb_noun pattern such as list_solutions, export_solution, and describe_table, but several use noun-first names like canvas_overview, app_checker_results, and solution_components, with outliers like pac_status and whoami. The mix is readable but not a consistent convention.
At 23 tools this falls in the 16-25 range that feels heavy for a single server, even though the broad Power Platform domain justifies much of the surface. It could plausibly be split into separate environment/solution and canvas-app analysis servers.
The tool set covers a complete workflow from environment discovery, solution export/unpack/diff, canvas app deep inspection, Dataverse metadata queries, to pack/import/publish with dry-run safeguards. Export outputs feed directly into the offline analysis tools, so there are no obvious dead ends.