From 74de6d37ffd7b52a7118be75011c0496938389e6 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Wed, 15 Apr 2026 15:42:32 +0300 Subject: [PATCH] fix(litellm): idempotent MCP server provisioning + self-heal on login (SB-56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provisionMcpServer now GETs /v1/mcp/server first and skips the POST if the sitebridge_ entry already exists. upsertCustomer calls it on every existing-customer login (fire-and-forget) so pre-existing customers whose row was created before this code path shipped — or whose LiteLLM entry was deleted/rebuilt — get their /mcp/ routing restored on next login. Co-Authored-By: Claude Opus 4.6 --- src/auth/customers.ts | 10 +++++++++- src/auth/litellm.ts | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/auth/customers.ts b/src/auth/customers.ts index 412f0a9..6dbfb1c 100644 --- a/src/auth/customers.ts +++ b/src/auth/customers.ts @@ -1,6 +1,6 @@ import { query, queryOne } from "../db/pool.js"; import { encrypt, decrypt } from "../utils/crypto.js"; -import { provisionLiteLLMKey } from "./litellm.js"; +import { provisionLiteLLMKey, provisionMcpServer } from "./litellm.js"; export interface Customer { id: string; @@ -67,6 +67,14 @@ export async function upsertCustomer(params: { ); existing.last_login_at = new Date(); existing.name = params.name; + + // Self-heal: ensure LiteLLM MCP server entry exists for this customer. + // Idempotent in provisionMcpServer; swallow errors so a LiteLLM hiccup + // doesn't block login. + provisionMcpServer(existing.id).catch((err) => { + console.error(`[customers] provisionMcpServer self-heal failed for ${existing.id}:`, err); + }); + return { customer: existing, virtualKey: getVirtualKey(existing), isNew: false }; } diff --git a/src/auth/litellm.ts b/src/auth/litellm.ts index 6556b13..29a53b2 100644 --- a/src/auth/litellm.ts +++ b/src/auth/litellm.ts @@ -25,8 +25,26 @@ function toServerAlias(customerId: string): string { return `sitebridge_${customerId.replace(/-/g, "_")}`; } +async function getMcpServer(serverAlias: string): Promise { + const res = await fetch(`${config.litellmUrl}/v1/mcp/server`, { + headers: { Authorization: `Bearer ${config.litellmMasterKey}` }, + }); + if (!res.ok) throw new Error(`LiteLLM /v1/mcp/server GET failed ${res.status}`); + const data = (await res.json()) as unknown; + const list = (Array.isArray(data) + ? data + : ((data as { data?: unknown; servers?: unknown }).data ?? + (data as { servers?: unknown }).servers ?? + [])) as LiteLLMMCPServerResponse[]; + return list.find((s) => s.server_name === serverAlias || s.alias === serverAlias) ?? null; +} + export async function provisionMcpServer(customerId: string): Promise { const serverAlias = toServerAlias(customerId); + + const existing = await getMcpServer(serverAlias); + if (existing) return serverAlias; + const demuxUrl = `${config.agilitonAccountUrl}/mcp/demux/${customerId}/mcp`; const body = {