Bug Description
The fix for #27 traded one failure for another. updateEntry() now removes the slug from the payload before blueprint validation (unset($data['slug'])). That resolves the "already been taken" false positive, but the blueprint's required rule on the same field can no longer be satisfied by any caller. Entry updates still fail — same tool, same method, opposite error.
|
#27 |
now (v2.8.0) |
| Error |
slug: This value has already been taken. |
slug: The Slug field is required. |
| Cause |
slug is in the payload, UniqueEntryValue compares it against itself |
slug is not in the payload, required fires |
This affects every collection whose blueprint contains the standard slug field that Statamic generates itself, so in practice every collection.
How to Reproduce
-
Use a collection whose blueprint has the default Statamic slug field:
handle: slug
field:
type: slug
localizable: true
validate:
- required
- 'new \Statamic\Rules\UniqueEntryValue({collection}, {id}, {site})'
-
Call statamic-entries with action: update. Both variants fail:
Omitting the slug:
{
"action": "update",
"collection": "blog",
"id": "<entry-id>",
"data": { "bard": [ { "type": "paragraph", "content": [ { "type": "text", "text": "TEST" } ] } ] }
}
Sending the correct, unchanged slug:
{
"action": "update",
"collection": "blog",
"id": "<entry-id>",
"data": {
"title": "My Entry",
"slug": "my-entry",
"date": "2025-01-15T00:00:00.000Z",
"bard": [ { "type": "paragraph", "content": [ { "type": "text", "text": "TEST" } ] } ]
}
}
-
There is no payload that succeeds.
Expected Behavior
The update succeeds. When the caller omits slug, the entry keeps its current slug and the required rule is satisfied from the existing value. When the caller sends a slug, uniqueness is checked with the current entry excluded.
Actual Behavior
Field validation failed: slug: The Slug field is required.
When date is also omitted, both required entry properties are reported:
Field validation failed: slug: The Slug field is required.; date: The Date field is required.
Tool Name
statamic-entries, action update.
Environment
- cboxdk/statamic-mcp: v2.8.0
- statamic/cms: v6.28.0 (Pro, licensed)
- laravel/framework: v12.67.0
- laravel/mcp: v0.6.7
- PHP: 8.4
- Installation: custom, no starter kit
- Clients: Claude Code and Claude Desktop
- Servers: both affected — stdio (
php artisan mcp:start) and the web endpoint with Bearer token
Additional Context
Root cause, in src/Mcp/Tools/Routers/EntriesRouter.php:
- L541–561 — the slug is validated separately, applied via
$entry->slug(), then unset($data['slug'])
- L595–600 — an explicit note not to inject the slug into
$mergedData
- L602 —
$mergedData = array_merge($entry->data()->all(), $data). $entry->data() does not contain
the slug either, since it is an entry property, so the key is absent in every case
- L611–615 —
FieldsValidator still validates against the full blueprint, where slug is required
withReplacements() — the fix recommended by the maintainer in #27 — is still not called on either
FieldsValidator invocation.
Suggested fix. Keep the slug in the validated payload and resolve the rule placeholders instead:
(new FieldsValidator)
->fields($blueprint->fields()->addValues($mergedData))
->withContext($validationContext)
->withReplacements([
'id' => $entry->id(),
'collection' => $entry->collectionHandle(),
'site' => $site,
])
->validate();
Applies to both calls, including the one in the catch (\TypeError $e) fallback.
date shows the pattern that works. L565–569 keeps the normalized value in $data with the
comment "Keep a normalized copy in data so the FieldsValidator sees the required field". The same
treatment would fix slug. Side effect worth noting: date must be resent on every update, or
validation fails with date: The Date field is required. even when the caller does not intend to
change it.
Related, probably from the same refactor. createEntry() reads the slug from
$arguments['slug'] (L353), but slug is not declared in the tool schema. It is therefore never
set, and the slug is always derived from the title. Setting an explicit slug on create is not
possible.
Current workaround, in case it helps others: remove required from the slug field's validate
list in the blueprint. UniqueEntryValue stays in force, and no MCP path can produce an empty slug
(omitting it leaves the entry's slug untouched, and the router's own slug check rejects an empty
string). It does weaken CP-side validation, so it is not a fix.
Checklist
Bug Description
The fix for #27 traded one failure for another.
updateEntry()now removes the slug from the payload before blueprint validation (unset($data['slug'])). That resolves the "already been taken" false positive, but the blueprint'srequiredrule on the same field can no longer be satisfied by any caller. Entry updates still fail — same tool, same method, opposite error.slug: This value has already been taken.slug: The Slug field is required.UniqueEntryValuecompares it against itselfrequiredfiresThis affects every collection whose blueprint contains the standard slug field that Statamic generates itself, so in practice every collection.
How to Reproduce
Use a collection whose blueprint has the default Statamic slug field:
Call
statamic-entrieswithaction: update. Both variants fail:Omitting the slug:
{ "action": "update", "collection": "blog", "id": "<entry-id>", "data": { "bard": [ { "type": "paragraph", "content": [ { "type": "text", "text": "TEST" } ] } ] } }Sending the correct, unchanged slug:
{ "action": "update", "collection": "blog", "id": "<entry-id>", "data": { "title": "My Entry", "slug": "my-entry", "date": "2025-01-15T00:00:00.000Z", "bard": [ { "type": "paragraph", "content": [ { "type": "text", "text": "TEST" } ] } ] } }There is no payload that succeeds.
Expected Behavior
The update succeeds. When the caller omits
slug, the entry keeps its current slug and therequiredrule is satisfied from the existing value. When the caller sends a slug, uniqueness is checked with the current entry excluded.Actual Behavior
When
dateis also omitted, both required entry properties are reported:Tool Name
statamic-entries, actionupdate.Environment
php artisan mcp:start) and the web endpoint with Bearer tokenAdditional Context
Root cause, in
src/Mcp/Tools/Routers/EntriesRouter.php:$entry->slug(), thenunset($data['slug'])$mergedData$mergedData = array_merge($entry->data()->all(), $data).$entry->data()does not containthe slug either, since it is an entry property, so the key is absent in every case
FieldsValidatorstill validates against the full blueprint, whereslugisrequiredwithReplacements()— the fix recommended by the maintainer in #27 — is still not called on eitherFieldsValidatorinvocation.Suggested fix. Keep the slug in the validated payload and resolve the rule placeholders instead:
Applies to both calls, including the one in the
catch (\TypeError $e)fallback.dateshows the pattern that works. L565–569 keeps the normalized value in$datawith thecomment "Keep a normalized copy in data so the FieldsValidator sees the required field". The same
treatment would fix
slug. Side effect worth noting:datemust be resent on every update, orvalidation fails with
date: The Date field is required.even when the caller does not intend tochange it.
Related, probably from the same refactor.
createEntry()reads the slug from$arguments['slug'](L353), butslugis not declared in the tool schema. It is therefore neverset, and the slug is always derived from the title. Setting an explicit slug on create is not
possible.
Current workaround, in case it helps others: remove
requiredfrom the slug field'svalidatelist in the blueprint.
UniqueEntryValuestays in force, and no MCP path can produce an empty slug(omitting it leaves the entry's slug untouched, and the router's own slug check rejects an empty
string). It does weaken CP-side validation, so it is not a fix.
Checklist