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}" } } } }
.vscode/mcp.json settings file.
dotnet tool install --global SSRS2.Mcp --version 1.5.0
dotnet new tool-manifest
dotnet tool install --local SSRS2.Mcp --version 1.5.0
#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 (truefor 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 writtenBlock?: 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:
compare_reportswith"paths": "Reports": the receipts in groups of one structure (here 24, 5, 1, 1 and 1), each with whatrole:signaturenames in it (text boxes under a line, rows of the table, a rectangle) and how it differs from the 24.describe_reporton one of the 24: its outline, each item with its place and role.edit_reporton the folder with the 24'sstructure, the operations and"dryRun": true: what the files would become, one diff for those changed alike, each checked with its pages before and after.- The same without
dryRun; the other groups by calls of their own. validate_reportwith the samepaths,"baseline": "git:HEAD"and the rule (expect), thevariantsand therowCountsabove: 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
restrictedCodeerror 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,
sampleDataorsampleDataFile; the server runs no query. - .NET 10 SDK required (
dnx).
More: ssrs2.net · SSRS2 on GitHub · SSRS2.NETCore on NuGet
| Product | Versions 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. |
This package has no dependencies.