feat(mcp): support taxonomies field on content_create/content_update (#953)#1874
Conversation
…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 detectedLatest commit: 4bab2a2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
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_createtool description points agents to aterm_listtool that does not exist in this server (the actual tools aretaxonomy_listandtaxonomy_list_terms). That should be corrected so agents can self-serve slug lookups. - The cache-invalidation comment in the new helper references a
hasAnyTermAssignmentscache that is not defined anywhere; the code actually invalidates the taxonomy object cache.
| .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.", |
There was a problem hiding this comment.
[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.
| "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.", |
| // Match the REST route's behaviour: term assignments changed, so | ||
| // invalidate the `hasAnyTermAssignments` cache used during hydration. |
There was a problem hiding this comment.
[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.
| // 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. |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
|
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:
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. |
|
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 |
- 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).
|
Thanks @ascorbic. Pushed 4bab2a2 addressing both bot findings:
Also rewrote the PR description to use the required template. All 7 tests in |
…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).
What does this PR do?
Extends MCP
content_createandcontent_update(plus their RESTPOST/PUTcounterparts) with ataxonomiesfield of shape{ [taxonomyName]: [termSlug, ...] }. Slugs are resolved in the entry's locale and persisted through the sameTaxonomyRepository.setTermsForEntrypath thePUT /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_ERRORand rolls back the whole write.Closes #953
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
packages/core/tests/integration/content/content-taxonomies.test.tsruns underdescribeEachDialectand covers:VALIDATION_ERRORwhen a slug is unknown; the content row must not survive.setTermsForEntrysemantics).7/7 pass on SQLite (Postgres skipped locally without
EMDASH_TEST_PG).pnpm typecheckandpnpm lint --deny-warningsclean.Design notes
TaxonomyRepository.findBySlug, callssetTermsForEntry, theninvalidateTermCache(). The two entry points can't drift.withTransactionblock as the content write. On unknown slug or shape error, the entire content write rolls back.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
setTaxonomiesForEntrymethod). Comparison table in earlier comment; happy to close this and rebase the extra i18n / dual-dialect / rollback tests onto whichever version lands.