SSRS2.Mcp 1.5.0

{
  "inputs": [
    {
      "type": "promptString",
      "id": "SSRS2_LICENSE",
      "description": "Your SSRS2 license key. Optional: without one, previews carry a trial watermark.",
      "password": true
    }
  ],
  "servers": {
    "SSRS2.Mcp": {
      "type": "stdio",
      "command": "dnx",
      "args": ["SSRS2.Mcp@1.5.0", "--yes"],
      "env": {
        "SSRS2_LICENSE": "${input:SSRS2_LICENSE}"
      }
    }
  }
}
                    
This package contains an MCP Server. The server can be used in VS Code by copying the generated JSON to your VS Code workspace's .vscode/mcp.json settings file.
dotnet tool install --global SSRS2.Mcp --version 1.5.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local SSRS2.Mcp --version 1.5.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=SSRS2.Mcp&version=1.5.0
                    
nuke :add-package SSRS2.Mcp --version 1.5.0
                    

SSRS2 MCP server

Tools for AI coding agents that write or change RDLC and RDL reports (the report format of SQL Server Reporting Services and Microsoft's Report Viewer), and Markdown documents and slide decks to turn into PowerPoint, PDF or Word. The agent writes the file, or changes one or many through edit_report; these tools tell it what the Report Viewer engine makes of it, so it can fix its own mistakes, look at the result and write the file people asked for:

Tool What it does
validate_report Checks a report definition: its XML, the RDL schema, every field, parameter, scope and expression compiled, then a render with rows, which finds what only fails at run time. Each problem comes back with a code, the item, the property, the expression, its line in the file and often a hint ("Did you mean 'Name'? The report's fields: dataset Items has Name, Value").
render_preview Renders the report and returns its pages as PNG images, with the same diagnostics and what the pages print as text, where a value is read exactly; firstPage reaches a long deck's later pages, and sheet shows up to 30 pages in one image, as numbered thumbnails.
convert_report Checks the report as validate_report does, then writes it as a file beside it (or where output says, in a folder the configuration allows): pptx (editable slides, a slide per page), pdf, docx, xlsx, mhtml, tiff, png, csv or xml.
describe_report Outlines a report in a page of text: its page, datasets and fields, parameters, and every item top to bottom with its place, size, what it shows and does, and the role its texts and place give it (title, table, totals, declaration, signature...). item gives one part's XML, to edit it exactly; "detail": "full" gives every text whole and why each role was guessed.
compare_reports Groups many reports (a folder, a glob, a list) by structure: reports of one structure have the same items and differ only in places, texts and looks, so one change reaches them all. Each group says what its roles name, how it differs from the largest and each file's version ("detail": "full" lists the files apart too).
edit_report Changes report files in place, one or up to 200 in one call, with short operations (the SSRS2 designer's own) or exact replacements. Each file is patched, not rewritten (what no operation touched stays byte for byte, its encoding and line ends too), then checked: the answer says only the problems the change brought, and the pages before and after. dryRun writes nothing and gives the diff.

validate_report, render_preview and convert_report take a Markdown file (.md) too: see Markdown to PowerPoint. To change many reports at once, see Changing many reports.

What the server tells an agent before its first call is short (the recipes and the rules that prevent mistakes, some 5 KB, the same on every start so that a client's prompt cache keeps it), and so is each tool's description. The reference is a call away: validate_report, render_preview and convert_report answer "help": ["markdown", "decks"] with those topics and nothing else done (markdown, documents, decks, diagrams, columns, css, checks, answers, example; "help": true gives them all), as edit_report answers its operations' words.

An item hidden by its Hidden or an expression is not printed but keeps its space, as in Report Viewer: what is under it does not move up. Only a table's rows collapse. A block shown only sometimes is best made of rows of a table (edit_report's row operation), or added by an operation with collapse: true.

The same package audits a whole folder of reports from a terminal: see Migration audit.

It runs on your machine, over stdio. Your reports and data stay there; the server calls no service and no model. It is built on SSRS2, a cross-platform .NET port of the Report Viewer engine, and is part of its license. Without a license key the tools work in full and previews carry a trial watermark.

Setup

The server is a NuGet package started with dnx, which comes with the .NET 10 SDK. Run it once first: it downloads the tool (65 MB), so your client's first start does not wait for it (and does not read what dnx says while it installs).

dnx SSRS2.Mcp --yes -- --help

Claude Code

claude mcp add ssrs2 -- dnx SSRS2.Mcp --yes

VS Code (.vscode/mcp.json), Visual Studio (.mcp.json beside the solution)

{
  "servers": {
    "ssrs2": {
      "type": "stdio",
      "command": "dnx",
      "args": ["SSRS2.Mcp", "--yes"]
    }
  }
}

Cursor (.cursor/mcp.json), Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "ssrs2": {
      "command": "dnx",
      "args": ["SSRS2.Mcp", "--yes"],
      "env": { "SSRS2_LICENSE": "SSRS2-..." }
    }
  }
}

