{config.realtimeConfig.enable && (
diff --git a/app/components/home.tsx b/app/components/home.tsx
index 32c5b4ac6..8a03c50b6 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -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
;
}
diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss
index 5e4b6e9b0..93c6b67de 100644
--- a/app/components/mcp-market.module.scss
+++ b/app/components/mcp-market.module.scss
@@ -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;
diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx
index 926e64b29..d93754549 100644
--- a/app/components/mcp-market.tsx
+++ b/app/components/mcp-market.tsx
@@ -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
({ mcpServers: {} });
- const [editingServerId, setEditingServerId] = useState();
- const [viewingServerId, setViewingServerId] = useState();
- const [primitives, setPrimitives] = useState([]);
const [userConfig, setUserConfig] = useState>({});
+ const [editingServerId, setEditingServerId] = useState();
+ const [tools, setTools] = useState(null);
+ const [viewingServerId, setViewingServerId] = useState();
const [isLoading, setIsLoading] = useState(false);
- const [clientErrors, setClientErrors] = useState<
- Record
+ const [config, setConfig] = useState();
+ 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 = {};
+ 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 = {};
@@ -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 = {};
+ 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 (
-
+
{(currentValue as string[]).map(
(value: string, index: number) => (
@@ -197,7 +309,7 @@ export function McpMarketPage() {
{
const newValue = [...currentValue] as string[];
newValue[index] = e.target.value;
@@ -218,7 +330,7 @@ export function McpMarketPage() {
)}
}
- 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) => (
+
+
+
+
+ {server.name}
+ {checkServerStatus(server.id).status !== "undefined" && (
+
+ {checkServerStatus(server.id).status === "error" ? (
+ <>
+ Error
+
+ : {checkServerStatus(server.id).errorMsg}
+
+ >
+ ) : (
+ "Active"
+ )}
+
+ )}
+ {server.repo && (
+
+
+
+ )}
+
+
+ {server.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ {server.description}
+
+
+
+ {isServerAdded(server.id) ? (
+ <>
+ {server.configurable && (
+ }
+ text="Configure"
+ className={clsx({
+ [styles["action-error"]]:
+ checkServerStatus(server.id).status === "error",
+ })}
+ onClick={() => setEditingServerId(server.id)}
+ disabled={isLoading}
+ />
+ )}
+ }
+ text="Tools"
+ onClick={async () => {
+ setViewingServerId(server.id);
+ await loadTools(server.id);
+ }}
+ disabled={
+ isLoading ||
+ checkServerStatus(server.id).status === "error"
+ }
+ />
+ }
+ text="Remove"
+ className={styles["action-danger"]}
+ onClick={() => removeServer(server.id)}
+ disabled={isLoading}
+ />
+ >
+ ) : (
+ }
+ text="Add"
+ className={styles["action-primary"]}
+ onClick={() => addServer(server)}
+ disabled={isLoading}
+ />
+ )}
+
+
+
+ ));
};
return (
@@ -342,7 +517,7 @@ export function McpMarketPage() {
)}
- {Object.keys(config.mcpServers).length} servers configured
+ {Object.keys(config?.mcpServers ?? {}).length} servers configured
@@ -351,7 +526,7 @@ export function McpMarketPage() {
- {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) => (
-
-
-
-
- {server.name}
- {isServerAdded(server.id) && (
-
- {clientErrors[server.id] === null
- ? "Active"
- : "Error"}
- {clientErrors[server.id] && (
-
- : {clientErrors[server.id]}
-
- )}
-
- )}
-
-
- {server.description}
-
-
-
-
- {isServerAdded(server.id) ? (
- <>
- {server.configurable && (
- }
- text="Configure"
- className={clsx({
- [styles["action-error"]]:
- clientErrors[server.id] !== null,
- })}
- onClick={() => setEditingServerId(server.id)}
- disabled={isLoading}
- />
- )}
- {isServerAdded(server.id) && (
- }
- text="Tools"
- onClick={async () => {
- if (clientErrors[server.id] !== null) {
- showToast("Server is not running");
- return;
- }
- setViewingServerId(server.id);
- await loadPrimitives(server.id);
- }}
- disabled={isLoading}
- />
- )}
- }
- text="Remove"
- className={styles["action-danger"]}
- onClick={() => removeServer(server.id)}
- disabled={isLoading}
- />
- >
- ) : (
- }
- text="Add"
- className={styles["action-primary"]}
- onClick={() => addServer(server)}
- disabled={isLoading}
- />
- )}
-
-
- ))}
-
+