Word MCP Server (FsWordDsl)
Word tools for AI agents: read/write, decompile to F#/C#/XML/JSON, or build from XML/JSON.
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
Kookerella.FsWordDsl.Mcpclaude mcp add word-mcp-server-fsworddsl -- dnx Kookerella.FsWordDsl.Mcp@0.1.1 --yes
{
"mcpServers": {
"word-mcp-server-fsworddsl": {
"command": "dnx",
"args": [
"Kookerella.FsWordDsl.Mcp@0.1.1",
"--yes"
]
}
}
}
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
Kookerella.FsWordDsl
A typesafe F# DSL for building Word documents, 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 .docx back into the
same DSL.
This is the WordprocessingML sibling of Kookerella.FsOpenXmlDsl (the Excel/SpreadsheetML one) - same objectives, same round-trip philosophy, translated to Word's own document model. See MAPPING.md for exactly which WordprocessingML features map 1:1, which are approximated, and which aren't modeled yet.
This round-trips in both directions, which most Word libraries don't: they give you an
imperative API to build a document from scratch, but no way to turn an existing file back
into readable source. Here, Reader parses a real .docx/.docm back into the same DSL,
and Document.generateScript goes one step further and renders that model back out as a
self-contained script that rebuilds an equivalent file - a decompiler for Word documents,
not just a writer. Two more surfaces, Xml.toDocument/Xml.ofDocument (see "## XML"
below) and Json.toDocument/Json.ofDocument (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.
A fluent C# wrapper (Kookerella.CsWordDsl) sits on top of the F# core for callers who'd
rather not touch F# discriminated unions/option types directly - immutable records with
With* builders, plus its own CsCodeGen decompiler that renders a Document back out as
runnable C# source. See "## The C# wrapper" below.
An MCP server (Kookerella.FsWordDsl.Mcp) exposes the same read/write/decompile
capabilities as tools any MCP-compatible AI agent can call directly, and doubles as a plain
CLI (fsworddsl-mcp convert/build) for anyone not going through an MCP client at all -
see its own README for the full tool list.
Don't have .NET installed? Download a self-contained build of the MCP server for your platform (win/linux/osx, x64/arm64) from the Releases page - unzip and run, no install required.
Layout
src/Kookerella.FsWordDsl- the library.Units.fs- conversions between points/inches/pixels and the physical units WordprocessingML uses on the wire (twips for page geometry/spacing, EMU for image sizing).Styles.fs- character and paragraph formatting:Color(Rgb,Auto, or a theme-relativeThemecolor - seeThemeColorKind),HighlightColor(Word's own fixed highlight palette),UnderlineStyle,RunStyle(including small caps/all caps/hidden text),ParagraphAlignment,Indentation,LineSpacingRule,TabStopAlignment/TabLeader/TabStop,ParagraphFormat(including paragraph borders, shading, and custom tab stops),BorderLineStyle,BorderSide,BorderStyle(reused for both paragraph and table borders).NamedStyles.fs-StyleDefinition(paragraph or character, withBasedOninheritance) and a smallBuiltInStylescatalog (normal,heading1/2/3,title,listParagraph,hyperlinkCharStyle).Numbering.fs-NumberFormatKind,ListLevel,NumberingDefinitionfor numbered/bulleted lists, including multi-level ones (ListLevelisn't limited to one per definition - seeBuilders.multiLevelNumberedListDef).Hyperlinks.fs-HyperlinkTarget(external URL vs. internal bookmark reference).Protection.fs-EditRestrictionandDocumentProtection, document-level (Word has no per-section equivalent of Excel's per-sheet protection).Revisions.fs-RevisionKind/Revisionfor track changes (Inline.TrackedChange,Paragraph.MarkRevision) - narrowly scoped to inserted/deleted content and paragraph marks, seeMAPPING.mdfor what isn't covered.ContentControls.fs-ContentControlType/ContentControlPropsfor content controls (structured document tags,w:sdt): plain text, rich text, dropdown/combo box, date picker, checkbox - seeMAPPING.mdfor what isn't covered.PageSetup.fs-PageOrientation,PageSize,PageMargins,SectionBreakType,NoteNumberRestart/NoteNumberingSettings(a section's own footnote/endnote numbering).Tables.fs-TableBorders,VerticalMergeKind,TableCellProps(including a per-cellMarginsoverride),TableStyleRef,TableStyleRegion/TableStyleDefinition(custom table style definitions - all thirteen of OOXML's conditional-formatting regions), andCellMargins(shared shape for a table's default margins and a single cell's own override).Images.fs-ImageFormat,ImageEntry(raw file bytes plus an on-page size), anchored inline within a run.DocumentProperties.fs-DocumentProperties(Title, Author, Subject, Keywords, Comments, Category, Company) - core document metadata,Document.Properties.Model.fs- the recursive content model:Inline(runs, breaks, images, hyperlinks, bookmarks and comments - both the single-paragraphBookmark/Commentcases and the cross-paragraphBookmarkRangeStart/End/CommentRangeStart/Endmarkers, simple fields, footnotes/endnotes,TrackedChangefor track changes, andInlineContentControlfor content controls),Paragraph(includingMarkRevision),Block(paragraph, table, orContentControlBlock- the block-level counterpart toInlineContentControl),TableCell/TableRow(includingRepeatAsHeader)/TableEntry(includingCellMargins),HeaderFooterSet,SectionProperties(includingBreakTypeandFootnoteNumbering/EndnoteNumbering),Section,Document(includingDocument.VbaProject, a macro-enabled document's rawvbaProject.binbytes,Document.Properties, andDocument.TableStyles).Xml.fs/Xml.xsd- the XML surface:Xml.toDocument/Xml.ofDocumenttranslate aDocumentto/from anXElementtree, andXml.schemaSet()loads the paired schema (embedded in the assembly as a resource) for validating either direction. See "## XML" below.Json.fs/Json.schema.json- the JSON surface:Json.toDocument/Json.ofDocumenttranslate aDocumentto/from aSystem.Text.Json.Nodes.JsonObjecttree. Schema validation is test-suite only, not a public API. See "## JSON" below.Builders.fs- plain functional constructors (section,document,withStyles,withNumbering,withProtection,withVbaProject,withDocumentProperties,withTableStyles,bulletListDef,numberedListDef,multiLevelNumberedListDef) plusDocumentDsl- smart constructors (run,para(withmarkRevision),hyperlink,bookmark,comment,inserted/deleted(track changes),contentControl/contentControlBlock(content controls),image,footnote,endnote,tableCell,tableRow(withheight/repeatAsHeader),table(withstyle/borders/cellMargins)) with real optional parameters, the Word analog of the Excel repo'sSheetDsl.Interpreter/StyleRegistry.fs- shared run/paragraph/border/color conversions plusDocument.Styles<->styles.xml(internal).Interpreter/ImageWriter.fs/ImageReader.fs- an inline image's own DSL <-> DrawingML translation (internal).Interpreter/Writer.fs- DSL -> OOXML (internal).Interpreter/Reader.fs- OOXML -> DSL, the reverse transform (internal).Interpreter/CodeGen.fs- DSL -> F# source text: renders aDocumentback out as a self-contained.fsxscript that rebuilds an equivalent file when run (internal).Api.fs- the publicDocument.save/saveToStream/load/loadFromStream/generateScriptentry points.
src/Kookerella.CsWordDsl- the fluent C# wrapper (see "## The C# wrapper" below): immutable records/sealed recordclosed hierarchies mirroring the F# core's own types one-for-one,DocumentConverter.cs(internal, the two-way F#<->C# translation),DocumentIO.cs(Save/Load, the one place this project does I/O),CsCodeGen.cs(DSL -> C# source text, the C# analog ofInterpreter/CodeGen.fs).src/Kookerella.FsWordDsl.Mcp- the MCP server (see its own README):DocumentTools.fs(the tool surface, one[<McpServerTool>]-tagged member per tool),Program.fs(dispatches to the MCP stdio server, or to a plainconvert/buildCLI, depending onargv). Distributed as adotnet tool(fsworddsl-mcp), same as the Excel sibling's ownKookerella.FsOpenXmlDsl.Mcp.tests/Kookerella.FsWordDsl.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 document it builds toExamples/<test name>/output.docx(checked into the repo), plusscript.fsx(regenerates the file - a separate, slowerCategory=Slowtest group actually executes each one viadotnet fsi),document.xml, anddocument.json- one folder always has four views of the same example.tests/Kookerella.CsWordDsl.Tests-DriftGuardTests.cs(a reflection-based tripwire checking the C# wrapper's DU mirrors haven't fallen behind the F# core's own case counts),DocumentTests.cs(targeted round-trip assertions per feature),ExampleTests.cs(reloads the F# suite's own checked-inExamples/*/output.docxfixtures rather than re-authoring every scenario a second time),CsCodeGenTests.cs(actually executes a generated file viadotnet run --file, the C# analog of the F# suite'sCategory=Slowdotnet fsigroup).samples/Kookerella.FsWordDsl.Sample- a small console app that builds a document, saves it, and reads it back.
Quick start
open Kookerella.FsWordDsl
open type Kookerella.FsWordDsl.DocumentDsl
let doc =
document
[ section
[ para ([ run "Quarterly Report" ], styleId = "Title")
para
[ run "This report covers "
run ("Q1 2026", style = { RunStyle.Default with Bold = true })
run ", see the "
hyperlink ("full dataset", ExternalUrl "https://example.com/data")
run " for details." ] ] ]
doc |> Document.save "report.docx"
// Reverse transform:
let roundTripped = Document.load "report.docx"
document defaults Styles to BuiltInStyles.all, so styleId = "Heading1" (or any other
built-in id) just works without registering it first - pipe withStyles afterward to
replace or extend that set. run/para/hyperlink/bookmark/comment/image/
tableCell/tableRow/table are DocumentDsl members with real optional parameters
(open type Kookerella.FsWordDsl.DocumentDsl brings them into scope unqualified, same as
open type SheetDsl does in the Excel repo) - plain F# let bindings can't have optional
parameters, which is why this part of the DSL is a type.
A Paragraph's Inlines are naturally several independently-styled runs - rich text
(mixed formatting within one paragraph) is first-class, not a documented gap the way
Excel's single-uniform-run Text cell is:
para
[ run "Plain text, "
run ("bold", style = { RunStyle.Default with Bold = true })
run ", and "
run ("colored", style = { RunStyle.Default with Color = Some Color.red }) ]
RunStyle also covers small caps, all caps, and hidden text; ParagraphFormat covers
borders (BorderStyle, the same shape used for table borders) and shading:
para
([ run "ALL CAPS AND SMALL CAPS" ], format =
{ ParagraphFormat.Default with
Borders = Some { BorderStyle.None with Bottom = Some { Style = SingleLine; Width = Some 1.0; Color = Some Color.black } }
Shading = Some(Rgb(0xD9uy, 0xD9uy, 0xD9uy)) })
Custom tab stops (TabStop) sit on ParagraphFormat.TabStops - a right-aligned stop with a
dot leader is the classic table-of-contents pattern:
para
([ run "Introduction"; Tab; run "1" ], format =
{ ParagraphFormat.Default with TabStops = [ { Position = 288.0; Alignment = RightTab; Leader = DotLeader } ] })
Color also accepts a theme-relative token (Theme) alongside plain Rgb/Auto - since
this DSL has no theme part to resolve it against, real Word does that; Fallback is what a
themeless reader sees instead, the same "always also write a computed value" convention Word
itself follows:
run ("Accent-colored text", style = { RunStyle.Default with Color = Some(Theme(Accent1Theme, (0x1Fuy, 0x49uy, 0x7Duy), None, None)) })
Lists use a (numId, level) reference on the paragraph, resolved against a
NumberingDefinition attached to the document - NumberingDefinition.Levels isn't limited
to one level, and multiLevelNumberedListDef builds the common correctly-linked outline
shape for you:
document
[ section
[ para ([ run "First bullet" ], numbering = (1, 0))
para ([ run "Second bullet" ], numbering = (1, 0)) ] ]
|> withNumbering [ bulletListDef 1 ]
document
[ section
[ para ([ run "First topic" ], numbering = (1, 0))
para ([ run "First subtopic" ], numbering = (1, 1))
para ([ run "Second topic" ], numbering = (1, 0)) ] ]
|> withNumbering [ multiLevelNumberedListDef 1 3 ]
Tables are built from tableRow/tableCell, with column widths given once for the whole
table - a cell without an explicit width falls back to its column's width at write time:
table (
[ tableRow [ tableCell [ para [ run "Item" ] ]; tableCell [ para [ run "Qty" ] ] ]
tableRow [ tableCell [ para [ run "Widgets" ] ]; tableCell [ para [ run "12" ] ] ] ],
[ 200.0; 100.0 ],
style = TableStyleRef.Default
)
Cell merging - horizontal (GridSpan) and vertical (RestartMerge/ContinueMerge) - are
independent and combine on the same cell, matching real Word:
tableCell ([ para [ run "Spans 2 columns" ] ], props = { TableCellProps.Default with GridSpan = Some 2 })
A cell's own margins override the table's default the same CellMargins shape covers both:
tableCell ([ para [ run "Extra padding" ] ], props = { TableCellProps.Default with Margins = Some { CellMargins.Default with Top = Some 8.0; Bottom = Some 8.0 } })
A custom table style (TableStyleDefinition) lives in Document.TableStyles and is applied
by name, the same way a built-in like "TableGrid" is - here with a bold white header row on
a blue background, an italic last row, and alternating row shading, plus a table-wide default
cell margin and a row that repeats on every page:
let corporateStyle: TableStyleDefinition =
{ TableStyleDefinition.Default with
Id = "Corporate"
Name = "Corporate"
FirstRow =
{ TableStyleRegion.None with
RunFormat = Some { RunStyle.Default with Bold = true; Color = Some Color.white }
CellShading = Some(Rgb(0x4Fuy, 0x81uy, 0xBDuy)) }
LastRow = { TableStyleRegion.None with RunFormat = Some { RunStyle.Default with Italic = true } }
BandedRow = { TableStyleRegion.None with CellShading = Some(Rgb(0xDCuy, 0xE6uy, 0xF1uy)) } }
document
[ section
[ table (
[ tableRow ([ tableCell [ para [ run "Item" ] ]; tableCell [ para [ run "Qty" ] ] ], repeatAsHeader = true)
tableRow [ tableCell [ para [ run "Widgets" ] ]; tableCell [ para [ run "12" ] ] ] ],
[ 200.0; 100.0 ],
style = { TableStyleRef.Default with Name = "Corporate" },
cellMargins = { Top = Some 4.0; Bottom = Some 4.0; Left = Some 6.0; Right = Some 6.0 }
) ] ]
|> withTableStyles [ corporateStyle ]
TableStyleDefinition also covers FirstColumn/LastColumn, BandedColumn, and the four
corner cells (NorthEastCell/NorthWestCell/SouthEastCell/SouthWestCell) - the two
regions not modeled are each banding axis's second band, since in practice that's just
WholeTable's own background showing through (see MAPPING.md).
Sections carry their own page setup - a document is a sequence of Sections, mapping 1:1
onto real Word section breaks. BreakType is how a section begins relative to the
previous one - meaningless (and not written) on the very first section:
let landscape = { SectionProperties.Default with Orientation = Landscape }
document [ sectionWith landscape [ para [ run "A landscape-oriented page." ] ] ]
let continuous = { SectionProperties.Default with BreakType = ContinuousBreak }
document
[ section [ para [ run "Section 1." ] ]
sectionWith continuous [ para [ run "Section 2 - no page break from section 1." ] ] ]
Footnotes and endnotes mark a point in a paragraph's own Inlines - content is the
note's own body, written to word/footnotes.xml/endnotes.xml with an id Writer assigns
automatically (the reference-mark run itself is generated for you, on both ends):
para
[ run "This claim needs a citation"
footnote "Smith, J. (2023). A Study of Claims."
run ", and this one refers to a fuller discussion"
endnote [ para [ run "See the appendix for the full derivation." ] ] ]
A section's own footnote/endnote numbering (w:footnotePr/w:endnotePr) - None is Word's
own default (continuous decimal from 1); here footnotes are lower-roman and restart every
page, matching a common legal-document convention:
sectionWith
{ SectionProperties.Default with FootnoteNumbering = Some { Format = LowerRomanFormat; StartAt = None; Restart = RestartEachPage } }
[ para [ run "Body text."; footnote "A footnote numbered i, ii, iii, ... restarting each page." ] ]
Headers and footers are per-section, with Default/First/Even variants (the
titlePg/evenAndOddHeaders flags real Word needs are set automatically):
let footer = { HeaderFooterSet.None with Default = Some [ para [ run "Page "; Field("PAGE", Some "1") ] ] }
sectionWith { SectionProperties.Default with Footer = Some footer } [ para [ run "Body text." ] ]
Comments and bookmarks wrap inline content directly, the common single-paragraph case:
para [ comment ([ run "This figure needs review." ], "Please double check the totals.", author = "Alex") ]
Either spanning more than one paragraph uses two independent markers placed directly in
separate paragraphs instead, sharing an id - BookmarkRangeStart/BookmarkRangeEnd for
bookmarks, CommentRangeStart/CommentRangeEnd for comments (which carries the comment's
own metadata on its Start, since there's no wrapping case here to hang it off - see
MAPPING.md on why that id is write-time-only, unlike a bookmark's own name):
document
[ section
[ para [ BookmarkRangeStart "Section2"; run "This paragraph starts the bookmark" ]
para [ run "and this one ends it."; BookmarkRangeEnd "Section2" ] ] ]
document
[ section
[ para [ CommentRangeStart("review1", "Alex", None, None, "This section needs review."); run "Comment starts here" ]
para [ run "and ends here."; CommentRangeEnd "review1" ] ] ]
Track changes (inserted/deleted) wrap inline content the same way, marking it as
inserted or deleted under an author and date; a whole inserted or deleted paragraph
(rather than just some of its content) uses para's own markRevision instead, for the
paragraph's closing mark:
para
[ run "The quick "
inserted ([ run "brown " ], "Alex")
run "fox jumps over the "
deleted ([ run "lazy " ], "Alex")
run "dog." ]
para ([ run "This whole paragraph was inserted." ], markRevision = { Kind = Inserted; Author = "Alex"; Date = None })
Content controls (contentControl for run-level, contentControlBlock for block-level)
wrap their own currently-displayed content the same way, plus a ContentControlType (see
MAPPING.md for the full set - plain text, rich text, dropdown/combo box, date
picker, checkbox):
para
[ run "Client name: "
contentControl ([ run "Type here" ], PlainTextControl false, alias = "Client Name", tag = "clientName") ]
para
[ run "Favorite color: "
contentControl ([ run "Blue" ], DropDownControl([ "Red", "red"; "Green", "green"; "Blue", "blue" ], false)) ]
contentControlBlock([ para [ run "This whole paragraph is a rich-text content control." ] ], RichTextControl, alias = "Notes")
Document-level protection, macros, and core properties are all pipe-friendly, same shape as
Excel's own withProtection/withVbaProject:
document [...] |> withProtection { Edit = Some ReadOnlyRestriction; Password = Some "hunter2" }
document [...] |> withVbaProject (System.IO.File.ReadAllBytes("vbaProject.bin"))
document [...] |> withDocumentProperties { DocumentProperties.Default with Title = Some "Quarterly Report"; Author = Some "Kookerella" }
Save the result with a .docm path - Document.save/saveToStream automatically switch
the file's own declared content type to Word's macro-enabled kind whenever a VbaProject
is present, but real Word also expects the .docm extension to trust and run macros at all.
Regenerating a file as F# source
Given a Document (typically one you just Document.loaded from an existing file),
Document.generateScript renders it back out as a self-contained .fsx script that
rebuilds an equivalent file when run - a code-generating counterpart to Document.load:
let doc = Document.load "input.docx"
let referenceLines =
[ "#r \"path/to/Kookerella.FsWordDsl.dll\""
"#r \"path/to/DocumentFormat.OpenXml.dll\"" ]
let script = Document.generateScript referenceLines "output.docx" doc
System.IO.File.WriteAllText("regenerate.fsx", script)
Running dotnet fsi regenerate.fsx produces output.docx - 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. Every scenario under tests/
Kookerella.FsWordDsl.Tests/Examples/ has a committed script.fsx generated exactly this
way; the Category=Slow test group actually executes each one via dotnet fsi and checks
it reproduces the committed .docx.
The C# wrapper
Kookerella.CsWordDsl is an idiomatic, immutable, fluent C# wrapper over the F# core, for
callers who'd rather not touch F# discriminated unions or option types directly. Every F#
type has a C# mirror: plain records with With*/factory-method builders for product types,
enums for parameterless choices, and sealed record closed hierarchies (abstract record
base, private constructor, nested cases) for everything else - the same "sealed hierarchy"
pattern the Excel repo's own Kookerella.CsOpenXmlDsl uses for CellValue/
ConditionalFormatRule. Reference Kookerella.CsWordDsl instead of Kookerella.FsWordDsl
and never see an FSharpOption:
using Kookerella.CsWordDsl;
var doc = Document.Create(
Section.Of([
Block.Paragraph([new Inline.Run("Quarterly Report")], styleId: "Title"),
Block.Paragraph([
new Inline.Run("This report covers "),
new Inline.Run("Q1 2026", new RunStyle { Bold = true }),
Inline.HyperlinkText("full dataset", new HyperlinkTarget.ExternalUrl("https://example.com/data")),
new Inline.Run(" for details.")
])
]));
DocumentIO.Save(doc, "report.docx");
var loaded = DocumentIO.Load("report.docx");
Content controls, tables, track changes, comments, and every other feature the F# core
models are covered the same way - see tests/Kookerella.CsWordDsl.Tests/DocumentTests.cs
for a worked example per feature. CsCodeGen.Generate is the C# analog of
Document.generateScript: it renders a Document back out as a self-contained C# file
targeting .NET's "file-based apps" feature (dotnet run --file script.cs), rather than an
.fsx script:
var script = CsCodeGen.Generate(["#:project path/to/Kookerella.CsWordDsl.csproj"], "output.docx", loaded);
File.WriteAllText("regenerate.cs", script);
DocumentIO also exposes the F# core's other two ways in and out directly - schema-backed
XML/JSON (ToXml/FromXml, ToJson/FromJson) and F# script generation
(GenerateFSharpScript, CsCodeGen.Generate's F#-targeting sibling) - so a C# caller never
needs its own reference to Kookerella.FsWordDsl to reach any of the F# core's four I/O
surfaces from C#.
One design note worth stating explicitly: this wrapper's records use IReadOnlyList<T>
properties, and C#'s compiler-synthesized record equality does not deep-compare list
contents (two records holding equal-but-distinct list instances compare unequal via plain
.Equals()) - the same limitation Kookerella.CsOpenXmlDsl's own records have. Don't rely
on whole-Document equality in your own code; compare the specific values you care about,
the same way this repo's own DocumentTests.cs does.
XML
Xml.toDocument/Xml.ofDocument (in Xml.fs) are a third way in and out of the DSL,
alongside writing F# directly and code generation: plain XML, against a real schema
(Xml.xsd, embedded in the assembly). A data-carrying DU case becomes an element named
after the case; a parameterless-choice case becomes an attribute value or bare string,
matching the convention the Excel repo's own Xml.fs documents.
open System.Xml.Linq
// XML -> Document -> .docx
let doc = XElement.Load "report.xml" |> Xml.ofDocument
Document.save "report.docx" doc
// .docx -> Document -> XML
let xml = Document.load "report.docx" |> Xml.toDocument
xml.Save "report.xml"
A run with direct formatting and a hyperlink, in XML:
<para>
<run>Visit </run>
<hyperlink tooltip="Kookerella on GitHub">
<externalHyperlink>https://github.com/Kookerella-Ltd</externalHyperlink>
<content>
<run styleId="Hyperlink">Kookerella on GitHub</run>
</content>
</hyperlink>
<run> for more.</run>
</para>
Xml.schemaSet() loads the compiled schema for validating either direction yourself
(XDocument.Validate) - every scenario under tests/Kookerella.FsWordDsl.Tests/Examples/
has a committed document.xml validated against it this way as part of the same test that
generates it.
toDocument's output is deterministically ordered (Styles/Numbering/TableStyles sorted
by Id, regardless of the order the underlying Document's own lists happen to be in) -
paragraph/run content is already real document order and needs no sorting, but these three
are ID-referenced catalogs whose own list order carries no meaning, so this is what makes
committing document.xml to version control and diffing it across commits actually
meaningful: a genuine content change produces a small, isolated diff rather than a spurious
one from a catalog getting reshuffled between two otherwise-identical documents.
JSON
Json.toDocument/Json.ofDocument (in Json.fs) are a fourth way in and out of the DSL,
alongside writing F# directly, code generation, and XML: plain JSON, for a caller whose
tooling speaks JSON rather than XML. The same DU-case conventions apply, in JSON's own
idiom (a single-key object for a data-carrying case, a bare string for a parameterless one):
open System.Text.Json.Nodes
// JSON -> Document -> .docx
let doc = JsonNode.Parse(File.ReadAllText "report.json").AsObject() |> Json.ofDocument
Document.save "report.docx" doc
// .docx -> Document -> JSON
let json = Document.load "report.docx" |> Json.toDocument
File.WriteAllText("report.json", json.ToJsonString())
The same hyperlink example as above, in JSON:
{
"para": {
"inlines": [
{ "run": { "text": "Visit " } },
{
"hyperlink": {
"target": { "externalHyperlink": "https://github.com/Kookerella-Ltd" },
"runs": [ { "run": { "text": "Kookerella on GitHub", "styleId": "Hyperlink" } } ],
"tooltip": "Kookerella on GitHub"
}
},
{ "run": { "text": " for more." } }
]
}
}
The same determinism Xml.toDocument has (Styles/Numbering/TableStyles sorted by
Id) applies to Json.toDocument's output too, for the same reason: a genuine content
change produces a small, isolated diff rather than a spurious one from a catalog getting
reshuffled between two otherwise-identical documents.
Unlike XML, .NET has no built-in JSON Schema validator, so Json.schema.json (in the repo)
is validated only from this repo's own test suite (via a test-only JsonSchema.Net
dependency) rather than exposed as a public API - see Json.fs's own doc comment.
Building and testing
dotnet build
dotnet test --filter "Category!=Slow"
dotnet run --project samples/Kookerella.FsWordDsl.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).
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.
The C# wrapper's own suite has no fast/slow split - CsCodeGenTests.cs shells out to
dotnet run --file itself, so a single run already covers the C# analog of the F# suite's
slow group:
dotnet test tests/Kookerella.CsWordDsl.Tests
Frequently asked questions
What is Word MCP Server (FsWordDsl)?
Word tools for AI agents: read/write, decompile to F#/C#/XML/JSON, or build from XML/JSON.
How do I install Word MCP Server (FsWordDsl)?
It runs on your machine. In Claude Code: claude mcp add word-mcp-server-fsworddsl -- dnx Kookerella.FsWordDsl.Mcp@0.1.1 --yes. For other clients, paste the JSON configuration from this page into their MCP settings.
Does Word MCP Server (FsWordDsl) need an API key?
Its registry entry lists no required key or secret. Some features may still need an account with the service it connects to.
Is Word MCP Server (FsWordDsl) open source?
Yes. Its code is on GitHub under the MIT license.
Who maintains Word MCP Server (FsWordDsl)?
It is published by Kookerella-Ltd. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.