chore(ui): satisfy frontend-lint gate on touched MCP files

The frontend-lint job (added on the base branch after this branch diverged)
runs prettier and eslint on the UI files a PR touches, measuring eslint errors
against the committed eslint-suppressions.json baseline. Pulling the base in
brings that gate, its config, and the baseline.

Format the touched MCP env var components and networking.tsx so they are
prettier-clean, and extend the suppressions baseline to cover the findings this
branch adds in files that already carry grandfathered entries: the four extra
raw fetch wrappers in networking.tsx (the API layer, where 270 raw fetches are
already grandfathered and there is no React Query alternative) and the
setState-in-effect findings in mcp_servers.tsx and UserEnvVarsModal.tsx, matching
the same rule already baselined in the sibling MCP components.
This commit is contained in:
mateo-berri 2026-06-04 15:19:14 +00:00
parent fe20b4b904
commit 63a9f00103
No known key found for this signature in database
10 changed files with 303 additions and 431 deletions

View file

@ -54,9 +54,7 @@ test.describe("MCP Servers", () => {
// the MCP servers grid so the form modal's `server_name` input — which
// still holds the timestamped value during its close animation — can't
// satisfy the assertion before the server actually lands in the list.
await expect(page.getByText("MCP Server created successfully").first())
.toBeVisible({ timeout: 15_000 });
await expect(page.getByTestId("mcp-servers-grid").getByText(uniqueName).first())
.toBeVisible({ timeout: 10_000 });
await expect(page.getByText("MCP Server created successfully").first()).toBeVisible({ timeout: 15_000 });
await expect(page.getByTestId("mcp-servers-grid").getByText(uniqueName).first()).toBeVisible({ timeout: 10_000 });
});
});

View file

@ -1307,6 +1307,11 @@
"count": 1
}
},
"src/components/mcp_tools/UserEnvVarsModal.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/components/mcp_tools/create_mcp_server.tsx": {
"no-restricted-imports": {
"count": 1
@ -1377,7 +1382,7 @@
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 2
"count": 4
}
},
"src/components/mcp_tools/mcp_tool_configuration.tsx": {
@ -1504,7 +1509,7 @@
"count": 23
},
"no-restricted-syntax": {
"count": 270
"count": 274
}
},
"src/components/object_permissions_view.tsx": {

View file

@ -1,10 +1,6 @@
import React from "react";
import { Form, Input, Select, Button, Tooltip, Typography } from "antd";
import {
InfoCircleOutlined,
MinusCircleOutlined,
PlusOutlined,
} from "@ant-design/icons";
import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
const { Text } = Typography;
@ -33,12 +29,12 @@ const EnvVarsSection: React.FC = () => {
<Tooltip
title={
<>
Define variables you can interpolate in Static Headers or
Authentication using <code>{"${VAR_NAME}"}</code>. <br />
Define variables you can interpolate in Static Headers or Authentication using{" "}
<code>{"${VAR_NAME}"}</code>. <br />
<b>Instance</b>: admin-defined value used for every user.
<br />
<b>Per-user</b>: each user supplies their own value (e.g. personal
credentials) via the MCP Gateway dashboard.
<b>Per-user</b>: each user supplies their own value (e.g. personal credentials) via the MCP Gateway
dashboard.
</>
}
>
@ -46,8 +42,7 @@ const EnvVarsSection: React.FC = () => {
</Tooltip>
</div>
<Text className="text-xs text-gray-600 block mb-3">
Reference these in Static Headers or Authentication as{" "}
<code>{"${VAR_NAME}"}</code>. For example:{" "}
Reference these in Static Headers or Authentication as <code>{"${VAR_NAME}"}</code>. For example:{" "}
<code className="bg-white px-1 rounded border border-gray-200">
{"${DB_PROTOCOL}://${CORP_USERNAME}:${CORP_PASSWORD}@${DB_HOSTNAME}"}
</code>
@ -79,10 +74,7 @@ const EnvVarsSection: React.FC = () => {
},
]}
>
<Input
placeholder="e.g. DB_PROTOCOL"
className="rounded-md font-mono"
/>
<Input placeholder="e.g. DB_PROTOCOL" className="rounded-md font-mono" />
</Form.Item>
<div style={{ flex: 1 }}>
<ScopedValueOrDescription name={name} restField={restField} />
@ -96,10 +88,7 @@ const EnvVarsSection: React.FC = () => {
>
<Select options={SCOPE_OPTIONS} />
</Form.Item>
<div
style={{ width: 24, height: 32 }}
className="flex items-center justify-center"
>
<div style={{ width: 24, height: 32 }} className="flex items-center justify-center">
<MinusCircleOutlined
onClick={() => remove(name)}
className="text-gray-500 hover:text-red-500 cursor-pointer"
@ -107,12 +96,7 @@ const EnvVarsSection: React.FC = () => {
</div>
</div>
))}
<Button
type="dashed"
onClick={() => add({ scope: "global" })}
icon={<PlusOutlined />}
block
>
<Button type="dashed" onClick={() => add({ scope: "global" })} icon={<PlusOutlined />} block>
Add Variable
</Button>
</div>

View file

