mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-19 20:20:16 +09:00
feat: Optimize MCP configuration logic
This commit is contained in:
parent
ce13cf61a7
commit
8aa9a500fd
@ -46,6 +46,7 @@ import QualityIcon from "../icons/hd.svg";
|
|||||||
import StyleIcon from "../icons/palette.svg";
|
import StyleIcon from "../icons/palette.svg";
|
||||||
import PluginIcon from "../icons/plugin.svg";
|
import PluginIcon from "../icons/plugin.svg";
|
||||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||||
|
import McpToolIcon from "../icons/tool.svg";
|
||||||
import HeadphoneIcon from "../icons/headphone.svg";
|
import HeadphoneIcon from "../icons/headphone.svg";
|
||||||
import {
|
import {
|
||||||
BOT_HELLO,
|
BOT_HELLO,
|
||||||
@ -121,6 +122,7 @@ import { isEmpty } from "lodash-es";
|
|||||||
import { getModelProvider } from "../utils/model";
|
import { getModelProvider } from "../utils/model";
|
||||||
import { RealtimeChat } from "@/app/components/realtime-chat";
|
import { RealtimeChat } from "@/app/components/realtime-chat";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { getAvailableClientsCount } from "../mcp/actions";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
@ -130,6 +132,27 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|||||||
loading: () => <LoadingIcon />,
|
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 }) {
|
export function SessionConfigModel(props: { onClose: () => void }) {
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
@ -799,6 +822,7 @@ export function ChatActions(props: {
|
|||||||
icon={<ShortcutkeyIcon />}
|
icon={<ShortcutkeyIcon />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!isMobileScreen && <MCPAction />}
|
||||||
</>
|
</>
|
||||||
<div className={styles["chat-input-actions-end"]}>
|
<div className={styles["chat-input-actions-end"]}>
|
||||||
{config.realtimeConfig.enable && (
|
{config.realtimeConfig.enable && (
|
||||||
|
@ -29,6 +29,8 @@ import { getClientConfig } from "../config/client";
|
|||||||
import { type ClientApi, getClientApi } from "../client/api";
|
import { type ClientApi, getClientApi } from "../client/api";
|
||||||
import { useAccessStore } from "../store";
|
import { useAccessStore } from "../store";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { initializeMcpSystem } from "../mcp/actions";
|
||||||
|
import { showToast } from "./ui-lib";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@ -243,6 +245,14 @@ export function Home() {
|
|||||||
useAccessStore.getState().fetch();
|
useAccessStore.getState().fetch();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 初始化 MCP 系统
|
||||||
|
initializeMcpSystem().catch((error) => {
|
||||||
|
console.error("Failed to initialize MCP system:", error);
|
||||||
|
showToast("Failed to initialize MCP system");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!useHasHydrated()) {
|
if (!useHasHydrated()) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mcp-market-item {
|
.mcp-market-item {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
animation: slide-in ease 0.3s;
|
animation: slide-in ease 0.3s;
|
||||||
@ -68,52 +66,96 @@
|
|||||||
|
|
||||||
.mcp-market-header {
|
.mcp-market-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.mcp-market-title {
|
.mcp-market-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 20px;
|
||||||
|
max-width: calc(100% - 300px);
|
||||||
|
}
|
||||||
|
|
||||||
.mcp-market-name {
|
.mcp-market-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.server-status {
|
.server-status {
|
||||||
font-size: 12px;
|
display: inline-flex;
|
||||||
padding: 2px 6px;
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-left: 8px;
|
font-size: 12px;
|
||||||
background-color: #10b981;
|
background-color: #22c55e;
|
||||||
color: white;
|
color: #fff;
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background-color: #ef4444;
|
background-color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.waiting {
|
.error-message {
|
||||||
background-color: #f59e0b;
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.repo-link {
|
||||||
font-size: 11px;
|
color: var(--primary);
|
||||||
opacity: 0.9;
|
font-size: 12px;
|
||||||
margin-left: 4px;
|
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 {
|
.mcp-market-info {
|
||||||
|
color: var(--black);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--black-50);
|
overflow: hidden;
|
||||||
margin-top: 4px;
|
text-overflow: ellipsis;
|
||||||
}
|
white-space: nowrap;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mcp-market-actions {
|
.mcp-market-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 180px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
:global(.icon-button) {
|
:global(.icon-button) {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@ -123,63 +165,7 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.mcp-market-actions {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -312,11 +298,6 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2px var(--primary-10);
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--gray-300) !important;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.browse-button {
|
.browse-button {
|
||||||
@ -534,7 +515,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.primitives-list {
|
.tools-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@ -545,11 +526,11 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.primitive-item {
|
.tool-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.primitive-name {
|
.tool-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
@ -560,7 +541,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primitive-description {
|
.tool-description {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--gray-500);
|
color: var(--gray-500);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@ -590,9 +571,12 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
.list-header {
|
.list-header {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 0;
|
||||||
|
|
||||||
.list-title {
|
.list-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -7,22 +7,29 @@ import CloseIcon from "../icons/close.svg";
|
|||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import RestartIcon from "../icons/reload.svg";
|
import RestartIcon from "../icons/reload.svg";
|
||||||
import EyeIcon from "../icons/eye.svg";
|
import EyeIcon from "../icons/eye.svg";
|
||||||
|
import GithubIcon from "../icons/github.svg";
|
||||||
import { List, ListItem, Modal, showToast } from "./ui-lib";
|
import { List, ListItem, Modal, showToast } from "./ui-lib";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import presetServersJson from "../mcp/preset-server.json";
|
import presetServersJson from "../mcp/preset-server.json";
|
||||||
const presetServers = presetServersJson as PresetServer[];
|
|
||||||
import {
|
import {
|
||||||
getMcpConfig,
|
addMcpServer,
|
||||||
updateMcpConfig,
|
getClientStatus,
|
||||||
getClientPrimitives,
|
getClientTools,
|
||||||
|
getMcpConfigFromFile,
|
||||||
|
removeMcpServer,
|
||||||
restartAllClients,
|
restartAllClients,
|
||||||
getClientErrors,
|
|
||||||
refreshClientStatus,
|
|
||||||
} from "../mcp/actions";
|
} from "../mcp/actions";
|
||||||
import { McpConfig, PresetServer, ServerConfig } from "../mcp/types";
|
import {
|
||||||
|
ListToolsResponse,
|
||||||
|
McpConfigData,
|
||||||
|
PresetServer,
|
||||||
|
ServerConfig,
|
||||||
|
} from "../mcp/types";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const presetServers = presetServersJson as PresetServer[];
|
||||||
|
|
||||||
interface ConfigProperty {
|
interface ConfigProperty {
|
||||||
type: string;
|
type: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -33,67 +40,71 @@ interface ConfigProperty {
|
|||||||
export function McpMarketPage() {
|
export function McpMarketPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchText, setSearchText] = useState("");
|
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 [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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [clientErrors, setClientErrors] = useState<
|
const [config, setConfig] = useState<McpConfigData>();
|
||||||
Record<string, string | null>
|
const [clientStatuses, setClientStatuses] = useState<
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
status: "active" | "error" | "undefined";
|
||||||
|
errorMsg: string | null;
|
||||||
|
}
|
||||||
|
>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
// 更新服务器状态
|
|
||||||
const updateServerStatus = async () => {
|
|
||||||
await refreshClientStatus();
|
|
||||||
const errors = await getClientErrors();
|
|
||||||
setClientErrors(errors);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始加载配置
|
|
||||||
useEffect(() => {
|
|
||||||
const init = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const data = await getMcpConfig();
|
|
||||||
setConfig(data);
|
|
||||||
await updateServerStatus();
|
|
||||||
} catch (error) {
|
|
||||||
showToast("Failed to load configuration");
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
init().then();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
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) => {
|
const isServerAdded = (id: string) => {
|
||||||
return id in config.mcpServers;
|
return id in (config?.mcpServers ?? {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取客户端状态
|
||||||
|
const updateClientStatus = async (clientId: string) => {
|
||||||
|
const status = await getClientStatus(clientId);
|
||||||
|
setClientStatuses((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[clientId]: status,
|
||||||
|
}));
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从服务器获取初始状态
|
||||||
|
useEffect(() => {
|
||||||
|
const loadInitialState = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
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) {
|
||||||
|
console.error("Failed to load initial state:", error);
|
||||||
|
showToast("Failed to load initial state");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadInitialState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Debug: 监控状态变化
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("MCP Market - Current config:", config);
|
||||||
|
console.log("MCP Market - Current clientStatuses:", clientStatuses);
|
||||||
|
}, [config, clientStatuses]);
|
||||||
|
|
||||||
// 加载当前编辑服务器的配置
|
// 加载当前编辑服务器的配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingServerId) {
|
if (editingServerId && config) {
|
||||||
const currentConfig = config.mcpServers[editingServerId];
|
const currentConfig = config.mcpServers[editingServerId];
|
||||||
if (currentConfig) {
|
if (currentConfig) {
|
||||||
// 从当前配置中提取用户配置
|
// 从当前配置中提取用户配置
|
||||||
@ -123,7 +134,7 @@ export function McpMarketPage() {
|
|||||||
setUserConfig({});
|
setUserConfig({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [editingServerId, config.mcpServers]);
|
}, [editingServerId, config]);
|
||||||
|
|
||||||
// 保存服务器配置
|
// 保存服务器配置
|
||||||
const saveServerConfig = async () => {
|
const saveServerConfig = async () => {
|
||||||
@ -131,6 +142,7 @@ export function McpMarketPage() {
|
|||||||
if (!preset || !preset.configSchema || !editingServerId) return;
|
if (!preset || !preset.configSchema || !editingServerId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
// 构建服务器配置
|
// 构建服务器配置
|
||||||
const args = [...preset.baseArgs];
|
const args = [...preset.baseArgs];
|
||||||
const env: Record<string, string> = {};
|
const env: Record<string, string> = {};
|
||||||
@ -160,22 +172,113 @@ export function McpMarketPage() {
|
|||||||
...(Object.keys(env).length > 0 ? { env } : {}),
|
...(Object.keys(env).length > 0 ? { env } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新配置
|
// 更新配置并初始化新服务器
|
||||||
const newConfig = {
|
const newConfig = await addMcpServer(editingServerId, serverConfig);
|
||||||
...config,
|
setConfig(newConfig);
|
||||||
mcpServers: {
|
|
||||||
...config.mcpServers,
|
// 更新状态
|
||||||
[editingServerId]: serverConfig,
|
const status = await getClientStatus(editingServerId);
|
||||||
},
|
setClientStatuses((prev) => ({
|
||||||
};
|
...prev,
|
||||||
|
[editingServerId]: status,
|
||||||
|
}));
|
||||||
|
|
||||||
await saveConfig(newConfig);
|
|
||||||
setEditingServerId(undefined);
|
setEditingServerId(undefined);
|
||||||
showToast("Server configuration saved successfully");
|
showToast("Server configuration saved successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(
|
showToast(
|
||||||
error instanceof Error ? error.message : "Failed to save configuration",
|
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]) => {
|
([key, prop]: [string, ConfigProperty]) => {
|
||||||
if (prop.type === "array") {
|
if (prop.type === "array") {
|
||||||
const currentValue = userConfig[key as keyof typeof userConfig] || [];
|
const currentValue = userConfig[key as keyof typeof userConfig] || [];
|
||||||
|
const itemLabel = (prop as any).itemLabel || key;
|
||||||
|
const addButtonText =
|
||||||
|
(prop as any).addButtonText || `Add ${itemLabel}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem key={key} title={key} subTitle={prop.description}>
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
title={key}
|
||||||
|
subTitle={prop.description}
|
||||||
|
vertical
|
||||||
|
>
|
||||||
<div className={styles["path-list"]}>
|
<div className={styles["path-list"]}>
|
||||||
{(currentValue as string[]).map(
|
{(currentValue as string[]).map(
|
||||||
(value: string, index: number) => (
|
(value: string, index: number) => (
|
||||||
@ -197,7 +309,7 @@ export function McpMarketPage() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={`Path ${index + 1}`}
|
placeholder={`${itemLabel} ${index + 1}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = [...currentValue] as string[];
|
const newValue = [...currentValue] as string[];
|
||||||
newValue[index] = e.target.value;
|
newValue[index] = e.target.value;
|
||||||
@ -218,7 +330,7 @@ export function McpMarketPage() {
|
|||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
text="Add Path"
|
text={addButtonText}
|
||||||
className={styles["add-button"]}
|
className={styles["add-button"]}
|
||||||
bordered
|
bordered
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -251,83 +363,146 @@ export function McpMarketPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取服务器的 Primitives
|
// 检查服务器状态
|
||||||
const loadPrimitives = async (id: string) => {
|
const checkServerStatus = (clientId: string) => {
|
||||||
try {
|
return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
|
||||||
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 handleRestart = async () => {
|
const renderServerList = () => {
|
||||||
try {
|
return presetServers
|
||||||
setIsLoading(true);
|
.filter((server) => {
|
||||||
await restartAllClients();
|
if (searchText.length === 0) return true;
|
||||||
await updateServerStatus();
|
const searchLower = searchText.toLowerCase();
|
||||||
showToast("All clients restarted successfully");
|
return (
|
||||||
} catch (error) {
|
server.name.toLowerCase().includes(searchLower) ||
|
||||||
showToast("Failed to restart clients");
|
server.description.toLowerCase().includes(searchLower) ||
|
||||||
console.error(error);
|
server.tags.some((tag) => tag.toLowerCase().includes(searchLower))
|
||||||
} finally {
|
);
|
||||||
setIsLoading(false);
|
})
|
||||||
}
|
.sort((a, b) => {
|
||||||
|
const aStatus = checkServerStatus(a.id).status;
|
||||||
|
const bStatus = checkServerStatus(b.id).status;
|
||||||
|
|
||||||
|
// 定义状态优先级
|
||||||
|
const statusPriority = {
|
||||||
|
error: 0,
|
||||||
|
active: 1,
|
||||||
|
undefined: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加服务器
|
// 首先按状态排序
|
||||||
const addServer = async (preset: PresetServer) => {
|
if (aStatus !== bStatus) {
|
||||||
if (!preset.configurable) {
|
return statusPriority[aStatus] - statusPriority[bStatus];
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
showToast("Creating MCP client...");
|
|
||||||
// 如果服务器不需要配置,直接添加
|
|
||||||
const serverConfig: ServerConfig = {
|
|
||||||
command: preset.command,
|
|
||||||
args: [...preset.baseArgs],
|
|
||||||
};
|
|
||||||
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) => {
|
return a.name.localeCompare(b.name);
|
||||||
try {
|
})
|
||||||
setIsLoading(true);
|
.map((server) => (
|
||||||
const { [id]: _, ...rest } = config.mcpServers;
|
<div
|
||||||
const newConfig = {
|
className={clsx(styles["mcp-market-item"], {
|
||||||
...config,
|
[styles["disabled"]]: isLoading,
|
||||||
mcpServers: rest,
|
})}
|
||||||
};
|
key={server.id}
|
||||||
await saveConfig(newConfig);
|
>
|
||||||
} finally {
|
<div className={styles["mcp-market-header"]}>
|
||||||
setIsLoading(false);
|
<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 (
|
return (
|
||||||
@ -342,7 +517,7 @@ export function McpMarketPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="window-header-sub-title">
|
<div className="window-header-sub-title">
|
||||||
{Object.keys(config.mcpServers).length} servers configured
|
{Object.keys(config?.mcpServers ?? {}).length} servers configured
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -351,7 +526,7 @@ export function McpMarketPage() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<RestartIcon />}
|
icon={<RestartIcon />}
|
||||||
bordered
|
bordered
|
||||||
onClick={handleRestart}
|
onClick={handleRestartAll}
|
||||||
text="Restart All"
|
text="Restart All"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
@ -378,121 +553,10 @@ export function McpMarketPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles["server-list"]}>
|
<div className={styles["server-list"]}>{renderServerList()}</div>
|
||||||
{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>
|
</div>
|
||||||
|
|
||||||
|
{/*编辑服务器配置*/}
|
||||||
{editingServerId && (
|
{editingServerId && (
|
||||||
<div className="modal-mask">
|
<div className="modal-mask">
|
||||||
<Modal
|
<Modal
|
||||||
@ -521,6 +585,7 @@ export function McpMarketPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/*支持的Tools*/}
|
||||||
{viewingServerId && (
|
{viewingServerId && (
|
||||||
<div className="modal-mask">
|
<div className="modal-mask">
|
||||||
<Modal
|
<Modal
|
||||||
@ -535,24 +600,20 @@ export function McpMarketPage() {
|
|||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className={styles["primitives-list"]}>
|
<div className={styles["tools-list"]}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : primitives.filter((p) => p.type === "tool").length > 0 ? (
|
) : tools?.tools ? (
|
||||||
primitives
|
tools.tools.map(
|
||||||
.filter((p) => p.type === "tool")
|
(tool: ListToolsResponse["tools"], index: number) => (
|
||||||
.map((primitive, index) => (
|
<div key={index} className={styles["tool-item"]}>
|
||||||
<div key={index} className={styles["primitive-item"]}>
|
<div className={styles["tool-name"]}>{tool.name}</div>
|
||||||
<div className={styles["primitive-name"]}>
|
<div className={styles["tool-description"]}>
|
||||||
{primitive.value.name}
|
{tool.description}
|
||||||
</div>
|
</div>
|
||||||
{primitive.value.description && (
|
|
||||||
<div className={styles["primitive-description"]}>
|
|
||||||
{primitive.value.description}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
),
|
||||||
</div>
|
)
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<div>No tools available</div>
|
<div>No tools available</div>
|
||||||
)}
|
)}
|
||||||
|
@ -88,6 +88,7 @@ export enum StoreKey {
|
|||||||
Update = "chat-update",
|
Update = "chat-update",
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
SdList = "sd-list",
|
SdList = "sd-list",
|
||||||
|
Mcp = "mcp-store",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||||
@ -254,18 +255,18 @@ Latex inline: \\(x^2\\)
|
|||||||
Latex block: $$e=mc^2$$
|
Latex block: $$e=mc^2$$
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MCP_PRIMITIVES_TEMPLATE = `
|
export const MCP_TOOLS_TEMPLATE = `
|
||||||
[clientId]
|
[clientId]
|
||||||
{{ clientId }}
|
{{ clientId }}
|
||||||
[primitives]
|
[tools]
|
||||||
{{ primitives }}
|
{{ tools }}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MCP_SYSTEM_TEMPLATE = `
|
export const MCP_SYSTEM_TEMPLATE = `
|
||||||
You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
|
You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
|
||||||
|
|
||||||
1. TOOLS AVAILABLE:
|
1. AVAILABLE TOOLS:
|
||||||
{{ MCP_PRIMITIVES }}
|
{{ MCP_TOOLS }}
|
||||||
|
|
||||||
2. WHEN TO USE TOOLS:
|
2. WHEN TO USE TOOLS:
|
||||||
- ALWAYS USE TOOLS when they can help answer user questions
|
- ALWAYS USE TOOLS when they can help answer user questions
|
||||||
|
5
app/icons/tool.svg
Normal file
5
app/icons/tool.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16" height="16">
|
||||||
|
<path d="M106.88 974.72a106.282667 106.282667 0 0 1-75.605333-31.317333c-20.181333-20.181333-31.317333-47.061333-31.317334-75.605334s11.136-55.424 31.317334-75.605333l413.098666-413.098667a10.965333 10.965333 0 0 1 0.896-1.024l57.173334-57.130666c6.016-6.016 18.517333-18.517333-26.922667-64.042667a214.442667 214.442667 0 0 0-160.469333-62.378667l-1.706667 0.042667a42.453333 42.453333 0 0 1-34.304-17.109333 42.410667 42.410667 0 0 1-8.064-31.658667 42.282667 42.282667 0 0 1 16.682667-28.074667 330.666667 330.666667 0 0 1 203.349333-69.333333 332.8 332.8 0 0 1 225.322667 87.765333l0.896 0.938667c10.026667 8.277333 32.426667 30.677333 97.621333 96.085333a64.426667 64.426667 0 0 1 14.336 68.736 42.666667 42.666667 0 0 0 69.973333 46.08 42.88 42.88 0 0 1 60.629334-0.128l45.226666 45.397334a64 64 0 0 1-0.085333 90.709333l-151.253333 151.253333a61.696 61.696 0 0 1-45.141334 18.858667c-17.28 0-33.450667-6.741333-45.610666-18.944l-45.141334-45.141333a42.666667 42.666667 0 0 1-0.170666-60.672 42.496 42.496 0 0 0 12.501333-30.250667 42.453333 42.453333 0 0 0-12.544-30.293333 42.538667 42.538667 0 0 0-30.293333-12.544 42.410667 42.410667 0 0 0-30.293334 12.544l-59.52 59.52-0.725333 0.853333-414.293333 414.293333a106.154667 106.154667 0 0 1-75.562667 31.274667zM61.44 822.357333a63.914667 63.914667 0 0 0-18.816 45.44c0 17.152 6.698667 33.322667 18.816 45.44s28.288 18.816 45.44 18.816 33.322667-6.698667 45.44-18.816l398.976-398.976-90.88-90.88-398.976 398.976z m731.733333-217.344a21.248 21.248 0 0 0 30.208 0.128l151.466667-151.466666a21.333333 21.333333 0 0 0 0.170667-30.208l-45.44-45.568-181.674667 181.589333-0.810667 0.725333 46.08 44.8z m-211.797333-120.96l45.44-45.44a84.906667 84.906667 0 0 1 60.458667-25.002666c22.869333 0 44.373333 8.874667 60.501333 25.045333 10.837333 10.837333 18.389333 24.064 22.229333 38.528l76.544-76.544a84.053333 84.053333 0 0 1-37.546666-21.290667 84.992 84.992 0 0 1-19.498667-93.184 21.546667 21.546667 0 0 0-4.906667-22.869333c-74.410667-74.624-91.136-90.538667-94.848-93.824a17.024 17.024 0 0 1-2.218666-1.792 290.389333 290.389333 0 0 0-196.48-76.544 288.554667 288.554667 0 0 0-177.578667 60.629333l11.477333-0.085333c67.328 0 133.205333 27.392 180.778667 75.136 18.688 18.730667 75.562667 75.690667 26.88 124.373333l-42.069333 42.069334 90.837333 90.794666z"
|
||||||
|
fill="#000000"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
@ -1,236 +1,217 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
executeRequest,
|
executeRequest,
|
||||||
listPrimitives,
|
listTools,
|
||||||
Primitive,
|
removeClient,
|
||||||
} from "./client";
|
} from "./client";
|
||||||
import { MCPClientLogger } from "./logger";
|
import { MCPClientLogger } from "./logger";
|
||||||
import { McpRequestMessage, McpConfig, ServerConfig } from "./types";
|
import {
|
||||||
|
DEFAULT_MCP_CONFIG,
|
||||||
|
McpClientData,
|
||||||
|
McpConfigData,
|
||||||
|
McpRequestMessage,
|
||||||
|
ServerConfig,
|
||||||
|
} from "./types";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const logger = new MCPClientLogger("MCP Actions");
|
const logger = new MCPClientLogger("MCP Actions");
|
||||||
|
|
||||||
// Use Map to store all clients
|
|
||||||
const clientsMap = new Map<
|
|
||||||
string,
|
|
||||||
{ client: Client | null; primitives: Primitive[]; errorMsg: string | null }
|
|
||||||
>();
|
|
||||||
|
|
||||||
// Whether initialized
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
// Store failed clients
|
|
||||||
let errorClients: string[] = [];
|
|
||||||
|
|
||||||
const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
|
const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
|
||||||
|
|
||||||
// 获取 MCP 配置
|
const clientsMap = new Map<string, McpClientData>();
|
||||||
export async function getMcpConfig(): Promise<McpConfig> {
|
|
||||||
try {
|
// 获取客户端状态
|
||||||
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
|
export async function getClientStatus(clientId: string) {
|
||||||
return JSON.parse(configStr);
|
const status = clientsMap.get(clientId);
|
||||||
} catch (error) {
|
if (!status) return { status: "undefined" as const, errorMsg: null };
|
||||||
console.error("Failed to read MCP config:", error);
|
|
||||||
return { mcpServers: {} };
|
return {
|
||||||
}
|
status: status.errorMsg ? ("error" as const) : ("active" as const),
|
||||||
|
errorMsg: status.errorMsg,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 MCP 配置
|
// 获取客户端工具
|
||||||
export async function updateMcpConfig(config: McpConfig): Promise<void> {
|
export async function getClientTools(clientId: string) {
|
||||||
try {
|
return clientsMap.get(clientId)?.tools ?? null;
|
||||||
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to write MCP config:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新初始化所有客户端
|
// 获取可用客户端数量
|
||||||
export async function reinitializeMcpClients() {
|
export async function getAvailableClientsCount() {
|
||||||
logger.info("Reinitializing MCP clients...");
|
let count = 0;
|
||||||
// 遍历所有客户端,关闭
|
clientsMap.forEach((map) => {
|
||||||
try {
|
if (!map.errorMsg) {
|
||||||
for (const [clientId, clientData] of clientsMap.entries()) {
|
count += map?.tools?.tools?.length ?? 0;
|
||||||
clientData.client?.close();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
logger.error(`Failed to close clients: ${error}`);
|
return count;
|
||||||
}
|
|
||||||
// 清空状态
|
|
||||||
clientsMap.clear();
|
|
||||||
errorClients = [];
|
|
||||||
initialized = false;
|
|
||||||
// 重新初始化
|
|
||||||
return initializeMcpClients();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize all configured clients
|
// 获取所有客户端工具
|
||||||
export async function initializeMcpClients() {
|
export async function getAllTools() {
|
||||||
// If already initialized, return
|
const result = [];
|
||||||
if (initialized) {
|
for (const [clientId, status] of clientsMap.entries()) {
|
||||||
return { errorClients };
|
result.push({
|
||||||
|
clientId,
|
||||||
|
tools: status.tools,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Starting to initialize MCP clients...");
|
// 初始化单个客户端
|
||||||
errorClients = [];
|
async function initializeSingleClient(
|
||||||
|
clientId: string,
|
||||||
const config = await getMcpConfig();
|
serverConfig: ServerConfig,
|
||||||
// Initialize all clients, key is clientId, value is client config
|
) {
|
||||||
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
logger.info(`Initializing client [${clientId}]...`);
|
||||||
try {
|
try {
|
||||||
logger.info(`Initializing MCP client: ${clientId}`);
|
const client = await createClient(clientId, serverConfig);
|
||||||
const client = await createClient(serverConfig as ServerConfig, clientId);
|
const tools = await listTools(client);
|
||||||
const primitives = await listPrimitives(client);
|
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||||
clientsMap.set(clientId, { client, primitives, errorMsg: null });
|
logger.success(`Client [${clientId}] initialized successfully`);
|
||||||
logger.success(
|
|
||||||
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorClients.push(clientId);
|
|
||||||
clientsMap.set(clientId, {
|
clientsMap.set(clientId, {
|
||||||
client: null,
|
client: null,
|
||||||
primitives: [],
|
tools: null,
|
||||||
errorMsg: error instanceof Error ? error.message : String(error),
|
errorMsg: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
logger.error(`Failed to initialize client ${clientId}: ${error}`);
|
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
if (errorClients.length > 0) {
|
|
||||||
logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`);
|
|
||||||
} else {
|
|
||||||
logger.success("All MCP clients initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableClients = await getAvailableClients();
|
|
||||||
logger.info(`Available clients: ${availableClients.join(",")}`);
|
|
||||||
|
|
||||||
return { errorClients };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute MCP request
|
// 初始化系统
|
||||||
export async function executeMcpAction(
|
export async function initializeMcpSystem() {
|
||||||
clientId: string,
|
logger.info("MCP Actions starting...");
|
||||||
request: McpRequestMessage,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
// Find the corresponding client
|
const config = await getMcpConfigFromFile();
|
||||||
const client = clientsMap.get(clientId)?.client;
|
// 初始化所有客户端
|
||||||
if (!client) {
|
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
logger.error(`Client ${clientId} not found`);
|
await initializeSingleClient(clientId, serverConfig);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return config;
|
||||||
logger.info(`Executing MCP request for ${clientId}`);
|
|
||||||
|
|
||||||
// Execute request and return result
|
|
||||||
return await executeRequest(client, request);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`MCP execution error: ${error}`);
|
logger.error(`Failed to initialize MCP system: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all available client IDs
|
// 添加服务器
|
||||||
export async function getAvailableClients() {
|
export async function addMcpServer(clientId: string, config: ServerConfig) {
|
||||||
return Array.from(clientsMap.entries())
|
|
||||||
.filter(([_, data]) => data.errorMsg === null)
|
|
||||||
.map(([clientId]) => clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all primitives from all clients
|
|
||||||
export async function getAllPrimitives(): Promise<
|
|
||||||
{
|
|
||||||
clientId: string;
|
|
||||||
primitives: Primitive[];
|
|
||||||
}[]
|
|
||||||
> {
|
|
||||||
return Array.from(clientsMap.entries()).map(([clientId, { primitives }]) => ({
|
|
||||||
clientId,
|
|
||||||
primitives,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取客户端的 Primitives
|
|
||||||
export async function getClientPrimitives(clientId: string) {
|
|
||||||
try {
|
try {
|
||||||
const clientData = clientsMap.get(clientId);
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
if (!clientData) {
|
const newConfig = {
|
||||||
console.warn(`Client ${clientId} not found in map`);
|
...currentConfig,
|
||||||
return null;
|
mcpServers: {
|
||||||
}
|
...currentConfig.mcpServers,
|
||||||
if (clientData.errorMsg) {
|
[clientId]: config,
|
||||||
console.warn(`Client ${clientId} has error: ${clientData.errorMsg}`);
|
},
|
||||||
return null;
|
};
|
||||||
}
|
await updateMcpConfig(newConfig);
|
||||||
return clientData.primitives;
|
// 只初始化新添加的服务器
|
||||||
|
await initializeSingleClient(clientId, config);
|
||||||
|
return newConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get primitives for client ${clientId}:`, error);
|
logger.error(`Failed to add server [${clientId}]: ${error}`);
|
||||||
return null;
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除服务器
|
||||||
|
export async function removeMcpServer(clientId: string) {
|
||||||
|
try {
|
||||||
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const { [clientId]: _, ...rest } = currentConfig.mcpServers;
|
||||||
|
const newConfig = {
|
||||||
|
...currentConfig,
|
||||||
|
mcpServers: rest,
|
||||||
|
};
|
||||||
|
await updateMcpConfig(newConfig);
|
||||||
|
|
||||||
|
// 关闭并移除客户端
|
||||||
|
const client = clientsMap.get(clientId);
|
||||||
|
if (client?.client) {
|
||||||
|
await removeClient(client.client);
|
||||||
|
}
|
||||||
|
clientsMap.delete(clientId);
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to remove server [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重启所有客户端
|
// 重启所有客户端
|
||||||
export async function restartAllClients() {
|
export async function restartAllClients() {
|
||||||
logger.info("Restarting all MCP clients...");
|
logger.info("Restarting all clients...");
|
||||||
|
try {
|
||||||
|
// 关闭所有客户端
|
||||||
|
for (const client of clientsMap.values()) {
|
||||||
|
if (client.client) {
|
||||||
|
await removeClient(client.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 清空状态
|
// 清空状态
|
||||||
clientsMap.clear();
|
clientsMap.clear();
|
||||||
errorClients = [];
|
|
||||||
initialized = false;
|
|
||||||
|
|
||||||
// 重新初始化
|
// 重新初始化
|
||||||
await initializeMcpClients();
|
const config = await getMcpConfigFromFile();
|
||||||
|
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
return {
|
await initializeSingleClient(clientId, serverConfig);
|
||||||
success: errorClients.length === 0,
|
}
|
||||||
errorClients,
|
return config;
|
||||||
};
|
} catch (error) {
|
||||||
|
logger.error(`Failed to restart clients: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有客户端状态
|
// 执行 MCP 请求
|
||||||
export async function getAllClientStatus(): Promise<
|
export async function executeMcpAction(
|
||||||
Record<string, string | null>
|
clientId: string,
|
||||||
> {
|
request: McpRequestMessage,
|
||||||
const status: Record<string, string | null> = {};
|
) {
|
||||||
for (const [clientId, data] of clientsMap.entries()) {
|
try {
|
||||||
status[clientId] = data.errorMsg;
|
const client = clientsMap.get(clientId);
|
||||||
|
if (!client?.client) {
|
||||||
|
throw new Error(`Client ${clientId} not found`);
|
||||||
|
}
|
||||||
|
logger.info(`Executing request for [${clientId}]`);
|
||||||
|
return await executeRequest(client.client, request);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to execute request for [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查客户端状态
|
// 获取 MCP 配置文件
|
||||||
export async function getClientErrors(): Promise<
|
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
|
||||||
Record<string, string | null>
|
try {
|
||||||
> {
|
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||||
const errors: Record<string, string | null> = {};
|
return JSON.parse(configStr);
|
||||||
for (const [clientId, data] of clientsMap.entries()) {
|
} catch (error) {
|
||||||
errors[clientId] = data.errorMsg;
|
logger.error(`Failed to load MCP config, using default config: ${error}`);
|
||||||
|
return DEFAULT_MCP_CONFIG;
|
||||||
}
|
}
|
||||||
return errors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取客户端状态,不重新初始化
|
// 更新 MCP 配置文件
|
||||||
export async function refreshClientStatus() {
|
async function updateMcpConfig(config: McpConfigData): Promise<void> {
|
||||||
logger.info("Refreshing client status...");
|
try {
|
||||||
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||||
// 如果还没初始化过,则初始化
|
} catch (error) {
|
||||||
if (!initialized) {
|
throw error;
|
||||||
return initializeMcpClients();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 否则只更新错误状态
|
|
||||||
errorClients = [];
|
// 重新初始化单个客户端
|
||||||
for (const [clientId, clientData] of clientsMap.entries()) {
|
export async function reinitializeClient(clientId: string) {
|
||||||
if (clientData.errorMsg !== null) {
|
const config = await getMcpConfigFromFile();
|
||||||
errorClients.push(clientId);
|
const serverConfig = config.mcpServers[clientId];
|
||||||
}
|
if (!serverConfig) {
|
||||||
}
|
throw new Error(`Server config not found for client ${clientId}`);
|
||||||
|
}
|
||||||
return { errorClients };
|
await initializeSingleClient(clientId, serverConfig);
|
||||||
}
|
}
|
||||||
|
@ -1,85 +1,45 @@
|
|||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
import { MCPClientLogger } from "./logger";
|
import { MCPClientLogger } from "./logger";
|
||||||
import { McpRequestMessage } from "./types";
|
import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export interface ServerConfig {
|
|
||||||
command: string;
|
|
||||||
args?: string[];
|
|
||||||
env?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = new MCPClientLogger();
|
const logger = new MCPClientLogger();
|
||||||
|
|
||||||
export async function createClient(
|
export async function createClient(
|
||||||
serverConfig: ServerConfig,
|
id: string,
|
||||||
name: string,
|
config: ServerConfig,
|
||||||
): Promise<Client> {
|
): Promise<Client> {
|
||||||
logger.info(`Creating client for server ${name}`);
|
logger.info(`Creating client for ${id}...`);
|
||||||
|
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
command: serverConfig.command,
|
command: config.command,
|
||||||
args: serverConfig.args,
|
args: config.args,
|
||||||
env: serverConfig.env,
|
env: config.env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new Client(
|
const client = new Client(
|
||||||
{
|
{
|
||||||
name: `nextchat-mcp-client-${name}`,
|
name: `nextchat-mcp-client-${id}`,
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {},
|
||||||
// roots: {
|
|
||||||
// listChanged: true,
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await client.connect(transport);
|
await client.connect(transport);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Primitive {
|
export async function removeClient(client: Client) {
|
||||||
type: "resource" | "tool" | "prompt";
|
logger.info(`Removing client...`);
|
||||||
value: any;
|
await client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** List all resources, tools, and prompts */
|
export async function listTools(client: Client): Promise<ListToolsResponse> {
|
||||||
export async function listPrimitives(client: Client): Promise<Primitive[]> {
|
return client.listTools();
|
||||||
const capabilities = client.getServerCapabilities();
|
|
||||||
const primitives: Primitive[] = [];
|
|
||||||
const promises = [];
|
|
||||||
if (capabilities?.resources) {
|
|
||||||
promises.push(
|
|
||||||
client.listResources().then(({ resources }) => {
|
|
||||||
resources.forEach((item) =>
|
|
||||||
primitives.push({ type: "resource", value: item }),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (capabilities?.tools) {
|
|
||||||
promises.push(
|
|
||||||
client.listTools().then(({ tools }) => {
|
|
||||||
tools.forEach((item) => primitives.push({ type: "tool", value: item }));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (capabilities?.prompts) {
|
|
||||||
promises.push(
|
|
||||||
client.listPrompts().then(({ prompts }) => {
|
|
||||||
prompts.forEach((item) =>
|
|
||||||
primitives.push({ type: "prompt", value: item }),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await Promise.all(promises);
|
|
||||||
return primitives;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Execute a request */
|
|
||||||
export async function executeRequest(
|
export async function executeRequest(
|
||||||
client: Client,
|
client: Client,
|
||||||
request: McpRequestMessage,
|
request: McpRequestMessage,
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
import { createClient, listPrimitives } from "@/app/mcp/client";
|
import { createClient, listTools } from "@/app/mcp/client";
|
||||||
import { MCPClientLogger } from "@/app/mcp/logger";
|
import { MCPClientLogger } from "@/app/mcp/logger";
|
||||||
import conf from "./mcp_config.json";
|
import conf from "./mcp_config.json";
|
||||||
|
|
||||||
const logger = new MCPClientLogger("MCP Server Example", true);
|
const logger = new MCPClientLogger("MCP Server Example", true);
|
||||||
|
|
||||||
const TEST_SERVER = "everything";
|
const TEST_SERVER = "filesystem";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
|
logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
|
||||||
|
|
||||||
logger.info(`Connecting to server ${TEST_SERVER}...`);
|
logger.info(`Connecting to server ${TEST_SERVER}...`);
|
||||||
|
|
||||||
const client = await createClient(conf.mcpServers[TEST_SERVER], TEST_SERVER);
|
const client = await createClient(TEST_SERVER, conf.mcpServers[TEST_SERVER]);
|
||||||
const primitives = await listPrimitives(client);
|
const tools = await listTools(client);
|
||||||
|
|
||||||
logger.success(`Connected to server ${TEST_SERVER}`);
|
logger.success(`Connected to server ${TEST_SERVER}`);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`${TEST_SERVER} supported primitives:\n${JSON.stringify(
|
`${TEST_SERVER} supported primitives:\n${JSON.stringify(tools, null, 2)}`,
|
||||||
primitives.filter((i) => i.type === "tool"),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {}
|
"mcpServers": {
|
||||||
|
"filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-filesystem",
|
||||||
|
"."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,7 +2,9 @@
|
|||||||
{
|
{
|
||||||
"id": "filesystem",
|
"id": "filesystem",
|
||||||
"name": "Filesystem",
|
"name": "Filesystem",
|
||||||
"description": "Secure file operations with configurable access controls",
|
"description": "Secure file operations with configurable access controlsSecure file operations with configurable access controlsSecure file operations with configurable access controlsSecure file operations with configurable access controls",
|
||||||
|
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
|
||||||
|
"tags": ["filesystem", "storage", "local"],
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
|
"baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -12,7 +14,9 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Allowed file system paths",
|
"description": "Allowed file system paths",
|
||||||
"required": true,
|
"required": true,
|
||||||
"minItems": 1
|
"minItems": 1,
|
||||||
|
"itemLabel": "Path",
|
||||||
|
"addButtonText": "Add Path"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -27,6 +31,8 @@
|
|||||||
"id": "github",
|
"id": "github",
|
||||||
"name": "GitHub",
|
"name": "GitHub",
|
||||||
"description": "Repository management, file operations, and GitHub API integration",
|
"description": "Repository management, file operations, and GitHub API integration",
|
||||||
|
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/github",
|
||||||
|
"tags": ["github", "git", "api", "vcs"],
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"baseArgs": ["-y", "@modelcontextprotocol/server-github"],
|
"baseArgs": ["-y", "@modelcontextprotocol/server-github"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -50,6 +56,8 @@
|
|||||||
"id": "gdrive",
|
"id": "gdrive",
|
||||||
"name": "Google Drive",
|
"name": "Google Drive",
|
||||||
"description": "File access and search capabilities for Google Drive",
|
"description": "File access and search capabilities for Google Drive",
|
||||||
|
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive",
|
||||||
|
"tags": ["google", "drive", "storage", "cloud"],
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
|
"baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
|
||||||
"configurable": false
|
"configurable": false
|
||||||
@ -58,6 +66,8 @@
|
|||||||
"id": "playwright",
|
"id": "playwright",
|
||||||
"name": "Playwright",
|
"name": "Playwright",
|
||||||
"description": "Browser automation and webscrapping with Playwright",
|
"description": "Browser automation and webscrapping with Playwright",
|
||||||
|
"repo": "https://github.com/executeautomation/mcp-playwright",
|
||||||
|
"tags": ["browser", "automation", "scraping"],
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
|
"baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
|
||||||
"configurable": false
|
"configurable": false
|
||||||
@ -66,6 +76,8 @@
|
|||||||
"id": "mongodb",
|
"id": "mongodb",
|
||||||
"name": "MongoDB",
|
"name": "MongoDB",
|
||||||
"description": "Direct interaction with MongoDB databases",
|
"description": "Direct interaction with MongoDB databases",
|
||||||
|
"repo": "",
|
||||||
|
"tags": ["database", "mongodb", "nosql"],
|
||||||
"command": "node",
|
"command": "node",
|
||||||
"baseArgs": ["dist/index.js"],
|
"baseArgs": ["dist/index.js"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -89,6 +101,8 @@
|
|||||||
"id": "difyworkflow",
|
"id": "difyworkflow",
|
||||||
"name": "Dify Workflow",
|
"name": "Dify Workflow",
|
||||||
"description": "Tools to query and execute Dify workflows",
|
"description": "Tools to query and execute Dify workflows",
|
||||||
|
"repo": "https://github.com/gotoolkits/mcp-difyworkflow-server",
|
||||||
|
"tags": ["workflow", "automation", "dify"],
|
||||||
"command": "mcp-difyworkflow-server",
|
"command": "mcp-difyworkflow-server",
|
||||||
"baseArgs": ["-base-url"],
|
"baseArgs": ["-base-url"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -130,6 +144,8 @@
|
|||||||
"id": "postgres",
|
"id": "postgres",
|
||||||
"name": "PostgreSQL",
|
"name": "PostgreSQL",
|
||||||
"description": "Read-only database access with schema inspection",
|
"description": "Read-only database access with schema inspection",
|
||||||
|
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
|
||||||
|
"tags": ["database", "postgresql", "sql"],
|
||||||
"command": "docker",
|
"command": "docker",
|
||||||
"baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
|
"baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -153,6 +169,8 @@
|
|||||||
"id": "brave-search",
|
"id": "brave-search",
|
||||||
"name": "Brave Search",
|
"name": "Brave Search",
|
||||||
"description": "Web and local search using Brave's Search API",
|
"description": "Web and local search using Brave's Search API",
|
||||||
|
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
|
||||||
|
"tags": ["search", "brave", "api"],
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
|
"baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -176,6 +194,8 @@
|
|||||||
"id": "google-maps",
|
"id": "google-maps",
|
||||||
"name": "Google Maps",
|
"name": "Google Maps",
|
||||||
"description": "Location services, directions, and place details",
|
"description": "Location services, directions, and place details",
|
||||||
|
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps",
|
||||||
|
"tags": ["maps", "google", "location", "api"],
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
|
"baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@ -199,6 +219,8 @@
|
|||||||
"id": "docker-mcp",
|
"id": "docker-mcp",
|
||||||
"name": "Docker",
|
"name": "Docker",
|
||||||
"description": "Run and manage docker containers, docker compose, and logs",
|
"description": "Run and manage docker containers, docker compose, and logs",
|
||||||
|
"repo": "https://github.com/QuantGeekDev/docker-mcp",
|
||||||
|
"tags": ["docker", "container", "devops"],
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"baseArgs": ["docker-mcp"],
|
"baseArgs": ["docker-mcp"],
|
||||||
"configurable": false
|
"configurable": false
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/
|
// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
|
||||||
export interface McpRequestMessage {
|
export interface McpRequestMessage {
|
||||||
jsonrpc?: "2.0";
|
jsonrpc?: "2.0";
|
||||||
@ -60,6 +61,32 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
|
|||||||
params: z.record(z.unknown()).optional(),
|
params: z.record(z.unknown()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
////////////
|
||||||
|
// Next Chat
|
||||||
|
////////////
|
||||||
|
export interface ListToolsResponse {
|
||||||
|
tools: {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
inputSchema?: object;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type McpClientData = McpActiveClient | McpErrorClient;
|
||||||
|
|
||||||
|
interface McpActiveClient {
|
||||||
|
client: Client;
|
||||||
|
tools: ListToolsResponse;
|
||||||
|
errorMsg: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpErrorClient {
|
||||||
|
client: null;
|
||||||
|
tools: null;
|
||||||
|
errorMsg: string;
|
||||||
|
}
|
||||||
|
|
||||||
// MCP 服务器配置相关类型
|
// MCP 服务器配置相关类型
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
command: string;
|
command: string;
|
||||||
@ -67,23 +94,52 @@ export interface ServerConfig {
|
|||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface McpConfig {
|
export interface McpConfigData {
|
||||||
|
// MCP Server 的配置
|
||||||
mcpServers: Record<string, ServerConfig>;
|
mcpServers: Record<string, ServerConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MCP_CONFIG: McpConfigData = {
|
||||||
|
mcpServers: {},
|
||||||
|
};
|
||||||
|
|
||||||
export interface ArgsMapping {
|
export interface ArgsMapping {
|
||||||
|
// 参数映射的类型
|
||||||
type: "spread" | "single" | "env";
|
type: "spread" | "single" | "env";
|
||||||
|
|
||||||
|
// 参数映射的位置
|
||||||
position?: number;
|
position?: number;
|
||||||
|
|
||||||
|
// 参数映射的 key
|
||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PresetServer {
|
export interface PresetServer {
|
||||||
|
// MCP Server 的唯一标识,作为最终配置文件 Json 的 key
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
// MCP Server 的显示名称
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
// MCP Server 的描述
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
// MCP Server 的仓库地址
|
||||||
|
repo: string;
|
||||||
|
|
||||||
|
// MCP Server 的标签
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
|
// MCP Server 的命令
|
||||||
command: string;
|
command: string;
|
||||||
|
|
||||||
|
// MCP Server 的参数
|
||||||
baseArgs: string[];
|
baseArgs: string[];
|
||||||
|
|
||||||
|
// MCP Server 是否需要配置
|
||||||
configurable: boolean;
|
configurable: boolean;
|
||||||
|
|
||||||
|
// MCP Server 的配置 schema
|
||||||
configSchema?: {
|
configSchema?: {
|
||||||
properties: Record<
|
properties: Record<
|
||||||
string,
|
string,
|
||||||
@ -95,5 +151,7 @@ export interface PresetServer {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MCP Server 的参数映射
|
||||||
argsMapping?: Record<string, ArgsMapping>;
|
argsMapping?: Record<string, ArgsMapping>;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
import { Home } from "./components/home";
|
import { Home } from "./components/home";
|
||||||
import { getServerSideConfig } from "./config/server";
|
import { getServerSideConfig } from "./config/server";
|
||||||
import { initializeMcpClients } from "./mcp/actions";
|
import { initializeMcpSystem } from "./mcp/actions";
|
||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
export default async function App() {
|
export default async function App() {
|
||||||
await initializeMcpClients();
|
// 初始化 MCP 系统
|
||||||
|
await initializeMcpSystem();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -21,8 +21,8 @@ import {
|
|||||||
DEFAULT_SYSTEM_TEMPLATE,
|
DEFAULT_SYSTEM_TEMPLATE,
|
||||||
GEMINI_SUMMARIZE_MODEL,
|
GEMINI_SUMMARIZE_MODEL,
|
||||||
KnowledgeCutOffDate,
|
KnowledgeCutOffDate,
|
||||||
MCP_PRIMITIVES_TEMPLATE,
|
|
||||||
MCP_SYSTEM_TEMPLATE,
|
MCP_SYSTEM_TEMPLATE,
|
||||||
|
MCP_TOOLS_TEMPLATE,
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
StoreKey,
|
StoreKey,
|
||||||
SUMMARIZE_MODEL,
|
SUMMARIZE_MODEL,
|
||||||
@ -35,7 +35,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
|
|||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { createEmptyMask, Mask } from "./mask";
|
import { createEmptyMask, Mask } from "./mask";
|
||||||
import { executeMcpAction, getAllPrimitives } from "../mcp/actions";
|
import { executeMcpAction, getAllTools } from "../mcp/actions";
|
||||||
import { extractMcpJson, isMcpJson } from "../mcp/utils";
|
import { extractMcpJson, isMcpJson } from "../mcp/utils";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
@ -199,23 +199,24 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getMcpSystemPrompt(): Promise<string> {
|
async function getMcpSystemPrompt(): Promise<string> {
|
||||||
let primitives = await getAllPrimitives();
|
const tools = await getAllTools();
|
||||||
primitives = primitives.filter((i) =>
|
|
||||||
i.primitives.some((p) => p.type === "tool"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let primitivesString = "";
|
let toolsStr = "";
|
||||||
primitives.forEach((i) => {
|
|
||||||
primitivesString += MCP_PRIMITIVES_TEMPLATE.replace(
|
tools.forEach((i) => {
|
||||||
|
// error client has no tools
|
||||||
|
if (!i.tools) return;
|
||||||
|
|
||||||
|
toolsStr += MCP_TOOLS_TEMPLATE.replace(
|
||||||
"{{ clientId }}",
|
"{{ clientId }}",
|
||||||
i.clientId,
|
i.clientId,
|
||||||
).replace(
|
).replace(
|
||||||
"{{ primitives }}",
|
"{{ tools }}",
|
||||||
i.primitives.map((p) => JSON.stringify(p, null, 2)).join("\n"),
|
i.tools.tools.map((p: object) => JSON.stringify(p, null, 2)).join("\n"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_PRIMITIVES }}", primitivesString);
|
return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_TOOLS }}", toolsStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CHAT_STATE = {
|
const DEFAULT_CHAT_STATE = {
|
||||||
|
Loading…
Reference in New Issue
Block a user