Skip to main content
Glama
Kookerella-Ltd

Excel MCP Server (FsOpenXmlDsl)

Official

Kookerella.FsOpenXmlDsl

A typesafe F# DSL for building Excel workbooks, interpreted into calls against the DocumentFormat.OpenXml SDK. The DSL is a plain data model (records/DUs with structural equality) — the interpreter (Writer) compiles it to OOXML, and the reverse transform (Reader) parses an existing .xlsx back into the same DSL.

See MAPPING.md for exactly which SpreadsheetML features map 1:1, which are approximated, and which aren't modeled yet.

This round-trips in both directions, which most Excel libraries (EPPlus, ClosedXML, NPOI, ...) don't: they give you an imperative API to build a workbook from scratch or mutate an existing one, but no way to turn an existing file back into readable source. Here, Reader parses a real .xlsx/.xlsm back into the same DSL, and Workbook.generateScript (F#) / CsCodeGen.Generate (C#) go one step further and render that model back out as a self-contained script that rebuilds an equivalent file - a decompiler for spreadsheets, not just a writer. Two more surfaces, Xml.ofWorkbook/Xml.toWorkbook (see "## XML" below) and Json.ofWorkbook/Json.toWorkbook (see "## JSON" below), do the same translation to/from plain XML or JSON against a real schema - for a caller who'd rather generate or consume data than write code at all, e.g. an XSLT pipeline producing a report. Kookerella.FsOpenXmlDsl.Mcp exposes all four directions as MCP tools (generate_fsharp_script/generate_csharp_script/generate_xml/create_workbook_from_xml/ generate_json/create_workbook_from_json) for an AI agent, and as fsopenxmldsl-mcp convert/build CLI commands for anyone else - try it on any spreadsheet you already have, no code required:

dotnet tool install -g Kookerella.FsOpenXmlDsl.Mcp
fsopenxmldsl-mcp convert your-file.xlsx --lang csharp

Demos

Full worked examples of the decompile-then-extend workflow above - reverse-engineering the same invoice template into C#, F#, an XSLT transform, and a plain JSON-generation script, each wired up to real data and real tests proving the result stays schema-valid - live in a companion repo: Kookerella.Demo.DecompileToSource.

Related MCP server: Excel MCP Server

