diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index b3400bd..080d363 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -84,6 +84,20 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission const [urlRegisterLoading, setUrlRegisterLoading] = useState(false) const [appQuitLoading, setAppQuitLoading] = useState(false) const [appRestartLoading, setAppRestartLoading] = useState(false) + const [mcpLoading, setMcpLoading] = useState(false) + const [mcpConfig, setMcpConfig] = useState<{ host: string; port: number }>({ + host: "127.0.0.1", + port: 3901, + }) + const [mcpStatus, setMcpStatus] = useState<{ + is_running: boolean + config: { host: string; port: number } + url?: string | null + }>({ + is_running: false, + config: { host: "127.0.0.1", port: 3901 }, + url: null, + }) const canAdmin = permission === "admin" const [messageApi, contextHolder] = message.useMessage() @@ -115,6 +129,21 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } })) } + const loadMcpStatus = async () => { + if (!(window as any).api?.mcpServerStatus) return + try { + const res = await (window as any).api.mcpServerStatus() + if (res.success && res.data) { + setMcpStatus(res.data) + if (res.data.config?.host && res.data.config?.port) { + setMcpConfig({ host: res.data.config.host, port: res.data.config.port }) + } + } + } catch { + // ignore status polling errors in settings page + } + } + const loadAll = async () => { if (!(window as any).api) return const res = await (window as any).api.getAllSettings() @@ -125,6 +154,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission } const authRes = await (window as any).api.authGetStatus() if (authRes.success && authRes.data) setSecurityStatus(authRes.data) + await loadMcpStatus() } useEffect(() => { @@ -456,6 +486,61 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission } } + const startMcpServer = async () => { + if (!(window as any).api?.mcpServerStart) return + const host = mcpConfig.host.trim() + const port = Number(mcpConfig.port) + if (!host) { + messageApi.warning(t("settings.mcp.hostRequired")) + return + } + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + messageApi.warning(t("settings.mcp.portInvalid")) + return + } + + setMcpLoading(true) + try { + const res = await withTimeout( + (window as any).api.mcpServerStart({ host, port }), + 10_000, + t("settings.mcp.startTimeout") + ) + if (res.success) { + messageApi.success(t("settings.mcp.startSuccess")) + } else { + messageApi.error(res.message || t("settings.mcp.startFailed")) + } + } catch (e: any) { + messageApi.error(e?.message || t("settings.mcp.startFailed")) + } finally { + await loadMcpStatus() + setMcpLoading(false) + } + } + + const stopMcpServer = async () => { + if (!(window as any).api?.mcpServerStop) return + setMcpLoading(true) + try { + const res = await withTimeout( + (window as any).api.mcpServerStop(), + 10_000, + t("settings.mcp.stopTimeout") + ) + if (res.success) { + messageApi.success(t("settings.mcp.stopSuccess")) + } else { + messageApi.error(res.message || t("settings.mcp.stopFailed")) + } + } catch (e: any) { + messageApi.error(e?.message || t("settings.mcp.stopFailed")) + } finally { + await loadMcpStatus() + setMcpLoading(false) + } + } + const currentYear = new Date().getFullYear() const confirmQuitApp = () => { @@ -1057,6 +1142,76 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission +
+ {t("settings.mcp.title")} +
+
+ {t("settings.mcp.description")} +
+ + + {mcpStatus.is_running ? t("settings.mcp.running") : t("settings.mcp.stopped")} + + {mcpStatus.url ? ( + {mcpStatus.url} + ) : ( + {t("settings.mcp.noUrl")} + )} + +
+ + setMcpConfig((prev) => ({ ...prev, host: e.target.value }))} + disabled={!canAdmin || mcpStatus.is_running} + style={{ width: "280px" }} + placeholder="127.0.0.1" + /> + + + { + const next = Number(e.target.value.replace(/[^\d]/g, "")) + if (Number.isFinite(next) && next > 0) { + setMcpConfig((prev) => ({ ...prev, port: next })) + } else if (!e.target.value) { + setMcpConfig((prev) => ({ ...prev, port: 0 })) + } + }} + disabled={!canAdmin || mcpStatus.is_running} + style={{ width: "180px" }} + placeholder="3901" + /> + + + + + + + + +
+
+ {t("settings.mcp.hint")} +
+
{t("settings.app.title")}
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 8b723a1..a395713 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -276,6 +276,27 @@ "registerFailed": "Registration failed", "installerRequired": "Requires installed SecScore, may not work in development mode." }, + "mcp": { + "title": "MCP Service", + "description": "Manage the built-in MCP HTTP service for external MCP clients.", + "running": "Running", + "stopped": "Stopped", + "noUrl": "No URL", + "host": "Host", + "port": "Port", + "start": "Start", + "stop": "Stop", + "refresh": "Refresh Status", + "hint": "Recommended to bind only to 127.0.0.1; after start, endpoint is http://host:port/mcp", + "hostRequired": "Please enter MCP host", + "portInvalid": "Please enter a valid port (1-65535)", + "startSuccess": "MCP server started", + "startFailed": "Failed to start MCP server", + "stopSuccess": "MCP server stopped", + "stopFailed": "Failed to stop MCP server", + "startTimeout": "MCP server start timed out, please try again", + "stopTimeout": "MCP server stop timed out, please try again" + }, "app": { "title": "App Controls", "description": "Quickly restart or quit the app.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 0b66ec7..71d39a2 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -276,6 +276,27 @@ "registerFailed": "注册失败", "installerRequired": "需要安装版 SecScore,开发模式下可能无效。" }, + "mcp": { + "title": "MCP 服务", + "description": "管理内置 MCP HTTP 服务,供外部 MCP 客户端调用。", + "running": "运行中", + "stopped": "已停止", + "noUrl": "暂无地址", + "host": "主机", + "port": "端口", + "start": "启动", + "stop": "停止", + "refresh": "刷新状态", + "hint": "建议仅绑定到 127.0.0.1;启动后 MCP 入口为 http://host:port/mcp", + "hostRequired": "请输入 MCP 主机地址", + "portInvalid": "请输入有效端口(1-65535)", + "startSuccess": "MCP 服务启动成功", + "startFailed": "MCP 服务启动失败", + "stopSuccess": "MCP 服务已停止", + "stopFailed": "MCP 服务停止失败", + "startTimeout": "启动 MCP 服务超时,请稍后重试", + "stopTimeout": "停止 MCP 服务超时,请稍后重试" + }, "app": { "title": "应用控制", "description": "快速重启或退出应用。", diff --git a/src/preload/types.ts b/src/preload/types.ts index 136c600..9fdacb7 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -291,6 +291,22 @@ const api = { data: { isRunning: boolean; config?: any; url?: string } }> => invoke("http_server_status"), + // MCP Server + mcpServerStart: (config?: { + port?: number + host?: string + }): Promise<{ success: boolean; data?: { url: string; config: { port: number; host: string } } }> => + invoke("mcp_server_start", { config }), + mcpServerStop: (): Promise<{ success: boolean }> => invoke("mcp_server_stop"), + mcpServerStatus: (): Promise<{ + success: boolean + data?: { + is_running: boolean + config: { port: number; host: string } + url?: string | null + } + }> => invoke("mcp_server_status"), + // File System fsGetConfigStructure: (): Promise<{ success: boolean