Skip to content

fix(core): accept markdown strings in portableText fields over MCP#1164

Open
officialasishkumar wants to merge 2 commits into
emdash-cms:mainfrom
officialasishkumar:fix/1005-mcp-portabletext-markdown
Open

fix(core): accept markdown strings in portableText fields over MCP#1164
officialasishkumar wants to merge 2 commits into
emdash-cms:mainfrom
officialasishkumar:fix/1005-mcp-portabletext-markdown

Conversation

@officialasishkumar
Copy link
Copy Markdown

What does this PR do?

Fixes the MCP content_create and content_update tools rejecting a markdown string in a portableText field with [VALIDATION_ERROR] content: Invalid input: expected array, received string.

The emdash CLI client already converts markdown to Portable Text via convertDataForWrite() before sending (packages/core/src/client/index.ts), but the MCP server passed args.data straight to handleContentCreate/handleContentUpdate without that step. Since LLM callers reliably produce markdown but not valid Portable Text JSON (unique _keys, resolved markDefs, correct block shapes), this effectively blocked writing rich text over MCP — the documented workaround was to fall back to the CLI.

The MCP server now performs the same conversion before the data reaches the handlers, so a portableText field accepts either a Portable Text array or a markdown string. Existing Portable Text arrays pass through unchanged.

Closes #1005

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 (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

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

Root cause

convertDataForWrite() (packages/core/src/client/portable-text.ts) maps markdown strings to Portable Text for any field declared as portableText. The CLI client calls it before every create/update:

// packages/core/src/client/index.ts
const fields = await this.getFieldSchemas(collection);
const data = convertDataForWrite(input.data, fields);

The MCP server did not — packages/core/src/mcp/server.ts passed args.data directly:

await emdash.handleContentCreate(args.collection, { data: args.data, ... });

A portableText field's generated Zod schema expects an array, so a markdown string is rejected at validation time before the write happens.

Fix

Add a small helper that resolves the collection's field types via SchemaRegistry and runs the same convertDataForWrite() the CLI uses, then call it in both MCP tools before the handlers:

async function convertContentDataForWrite(emdash, collection, data) {
  const { SchemaRegistry } = await import("../schema/index.js");
  const def = await new SchemaRegistry(emdash.db).getCollectionWithFields(collection);
  if (!def) return data; // unknown collection -> let the handler raise the real error
  return convertDataForWrite(data, def.fields);
}

content_create converts args.data; content_update converts it when present. The content_create tool description is updated to note that portableText fields accept markdown.

How to test

Added packages/core/tests/integration/mcp/content-portabletext-markdown.test.ts, which drives the real MCP client/server harness against a real SQLite runtime and verifies:

  1. content_create with content: "## Heading\n\nSome **bold** text." succeeds and stores a Portable Text array (_type: "block", style: "h2");
  2. content_update with a markdown string for content succeeds and stores Portable Text;
  3. an existing Portable Text array passed to content_create is stored unchanged.

The first two reproduce the bug — on main the create/update fail validation; with this change they succeed.

The conversion itself (markdown → blocks/spans with marks, markDefs, and unique _keys, plus array pass-through) is exercised directly by convertDataForWrite, which this change reuses rather than reimplements.

The MCP content_create and content_update tools passed `args.data`
straight to the write handlers, so a markdown string in a `portableText`
field failed schema validation with 'expected array, received string'.
The emdash CLI client already converts markdown to Portable Text via
convertDataForWrite before sending; the MCP server skipped that step.

LLM callers reliably produce markdown but not valid Portable Text JSON
(unique _key values, markDefs, block shapes), so this effectively blocked
writing rich text over MCP.

Convert markdown to Portable Text in the MCP server before the data
reaches the handlers, resolving the collection's field types via
SchemaRegistry — the same conversion the CLI performs. Existing Portable
Text arrays pass through unchanged.

Closes emdash-cms#1005
Copilot AI review requested due to automatic review settings May 25, 2026 02:16
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 25, 2026

🦋 Changeset detected

Latest commit: 1676290

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

This PR includes changesets to release 14 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/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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes MCP content_create / content_update rejecting markdown strings in portableText fields by converting markdown → Portable Text in the MCP server before schema validation, matching the emdash CLI client behavior (closes #1005).

Changes:

  • Add convertContentDataForWrite() in the MCP server and apply it to content_create and content_update payloads.
  • Update the content_create MCP tool description to document markdown acceptance for portableText.
  • Add an MCP integration regression test covering create/update markdown conversion and PT-array passthrough; add a patch changeset.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/core/src/mcp/server.ts Converts markdown strings in portableText fields to Portable Text before invoking content write handlers; updates content_create tool docs.
packages/core/tests/integration/mcp/content-portabletext-markdown.test.ts Adds end-to-end MCP regression coverage for markdown → Portable Text conversion on create/update and passthrough behavior for PT arrays.
.changeset/fix-mcp-portabletext-markdown.md Publishes the MCP behavior fix as a patch release note for emdash.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 727 to 732
const resolvedId = extractContentId(existing.data) ?? args.id;
const data =
args.data === undefined
? undefined
: await convertContentDataForWrite(emdash, args.collection, args.data);

@officialasishkumar
Copy link
Copy Markdown
Author

recheck

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.

MCP: content_create/content_update rejects markdown strings in portableText fields

2 participants