feat: Optimize MCP configuration logic

This commit is contained in:
Kadxy 2025-01-15 16:52:54 +08:00
parent ce13cf61a7
commit 8aa9a500fd
14 changed files with 766 additions and 653 deletions

View File

@ -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 && (

View File

@ -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 />;
} }

View File

@ -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,118 +66,106 @@
.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 {
.mcp-market-name { flex-grow: 1;
font-size: 14px; margin-right: 20px;
font-weight: bold; max-width: calc(100% - 300px);
display: flex; }
.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; align-items: center;
gap: 8px; margin-left: 10px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background-color: #22c55e;
color: #fff;
.server-status { &.error {
background-color: #ef4444;
}
.error-message {
margin-left: 4px;
font-size: 12px; font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
background-color: #10b981;
color: white;
&.error {
background-color: #ef4444;
}
&.waiting {
background-color: #f59e0b;
}
.error-message {
font-size: 11px;
opacity: 0.9;
margin-left: 4px;
}
} }
} }
.mcp-market-info {
font-size: 12px;
color: var(--black-50);
margin-top: 4px;
}
} }
}
.mcp-market-actions { .repo-link {
display: flex; color: var(--primary);
gap: 8px; font-size: 12px;
align-items: center; display: inline-flex;
align-items: center;
:global(.icon-button) { gap: 4px;
transition: all 0.3s ease; text-decoration: none;
border: 1px solid transparent; opacity: 0.8;
transition: opacity 0.2s;
&:hover { &:hover {
transform: translateY(-1px); opacity: 1;
filter: brightness(1.1);
} }
&.action-primary { svg {
background-color: var(--primary); width: 14px;
color: white; height: 14px;
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) { .tags-container {
flex-direction: column; display: flex;
gap: 10px; 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 { .mcp-market-actions {
display: flex;
gap: 8px;
align-items: flex-start;
flex-shrink: 0;
min-width: 180px;
justify-content: flex-end; justify-content: flex-end;
:global(.icon-button) {
transition: all 0.3s ease;
border: 1px solid transparent;
&:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
}
} }
} }
} }
@ -312,11 +298,6 @@
outline: none; 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;

View File

@ -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 () => { const isServerAdded = (id: string) => {
await refreshClientStatus(); return id in (config?.mcpServers ?? {});
const errors = await getClientErrors();
setClientErrors(errors);
}; };
// 初始加载配置 // 获取客户端状态
const updateClientStatus = async (clientId: string) => {
const status = await getClientStatus(clientId);
setClientStatuses((prev) => ({
...prev,
[clientId]: status,
}));
return status;
};
// 从服务器获取初始状态
useEffect(() => { useEffect(() => {
const init = async () => { const loadInitialState = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const data = await getMcpConfig(); const config = await getMcpConfigFromFile();
setConfig(data); setConfig(config);
await updateServerStatus();
// 获取所有客户端的状态
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) { } catch (error) {
showToast("Failed to load configuration"); console.error("Failed to load initial state:", error);
console.error(error); showToast("Failed to load initial state");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
init().then(); loadInitialState();
}, []); }, []);
// 保存配置 // Debug: 监控状态变化
const saveConfig = async (newConfig: McpConfig) => { useEffect(() => {
try { console.log("MCP Market - Current config:", config);
setIsLoading(true); console.log("MCP Market - Current clientStatuses:", clientStatuses);
await updateMcpConfig(newConfig); }, [config, clientStatuses]);
setConfig(newConfig);
// 配置改变时需要重新初始化
await restartAllClients();
await updateServerStatus();
showToast("Configuration saved successfully");
} catch (error) {
showToast("Failed to save configuration");
console.error(error);
} finally {
setIsLoading(false);
}
};
// 检查服务器是否已添加
const isServerAdded = (id: string) => {
return id in config.mcpServers;
};
// 加载当前编辑服务器的配置 // 加载当前编辑服务器的配置
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 addServer = async (preset: PresetServer) => { const statusPriority = {
if (!preset.configurable) { error: 0,
try { active: 1,
setIsLoading(true); undefined: 2,
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) => { if (aStatus !== bStatus) {
try { return statusPriority[aStatus] - statusPriority[bStatus];
setIsLoading(true); }
const { [id]: _, ...rest } = config.mcpServers;
const newConfig = { // 然后按名称排序
...config, return a.name.localeCompare(b.name);
mcpServers: rest, })
}; .map((server) => (
await saveConfig(newConfig); <div
} finally { className={clsx(styles["mcp-market-item"], {
setIsLoading(false); [styles["disabled"]]: isLoading,
} })}
key={server.id}
>
<div className={styles["mcp-market-header"]}>
<div className={styles["mcp-market-title"]}>
<div className={styles["mcp-market-name"]}>
{server.name}
{checkServerStatus(server.id).status !== "undefined" && (
<span
className={clsx(styles["server-status"], {
[styles["error"]]:
checkServerStatus(server.id).status === "error",
})}
>
{checkServerStatus(server.id).status === "error" ? (
<>
Error
<span className={styles["error-message"]}>
: {checkServerStatus(server.id).errorMsg}
</span>
</>
) : (
"Active"
)}
</span>
)}
{server.repo && (
<a
href={server.repo}
target="_blank"
rel="noopener noreferrer"
className={styles["repo-link"]}
title="Open repository"
>
<GithubIcon />
</a>
)}
</div>
<div className={styles["tags-container"]}>
{server.tags.map((tag, index) => (
<span key={index} className={styles["tag"]}>
{tag}
</span>
))}
</div>
<div
className={clsx(styles["mcp-market-info"], "one-line")}
title={server.description}
>
{server.description}
</div>
</div>
<div className={styles["mcp-market-actions"]}>
{isServerAdded(server.id) ? (
<>
{server.configurable && (
<IconButton
icon={<EditIcon />}
text="Configure"
className={clsx({
[styles["action-error"]]:
checkServerStatus(server.id).status === "error",
})}
onClick={() => setEditingServerId(server.id)}
disabled={isLoading}
/>
)}
<IconButton
icon={<EyeIcon />}
text="Tools"
onClick={async () => {
setViewingServerId(server.id);
await loadTools(server.id);
}}
disabled={
isLoading ||
checkServerStatus(server.id).status === "error"
}
/>
<IconButton
icon={<DeleteIcon />}
text="Remove"
className={styles["action-danger"]}
onClick={() => removeServer(server.id)}
disabled={isLoading}
/>
</>
) : (
<IconButton
icon={<AddIcon />}
text="Add"
className={styles["action-primary"]}
onClick={() => addServer(server)}
disabled={isLoading}
/>
)}
</div>
</div>
</div>
));
}; };
return ( 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>
)} )}

