MP-301a · Module 3

Timeout Handling

3 min read

Every MCP tool call has an implicit timeout — the client will not wait forever. Claude Code uses a 60-second default; other clients may be shorter. If your tool regularly takes 45 seconds, you are one slow database query away from a timeout that returns nothing to the LLM. The fix is not to increase the timeout — it is to design your tool to return something useful within the budget. Set an internal timeout shorter than the client timeout (e.g., 30 seconds for a 60-second client), and use the remaining time to package a partial result.

Timeout strategies follow a hierarchy. First, prevent timeouts by making tools fast — index your queries, cache frequently accessed data, set aggressive timeouts on external API calls. Second, detect timeouts early by tracking elapsed time inside long-running operations and bailing out when you approach the budget. Third, return partial results when full results are not achievable within the budget — "here are the first 50 results, 200 more are available, call with offset=50 to continue."

// Timeout-aware search that returns partial results
async function searchWithBudget(
  query: string,
  sources: string[],
  budgetMs: number = 25_000,
) {
  const deadline = Date.now() + budgetMs;
  const results: SearchResult[] = [];
  const skipped: string[] = [];

  for (const source of sources) {
    const remaining = deadline - Date.now();
    if (remaining < 2000) {
      // Not enough time — record skipped sources
      skipped.push(source);
      continue;
    }

    try {
      const result = await Promise.race([
        searchSource(source, query),
        rejectAfter(Math.min(remaining - 1000, 5000)),
      ]);
      results.push(result);
    } catch {
      skipped.push(source);
    }
  }

  return {
    content: [{ type: "text" as const, text: JSON.stringify({
      results,
      complete: skipped.length === 0,
      skippedSources: skipped,
      hint: skipped.length > 0
        ? `${skipped.length} sources skipped due to time budget. Call search_source for individual results.`
        : undefined,
    }, null, 2) }],
  };
}

Do This

  • Set internal timeouts shorter than the client timeout to leave room for response packaging
  • Return partial results with metadata about what was completed and what was skipped
  • Include a hint in partial results telling the LLM how to get the remaining data
  • Track elapsed time inside loops and bail out before the budget expires

Avoid This

  • Let external API calls run without individual timeouts — one slow call blocks everything
  • Return an empty error on timeout without explaining what happened
  • Assume the client timeout is infinite — every client has a limit
  • Retry the entire operation on timeout — the same slow path will time out again