Then ask your agent for a report, or to change one: "Add a total row to Reports/Invoice.rdlc and check it."

What a call returns

{
  "ok": false,
  "summary": "1 error. The report does not run until the errors are fixed.",
  "file": "/src/app/Reports/Invoice.rdlc",
  "diagnostics": [
    {
      "code": "rsFieldReference",
      "severity": "error",
      "stage": "compile",
      "objectType": "Textbox",
      "objectName": "Total",
      "property": "Value",
      "expression": "=Sum(Fields!Amuont.Value)",
      "line": 212,
      "message": "The Value expression for the text box 'Total' refers to the field 'Amuont'. ...",
      "hint": "Did you mean 'Amount'? The report's fields: dataset Lines has Product, Quantity, Amount. Names are case-sensitive."
    }
  ]
}

stage says how far the report got: xml, schema, compile, policy (see below) or render. Errors mean the report does not run. Warnings of stage render mean it runs and something on its pages is wrong (an expression that failed shows #Error in its cell, a Lookup that finds no row for its key and prints nothing: lookupNoMatch): the summary says so, and atRunTime counts them, so ok alone does not mean the pages are right. Other warnings say what the pages only show: cultureDependent (an expression reads a text field as a number or a date in the culture the report runs in, with what to write instead), formatOfText (Format of a text field prints its format code, N2, on every machine), fontFallback and styleSynthesized (text drawn with another font than the report names, or bold or italic made of a face without it). Where a face is not the same on every machine, the answer lists the fonts the text came out in (fonts: family, face and how). render_preview returns the same answer, then one image per page (with sheet: true, one image of them all side by side, numbered: a deck at a glance), and text: what the report's text boxes and tables print, in reading order, a table row per line with its cells between bars, then each chart's points, a line per series ([chart Sales: Amount] North 120; South 80).

Reports are rendered with made-up rows, from each dataset's fields and their types (a field without a type is made up as the report uses it: a number where it is summed). A preview sums them up in madeUpRows (each dataset's fields and types, its row count, its first row; "madeUpRows": "all" gives every row), in the form the agent passes rows of its own in: sampleData: {"Lines": [{"Product": "Widget", "Quantity": 2, "Amount": 21.5}]} (dates as ISO text). Rows kept in files are named once instead of pasted into every call: sampleDataFile, a JSON file of that shape ("rows.json"), or a file per dataset ({"Labels": "labels.csv"}: CSV with a header row, comma, semicolon or tab separated, as Excel saves it, or JSON); sampleData given too wins for its datasets. Rows in another shape are refused with the shape they take; a key that is no field of its dataset, or a value that is not of its field's type, is said in a warning.

A report that names no Language takes its numbers, dates and currency from the machine it runs on, as it does in an application; the answer says which culture that was (culture), and the agent can ask for another ("culture": "en-US").

Where items print, and many reports at once