@ -27,10 +27,9 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
const isOAuth2 = watchedAuthType === AUTH_TYPE.OAUTH2;
const isNoneAuth = watchedAuthType === AUTH_TYPE.NONE || watchedAuthType == null;
const watchedExtraHeaders = Form.useWatch("extra_headers", form);
const hasAuthorizationHeader = Array.isArray(watchedExtraHeaders)
&& watchedExtraHeaders.some(
(h) => typeof h === "string" && h.toLowerCase() === "authorization",
);
const hasAuthorizationHeader =
Array.isArray(watchedExtraHeaders) &&
watchedExtraHeaders.some((h) => typeof h === "string" && h.toLowerCase() === "authorization");
// Two distinct, independent opt-ins:
// - delegate_auth_to_upstream: oauth2 servers only (PKCE passthrough —
// bypass LiteLLM admission).
@ -42,10 +41,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
const canEnableOAuthPassthrough = isNoneAuth && hasAuthorizationHeader;
const watchedDelegateAuth = Form.useWatch("delegate_auth_to_upstream", form);
const watchedPublicInternet = Form.useWatch("available_on_public_internet", form);
const showInternalDelegatePkceWarning =
isOAuth2 &&
watchedDelegateAuth === true &&
watchedPublicInternet === false;
const showInternalDelegatePkceWarning = isOAuth2 && watchedDelegateAuth === true && watchedPublicInternet === false;
// Set initial values when mcpServer changes
useEffect(() => {
@ -130,7 +126,9 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
<p className="text-sm text-gray-600 mt-1">Enable if this server should be &quot;public&quot; to all keys.</p>
<p className="text-sm text-gray-600 mt-1">
Enable if this server should be &quot;public&quot; to all keys.
</p>
</div>
<Form.Item
name="allow_all_keys"
@ -150,7 +148,9 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
<p className="text-sm text-gray-600 mt-1">Turn on to restrict access to callers within your internal network only.</p>
<p className="text-sm text-gray-600 mt-1">
Turn on to restrict access to callers within your internal network only.
</p>
</div>
<Form.Item
name="available_on_public_internet"
@ -198,7 +198,8 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
</Tooltip>
</span>
<p className="text-sm text-gray-600 mt-1">
Forward upstream OAuth discovery and 401 challenges so clients negotiate OAuth directly with the upstream MCP server.
Forward upstream OAuth discovery and 401 challenges so clients negotiate OAuth directly with the
upstream MCP server.
</p>
</div>
<Form.Item
@ -313,12 +314,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
className="flex-1"
rules={[{ required: true, message: "Header value is required" }]}
>
<Input
size="large"
allowClear
className="rounded-lg"
placeholder="Header value"
/>
<Input size="large" allowClear className="rounded-lg" placeholder="Header value" />
</Form.Item>
<MinusCircleOutlined
onClick={() => remove(name)}

View file

@ -53,18 +53,14 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
// Logo is sourced exclusively from the admin-set `mcp_info.logo_url`.
const candidateLogo = server.mcp_info?.logo_url ?? undefined;
const [failedLogoUrl, setFailedLogoUrl] = useState<string | null>(null);
const logoUrl =
candidateLogo && failedLogoUrl !== candidateLogo ? candidateLogo : undefined;
const logoUrl = candidateLogo && failedLogoUrl !== candidateLogo ? candidateLogo : undefined;
const transport = server.transport || "http";
const displayTransport =
server.spec_path && transport !== "stdio" ? "openapi" : transport;
const displayTransport = server.spec_path && transport !== "stdio" ? "openapi" : transport;
const authType = server.auth_type || "none";
const status = server.status || "unknown";
const healthTone = HEALTH_TONE[status] ?? HEALTH_TONE.unknown;
const isPublic = server.available_on_public_internet;
const accessGroups = (server.mcp_access_groups ?? []).filter(
(g): g is string => typeof g === "string",
);
const accessGroups = (server.mcp_access_groups ?? []).filter((g): g is string => typeof g === "string");
const missing = missingUserFields ?? [];
const needsAttention = missing.length > 0;
@ -82,8 +78,9 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
let subtitle = "";
let subtitleTooltip = "";
if (transport === "stdio") {
const parts = [server.command, ...(server.args ?? [])]
.filter((p): p is string => typeof p === "string" && p.length > 0);
const parts = [server.command, ...(server.args ?? [])].filter(
(p): p is string => typeof p === "string" && p.length > 0,
);
subtitle = parts.join(" ");
subtitleTooltip = subtitle;
} else if (server.spec_path) {
@ -156,28 +153,19 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
</div>
)}
<div className="min-w-0 flex-1">
<div
className="block w-full truncate text-left font-semibold text-gray-900"
title={name}
>
<div className="block w-full truncate text-left font-semibold text-gray-900" title={name}>
{name}
</div>
<div className="mt-0.5 flex items-center gap-2 text-xs text-gray-500">
{alias && <span className="truncate">{alias}</span>}
{alias && <span className="text-gray-300">·</span>}
<Tooltip title={server.server_id}>
<span className="font-mono text-blue-600">
{server.server_id.slice(0, 7)}
</span>
<span className="font-mono text-blue-600">{server.server_id.slice(0, 7)}</span>
</Tooltip>
</div>
</div>
{menuItems.length > 0 && (
<Dropdown
menu={{ items: menuItems }}
trigger={["click"]}
placement="bottomRight"
>
<Dropdown menu={{ items: menuItems }} trigger={["click"]} placement="bottomRight">
<button
type="button"
onClick={stop}
@ -193,10 +181,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
{subtitle ? (
<Tooltip title={subtitleTooltip}>
<Text
className="truncate font-mono text-xs text-gray-500"
ellipsis
>
<Text className="truncate font-mono text-xs text-gray-500" ellipsis>
{subtitle}
</Text>
</Tooltip>
@ -220,11 +205,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
<Tag className="m-0">{authType}</Tag>
<Tag color={isPublic ? "green" : "orange"} className="m-0">
<span className="inline-flex items-center gap-1">
<span
className={`h-1.5 w-1.5 rounded-full ${
isPublic ? "bg-green-500" : "bg-orange-500"
}`}
/>
<span className={`h-1.5 w-1.5 rounded-full ${isPublic ? "bg-green-500" : "bg-orange-500"}`} />
{isPublic ? "Public" : "Internal"}
</span>
</Tag>
@ -242,12 +223,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
{(server.is_byok || needsAttention) && (
<div className="mt-auto flex flex-col gap-2">
{server.is_byok && (
<ByokRow
connected={!!server.has_user_credential}
onConnect={onByokConnect}
/>
)}
{server.is_byok && <ByokRow connected={!!server.has_user_credential} onConnect={onByokConnect} />}
{needsAttention && (
<div className="flex items-center justify-between gap-2 text-xs">
<Tooltip
@ -320,20 +296,14 @@ const HealthChip: FC<HealthChipProps> = ({
const tooltip = (
<div className="max-w-xs">
<div className="font-semibold mb-1">Health: {status}</div>
{lastCheck && (
<div className="text-xs mb-1">
Last check: {new Date(lastCheck).toLocaleString()}
</div>
)}
{lastCheck && <div className="text-xs mb-1">Last check: {new Date(lastCheck).toLocaleString()}</div>}
{error && (
<div className="text-xs">
<div className="font-medium text-red-300 mb-1">Error</div>
<div className="break-words">{error}</div>
</div>
)}
{!lastCheck && !error && (
<div className="text-xs text-gray-400">No health data</div>
)}
{!lastCheck && !error && <div className="text-xs text-gray-400">No health data</div>}
{onRecheck && <div className="mt-1 text-xs text-gray-300">Click to recheck</div>}
</div>
);

View file

@ -1,10 +1,7 @@
import React, { useEffect, useState } from "react";
import { Modal, Form, Input, Button, Alert, Spin, Tag, Typography } from "antd";
import { MCPServer, MCPUserEnvVarsStatus } from "./types";
import {
getMCPUserEnvVars,
storeMCPUserEnvVars,
} from "../networking";
import { getMCPUserEnvVars, storeMCPUserEnvVars } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
const { Text, Title } = Typography;
@ -24,13 +21,7 @@ interface UserEnvVarsModalProps {
* the admin marked as ``scope=user`` shows up with the admin-supplied
* description as the placeholder.
*/
const UserEnvVarsModal: React.FC<UserEnvVarsModalProps> = ({
server,
open,
accessToken,
onClose,
onSaved,
}) => {
const UserEnvVarsModal: React.FC<UserEnvVarsModalProps> = ({ server, open, accessToken, onClose, onSaved }) => {
const [form] = Form.useForm();
const [status, setStatus] = useState<MCPUserEnvVarsStatus | null>(null);
const [isLoading, setIsLoading] = useState(false);
@ -77,9 +68,7 @@ const UserEnvVarsModal: React.FC<UserEnvVarsModalProps> = ({
if (onSaved) onSaved(saved);
onClose();
} catch (err) {
NotificationsManager.fromBackend(
`Failed to save env vars: ${err instanceof Error ? err.message : String(err)}`,
);
NotificationsManager.fromBackend(`Failed to save env vars: ${err instanceof Error ? err.message : String(err)}`);
} finally {
setIsSaving(false);
}
@ -115,49 +104,31 @@ const UserEnvVarsModal: React.FC<UserEnvVarsModalProps> = ({
<Spin />
</div>
) : required.length === 0 ? (
<Alert
type="info"
showIcon
message="No per-user fields configured for this server."
/>
<Alert type="info" showIcon message="No per-user fields configured for this server." />
) : (
<>
<Text className="text-sm text-gray-600 block">
These values are private to you. Your admin configured this MCP
server to require these per-user credentials. Saved values are
never shown back; leave an already-set field blank to keep it, or
enter a value to set or change it.
These values are private to you. Your admin configured this MCP server to require these per-user
credentials. Saved values are never shown back; leave an already-set field blank to keep it, or enter a
value to set or change it.
</Text>
<Form
form={form}
layout="vertical"
onFinish={handleSave}
disabled={isSaving}
>
<Form form={form} layout="vertical" onFinish={handleSave} disabled={isSaving}>
{required.map((spec) => (
<Form.Item
key={spec.name}
name={spec.name}
label={
<span className="flex items-center gap-2">
<span className="font-mono text-sm font-semibold">
{spec.name}
</span>
<span className="font-mono text-sm font-semibold">{spec.name}</span>
{spec.is_set && <Tag color="green">Set</Tag>}
</span>
}
extra={spec.description || undefined}
rules={
spec.is_set
? undefined
: [{ required: true, message: `${spec.name} is required` }]
}
rules={spec.is_set ? undefined : [{ required: true, message: `${spec.name} is required` }]}
>
<Input.Password
placeholder={
spec.is_set
? "Enter a new value to overwrite"
: spec.description || `Enter your ${spec.name}`
spec.is_set ? "Enter a new value to overwrite" : spec.description || `Enter your ${spec.name}`
}
visibilityToggle
/>

View file

@ -81,12 +81,13 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
const [oauthDocsUrl, setOauthDocsUrl] = useState<string | null>(null);
// Single hook call shared by MCPConnectionStatus and MCPToolConfiguration to avoid duplicate requests.
const { tools, isLoadingTools, toolsError, toolsErrorStackTrace, canFetchTools, fetchTools, clearTools } = useTestMCPConnection({
accessToken,
oauthAccessToken,
formValues,
enabled: true,
});
const { tools, isLoadingTools, toolsError, toolsErrorStackTrace, canFetchTools, fetchTools, clearTools } =
useTestMCPConnection({
accessToken,
oauthAccessToken,
formValues,
enabled: true,
});
const authType = formValues.auth_type as string | undefined;
const shouldShowAuthValueField = authType ? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType) : false;
@ -439,9 +440,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
}
NotificationsManager.success(
isAdmin
? "MCP Server created successfully"
: "MCP Server submitted for admin review"
isAdmin ? "MCP Server created successfully" : "MCP Server submitted for admin review",
);
form.resetFields();
setCostConfig({});
@ -456,7 +455,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
NotificationsManager.fromBackend(
isAdmin ? `Error creating MCP Server: ${reason}` : `Error submitting MCP Server: ${reason}`
isAdmin ? `Error creating MCP Server: ${reason}` : `Error submitting MCP Server: ${reason}`,
);
} finally {
setIsLoading(false);
@ -587,8 +586,8 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
>
{!isAdmin && (
<div className="rounded-md bg-blue-50 border border-blue-200 px-4 py-3 text-sm text-blue-800">
Your submission will be sent for admin review before it becomes active.
{" "}Note: the request must be made with a team-scoped API key.
Your submission will be sent for admin review before it becomes active. Note: the request must be made
with a team-scoped API key.
</div>
)}
<div className="grid grid-cols-1 gap-6">
@ -701,9 +700,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
<OpenAPIFormSection
form={form}
accessToken={isModalVisible ? accessToken : null}
onValuesChange={(updates) =>
setFormValues((prev) => ({ ...prev, ...updates }))
}
onValuesChange={(updates) => setFormValues((prev) => ({ ...prev, ...updates }))}
onKeyToolsChange={setKeyTools}
onLogoUrlChange={setLogoUrl}
onOAuthDocsUrlChange={setOauthDocsUrl}
@ -728,7 +725,10 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
<Switch />
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.is_byok !== cur.is_byok || prev.auth_type !== cur.auth_type}>
<Form.Item
noStyle
shouldUpdate={(prev, cur) => prev.is_byok !== cur.is_byok || prev.auth_type !== cur.auth_type}
>
{({ getFieldValue }) =>
getFieldValue("is_byok") ? (
<>
@ -752,7 +752,10 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
{!getFieldValue("auth_type") && (
<div className="mb-4 p-3 bg-yellow-50 rounded-lg text-sm text-yellow-700 flex items-start gap-2">
<InfoCircleOutlined className="mt-0.5 flex-shrink-0" />
<span>Set the <strong>Authentication Type</strong> below to specify how user keys are sent (e.g., Bearer Token, API Key header).</span>
<span>
Set the <strong>Authentication Type</strong> below to specify how user keys are sent
(e.g., Bearer Token, API Key header).
</span>
</div>
)}
<Form.Item
@ -805,10 +808,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
label: <span className="text-sm font-semibold text-gray-700">Authentication</span>,
children: (
<>
<Form.Item
name="auth_type"
rules={[{ required: true, message: "Please select an auth type" }]}
>
<Form.Item name="auth_type" rules={[{ required: true, message: "Please select an auth type" }]}>
<Select placeholder="Select auth type" className="rounded-lg" size="large">
<Select.Option value="none">None</Select.Option>
<Select.Option value="api_key">API Key</Select.Option>
@ -872,7 +872,12 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
<>
<p className="text-sm text-gray-500 mb-2">
For MCP servers hosted on AWS Bedrock AgentCore.{" "}
<a href="https://docs.litellm.ai/docs/mcp_aws_sigv4" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700">
<a
href="https://docs.litellm.ai/docs/mcp_aws_sigv4"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700"
>
View docs &rarr;
</a>
</p>
@ -925,7 +930,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
validator(_, value) {
const secretKey = getFieldValue(["credentials", "aws_secret_access_key"]);
if (secretKey && !value) {
return Promise.reject(new Error("Access Key ID is required when Secret Access Key is provided"));
return Promise.reject(
new Error("Access Key ID is required when Secret Access Key is provided"),
);
}
return Promise.resolve();
},
@ -953,7 +960,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
validator(_, value) {
const accessKeyId = getFieldValue(["credentials", "aws_access_key_id"]);
if (accessKeyId && !value) {
return Promise.reject(new Error("Secret Access Key is required when Access Key ID is provided"));
return Promise.reject(
new Error("Secret Access Key is required when Access Key ID is provided"),
);
}
return Promise.resolve();
},

View file

@ -599,20 +599,15 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
// configuration is later switched back.
delegate_auth_to_upstream: (() => {
const isOauth2 = restValues.auth_type === AUTH_TYPE.OAUTH2;
return isOauth2
? Boolean(delegateAuthToUpstreamRaw ?? mcpServer.delegate_auth_to_upstream)
: false;
return isOauth2 ? Boolean(delegateAuthToUpstreamRaw ?? mcpServer.delegate_auth_to_upstream) : false;
})(),
// ``oauth_passthrough`` is the dedicated, non-oauth2 opt-in. It is only
// honored for ``auth_type=none`` servers that forward ``Authorization``
// upstream. Kept separate from ``delegate_auth_to_upstream`` so enabling
// pass-through never regresses oauth2 servers. Force false otherwise.
oauth_passthrough: (() => {
const isNoneAuth =
restValues.auth_type === AUTH_TYPE.NONE || restValues.auth_type == null;
const extraHeaders = Array.isArray(restValues.extra_headers)
? restValues.extra_headers
: [];
const isNoneAuth = restValues.auth_type === AUTH_TYPE.NONE || restValues.auth_type == null;
const extraHeaders = Array.isArray(restValues.extra_headers) ? restValues.extra_headers : [];
const hasAuthorizationHeader = extraHeaders.some(
(h: unknown) => typeof h === "string" && h.toLowerCase() === "authorization",
);

View file

@ -38,11 +38,7 @@ const HEALTH_RANK: Record<string, number> = {
healthy: 2,
};
const compareServers = (
a: MCPServer,
b: MCPServer,
sort: SortKey,
): number => {
const compareServers = (a: MCPServer, b: MCPServer, sort: SortKey): number => {
switch (sort) {
case "name_asc": {
const nameA = (a.server_name || a.alias || a.server_id).toLowerCase();
@ -80,7 +76,12 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
const { data: mcpServers, isLoading: isLoadingServers, refetch } = useMCPServers();
// Fetch health status for all servers
const { data: healthStatuses, isLoading: isLoadingHealth, recheckServerHealth, recheckingServerIds } = useMCPServerHealth();
const {
data: healthStatuses,
isLoading: isLoadingHealth,
recheckServerHealth,
recheckingServerIds,
} = useMCPServerHealth();
// Merge health status data into servers
const serversWithHealth = useMemo(() => {
@ -93,9 +94,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
const healthStatus = healthMap.get(server.server_id);
return {
...server,
status: healthStatus
? (healthStatus as "healthy" | "unhealthy" | "unknown")
: server.status,
status: healthStatus ? (healthStatus as "healthy" | "unhealthy" | "unknown") : server.status,
};
});
}, [mcpServers, healthStatuses]);
@ -147,9 +146,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
const missingFieldsByServer = useMemo(() => {
const map: Record<string, string[]> = {};
for (const [serverId, status] of Object.entries(envVarStatusByServer)) {
map[serverId] = (status.required ?? [])
.filter((spec) => !spec.is_set)
.map((spec) => spec.name);
map[serverId] = (status.required ?? []).filter((spec) => !spec.is_set).map((spec) => spec.name);
}
return map;
}, [envVarStatusByServer]);
@ -168,10 +165,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
setEnvVarsModalServer(match);
params.delete("fill_env_vars");
const newSearch = params.toString();
const newUrl =
window.location.pathname +
(newSearch ? `?${newSearch}` : "") +
window.location.hash;
const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : "") + window.location.hash;
window.history.replaceState({}, "", newUrl);
}
}, [serversWithHealth]);
@ -219,35 +213,40 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
if (!serversWithHealth) return [];
return Array.from(
new Set(
serversWithHealth.flatMap((server) => server.mcp_access_groups).filter((group): group is string => group != null),
serversWithHealth
.flatMap((server) => server.mcp_access_groups)
.filter((group): group is string => group != null),
),
);
}, [serversWithHealth]);
// Filtering logic for both team and access group
const filterServers = useCallback((teamId: string, group: string) => {
if (!serversWithHealth) return setFilteredServers([]);
let filtered = serversWithHealth;
if (teamId === "personal") {
setFilteredServers([]);
return;
}
if (teamId !== "all") {
filtered = filtered.filter((server) => server.teams?.some((team) => team.team_id === teamId));
}
if (group !== "all") {
filtered = filtered.filter((server) =>
server.mcp_access_groups?.some((g: any) => (typeof g === "string" ? g === group : g && g.name === group)),
);
}
const sorted = [...filtered].sort((a, b) => {
if (!a.created_at && !b.created_at) return 0;
if (!a.created_at) return 1;
if (!b.created_at) return -1;
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
setFilteredServers(sorted);
}, [serversWithHealth]);
const filterServers = useCallback(
(teamId: string, group: string) => {
if (!serversWithHealth) return setFilteredServers([]);
let filtered = serversWithHealth;
if (teamId === "personal") {
setFilteredServers([]);
return;
}
if (teamId !== "all") {
filtered = filtered.filter((server) => server.teams?.some((team) => team.team_id === teamId));
}
if (group !== "all") {
filtered = filtered.filter((server) =>
server.mcp_access_groups?.some((g: any) => (typeof g === "string" ? g === group : g && g.name === group)),
);
}
const sorted = [...filtered].sort((a, b) => {
if (!a.created_at && !b.created_at) return 0;
if (!a.created_at) return 1;
if (!b.created_at) return -1;
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
setFilteredServers(sorted);
},
[serversWithHealth],
);
// Handle team filter change
const handleTeamChange = (teamId: string) => {
@ -275,12 +274,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
const alias = (s.alias || "").toLowerCase();
const url = (s.url || "").toLowerCase();
const id = s.server_id.toLowerCase();
return (
name.includes(q) ||
alias.includes(q) ||
url.includes(q) ||
id.includes(q)
);
return name.includes(q) || alias.includes(q) || url.includes(q) || id.includes(q);
})
: filteredServers;
return [...matches].sort((a, b) => compareServers(a, b, sortKey));
@ -335,18 +329,20 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
// Memoize the selected server to prevent unnecessary re-renders
const selectedServer = React.useMemo(() => {
return filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || {
server_id: "",
server_name: "",
alias: "",
url: "",
transport: "",
auth_type: "",
created_at: "",
created_by: "",
updated_at: "",
updated_by: "",
};
return (
filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || {
server_id: "",
server_name: "",
alias: "",
url: "",
transport: "",
auth_type: "",
created_at: "",
created_by: "",
updated_at: "",
updated_by: "",
}
);
}, [filteredServers, selectedServerId]);
// Memoize the onBack callback to prevent unnecessary re-renders
@ -375,14 +371,18 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
confirmLoading={isDeletingServer}
>
<div className="space-y-4">
<AntdText className="text-gray-600">This action is permanent and cannot be undone. All associated configurations will be removed.</AntdText>
<AntdText className="text-gray-600">
This action is permanent and cannot be undone. All associated configurations will be removed.
</AntdText>
{serverToDelete && (
<div className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
<Descriptions column={1} size="small" colon={false}>
{serverToDelete.server_name && (
<Descriptions.Item label={<span className="text-gray-500 text-sm">Name</span>}>
<AntdText strong className="text-sm">{serverToDelete.server_name}</AntdText>
<AntdText strong className="text-sm">
{serverToDelete.server_name}
</AntdText>
</Descriptions.Item>
)}
<Descriptions.Item label={<span className="text-gray-500 text-sm">ID</span>}>
@ -472,7 +472,13 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
<Tab>Connect</Tab>
<Tab>Semantic Filter</Tab>
<Tab>Network Settings</Tab>
{isAdminRole(userRole) && <Tab><span className="flex items-center gap-2">Submitted MCPs <NewBadge /></span></Tab>}
{isAdminRole(userRole) && (
<Tab>
<span className="flex items-center gap-2">
Submitted MCPs <NewBadge />
</span>
</Tab>
)}
</div>
</TabList>
<TabPanels>
@ -498,7 +504,9 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
<Text className="text-sm font-medium text-gray-600 whitespace-nowrap">Team</Text>
<Select value={selectedTeam} onChange={handleTeamChange} style={{ width: 220 }} size="middle">
<Option value="all">
<span className="font-medium">{isInternalUser ? "All Available Servers" : "All Servers"}</span>
<span className="font-medium">
{isInternalUser ? "All Available Servers" : "All Servers"}
</span>
</Option>
<Option value="personal">
<span className="font-medium">Personal</span>
@ -518,7 +526,12 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
<QuestionCircleOutlined style={{ marginLeft: 4, color: "#9ca3af" }} />
</Tooltip>
</Text>
<Select value={selectedMcpAccessGroup} onChange={handleMcpAccessGroupChange} style={{ width: 220 }} size="middle">
<Select
value={selectedMcpAccessGroup}
onChange={handleMcpAccessGroupChange}
style={{ width: 220 }}
size="middle"
>
<Option value="all">
<span className="font-medium">All Access Groups</span>
</Option>
@ -542,9 +555,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
style={{ maxWidth: 320 }}
/>
<div className="flex items-center gap-2">
<Text className="whitespace-nowrap text-sm font-medium text-gray-600">
Sort
</Text>
<Text className="whitespace-nowrap text-sm font-medium text-gray-600">Sort</Text>
<Select
value={sortKey}
onChange={(v: SortKey) => setSortKey(v)}
@ -594,19 +605,11 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
setEditServer(true);
}}
onRecheckHealth={
recheckServerHealth
? () => recheckServerHealth(server.server_id)
: undefined
}
onByokConnect={
server.is_byok ? () => setByokModalServer(server) : undefined
recheckServerHealth ? () => recheckServerHealth(server.server_id) : undefined
}
onByokConnect={server.is_byok ? () => setByokModalServer(server) : undefined}
onOpenFillFields={() => setEnvVarsModalServer(server)}
onDelete={
isAdminRole(userRole)
? () => handleDelete(server.server_id)
: undefined
}
onDelete={isAdminRole(userRole) ? () => handleDelete(server.server_id) : undefined}
/>
))}
</div>

View file

@ -81,10 +81,9 @@ import type { MCPUserEnvVarsStatus } from "./mcp_tools/types";
const isLocal = process.env.NODE_ENV === "development";
// In dev, if NEXT_PUBLIC_USE_REWRITES=true the Next.js dev server proxies API calls
// to the backend — use relative URLs (null) so rewrites can intercept them.
const defaultProxyBaseUrl =
process.env.NEXT_PUBLIC_BASE_URL
? process.env.NEXT_PUBLIC_BASE_URL
: isLocal && process.env.NEXT_PUBLIC_USE_REWRITES !== "true"
const defaultProxyBaseUrl = process.env.NEXT_PUBLIC_BASE_URL
? process.env.NEXT_PUBLIC_BASE_URL
: isLocal && process.env.NEXT_PUBLIC_USE_REWRITES !== "true"
? "http://localhost:4000"
: null;
const defaultServerRootPath = "/";
@ -92,22 +91,23 @@ export let serverRootPath = defaultServerRootPath;
const WORKER_URL_KEY = "litellm_worker_url";
// If a worker URL is in localStorage, use it as the initial proxyBaseUrl.
// This survives page navigation and the sessionStorage.clear() in user_dashboard.
const _rawWorkerUrl =
typeof window !== "undefined" ? window.localStorage.getItem(WORKER_URL_KEY) : null;
const _rawWorkerUrl = typeof window !== "undefined" ? window.localStorage.getItem(WORKER_URL_KEY) : null;
// Validate stored worker URL — reject non-HTTP schemes to prevent exfiltration
const _initialWorkerUrl = (() => {
if (!_rawWorkerUrl) return null;
try {
const parsed = new URL(_rawWorkerUrl);
if (parsed.protocol === "http:" || parsed.protocol === "https:") return _rawWorkerUrl;
} catch { /* invalid URL */ }
} catch {
/* invalid URL */
}
// Invalid URL in storage — clear it
if (typeof window !== "undefined") window.localStorage.removeItem(WORKER_URL_KEY);
return null;
})();
export let proxyBaseUrl: string | null = _initialWorkerUrl ?? defaultProxyBaseUrl;
if (isLocal != true) {
console.log = function () { };
console.log = function () {};
}
const getWindowLocation = () => {
@ -129,8 +129,8 @@ const updateProxyBaseUrl = (serverRootPath: string, receivedProxyBaseUrl: string
const resolvedDefaultProxyBaseUrl = process.env.NEXT_PUBLIC_BASE_URL
? process.env.NEXT_PUBLIC_BASE_URL
: isLocal && process.env.NEXT_PUBLIC_USE_REWRITES !== "true"
? "http://localhost:4000"
: browserLocation?.origin ?? null;
? "http://localhost:4000"
: browserLocation?.origin ?? null;
let initialProxyBaseUrl = receivedProxyBaseUrl || resolvedDefaultProxyBaseUrl;
console.log("proxyBaseUrl:", proxyBaseUrl);
console.log("serverRootPath:", serverRootPath);
@ -542,9 +542,7 @@ export const cancelModelCostMapReload = async (accessToken: string) => {
export const getModelCostMapSource = async (accessToken: string) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/model/cost_map/source`
: `/model/cost_map/source`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/model/cost_map/source` : `/model/cost_map/source`;
const response = await fetch(url, {
method: "GET",
headers: {
@ -1296,10 +1294,7 @@ export interface UserInfoV2Response {
* @param accessToken - Bearer token for auth
* @param userId - Optional user ID to look up. If omitted, returns the caller's own info.
*/
export const userGetInfoV2 = async (
accessToken: string,
userId?: string,
): Promise<UserInfoV2Response> => {
export const userGetInfoV2 = async (accessToken: string, userId?: string): Promise<UserInfoV2Response> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/user/info` : `/v2/user/info`;
if (userId) {
@ -1892,7 +1887,13 @@ const fetchDailyActivity = async ({
}
};
export const userDailyActivityCall = async (accessToken: string, startTime: Date, endTime: Date, page: number = 1, userId: string | null = null) => {
export const userDailyActivityCall = async (
accessToken: string,
startTime: Date,
endTime: Date,
page: number = 1,
userId: string | null = null,
) => {
/**
* Get daily user activity on proxy
*/
@ -3325,7 +3326,12 @@ export const keyAliasesCall = async (
}
};
export const userDailyActivityAggregatedCall = async (accessToken: string, startTime: Date, endTime: Date, userId: string | null = null) => {
export const userDailyActivityAggregatedCall = async (
accessToken: string,
startTime: Date,
endTime: Date,
userId: string | null = null,
) => {
/**
* Get aggregated daily user activity (no pagination)
*/
@ -5064,7 +5070,7 @@ interface ListGuardrailSubmissionsResponse {
export const listGuardrailSubmissions = async (
accessToken: string,
params?: { status?: string; team_id?: string; team_guardrail?: boolean; search?: string }
params?: { status?: string; team_id?: string; team_guardrail?: boolean; search?: string },
): Promise<ListGuardrailSubmissionsResponse> => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/submissions` : `/guardrails/submissions`;
const searchParams = new URLSearchParams();
@ -5091,7 +5097,7 @@ export const listGuardrailSubmissions = async (
export const approveGuardrailSubmission = async (
accessToken: string,
guardrailId: string
guardrailId: string,
): Promise<{ guardrail_id: string; status: string; message: string }> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}/approve`
@ -5114,7 +5120,7 @@ export const approveGuardrailSubmission = async (
export const rejectGuardrailSubmission = async (
accessToken: string,
guardrailId: string
guardrailId: string,
): Promise<{ guardrail_id: string; status: string; message: string }> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}/reject`
@ -5136,11 +5142,7 @@ export const rejectGuardrailSubmission = async (
};
// Guardrails / Policies usage (dashboard)
export const getGuardrailsUsageOverview = async (
accessToken: string,
startDate?: string,
endDate?: string
) => {
export const getGuardrailsUsageOverview = async (accessToken: string, startDate?: string, endDate?: string) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/usage/overview` : `/guardrails/usage/overview`;
const params = new URLSearchParams();
@ -5169,10 +5171,12 @@ export const getGuardrailsUsageDetail = async (
accessToken: string,
guardrailId: string,
startDate?: string,
endDate?: string
endDate?: string,
) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/usage/detail/${encodeURIComponent(guardrailId)}` : `/guardrails/usage/detail/${encodeURIComponent(guardrailId)}`;
let url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/usage/detail/${encodeURIComponent(guardrailId)}`
: `/guardrails/usage/detail/${encodeURIComponent(guardrailId)}`;
const params = new URLSearchParams();
if (startDate) params.append("start_date", startDate);
if (endDate) params.append("end_date", endDate);
@ -5197,7 +5201,15 @@ export const getGuardrailsUsageDetail = async (
export const getGuardrailsUsageLogs = async (
accessToken: string,
options: { guardrailId?: string; policyId?: string; page?: number; pageSize?: number; action?: string; startDate?: string; endDate?: string }
options: {
guardrailId?: string;
policyId?: string;
page?: number;
pageSize?: number;
action?: string;
startDate?: string;
endDate?: string;
},
) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/usage/logs` : `/guardrails/usage/logs`;
@ -5299,7 +5311,7 @@ interface TestPoliciesAndGuardrailsResponse {
export const testPoliciesAndGuardrails = async (
accessToken: string,
body: TestPoliciesAndGuardrailsRequest,
signal?: AbortSignal
signal?: AbortSignal,
): Promise<TestPoliciesAndGuardrailsResponse> => {
try {
const url = proxyBaseUrl
@ -5328,7 +5340,8 @@ export const testPoliciesAndGuardrails = async (
let errorMessage = "Failed to test policies and guardrails";
try {
const errorJson = JSON.parse(errorData);
if (errorJson.detail) errorMessage = typeof errorJson.detail === "string" ? errorJson.detail : JSON.stringify(errorJson.detail);
if (errorJson.detail)
errorMessage = typeof errorJson.detail === "string" ? errorJson.detail : JSON.stringify(errorJson.detail);
else if (errorJson.message) errorMessage = errorJson.message;
} catch {
errorMessage = errorData || errorMessage;
@ -5401,12 +5414,10 @@ export const enrichPolicyTemplate = async (
templateId: string,
parameters: Record<string, string>,
model?: string,
competitors?: string[]
competitors?: string[],
) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policy/templates/enrich`
: `/policy/templates/enrich`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/policy/templates/enrich` : `/policy/templates/enrich`;
const body: any = { template_id: templateId, parameters };
if (model) body.model = model;
if (competitors) body.competitors = competitors;
@ -5438,12 +5449,10 @@ export const suggestPolicyTemplates = async (
accessToken: string,
attackExamples: string[],
description: string,
model: string
model: string,
) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policy/templates/suggest`
: `/policy/templates/suggest`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/policy/templates/suggest` : `/policy/templates/suggest`;
const response = await fetch(url, {
method: "POST",
headers: {
@ -5471,15 +5480,9 @@ export const suggestPolicyTemplates = async (
}
};
export const testPolicyTemplate = async (
accessToken: string,
guardrailDefinitions: any[],
text: string
) => {
export const testPolicyTemplate = async (accessToken: string, guardrailDefinitions: any[], text: string) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policy/templates/test`
: `/policy/templates/test`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/policy/templates/test` : `/policy/templates/test`;
const response = await fetch(url, {
method: "POST",
headers: {
@ -5519,11 +5522,9 @@ export const enrichPolicyTemplateStream = async (
}) => void,
onError?: (error: string) => void,
options?: { instruction?: string; existingCompetitors?: string[] },
onStatus?: (message: string) => void
onStatus?: (message: string) => void,
) => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policy/templates/enrich/stream`
: `/policy/templates/enrich/stream`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/policy/templates/enrich/stream` : `/policy/templates/enrich/stream`;
const body: any = { template_id: templateId, parameters, model };
if (options?.instruction) body.instruction = options.instruction;
if (options?.existingCompetitors) body.competitors = options.existingCompetitors;
@ -5598,9 +5599,7 @@ export const usageAiChatStream = async (
onToolCall?: (event: UsageAiToolCallEvent) => void,
signal?: AbortSignal,
) => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/usage/ai/chat`
: `/usage/ai/chat`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/usage/ai/chat` : `/usage/ai/chat`;
const response = await fetch(url, {
method: "POST",
@ -5712,7 +5711,7 @@ export const updatePolicyCall = async (accessToken: string, policyId: string, po
export const listPolicyVersions = async (
accessToken: string,
policyName: string
policyName: string,
): Promise<{ policy_name: string; versions: any[]; total_count: number }> => {
try {
const encodedName = encodeURIComponent(policyName);
@ -5744,7 +5743,7 @@ export const listPolicyVersions = async (
export const createPolicyVersion = async (
accessToken: string,
policyName: string,
sourcePolicyId?: string | null
sourcePolicyId?: string | null,
): Promise<any> => {
try {
const encodedName = encodeURIComponent(policyName);
@ -5777,12 +5776,10 @@ export const createPolicyVersion = async (
export const updatePolicyVersionStatus = async (
accessToken: string,
policyId: string,
versionStatus: "published" | "production"
versionStatus: "published" | "production",
): Promise<any> => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policies/${policyId}/status`
: `/policies/${policyId}/status`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}/status` : `/policies/${policyId}/status`;
const response = await fetch(url, {
method: "PUT",
headers: {
@ -5944,7 +5941,7 @@ export const deletePolicyAttachmentCall = async (accessToken: string, attachment
export const testPipelineCall = async (
accessToken: string,
pipeline: any,
testMessages: Array<{role: string, content: string}>
testMessages: Array<{ role: string; content: string }>,
) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/test-pipeline` : `/policies/test-pipeline`;
@ -6002,12 +5999,10 @@ export const getResolvedGuardrails = async (accessToken: string, policyId: strin
export const resolvePoliciesCall = async (
accessToken: string,
context: { team_alias?: string; key_alias?: string; model?: string; tags?: string[] }
context: { team_alias?: string; key_alias?: string; model?: string; tags?: string[] },
) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policies/resolve`
: `/policies/resolve`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/resolve` : `/policies/resolve`;
const response = await fetch(url, {
method: "POST",
headers: {
@ -6031,10 +6026,7 @@ export const resolvePoliciesCall = async (
}
};
export const estimateAttachmentImpactCall = async (
accessToken: string,
attachmentData: any
) => {
export const estimateAttachmentImpactCall = async (accessToken: string, attachmentData: any) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/policies/attachments/estimate-impact`
@ -6062,10 +6054,7 @@ export const estimateAttachmentImpactCall = async (
}
};
export const getPromptsList = async (
accessToken: string,
environment?: string,
): Promise<ListPromptsResponse> => {
export const getPromptsList = async (accessToken: string, environment?: string): Promise<ListPromptsResponse> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/list` : `/prompts/list`;
if (environment) {
@ -6094,7 +6083,11 @@ export const getPromptsList = async (
}
};
export const getPromptInfo = async (accessToken: string, promptId: string, environment?: string): Promise<PromptInfoResponse> => {
export const getPromptInfo = async (
accessToken: string,
promptId: string,
environment?: string,
): Promise<PromptInfoResponse> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/${promptId}/info` : `/prompts/${promptId}/info`;
if (environment) {
@ -6123,7 +6116,11 @@ export const getPromptInfo = async (accessToken: string, promptId: string, envir
}
};
export const getPromptVersions = async (accessToken: string, promptId: string, environment?: string): Promise<ListPromptsResponse> => {
export const getPromptVersions = async (
accessToken: string,
promptId: string,
environment?: string,
): Promise<ListPromptsResponse> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/${promptId}/versions` : `/prompts/${promptId}/versions`;
if (environment) {
@ -6501,9 +6498,7 @@ export const updateInternalUserSettings = async (accessToken: string, settings:
export const fetchOpenAPIRegistry = async (accessToken: string) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/openapi-registry`
: `/v1/mcp/openapi-registry`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/mcp/openapi-registry` : `/v1/mcp/openapi-registry`;
const response = await fetch(url, {
method: HTTP_REQUEST.GET,
@ -6527,9 +6522,7 @@ export const fetchOpenAPIRegistry = async (accessToken: string) => {
export const fetchDiscoverableMCPServers = async (accessToken: string) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/discover`
: `/v1/mcp/discover`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/mcp/discover` : `/v1/mcp/discover`;
const response = await fetch(url, {
method: HTTP_REQUEST.GET,
@ -6660,9 +6653,7 @@ export const fetchMCPAccessGroups = async (accessToken: string) => {
export const fetchMCPClientIp = async (accessToken: string): Promise<string | null> => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/network/client-ip`
: `/v1/mcp/network/client-ip`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/mcp/network/client-ip` : `/v1/mcp/network/client-ip`;
const response = await fetch(url, {
method: HTTP_REQUEST.GET,
@ -7141,11 +7132,7 @@ export const testSearchToolConnection = async (accessToken: string, litellmParam
}
};
export const listMCPTools = async (
accessToken: string,
serverId: string,
customHeaders?: Record<string, string>,
) => {
export const listMCPTools = async (accessToken: string, serverId: string, customHeaders?: Record<string, string>) => {
// Construct base URL
let url = proxyBaseUrl
? `${proxyBaseUrl}/mcp-rest/tools/list?server_id=${serverId}`
@ -7199,8 +7186,7 @@ export const listMCPTools = async (
// to inspect `result.error` / `result.message`. Attach `status` so
// callers that need to react to auth failures (e.g. the useQuery in
// mcp_tools.tsx) can still detect 401s from the returned object.
const errorMessage =
(data && (data.message || data.error)) || "Failed to fetch MCP tools";
const errorMessage = (data && (data.message || data.error)) || "Failed to fetch MCP tools";
return {
tools: [],
error: (data && data.error) || `http_${response.status}`,
@ -8092,9 +8078,7 @@ export const getCategoryYaml = async (accessToken: string, categoryName: string)
export const getMajorAirlines = async (accessToken: string) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/ui/major_airlines`
: `/guardrails/ui/major_airlines`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/ui/major_airlines` : `/guardrails/ui/major_airlines`;
const response = await fetch(url, {
method: "GET",
@ -8106,10 +8090,7 @@ export const getMajorAirlines = async (accessToken: string) => {
if (!response.ok) {
const errorData = await response.text();
console.error(
`Failed to get major airlines. Status: ${response.status}, Error:`,
errorData
);
console.error(`Failed to get major airlines. Status: ${response.status}, Error:`, errorData);
handleError(errorData);
throw new Error(`Failed to get major airlines: ${response.status} ${errorData}`);
}
@ -8641,9 +8622,7 @@ export interface LicenseInfo {
};
}
export const getLicenseInfo = async (
accessToken: string,
): Promise<LicenseInfo | null> => {
export const getLicenseInfo = async (accessToken: string): Promise<LicenseInfo | null> => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/health/license` : `/health/license`;
@ -9333,7 +9312,8 @@ export const deriveErrorMessage = (errorData: any): string => {
? detail
: undefined;
return (
(errorData?.error && (errorData.error.message || (typeof errorData.error === "string" ? errorData.error : undefined))) ||
(errorData?.error &&
(errorData.error.message || (typeof errorData.error === "string" ? errorData.error : undefined))) ||
errorData?.message ||
detailStr ||
JSON.stringify(errorData)
@ -9382,9 +9362,7 @@ export const loginCall = async (username: string, password: string, useV3?: bool
// v3 returns an opaque code — exchange it for the real JWT
if (useV3 && data.code) {
const exchangeUrl = proxyBaseUrl
? `${proxyBaseUrl}/v3/login/exchange`
: "/v3/login/exchange";
const exchangeUrl = proxyBaseUrl ? `${proxyBaseUrl}/v3/login/exchange` : "/v3/login/exchange";
const exchangeResponse = await fetch(exchangeUrl, {
method: "POST",
@ -9472,7 +9450,6 @@ export const updateUiSettings = async (accessToken: string, settings: Record<str
return data;
};
// Claude Code Marketplace Networking Functions
/**
@ -9752,11 +9729,9 @@ export interface ComplianceCheckRequest {
export const checkEuAiActCompliance = async (
accessToken: string,
payload: ComplianceCheckRequest
payload: ComplianceCheckRequest,
): Promise<ComplianceResponse> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/compliance/eu-ai-act`
: `/compliance/eu-ai-act`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/compliance/eu-ai-act` : `/compliance/eu-ai-act`;
const response = await fetch(url, {
method: "POST",
headers: {
@ -9774,11 +9749,9 @@ export const checkEuAiActCompliance = async (
export const checkGdprCompliance = async (
accessToken: string,
payload: ComplianceCheckRequest
payload: ComplianceCheckRequest,
): Promise<ComplianceResponse> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/compliance/gdpr`
: `/compliance/gdpr`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/compliance/gdpr` : `/compliance/gdpr`;
const response = await fetch(url, {
method: "POST",
headers: {
@ -9824,12 +9797,8 @@ export interface ToolPolicyOptionsResponse {
output_policies: ToolPolicyOption[];
}
export const fetchToolPolicyOptions = async (
accessToken: string
): Promise<ToolPolicyOptionsResponse> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/tool/policy/options`
: `/v1/tool/policy/options`;
export const fetchToolPolicyOptions = async (accessToken: string): Promise<ToolPolicyOptionsResponse> => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/tool/policy/options` : `/v1/tool/policy/options`;
const response = await fetch(url, {
method: "GET",
headers: {
@ -9896,12 +9865,10 @@ export interface ToolUsageLogsResponse {
export const getToolUsageLogs = async (
accessToken: string,
toolName: string,
options: { page?: number; pageSize?: number; startDate?: string; endDate?: string }
options: { page?: number; pageSize?: number; startDate?: string; endDate?: string },
): Promise<ToolUsageLogsResponse> => {
const encoded = encodeURIComponent(toolName);
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/tool/${encoded}/logs`
: `/v1/tool/${encoded}/logs`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/tool/${encoded}/logs` : `/v1/tool/${encoded}/logs`;
const params = new URLSearchParams();
if (options.page != null) params.append("page", String(options.page));
if (options.pageSize != null) params.append("page_size", String(options.pageSize));
@ -9922,14 +9889,9 @@ export const getToolUsageLogs = async (
return response.json();
};
export const fetchToolDetail = async (
accessToken: string,
toolName: string
): Promise<ToolDetailResponse> => {
export const fetchToolDetail = async (accessToken: string, toolName: string): Promise<ToolDetailResponse> => {
const encoded = encodeURIComponent(toolName);
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/tool/${encoded}/detail`
: `/v1/tool/${encoded}/detail`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/tool/${encoded}/detail` : `/v1/tool/${encoded}/detail`;
const response = await fetch(url, {
method: "GET",
headers: {
@ -9948,7 +9910,7 @@ export const updateToolPolicy = async (
accessToken: string,
toolName: string,
policies: { input_policy?: string; output_policy?: string },
options?: { team_id?: string | null; key_hash?: string | null; key_alias?: string | null }
options?: { team_id?: string | null; key_hash?: string | null; key_alias?: string | null },
): Promise<ToolRow> => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/tool/policy` : `/v1/tool/policy`;
const body: Record<string, string | undefined | null> = {
@ -9977,7 +9939,7 @@ export const updateToolPolicy = async (
export const deleteToolPolicyOverride = async (
accessToken: string,
toolName: string,
params: { team_id?: string | null; key_hash?: string | null }
params: { team_id?: string | null; key_hash?: string | null },
): Promise<{ deleted: boolean; tool_name: string }> => {
const encoded = encodeURIComponent(toolName);
const q = new URLSearchParams();
@ -10040,14 +10002,17 @@ export const storeMCPOAuthUserCredential = async (
const err = await response.json().catch(() => ({}));
const errObj = err as { detail?: unknown };
const detail = errObj?.detail;
const detailMsg =
Array.isArray(detail)
? detail.map((d: unknown) => (d && typeof d === "object" ? (d as Record<string, unknown>).msg ?? JSON.stringify(d) : String(d))).join("; ")
: typeof detail === "string"
? detail
: detail && typeof (detail as Record<string, unknown>).error === "string"
? (detail as Record<string, unknown>).error as string
: undefined;
const detailMsg = Array.isArray(detail)
? detail
.map((d: unknown) =>
d && typeof d === "object" ? (d as Record<string, unknown>).msg ?? JSON.stringify(d) : String(d),
)
.join("; ")
: typeof detail === "string"
? detail
: detail && typeof (detail as Record<string, unknown>).error === "string"
? ((detail as Record<string, unknown>).error as string)
: undefined;
throw new Error(detailMsg || "Failed to store OAuth credential");
}
return response.json();
@ -10068,14 +10033,17 @@ export const deleteMCPOAuthUserCredential = async (
const err = await response.json().catch(() => ({}));
const errObj = err as { detail?: unknown };
const detail = errObj?.detail;
const detailMsg =
Array.isArray(detail)
? detail.map((d: unknown) => (d && typeof d === "object" ? (d as Record<string, unknown>).msg ?? JSON.stringify(d) : String(d))).join("; ")
: typeof detail === "string"
? detail
: detail && typeof (detail as Record<string, unknown>).error === "string"
? (detail as Record<string, unknown>).error as string
: undefined;
const detailMsg = Array.isArray(detail)
? detail
.map((d: unknown) =>
d && typeof d === "object" ? (d as Record<string, unknown>).msg ?? JSON.stringify(d) : String(d),
)
.join("; ")
: typeof detail === "string"
? detail
: detail && typeof (detail as Record<string, unknown>).error === "string"
? ((detail as Record<string, unknown>).error as string)
: undefined;
throw new Error(detailMsg || "Failed to revoke OAuth credential");
}
return response.json();
@ -10098,12 +10066,8 @@ export const getMCPOAuthUserCredentialStatus = async (
return response.json();
};
export const listMCPUserCredentials = async (
accessToken: string,
): Promise<MCPUserCredentialListItem[]> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/user-credentials`
: `/v1/mcp/user-credentials`;
export const listMCPUserCredentials = async (accessToken: string): Promise<MCPUserCredentialListItem[]> => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/mcp/user-credentials` : `/v1/mcp/user-credentials`;
const response = await fetch(url, {
method: "GET",
headers: { [globalLitellmHeaderName]: `Bearer ${accessToken}` },
@ -10116,10 +10080,7 @@ export const listMCPUserCredentials = async (
// MCP per-user env vars (/v1/mcp/server/{id}/user-env-vars)
// ============================================================
export const getMCPUserEnvVars = async (
accessToken: string,
serverId: string,
): Promise<MCPUserEnvVarsStatus> => {
export const getMCPUserEnvVars = async (accessToken: string, serverId: string): Promise<MCPUserEnvVarsStatus> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/server/${serverId}/user-env-vars`
: `/v1/mcp/server/${serverId}/user-env-vars`;
@ -10131,9 +10092,7 @@ export const getMCPUserEnvVars = async (
const err = await response.json().catch(() => ({}));
const detail = (err as { detail?: unknown })?.detail;
const message =
typeof detail === "string"
? detail
: (detail as { error?: string })?.error || "Failed to load env vars";
typeof detail === "string" ? detail : (detail as { error?: string })?.error || "Failed to load env vars";
throw new Error(message);
}
return response.json();
@ -10159,18 +10118,13 @@ export const storeMCPUserEnvVars = async (
const err = await response.json().catch(() => ({}));
const detail = (err as { detail?: unknown })?.detail;
const message =
typeof detail === "string"
? detail
: (detail as { error?: string })?.error || "Failed to save env vars";
typeof detail === "string" ? detail : (detail as { error?: string })?.error || "Failed to save env vars";
throw new Error(message);
}
return response.json();
};
export const clearMCPUserEnvVars = async (
accessToken: string,
serverId: string,
): Promise<MCPUserEnvVarsStatus> => {
export const clearMCPUserEnvVars = async (accessToken: string, serverId: string): Promise<MCPUserEnvVarsStatus> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/server/${serverId}/user-env-vars`
: `/v1/mcp/server/${serverId}/user-env-vars`;
@ -10184,12 +10138,8 @@ export const clearMCPUserEnvVars = async (
return response.json();
};
export const listMCPUserEnvVarStatus = async (
accessToken: string,
): Promise<MCPUserEnvVarsStatus[]> => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/mcp/user-env-vars/status`
: `/v1/mcp/user-env-vars/status`;
export const listMCPUserEnvVarStatus = async (accessToken: string): Promise<MCPUserEnvVarsStatus[]> => {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/mcp/user-env-vars/status` : `/v1/mcp/user-env-vars/status`;
const response = await fetch(url, {
method: "GET",
headers: { [globalLitellmHeaderName]: `Bearer ${accessToken}` },
@ -10215,8 +10165,7 @@ export const listMCPUserEnvVarStatus = async (
* other potentially-unsafe character (spaces, `?`, `#`, `%`, etc.) per
* path segment.
*/
const encodeMemoryKeyForPath = (key: string): string =>
key.split("/").map(encodeURIComponent).join("/");
const encodeMemoryKeyForPath = (key: string): string => key.split("/").map(encodeURIComponent).join("/");
export interface MemoryRow {
memory_id: string;
@ -10255,8 +10204,7 @@ export const fetchMemoryList = async (
params.append("key", options.key);
}
if (options.page != null) params.append("page", String(options.page));
if (options.pageSize != null)
params.append("page_size", String(options.pageSize));
if (options.pageSize != null) params.append("page_size", String(options.pageSize));
const url = params.toString() ? `${base}?${params.toString()}` : base;
const response = await fetch(url, {
method: "GET",
@ -10303,9 +10251,7 @@ export const updateMemory = async (
payload: { value?: string; metadata?: unknown },
): Promise<MemoryRow> => {
const encoded = encodeMemoryKeyForPath(key);
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/memory/${encoded}`
: `/v1/memory/${encoded}`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/memory/${encoded}` : `/v1/memory/${encoded}`;
const response = await fetch(url, {
method: "PUT",
headers: {
@ -10321,14 +10267,9 @@ export const updateMemory = async (
return response.json();
};
export const deleteMemory = async (
accessToken: string,
key: string,
): Promise<void> => {
export const deleteMemory = async (accessToken: string, key: string): Promise<void> => {
const encoded = encodeMemoryKeyForPath(key);
const url = proxyBaseUrl
? `${proxyBaseUrl}/v1/memory/${encoded}`
: `/v1/memory/${encoded}`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/memory/${encoded}` : `/v1/memory/${encoded}`;
const response = await fetch(url, {
method: "DELETE",
headers: {