Skip to content

fix(mcp): prevent + clarify empty-args failures on axme_save_memory#155

Merged
George-iam merged 1 commit into
mainfrom
fix/save-memory-empty-args-guidance-20260621
Jun 21, 2026
Merged

fix(mcp): prevent + clarify empty-args failures on axme_save_memory#155
George-iam merged 1 commit into
mainfrom
fix/save-memory-empty-args-guidance-20260621

Conversation

@George-iam

Copy link
Copy Markdown
Contributor

Summary

Two separate agent sessions hit axme_save_memory failing with "expected string, received undefined" on every required field — the args object arrived empty {}. One agent retried 9× and filed a server-bug report; another did a controlled root-cause writeup (SAVE_MEMORY_ARG_REPORT.md).

Verified it is NOT a server/handler/schema defect:

  • save_memory and save_decision handlers read args identically (server.tool(name, desc, schema, async (args) => …)). No per-tool routing difference — disproves the "args not forwarded for this tool" hypothesis.
  • The zod schema is correct; every call whose args actually reached the server persisted fine. save_decision worked in the same session, same client.
  • The MCP SDK validates against the schema before our handler runs, so an empty {} payload is rejected by the SDK — our code never sees it.

Root cause is client-side: the agent emits the tool-call shell while deferring the heavy free-text fields, and the fill never happens. Two factors make save_memory uniquely prone:

  1. Heaviest required surface among axme tools (enum type + two large free-text fields).
  2. An over-generalized "batch axme calls in parallel" habit inherited from the read-tool instructions (axme_context tells the agent to call oracle/decisions/memories in parallel — that habit bleeds onto the heavy-payload write tool).

Why not "echo received keys"

The report's top hardening idea was to echo received argument keys in the error. But the SDK auto-validates before our handler, so we'd have to loosen the advertised schema and validate manually — which would worsen the root cause by hiding the required fields from the model's tool picker. Dropped. The strict schema is what advertises the required fields in the first place.

Fix (text-only, no schema loosening, no behavior change for valid calls)

A. Custom zod v4 { error } messages on the required fields of save_memory (type/title/description) and save_decision (title/decision/reasoning). Instead of the bland "received undefined" that got misread as "the server lost my args", each now reads e.g.:

description is REQUIRED — compose it in THIS call (1-2 sentences, self-contained). An empty axme_save_memory call is the usual cause of this error: re-send with all three required fields.

B. Hardened tool descriptions: explicit "call standalone, not in a parallel batch; include all required fields in the same call" + a worked example arguments object for save_memory.

C. Clarified server instructions: the axme_context-start guidance now says parallelism is for the READ tools (oracle/decisions/memories) only, and a new SAVE-TOOL RULE states save_memory/save_decision/update_safety are called one at a time with all required fields composed in the same call.

Test plan

  • zod v4.3.6 { error } messages verified to surface per-field on empty {} (simulated the SDK's z.object(shape).safeParse({})).
  • Valid call {type, title, description} still parses unchanged.
  • npx tsc --noEmit clean.
  • npm test — 613/613 (one flaky concurrent-lock/live-PR subtest passed on rerun; unrelated to this change).
  • npm run build clean.

Effect

A future agent that emits an empty save_memory call now gets a first-try, self-correcting message naming each field and telling it to compose them in the same standalone call — instead of a 9×-retry loop ending in a false server-bug report.

🤖 Generated with Claude Code

Two agent sessions independently hit axme_save_memory failing with
"expected string, received undefined" for every required field — the
args object arrived empty {}. Controlled testing (see
SAVE_MEMORY_ARG_REPORT.md) confirmed it is NOT a server/handler/schema
defect: every call whose args actually reached the server persisted
fine, and save_decision worked in the same session. Root cause is a
client-side generative slip — the agent emits the tool-call shell while
deferring the heavy free-text fields, and the fill never happens. Two
factors make save_memory uniquely prone: heaviest required surface
among the axme tools, and an over-generalized "batch axme calls in
parallel" habit inherited from the read-tool instructions.

The SDK validates against the zod schema BEFORE our handler runs, so an
empty payload never reaches our code — we cannot echo received keys
without loosening the advertised schema (which would worsen the root
cause by hiding the required fields from the model). So the fixes are
all on the instruction/description surface, which is what actually
steers generation, plus better error text:

A. Custom zod v4 { error } messages on the required fields of
   save_memory (type/title/description) and save_decision
   (title/decision/reasoning). Instead of a bland "received undefined"
   (which the first agent misread as "the server lost my arguments"
   and retried 9x before filing a server-bug report), each field now
   says it is REQUIRED, must be composed in THIS call, and that an
   empty/deferred emission is the usual cause.
B. Hardened tool descriptions: explicit "call standalone, not in a
   parallel batch; include all required fields in the same call" plus
   a worked example object for save_memory.
C. Clarified server instructions: parallelism is for the READ tools
   (oracle/decisions/memories) only; a new SAVE-TOOL RULE states that
   save_memory/save_decision/update_safety are called one at a time
   with all required fields composed in that same call.

No schema loosening, no behavior change for valid calls (verified:
{type,title,description} still parses; empty {} now returns the
actionable messages). Type-check clean; 613/613 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@George-iam George-iam merged commit af47e77 into main Jun 21, 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