refactor: extract validation logic into useMcpValidation hook
Extract all MCP form validation logic into a reusable custom hook to improve code organization and enable reuse across components. Changes: - Create useMcpValidation hook with 4 validation functions: * validateJson: basic JSON structure validation * formatTomlError: unified TOML error formatting with i18n * validateTomlConfig: complete TOML validation with required fields * validateJsonConfig: complete JSON validation with structure checks - Update McpFormModal to use the hook instead of inline validation - Simplify validation calls throughout the component - Reduce code duplication while maintaining all functionality Benefits: - Validation logic can be reused in other MCP-related components - Easier to test validation in isolation - Better separation of concerns - McpFormModal remains at 699 lines (original: 767), kept cohesive The component stays as one piece since its 700 lines represent a single, cohesive form feature rather than multiple unrelated concerns.
This commit is contained in:
@@ -29,11 +29,11 @@ import {
|
|||||||
translateMcpBackendError,
|
translateMcpBackendError,
|
||||||
} from "../../utils/errorUtils";
|
} from "../../utils/errorUtils";
|
||||||
import {
|
import {
|
||||||
validateToml,
|
|
||||||
tomlToMcpServer,
|
tomlToMcpServer,
|
||||||
extractIdFromToml,
|
extractIdFromToml,
|
||||||
mcpServerToToml,
|
mcpServerToToml,
|
||||||
} from "../../utils/tomlUtils";
|
} from "../../utils/tomlUtils";
|
||||||
|
import { useMcpValidation } from "./useMcpValidation";
|
||||||
|
|
||||||
interface McpFormModalProps {
|
interface McpFormModalProps {
|
||||||
appType: AppType;
|
appType: AppType;
|
||||||
@@ -68,29 +68,9 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
onNotify,
|
onNotify,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { formatTomlError, validateTomlConfig, validateJsonConfig } =
|
||||||
|
useMcpValidation();
|
||||||
|
|
||||||
// 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(
|
const [formId, setFormId] = useState(
|
||||||
() => editingId || initialData?.id || "",
|
() => editingId || initialData?.id || "",
|
||||||
);
|
);
|
||||||
@@ -231,14 +211,11 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
if (useToml) {
|
if (useToml) {
|
||||||
const toml = mcpServerToToml(presetWithDesc.server);
|
const toml = mcpServerToToml(presetWithDesc.server);
|
||||||
setFormConfig(toml);
|
setFormConfig(toml);
|
||||||
{
|
setConfigError(validateTomlConfig(toml));
|
||||||
const err = validateToml(toml);
|
|
||||||
setConfigError(formatTomlError(err));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const json = JSON.stringify(presetWithDesc.server, null, 2);
|
const json = JSON.stringify(presetWithDesc.server, null, 2);
|
||||||
setFormConfig(json);
|
setFormConfig(json);
|
||||||
setConfigError(validateJson(json));
|
setConfigError(validateJsonConfig(json));
|
||||||
}
|
}
|
||||||
setSelectedPreset(index);
|
setSelectedPreset(index);
|
||||||
};
|
};
|
||||||
@@ -261,71 +238,27 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
setFormConfig(value);
|
setFormConfig(value);
|
||||||
|
|
||||||
if (useToml) {
|
if (useToml) {
|
||||||
// TOML 校验
|
// TOML validation (use hook's complete validation)
|
||||||
const err = validateToml(value);
|
const err = validateTomlConfig(value);
|
||||||
if (err) {
|
if (err) {
|
||||||
setConfigError(formatTomlError(err));
|
setConfigError(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试解析并做必填字段提示
|
// Try to extract ID (if user hasn't filled it yet)
|
||||||
if (value.trim()) {
|
if (value.trim() && !formId.trim()) {
|
||||||
try {
|
const extractedId = extractIdFromToml(value);
|
||||||
const server = tomlToMcpServer(value);
|
if (extractedId) {
|
||||||
if (server.type === "stdio" && !server.command?.trim()) {
|
setFormId(extractedId);
|
||||||
setConfigError(t("mcp.error.commandRequired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (server.type === "http" && !server.url?.trim()) {
|
|
||||||
setConfigError(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 {
|
} else {
|
||||||
// JSON 校验
|
// JSON validation (use hook's complete validation)
|
||||||
const baseErr = validateJson(value);
|
const err = validateJsonConfig(value);
|
||||||
if (baseErr) {
|
if (err) {
|
||||||
setConfigError(baseErr);
|
setConfigError(err);
|
||||||
return;
|
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 {
|
|
||||||
// 解析异常已在基础校验覆盖
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigError("");
|
setConfigError("");
|
||||||
@@ -336,20 +269,19 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
if (!formName.trim()) {
|
if (!formName.trim()) {
|
||||||
setFormName(title);
|
setFormName(title);
|
||||||
}
|
}
|
||||||
// Wizard 返回的是 JSON,根据格式决定是否需要转换
|
// Wizard returns JSON, convert based on format if needed
|
||||||
if (useToml) {
|
if (useToml) {
|
||||||
try {
|
try {
|
||||||
const server = JSON.parse(json) as McpServerSpec;
|
const server = JSON.parse(json) as McpServerSpec;
|
||||||
const toml = mcpServerToToml(server);
|
const toml = mcpServerToToml(server);
|
||||||
setFormConfig(toml);
|
setFormConfig(toml);
|
||||||
const err = validateToml(toml);
|
setConfigError(validateTomlConfig(toml));
|
||||||
setConfigError(formatTomlError(err));
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setConfigError(t("mcp.error.jsonInvalid"));
|
setConfigError(t("mcp.error.jsonInvalid"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setFormConfig(json);
|
setFormConfig(json);
|
||||||
setConfigError(validateJson(json));
|
setConfigError(validateJsonConfig(json));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -366,20 +298,20 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证配置格式
|
// Validate configuration format
|
||||||
let serverSpec: McpServerSpec;
|
let serverSpec: McpServerSpec;
|
||||||
|
|
||||||
if (useToml) {
|
if (useToml) {
|
||||||
// TOML 模式
|
// TOML mode
|
||||||
const tomlError = validateToml(formConfig);
|
const tomlError = validateTomlConfig(formConfig);
|
||||||
setConfigError(formatTomlError(tomlError));
|
setConfigError(tomlError);
|
||||||
if (tomlError) {
|
if (tomlError) {
|
||||||
onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000);
|
onNotify?.(t("mcp.error.tomlInvalid"), "error", 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formConfig.trim()) {
|
if (!formConfig.trim()) {
|
||||||
// 空配置
|
// Empty configuration
|
||||||
serverSpec = {
|
serverSpec = {
|
||||||
type: "stdio",
|
type: "stdio",
|
||||||
command: "",
|
command: "",
|
||||||
@@ -396,8 +328,8 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// JSON 模式
|
// JSON mode
|
||||||
const jsonError = validateJson(formConfig);
|
const jsonError = validateJsonConfig(formConfig);
|
||||||
setConfigError(jsonError);
|
setConfigError(jsonError);
|
||||||
if (jsonError) {
|
if (jsonError) {
|
||||||
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
onNotify?.(t("mcp.error.jsonInvalid"), "error", 3000);
|
||||||
@@ -405,7 +337,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!formConfig.trim()) {
|
if (!formConfig.trim()) {
|
||||||
// 空配置
|
// Empty configuration
|
||||||
serverSpec = {
|
serverSpec = {
|
||||||
type: "stdio",
|
type: "stdio",
|
||||||
command: "",
|
command: "",
|
||||||
|
|||||||
94
src/components/mcp/useMcpValidation.ts
Normal file
94
src/components/mcp/useMcpValidation.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { validateToml, tomlToMcpServer } from "@/utils/tomlUtils";
|
||||||
|
|
||||||
|
export function useMcpValidation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// JSON basic validation (returns i18n text)
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unified TOML error formatting (localization + details)
|
||||||
|
const formatTomlError = (err: string): string => {
|
||||||
|
if (!err) return "";
|
||||||
|
if (err === "mustBeObject" || err === "parseError") {
|
||||||
|
return t("mcp.error.tomlInvalid");
|
||||||
|
}
|
||||||
|
return `${t("mcp.error.tomlInvalid")}: ${err}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Full TOML validation (including required field checks)
|
||||||
|
const validateTomlConfig = (value: string): string => {
|
||||||
|
const err = validateToml(value);
|
||||||
|
if (err) {
|
||||||
|
return formatTomlError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse and check required fields
|
||||||
|
if (value.trim()) {
|
||||||
|
try {
|
||||||
|
const server = tomlToMcpServer(value);
|
||||||
|
if (server.type === "stdio" && !server.command?.trim()) {
|
||||||
|
return t("mcp.error.commandRequired");
|
||||||
|
}
|
||||||
|
if (server.type === "http" && !server.url?.trim()) {
|
||||||
|
return t("mcp.wizard.urlRequired");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e?.message || String(e);
|
||||||
|
return formatTomlError(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Full JSON validation (including structure checks)
|
||||||
|
const validateJsonConfig = (value: string): string => {
|
||||||
|
const baseErr = validateJson(value);
|
||||||
|
if (baseErr) {
|
||||||
|
return baseErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Further structure validation
|
||||||
|
if (value.trim()) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(value);
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, "mcpServers")) {
|
||||||
|
return t("mcp.error.singleServerObjectRequired");
|
||||||
|
}
|
||||||
|
|
||||||
|
const typ = (obj as any)?.type;
|
||||||
|
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
||||||
|
return t("mcp.error.commandRequired");
|
||||||
|
}
|
||||||
|
if (typ === "http" && !(obj as any)?.url?.trim()) {
|
||||||
|
return t("mcp.wizard.urlRequired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Parse errors already covered by base validation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateJson,
|
||||||
|
formatTomlError,
|
||||||
|
validateTomlConfig,
|
||||||
|
validateJsonConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user