{"record":{"id":"aa07f43f67186758","repo":"JuliusBrussee/caveman","slug":"recovery-requires-its-scoped-handle","errorCode":null,"errorMessage":"Recovery requires its scoped handle","messagePattern":"Recovery requires its scoped handle","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/typescript/src/mcp.ts","lineNumber":37,"sourceCode":"\n/** Native host views only. No MCP framing, server or inference loop is added. */\nexport class CavemanMCPHost {\n  readonly recovery: MCPToolBinding;\n  private readonly binding;\n  private readonly adapter;\n  private readonly recoveryExecutor;\n  private readonly recoveryDefinition;\n  private readonly versionSupported;\n  constructor(private readonly options: MCPHostOptions) {\n    if (!options.serverId || !options.protocolVersion) throw new TypeError('Provide the native server identity and negotiated MCP protocol version');\n    this.versionSupported = adapterCompatible('mcp');\n    if (!this.versionSupported && options.runtime.mode !== 'off') options.runtime.decline('unsupported_version');\n    this.binding = options.runtime.recovery(options.scope);\n    this.adapter = { id: 'mcp', version: '0.1.0', framework_version: frameworkVersion('@modelcontextprotocol/sdk') ?? 'unknown', serialization_revision: `mcp-native-${options.protocolVersion}-v1` };\n    this.recovery = {\n      tool: { name: this.binding.name, description: this.binding.description, inputSchema: structuredClone(this.binding.inputSchema) as Tool['inputSchema'] },\n      execute: async (arguments_, options) => {\n        if (typeof arguments_.handle !== 'string') throw new TypeError('Recovery requires its scoped handle');\n        const page = await this.binding.execute({ ...arguments_, handle: arguments_.handle }, options?.signal ? { signal: options.signal } : undefined);\n        return { content: [{ type: 'text', text: JSON.stringify(page) }] };\n      },\n    };\n    this.recoveryExecutor = this.recovery.execute;\n    this.recoveryDefinition = JSON.stringify(this.recovery.tool);\n  }\n\n  register(tools: readonly MCPToolBinding[]): MCPToolBinding[] {\n    return !this.versionSupported || this.options.runtime.mode !== 'compress' || tools.some(item => item.tool.name === this.recovery.tool.name) ? [...tools] : [...tools, this.recovery];\n  }\n\n  /** Persist the original result; pass only this copied view to the model.\n   * The host supplies its original append-only context manifest across resumes.\n   * Final provider serialization and dispatch are outside this result boundary. */\n  async projectResult(result: CallToolResult, options: {\n    tool: Tool; callId: string; contextManifest: readonly ManifestItem[];\n    registeredTools?: readonly MCPToolBinding[]; sequence?: number; signal?: AbortSignal;","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/typescript/src/mcp.ts#L19-L55","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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.","solutions":["Ensure the tool inputSchema marks `handle` (recovery_handle) as required so the model and host enforce it.","Validate/extract the handle in your tool-call handler before invoking the recovery tool.","If the handle is missing at runtime, re-issue retrieval with a fresh scope so a new handle is generated."],"exampleFix":"// before\nawait recoveryTool.execute({ query: 'summarize' });\n// after\nawait recoveryTool.execute({ handle: 'ccr_abc123', query: 'summarize' });","handlingStrategy":"validation","validationCode":"function hasHandle(args: unknown): args is { handle: string; [k: string]: unknown } {\n  return typeof args === 'object' && args !== null && typeof (args as any).handle === 'string' && (args as any).handle.length > 0;\n}","typeGuard":"const isRetrieveArgs = (a: unknown): a is { handle: string; query?: string } =>\n  typeof a === 'object' && a !== null && typeof (a as any).handle === 'string';","tryCatchPattern":"try {\n  const page = await recovery.execute(args, { signal });\n} catch (e) {\n  if (e instanceof TypeError && /scoped handle/.test(e.message)) {\n    return { isError: true, content: [{ type: 'text', text: 'A recovery handle is required; use the ccr_ handle from the prior result.' }] };\n  }\n  throw e;\n}","preventionTips":["Keep the tool inputSchema with handle marked required so hosts and models enforce it.","Surface handle usage instructions in the tool description so models pass it correctly.","Log tool-call arguments in dev to catch models omitting the handle."],"tags":["mcp","tool-call","missing-argument","validation"],"backgroundTag":"missing-required-argument","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}