Skip to content

Releases: dynamic/silverstripe-content-api-mcp

2.0.0

Choose a tag to compare

@jsirish jsirish released this 18 Oct 20:23
dfb9ce5

The server is rebuilt on a shared core, content_api_mcp.core, which DAISY's hosted multi-site
daisy-content server now also depends on, so new tool behaviour lands once for both. The
short-lived maintenance-mode notice (unreleased) is withdrawn.

Breaking

  • Error text. A failed tool call's text is the readable message followed by a final
    content-error: {json} line (kind, status, error_code, retry_after, details,
    valid_options). The message no longer embeds a Python repr of details.
  • 403 is authorization_failed, 401 authentication_failed. 403s distinguish the
    population gate and the single-record update gate, each naming its remedy.
  • content_records_list defaults to concise records ({id, classRef, externalId, title, stage}) and limit: 25. Pass response_format: "detailed" for full records, or fields to
    add named fields or relations to concise ones.
  • Arguments are validated before any request: a typo'd argument, wrong type or bogus enum
    fails as validation_error with nothing sent (#36).
  • Every tool takes an optional site (the configured site's host name).
  • Per-tool timeouts when CONTENT_API_TIMEOUT is unset: 10 s for content_auth_session
    and content_asset_read, 120 s for uploads, compositions, batches and the fingerprint, 30 s
    for the rest. Setting CONTENT_API_TIMEOUT explicitly still applies it to every call.

Added

  • content_get_started: the token's permissions, write gates, integrations, class refs grouped
    into pages / elements / other, blocking-write warnings and the page-building call order.
  • content_get_workflow and four MCP prompts (content-build-landing-page,
    content-add-section, content-populate-from-brief, content-check-what-changed).
  • content_list_sites.
  • response_format on content_fingerprint and content_schema_site; a response-size cap
    (CONTENT_MAX_RESPONSE_CHARS, default 40000) with a meta.truncated marker.
  • A schema cache (CONTENT_SCHEMA_CACHE_TTL, default 300 s); UNKNOWN_CLASS errors list the
    site's exposed class refs.
  • MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint) on every tool.
  • content_api_mcp/schema/module-ref records the module commit the bundled spec came from;
    scripts/sync-spec.sh writes it and scripts/check-module-ref.sh (a local-ci check)
    verifies it.

Fixed

  • Boolean filters and query args are sent as 1/0, so filters: {"ShowInMenus": true} no
    longer matches the opposite rows (#36).
  • GET retries respect a bounded policy (5xx only, Retry-After not slept on) and the session
    refuses cookies.
  • Setup snippets pin a real tag: tags carry no v prefix (@X.Y.Z).

Changed

  • Empty and dot-only path values (., ..) are refused before any request. Path values were
    already percent-encoded as one segment with ext: literal; that is unchanged.

1.8.0

Choose a tag to compare

@jsirish jsirish released this 18 Oct 16:11
efacd6c

Added

  • Synced bundled content_api_mcp/schema/endpoints.json to module spec v1.18, from the
    module's 2026-10 security/correctness work:
    • New content_records_update tool (PATCH records/{classRef}/{id}, module #232): a
      single-record field update that needs CONTENT_API_ACCESS only and works on a live site,
      where content_batch/content_compose_page are closed by the population gate. The client
      already sends any non-GET method with a JSON body, so no code change was needed.
    • content_batch/content_compose_page: has_one guidance moves from relations (which
      rejects has_one names) to fields; relations describes its real {mode, items} contract;
      page.elementsRelation is documented (module #243).
    • content_records_list: limit/offset minimums and the empty-filter rule (module #244).
    • genericCrud note: an /api write lands on the request's stage, usually LIVE.

Changed

  • client.py/docs: the ext: id after the prefix is percent-decoded server-side (module #240).

Matches dynamic/silverstripe-content-api 1.15.0 / 2.9.0 (spec v1.18).

1.7.0

Choose a tag to compare

@jsirish jsirish released this 02 Sep 03:32
ac20085

Added

  • Synced bundled content_api_mcp/schema/endpoints.json to module spec v1.17 (v1.16 and v1.17
    in one jump — the previous release skipped v1.16): content_records_read/content_records_stage
    now describe their classRef parameter (a short registry reference, not the fully-qualified PHP
    class name — the exact mistake that 404s UNKNOWN_CLASS) and their id parameter (numeric or
    ext:<external-id>). content_compose_page's description now also documents 409 CROSS_PAGE_REPARENT: a composition whose resolved element externalId already belongs to a
    different page than the one page.match just resolved is rejected rather than silently
    reparented — the agent-facing signal for module #201. Same as previous syncs, this server
    hardcodes nothing per-tool — the new descriptions and error-code documentation reach the
    generated tool automatically via the synced spec, no code change here.

1.6.0

Choose a tag to compare

@jsirish jsirish released this 18 Aug 21:10
08963e5

Added

  • Synced bundled content_api_mcp/schema/endpoints.json to module spec v1.15 (#119): a new
    owns mode is now selectable on content_records_stage.mode for the unpublish action, not
    just publish — unpublishes a record plus every $owns-reachable descendant, excluding any
    File/Image reached through the walk (a shared asset is routinely owned by more than one
    live record; reported instead in a skipped response list rather than unpublished), and takes
    dryRun (previously rejected outright on every unpublish call — narrowed to permit it for this
    mode specifically). Root unpublishes first, the opposite order from the owns publish mode.
    Same as the previous sync, this server hardcodes nothing per-tool — the new enum value and
    description text reach the generated tool automatically via the synced spec, no code change
    here.

1.5.0

Choose a tag to compare

@jsirish jsirish released this 18 Aug 05:44
bc8b66e

Added

  • Synced bundled content_api_mcp/schema/endpoints.json to module spec v1.14 (#119, #168): a
    new owns publish mode is now selectable on content_records_stage.mode,
    content_batch.defaultPublish, content_batch's per-op publish, and
    content_page_convert.publish — publishes a record plus every descendant reachable through its
    $owns config (e.g. an elemental area and its elements), authorization-checked the same way
    subtree already is, and takes dryRun (but not liveOnly, which stays subtree-only). This
    server hardcodes nothing per-tool — the new enum value and description text reach every
    generated tool automatically via the synced spec, no code change here.

1.4.2

Choose a tag to compare

@jsirish jsirish released this 18 Aug 03:38
94390ad

Docs

  • Synced bundled content_api_mcp/schema/endpoints.json to module spec v1.13 (#89, #80,
    #114): unpublish's force field description now explains the delete-verb requirement is
    scoped to a SiteTree class with enforce_strict_hierarchy enabled — on any other class the
    bypass is already a no-op, so delete isn't required there. Description text only; no
    tool/schema shape change.

1.3.0

Choose a tag to compare

@jsirish jsirish released this 07 Aug 15:24
1af9c2a

Synced bundled spec (content_api_mcp/schema/endpoints.json) to the module's v1.11 (previously v1.7 — four versions behind), adding two new tools and extending two existing ones:

  • content_records_parity (module #120): GET records/{classRef}/{id}/parity — compares a record and everything it $owns between draft and live, reporting a machine-readable structure plus a flat report list. See dynamic/silverstripe-content-api PR #138.
  • content_fingerprint (module #131): GET fingerprint — a deterministic, path-keyed snapshot of the site's content for diffing across gates or environments, plus a reachability-invariant check (violations). See dynamic/silverstripe-content-api PR #139.
  • content_records_stage gains liveOnly and dryRun (module #90/#102, mode: subtree only) — preview or selectively skip a subtree publish without writing. mode: subtree itself now also authorization-checks every descendant before writing anything, refusing the whole call (403 FORBIDDEN_CLASS/FORBIDDEN_RECORD, nothing written) on the first one the caller can't publish. See dynamic/silverstripe-content-api PR #113.
  • content_batch gains dryRun (module #130) — runs the batch with full authorization/validation inside an unconditionally-rolled-back transaction, prefixing response statuses would* on success. Its rollback is verified the same way an atomic failure's is (module #127), including the new updated-op re-check that verification gained alongside it. See dynamic/silverstripe-content-api PR #140 (#130) and PR #135 (#127).
  • No client code change needed — client.py's path-substitution/query-vs-body handling is already generic; both new tools and both new parameters work as soon as the spec lands.

Supersedes the never-tagged 1.2.1. See CHANGELOG.md for the full 1.2.1/1.3.0 history.

1.1.0

Choose a tag to compare

@jsirish jsirish released this 16 Jul 03:26
fd9a78c

Added

  • filePath as an alternative to base64 on content_asset_upload (synced from
    dynamic/silverstripe-content-api spec v1.2, module issue #39). The client resolves it
    locally — reads and base64-encodes the file on the machine running the MCP host — and never
    forwards the path itself upstream. Fixes a real failure mode: without this, an agent whose only
    handle on a file is a local path has to reproduce the entire base64 payload as literal text to
    call the tool, which for a real image (hundreds of KB or more) risks silent corruption on
    reassembly — an image's dimensions parse fine from its header even when the body is truncated
    or garbled, so a corrupted upload can still report success.

Fixed

  • scripts/sync-spec.sh's default MODULE_DIR pointed at a checkout deleted 2026-07-12; now
    defaults to ~/Sites/content-api-testbed/vendor/dynamic/silverstripe-content-api, where the
    module actually lives (flagged as a known follow-up in 1.0.0).
  • Code review on the filePath change (above) surfaced four issues in the first pass, all fixed
    before release:
    • Dropping base64 from content_asset_upload's required list left nothing enforcing that
      one of base64/filePath is actually provided — a call with neither now raises a clear
      client-side error instead of forwarding a bodyless request upstream.
    • The base64/filePath mutual-exclusion check used a truthy test, so an explicit
      base64: "" alongside filePath silently bypassed it; now presence-based (is not None).
    • filePath had no size cap — the whole file is read into memory then base64-encoded (~33%
      larger again) before sending, so an oversized file meant a memory spike and a slow, likely-
      timing-out request instead of a fast, clear rejection. Capped at 25 MiB.
    • filePath had no restriction on what could be read — a caller (or a compromised/injected
      tool argument) could point it at any local file the MCP host can access, e.g. ~/.ssh/id_rsa,
      and it would be read, base64-encoded, and POSTed upstream as if it were an image. Added a
      denylist for dotfiles/dotdirs and common credential-file patterns (id_rsa, *.pem, *.key,
      etc.) as defense-in-depth — explicitly not a security boundary by itself (this MCP host already
      has the same filesystem access as the agent invoking it, and a denylist is trivially bypassed
      by renaming a file); the actual boundary remains not letting untrusted input control tool
      arguments in the first place. This closes the easy/accidental case with a clear error rather
      than a silent exfiltration.
  • Re-syncing the vendored spec (scripts/sync-spec.sh) silently dropped a relations field
    description — the only documented warning that a polymorphic has_one write rejects a bare
    id/externalId and needs an explicit {"class": ...} hint — from content_batch and
    content_compose_page. That description was only ever patched into this repo's bundled copy in
    the 1.0.0 release (#6), never fed back into the module's own spec (the real sync source), so a
    blind cp from there couldn't know to keep it. Restored at the source
    (dynamic/silverstripe-content-api's own schema/endpoints.json) so it survives every future
    sync instead of being silently lost again.

0.1.0

Choose a tag to compare

@jsirish jsirish released this 11 Jul 16:21
d210f0e

Initial release: stdio MCP server proxying dynamic/silverstripe-content-api's /content-api/v1 REST surface.

  • 12 tools generated at startup from a bundled, version-pinned copy of the module's own schema/endpoints.json spec — schema passthrough, no hand-written per-tool handlers.
  • Built on mcp-base for settings/HTTP-session/logging conventions, with a custom X-Silverstripe-Apitoken header for colymba's token auth.
  • Live-verified end-to-end against a running SilverStripe 6 site (auth, schema introspection, records list, idempotent page composition, batch operations).

See PR #1 for the full change list and code-review findings fixed before release.