fix(app): eliminate startup white screen by replacing @iarna/toml with browser-friendly smol-toml
- chore(deps): switch TOML dependency from @iarna/toml to smol-toml and update lockfile - feat(mcp): add TOML editing/validation for Codex while keeping JSON for Claude; support auto ID extraction from TOML and JSON->TOML conversion for wizard output; add pre-submit required checks (stdio.command / http.url) - refactor(mcp): unify JSON/TOML validation errors via i18n; add formatTomlError for consistent, localized messages; consolidate state into formConfig/configError - feat(i18n): add TOML labels/placeholders and error keys (tomlConfig, tomlPlaceholder, tomlInvalid) - feat(utils): introduce tomlUtils with parse/stringify/validate/convert helpers using smol-toml; provide tomlToMcpServer, mcpServerToToml, extractIdFromToml, validateToml - build: confirm Vite no longer externalizes Node builtins during build; renderer builds without 'Module 'stream' has been externalized' warning
This commit is contained in:
@@ -7,6 +7,12 @@ import { buttonStyles, inputStyles } from "../../lib/styles";
|
||||
import McpWizardModal from "./McpWizardModal";
|
||||
import { extractErrorMessage } from "../../utils/errorUtils";
|
||||
import { AppType } from "../../lib/tauri-api";
|
||||
import {
|
||||
validateToml,
|
||||
tomlToMcpServer,
|
||||
extractIdFromToml,
|
||||
mcpServerToToml,
|
||||
} from "../../utils/tomlUtils";
|
||||
|
||||
interface McpFormModalProps {
|
||||
appType: AppType;
|
||||
@@ -22,25 +28,10 @@ interface McpFormModalProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 JSON 格式
|
||||
*/
|
||||
const validateJson = (text: string): string => {
|
||||
if (!text.trim()) return "";
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return "JSON 必须是对象";
|
||||
}
|
||||
return "";
|
||||
} catch {
|
||||
return "JSON 格式错误";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP 表单模态框组件(简化版)
|
||||
* 仅包含:标题(必填)、描述(可选)、JSON 配置(可选,带格式校验)
|
||||
* Claude: 使用 JSON 格式
|
||||
* Codex: 使用 TOML 格式
|
||||
*/
|
||||
const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
appType,
|
||||
@@ -52,14 +43,45 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// JSON 基本校验(返回 i18n 文案)
|
||||
const validateJson = (text: string): string => {
|
||||
if (!text.trim()) return "";
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return t("mcp.error.jsonInvalid");
|
||||
}
|
||||
return "";
|
||||
} catch {
|
||||
return t("mcp.error.jsonInvalid");
|
||||
}
|
||||
};
|
||||
|
||||
// 统一格式化 TOML 错误(本地化 + 详情)
|
||||
const formatTomlError = (err: string): string => {
|
||||
if (!err) return "";
|
||||
if (err === "mustBeObject" || err === "parseError") {
|
||||
return t("mcp.error.tomlInvalid");
|
||||
}
|
||||
return `${t("mcp.error.tomlInvalid")}: ${err}`;
|
||||
};
|
||||
const [formId, setFormId] = useState(editingId || "");
|
||||
const [formDescription, setFormDescription] = useState(
|
||||
(initialData as any)?.description || "",
|
||||
);
|
||||
const [formJson, setFormJson] = useState(
|
||||
initialData ? JSON.stringify(initialData, null, 2) : "",
|
||||
);
|
||||
const [jsonError, setJsonError] = useState("");
|
||||
|
||||
// 根据 appType 决定初始格式
|
||||
const [formConfig, setFormConfig] = useState(() => {
|
||||
if (!initialData) return "";
|
||||
if (appType === "codex") {
|
||||
return mcpServerToToml(initialData);
|
||||
} else {
|
||||
return JSON.stringify(initialData, null, 2);
|
||||
}
|
||||
});
|
||||
|
||||
const [configError, setConfigError] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [idError, setIdError] = useState("");
|
||||
@@ -67,6 +89,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
// 编辑模式下禁止修改 ID
|
||||
const isEditing = !!editingId;
|
||||
|
||||
// 判断是否使用 TOML 格式
|
||||
const useToml = appType === "codex";
|
||||
|
||||
// 预设选择状态(仅新增模式显示;-1 表示自定义)
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||
isEditing ? null : -1,
|
||||
@@ -96,10 +121,20 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
const id = ensureUniqueId(p.id);
|
||||
setFormId(id);
|
||||
setFormDescription(p.description || "");
|
||||
const json = JSON.stringify(p.server, null, 2);
|
||||
setFormJson(json);
|
||||
// 触发一次校验
|
||||
setJsonError(validateJson(json));
|
||||
|
||||
// 根据格式转换配置
|
||||
if (useToml) {
|
||||
const toml = mcpServerToToml(p.server);
|
||||
setFormConfig(toml);
|
||||
{
|
||||
const err = validateToml(toml);
|
||||
setConfigError(formatTomlError(err));
|
||||
}
|
||||
} else {
|
||||
const json = JSON.stringify(p.server, null, 2);
|
||||
setFormConfig(json);
|
||||
setConfigError(validateJson(json));
|
||||
}
|
||||
setSelectedPreset(index);
|
||||
};
|
||||
|
||||
@@ -109,53 +144,101 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
// 恢复到空白模板
|
||||
setFormId("");
|
||||
setFormDescription("");
|
||||
setFormJson("");
|
||||
setJsonError("");
|
||||
setFormConfig("");
|
||||
setConfigError("");
|
||||
};
|
||||
|
||||
const handleJsonChange = (value: string) => {
|
||||
setFormJson(value);
|
||||
const handleConfigChange = (value: string) => {
|
||||
setFormConfig(value);
|
||||
|
||||
// 基础 JSON 校验
|
||||
const baseErr = validateJson(value);
|
||||
if (baseErr) {
|
||||
setJsonError(baseErr);
|
||||
return;
|
||||
}
|
||||
if (useToml) {
|
||||
// TOML 校验
|
||||
const err = validateToml(value);
|
||||
if (err) {
|
||||
setConfigError(formatTomlError(err));
|
||||
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"));
|
||||
// 尝试解析并做必填字段提示
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const server = tomlToMcpServer(value);
|
||||
if (server.type === "stdio" && !server.command?.trim()) {
|
||||
setConfigError(t("mcp.error.commandRequired"));
|
||||
return;
|
||||
}
|
||||
if (server.type === "http" && !server.url?.trim()) {
|
||||
setConfigError(t("mcp.wizard.urlRequired"));
|
||||
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;
|
||||
// 尝试提取 ID(如果用户还没有填写)
|
||||
if (!formId.trim()) {
|
||||
const extractedId = extractIdFromToml(value);
|
||||
if (extractedId) {
|
||||
setFormId(extractedId);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || String(e);
|
||||
setConfigError(formatTomlError(msg));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// JSON 校验
|
||||
const baseErr = validateJson(value);
|
||||
if (baseErr) {
|
||||
setConfigError(baseErr);
|
||||
return;
|
||||
}
|
||||
|
||||
// 进一步结构校验
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const obj = JSON.parse(value);
|
||||
if (obj && typeof obj === "object") {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) {
|
||||
setConfigError(t("mcp.error.singleServerObjectRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
const typ = (obj as any)?.type;
|
||||
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
||||
setConfigError(t("mcp.error.commandRequired"));
|
||||
return;
|
||||
}
|
||||
if (typ === "http" && !(obj as any)?.url?.trim()) {
|
||||
setConfigError(t("mcp.wizard.urlRequired"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 解析异常已在基础校验覆盖
|
||||
}
|
||||
} catch {
|
||||
// 解析异常已在基础校验覆盖
|
||||
}
|
||||
}
|
||||
|
||||
setJsonError("");
|
||||
setConfigError("");
|
||||
};
|
||||
|
||||
const handleWizardApply = (title: string, json: string) => {
|
||||
setFormId(title);
|
||||
setFormJson(json);
|
||||
setJsonError(validateJson(json));
|
||||
// Wizard 返回的是 JSON,根据格式决定是否需要转换
|
||||
if (useToml) {
|
||||
try {
|
||||
const server = JSON.parse(json) as McpServer;
|
||||
const toml = mcpServerToToml(server);
|
||||
setFormConfig(toml);
|
||||
const err = validateToml(toml);
|
||||
setConfigError(formatTomlError(err));
|
||||
} catch (e: any) {
|
||||
setConfigError(t("mcp.error.jsonInvalid"));
|
||||
}
|
||||
} else {
|
||||
setFormConfig(json);
|
||||
setConfigError(validateJson(json));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -170,39 +253,74 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 JSON
|
||||
const currentJsonError = validateJson(formJson);
|
||||
setJsonError(currentJsonError);
|
||||
if (currentJsonError) {
|
||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
// 验证配置格式
|
||||
let server: McpServer;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
let server: McpServer;
|
||||
if (formJson.trim()) {
|
||||
// 解析 JSON 配置
|
||||
server = JSON.parse(formJson) as McpServer;
|
||||
if (useToml) {
|
||||
// TOML 模式
|
||||
const tomlError = validateToml(formConfig);
|
||||
setConfigError(formatTomlError(tomlError));
|
||||
if (tomlError) {
|
||||
onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 前置必填校验,避免后端拒绝后才提示
|
||||
if (server?.type === "stdio" && !server?.command?.trim()) {
|
||||
onNotify?.(t("mcp.error.commandRequired"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
if (server?.type === "http" && !server?.url?.trim()) {
|
||||
onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 空 JSON 时提供默认值(注意:后端会校验 stdio 需要非空 command / http 需要 url)
|
||||
if (!formConfig.trim()) {
|
||||
// 空配置
|
||||
server = {
|
||||
type: "stdio",
|
||||
command: "",
|
||||
args: [],
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
server = tomlToMcpServer(formConfig);
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || String(e);
|
||||
setConfigError(formatTomlError(msg));
|
||||
onNotify?.(t("mcp.error.tomlInvalid"), "error", 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// JSON 模式
|
||||
const jsonError = validateJson(formConfig);
|
||||
setConfigError(jsonError);
|
||||
if (jsonError) {
|
||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formConfig.trim()) {
|
||||
// 空配置
|
||||
server = {
|
||||
type: "stdio",
|
||||
command: "",
|
||||
args: [],
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
server = JSON.parse(formConfig) as McpServer;
|
||||
} catch (e: any) {
|
||||
setConfigError(t("mcp.error.jsonInvalid"));
|
||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 前置必填校验
|
||||
if (server?.type === "stdio" && !server?.command?.trim()) {
|
||||
onNotify?.(t("mcp.error.commandRequired"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
if (server?.type === "http" && !server?.url?.trim()) {
|
||||
onNotify?.(t("mcp.wizard.urlRequired"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// 保留原有的 enabled 状态
|
||||
if (initialData?.enabled !== undefined) {
|
||||
server.enabled = initialData.enabled;
|
||||
@@ -213,10 +331,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
(server as any).description = formDescription.trim();
|
||||
}
|
||||
|
||||
// 显式等待父组件保存流程,以便正确处理成功/失败
|
||||
// 显式等待父组件保存流程
|
||||
await onSave(formId.trim(), server);
|
||||
} catch (error: any) {
|
||||
// 提取后端错误信息(支持 string / {message} / tauri payload)
|
||||
const detail = extractErrorMessage(error);
|
||||
const msg = detail || t("mcp.error.saveFailed");
|
||||
onNotify?.(msg, "error", detail ? 6000 : 4000);
|
||||
@@ -328,11 +445,11 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* JSON 配置 */}
|
||||
{/* 配置输入框(根据格式显示 JSON 或 TOML) */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t("mcp.form.jsonConfig")}
|
||||
{useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")}
|
||||
</label>
|
||||
{(isEditing || selectedPreset === -1) && (
|
||||
<button
|
||||
@@ -346,14 +463,18 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
||||
</div>
|
||||
<textarea
|
||||
className={`${inputStyles.text} h-48 resize-none font-mono text-xs`}
|
||||
placeholder={t("mcp.form.jsonPlaceholder")}
|
||||
value={formJson}
|
||||
onChange={(e) => handleJsonChange(e.target.value)}
|
||||
placeholder={
|
||||
useToml
|
||||
? t("mcp.form.tomlPlaceholder")
|
||||
: t("mcp.form.jsonPlaceholder")
|
||||
}
|
||||
value={formConfig}
|
||||
onChange={(e) => handleConfigChange(e.target.value)}
|
||||
/>
|
||||
{jsonError && (
|
||||
{configError && (
|
||||
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm">
|
||||
<AlertCircle size={16} />
|
||||
<span>{jsonError}</span>
|
||||
<span>{configError}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -285,6 +285,8 @@
|
||||
"descriptionPlaceholder": "Optional description",
|
||||
"jsonConfig": "JSON Configuration",
|
||||
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||
"tomlConfig": "TOML Configuration",
|
||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||
"useWizard": "Config Wizard"
|
||||
},
|
||||
"wizard": {
|
||||
@@ -328,6 +330,7 @@
|
||||
"idRequired": "Please enter identifier",
|
||||
"idExists": "Identifier already exists. Please choose another.",
|
||||
"jsonInvalid": "Invalid JSON format",
|
||||
"tomlInvalid": "Invalid TOML format",
|
||||
"commandRequired": "Please enter command",
|
||||
"singleServerObjectRequired": "Please paste a single MCP server object (do not include top-level mcpServers)",
|
||||
"saveFailed": "Save failed",
|
||||
|
||||
@@ -285,6 +285,8 @@
|
||||
"descriptionPlaceholder": "可选的描述信息",
|
||||
"jsonConfig": "JSON 配置",
|
||||
"jsonPlaceholder": "{\n \"type\": \"stdio\",\n \"command\": \"uvx\",\n \"args\": [\"mcp-server-fetch\"]\n}",
|
||||
"tomlConfig": "TOML 配置",
|
||||
"tomlPlaceholder": "type = \"stdio\"\ncommand = \"uvx\"\nargs = [\"mcp-server-fetch\"]",
|
||||
"useWizard": "配置向导"
|
||||
},
|
||||
"wizard": {
|
||||
@@ -328,6 +330,7 @@
|
||||
"idRequired": "请填写标识",
|
||||
"idExists": "该标识已存在,请更换",
|
||||
"jsonInvalid": "JSON 格式错误,请检查",
|
||||
"tomlInvalid": "TOML 格式错误,请检查",
|
||||
"commandRequired": "请填写命令",
|
||||
"singleServerObjectRequired": "此处只需单个服务器对象,请不要粘贴包含 mcpServers 的整份配置",
|
||||
"saveFailed": "保存失败",
|
||||
|
||||
202
src/utils/tomlUtils.ts
Normal file
202
src/utils/tomlUtils.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
||||
import { McpServer } from "../types";
|
||||
|
||||
/**
|
||||
* 验证 TOML 格式并转换为 JSON 对象
|
||||
* @param text TOML 文本
|
||||
* @returns 错误信息(空字符串表示成功)
|
||||
*/
|
||||
export const validateToml = (text: string): string => {
|
||||
if (!text.trim()) return "";
|
||||
try {
|
||||
const parsed = parseToml(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return "mustBeObject";
|
||||
}
|
||||
return "";
|
||||
} catch (e: any) {
|
||||
// 返回底层错误信息,由上层进行 i18n 包装
|
||||
return e?.message || "parseError";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 McpServer 对象转换为 TOML 字符串
|
||||
* 使用 @iarna/toml 的 stringify,自动处理转义与嵌套表
|
||||
*/
|
||||
export const mcpServerToToml = (server: McpServer): string => {
|
||||
const obj: any = {};
|
||||
if (server.type) obj.type = server.type;
|
||||
|
||||
if (server.type === "stdio") {
|
||||
if (server.command !== undefined) obj.command = server.command;
|
||||
if (server.args && Array.isArray(server.args)) obj.args = server.args;
|
||||
if (server.cwd !== undefined) obj.cwd = server.cwd;
|
||||
if (server.env && typeof server.env === "object") obj.env = server.env;
|
||||
} else if (server.type === "http") {
|
||||
if (server.url !== undefined) obj.url = server.url;
|
||||
if (server.headers && typeof server.headers === "object")
|
||||
obj.headers = server.headers;
|
||||
}
|
||||
|
||||
// 去除未定义字段,确保输出更干净
|
||||
for (const k of Object.keys(obj)) {
|
||||
if (obj[k] === undefined) delete obj[k];
|
||||
}
|
||||
|
||||
// stringify 默认会带换行,做一次 trim 以适配文本框展示
|
||||
return stringifyToml(obj).trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 TOML 文本转换为 McpServer 对象(单个服务器配置)
|
||||
* 支持两种格式:
|
||||
* 1. 直接的服务器配置(type, command, args 等)
|
||||
* 2. [mcp.servers.<id>] 或 [mcp_servers.<id>] 格式(取第一个服务器)
|
||||
* @param tomlText TOML 文本
|
||||
* @returns McpServer 对象
|
||||
* @throws 解析或转换失败时抛出错误
|
||||
*/
|
||||
export const tomlToMcpServer = (tomlText: string): McpServer => {
|
||||
if (!tomlText.trim()) {
|
||||
throw new Error("TOML 内容不能为空");
|
||||
}
|
||||
|
||||
const parsed = parseToml(tomlText);
|
||||
|
||||
// 情况 1: 直接是服务器配置(包含 type/command/url 等字段)
|
||||
if (
|
||||
parsed.type ||
|
||||
parsed.command ||
|
||||
parsed.url ||
|
||||
parsed.args ||
|
||||
parsed.env
|
||||
) {
|
||||
return normalizeServerConfig(parsed);
|
||||
}
|
||||
|
||||
// 情况 2: [mcp.servers.<id>] 格式
|
||||
if (parsed.mcp && typeof parsed.mcp === "object") {
|
||||
const mcpObj = parsed.mcp as any;
|
||||
if (mcpObj.servers && typeof mcpObj.servers === "object") {
|
||||
const serverIds = Object.keys(mcpObj.servers);
|
||||
if (serverIds.length > 0) {
|
||||
const firstServer = mcpObj.servers[serverIds[0]];
|
||||
return normalizeServerConfig(firstServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 情况 3: [mcp_servers.<id>] 格式
|
||||
if (parsed.mcp_servers && typeof parsed.mcp_servers === "object") {
|
||||
const serverIds = Object.keys(parsed.mcp_servers);
|
||||
if (serverIds.length > 0) {
|
||||
const firstServer = (parsed.mcp_servers as any)[serverIds[0]];
|
||||
return normalizeServerConfig(firstServer);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"无法识别的 TOML 格式。请提供单个 MCP 服务器配置,或使用 [mcp.servers.<id>] 格式",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 规范化服务器配置对象为 McpServer 格式
|
||||
*/
|
||||
function normalizeServerConfig(config: any): McpServer {
|
||||
if (!config || typeof config !== "object") {
|
||||
throw new Error("服务器配置必须是对象");
|
||||
}
|
||||
|
||||
const type = (config.type as string) || "stdio";
|
||||
|
||||
if (type === "stdio") {
|
||||
if (!config.command || typeof config.command !== "string") {
|
||||
throw new Error("stdio 类型的 MCP 服务器必须包含 command 字段");
|
||||
}
|
||||
|
||||
const server: McpServer = {
|
||||
type: "stdio",
|
||||
command: config.command,
|
||||
};
|
||||
|
||||
// 可选字段
|
||||
if (config.args && Array.isArray(config.args)) {
|
||||
server.args = config.args.map((arg: any) => String(arg));
|
||||
}
|
||||
if (config.env && typeof config.env === "object") {
|
||||
const env: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(config.env)) {
|
||||
env[k] = String(v);
|
||||
}
|
||||
server.env = env;
|
||||
}
|
||||
if (config.cwd && typeof config.cwd === "string") {
|
||||
server.cwd = config.cwd;
|
||||
}
|
||||
|
||||
return server;
|
||||
} else if (type === "http") {
|
||||
if (!config.url || typeof config.url !== "string") {
|
||||
throw new Error("http 类型的 MCP 服务器必须包含 url 字段");
|
||||
}
|
||||
|
||||
const server: McpServer = {
|
||||
type: "http",
|
||||
url: config.url,
|
||||
};
|
||||
|
||||
// 可选字段
|
||||
if (config.headers && typeof config.headers === "object") {
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(config.headers)) {
|
||||
headers[k] = String(v);
|
||||
}
|
||||
server.headers = headers;
|
||||
}
|
||||
|
||||
return server;
|
||||
} else {
|
||||
throw new Error(`不支持的 MCP 服务器类型: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试从 TOML 中提取合理的服务器 ID/标题
|
||||
* @param tomlText TOML 文本
|
||||
* @returns 建议的 ID,失败返回空字符串
|
||||
*/
|
||||
export const extractIdFromToml = (tomlText: string): string => {
|
||||
try {
|
||||
const parsed = parseToml(tomlText);
|
||||
|
||||
// 尝试从 [mcp.servers.<id>] 或 [mcp_servers.<id>] 中提取 ID
|
||||
if (parsed.mcp && typeof parsed.mcp === "object") {
|
||||
const mcpObj = parsed.mcp as any;
|
||||
if (mcpObj.servers && typeof mcpObj.servers === "object") {
|
||||
const serverIds = Object.keys(mcpObj.servers);
|
||||
if (serverIds.length > 0) {
|
||||
return serverIds[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.mcp_servers && typeof parsed.mcp_servers === "object") {
|
||||
const serverIds = Object.keys(parsed.mcp_servers);
|
||||
if (serverIds.length > 0) {
|
||||
return serverIds[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从 command 中推断
|
||||
if (parsed.command && typeof parsed.command === "string") {
|
||||
const cmd = parsed.command.split(/[\\/]/).pop() || "";
|
||||
return cmd.replace(/\.(exe|bat|sh|js|py)$/i, "");
|
||||
}
|
||||
} catch {
|
||||
// 解析失败,返回空
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
Reference in New Issue
Block a user