Skip to content
plugmcp
DirectorySkillsCategoriesBlog
EN/中文
Sign inSign up
Home/Servers/Word MCP Server (FsWordDsl)

Word MCP Server (FsWordDsl)

I
by io.github.MarkNicholls

Word tools for AI agents: read/write, decompile to F#/C#/XML/JSON, or build from XML/JSON.

Power your agent

Add this server

The publisher's own command. Not a PlugMCP endpoint.

Local command
nuget Kookerella.FsWordDsl.Mcp
Provider
Claude Code
claude mcp add io.github.marknicholls-fsworddsl-mcp -- nuget Kookerella.FsWordDsl.Mcp

Run in a terminal.

Claude Desktop
{
  "mcpServers": {
    "io.github.marknicholls-fsworddsl-mcp": {
      "command": "nuget",
      "args": [
        "Kookerella.FsWordDsl.Mcp"
      ]
    }
  }
}

claude_desktop_config.json

Step-by-step: add this in Claude

Remote URL or a command on your machine? How the two installs differ

Run on PlugMCP

Host a copy with our keys and logs.

A new server you create. It is not this listing's official endpoint. Use the publisher install when you want their URL and their credentials.

Launch

Related MCP

View more
  • Firecrawl🔥 Official Firecrawl MCP Server - Adds powerful web scraping and search to Cursor, Claude and any other LLM clients.
  • N
    NotionToken-auth Notion MCP: pages, databases, blocks, comments, files. Runs headless in CI and agents.
  • Context7Context7 Platform -- Up-to-date code documentation for LLMs and AI code editors

Related skills

View all
  • reaReverse engineer anything with agents, from app behavior down to native binaries.
  • rea-changelog-updatePrepare or rewrite REA release changelogs and GitHub release notes from pinned Git history, with verified contributor thanks and Release Please synchronization.
  • rea-tool-designDesign or change REA investigation tools, CLI/MCP contracts, provider capabilities, and Evidence semantics.

Questions

How do I install Word MCP Server (FsWordDsl)?
Copy the stdio config for Cursor, Claude Desktop, Claude Code, or VS Code on this page. It uses the publisher's install, not a PlugMCP proxy.
Does Word MCP Server (FsWordDsl) need an API key?
No required environment variable is listed on this record.
What transport does Word MCP Server (FsWordDsl) use?
Word MCP Server (FsWordDsl) runs locally over stdio.
When was Word MCP Server (FsWordDsl) last checked?
Last checked 2026-10-16. Word tools for AI agents: read/write, decompile to F#/C#/XML/JSON, or build from XML/JSON.

Word tools for AI agents: read/write, decompile to F#/C#/XML/JSON, or build from XML/JSON.

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-relative Theme color - see ThemeColorKind), 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, with BasedOn inheritance) and a small BuiltInStyles catalog (normal, heading1/2/3, title, listParagraph, hyperlinkCharStyle).
    • Numbering.fs - NumberFormatKind, ListLevel, NumberingDefinition for numbered/bulleted lists, including multi-level ones (ListLevel isn't limited to one per definition - see Builders.multiLevelNumberedListDef).
    • Hyperlinks.fs - HyperlinkTarget (external URL vs. internal bookmark reference).
    • Protection.fs - EditRestriction and DocumentProtection, document-level (Word has no per-section equivalent of Excel's per-sheet protection).
    • Revisions.fs - RevisionKind/Revision for track changes (Inline.TrackedChange, Paragraph.MarkRevision) - narrowly scoped to inserted/deleted content and paragraph marks, see MAPPING.md for what isn't covered.
    • ContentControls.fs - ContentControlType/ContentControlProps for content controls (structured document tags, w:sdt): plain text, rich text, dropdown/combo box, date picker, checkbox - see MAPPING.md for 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-cell Margins override), TableStyleRef, TableStyleRegion/ TableStyleDefinition (custom table style definitions - all thirteen of OOXML's conditional-formatting regions), and CellMargins (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-paragraph Bookmark/Comment cases and the cross-paragraph BookmarkRangeStart/End/CommentRangeStart/End markers, simple fields, footnotes/endnotes, TrackedChange for track changes, and InlineContentControl for content controls), Paragraph (including MarkRevision), Block (paragraph, table, or ContentControlBlock - the block-level counterpart to InlineContentControl), TableCell/ TableRow (including RepeatAsHeader)/TableEntry (including CellMargins), HeaderFooterSet, SectionProperties (including BreakType and FootnoteNumbering/ EndnoteNumbering), Section, Document (including Document.VbaProject, a macro-enabled document's raw vbaProject.bin bytes, Document.Properties, and Document.TableStyles).
    • Xml.fs / Xml.xsd - the XML surface: Xml.toDocument/Xml.ofDocument translate a Document 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 / Json.schema.json - the JSON surface: Json.toDocument/Json.ofDocument translate a Document to/from a System.Text.Json.Nodes.JsonObject tree. 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) plus DocumentDsl - smart constructors (run, para (with markRevision), hyperlink, bookmark, comment, inserted/deleted (track changes), contentControl/contentControlBlock (content controls), image, footnote, endnote, tableCell, tableRow (with height/repeatAsHeader), table (with style/borders/cellMargins)) with real optional parameters, the Word analog of the Excel repo's SheetDsl.
    • Interpreter/StyleRegistry.fs - shared run/paragraph/border/color conversions plus Document.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 a Document back out as a self-contained .fsx script that rebuilds an equivalent file when run (internal).
    • Api.fs - the public Document.save/saveToStream/load/loadFromStream/ generateScript entry points.
  • src/Kookerella.CsWordDsl - the fluent C# wrapper (see "## The C# wrapper" below): immutable records/sealed record closed 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 of Interpreter/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 plain convert/build CLI, depending on argv). Distributed as a dotnet tool (fsworddsl-mcp), same as the Excel sibling's own Kookerella.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 to Examples/<test name>/output.docx (checked into the repo), plus script.fsx (regenerates the file - a separate, slower Category=Slow test group actually executes each one via dotnet fsi), document.xml, and document.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-in Examples/*/output.docx fixtures rather than re-authoring every scenario a second time), CsCodeGenTests.cs (actually executes a generated file via dotnet run --file, the C# analog of the F# suite's Category=Slow dotnet fsi group).
  • 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
How do I install Word MCP Server (FsWordDsl)?
Copy the stdio config for Cursor, Claude Desktop, Claude Code, or VS Code on this page. It uses the publisher's install, not a PlugMCP proxy.
Does Word MCP Server (FsWordDsl) need an API key?
No required environment variable is listed on this record.
What transport does Word MCP Server (FsWordDsl) use?
Word MCP Server (FsWordDsl) runs locally over stdio.
When was Word MCP Server (FsWordDsl) last checked?
Last checked 2026-10-16. Word tools for AI agents: read/write, decompile to F#/C#/XML/JSON, or build from XML/JSON.

No tool list yet. Remote servers get one after the first health check.

plugmcp

Plug any tool into any agent. MCP servers for people who do not write servers. Gateway for volume, n8n for apps.

  • github.com/TODO
  • x.com/TODO
  • linkedin.com/company/TODO

PlugMCP, 2026

Product
  • Browse
  • Install
  • Host your own
  • Pricing
  • Voice mode
  • Browser
Directory
  • All MCP servers
  • MCP Apps
  • Tools
  • Agent skills
  • Categories
  • New
  • Voice
  • Submit a server
Developers
  • Docs
  • Blog
  • Club
  • Work with us
  • What is MCP
  • What is WebMCP
  • Status
Company
  • About
  • Contact
  • Terms
  • Privacy
  • DPA