Skip to content

feat(mcp): attach to live Yjs collaboration rooms (co-edit + attributed tracked changes)#3569

Draft
jacobjove wants to merge 1 commit into
superdoc-dev:mainfrom
jacobjove:feature/mcp-collab-attach
Draft

feat(mcp): attach to live Yjs collaboration rooms (co-edit + attributed tracked changes)#3569
jacobjove wants to merge 1 commit into
superdoc-dev:mainfrom
jacobjove:feature/mcp-collab-attach

Conversation

@jacobjove
Copy link
Copy Markdown

@jacobjove jacobjove commented May 29, 2026

Closes #3568.

Opened as a draft alongside #3568.

What this adds

A collaboration-attach path for apps/mcp, alongside the existing file-open path, so an MCP client can join a live Yjs room instead of only round-tripping a local .docx. The motivating use case is agent-assisted review: suggest tracked redlines into the document a human has open, attributed to a named reviewer, for accept/reject.

Three capabilities, all kept in the MCP layer — no super-editor core edits:

  1. superdoc_attach({ ws_url, document_id, token?, user? }) — connects to a Yjs room over WebSocket, awaits initial sync, returns a session_id usable with every existing tool. Like superdoc_open, it creates a session rather than consuming one.
  2. Tracked-change attribution. user: { id?, name?, email? } is threaded into the headless Editor config. Without it, superdoc_mutations({ changeMode: "tracked" }) over an attach throws forceTrackChanges requires a user to be configured on the editor instance (the gate reads exactly this.options.user). The file-open path already defaulted a user; attach was the only path missing the seam. Caller-supplied so changes attribute to the real reviewer, not a hardcoded identity.
  3. 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: seed the blank-docx template (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.

Commits

  • feat(mcp): collab attach + save export for live Yjs rooms
  • feat(mcp): attach can configure a tracked-change user
  • test(mcp): cover superdoc_attach in protocol tool-list assertions

Dependencies added: yjs, y-websocket (via catalog:), ws + @types/ws.

Testing

  • New apps/mcp/src/__tests__/collab-export.test.ts (joiner-editor export → valid PK-zip with word/document.xml + word/styles.xml + word/_rels/document.xml.rels) and collab-attach-user.test.ts (asserts editor.options.user is set with a user, null without).
  • protocol.test.ts updated to include superdoc_attach in the tool-list / action-enum / session_id assertions (it's a session-creating lifecycle tool like superdoc_open).
  • Full apps/mcp suite green: 38 pass / 0 fail (pnpm run build:superdoc then pnpm --prefix apps/mcp run test, matching ci-mcp.yml).
  • Verified end-to-end against a live collab server: attach with user → 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.

Note for reviewers (latent, left untouched here)

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 reaches stderr. For a collab joiner the throw originates earlier (SuperConverter header/footer export with empty convertedXml). Seeding the template avoids the throw; I left the swallowing catch alone since it's on the shared export path, but it masks the root cause for anyone who hits it. Flagged in #3568.

Checklist

…cked changes

Add `superdoc_attach` so an MCP client can join a live SuperDoc Yjs
collaboration room over WebSocket (openRoom + WebsocketProvider, awaiting
initial sync) instead of only round-tripping a local .docx. The returned
session_id works with every existing tool. The motivating use case is
agent-assisted review: suggesting tracked redlines into a document a human has
open, attributed to a named reviewer, for accept/reject.

Tracked-change attribution: `superdoc_attach` accepts an optional
user ({ id, name, email }) threaded through openRoom into buildAttachEditor's
headless Editor config. Without a configured user, forceTrackChanges rejects
tracked edits, so suggested edits over an attach could not be attributed. The
file-open path already sets a default user; this brings the attach path to
parity, scoped to a caller-supplied identity.

Collab-aware save export: a joiner editor is built with no docx source, so
converter.convertedXml carried none of the base OOXML parts that
Editor.exportDocx dereferences. The deref threw and (via exportDocx's catch)
surfaced as "not binary (got undefined)". buildAttachEditor now seeds the
blank-docx template via Editor.loadXmlData so export has valid scaffolding;
Yjs still drives the body (the initial ProseMirror doc is seeded from `content`
only when no ydoc is present). save() rejects room saves without an explicit
output path.

Tests: protocol.test.ts now covers superdoc_attach in the tool-list,
action-enum, and session_id assertions (like superdoc_open, it creates a
session rather than consuming one). New collab-export.test.ts asserts a binary
PK-zip round-trip (word/document.xml + styles.xml + document.xml.rels) from a
collab-joiner editor, and collab-attach-user.test.ts asserts the tracked-change
user is configured when supplied and left unset otherwise.

Adds yjs, y-websocket, and ws dependencies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jacobjove jacobjove force-pushed the feature/mcp-collab-attach branch from 2d8c4dc to 5345d8c Compare May 29, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: attach to live Yjs collaboration rooms (co-edit + attributed tracked changes)

1 participant