feat: Optimize MCP configuration logic

This commit is contained in:
Kadxy
2025-01-15 16:52:54 +08:00
parent ce13cf61a7
commit 8aa9a500fd
14 changed files with 766 additions and 653 deletions

View File

@@ -7,22 +7,29 @@ import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import RestartIcon from "../icons/reload.svg";
import EyeIcon from "../icons/eye.svg";
import GithubIcon from "../icons/github.svg";
import { List, ListItem, Modal, showToast } from "./ui-lib";
import { useNavigate } from "react-router-dom";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import presetServersJson from "../mcp/preset-server.json";
const presetServers = presetServersJson as PresetServer[];
import {
getMcpConfig,
updateMcpConfig,
getClientPrimitives,
addMcpServer,
getClientStatus,
getClientTools,
getMcpConfigFromFile,
removeMcpServer,
restartAllClients,
getClientErrors,
refreshClientStatus,
} from "../mcp/actions";
import { McpConfig, PresetServer, ServerConfig } from "../mcp/types";
import {
ListToolsResponse,
McpConfigData,
PresetServer,
ServerConfig,
} from "../mcp/types";
import clsx from "clsx";
const presetServers = presetServersJson as PresetServer[];
interface ConfigProperty {
type: string;
description?: string;
@@ -33,67 +40,71 @@ interface ConfigProperty {
export function McpMarketPage() {
const navigate = useNavigate();
const [searchText, setSearchText] = useState("");
const [config, setConfig] = useState<McpConfig>({ mcpServers: {} });
const [editingServerId, setEditingServerId] = useState<string | undefined>();
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
const [primitives, setPrimitives] = useState<any[]>([]);
const [userConfig, setUserConfig] = useState<Record<string, any>>({});
const [editingServerId, setEditingServerId] = useState<string | undefined>();
const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [clientErrors, setClientErrors] = useState<
Record<string, string | null>
const [config, setConfig] = useState<McpConfigData>();
const [clientStatuses, setClientStatuses] = useState<
Record<
string,
{
status: "active" | "error" | "undefined";
errorMsg: string | null;
}
>
>({});
// 更新服务器状态
const updateServerStatus = async () => {
await refreshClientStatus();
const errors = await getClientErrors();
setClientErrors(errors);
// 检查服务器是否已添加
const isServerAdded = (id: string) => {
return id in (config?.mcpServers ?? {});
};
// 初始加载配置
// 获取客户端状态
const updateClientStatus = async (clientId: string) => {
const status = await getClientStatus(clientId);
setClientStatuses((prev) => ({
...prev,
[clientId]: status,
}));
return status;
};
// 从服务器获取初始状态
useEffect(() => {
const init = async () => {
const loadInitialState = async () => {
try {
setIsLoading(true);
const data = await getMcpConfig();
setConfig(data);
await updateServerStatus();
const config = await getMcpConfigFromFile();
setConfig(config);
// 获取所有客户端的状态
const statuses: Record<string, any> = {};
for (const clientId of Object.keys(config.mcpServers)) {
const status = await getClientStatus(clientId);
statuses[clientId] = status;
}
setClientStatuses(statuses);
} catch (error) {
showToast("Failed to load configuration");
console.error(error);
console.error("Failed to load initial state:", error);
showToast("Failed to load initial state");
} finally {
setIsLoading(false);
}
};
init().then();
loadInitialState();
}, []);
// 保存配置
const saveConfig = async (newConfig: McpConfig) => {
try {
setIsLoading(true);
await updateMcpConfig(newConfig);
setConfig(newConfig);
// 配置改变时需要重新初始化
await restartAllClients();
await updateServerStatus();
showToast("Configuration saved successfully");
} catch (error) {
showToast("Failed to save configuration");
console.error(error);
} finally {
setIsLoading(false);
}
};
// 检查服务器是否已添加
const isServerAdded = (id: string) => {
return id in config.mcpServers;
};
// Debug: 监控状态变化
useEffect(() => {
console.log("MCP Market - Current config:", config);
console.log("MCP Market - Current clientStatuses:", clientStatuses);
}, [config, clientStatuses]);
// 加载当前编辑服务器的配置
useEffect(() => {
if (editingServerId) {
if (editingServerId && config) {
const currentConfig = config.mcpServers[editingServerId];
if (currentConfig) {
// 从当前配置中提取用户配置
@@ -123,7 +134,7 @@ export function McpMarketPage() {
setUserConfig({});
}
}
}, [editingServerId, config.mcpServers]);
}, [editingServerId, config]);
// 保存服务器配置
const saveServerConfig = async () => {
@@ -131,6 +142,7 @@ export function McpMarketPage() {
if (!preset || !preset.configSchema || !editingServerId) return;
try {
setIsLoading(true);
// 构建服务器配置
const args = [...preset.baseArgs];
const env: Record<string, string> = {};
@@ -160,22 +172,113 @@ export function McpMarketPage() {
...(Object.keys(env).length > 0 ? { env } : {}),
};
// 更新配置
const newConfig = {
...config,
mcpServers: {
...config.mcpServers,
[editingServerId]: serverConfig,
},
};
// 更新配置并初始化新服务器
const newConfig = await addMcpServer(editingServerId, serverConfig);
setConfig(newConfig);
// 更新状态
const status = await getClientStatus(editingServerId);
setClientStatuses((prev) => ({
...prev,
[editingServerId]: status,
}));
await saveConfig(newConfig);
setEditingServerId(undefined);
showToast("Server configuration saved successfully");
} catch (error) {
showToast(
error instanceof Error ? error.message : "Failed to save configuration",
);
} finally {
setIsLoading(false);
}
};
// 获取服务器支持的 Tools
const loadTools = async (id: string) => {
try {
const result = await getClientTools(id);
if (result) {
setTools(result);
} else {
throw new Error("Failed to load tools");
}
} catch (error) {
showToast("Failed to load tools");
console.error(error);
setTools(null);
}
};
// 重启所有客户端
const handleRestartAll = async () => {
try {
setIsLoading(true);
const newConfig = await restartAllClients();
setConfig(newConfig);
// 更新所有客户端状态
const statuses: Record<string, any> = {};
for (const clientId of Object.keys(newConfig.mcpServers)) {
const status = await getClientStatus(clientId);
statuses[clientId] = status;
}
setClientStatuses(statuses);
showToast("Successfully restarted all clients");
} catch (error) {
showToast("Failed to restart clients");
console.error(error);
} finally {
setIsLoading(false);
}
};
// 添加服务器
const addServer = async (preset: PresetServer) => {
if (!preset.configurable) {
try {
setIsLoading(true);
showToast("Creating MCP client...");
// 如果服务器不需要配置,直接添加
const serverConfig: ServerConfig = {
command: preset.command,
args: [...preset.baseArgs],
};
const newConfig = await addMcpServer(preset.id, serverConfig);
setConfig(newConfig);
// 更新状态
const status = await getClientStatus(preset.id);
setClientStatuses((prev) => ({
...prev,
[preset.id]: status,
}));
} finally {
setIsLoading(false);
}
} else {
// 如果需要配置,打开配置对话框
setEditingServerId(preset.id);
setUserConfig({});
}
};
// 移除服务器
const removeServer = async (id: string) => {
try {
setIsLoading(true);
const newConfig = await removeMcpServer(id);
setConfig(newConfig);
// 移除状态
setClientStatuses((prev) => {
const newStatuses = { ...prev };
delete newStatuses[id];
return newStatuses;
});
} finally {
setIsLoading(false);
}
};
@@ -188,8 +291,17 @@ export function McpMarketPage() {
([key, prop]: [string, ConfigProperty]) => {
if (prop.type === "array") {
const currentValue = userConfig[key as keyof typeof userConfig] || [];
const itemLabel = (prop as any).itemLabel || key;
const addButtonText =
(prop as any).addButtonText || `Add ${itemLabel}`;
return (
<ListItem key={key} title={key} subTitle={prop.description}>
<ListItem
key={key}
title={key}
subTitle={prop.description}
vertical
>
<div className={styles["path-list"]}>
{(currentValue as string[]).map(
(value: string, index: number) => (
@@ -197,7 +309,7 @@ export function McpMarketPage() {
<input
type="text"
value={value}
placeholder={`Path ${index + 1}`}
placeholder={`${itemLabel} ${index + 1}`}
onChange={(e) => {
const newValue = [...currentValue] as string[];
newValue[index] = e.target.value;
@@ -218,7 +330,7 @@ export function McpMarketPage() {
)}
<IconButton
icon={<AddIcon />}
text="Add Path"
text={addButtonText}
className={styles["add-button"]}
bordered
onClick={() => {
@@ -251,83 +363,146 @@ export function McpMarketPage() {
);
};
// 获取服务器的 Primitives
const loadPrimitives = async (id: string) => {
try {
setIsLoading(true);
const result = await getClientPrimitives(id);
if (result) {
setPrimitives(result);
} else {
showToast("Server is not running");
setPrimitives([]);
}
} catch (error) {
showToast("Failed to load primitives");
console.error(error);
setPrimitives([]);
} finally {
setIsLoading(false);
}
// 检查服务器状态
const checkServerStatus = (clientId: string) => {
return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
};
// 重启所有客户端
const handleRestart = async () => {
try {
setIsLoading(true);
await restartAllClients();
await updateServerStatus();
showToast("All clients restarted successfully");
} catch (error) {
showToast("Failed to restart clients");
console.error(error);
} finally {
setIsLoading(false);
}
};
// 渲染服务器列表
const renderServerList = () => {
return presetServers
.filter((server) => {
if (searchText.length === 0) return true;
const searchLower = searchText.toLowerCase();
return (
server.name.toLowerCase().includes(searchLower) ||
server.description.toLowerCase().includes(searchLower) ||
server.tags.some((tag) => tag.toLowerCase().includes(searchLower))
);
})
.sort((a, b) => {
const aStatus = checkServerStatus(a.id).status;
const bStatus = checkServerStatus(b.id).status;
// 添加服务器
const addServer = async (preset: PresetServer) => {
if (!preset.configurable) {
try {
setIsLoading(true);
showToast("Creating MCP client...");
// 如果服务器不需要配置,直接添加
const serverConfig: ServerConfig = {
command: preset.command,
args: [...preset.baseArgs],
// 定义状态优先级
const statusPriority = {
error: 0,
active: 1,
undefined: 2,
};
const newConfig = {
...config,
mcpServers: {
...config.mcpServers,
[preset.id]: serverConfig,
},
};
await saveConfig(newConfig);
} finally {
setIsLoading(false);
}
} else {
// 如果需要配置,打开配置对话框
setEditingServerId(preset.id);
setUserConfig({});
}
};
// 移除服务器
const removeServer = async (id: string) => {
try {
setIsLoading(true);
const { [id]: _, ...rest } = config.mcpServers;
const newConfig = {
...config,
mcpServers: rest,
};
await saveConfig(newConfig);
} finally {
setIsLoading(false);
}
// 首先按状态排序
if (aStatus !== bStatus) {
return statusPriority[aStatus] - statusPriority[bStatus];
}
// 然后按名称排序
return a.name.localeCompare(b.name);
})
.map((server) => (
<div
className={clsx(styles["mcp-market-item"], {
[styles["disabled"]]: isLoading,
})}
key={server.id}
>
<div className={styles["mcp-market-header"]}>
<div className={styles["mcp-market-title"]}>
<div className={styles["mcp-market-name"]}>
{server.name}
{checkServerStatus(server.id).status !== "undefined" && (
<span
className={clsx(styles["server-status"], {
[styles["error"]]:
checkServerStatus(server.id).status === "error",
})}
>
{checkServerStatus(server.id).status === "error" ? (
<>
Error
<span className={styles["error-message"]}>
: {checkServerStatus(server.id).errorMsg}
</span>
</>
) : (
"Active"
)}
</span>
)}
{server.repo && (
<a
href={server.repo}
target="_blank"
rel="noopener noreferrer"
className={styles["repo-link"]}
title="Open repository"
>
<GithubIcon />
</a>
)}
</div>
<div className={styles["tags-container"]}>
{server.tags.map((tag, index) => (
<span key={index} className={styles["tag"]}>
{tag}
</span>
))}
</div>
<div
className={clsx(styles["mcp-market-info"], "one-line")}
title={server.description}
>
{server.description}
</div>
</div>
<div className={styles["mcp-market-actions"]}>
{isServerAdded(server.id) ? (
<>
{server.configurable && (
<IconButton
icon={<EditIcon />}
text="Configure"
className={clsx({
[styles["action-error"]]:
checkServerStatus(server.id).status === "error",
})}
onClick={() => setEditingServerId(server.id)}
disabled={isLoading}
/>
)}
<IconButton
icon={<EyeIcon />}
text="Tools"
onClick={async () => {
setViewingServerId(server.id);
await loadTools(server.id);
}}
disabled={
isLoading ||
checkServerStatus(server.id).status === "error"
}
/>
<IconButton
icon={<DeleteIcon />}
text="Remove"
className={styles["action-danger"]}
onClick={() => removeServer(server.id)}
disabled={isLoading}
/>
</>
) : (
<IconButton
icon={<AddIcon />}
text="Add"
className={styles["action-primary"]}
onClick={() => addServer(server)}
disabled={isLoading}
/>
)}
</div>
</div>
</div>
));
};
return (
@@ -342,7 +517,7 @@ export function McpMarketPage() {
)}
</div>
<div className="window-header-sub-title">
{Object.keys(config.mcpServers).length} servers configured
{Object.keys(config?.mcpServers ?? {}).length} servers configured
</div>
</div>
@@ -351,7 +526,7 @@ export function McpMarketPage() {
<IconButton
icon={<RestartIcon />}
bordered
onClick={handleRestart}
onClick={handleRestartAll}
text="Restart All"
disabled={isLoading}
/>
@@ -378,121 +553,10 @@ export function McpMarketPage() {
/>
</div>
<div className={styles["server-list"]}>
{presetServers
.filter(
(m) =>
searchText.length === 0 ||
m.name.toLowerCase().includes(searchText.toLowerCase()) ||
m.description
.toLowerCase()
.includes(searchText.toLowerCase()),
)
.sort((a, b) => {
const aAdded = isServerAdded(a.id);
const bAdded = isServerAdded(b.id);
const aError = clientErrors[a.id] !== null;
const bError = clientErrors[b.id] !== null;
if (aAdded !== bAdded) {
return aAdded ? -1 : 1;
}
if (aAdded && bAdded) {
if (aError !== bError) {
return aError ? -1 : 1;
}
}
return 0;
})
.map((server) => (
<div
className={clsx(styles["mcp-market-item"], {
[styles["disabled"]]: isLoading,
})}
key={server.id}
>
<div className={styles["mcp-market-header"]}>
<div className={styles["mcp-market-title"]}>
<div className={styles["mcp-market-name"]}>
{server.name}
{isServerAdded(server.id) && (
<span
className={clsx(styles["server-status"], {
[styles["error"]]:
clientErrors[server.id] !== null,
})}
>
{clientErrors[server.id] === null
? "Active"
: "Error"}
{clientErrors[server.id] && (
<span className={styles["error-message"]}>
: {clientErrors[server.id]}
</span>
)}
</span>
)}
</div>
<div
className={clsx(styles["mcp-market-info"], "one-line")}
>
{server.description}
</div>
</div>
</div>
<div className={styles["mcp-market-actions"]}>
{isServerAdded(server.id) ? (
<>
{server.configurable && (
<IconButton
icon={<EditIcon />}
text="Configure"
className={clsx({
[styles["action-error"]]:
clientErrors[server.id] !== null,
})}
onClick={() => setEditingServerId(server.id)}
disabled={isLoading}
/>
)}
{isServerAdded(server.id) && (
<IconButton
icon={<EyeIcon />}
text="Tools"
onClick={async () => {
if (clientErrors[server.id] !== null) {
showToast("Server is not running");
return;
}
setViewingServerId(server.id);
await loadPrimitives(server.id);
}}
disabled={isLoading}
/>
)}
<IconButton
icon={<DeleteIcon />}
text="Remove"
className={styles["action-danger"]}
onClick={() => removeServer(server.id)}
disabled={isLoading}
/>
</>
) : (
<IconButton
icon={<AddIcon />}
text="Add"
className={styles["action-primary"]}
onClick={() => addServer(server)}
disabled={isLoading}
/>
)}
</div>
</div>
))}
</div>
<div className={styles["server-list"]}>{renderServerList()}</div>
</div>
{/*编辑服务器配置*/}
{editingServerId && (
<div className="modal-mask">
<Modal
@@ -521,6 +585,7 @@ export function McpMarketPage() {
</div>
)}
{/*支持的Tools*/}
{viewingServerId && (
<div className="modal-mask">
<Modal
@@ -535,24 +600,20 @@ export function McpMarketPage() {
/>,
]}
>
<div className={styles["primitives-list"]}>
<div className={styles["tools-list"]}>
{isLoading ? (
<div>Loading...</div>
) : primitives.filter((p) => p.type === "tool").length > 0 ? (
primitives
.filter((p) => p.type === "tool")
.map((primitive, index) => (
<div key={index} className={styles["primitive-item"]}>
<div className={styles["primitive-name"]}>
{primitive.value.name}
) : tools?.tools ? (
tools.tools.map(
(tool: ListToolsResponse["tools"], index: number) => (
<div key={index} className={styles["tool-item"]}>
<div className={styles["tool-name"]}>{tool.name}</div>
<div className={styles["tool-description"]}>
{tool.description}
</div>
{primitive.value.description && (
<div className={styles["primitive-description"]}>
{primitive.value.description}
</div>
)}
</div>
))
),
)
) : (
<div>No tools available</div>
)}