Repository navigation
Releases: dynamic/silverstripe-content-api-mcp
Release list
2.0.0
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 ofdetails. - 403 is
authorization_failed, 401authentication_failed. 403s distinguish the
population gate and the single-record update gate, each naming its remedy. content_records_listdefaults to concise records ({id, classRef, externalId, title, stage}) andlimit: 25. Passresponse_format: "detailed"for full records, orfieldsto
add named fields or relations to concise ones.- Arguments are validated before any request: a typo'd argument, wrong type or bogus enum
fails asvalidation_errorwith nothing sent (#36). - Every tool takes an optional
site(the configured site's host name). - Per-tool timeouts when
CONTENT_API_TIMEOUTis unset: 10 s forcontent_auth_session
andcontent_asset_read, 120 s for uploads, compositions, batches and the fingerprint, 30 s
for the rest. SettingCONTENT_API_TIMEOUTexplicitly 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_workflowand four MCP prompts (content-build-landing-page,
content-add-section,content-populate-from-brief,content-check-what-changed).content_list_sites.response_formatoncontent_fingerprintandcontent_schema_site; a response-size cap
(CONTENT_MAX_RESPONSE_CHARS, default 40000) with ameta.truncatedmarker.- A schema cache (
CONTENT_SCHEMA_CACHE_TTL, default 300 s);UNKNOWN_CLASSerrors list the
site's exposed class refs. - MCP tool annotations (
readOnlyHint,destructiveHint,idempotentHint) on every tool. content_api_mcp/schema/module-refrecords the module commit the bundled spec came from;
scripts/sync-spec.shwrites it andscripts/check-module-ref.sh(a local-ci check)
verifies it.
Fixed
- Boolean filters and query args are sent as
1/0, sofilters: {"ShowInMenus": true}no
longer matches the opposite rows (#36). - GET retries respect a bounded policy (5xx only,
Retry-Afternot slept on) and the session
refuses cookies. - Setup snippets pin a real tag: tags carry no
vprefix (@X.Y.Z).
Changed
- Empty and dot-only path values (
.,..) are refused before any request. Path values were
already percent-encoded as one segment withext:literal; that is unchanged.
1.8.0
Added
- Synced bundled
content_api_mcp/schema/endpoints.jsonto module specv1.18, from the
module's 2026-10 security/correctness work:- New
content_records_updatetool (PATCH records/{classRef}/{id}, module #232): a
single-record field update that needsCONTENT_API_ACCESSonly and works on a live site,
wherecontent_batch/content_compose_pageare 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 fromrelations(which
rejects has_one names) tofields;relationsdescribes its real{mode, items}contract;
page.elementsRelationis documented (module #243).content_records_list:limit/offsetminimums and the empty-filter rule (module #244).genericCrudnote: an/apiwrite lands on the request's stage, usually LIVE.
- New
Changed
client.py/docs: theext: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
Added
- Synced bundled
content_api_mcp/schema/endpoints.jsonto module specv1.17(v1.16 and v1.17
in one jump — the previous release skipped v1.16):content_records_read/content_records_stage
now describe theirclassRefparameter (a short registry reference, not the fully-qualified PHP
class name — the exact mistake that 404sUNKNOWN_CLASS) and theiridparameter (numeric or
ext:<external-id>).content_compose_page's description now also documents409 CROSS_PAGE_REPARENT: a composition whose resolved elementexternalIdalready belongs to a
different page than the onepage.matchjust 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
Added
- Synced bundled
content_api_mcp/schema/endpoints.jsonto module specv1.15(#119): a new
ownsmode is now selectable oncontent_records_stage.modefor theunpublishaction, not
justpublish— unpublishes a record plus every$owns-reachable descendant, excluding any
File/Imagereached through the walk (a shared asset is routinely owned by more than one
live record; reported instead in askippedresponse 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 theownspublish 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
Added
- Synced bundled
content_api_mcp/schema/endpoints.jsonto module specv1.14(#119, #168): a
newownspublish mode is now selectable oncontent_records_stage.mode,
content_batch.defaultPublish,content_batch's per-oppublish, and
content_page_convert.publish— publishes a record plus every descendant reachable through its
$ownsconfig (e.g. an elemental area and its elements), authorization-checked the same way
subtreealready is, and takesdryRun(but notliveOnly, which stayssubtree-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
Docs
- Synced bundled
content_api_mcp/schema/endpoints.jsonto module specv1.13(#89, #80,
#114):unpublish'sforcefield description now explains thedelete-verb requirement is
scoped to aSiteTreeclass withenforce_strict_hierarchyenabled — on any other class the
bypass is already a no-op, sodeleteisn't required there. Description text only; no
tool/schema shape change.
1.3.0
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$ownsbetween draft and live, reporting a machine-readable structure plus a flatreportlist. Seedynamic/silverstripe-content-apiPR #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). Seedynamic/silverstripe-content-apiPR #139.content_records_stagegainsliveOnlyanddryRun(module #90/#102,mode: subtreeonly) — preview or selectively skip a subtree publish without writing.mode: subtreeitself 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. Seedynamic/silverstripe-content-apiPR #113.content_batchgainsdryRun(module #130) — runs the batch with full authorization/validation inside an unconditionally-rolled-back transaction, prefixing response statuseswould*on success. Its rollback is verified the same way an atomic failure's is (module #127), including the newupdated-op re-check that verification gained alongside it. Seedynamic/silverstripe-content-apiPR #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
Added
filePathas an alternative tobase64oncontent_asset_upload(synced from
dynamic/silverstripe-content-apispec 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 defaultMODULE_DIRpointed 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
filePathchange (above) surfaced four issues in the first pass, all fixed
before release:- Dropping
base64fromcontent_asset_upload'srequiredlist left nothing enforcing that
one ofbase64/filePathis actually provided — a call with neither now raises a clear
client-side error instead of forwarding a bodyless request upstream. - The
base64/filePathmutual-exclusion check used a truthy test, so an explicit
base64: ""alongsidefilePathsilently bypassed it; now presence-based (is not None). filePathhad 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.filePathhad 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.
- Dropping
- Re-syncing the vendored spec (
scripts/sync-spec.sh) silently dropped arelationsfield
description — the only documented warning that a polymorphichas_onewrite rejects a bare
id/externalId and needs an explicit{"class": ...}hint — fromcontent_batchand
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
blindcpfrom there couldn't know to keep it. Restored at the source
(dynamic/silverstripe-content-api's ownschema/endpoints.json) so it survives every future
sync instead of being silently lost again.
0.1.0
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.jsonspec — schema passthrough, no hand-written per-tool handlers. - Built on
mcp-basefor settings/HTTP-session/logging conventions, with a customX-Silverstripe-Apitokenheader 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.