View File

@ -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
View 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

View File

@ -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 = [];
const config = await getMcpConfig();
// Initialize all clients, key is clientId, value is client config
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
try {
logger.info(`Initializing MCP client: ${clientId}`);
const client = await createClient(serverConfig as ServerConfig, clientId);
const primitives = await listPrimitives(client);
clientsMap.set(clientId, { client, primitives, errorMsg: null });
logger.success(
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
);
} catch (error) {
errorClients.push(clientId);
clientsMap.set(clientId, {
client: null,
primitives: [],
errorMsg: error instanceof Error ? error.message : String(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( async function initializeSingleClient(
clientId: string, clientId: string,
request: McpRequestMessage, serverConfig: ServerConfig,
) { ) {
logger.info(`Initializing client [${clientId}]...`);
try { try {
// Find the corresponding client const client = await createClient(clientId, serverConfig);
const client = clientsMap.get(clientId)?.client; const tools = await listTools(client);
if (!client) { clientsMap.set(clientId, { client, tools, errorMsg: null });
logger.error(`Client ${clientId} not found`); logger.success(`Client [${clientId}] initialized successfully`);
return;
}
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}`); clientsMap.set(clientId, {
client: null,
tools: null,
errorMsg: error instanceof Error ? error.message : String(error),
});
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
}
}
// 初始化系统
export async function initializeMcpSystem() {
logger.info("MCP Actions starting...");
try {
const config = await getMcpConfigFromFile();
// 初始化所有客户端
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
await initializeSingleClient(clientId, serverConfig);
}
return config;
} catch (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 {
// 清空状态 // 关闭所有客户端
clientsMap.clear(); for (const client of clientsMap.values()) {
errorClients = []; if (client.client) {
initialized = false; await removeClient(client.client);
}
// 重新初始化
await initializeMcpClients();
return {
success: errorClients.length === 0,
errorClients,
};
}
// 获取所有客户端状态
export async function getAllClientStatus(): Promise<
Record<string, string | null>
> {
const status: Record<string, string | null> = {};
for (const [clientId, data] of clientsMap.entries()) {
status[clientId] = data.errorMsg;
}
return status;
}
// 检查客户端状态
export async function getClientErrors(): Promise<
Record<string, string | null>
> {
const errors: Record<string, string | null> = {};
for (const [clientId, data] of clientsMap.entries()) {
errors[clientId] = data.errorMsg;
}
return errors;
}
// 获取客户端状态,不重新初始化
export async function refreshClientStatus() {
logger.info("Refreshing client status...");
// 如果还没初始化过,则初始化
if (!initialized) {
return initializeMcpClients();
}
// 否则只更新错误状态
errorClients = [];
for (const [clientId, clientData] of clientsMap.entries()) {
if (clientData.errorMsg !== null) {
errorClients.push(clientId);
} }
} // 清空状态
clientsMap.clear();
return { errorClients }; // 重新初始化
const config = await getMcpConfigFromFile();
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
await initializeSingleClient(clientId, serverConfig);
}
return config;
} catch (error) {
logger.error(`Failed to restart clients: ${error}`);
throw error;
}
}
// 执行 MCP 请求
export async function executeMcpAction(
clientId: string,
request: McpRequestMessage,
) {
try {
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;
}
}
// 获取 MCP 配置文件
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
try {
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
return JSON.parse(configStr);
} catch (error) {
logger.error(`Failed to load MCP config, using default config: ${error}`);
return DEFAULT_MCP_CONFIG;
}
}
// 更新 MCP 配置文件
async function updateMcpConfig(config: McpConfigData): Promise<void> {
try {
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
} catch (error) {
throw error;
}
}
// 重新初始化单个客户端
export async function reinitializeClient(clientId: string) {
const config = await getMcpConfigFromFile();
const serverConfig = config.mcpServers[clientId];
if (!serverConfig) {
throw new Error(`Server config not found for client ${clientId}`);
}
await initializeSingleClient(clientId, serverConfig);
} }

View File

@ -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,

View File

@ -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,
)}`,
); );
} }

View File

@ -1,3 +1,12 @@
{ {
"mcpServers": {} "mcpServers": {
} "filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"."
]
}
}
}

View File

@ -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

View File

@ -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>;
} }

View File

@ -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 (
<> <>

View File

@ -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 = {