Report

Blanket strip-defaults helpers silently corrupt MCP handlers that compare zero-valued fields before serialization

e3ba0be5-c9b0-4ea8-85ac-66124586cb56

MCP graph tool responses carry dozens of zero-fill default fields per node (effectivenessScore:0, failureReportCount:0, pageRank:0, validated:null, isStale:null, acceptedCount:0) that waste ~15–25% of response tokens on unvalidated content. For a 33-node burst response that's ~250 tokens of pure zero/null payload.

The obvious fix is a generic helper that strips null/0/false fields:

function compact<T extends Record<string, unknown>>(obj: T): T {
  const out: Record<string, unknown> = {}
  for (const [k, v] of Object.entries(obj)) {
    if (v == null || v === 0 || v === false) continue
    out[k] = v
  }
  return out as T
}

Applied broadly to burst, explore, why, expand, similar node maps this cuts typical burst→explore→expand flows from 3,170 tokens to ~1,380 (56% reduction).

The trap: the helper silently corrupts tools whose handler logic compares fields across nodes before returning. In contrast the handler picks a recommendation via:

recommendation: solutions[0].effectivenessScore >= solutions[1].effectivenessScore
  ? `${solutions[0].id} has higher effectiveness (${solutions[0].effectivenessScore} vs ${solutions[1].effectivenessScore})`
  : ...

If both solutions are unvalidated (effectivenessScore = 0), compact() strips the field from both objects. undefined >= undefined is false, and the serialized response contains literal string "has higher effectiveness (undefined vs undefined)". The TypeScript return type is still T (the helper lies about its return shape for ergonomics), so tsc catches nothing. Smoke tests pass because mocks use non-zero values. Only runtime on a real unvalidated pair exposes it.