feat(agiliton-account): per-customer LiteLLM MCP server provisioning
At first login, provisionMcpServer() creates a sitebridge-{customer_id}
entry in LiteLLM DB via POST /v1/mcp/server, pointing to the customer's
demux URL. The virtual key is then scoped to ["sitebridge-{customer_id}"]
so LiteLLM routes tool calls only to that customer's WebSocket.
Also adds AGILITON_ACCOUNT_URL config for self-referencing in MCP URLs.
CF-3032
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,10 @@ JWT_EXPIRES_IN=7d
|
|||||||
# AES-256-GCM encryption for LiteLLM virtual keys (64 hex chars = 32 bytes)
|
# AES-256-GCM encryption for LiteLLM virtual keys (64 hex chars = 32 bytes)
|
||||||
ENCRYPTION_KEY=changeme-generate-with-openssl-rand-hex-32
|
ENCRYPTION_KEY=changeme-generate-with-openssl-rand-hex-32
|
||||||
|
|
||||||
|
# Self-referencing URL used when registering per-customer MCP servers in LiteLLM
|
||||||
|
# LiteLLM calls this URL to forward tool calls to the right customer's WebSocket
|
||||||
|
AGILITON_ACCOUNT_URL=http://agiliton-account:4100
|
||||||
|
|
||||||
# LiteLLM
|
# LiteLLM
|
||||||
LITELLM_URL=http://litellm:4000
|
LITELLM_URL=http://litellm:4000
|
||||||
LITELLM_MASTER_KEY=sk-litellm-master-key
|
LITELLM_MASTER_KEY=sk-litellm-master-key
|
||||||
|
|||||||
@@ -5,14 +5,67 @@ interface LiteLLMKeyResponse {
|
|||||||
key_alias: string;
|
key_alias: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LiteLLMMCPServerResponse {
|
||||||
|
server_id: string;
|
||||||
|
server_name: string;
|
||||||
|
alias: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a per-customer sitebridge MCP server entry in LiteLLM DB.
|
||||||
|
* Returns the server_id (alias) used when provisioning the virtual key.
|
||||||
|
*
|
||||||
|
* LiteLLM does not support URL templates per virtual key (verified in 1.83.3),
|
||||||
|
* so we create one MCP server entry per customer with the customer-specific
|
||||||
|
* demux URL. The virtual key is then scoped to only that server.
|
||||||
|
*/
|
||||||
|
export async function provisionMcpServer(customerId: string): Promise<string> {
|
||||||
|
const serverAlias = `sitebridge-${customerId}`;
|
||||||
|
const demuxUrl = `${config.agilitonAccountUrl}/mcp/demux/${customerId}/mcp`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
server_name: serverAlias,
|
||||||
|
alias: serverAlias,
|
||||||
|
description: `SiteBridge MCP bridge for customer ${customerId}`,
|
||||||
|
url: demuxUrl,
|
||||||
|
transport: "http",
|
||||||
|
auth_type: "bearer_token",
|
||||||
|
auth_value: config.mcpBridgeSecret,
|
||||||
|
allow_all_keys: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${config.litellmUrl}/v1/mcp/server`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${config.litellmMasterKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`LiteLLM /v1/mcp/server failed ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverAlias;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provision a new virtual key for a customer. Called once at first login.
|
* Provision a new virtual key for a customer. Called once at first login.
|
||||||
|
* Also creates a per-customer MCP server entry so LiteLLM routes sitebridge
|
||||||
|
* calls to the correct agiliton-account demux endpoint.
|
||||||
*/
|
*/
|
||||||
export async function provisionLiteLLMKey(customerId: string, email: string): Promise<string> {
|
export async function provisionLiteLLMKey(customerId: string, email: string): Promise<string> {
|
||||||
|
// 1. Register per-customer MCP server
|
||||||
|
const mcpServerAlias = await provisionMcpServer(customerId);
|
||||||
|
|
||||||
|
// 2. Create virtual key scoped to that MCP server
|
||||||
const body = {
|
const body = {
|
||||||
key_alias: `sb-${customerId}`,
|
key_alias: `sb-${customerId}`,
|
||||||
models: config.defaultModels,
|
models: config.defaultModels,
|
||||||
mcp_servers: ["sitebridge"],
|
mcp_servers: [mcpServerAlias],
|
||||||
max_budget: config.defaultBudgetUsd,
|
max_budget: config.defaultBudgetUsd,
|
||||||
budget_duration: config.defaultBudgetDuration,
|
budget_duration: config.defaultBudgetDuration,
|
||||||
rpm_limit: config.defaultRpmLimit,
|
rpm_limit: config.defaultRpmLimit,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const config = {
|
|||||||
litellmUrl: envOptional("LITELLM_URL", "http://litellm:4000"),
|
litellmUrl: envOptional("LITELLM_URL", "http://litellm:4000"),
|
||||||
litellmMasterKey: env("LITELLM_MASTER_KEY"),
|
litellmMasterKey: env("LITELLM_MASTER_KEY"),
|
||||||
|
|
||||||
|
// Self-referencing URL for MCP demux (used in LiteLLM MCP server registration)
|
||||||
|
agilitonAccountUrl: envOptional("AGILITON_ACCOUNT_URL", "http://agiliton-account:4100"),
|
||||||
|
|
||||||
// Shared secret used by LiteLLM when calling /mcp/demux/:id/mcp
|
// Shared secret used by LiteLLM when calling /mcp/demux/:id/mcp
|
||||||
mcpBridgeSecret: env("MCP_BRIDGE_SECRET"),
|
mcpBridgeSecret: env("MCP_BRIDGE_SECRET"),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user