diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/AwsSigV4Fields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/AwsSigV4Fields.tsx index 5f64d003c11..b153d0a6456 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/AwsSigV4Fields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/AwsSigV4Fields.tsx @@ -1,5 +1,6 @@ import { Info } from "lucide-react"; import React from "react"; +import { useTranslation } from "react-i18next"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { MountedFormField } from "@/components/common_components/MountedFormField"; @@ -22,134 +23,150 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt const ACCESS_KEY_PATH = ["credentials", "aws_access_key_id"] as const; const SECRET_KEY_PATH = ["credentials", "aws_secret_access_key"] as const; -const AwsSigV4Fields: React.FC = () => ( - <> -

- For MCP servers hosted on AWS Bedrock AgentCore.{" "} - { + const { t } = useTranslation(); + return ( + <> +

+ {t("mcpServers.awsSigV4Fields.intro")}{" "} + + {t("mcpServers.awsSigV4Fields.viewDocs")} + +

+ + } + name={["credentials", "aws_region_name"]} + required + rules={{ validate: { required: requiredRule(t("mcpServers.awsSigV4Fields.region.required")) } }} > - View docs → - -

- } - name={["credentials", "aws_region_name"]} - required - rules={{ validate: { required: requiredRule("AWS region is required for SigV4 auth") } }} - > - {(control) => } - - - } - name={["credentials", "aws_service_name"]} - > - {(control) => } - - - } - name={ACCESS_KEY_PATH} - rules={{ - deps: ["credentials.aws_secret_access_key"], - validate: { - pairedWithSecret: requiredWhenSiblingSet( - SECRET_KEY_PATH, - "Access Key ID is required when Secret Access Key is provided", - ), - }, - }} - > - {(control) => ( - - )} - - - } - name={SECRET_KEY_PATH} - rules={{ - deps: ["credentials.aws_access_key_id"], - validate: { - pairedWithAccessKey: requiredWhenSiblingSet( - ACCESS_KEY_PATH, - "Secret Access Key is required when Access Key ID is provided", - ), - }, - }} - > - {(control) => ( - - )} - - } - name={["credentials", "aws_session_token"]} - > - {(control) => ( - - )} - - - } - name={["credentials", "aws_role_name"]} - > - {(control) => ( - - )} - - - } - name={["credentials", "aws_session_name"]} - > - {(control) => ( - - )} - - -); + {(control) => } +
+ + } + name={["credentials", "aws_service_name"]} + > + {(control) => } + + + } + name={ACCESS_KEY_PATH} + rules={{ + deps: ["credentials.aws_secret_access_key"], + validate: { + pairedWithSecret: requiredWhenSiblingSet( + SECRET_KEY_PATH, + t("mcpServers.awsSigV4Fields.accessKeyId.requiredWithSecret"), + ), + }, + }} + > + {(control) => ( + + )} + + + } + name={SECRET_KEY_PATH} + rules={{ + deps: ["credentials.aws_access_key_id"], + validate: { + pairedWithAccessKey: requiredWhenSiblingSet( + ACCESS_KEY_PATH, + t("mcpServers.awsSigV4Fields.secretAccessKey.requiredWithAccessKey"), + ), + }, + }} + > + {(control) => ( + + )} + + + } + name={["credentials", "aws_session_token"]} + > + {(control) => ( + + )} + + + } + name={["credentials", "aws_role_name"]} + > + {(control) => ( + + )} + + + } + name={["credentials", "aws_session_name"]} + > + {(control) => ( + + )} + + + ); +}; export default AwsSigV4Fields; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index 07618abb9a4..14a6e3d941f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -1,5 +1,7 @@ +import type { TFunction } from "i18next"; import React, { useState } from "react"; import { FormProvider, useForm, useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { ChevronDown, Info } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; @@ -35,7 +37,7 @@ import { buildCreateServerPayload, reduceStaticHeaders, } from "./createServerPayload"; -import { DUPLICATE_IDENTIFIER_MESSAGE, findDuplicateMcpServer, mcpSubmitErrorReason } from "./duplicateServerCheck"; +import { DUPLICATE_IDENTIFIER_MESSAGE_KEY, findDuplicateMcpServer, mcpSubmitErrorReason } from "./duplicateServerCheck"; import { readCreateUiSnapshot, writeCreateUiSnapshot } from "./createOAuthUiState"; import AwsSigV4Fields from "./AwsSigV4Fields"; import OpenApiByokFields from "./OpenApiByokFields"; @@ -86,14 +88,14 @@ interface CreateMCPServerProps { stdioEnabled?: boolean; } -const payloadErrorMessage = (result: Exclude): string => { +const payloadErrorMessage = (result: Exclude, t: TFunction): string => { switch (result.kind) { case "invalid_tool_display_name": - return `Tool display name "${result.displayName}" is invalid. Only letters, digits, underscores, and hyphens are allowed (no spaces).`; + return t("mcpServers.createMcpServer.payloadErrors.invalidToolDisplayName", { displayName: result.displayName }); case "invalid_stdio_json": - return "Invalid JSON in stdio configuration"; + return t("mcpServers.createMcpServer.payloadErrors.invalidStdioJson"); case "invalid_token_validation_json": - return "Invalid JSON in Token Validation Rules"; + return t("mcpServers.createMcpServer.payloadErrors.invalidTokenValidationJson"); } }; @@ -117,6 +119,7 @@ const CreateMCPServer: React.FC = ({ onBackToDiscovery, stdioEnabled = true, }) => { + const { t } = useTranslation(); const form = useForm({ mode: "onChange", defaultValues: CREATE_DEFAULTS }); const registry = useMountRegistry(); const [isLoading, setIsLoading] = useState(false); @@ -255,9 +258,7 @@ const CreateMCPServer: React.FC = ({ // which would persist it as server-level credentials on the created server row. Mirrors the // edit form's onTokenReceived early return. setAuthorizedIdentity(getOAuthAuthorizationIdentity(allFieldsValue(form))); - toast.success( - "Token held for this browser session. Tools can now be previewed and configured; the token is not saved to LiteLLM.", - ); + toast.success(t("mcpServers.createMcpServer.toasts.browserTokenHeld")); return; } @@ -283,7 +284,7 @@ const CreateMCPServer: React.FC = ({ // its own credential write. setAuthorizedIdentity(getOAuthAuthorizationIdentity(allFieldsValue(form))); - toast.success("OAuth authorization successful! Please click 'Create MCP Server' to save the configuration."); + toast.success(t("mcpServers.createMcpServer.toasts.oauthAuthorized")); }, onBeforeRedirect: persistCreateUiState, flowSource: "create", @@ -425,8 +426,8 @@ const CreateMCPServer: React.FC = ({ typeof values.alias === "string" ? values.alias : undefined, ); if (duplicate) { - form.setError(duplicate.field, { type: "duplicate", message: DUPLICATE_IDENTIFIER_MESSAGE }); - toast.fromError(DUPLICATE_IDENTIFIER_MESSAGE); + form.setError(duplicate.field, { type: "duplicate", message: t(DUPLICATE_IDENTIFIER_MESSAGE_KEY) }); + toast.fromError(t(DUPLICATE_IDENTIFIER_MESSAGE_KEY)); return; } const built = buildCreateServerPayload(values, { @@ -440,7 +441,7 @@ const CreateMCPServer: React.FC = ({ dcrClient: dcrClientRef.current, }); if (built.kind !== "ok") { - toast.fromError(payloadErrorMessage(built)); + toast.fromError(payloadErrorMessage(built, t)); return; } const payload = built.payload; @@ -482,10 +483,10 @@ const CreateMCPServer: React.FC = ({ } if (isAdmin) { - toast.success("MCP Server created successfully"); + toast.success(t("mcpServers.createMcpServer.toasts.created")); } else { - toast.success("MCP Server submitted for admin review", { - description: "Once an admin approves it, the server will appear in your MCP Servers list.", + toast.success(t("mcpServers.createMcpServer.toasts.submittedForReview"), { + description: t("mcpServers.createMcpServer.toasts.submittedForReviewDescription"), }); } form.reset(CREATE_DEFAULTS); @@ -500,7 +501,11 @@ const CreateMCPServer: React.FC = ({ } } catch (error) { const reason = mcpSubmitErrorReason(error); - toast.fromError(isAdmin ? `Error creating MCP Server: ${reason}` : `Error submitting MCP Server: ${reason}`); + toast.fromError( + isAdmin + ? t("mcpServers.createMcpServer.toasts.createFailed", { reason }) + : t("mcpServers.createMcpServer.toasts.submitFailed", { reason }), + ); } finally { setIsLoading(false); } @@ -640,9 +645,11 @@ const CreateMCPServer: React.FC = ({ ← )} - MCP Logo + {t("mcpServers.createMcpServer.logoAlt")} - {isAdmin ? "Add New MCP Server" : "Submit MCP Server for Review"} + {isAdmin + ? t("mcpServers.createMcpServer.title.admin") + : t("mcpServers.createMcpServer.title.submitForReview")} @@ -653,27 +660,26 @@ const CreateMCPServer: React.FC = ({
{!isAdmin && (
- Your submission will be sent for admin review. Once approved, the server will appear in your MCP - Servers list. The request must be made with a team-scoped API key. + {t("mcpServers.createMcpServer.reviewNotice")}
)}
- MCP Server Name - + {t("mcpServers.createMcpServer.serverName.label")} + } name="server_name" - rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value) }) }} + rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value, t) }) }} > {(control) => ( )} @@ -682,19 +688,19 @@ const CreateMCPServer: React.FC = ({ - Alias - + {t("mcpServers.createMcpServer.alias.label")} + } name="alias" - rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value) }) }} + rules={{ validate: validatorRules({ validator: (_, value) => validateMCPServerName(value, t) }) }} > {(control) => ( { control.onChange(event); @@ -705,13 +711,17 @@ const CreateMCPServer: React.FC = ({ Description} + label={ + + {t("mcpServers.createMcpServer.description.label")} + + } name="description" > {(control) => ( )} @@ -720,7 +730,11 @@ const CreateMCPServer: React.FC = ({ GitHub / Source URL} + label={ + + {t("mcpServers.createMcpServer.sourceUrl.label")} + + } name="source_url" > {(control) => ( @@ -733,10 +747,14 @@ const CreateMCPServer: React.FC = ({ Transport Type} + label={ + + {t("mcpServers.createMcpServer.transport.label")} + + } name="transport" required - rules={{ validate: { required: requiredRule("Please select a transport type") } }} + rules={{ validate: { required: requiredRule(t("mcpServers.createMcpServer.transport.required")) } }} > {(control) => ( (control)} items={AUTH_TYPE_ITEMS}> - + {AUTH_TYPE_ITEMS.map((item) => ( @@ -886,8 +912,8 @@ const CreateMCPServer: React.FC = ({ - Authentication Value - + {t("mcpServers.createMcpServer.auth.valueLabel")} + @@ -895,14 +921,14 @@ const CreateMCPServer: React.FC = ({ name={["credentials", "auth_value"]} rules={{ validate: { - notWhitespace: notOnlyWhitespace("Authentication value cannot be empty whitespace"), + notWhitespace: notOnlyWhitespace(t("mcpServers.createMcpServer.auth.valueWhitespace")), }, }} > {(control) => ( )} @@ -1000,11 +1026,13 @@ const CreateMCPServer: React.FC = ({
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx index 893981fa9c6..48a8f4d7a64 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx @@ -1,5 +1,6 @@ import { Info } from "lucide-react"; import React from "react"; +import { useTranslation } from "react-i18next"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { Switch } from "@/components/ui/switch"; @@ -23,13 +24,14 @@ export default function DcrBridgeToggle({ authType?: string | null; initialChecked?: boolean; }) { + const { t } = useTranslation(); if (!isClientForwardedTokenMode(authType)) return null; return ( - Gateway-hosted sign-in (DCR bridge) - + {t("mcpServers.dcrBridgeToggle.label")} + diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx index 378410e4790..a804d4697c4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx @@ -1,5 +1,7 @@ +import type { ParseKeys } from "i18next"; import { CircleMinus, Info, Plus } from "lucide-react"; import React from "react"; +import { Trans, useTranslation } from "react-i18next"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; @@ -16,11 +18,13 @@ import { requiredRule } from "@/components/common_components/formRules"; import { matchesPattern, selectControl, selectTriggerControl, textControl } from "./mcpFieldRules"; import { listControl } from "./mcpFormStore"; -const SCOPE_OPTIONS = [ - { value: "global", label: "Instance" }, - { value: "user", label: "Per-user" }, +const SCOPE_OPTIONS: ReadonlyArray<{ value: string; labelKey: ParseKeys }> = [ + { value: "global", labelKey: "mcpServers.envVarsSection.scopes.instance" }, + { value: "user", labelKey: "mcpServers.envVarsSection.scopes.perUser" }, ]; +const VARIABLE_SYNTAX = "${VAR_NAME}"; + const VARIABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; /** @@ -34,31 +38,34 @@ const VARIABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; * The parent form reads the ``env_vars`` field from the form values. */ const EnvVarsSection: React.FC = () => { + const { t } = useTranslation(); const { control } = useFormContext(); const { fields, append, remove } = useFieldArray({ control: listControl(control), name: "env_vars" }); useMountedName("env_vars"); + const scopeOptions = SCOPE_OPTIONS.map((option) => ({ value: option.value, label: t(option.labelKey) })); return (
- Variables + {t("mcpServers.envVarsSection.title")} - Define variables you can interpolate in Static Headers or Authentication using{" "} - {"${VAR_NAME}"}.
- Instance: admin-defined value used for every user. -
- Per-user: each user supplies their own value (e.g. personal credentials) via the MCP Gateway - dashboard. - + , b: }} + /> } >
- Reference these in Static Headers or Authentication as {"${VAR_NAME}"}. For example:{" "} + }} + />{" "} {"${DB_PROTOCOL}://${CORP_USERNAME}:${CORP_PASSWORD}@${DB_HOSTNAME}"} @@ -67,9 +74,9 @@ const EnvVarsSection: React.FC = () => {
{fields.length > 0 && (
-
Variable Name
-
Value / Description
-
Scope
+
{t("mcpServers.envVarsSection.columns.name")}
+
{t("mcpServers.envVarsSection.columns.value")}
+
{t("mcpServers.envVarsSection.columns.scope")}
)} @@ -80,16 +87,17 @@ const EnvVarsSection: React.FC = () => { className="mb-0 flex-1" rules={{ validate: { - required: requiredRule("Variable name is required"), - pattern: matchesPattern( - VARIABLE_NAME_PATTERN, - "Use letters, digits, underscores; cannot start with a digit.", - ), + required: requiredRule(t("mcpServers.envVarsSection.name.required")), + pattern: matchesPattern(VARIABLE_NAME_PATTERN, t("mcpServers.envVarsSection.name.invalid")), }, }} > {(control) => ( - + )}
@@ -97,12 +105,12 @@ const EnvVarsSection: React.FC = () => {
{(control) => ( - (control)} items={scopeOptions}> - {SCOPE_OPTIONS.map((option) => ( + {scopeOptions.map((option) => ( {option.label} @@ -121,7 +129,7 @@ const EnvVarsSection: React.FC = () => { ))}
@@ -132,6 +140,7 @@ const EnvVarsSection: React.FC = () => { // vars the value comes from each user later, so the column instead captures an // optional description that the per-user fill-in modal shows as a hint. const ScopedValueOrDescription: React.FC<{ index: number }> = ({ index }) => { + const { t } = useTranslation(); const isPerUser = useWatch({ name: `env_vars.${index}.scope` }) === "user"; if (isPerUser) { return ( @@ -139,16 +148,16 @@ const ScopedValueOrDescription: React.FC<{ index: number }> = ({ index }) => { {(control) => ( - + - Hint + {t("mcpServers.envVarsSection.hint.label")} @@ -158,7 +167,13 @@ const ScopedValueOrDescription: React.FC<{ index: number }> = ({ index }) => { } return ( - {(control) => } + {(control) => ( + + )} ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx index 70d1bc40c18..98bb3254ba5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx @@ -1,5 +1,6 @@ import { Info } from "lucide-react"; import React from "react"; +import { useTranslation } from "react-i18next"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { MountedFormField } from "@/components/common_components/MountedFormField"; @@ -27,9 +28,10 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt ); const PRIVATE_KEY_PATH = ["credentials", "client_private_key"] as const; +const PEM_HEADER = "-----BEGIN PRIVATE KEY-----"; const IdJagFormFields: React.FC = ({ isEditing = false }) => { - const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : ""; + const { t } = useTranslation(); const requiredWhenCreating = (message: string) => isEditing ? undefined : { validate: { required: requiredRule(message) } }; @@ -38,13 +40,13 @@ const IdJagFormFields: React.FC = ({ isEditing = false }) } name="token_exchange_endpoint" required={!isEditing} - rules={requiredWhenCreating("The org token endpoint is required for ID-JAG")} + rules={requiredWhenCreating(t("mcpServers.idJagFormFields.orgTokenEndpoint.required"))} > {(control) => ( = ({ isEditing = false }) } name={["credentials", "id_jag_resource_token_endpoint"]} required={!isEditing} - rules={requiredWhenCreating("The resource token endpoint is required for ID-JAG")} + rules={requiredWhenCreating(t("mcpServers.idJagFormFields.resourceTokenEndpoint.required"))} > {(control) => ( = ({ isEditing = false }) )} } + label={ + + } name={["credentials", "client_id"]} required={!isEditing} - rules={requiredWhenCreating("Client ID is required for ID-JAG")} + rules={requiredWhenCreating(t("mcpServers.idJagFormFields.clientId.required"))} > {(control) => ( )} @@ -90,8 +101,8 @@ const IdJagFormFields: React.FC = ({ isEditing = false }) } name={["credentials", "client_secret"]} @@ -103,7 +114,7 @@ const IdJagFormFields: React.FC = ({ isEditing = false }) validate: { secretOrPrivateKey: requiredUnlessSiblingSet( PRIVATE_KEY_PATH, - "Provide either a client secret or a client private key", + t("mcpServers.idJagFormFields.clientSecret.secretOrPrivateKey"), ), }, } @@ -112,7 +123,11 @@ const IdJagFormFields: React.FC = ({ isEditing = false }) {(control) => ( )} @@ -120,8 +135,8 @@ const IdJagFormFields: React.FC = ({ isEditing = false }) } name={PRIVATE_KEY_PATH} @@ -130,7 +145,11 @@ const IdJagFormFields: React.FC = ({ isEditing = false })