fix: mcp refactor

This commit is contained in:
wangcham
2025-10-23 15:47:44 +00:00
parent d0a3dee083
commit 075091ed06
7 changed files with 577 additions and 437 deletions

View File

@@ -25,23 +25,42 @@ class MCPRouterGroup(group.RouterGroup):
result = await self.ap.persistence_mgr.execute_async( result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(MCPServer).order_by(MCPServer.created_at.desc()) sqlalchemy.select(MCPServer).order_by(MCPServer.created_at.desc())
) )
servers = [self.ap.persistence_mgr.serialize_model(MCPServer, row) for row in result.scalars().all()] raw_results = result.all()
servers = [self.ap.persistence_mgr.serialize_model(MCPServer, row) for row in raw_results]
servers_with_status = [] servers_with_status = []
for server in servers: for server in servers:
if servers['enable']: # 设置状态
if server['enable']:
status = 'enabled' status = 'enabled'
else: else:
status = 'disabled' status = 'disabled'
# 这里先写成开关状态,先不写连接状态 # 构建 config 对象 (前端期望的格式)
extra_args = server.get('extra_args', {})
config = {
'name': server['name'],
'mode': server['mode'],
'enable': server['enable'],
}
# 根据模式添加相应的配置
if server['mode'] == 'sse':
config['url'] = extra_args.get('url', '')
config['headers'] = extra_args.get('headers', {})
config['timeout'] = extra_args.get('timeout', 60)
elif server['mode'] == 'stdio':
config['command'] = extra_args.get('command', '')
config['args'] = extra_args.get('args', [])
config['env'] = extra_args.get('env', {})
server_info = { server_info = {
'name': server['name'], 'name': server['name'],
'mode': server['mode'], 'mode': server['mode'],
'enable': server['enable'], 'enable': server['enable'],
'description': server.get('description',''),
'extra_args': server.get('extra_args',{}),
'status': status, 'status': status,
'tools': [], # 暂时返回空数组需要连接到MCP服务器才能获取工具列表
'config': config,
} }
servers_with_status.append(server_info) servers_with_status.append(server_info)

View File

@@ -304,7 +304,7 @@ export default function LLMForm({
onLLMDeleted(); onLLMDeleted();
toast.success(t('models.deleteSuccess')); toast.success(t('models.deleteSuccess'));
}) })
.catch ((err) => { .catch((err) => {
toast.error(t('models.deleteError') + err.message); toast.error(t('models.deleteError') + err.message);
}); });
} }

View File

