mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for mcp_tools batch 1
This commit is contained in:
parent
dc9c44f3b1
commit
c3617bc3d2
11 changed files with 453 additions and 150 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React, { useState } from "react";
|
||||
import { Modal, Input, Switch } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import {
|
||||
KeyOutlined,
|
||||
|
|
@ -29,6 +30,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
onSuccess,
|
||||
accessToken,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [step, setStep] = useState<1 | 2>(1);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [saveKey, setSaveKey] = useState(true);
|
||||
|
|
@ -47,7 +49,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
|
||||
const handleAuthorize = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
MessageManager.error("Please enter your API key");
|
||||
MessageManager.error(t("mcpTools.byokCredentialModal.pleaseEnterApiKey"));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
|
@ -64,11 +66,11 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
const err = await response.json();
|
||||
throw new Error(err?.detail?.error || "Failed to save credential");
|
||||
}
|
||||
MessageManager.success(`Connected to ${serverDisplayName}`);
|
||||
MessageManager.success(t("mcpTools.byokCredentialModal.connectedSuccess", { name: serverDisplayName }));
|
||||
onSuccess(server.server_id);
|
||||
handleClose();
|
||||
} catch (e: any) {
|
||||
MessageManager.error(e.message || "Failed to connect");
|
||||
MessageManager.error(e.message || t("mcpTools.byokCredentialModal.failedToConnect"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -84,7 +86,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
onClick={() => setStep(1)}
|
||||
className="flex items-center gap-1 text-gray-500 hover:text-gray-800 text-sm"
|
||||
>
|
||||
<ArrowLeftOutlined /> Back
|
||||
<ArrowLeftOutlined /> {t("common.back")}
|
||||
</button>
|
||||
) : (
|
||||
<div />
|
||||
|
|
@ -111,8 +113,12 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Connect {serverDisplayName}</h2>
|
||||
<p className="text-gray-500 mb-6">LiteLLM needs access to {serverDisplayName} to complete your request.</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
{t("mcpTools.byokCredentialModal.connectTitle", { name: serverDisplayName })}
|
||||
</h2>
|
||||
<p className="text-gray-500 mb-6">
|
||||
{t("mcpTools.byokCredentialModal.connectDesc", { name: serverDisplayName })}
|
||||
</p>
|
||||
|
||||
{/* How it works */}
|
||||
<div className="bg-gray-50 rounded-xl p-4 text-left mb-4">
|
||||
|
|
@ -124,10 +130,11 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 mb-1">How it works</p>
|
||||
<p className="font-semibold text-gray-800 mb-1">
|
||||
{t("mcpTools.byokCredentialModal.howItWorksTitle")}
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
LiteLLM acts as a secure bridge. Your requests are routed through our MCP client directly to{" "}
|
||||
{serverDisplayName}'s API.
|
||||
{t("mcpTools.byokCredentialModal.howItWorksDesc", { name: serverDisplayName })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -141,7 +148,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
<path d="M12 2L12 22M2 12L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
Requested Access
|
||||
{t("mcpTools.byokCredentialModal.requestedAccess")}
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{server.byok_description.map((item, i) => (
|
||||
|
|
@ -158,10 +165,10 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
onClick={() => setStep(2)}
|
||||
className="w-full bg-gray-900 hover:bg-gray-700 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
Continue to Authentication <ArrowRightOutlined />
|
||||
{t("mcpTools.byokCredentialModal.continueToAuth")} <ArrowRightOutlined />
|
||||
</button>
|
||||
<button onClick={handleClose} className="mt-3 w-full text-gray-400 hover:text-gray-600 text-sm py-2">
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -171,13 +178,19 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
<KeyOutlined className="text-blue-400 text-xl" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Provide API Key</h2>
|
||||
<p className="text-gray-500 mb-6">Enter your {serverDisplayName} API key to authorize this connection.</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
{t("mcpTools.byokCredentialModal.provideApiKeyTitle")}
|
||||
</h2>
|
||||
<p className="text-gray-500 mb-6">
|
||||
{t("mcpTools.byokCredentialModal.provideApiKeyDesc", { name: serverDisplayName })}
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-semibold text-gray-800 mb-2">{serverDisplayName} API Key</label>
|
||||
<label className="block text-sm font-semibold text-gray-800 mb-2">
|
||||
{t("mcpTools.byokCredentialModal.apiKeyLabel", { name: serverDisplayName })}
|
||||
</label>
|
||||
<Input.Password
|
||||
placeholder="Enter your API key"
|
||||
placeholder={t("mcpTools.byokCredentialModal.apiKeyPlaceholder")}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
size="large"
|
||||
|
|
@ -190,7 +203,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 text-sm mt-2 flex items-center gap-1"
|
||||
>
|
||||
Where do I find my API key? <LinkOutlined />
|
||||
{t("mcpTools.byokCredentialModal.whereIsMyApiKey")} <LinkOutlined />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -204,7 +217,9 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-800">Save key for future use</span>
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{t("mcpTools.byokCredentialModal.saveKeyLabel")}
|
||||
</span>
|
||||
</div>
|
||||
<Switch checked={saveKey} onChange={setSaveKey} />
|
||||
</div>
|
||||
|
|
@ -212,9 +227,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
{/* Security note */}
|
||||
<div className="bg-blue-50 rounded-xl p-4 flex items-start gap-3 mb-6">
|
||||
<LockOutlined className="text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-blue-700">
|
||||
Your key is stored securely and transmitted over HTTPS. It is never shared with third parties.
|
||||
</p>
|
||||
<p className="text-sm text-blue-700">{t("mcpTools.byokCredentialModal.securityNote")}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -222,7 +235,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
disabled={loading}
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-60 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<LockOutlined /> Connect & Authorize
|
||||
<LockOutlined /> {t("mcpTools.byokCredentialModal.connectAndAuthorize")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React from "react";
|
||||
import { Form, Input, Select, Button, Tooltip, Typography } from "antd";
|
||||
import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const SCOPE_OPTIONS = [
|
||||
{ value: "global", label: "Instance" },
|
||||
{ value: "user", label: "Per-user" },
|
||||
export const getScopeOptions = (t: TFunction) => [
|
||||
{ value: "global", label: t("mcpTools.envVarsSection.scopeInstance") },
|
||||
{ value: "user", label: t("mcpTools.envVarsSection.scopePerUser") },
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -20,21 +22,21 @@ const SCOPE_OPTIONS = [
|
|||
* The parent form reads the ``env_vars`` field from the form values.
|
||||
*/
|
||||
const EnvVarsSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const scopeOptions = React.useMemo(() => getScopeOptions(t), [t]);
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Text strong className="text-sm">
|
||||
Variables
|
||||
{t("mcpTools.envVarsSection.sectionTitle")}
|
||||
</Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
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.
|
||||
{t("mcpTools.envVarsSection.tooltipDefine")} <code>{"${VAR_NAME}"}</code>. <br />
|
||||
<b>{t("mcpTools.envVarsSection.scopeInstance")}</b>: {t("mcpTools.envVarsSection.tooltipInstance")}
|
||||
<br />
|
||||
<b>Per-user</b>: each user supplies their own value (e.g. personal credentials) via the MCP Gateway
|
||||
dashboard.
|
||||
<b>{t("mcpTools.envVarsSection.scopePerUser")}</b>: {t("mcpTools.envVarsSection.tooltipPerUser")}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
|
@ -42,7 +44,8 @@ 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:{" "}
|
||||
{t("mcpTools.envVarsSection.referenceHint")} <code>{"${VAR_NAME}"}</code>.{" "}
|
||||
{t("mcpTools.envVarsSection.exampleLabel")}{" "}
|
||||
<code className="bg-white px-1 rounded border border-gray-200">
|
||||
{"${DB_PROTOCOL}://${CORP_USERNAME}:${CORP_PASSWORD}@${DB_HOSTNAME}"}
|
||||
</code>
|
||||
|
|
@ -53,9 +56,9 @@ const EnvVarsSection: React.FC = () => {
|
|||
<div className="space-y-2">
|
||||
{fields.length > 0 && (
|
||||
<div className="flex gap-3 px-1 text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
<div style={{ flex: 1 }}>Variable Name</div>
|
||||
<div style={{ flex: 1 }}>Value / Description</div>
|
||||
<div style={{ width: 160 }}>Scope</div>
|
||||
<div style={{ flex: 1 }}>{t("mcpTools.envVarsSection.colVarName")}</div>
|
||||
<div style={{ flex: 1 }}>{t("mcpTools.envVarsSection.colValueDesc")}</div>
|
||||
<div style={{ width: 160 }}>{t("mcpTools.envVarsSection.colScope")}</div>
|
||||
<div style={{ width: 24 }} />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -67,10 +70,10 @@ const EnvVarsSection: React.FC = () => {
|
|||
className="mb-0"
|
||||
style={{ flex: 1 }}
|
||||
rules={[
|
||||
{ required: true, message: "Variable name is required" },
|
||||
{ required: true, message: t("mcpTools.envVarsSection.varNameRequired") },
|
||||
{
|
||||
pattern: /^[A-Za-z_][A-Za-z0-9_]*$/,
|
||||
message: "Use letters, digits, underscores; cannot start with a digit.",
|
||||
message: t("mcpTools.envVarsSection.varNamePattern"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
|
@ -86,7 +89,7 @@ const EnvVarsSection: React.FC = () => {
|
|||
initialValue="global"
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
<Select options={SCOPE_OPTIONS} />
|
||||
<Select options={scopeOptions} />
|
||||
</Form.Item>
|
||||
<div style={{ width: 24, height: 32 }} className="flex items-center justify-center">
|
||||
<MinusCircleOutlined
|
||||
|
|
@ -97,7 +100,7 @@ const EnvVarsSection: React.FC = () => {
|
|||
</div>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add({ scope: "global" })} icon={<PlusOutlined />} block>
|
||||
Add Variable
|
||||
{t("mcpTools.envVarsSection.addVariable")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -113,20 +116,21 @@ const ScopedValueOrDescription: React.FC<{
|
|||
name: number;
|
||||
restField: object;
|
||||
}> = ({ name, restField }) => {
|
||||
const { t } = useTranslation();
|
||||
const isPerUser = Form.useWatch(["env_vars", name, "scope"]) === "user";
|
||||
if (isPerUser) {
|
||||
return (
|
||||
<Form.Item {...restField} name={[name, "description"]} className="mb-0">
|
||||
<Input
|
||||
addonBefore={
|
||||
<Tooltip title="Per-user variables have no shared value. This text is only a hint shown to each user when they fill in their own value.">
|
||||
<Tooltip title={t("mcpTools.envVarsSection.perUserHintTooltip")}>
|
||||
<span className="text-xs text-gray-500 cursor-help whitespace-nowrap">
|
||||
<InfoCircleOutlined className="mr-1" />
|
||||
Hint
|
||||
{t("mcpTools.envVarsSection.hintLabel")}
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
placeholder="e.g. Your DB username"
|
||||
placeholder={t("mcpTools.envVarsSection.perUserPlaceholder")}
|
||||
styles={{ input: { color: "#9ca3af" } }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
@ -134,7 +138,7 @@ const ScopedValueOrDescription: React.FC<{
|
|||
}
|
||||
return (
|
||||
<Form.Item {...restField} name={[name, "value"]} className="mb-0">
|
||||
<Input placeholder="e.g. postgresql" className="rounded-md font-mono" />
|
||||
<Input placeholder={t("mcpTools.envVarsSection.valuePlaceholder")} className="rounded-md font-mono" />
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { Input, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined, LinkOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const logos = "/ui/assets/logos/";
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ interface MCPLogoSelectorProps {
|
|||
}
|
||||
|
||||
const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [imgErrors, setImgErrors] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleSelect = (url: string) => {
|
||||
|
|
@ -46,8 +48,8 @@ const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) =>
|
|||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">Logo</span>
|
||||
<Tooltip title="Select a well-known logo or paste a URL to any image. The logo is shown on the admin and chat pages.">
|
||||
<span className="text-sm font-medium text-gray-700">{t("mcpTools.mcpLogoSelector.logoLabel")}</span>
|
||||
<Tooltip title={t("mcpTools.mcpLogoSelector.logoTooltip")}>
|
||||
<InfoCircleOutlined className="text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -57,7 +59,7 @@ const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) =>
|
|||
<div className="flex items-center gap-3 mb-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<img
|
||||
src={value}
|
||||
alt="Selected logo"
|
||||
alt={t("mcpTools.mcpLogoSelector.selectedLogoAlt")}
|
||||
className="w-10 h-10 object-contain rounded"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
|
|
@ -110,7 +112,7 @@ const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) =>
|
|||
{/* Custom URL input */}
|
||||
<Input
|
||||
prefix={<LinkOutlined className="text-gray-400" />}
|
||||
placeholder="Or paste a custom logo URL..."
|
||||
placeholder={t("mcpTools.mcpLogoSelector.customUrlPlaceholder")}
|
||||
value={value && !WELL_KNOWN_LOGOS.some((l) => l.url === value) ? value : ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value.trim();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Select, Button, Card, Typography, Spin, Tag } from "antd";
|
||||
import { SaveOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getGeneralSettingsCall,
|
||||
updateConfigFieldSetting,
|
||||
|
|
@ -24,6 +25,7 @@ function ipToSlash24(ip: string): string {
|
|||
}
|
||||
|
||||
const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({ accessToken }) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [privateRanges, setPrivateRanges] = useState<string[]>([]);
|
||||
|
|
@ -94,22 +96,20 @@ const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({ accessToken })
|
|||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<div>
|
||||
<Text className="text-lg font-semibold">Private IP Ranges</Text>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers.
|
||||
Callers from any other IP can only see servers marked "Available on Public Internet".
|
||||
</p>
|
||||
<Text className="text-lg font-semibold">{t("mcpTools.mcpNetworkSettings.privateIpRangesTitle")}</Text>
|
||||
<p className="text-sm text-gray-500 mt-1">{t("mcpTools.mcpNetworkSettings.privateIpRangesDesc")}</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
{currentIp && (
|
||||
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
|
||||
<Text className="text-sm text-blue-700">
|
||||
Your current IP: <span className="font-mono font-medium">{currentIp}</span>
|
||||
{t("mcpTools.mcpNetworkSettings.yourCurrentIp")}{" "}
|
||||
<span className="font-mono font-medium">{currentIp}</span>
|
||||
</Text>
|
||||
{suggestedRange && !privateRanges.includes(suggestedRange) && (
|
||||
<div className="mt-1">
|
||||
<Text className="text-sm text-blue-600">Suggested range: </Text>
|
||||
<Text className="text-sm text-blue-600">{t("mcpTools.mcpNetworkSettings.suggestedRange")} </Text>
|
||||
<Tag
|
||||
className="cursor-pointer font-mono"
|
||||
color="blue"
|
||||
|
|
@ -124,26 +124,24 @@ const MCPNetworkSettings: React.FC<MCPNetworkSettingsProps> = ({ accessToken })
|
|||
)}
|
||||
|
||||
<div className="flex items-center mb-2">
|
||||
<Text className="font-medium">Your Private Network Ranges</Text>
|
||||
<Text className="font-medium">{t("mcpTools.mcpNetworkSettings.yourPrivateNetworkRanges")}</Text>
|
||||
</div>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={privateRanges}
|
||||
onChange={setPrivateRanges}
|
||||
placeholder="Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8"
|
||||
placeholder={t("mcpTools.mcpNetworkSettings.rangesPlaceholder")}
|
||||
tokenSeparators={[","]}
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP ranges are used.
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-2">{t("mcpTools.mcpNetworkSettings.cidrHelp")}</p>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { Alert, Form, Select, Tooltip, Collapse, Input, Space, Button, Switch } from "antd";
|
||||
import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MCPServer, AUTH_TYPE } from "./types";
|
||||
const { Panel } = Collapse;
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
setSearchValue,
|
||||
getAccessGroupOptions,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
const watchedAuthType = Form.useWatch("auth_type", form);
|
||||
const isOAuth2 = watchedAuthType === AUTH_TYPE.OAUTH2;
|
||||
|
|
@ -108,9 +110,11 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Permission Management / Access Control</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{t("mcpTools.mcpPermissionManagement.panelTitle")}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 ml-4">Configure access permissions and security settings (Optional)</p>
|
||||
<p className="text-sm text-gray-600 ml-4">{t("mcpTools.mcpPermissionManagement.panelSubtitle")}</p>
|
||||
</div>
|
||||
}
|
||||
key="permissions"
|
||||
|
|
@ -121,14 +125,12 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Allow All LiteLLM Keys
|
||||
<Tooltip title="When enabled, every API key can access this MCP server.">
|
||||
{t("mcpTools.mcpPermissionManagement.allowAllKeysLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.allowAllKeysTooltip")}>
|
||||
<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 "public" to all keys.
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mt-1">{t("mcpTools.mcpPermissionManagement.allowAllKeysDesc")}</p>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="allow_all_keys"
|
||||
|
|
@ -143,13 +145,13 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Internal network only
|
||||
<Tooltip title="When on, only requests from within your internal network are accepted. Turn off to allow external clients (other clusters, ChatGPT, etc). API key authentication is always required regardless of this setting.">
|
||||
{t("mcpTools.mcpPermissionManagement.internalNetworkOnlyLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.internalNetworkOnlyTooltip")}>
|
||||
<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.
|
||||
{t("mcpTools.mcpPermissionManagement.internalNetworkOnlyDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Form.Item
|
||||
|
|
@ -168,14 +170,12 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Delegate auth to upstream (PKCE passthrough)
|
||||
<Tooltip title="When on, LiteLLM skips its own API key/SSO check for this server and lets the client complete PKCE directly with the upstream MCP server. Only honored when Auth Type is oauth2. No spend tracking or per-key rate limiting will run on this route.">
|
||||
{t("mcpTools.mcpPermissionManagement.delegateAuthLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.delegateAuthTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Bypass LiteLLM auth so clients authenticate directly with the upstream OAuth MCP server.
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mt-1">{t("mcpTools.mcpPermissionManagement.delegateAuthDesc")}</p>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="delegate_auth_to_upstream"
|
||||
|
|
@ -192,14 +192,13 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
OAuth pass-through
|
||||
<Tooltip title="When on, this server is treated as an OAuth pass-through: the gateway proxies the upstream /.well-known/oauth-protected-resource metadata, emits spec-compliant 401 challenges when no bearer is supplied, and propagates upstream 401/403 responses. Only honored when Auth Type is None and 'Authorization' is in Extra Headers.">
|
||||
{t("mcpTools.mcpPermissionManagement.oauthPassthroughLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.oauthPassthroughTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</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.
|
||||
{t("mcpTools.mcpPermissionManagement.oauthPassthroughDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Form.Item
|
||||
|
|
@ -218,16 +217,16 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
type="warning"
|
||||
showIcon
|
||||
className="mb-2"
|
||||
message="Internal server with upstream OAuth delegation"
|
||||
description="This MCP server is configured as internal-only but delegates auth to upstream. Anonymous users will be able to reach the upstream OAuth2 /authorize flow without a LiteLLM session. Ensure your upstream provider and network enforce access controls."
|
||||
message={t("mcpTools.mcpPermissionManagement.internalDelegateWarningTitle")}
|
||||
description={t("mcpTools.mcpPermissionManagement.internalDelegateWarningDesc")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
MCP Access Groups
|
||||
<Tooltip title="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
|
||||
{t("mcpTools.mcpPermissionManagement.accessGroupsLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.accessGroupsTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -238,7 +237,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<Select
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
placeholder={t("mcpTools.mcpPermissionManagement.accessGroupsPlaceholder")}
|
||||
optionFilterProp="value"
|
||||
filterOption={(input, option) => (option?.value ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
onSearch={(value) => setSearchValue(value)}
|
||||
|
|
@ -252,13 +251,13 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Extra Headers
|
||||
<Tooltip title="Forward custom headers from incoming requests to this MCP server (e.g., Authorization, X-Custom-Header, User-Agent)">
|
||||
{t("mcpTools.mcpPermissionManagement.extraHeadersLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.extraHeadersTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
{mcpServer?.extra_headers && mcpServer.extra_headers.length > 0 && (
|
||||
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
|
||||
{mcpServer.extra_headers.length} configured
|
||||
{t("mcpTools.mcpPermissionManagement.configuredCount", { count: mcpServer.extra_headers.length })}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
|
@ -269,8 +268,10 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
mode="tags"
|
||||
placeholder={
|
||||
mcpServer?.extra_headers && mcpServer.extra_headers.length > 0
|
||||
? `Currently: ${mcpServer.extra_headers.join(", ")}`
|
||||
: "Enter header names (e.g., Authorization, X-Custom-Header)"
|
||||
? t("mcpTools.mcpPermissionManagement.extraHeadersCurrently", {
|
||||
headers: mcpServer.extra_headers.join(", "),
|
||||
})
|
||||
: t("mcpTools.mcpPermissionManagement.extraHeadersPlaceholder")
|
||||
}
|
||||
className="rounded-lg"
|
||||
size="large"
|
||||
|
|
@ -282,8 +283,8 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Static Headers
|
||||
<Tooltip title="Send these key-value headers with every request to this MCP server.">
|
||||
{t("mcpTools.mcpPermissionManagement.staticHeadersLabel")}
|
||||
<Tooltip title={t("mcpTools.mcpPermissionManagement.staticHeadersTooltip")}>
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -299,22 +300,27 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
{...restField}
|
||||
name={[name, "header"]}
|
||||
className="flex-1"
|
||||
rules={[{ required: true, message: "Header name is required" }]}
|
||||
rules={[{ required: true, message: t("mcpTools.mcpPermissionManagement.headerNameRequired") }]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
allowClear
|
||||
className="rounded-lg"
|
||||
placeholder="Header name (e.g., X-API-Key)"
|
||||
placeholder={t("mcpTools.mcpPermissionManagement.headerNamePlaceholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, "value"]}
|
||||
className="flex-1"
|
||||
rules={[{ required: true, message: "Header value is required" }]}
|
||||
rules={[{ required: true, message: t("mcpTools.mcpPermissionManagement.headerValueRequired") }]}
|
||||
>
|
||||
<Input size="large" allowClear className="rounded-lg" placeholder="Header value" />
|
||||
<Input
|
||||
size="large"
|
||||
allowClear
|
||||
className="rounded-lg"
|
||||
placeholder={t("mcpTools.mcpPermissionManagement.headerValuePlaceholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<MinusCircleOutlined
|
||||
onClick={() => remove(name)}
|
||||
|
|
@ -323,7 +329,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} block>
|
||||
Add Static Header
|
||||
{t("mcpTools.mcpPermissionManagement.addStaticHeader")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, type FC, type KeyboardEvent, type MouseEvent } from "react";
|
||||
import { Dropdown, Tooltip, Typography, Tag } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
|
|
@ -48,6 +49,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
onOpenFillFields,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const alias = server.alias || server.server_name || "";
|
||||
const name = server.server_name || alias || server.server_id;
|
||||
// Logo is sourced exclusively from the admin-set `mcp_info.logo_url`.
|
||||
|
|
@ -102,7 +104,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
if (onRecheckHealth) {
|
||||
menuItems.push({
|
||||
key: "test-connection",
|
||||
label: "Test Connection",
|
||||
label: t("mcpTools.mcpServerCard.testConnection"),
|
||||
icon: <ThunderboltOutlined />,
|
||||
disabled: isRechecking,
|
||||
onClick: ({ domEvent }) => {
|
||||
|
|
@ -117,7 +119,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
}
|
||||
menuItems.push({
|
||||
key: "delete",
|
||||
label: "Delete",
|
||||
label: t("common.delete"),
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: ({ domEvent }) => {
|
||||
|
|
@ -143,7 +145,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
{logoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
alt={t("mcpTools.mcpServerCard.logoAlt", { name })}
|
||||
className="h-10 w-10 flex-shrink-0 rounded object-contain"
|
||||
onError={() => setFailedLogoUrl(logoUrl)}
|
||||
/>
|
||||
|
|
@ -170,7 +172,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
type="button"
|
||||
onClick={stop}
|
||||
onKeyDown={stop}
|
||||
aria-label="Server actions"
|
||||
aria-label={t("mcpTools.mcpServerCard.serverActionsAriaLabel")}
|
||||
className="-mr-1 -mt-1 inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-blue-600"
|
||||
>
|
||||
<MoreOutlined style={{ fontSize: 20 }} />
|
||||
|
|
@ -206,7 +208,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
<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"}`} />
|
||||
{isPublic ? "Public" : "Internal"}
|
||||
{isPublic ? t("mcpTools.mcpServerCard.public") : t("mcpTools.mcpServerCard.internal")}
|
||||
</span>
|
||||
</Tag>
|
||||
{accessGroups.slice(0, 2).map((g) => (
|
||||
|
|
@ -229,7 +231,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
<div className="font-semibold mb-1">Missing user fields:</div>
|
||||
<div className="font-semibold mb-1">{t("mcpTools.mcpServerCard.missingUserFieldsTitle")}</div>
|
||||
<ul className="ml-3">
|
||||
{missing.map((m) => (
|
||||
<li key={m}>• {m}</li>
|
||||
|
|
@ -240,8 +242,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
>
|
||||
<span className="inline-flex items-center gap-1 font-semibold text-red-700">
|
||||
<ExclamationCircleFilled />
|
||||
{missing.length} user field
|
||||
{missing.length === 1 ? "" : "s"} missing
|
||||
{t("mcpTools.mcpServerCard.missingUserFields", { count: missing.length })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{onOpenFillFields && (
|
||||
|
|
@ -253,7 +254,7 @@ const MCPServerCard: FC<MCPServerCardProps> = ({
|
|||
}}
|
||||
className="rounded-md bg-red-600 px-3 py-1 text-xs font-medium text-white shadow-sm transition-colors hover:bg-red-700"
|
||||
>
|
||||
Set
|
||||
{t("mcpTools.mcpServerCard.setButton")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -283,28 +284,33 @@ const HealthChip: FC<HealthChipProps> = ({
|
|||
error,
|
||||
dotClass,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (isLoadingHealth || isRechecking) {
|
||||
return (
|
||||
<Tag className="m-0">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-gray-300" />
|
||||
Checking
|
||||
{t("mcpTools.mcpServerCard.checking")}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
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>}
|
||||
<div className="font-semibold mb-1">{t("mcpTools.mcpServerCard.healthLabel", { status })}</div>
|
||||
{lastCheck && (
|
||||
<div className="text-xs mb-1">
|
||||
{t("mcpTools.mcpServerCard.lastCheck", { time: new Date(lastCheck).toLocaleString() })}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-xs">
|
||||
<div className="font-medium text-red-300 mb-1">Error</div>
|
||||
<div className="font-medium text-red-300 mb-1">{t("common.error")}</div>
|
||||
<div className="break-words">{error}</div>
|
||||
</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>}
|
||||
{!lastCheck && !error && <div className="text-xs text-gray-400">{t("mcpTools.mcpServerCard.noHealthData")}</div>}
|
||||
{onRecheck && <div className="mt-1 text-xs text-gray-300">{t("mcpTools.mcpServerCard.clickToRecheck")}</div>}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
|
|
@ -335,13 +341,14 @@ interface ByokRowProps {
|
|||
}
|
||||
|
||||
const ByokRow: FC<ByokRowProps> = ({ connected, onConnect }) => {
|
||||
const { t } = useTranslation();
|
||||
if (connected) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<span className="text-gray-500">BYOK credential</span>
|
||||
<span className="text-gray-500">{t("mcpTools.mcpServerCard.byokCredential")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-green-200 bg-green-50 px-2 py-0.5 font-medium text-green-700">
|
||||
<CheckOutlined style={{ fontSize: 10 }} /> Connected
|
||||
<CheckOutlined style={{ fontSize: 10 }} /> {t("mcpTools.mcpServerCard.connected")}
|
||||
</span>
|
||||
{onConnect && (
|
||||
<button
|
||||
|
|
@ -352,7 +359,7 @@ const ByokRow: FC<ByokRowProps> = ({ connected, onConnect }) => {
|
|||
}}
|
||||
className="text-xs text-gray-400 transition-colors hover:text-blue-600"
|
||||
>
|
||||
Update
|
||||
{t("common.update")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -361,7 +368,7 @@ const ByokRow: FC<ByokRowProps> = ({ connected, onConnect }) => {
|
|||
}
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<span className="text-gray-500">BYOK credential</span>
|
||||
<span className="text-gray-500">{t("mcpTools.mcpServerCard.byokCredential")}</span>
|
||||
{onConnect ? (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -371,7 +378,7 @@ const ByokRow: FC<ByokRowProps> = ({ connected, onConnect }) => {
|
|||
}}
|
||||
className="rounded-md bg-blue-600 px-3 py-1 text-xs font-medium text-white shadow-sm transition-colors hover:bg-blue-700"
|
||||
>
|
||||
Connect
|
||||
{t("mcpTools.mcpServerCard.connectButton")}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-400">—</span>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings";
|
||||
import {
|
||||
getFieldGroups,
|
||||
getMcpRequiredFieldDefs,
|
||||
SETTINGS_KEY,
|
||||
FieldGroup,
|
||||
RequiredFieldDef,
|
||||
} from "./MCPStandardsSettings";
|
||||
import type { TFunction } from "i18next";
|
||||
import { MCPServer } from "./types";
|
||||
|
||||
const t = ((key: string) => key) as unknown as TFunction;
|
||||
|
||||
const makeServer = (overrides: Partial<MCPServer> = {}): MCPServer => ({
|
||||
server_id: "s1",
|
||||
created_at: "2024-01-01",
|
||||
|
|
@ -11,22 +20,24 @@ const makeServer = (overrides: Partial<MCPServer> = {}): MCPServer => ({
|
|||
...overrides,
|
||||
});
|
||||
|
||||
describe("FIELD_GROUPS", () => {
|
||||
describe("getFieldGroups", () => {
|
||||
it("should contain four groups", () => {
|
||||
expect(FIELD_GROUPS).toHaveLength(4);
|
||||
expect(FIELD_GROUPS.map((g) => g.label)).toEqual(["Documentation", "Source", "Connection", "Security"]);
|
||||
const groups = getFieldGroups(t);
|
||||
expect(groups).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MCP_REQUIRED_FIELD_DEFS", () => {
|
||||
describe("getMcpRequiredFieldDefs", () => {
|
||||
it("should flatten all fields from groups", () => {
|
||||
const totalFields = FIELD_GROUPS.reduce((sum, g) => sum + g.fields.length, 0);
|
||||
expect(MCP_REQUIRED_FIELD_DEFS).toHaveLength(totalFields);
|
||||
const groups = getFieldGroups(t);
|
||||
const defs = getMcpRequiredFieldDefs(t);
|
||||
const totalFields = groups.reduce((sum: number, g: FieldGroup) => sum + g.fields.length, 0);
|
||||
expect(defs).toHaveLength(totalFields);
|
||||
});
|
||||
});
|
||||
|
||||
describe("field check functions", () => {
|
||||
const findCheck = (key: string) => MCP_REQUIRED_FIELD_DEFS.find((f) => f.key === key)!.check;
|
||||
const findCheck = (key: string) => getMcpRequiredFieldDefs(t).find((f: RequiredFieldDef) => f.key === key)!.check;
|
||||
|
||||
it("should pass description check when description is present", () => {
|
||||
expect(findCheck("description")(makeServer({ description: "A service" }))).toBe(true);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import type { TFunction } from "i18next";
|
||||
import { MCPServer } from "./types";
|
||||
|
||||
export interface RequiredFieldDef {
|
||||
|
|
@ -14,59 +15,59 @@ export interface FieldGroup {
|
|||
fields: RequiredFieldDef[];
|
||||
}
|
||||
|
||||
export const FIELD_GROUPS: FieldGroup[] = [
|
||||
export const getFieldGroups = (t: TFunction): FieldGroup[] => [
|
||||
{
|
||||
label: "Documentation",
|
||||
label: t("mcpTools.mcpStandardsSettings.groupDocumentation"),
|
||||
fields: [
|
||||
{
|
||||
key: "description",
|
||||
label: "Description",
|
||||
description: "Must have a non-empty description",
|
||||
label: t("mcpTools.mcpStandardsSettings.fieldDescriptionLabel"),
|
||||
description: t("mcpTools.mcpStandardsSettings.fieldDescriptionDesc"),
|
||||
check: (s) => !!s.description?.trim(),
|
||||
},
|
||||
{
|
||||
key: "alias",
|
||||
label: "Alias",
|
||||
description: "Must have a display alias",
|
||||
label: t("mcpTools.mcpStandardsSettings.fieldAliasLabel"),
|
||||
description: t("mcpTools.mcpStandardsSettings.fieldAliasDesc"),
|
||||
check: (s) => !!s.alias?.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Source",
|
||||
label: t("mcpTools.mcpStandardsSettings.groupSource"),
|
||||
fields: [
|
||||
{
|
||||
key: "source_url",
|
||||
label: "GitHub / Source URL",
|
||||
description: "Must link to a source repository",
|
||||
label: t("mcpTools.mcpStandardsSettings.fieldSourceUrlLabel"),
|
||||
description: t("mcpTools.mcpStandardsSettings.fieldSourceUrlDesc"),
|
||||
check: (s) => !!s.source_url?.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Connection",
|
||||
label: t("mcpTools.mcpStandardsSettings.groupConnection"),
|
||||
fields: [
|
||||
{
|
||||
key: "url",
|
||||
label: "Server URL",
|
||||
description: "Must have a URL configured",
|
||||
label: t("mcpTools.mcpStandardsSettings.fieldServerUrlLabel"),
|
||||
description: t("mcpTools.mcpStandardsSettings.fieldServerUrlDesc"),
|
||||
check: (s) => !!s.url?.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Security",
|
||||
label: t("mcpTools.mcpStandardsSettings.groupSecurity"),
|
||||
fields: [
|
||||
{
|
||||
key: "auth_type",
|
||||
label: "Auth configured",
|
||||
description: "Must use authentication (not 'none')",
|
||||
label: t("mcpTools.mcpStandardsSettings.fieldAuthLabel"),
|
||||
description: t("mcpTools.mcpStandardsSettings.fieldAuthDesc"),
|
||||
check: (s) => !!s.auth_type && s.auth_type !== "none",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = FIELD_GROUPS.flatMap((g) => g.fields);
|
||||
export const getMcpRequiredFieldDefs = (t: TFunction): RequiredFieldDef[] => getFieldGroups(t).flatMap((g) => g.fields);
|
||||
|
||||
export const SETTINGS_KEY = "mcp_required_fields";
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@ import {
|
|||
updateConfigFieldSetting,
|
||||
} from "@/components/networking";
|
||||
import { MCPServer, MCPSubmissionsSummary } from "./types";
|
||||
import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings";
|
||||
import {
|
||||
getFieldGroups,
|
||||
getMcpRequiredFieldDefs,
|
||||
FieldGroup,
|
||||
RequiredFieldDef,
|
||||
SETTINGS_KEY,
|
||||
} from "./MCPStandardsSettings";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MCPStatus = "active" | "pending_review" | "rejected";
|
||||
|
||||
|
|
@ -139,8 +146,11 @@ type SubmissionRulesPanelProps = {
|
|||
};
|
||||
|
||||
function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: SubmissionRulesPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const fieldDefs = getMcpRequiredFieldDefs(t);
|
||||
const fieldGroups = getFieldGroups(t);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const activeLabels = MCP_REQUIRED_FIELD_DEFS.filter((f) => requiredFields.includes(f.key));
|
||||
const activeLabels = fieldDefs.filter((f: RequiredFieldDef) => requiredFields.includes(f.key));
|
||||
|
||||
const toggle = (key: string) => {
|
||||
onChange(requiredFields.includes(key) ? requiredFields.filter((k) => k !== key) : [...requiredFields, key]);
|
||||
|
|
@ -195,11 +205,11 @@ function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: Su
|
|||
for each rule on every submission card below.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5">
|
||||
{FIELD_GROUPS.map((group) => (
|
||||
{fieldGroups.map((group: FieldGroup) => (
|
||||
<div key={group.label}>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">{group.label}</div>
|
||||
<div className="space-y-2">
|
||||
{group.fields.map((field) => {
|
||||
{group.fields.map((field: RequiredFieldDef) => {
|
||||
const active = requiredFields.includes(field.key);
|
||||
return (
|
||||
<label key={field.key} className="flex items-start gap-2.5 cursor-pointer group">
|
||||
|
|
@ -256,15 +266,18 @@ type MCPServerCardProps = {
|
|||
};
|
||||
|
||||
function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServerCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const approvalStatus = (server.approval_status ?? "active") as MCPStatus;
|
||||
const statusCfg = STATUS_CONFIG[approvalStatus] ?? STATUS_CONFIG["active"];
|
||||
|
||||
const checks = MCP_REQUIRED_FIELD_DEFS.filter((f) => requiredFields.includes(f.key)).map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
description: f.description,
|
||||
passed: f.check(server),
|
||||
}));
|
||||
const checks = getMcpRequiredFieldDefs(t)
|
||||
.filter((f: RequiredFieldDef) => requiredFields.includes(f.key))
|
||||
.map((f: RequiredFieldDef) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
description: f.description,
|
||||
passed: f.check(server),
|
||||
}));
|
||||
const passCount = checks.filter((c) => c.passed).length;
|
||||
const failCount = checks.length - passCount;
|
||||
const allPassed = checks.length > 0 && failCount === 0;
|
||||
|
|
|
|||
|
|
@ -3043,5 +3043,129 @@
|
|||
"updateFailed": "Failed to update guardrail",
|
||||
"notFound": "Guardrail not found"
|
||||
}
|
||||
},
|
||||
"mcpTools": {
|
||||
"byokCredentialModal": {
|
||||
"pleaseEnterApiKey": "Please enter your API key",
|
||||
"connectedSuccess": "Connected to {{name}}",
|
||||
"failedToConnect": "Failed to connect",
|
||||
"connectTitle": "Connect {{name}}",
|
||||
"connectDesc": "LiteLLM needs access to {{name}} to complete your request.",
|
||||
"howItWorksTitle": "How it works",
|
||||
"howItWorksDesc": "LiteLLM acts as a secure bridge. Your requests are routed through our MCP client directly to {{name}}'s API.",
|
||||
"requestedAccess": "Requested Access",
|
||||
"continueToAuth": "Continue to Authentication",
|
||||
"provideApiKeyTitle": "Provide API Key",
|
||||
"provideApiKeyDesc": "Enter your {{name}} API key to authorize this connection.",
|
||||
"apiKeyLabel": "{{name}} API Key",
|
||||
"apiKeyPlaceholder": "Enter your API key",
|
||||
"whereIsMyApiKey": "Where do I find my API key?",
|
||||
"saveKeyLabel": "Save key for future use",
|
||||
"securityNote": "Your key is stored securely and transmitted over HTTPS. It is never shared with third parties.",
|
||||
"connectAndAuthorize": "Connect & Authorize"
|
||||
},
|
||||
"envVarsSection": {
|
||||
"sectionTitle": "Variables",
|
||||
"tooltipDefine": "Define variables you can interpolate in Static Headers or Authentication using",
|
||||
"tooltipInstance": "admin-defined value used for every user.",
|
||||
"tooltipPerUser": "each user supplies their own value (e.g. personal credentials) via the MCP Gateway dashboard.",
|
||||
"referenceHint": "Reference these in Static Headers or Authentication as",
|
||||
"exampleLabel": "For example:",
|
||||
"colVarName": "Variable Name",
|
||||
"colValueDesc": "Value / Description",
|
||||
"colScope": "Scope",
|
||||
"scopeInstance": "Instance",
|
||||
"scopePerUser": "Per-user",
|
||||
"varNameRequired": "Variable name is required",
|
||||
"varNamePattern": "Use letters, digits, underscores; cannot start with a digit.",
|
||||
"perUserHintTooltip": "Per-user variables have no shared value. This text is only a hint shown to each user when they fill in their own value.",
|
||||
"hintLabel": "Hint",
|
||||
"perUserPlaceholder": "e.g. Your DB username",
|
||||
"valuePlaceholder": "e.g. postgresql",
|
||||
"addVariable": "Add Variable"
|
||||
},
|
||||
"mcpLogoSelector": {
|
||||
"logoLabel": "Logo",
|
||||
"logoTooltip": "Select a well-known logo or paste a URL to any image. The logo is shown on the admin and chat pages.",
|
||||
"selectedLogoAlt": "Selected logo",
|
||||
"customUrlPlaceholder": "Or paste a custom logo URL..."
|
||||
},
|
||||
"mcpNetworkSettings": {
|
||||
"privateIpRangesTitle": "Private IP Ranges",
|
||||
"privateIpRangesDesc": "Define which IP ranges are part of your private network. Callers from these IPs can see all MCP servers. Callers from any other IP can only see servers marked \"Available on Public Internet\".",
|
||||
"yourCurrentIp": "Your current IP:",
|
||||
"suggestedRange": "Suggested range:",
|
||||
"yourPrivateNetworkRanges": "Your Private Network Ranges",
|
||||
"rangesPlaceholder": "Leave empty to use defaults: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8",
|
||||
"cidrHelp": "Enter CIDR ranges (e.g., 10.0.0.0/8). When empty, standard private IP ranges are used."
|
||||
},
|
||||
"mcpPermissionManagement": {
|
||||
"panelTitle": "Permission Management / Access Control",
|
||||
"panelSubtitle": "Configure access permissions and security settings (Optional)",
|
||||
"allowAllKeysLabel": "Allow All LiteLLM Keys",
|
||||
"allowAllKeysTooltip": "When enabled, every API key can access this MCP server.",
|
||||
"allowAllKeysDesc": "Enable if this server should be \"public\" to all keys.",
|
||||
"internalNetworkOnlyLabel": "Internal network only",
|
||||
"internalNetworkOnlyTooltip": "When on, only requests from within your internal network are accepted. Turn off to allow external clients (other clusters, ChatGPT, etc). API key authentication is always required regardless of this setting.",
|
||||
"internalNetworkOnlyDesc": "Turn on to restrict access to callers within your internal network only.",
|
||||
"delegateAuthLabel": "Delegate auth to upstream (PKCE passthrough)",
|
||||
"delegateAuthTooltip": "When on, LiteLLM skips its own API key/SSO check for this server and lets the client complete PKCE directly with the upstream MCP server. Only honored when Auth Type is oauth2. No spend tracking or per-key rate limiting will run on this route.",
|
||||
"delegateAuthDesc": "Bypass LiteLLM auth so clients authenticate directly with the upstream OAuth MCP server.",
|
||||
"oauthPassthroughLabel": "OAuth pass-through",
|
||||
"oauthPassthroughTooltip": "When on, this server is treated as an OAuth pass-through: the gateway proxies the upstream /.well-known/oauth-protected-resource metadata, emits spec-compliant 401 challenges when no bearer is supplied, and propagates upstream 401/403 responses. Only honored when Auth Type is None and 'Authorization' is in Extra Headers.",
|
||||
"oauthPassthroughDesc": "Forward upstream OAuth discovery and 401 challenges so clients negotiate OAuth directly with the upstream MCP server.",
|
||||
"internalDelegateWarningTitle": "Internal server with upstream OAuth delegation",
|
||||
"internalDelegateWarningDesc": "This MCP server is configured as internal-only but delegates auth to upstream. Anonymous users will be able to reach the upstream OAuth2 /authorize flow without a LiteLLM session. Ensure your upstream provider and network enforce access controls.",
|
||||
"accessGroupsLabel": "MCP Access Groups",
|
||||
"accessGroupsTooltip": "Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.",
|
||||
"accessGroupsPlaceholder": "Select existing groups or type to create new ones",
|
||||
"extraHeadersLabel": "Extra Headers",
|
||||
"extraHeadersTooltip": "Forward custom headers from incoming requests to this MCP server (e.g., Authorization, X-Custom-Header, User-Agent)",
|
||||
"configuredCount": "{{count}} configured",
|
||||
"extraHeadersCurrently": "Currently: {{headers}}",
|
||||
"extraHeadersPlaceholder": "Enter header names (e.g., Authorization, X-Custom-Header)",
|
||||
"staticHeadersLabel": "Static Headers",
|
||||
"staticHeadersTooltip": "Send these key-value headers with every request to this MCP server.",
|
||||
"headerNameRequired": "Header name is required",
|
||||
"headerNamePlaceholder": "Header name (e.g., X-API-Key)",
|
||||
"headerValueRequired": "Header value is required",
|
||||
"headerValuePlaceholder": "Header value",
|
||||
"addStaticHeader": "Add Static Header"
|
||||
},
|
||||
"mcpServerCard": {
|
||||
"testConnection": "Test Connection",
|
||||
"serverActionsAriaLabel": "Server actions",
|
||||
"logoAlt": "{{name}} logo",
|
||||
"public": "Public",
|
||||
"internal": "Internal",
|
||||
"missingUserFieldsTitle": "Missing user fields:",
|
||||
"missingUserFields_one": "{{count}} user field missing",
|
||||
"missingUserFields_other": "{{count}} user fields missing",
|
||||
"setButton": "Set",
|
||||
"checking": "Checking",
|
||||
"healthLabel": "Health: {{status}}",
|
||||
"lastCheck": "Last check: {{time}}",
|
||||
"noHealthData": "No health data",
|
||||
"clickToRecheck": "Click to recheck",
|
||||
"byokCredential": "BYOK credential",
|
||||
"connected": "Connected",
|
||||
"connectButton": "Connect"
|
||||
},
|
||||
"mcpStandardsSettings": {
|
||||
"groupDocumentation": "Documentation",
|
||||
"fieldDescriptionLabel": "Description",
|
||||
"fieldDescriptionDesc": "Must have a non-empty description",
|
||||
"fieldAliasLabel": "Alias",
|
||||
"fieldAliasDesc": "Must have a display alias",
|
||||
"groupSource": "Source",
|
||||
"fieldSourceUrlLabel": "GitHub / Source URL",
|
||||
"fieldSourceUrlDesc": "Must link to a source repository",
|
||||
"groupConnection": "Connection",
|
||||
"fieldServerUrlLabel": "Server URL",
|
||||
"fieldServerUrlDesc": "Must have a URL configured",
|
||||
"groupSecurity": "Security",
|
||||
"fieldAuthLabel": "Auth configured",
|
||||
"fieldAuthDesc": "Must use authentication (not 'none')"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3043,5 +3043,129 @@
|
|||
"updateFailed": "更新护栏失败",
|
||||
"notFound": "未找到护栏"
|
||||
}
|
||||
},
|
||||
"mcpTools": {
|
||||
"byokCredentialModal": {
|
||||
"pleaseEnterApiKey": "请输入您的 API 密钥",
|
||||
"connectedSuccess": "已连接到 {{name}}",
|
||||
"failedToConnect": "连接失败",
|
||||
"connectTitle": "连接 {{name}}",
|
||||
"connectDesc": "LiteLLM 需要访问 {{name}} 以完成您的请求。",
|
||||
"howItWorksTitle": "工作原理",
|
||||
"howItWorksDesc": "LiteLLM 充当安全桥梁。您的请求通过我们的 MCP 客户端直接路由到 {{name}} 的 API。",
|
||||
"requestedAccess": "请求的访问权限",
|
||||
"continueToAuth": "继续进行认证",
|
||||
"provideApiKeyTitle": "提供 API 密钥",
|
||||
"provideApiKeyDesc": "请输入您的 {{name}} API 密钥以授权此连接。",
|
||||
"apiKeyLabel": "{{name}} API 密钥",
|
||||
"apiKeyPlaceholder": "输入您的 API 密钥",
|
||||
"whereIsMyApiKey": "在哪里可以找到我的 API 密钥?",
|
||||
"saveKeyLabel": "保存密钥以供以后使用",
|
||||
"securityNote": "您的密钥已安全存储并通过 HTTPS 传输,不会与第三方共享。",
|
||||
"connectAndAuthorize": "连接并授权"
|
||||
},
|
||||
"envVarsSection": {
|
||||
"sectionTitle": "变量",
|
||||
"tooltipDefine": "定义可在静态请求头或认证中使用的变量,插值方式为",
|
||||
"tooltipInstance": "由管理员定义,对所有用户生效。",
|
||||
"tooltipPerUser": "每位用户通过 MCP Gateway 仪表盘填入自己的值(例如个人凭据)。",
|
||||
"referenceHint": "在静态请求头或认证中以此方式引用这些变量:",
|
||||
"exampleLabel": "例如:",
|
||||
"colVarName": "变量名",
|
||||
"colValueDesc": "值 / 说明",
|
||||
"colScope": "作用域",
|
||||
"scopeInstance": "实例",
|
||||
"scopePerUser": "按用户",
|
||||
"varNameRequired": "变量名为必填项",
|
||||
"varNamePattern": "只能使用字母、数字、下划线,且不能以数字开头。",
|
||||
"perUserHintTooltip": "按用户变量没有共享值。此文本仅作为提示,在每位用户填写自己的值时显示。",
|
||||
"hintLabel": "提示",
|
||||
"perUserPlaceholder": "例如:您的数据库用户名",
|
||||
"valuePlaceholder": "例如:postgresql",
|
||||
"addVariable": "添加变量"
|
||||
},
|
||||
"mcpLogoSelector": {
|
||||
"logoLabel": "Logo",
|
||||
"logoTooltip": "选择常用 Logo 或粘贴任意图片 URL。Logo 将显示在管理页面和对话页面。",
|
||||
"selectedLogoAlt": "已选 Logo",
|
||||
"customUrlPlaceholder": "或粘贴自定义 Logo URL..."
|
||||
},
|
||||
"mcpNetworkSettings": {
|
||||
"privateIpRangesTitle": "私有 IP 范围",
|
||||
"privateIpRangesDesc": "定义哪些 IP 范围属于您的私有网络。来自这些 IP 的调用方可以看到所有 MCP 服务器;来自其他 IP 的调用方只能看到标记为「可在公共互联网上访问」的服务器。",
|
||||
"yourCurrentIp": "您当前的 IP:",
|
||||
"suggestedRange": "建议范围:",
|
||||
"yourPrivateNetworkRanges": "您的私有网络范围",
|
||||
"rangesPlaceholder": "留空以使用默认值:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8",
|
||||
"cidrHelp": "输入 CIDR 范围(例如:10.0.0.0/8)。留空时使用标准私有 IP 范围。"
|
||||
},
|
||||
"mcpPermissionManagement": {
|
||||
"panelTitle": "权限管理 / 访问控制",
|
||||
"panelSubtitle": "配置访问权限和安全设置(可选)",
|
||||
"allowAllKeysLabel": "允许所有 LiteLLM 密钥",
|
||||
"allowAllKeysTooltip": "启用后,每个 API 密钥都可以访问此 MCP 服务器。",
|
||||
"allowAllKeysDesc": "若希望此服务器对所有密钥「公开」,请启用此选项。",
|
||||
"internalNetworkOnlyLabel": "仅限内部网络",
|
||||
"internalNetworkOnlyTooltip": "开启后,仅接受来自内部网络的请求。关闭后可允许外部客户端(其他集群、ChatGPT 等)访问。无论此设置如何,始终需要 API 密钥认证。",
|
||||
"internalNetworkOnlyDesc": "开启后将访问限制为仅限内部网络中的调用方。",
|
||||
"delegateAuthLabel": "将认证委托给上游(PKCE 透传)",
|
||||
"delegateAuthTooltip": "开启后,LiteLLM 跳过自身的 API 密钥/SSO 检查,让客户端直接与上游 MCP 服务器完成 PKCE 认证。仅在认证类型为 oauth2 时生效。此路由不会执行花费追踪或按密钥速率限制。",
|
||||
"delegateAuthDesc": "绕过 LiteLLM 认证,让客户端直接与上游 OAuth MCP 服务器进行认证。",
|
||||
"oauthPassthroughLabel": "OAuth 透传",
|
||||
"oauthPassthroughTooltip": "开启后,此服务器将作为 OAuth 透传处理:网关代理上游 /.well-known/oauth-protected-resource 元数据,在未提供 bearer 令牌时发出符合规范的 401 挑战,并传播上游 401/403 响应。仅在认证类型为 None 且「Authorization」在额外请求头中时生效。",
|
||||
"oauthPassthroughDesc": "转发上游 OAuth 发现信息和 401 挑战,让客户端直接与上游 MCP 服务器协商 OAuth 认证。",
|
||||
"internalDelegateWarningTitle": "内部服务器已启用上游 OAuth 委托",
|
||||
"internalDelegateWarningDesc": "此 MCP 服务器配置为仅限内部访问,但已将认证委托给上游。匿名用户将能在没有 LiteLLM 会话的情况下访问上游 OAuth2 /authorize 流程。请确保您的上游提供商和网络已实施访问控制。",
|
||||
"accessGroupsLabel": "MCP 访问组",
|
||||
"accessGroupsTooltip": "为此 MCP 服务器指定访问组。用户必须属于至少一个访问组才能访问此服务器。",
|
||||
"accessGroupsPlaceholder": "选择现有组或输入新组名",
|
||||
"extraHeadersLabel": "额外请求头",
|
||||
"extraHeadersTooltip": "将传入请求中的自定义请求头转发到此 MCP 服务器(例如:Authorization、X-Custom-Header、User-Agent)",
|
||||
"configuredCount": "已配置 {{count}} 个",
|
||||
"extraHeadersCurrently": "当前:{{headers}}",
|
||||
"extraHeadersPlaceholder": "输入请求头名称(例如:Authorization、X-Custom-Header)",
|
||||
"staticHeadersLabel": "静态请求头",
|
||||
"staticHeadersTooltip": "向此 MCP 服务器的每次请求发送这些键值对请求头。",
|
||||
"headerNameRequired": "请求头名称为必填项",
|
||||
"headerNamePlaceholder": "请求头名称(例如:X-API-Key)",
|
||||
"headerValueRequired": "请求头值为必填项",
|
||||
"headerValuePlaceholder": "请求头值",
|
||||
"addStaticHeader": "添加静态请求头"
|
||||
},
|
||||
"mcpServerCard": {
|
||||
"testConnection": "测试连接",
|
||||
"serverActionsAriaLabel": "服务器操作",
|
||||
"logoAlt": "{{name}} Logo",
|
||||
"public": "公开",
|
||||
"internal": "内部",
|
||||
"missingUserFieldsTitle": "缺少用户字段:",
|
||||
"missingUserFields_one": "缺少 {{count}} 个用户字段",
|
||||
"missingUserFields_other": "缺少 {{count}} 个用户字段",
|
||||
"setButton": "设置",
|
||||
"checking": "检查中",
|
||||
"healthLabel": "健康状态:{{status}}",
|
||||
"lastCheck": "上次检查:{{time}}",
|
||||
"noHealthData": "暂无健康数据",
|
||||
"clickToRecheck": "点击重新检查",
|
||||
"byokCredential": "BYOK 凭据",
|
||||
"connected": "已连接",
|
||||
"connectButton": "连接"
|
||||
},
|
||||
"mcpStandardsSettings": {
|
||||
"groupDocumentation": "文档",
|
||||
"fieldDescriptionLabel": "描述",
|
||||
"fieldDescriptionDesc": "必须有非空描述",
|
||||
"fieldAliasLabel": "别名",
|
||||
"fieldAliasDesc": "必须有显示别名",
|
||||
"groupSource": "来源",
|
||||
"fieldSourceUrlLabel": "GitHub / 源码 URL",
|
||||
"fieldSourceUrlDesc": "必须链接到源码仓库",
|
||||
"groupConnection": "连接",
|
||||
"fieldServerUrlLabel": "服务器 URL",
|
||||
"fieldServerUrlDesc": "必须配置 URL",
|
||||
"groupSecurity": "安全",
|
||||
"fieldAuthLabel": "已配置认证",
|
||||
"fieldAuthDesc": "必须使用认证(不能为 'none')"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue