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:
POST /docbuilderexists 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 ofonlyofficeForceSave.- 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. - No watermark in the output — our licence covers builder output. (Community/AGPL builds watermark it; ours doesn't.)
- 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. - 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_documentprependsbuilder.OpenFile(<internal URL of the current version>)itself. The agent's script starts atApi.GetDocument()and can only express deltas — it never supplies document bytes, so wholesale recreation is impossible by default. - Scripts containing
builder.CreateFile/builder.OpenFileare rejected unless the call passes explicitmode:"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>/docbuilderwith 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)
- 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.)
- DLP is currently text-scoped.
allow_ai_processinggates TEXT reads. An edit both reads and writes bytes. Decision needed: same gate (simplest, recommended) or a separateallow_ai_editingflag per classification level. Remember: a level with no dlp_policies row is wide open — the edit gate must fail closed. - 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_documentrow. - Downstream invalidation. New version ⇒ extract hook re-runs (free), but also bump
ConvertGenerationso previews regenerate, and remember preview cache keys are hash+fingerprint.create → edit → previewmust 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)
- ~~Licence / endpoint / watermark~~ — ANSWERED by the probe: all clear.
- DLP: reuse
allow_ai_processingfor edits, or new flag? - Edit conflict policy: refuse-when-open vs queue-until-close?
- Do secure-folder / vault docs get edit tools at all? (suggest: no, v1)
upload_documentsize 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