@@ -1,47 +1,51 @@
'use client'; 'use client';
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState } from 'react';
import styles from '@/app/home/plugins/plugins.module.css'; import styles from '@/app/home/plugins/plugins.module.css';
import { MCPMarketCardVO } from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardVO'; // import { MCPMarketCardVO } from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardVO';
import MCPMarketCardComponent from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardComponent'; // import MCPMarketCardComponent from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardComponent';
import MCPCardComponent from '@/app/home/plugins/mcp/mcp-card/MCPCardComponent';
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO';
// import { spaceClient } from '@/app/infra/http/HttpClient'; // import { spaceClient } from '@/app/infra/http/HttpClient';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input'; // import { Input } from '@/components/ui/input';
import { // import {
Pagination, // Pagination,
PaginationContent, // PaginationContent,
PaginationItem, // PaginationItem,
PaginationLink, // PaginationLink,
PaginationNext, // PaginationNext,
PaginationPrevious, // PaginationPrevious,
} from '@/components/ui/pagination'; // } from '@/components/ui/pagination';
import { // import {
Select, // Select,
SelectContent, // SelectContent,
SelectItem, // SelectItem,
SelectTrigger, // SelectTrigger,
SelectValue, // SelectValue,
} from '@/components/ui/select'; // } from '@/components/ui/select';
import { httpClient, HttpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPMarketComponent({ export default function MCPMarketComponent({
askInstallServer, onEditServer,
}: { }: {
askInstallServer: (githubURL: string) => void; askInstallServer?: (githubURL: string) => void;
onEditServer?: (serverName: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>( // const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
[], // [],
); // );
const [totalCount, setTotalCount] = useState(0); const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
const [nowPage, setNowPage] = useState(1); // const [totalCount, setTotalCount] = useState(0);
const [searchKeyword, setSearchKeyword] = useState(''); // const [nowPage, setNowPage] = useState(1);
// const [searchKeyword, setSearchKeyword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sortByValue, setSortByValue] = useState<string>('pushed_at'); // const [sortByValue, setSortByValue] = useState<string>('pushed_at');
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC'); // const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
const searchTimeout = useRef<NodeJS.Timeout | null>(null); // const searchTimeout = useRef<NodeJS.Timeout | null>(null);
const pageSize = 12; // const pageSize = 12;
useEffect(() => { useEffect(() => {
initData(); initData();
@@ -49,95 +53,131 @@ export default function MCPMarketComponent({
}, []); }, []);
function initData() { function initData() {
getServerList(); fetchInstalledServers();
// getServerList(); // GitHub 市场功能暂时注释
} }
function onInputSearchKeyword(keyword: string) { function fetchInstalledServers() {
setSearchKeyword(keyword); setLoading(true);
httpClient
// 清除之前的定时器 .getMCPServers()
if (searchTimeout.current) { .then((resp) => {
clearTimeout(searchTimeout.current); const servers = resp.servers.map((server) => new MCPCardVO(server));
} setInstalledServers(servers);
setLoading(false);
// 设置新的定时器 })
searchTimeout.current = setTimeout(() => { .catch((error) => {
setNowPage(1); console.error('Failed to fetch MCP servers:', error);
getServerList(1, keyword); setLoading(false);
}, 500); });
} }
function getServerList( // GitHub 市场功能暂时注释
page: number = nowPage, // function onInputSearchKeyword(keyword: string) {
keyword: string = searchKeyword, // setSearchKeyword(keyword);
sortBy: string = sortByValue, // if (searchTimeout.current) {
sortOrder: string = sortOrderValue, // clearTimeout(searchTimeout.current);
) { // }
// setLoading(true); // searchTimeout.current = setTimeout(() => {
// setNowPage(1);
// getServerList(1, keyword);
// }, 500);
// }
// 获取后端的 MCP Market 服务器列表 // function getServerList(
httpClient.getMCPServers().then( // page: number = nowPage,
); // keyword: string = searchKeyword,
// sortBy: string = sortByValue,
// sortOrder: string = sortOrderValue,
// ) {
// // GitHub 安装功能暂时注释
// // spaceClient
// // .getMCPMarketServers(page, pageSize, keyword, sortBy, sortOrder)
// // .then((res) => {
// // setMarketServerList(
// // res.servers.map((marketServer) => {
// // let repository = marketServer.repository;
// // if (repository.startsWith('https://github.com/')) {
// // repository = repository.replace('https://github.com/', '');
// // }
// // if (repository.startsWith('github.com/')) {
// // repository = repository.replace('github.com/', '');
// // }
// // const author = repository.split('/')[0];
// // const name = repository.split('/')[1];
// // return new MCPMarketCardVO({
// // author: author,
// // description: marketServer.description,
// // githubURL: `https://github.com/${repository}`,
// // name: name,
// // serverId: String(marketServer.ID),
// // starCount: marketServer.stars,
// // version:
// // 'version' in marketServer
// // ? String(marketServer.version)
// // : '1.0.0',
// // });
// // }),
// // );
// // setTotalCount(res.total);
// // setLoading(false);
// // console.log('market servers:', res);
// // })
// // .catch((error) => {
// // console.error(t('mcp.getServerListError'), error);
// // setLoading(false);
// // });
// }
// function handlePageChange(page: number) {
// spaceClient // setNowPage(page);
// .getMCPMarketServers(page, pageSize, keyword, sortBy, sortOrder) // getServerList(page);
// .then((res) => { // }
// setMarketServerList(
// res.servers.map((marketServer) => {
// let repository = marketServer.repository;
// if (repository.startsWith('https://github.com/')) {
// repository = repository.replace('https://github.com/', '');
// }
// if (repository.startsWith('github.com/')) { // function handleSortChange(value: string) {
// repository = repository.replace('github.com/', ''); // const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
// } // setSortByValue(newSortBy);
// setSortOrderValue(newSortOrder);
// const author = repository.split('/')[0]; // setNowPage(1);
// const name = repository.split('/')[1]; // getServerList(1, searchKeyword, newSortBy, newSortOrder);
// return new MCPMarketCardVO({ // }
// author: author,
// description: marketServer.description,
// githubURL: `https://github.com/${repository}`,
// name: name,
// serverId: String(marketServer.ID),
// starCount: marketServer.stars,
// version:
// 'version' in marketServer
// ? String(marketServer.version)
// : '1.0.0', // 如果没有提供版本则默认为1.0.0
// });
// }),
// );
// setTotalCount(res.total);
// setLoading(false);
// console.log('market servers:', res);
// })
// .catch((error) => {
// console.error(t('mcp.getServerListError'), error);
// setLoading(false);
// });
}
function handlePageChange(page: number) {
setNowPage(page);
getServerList(page);
}
function handleSortChange(value: string) {
const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
setSortByValue(newSortBy);
setSortOrderValue(newSortOrder);
setNowPage(1);
getServerList(1, searchKeyword, newSortBy, newSortOrder);
}
return ( return (
<div className={`${styles.marketComponentBody}`}> <div className={`${styles.marketComponentBody}`}>
<div className="flex items-center justify-start mb-2 mt-2 pl-[0.8rem] pr-[0.8rem]"> {/* 已安装的服务器列表 */}
<div className="mb-6">
<h2 className="text-xl font-semibold mb-4 pl-[0.8rem] pt-4">
{t('mcp.installedServers')}
</h2>
<div className={`${styles.pluginListContainer}`}>
{loading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
{t('mcp.loading')}
</div>
) : installedServers.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
{t('mcp.noInstalledServers')}
</div>
) : (
installedServers.map((server, index) => (
<div key={`${server.name}-${index}`}>
<MCPCardComponent
cardVO={server}
onCardClick={() => {
if (onEditServer) {
onEditServer(server.name);
}
}}
onRefresh={fetchInstalledServers}
/>
</div>
))
)}
</div>
</div>
{/* GitHub 市场功能暂时注释 */}
{/* <div className="flex items-center justify-start mb-2 mt-2 pl-[0.8rem] pr-[0.8rem]">
<Input <Input
style={{ style={{
width: '300px', width: '300px',
@@ -178,7 +218,6 @@ export default function MCPMarketComponent({
/> />
</PaginationItem> </PaginationItem>
{/* 如果总页数大于5则只显示5页如果总页数小于5则显示所有页 */}
{(() => { {(() => {
const totalPages = Math.ceil(totalCount / pageSize); const totalPages = Math.ceil(totalCount / pageSize);
const maxVisiblePages = 5; const maxVisiblePages = 5;
@@ -255,7 +294,7 @@ export default function MCPMarketComponent({
</div> </div>
)) ))
)} )}
</div> </div> */}
</div> </div>
); );
} }

View File

@@ -4,7 +4,6 @@ import PluginInstalledComponent, {
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent';
// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; // import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent';
import MCPComponent, { import MCPComponent, {
MCPComponentRef, MCPComponentRef,
} from '@/app/home/plugins/mcp/MCPComponent'; } from '@/app/home/plugins/mcp/MCPComponent';
@@ -34,19 +33,23 @@ import {
DialogFooter, DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import React, { useState, useRef, useCallback, useEffect, use } from 'react'; import React, { useState, useRef, useCallback, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PluginV4 } from '@/app/infra/entities/plugin'; import { PluginV4 } from '@/app/infra/entities/plugin';
import { systemInfo } from '@/app/infra/http/HttpClient'; import { systemInfo } from '@/app/infra/http/HttpClient';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import { set } from 'lodash'; import {
import { passiveEventSupported } from '@tanstack/react-table'; Select,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@radix-ui/react-select'; SelectContent,
import { useForm } from 'react-hook-form'; SelectItem,
SelectTrigger,
SelectValue,
} from '@radix-ui/react-select';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { number, z } from 'zod'; import { z } from 'zod';
import { DialogDescription } from '@radix-ui/react-dialog'; import { DialogDescription } from '@radix-ui/react-dialog';
import { import {
Form, Form,
@@ -58,7 +61,6 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
enum PluginInstallStatus { enum PluginInstallStatus {
WAIT_INPUT = 'wait_input', WAIT_INPUT = 'wait_input',
ASK_CONFIRM = 'ask_confirm', ASK_CONFIRM = 'ask_confirm',
@@ -66,38 +68,16 @@ enum PluginInstallStatus {
ERROR = 'error', ERROR = 'error',
} }
export default function PluginConfigPage( export default function PluginConfigPage() {
{
editMode = false,
initMCPId,
onFormSubmit,
onFormCancel,
onMcpDeleted,
}:
{
editMode?: boolean;
initMCPId?: string;
onFormSubmit?: () => void;
onFormCancel?: () => void;
onMcpDeleted?: () => void;
} = {}
) {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('installed'); const [activeTab, setActiveTab] = useState('installed');
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
// const [sortModalOpen, setSortModalOpen] = useState(false);
const [installSource, setInstallSource] = useState<string>('local'); const [installSource, setInstallSource] = useState<string>('local');
const [installInfo, setInstallInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any const [installInfo, setInstallInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
const [sortModalOpen, setSortModalOpen] = useState(false);
// const [mcpModalOpen, setMcpModalOpen] = useState(false);
const [mcpMarketInstallModalOpen, setMcpMarketInstallModalOpen] =
useState(false);
const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false); const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false);
const [pluginInstallStatus, setPluginInstallStatus] = const [pluginInstallStatus, setPluginInstallStatus] =
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT); useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
const [installError, setInstallError] = useState<string | null>(null); const [installError, setInstallError] = useState<string | null>(null);
const [mcpInstallError, setMcpInstallError] = useState<string | null>(null);
const [githubURL, setGithubURL] = useState(''); const [githubURL, setGithubURL] = useState('');
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
const [pluginSystemStatus, setPluginSystemStatus] = const [pluginSystemStatus, setPluginSystemStatus] =
@@ -134,7 +114,7 @@ export default function PluginConfigPage(
}); });
} }
}); });
const removeExtraArg = (index: number) => { const removeExtraArg = (index: number) => {
const newArgs = extraArgs.filter((_, i) => i !== index); const newArgs = extraArgs.filter((_, i) => i !== index);
setExtraArgs(newArgs); setExtraArgs(newArgs);
form.setValue('extra_args', newArgs); form.setValue('extra_args', newArgs);
@@ -143,7 +123,9 @@ export default function PluginConfigPage(
z.object({ z.object({
name: z.string().min(1, { message: t('mcp.nameRequired') }), name: z.string().min(1, { message: t('mcp.nameRequired') }),
timeout: z.number().min(30, { message: t('mcp.timeoutMin30') }), timeout: z.number().min(30, { message: t('mcp.timeoutMin30') }),
ssereadtimeout: z.number().min(300, { message: t('mcp.sseTimeoutMin300') }), ssereadtimeout: z
.number()
.min(300, { message: t('mcp.sseTimeoutMin300') }),
url: z.string().min(1, { message: t('mcp.requestURLRequired') }), url: z.string().min(1, { message: t('mcp.requestURLRequired') }),
extra_args: z.array(getExtraArgSchema(t)).optional(), extra_args: z.array(getExtraArgSchema(t)).optional(),
}); });
@@ -219,22 +201,35 @@ export default function PluginConfigPage(
}); });
}, 1000); }, 1000);
} }
const [mcpGithubURL, setMcpGithubURL] = useState('');
const [mcpSSEURL, setMcpSSEURL] = useState('');
const [mcpSSEConfig, setMcpSSEConfig] = useState<Record<string, any> | null>(null);
const [mcpInstallConfig, setMcpInstallConfig] = useState<Record<string, any> | null>(null);
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null); const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
const mcpComponentRef = useRef<MCPComponentRef>(null); const mcpComponentRef = useRef<MCPComponentRef>(null);
const [mcpTesting, setMcpTesting] = useState(false); const [mcpTesting, setMcpTesting] = useState(false);
const [editingServerName, setEditingServerName] = useState<string | null>(
null,
);
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
// 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题 // 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题
useEffect(() => { useEffect(() => {
console.log('[Dialog Debug] States:', { mcpSSEModalOpen, modalOpen, showDeleteConfirmModal }); console.log('[Dialog Debug] States:', {
mcpSSEModalOpen,
modalOpen,
showDeleteConfirmModal,
});
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
console.log('[Dialog Debug] All dialogs closed, cleaning up body styles...'); console.log(
console.log('[Dialog Debug] Before cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); '[Dialog Debug] All dialogs closed, cleaning up body styles...',
console.log('[Dialog Debug] Before cleanup - body.style.overflow:', document.body.style.overflow); );
console.log(
'[Dialog Debug] Before cleanup - body.style.pointerEvents:',
document.body.style.pointerEvents,
);
console.log(
'[Dialog Debug] Before cleanup - body.style.overflow:',
document.body.style.overflow,
);
const cleanup = () => { const cleanup = () => {
// 强制移除 body 上可能残留的样式 // 强制移除 body 上可能残留的样式
@@ -249,12 +244,21 @@ export default function PluginConfigPage(
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
console.log('[Dialog Debug] After cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); console.log(
console.log('[Dialog Debug] After cleanup - body.style.overflow:', document.body.style.overflow); '[Dialog Debug] After cleanup - body.style.pointerEvents:',
document.body.style.pointerEvents,
);
console.log(
'[Dialog Debug] After cleanup - body.style.overflow:',
document.body.style.overflow,
);
// 检查计算后的样式 // 检查计算后的样式
const computedStyle = window.getComputedStyle(document.body); const computedStyle = window.getComputedStyle(document.body);
console.log('[Dialog Debug] Computed pointerEvents:', computedStyle.pointerEvents); console.log(
'[Dialog Debug] Computed pointerEvents:',
computedStyle.pointerEvents,
);
}; };
// 多次清理以确保覆盖 Radix 的设置 // 多次清理以确保覆盖 Radix 的设置
@@ -280,7 +284,9 @@ export default function PluginConfigPage(
const interval = setInterval(() => { const interval = setInterval(() => {
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
if (document.body.style.pointerEvents === 'none') { if (document.body.style.pointerEvents === 'none') {
console.log('[Global Cleanup] Found stale pointer-events, cleaning...'); console.log(
'[Global Cleanup] Found stale pointer-events, cleaning...',
);
document.body.style.removeProperty('pointer-events'); document.body.style.removeProperty('pointer-events');
document.body.style.pointerEvents = ''; document.body.style.pointerEvents = '';
} }
@@ -294,10 +300,15 @@ export default function PluginConfigPage(
useEffect(() => { useEffect(() => {
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') { if (
mutation.type === 'attributes' &&
mutation.attributeName === 'style'
) {
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
if (document.body.style.pointerEvents === 'none') { if (document.body.style.pointerEvents === 'none') {
console.log('[MutationObserver] Detected pointer-events being set to none, reverting...'); console.log(
'[MutationObserver] Detected pointer-events being set to none, reverting...',
);
document.body.style.removeProperty('pointer-events'); document.body.style.removeProperty('pointer-events');
document.body.style.pointerEvents = ''; document.body.style.pointerEvents = '';
} }
@@ -360,8 +371,63 @@ export default function PluginConfigPage(
} }
} }
function deleteMCPServer() { async function deleteMCPServer() {
if (!editingServerName) return;
try {
await httpClient.deleteMCPServer(editingServerName);
toast.success(t('mcp.deleteSuccess'));
// 关闭所有对话框
setShowDeleteConfirmModal(false);
setMcpSSEModalOpen(false);
// 重置状态
form.reset();
setExtraArgs([]);
setEditingServerName(null);
setIsEditMode(false);
// 刷新服务器列表
setRefreshKey((prev) => prev + 1);
} catch (error) {
console.error('Failed to delete server:', error);
toast.error(t('mcp.deleteFailed'));
}
}
// 加载服务器数据用于编辑
async function loadServerForEdit(serverName: string) {
try {
const resp = await httpClient.getMCPServer(serverName);
const server = resp.server;
// 填充表单数据
form.setValue('name', server.name);
form.setValue('url', server.config.url || '');
form.setValue('timeout', server.config.timeout || 30);
form.setValue('ssereadtimeout', 300); // 默认值,如果后端有返回则使用后端的
// 填充 headers 作为 extra_args
if (server.config.headers) {
const headers = Object.entries(server.config.headers).map(
([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}),
);
setExtraArgs(headers);
form.setValue('extra_args', headers);
}
setEditingServerName(serverName);
setIsEditMode(true);
setMcpSSEModalOpen(true);
} catch (error) {
console.error('Failed to load server:', error);
toast.error(t('mcp.loadFailed'));
}
} }
async function handleFormSubmit(value: z.infer<typeof formSchema>) { async function handleFormSubmit(value: z.infer<typeof formSchema>) {
@@ -389,22 +455,30 @@ export default function PluginConfigPage(
timeout: value.timeout, timeout: value.timeout,
}; };
await httpClient.createMCPServer(serverConfig); if (isEditMode && editingServerName) {
// 编辑模式:更新服务器
toast.success(t('mcp.createSuccess')); await httpClient.updateMCPServer(editingServerName, serverConfig);
toast.success(t('mcp.updateSuccess'));
} else {
// 创建模式:新建服务器
await httpClient.createMCPServer(serverConfig);
toast.success(t('mcp.createSuccess'));
}
// 只有在异步操作成功后才关闭对话框 // 只有在异步操作成功后才关闭对话框
setMcpSSEModalOpen(false); setMcpSSEModalOpen(false);
// 重置表单 // 重置表单和状态
form.reset(); form.reset();
setExtraArgs([]); setExtraArgs([]);
setEditingServerName(null);
setIsEditMode(false);
// 调用回调通知父组件刷新 // 刷新服务器列表
onFormSubmit?.(); setRefreshKey((prev) => prev + 1);
} catch (error) { } catch (error) {
console.error('Failed to create MCP server:', error); console.error('Failed to save MCP server:', error);
toast.error(t('mcp.createFailed')); toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
} }
} }
@@ -422,9 +496,9 @@ export default function PluginConfigPage(
extraArgsObj[arg.key] = arg.value; extraArgsObj[arg.key] = arg.value;
} }
}); });
httpClient.testMCPServer( httpClient
form.getValues('name'), .testMCPServer(form.getValues('name'))
).then((res) => { .then((res) => {
console.log(res); console.log(res);
toast.success(t('models.testSuccess')); toast.success(t('models.testSuccess'));
}) })
@@ -573,7 +647,6 @@ export default function PluginConfigPage(
return renderPluginConnectionErrorState(); return renderPluginConnectionErrorState();
} }
return ( return (
<div <div
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`} className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
@@ -599,9 +672,12 @@ export default function PluginConfigPage(
{t('plugins.marketplace')} {t('plugins.marketplace')}
</TabsTrigger> </TabsTrigger>
)} )}
<TabsTrigger value="mcp-servers" className="px-6 py-4 cursor-pointer"> <TabsTrigger
{t('mcp.title')} value="mcp-servers"
</TabsTrigger> className="px-6 py-4 cursor-pointer"
>
{t('mcp.title')}
</TabsTrigger>
</TabsList> </TabsList>
<div className="flex flex-row justify-end items-center"> <div className="flex flex-row justify-end items-center">
@@ -618,7 +694,9 @@ export default function PluginConfigPage(
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="default" className="px-6 py-4 cursor-pointer"> <Button variant="default" className="px-6 py-4 cursor-pointer">
<PlusIcon className="w-4 h-4" /> <PlusIcon className="w-4 h-4" />
{activeTab === 'mcp-servers' ? t('mcp.add') : t('plugins.install')} {activeTab === 'mcp-servers'
? t('mcp.add')
: t('plugins.install')}
<ChevronDownIcon className="ml-2 w-4 h-4" /> <ChevronDownIcon className="ml-2 w-4 h-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -637,9 +715,13 @@ export default function PluginConfigPage(
<PlusIcon className="w-4 h-4" /> <PlusIcon className="w-4 h-4" />
{t('mcp.installFromGithub')} {t('mcp.installFromGithub')}
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setActiveTab('mcp-servers'); setActiveTab('mcp-servers');
setIsEditMode(false);
setEditingServerName(null);
form.reset();
setExtraArgs([]);
setMcpSSEModalOpen(true); setMcpSSEModalOpen(true);
}} }}
> >
@@ -691,11 +773,9 @@ export default function PluginConfigPage(
</TabsContent> </TabsContent>
<TabsContent value="mcp-servers"> <TabsContent value="mcp-servers">
<MCPMarketComponent <MCPMarketComponent
askInstallServer={(githubURL) => { key={refreshKey}
setMcpGithubURL(githubURL); onEditServer={(serverName) => {
setMcpMarketInstallModalOpen(true); loadServerForEdit(serverName);
// setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null);
}} }}
/> />
</TabsContent> </TabsContent>
@@ -781,229 +861,222 @@ export default function PluginConfigPage(
open={showDeleteConfirmModal} open={showDeleteConfirmModal}
onOpenChange={setShowDeleteConfirmModal} onOpenChange={setShowDeleteConfirmModal}
> >
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle> <DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
</DialogHeader> </DialogHeader>
<DialogDescription> <DialogDescription>
{t('plugins.deleteConfirmation')} {t('plugins.deleteConfirmation')}
</DialogDescription> </DialogDescription>
<DialogFooter> <DialogFooter>
<Button <Button
variant='destructive' variant="destructive"
onClick={() => { onClick={() => {
deleteMCPServer(); deleteMCPServer();
setShowDeleteConfirmModal(false); setShowDeleteConfirmModal(false);
}} }}
> >
{t('common.confirm')} {t('common.confirm')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog <Dialog
open={mcpSSEModalOpen} open={mcpSSEModalOpen}
onOpenChange={setMcpSSEModalOpen} onOpenChange={(open) => {
setMcpSSEModalOpen(open);
if (!open) {
// 关闭对话框时重置编辑状态
setIsEditMode(false);
setEditingServerName(null);
form.reset();
setExtraArgs([]);
}
}}
> >
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{t('mcp.createServer')} {isEditMode ? t('mcp.editServer') : t('mcp.createServer')}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(handleFormSubmit)} onSubmit={form.handleSubmit(handleFormSubmit)}
className='space-y-4' className="space-y-4"
> >
<div className='space-y-4'> <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name='name' name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem>
<FormLabel>{t('mcp.name')}</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control = {form.control}
name = 'url'
render={
({field}) => (
<FormItem>
<FormLabel>
{t('mcp.url')}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name='timeout'
render = {
({field}) => (
<FormItem>
<FormLabel>
{t('mcp.timeout')}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage/>
</FormItem>
)
}
/>
<FormField
control={form.control}
name='ssereadtimeout'
render = {
(field) =>
(
<FormItem> <FormItem>
<FormLabel> <FormLabel>{t('mcp.name')}</FormLabel>
{t('mcp.ssereadtimeout')}
</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
placeholder={t('mcp.sseTimeout')}
{...field}
/>
</FormControl> </FormControl>
<FormMessage/> <FormMessage />
</FormItem> </FormItem>
) )}
}
/> />
<FormItem> <FormField
<FormLabel>{t('models.extraParameters')}</FormLabel> control={form.control}
<div className="space-y-2"> name="url"
{extraArgs.map((arg, index) => ( render={({ field }) => (
<div key={index} className="flex gap-2"> <FormItem>
<Input <FormLabel>{t('mcp.url')}</FormLabel>
placeholder={t('models.keyName')} <FormControl>
value={arg.key} <Input {...field} />
onChange={(e) => </FormControl>
updateExtraArg(index, 'key', e.target.value) <FormMessage />
} </FormItem>
/> )}
<Select />
value={arg.type}
onValueChange={(value) => <FormField
updateExtraArg(index, 'type', value) control={form.control}
} name="timeout"
> render={({ field }) => (
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]"> <FormItem>
<SelectValue placeholder={t('models.type')} /> <FormLabel>{t('mcp.timeout')}</FormLabel>
</SelectTrigger> <FormControl>
<SelectContent> <Input {...field} />
<SelectItem value="string"> </FormControl>
{t('models.string')} <FormMessage />
</SelectItem> </FormItem>
<SelectItem value="number"> )}
{t('models.number')} />
</SelectItem>
<SelectItem value="boolean"> <FormField
{t('models.boolean')} control={form.control}
</SelectItem> name="ssereadtimeout"
</SelectContent> render={(field) => (
</Select> <FormItem>
<Input <FormLabel>{t('mcp.ssereadtimeout')}</FormLabel>
placeholder={t('models.value')} <FormControl>
value={arg.value} <Input placeholder={t('mcp.sseTimeout')} {...field} />
onChange={(e) => </FormControl>
updateExtraArg(index, 'value', e.target.value) <FormMessage />
} </FormItem>
/> )}
<button />
type="button"
className="p-2 hover:bg-gray-100 rounded" <FormItem>
onClick={() => removeExtraArg(index)} <FormLabel>{t('models.extraParameters')}</FormLabel>
> <div className="space-y-2">
<svg {extraArgs.map((arg, index) => (
xmlns="http://www.w3.org/2000/svg" <div key={index} className="flex gap-2">
viewBox="0 0 24 24" <Input
fill="currentColor" placeholder={t('models.keyName')}
className="w-5 h-5 text-red-500" value={arg.key}
onChange={(e) =>
updateExtraArg(index, 'key', e.target.value)
}
/>
<Select
value={arg.type}
onValueChange={(value) =>
updateExtraArg(index, 'type', value)
}
>
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('models.type')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">
{t('models.string')}
</SelectItem>
<SelectItem value="number">
{t('models.number')}
</SelectItem>
<SelectItem value="boolean">
{t('models.boolean')}
</SelectItem>
</SelectContent>
</Select>
<Input
placeholder={t('models.value')}
value={arg.value}
onChange={(e) =>
updateExtraArg(index, 'value', e.target.value)
}
/>
<button
type="button"
className="p-2 hover:bg-gray-100 rounded"
onClick={() => removeExtraArg(index)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-5 h-5 text-red-500"
>
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path>
</svg>
</button>
</div>
))}
<Button
type="button"
variant="outline"
onClick={addExtraArg}
> >
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path> {t('models.addParameter')}
</svg> </Button>
</button> </div>
</div> <FormDescription>
))} {t('llm.extraParametersDescription')}
<Button type="button" variant="outline" onClick={addExtraArg}> </FormDescription>
{t('models.addParameter')} <FormMessage />
</Button> </FormItem>
</div>
<FormDescription>
{t('llm.extraParametersDescription')}
</FormDescription>
<FormMessage />
</FormItem>
<DialogFooter> <DialogFooter>
{editMode && ( {isEditMode && (
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)} onClick={() => setShowDeleteConfirmModal(true)}
> >
{t('common.delete')} {t('common.delete')}
</Button> </Button>
)} )}
<Button type="submit"> <Button type="submit">
{editMode ? t('common.save') : t('common.submit')} {isEditMode ? t('common.save') : t('common.submit')}
</Button> </Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => testMcp()} onClick={() => testMcp()}
disabled={mcpTesting} disabled={mcpTesting}
> >
{t('common.test')} {t('common.test')}
</Button> </Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setMcpSSEModalOpen(false); setMcpSSEModalOpen(false);
form.reset(); form.reset();
setExtraArgs([]); setExtraArgs([]);
onFormCancel?.(); setIsEditMode(false);
}} setEditingServerName(null);
> }}
{t('common.cancel')} >
</Button> {t('common.cancel')}
</DialogFooter> </Button>
</div> </DialogFooter>
</form> </div>
</Form> </form>
</DialogContent> </Form>
</DialogContent>
</Dialog> </Dialog>
</div> </div>
</div> </div>
); );
} }

View File

@@ -553,7 +553,7 @@ export class BackendClient extends BaseHttpClient {
} }
public installMCPServerFromSSE( public installMCPServerFromSSE(
source: {}, source: object,
): Promise<AsyncTaskCreatedResp> { ): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', { source }); return this.post('/api/v1/mcp/servers', { source });
} }

View File

@@ -11,32 +11,35 @@ function Dialog({
open, open,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
const handleOpenChange = React.useCallback((isOpen: boolean) => { const handleOpenChange = React.useCallback(
onOpenChange?.(isOpen); (isOpen: boolean) => {
onOpenChange?.(isOpen);
// 当对话框关闭时,确保清理 body 样式 // 当对话框关闭时,确保清理 body 样式
if (!isOpen) { if (!isOpen) {
// 立即清理 // 立即清理
document.body.style.removeProperty('pointer-events');
document.body.style.removeProperty('overflow');
// 延迟再次清理,确保覆盖 Radix 的设置
setTimeout(() => {
document.body.style.removeProperty('pointer-events'); document.body.style.removeProperty('pointer-events');
document.body.style.removeProperty('overflow'); document.body.style.removeProperty('overflow');
}, 0);
setTimeout(() => { // 延迟再次清理,确保覆盖 Radix 的设置
document.body.style.removeProperty('pointer-events'); setTimeout(() => {
document.body.style.removeProperty('overflow'); document.body.style.removeProperty('pointer-events');
}, 50); document.body.style.removeProperty('overflow');
}, 0);
setTimeout(() => { setTimeout(() => {
document.body.style.removeProperty('pointer-events'); document.body.style.removeProperty('pointer-events');
document.body.style.removeProperty('overflow'); document.body.style.removeProperty('overflow');
}, 150); }, 50);
}
}, [onOpenChange]); setTimeout(() => {
document.body.style.removeProperty('pointer-events');
document.body.style.removeProperty('overflow');
}, 150);
}
},
[onOpenChange],
);
// 使用 effect 监控 open 状态变化 // 使用 effect 监控 open 状态变化
React.useEffect(() => { React.useEffect(() => {
@@ -61,7 +64,14 @@ function Dialog({
} }
}, [open]); }, [open]);
return <DialogPrimitive.Root data-slot="dialog" open={open} {...props} onOpenChange={handleOpenChange} />; return (
<DialogPrimitive.Root
data-slot="dialog"
open={open}
{...props}
onOpenChange={handleOpenChange}
/>
);
} }
function DialogTrigger({ function DialogTrigger({

View File

@@ -1,4 +1,3 @@
const zhHans = { const zhHans = {
common: { common: {
login: '登录', login: '登录',
@@ -335,20 +334,20 @@ const zhHans = {
onlySupportGithub: '目前仅支持从Github安装MCP服务器', onlySupportGithub: '目前仅支持从Github安装MCP服务器',
enterGithubLink: '输入Github仓库链接', enterGithubLink: '输入Github仓库链接',
add: '添加', add: '添加',
name:'名称', name: '名称',
nameExplained:'用于区分不同的MCP服务器实例', nameExplained: '用于区分不同的MCP服务器实例',
mcpDescription:'描述', mcpDescription: '描述',
descriptionExplained:'简要描述这个MCP服务器的功能或用途', descriptionExplained: '简要描述这个MCP服务器的功能或用途',
sseURL:'SSE URL', sseURL: 'SSE URL',
sseHeaders:'SSE Headers', sseHeaders: 'SSE Headers',
nameRequired:'名称不能为空', nameRequired: '名称不能为空',
sseURLRequired:'SSE URL不能为空', sseURLRequired: 'SSE URL不能为空',
enterSSELink:'输入SSE URL', enterSSELink: '输入SSE URL',
timeoutRequired:'超时时间不能为空', timeoutRequired: '超时时间不能为空',
headersExample:'示例: Authorization: Bearer token123', headersExample: '示例: Authorization: Bearer token123',
enterTimeout:'输入超时时间,单位为毫秒', enterTimeout: '输入超时时间,单位为毫秒',
installFromSSE:'从SSE安装', installFromSSE: '从SSE安装',
sseTimeout:'SSE超时时间' sseTimeout: 'SSE超时时间',
}, },
pipelines: { pipelines: {
title: '流水线', title: '流水线',