fix(core): accept markdown strings in portableText fields over MCP#1164
fix(core): accept markdown strings in portableText fields over MCP#1164officialasishkumar wants to merge 2 commits into
Conversation
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
🦋 Changeset detectedLatest commit: 1676290 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 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 |
|
All contributors have signed the CLA ✍️ ✅ |
There was a problem hiding this comment.
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 tocontent_createandcontent_updatepayloads. - Update the
content_createMCP tool description to document markdown acceptance forportableText. - 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.
| const resolvedId = extractContentId(existing.data) ?? args.id; | ||
| const data = | ||
| args.data === undefined | ||
| ? undefined | ||
| : await convertContentDataForWrite(emdash, args.collection, args.data); | ||
|
|
|
recheck |
What does this PR do?
Fixes the MCP
content_createandcontent_updatetools rejecting a markdown string in aportableTextfield with[VALIDATION_ERROR] content: Invalid input: expected array, received string.The
emdashCLI client already converts markdown to Portable Text viaconvertDataForWrite()before sending (packages/core/src/client/index.ts), but the MCP server passedargs.datastraight tohandleContentCreate/handleContentUpdatewithout that step. Since LLM callers reliably produce markdown but not valid Portable Text JSON (unique_keys, resolvedmarkDefs, 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
portableTextfield accepts either a Portable Text array or a markdown string. Existing Portable Text arrays pass through unchanged.Closes #1005
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Root cause
convertDataForWrite()(packages/core/src/client/portable-text.ts) maps markdown strings to Portable Text for any field declared asportableText. The CLI client calls it before every create/update:The MCP server did not —
packages/core/src/mcp/server.tspassedargs.datadirectly:A
portableTextfield'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
SchemaRegistryand runs the sameconvertDataForWrite()the CLI uses, then call it in both MCP tools before the handlers:content_createconvertsargs.data;content_updateconverts it when present. Thecontent_createtool description is updated to note thatportableTextfields 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:content_createwithcontent: "## Heading\n\nSome **bold** text."succeeds and stores a Portable Text array (_type: "block",style: "h2");content_updatewith a markdown string forcontentsucceeds and stores Portable Text;content_createis stored unchanged.The first two reproduce the bug — on
mainthe 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 byconvertDataForWrite, which this change reuses rather than reimplements.