{
  "markdown": "# pbirb-mcp\n\n[![CI](https://github.com/mafaq229/pbirb-mcp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mafaq229/pbirb-mcp/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/pbirb-mcp.svg)](https://pypi.org/project/pbirb-mcp/)\n[![Python versions](https://img.shields.io/pypi/pyversions/pbirb-mcp.svg)](https://pypi.org/project/pbirb-mcp/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nAn MCP server for editing **Power BI Report Builder paginated reports** (`.rdl`)\nthrough Claude (Desktop, CLI, or any MCP client). 140+ tools cover the gaps\nthat otherwise force hand-written XML: report creation, data sources and\ndatasets, calculated fields, dataset and tablix filters, groupings (row,\ncolumn, matrix), sorting, charts, headers and footers, body composition,\nlayout containers, positioning, styling, page setup, pagination, advanced\nparameters, embedded images, interactivity (actions, tooltips, document\nmap), transactions, and validation.\n\nThe server speaks JSON-RPC 2.0 over stdio. It opens an `.rdl` from disk,\nmutates it in place via lxml, validates structure, and writes atomically — a\nfailed save never leaves a half-written report or scrubs the original.\n\n## Stability\n\nPre-1.0. The tool surface — tool names, `inputSchema`, output shapes, error\nsemantics — is the contract. While on `0.x`, MINOR releases may include a\nsmall breaking change with a migration note in\n[CHANGELOG.md](CHANGELOG.md); after v1.0, breaking changes require MAJOR.\nSee [CONTRIBUTING.md](CONTRIBUTING.md#versioning) for the full bump rules\nadapted from SemVer for an MCP tool surface.\n\nPin to a MINOR while on `0.x` (e.g. `pbirb-mcp~=0.1`) if your prompts\ndepend on specific tool names or schemas.\n\n---\n\n## Quick start\n\n### 1. Install\n\nThe simplest path is [uv](https://docs.astral.sh/uv/) + PyPI — no clone, no\nvenv, no install step:\n\n```bash\nuvx pbirb-mcp\n```\n\nuvx fetches the package into a throwaway environment, runs the\n`pbirb-mcp` console script, and exits. The MCP server speaks JSON-RPC\nover stdio, so any MCP client (Claude Desktop, Claude Code, etc.) can\nspawn it directly.\n\nFor local development against this codebase instead:\n\n```bash\ngit clone https://github.com/mafaq229/pbirb-mcp\ncd pbirb-mcp\nuv venv .venv\nuv pip install --python .venv/bin/python -e \".[dev]\"\n```\n\nVerify the binary works:\n\n```bash\nprintf '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}\\n' \\\n  | .venv/bin/pbirb-mcp\n```\n\nYou should see a single JSON-RPC response with `protocolVersion`,\n`capabilities.tools`, and `serverInfo.name = \"pbirb-mcp\"`.\n\n### 2. Wire into Claude Desktop\n\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)\nor `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"pbirb\": {\n      \"command\": \"uvx\",\n      \"args\": [\"pbirb-mcp\"]\n    }\n  }\n}\n```\n\nRestart Claude Desktop. The hammer icon should show `pbirb` and the 140+ tools\nlisted below.\n\nTo enable file logging, add an `env` block — but keep it platform-appropriate.\n`PBIRB_MCP_LOG_FILE` takes an OS-native path: a Unix path like\n`/tmp/pbirb-mcp.log` only works on macOS/Linux. On Windows use a Windows path\n(e.g. `%TEMP%\\\\pbirb-mcp.log`). See [Logging](#logging). When unset, logs go to\nstderr, which Claude Desktop captures in its MCP debug pane on every platform.\n\nFor development against a local checkout, swap the `args` for\n`[\"--from\", \"/absolute/path/to/pbirb-mcp\", \"pbirb-mcp\"]` so uvx runs\nyour working tree instead of the published version.\n\n### 3. Wire into Claude Code\n\n```bash\nclaude mcp add pbirb -- uvx pbirb-mcp\n```\n\nOr add to `.mcp.json` at the workspace root:\n\n```json\n{\n  \"mcpServers\": {\n    \"pbirb\": {\n      \"command\": \"uvx\",\n      \"args\": [\"pbirb-mcp\"]\n    }\n  }\n}\n```\n\n**Or install it as a Claude Code plugin** — one command instead of editing\nconfig by hand (it wires up the same `uvx pbirb-mcp` server for you):\n\n```bash\n/plugin marketplace add mafaq229/pbirb-mcp\n/plugin install pbirb-mcp@pbirb\n```\n\n### 4. Smoke test\n\nIn any chat, ask the model:\n\n> Open `~/path/to/some-report.rdl` and tell me what datasets and parameters it has.\n\nThe model should call `describe_report`, `get_datasets`, and `get_parameters`\nand summarise the result. If it doesn't, your client probably hasn't loaded\nthe server — check the client's MCP debug log.\n\n---\n\n## Working with a report\n\nThe recommended flow is **read first, then edit by name**. Every mutating\ntool addresses elements by their RDL `Name` attribute (Tablix name,\nTextbox name, ReportParameter name, DataSet name) — never by index.\nIndices break across multi-step edits.\n\nA typical session:\n\n```text\nYou:    Bind the existing report at ~/Reports/sales.rdl to the Finance\n        workspace's \"FY26 Sales\" dataset, change the body DAX to a date-\n        ranged query, and add a header with a logo and a parameter line.\n\nClaude: → describe_report(path=…)\n        → set_datasource_connection(name=\"PBIDataset\",\n              workspace_url=\"Finance\", dataset_name=\"FY26 Sales\")\n        → update_dataset_query(dataset_name=\"MainDataset\",\n              dax_body=\"EVALUATE FILTER('Sales',\n                'Sales'[OrderDate] >= @DateFrom &&\n                'Sales'[OrderDate] <= @DateTo)\")\n        → add_query_parameter(dataset_name=\"MainDataset\",\n              name=\"DateFrom\",\n              value_expression=\"=Parameters!DateFrom.Value\")\n        → add_query_parameter(dataset_name=\"MainDataset\",\n              name=\"DateTo\",\n              value_expression=\"=Parameters!DateTo.Value\")\n        → add_embedded_image(name=\"Logo\",\n              mime_type=\"image/png\", image_path=\"~/brand/logo.png\")\n        → set_page_header(height=\"0.75in\",\n              print_on_first_page=True, print_on_last_page=True)\n        → add_header_image(name=\"HeaderLogo\",\n              image_source=\"Embedded\", value=\"Logo\",\n              top=\"0in\", left=\"0in\", width=\"1in\", height=\"0.5in\")\n        → add_header_textbox(name=\"DateLine\",\n              text='=\"From: \" & Parameters!DateFrom.Value & \"  To: \"\n                    & Parameters!DateTo.Value',\n              top=\"0.1in\", left=\"1.2in\", width=\"4in\", height=\"0.25in\")\n```\n\nOpen the resulting `.rdl` in Report Builder; the change is visible in the\ndesigner and renders in Preview against the bound dataset.\n\n---\n\n## Tool reference\n\n143 tools, grouped by RDL concern. The highlights of each group are tabled\nbelow; the authoritative, always-current list with full schemas is the\nserver's `tools/list` output, visible to the LLM at registration time. Every\ntool takes a `path` argument (absolute path to the `.rdl`).\n\nMost mutating tools also accept an optional `transaction_id` so a multi-step\nedit batches into a single atomic save — see [Transactions](#transactions--validation)\nand [docs/TRANSACTIONS.md](docs/TRANSACTIONS.md).\n\n### Read-only inventory\n\nThe \"what's in this report?\" tools. Always the first calls when planning a\nmulti-step edit.\n\n| Tool | Returns |\n|------|---------|\n| `describe_report` | Top-level inventory: data sources, datasets, parameters, tablixes, page setup |\n| `get_datasets` / `get_dataset` | Full DAX command text, fields, query parameters, dataset filters (all, or one by name) |\n| `list_data_sources` / `get_data_source` | Data source inventory; one source's connection details |\n| `get_parameters` | Report parameters with data type, prompt, and flags (multi-value, hidden, nullable, allow-blank) |\n| `get_tablixes` | Tablix layout: columns, row/column groups, sort expressions, filters, visibility |\n| `list_tablix_filters` / `list_dataset_filters` | Filters in document order with stable indices |\n| `list_body_items` / `list_header_items` / `list_footer_items` | Named report items in each region |\n| `get_textbox` / `get_image` / `get_rectangle` / `get_chart` | Full properties of a named report item |\n| `list_embedded_images` / `get_embedded_image_data` | Embedded image names + MIME types; base64 bytes of one |\n| `get_expression_reference` | Cheat-sheet of common RDL expression patterns (`count_where`, `sum_where`, `iif_format` helpers build these) |\n\n### Datasource & dataset\n\n| Tool | What it edits |\n|------|---------------|\n| `set_datasource_connection` | Repoint a `<DataSource>` at a Power BI XMLA endpoint. `DataProvider=SQL` (the AS provider id). |\n| `add_data_source` / `remove_data_source` / `rename_data_source` | Manage `<DataSource>` elements |\n| `update_dataset_query` | Replace `<DataSet>/<Query>/<CommandText>` with a DAX expression |\n| `add_query_parameter` | Append `<QueryParameter>` (e.g. `=Parameters!DateFrom.Value`) |\n| `update_query_parameter` | Change the value expression of an existing query parameter |\n| `remove_query_parameter` | Drop a query parameter (and clean up empty `<QueryParameters>`) |\n| `add_dataset_field` / `remove_dataset_field` | Manage `<Field>` entries on a dataset |\n| `add_calculated_field` / `remove_calculated_field` | Manage `<Value>`-backed calculated fields |\n| `refresh_dataset_fields` | Re-derive the `<Fields>` list from the query's column metadata |\n| `add_dataset_filter` / `remove_dataset_filter` | Filters applied at the dataset level (vs. tablix) |\n\n### Tablix\n\n| Tool | What it edits |\n|------|---------------|\n| `add_tablix_filter` | Append a `<Filter>`. Operators: Equal, NotEqual, GreaterThan, In, Between, Like, TopN, ... |\n| `remove_tablix_filter` | Remove by ordinal index from `list_tablix_filters` |\n| `add_row_group` / `remove_row_group` | Wrap the row hierarchy in a new outer group + header row (and its inverse) |\n| `add_column_group` / `remove_column_group` | Same, on the column axis |\n| `convert_to_matrix` | Promote a table to a matrix (row + column groups) — see [docs/MATRIX-cookbook.md](docs/MATRIX-cookbook.md) |\n| `set_tablix_corner` | Set the matrix corner cell text/expression |\n| `set_group_sort` / `set_column_group_sort` | Replace `<SortExpressions>` on a group |\n| `set_group_visibility` / `set_column_group_visibility` | Set `<Visibility>` on a group's TablixMember |\n| `set_detail_row_visibility` | Set `<Visibility>` on the Details group |\n| `add_tablix_column` / `remove_tablix_column` | Add/drop a column across the tablix grid |\n| `add_static_row` / `add_static_column` | Insert a non-grouped row/column |\n| `add_subtotal_row` / `add_subtotal_column` | Insert an aggregate row/column on a group |\n| `set_cell_span` | Set `RowSpan` / `ColSpan` on a cell |\n| `set_column_width` / `set_row_height` | Set `<Width>` / `<Height>` on the Nth column/row |\n| `set_tablix_size` | Set the tablix's overall `<Width>` / `<Height>` |\n\n### Page\n\n| Tool | What it edits |\n|------|---------------|\n| `set_page_setup` | Page dimensions, margins, columns. All fields optional. |\n| `set_page_orientation` | Swap PageHeight/PageWidth to match `Portrait` or `Landscape`. Idempotent. |\n\n### Page header & footer\n\nSame set of operations for each region, each accepts named items so\nfollow-up edits don't drift on indices.\n\n| Tool | What it edits |\n|------|---------------|\n| `set_page_header` / `set_page_footer` | Section height + `PrintOnFirstPage` / `PrintOnLastPage` |\n| `add_header_textbox` / `add_footer_textbox` | Append a Textbox (static text or `=expression`) |\n| `add_header_image` / `add_footer_image` | Append an Image (External URL, Embedded name, or Database expression) |\n| `remove_header_item` / `remove_footer_item` | Remove by name; tidies empty `<ReportItems>` |\n\n### Body composition\n\n| Tool | What it edits |\n|------|---------------|\n| `add_body_textbox` | Append a Textbox to `<Body>/<ReportItems>` |\n| `add_body_image` | Append an Image to the body |\n| `remove_body_item` | Remove a named Textbox / Image / Tablix from the body |\n\n### Snippet templates\n\nSingle-call inserts of common report items, programmatically built and\nappended to the body.\n\n| Tool | What it builds |\n|------|----------------|\n| `insert_tablix_from_template` | A basic Tablix mirroring the fixture's shape — header row with the column name as a static label, detail row binding to `=Fields!<column>.Value`. One column per requested field. |\n| `insert_chart_from_template` | A basic Column chart: single category axis grouped by `category_field`, single Y series `=Sum(Fields!<value_field>.Value)`. Change `<Type>` post-insert (Bar / Line / Pie / etc.). |\n\n### Charts\n\nRefine a chart after `insert_chart_from_template` (or any existing `<Chart>`).\n\n| Tool | What it edits |\n|------|---------------|\n| `add_chart_series` / `remove_chart_series` | Manage Y-axis `<ChartSeries>` |\n| `set_chart_series_type` | Column / Bar / Line / Area / Pie / ... per series |\n| `set_chart_series_grouping` | Category/series grouping expression |\n| `set_chart_axis` | Category / value axis title, scale, format |\n| `set_chart_legend` | Legend visibility and placement |\n| `set_chart_data_labels` | Toggle and format data labels |\n| `set_chart_title` | Chart title text/expression |\n| `set_chart_palette` / `set_series_color` | Palette name; explicit per-series color |\n\n### Styling\n\n| Tool | What it edits |\n|------|---------------|\n| `set_textbox_style` | Routes properties to the right nested `<Style>` node automatically: box-level (BackgroundColor, Border, VerticalAlign), paragraph-level (TextAlign), run-level (FontFamily, FontSize, FontWeight, Color, Format) |\n| `set_textbox_style_bulk` | Apply one style to many textboxes in a single call |\n| `set_textbox_runs` / `set_textbox_value` | Rich multi-run paragraph content; or replace the value |\n| `find_textboxes_by_style` / `find_textbox_by_value` | Locate textboxes to target follow-up edits |\n| `style_tablix_row` | Style every cell of a tablix row at once (header / detail / footer) |\n| `set_alternating_row_color` | Zebra-stripe a tablix's detail row with `BackgroundColor=IIf(RowNumber(Nothing) Mod 2, \"<a>\", \"<b>\")` |\n| `set_conditional_row_color` | Drive detail-row `BackgroundColor` from an expression |\n| `set_image_sizing` / `set_image_source` | Image `<Sizing>`; switch External / Embedded / Database source |\n\n### Visibility\n\n| Tool | What it edits |\n|------|---------------|\n| `set_element_visibility` | Set `<Visibility>` on any named ReportItem (Tablix, Textbox, Image, Rectangle, Subreport, Chart). Group / detail-row visibility have their own tools. |\n\n### Layout containers\n\n| Tool | What it builds |\n|------|----------------|\n| `add_rectangle` | A `<Rectangle>` container (group other items, control page breaks) |\n| `add_list` | A list region (single-column tablix template) |\n| `add_line` | A `<Line>` report item |\n\n### Positioning & sizing\n\nMove and resize named items in each region. Coordinates are RDL sizes\n(`\"1in\"`, `\"2.5cm\"`, ...).\n\n| Tool | What it edits |\n|------|---------------|\n| `set_body_item_position` / `set_header_item_position` / `set_footer_item_position` | `Top` / `Left` of a named item |\n| `set_body_item_size` / `set_header_item_size` / `set_footer_item_size` | `Width` / `Height` of a named item |\n| `set_body_size` | The `<Body>` region's overall height |\n\n### Interactivity\n\n| Tool | What it edits |\n|------|---------------|\n| `set_textbox_action` / `set_image_action` / `set_chart_series_action` | `<Action>`: hyperlink, drill-through, or bookmark |\n| `set_textbox_tooltip` | Textbox `<ToolTip>` |\n| `set_document_map_label` | `<DocumentMapLabel>` for the navigation pane |\n\n### Pagination\n\n| Tool | What it edits |\n|------|---------------|\n| `set_group_page_break` | `<Group><PageBreak>` (Start / End / Between) |\n| `set_repeat_on_new_page` | Repeat a group header/footer on each page |\n| `set_keep_together` / `set_keep_with_group` | Keep-together rendering hints |\n\n### Parameters (advanced)\n\n| Tool | What it edits |\n|------|---------------|\n| `add_parameter` / `remove_parameter` / `rename_parameter` | Manage `<ReportParameter>` elements |\n| `set_parameter_prompt` / `set_parameter_type` | Prompt text; data type (Boolean / DateTime / Integer / Float / Text) |\n| `set_parameter_available_values` | Static `<ParameterValues>` list (strings or `{value, label}` dicts) **or** `<DataSetReference>` to a lookup dataset |\n| `set_parameter_default_values` | Static `<Values>` list **or** `<DataSetReference>` (defaults take ValueField only — defaults are values, not display strings) |\n| `update_parameter_advanced` | Toggle the four boolean flags: `multi_value`, `hidden`, `allow_null` (writes `<Nullable>`), `allow_blank` |\n| `reorder_parameters` | Reorder `<ReportParameters>` (controls prompt order) |\n| `set_parameter_layout` / `sync_parameter_layout` | Position parameters in the `<ReportParametersLayout>` grid |\n\n#### Cascading parameters\n\nRDL has no `<DependsOn>` element — cascading is inferred from\n`=Parameters!X.Value` references in a lookup dataset's `<QueryParameters>`.\nTo wire parameter `B` to depend on parameter `A`:\n\n1. `set_parameter_available_values(name=\"B\", source=\"query\", query_dataset=\"LookupB\", ...)`\n2. `add_query_parameter(dataset_name=\"LookupB\", name=\"@A\", value_expression=\"=Parameters!A.Value\")`\n\nReport Builder figures out the dependency graph by parsing those expressions.\n\n### Embedded images\n\n| Tool | What it edits |\n|------|---------------|\n| `add_embedded_image` | Read a real file off disk, base64-encode it, store under `<EmbeddedImages>` |\n| `list_embedded_images` | Names + MIME types |\n| `remove_embedded_image` | Remove by name; tidies empty `<EmbeddedImages>` |\n\nReference an embedded image with\n`add_*_image(image_source=\"Embedded\", value=\"<image-name>\")`.\n\n### Report lifecycle\n\n| Tool | What it does |\n|------|--------------|\n| `create_report` | Scratch-create a minimal valid `.rdl` to start from |\n| `duplicate_report` | Copy a report to a new path |\n| `backup_report` / `restore_from_backup` | Snapshot a report and roll back to it |\n\n### Expression helpers\n\nThese don't mutate the report — they build correct RDL expression strings to\npass into other tools (text, filters, conditional styling).\n\n| Tool | What it returns |\n|------|-----------------|\n| `count_where` / `sum_where` | A `Count`/`Sum` aggregate expression with an inline condition |\n| `iif_format` | An `IIf(...)` expression for conditional values/formatting |\n| `get_expression_reference` | A reference sheet of common RDL expression patterns |\n\n### Transactions & validation\n\nBatch many edits into one atomic save, and check correctness before/after.\n\n| Tool | What it does |\n|------|--------------|\n| `start_editing_transaction` / `commit_editing_transaction` / `cancel_editing_transaction` | Open an in-memory transaction, lint-and-save once, or discard. See [docs/TRANSACTIONS.md](docs/TRANSACTIONS.md). |\n| `apply_edits` | Apply a list of tool calls in one transaction |\n| `dry_run_edit` | Preview an edit's effect without writing to disk |\n| `validate_report` / `verify_report` | Structural validation (and opt-in XSD validation against the bundled `reportdefinition.xsd`) |\n| `lint_report` | Surface warnings/errors Report Builder would flag |\n\n### Raw XML escape hatch\n\n| Tool | What it does |\n|------|--------------|\n| `raw_xml_view` | Read the XML under an XPath |\n| `raw_xml_replace` | Replace the XML at an XPath — last resort for anything without a dedicated tool |\n\n---\n\n## Power BI specifics\n\n### XMLA connection strings\n\n`set_datasource_connection` writes the canonical form:\n\n```\nData Source=powerbi://api.powerbi.com/v1.0/myorg/<workspace>;Initial Catalog=<dataset>\n```\n\n`workspace_url` accepts a bare workspace name (`Finance`) or a full\n`powerbi://` URL — the tool detects the latter and avoids double-prefixing.\n`DataProvider` is set to `SQL` (the Analysis Services provider id RDL uses\nfor PBI XMLA, despite the misleading name).\n\n### DAX queries\n\nDAX bodies are accepted verbatim — `pbirb-mcp` doesn't parse DAX, so the\nuser (or Report Builder at preview time) is the source of truth for syntax.\nEmpty bodies are rejected up front because Report Builder loads them but\nerrors at preview, which is a worse signal than a clear `ValueError` here.\n\nPBI paginated reports do **not** carry `<CommandType>` for DAX (unlike\nSSRS where you'd set `CommandType=StoredProcedure`); these tools never\nemit it.\n\n### Pre-commit hooks (contributors)\n\n`pre-commit` is in the `[dev]` extras. After a fresh checkout:\n\n```bash\nuv pip install --python .venv/bin/python -e \".[dev]\"\n.venv/bin/pre-commit install                # one-time — installs the git hook\n.venv/bin/pre-commit run --all-files        # one-time — clean any drift\n```\n\nAfter the hook is installed, every `git commit` runs ruff format +\nruff check (with `--fix`) + the fast pytest suite. If lint or tests\nfail the commit is aborted; fix and re-stage before retrying.\n\nTo run individual hooks ad-hoc:\n\n```bash\n.venv/bin/pre-commit run ruff --all-files\n.venv/bin/pre-commit run ruff-format --all-files\n.venv/bin/pre-commit run pytest-fast --all-files\n```\n\nThe full config lives in `.pre-commit-config.yaml`. Ruff settings\n(line length, rule selection, per-file ignores) live under\n`[tool.ruff*]` in `pyproject.toml`.\n\n### Report Builder install\n\nPower BI Report Builder is a free Microsoft-distributed Windows app:\n\n> https://www.microsoft.com/en-us/download/details.aspx?id=105942\n\nOpen any `.rdl` produced by `pbirb-mcp` directly in Report Builder. The\n\"opens cleanly with no upgrade prompt\" check is the actual integration\ntest — the unit tests verify schema correctness, but lxml will\nround-trip XML that Report Builder's deserialiser still rejects (we hit\nthis twice in the chart-template work; both fixes are documented in the\ngit history).\n\n---\n\n## Architecture\n\n```\npbirb-mcp/\n├── pbirb_mcp_server.py         # Entry point (logging + main())\n├── pbirb_mcp/\n│   ├── server.py               # JSON-RPC stdio dispatch\n│   ├── tools.py                # Tool registry — wires ops into the server\n│   ├── core/\n│   │   ├── document.py         # RDLDocument: open/save (lxml), atomic write\n│   │   ├── xpath.py            # Namespace-aware XPath helpers\n│   │   ├── ids.py              # Stable element addressing\n│   │   ├── encoding.py         # XML declaration / self-closing-tag fidelity\n│   │   ├── transactions.py     # In-memory transaction registry\n│   │   └── schema.py           # Structural + opt-in XSD validation\n│   ├── ops/                    # One module per RDL concern; wired into tools.py\n│   │   ├── reader.py           # describe / get_datasets / get_params / get_tablixes\n│   │   ├── datasource.py       # PBI XMLA connection + data-source management\n│   │   ├── dataset.py          # DAX body, query params, fields, calc fields, filters\n│   │   ├── tablix.py           # Row/column groups, sort, visibility, matrix\n│   │   ├── tablix_columns.py   # Add/remove tablix columns\n│   │   ├── tablix_cells.py     # Cell span\n│   │   ├── tablix_static.py    # Static rows / columns\n│   │   ├── tablix_subtotals.py # Subtotal rows / columns\n│   │   ├── chart.py            # Chart series, axes, legend, labels, palette\n│   │   ├── page.py             # Page setup + orientation\n│   │   ├── layout.py           # Pagination (page breaks, keep-together, repeat)\n│   │   ├── header_footer.py    # Page header / footer authoring\n│   │   ├── body.py             # Body textboxes / images / containers / removal\n│   │   ├── positioning.py      # Move / resize named items per region\n│   │   ├── templates.py        # Chart + tablix snippet builders\n│   │   ├── styling.py          # Textbox styles, runs, bulk, row styling, find\n│   │   ├── images.py           # Image sizing / source\n│   │   ├── actions.py          # Actions, tooltips, document-map labels\n│   │   ├── visibility.py       # Element-level visibility\n│   │   ├── parameters.py       # Lifecycle, values, advanced flags, layout\n│   │   ├── embedded_images.py  # Base64 image embedding\n│   │   ├── expressions.py      # Expression-builder helpers (count/sum/iif)\n│   │   ├── filter_types.py     # Filter operator definitions\n│   │   ├── clone.py            # duplicate_report\n│   │   ├── scratch.py          # create_report\n│   │   ├── snapshot.py         # backup / restore\n│   │   ├── transactions.py     # start/commit/cancel + apply_edits\n│   │   ├── dry_run.py          # dry_run_edit\n│   │   ├── validate.py         # validate / verify\n│   │   ├── lint.py             # lint_report\n│   │   └── escape.py           # raw_xml_view / raw_xml_replace\n│   └── schemas/                # Bundled RDL 2016 XSD (reportdefinition.xsd) for opt-in validation\n└── tests/\n    ├── fixtures/\n    │   └── pbi_paginated_minimal.rdl  # Hand-tuned to match Report Builder's emitted style\n    └── test_*.py               # 1188 tests — every tool plus round-trip invariants\n```\n\n### Hard rules (enforced by tests)\n\n- **Tests first.** Every commit writes failing tests, then makes them pass.\n- **lxml, not stdlib `xml.etree`.** Round-trip fidelity is a feature, not\n  polish. Report Builder reads what's on disk; formatting drift causes\n  silent corruption.\n- **Stable IDs, never indices.** Tools take `tablix_name` + `group_name`,\n  not `column_index: 2`. Indices break across multi-step edits.\n- **Atomic save.** `RDLDocument.save_as` writes to `<path>.tmp` then renames.\n  A failure mid-write never leaves a half-written report.\n- **Round-trip byte-identity is enforced** by\n  `tests/test_document.py`'s `test_round_trip_byte_identical_to_fixture`.\n  A no-op open → save → reopen produces a byte-identical file.\n\n### RDL gotchas learned the hard way\n\n- `<?xml version=\"1.0\" encoding=\"utf-8\"?>` uses **double quotes**, not\n  lxml's default single quotes. Fixed in `RDLDocument.save_as` via a\n  manual declaration.\n- Self-closing tags use `<Tag />` with a space, not `<Tag/>`. Fixed via\n  a post-process regex.\n- The `rd:` prefix\n  (`http://schemas.microsoft.com/SQLServer/reporting/reportdesigner`)\n  carries designer metadata Report Builder relies on. Don't strip it;\n  preserve prefixes.\n- Do **not** put `MustUnderstand=\"df\"` on `<Report>` unless you also\n  declare `xmlns:df=...`.\n- `<ChartCategoryAxes>` / `<ChartValueAxes>` hold `<ChartAxis>`\n  children **directly** — there is no `<ChartCategoryAxis>` /\n  `<ChartValueAxis>` wrapper.\n- `<ChartMember>` requires a `<Label>` child, even an empty one.\n- DAX queries live in\n  `<DataSet><Query><CommandText>EVALUATE ...</CommandText></Query></DataSet>`.\n  No `<CommandType>` element for DAX (unlike SSRS).\n\n---\n\n## Logging\n\nTwo environment variables control the logger:\n\n| Variable | Default | Purpose |\n|----------|---------|---------|\n| `PBIRB_MCP_LOG_LEVEL` | `WARNING` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |\n| `PBIRB_MCP_LOG_FILE` | stderr | Path to a log file; otherwise logs go to stderr (where Claude Desktop captures them in its MCP debug pane) |\n\n```bash\nPBIRB_MCP_LOG_LEVEL=DEBUG PBIRB_MCP_LOG_FILE=/tmp/pbirb-mcp.log pbirb-mcp\n```\n\n---\n\n## Development\n\n### Running tests\n\n```bash\n.venv/bin/python -m pytest tests/ -v\n```\n\nThe suite is fast (~1.7s for 1188 tests) so re-run on every change.\n\n### Smoke testing the live binary\n\n```bash\nprintf '%s\\n' \\\n  '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' \\\n  '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}' \\\n  | .venv/bin/pbirb-mcp\n```\n\nFor an end-to-end sanity check, drive an actual mutation against a copy of\nthe bundled fixture:\n\n```bash\nSCRATCH=$(mktemp -d)/r.rdl\ncp tests/fixtures/pbi_paginated_minimal.rdl \"$SCRATCH\"\n.venv/bin/python -c \"\nfrom pbirb_mcp.ops.dataset import update_dataset_query\nupdate_dataset_query(path='$SCRATCH', dataset_name='MainDataset',\n    dax_body=\\\"EVALUATE TOPN(10, 'Sales')\\\")\nprint('Wrote', '$SCRATCH')\n\"\n```\n\nOpen the resulting file in Power BI Report Builder. **Manual verification\nthat an `.rdl` opens cleanly is the actual integration test** — the unit\ntests catch schema-level mistakes, but only Report Builder catches\ndeserialiser nits.\n\n### Adding a new tool\n\n1. Write tests first under `tests/test_*.py`.\n2. Implement in the appropriate `pbirb_mcp/ops/*.py` module (or create a\n   new one).\n3. Register in `pbirb_mcp/tools.py` with a clear `description` and\n   strict `inputSchema`.\n4. Run the full suite and a JSON-RPC smoke against the fixture.\n5. Open the modified RDL in Report Builder.\n\nThe commit-by-commit history shows the pattern in practice.\n\n---\n\n## Releases\n\n[CHANGELOG.md](CHANGELOG.md) tracks every release in\n[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. Releases\nare also published as\n[GitHub Releases](https://github.com/mafaq229/pbirb-mcp/releases) and to\n[PyPI](https://pypi.org/project/pbirb-mcp/).\n\nVersions follow SemVer adapted for an MCP tool surface — see\n[CONTRIBUTING.md § Versioning](CONTRIBUTING.md#versioning).\n\n## Contributing\n\nPRs welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers dev setup, the\nhard rules (tests-first, lxml only, stable IDs, atomic save,\nbyte-identity round-trip, smoke in Report Builder), the SemVer-for-MCP\nbump table, and the PR review checklist.\n\nBug reports and tool proposals: please use the\n[issue templates](https://github.com/mafaq229/pbirb-mcp/issues/new/choose).\nFor security issues, see [SECURITY.md](SECURITY.md). All participants\nare expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).\n\n---\n\n## Acknowledgments\n\nThe existing [bethmaloney/rdl-mcp](https://github.com/bethmaloney/rdl-mcp)\nserver pioneered the MCP-over-RDL pattern but is scoped to SSRS: column\nmetadata, basic parameter management, stored-procedure swap. Power BI\npaginated reports use the same RDL 2016 schema as SSRS, but:\n\n1. Data sources are **Power BI XMLA endpoints**, not SQL Server.\n2. Queries are **DAX**, and the upstream tool exposes no body-edit (only\n   stored-procedure name swap, useless here).\n3. Report Builder is **picky about XML round-tripping** — formatting drift,\n   namespace prefix loss, or unrecognised `MustUnderstand` attributes cause\n   silent corruption or \"this report needs to be upgraded\" prompts.\n\n`pbirb-mcp` is built around lxml so a no-op edit produces a byte-identical\nfile, addresses every element by stable name (never index), and treats\n\"opens cleanly in Report Builder\" as the actual integration test.\n\n---\n\n<!-- Ownership marker read by registry.modelcontextprotocol.io to verify this\n     PyPI package maps to the io.github.mafaq229/pbirb-mcp registry entry. -->\n\n```\nmcp-name: io.github.mafaq229/pbirb-mcp\n```\n",
  "bytes": 30111,
  "sha": "a7b48369429101834f8bc49c52511012af3c8d70a75db52a3693304780f25b01",
  "repo_slug": "mafaq229/pbirb-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_mafaq229_pbirb_mcp_dcf65bfc/readme"
}