feat: support concurrent sessions via document handles#2
Open
FeritMelih wants to merge 1 commit intoSecurityRonin:mainfrom
Open
feat: support concurrent sessions via document handles#2FeritMelih wants to merge 1 commit intoSecurityRonin:mainfrom
FeritMelih wants to merge 1 commit intoSecurityRonin:mainfrom
Conversation
The server held the currently-open document in a single module-level global (_doc), so two clients (e.g. parallel agents on one stdio transport) editing different files would silently clobber each other's state — paraIds going stale, save_document writing to the wrong path, get_document_info returning the peer's content. Replace the global with `_docs: dict[str, DocxDocument]` keyed by an opaque `document_handle` string. Add an optional `document_handle` parameter to all 45 tools. open_document / create_document / create_from_markdown return the handle so callers can thread it through subsequent calls. Backward compatible: an empty handle resolves to a shared `__default__` slot — identical behavior to the previous single-document semantics. Single-client callers don't need to change anything; parallel-aware clients pass a unique handle (e.g. a UUID) per session for isolation. Verified end-to-end with two parallel test agents editing different files under different handles: zero cross-session leakage, real OOXML w:ins/w:del/w:comment elements still produced, accept_changes still strips revision markup correctly.
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.
Summary
Replaces the single module-level
_docglobal with a dict keyed by an opaquedocument_handle, so multiple clients can edit different documents through one server process without clobbering each other's state.The bug
server.py:25held the currently-open document in a module global:Every tool read this via
_require_doc(); four tools (open_document,close_document,create_document,create_from_markdown) mutated it. When two clients shared one stdio MCP connection (e.g. parallel agents from a single Claude Code session), opens raced — one client'sparaIdhandles silently went stale,save_documentretargeted to the peer's path, andget_document_inforeturned the wrong document's content. The "stale last output path onsave_document(output_path='')" symptom was a side effect ofDocxDocument.save(None)falling back toself.source_pathon whatever instance_doccurrently pointed at — not a separate cache.The fix
_doc. Add_docs: dict[str, DocxDocument]and a_DEFAULT_HANDLE = "__default__"constant._resolve(handle) -> (key, doc)helper that replaces_require_doc().document_handle: str = ""parameter to all 45@mcp.tool()functions.open_document,create_document,create_from_markdownnow return{"handle": key, ...info}so callers can capture and thread the handle through subsequent calls.Backward compatibility
Empty-string handle resolves to
__default__. Single-client callers that never pass a handle see identical behavior to before — same shared slot, same lifecycle. Parallel-aware clients pass a unique handle (e.g. a UUID) per session for isolation. No client needs to be updated to keep working.Verification
Tested with two parallel agents under one Claude Code session, each editing a different file under a different handle:
alpha-basic-test,test3_basic.docx) — 20 steps PASSbeta-redline-test,test4_redline.docx) — 22 steps PASSsearch_textfor the peer's content from each side returned[]<w:ins>=2,<w:del>=1,<w:comment>=3 in beta's documentaccept_changesstill strips revision markup correctly:<w:ins>=0,<w:del>=0 after,<w:comment>preservedclose_document→open_documentround-tripsBefore this change, the same parallel run had Beta's contract content silently overwriting Alpha's
test1_basic.docx, Alpha'ssave_documentwriting to Beta's path, and Alpha'sget_document_inforeturning Beta's stats.Scope
One file changed (
docx_mcp/server.py), no changes to anydocument/*mixin (they already operate onself, never globals). No changes to dependencies, packaging, or CLI.Test plan
__default__)open_document/create_document/create_from_markdownreturn the handle in their responseclose_documentonly closes the document under the given handlesave_document(output_path="")saves to the correct per-handle source path