mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for mcp_tools batch 2
This commit is contained in:
parent
c3617bc3d2
commit
dff009460e
5 changed files with 366 additions and 117 deletions
|
|
@ -28,29 +28,32 @@ import {
|
|||
} from "./MCPStandardsSettings";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
type MCPStatus = "active" | "pending_review" | "rejected";
|
||||
|
||||
const STATUS_CONFIG: Record<MCPStatus, { label: string; bg: string; text: string; dot: string }> = {
|
||||
const getStatusConfig = (
|
||||
t: TFunction,
|
||||
): Record<MCPStatus, { label: string; bg: string; text: string; dot: string }> => ({
|
||||
active: {
|
||||
label: "Active",
|
||||
label: t("mcpTools.mCPSubmissionsTab.statusActive"),
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
dot: "bg-green-500",
|
||||
},
|
||||
pending_review: {
|
||||
label: "Pending Review",
|
||||
label: t("mcpTools.mCPSubmissionsTab.statusPendingReview"),
|
||||
bg: "bg-yellow-50",
|
||||
text: "text-yellow-700",
|
||||
dot: "bg-yellow-500",
|
||||
},
|
||||
rejected: {
|
||||
label: "Rejected",
|
||||
label: t("mcpTools.mCPSubmissionsTab.statusRejected"),
|
||||
bg: "bg-red-50",
|
||||
text: "text-red-700",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function formatDate(value: string | null | undefined): string {
|
||||
if (!value) return "—";
|
||||
|
|
@ -80,11 +83,12 @@ type ConfirmDialogProps = {
|
|||
};
|
||||
|
||||
function ConfirmDialog({ action, serverName, isCurrentlyActive, onConfirm, onCancel }: ConfirmDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [reviewNotes, setReviewNotes] = useState("");
|
||||
const isApprove = action === "approve";
|
||||
const rejectBody = isCurrentlyActive
|
||||
? "This server is currently live. Rejecting it will immediately remove it from the proxy runtime."
|
||||
: "This will mark the submission as rejected.";
|
||||
? t("mcpTools.mCPSubmissionsTab.rejectBodyActive")
|
||||
: t("mcpTools.mCPSubmissionsTab.rejectBodyInactive");
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
|
||||
|
|
@ -100,15 +104,18 @@ function ConfirmDialog({ action, serverName, isCurrentlyActive, onConfirm, onCan
|
|||
)}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-1">
|
||||
{isApprove ? "Approve MCP Server" : "Reject MCP Server"}
|
||||
{isApprove
|
||||
? t("mcpTools.mCPSubmissionsTab.confirmApproveMcpServer")
|
||||
: t("mcpTools.mCPSubmissionsTab.confirmRejectMcpServer")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Are you sure you want to {action} <span className="font-medium text-gray-700">"{serverName}"</span>?{" "}
|
||||
{isApprove ? "This will make it active and available for use." : rejectBody}
|
||||
{t("mcpTools.mCPSubmissionsTab.confirmAreYouSure", { action })}{" "}
|
||||
<span className="font-medium text-gray-700">"{serverName}"</span>?{" "}
|
||||
{isApprove ? t("mcpTools.mCPSubmissionsTab.confirmApproveBody") : rejectBody}
|
||||
</p>
|
||||
{!isApprove && (
|
||||
<textarea
|
||||
placeholder="Reason for rejection (optional)"
|
||||
placeholder={t("mcpTools.mCPSubmissionsTab.rejectionReasonPlaceholder")}
|
||||
value={reviewNotes}
|
||||
onChange={(e) => setReviewNotes(e.target.value)}
|
||||
className="w-full border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 mb-4 resize-none"
|
||||
|
|
@ -121,7 +128,7 @@ function ConfirmDialog({ action, serverName, isCurrentlyActive, onConfirm, onCan
|
|||
onClick={onCancel}
|
||||
className="flex-1 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -130,7 +137,7 @@ function ConfirmDialog({ action, serverName, isCurrentlyActive, onConfirm, onCan
|
|||
isApprove ? "bg-green-500 hover:bg-green-600" : "bg-red-500 hover:bg-red-600"
|
||||
}`}
|
||||
>
|
||||
{isApprove ? "Approve" : "Reject"}
|
||||
{isApprove ? t("mcpTools.mCPSubmissionsTab.approveButton") : t("mcpTools.mCPSubmissionsTab.rejectButton")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -165,13 +172,15 @@ function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: Su
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm font-semibold text-gray-800">Submission Rules</span>
|
||||
<span className="text-sm font-semibold text-gray-800">
|
||||
{t("mcpTools.mCPSubmissionsTab.submissionRulesTitle")}
|
||||
</span>
|
||||
{activeLabels.length > 0 ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
({activeLabels.length} required field{activeLabels.length !== 1 ? "s" : ""})
|
||||
({t("mcpTools.mCPSubmissionsTab.requiredFieldCount", { count: activeLabels.length })})
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 italic">no rules set</span>
|
||||
<span className="text-xs text-gray-400 italic">{t("mcpTools.mCPSubmissionsTab.noRulesSet")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -200,10 +209,7 @@ function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: Su
|
|||
{/* Expanded editor */}
|
||||
{expanded && (
|
||||
<div className="border-t border-gray-100 px-4 pt-4 pb-4">
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Select which fields must be filled in before a submission is considered compliant. LiteLLM will show ✓ / ✗
|
||||
for each rule on every submission card below.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-4">{t("mcpTools.mCPSubmissionsTab.submissionRulesDesc")}</p>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5">
|
||||
{fieldGroups.map((group: FieldGroup) => (
|
||||
<div key={group.label}>
|
||||
|
|
@ -242,14 +248,14 @@ function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: Su
|
|||
}}
|
||||
className="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors"
|
||||
>
|
||||
{isSaving ? "Saving…" : "Save Rules"}
|
||||
{isSaving ? t("common.saving") : t("mcpTools.mCPSubmissionsTab.saveRulesButton")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(false)}
|
||||
className="px-4 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -267,8 +273,9 @@ type MCPServerCardProps = {
|
|||
|
||||
function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServerCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const statusConfig = getStatusConfig(t);
|
||||
const approvalStatus = (server.approval_status ?? "active") as MCPStatus;
|
||||
const statusCfg = STATUS_CONFIG[approvalStatus] ?? STATUS_CONFIG["active"];
|
||||
const statusCfg = statusConfig[approvalStatus] ?? statusConfig["active"];
|
||||
|
||||
const checks = getMcpRequiredFieldDefs(t)
|
||||
.filter((f: RequiredFieldDef) => requiredFields.includes(f.key))
|
||||
|
|
@ -308,17 +315,21 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
)}
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-gray-400">
|
||||
<span>
|
||||
Transport: <span className="text-gray-600">{server.transport ?? "sse"}</span>
|
||||
{t("mcpTools.mCPSubmissionsTab.transportLabel")}{" "}
|
||||
<span className="text-gray-600">{server.transport ?? "sse"}</span>
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>
|
||||
Submitted by: <span className="text-gray-600">{server.submitted_by ?? "—"}</span>
|
||||
{t("mcpTools.mCPSubmissionsTab.submittedByLabel")}{" "}
|
||||
<span className="text-gray-600">{server.submitted_by ?? "—"}</span>
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{formatDate(server.submitted_at)}</span>
|
||||
</div>
|
||||
{approvalStatus === "rejected" && server.review_notes && (
|
||||
<p className="text-xs text-red-600 mt-1.5">Rejection reason: {server.review_notes}</p>
|
||||
<p className="text-xs text-red-600 mt-1.5">
|
||||
{t("mcpTools.mCPSubmissionsTab.rejectionReasonLabel", { reason: server.review_notes })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Approve/Reject when no checks panel (no rules configured) */}
|
||||
|
|
@ -330,7 +341,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
onClick={onApprove}
|
||||
className="text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Approve
|
||||
{t("mcpTools.mCPSubmissionsTab.approveButton")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
|
@ -338,7 +349,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
onClick={onReject}
|
||||
className="text-xs border border-red-300 text-red-600 hover:bg-red-50 px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Reject
|
||||
{t("mcpTools.mCPSubmissionsTab.rejectButton")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -349,7 +360,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
onClick={onApprove}
|
||||
className="text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Re-approve
|
||||
{t("mcpTools.mCPSubmissionsTab.reapproveButton")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -375,10 +386,12 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm font-semibold leading-tight ${allPassed ? "text-green-800" : "text-red-800"}`}>
|
||||
{allPassed ? "All checks passed" : `${failCount} check${failCount !== 1 ? "s" : ""} failed`}
|
||||
{allPassed
|
||||
? t("mcpTools.mCPSubmissionsTab.allChecksPassed")
|
||||
: t("mcpTools.mCPSubmissionsTab.checksFailed", { count: failCount })}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{passCount} passing, {failCount} failing
|
||||
{t("mcpTools.mCPSubmissionsTab.checksPassFail", { passCount, failCount })}
|
||||
</div>
|
||||
</div>
|
||||
{/* Approve / Reject in header */}
|
||||
|
|
@ -389,7 +402,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
onClick={onApprove}
|
||||
className="text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Approve
|
||||
{t("mcpTools.mCPSubmissionsTab.approveButton")}
|
||||
</button>
|
||||
)}
|
||||
{approvalStatus === "rejected" && (
|
||||
|
|
@ -398,7 +411,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
onClick={onApprove}
|
||||
className="text-xs bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Re-approve
|
||||
{t("mcpTools.mCPSubmissionsTab.reapproveButton")}
|
||||
</button>
|
||||
)}
|
||||
{approvalStatus !== "rejected" && (
|
||||
|
|
@ -407,7 +420,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
onClick={onReject}
|
||||
className="text-xs border border-red-300 text-red-600 hover:bg-red-50 bg-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Reject
|
||||
{t("mcpTools.mCPSubmissionsTab.rejectButton")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -431,7 +444,9 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
</div>
|
||||
<span className={`text-sm flex-1 ${c.passed ? "text-gray-700" : "text-gray-800"}`}>{c.label}</span>
|
||||
<span className={`text-xs ${c.passed ? "text-green-600" : "text-red-500"}`}>
|
||||
{c.passed ? "Passes" : "Missing"}
|
||||
{c.passed
|
||||
? t("mcpTools.mCPSubmissionsTab.checkPasses")
|
||||
: t("mcpTools.mCPSubmissionsTab.checkMissing")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -447,6 +462,7 @@ interface MCPSubmissionsTabProps {
|
|||
}
|
||||
|
||||
export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const [summary, setSummary] = useState<MCPSubmissionsSummary>({
|
||||
total: 0,
|
||||
pending_review: 0,
|
||||
|
|
@ -492,7 +508,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load submissions");
|
||||
setError(err instanceof Error ? err.message : t("mcpTools.mCPSubmissionsTab.failedToLoadSubmissions"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -507,9 +523,9 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
setIsSavingRules(true);
|
||||
try {
|
||||
await updateConfigFieldSetting(accessToken, SETTINGS_KEY, requiredFields);
|
||||
NotificationsManager.success("Submission rules saved");
|
||||
NotificationsManager.success(t("mcpTools.mCPSubmissionsTab.submissionRulesSaved"));
|
||||
} catch {
|
||||
NotificationsManager.fromBackend("Failed to save submission rules");
|
||||
NotificationsManager.fromBackend(t("mcpTools.mCPSubmissionsTab.failedToSaveRules"));
|
||||
} finally {
|
||||
setIsSavingRules(false);
|
||||
}
|
||||
|
|
@ -531,9 +547,9 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
try {
|
||||
await approveMCPServer(accessToken, serverId);
|
||||
await fetchData();
|
||||
NotificationsManager.success(`MCP server "${serverName}" approved`);
|
||||
NotificationsManager.success(t("mcpTools.mCPSubmissionsTab.approvedSuccess", { name: serverName }));
|
||||
} catch {
|
||||
NotificationsManager.fromBackend("Failed to approve MCP server");
|
||||
NotificationsManager.fromBackend(t("mcpTools.mCPSubmissionsTab.failedToApprove"));
|
||||
} finally {
|
||||
setConfirmAction(null);
|
||||
}
|
||||
|
|
@ -544,9 +560,9 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
try {
|
||||
await rejectMCPServer(accessToken, serverId, reviewNotes);
|
||||
await fetchData();
|
||||
NotificationsManager.success(`MCP server "${serverName}" rejected`);
|
||||
NotificationsManager.success(t("mcpTools.mCPSubmissionsTab.rejectedSuccess", { name: serverName }));
|
||||
} catch {
|
||||
NotificationsManager.fromBackend("Failed to reject MCP server");
|
||||
NotificationsManager.fromBackend(t("mcpTools.mCPSubmissionsTab.failedToReject"));
|
||||
} finally {
|
||||
setConfirmAction(null);
|
||||
}
|
||||
|
|
@ -563,10 +579,18 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
/>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<StatCard label="Total Submitted" value={summary.total} color="text-gray-900" />
|
||||
<StatCard label="Pending Review" value={summary.pending_review} color="text-yellow-600" />
|
||||
<StatCard label="Active" value={summary.active} color="text-green-600" />
|
||||
<StatCard label="Rejected" value={summary.rejected} color="text-red-600" />
|
||||
<StatCard
|
||||
label={t("mcpTools.mCPSubmissionsTab.statTotalSubmitted")}
|
||||
value={summary.total}
|
||||
color="text-gray-900"
|
||||
/>
|
||||
<StatCard
|
||||
label={t("mcpTools.mCPSubmissionsTab.statPendingReview")}
|
||||
value={summary.pending_review}
|
||||
color="text-yellow-600"
|
||||
/>
|
||||
<StatCard label={t("mcpTools.mCPSubmissionsTab.statActive")} value={summary.active} color="text-green-600" />
|
||||
<StatCard label={t("mcpTools.mCPSubmissionsTab.statRejected")} value={summary.rejected} color="text-red-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
|
|
@ -574,7 +598,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search MCP servers..."
|
||||
placeholder={t("common.searchPlaceholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||
|
|
@ -585,18 +609,24 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
|
||||
className="border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="pending_review">Pending Review</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="all">{t("mcpTools.mCPSubmissionsTab.filterAllStatus")}</option>
|
||||
<option value="pending_review">{t("mcpTools.mCPSubmissionsTab.statusPendingReview")}</option>
|
||||
<option value="active">{t("mcpTools.mCPSubmissionsTab.statusActive")}</option>
|
||||
<option value="rejected">{t("mcpTools.mCPSubmissionsTab.statusRejected")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{isLoading && <div className="text-center py-12 text-gray-500 text-sm">Loading submissions…</div>}
|
||||
{isLoading && (
|
||||
<div className="text-center py-12 text-gray-500 text-sm">
|
||||
{t("mcpTools.mCPSubmissionsTab.loadingSubmissions")}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-center py-12 text-red-600 text-sm">{error}</div>}
|
||||
{!isLoading && !error && filtered.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-400 text-sm">No MCP server submissions match your filters.</div>
|
||||
<div className="text-center py-12 text-gray-400 text-sm">
|
||||
{t("mcpTools.mCPSubmissionsTab.noSubmissionsMatch")}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { forwardRef, useImperativeHandle, useMemo } from "react";
|
|||
import { Form, Input, InputNumber, Select, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { MCPTool, InputSchema, InputSchemaProperty } from "./types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, any> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
|
@ -140,6 +141,7 @@ interface MCPToolArgumentsFormProps {
|
|||
|
||||
const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgumentsFormProps>(
|
||||
({ tool, className }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const schema: InputSchema = useMemo(() => {
|
||||
|
|
@ -149,14 +151,14 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
properties: {
|
||||
input: {
|
||||
type: "string",
|
||||
description: "Input for this tool",
|
||||
description: t("mcpTools.mCPToolArgumentsForm.inputDescription"),
|
||||
},
|
||||
},
|
||||
required: ["input"],
|
||||
};
|
||||
}
|
||||
return tool.inputSchema as InputSchema;
|
||||
}, [tool.inputSchema]);
|
||||
}, [tool.inputSchema, t]);
|
||||
|
||||
const actualSchema: InputSchema = useMemo(() => {
|
||||
if (schema.properties?.params?.type === "object" && schema.properties.params.properties) {
|
||||
|
|
@ -193,13 +195,13 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Input <span className="text-red-500">*</span>
|
||||
{t("mcpTools.mCPToolArgumentsForm.inputLabel")} <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="input"
|
||||
rules={[{ required: true, message: "Please enter input for this tool" }]}
|
||||
rules={[{ required: true, message: t("mcpTools.mCPToolArgumentsForm.inputRequired") }]}
|
||||
>
|
||||
<Input placeholder="Enter input for this tool" />
|
||||
<Input placeholder={t("mcpTools.mCPToolArgumentsForm.inputPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
|
@ -208,7 +210,9 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
if (!actualSchema.properties) {
|
||||
return (
|
||||
<Form form={form} layout="vertical" className={className}>
|
||||
<div className="py-4 text-center text-sm text-gray-500">No parameters required for this tool.</div>
|
||||
<div className="py-4 text-center text-sm text-gray-500">
|
||||
{t("mcpTools.mCPToolArgumentsForm.noParameters")}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -236,7 +240,7 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
rules={[
|
||||
{
|
||||
required: actualSchema.required?.includes(key),
|
||||
message: `Please enter ${key}`,
|
||||
message: t("mcpTools.mCPToolArgumentsForm.fieldRequired", { key }),
|
||||
},
|
||||
...(prop.type === "object" || prop.type === "array"
|
||||
? [
|
||||
|
|
@ -261,11 +265,13 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
}
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
prop.type === "object" ? "Please enter a JSON object" : "Please enter a JSON array",
|
||||
prop.type === "object"
|
||||
? t("mcpTools.mCPToolArgumentsForm.invalidJsonObject")
|
||||
: t("mcpTools.mCPToolArgumentsForm.invalidJsonArray"),
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
return Promise.reject(new Error("Invalid JSON"));
|
||||
return Promise.reject(new Error(t("mcpTools.mCPToolArgumentsForm.invalidJson")));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -275,26 +281,29 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
>
|
||||
{prop.type === "string" && prop.enum ? (
|
||||
<Select
|
||||
placeholder={`Select ${key}`}
|
||||
placeholder={t("mcpTools.mCPToolArgumentsForm.selectPlaceholder", { key })}
|
||||
allowClear={!actualSchema.required?.includes(key)}
|
||||
options={prop.enum.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
) : prop.type === "string" && !prop.enum ? (
|
||||
<Input placeholder={prop.description || `Enter ${key}`} allowClear />
|
||||
<Input
|
||||
placeholder={prop.description || t("mcpTools.mCPToolArgumentsForm.enterPlaceholder", { key })}
|
||||
allowClear
|
||||
/>
|
||||
) : prop.type === "number" || prop.type === "integer" ? (
|
||||
<InputNumber
|
||||
step={prop.type === "integer" ? 1 : undefined}
|
||||
placeholder={prop.description || `Enter ${key}`}
|
||||
placeholder={prop.description || t("mcpTools.mCPToolArgumentsForm.enterPlaceholder", { key })}
|
||||
className="w-full"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : prop.type === "boolean" ? (
|
||||
<Select
|
||||
placeholder={`Select ${key}`}
|
||||
placeholder={t("mcpTools.mCPToolArgumentsForm.selectPlaceholder", { key })}
|
||||
allowClear={!actualSchema.required?.includes(key)}
|
||||
options={[
|
||||
{ value: true, label: "True" },
|
||||
{ value: false, label: "False" },
|
||||
{ value: true, label: t("mcpTools.mCPToolArgumentsForm.booleanTrue") },
|
||||
{ value: false, label: t("mcpTools.mCPToolArgumentsForm.booleanFalse") },
|
||||
]}
|
||||
/>
|
||||
) : prop.type === "object" || prop.type === "array" ? (
|
||||
|
|
@ -302,13 +311,18 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
rows={prop.type === "object" ? 4 : 3}
|
||||
placeholder={
|
||||
prop.description ||
|
||||
(prop.type === "object" ? `Enter JSON object for ${key}` : `Enter JSON array for ${key}`)
|
||||
(prop.type === "object"
|
||||
? t("mcpTools.mCPToolArgumentsForm.jsonObjectPlaceholder", { key })
|
||||
: t("mcpTools.mCPToolArgumentsForm.jsonArrayPlaceholder", { key }))
|
||||
}
|
||||
spellCheck={false}
|
||||
className="font-mono"
|
||||
/>
|
||||
) : (
|
||||
<Input placeholder={prop.description || `Enter ${key}`} allowClear />
|
||||
<Input
|
||||
placeholder={prop.description || t("mcpTools.mCPToolArgumentsForm.enterPlaceholder", { key })}
|
||||
allowClear
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||
import { DataTable } from "../view_logs/table";
|
||||
import { createMCPToolset, updateMCPToolset, deleteMCPToolset, listMCPTools, getProxyBaseUrl } from "../networking";
|
||||
import { MCPToolset, MCPToolsetTool } from "./types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
const { Text: AntdText } = Typography;
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ interface ToolEntry {
|
|||
}
|
||||
|
||||
function MCPToolList({ serverId, serverName, accessToken, selectedTools, onToggle }: MCPToolListProps) {
|
||||
const { t } = useTranslation();
|
||||
const [tools, setTools] = useState<ToolEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
|
@ -72,7 +75,9 @@ function MCPToolList({ serverId, serverName, accessToken, selectedTools, onToggl
|
|||
<span className="inline-block w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />
|
||||
{serverName}
|
||||
{selectedSet.size > 0 && (
|
||||
<span className="ml-1 text-xs text-purple-600 font-semibold">{selectedSet.size} selected</span>
|
||||
<span className="ml-1 text-xs text-purple-600 font-semibold">
|
||||
{t("mcpTools.mCPToolsetsTab.selectedCount", { count: selectedSet.size })}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-gray-400 text-xs">{expanded ? "▲" : "▼"}</span>
|
||||
|
|
@ -84,7 +89,7 @@ function MCPToolList({ serverId, serverName, accessToken, selectedTools, onToggl
|
|||
<Spin size="small" />
|
||||
</div>
|
||||
) : tools.length === 0 ? (
|
||||
<p className="text-xs text-gray-400 px-2 py-2">No tools found for this server.</p>
|
||||
<p className="text-xs text-gray-400 px-2 py-2">{t("mcpTools.mCPToolsetsTab.noToolsFound")}</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{tools.map((tool) => {
|
||||
|
|
@ -133,6 +138,7 @@ interface CreateToolsetModalProps {
|
|||
}
|
||||
|
||||
function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset }: CreateToolsetModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<ToolsetFormValues>();
|
||||
const [selectedTools, setSelectedTools] = useState<MCPToolsetTool[]>(initialToolset?.tools || []);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -179,7 +185,7 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title={initialToolset ? "Edit Toolset" : "New Toolset"}
|
||||
title={initialToolset ? t("mcpTools.mCPToolsetsTab.editToolset") : t("mcpTools.mCPToolsetsTab.newToolset")}
|
||||
width={960}
|
||||
footer={null}
|
||||
forceRender
|
||||
|
|
@ -187,15 +193,15 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
<Form form={form} layout="vertical" className="mt-2">
|
||||
<div className="flex gap-4 mb-4">
|
||||
<Form.Item
|
||||
label="Toolset Name"
|
||||
label={t("mcpTools.mCPToolsetsTab.toolsetNameLabel")}
|
||||
name="toolset_name"
|
||||
rules={[{ required: true, message: "Please enter a toolset name" }]}
|
||||
rules={[{ required: true, message: t("mcpTools.mCPToolsetsTab.toolsetNameRequired") }]}
|
||||
className="flex-1 mb-0"
|
||||
>
|
||||
<Input placeholder="e.g. github-linear-tools" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description" className="flex-1 mb-0">
|
||||
<Input placeholder="Optional description" />
|
||||
<Form.Item label={t("common.description")} name="description" className="flex-1 mb-0">
|
||||
<Input placeholder={t("mcpTools.mCPToolsetsTab.descriptionPlaceholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -204,10 +210,10 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
{/* Left panel: Available Tools */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Text className="text-sm font-semibold text-gray-700">Available Tools</Text>
|
||||
<Text className="text-sm font-semibold text-gray-700">{t("mcpTools.mCPToolsetsTab.availableTools")}</Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search MCP servers..."
|
||||
placeholder={t("common.searchPlaceholder")}
|
||||
value={serverSearch}
|
||||
onChange={(e) => setServerSearch(e.target.value)}
|
||||
className="mb-2"
|
||||
|
|
@ -216,7 +222,9 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
<div className="space-y-2 overflow-y-auto" style={{ maxHeight: 300 }}>
|
||||
{filteredServers.length === 0 ? (
|
||||
<Text className="text-gray-400 text-sm">
|
||||
{mcpServers.length === 0 ? "No MCP servers configured" : "No servers match your search"}
|
||||
{mcpServers.length === 0
|
||||
? t("mcpTools.mCPToolsetsTab.noMcpServersConfigured")
|
||||
: t("mcpTools.mCPToolsetsTab.noServersMatchSearch")}
|
||||
</Text>
|
||||
) : (
|
||||
filteredServers.map((server) => (
|
||||
|
|
@ -239,11 +247,14 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
{/* Right panel: Your Toolset */}
|
||||
<div className="w-72 flex-shrink-0">
|
||||
<Text className="text-sm font-semibold text-gray-700 mb-2 block">
|
||||
Your Toolset <span className="text-xs font-normal text-gray-400">({selectedTools.length} tools)</span>
|
||||
{t("mcpTools.mCPToolsetsTab.yourToolset")}{" "}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
({t("mcpTools.mCPToolsetsTab.toolCount", { count: selectedTools.length })})
|
||||
</span>
|
||||
</Text>
|
||||
<div className="space-y-1 overflow-y-auto" style={{ maxHeight: 340 }}>
|
||||
{selectedTools.length === 0 ? (
|
||||
<Text className="text-gray-400 text-sm">No tools added yet</Text>
|
||||
<Text className="text-gray-400 text-sm">{t("mcpTools.mCPToolsetsTab.noToolsAdded")}</Text>
|
||||
) : (
|
||||
selectedTools.map((tool, idx) => (
|
||||
<button
|
||||
|
|
@ -268,10 +279,10 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
|
||||
<div className="flex justify-end gap-2 mt-4 pt-4 border-t border-gray-200">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={saving}>
|
||||
{initialToolset ? "Save Changes" : "Create Toolset"}
|
||||
{initialToolset ? t("mcpTools.mCPToolsetsTab.saveChanges") : t("mcpTools.mCPToolsetsTab.createToolset")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -279,14 +290,15 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
}
|
||||
|
||||
function toolsetColumns(
|
||||
t: TFunction,
|
||||
isAdmin: boolean,
|
||||
onEdit: (t: MCPToolset) => void,
|
||||
onEdit: (toolset: MCPToolset) => void,
|
||||
onDelete: (id: string) => void,
|
||||
proxyBaseUrl: string,
|
||||
): ColumnDef<MCPToolset>[] {
|
||||
return [
|
||||
{
|
||||
header: "Toolset ID",
|
||||
header: t("mcpTools.mCPToolsetsTab.colToolsetId"),
|
||||
accessorKey: "toolset_id",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono text-xs bg-gray-100 px-2 py-0.5 rounded text-gray-600">
|
||||
|
|
@ -295,7 +307,7 @@ function toolsetColumns(
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
header: t("common.name"),
|
||||
accessorKey: "toolset_name",
|
||||
cell: ({ row }) => {
|
||||
const url = `${proxyBaseUrl}/toolset/${row.original.toolset_name}/mcp`;
|
||||
|
|
@ -309,7 +321,7 @@ function toolsetColumns(
|
|||
type="button"
|
||||
className="text-xs text-gray-400 hover:text-purple-600 font-mono truncate max-w-xs text-left transition-colors"
|
||||
onClick={() => navigator.clipboard.writeText(url)}
|
||||
title="Click to copy endpoint URL"
|
||||
title={t("mcpTools.mCPToolsetsTab.copyEndpointTitle")}
|
||||
>
|
||||
{url}
|
||||
</button>
|
||||
|
|
@ -318,32 +330,36 @@ function toolsetColumns(
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
header: t("common.description"),
|
||||
accessorKey: "description",
|
||||
cell: ({ row }) => <span className="text-sm text-gray-500">{row.original.description || "—"}</span>,
|
||||
},
|
||||
{
|
||||
header: "Tools",
|
||||
header: t("mcpTools.mCPToolsetsTab.colTools"),
|
||||
accessorKey: "tools",
|
||||
cell: ({ row }) => {
|
||||
const tools = row.original.tools;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{tools.slice(0, 4).map((t, i) => (
|
||||
{tools.slice(0, 4).map((tool, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded bg-purple-50 border border-purple-200 text-purple-700 text-xs"
|
||||
>
|
||||
{t.tool_name}
|
||||
{tool.tool_name}
|
||||
</span>
|
||||
))}
|
||||
{tools.length > 4 && <span className="text-xs text-gray-400 self-center">+{tools.length - 4} more</span>}
|
||||
{tools.length > 4 && (
|
||||
<span className="text-xs text-gray-400 self-center">
|
||||
{t("mcpTools.mCPToolsetsTab.moreTools", { count: tools.length - 4 })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created",
|
||||
header: t("common.createdAt"),
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-xs text-gray-500">
|
||||
|
|
@ -381,6 +397,7 @@ function toolsetColumns(
|
|||
}
|
||||
|
||||
function ToolsetUsageGuide() {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
|
||||
|
|
@ -405,13 +422,13 @@ function ToolsetUsageGuide() {
|
|||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">How toolsets work</p>
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">{t("mcpTools.mCPToolsetsTab.howToolsetsWorkTitle")}</p>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Create a toolset, assign it to a key via{" "}
|
||||
<span className="font-medium text-gray-700">API Keys → Edit Key → MCP Servers</span>, then point your MCP client
|
||||
at the toolset URL. The client only sees the tools you picked.
|
||||
{t("mcpTools.mCPToolsetsTab.howToolsetsWorkDesc1")}{" "}
|
||||
<span className="font-medium text-gray-700">{t("mcpTools.mCPToolsetsTab.howToolsetsWorkPath")}</span>
|
||||
{t("mcpTools.mCPToolsetsTab.howToolsetsWorkDesc2")}
|
||||
</p>
|
||||
<div className="text-xs text-gray-400 mb-1">Claude Code / Cursor config</div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t("mcpTools.mCPToolsetsTab.configLabel")}</div>
|
||||
<div className="relative">
|
||||
<pre className="bg-white border border-gray-200 rounded px-4 py-3 text-xs font-mono text-gray-700 overflow-x-auto leading-relaxed pr-14">
|
||||
{snippet}
|
||||
|
|
@ -421,7 +438,7 @@ function ToolsetUsageGuide() {
|
|||
onClick={copy}
|
||||
className="absolute top-2 right-2 px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 text-gray-400 hover:text-gray-600 border-gray-200 transition-colors"
|
||||
>
|
||||
{copied ? "✓" : "copy"}
|
||||
{copied ? "✓" : t("common.copy")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -429,6 +446,7 @@ function ToolsetUsageGuide() {
|
|||
}
|
||||
|
||||
export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: toolsets = [], isLoading } = useMCPToolsets();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
|
@ -441,14 +459,14 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
const handleCreate = async (name: string, description: string | undefined, tools: MCPToolsetTool[]) => {
|
||||
if (!accessToken) return;
|
||||
await createMCPToolset(accessToken, { toolset_name: name, description, tools });
|
||||
message.success("Toolset created");
|
||||
message.success(t("mcpTools.mCPToolsetsTab.toolsetCreated"));
|
||||
queryClient.invalidateQueries({ queryKey: ["mcpToolsets"] });
|
||||
};
|
||||
|
||||
const handleUpdate = async (name: string, description: string | undefined, tools: MCPToolsetTool[]) => {
|
||||
if (!accessToken || !editToolset) return;
|
||||
await updateMCPToolset(accessToken, { toolset_id: editToolset.toolset_id, toolset_name: name, description, tools });
|
||||
message.success("Toolset updated");
|
||||
message.success(t("mcpTools.mCPToolsetsTab.toolsetUpdated"));
|
||||
queryClient.invalidateQueries({ queryKey: ["mcpToolsets"] });
|
||||
setEditToolset(null);
|
||||
};
|
||||
|
|
@ -458,7 +476,7 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
setDeleting(true);
|
||||
try {
|
||||
await deleteMCPToolset(accessToken, deleteId);
|
||||
message.success("Toolset deleted");
|
||||
message.success(t("mcpTools.mCPToolsetsTab.toolsetDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["mcpToolsets"] });
|
||||
setDeleteId(null);
|
||||
} finally {
|
||||
|
|
@ -467,21 +485,18 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
};
|
||||
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const columns = toolsetColumns(isAdmin, setEditToolset, setDeleteId, proxyBaseUrl);
|
||||
const columns = toolsetColumns(t, isAdmin, setEditToolset, setDeleteId, proxyBaseUrl);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<Title>MCP Toolsets</Title>
|
||||
<Text className="text-gray-500 text-sm">
|
||||
Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP
|
||||
permissions dropdown.
|
||||
</Text>
|
||||
<Title>{t("mcpTools.mCPToolsetsTab.pageTitle")}</Title>
|
||||
<Text className="text-gray-500 text-sm">{t("mcpTools.mCPToolsetsTab.pageSubtitle")}</Text>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Button icon={PlusIcon} onClick={() => setCreateOpen(true)}>
|
||||
New Toolset
|
||||
{t("mcpTools.mCPToolsetsTab.newToolset")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -494,8 +509,8 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
renderSubComponent={() => <div />}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={isLoading}
|
||||
noDataMessage="No toolsets yet. Click 'New Toolset' to create one."
|
||||
loadingMessage="Loading toolsets..."
|
||||
noDataMessage={t("mcpTools.mCPToolsetsTab.noToolsets")}
|
||||
loadingMessage={t("mcpTools.mCPToolsetsTab.loadingToolsets")}
|
||||
enableSorting={true}
|
||||
/>
|
||||
|
||||
|
|
@ -520,13 +535,11 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
open={!!deleteId}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
onOk={handleDelete}
|
||||
okText="Delete"
|
||||
okText={t("common.delete")}
|
||||
okButtonProps={{ danger: true, loading: deleting }}
|
||||
title="Delete Toolset"
|
||||
title={t("mcpTools.mCPToolsetsTab.deleteToolsetTitle")}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete this toolset? Keys and teams using it will lose access to the scoped tools.
|
||||
</p>
|
||||
<p>{t("mcpTools.mCPToolsetsTab.deleteToolsetConfirm")}</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3166,6 +3166,102 @@
|
|||
"groupSecurity": "Security",
|
||||
"fieldAuthLabel": "Auth configured",
|
||||
"fieldAuthDesc": "Must use authentication (not 'none')"
|
||||
},
|
||||
"mCPSubmissionsTab": {
|
||||
"statusActive": "Active",
|
||||
"statusPendingReview": "Pending Review",
|
||||
"statusRejected": "Rejected",
|
||||
"rejectBodyActive": "This server is currently live. Rejecting it will immediately remove it from the proxy runtime.",
|
||||
"rejectBodyInactive": "This will mark the submission as rejected.",
|
||||
"confirmApproveMcpServer": "Approve MCP Server",
|
||||
"confirmRejectMcpServer": "Reject MCP Server",
|
||||
"confirmAreYouSure": "Are you sure you want to {{action}}",
|
||||
"confirmApproveBody": "This will make it active and available for use.",
|
||||
"rejectionReasonPlaceholder": "Reason for rejection (optional)",
|
||||
"approveButton": "Approve",
|
||||
"rejectButton": "Reject",
|
||||
"reapproveButton": "Re-approve",
|
||||
"submissionRulesTitle": "Submission Rules",
|
||||
"requiredFieldCount_one": "{{count}} required field",
|
||||
"requiredFieldCount_other": "{{count}} required fields",
|
||||
"noRulesSet": "no rules set",
|
||||
"submissionRulesDesc": "Select which fields must be filled in before a submission is considered compliant. LiteLLM will show ✓ / ✗ for each rule on every submission card below.",
|
||||
"saveRulesButton": "Save Rules",
|
||||
"transportLabel": "Transport:",
|
||||
"submittedByLabel": "Submitted by:",
|
||||
"rejectionReasonLabel": "Rejection reason: {{reason}}",
|
||||
"allChecksPassed": "All checks passed",
|
||||
"checksFailed_one": "{{count}} check failed",
|
||||
"checksFailed_other": "{{count}} checks failed",
|
||||
"checksPassFail": "{{passCount}} passing, {{failCount}} failing",
|
||||
"checkPasses": "Passes",
|
||||
"checkMissing": "Missing",
|
||||
"statTotalSubmitted": "Total Submitted",
|
||||
"statPendingReview": "Pending Review",
|
||||
"statActive": "Active",
|
||||
"statRejected": "Rejected",
|
||||
"filterAllStatus": "All Status",
|
||||
"loadingSubmissions": "Loading submissions…",
|
||||
"noSubmissionsMatch": "No MCP server submissions match your filters.",
|
||||
"failedToLoadSubmissions": "Failed to load submissions",
|
||||
"submissionRulesSaved": "Submission rules saved",
|
||||
"failedToSaveRules": "Failed to save submission rules",
|
||||
"approvedSuccess": "MCP server \"{{name}}\" approved",
|
||||
"failedToApprove": "Failed to approve MCP server",
|
||||
"rejectedSuccess": "MCP server \"{{name}}\" rejected",
|
||||
"failedToReject": "Failed to reject MCP server"
|
||||
},
|
||||
"mCPToolArgumentsForm": {
|
||||
"inputDescription": "Input for this tool",
|
||||
"inputLabel": "Input",
|
||||
"inputRequired": "Please enter input for this tool",
|
||||
"inputPlaceholder": "Enter input for this tool",
|
||||
"noParameters": "No parameters required for this tool.",
|
||||
"fieldRequired": "Please enter {{key}}",
|
||||
"invalidJsonObject": "Please enter a JSON object",
|
||||
"invalidJsonArray": "Please enter a JSON array",
|
||||
"invalidJson": "Invalid JSON",
|
||||
"selectPlaceholder": "Select {{key}}",
|
||||
"enterPlaceholder": "Enter {{key}}",
|
||||
"booleanTrue": "True",
|
||||
"booleanFalse": "False",
|
||||
"jsonObjectPlaceholder": "Enter JSON object for {{key}}",
|
||||
"jsonArrayPlaceholder": "Enter JSON array for {{key}}"
|
||||
},
|
||||
"mCPToolsetsTab": {
|
||||
"selectedCount": "{{count}} selected",
|
||||
"noToolsFound": "No tools found for this server.",
|
||||
"editToolset": "Edit Toolset",
|
||||
"newToolset": "New Toolset",
|
||||
"toolsetNameLabel": "Toolset Name",
|
||||
"toolsetNameRequired": "Please enter a toolset name",
|
||||
"descriptionPlaceholder": "Optional description",
|
||||
"availableTools": "Available Tools",
|
||||
"noMcpServersConfigured": "No MCP servers configured",
|
||||
"noServersMatchSearch": "No servers match your search",
|
||||
"yourToolset": "Your Toolset",
|
||||
"toolCount": "{{count}} tools",
|
||||
"noToolsAdded": "No tools added yet",
|
||||
"saveChanges": "Save Changes",
|
||||
"createToolset": "Create Toolset",
|
||||
"colToolsetId": "Toolset ID",
|
||||
"colTools": "Tools",
|
||||
"copyEndpointTitle": "Click to copy endpoint URL",
|
||||
"moreTools": "+{{count}} more",
|
||||
"toolsetCreated": "Toolset created",
|
||||
"toolsetUpdated": "Toolset updated",
|
||||
"toolsetDeleted": "Toolset deleted",
|
||||
"pageTitle": "MCP Toolsets",
|
||||
"pageSubtitle": "Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP permissions dropdown.",
|
||||
"noToolsets": "No toolsets yet. Click 'New Toolset' to create one.",
|
||||
"loadingToolsets": "Loading toolsets...",
|
||||
"deleteToolsetTitle": "Delete Toolset",
|
||||
"deleteToolsetConfirm": "Are you sure you want to delete this toolset? Keys and teams using it will lose access to the scoped tools.",
|
||||
"howToolsetsWorkTitle": "How toolsets work",
|
||||
"howToolsetsWorkDesc1": "Create a toolset, assign it to a key via",
|
||||
"howToolsetsWorkPath": "API Keys → Edit Key → MCP Servers",
|
||||
"howToolsetsWorkDesc2": ", then point your MCP client at the toolset URL. The client only sees the tools you picked.",
|
||||
"configLabel": "Claude Code / Cursor config"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3166,6 +3166,102 @@
|
|||
"groupSecurity": "安全",
|
||||
"fieldAuthLabel": "已配置认证",
|
||||
"fieldAuthDesc": "必须使用认证(不能为 'none')"
|
||||
},
|
||||
"mCPSubmissionsTab": {
|
||||
"statusActive": "活跃",
|
||||
"statusPendingReview": "待审核",
|
||||
"statusRejected": "已拒绝",
|
||||
"rejectBodyActive": "此服务器当前正在运行。拒绝后将立即从代理运行时中移除。",
|
||||
"rejectBodyInactive": "这将把提交标记为已拒绝。",
|
||||
"confirmApproveMcpServer": "批准 MCP 服务器",
|
||||
"confirmRejectMcpServer": "拒绝 MCP 服务器",
|
||||
"confirmAreYouSure": "确定要对以下服务器执行{{action}}操作吗",
|
||||
"confirmApproveBody": "这将使其处于活跃状态并可供使用。",
|
||||
"rejectionReasonPlaceholder": "拒绝原因(可选)",
|
||||
"approveButton": "批准",
|
||||
"rejectButton": "拒绝",
|
||||
"reapproveButton": "重新批准",
|
||||
"submissionRulesTitle": "提交规则",
|
||||
"requiredFieldCount_one": "{{count}} 个必填字段",
|
||||
"requiredFieldCount_other": "{{count}} 个必填字段",
|
||||
"noRulesSet": "未设置规则",
|
||||
"submissionRulesDesc": "选择提交内容被视为合规前必须填写的字段。LiteLLM 将在下方每张提交卡片上为每条规则显示 ✓ / ✗。",
|
||||
"saveRulesButton": "保存规则",
|
||||
"transportLabel": "传输方式:",
|
||||
"submittedByLabel": "提交者:",
|
||||
"rejectionReasonLabel": "拒绝原因:{{reason}}",
|
||||
"allChecksPassed": "所有检查已通过",
|
||||
"checksFailed_one": "{{count}} 项检查失败",
|
||||
"checksFailed_other": "{{count}} 项检查失败",
|
||||
"checksPassFail": "{{passCount}} 项通过,{{failCount}} 项未通过",
|
||||
"checkPasses": "通过",
|
||||
"checkMissing": "缺失",
|
||||
"statTotalSubmitted": "总提交数",
|
||||
"statPendingReview": "待审核",
|
||||
"statActive": "活跃",
|
||||
"statRejected": "已拒绝",
|
||||
"filterAllStatus": "全部状态",
|
||||
"loadingSubmissions": "加载提交中…",
|
||||
"noSubmissionsMatch": "没有符合筛选条件的 MCP 服务器提交。",
|
||||
"failedToLoadSubmissions": "加载提交失败",
|
||||
"submissionRulesSaved": "提交规则已保存",
|
||||
"failedToSaveRules": "保存提交规则失败",
|
||||
"approvedSuccess": "MCP 服务器「{{name}}」已批准",
|
||||
"failedToApprove": "批准 MCP 服务器失败",
|
||||
"rejectedSuccess": "MCP 服务器「{{name}}」已拒绝",
|
||||
"failedToReject": "拒绝 MCP 服务器失败"
|
||||
},
|
||||
"mCPToolArgumentsForm": {
|
||||
"inputDescription": "此工具的输入",
|
||||
"inputLabel": "输入",
|
||||
"inputRequired": "请为此工具输入内容",
|
||||
"inputPlaceholder": "为此工具输入内容",
|
||||
"noParameters": "此工具不需要任何参数。",
|
||||
"fieldRequired": "请输入 {{key}}",
|
||||
"invalidJsonObject": "请输入 JSON 对象",
|
||||
"invalidJsonArray": "请输入 JSON 数组",
|
||||
"invalidJson": "无效的 JSON",
|
||||
"selectPlaceholder": "选择 {{key}}",
|
||||
"enterPlaceholder": "输入 {{key}}",
|
||||
"booleanTrue": "是",
|
||||
"booleanFalse": "否",
|
||||
"jsonObjectPlaceholder": "为 {{key}} 输入 JSON 对象",
|
||||
"jsonArrayPlaceholder": "为 {{key}} 输入 JSON 数组"
|
||||
},
|
||||
"mCPToolsetsTab": {
|
||||
"selectedCount": "已选 {{count}} 个",
|
||||
"noToolsFound": "未找到此服务器的工具。",
|
||||
"editToolset": "编辑工具集",
|
||||
"newToolset": "新建工具集",
|
||||
"toolsetNameLabel": "工具集名称",
|
||||
"toolsetNameRequired": "请输入工具集名称",
|
||||
"descriptionPlaceholder": "可选描述",
|
||||
"availableTools": "可用工具",
|
||||
"noMcpServersConfigured": "未配置 MCP 服务器",
|
||||
"noServersMatchSearch": "没有服务器匹配您的搜索",
|
||||
"yourToolset": "您的工具集",
|
||||
"toolCount": "{{count}} 个工具",
|
||||
"noToolsAdded": "尚未添加工具",
|
||||
"saveChanges": "保存更改",
|
||||
"createToolset": "创建工具集",
|
||||
"colToolsetId": "工具集 ID",
|
||||
"colTools": "工具",
|
||||
"copyEndpointTitle": "点击复制端点 URL",
|
||||
"moreTools": "+{{count}} 个",
|
||||
"toolsetCreated": "工具集已创建",
|
||||
"toolsetUpdated": "工具集已更新",
|
||||
"toolsetDeleted": "工具集已删除",
|
||||
"pageTitle": "MCP 工具集",
|
||||
"pageSubtitle": "来自一个或多个 MCP 服务器的精选工具集合。通过 MCP 权限下拉菜单将工具集分配给密钥和团队。",
|
||||
"noToolsets": "暂无工具集。点击「新建工具集」创建一个。",
|
||||
"loadingToolsets": "加载工具集中...",
|
||||
"deleteToolsetTitle": "删除工具集",
|
||||
"deleteToolsetConfirm": "确定要删除此工具集吗?使用它的密钥和团队将失去对已选工具的访问权限。",
|
||||
"howToolsetsWorkTitle": "工具集如何工作",
|
||||
"howToolsetsWorkDesc1": "创建工具集,通过",
|
||||
"howToolsetsWorkPath": "API 密钥 → 编辑密钥 → MCP 服务器",
|
||||
"howToolsetsWorkDesc2": "将其分配给密钥,然后将您的 MCP 客户端指向工具集 URL。客户端只会看到您选定的工具。",
|
||||
"configLabel": "Claude Code / Cursor 配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue