Skip to content

fix(mcp): idempotent element creation via content-derived IDs#1

Merged
vanducng merged 1 commit into
mainfrom
fix/mcp-idempotent-element-writes
May 3, 2026
Merged

fix(mcp): idempotent element creation via content-derived IDs#1
vanducng merged 1 commit into
mainfrom
fix/mcp-idempotent-element-writes

Conversation

@vanducng

@vanducng vanducng commented May 3, 2026

Copy link
Copy Markdown
Contributor

Server now derives content-based IDs (sha256 of project|type|coords|text|refs) when caller omits id. Same content → same id → upsert; re-issued batch_create no longer doubles diagrams. Also reworded MCP success-path warnings (was the dominant retry trigger), MCP-side ID generation removed, frontend WS handlers now update sync baseline, added scripts/dedupe-elements.cjs cleanup, skill re-render guidance updated. Tests 304/304 (16 new). Plan: plans/260504-0319-mcp-idempotent-element-writes/

Summary by CodeRabbit

Release Notes

  • New Features

    • Server now generates deterministic IDs for new elements, preventing duplicates from creation retries.
    • Improved WebSocket sync baseline tracking to ensure frontend accurately reflects server state.
  • Bug Fixes

    • Fixed misleading success messages that could trigger unintended retries.
  • Chores

    • Added one-time deduplication script to clean up existing duplicate elements.
  • Tests

    • Added comprehensive idempotency and deduplication validation tests.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@vanducng has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 51 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d54c6f2-b669-46c9-a2b6-dfdf1edeabcf

📥 Commits

Reviewing files that changed from the base of the PR and between 39f5379 and 3c23202.

📒 Files selected for processing (7)
  • .gitignore
  • frontend/src/App.tsx
  • scripts/dedupe-elements.cjs
  • src/index.ts
  • src/server.ts
  • tests/backend/dedupe-script.test.ts
  • tests/backend/idempotency.test.ts
📝 Walkthrough

Walkthrough

This PR implements idempotent element creation for Excalidraw by introducing deterministic server-derived IDs based on content hash, updating MCP to stop minting IDs, synchronizing frontend sync baselines after WebSocket broadcasts, and adding a deduplication cleanup script. The changes span server ID derivation, MCP handler rewording and behavior modifications, frontend sync tracking, and comprehensive test coverage.

Changes

Idempotent Element Writes Implementation

