JuliusBrussee/caveman · error · TypeError

Recovery requires its scoped handle

Error message

Recovery requires its scoped handle

What it means

The recovery tool executed via MCP requires the caller's model/tool-call to pass a `handle` string argument — the scoped handle issued by Caveman (e.g. a ccr_ handle). The execute callback in packages/middleware/typescript/src/mcp.ts:37 throws this TypeError when the tool arguments omit `handle` or pass a non-string value, because the underlying binding cannot resolve compressed context without it.

Solutions

  1. Ensure the tool inputSchema marks `handle` (recovery_handle) as required so the model and host enforce it.
  2. Validate/extract the handle in your tool-call handler before invoking the recovery tool.
  3. If the handle is missing at runtime, re-issue retrieval with a fresh scope so a new handle is generated.

Example fix

// before
await recoveryTool.execute({ query: 'summarize' });
// after
await recoveryTool.execute({ handle: 'ccr_abc123', query: 'summarize' });
Defensive patterns

Strategy: validation

Validate before calling

function hasHandle(args: unknown): args is { handle: string; [k: string]: unknown } {
  return typeof args === 'object' && args !== null && typeof (args as any).handle === 'string' && (args as any).handle.length > 0;
}

Type guard

const isRetrieveArgs = (a: unknown): a is { handle: string; query?: string } =>
  typeof a === 'object' && a !== null && typeof (a as any).handle === 'string';

Try / catch

try {
  const page = await recovery.execute(args, { signal });
} catch (e) {
  if (e instanceof TypeError && /scoped handle/.test(e.message)) {
    return { isError: true, content: [{ type: 'text', text: 'A recovery handle is required; use the ccr_ handle from the prior result.' }] };
  }
  throw e;
}

Prevention

When it happens

Trigger: An LLM tool call to the recovery tool with arguments lacking `handle` (or containing a non-string, e.g. a number or object), passed into `this.binding.execute`.

Common situations: A model hallucinating tool arguments and skipping the handle; a host schema not enforcing the required `recovery_handle`/`handle` property; hand-crafted tool invocation in tests.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/aa07f43f67186758. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/typescript/src/mcp.ts:37

/** Native host views only. No MCP framing, server or inference loop is added. */
export class CavemanMCPHost {
  readonly recovery: MCPToolBinding;
  private readonly binding;
  private readonly adapter;
  private readonly recoveryExecutor;
  private readonly recoveryDefinition;
  private readonly versionSupported;
  constructor(private readonly options: MCPHostOptions) {
    if (!options.serverId || !options.protocolVersion) throw new TypeError('Provide the native server identity and negotiated MCP protocol version');
    this.versionSupported = adapterCompatible('mcp');
    if (!this.versionSupported && options.runtime.mode !== 'off') options.runtime.decline('unsupported_version');
    this.binding = options.runtime.recovery(options.scope);
    this.adapter = { id: 'mcp', version: '0.1.0', framework_version: frameworkVersion('@modelcontextprotocol/sdk') ?? 'unknown', serialization_revision: `mcp-native-${options.protocolVersion}-v1` };
    this.recovery = {
      tool: { name: this.binding.name, description: this.binding.description, inputSchema: structuredClone(this.binding.inputSchema) as Tool['inputSchema'] },
      execute: async (arguments_, options) => {
        if (typeof arguments_.handle !== 'string') throw new TypeError('Recovery requires its scoped handle');
        const page = await this.binding.execute({ ...arguments_, handle: arguments_.handle }, options?.signal ? { signal: options.signal } : undefined);
        return { content: [{ type: 'text', text: JSON.stringify(page) }] };
      },
    };
    this.recoveryExecutor = this.recovery.execute;
    this.recoveryDefinition = JSON.stringify(this.recovery.tool);
  }

  register(tools: readonly MCPToolBinding[]): MCPToolBinding[] {
    return !this.versionSupported || this.options.runtime.mode !== 'compress' || tools.some(item => item.tool.name === this.recovery.tool.name) ? [...tools] : [...tools, this.recovery];
  }

  /** Persist the original result; pass only this copied view to the model.
   * The host supplies its original append-only context manifest across resumes.
   * Final provider serialization and dispatch are outside this result boundary. */
  async projectResult(result: CallToolResult, options: {
    tool: Tool; callId: string; contextManifest: readonly ManifestItem[];
    registeredTools?: readonly MCPToolBinding[]; sequence?: number; signal?: AbortSignal;

View on GitHub (pinned to 3ee70a1026)