different-ai/openwork · error

MCP tool result was incomplete.

Error message

MCP tool result was incomplete.

What it means

After a successful (ok) run-tool response, useRunMcpConnectionTool validates that the payload is a record containing string referenceId, numeric durationMs, and a 'result' key. If any of these is missing/mistyped it throws 'MCP tool result was incomplete.' Notably, a missing/unparseable inspection is tolerated, but the core result envelope is mandatory — without referenceId the run cannot be traced or audited.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_components/mcp-connections-data.tsx:385

            typeof payload.message === "string" ? payload.message : "This tool is disabled by organization policy.",
            typeof payload.disabledBy === "string" ? payload.disabledBy : null,
            typeof payload.disabledAt === "string" ? payload.disabledAt : null,
          );
        }
        const requestError = getRequestError(payload, response, `Failed to run MCP tool (${response.status}).`);
        throw new ExternalMcpToolRunError(
          requestError.message,
          isRecord(payload) ? parseToolCallInspection(payload.inspection) : null,
          isRecord(payload) ? parseExternalMcpDiagnostic(payload.diagnostic) : null,
        );
      }
      if (
        !isRecord(payload)
        || typeof payload.referenceId !== "string"
        || typeof payload.durationMs !== "number"
        || !("result" in payload)
      ) {
        throw new Error("MCP tool result was incomplete.");
      }
      return {
        referenceId: payload.referenceId,
        durationMs: payload.durationMs,
        result: payload.result,
        // A missing or unparseable inspection must not fail a tool run that
        // succeeded (for example across a den-api/den-web deploy skew); the
        // runner simply renders without the inspector panel.
        inspection: parseToolCallInspection(payload.inspection),
      };
    },
  });
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Log the raw response body and compare against the expected { referenceId: string, durationMs: number, result: unknown } envelope
  2. Fix the server route to always return referenceId, durationMs, and result on success
  3. If durationMs is sent as a string, normalize it server-side (or coerce client-side)
  4. Ensure no proxy/middleware truncates or reshapes large JSON responses

Example fix

// before
if (!isRecord(payload) || typeof payload.referenceId !== "string" || typeof payload.durationMs !== "number" || !("result" in payload)) {
  throw new Error("MCP tool result was incomplete.");
}
// after
const durationMs = typeof payload?.durationMs === "string" ? Number(payload.durationMs) : payload?.durationMs;
if (!isRecord(payload) || typeof payload.referenceId !== "string" || typeof durationMs !== "number" || Number.isNaN(durationMs) || !("result" in payload)) {
  throw new Error("MCP tool result was incomplete.");
}
Defensive patterns

Strategy: validation

Validate before calling

function isCompleteToolResult(payload: unknown): payload is { referenceId: string; durationMs: number; result: unknown } {
  return isRecord(payload) && typeof payload.referenceId === "string" && typeof payload.durationMs === "number" && "result" in payload;
}

Type guard

function isToolResultEnvelope(v: unknown): v is { referenceId: string; durationMs: number; result: unknown } {
  return isRecord(v) && typeof v.referenceId === "string" && typeof v.durationMs === "number" && "result" in v;
}

Try / catch

try {
  const run = await runTool({ tool, input });
} catch (err) {
  if (err.message === "MCP tool result was incomplete.") {
    // keep referenceId-less runs out of history; offer re-run
  }
}

Prevention

When it happens

Trigger: Run returns 200 with an empty or differently-shaped body (e.g. {output:...} instead of {referenceId,durationMs,result}); durationMs serialized as a string; server bug omitting result on partial failure while still returning ok.

Common situations: den-api version predating the result-envelope contract; gateway rewriting the JSON; tool returning a huge result that got truncated downstream; server returning {ok:true} acknowledgement instead of the full result for async tools.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/4fe310b738b7ccac. Report an issue: GitHub.