Metadata-Version: 2.5
Name: letin
Version: 0.2.0
Summary: MCP server for Power Query (M): inspect, refactor, lint, version and guide query edits in Excel workbooks and Power BI PBIP projects
Project-URL: Homepage, https://github.com/S-CurveLabs/letin
Project-URL: Repository, https://github.com/S-CurveLabs/letin
Project-URL: Issues, https://github.com/S-CurveLabs/letin/issues
Author: S-Curve Labs
License-Expression: MIT
License-File: LICENSE
Keywords: excel,m,mcp,model-context-protocol,pbip,power-bi,power-query,tmdl
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Requires-Dist: mcp>=1.2
Requires-Dist: pywin32>=306; sys_platform == 'win32'
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# letin

<!-- mcp-name: io.github.S-CurveLabs/letin -->

An MCP server that gives an AI assistant (GitHub Copilot in VS Code, Claude, …) real tools for
**managing Power Query (M) queries**, in Excel workbooks and in Power BI projects.

Copilot in Excel / Power BI can suggest M. It cannot see how your queries depend on each other,
rename one without breaking the rest, repoint forty queries at a new server, tell you which step
killed query folding, put your queries in git, **run** a query to see what a step really returns, or
check the M it just wrote against the real function library. This server does those things.

## What it does

