{"record":{"id":"8c9c5e9e940f41e6","repo":"jamiepine/voicebox","slug":"binding-not-found","errorCode":null,"errorMessage":"Binding not found","messagePattern":"Binding not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"backend/routes/mcp_bindings.py","lineNumber":76,"sourceCode":"    row.default_personality = data.default_personality\n    row.updated_at = datetime.now(timezone.utc)\n    db.commit()\n    db.refresh(row)\n    return models.MCPClientBindingResponse.model_validate(row)\n\n\n@router.delete(\"/mcp/bindings/{client_id}\")\nasync def delete_mcp_binding(\n    client_id: str,\n    db: Session = Depends(get_db),\n):\n    row = (\n        db.query(MCPClientBinding)\n        .filter(MCPClientBinding.client_id == client_id)\n        .first()\n    )\n    if row is None:\n        raise HTTPException(status_code=404, detail=\"Binding not found\")\n    db.delete(row)\n    db.commit()\n    return {\"deleted\": client_id}\n","sourceCodeStart":58,"sourceCodeEnd":80,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/mcp_bindings.py#L58-L80","documentation":"404 from DELETE /mcp/bindings/{client_id}. The route queries MCPClientBinding by client_id; if no row matches (the client was never bound, or was already deleted) it raises HTTPException(404, 'Binding not found'). client_id is the same value the MCP client sends in X-Voicebox-Client-Id or the stdio shim reads from VOICEBOX_CLIENT_ID.","triggerScenarios":"DELETE /mcp/bindings/ClaudeCode when no row exists for that client_id; calling delete twice (idempotency not implemented — the second call 404s); passing a client_id with different casing/whitespace than the one stored at PUT /mcp/bindings.","commonSituations":"UI delete fired twice on a quick double-click; client_id mismatch after renaming an MCP client; stale frontend state holding a binding id that a coworker already removed; race between two browser tabs editing bindings.","solutions":["Treat 404 as success if your goal is 'ensure this binding is gone' (the end state is what you wanted).","Before deleting, GET /mcp/bindings and confirm the client_id is present.","Verify the exact client_id string (case-sensitive) matches what was used in the PUT that created it.","Guard the UI against double-submit (disable the button after the first request)."],"exampleFix":"// before\nawait api.delete(`/mcp/bindings/${clientId}`)  // throws on 404\n// after\nconst res = await api.delete(`/mcp/bindings/${clientId}`).catch(e => e.response)\nif (res?.status === 404) { /* already gone — fine */ }","handlingStrategy":"validation","validationCode":"async function deleteBinding(clientId: string) {\n  const list = await (await fetch('/mcp/bindings')).json();\n  const exists = list.items.some(b => b.client_id === clientId);\n  if (!exists) return { deleted: clientId, alreadyGone: true }; // treat as success\n  const res = await fetch(`/mcp/bindings/${encodeURIComponent(clientId)}`, {method:'DELETE'});\n  if (!res.ok && res.status !== 404) throw new Error(`delete failed: ${res.status}`);\n  return { deleted: clientId };\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.delete(`/mcp/bindings/${clientId}`);\n} catch (e) {\n  if (e.response?.status !== 404) throw e;\n  // 404 is the desired end state — binding is gone\n}","preventionTips":["GET /mcp/bindings before showing delete buttons; only show bindings that exist.","Make delete idempotent in the client by swallowing 404.","Match client_id exactly (case-sensitive) — it's the key the server queries on."],"tags":["mcp","bindings","http-404","idempotency","fastapi"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}