feat(mcp): add configuration wizard and simplify form modal
- Simplify McpFormModal to 3 inputs: title (required), description (optional), and JSON config (optional) - Add JSON validation similar to ProviderForm (must be object, real-time error display) - Create McpWizardModal component for quick configuration: - 5 input fields: type (stdio/sse), command (required), args, cwd, env - Real-time JSON preview - Emerald theme color (consistent with MCP button) - Z-index 70 (above McpFormModal's 60) - Add "or use configuration wizard" link next to JSON config label - Update i18n translations (zh/en) for form and wizard - All changes pass TypeScript typecheck and Prettier formatting
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { X, Save, Wrench } from "lucide-react";
|
import { X, Save, AlertCircle } from "lucide-react";
|
||||||
import { McpServer } from "../../types";
|
import { McpServer } from "../../types";
|
||||||
import { buttonStyles, inputStyles } from "../../lib/styles";
|
import { buttonStyles, inputStyles } from "../../lib/styles";
|
||||||
|
import McpWizardModal from "./McpWizardModal";
|
||||||
|
|
||||||
interface McpFormModalProps {
|
interface McpFormModalProps {
|
||||||
editingId?: string;
|
editingId?: string;
|
||||||
@@ -11,33 +12,25 @@ interface McpFormModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseEnvText = (text: string): Record<string, string> => {
|
/**
|
||||||
const lines = text
|
* 验证 JSON 格式
|
||||||
.split("\n")
|
*/
|
||||||
.map((l) => l.trim())
|
const validateJson = (text: string): string => {
|
||||||
.filter((l) => l.length > 0);
|
if (!text.trim()) return "";
|
||||||
const env: Record<string, string> = {};
|
try {
|
||||||
for (const l of lines) {
|
const parsed = JSON.parse(text);
|
||||||
const idx = l.indexOf("=");
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
if (idx > 0) {
|
return "JSON 必须是对象";
|
||||||
const k = l.slice(0, idx).trim();
|
|
||||||
const v = l.slice(idx + 1).trim();
|
|
||||||
if (k) env[k] = v;
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
} catch {
|
||||||
|
return "JSON 格式错误";
|
||||||
}
|
}
|
||||||
return env;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatEnvText = (env?: Record<string, string>): string => {
|
|
||||||
if (!env) return "";
|
|
||||||
return Object.entries(env)
|
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
|
||||||
.join("\n");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP 表单模态框组件
|
* MCP 表单模态框组件(简化版)
|
||||||
* 用于添加或编辑 MCP 服务器
|
* 仅包含:标题(必填)、描述(可选)、JSON 配置(可选,带格式校验)
|
||||||
*/
|
*/
|
||||||
const McpFormModal: React.FC<McpFormModalProps> = ({
|
const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||||
editingId,
|
editingId,
|
||||||
@@ -47,32 +40,25 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [formId, setFormId] = useState(editingId || "");
|
const [formId, setFormId] = useState(editingId || "");
|
||||||
const [formType, setFormType] = useState<"stdio" | "sse">(
|
const [formDescription, setFormDescription] = useState("");
|
||||||
initialData?.type || "stdio",
|
const [formJson, setFormJson] = useState(
|
||||||
|
initialData ? JSON.stringify(initialData, null, 2) : "",
|
||||||
);
|
);
|
||||||
const [formCommand, setFormCommand] = useState(initialData?.command || "");
|
const [jsonError, setJsonError] = useState("");
|
||||||
const [formArgsText, setFormArgsText] = useState(
|
|
||||||
(initialData?.args || []).join(" "),
|
|
||||||
);
|
|
||||||
const [formEnvText, setFormEnvText] = useState(
|
|
||||||
formatEnvText(initialData?.env),
|
|
||||||
);
|
|
||||||
const [formCwd, setFormCwd] = useState(initialData?.cwd || "");
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
|
|
||||||
// 编辑模式下禁止修改 ID
|
// 编辑模式下禁止修改 ID
|
||||||
const isEditing = !!editingId;
|
const isEditing = !!editingId;
|
||||||
|
|
||||||
const handleValidateCommand = async () => {
|
const handleJsonChange = (value: string) => {
|
||||||
if (!formCommand) return;
|
setFormJson(value);
|
||||||
try {
|
setJsonError(validateJson(value));
|
||||||
const ok = await window.api.validateMcpCommand(formCommand.trim());
|
};
|
||||||
const message = ok ? t("mcp.validation.ok") : t("mcp.validation.fail");
|
|
||||||
// 这里简单使用 alert,实际项目中应该使用 notification 系统
|
const handleWizardApply = (json: string) => {
|
||||||
alert(message);
|
setFormJson(json);
|
||||||
} catch (_error) {
|
setJsonError(validateJson(json));
|
||||||
alert(t("mcp.validation.fail"));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@@ -80,29 +66,38 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
alert(t("mcp.error.idRequired"));
|
alert(t("mcp.error.idRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!formCommand.trim()) {
|
|
||||||
alert(t("mcp.error.commandRequired"));
|
// 验证 JSON
|
||||||
|
const currentJsonError = validateJson(formJson);
|
||||||
|
setJsonError(currentJsonError);
|
||||||
|
if (currentJsonError) {
|
||||||
|
alert(t("mcp.error.jsonInvalid"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const server: McpServer = {
|
let server: McpServer;
|
||||||
type: formType,
|
if (formJson.trim()) {
|
||||||
command: formCommand.trim(),
|
// 解析 JSON 配置
|
||||||
args: formArgsText
|
server = JSON.parse(formJson) as McpServer;
|
||||||
.split(/\s+/)
|
} else {
|
||||||
.map((s) => s.trim())
|
// 空 JSON 时提供默认值
|
||||||
.filter((s) => s.length > 0),
|
server = {
|
||||||
env: parseEnvText(formEnvText),
|
type: "stdio",
|
||||||
...(formCwd ? { cwd: formCwd } : {}),
|
command: "",
|
||||||
// 保留原有的 enabled 状态
|
args: [],
|
||||||
...(initialData?.enabled !== undefined
|
};
|
||||||
? { enabled: initialData.enabled }
|
}
|
||||||
: {}),
|
|
||||||
};
|
// 保留原有的 enabled 状态
|
||||||
|
if (initialData?.enabled !== undefined) {
|
||||||
|
server.enabled = initialData.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
onSave(formId.trim(), server);
|
onSave(formId.trim(), server);
|
||||||
|
} catch (error) {
|
||||||
|
alert(t("mcp.error.saveFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -133,94 +128,59 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
{/* ID */}
|
{/* ID (标题) */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{t("mcp.id")}
|
{t("mcp.form.title")} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
className={inputStyles.text}
|
className={inputStyles.text}
|
||||||
placeholder="my-mcp"
|
placeholder={t("mcp.form.titlePlaceholder")}
|
||||||
value={formId}
|
value={formId}
|
||||||
onChange={(e) => setFormId(e.target.value)}
|
onChange={(e) => setFormId(e.target.value)}
|
||||||
disabled={isEditing}
|
disabled={isEditing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Type & CWD */}
|
{/* Description (描述) */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{t("mcp.type")}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className={inputStyles.select}
|
|
||||||
value={formType}
|
|
||||||
onChange={(e) => setFormType(e.target.value as "stdio" | "sse")}
|
|
||||||
>
|
|
||||||
<option value="stdio">stdio</option>
|
|
||||||
<option value="sse">sse</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{t("mcp.cwd")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className={inputStyles.text}
|
|
||||||
placeholder="/path/to/project"
|
|
||||||
value={formCwd}
|
|
||||||
onChange={(e) => setFormCwd(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Command */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
{t("mcp.command")}
|
{t("mcp.form.description")}
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
className={inputStyles.text}
|
|
||||||
placeholder="uvx"
|
|
||||||
value={formCommand}
|
|
||||||
onChange={(e) => setFormCommand(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleValidateCommand}
|
|
||||||
className="px-3 py-2 rounded-md bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700 text-sm inline-flex items-center gap-1 flex-shrink-0 transition-colors"
|
|
||||||
>
|
|
||||||
<Wrench size={16} /> {t("mcp.validateCommand")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Args */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{t("mcp.args")}
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
className={inputStyles.text}
|
className={inputStyles.text}
|
||||||
placeholder={t("mcp.argsPlaceholder")}
|
placeholder={t("mcp.form.descriptionPlaceholder")}
|
||||||
value={formArgsText}
|
value={formDescription}
|
||||||
onChange={(e) => setFormArgsText(e.target.value)}
|
onChange={(e) => setFormDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Env */}
|
{/* JSON 配置 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{t("mcp.env")}
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
</label>
|
{t("mcp.form.jsonConfig")}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsWizardOpen(true)}
|
||||||
|
className="text-xs text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 transition-colors"
|
||||||
|
>
|
||||||
|
{t("mcp.form.useWizard")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className={`${inputStyles.text} h-24 resize-none`}
|
className={`${inputStyles.text} h-64 resize-none font-mono text-xs`}
|
||||||
placeholder={t("mcp.envPlaceholder")}
|
placeholder={t("mcp.form.jsonPlaceholder")}
|
||||||
value={formEnvText}
|
value={formJson}
|
||||||
onChange={(e) => setFormEnvText(e.target.value)}
|
onChange={(e) => handleJsonChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
{jsonError && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm">
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
<span>{jsonError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -243,6 +203,13 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Wizard Modal */}
|
||||||
|
<McpWizardModal
|
||||||
|
isOpen={isWizardOpen}
|
||||||
|
onClose={() => setIsWizardOpen(false)}
|
||||||
|
onApply={handleWizardApply}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
289
src/components/mcp/McpWizardModal.tsx
Normal file
289
src/components/mcp/McpWizardModal.tsx
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析环境变量文本为对象
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 配置向导模态框
|
||||||
|
* 帮助用户快速生成 MCP JSON 配置
|
||||||
|
*/
|
||||||
|
const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onApply,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [wizardType, setWizardType] = useState<"stdio" | "sse">("stdio");
|
||||||
|
const [wizardCommand, setWizardCommand] = useState("");
|
||||||
|
const [wizardArgs, setWizardArgs] = useState("");
|
||||||
|
const [wizardCwd, setWizardCwd] = useState("");
|
||||||
|
const [wizardEnv, setWizardEnv] = useState("");
|
||||||
|
|
||||||
|
// 生成预览 JSON
|
||||||
|
const generatePreview = (): string => {
|
||||||
|
const config: McpServer = {
|
||||||
|
type: wizardType,
|
||||||
|
command: wizardCommand.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加可选字段
|
||||||
|
if (wizardArgs.trim()) {
|
||||||
|
config.args = wizardArgs
|
||||||
|
.split(/\s+/)
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(config, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
if (!wizardCommand.trim()) {
|
||||||
|
alert(t("mcp.error.commandRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = generatePreview();
|
||||||
|
onApply(json);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// 重置表单
|
||||||
|
setWizardType("stdio");
|
||||||
|
setWizardCommand("");
|
||||||
|
setWizardArgs("");
|
||||||
|
setWizardCwd("");
|
||||||
|
setWizardEnv("");
|
||||||
|
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) =>
|
||||||
|
setWizardType(e.target.value as "stdio" | "sse")
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
value="sse"
|
||||||
|
checked={wizardType === "sse"}
|
||||||
|
onChange={(e) =>
|
||||||
|
setWizardType(e.target.value as "stdio" | "sse")
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
sse
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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")}
|
||||||
|
required
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Args */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{t("mcp.wizard.args")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={wizardArgs}
|
||||||
|
onChange={(e) => setWizardArgs(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t("mcp.wizard.argsPlaceholder")}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
{(wizardCommand || wizardArgs || wizardCwd || wizardEnv) && (
|
||||||
|
<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;
|
||||||
@@ -265,6 +265,30 @@
|
|||||||
"template": {
|
"template": {
|
||||||
"fetch": "Quick Template: mcp-fetch"
|
"fetch": "Quick Template: mcp-fetch"
|
||||||
},
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "Server Title",
|
||||||
|
"titlePlaceholder": "my-mcp-server",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "Optional description",
|
||||||
|
"jsonConfig": "JSON Configuration",
|
||||||
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
|
"useWizard": "or use configuration wizard"
|
||||||
|
},
|
||||||
|
"wizard": {
|
||||||
|
"title": "MCP Configuration Wizard",
|
||||||
|
"hint": "Quickly configure MCP server and auto-generate JSON configuration",
|
||||||
|
"type": "Type",
|
||||||
|
"command": "Command",
|
||||||
|
"commandPlaceholder": "uvx",
|
||||||
|
"args": "Arguments",
|
||||||
|
"argsPlaceholder": "mcp-server-fetch",
|
||||||
|
"cwd": "Working Directory",
|
||||||
|
"cwdPlaceholder": "/path/to/project",
|
||||||
|
"env": "Environment Variables",
|
||||||
|
"envPlaceholder": "KEY=VALUE\n(one per line)",
|
||||||
|
"preview": "Configuration Preview",
|
||||||
|
"apply": "Apply Configuration"
|
||||||
|
},
|
||||||
"id": "Identifier (unique)",
|
"id": "Identifier (unique)",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"cwd": "Working Directory (optional)",
|
"cwd": "Working Directory (optional)",
|
||||||
@@ -287,6 +311,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"idRequired": "Please enter identifier",
|
"idRequired": "Please enter identifier",
|
||||||
|
"jsonInvalid": "Invalid JSON format",
|
||||||
"commandRequired": "Please enter command",
|
"commandRequired": "Please enter command",
|
||||||
"saveFailed": "Save failed",
|
"saveFailed": "Save failed",
|
||||||
"deleteFailed": "Delete failed"
|
"deleteFailed": "Delete failed"
|
||||||
|
|||||||
@@ -265,6 +265,30 @@
|
|||||||
"template": {
|
"template": {
|
||||||
"fetch": "快速模板:mcp-fetch"
|
"fetch": "快速模板:mcp-fetch"
|
||||||
},
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "服务器标题",
|
||||||
|
"titlePlaceholder": "my-mcp-server",
|
||||||
|
"description": "描述",
|
||||||
|
"descriptionPlaceholder": "可选的描述信息",
|
||||||
|
"jsonConfig": "JSON 配置",
|
||||||
|
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||||
|
"useWizard": "或者使用配置向导"
|
||||||
|
},
|
||||||
|
"wizard": {
|
||||||
|
"title": "MCP 配置向导",
|
||||||
|
"hint": "快速配置 MCP 服务器,自动生成 JSON 配置",
|
||||||
|
"type": "类型",
|
||||||
|
"command": "命令",
|
||||||
|
"commandPlaceholder": "uvx",
|
||||||
|
"args": "参数",
|
||||||
|
"argsPlaceholder": "mcp-server-fetch",
|
||||||
|
"cwd": "工作目录",
|
||||||
|
"cwdPlaceholder": "/path/to/project",
|
||||||
|
"env": "环境变量",
|
||||||
|
"envPlaceholder": "KEY=VALUE\n(一行一个)",
|
||||||
|
"preview": "配置预览",
|
||||||
|
"apply": "应用配置"
|
||||||
|
},
|
||||||
"id": "标识 (唯一)",
|
"id": "标识 (唯一)",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"cwd": "工作目录 (可选)",
|
"cwd": "工作目录 (可选)",
|
||||||
@@ -287,6 +311,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"idRequired": "请填写标识",
|
"idRequired": "请填写标识",
|
||||||
|
"jsonInvalid": "JSON 格式错误,请检查",
|
||||||
"commandRequired": "请填写命令",
|
"commandRequired": "请填写命令",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
"deleteFailed": "删除失败"
|
"deleteFailed": "删除失败"
|
||||||
|
|||||||
Reference in New Issue
Block a user