feat(mcp): inline presets in panel with one-click enable

- Show not-installed MCP presets directly in the list, consistent with existing UI (no modal)
- Toggle now supports enabling presets by writing to ~/.claude.json (mcpServers) and refreshing list
- Keep installed MCP entries unchanged (edit/delete/toggle)

fix(mcp): robust error handling and pre-submit validation

- Use extractErrorMessage in MCP panel and form to surface backend details
- Prevent pasting full config (with mcpServers) into single-server JSON field
- Add required-field checks: stdio requires non-empty command; http requires non-empty url

i18n: add messages for single-server validation and preset labels

chore: add data-only MCP presets file (no new dependencies)
This commit is contained in:
Jason
2025-10-09 17:21:03 +08:00
parent 2bb847cb3d
commit 0be596afb5
5 changed files with 214 additions and 36 deletions

View File

@@ -4,6 +4,7 @@ import { X, Save, AlertCircle } from "lucide-react";
import { McpServer } from "../../types";
import { buttonStyles, inputStyles } from "../../lib/styles";
import McpWizardModal from "./McpWizardModal";
import { extractErrorMessage } from "../../utils/errorUtils";
interface McpFormModalProps {
editingId?: string;
@@ -53,7 +54,41 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
const handleJsonChange = (value: string) => {
setFormJson(value);
setJsonError(validateJson(value));
// 基础 JSON 校验
const baseErr = validateJson(value);
if (baseErr) {
setJsonError(baseErr);
return;
}
// 进一步结构校验:仅允许单个服务器对象,禁止整份配置
if (value.trim()) {
try {
const obj = JSON.parse(value);
if (obj && typeof obj === "object") {
if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) {
setJsonError(t("mcp.error.singleServerObjectRequired"));
return;
}
// 若带有类型,做必填字段提示(不阻止输入,仅给出即时反馈)
const typ = (obj as any)?.type;
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
setJsonError(t("mcp.error.commandRequired"));
return;
}
if (typ === "http" && !(obj as any)?.url?.trim()) {
setJsonError(t("mcp.wizard.urlRequired"));
return;
}
}
} catch {
// 解析异常已在基础校验覆盖
}
}
setJsonError("");
};
const handleWizardApply = (json: string) => {
@@ -81,6 +116,16 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
if (formJson.trim()) {
// 解析 JSON 配置
server = JSON.parse(formJson) as McpServer;
// 前置必填校验,避免后端拒绝后才提示
if (server?.type === "stdio" && !server?.command?.trim()) {
alert(t("mcp.error.commandRequired"));
return;
}
if (server?.type === "http" && !server?.url?.trim()) {
alert(t("mcp.wizard.urlRequired"));
return;
}
} else {
// 空 JSON 时提供默认值(注意:后端会校验 stdio 需要非空 command / http 需要 url
server = {
@@ -98,8 +143,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
// 显式等待父组件保存流程,以便正确处理成功/失败
await onSave(formId.trim(), server);
} catch (error: any) {
// 后端错误信息直接提示给用户(例如缺少 command/url 等
const msg = error?.message || t("mcp.error.saveFailed");
// 提取后端错误信息(支持 string / {message} / tauri payload
const detail = extractErrorMessage(error);
const msg = detail || t("mcp.error.saveFailed");
alert(msg);
} finally {
setSaving(false);