jamiepine/voicebox · warning · HTTPException

Binding not found

Error message

Binding not found

What it means

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.

Source

Thrown at backend/routes/mcp_bindings.py:76

    row.default_personality = data.default_personality
    row.updated_at = datetime.now(timezone.utc)
    db.commit()
    db.refresh(row)
    return models.MCPClientBindingResponse.model_validate(row)


@router.delete("/mcp/bindings/{client_id}")
async def delete_mcp_binding(
    client_id: str,
    db: Session = Depends(get_db),
):
    row = (
        db.query(MCPClientBinding)
        .filter(MCPClientBinding.client_id == client_id)
        .first()
    )
    if row is None:
        raise HTTPException(status_code=404, detail="Binding not found")
    db.delete(row)
    db.commit()
    return {"deleted": client_id}

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Treat 404 as success if your goal is 'ensure this binding is gone' (the end state is what you wanted).
  2. Before deleting, GET /mcp/bindings and confirm the client_id is present.
  3. Verify the exact client_id string (case-sensitive) matches what was used in the PUT that created it.
  4. Guard the UI against double-submit (disable the button after the first request).

Example fix

// before
await api.delete(`/mcp/bindings/${clientId}`)  // throws on 404
// after
const res = await api.delete(`/mcp/bindings/${clientId}`).catch(e => e.response)
if (res?.status === 404) { /* already gone — fine */ }
Defensive patterns

Strategy: validation

Validate before calling

async function deleteBinding(clientId: string) {
  const list = await (await fetch('/mcp/bindings')).json();
  const exists = list.items.some(b => b.client_id === clientId);
  if (!exists) return { deleted: clientId, alreadyGone: true }; // treat as success
  const res = await fetch(`/mcp/bindings/${encodeURIComponent(clientId)}`, {method:'DELETE'});
  if (!res.ok && res.status !== 404) throw new Error(`delete failed: ${res.status}`);
  return { deleted: clientId };
}

Try / catch

try {
  await api.delete(`/mcp/bindings/${clientId}`);
} catch (e) {
  if (e.response?.status !== 404) throw e;
  // 404 is the desired end state — binding is gone
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/8c9c5e9e940f41e6. Report an issue: GitHub.