mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 23:20:28 +08:00
feat: Optimize MCP configuration logic
This commit is contained in:
@@ -46,6 +46,7 @@ import QualityIcon from "../icons/hd.svg";
|
||||
import StyleIcon from "../icons/palette.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||
import McpToolIcon from "../icons/tool.svg";
|
||||
import HeadphoneIcon from "../icons/headphone.svg";
|
||||
import {
|
||||
BOT_HELLO,
|
||||
@@ -121,6 +122,7 @@ import { isEmpty } from "lodash-es";
|
||||
import { getModelProvider } from "../utils/model";
|
||||
import { RealtimeChat } from "@/app/components/realtime-chat";
|
||||
import clsx from "clsx";
|
||||
import { getAvailableClientsCount } from "../mcp/actions";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
@@ -130,6 +132,27 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
|
||||
const MCPAction = () => {
|
||||
const navigate = useNavigate();
|
||||
const [count, setCount] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const loadCount = async () => {
|
||||
const count = await getAvailableClientsCount();
|
||||
setCount(count);
|
||||
};
|
||||
loadCount();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ChatAction
|
||||
onClick={() => navigate(Path.McpMarket)}
|
||||
text={`MCP${count ? ` (${count})` : ""}`}
|
||||
icon={<McpToolIcon />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function SessionConfigModel(props: { onClose: () => void }) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
@@ -799,6 +822,7 @@ export function ChatActions(props: {
|
||||
icon={<ShortcutkeyIcon />}
|
||||
/>
|
||||
)}
|
||||
{!isMobileScreen && <MCPAction />}
|
||||
</>
|
||||
<div className={styles["chat-input-actions-end"]}>
|
||||
{config.realtimeConfig.enable && (
|
||||
|
@@ -29,6 +29,8 @@ import { getClientConfig } from "../config/client";
|
||||
import { type ClientApi, getClientApi } from "../client/api";
|
||||
import { useAccessStore } from "../store";
|
||||
import clsx from "clsx";
|
||||
import { initializeMcpSystem } from "../mcp/actions";
|
||||
import { showToast } from "./ui-lib";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
@@ -243,6 +245,14 @@ export function Home() {
|
||||
useAccessStore.getState().fetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化 MCP 系统
|
||||
initializeMcpSystem().catch((error) => {
|
||||
console.error("Failed to initialize MCP system:", error);
|
||||
showToast("Failed to initialize MCP system");
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!useHasHydrated()) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
@@ -39,8 +39,6 @@
|
||||
}
|
||||
|
||||
.mcp-market-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border: var(--border-in-light);
|
||||
animation: slide-in ease 0.3s;
|
||||
@@ -68,118 +66,106 @@
|
||||
|
||||
.mcp-market-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.mcp-market-title {
|
||||
.mcp-market-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 20px;
|
||||
max-width: calc(100% - 300px);
|
||||
}
|
||||
|
||||
.mcp-market-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.server-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background-color: #22c55e;
|
||||
color: #fff;
|
||||
|
||||
.server-status {
|
||||
&.error {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
background-color: #10b981;
|
||||
color: white;
|
||||
|
||||
&.error {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
background-color: #f59e0b;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-market-info {
|
||||
font-size: 12px;
|
||||
color: var(--black-50);
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-market-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
:global(.icon-button) {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
.repo-link {
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.action-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
svg {
|
||||
filter: brightness(2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.action-warning {
|
||||
background-color: var(--warning);
|
||||
color: white;
|
||||
|
||||
svg {
|
||||
filter: brightness(2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--warning);
|
||||
border-color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.action-danger {
|
||||
background-color: transparent;
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--danger);
|
||||
color: white;
|
||||
|
||||
svg {
|
||||
filter: brightness(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.action-error {
|
||||
color: #ef4444 !important;
|
||||
border-color: #ef4444 !important;
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
.tags-container {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--gray);
|
||||
color: var(--black);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mcp-market-info {
|
||||
color: var(--black);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mcp-market-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,11 +298,6 @@
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--primary-10);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--gray-300) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.browse-button {
|
||||
@@ -534,7 +515,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.primitives-list {
|
||||
.tools-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@@ -545,11 +526,11 @@
|
||||
word-break: break-word;
|
||||
box-sizing: border-box;
|
||||
|
||||
.primitive-item {
|
||||
.tool-item {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.primitive-name {
|
||||
.tool-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--black);
|
||||
@@ -560,7 +541,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primitive-description {
|
||||
.tool-description {
|
||||
font-size: 13px;
|
||||
color: var(--gray-500);
|
||||
line-height: 1.6;
|
||||
@@ -590,9 +571,12 @@
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.list-header {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.list-title {
|
||||
font-size: 14px;
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user