Skip to content

feat(mcp): support taxonomies field on content_create/content_update (#953)#1874

Merged
ascorbic merged 2 commits into
emdash-cms:mainfrom
Vallhalen:feat/mcp-taxonomies-field
Jul 9, 2026
Merged

feat(mcp): support taxonomies field on content_create/content_update (#953)#1874
ascorbic merged 2 commits into
emdash-cms:mainfrom
Vallhalen:feat/mcp-taxonomies-field

Conversation

@Vallhalen

@Vallhalen Vallhalen commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Extends MCP content_create and content_update (plus their REST POST/PUT counterparts) with a taxonomies field of shape { [taxonomyName]: [termSlug, ...] }. Slugs are resolved in the entry's locale and persisted through the same TaxonomyRepository.setTermsForEntry path the PUT /content/{collection}/{id}/terms/{taxonomy} REST route uses, inside the content-write transaction.

Prior to this PR the field was silently accepted and dropped, forcing agents to make one follow-up REST call per taxonomy after every create. Unknown slug or non-array shape now surfaces a VALIDATION_ERROR and rolls back the whole write.

Closes #953

Type of change

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

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (n/a; MCP tool, no admin UI strings)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: n/a (bug fix)

AI-generated code disclosure

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

Screenshots / test output

packages/core/tests/integration/content/content-taxonomies.test.ts runs under describeEachDialect and covers:

  1. Attaches terms on create by slug (baseline).
  2. Rolls back the whole transaction and returns VALIDATION_ERROR when a slug is unknown; the content row must not survive.
  3. Rejects non-array values.
  4. Update replaces assignments (setTermsForEntry semantics).
  5. Empty array clears one taxonomy.
  6. Update leaves untouched taxonomies alone.
  7. Locale scoping: EN slug fails on an FR entry when both locales have distinct term variants.

7/7 pass on SQLite (Postgres skipped locally without EMDASH_TEST_PG). pnpm typecheck and pnpm lint --deny-warnings clean.

Design notes

  • Slugs, not IDs. Agents can look up a taxonomy's term slugs from the schema and use them directly; it also matches how editors think about tags.
  • Same code path as the REST terms route. Resolves slugs via TaxonomyRepository.findBySlug, calls setTermsForEntry, then invalidateTermCache(). The two entry points can't drift.
  • Atomic. Assignment runs inside the same withTransaction block as the content write. On unknown slug or shape error, the entire content write rolls back.
  • Update semantics match setTermsForEntry. Only taxonomies named in the payload are touched; an empty array clears one taxonomy; a missing key leaves it alone.

Overlap with #1841

#1841 covers the same issue with a different shape (id-or-slug payload, adds a new setTaxonomiesForEntry method). Comparison table in earlier comment; happy to close this and rebase the extra i18n / dual-dialect / rollback tests onto whichever version lands.

…mdash-cms#953)

Extends MCP content_create and content_update (and the matching REST
POST/PUT bodies) with a `taxonomies` field of shape
`{ [taxonomyName]: [termSlug, ...] }`. Term slugs are resolved in the
entry's locale via `TaxonomyRepository.findBySlug` and persisted through
`setTermsForEntry` in the same transaction as the content write, so
crash recovery leaves either both changes or neither.

- Unknown slug or wrong-shape input surfaces a VALIDATION_ERROR from the
  handler; the entire content write rolls back.
- Update semantics match `setTermsForEntry`: only the taxonomies named in
  the payload are touched, and an empty array clears one taxonomy.
- Reuses the exact code path the `/terms/{taxonomy}` REST route uses so
  the two entry points can't drift, including the `invalidateTermCache`
  side-effect for the hasAny hydration cache.

Prior behaviour: `taxonomies` was silently accepted and ignored, forcing
agents to make N follow-up REST calls per taxonomy after every create.
@changeset-bot

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4bab2a2

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

This PR includes changesets to release 16 packages
Name Type
emdash 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/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto 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

@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 4bab2a2

@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 is the right change for the reported issue: agents previously had to make N follow-up REST calls to categorize a newly-created entry, and the new taxonomies field on content_create/content_update resolves slugs in the entry’s locale inside the same transaction as the content write. The implementation reuses TaxonomyRepository.setTermsForEntry (the same path as the /terms/{taxonomy} REST route), wraps everything in the existing withTransaction block, and lets unknown-slug / shape errors roll back the whole write. Tests cover create, update, empty arrays, untouched-taxonomy preservation, rollback, and locale scoping.

I read the diff, the full handler/runtime/MCP files, the taxonomy repository, the REST terms route, and the content API routes. No logic bug or regression jumped out; the schemas, route wrappers, runtime pass-through, and type interfaces are all consistent. I did not run the test suite or linter.

Two small items:

  • The MCP content_create tool description points agents to a term_list tool that does not exist in this server (the actual tools are taxonomy_list and taxonomy_list_terms). That should be corrected so agents can self-serve slug lookups.
  • The cache-invalidation comment in the new helper references a hasAnyTermAssignments cache that is not defined anywhere; the code actually invalidates the taxonomy object cache.

Comment thread packages/core/src/mcp/server.ts Outdated
.record(z.string(), z.array(z.string()))
.optional()
.describe(
"Taxonomy term assignments as { taxonomyName: [termSlug, ...] }. Term slugs are resolved in the entry's locale — call taxonomy_list to see available taxonomies and term_list to look up slugs. Missing keys leave that taxonomy unchanged.",

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.

[needs fixing] The description tells agents to call term_list, but no MCP tool by that name exists. The available taxonomy tools are taxonomy_list and taxonomy_list_terms (see lines ~1979 and ~2005). Pointing to a non-existent tool breaks the self-correction path for agents that need to resolve slugs before writing.

Suggested change
"Taxonomy term assignments as { taxonomyName: [termSlug, ...] }. Term slugs are resolved in the entry's locale — call taxonomy_list to see available taxonomies and term_list to look up slugs. Missing keys leave that taxonomy unchanged.",
"Taxonomy term assignments as { taxonomyName: [termSlug, ...] }. Term slugs are resolved in the entry's locale — call taxonomy_list to see available taxonomies and taxonomy_list_terms to look up slugs. Missing keys leave that taxonomy unchanged.",

Comment on lines +1851 to +1852
// Match the REST route's behaviour: term assignments changed, so
// invalidate the `hasAnyTermAssignments` cache used during hydration.

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.

[suggestion] The helper calls invalidateTermCache(), which bumps the taxonomy object cache; there is no hasAnyTermAssignments cache in this codebase (matches the stale comment already present in the REST terms route). Update the comment so it names the cache that is actually invalidated.

Suggested change
// Match the REST route's behaviour: term assignments changed, so
// invalidate the `hasAnyTermAssignments` cache used during hydration.
// Match the REST route's behaviour: taxonomy term assignments changed, so
// invalidate the taxonomy object cache used during hydration.

@Vallhalen Vallhalen mentioned this pull request Jul 8, 2026
51 tasks
@github-actions

github-actions Bot commented Jul 8, 2026

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.

@Vallhalen

Copy link
Copy Markdown
Contributor Author

Just noticed #1841 (opened 2 days earlier) covers the same issue. I'd missed it when I opened this. Not asking anyone to pick a favourite, but leaving a quick comparison so a maintainer can choose which shape they want to merge:

This PR (#1874) #1841
Payload slug only id or slug
Repo change reuses existing setTermsForEntry adds new setTaxonomiesForEntry (35 lines)
Tests 7, run under describeEachDialect (SQLite + Postgres): i18n locale scoping, atomic rollback on unknown slug, selective/empty-array clear, non-array shape 3 (single dialect)
Rollback proof asserts the content row does not survive an unknown slug not covered
Locale slugs resolved in entry's own locale, with a test showing EN slug fails on an FR entry not exercised in tests

If #1841 is what you want to ship (it's earlier and the id-or-slug shape is arguably more agent-friendly), I'm happy to close this one and rebase my extra tests (i18n scoping, dual-dialect, rollback) as a follow-up PR against whichever version lands. Either way, glad to see this getting fixed.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 8, 2026
@ascorbic

ascorbic commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the comparison. I think this PR is the best one to land. Could you address the issues raised in the bot review and I'll merge this instead of #1841

@ascorbic ascorbic added this to the 1.0 milestone Jul 8, 2026
- Point content_create's taxonomies description at taxonomy_list_terms
  (the real tool) instead of the non-existent term_list, so agents can
  self-serve slug lookups.
- Rewrite the stale hasAnyTermAssignments cache comment on the helper's
  invalidateTermCache call to name what actually gets bumped (the
  taxonomy object cache).
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 9, 2026
@Vallhalen

Copy link
Copy Markdown
Contributor Author

Thanks @ascorbic. Pushed 4bab2a2 addressing both bot findings:

  1. term_listtaxonomy_list_terms in the content_create tool description. Agents following the description now hit a real tool. content_update's description didn't have this reference, so only the create side needed the edit.
  2. Stale hasAnyTermAssignments cache comment rewritten to name the taxonomy object cache that invalidateTermCache() actually bumps. (Same stale wording exists in the REST terms route comment; happy to fix that in a follow-up if you want it consolidated with this cleanup, or I can leave it since it's out of scope for MCP: content_create/content_update ignores taxonomies field (categories/tags not assigned) #953.)

Also rewrote the PR description to use the required template. All 7 tests in content-taxonomies.test.ts still pass; pnpm typecheck and pnpm lint --deny-warnings clean.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@ascorbic
ascorbic merged commit 1526f96 into emdash-cms:main Jul 9, 2026
45 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jul 9, 2026
marcusbellamyshaw-cell pushed a commit to Emdash-Bug-Testing/emdash that referenced this pull request Jul 22, 2026
…mdash-cms#953) (emdash-cms#1874)

* feat(mcp): support taxonomies field on content_create/content_update (emdash-cms#953)

Extends MCP content_create and content_update (and the matching REST
POST/PUT bodies) with a `taxonomies` field of shape
`{ [taxonomyName]: [termSlug, ...] }`. Term slugs are resolved in the
entry's locale via `TaxonomyRepository.findBySlug` and persisted through
`setTermsForEntry` in the same transaction as the content write, so
crash recovery leaves either both changes or neither.

- Unknown slug or wrong-shape input surfaces a VALIDATION_ERROR from the
  handler; the entire content write rolls back.
- Update semantics match `setTermsForEntry`: only the taxonomies named in
  the payload are touched, and an empty array clears one taxonomy.
- Reuses the exact code path the `/terms/{taxonomy}` REST route uses so
  the two entry points can't drift, including the `invalidateTermCache`
  side-effect for the hasAny hydration cache.

Prior behaviour: `taxonomies` was silently accepted and ignored, forcing
agents to make N follow-up REST calls per taxonomy after every create.

* fix(mcp): address review feedback on taxonomies field

- Point content_create's taxonomies description at taxonomy_list_terms
  (the real tool) instead of the non-existent term_list, so agents can
  self-serve slug lookups.
- Rewrite the stale hasAnyTermAssignments cache comment on the helper's
  invalidateTermCache call to name what actually gets bumped (the
  taxonomy object cache).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core overlap review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: content_create/content_update ignores taxonomies field (categories/tags not assigned)

2 participants