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
- 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).
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
- 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.
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.