feat: support stop/start MCP servers

This commit is contained in:
Kadxy
2025-01-16 08:52:54 +08:00
parent e440ff56c8
commit 07c63497dc
7 changed files with 298 additions and 132 deletions

View File

@@ -98,6 +98,10 @@
background-color: #ef4444;
}
&.stopped {
background-color: #6b7280;
}
.error-message {
margin-left: 4px;
font-size: 12px;
@@ -151,21 +155,11 @@
.mcp-market-actions {
display: flex;
gap: 8px;
gap: 12px;
align-items: flex-start;
flex-shrink: 0;
min-width: 180px;
justify-content: flex-end;
:global(.icon-button) {
transition: all 0.3s ease;
border: 1px solid transparent;
&:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
}
}
}
}
@@ -213,30 +207,6 @@
color: var(--gray-300);
}
}
:global(.icon-button) {
width: 32px;
height: 32px;
padding: 0;
border-radius: 6px;
background-color: transparent;
border: 1px solid var(--gray-200);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: var(--gray-100);
border-color: var(--gray-300);
}
svg {
width: 16px;
height: 16px;
opacity: 0.7;
}
}
}
:global(.icon-button.add-path-button) {

View File

@@ -17,16 +17,20 @@ import {
getClientStatus,
getClientTools,
getMcpConfigFromFile,
removeMcpServer,
restartAllClients,
pauseMcpServer,
resumeMcpServer,
} from "../mcp/actions";
import {
ListToolsResponse,
McpConfigData,
PresetServer,
ServerConfig,
ServerStatusResponse,
} from "../mcp/types";
import clsx from "clsx";
import PlayIcon from "../icons/play.svg";
import StopIcon from "../icons/pause.svg";
const presetServers = presetServersJson as PresetServer[];
@@ -47,13 +51,7 @@ export function McpMarketPage() {
const [isLoading, setIsLoading] = useState(false);
const [config, setConfig] = useState<McpConfigData>();
const [clientStatuses, setClientStatuses] = useState<
Record<
string,
{
status: "active" | "error" | "undefined";
errorMsg: string | null;
}
>
Record<string, ServerStatusResponse>
>({});
// 检查服务器是否已添加
@@ -253,18 +251,74 @@ export function McpMarketPage() {
};
// 移除服务器
const removeServer = async (id: string) => {
// 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);
// }
// };
// 暂停服务器
const pauseServer = async (id: string) => {
try {
setIsLoading(true);
const newConfig = await removeMcpServer(id);
showToast("Stopping server...");
const newConfig = await pauseMcpServer(id);
setConfig(newConfig);
// 移除状态
setClientStatuses((prev) => {
const newStatuses = { ...prev };
delete newStatuses[id];
return newStatuses;
});
// 更新状态为暂停
setClientStatuses((prev) => ({
...prev,
[id]: { status: "paused", errorMsg: null },
}));
showToast("Server stopped successfully");
} catch (error) {
showToast("Failed to stop server");
console.error(error);
} finally {
setIsLoading(false);
}
};
// 恢复服务器
const resumeServer = async (id: string) => {
try {
setIsLoading(true);
showToast("Starting server...");
// 尝试启动服务器
const success = await resumeMcpServer(id);
// 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态)
const status = await getClientStatus(id);
setClientStatuses((prev) => ({
...prev,
[id]: status,
}));
// 根据启动结果显示消息
if (success) {
showToast("Server started successfully");
} else {
throw new Error("Failed to start server");
}
} catch (error) {
showToast(
error instanceof Error
? error.message
: "Failed to start server, please check logs",
);
console.error(error);
} finally {
setIsLoading(false);
}
@@ -332,7 +386,12 @@ export function McpMarketPage() {
} else if (prop.type === "string") {
const currentValue = userConfig[key as keyof typeof userConfig] || "";
return (
<ListItem key={key} title={key} subTitle={prop.description}>
<ListItem
key={key}
title={key}
subTitle={prop.description}
vertical
>
<div className={styles["input-item"]}>
<input
type="text"
@@ -356,6 +415,29 @@ export function McpMarketPage() {
return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
};
// 修改状态显示逻辑
const getServerStatusDisplay = (clientId: string) => {
const status = checkServerStatus(clientId);
const statusMap = {
undefined: null, // 未配置/未找到不显示
paused: (
<span className={clsx(styles["server-status"], styles["stopped"])}>
Stopped
</span>
),
active: <span className={styles["server-status"]}>Running</span>,
error: (
<span className={clsx(styles["server-status"], styles["error"])}>
Error
<span className={styles["error-message"]}>: {status.errorMsg}</span>
</span>
),
};
return statusMap[status.status];
};
// 渲染服务器列表
const renderServerList = () => {
return presetServers
@@ -373,15 +455,18 @@ export function McpMarketPage() {
const bStatus = checkServerStatus(b.id).status;
// 定义状态优先级
const statusPriority = {
error: 0,
active: 1,
undefined: 2,
const statusPriority: Record<string, number> = {
error: 0, // 最高优先级
active: 1, // 运行中
paused: 2, // 已暂停
undefined: 3, // 未配置/未找到
};
// 首先按状态排序
if (aStatus !== bStatus) {
return statusPriority[aStatus] - statusPriority[bStatus];
return (
(statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3)
);
}
// 然后按名称排序
@@ -398,25 +483,7 @@ export function McpMarketPage() {
<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>
)}
{getServerStatusDisplay(server.id)}
{server.repo && (
<a
href={server.repo}
@@ -450,39 +517,52 @@ export function McpMarketPage() {
<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}
/>
{checkServerStatus(server.id).status === "paused" ? (
<>
<IconButton
icon={<PlayIcon />}
text="Start"
onClick={() => resumeServer(server.id)}
disabled={isLoading}
/>
{/* <IconButton
icon={<DeleteIcon />}
text="Remove"
onClick={() => removeServer(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={<StopIcon />}
text="Stop"
onClick={() => pauseServer(server.id)}
disabled={isLoading}
/>
</>
)}
</>
) : (
<IconButton
icon={<AddIcon />}
text="Add"
className={styles["action-primary"]}
onClick={() => addServer(server)}
disabled={isLoading}
/>