think
16px
820px

Office-editing MCP — brainstorm (capability + cost)

Goal: Claude Code / any MCP agent edits docx, xlsx (pptx later) stored in Obscura with near-full MS Word feature coverage — not string-replace hacks — and the result is exactly what a human sees when they open the doc in the editor.

Date: 2026-08-21 · Status: brainstorm + probe PASSED on demo

Probe results (demo, 2026-08-21) — option B is de-risked

Ran live against deploy-onlyoffice-1 (our custom obscura-onlyoffice:8.3 image) on dms2:

  1. POST /docbuilder exists and works on our image. Same dual-JWT scheme as our existing convert/command clients (signJWT: token claim in body + Bearer {"payload":...} header) — the Go client is a ~50-line copy of onlyofficeForceSave.
  2. Modify-in-place works: builder.OpenFile(url) opened an existing docx, appended a paragraph, and the original content survived byte-for-byte in the output. This is the load-bearing capability.
  3. No watermark in the output — our licence covers builder output. (Community/AGPL builds watermark it; ours doesn't.)
  4. Caveats found: the doc-server cannot reach host IPs (demo firewalling) — script + source files must be served from inside the compose network, which the backend already does for editor sessions (ONLYOFFICE_OBSCURA_URL=http://obscura:8080). So production shape: backend exposes the current version + generated script at internal token URLs, exactly like editor opens today. Zero new containers confirmed.
  5. Runtime: ~2–4 s per round-trip including save.

Doctrine: MODIFY, never recreate (unless told to)

Requirement from Efran: agents must edit the existing document, not regenerate it wholesale.
Enforced structurally, not by prompt-begging:

  • The server, not the agent, opens the file: edit_office_document prepends builder.OpenFile(<internal URL of the current version>) itself. The agent's script starts at Api.GetDocument() and can only express deltas — it never supplies document bytes, so wholesale recreation is impossible by default.
  • Scripts containing builder.CreateFile / builder.OpenFile are rejected unless the call passes explicit mode:"recreate" (for the rare "rewrite this from scratch" instruction).
  • Curated tools are delta operations by construction (replace_text, set_cells, …).
  • Tool descriptions still say it plainly ("preserve existing content and formatting; smallest change that satisfies the request") for agents choosing between approaches.

What we already have (the head start)

Asset Where Why it matters
MCP module (paid, API-key, ACL-filtered) httpapi/handlers_mcp.go 12 tools live incl. read_document, preview_document, create_document_from_template. The edit tools slot into this exact surface: same auth, same audit rows (mcp.tool.*), same DLP gate.
Licensed OnlyOffice Document Server 8.3 same-origin proxy + dual JWT The same engine already renders and edits every office file users see.
Command-Service client (forcesave) handlers_office_apply.go Live-session detection + forcesave already built and battle-tested ("Apply now"). Reuse for the edit-vs-open-editor conflict.
AddVersion write path dms/app/service.go:1480 Versioning, blob store, post-version extract hook (search index re-runs automatically), audit.
Hand-rolled OOXML surgery in Go platform/docx/*, platform/office/pagesetup Proof we can do byte-level edits — and the scar tissue proving how painful the long tail is (TOC repair, sheet XML).

Options, ranked

B. OnlyOffice Document Builder as the edit engine — RECOMMENDED

The Document Server we already run exposes a builder HTTP endpoint (POST /docbuilder): it executes a JS script against a document using the editor's own API — paragraphs, runs, styles, tables, headers/footers, sections, TOC update, comments, tracked changes, forms; full worksheet API for xlsx. That is "full Word features natively": same engine, so agent output == what the user sees on open. No second fidelity domain (unlike LibreOffice, which already wrecked margins once on the PDF/A pass).

MCP shape, two layers:
1. edit_office_document(doc_id, script, dry_run) — raw builder JS, full power, for capable agents. Returns new version number + a rendered preview reference so the agent can look at what it did (preview_document closes the loop).
2. Curated high-level tools compiled onto builder scripts server-side: replace_text (format-preserving), set_cells, insert_paragraphs/table/image, update_toc, set_header_footer, add_comment, apply_tracked_changes. These are what smaller agents and other vendors' agents will actually use reliably.

  • Fidelity: editor-grade. Feature ceiling: everything the OnlyOffice editor can do.
  • Infra: possibly zero new containers — the endpoint may already be live on our doc-server. Verify: curl -X POST https://<internal>/docbuilder with JWT on demo.
  • Licence: Builder is bundled in Docs Developer Edition (the integrator edition — almost certainly what we hold); Community/AGPL builds watermark builder output. Action: confirm our edition + terms; get vendor word in writing. (Builder-via-Docs API, editions)

A. Structured OOXML op-set in Go (extend platform/docx)

Typed operations applied by our own code (python-docx-style, but in-process Go). Good as a fallback for a handful of ops if builder licensing surprises us — but "full Word features" via hand-rolled XML is a treadmill: fields, section breaks, numbering, charts, revision marks each become their own toc_repair.go. Not the main path.

C. Live co-editing connector (agent joins the session) — phase-3 moonshot

OnlyOffice Automation API / connector: the agent drives a live editor session — edits stream in next to the human, cursor and all. Best UX, and it dissolves the stale-open-editor problem by construction. Paid add-on + most engineering; do it only after B proves demand.

D. Microsoft Graph (real Word in M365) — rejected

Full fidelity, but bytes leave the premises, per-tenant M365 licensing, and it torpedoes the on-prem/records-compliance selling point. One line, closed.

E. LibreOffice UNO sidecar — rejected as editor, kept as converter

Second engine ⇒ second fidelity domain that users never see. Gotenberg stays for conversion only (and stays serial — no LibreOffice concurrency knob).


The four traps (design constraints, not afterthoughts)

  1. Open-editor staleness. OnlyOffice sessions never re-fetch stored bytes; an out-of-band edit is invisible to an open editor and gets overwritten on their next save. Rule: before an MCP edit, ask the Command Service for a live session (we already track live keys). If live → refuse with a clear error ("user X has this open") or forcesave-then-refuse. Never silently race. (Option C is the only design that truly co-edits.)
  2. DLP is currently text-scoped. allow_ai_processing gates TEXT reads. An edit both reads and writes bytes. Decision needed: same gate (simplest, recommended) or a separate allow_ai_editing flag per classification level. Remember: a level with no dlp_policies row is wide open — the edit gate must fail closed.
  3. Builder scripts are untrusted code. Arbitrary JS executed server-side. Sandbox: run with no egress, hard timeout (~30 s), size cap, serial queue per tenant (same doctrine as Gotenberg/pdfium — scale by processes, never threads). Audit the full script text in the mcp.tool.edit_office_document row.
  4. Downstream invalidation. New version ⇒ extract hook re-runs (free), but also bump ConvertGeneration so previews regenerate, and remember preview cache keys are hash+fingerprint. create → edit → preview must round-trip fresh bytes.

Licence/SKU question: these tools sit at the intersection of mcp (transport) and office (capability). Suggest: tools ship in the MCP surface but require the office module licence bit — matches the templates→office consolidation.


Writer-support tools (agent as author, not just editor)

Editing is half the job; the agent also needs to file work. New MCP tools, all through the existing service layer (ACL DENY-WINS, quota check 00011, audit row each):

Tool Notes
create_folder(parent_id?, name) root when parent_id omitted; returns folder id
create_document(folder_id?, title, content_md) markdown/HTML → docx via a server-side builder script — same engine, so the result is editor-perfect; lands through AddVersion ⇒ filed + extracted immediately (the 00210 filed-at lesson is already in that path)
upload_document(folder_id?, filename, content_base64) for real binaries; size-capped (~10 MB — MCP transport is JSON)
move_document(doc_id, folder_id) / rename_document(doc_id, title) filing hygiene
create_document_from_template already live — unchanged

Deliberately not in v1: delete anything, folder rename/move trees, secure-folder targets, sharing. Each is a policy conversation, not a code problem.


Cost

Item Estimate
Phase 1 — probe /docbuilder on demo, edit_office_document (raw script), live-session guard, AddVersion + audit + DLP wiring, e2e on demo ~1–2 weeks, one dev
Phase 2 — 6–8 curated high-level tools + agent-facing docs/examples + preview round-trip loop ~1 week
Phase 3 — live co-edit connector (option C) vendor quote first; multi-week
Licence Likely Rp 0 incremental if our Docs edition bundles Builder (Developer Ed. does). Standalone commercial Builder exists if not — get quote. Watermark on output = the tell that we're on the wrong licence.
Runtime ~1–3 s per edit, serial per tenant; zero new containers if the doc-server endpoint works, else one sidecar (~0.5 GB RAM)
Pricing upside "AI agents edit your documents in place, on-prem" is a demo-able differentiator — fits the modules-as-%-of-core sheet under office or ai

Open questions (ranked)

  1. ~~Licence / endpoint / watermark~~ — ANSWERED by the probe: all clear.
  2. DLP: reuse allow_ai_processing for edits, or new flag?
  3. Edit conflict policy: refuse-when-open vs queue-until-close?
  4. Do secure-folder / vault docs get edit tools at all? (suggest: no, v1)
  5. upload_document size cap + whether base64-over-MCP is acceptable v1 transport.

Next action: DONE — see the phase-1 implementation plan: 2026-08-21-office-editing-mcp-phase1.md.

Sources: Document Builder API via Docs · Builder overview · Editions & licensing