Skip to content

fix(core): accept Markdown for portableText fields in MCP content tools#1563

Merged
ascorbic merged 2 commits into
mainfrom
fix/mcp-markdown-portable-text
Jun 21, 2026
Merged

fix(core): accept Markdown for portableText fields in MCP content tools#1563
ascorbic merged 2 commits into
mainfrom
fix/mcp-markdown-portable-text

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Agents drive the MCP content_create tool by emitting the whole arguments object as one inline JSON blob. When a rich text (portableText) field is authored as a deeply nested Portable Text array, that blob gets large and the model frequently truncates or mis-escapes it — so the entire tool call fails to parse (Invalid input for tool content_create: JSON parsing failed: Text: {...}) before any server code runs. It's not a serialization bug in EmDash; it's that we were forcing the model down the one path it's worst at (hand-emitting a big nested JSON tree).

This wires the same Markdown↔Portable Text conversion the EmDash client already uses into the MCP tools:

  • content_create / content_update now accept a Markdown string for portableText fields and convert it to Portable Text before calling the handlers. A Portable Text JSON array is still accepted unchanged (conversion only touches string values on portableText fields). Collapsing the nested array into one flat scalar is trivial for a model to emit correctly.
  • content_get / content_list gain an optional markdown flag (default false, so the response shape is unchanged for existing consumers) that returns rich text as Markdown instead of Portable Text arrays.
  • The tool descriptions now steer callers toward Markdown and explain why.

Conversion is best-effort: if the collection schema can't be loaded, data passes through and the handler validates it as before.

Closes #

Type of change

  • Bug fix

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 (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (Claude Code)

Screenshots / test output

Test Files  19 passed (19)
     Tests  336 passed (336)

(full packages/core MCP integration + unit suites, including the new markdown-portable-text.test.ts)

Agents emit the content_create arguments as one inline JSON blob. A rich
text field authored as a deeply nested Portable Text array makes that blob
large and easy to truncate or mis-escape, so the whole tool call fails to
parse before any handler runs ("JSON parsing failed").

content_create and content_update now convert a Markdown string to Portable
Text for portableText fields before calling the handlers, mirroring
EmDashClient. A Portable Text array is still accepted unchanged. content_get
and content_list gain an optional `markdown` flag (default false) that
returns rich text as Markdown instead of Portable Text arrays.
@changeset-bot

changeset-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 523bc08

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

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

@github-actions github-actions Bot added area/core size/L review/needs-review No maintainer or bot review yet labels Jun 21, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 523bc08 Jun 21 2026, 07:02 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 523bc08 Jun 21 2026, 07:03 AM

@pkg-pr-new

pkg-pr-new Bot commented Jun 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 523bc08

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 523bc08 Jun 21 2026, 07:03 AM

@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.

Approach

This is the right change, solving the right problem, done in a way that fits EmDash. Agents emit MCP content_create/content_update arguments as one inline JSON blob, and a deeply nested Portable Text array for a portableText field is exactly the shape models truncate/mis-escape — so the tool call fails to parse before any server code runs. Letting agents pass a Markdown scalar instead, and read it back as Markdown, collapses the failure-prone payload. Crucially, the PR doesn't reinvent conversion: it reuses convertDataForRead/convertDataForWrite from client/portable-text.ts, the same code path the EmDash JS client and CLI already use. The read-side markdown flag defaults to false, so the response shape is unchanged for existing consumers, and PT JSON arrays still pass through on writes (convertDataForWrite only touches string values on portableText fields). Backwards-compatible, additive, idiomatic.

(The author marks this a bug fix and skips the "approved Discussion" item. It's borderline feature/fix, but the change is additive, opt-in on reads, reuses existing infrastructure, and addresses a genuine usability failure of the existing tools — I'd accept the framing.)

What I checked

Traced every changed hunk in packages/core/src/mcp/server.ts and the conversion functions in client/portable-text.ts:

  • getCollectionFields returns null on any failure → convertWriteData/applyReadMarkdown fall back to pass-through, documented as best-effort. On a write where the schema can't load, the Markdown string reaches the handler and surfaces as a field validation error — acceptable degradation, and aligns with the collection-not-found / DB-error cases where the handler errors anyway.
  • No aliasing/mutation hazard: convertDataForWrite/convertDataForRead shallow-copy before reassigning field values, so args.data isn't mutated; applyReadMarkdown reassigns item.data on fresh handler results (handleContentGet/handleContentList build a new ContentRepository per call and return fresh objects — no request cache to poison).
  • content_get: the Markdown conversion runs after the subscriber not-found gate, so draft content is never leaked as Markdown to users without content:read_drafts. content_list forces status: "published" for subscribers before the conversion runs.
  • content_list narrows payload (object + "items" in + Array.isArray) before mutating; nextCursor/total are left untouched.
  • Both content_create branches (published + default) and all three content_update branches (published/draft/default) use the converted data variable; the args.data || gates still key off the original input, which is correct.
  • No new SQL against content tables — getCollectionWithFields hits _emdash_collections/_emdash_fields (system tables), so no locale-filter concern; the registry's queries are parameterized, no injection surface.
  • Authorization unchanged: requireScope/requireRole/requireOwnership all intact; the markdown flag adds a schema read but exposes no data the caller couldn't already see (they already receive the field values).
  • Types: FieldSchema is imported import type (satisfies verbatimModuleSyntax); the as Record<string, unknown> casts are guarded by typeof === "object" + truthiness checks.

Tests

markdown-portable-text.test.ts runs five real integration tests through the in-memory MCP harness against a migrated SQLite DB with a post collection (title: string, content: portableText) — no mocks. Covers: create Markdown→PT (asserts block structure + strong mark), create PT-array passthrough, get default-PT vs markdown:true round-trip, list with markdown:true, and update Markdown→PT. This satisfies the "reproducing test" requirement.

Changeset

.changeset/mcp-markdown-portable-text.mdemdash patch, accurate, present, describes the observable effect rather than internals.

Conclusion

Implementation is clean. No needs_fixing issues found. One non-blocking observation: the markdown read flag is added to content_get/content_list but not to content_compare or content_list_trashed, which also return portableText field data — an agent reading Markdown might want it there too, though returning raw PT from content_compare is arguably intentional for precise diffing. Worth a maintainer's glance, not a blocker. LGTM.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jun 21, 2026
Shorten the convertWriteData/applyReadMarkdown doc comments to state what
they do, and steer content_create/content_update callers to use Portable
Text for complex content that Markdown can't express.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Jun 21, 2026
@ascorbic ascorbic merged commit c219aff into main Jun 21, 2026
45 checks passed
@ascorbic ascorbic deleted the fix/mcp-markdown-portable-text branch June 21, 2026 07:18
@emdashbot emdashbot Bot mentioned this pull request Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core 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.

1 participant