Problem/Motivation
On multilingual sites an article is one node with linked translations. The tool input schemas generated by ToolApiSchemaConverter::inputDefinitionsToSchema() never emit additionalProperties: false, and ToolApiCallToolHandler only copies declared inputs to the plugin — so an unknown argument passes Opis validation and is silently dropped.
Real-world consequence (hit during a production content migration): mcp_delete_content {"nid": 416, "language": "it"} — the caller intended to delete only the Italian translation. The undeclared language key was ignored and the tool deleted the entire node with all three translations. destructive: TRUE on the Tool attribute is only a client-side hint (destructiveHint), so nothing guards the destructive default at runtime.
Related gaps in the content tools:
mcp_update_contentalways writes the source-language entity — there is no way to target a specific translation, and unknown field names inupdatesare silently skipped (the call reports success without applying anything).- There is no way to delete a single translation while keeping the other language versions.
AuditLogger::sanitizeDetails()throws a TypeError under strict_types when details contain list values (strtolower()on integer array keys).
Steps to reproduce
- Enable content_translation and create a node with one or more translations.
- Call
mcp_delete_contentwith{"nid": N, "language": "xx"}. - The call passes input validation, the
languagekey is dropped before reaching the plugin, and the whole node — every translation — is deleted.
Proposed resolution
Patch attached (rolled against 1.0.0-beta14):
- Fail loudly on unknown parameters, for every tool: emit
additionalProperties: falsein each generated input schema (also visible to MCP clients intools/list);ToolInputValidatorappends the allowed parameter list toadditionalPropertieserrors. A typo or unsupported key can no longer fall through to a destructive default. mcp_delete_content: new optionallanguagedeletes only that translation — saved as a new revision so earlier revisions retain the removed translation as a recovery path; the node's source language is protected while translations exist. A bare{"nid": N}is now rejected: deleting the whole node requires an explicitconfirm_delete_all: true, and the response lists the removed language versions. Passing bothlanguageandconfirm_delete_allis rejected as ambiguous. The response is trash-aware (ModuleHandler injected) instead of always claiming the delete is permanent.mcp_update_content: new optionallanguageroutes the update to that translation via the mcp_tools_translate submodule proposed in #3611087 (soft dependency — a clear error is returned when it is not installed). Unknown field names inupdatesare rejected before anything is saved, and the response names the affected language and the updated fields.AuditLogger: cast array keys to string beforestrtolower().- Unit tests (schema converter, input validator, ContentService) and Kernel tests (full delete-guard matrix, translation delete with revision retention, trash integration, fail-loud update validation) included.
The translation-side write mechanics (replacing an existing translation via an explicit overwrite flag, field-level per-language updates including paragraph content) live in the submodule proposed in #3611087.
Remaining tasks
- Review.
API changes
- Behavior change: unknown tool parameters now return a validation error (previously accepted and ignored).
- Behavior change:
mcp_delete_contentwith onlynidis rejected; passconfirm_delete_all: truefor the previous full-delete behavior. - New optional inputs:
language+confirm_delete_all(DeleteContent),language(UpdateContent). ContentService::deleteContent()gained optional parameters; the service constructor gainedModuleHandlerInterface(services.yml updated in the patch).
| Comment | File | Size | Author |
|---|---|---|---|
| #2 | mcp_tools-language-aware-content-tools-3612659-2.patch | 56.45 KB | julien |
Comments
Comment #2
julien commentedComment #3
mowens commentedshipped in beta15 - sorry this cost you a node. Thanks for turning an incident into a proper fix.
Comment #4
mowens commented