Skip to content

feat(blueprints): emit per-field wire-format spec to guide MCP agents#29

Merged
sylvesterdamgaard merged 1 commit into
mainfrom
feat/eng-804-field-format-spec
May 5, 2026
Merged

feat(blueprints): emit per-field wire-format spec to guide MCP agents#29
sylvesterdamgaard merged 1 commit into
mainfrom
feat/eng-804-field-format-spec

Conversation

@sylvesterdamgaard
Copy link
Copy Markdown
Contributor

Fixes ENG-804.

Why

Agents using the Statamic MCP keep failing on bard/replicator writes because the blueprint output exposes raw fieldtype config but never explains the wire format — inline-vs-full bard, ProseMirror vs markdown nesting, the id/type/enabled shape replicator items need, which set handles are allowed where, and so on. ENG-804 was the visible symptom (silent generic error on bard/replicator updates), but the underlying issue is information architecture: agents are guessing because the server doesn't tell them.

This PR shifts that — the server now derives and emits a structured wire-format spec from the fieldtype config, and surfaces validation errors with field paths through the production sanitization layer.

What changed

  • New FieldFormatSpec service (src/Mcp/Support/FieldFormatSpec.php) — projects any Statamic Field into a normalized spec: shape, allowed node types/marks/heading levels, allowed set types, recursive set definitions, common mistakes, canonical example. Covers bard (inline + full), replicator, grid, group, markdown, scalar, select/checkbox, relationship, asset, table, date.
  • BlueprintsRouter::get returns _format_spec per field by default. New flags include_format_spec (default true) and max_format_depth (default 2, max 5) for callers that want to opt out or change recursion depth.
  • New FieldFormatException class — curated subclass of InvalidArgumentException. SanitizesFieldData throws this specific class for malformed bard/replicator/grid/table input, replacing bare InvalidArgumentException.
  • BaseStatamicTool::isClientSafeException() — allow-list of exception classes whose messages survive production sanitization (FieldFormatException, ValidationException, FieldtypeNotFoundException, BlueprintNotFoundException). Other Throwable types continue to be genericised in production.
  • Schema description corrections on BlueprintsRouter for include_fields, include_config, include_format_spec, max_format_depth — descriptions now reflect actual list-vs-get scoping.

Why this fixes ENG-804

  1. The blueprint response now tells the agent the exact payload contract before it writes — no more guessing about whether a bard field is inline, which set handles a replicator accepts, or whether callout.content is markdown or ProseMirror.
  2. When the agent still produces malformed data, SanitizesFieldData throws FieldFormatException with a precise field path, and that message now reaches the client in production rather than being replaced with the generic placeholder.

Test plan

  • composer pint:test — clean
  • composer stan — PHPStan Level 8 zero errors
  • composer test — 1009 passed, 5234 assertions
  • Unit tests for every covered fieldtype (tests/Unit/FieldFormatSpecTest.php)
  • Integration tests confirming BlueprintsRouter::get returns _format_spec and that malformed entry writes surface field-path errors (tests/Integration/BlueprintFormatSpecTest.php)
  • Production-environment test confirming FieldFormatException messages survive while unrelated RuntimeException / TypeError get genericised — including assertions that vendor paths do not appear in the response (tests/Integration/ClientSafeExceptionTest.php)
  • Smoke test against a real Geocodio-shaped blueprint to validate _format_spec payload size stays within the 100KB response cap at default depth

Notes for reviewers

  • _format_spec defaults to true on get. Verify this is acceptable for your largest blueprints; pass include_format_spec=false to disable, or lower max_format_depth to trim the response.
  • The Throwable widening that an earlier draft of this PR introduced was reverted — the create/update catches are back at \Exception, with non-Exception Throwable types flowing through BaseStatamicTool for centralized logging and environment-aware sanitization.

Fixes ENG-804.

Adds a FieldFormatSpec service that derives a wire-format guidance object
from each Statamic Field — bard inline vs full, replicator/grid/group item
shape, allowed set types, recursive set definitions, markdown vs ProseMirror
distinction, relationship/asset/date input shapes — and returns it via
BlueprintsRouter::get on the new _format_spec key for every field. Agents
reading a blueprint now see the exact payload contract instead of having
to interpret raw fieldtype config.

Backs the format spec with a dedicated FieldFormatException class. The
SanitizesFieldData trait throws this specific subclass for malformed
bard/replicator/grid/table input, and BaseStatamicTool::execute() allow-lists
it (alongside Laravel's ValidationException and Statamic's
FieldtypeNotFoundException / BlueprintNotFoundException) so the field-path
error message reaches clients in production rather than being replaced with
the generic "An error occurred" placeholder. Other Throwables continue to
flow through the production sanitizer to avoid leaking internals.

Schema descriptions on BlueprintsRouter were also corrected to reflect the
actual list-vs-get scoping of include_fields, include_config,
include_format_spec, and max_format_depth.
@sylvesterdamgaard sylvesterdamgaard merged commit 1765e71 into main May 5, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant