What problem does this solve?
The apps/mcp server today is file-oriented: superdoc_open({path}) loads a .docx from disk, you edit, and superdoc_save writes it back. That's great for batch/headless document generation, but it can't participate in a live collaboration session. An AI agent (or any MCP client) currently has no way to:
- Join a Yjs room that a human already has open in the SuperDoc editor and co-edit it in real time, or
- Author tracked (suggested) changes attributed to a specific reviewer so a human can accept/reject them — the core "AI suggests redlines, human decides" workflow.
For agent-assisted legal/contract review this is the whole point: the agent should suggest changes into the document the human is looking at, attributed to a named author, not produce a detached file copy.
Proposed solution
A collab-attach path alongside the existing file-open path, kept entirely in the MCP layer (no super-editor core edits):
superdoc_attach({ ws_url, document_id, token?, user? }) — connects to a live Yjs room over WebSocket, awaits initial sync, and returns a session_id usable with every existing tool (superdoc_get_content, superdoc_mutations, superdoc_track_changes, …). Like superdoc_open, it creates a session rather than consuming one.
user: { id?, name?, email? } is threaded into the headless Editor config so forceTrackChanges has an author. Without it, superdoc_mutations({ changeMode: "tracked" }) over an attach throws forceTrackChanges requires a user to be configured on the editor instance. With it, tracked changes attribute to the actual reviewer. The file-open path already defaults to { id: 'mcp' }; attach was the only path missing the seam, and the gate reads exactly this.options.user.
- Collab-aware save export. A joiner editor is built with no docx source (content arrives via the Yjs fragment), so
convertedXml lacks the base OOXML parts and exportDocx throws on the first unguarded deref. Fix (MCP-layer): seed the blank-docx template into the attach editor (Editor.loadXmlData(blankDocxBytes, true)) so SuperConverter populates the standard parts; with a ydoc present, #createInitialState only uses the seeded content as export scaffolding (Yjs still drives the live body).
Verified end-to-end against a running collab server: attach → tracked mutation succeeds (no "requires a user") → superdoc_track_changes list shows the change attributed to the supplied author → save exports a valid OOXML file with the live-room body intact.
Draft PR #3569 is up with the full diff, implemented and tested on top of apps/mcp (new unit + integration tests; full apps/mcp suite green). I opened it as a draft to check on design alignment first (e.g., whether you'd prefer the collab transport wired differently, or user sourced from the Yjs awareness state rather than a tool param). Happy to rework before marking it ready for review.
Alternatives considered
- Direct edits only over attach (
changeMode: "direct") already work without a user, but give no suggest/accept workflow and no attribution.
- File round-trip (open local copy, edit, save, re-import) — loses real-time collaboration and the human's live view entirely.
Additional context
While implementing the save path I hit a latent upstream papercut worth flagging separately: Editor.exportDocx() wraps its whole body in try { … } catch (e) { this.emit('exception', …); console.error(e); } with no return in the catch, so on any export error it silently returns undefined and the real TypeError only lands in stderr. For a collab-joiner editor the throw originates earlier (SuperConverter header/footer export with empty convertedXml). Seeding the template avoids the throw, but the swallowing catch masks the root cause for anyone who hits it. Might be worth returning or rethrowing there.
Environment: running apps/mcp from source via bun run --conditions source against a local Yjs collab server + the editor on vite. superdoc-dev/superdoc @ current main.
What problem does this solve?
The
apps/mcpserver today is file-oriented:superdoc_open({path})loads a.docxfrom disk, you edit, andsuperdoc_savewrites it back. That's great for batch/headless document generation, but it can't participate in a live collaboration session. An AI agent (or any MCP client) currently has no way to:For agent-assisted legal/contract review this is the whole point: the agent should suggest changes into the document the human is looking at, attributed to a named author, not produce a detached file copy.
Proposed solution
A collab-attach path alongside the existing file-open path, kept entirely in the MCP layer (no
super-editorcore edits):superdoc_attach({ ws_url, document_id, token?, user? })— connects to a live Yjs room over WebSocket, awaits initial sync, and returns asession_idusable with every existing tool (superdoc_get_content,superdoc_mutations,superdoc_track_changes, …). Likesuperdoc_open, it creates a session rather than consuming one.user: { id?, name?, email? }is threaded into the headlessEditorconfig soforceTrackChangeshas an author. Without it,superdoc_mutations({ changeMode: "tracked" })over an attach throwsforceTrackChanges requires a user to be configured on the editor instance. With it, tracked changes attribute to the actual reviewer. The file-open path already defaults to{ id: 'mcp' }; attach was the only path missing the seam, and the gate reads exactlythis.options.user.convertedXmllacks the base OOXML parts andexportDocxthrows on the first unguarded deref. Fix (MCP-layer): seed the blank-docx template into the attach editor (Editor.loadXmlData(blankDocxBytes, true)) soSuperConverterpopulates the standard parts; with a ydoc present,#createInitialStateonly uses the seeded content as export scaffolding (Yjs still drives the live body).Verified end-to-end against a running collab server: attach → tracked mutation succeeds (no "requires a user") →
superdoc_track_changes listshows the change attributed to the supplied author → save exports a valid OOXML file with the live-room body intact.Draft PR #3569 is up with the full diff, implemented and tested on top of
apps/mcp(new unit + integration tests; fullapps/mcpsuite green). I opened it as a draft to check on design alignment first (e.g., whether you'd prefer the collab transport wired differently, orusersourced from the Yjs awareness state rather than a tool param). Happy to rework before marking it ready for review.Alternatives considered
changeMode: "direct") already work without a user, but give no suggest/accept workflow and no attribution.Additional context
While implementing the save path I hit a latent upstream papercut worth flagging separately:
Editor.exportDocx()wraps its whole body intry { … } catch (e) { this.emit('exception', …); console.error(e); }with no return in the catch, so on any export error it silently returnsundefinedand the realTypeErroronly lands in stderr. For a collab-joiner editor the throw originates earlier (SuperConverterheader/footer export with emptyconvertedXml). Seeding the template avoids the throw, but the swallowing catch masks the root cause for anyone who hits it. Might be worth returning or rethrowing there.Environment: running
apps/mcpfrom source viabun run --conditions sourceagainst a local Yjs collab server + the editor on vite.superdoc-dev/superdoc@ currentmain.