2025-10-09 11:30:28 +08:00
|
|
|
|
import React, { useState } from "react";
|
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
import { X, Save } from "lucide-react";
|
|
|
|
|
|
import { McpServer } from "../../types";
|
|
|
|
|
|
import { isLinux } from "../../lib/platform";
|
|
|
|
|
|
|
|
|
|
|
|
interface McpWizardModalProps {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
onApply: (json: string) => void;
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify?: (
|
|
|
|
|
|
message: string,
|
|
|
|
|
|
type: "success" | "error",
|
|
|
|
|
|
duration?: number,
|
|
|
|
|
|
) => void;
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析环境变量文本为对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
const parseEnvText = (text: string): Record<string, string> => {
|
|
|
|
|
|
const lines = text
|
|
|
|
|
|
.split("\n")
|
|
|
|
|
|
.map((l) => l.trim())
|
|
|
|
|
|
.filter((l) => l.length > 0);
|
|
|
|
|
|
const env: Record<string, string> = {};
|
|
|
|
|
|
for (const l of lines) {
|
|
|
|
|
|
const idx = l.indexOf("=");
|
|
|
|
|
|
if (idx > 0) {
|
|
|
|
|
|
const k = l.slice(0, idx).trim();
|
|
|
|
|
|
const v = l.slice(idx + 1).trim();
|
|
|
|
|
|
if (k) env[k] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return env;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 解析headers文本为对象(支持 KEY: VALUE 或 KEY=VALUE)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const parseHeadersText = (text: string): Record<string, string> => {
|
|
|
|
|
|
const lines = text
|
|
|
|
|
|
.split("\n")
|
|
|
|
|
|
.map((l) => l.trim())
|
|
|
|
|
|
.filter((l) => l.length > 0);
|
|
|
|
|
|
const headers: Record<string, string> = {};
|
|
|
|
|
|
for (const l of lines) {
|
|
|
|
|
|
// 支持 KEY: VALUE 或 KEY=VALUE
|
|
|
|
|
|
const colonIdx = l.indexOf(":");
|
|
|
|
|
|
const equalIdx = l.indexOf("=");
|
|
|
|
|
|
let idx = -1;
|
|
|
|
|
|
if (colonIdx > 0 && (equalIdx === -1 || colonIdx < equalIdx)) {
|
|
|
|
|
|
idx = colonIdx;
|
|
|
|
|
|
} else if (equalIdx > 0) {
|
|
|
|
|
|
idx = equalIdx;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (idx > 0) {
|
|
|
|
|
|
const k = l.slice(0, idx).trim();
|
|
|
|
|
|
const v = l.slice(idx + 1).trim();
|
|
|
|
|
|
if (k) headers[k] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return headers;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 11:30:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* MCP 配置向导模态框
|
|
|
|
|
|
* 帮助用户快速生成 MCP JSON 配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
onApply,
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify,
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
const { t } = useTranslation();
|
2025-10-09 12:04:37 +08:00
|
|
|
|
const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio");
|
|
|
|
|
|
// stdio 字段
|
2025-10-09 11:30:28 +08:00
|
|
|
|
const [wizardCommand, setWizardCommand] = useState("");
|
|
|
|
|
|
const [wizardArgs, setWizardArgs] = useState("");
|
|
|
|
|
|
const [wizardCwd, setWizardCwd] = useState("");
|
|
|
|
|
|
const [wizardEnv, setWizardEnv] = useState("");
|
2025-10-09 12:04:37 +08:00
|
|
|
|
// http 字段
|
|
|
|
|
|
const [wizardUrl, setWizardUrl] = useState("");
|
|
|
|
|
|
const [wizardHeaders, setWizardHeaders] = useState("");
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 生成预览 JSON
|
|
|
|
|
|
const generatePreview = (): string => {
|
|
|
|
|
|
const config: McpServer = {
|
|
|
|
|
|
type: wizardType,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
if (wizardType === "stdio") {
|
|
|
|
|
|
// stdio 类型必需字段
|
|
|
|
|
|
config.command = wizardCommand.trim();
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
// 可选字段
|
|
|
|
|
|
if (wizardArgs.trim()) {
|
|
|
|
|
|
config.args = wizardArgs
|
|
|
|
|
|
.split("\n")
|
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
|
.filter((s) => s.length > 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (wizardCwd.trim()) {
|
|
|
|
|
|
config.cwd = wizardCwd.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (wizardEnv.trim()) {
|
|
|
|
|
|
const env = parseEnvText(wizardEnv);
|
|
|
|
|
|
if (Object.keys(env).length > 0) {
|
|
|
|
|
|
config.env = env;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// http 类型必需字段
|
|
|
|
|
|
config.url = wizardUrl.trim();
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
// 可选字段
|
|
|
|
|
|
if (wizardHeaders.trim()) {
|
|
|
|
|
|
const headers = parseHeadersText(wizardHeaders);
|
|
|
|
|
|
if (Object.keys(headers).length > 0) {
|
|
|
|
|
|
config.headers = headers;
|
|
|
|
|
|
}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return JSON.stringify(config, null, 2);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleApply = () => {
|
2025-10-09 12:04:37 +08:00
|
|
|
|
if (wizardType === "stdio" && !wizardCommand.trim()) {
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify?.(t("mcp.error.commandRequired"), "error", 3000);
|
2025-10-09 11:30:28 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-09 12:04:37 +08:00
|
|
|
|
if (wizardType === "http" && !wizardUrl.trim()) {
|
2025-10-10 20:52:16 +08:00
|
|
|
|
onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000);
|
2025-10-09 12:04:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
|
|
|
|
|
const json = generatePreview();
|
|
|
|
|
|
onApply(json);
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
// 重置表单
|
|
|
|
|
|
setWizardType("stdio");
|
|
|
|
|
|
setWizardCommand("");
|
|
|
|
|
|
setWizardArgs("");
|
|
|
|
|
|
setWizardCwd("");
|
|
|
|
|
|
setWizardEnv("");
|
2025-10-09 12:04:37 +08:00
|
|
|
|
setWizardUrl("");
|
|
|
|
|
|
setWizardHeaders("");
|
2025-10-09 11:30:28 +08:00
|
|
|
|
onClose();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
|
|
|
if (e.key === "Enter" && e.metaKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
handleApply();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const preview = generatePreview();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="fixed inset-0 z-[70] flex items-center justify-center"
|
|
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
|
handleClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Backdrop */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
|
|
|
|
|
isLinux() ? "" : " backdrop-blur-sm"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Modal */}
|
|
|
|
|
|
<div className="relative mx-4 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white shadow-lg dark:bg-gray-900">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800">
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.title")}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
|
|
|
|
|
|
aria-label={t("common.close")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<X size={18} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
|
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6">
|
|
|
|
|
|
{/* Hint */}
|
|
|
|
|
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
|
|
|
|
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
|
|
|
|
{t("mcp.wizard.hint")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Form Fields */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* Type */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.type")} <span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="radio"
|
|
|
|
|
|
value="stdio"
|
|
|
|
|
|
checked={wizardType === "stdio"}
|
|
|
|
|
|
onChange={(e) =>
|
2025-10-09 12:04:37 +08:00
|
|
|
|
setWizardType(e.target.value as "stdio" | "http")
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
stdio
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="radio"
|
2025-10-09 12:04:37 +08:00
|
|
|
|
value="http"
|
|
|
|
|
|
checked={wizardType === "http"}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
onChange={(e) =>
|
2025-10-09 12:04:37 +08:00
|
|
|
|
setWizardType(e.target.value as "stdio" | "http")
|
2025-10-09 11:30:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
2025-10-09 12:04:37 +08:00
|
|
|
|
http
|
2025-10-09 11:30:28 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
{/* Stdio 类型字段 */}
|
|
|
|
|
|
{wizardType === "stdio" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Command */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.command")}{" "}
|
|
|
|
|
|
<span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={wizardCommand}
|
|
|
|
|
|
onChange={(e) => setWizardCommand(e.target.value)}
|
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
|
placeholder={t("mcp.wizard.commandPlaceholder")}
|
|
|
|
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
{/* Args */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.args")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={wizardArgs}
|
|
|
|
|
|
onChange={(e) => setWizardArgs(e.target.value)}
|
|
|
|
|
|
placeholder={t("mcp.wizard.argsPlaceholder")}
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 resize-y"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
{/* CWD */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.cwd")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={wizardCwd}
|
|
|
|
|
|
onChange={(e) => setWizardCwd(e.target.value)}
|
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
|
placeholder={t("mcp.wizard.cwdPlaceholder")}
|
|
|
|
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-10-09 11:30:28 +08:00
|
|
|
|
|
2025-10-09 12:04:37 +08:00
|
|
|
|
{/* Env */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.env")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={wizardEnv}
|
|
|
|
|
|
onChange={(e) => setWizardEnv(e.target.value)}
|
|
|
|
|
|
placeholder={t("mcp.wizard.envPlaceholder")}
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 resize-y"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* HTTP 类型字段 */}
|
|
|
|
|
|
{wizardType === "http" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* URL */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.url")}{" "}
|
|
|
|
|
|
<span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={wizardUrl}
|
|
|
|
|
|
onChange={(e) => setWizardUrl(e.target.value)}
|
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
|
placeholder={t("mcp.wizard.urlPlaceholder")}
|
|
|
|
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Headers */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.headers")}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={wizardHeaders}
|
|
|
|
|
|
onChange={(e) => setWizardHeaders(e.target.value)}
|
|
|
|
|
|
placeholder={t("mcp.wizard.headersPlaceholder")}
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 resize-y"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-10-09 11:30:28 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Preview */}
|
2025-10-09 12:04:37 +08:00
|
|
|
|
{(wizardCommand ||
|
|
|
|
|
|
wizardArgs ||
|
|
|
|
|
|
wizardCwd ||
|
|
|
|
|
|
wizardEnv ||
|
|
|
|
|
|
wizardUrl ||
|
|
|
|
|
|
wizardHeaders) && (
|
2025-10-09 11:30:28 +08:00
|
|
|
|
<div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700">
|
|
|
|
|
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{t("mcp.wizard.preview")}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<pre className="overflow-x-auto rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
|
|
|
|
|
{preview}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
|
<div className="flex items-center justify-end gap-3 border-t border-gray-200 bg-gray-100 p-6 dark:border-gray-800 dark:bg-gray-800">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
|
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
{t("common.cancel")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleApply}
|
|
|
|
|
|
className="flex items-center gap-2 rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Save className="h-4 w-4" />
|
|
|
|
|
|
{t("mcp.wizard.apply")}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default McpWizardModal;
|