Skip to content

fix(mcp): make axme_finalize_close required-field errors agent-actionable#145

Merged
George-iam merged 1 commit into
mainfrom
fix/finalize-close-required-field-errors-20260603
Jun 3, 2026
Merged

fix(mcp): make axme_finalize_close required-field errors agent-actionable#145
George-iam merged 1 commit into
mainfrom
fix/finalize-close-required-field-errors-20260603

Conversation

@George-iam
Copy link
Copy Markdown
Contributor

Summary

When an agent calls axme_finalize_close and omits any of the six required handoff strings (stopped_at, summary, in_progress, next_steps, worklog_entry, startup_text), Zod emits per-field \"Expected string, received undefined\" errors. Multiple agents have mis-read this as a per-field server bug rather than "you forgot to pass these arguments."

Most recent occurrence: another session's agent ran into the error, gave up after 4 retries with different content in the two fields it was already passing, and reported it as a server-side parser bug affecting four specific long-string fields.

Diagnosis

The schema is symmetric — all six handoff strings are plain z.string() with .describe(), no per-field preprocess, transform, or size limit. The error shape is real (Zod genuinely returns four errors when four required fields are missing), but the root cause is the missing payload, not server-side handling.

axme_begin_close's checklist output listed the six fields without marking them required, and the schema's .describe() text did not signal that omitting was an error. So the call-site contract was ambiguous → agent omitted → Zod fired multiple errors → agent inferred wrong cause.

Fix

Two surface-level changes, no behavior change for any valid call:

1. Actionable Zod messages with .min(1)

Each of the six required strings now uses .min(1, \"...\") with a custom message that:

  • Names the field by name (\"in_progress is REQUIRED — ...\").
  • Marks it explicit REQUIRED.
  • Suggests an empty-state placeholder so the agent can satisfy the contract when the session legitimately has nothing to report (\"(nothing in progress)\", \"(none — work is complete)\", \"- (no items)\").

Example new message:

in_progress is REQUIRED — pass branches / PRs / uncommitted work, or '(nothing in progress)' if the working tree is clean. Do not omit the field.

2. Mark required vs optional in axme_begin_close checklist

The handoff section in the checklist output now splits into two blocks:

  • Required (six fields) — with the omit-is-error warning and per-field placeholder examples.
  • Optionalprs, test_results, blockers, dirty_branches.

Each .describe() text also leads with [REQUIRED] or [optional] so the schema-rendered tool docs match the checklist.

What this fixes

Before After
Expected string, received undefined (4×) in_progress is REQUIRED — pass branches / PRs / uncommitted work, or '(nothing in progress)' if the working tree is clean. (per field)
Checklist: "in_progress: current state (branches, PRs, uncommitted work)" Checklist: "in_progress (REQUIRED) — branches, PRs, uncommitted work. Empty placeholder: \"(nothing in progress)\""

Behaviour change

For valid calls (non-empty strings for all six): no change. Same code path, same outputs.

For invalid calls that pass an empty string \"\" for a required field: now rejected with the new actionable message instead of being silently accepted and writing a blank handoff field. This is the correct behavior — a blank handoff field has no value.

For invalid calls that omit a required field: now get a much more useful Zod message naming the field, marking it required, and showing how to satisfy the contract with a placeholder.

Test plan

  • Type-check clean (npx tsc --noEmit)
  • Full unit test suite passes (608/608)
  • Bundle builds (npm run build)
  • Existing successful flows (the worklog has dozens of past successful axme_finalize_close calls) continue to work — all six fields are already populated in those, and .min(1) accepts any non-empty string.

Out of scope

  • Whether to make any of the six fields actually optional. They're load-bearing for handoff quality; an empty placeholder is fine, but skipping them entirely should remain an error.
  • MCP transport-layer size limits on tool arguments (not relevant — the server has none and the error wasn't a truncation, just missing fields).

🤖 Generated with Claude Code

…able

The schema for axme_finalize_close declares 6 handoff strings as
required z.string() (stopped_at, summary, in_progress, next_steps,
worklog_entry, startup_text). When an agent omits any of them Zod
emits "Expected string, received undefined" per missing field — which
has been mis-read as a per-field server bug rather than a missing
argument.

Two changes:

- Add .min(1, "<actionable message>") to all six fields. The message
  names the field, marks it REQUIRED, and gives an empty-state
  placeholder ("(nothing in progress)" / "(none — work is complete)")
  so the agent knows what to pass when there's literally nothing to
  report. No behavior change for valid (non-empty-string) calls.

- Mark each field as [REQUIRED] / [optional] in the .describe() text
  and in the axme_begin_close checklist output. Required block now
  carries an explicit "ALL must be present and non-empty" warning
  with omit-is-error rationale.

608/608 tests pass; type-check clean; bundle builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@George-iam George-iam merged commit e8d94ad into main Jun 3, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant