Skip to content

feat(core): refuse MCP content writes to an entry someone else is editing - #3059

Open
danielmlr wants to merge 3 commits into
emdash-cms:mainfrom
danielmlr:feat/mcp-honour-entry-lock
Open

feat(core): refuse MCP content writes to an entry someone else is editing#3059
danielmlr wants to merge 3 commits into
emdash-cms:mainfrom
danielmlr:feat/mcp-honour-entry-lock

Conversation

@danielmlr

Copy link
Copy Markdown
Contributor

What does this PR do?

Prevents an AI tool connected over MCP from writing over an entry someone else has open in the admin. #2919 made the REST routes refuse a write against another user's edit lock, but the MCP content tools call the handlers directly and skip that check.

content_update, content_delete, content_publish, content_unpublish, content_schedule, content_unschedule, content_discard_draft and revision_restore now run the same check after their permission checks and return ENTRY_LOCKED with the holder in _meta.details. overrideLock: true writes anyway. Re-reading never clears the refusal, so the flag's description tells agents to set it only when the user asks. content_restore, content_permanent_delete and collections with locking off are unchanged.

POST /revisions/{revisionId}/restore overwrites the entry too, but #2919 did not cover it, so it now runs the same check and takes overrideLock in an optional body. The admin's restore button sits inside the editor's read-only fieldset, so only the lock holder restores from the admin.

The unreleased #2919 changeset loses its sentence saying the MCP tools do not honour the lock yet.

Part of #2819; #2919 left this half to its own PR.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5

Screenshots / test output

entry-lock.test.ts runs each tool through a real MCP client against another user's lease. Every assertion fails under one source mutation, run one at a time: the check removed from a tool or the route, overrideLock ignored or dropped from a schema, the wrong user checked, or the lock checked before permissions. The authorization unit test's handler stub gets a migrated in-memory database for the lock query.

…ting

The edit lock refuses REST and CLI writes against another user's lease,
but the MCP content tools call the handlers directly and never reached
that check, so an agent could still write over an entry someone had open
in the admin.

content_update, content_delete, content_publish, content_unpublish,
content_schedule, content_unschedule, content_discard_draft and
revision_restore now run claimEntryLockForWrite after their permission
checks, as the REST routes do, and take an optional overrideLock. A
refusal carries ENTRY_LOCKED and the holder in _meta.details. The _rev
description already tells an agent to re-read and retry on CONFLICT, and
both codes come back from the same tools, so the overrideLock description
says that re-reading never clears ENTRY_LOCKED and that the flag is for
when the user asks to write anyway.

POST /revisions/{revisionId}/restore overwrites the entry as well, but
the edit lock never covered it, which left revision_restore without a
REST check to mirror. The route now runs the same check and reads
overrideLock from an optional body, as /publish and /unpublish do. The
admin's restore button sits inside the editor's read-only fieldset, so
only the lock holder restores from the admin, and the holder passes.

In content_update the publish-permission check for a status change moves
ahead of the lock check, so no tool names a holder to a caller who lacks
permission for the write. Every role that passes the edit check also
passes that one, so only the order changes.

The MCP authorization unit test stubbed the handlers with an empty object
for db, which the lock query cannot run against, so it now gets a
migrated in-memory database.

The unreleased entry-edit-lock changeset drops the sentence saying the
MCP content tools do not honour the lock yet, which would be false in a
release that ships both changes.
@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bb6f089

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
create-emdash Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/release-service Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@3059

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@3059

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@3059

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@3059

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@3059

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@3059

emdash

npm i https://pkg.pr.new/emdash@3059

create-emdash

npm i https://pkg.pr.new/create-emdash@3059

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@3059

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@3059

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@3059

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@3059

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@3059

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@3059

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@3059

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@3059

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@3059

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@3059

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@3059

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@3059

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@3059

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@3059

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@3059

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@3059

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@3059

commit: bb6f089

Resolves the conflict in guides/working-with-content.mdx: emdash-cms#3040 rewrote
the guide, so the MCP sentence and the mcp-server reference link now sit
in the lock paragraph under "Work with another editor".
@danielmlr
danielmlr marked this pull request as ready for review September 11, 2026 18:48

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds the missing entry edit-lock checks to MCP content-write tools and the POST /revisions/{revisionId}/restore route, which #2919 left out. The approach is sound: it reuses claimEntryLockForWrite, places the check after permission/ownership gates so unauthorized callers do not learn the lock holder, and adds an overrideLock escape hatch matching the REST API. The corresponding docs and changeset updates are clean.

I checked the changed server code, the revision-restore route, the shared lock handler, the schema additions, the integration/unit tests, and the docs/changeset prose. The lock checks cover content_update, content_delete, content_publish, content_unpublish, content_schedule, content_unschedule, content_discard_draft, and revision_restore in MCP, plus the equivalent REST restore route. Tests exercise refusal, override, own-lease extension (implicitly through success), and permission-before-lock ordering. No logic bugs, regressions, or convention violations were found.

One note: the activate_skill call for writing-emdash-docs could not be satisfied because that skill name is not registered as an active skill in this environment, but I reviewed the documentation changes manually against the implementation. Everything checks out. Clean change.

@github-actions github-actions Bot added the review/approved Approved; no new commits since label Sep 11, 2026
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.

1 participant