feat(mcp): attach to live Yjs collaboration rooms (co-edit + attributed tracked changes)#3569
Draft
jacobjove wants to merge 1 commit into
Draft
feat(mcp): attach to live Yjs collaboration rooms (co-edit + attributed tracked changes)#3569jacobjove wants to merge 1 commit into
jacobjove wants to merge 1 commit into
Conversation
907bd31 to
2d8c4dc
Compare
…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>
2d8c4dc to
5345d8c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-editorcore edits:superdoc_attach({ ws_url, document_id, token?, user? })— connects to a Yjs room over WebSocket, awaits initial sync, returns asession_idusable with every existing tool. Likesuperdoc_open, it creates a session rather than consuming one.user: { id?, name?, email? }is threaded into the headlessEditorconfig. Without it,superdoc_mutations({ changeMode: "tracked" })over an attach throwsforceTrackChanges requires a user to be configured on the editor instance(the gate reads exactlythis.options.user). The file-open path already defaulted auser; attach was the only path missing the seam. Caller-supplied so changes attribute to the real reviewer, not a hardcoded identity.convertedXmllacks the base OOXML parts andexportDocxthrows on the first unguarded deref. Fix: seed the blank-docx template (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.Commits
feat(mcp): collab attach + save export for live Yjs roomsfeat(mcp): attach can configure a tracked-change usertest(mcp): cover superdoc_attach in protocol tool-list assertionsDependencies added:
yjs,y-websocket(viacatalog:),ws+@types/ws.Testing
apps/mcp/src/__tests__/collab-export.test.ts(joiner-editor export → valid PK-zip withword/document.xml+word/styles.xml+word/_rels/document.xml.rels) andcollab-attach-user.test.ts(assertseditor.options.useris set with a user,nullwithout).protocol.test.tsupdated to includesuperdoc_attachin the tool-list / action-enum / session_id assertions (it's a session-creating lifecycle tool likesuperdoc_open).apps/mcpsuite green: 38 pass / 0 fail (pnpm run build:superdocthenpnpm --prefix apps/mcp run test, matchingci-mcp.yml).user→ 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.Note for reviewers (latent, left untouched here)
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 reaches stderr. For a collab joiner the throw originates earlier (SuperConverterheader/footer export with emptyconvertedXml). 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
pnpm --prefix apps/mcp run test, afterbuild:superdoc)prettier --checkclean on changed files)