@@ -576,9 +562,14 @@ export function ImagePreviewer(props: {
key={i}
>
diff --git a/app/components/home.tsx b/app/components/home.tsx
index 5da490378..98f759a48 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -2,7 +2,7 @@
require("../polyfill");
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
import styles from "./home.module.scss";
import BotIcon from "../icons/bot.svg";
@@ -18,8 +18,8 @@ import { getISOLang, getLang } from "../locales";
import {
HashRouter as Router,
- Routes,
Route,
+ Routes,
useLocation,
} from "react-router-dom";
import { SideBar } from "./sidebar";
@@ -29,6 +29,7 @@ import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api";
import { useAccessStore } from "../store";
import clsx from "clsx";
+import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -74,6 +75,13 @@ const Sd = dynamic(async () => (await import("./sd")).Sd, {
loading: () =>
,
});
+const McpMarketPage = dynamic(
+ async () => (await import("./mcp-market")).McpMarketPage,
+ {
+ loading: () =>
,
+ },
+);
+
export function useSwitchTheme() {
const config = useAppConfig();
@@ -193,6 +201,7 @@ function Screen() {
} />
} />
} />
+
} />
>
@@ -233,6 +242,20 @@ export function Home() {
useEffect(() => {
console.log("[Config] got config from build time", getClientConfig());
useAccessStore.getState().fetch();
+
+ const initMcp = async () => {
+ try {
+ const enabled = await isMcpEnabled();
+ if (enabled) {
+ console.log("[MCP] initializing...");
+ await initializeMcpSystem();
+ console.log("[MCP] initialized");
+ }
+ } catch (err) {
+ console.error("[MCP] failed to initialize:", err);
+ }
+ };
+ initMcp();
}, []);
if (!useHasHydrated()) {
diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss
new file mode 100644
index 000000000..283436c7f
--- /dev/null
+++ b/app/components/mcp-market.module.scss
@@ -0,0 +1,657 @@
+@import "../styles/animation.scss";
+
+.mcp-market-page {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .loading-indicator {
+ font-size: 12px;
+ color: var(--primary);
+ margin-left: 8px;
+ font-weight: normal;
+ opacity: 0.8;
+ }
+
+ .mcp-market-page-body {
+ padding: 20px;
+ overflow-y: auto;
+
+ .loading-container,
+ .empty-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 200px;
+ width: 100%;
+ background-color: var(--white);
+ border: var(--border-in-light);
+ border-radius: 10px;
+ animation: slide-in ease 0.3s;
+ }
+
+ .loading-text,
+ .empty-text {
+ font-size: 14px;
+ color: var(--black);
+ opacity: 0.5;
+ text-align: center;
+ }
+
+ .mcp-market-filter {
+ width: 100%;
+ max-width: 100%;
+ margin-bottom: 20px;
+ animation: slide-in ease 0.3s;
+ height: 40px;
+ display: flex;
+
+ .search-bar {
+ flex-grow: 1;
+ max-width: 100%;
+ min-width: 0;
+ }
+ }
+
+ .server-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ }
+
+ .mcp-market-item {
+ padding: 20px;
+ border: var(--border-in-light);
+ animation: slide-in ease 0.3s;
+ background-color: var(--white);
+ transition: all 0.3s ease;
+
+ &.disabled {
+ opacity: 0.7;
+ pointer-events: none;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 0;
+ }
+
+ &:first-child {
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ }
+
+ &:last-child {
+ border-bottom-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+ }
+
+ &.loading {
+ position: relative;
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.2),
+ transparent
+ );
+ background-size: 200% 100%;
+ animation: loading-pulse 1.5s infinite;
+ }
+ }
+
+ .operation-status {
+ display: inline-flex;
+ align-items: center;
+ margin-left: 10px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ background-color: #16a34a;
+ color: #fff;
+ animation: pulse 1.5s infinite;
+
+ &[data-status="stopping"] {
+ background-color: #9ca3af;
+ }
+
+ &[data-status="starting"] {
+ background-color: #4ade80;
+ }
+
+ &[data-status="error"] {
+ background-color: #f87171;
+ }
+ }
+
+ .mcp-market-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+
+ .mcp-market-title {
+ 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;
+ margin-left: 10px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ background-color: #22c55e;
+ color: #fff;
+
+ &.error {
+ background-color: #ef4444;
+ }
+
+ &.stopped {
+ background-color: #6b7280;
+ }
+
+ &.initializing {
+ background-color: #f59e0b;
+ animation: pulse 1.5s infinite;
+ }
+
+ .error-message {
+ margin-left: 4px;
+ font-size: 12px;
+ }
+ }
+ }
+
+ .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 {
+ opacity: 1;
+ }
+
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+ }
+
+ .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: 12px;
+ align-items: flex-start;
+ flex-shrink: 0;
+ min-width: 180px;
+ justify-content: flex-end;
+ }
+ }
+ }
+ }
+
+ .array-input {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ padding: 16px;
+ border: 1px solid var(--gray-200);
+ border-radius: 10px;
+ background-color: var(--white);
+
+ .array-input-item {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ width: 100%;
+ padding: 0;
+
+ input {
+ width: 100%;
+ padding: 8px 12px;
+ background-color: var(--gray-50);
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ font-size: 13px;
+ border: 1px solid var(--gray-200);
+
+ &:hover {
+ background-color: var(--gray-100);
+ border-color: var(--gray-300);
+ }
+
+ &:focus {
+ background-color: var(--white);
+ border-color: var(--primary);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--primary-10);
+ }
+
+ &::placeholder {
+ color: var(--gray-300);
+ }
+ }
+ }
+
+ :global(.icon-button.add-path-button) {
+ width: 100%;
+ background-color: var(--primary);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ margin-top: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ height: 36px;
+
+ &:hover {
+ background-color: var(--primary-dark);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ filter: brightness(2);
+ }
+ }
+ }
+
+ .path-list {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .path-item {
+ display: flex;
+ gap: 10px;
+ width: 100%;
+
+ input {
+ flex: 1;
+ width: 100%;
+ max-width: 100%;
+ padding: 10px;
+ border: var(--border-in-light);
+ border-radius: 10px;
+ box-sizing: border-box;
+ font-size: 14px;
+ background-color: var(--white);
+ color: var(--black);
+
+ &:hover {
+ border-color: var(--gray-300);
+ }
+
+ &:focus {
+ border-color: var(--primary);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--primary-10);
+ }
+ }
+
+ .browse-button {
+ padding: 8px;
+ border: var(--border-in-light);
+ border-radius: 10px;
+ background-color: transparent;
+ color: var(--black-50);
+
+ &:hover {
+ border-color: var(--primary);
+ color: var(--primary);
+ background-color: transparent;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ .delete-button {
+ padding: 8px;
+ border: var(--border-in-light);
+ border-radius: 10px;
+ background-color: transparent;
+ color: var(--black-50);
+
+ &:hover {
+ border-color: var(--danger);
+ color: var(--danger);
+ background-color: transparent;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ .file-input {
+ display: none;
+ }
+ }
+
+ .add-button {
+ align-self: flex-start;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 8px 12px;
+ background-color: transparent;
+ border: var(--border-in-light);
+ border-radius: 10px;
+ color: var(--black);
+ font-size: 12px;
+ margin-top: 5px;
+
+ &:hover {
+ border-color: var(--primary);
+ color: var(--primary);
+ background-color: transparent;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+ }
+
+ .config-section {
+ width: 100%;
+
+ .config-header {
+ margin-bottom: 12px;
+
+ .config-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--black);
+ text-transform: capitalize;
+ }
+
+ .config-description {
+ font-size: 12px;
+ color: var(--gray-500);
+ margin-top: 4px;
+ }
+ }
+
+ .array-input {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ padding: 16px;
+ border: 1px solid var(--gray-200);
+ border-radius: 10px;
+ background-color: var(--white);
+
+ .array-input-item {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ width: 100%;
+ padding: 0;
+
+ input {
+ width: 100%;
+ padding: 8px 12px;
+ background-color: var(--gray-50);
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ font-size: 13px;
+ border: 1px solid var(--gray-200);
+
+ &:hover {
+ background-color: var(--gray-100);
+ border-color: var(--gray-300);
+ }
+
+ &:focus {
+ background-color: var(--white);
+ border-color: var(--primary);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--primary-10);
+ }
+
+ &::placeholder {
+ 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) {
+ width: 100%;
+ background-color: var(--primary);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ margin-top: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ height: 36px;
+
+ &:hover {
+ background-color: var(--primary-dark);
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ filter: brightness(2);
+ }
+ }
+ }
+ }
+
+ .input-item {
+ width: 100%;
+
+ input {
+ width: 100%;
+ padding: 10px;
+ border: var(--border-in-light);
+ border-radius: 10px;
+ box-sizing: border-box;
+ font-size: 14px;
+ background-color: var(--white);
+ color: var(--black);
+
+ &:hover {
+ border-color: var(--gray-300);
+ }
+
+ &:focus {
+ border-color: var(--primary);
+ outline: none;
+ box-shadow: 0 0 0 2px var(--primary-10);
+ }
+
+ &::placeholder {
+ color: var(--gray-300) !important;
+ opacity: 1;
+ }
+ }
+ }
+
+ .tools-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+ padding: 20px;
+ max-width: 100%;
+ overflow-x: hidden;
+ word-break: break-word;
+ box-sizing: border-box;
+
+ .tool-item {
+ width: 100%;
+ box-sizing: border-box;
+
+ .tool-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--black);
+ margin-bottom: 8px;
+ padding-left: 12px;
+ border-left: 3px solid var(--primary);
+ box-sizing: border-box;
+ width: 100%;
+ }
+
+ .tool-description {
+ font-size: 13px;
+ color: var(--gray-500);
+ line-height: 1.6;
+ padding-left: 15px;
+ box-sizing: border-box;
+ width: 100%;
+ }
+ }
+ }
+
+ :global {
+ .modal-content {
+ margin-top: 20px;
+ max-width: 100%;
+ overflow-x: hidden;
+ }
+
+ .list {
+ padding: 10px;
+ margin-bottom: 10px;
+ background-color: var(--white);
+ }
+
+ .list-item {
+ border: none;
+ background-color: transparent;
+ border-radius: 10px;
+ padding: 10px;
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .list-header {
+ margin-bottom: 0;
+
+ .list-title {
+ font-size: 14px;
+ font-weight: bold;
+ text-transform: capitalize;
+ color: var(--black);
+ }
+
+ .list-sub-title {
+ font-size: 12px;
+ color: var(--gray-500);
+ margin-top: 4px;
+ }
+ }
+ }
+ }
+}
+
+@keyframes loading-pulse {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 0.6;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.6;
+ }
+}
diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx
new file mode 100644
index 000000000..235f63b1c
--- /dev/null
+++ b/app/components/mcp-market.tsx
@@ -0,0 +1,755 @@
+import { IconButton } from "./button";
+import { ErrorBoundary } from "./error";
+import styles from "./mcp-market.module.scss";
+import EditIcon from "../icons/edit.svg";
+import AddIcon from "../icons/add.svg";
+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 { useEffect, useState } from "react";
+import {
+ addMcpServer,
+ getClientsStatus,
+ getClientTools,
+ getMcpConfigFromFile,
+ isMcpEnabled,
+ pauseMcpServer,
+ restartAllClients,
+ 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";
+import { Path } from "../constant";
+
+interface ConfigProperty {
+ type: string;
+ description?: string;
+ required?: boolean;
+ minItems?: number;
+}
+
+export function McpMarketPage() {
+ const navigate = useNavigate();
+ const [mcpEnabled, setMcpEnabled] = useState(false);
+ const [searchText, setSearchText] = useState("");
+ const [userConfig, setUserConfig] = useState
>({});
+ const [editingServerId, setEditingServerId] = useState();
+ const [tools, setTools] = useState(null);
+ const [viewingServerId, setViewingServerId] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+ const [config, setConfig] = useState();
+ const [clientStatuses, setClientStatuses] = useState<
+ Record
+ >({});
+ const [loadingPresets, setLoadingPresets] = useState(true);
+ const [presetServers, setPresetServers] = useState([]);
+ const [loadingStates, setLoadingStates] = useState>(
+ {},
+ );
+
+ // 检查 MCP 是否启用
+ useEffect(() => {
+ const checkMcpStatus = async () => {
+ const enabled = await isMcpEnabled();
+ setMcpEnabled(enabled);
+ if (!enabled) {
+ navigate(Path.Home);
+ }
+ };
+ checkMcpStatus();
+ }, [navigate]);
+
+ // 添加状态轮询
+ useEffect(() => {
+ if (!mcpEnabled || !config) return;
+
+ const updateStatuses = async () => {
+ const statuses = await getClientsStatus();
+ setClientStatuses(statuses);
+ };
+
+ // 立即执行一次
+ updateStatuses();
+ // 每 1000ms 轮询一次
+ const timer = setInterval(updateStatuses, 1000);
+
+ return () => clearInterval(timer);
+ }, [mcpEnabled, config]);
+
+ // 加载预设服务器
+ useEffect(() => {
+ const loadPresetServers = async () => {
+ if (!mcpEnabled) return;
+ try {
+ setLoadingPresets(true);
+ const response = await fetch("https://nextchat.club/mcp/list");
+ if (!response.ok) {
+ throw new Error("Failed to load preset servers");
+ }
+ const data = await response.json();
+ setPresetServers(data?.data ?? []);
+ } catch (error) {
+ console.error("Failed to load preset servers:", error);
+ showToast("Failed to load preset servers");
+ } finally {
+ setLoadingPresets(false);
+ }
+ };
+ loadPresetServers();
+ }, [mcpEnabled]);
+
+ // 加载初始状态
+ useEffect(() => {
+ const loadInitialState = async () => {
+ if (!mcpEnabled) return;
+ try {
+ setIsLoading(true);
+ const config = await getMcpConfigFromFile();
+ setConfig(config);
+
+ // 获取所有客户端的状态
+ const statuses = await getClientsStatus();
+ setClientStatuses(statuses);
+ } catch (error) {
+ console.error("Failed to load initial state:", error);
+ showToast("Failed to load initial state");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadInitialState();
+ }, [mcpEnabled]);
+
+ // 加载当前编辑服务器的配置
+ useEffect(() => {
+ if (!editingServerId || !config) return;
+ const currentConfig = config.mcpServers[editingServerId];
+ if (currentConfig) {
+ // 从当前配置中提取用户配置
+ const preset = presetServers.find((s) => s.id === editingServerId);
+ if (preset?.configSchema) {
+ const userConfig: Record = {};
+ Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
+ if (mapping.type === "spread") {
+ // For spread types, extract the array from args.
+ const startPos = mapping.position ?? 0;
+ userConfig[key] = currentConfig.args.slice(startPos);
+ } else if (mapping.type === "single") {
+ // For single types, get a single value
+ userConfig[key] = currentConfig.args[mapping.position ?? 0];
+ } else if (
+ mapping.type === "env" &&
+ mapping.key &&
+ currentConfig.env
+ ) {
+ // For env types, get values from environment variables
+ userConfig[key] = currentConfig.env[mapping.key];
+ }
+ });
+ setUserConfig(userConfig);
+ }
+ } else {
+ setUserConfig({});
+ }
+ }, [editingServerId, config, presetServers]);
+
+ if (!mcpEnabled) {
+ return null;
+ }
+
+ // 检查服务器是否已添加
+ const isServerAdded = (id: string) => {
+ return id in (config?.mcpServers ?? {});
+ };
+
+ // 保存服务器配置
+ const saveServerConfig = async () => {
+ const preset = presetServers.find((s) => s.id === editingServerId);
+ if (!preset || !preset.configSchema || !editingServerId) return;
+
+ const savingServerId = editingServerId;
+ setEditingServerId(undefined);
+
+ try {
+ updateLoadingState(savingServerId, "Updating configuration...");
+ // 构建服务器配置
+ const args = [...preset.baseArgs];
+ const env: Record = {};
+
+ Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
+ const value = userConfig[key];
+ if (mapping.type === "spread" && Array.isArray(value)) {
+ const pos = mapping.position ?? 0;
+ args.splice(pos, 0, ...value);
+ } else if (
+ mapping.type === "single" &&
+ mapping.position !== undefined
+ ) {
+ args[mapping.position] = value;
+ } else if (
+ mapping.type === "env" &&
+ mapping.key &&
+ typeof value === "string"
+ ) {
+ env[mapping.key] = value;
+ }
+ });
+
+ const serverConfig: ServerConfig = {
+ command: preset.command,
+ args,
+ ...(Object.keys(env).length > 0 ? { env } : {}),
+ };
+
+ const newConfig = await addMcpServer(savingServerId, serverConfig);
+ setConfig(newConfig);
+ showToast("Server configuration updated successfully");
+ } catch (error) {
+ showToast(
+ error instanceof Error ? error.message : "Failed to save configuration",
+ );
+ } finally {
+ updateLoadingState(savingServerId, null);
+ }
+ };
+
+ // 获取服务器支持的 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 updateLoadingState = (id: string, message: string | null) => {
+ setLoadingStates((prev) => {
+ if (message === null) {
+ const { [id]: _, ...rest } = prev;
+ return rest;
+ }
+ return { ...prev, [id]: message };
+ });
+ };
+
+ // 修改添加服务器函数
+ const addServer = async (preset: PresetServer) => {
+ if (!preset.configurable) {
+ try {
+ const serverId = preset.id;
+ updateLoadingState(serverId, "Creating MCP client...");
+
+ const serverConfig: ServerConfig = {
+ command: preset.command,
+ args: [...preset.baseArgs],
+ };
+ const newConfig = await addMcpServer(preset.id, serverConfig);
+ setConfig(newConfig);
+
+ // 更新状态
+ const statuses = await getClientsStatus();
+ setClientStatuses(statuses);
+ } finally {
+ updateLoadingState(preset.id, null);
+ }
+ } else {
+ // 如果需要配置,打开配置对话框
+ setEditingServerId(preset.id);
+ setUserConfig({});
+ }
+ };
+
+ // 修改暂停服务器函数
+ const pauseServer = async (id: string) => {
+ try {
+ updateLoadingState(id, "Stopping server...");
+ const newConfig = await pauseMcpServer(id);
+ setConfig(newConfig);
+ showToast("Server stopped successfully");
+ } catch (error) {
+ showToast("Failed to stop server");
+ console.error(error);
+ } finally {
+ updateLoadingState(id, null);
+ }
+ };
+
+ // Restart server
+ const restartServer = async (id: string) => {
+ try {
+ updateLoadingState(id, "Starting server...");
+ await resumeMcpServer(id);
+ } catch (error) {
+ showToast(
+ error instanceof Error
+ ? error.message
+ : "Failed to start server, please check logs",
+ );
+ console.error(error);
+ } finally {
+ updateLoadingState(id, null);
+ }
+ };
+
+ // Restart all clients
+ const handleRestartAll = async () => {
+ try {
+ updateLoadingState("all", "Restarting all servers...");
+ const newConfig = await restartAllClients();
+ setConfig(newConfig);
+ showToast("Restarting all clients");
+ } catch (error) {
+ showToast("Failed to restart clients");
+ console.error(error);
+ } finally {
+ updateLoadingState("all", null);
+ }
+ };
+
+ // Render configuration form
+ const renderConfigForm = () => {
+ const preset = presetServers.find((s) => s.id === editingServerId);
+ if (!preset?.configSchema) return null;
+
+ return Object.entries(preset.configSchema.properties).map(
+ ([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) => (
+
+ {
+ const newValue = [...currentValue] as string[];
+ newValue[index] = e.target.value;
+ setUserConfig({ ...userConfig, [key]: newValue });
+ }}
+ />
+ }
+ className={styles["delete-button"]}
+ onClick={() => {
+ const newValue = [...currentValue] as string[];
+ newValue.splice(index, 1);
+ setUserConfig({ ...userConfig, [key]: newValue });
+ }}
+ />
+
+ ),
+ )}
+
}
+ text={addButtonText}
+ className={styles["add-button"]}
+ bordered
+ onClick={() => {
+ const newValue = [...currentValue, ""] as string[];
+ setUserConfig({ ...userConfig, [key]: newValue });
+ }}
+ />
+
+
+ );
+ } else if (prop.type === "string") {
+ const currentValue = userConfig[key as keyof typeof userConfig] || "";
+ return (
+
+ {
+ setUserConfig({ ...userConfig, [key]: e.target.value });
+ }}
+ />
+
+ );
+ }
+ return null;
+ },
+ );
+ };
+
+ const checkServerStatus = (clientId: string) => {
+ return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
+ };
+
+ const getServerStatusDisplay = (clientId: string) => {
+ const status = checkServerStatus(clientId);
+
+ const statusMap = {
+ undefined: null, // 未配置/未找到不显示
+ // 添加初始化状态
+ initializing: (
+
+ Initializing
+
+ ),
+ paused: (
+
+ Stopped
+
+ ),
+ active: Running,
+ error: (
+
+ Error
+ : {status.errorMsg}
+
+ ),
+ };
+
+ return statusMap[status.status];
+ };
+
+ // Get the type of operation status
+ const getOperationStatusType = (message: string) => {
+ if (message.toLowerCase().includes("stopping")) return "stopping";
+ if (message.toLowerCase().includes("starting")) return "starting";
+ if (message.toLowerCase().includes("error")) return "error";
+ return "default";
+ };
+
+ // 渲染服务器列表
+ const renderServerList = () => {
+ if (loadingPresets) {
+ return (
+
+
+ Loading preset server list...
+
+
+ );
+ }
+
+ if (!Array.isArray(presetServers) || presetServers.length === 0) {
+ return (
+
+ );
+ }
+
+ 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 aLoading = loadingStates[a.id];
+ const bLoading = loadingStates[b.id];
+
+ // 定义状态优先级
+ const statusPriority: Record = {
+ error: 0, // Highest priority for error status
+ active: 1, // Second for active
+ initializing: 2, // Initializing
+ starting: 3, // Starting
+ stopping: 4, // Stopping
+ paused: 5, // Paused
+ undefined: 6, // Lowest priority for undefined
+ };
+
+ // Get actual status (including loading status)
+ const getEffectiveStatus = (status: string, loading?: string) => {
+ if (loading) {
+ const operationType = getOperationStatusType(loading);
+ return operationType === "default" ? status : operationType;
+ }
+
+ if (status === "initializing" && !loading) {
+ return "active";
+ }
+
+ return status;
+ };
+
+ const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
+ const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
+
+ // 首先按状态排序
+ if (aEffectiveStatus !== bEffectiveStatus) {
+ return (
+ (statusPriority[aEffectiveStatus] ?? 6) -
+ (statusPriority[bEffectiveStatus] ?? 6)
+ );
+ }
+
+ // Sort by name when statuses are the same
+ return a.name.localeCompare(b.name);
+ })
+ .map((server) => (
+
+
+
+
+ {server.name}
+ {loadingStates[server.id] && (
+
+ {loadingStates[server.id]}
+
+ )}
+ {!loadingStates[server.id] && getServerStatusDisplay(server.id)}
+ {server.repo && (
+
+
+
+ )}
+
+
+ {server.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ {server.description}
+
+
+
+ {isServerAdded(server.id) ? (
+ <>
+ {server.configurable && (
+ }
+ text="Configure"
+ onClick={() => setEditingServerId(server.id)}
+ disabled={isLoading}
+ />
+ )}
+ {checkServerStatus(server.id).status === "paused" ? (
+ <>
+ }
+ text="Start"
+ onClick={() => restartServer(server.id)}
+ disabled={isLoading}
+ />
+ {/* }
+ text="Remove"
+ onClick={() => removeServer(server.id)}
+ disabled={isLoading}
+ /> */}
+ >
+ ) : (
+ <>
+ }
+ text="Tools"
+ onClick={async () => {
+ setViewingServerId(server.id);
+ await loadTools(server.id);
+ }}
+ disabled={
+ isLoading ||
+ checkServerStatus(server.id).status === "error"
+ }
+ />
+ }
+ text="Stop"
+ onClick={() => pauseServer(server.id)}
+ disabled={isLoading}
+ />
+ >
+ )}
+ >
+ ) : (
+ }
+ text="Add"
+ onClick={() => addServer(server)}
+ disabled={isLoading}
+ />
+ )}
+
+
+
+ ));
+ };
+
+ return (
+
+
+
+
+
+ MCP Market
+ {loadingStates["all"] && (
+
+ {loadingStates["all"]}
+
+ )}
+
+
+ {Object.keys(config?.mcpServers ?? {}).length} servers configured
+
+
+
+
+
+ }
+ bordered
+ onClick={handleRestartAll}
+ text="Restart All"
+ disabled={isLoading}
+ />
+
+
+ }
+ bordered
+ onClick={() => navigate(-1)}
+ disabled={isLoading}
+ />
+
+
+
+
+
+
+ setSearchText(e.currentTarget.value)}
+ />
+
+
+
{renderServerList()}
+
+
+ {/*编辑服务器配置*/}
+ {editingServerId && (
+
+ !isLoading && setEditingServerId(undefined)}
+ actions={[
+ setEditingServerId(undefined)}
+ bordered
+ disabled={isLoading}
+ />,
+ ,
+ ]}
+ >
+ {renderConfigForm()}
+
+
+ )}
+
+ {viewingServerId && (
+
+
setViewingServerId(undefined)}
+ actions={[
+ setViewingServerId(undefined)}
+ bordered
+ />,
+ ]}
+ >
+
+ {isLoading ? (
+
Loading...
+ ) : tools?.tools ? (
+ tools.tools.map(
+ (tool: ListToolsResponse["tools"], index: number) => (
+
+
{tool.name}
+
+ {tool.description}
+
+
+ ),
+ )
+ ) : (
+
No tools available
+ )}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index ddbda1b73..68ebcf084 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -73,6 +73,8 @@ import {
Iflytek,
SAAS_CHAT_URL,
ChatGLM,
+ DeepSeek,
+ SiliconFlow,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
@@ -1197,6 +1199,47 @@ export function Settings() {
>
);
+ const deepseekConfigComponent = accessStore.provider ===
+ ServiceProvider.DeepSeek && (
+ <>
+
+
+ accessStore.update(
+ (access) => (access.deepseekUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) => (access.deepseekApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
+
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
<>
>
);
+ const siliconflowConfigComponent = accessStore.provider ===
+ ServiceProvider.SiliconFlow && (
+ <>
+
+
+ accessStore.update(
+ (access) => (access.siliconflowUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) => (access.siliconflowApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
const stabilityConfigComponent = accessStore.provider ===
ServiceProvider.Stability && (
@@ -1733,10 +1816,12 @@ export function Settings() {
{alibabaConfigComponent}
{tencentConfigComponent}
{moonshotConfigComponent}
+ {deepseekConfigComponent}
{stabilityConfigComponent}
{lflytekConfigComponent}
{XAIConfigComponent}
{chatglmConfigComponent}
+ {siliconflowConfigComponent}
>
)}
>
@@ -1771,9 +1856,11 @@ export function Settings() {
(await import("./chat-list")).ChatList, {
loading: () => null,
@@ -128,6 +135,7 @@ export function useDragSideBar() {
shouldNarrow,
};
}
+
export function SideBarContainer(props: {
children: React.ReactNode;
onDragStart: (e: MouseEvent) => void;
@@ -219,10 +227,21 @@ export function SideBarTail(props: {
export function SideBar(props: { className?: string }) {
useHotKey();
const { onDragStart, shouldNarrow } = useDragSideBar();
- const [showPluginSelector, setShowPluginSelector] = useState(false);
+ const [showDiscoverySelector, setshowDiscoverySelector] = useState(false);
const navigate = useNavigate();
const config = useAppConfig();
const chatStore = useChatStore();
+ const [mcpEnabled, setMcpEnabled] = useState(false);
+
+ useEffect(() => {
+ // 检查 MCP 是否启用
+ const checkMcpStatus = async () => {
+ const enabled = await isMcpEnabled();
+ setMcpEnabled(enabled);
+ console.log("[SideBar] MCP enabled:", enabled);
+ };
+ checkMcpStatus();
+ }, []);
return (
+ {mcpEnabled && (
+ }
+ text={shouldNarrow ? undefined : Locale.Mcp.Name}
+ className={styles["sidebar-bar-button"]}
+ onClick={() => {
+ navigate(Path.McpMarket, { state: { fromHome: true } });
+ }}
+ shadow
+ />
+ )}
}
text={shouldNarrow ? undefined : Locale.Discovery.Name}
className={styles["sidebar-bar-button"]}
- onClick={() => setShowPluginSelector(true)}
+ onClick={() => setshowDiscoverySelector(true)}
shadow
/>
- {showPluginSelector && (
+ {showDiscoverySelector && (