Layer / File(s) Summary
Server ID Derivation
src/server.ts
New exported deriveContentId() computes stable element IDs from project ID, element type, rounded geometry, text content, and (for arrows) start/end reference IDs using SHA-256 hash. Wired into POST /api/elements and POST /api/elements/batch to replace generateId() when id is absent.
MCP Handler Updates
src/index.ts
create_element and batch_create_elements handlers no longer pre-generate IDs; element payloads now have optional id to allow server-side deterministic derivation. Tool responses extract assigned ID from canvasResponse.element?.id. Success messaging updated to remove "⚠️ Canvas sync not confirmed" trigger and use "Visible on canvas now" / "Persisted…" wording.
Frontend WebSocket Baseline Sync
frontend/src/App.tsx
New applyWsBaseline() helper updates lastSyncedElementsRef, lastSyncedHashRef, and lastSyncVersionRef after applying WS-broadcasted element changes. Integrated into element_created, element_updated, element_deleted, and elements_batch_created handlers to ensure subsequent delta detection treats WS-applied elements as already synced.
Element Deduplication Script
scripts/dedupe-elements.cjs
CLI script exports fingerprint(), bucketByFingerprint(), and dedupeProject() to identify and soft-delete duplicate elements by content fingerprint. Runs in dry-run mode by default; --execute flag performs soft-deletion of duplicates while keeping lowest-sync_version row and bumping survivor's sync_version for delta-sync broadcast.
Backend Idempotency Tests
tests/backend/idempotency.test.ts, tests/backend/dedupe-script.test.ts
Comprehensive test suite validates deriveContentId() determinism across input variations, API idempotency for single and batch creates, cross-tenant isolation, bound-arrow ID distinctness, and dedupe script dry-run / execute / re-run behavior including sync_version bump verification.
Planning & Documentation
plans/260504-0319-mcp-idempotent-element-writes/*, plans/reports/
Six-phase rollout plan (Phase 0: MCP response rewording; Phase 1: cleanup; Phase 2: server deterministic IDs; Phase 3: MCP stop minting; Phase 4: frontend sync hygiene; Phase 5: skill workflow; Phase 6: tests). Includes RCA debug report and audit with P0/P1/P2 findings and recommended edits.

Sequence Diagram

sequenceDiagram
    participant Client as Client / MCP
    participant Server as Server API
    participant DB as Database
    participant WS as WebSocket
    participant Frontend as Frontend Browser

    rect rgba(100, 150, 200, 0.5)
    Note over Client,Frontend: Element Creation with Deterministic ID
    Client->>Server: POST /api/elements (no id provided)
    Server->>Server: deriveContentId(project, type, geometry, text, refs)
    Server->>DB: INSERT element with derived id
    DB-->>Server: Element inserted
    Server-->>Client: Response with element.id
    Client->>Client: Log assigned element.id
    end

    rect rgba(100, 150, 200, 0.5)
    Note over WS,Frontend: WebSocket Broadcast & Baseline Sync
    Server->>WS: Broadcast element_created {element, sync_version}
    WS->>Frontend: element_created message
    Frontend->>Frontend: updateScene with element
    Frontend->>Frontend: applyWsBaseline(scene, [element], [], sync_version)
    Frontend->>Frontend: Update lastSyncedElementsRef, lastSyncedHashRef, lastSyncVersionRef
    Frontend->>Frontend: Persist lastSyncVersionRef to localStorage
    Note over Frontend: syncToBackend detects zero changes
    end

    rect rgba(150, 100, 200, 0.5)
    Note over DB: One-time Deduplication (Phase 1)
    DB->>DB: fingerprint() hash elements by content
    DB->>DB: bucketByFingerprint() group by fingerprint
    DB->>DB: Keep min(sync_version), soft-delete rest
    DB->>DB: Bump survivor sync_version
    Server->>WS: Broadcast elements_synced
    WS->>Frontend: Trigger reconciliation
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A hashing tale, content-wise and true,
No more fresh IDs born anew!
Deterministic dreams, round and round,
Deduplicated, safe and sound. 🌿
Sync baselines dance, broadcasts align,
Idempotent writes—now that's just fine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(mcp): idempotent element creation via content-derived IDs' directly and clearly summarizes the main change: implementing idempotent element creation through content-derived server-side IDs to prevent duplicates from MCP retries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-idempotent-element-writes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 47 minutes and 51 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Stops the skill self-critique loop from doubling diagrams when it re-issues
batch_create_elements without clear_canvas. Two layers:

1. Reword MCP response: drop ⚠️ "Canvas sync not confirmed" on the success
   path of create_element / update_element / batch_create_elements. The warning
   wording was the dominant retry trigger — conservative LLMs read it as
   failure and re-invoked the same batch ~39ms later.

2. Server-side deterministic IDs: when no id is supplied, derive
   sha256(project|type|round(x,y,w,h)|text|startRefId|endRefId)[:12]. Same
   content + same position → same id → upsert (no new row). start/end refs
   are part of the hash so bound arrows in a batch don't collapse to one row
   when they share placeholder coords.

Also:
- MCP stops minting ids in create_element / batch_create_elements (server
  derives), keeps duplicate_elements minting (intentional copy semantics).
- Frontend updates lastSyncedElementsRef + sync_version after WS broadcasts
  so syncToBackend doesn't re-upload elements it just received.
- scripts/dedupe-elements.cjs cleans up existing duplicates (dry-run by
  default; --execute to mutate). Soft-deletes dups, keeps lowest sync_version.
- Skill self-critique loop guidance updated: clear_canvas before re-render
  OR use update_element for incremental fixes.

Tests: +16 new (idempotency + dedupe script). Full suite 304/304 green.
@vanducng vanducng force-pushed the fix/mcp-idempotent-element-writes branch from 39f5379 to 3c23202 Compare May 3, 2026 21:01
@vanducng vanducng merged commit 82e7cac into main May 3, 2026
8 checks passed
@vanducng vanducng deleted the fix/mcp-idempotent-element-writes branch May 3, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant