Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-3342Done

MCP write tools SILENTLY DROP a key their own schema forbids and report success — a card filed with `description` instead of `descriptionMd` lands with no body

Repo · motir-core. The MCP write tools accept a key their own schema forbids, silently discard it, and report success. A card filed with the wrong field name lands as a bare title and the caller is told it worked.

What happened (2026-08-20/21, four occurrences in one session)

create_work_item and update_work_item name the body descriptionMd. Passing description — the obvious guess, and the name the REST DTOs use — produces:

create_work_item({projectKey, kind, title, parentKey, description: "<2 000 words>"})
  → "Created MOTIR-3283 [bug] A story whose deliverable was a PAGE…"
     …and the card has NO BODY AT ALL.

update_work_item({key: "MOTIR-3334", description: "probe"})
  → "Updated MOTIR-3334 [bug] childStatusCascadeService exempts…"
     "Patched: nothing"

update_work_item({key: "MOTIR-3334", title: "…", description: "<body>"})
  → "Patched: title"          ← the title applied, the body vanished

Three bug cards — MOTIR-3283, MOTIR-3313, MOTIR-3334 — were filed with long, evidence-carrying bodies and landed as titles only (measured on read-back: 292, 272 and 312 characters, all of it header). They were reported to the user as filed with their evidence. The loss was found only because a later read of one card came back suspiciously short; nothing in any of the four responses says a field was dropped.

⚠️ The tools' own schema says this should be REJECTED

Both create_work_item and update_work_item publish "additionalProperties": false in their JSON Schema. That is a promise that an unknown key is an ERROR. The runtime does the opposite: it takes the call, drops the key, and returns a success line.

And the validator is not simply absent — it is inconsistent, which is what makes the failure so quiet. A MISSING REQUIRED field fails loudly and usefully:

link_work_items({fromKey, toKey, linkType: "relates_to"})
  → MCP error -32602: Input validation error: [ { "path": ["relationship"],
      "expected": "'blocked_by' | 'blocks' | 'relates_to' | 'duplicates' | 'clones'",
      "received": "undefined", "message": "Required" } ]

Note that this error names the missing relationship and says nothing about the unknown linkType sitting right beside it. So required-key validation works; unknown-key rejection does not, despite being advertised.

Likely mechanism, to be CONFIRMED rather than assumed: the tools pass a plain shape to server.registerTool(..., { inputSchema }, ...), and a non-.strict() zod object strips unknown keys, while zodToJsonSchema emits additionalProperties: false for that same object. The advertised contract and the runtime would then disagree by construction, for every tool. That is a hypothesis from reading lib/mcp/tools/updateWorkItem.ts and the published schemas — check it at the SDK boundary before fixing.

The second half: a NO-OP is reported as a success

summarize() in lib/mcp/tools/updateWorkItem.ts renders Patched: ${patchedKeys.join(', ') || 'nothing'} and the tool returns toolOk. So "Patched: nothing" is a successful call that changed nothing — the one line that could have caught this, styled exactly like the line that reports a real edit.

An update that patches no field is never what a caller meant. It is either a typo'd key (this bug) or a caller with nothing to say, and neither deserves a success.

Why it is worth a card rather than a note

  • It is silent data loss on a WRITE path, and the payload lost is the most expensive thing an agent produces — a card's reasoning, evidence and acceptance criteria.
  • It defeats the obvious check. Reading the tool's success line is the natural verification, and the success line is wrong. Only a read-back with a length check catches it.
  • The wrong guess is the likely guess: description is what the field is called nearly everywhere else, so the failure selects for callers who did not memorise the schema.
  • It scales with agent use. Every card an agent files goes through this door.

Proposed fix

  1. Honour the declared schema: reject an unknown key with an -32602 naming it and, ideally, the nearest valid field (descriptiondescriptionMd, linkTyperelationship). If the strip is the SDK's behaviour rather than ours, the schema should stop advertising additionalProperties: false — the two must not disagree, and rejecting is the better direction.
  2. Patched: nothing must not be a success. Either a -32602 (the caller asked for no change) or, at minimum, a result the caller cannot mistake for an edit.
  3. A test per write tool that passes one unknown key and asserts the call is refused — the class is per-tool, not per-field, so a fix that only special-cases description leaves linkType and every future rename open.

Reproduction

create_work_item({projectKey: "TEST", kind: "task", title: "probe",
                  description: "this text will vanish"})
→ succeeds; the created card has an empty body.

Context refs

  • lib/mcp/tools/updateWorkItem.tsinputSchema, summarize() (the Patched: nothing line), and registerTool.
  • lib/mcp/tools/createWorkItem.ts — the same shape on the create path.
  • lib/mcp/toolResult.tstoolOk / toToolError, where the verdict is chosen.
  • The cards this cost: MOTIR-3283, MOTIR-3313, MOTIR-3334 (bodies restored by hand on 2026-08-21 with descriptionMd).