| Area | Tools |
|---|---|
| **Inspect** | `list_sources` `list_queries` `get_query` `dependency_graph` (JSON or Mermaid, lineage, orphans, cycles, refresh order) `list_data_sources` `find_in_queries` |
| **Guided actions** | `list_transforms` `apply_transform` (29 editor actions as parameters, not hand-written M: unpivot, pivot, group by, merge, append, filter, types, conditional column, split, fill, dedupe, date parts, …) `remove_step` |
| **Bulk edit / refactor** | `rename_query` `rename_step` (reference-safe) `replace_in_queries` (strings-only by default) `extract_parameter` `copy_queries` (with dependencies) `set_query` `delete_query` |
| **Lint / performance** | `lint_queries`: folding breakers before filters, hard-coded paths/servers/URLs, `Csv.Document` pinned column counts, dead steps, wrong `in` step, untyped `Table.AddColumn`, repeated Changed Type, brittle `RemoveColumns`, late filtering, duplicated staging logic, orphans, cycles |
| **Version control** | `export_queries` / `import_queries` (one `.pq` per query + manifest) `diff_queries` `list_snapshots` `restore_snapshot` |
| **Run M** | `evaluate_query` (whole query or **up to a step**) `evaluate_steps` (every step of a query in **one** engine call, with the step where an error first appears) `evaluate_m` (any expression, may reference the source's queries): real column names, types, sample rows, row count, `Table.Profile`, or the engine's error. Runs in a private hidden Excel on a throwaway copy, under a timeout |
| **M language** | `validate_m` (syntax errors with line/column and cause; unknown steps/functions with did-you-mean; wrong argument counts) `format_m` / `format_queries` (token-verified pretty-printer, comments kept) `m_function_help` (858 library members with signatures, generated from the engine's `#shared`) |
| **Excel live** | `refresh_query` (returns the engine's error text) `preview_query` |

Every write **snapshots first**, returns a **unified diff**, accepts `dry_run=true`, and is **refused if it would turn valid M into a syntax error**.
All edits are **token-aware**: a query called `Sales` is never confused with a column `[Sales]`,
the string `"Sales"`, or a comment.

## Sources

Every tool takes a `source`:

| `source` | Backed by | Read | Write |
|---|---|---|---|
| `C:\path\book.xlsx` (`.xlsm`, `.xlsb`) | Closed: the file's `DataMashup` part, Excel is not started. Open in Excel: live COM. | ✅ | ✅ via COM. Already open → edited live, **left unsaved for you to review**. Closed → opened hidden with macros disabled, saved, closed. |
| `excel:active` | The workbook in front in Excel | ✅ | ✅ |
| `Report.pbip`, its folder, or `X.SemanticModel` | TMDL files (`tables/*.tmdl`, `expressions.tmdl`) | ✅ | ✅ M of any query; add/rename/delete for shared expressions only (renaming a *table* must be done in Desktop) |
| `model.bim` (or a project that contains one) | TMSL JSON: legacy PBIP, Tabular Editor, SSAS/AAS projects | ✅ | ✅ same rules as TMDL; string-vs-line-array form of each expression is kept |
| `pbi:desktop`, `pbi:desktop:<port>`, `pbi:desktop:<title>` | The model of a **running Power BI Desktop** (works for `.pbix`, sees unsaved edits), read through Desktop's own ADOMD client | ✅ | ❌ read-only by design: inspect, lint, diff, export, `copy_queries` out of it |
| `Orders.pq` / `.m` | One query per file, named after the file | ✅ | ✅ |
| `mashup.pq` (any section document) | `section Section1; shared A = ...;` e.g. Fabric **Dataflow Gen2** git items, Power Query SDK | ✅ | ✅ member-level edits; `[attributes]` and comments survive |
| a folder of `.pq` files | `export_queries` output | ✅ | ✅ |

A `.pbix` *file* is refused with instructions (save as **.pbip**, or open it and use `pbi:desktop`). Desktop must be
closed/reopened to pick up PBIP edits (that is how Desktop works, not a limitation of this server).

### How evaluation works

`evaluate_*` never runs anything in your session. A private, hidden, macro-disabled Excel opens a **throwaway copy** of the
workbook (unsaved edits included), or for non-Excel sources a blank workbook seeded with the source's queries. Your expression
is wrapped in `try`, the outcome is serialised to JSON by M itself, landed on a scratch sheet and read back. The refresh runs
in the background under a watchdog; on timeout that Excel is killed, so a credential or privacy prompt cannot hang the server.
Typical cost: 7-10 s per call. Non-Excel sources are evaluated by *Excel's* engine: Power BI-only connectors are unavailable
and credentials come from Excel's data source settings for your Windows user. `ignore_privacy_levels=true` turns the privacy
firewall off for that one throwaway evaluation.

## Install

Windows, Python 3.10+, Excel installed for workbook editing. letin is on [PyPI](https://pypi.org/project/letin/)
and in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.S-CurveLabs/letin`.
The simplest way to run it is with [uv](https://docs.astral.sh/uv/), which needs no separate install step:

```powershell
uvx letin
```

Or install it with `pip install letin` and run `letin`.

### GitHub Copilot in VS Code

Run *MCP: Open User Configuration* from the command palette and add:

```json
{
  "servers": {
    "letin": {
      "type": "stdio",
      "command": "uvx",
      "args": ["letin"]
    }
  }
}
```

Then in Copilot Chat switch to **Agent** mode and check that the `letin` tools are enabled.

### Claude Code

```powershell
claude mcp add letin -- uvx letin
```

### From source

```powershell
git clone https://github.com/S-CurveLabs/letin.git
cd letin
python -m venv .venv
.\.venv\Scripts\pip install -e .
```

Opening the clone in VS Code picks up `.vscode/mcp.json`, which runs that checkout with `.venv\Scripts\python.exe -m letin`.

## Things to ask

- "List the queries in `C:\Reports\Sales.xlsx` and draw the dependency graph as Mermaid."
- "Lint it. Which steps are breaking query folding?"
- "Every query points at `\\oldserver\finance`. Move that into a parameter called `SourceFolder`."
- "Rename `Query1` to `stg_Orders` everywhere."
- "In `Budget`, unpivot everything except Department and Account, then group by Department summing Value."
- "Export the queries to `./queries` so I can commit them." / "What changed in the workbook since that export?"
- "Undo that last change."
- "What columns does `Sales` have after the *Merged Queries* step? Show me 5 rows." / "Walk me through what every step of `Sales` does." / "At which step does the error first appear?"
- "Write a query that pivots months into columns, validate it, run it, then save it as `Sales by Month`."
- "What's the signature of `Table.NestedJoin`? Which functions split text?"
- "Format every query in this workbook." / "What queries are in the report I have open in Power BI Desktop?"

## Safety model

- Snapshots live in `%LOCALAPPDATA%\letin\snapshots\<workbook>-<hash>\` (last 50 per source);
  override the root with `LETIN_HOME`.
- Formulas with unbalanced brackets or unterminated strings are rejected before they reach the host.
- Hidden Excel instances are started with `AutomationSecurity = ForceDisable`: workbook macros never run.
- `refresh_query` only works on a workbook you have open, because a hidden refresh can block forever on a
  credentials or privacy-level prompt.
- The server itself makes no network calls. Only `evaluate_*` and `refresh_query` run M, always in Excel's engine, which then
  contacts whatever data sources that M reads, exactly as a refresh in Excel would. Every other tool is static analysis.
- Evaluation happens on a throwaway copy in a separate hidden Excel, under a timeout; nothing open on your screen is modified.

## Limits (v0.2)

- Evaluation needs Excel on the machine, and uses Excel's engine even for Power BI sources (see above).
- Live Power BI Desktop is read-only, and needs Windows PowerShell 5.1 (built into Windows) to drive Desktop's ADOMD client.
- Reference analysis is lexical. Names built at runtime (`Expression.Evaluate`, `#shared`) are reported, not rewritten.
- The validator knows the library of *this machine's Excel*; connectors that exist only in Power BI are reported as `info`, never as errors.
  Regenerate the catalog after an Office update with `python scripts/build_m_library.py`.
- Lint rules about folding are heuristics; confirm with *View Native Query*.
- `model.bim` is re-serialised as 2-space JSON, so the first diff can be noisy if your tool formats it differently.

## Development

```powershell
.\.venv\Scripts\pip install -e ".[dev]"
.\.venv\Scripts\pytest -m "not excel"   # pure tests
.\.venv\Scripts\pytest                  # also drives a real hidden Excel
```

Layout: `m/` (lexer, structural parser, full syntax parser, validator, formatter) → `graph.py` `lint.py` `refactor.py`
`transforms.py` (pure, host-agnostic) → `hosts/` (Excel COM + file, PBIP/TMDL, model.bim, live Desktop, M files, folder)
→ `evaluate.py` (hidden-Excel evaluation) → `server.py` (MCP tools). `data/m_library.json` is generated by
`scripts/build_m_library.py`.

## License

MIT. See [LICENSE](LICENSE).