Layout

  • src/Kookerella.FsOpenXmlDsl — the library.

    • Reference.fsCellRef and "A1"-style address conversions.

    • Styles.fs — cell formatting: Color, FontStyle, FillStyle, BorderStyle, AlignmentStyle, NumberFormat, CellProtection, CellStyle.

    • Validation.fs — conditional formatting and data validation: ComparisonOperator (shared by both), ConditionalFormatRule, ValidationKind, ValidationAlert, and the ConditionalFormatEntry/DataValidationEntry records stored on Worksheet.

    • Hyperlinks.fsHyperlinkTarget (external URL/mailto: vs. internal same-workbook reference) and the HyperlinkEntry record stored on Worksheet.

    • Comments.fsCommentEntry (classic cell comments, i.e. current Excel's "Notes" - see MAPPING.md for the modern threaded-comments gap).

    • Protection.fsSheetProtection, the sheet-level protection flags stored on Worksheet (pairs with CellStyle.Protection for per-cell locking), and WorkbookProtection, the workbook-level structure/window protection flags stored on Workbook.

    • DefinedNames.fsDefinedNameScope/DefinedNameEntry, stored on Workbook rather than Worksheet - the one DSL concept that's genuinely workbook-level.

    • PageSetup.fs — print settings: PageOrientation, PaperSize, PrintScaling, PageMargins, and the PageSetup record stored on Worksheet.

    • Tables.fs — Excel Tables: TableColumn, TableStyle, and the TableEntry record stored as a list on Worksheet (a sheet can have several).

    • Sparklines.fs — in-cell mini-charts: SparklineType, SparklineStyle, SparklineCell, and the SparklineGroupEntry record stored as a list on Worksheet (a sheet can have several independently-styled groups).

    • Charts.fs — column/bar/line/pie charts: ChartType, ChartSeries, and the ChartEntry record stored as a list on Worksheet (a sheet can have several).

    • Images.fs — raster images: ImageFormat and the ImageEntry record (raw file bytes plus a cell-range anchor) stored as a list on Worksheet.

    • PivotTables.fsPivotAggregation and the PivotTableEntry record (source range, row/column/value fields, an anchor cell) stored as a list on Worksheet.

    • Model.fsCellValue, Cell, Worksheet, Workbook (including Workbook. VbaProject, a macro-enabled workbook's raw vbaProject.bin bytes - see its own doc comment; there's no dedicated Macros.fs since it's a single opaque field, not a new type).

    • Xml.fs / Xml.xsd — the XML surface: Xml.toWorkbook/Xml.ofWorkbook translate a Workbook to/from an XElement tree, and Xml.schemaSet() loads the paired schema (embedded in the assembly as a resource) for validating either direction. See "## XML" below.

    • Json.fs — the JSON surface: Json.toWorkbook/Json.ofWorkbook translate a Workbook to/from a System.Text.Json.Nodes.JsonObject tree, covering the same worksheet/workbook-level feature set Xml.fs does. Schema validation (Json.schema.json) is test-suite only, not a public API - see "## JSON" below.

    • Builders.fs — ergonomic helpers: plain functional constructors (cellA1, ...) for the canonical model, plus the SheetItem/CellEntry types (each a single simple DU case with optional fields) and the sheet fold function - a small tree-shaped "AST for building a sheet" (rows of cells, plus sheet-level facts like column widths, merges, conditional formats, data validations, hyperlinks, comments, autofilter, and sheet protection) that mirrors how SpreadsheetML itself nests. SheetDsl is what you actually write against: cell/row/autoFilter/conditionalFormat/ dataValidation/hyperlink/comment members with real optional parameters (?col, ?style, ?index, the data validation alert fields, ?tooltip, ?author) - no builder objects, no separate "styled" function, no None-noise for the common case. (Protect is the one SheetItem case with no smart constructor - SheetProtection is a plain record you build the usual F# way, { SheetProtection.Default with ... }.)

    • Interpreter/StyleRegistry.fs — interns fonts/fills/borders/number formats into a shared OOXML stylesheet (internal).

    • Interpreter/ChartWriter.fs / ChartReader.fs — charts' own DSL ↔ DrawingML/ChartML translation, split out from Writer.fs/Reader.fs given how much larger that one feature's OOXML surface is than everything else combined (internal).

    • Interpreter/ImageWriter.fs / ImageReader.fs — images' own DSL ↔ DrawingML translation (internal).

    • Interpreter/DrawingWriter.fs / DrawingReader.fs — own the one DrawingsPart/ <drawing> relationship a worksheet gets when it has charts and/or images, since both features share that one drawing canvas rather than each managing their own (internal).

    • Interpreter/PivotTableWriter.fs / PivotTableReader.fs — pivot tables' own group-by

      • aggregate engine plus DSL ↔ OOXML translation (pivotCacheDefinition/ pivotCacheRecords/pivotTableDefinition), split out from Writer.fs/Reader.fs the same way charts and images are (internal).

    • Interpreter/Writer.fs — DSL → OOXML (internal).

    • Interpreter/Reader.fs — OOXML → DSL, the reverse transform (internal).

    • Interpreter/CodeGen.fs — DSL → F# source text: renders a Workbook back out as a self-contained .fsx script that rebuilds an equivalent file when run (internal).

    • Api.fs — the public Workbook.save / saveToStream / load / loadFromStream / generateScript entry points.

  • tests/Kookerella.FsOpenXmlDsl.Tests — one test per feature, each validating the produced file against the OOXML schema (DocumentFormat.OpenXml.Validation.OpenXmlValidator) and asserting an exact round trip back through the DSL. Each test also writes the workbook it builds to Examples/<test name>/output.xlsx (checked into the repo), so every feature has a real, openable .xlsx demonstrating it - a browsable gallery, not just assertions. Each scenario also gets an Examples/<test name>/script.fsx - see "Regenerating a file as F# source" below - which a separate, slower Category=Slow test group actually executes via dotnet fsi and verifies against the committed .xlsx, and an Examples/<test name>/workbook.xml - the same workbook through Xml.ofWorkbook, validated against Xml.xsd at generation time (see "## XML" below) - and an Examples/<test name>/workbook.json - the same workbook through Json.ofWorkbook, validated against Json.schema.json at generation time (see "## JSON" below) - so one folder always has four views of the same example: the real file, the F# source that rebuilds it, and the XML/JSON that also rebuild it. Assets/ holds the one test fixture too large to inline as a base64 literal like every other binary fixture in Tests.fs - a real vbaProject.bin extracted from a workbook actually saved by Excel, used by the macro example.

  • samples/Kookerella.FsOpenXmlDsl.Sample — a small console app that builds a workbook, saves it, and reads it back.

  • src/Kookerella.CsOpenXmlDsl — an idiomatic, immutable, fluent C# wrapper over this library, for callers who'd rather not touch F# discriminated unions/option types directly. Now covers every feature this library models at the worksheet/workbook level - see its own README for scope and an example. tests/Kookerella.CsOpenXmlDsl.Tests is its own C# xUnit suite, exercising the wrapper the way a real C# caller would rather than reusing the F# test project.

  • src/Kookerella.FsOpenXmlDsl.Mcp — a local MCP (Model Context Protocol) server exposing this library's read/write/code-generation/XML/JSON capabilities as tools any MCP-compatible AI agent can call directly, and the same conversion capability as plain fsopenxmldsl-mcp convert/build CLI commands for anyone not going through an MCP client - see its own README for the tool list and how to configure it.

Quick start

open Kookerella.FsOpenXmlDsl
open type Kookerella.FsOpenXmlDsl.SheetDsl

let headerStyle =
    { CellStyle.Default with
        Font = Some { FontStyle.Default with Bold = true }
        Fill = Some { Color = Rgb(220uy, 220uy, 220uy) } }

let data =
    sheet
        "Sheet1"
        [ row [ cell (Text "Name", style = headerStyle)
                cell (Text "Amount", style = headerStyle) ]
          row [ cell (Text "Widgets")
                cell (Number 42.5, style = { CellStyle.Default with NumberFormat = Some TwoDecimal }) ]
          Freeze(1, 0) ]

workbook [ data ] |> Workbook.save "out.xlsx"

// Reverse transform:
let roundTripped = Workbook.load "out.xlsx"

CellEntry and SheetItem's row case are each a single simple DU case with optional fields (Col/Index) rather than separate "styled" or "explicit position" cases - None means "the next column/row after the previous entry" (starting at 0), Some n jumps there explicitly and sequential numbering resumes right after it. You don't construct the case directly, though: SheetDsl.cell/SheetDsl.row are members with real optional parameters (?col/?style on cell, ?index on row) that hide the Nones for the common case - plain let functions can't have optional parameters in F#, which is why this one bit of the DSL is a type. open type Kookerella.FsOpenXmlDsl.SheetDsl (alongside open Kookerella.FsOpenXmlDsl) brings cell/row into scope unqualified, same as a module. Explicit column/row jumps go through the same two members, just with the optional argument supplied: cell (value, col = 2) and row (cells, index = 4). sheet is the one fold that interprets the resulting item list into the canonical Worksheet (the same relationship Writer has to OOXML). If you already have cells pre-addressed by CellRef rather than grouped by row, sheetOfCells builds a Worksheet directly from a flat Cell list instead.

A Formula cell is Formula(expression, cachedValue: float option) - this library never evaluates formulas itself, so cachedValue is the only number that will ever exist for that cell until something else computes one. Real Excel recalculates on open and overwrites it, so leaving it None is fine if a human always opens the result in Excel first. It's not safe for a headless pipeline - e.g. generating a workbook and piping it straight into a PDF converter, another automated reader, or anything else that never opens it in real Excel. Whether that downstream step shows a correct number, a blank, or a stale one depends entirely on whether it happens to have its own formula engine; some do (Aspose.Cells, Syncfusion, GemBox, real Excel via COM), many lighter-weight or headless converters don't and will just render whatever's already in the cell. Since you already have the numbers that fed into the formula, always pass the real result as cachedValue for anything that isn't guaranteed to pass through Excel first - it costs nothing and sidesteps the problem entirely, since a downstream reader with no evaluator at all can still show a correct value someone else already computed.

Conditional formatting and data validation are SheetItems too:

[ conditionalFormat (
    CellRef.ofA1 "A1",
    CellRef.ofA1 "A10",
    CellValueRule(GreaterThan, "100", None, { CellStyle.Default with Fill = Some { Color = Rgb(255uy, 199uy, 206uy) } })
  )
  dataValidation (CellRef.ofA1 "B1", CellRef.ofA1 "B10", ListValidation [ "Small"; "Medium"; "Large" ]) ]

See MAPPING.md for exactly which rule kinds of each are covered.

Defined names are workbook-level, so they attach to the Workbook, not a Worksheet:

workbook [ data ]
|> withDefinedNames
    [ definedName "TaxRate" "Sheet1!$A$1"
      sheetScopedDefinedName "Sheet1" "LocalTotal" "Sheet1!$A$2" ]

Workbook-level protection (as distinct from a Worksheet's own SheetProtection) is also workbook-level, same pipe-friendly shape:

workbook [ data ]
|> withProtection { WorkbookProtection.Default with LockStructure = Some true }

withDefinedNames/withProtection compose - pipe both onto the same workbook [...].

Macros are also workbook-level, same pipe-friendly shape - withVbaProject takes the raw bytes of an existing vbaProject.bin (extracted from an .xlsm you already have, e.g. via System.IO.Compression.ZipFile, or authored in Excel's VBA editor and harvested the same way). Core doesn't decode, generate, or otherwise understand VBA source - it embeds and reads back exactly the bytes you give it, the same "opaque payload" treatment ImageEntry.Data gets for raster images:

workbook [ data ]
|> withVbaProject (System.IO.File.ReadAllBytes("vbaProject.bin"))

Save the result with an .xlsm path - Workbook.save/saveToStream automatically switch the file's own declared content type to Excel's macro-enabled kind whenever a VbaProject is present, but real Excel also expects the .xlsm extension to trust and run macros at all. See MAPPING.md for what isn't modeled (authoring macro source, and the one case where the default sheet/workbook codenames Core writes won't match what a macro's original author intended).

Print settings are a SheetItem too - PageSetup (the DU case) takes a plain PageSetup record (the type), no smart constructor, same as Protect/SheetProtection. PrintArea is a list of ranges (Excel supports several disjoint print rectangles per sheet) - under the hood it's actually a hidden defined name, but Writer/Reader translate transparently, so it reads and writes like any other PageSetup field:

[ PageSetup
    { PageSetup.Default with
        Orientation = Landscape
        Scaling = Some(FitToPage(1, 0)) // 1 page wide, unlimited tall
        PrintArea = [ (CellRef.ofA1 "A1", CellRef.ofA1 "D10") ]
        Header = Some "&C&\"Arial,Bold\"Quarterly Report"
        FirstHeader = Some "&CCover Page" // shown only on page 1
        EvenFooter = Some "&L&F" } ] // shown only on even pages

See MAPPING.md for what isn't modeled (totals-row/headerless tables, and a handful of minor pageSetup attributes like print page order).

Tables are also a SheetItem - Table (the DU case) takes a plain TableEntry record (the type), no smart constructor, same as Protect/PageSetup. Core doesn't synthesize the header row's cell text for you, so it must already be there as ordinary cells - the same way conditional formatting/autofilter/merges only describe metadata layered on top of cells you've already placed:

sheet
    "Sheet1"
    [ row [ cell (Text "Item"); cell (Text "Quantity") ]
      row [ cell (Text "Widgets"); cell (Number 12.0) ]
      Table
          { TopLeft = CellRef.ofA1 "A1"
            BottomRight = CellRef.ofA1 "B2"
            Name = "Inventory"
            Columns = [ { Name = "Item"; CalculatedFormula = None }; { Name = "Quantity"; CalculatedFormula = None } ]
            Style = TableStyle.Default } ]

Structured references (Table1[Column]) need no special handling - they're just raw formula text in a Formula cell, same as any other formula. See MAPPING.md for what isn't modeled (totals row, headerless tables).

Sparklines follow the same shape - SparklineGroup (the DU case) takes a plain SparklineGroupEntry record:

[ SparklineGroup
    { Style = { SparklineStyle.Default with Type = Column; ShowNegative = true }
      Sparklines =
        [ { Cell = CellRef.ofA1 "E1"; DataTopLeft = CellRef.ofA1 "A1"; DataBottomRight = CellRef.ofA1 "D1" } ] } ]

Sparklines are a Microsoft extension (living in the worksheet's extLst), not core SpreadsheetML - unlike the rest of this library, schema validation alone can't confirm real Excel renders one correctly, so treat this one with a bit more caution and verify in real Excel before relying on it. See MAPPING.md for what isn't modeled (axis settings, per-role colors beyond the main series color).

Charts are the same shape too - EmbeddedChart (not bare Chart, which collides with the OOXML SDK's own type - see Builders.fs) takes a plain ChartEntry record. A series' Name is a reference to the cell that names it (its column header, typically), live-updating the same way a real Excel chart's series name does - not a static copy:

[ EmbeddedChart
    { Type = ChartColumn
      Title = Some "Sales by Quarter"
      CategoriesTopLeft = CellRef.ofA1 "A2"
      CategoriesBottomRight = CellRef.ofA1 "A4"
      Series = [ { Name = CellRef.ofA1 "B1"; ValuesTopLeft = CellRef.ofA1 "B2"; ValuesBottomRight = CellRef.ofA1 "B4" } ]
      ShowLegend = true
      TopLeftAnchor = CellRef.ofA1 "E1"
      BottomRightAnchor = CellRef.ofA1 "L15" } ]

Unlike Sparklines, charts are core, fully schema-driven DrawingML/ChartML - built from typed OOXML SDK classes the same way every other feature is, not an extension mechanism. See MAPPING.md for what isn't modeled (chart kinds beyond column/bar/line/ pie, per-series styling, stacked grouping).

Images are anchored the same way - EmbeddedImage takes a plain ImageEntry record. Data is just the image file's own raw bytes (read it with System.IO.File.ReadAllBytes, for example) - this DSL doesn't decode or re-encode anything, only embeds and hands back exactly what you give it:

[ EmbeddedImage
    { Data = System.IO.File.ReadAllBytes("logo.png")
      Format = Png
      TopLeftAnchor = CellRef.ofA1 "A1"
      BottomRightAnchor = CellRef.ofA1 "C6" } ]

A worksheet's charts and images share one drawing canvas under the hood (Excel only gives a sheet one at all), which is transparent to you as a caller - just add both kinds of SheetItem to the same sheet. See MAPPING.md for what isn't modeled (formats beyond PNG/JPEG/GIF/BMP, free-floating position, cropping, linked-not-embedded images).

Pivot tables are also a SheetItem - EmbeddedPivotTable (not bare PivotTable, again for naming consistency with EmbeddedChart/EmbeddedImage) takes a plain PivotTableEntry record. Unlike every other feature, this one does real work at write time rather than a pure translation: it groups the source range by RowField (and ColumnField, if given), aggregates ValueField, and writes both a real Excel pivot cache and the resulting grid of computed cells:

[ EmbeddedPivotTable
    { SourceSheet = None // defaults to this sheet; can name another
      SourceTopLeft = CellRef.ofA1 "A1"
      SourceBottomRight = CellRef.ofA1 "C5"
      RowField = "Region"
      ColumnField = Some "Quarter"
      ValueField = "Sales"
      Aggregation = PivotSum
      ValueCaption = Some "Total Sales"
      TopLeftAnchor = CellRef.ofA1 "E1" } ]

The source range's first row must be plain Text header cells naming each field. This is deliberately scoped to what a single field per axis can express - one row field, at most one column field, one value field, Tabular layout, grand totals only - see MAPPING.md for the reasoning and what a richer pivot table (nested fields, multiple value fields, page filters) would need instead.

Regenerating a file as F# source

Given a Workbook (typically one you just Workbook.loaded from an existing file), Workbook.generateScript renders it back out as a self-contained .fsx script that rebuilds an equivalent file when run - a code-generating counterpart to Workbook.load, one level further than the reverse transform: instead of data, you get DSL source text. It has no opinion on how the script locates the FsOpenXmlDsl assembly, so you supply the #r lines yourself:

let wb = Workbook.load "input.xlsx"

let referenceLines =
    [ "#r \"path/to/Kookerella.FsOpenXmlDsl.dll\""
      "#r \"path/to/DocumentFormat.OpenXml.dll\"" ]

let script = Workbook.generateScript referenceLines "output.xlsx" wb
System.IO.File.WriteAllText("regenerate.fsx", script)

Running dotnet fsi regenerate.fsx produces output.xlsx - not byte-identical to the original (zip metadata/timestamps differ) but structurally equivalent through the same round-trip lens every other test in this repo uses. Generated code only ever mentions fields that differ from CellStyle.Default/BorderStyle.None/etc., and only gives a row/cell an explicit index/col where the source actually has a gap - see Interpreter/CodeGen.fs. Every scenario under tests/Kookerella.FsOpenXmlDsl.Tests/Examples/ has a committed script.fsx generated exactly this way; the Category=Slow test group is what actually runs each one via dotnet fsi and checks it reproduces the committed .xlsx.

XML

Xml.toWorkbook/Xml.ofWorkbook (in Xml.fs) are a third way in and out of the DSL, alongside writing F#/C# directly and code generation: plain XML, against a real schema (Xml.xsd, embedded in the assembly). This exists for a caller who'd rather generate or consume data than write code at all. Two concrete uses:

  • Build an .xlsx from XML a transform engine already produces - an XSLT pipeline (or any templating that emits XML) can target Excel directly, without learning the OOXML schema or this library's own API.

  • Convert an existing .xlsx to XML for version control - .xlsx is a binary ZIP, so git diff on one is useless; converting to XML first makes a real, human-readable diff possible. Xml.ofWorkbook's output is deterministically ordered (sorted by cell position, or by name for defined names) regardless of the order the underlying Workbook's lists happen to be in, so a genuine content change produces a small, isolated diff rather than a spurious one from rows/rules getting reshuffled between runs.

open System.Xml.Linq

// XML -> Workbook -> .xlsx
let wb = XElement.Load "report.xml" |> Xml.toWorkbook
Workbook.save "report.xlsx" wb

// .xlsx -> Workbook -> XML
let xml = Workbook.load "report.xlsx" |> Xml.ofWorkbook
xml.Save "report.xml"

A discriminated union case becomes an XML element named after the case (camelCased) when it carries data of its own, or an attribute value (also camelCased) when it's one of several parameterless alternatives - e.g. a cell's value:

<cell ref="B2">
  <number>42.5</number>
  <style>
    <numberFormat kind="currency" />
  </style>
</cell>

A richer example - ValidationKind's six cases follow the same convention, and ValidationAlert's fields are written as attributes directly on <dataValidation> itself rather than nested:

<dataValidation topLeft="A2" bottomRight="A2" errorTitle="Invalid quantity"
                errorMessage="Quantity must be a positive whole number.">
  <wholeNumberValidation operator="greaterThan" formula1="0" />
</dataValidation>

ConditionalFormatRule's seven cases follow the same convention too, nesting a full CellStyle where the rule needs one - note <fill> holds <rgb>/<indexed>/<theme> directly, with no extra wrapper element:

<conditionalFormat topLeft="A1" bottomRight="A3">
  <cellValueRule operator="greaterThan" formula1="100">
    <style>
      <fill>
        <rgb r="255" g="199" b="206" />
      </fill>
    </style>
  </cellValueRule>
</conditionalFormat>

A Chart's Series list needs its own wrapper element (<series>) distinct from each item's own element name (<s>), to avoid a real ambiguity XML has and JSON doesn't - a list has no shape of its own in XML the way a JSON array does, so the container and its items need different names or a reader can't tell where the list starts:

<chart type="column" title="Sales by Quarter" showLegend="true"
       anchorTopLeft="E1" anchorBottomRight="L15">
  <categories topLeft="A2" bottomRight="A4" />
  <series>
    <s name="B1" valuesTopLeft="B2" valuesBottomRight="B4" />
    <s name="C1" valuesTopLeft="C2" valuesBottomRight="C4" />
  </series>
</chart>

An Excel Table shows the more usual case for that same wrapper/item split - columns already has a natural singular (column), so no <s>-style workaround is needed:

<table topLeft="A1" bottomRight="B4" name="Calc">
  <columns>
    <column name="Qty" />
    <column name="Doubled" calculatedFormula="Calc[Qty]*2" />
  </columns>
  <style name="TableStyleLight9" showFirstColumn="true" showLastColumn="true"
         showColumnStripes="true" />
</table>

A SparklineGroup's Color field wraps in its own <color> child element, same convention CellStyle's font/fill use:

<sparklineGroup>
  <style type="column" lineWeight="1.5" showNegative="true">
    <color>
      <rgb r="0" g="112" b="192" />
    </color>
  </style>
  <sparklines>
    <sparkline cell="E1" dataTopLeft="A1" dataBottomRight="D1" />
  </sparklines>
</sparklineGroup>

A PivotTable is the flattest shape here - just attributes, no nested elements at all. Note this only carries the description through: loading one via Xml.toWorkbook doesn't re-run the aggregation, unlike everything else this schema covers:

<pivotTable sourceSheet="Data" sourceTopLeft="A1" sourceBottomRight="C9"
            rowField="Region" columnField="Quarter" valueField="Sales"
            aggregation="average" valueCaption="Avg Sales" anchorTopLeft="F1" />

An Image's raw bytes are the element's own base64 text content, the same convention vbaProject below uses:

<image format="gif" topLeft="A1" bottomRight="D6">R0lGODlhAQABAIAAAAAAAP...</image>

A Hyperlink's Target nests the same way ValidationKind/ConditionalFormatRule do:

<hyperlink topLeft="A1" bottomRight="A1" tooltip="Visit site">
  <externalHyperlink>https://example.com</externalHyperlink>
</hyperlink>
<hyperlink topLeft="A2" bottomRight="B3" display="Go to top">
  <internalHyperlink>Sheet1!A1</internalHyperlink>
</hyperlink>

A Comment's text is also the element's own content, not an attribute - author is simply omitted when empty rather than written as author="":

<comment cell="A1" author="Alex">Check this figure</comment>
<comment cell="A2">Unnamed author</comment>

Sheet and workbook protection are both flat attribute bags - no nested elements needed, since none of SheetProtection/WorkbookProtection's fields are structured data:

<protection password="hunter2" sheet="true" formatCells="true" sort="true" autoFilter="true" />
<workbook>
  <sheets>...</sheets>
  <protection password="hunter2" lockStructure="true" />
</workbook>

PageSetup shows the mixed-DU convention again - PaperSize's named cases become a kind attribute, the same escape-hatch shape NumberFormat uses on a cell's style:

<pageSetup orientation="landscape">
  <paperSize kind="a4" />
  <margins left="0.5" right="0.5" top="1" bottom="1" header="0.2" footer="0.2" />
</pageSetup>

PrintScaling's two cases, PaperSize's escape hatch (other, for any of the several dozen paper codes not worth naming), PrintArea's list of ranges, and header/footer text all together:

<pageSetup orientation="portrait">
  <paperSize other="9" />
  <scaling fitWidth="1" fitHeight="0" />
  <margins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3" />
  <printArea>
    <range topLeft="A1" bottomRight="D10" />
  </printArea>
  <header>&amp;C&amp;"Arial,Bold"Report</header>
  <footer>&amp;LPage &amp;P of &amp;N</footer>
</pageSetup>

A macro-enabled workbook's VbaProject bytes sit at the workbook level, alongside sheets, not inside any one sheet:

<workbook>
  <sheets>...</sheets>
  <vbaProject>AQIDBA==</vbaProject>
</workbook>

DefinedNameScope's two cases show a different shape than PaperSize/NumberFormat's "kind attribute" trick: WorkbookScope carries no data of its own, yet still becomes its own (empty) element rather than an attribute value, since it sits in a <choice> alongside SheetScope, which does carry data:

<definedNames>
  <definedName name="LocalTotal" formula="Sheet1!$A$2" hidden="true">
    <sheetScope sheetName="Sheet1" />
  </definedName>
  <definedName name="TaxRate" formula="0.075">
    <workbookScope />
  </definedName>
</definedNames>

The smaller range-shaped fields (MergedRange, FreezePane, AutoFilter, ColumnProps, RowProps) are all straightforward attribute bags or lists of them:

<mergedRanges>
  <mergedRange topLeft="A1" bottomRight="C1" />
</mergedRanges>
<freezePane rows="1" columns="0" />
<autoFilter topLeft="A1" bottomRight="D11" />
<columnProps>
  <columnProp index="0" width="20" />
</columnProps>
<rowProps>
  <rowProp index="0" height="30" />
</rowProps>

Xml.schemaSet() loads the compiled schema for validating either direction yourself (XDocument.Validate) - every scenario under tests/Kookerella.FsOpenXmlDsl.Tests/Examples/ has a committed workbook.xml validated against it this way as part of the same test that generates it, so the schema and Xml.fs itself can never silently drift apart. Xml.fs covers the same worksheet/workbook-level feature set as the rest of this library and the C# wrapper - cell values, styles, merged ranges, freeze panes, autofilter, column/row sizing, VBA (base64), defined names, hyperlinks, comments, sheet/workbook protection, print settings, images (base64), Excel Tables, sparklines, charts, pivot tables (the description only - loading one doesn't re-run its aggregation, unlike everything else here), conditional formatting, and data validation.

Kookerella.FsOpenXmlDsl.Mcp exposes both directions without writing any F# at all: generate_xml/create_workbook_from_xml MCP tools for an AI agent, and fsopenxmldsl-mcp convert --lang xml/build CLI commands for anyone else - see that project's own README.

JSON

Json.toWorkbook/Json.ofWorkbook (in Json.fs) are a fourth way in and out of the DSL, alongside writing F#/C# directly, code generation, and XML: plain JSON, for a caller whose tooling speaks JSON rather than XML. The same two concrete uses XML has apply here:

  • Build an .xlsx from JSON a transform/generation pipeline already produces - without learning the OOXML schema or this library's own API.

  • Convert an existing .xlsx to JSON for version control - the same determinism Xml.ofWorkbook has (sorted by cell position, or by name for defined names) applies to Json.ofWorkbook's output too, for the same reason: a genuine content change produces a small, isolated diff rather than a spurious one from lists getting reshuffled between runs.

open System.Text.Json.Nodes

// JSON -> Workbook -> .xlsx
let wb = JsonNode.Parse(File.ReadAllText "report.json").AsObject() |> Json.toWorkbook
Workbook.save "report.xlsx" wb

// .xlsx -> Workbook -> JSON
let json = Workbook.load "report.xlsx" |> Json.ofWorkbook
File.WriteAllText("report.json", json.ToJsonString())

A discriminated union case becomes a single-key JSON object named after the case (camelCased) when it carries data of its own, or a bare JSON string (also camelCased) when it's one of several parameterless alternatives - e.g. a cell's value:

{
  "ref": "B2",
  "number": 42.5,
  "style": { "numberFormat": "currency" }
}

The same DataValidation example as above, in JSON - unlike the XML surface, which flattens ValidationAlert's fields onto <dataValidation> itself, JSON nests both kind and alert as their own objects, the more natural shape for this format:

{
  "topLeft": "A2",
  "bottomRight": "A2",
  "kind": { "wholeNumberValidation": { "operator": "greaterThan", "formula1": "0" } },
  "alert": {
    "errorTitle": "Invalid quantity",
    "errorMessage": "Quantity must be a positive whole number."
  }
}

The same ConditionalFormat example as above, in JSON - rule nests one of the seven cases the same way kind does above, and (unlike XML's bare <fill>) fill always wraps its color under an explicit key:

{
  "topLeft": "A1",
  "bottomRight": "A3",
  "rule": {
    "cellValueRule": {
      "operator": "greaterThan",
      "formula1": "100",
      "style": { "fill": { "color": { "rgb": { "r": 255, "g": 199, "b": 206 } } } }
    }
  }
}

The same Chart example as above, in JSON - series is a plain array, with no need for the wrapper-vs-item-name trick <series>/<s> exist for in XML, since a JSON array is self-delimiting:

{
  "type": "column",
  "title": "Sales by Quarter",
  "showLegend": true,
  "anchorTopLeft": "E1",
  "anchorBottomRight": "L15",
  "categories": { "topLeft": "A2", "bottomRight": "A4" },
  "series": [
    { "name": "B1", "valuesTopLeft": "B2", "valuesBottomRight": "B4" },
    { "name": "C1", "valuesTopLeft": "C2", "valuesBottomRight": "C4" }
  ]
}

The same Table example as above, in JSON - columns is just another plain array, same as series:

{
  "topLeft": "A1",
  "bottomRight": "B4",
  "name": "Calc",
  "columns": [
    { "name": "Qty" },
    { "name": "Doubled", "calculatedFormula": "Calc[Qty]*2" }
  ],
  "style": {
    "name": "TableStyleLight9",
    "showFirstColumn": true,
    "showLastColumn": true,
    "showColumnStripes": true
  }
}

The same SparklineGroup example as above, in JSON - color sits as a plain nested key alongside the style's other fields, the same way fill's does under CellStyle:

{
  "style": {
    "type": "column",
    "lineWeight": 1.5,
    "showNegative": true,
    "color": { "rgb": { "r": 0, "g": 112, "b": 192 } }
  },
  "sparklines": [
    { "cell": "E1", "dataTopLeft": "A1", "dataBottomRight": "D1" }
  ]
}

The same PivotTable example as above, in JSON - a flat object either way, since there's nothing here that's a list or a nested structure:

{
  "sourceSheet": "Data",
  "sourceTopLeft": "A1",
  "sourceBottomRight": "C9",
  "rowField": "Region",
  "columnField": "Quarter",
  "valueField": "Sales",
  "aggregation": "average",
  "valueCaption": "Avg Sales",
  "anchorTopLeft": "F1"
}

An Image's bytes are a base64 string value, same as vbaProject below:

{ "format": "gif", "topLeft": "A1", "bottomRight": "D6", "data": "R0lGODlhAQABAIAAAAAAAP..." }

A Hyperlink, in JSON:

{
  "topLeft": "A1",
  "bottomRight": "A1",
  "target": { "externalHyperlink": "https://example.com" },
  "tooltip": "Visit site"
}
{
  "topLeft": "A2",
  "bottomRight": "B3",
  "target": { "internalHyperlink": "Sheet1!A1" },
  "display": "Go to top"
}

A Comment - author is a plain optional field, omitted rather than an empty string:

{ "cell": "A1", "author": "Alex", "text": "Check this figure" }
{ "cell": "A2", "text": "Unnamed author" }

Sheet and workbook protection, in JSON - flat objects, same as the XML:

{ "password": "hunter2", "sheet": true, "formatCells": true, "sort": true, "autoFilter": true }
{ "sheets": [ { "name": "Sheet1" } ], "protection": { "password": "hunter2", "lockStructure": true } }

PageSetup - PaperSize's named cases are a bare string, the mixed-DU convention NumberFormat also uses:

{
  "orientation": "landscape",
  "paperSize": "a4",
  "margins": { "left": 0.5, "right": 0.5, "top": 1, "bottom": 1, "header": 0.2, "footer": 0.2 }
}

The same richer example as above, in JSON - PaperSize's escape hatch is {"other": 9}, PrintScaling's two cases are single-key objects same as everywhere else, and printArea is a plain array:

{
  "orientation": "portrait",
  "paperSize": { "other": 9 },
  "scaling": { "fitToPage": { "width": 1, "height": 0 } },
  "margins": { "left": 0.7, "right": 0.7, "top": 0.75, "bottom": 0.75, "header": 0.3, "footer": 0.3 },
  "printArea": [ { "topLeft": "A1", "bottomRight": "D10" } ],
  "header": "&C&\"Arial,Bold\"Report",
  "footer": "&LPage &P of &N"
}

Worth knowing: System.Text.Json's default encoder escapes every &, <, >, and " character inside a string value as a \uXXXX sequence (the conservative choice for JSON that might end up embedded in HTML), so generate_json's actual output for the header above has each of those characters replaced that way, not left as plain text the way it's shown here for readability. This is cosmetic, not a data-loss bug (Json.toWorkbook parses the escaped form back to the exact original string either way, verified by round-tripping this exact example) - but it's worth knowing before assuming a generate_json result is corrupted, especially since Excel's header/footer codes (&C/&L/&R/&P/&N/...) all begin with &, so real header/footer text is guaranteed to render this way.

VbaProject, at the workbook level:

{ "sheets": [ { "name": "Sheet1" } ], "vbaProject": "AQIDBA==" }

DefinedNameScope's two cases follow the standard JSON convention cleanly, unlike XML's <workbookScope />/<sheetScope> split - WorkbookScope is simply the bare string, same treatment as any other parameterless case:

{
  "definedNames": [
    { "name": "LocalTotal", "formula": "Sheet1!$A$2", "scope": { "sheetScope": "Sheet1" }, "hidden": true },
    { "name": "TaxRate", "formula": "0.075", "scope": "workbookScope" }
  ]
}

The smaller range-shaped fields, in JSON:

{
  "mergedRanges": [ { "topLeft": "A1", "bottomRight": "C1" } ],
  "freezePane": { "rows": 1, "columns": 0 },
  "autoFilter": { "topLeft": "A1", "bottomRight": "D11" },
  "columnProps": [ { "index": 0, "width": 20 } ],
  "rowProps": [ { "index": 0, "height": 30 } ]
}

Unlike XML, .NET has no built-in JSON Schema validator the way System.Xml.Schema exists for XML, so Json.schema.json (in the repo, matching this shape) is validated only from this repo's own test suite (via a test-only JsonSchema.Net dependency) rather than exposed as a public Json.schemaSet()-style API. Every scenario under tests/Kookerella.FsOpenXmlDsl.Tests/Examples/ has a committed workbook.json validated against it this way too, the same as workbook.xml is against Xml.xsd, so the schema and Json.fs itself can never silently drift apart there either. Json.fs covers the same worksheet/workbook-level feature set Xml.fs does - cell values, styles, merged ranges, freeze panes, autofilter, column/row sizing, VBA (base64), defined names, hyperlinks, comments, sheet/workbook protection, print settings, images (base64), Excel Tables, sparklines, charts, pivot tables (the description only - loading one doesn't re-run its aggregation, unlike everything else here), conditional formatting, and data validation.

Kookerella.FsOpenXmlDsl.Mcp exposes both directions without writing any F# at all: generate_json/create_workbook_from_json MCP tools for an AI agent, and fsopenxmldsl-mcp convert --lang json/build CLI commands for anyone else - see that project's own README.

Building and testing

dotnet build
dotnet test --filter "Category!=Slow"
dotnet run --project samples/Kookerella.FsOpenXmlDsl.Sample

The default loop above skips the slow Category=Slow tests, which actually invoke dotnet fsi on every generated Examples/*/script.fsx (multi-second process startup each, so ~30-60s total) rather than just checking the generated source parses. Run those explicitly, after the fast suite has populated the .fsx files at least once:

dotnet test --filter "Category=Slow"

Plain dotnet test (no filter) runs both groups.

Sponsorship

If this project is useful to you, sponsoring it helps fund ongoing development. Sponsorship supports the project - it doesn't include support SLAs, feature guarantees, or priority response times. The software is provided as-is under the MIT license, with or without sponsorship.

Available Tools

10 tools
create_workbookA

Creates a new Excel workbook (.xlsx) from a simple grid of sheets/rows/cells and saves it to disk. Each cell is given as plain text, the same way you'd type it into Excel: a leading '=' makes it a formula (e.g. "=SUM(A1:A2)"), 'true'/'false' makes a boolean, a bare number is numeric, and anything else is text. Rows don't need to be the same length. Does not support cell styling, tables, charts, or pivot tables in this version - reference the Kookerella.FsOpenXmlDsl library directly for those.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesOutput file path, e.g. "C:\reports\invoice.xlsx". The directory must already exist.
sheetsYesThe sheets to create, in order. Each sheet has a Name and a Rows grid (array of rows, each an array of cell text).

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full transparency burden and succeeds. It explains cell type inference (formula, boolean, numeric, text), allows variable-length rows, discloses unsupported features, and states the side effect of saving to disk. This gives an agent a clear model of what will happen when the tool is invoked.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, well-structured paragraph with no filler. The main purpose is front-loaded, followed by essential cell-format rules and a clear list of limitations. Every sentence contributes to correct invocation or expectation-setting.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter tool with no output schema and no annotations, this description is complete for invocation purposes. It explains what the sheets parameter should contain, how cells are interpreted, the file path expectation, and what is not supported. No critical information for calling the tool correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already documents both parameters at 100% coverage, which sets a baseline of 3. The description adds meaningful semantics beyond the schema by explaining how cell strings are interpreted (leading '=' for formulas, 'true'/'false' for booleans, bare numbers as numeric) and that rows need not be uniform in length. This helps the agent construct correct parameter values.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: creating a new Excel workbook (.xlsx) and saving it to disk. It also clearly scopes the creation mode to 'a simple grid of sheets/rows/cells', which distinguishes it from sibling tools like create_workbook_from_json and create_workbook_from_xml.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly communicates when to use the tool: when you have a simple grid of sheet/row/cell data. It also explicitly lists unsupported features and directs users to the underlying library for those needs. However, it does not name the sibling JSON/XML-based workbook tools as alternatives, so the routing is not fully explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_workbook_from_jsonA

Builds a new Excel workbook from JSON matching the shape generate_json produces (see the main library repo's Json.schema.json) and saves it to disk - the inverse of generate_json. The JSON-side equivalent of create_workbook_from_xml, for a caller that already produces data as JSON and wants to reach Excel without learning the OOXML schema or this library's own F#/C# API. Covers the same worksheet/workbook-level feature set generate_json does; unlike create_workbook, this isn't limited to plain cell values - styling, tables, charts, and every other modeled feature can be expressed in the JSON.

ParametersJSON Schema
NameRequiredDescriptionDefault
jsonYesThe workbook JSON content - an object matching Json.schema.json's root shape.
pathYesOutput file path, e.g. "C:\reports\invoice.xlsx". The directory must already exist.

TDQS

A4.3/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

There are no annotations, so the description carries the burden. It clearly discloses the side-effect of saving to disk and the inverse relationship to generate_json. However, it does not mention overwrite behavior, failure modes, validation of the JSON, or filesystem permissions, which are relevant for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is thorough and front-loaded with the core action, but longer than strictly necessary. Every sentence earns its place by clarifying the inverse relationship, sibling tool, and feature scope, though a more compact phrasing could preserve the same value with less verbosity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema and no annotations, the description gives an agent enough context to invoke correctly: it identifies the expected JSON shape, the output destination, and how this tool relates to alternative sibling tools. It is somewhat incomplete on overwrite behavior and errors, but those are minor gaps for a tool with only two well-described parameters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already covers both parameters fully, so baseline is 3. The description adds value by explaining that json must match the shape generate_json produces, linking to Json.schema.json, and enumerating the feature set expressible in JSON, which clarifies what content the json parameter can hold beyond the schema's generic wording.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

This says it builds a new Excel workbook from JSON and saves it to disk – a specific verb, resource, and output. It also differentiates itself from siblings: inverse of generate_json, JSON-side equivalent of create_workbook_from_xml, and distinct from create_workbook because it supports styling, tables, charts, and every other modeled feature.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly states when to use it: when a caller already has data as JSON and wants Excel without learning OOXML schema or the library's own F#/ C# API. It also names alternatives and exclusions: use create_workbook when only plain cell values are needed, and notes it covers the same feature set as generate_json.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_workbook_from_xmlA

Builds a new Excel workbook from XML matching Kookerella.FsOpenXmlDsl's own embedded schema (Xml.xsd) and saves it to disk - the inverse of generate_xml. The natural target for a caller that already produces data as XML (e.g. an XSLT pipeline generating a report) and wants to reach Excel without learning the OOXML schema or this library's own F#/C# API. Covers the same worksheet/workbook-level feature set generate_xml does; unlike create_workbook, this isn't limited to plain cell values - styling, tables, charts, and every other modeled feature can be expressed in the XML.

ParametersJSON Schema
NameRequiredDescriptionDefault
xmlYesThe workbook XML content - a <workbook> root element matching Xml.xsd.
pathYesOutput file path, e.g. "C:\reports\invoice.xlsx". The directory must already exist.

TDQS

A4.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the burden of behavioral disclosure. It does disclose that the tool writes to disk and requires XML matching the embedded Xml.xsd schema, and it conveys feature parity with generate_xml. However, it does not mention overwrite behavior, failure modes for invalid XML, or whether any status/return is produced, so coverage is adequate but incomplete.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences each serve a distinct purpose: core action, intended usage context, and differentiation from sibling tools. There is no filler or repetition of schema content, and the most important information is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 2-parameter tool with full schema coverage, the description provides the key contextual pieces: schema identity, disk-writing behavior, use-case, and relationships to generate_xml and create_workbook. Minor gaps remain around overwrite and error behavior, but the description is sufficient for correct invocation in most cases.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds useful parameter-level meaning by elaborating that the XML can express styling, tables, charts, and every modeled feature, going beyond the schema's terse 'matches Xml.xsd' note. The path parameter is fully covered by its schema description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states a specific action: builds a new Excel workbook from XML and saves it to disk, and explicitly frames itself as the inverse of generate_xml. It also distinguishes itself from create_workbook by noting it supports styling, tables, charts, and other modeled features beyond plain cell values.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly describes the natural caller profile: someone who already has data as XML (e.g., from an XSLT pipeline) and wants to reach Excel without learning OOXML or the library API. It also states when create_workbook is insufficient and names generate_xml as the inverse, giving clear routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

generate_csharp_scriptA

Reads an existing Excel workbook and returns a self-contained C# file (using Kookerella.CsOpenXmlDsl) that rebuilds an equivalent file when run via dotnet run <file>.cs (.NET 10's file-based apps feature - no .csproj needed). The C# equivalent of generate_fsharp_script, for a caller who wants pasteable/runnable C# rather than F# - useful for explaining how a file is structured, or as a starting point for the wrapper's fluent API (styling, tables, charts, pivot tables, sparklines, conditional formatting, data validation, hyperlinks, comments, print settings, defined names, protection, etc. - Kookerella.CsOpenXmlDsl now covers the same worksheet/workbook-level feature set as generate_fsharp_script's own Kookerella.FsOpenXmlDsl) beyond what create_workbook exposes.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesPath to an existing .xlsx/.xlsm file to reverse-engineer into C# source.
outputFileNameYesThe output filename the generated script should save its rebuilt file to, e.g. "output.xlsx".

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and discloses the key behavioral traits: it reads an existing workbook (non-mutating on input), produces a pasteable/runnable artifact, requires .NET 10 file-based apps (no .csproj needed), and scopes generated feature coverage explicitly (styling, tables, charts, pivot tables, conditional formatting, etc.). Does not address edge behavior — overwrite semantics for outputFileName or failure on invalid files — but the agent gets a realistic model of what happens.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core action is front-loaded in sentence one, with the runtime requirement attached. Sentence two is overloaded — a large parenthetical enumerating every supported feature (styling, tables, charts, pivot tables, sparklines, conditional formatting, data validation, hyperlinks, comments, print settings, defined names, protection) — which is informative but makes the definition read as a run-on. A trimmed version keeping the feature-scope summary would earn a 4.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 2-parameter tool with no output schema and no annotations, the description covers purpose, output artifact, execution model, and fidelity scope — the agent knows what the call produces and how it behaves. Remaining gaps (error handling, whether an existing output file is overwritten) are edge cases and not blocking for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so both path and outputFileName are already documented; the description does not add format or syntax details. It does supply the causal connection (path feeds the read, outputFileName receives the rebuilt file) that ties the two parameters into the tool's pipeline. Baseline 3 is correct since the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb-resource pair ('Reads an existing Excel workbook and returns a self-contained C# file') and states the generated artifact's runtime behavior. Explicitly contrasts with the sibling generate_fsharp_script ('The C# equivalent of'), so an agent can distinguish them by output language alone. The reverse-engineering intent is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Names the primary alternative directly — generate_fsharp_script — and gives the selection criterion ('for a caller who wants pasteable/runnable C# rather than F#'). Also positions the tool against create_workbook ('beyond what create_workbook exposes'). Stops short of a formal when-not-to-use statement for the remaining siblings, so 4 rather than 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

generate_fsharp_scriptA

Reads an existing Excel workbook and returns a self-contained F# script (using Kookerella.FsOpenXmlDsl) that rebuilds an equivalent file when run via dotnet fsi. Useful for explaining how a file is structured, or as a starting point for a caller who wants the library's full feature set (styling, tables, charts, pivot tables, etc.) beyond what create_workbook exposes.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesPath to an existing .xlsx/.xlsm file to reverse-engineer into F# source.
outputFileNameYesThe output filename the generated script should save its rebuilt file to, e.g. "output.xlsx".

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are present, so the description carries the full behavioral burden. It transparently discloses that the tool reads the input rather than mutating it, that the output is a self-contained script, and that the script should be executed via dotnet fsi. It does not mention edge cases like unsupported workbook features, but the core non-mutating and code-generation behavior is clear.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two focused sentences: the first states behavior and output, the second adds use cases. The parenthetical library reference and dotnet fsi command are relevant, and there is no padding or repeated schema information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter read-and-generate tool with no output schema, the description is near-complete: it explains the input, the generated output, the execution mechanism, and why one would prefer this over create_workbook. It could be strengthened by explicitly stating that the tool does not execute the script or write files itself, though 'returns a script' already conveys this reasonably well.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Both parameters are already fully documented in the input schema (100% schema description coverage), and the description does not add new semantics beyond what the schema says. The phrase 'rebuilds an equivalent file' corroborates the outputFileName role but does not provide extra detail, so the baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description is unambiguous: it reads an existing Excel workbook and produces a self-contained F# script that can rebuild an equivalent file. It clearly differs from the sibling JSON/XML/C# generators and from create_workbook, which builds a workbook directly rather than generating source code.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives concrete usage contexts: explaining how a file is structured, or starting from a point that exposes library features beyond create_workbook. It explicitly names create_workbook as an alternative, though it does not enumerate exclusions for other siblings like generate_csharp_script or read_workbook.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

generate_jsonA

Reads an existing Excel workbook and returns it as JSON. The JSON-side equivalent of generate_xml, for a caller whose tooling speaks JSON rather than XML - same use cases (inspect, transform, or archive a workbook's structure without any F#/C# source involved) and the same worksheet/workbook- level feature set. Unlike generate_xml, there's no runtime JSON Schema validation built into the core library itself (see generate_json_schema's own doc string for why) - but generate_json_schema still returns the documented shape this produces.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesPath to an existing .xlsx/.xlsm file to convert to JSON.

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must carry the behavioral disclosure burden. It does so by making clear this is a read operation with no source code involved, and by disclosing that no runtime JSON Schema validation occurs, which is a meaningful caveat for callers expecting validation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core action is front-loaded in the first sentence, and the remaining sentences earn their place by providing tool selection guidance, a caveat about validation, and a pointer to the schema companion. There is no filler or redundant restatement of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter read tool with no output schema, the description covers purpose, alternatives, use cases, and a known behavioral limitation. It also points to generate_json_schema for the return shape. A fully complete definition would still give a bit more direct detail about the returned JSON structure, hence not a 5.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% for the single 'path' parameter, with the schema already defining it as a path to an existing .xlsx/.xlsm file. The description adds no new parameter-level meaning beyond what the schema provides, so the baseline score of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a clear verb and resource: 'Reads an existing Excel workbook and returns it as JSON.' It also distinguishes itself from generate_xml by framing itself as the JSON-side equivalent, so an agent can tell immediately what this tool does and how it differs from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states the intended use case: callers whose tooling speaks JSON rather than XML, for inspecting, transforming, or archiving a workbook. It names generate_xml as the direct alternative and notes the lack of runtime JSON Schema validation, routing the agent to generate_json_schema for that purpose.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

generate_json_schemaA

Returns the raw JSON Schema (Json.schema.json) that generate_json's output and create_workbook_from_json's input both conform to. Meant for a caller authoring JSON by hand or by a generation script who wants real schema validation/autocomplete in their own editor or pipeline, rather than reverse-engineering the shape from a generate_json example. This schema isn't validated against at runtime by the core library itself the way Xml.xsd is (JSON Schema has no .NET-built-in equivalent to System.Xml.Schema, so wiring that up would mean adding a runtime dependency - JsonSchema.Net - to every consumer of the core library just for this) - it's bundled here, in the Mcp tool specifically, purely to hand back on request.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and succeeds admirably. It discloses that the schema is not runtime-validated by the core library (unlike Xml.xsd), explains the technical reason (.NET has no built-in JSON Schema equivalent), and clarifies that it is bundled in the MCP tool purely to hand back on request. This gives the agent an accurate mental model of the tool's limitations without opening any code.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose in the first sentence, followed by valuable context about the intended audience and a technical caveat. It's somewhat long, but every sentence earns its place; there is no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 0-parameter tool with no annotations and no output schema, the description is fully complete. It covers what the tool returns, who it is for, why the schema is bundled in the MCP tool rather than the core library, and hints at the alternative (using generate_json examples). An agent has everything it needs to decide when and why to call this.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has 0 parameters, so the schema itself is trivial (empty properties object). The baseline for 0 params is 4, and the description adds relevant context about what is returned, though it doesn't need to explain any parameter behavior since there are none.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb ('Returns'), a precise resource ('raw JSON Schema (Json.schema.json)'), and explicitly identifies what it conforms to (generate_json's output and create_workbook_from_json's input). It also differentiates from siblings by referencing the alternative (generate_json example) and the Xml.xsd contrast.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

States exactly when to use this tool: for a caller authoring JSON by hand or via a generation script who wants schema validation/autocomplete, rather than reverse-engineering from a generate_json example. This effectively routes the agent to the right tool for the right context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

generate_xmlA

Reads an existing Excel workbook and returns it as XML, validated against Kookerella.FsOpenXmlDsl's own embedded schema (Xml.xsd). A plain-data alternative to generate_fsharp_script/generate_csharp_script for a caller who wants to inspect, transform (e.g. via XSLT), or archive a workbook's structure without any F#/C# source involved - unlike those two, this returns data, not a runnable script, so there is no output-filename parameter to control what a rebuild saves as. Covers the same worksheet/workbook-level feature set generate_fsharp_script does.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesPath to an existing .xlsx/.xlsm file to convert to XML.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It clearly indicates that this is a read operation returning XML validated against an embedded schema, and it explains the important non-script behavior. It does not explicitly state the input workbook is unmodified, but 'Reads' strongly implies a non-destructive operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than minimal but every clause contributes: the core behavior, the intended use cases, the contrast with script-generating siblings, and the feature-set scope. It is front-loaded with the primary action, though the sibling-contrast material is slightly dense.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With only one parameter and no output schema, the description sufficiently clarifies that the tool returns XML data rather than a script and validates against Xml.xsd. It does not detail the exact XML structure, but that is acceptable because the output is validated by an embedded schema and the input surface is minimal.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single parameter path is already fully documented in the input schema, including the .xlsx/.xlsm nuance. The description adds no additional parameter-level information, so with 100% schema coverage the baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Reads an existing Excel workbook and returns it as XML.' It also distinguishes itself from siblings by explicitly framing itself as a plain-data alternative to generate_fsharp_script/generate_csharp_script, making its role clear relative to other generators.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit use cases: inspecting, transforming via XSLT, or archiving a workbook's structure without F#/C# source. It names the alternative tools and explains the deciding difference: generate_xml returns data, not a runnable script, and therefore has no output-filename parameter.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

generate_xml_schemaA

Returns the raw XSD (Xml.xsd) that generate_xml's output and create_workbook_from_xml's input both conform to. Meant for a caller authoring XML by hand or by transform (e.g. an XSLT stylesheet) who wants real schema validation/autocomplete in their own editor or pipeline, rather than reverse- engineering the shape from a generate_xml example.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the burden of behavioral disclosure. It adequately does so by describing a pure retrieval operation that returns a static XSD with a stated conformance relationship. The zero-parameter signature further implies no side effects, though the description does not explicitly say the operation is read-only.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single front-loaded sentence that begins with the action and resource, then adds focused context about target users. Every clause earns its place, with no redundant wording or repetition of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple, parameterless retrieval tool with no output schema, the description explains what is returned and why an agent might call it. The only minor gap is the lack of an explicit comparison to generate_json_schema, but the XML-specific wording and sibling context make the distinction clear enough.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the baseline is 4. The schema already documents an empty object, and the description adds all necessary context about what the returned value represents. No parameter documentation is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies a specific verb ('Returns'), a specific resource ('raw XSD (Xml.xsd)'), and the exact role the XSD plays between generate_xml's output and create_workbook_from_xml's input. This makes it clearly distinguishable from sibling schema tool generate_json_schema by grounding it in the XML workflow.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description states the intended caller ('a caller authoring XML by hand or by transform') and the concrete condition under which the tool is valuable ('wants real schema validation/autocomplete... rather than reverse-engineering'). It does not explicitly name alternatives to avoid, but the context makes the intended usage unambiguous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

read_workbookA

Reads an existing Excel workbook (.xlsx/.xlsm) and returns its sheets and cell contents as JSON. Formula cells are rendered as "=expression" (matching create_workbook's input convention), with any cached value included separately under CachedValue. Features outside the core cell model (charts, tables, pivot tables, styling, etc.) are not included in this output - see MAPPING.md in the main library repo for the full list of what round-trips.

ParametersJSON Schema
NameRequiredDescriptionDefault
pathYesPath to an existing .xlsx or .xlsm file.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full disclosure burden and does so well. It states how formula cells are represented, that cached values are included separately, and explicitly lists major omitted features such as charts, tables, and styling, pointing to MAPPING.md for full round-trip details.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded: the core behavior appears first, then the important formula edge case, then the limitation note. Each sentence contributes distinct, useful information with no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema, the description explains what is returned, how formulas appear, and what is intentionally excluded, and it references MAPPING.md for a full compatibility list. It could be slightly more explicit about the exact JSON layout of sheets and cell references, but it is complete enough for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single path parameter is already thoroughly documented in the schema at 100% coverage, including the required .xlsx/.xlsm format and existence requirement. The tool description's file-format mention adds no additional semantic meaning beyond the schema, so a baseline score is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: it reads an existing .xlsx/.xlsm workbook and returns sheets/cell contents as JSON. It also names the formula rendering convention, which further distinguishes this reader from the sibling creation/generation tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly implies the use case: read an existing workbook when you need its cell data as JSON. It does not explicitly name alternatives such as create_workbook for writing or generation tools for other outputs, so it lacks explicit exclusion routing but provides clear context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 10 tool updatesv1.0.0
    • First observedcreate_workbook
    • First observedcreate_workbook_from_json
    • First observedcreate_workbook_from_xml
    • First observedgenerate_csharp_script
    • First observedgenerate_fsharp_script
    • First observedgenerate_json
    • First observedgenerate_json_schema
    • First observedgenerate_xml
    • First observedgenerate_xml_schema
    • First observedread_workbook

TDQS

A4.3/5.0
Disambiguation4/5

Each tool has a distinct role: F#/ C# scripts, XML/ JSON round-trips, simple vs full-featured read/write, and schema retrieval. The only plausible confusion is between read_workbook and generate_json, which both return JSON but at different fidelities.

Naming Consistency5/5

All tools follow a consistent snake_case verb_noun pattern: generate_*, create_workbook_from_*, read_workbook, and create_workbook. Format modifiers like json/xml and fsharp/csharp are clear and uniformly applied.

Tool Count5/5

Ten tools are well-scoped for a workbook conversion/creation bridge. Each tool covers a meaningful mode of interaction (simple or full, JSON or XML, script or schema) without redundant bulk.

Completeness4/5

The set covers round-trip reading/creating in both XML and JSON, simple cell-level creation, full-featured creation through structured inputs, schema retrieval, and script generation for both F# and C#. No update/moidify output is provided, but that appears outside the stated conversion-oriented purpose. Minor gap only.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Kookerella-Ltd/Kookerella.FsOpenXmlDsl'

If you have feedback or need assistance with the MCP directory API, please join our Discord server