Summary
contentrain_model_save (@contentrain/mcp 3.0.2) enforces snake_case field names on every field in the payload, including fields that already exist in the model and are passed through unchanged. There is no grandfathering, so any legacy model containing a camelCase field becomes read-only via MCP: no edit to that model can be saved — not even one that doesn't touch the offending field.
Real-world case (lanista-landing migration, 2026-08-14)
Migrating lanista-landing (created on contentrain 0.6.x / query 6.x) to contentrain 0.9.2 went smoothly overall (validate --fix repaired 11 models' title_field), but then:
validate --fix picked title_field: "title" for testimonials. Wrong pick for this model — title holds the job title ("CEO, Popile"), name holds the person's name — exactly the "read the output and correct it with contentrain_model_save" case the workflow prescribes.
- The correction was impossible: the model has a legacy
creativeWork relation field, so any model_save for testimonials is rejected:
Validation failed
Field "creativeWork": invalid name — must be snake_case starting with letter
service-pages was worse: 7 legacy camelCase fields (approachContent, benefitsContent, challengeContent, heroDescription, metaDescription, metaTitle, relatedWorks), all pre-dating the snake_case rule.
The only way out was a full rename migration: 2 × model_save + renamed content keys for en+tr (20 entries) + SDK regeneration + updating every consuming component/server route. That's a coordinated content-and-code migration just to fix a one-line title_field correction. It worked, but every legacy project will hit this wall.
Inconsistency between validate and model_save
contentrain validate accepts these models — the project reports 0 errors while its models are un-editable through MCP. The two validators disagree on what a valid model is, and the agent only discovers the wall when a write fails.
Suggested resolution
Either (ideally both):
- Grandfather existing field names on upsert — when
model_save targets an existing model, enforce snake_case only for newly added fields. Renames stay possible but optional.
- Guided rename migration — a
validate --fix rule or CLI command that renames a model field and its content keys across all locales in one transaction (code updates stay with the agent, as usual). Without this, the manual sequence is easy to get wrong: model_save first leaves content with unknown keys until every locale is re-saved.
If strict enforcement is intentional, validate should at least surface a warning for legacy camelCase fields so the agent learns about the wall before a write fails mid-operation.
Environment
- contentrain 0.9.2 / @contentrain/mcp 3.0.2 (via
contentrain serve --stdio)
- @contentrain/query 7.0.4, stack
nuxt, locales en+tr
- macOS (darwin 25.5.0)
Repro
- Take any project with a pre-0.9 model containing a camelCase field (e.g.
creativeWork: { type: "relation", model: "workitems" }).
- Run
npx contentrain@latest validate → 0 errors.
- Call
contentrain_model_save for that model with the field passed through unchanged (e.g. only changing title_field).
- Save is rejected with
Field "creativeWork": invalid name — must be snake_case starting with letter.
Summary
contentrain_model_save(@contentrain/mcp 3.0.2) enforces snake_case field names on every field in the payload, including fields that already exist in the model and are passed through unchanged. There is no grandfathering, so any legacy model containing a camelCase field becomes read-only via MCP: no edit to that model can be saved — not even one that doesn't touch the offending field.Real-world case (lanista-landing migration, 2026-08-14)
Migrating
lanista-landing(created on contentrain 0.6.x / query 6.x) to contentrain 0.9.2 went smoothly overall (validate --fixrepaired 11 models'title_field), but then:validate --fixpickedtitle_field: "title"fortestimonials. Wrong pick for this model —titleholds the job title ("CEO, Popile"),nameholds the person's name — exactly the "read the output and correct it with contentrain_model_save" case the workflow prescribes.creativeWorkrelation field, so anymodel_savefortestimonialsis rejected:service-pageswas worse: 7 legacy camelCase fields (approachContent,benefitsContent,challengeContent,heroDescription,metaDescription,metaTitle,relatedWorks), all pre-dating the snake_case rule.The only way out was a full rename migration: 2 ×
model_save+ renamed content keys for en+tr (20 entries) + SDK regeneration + updating every consuming component/server route. That's a coordinated content-and-code migration just to fix a one-linetitle_fieldcorrection. It worked, but every legacy project will hit this wall.Inconsistency between validate and model_save
contentrain validateaccepts these models — the project reports 0 errors while its models are un-editable through MCP. The two validators disagree on what a valid model is, and the agent only discovers the wall when a write fails.Suggested resolution
Either (ideally both):
model_savetargets an existing model, enforce snake_case only for newly added fields. Renames stay possible but optional.validate --fixrule or CLI command that renames a model field and its content keys across all locales in one transaction (code updates stay with the agent, as usual). Without this, the manual sequence is easy to get wrong: model_save first leaves content with unknown keys until every locale is re-saved.If strict enforcement is intentional,
validateshould at least surface a warning for legacy camelCase fields so the agent learns about the wall before a write fails mid-operation.Environment
contentrain serve --stdio)nuxt, locales en+trRepro
creativeWork: { type: "relation", model: "workitems" }).npx contentrain@latest validate→ 0 errors.contentrain_model_savefor that model with the field passed through unchanged (e.g. only changingtitle_field).Field "creativeWork": invalid name — must be snake_case starting with letter.