validate_report also says what a change did to the pages, so an agent checks a placement in one call instead of measuring it by hand:

  • layout: where items printed, a line each, in cm from the page's top left (true for every item, or names and roles: ["Block", "role:signature", "role:table:last"]), with what each overlaps: Block p1 top 13.5 h 0.8 left 2.0 w 17.0.
  • expect: rules on that layout, each answered held or not with its numbers: {"order": ["role:table", "Block", "role:signature"]} (top to bottom), {"samePage": [...]}, {"page": {"Block": 1}}, {"pages": 1}, {"maxPages": 1}, {"noOverlap": ["Block"]}. A rule fails when an item it names is not printed, unless it is written Block?: an item a variant may hide. The summary says which rule fails where (the report's render or a variant) and in how many files, and how to write an item that may be absent.
  • variants: the report rendered again with other rows or parameters, each with its pages, its rules and the problems it adds: [{"name": "no amount", "sampleData": {"Amount": null}}, {"parameters": {"Market": "fr-CH"}}].
  • rowCounts: {"Requests": "1..30"} renders a dataset with each count of rows and says the first count that adds a page (found by halving: a few renders whatever the range; 40 renders a call with the variants).
  • baseline: "git:HEAD" (or another revision) or a copy of the report: the warnings it already had are left out and counted, and the answer gives the pages, each variant's and row count's before and after, and what moved. A committed version, or a file the server saw before it started, is its owner's: its code is treated as such.
  • paths: many files in one call, with the same arguments: a list, a folder or a glob (Receipts/*_Heating.rdlc; 200 files). The answer is a line for the files with the same outcome ("24 files: ok, 1 page (was 1), no new warning, rules 3/3"; the largest group last, as "The other 24 files: ...", the others named; "detail": "full" gives a line per file), then each problem and each other line once, with the files it is in ("every file", "every file but ..."), and what moved said as shifts (down 1.0 cm: SignatureLine, DateLabel). The call takes at most the server's --timeout: the files it did not reach are named, to check in the next call.

Changing many reports

Say an application has 32 variants of one receipt, and each needs a notice after its declaration and before the signature, without growing onto a second page. An agent does it in a few calls, and computes no position:

  1. compare_reports with "paths": "Reports": the receipts in groups of one structure (here 24, 5, 1, 1 and 1), each with what role:signature names in it (text boxes under a line, rows of the table, a rectangle) and how it differs from the 24.
  2. describe_report on one of the 24: its outline, each item with its place and role.
  3. edit_report on the folder with the 24's structure, the operations and "dryRun": true: what the files would become, one diff for those changed alike, each checked with its pages before and after.
  4. The same without dryRun; the other groups by calls of their own.
  5. validate_report with the same paths, "baseline": "git:HEAD" and the rule (expect), the variants and the rowCounts above: the problems the change brought, the pages before and after, where the notice printed.
{
  "paths": "Reports",
  "structure": "fa2e0d64c1ec7250",
  "operations": [
    { "op": "textbox", "name": "Notice", "after": "DeclarationText",
      "text": "=Lookup(\"notice\", Fields!Key.Value, Fields!Text.Value, \"Labels\")" }
  ]
}
{
  "ok": true,
  "summary": "32 files: 24 written, 8 skipped (another structure). The change brought no new problem. Each prints on 1 page.",
  "folder": "Reports",
  "files": [
    "Receipt_de-AT_Cooling.rdlc, Receipt_de-AT_Electrical.rdlc, … (24 files): written; changed Notice; 1 page; no new problem",
    "Receipt_de-CH_Sanitary.rdlc, … (8 files): skipped, of another structure than fa2e0d64c1ec7250"
  ],
  "done": ["Added text box Notice at 0cm, 8.5cm, 17cm x 0.6cm: “=Lookup(\"notice\", …)”."]
}

Each file is patched where the operations change it, so its diff is the change and nothing else; a file whose change does not apply is left as it was, with why. The checks render with made-up rows, or with the application's own: sampleDataFile names a JSON file of them ({"Labels": [{"Key": "title", "Text": "Quittung"}, …]}), sent once as a path rather than in every call. A file changed since the agent read it is not overwritten (and baseVersion, the versions compare_reports, describe_report or a dry run gave, as one version or {"file": "version"}, refuses an edit made on an older read). The answer says where the items added or moved are now (placed, in cm), and a file that cannot be written is refused on its own line while the others are written. With keepPages (or maxPages), a change that makes any file print more pages, with its rows, its variants and its rowCounts, or that makes a table's rows add a page sooner (the rows from which a page is added, found before and after the change for each dataset a table of the body repeats, with the rows given and under each variant, rowCounts or not: "Requests adds a page at 20 rows in variant deposit (at 22 before)"; a dataset given one row while another is given more, a header, is not judged), writes none of them (partial writes those that keep their pages). The forms of the block that would keep a file's rows are tried on it and said checked, ready to send: placed beside an item, each line at its text's height, its texts in one line; fit writes each such file in its form, said on its line, so one call fits one block to reports of several layouts. Whenever an item added pushes others down, the answer says how far, which items, and where the block costs nothing (beside the totals, say), as operations ready to send (after a write, first those that take the block back); a file whose added items went elsewhere than the others' (in a table's rows, in a frame) says so on its line. A picture an image operation names by its file is read from the report's folder and embedded. Operations cover data, text, tables, groups, totals, charts, pictures, styles and pages; the tool's description names them, and "help": ["textbox", "set"] gives their words ("help": true, all of them) without changing anything. For anything else, edits are exact replacements in the file. A report nested deeper than 256 XML levels, which no report is, is refused before it is read.

What a report may do

A report's expressions and custom code are .NET code: they run when the report is rendered. An agent's report, or text injected into one, must not reach your files or your network through a preview. So:

  • The report's code is restricted. It may use the report's fields, parameters and globals, Visual Basic's functions, and .NET's text, number, date, collection, regular expression and culture types. Files, the environment, processes, threads, reflection, native libraries and code modules are refused when the report is compiled, as a restrictedCode error naming the expression: nothing of the report runs.
  • Each report runs in a process of its own, started with an emptied environment (your tokens and connection strings are not there to read), a memory limit and a time limit, and stopped afterwards. On Linux that process can also start no program and open no Internet connection, and, where the kernel has Landlock (5.13 and later), read nothing of your home but its own program and your fonts, and nothing of the temporary folders.
  • Your own reports' code is left alone. A report that already called outside these rules before the agent touched it (in its committed version in git, or as it was when the server started) is yours: those calls come back as warnings, never as errors, and the agent is told to leave them. Such a report is checked but not rendered, unless you trust it (below). What the agent adds to it is still refused.
  • A report's code modules (assemblies your application deploys) are not loaded: the report is checked and rendered without them, and the answer says what was left empty.

This guards against mistakes and injected text. It is not a sandbox against a determined attacker, and an agent that can run commands on your machine does not need a report to do so.

Options

Arguments go after -- in the client's configuration: "args": ["SSRS2.Mcp", "--yes", "--", "--trust", "Reports"].

Argument Default
--trust <file or folder> none Lets the own code of these reports run unrestricted in previews (what they held when the server started, or in their committed version). Repeatable; SSRS2_MCP_TRUST lists paths too. Only you can set it: no tool call can.
--output <folder> none Lets convert_report write files there too, besides the folder of the file converted and the server's. Repeatable.
--timeout <seconds> 60 A check running longer is stopped; a call of many files (paths) takes no longer.
--max-pages <n> 10 The most pages a preview returns.
--memory <MB> 1024 The memory a check may use.
--network off Lets a report load external images over the Internet (the restriction is enforced on Linux).

SSRS2_LICENSE in the server's environment holds your license key.

Markdown to PowerPoint

A .md file is converted as SSRS2's designer imports it: a document, or, with marp: true or layout: slides in its front matter, slides (a page each, --- between them; PowerPoint's 16:9 unless size: 4:3). It reads GitHub's Markdown (headings, lists, task lists, tables, code, quotes, alerts, footnotes, links, images, emoji) and Marp's directives, in the front matter or in a slide's comment: class (lead, invert), color, backgroundColor, backgroundImage, header, footer, paginate, headingDivider; images ![bg], ![bg right:40%], ![w:300px]; `` headings; columns side by side (<div class="columns"> holding a <div> per column); cards, a <div> with a background, a border or rounded corners around what it holds. A deck's CSS (a <style> block, the front matter's style, <style scoped> for one slide) gives the slides their background, text color, font and size and can center their content, and colors the headings, links, bold text and table headers: one accent color in plain Markdown. In the text, a <span style="color: …"> or the usual inline tags (<b>, <i>, <u>, <small>, <mark>) give words their looks. A Mermaid flowchart (a ```mermaid block, flowchart LR or graph TD) is drawn with boxes and arrows, laid out as a person would (its main way straight, branches beside it), which a deck holds as PowerPoint's own shapes and connectors glued to their boxes: move a box and its arrows follow. Pictures are read from the Markdown's own folder.

The deck holds text boxes, PowerPoint tables that span the slide and pictures with their alternative text. Slide titles are in PowerPoint's title placeholders, on its Title Slide, Section Header, Title and Content, Two Content, Comparison and Title Only layouts: a slide's paragraphs and lists are one box, its content placeholder (two columns under a title are one each, under headings of their own PowerPoint's Comparison), which PowerPoint's outline shows and where a line typed moves the rest down. Lists are its own bullets and numbering (task lists with their box), and slide numbers and footers are its placeholders, kept up to date when slides move. The deck's master takes its looks, its bullets and its background, so a slide added in PowerPoint matches. A slide that holds a little too much has its text set smaller to fit, as PowerPoint does. What the conversion approximated or left out (a theme, CSS it does not draw, a picture not found, a slide that still holds too much) is in the answer's conversion, at the Markdown's lines, and the summary counts it. For a .pptx, written.slides reads each slide back as PowerPoint gets it.

"Write a 10-slide deck on our Q3 numbers in quarterly.md, check it, and convert it to PowerPoint."

From a terminal, without an agent:

dnx SSRS2.Mcp --yes -- convert quarterly.md --to pptx

Migration audit

The same tool audits a folder of reports for a move to SSRS2: a report server's content downloaded (Out-RsFolderContent), a Report Server project, or an application's .rdlc files.

dnx SSRS2.Mcp --yes -- audit ./Reports --out ./ssrs2-audit

Each report is compiled by the engine and rendered with made-up rows, in a process of its own, and graded: green (runs as it is), yellow (runs, with something for the application to do: a subreport to load, an assembly to deploy, a font to install) or red (does not load, compile or render, with the engine's error). The audit is written as index.html (the counts, what the application has to do across the estate, then each report with its first page, its findings and its inventory), audit.json (the same for a program), pages/ and, for reports that use a report server's shared datasets, converted/: the definition with the dataset written in, as an application loads it. Shared data sources are listed without their credentials. Nothing leaves the machine.

Reports of other tools are converted to RDLC first: ActiveReports page and section reports (.rdlx, .rpx), Telerik Reporting (.trdx, .trdp), FastReport (.frx, and FastReport VCL's .fr3), Stimulsoft (.mrt, XML or JSON, and packed .mrz), DevExpress (.repx, the XML layouts of v17.1 and later), Crystal Reports (the .xml that RptToXml exports of a .rpt, on Windows with Crystal's runtime: a .rpt itself is listed with how to export it), Microsoft Access (the text Access writes of a report, SaveAsText: .report, or a .bas or .txt told by its content) and JasperReports (.jrxml, the designs of JasperReports 3 to 7: a compiled .jasper is listed with how to get its design), Eclipse BIRT (.rptdesign) and Delphi's QuickReport and ReportBuilder (.dfm forms and ReportBuilder's .rtm templates, saved as text: a binary form is listed with how to save it as text) Visual Basic 6's Data Reports (.dsr), PowerBuilder's DataWindows exported as text (.srd) and Visual FoxPro's reports (.frx, read with the .frt beside it). The conversion is made by rules, not by a language model: each element comes out as it was, approximated or not converted, and what was not converted is a finding with what the original report said. The RDLC is written under converted/ (a FastReport or Stimulsoft subreport page, a DevExpress embedded subreport, as an .rdlc of its own), and the rows a report carried (Telerik's CSV and JSON sources, DevExpress' inline JSON, ActiveReports' JSON and XML data) are the ones its page is rendered with.

A report whose own code reaches files, the environment or other programs is compiled and not run, unless you pass --run-code. --no-render compiles only; --fail-on-red makes a build fail when a report does not run. What a folder of files cannot say (subscriptions, linked reports, caching, security: a report server's catalog) is listed at the end of the audit.

Limits

  • Subreports are not loaded: their places show an error message.
  • Data comes from made-up rows, sampleData or sampleDataFile; the server runs no query.
  • .NET 10 SDK required (dnx).

More: ssrs2.net · SSRS2 on GitHub · SSRS2.NETCore on NuGet

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
1.5.0 58 10/14/2026
1.4.1 85 10/8/2026
1.4.0 89 10/8/2026
1.3.0 89 10/3/2026