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(
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 = []
for server in servers:
if servers['enable']:
# 设置状态
if server['enable']:
status = 'enabled'
else:
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 = {
'name': server['name'],
'mode': server['mode'],
'enable': server['enable'],
'description': server.get('description',''),
'extra_args': server.get('extra_args',{}),
'status': status,
'tools': [], # 暂时返回空数组需要连接到MCP服务器才能获取工具列表
'config': config,
}
servers_with_status.append(server_info)

View File

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

View File

@@ -1,47 +1,51 @@
'use client';
import { useEffect, useState, useRef } from 'react';
import { useEffect, useState } from 'react';
import styles from '@/app/home/plugins/plugins.module.css';
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 { MCPMarketCardVO } from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardVO';
// 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 { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
// import { Input } from '@/components/ui/input';
// import {
// Pagination,
// PaginationContent,
// PaginationItem,
// PaginationLink,
// PaginationNext,
// PaginationPrevious,
// } from '@/components/ui/pagination';
// import {
// Select,
// SelectContent,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from '@/components/ui/select';
import { httpClient, HttpClient } from '@/app/infra/http/HttpClient';
import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPMarketComponent({
askInstallServer,
onEditServer,
}: {
askInstallServer: (githubURL: string) => void;
askInstallServer?: (githubURL: string) => void;
onEditServer?: (serverName: string) => void;
}) {
const { t } = useTranslation();
const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
[],
);
const [totalCount, setTotalCount] = useState(0);
const [nowPage, setNowPage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
// const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
// [],
// );
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
// const [totalCount, setTotalCount] = useState(0);
// const [nowPage, setNowPage] = useState(1);
// const [searchKeyword, setSearchKeyword] = useState('');
const [loading, setLoading] = useState(false);
const [sortByValue, setSortByValue] = useState<string>('pushed_at');
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
const searchTimeout = useRef<NodeJS.Timeout | null>(null);
const pageSize = 12;
// const [sortByValue, setSortByValue] = useState<string>('pushed_at');
// const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
// const searchTimeout = useRef<NodeJS.Timeout | null>(null);
// const pageSize = 12;
useEffect(() => {
initData();
@@ -49,95 +53,131 @@ export default function MCPMarketComponent({
}, []);
function initData() {
getServerList();
fetchInstalledServers();
// getServerList(); // GitHub 市场功能暂时注释
}
function onInputSearchKeyword(keyword: string) {
setSearchKeyword(keyword);
// 清除之前的定时器
if (searchTimeout.current) {
clearTimeout(searchTimeout.current);
}
// 设置新的定时器
searchTimeout.current = setTimeout(() => {
setNowPage(1);
getServerList(1, keyword);
}, 500);
function fetchInstalledServers() {
setLoading(true);
httpClient
.getMCPServers()
.then((resp) => {
const servers = resp.servers.map((server) => new MCPCardVO(server));
setInstalledServers(servers);
setLoading(false);
})
.catch((error) => {
console.error('Failed to fetch MCP servers:', error);
setLoading(false);
});
}
function getServerList(
page: number = nowPage,
keyword: string = searchKeyword,
sortBy: string = sortByValue,
sortOrder: string = sortOrderValue,
) {
// setLoading(true);
// GitHub 市场功能暂时注释
// function onInputSearchKeyword(keyword: string) {
// setSearchKeyword(keyword);
// if (searchTimeout.current) {
// clearTimeout(searchTimeout.current);
// }
// searchTimeout.current = setTimeout(() => {
// setNowPage(1);
// getServerList(1, keyword);
// }, 500);
// }
// 获取后端的 MCP Market 服务器列表
httpClient.getMCPServers().then(
);
// function getServerList(
// 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);
// // });
// }
// 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/', '');
// }
// function handlePageChange(page: number) {
// setNowPage(page);
// getServerList(page);
// }
// 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', // 如果没有提供版本则默认为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);
}
// 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 (
<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
style={{
width: '300px',
@@ -178,7 +218,6 @@ export default function MCPMarketComponent({
/>
</PaginationItem>
{/* 如果总页数大于5则只显示5页如果总页数小于5则显示所有页 */}
{(() => {
const totalPages = Math.ceil(totalCount / pageSize);
const maxVisiblePages = 5;
@@ -255,7 +294,7 @@ export default function MCPMarketComponent({
</div>
))
)}
</div>
</div> */}
</div>
);
}

View File

@@ -4,7 +4,6 @@ import PluginInstalledComponent, {
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent';
// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent';
import MCPComponent, {
MCPComponentRef,
} from '@/app/home/plugins/mcp/MCPComponent';
@@ -34,19 +33,23 @@ import {
DialogFooter,
} from '@/components/ui/dialog';
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 { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { PluginV4 } from '@/app/infra/entities/plugin';
import { systemInfo } from '@/app/infra/http/HttpClient';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import { set } from 'lodash';
import { passiveEventSupported } from '@tanstack/react-table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@radix-ui/react-select';
import { useForm } from 'react-hook-form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@radix-ui/react-select';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { number, z } from 'zod';
import { z } from 'zod';
import { DialogDescription } from '@radix-ui/react-dialog';
import {
Form,
@@ -58,7 +61,6 @@ import {
FormMessage,
} from '@/components/ui/form';
enum PluginInstallStatus {
WAIT_INPUT = 'wait_input',
ASK_CONFIRM = 'ask_confirm',
@@ -66,38 +68,16 @@ enum PluginInstallStatus {
ERROR = 'error',
}
export default function PluginConfigPage(
{
editMode = false,
initMCPId,
onFormSubmit,
onFormCancel,
onMcpDeleted,
}:
{
editMode?: boolean;
initMCPId?: string;
onFormSubmit?: () => void;
onFormCancel?: () => void;
onMcpDeleted?: () => void;
} = {}
) {
export default function PluginConfigPage() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('installed');
const [modalOpen, setModalOpen] = useState(false);
// const [sortModalOpen, setSortModalOpen] = useState(false);
const [installSource, setInstallSource] = useState<string>('local');
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 [pluginInstallStatus, setPluginInstallStatus] =
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
const [installError, setInstallError] = useState<string | null>(null);
const [mcpInstallError, setMcpInstallError] = useState<string | null>(null);
const [githubURL, setGithubURL] = useState('');
const [isDragOver, setIsDragOver] = useState(false);
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);
setExtraArgs(newArgs);
form.setValue('extra_args', newArgs);
@@ -143,7 +123,9 @@ export default function PluginConfigPage(
z.object({
name: z.string().min(1, { message: t('mcp.nameRequired') }),
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') }),
extra_args: z.array(getExtraArgSchema(t)).optional(),
});
@@ -219,22 +201,35 @@ export default function PluginConfigPage(
});
}, 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 mcpComponentRef = useRef<MCPComponentRef>(null);
const [mcpTesting, setMcpTesting] = useState(false);
const [editingServerName, setEditingServerName] = useState<string | null>(
null,
);
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
// 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题
useEffect(() => {
console.log('[Dialog Debug] States:', { mcpSSEModalOpen, modalOpen, showDeleteConfirmModal });
console.log('[Dialog Debug] States:', {
mcpSSEModalOpen,
modalOpen,
showDeleteConfirmModal,
});
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
console.log('[Dialog Debug] All dialogs closed, cleaning up body styles...');
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);
console.log(
'[Dialog Debug] All dialogs closed, cleaning up body styles...',
);
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 = () => {
// 强制移除 body 上可能残留的样式
@@ -249,12 +244,21 @@ export default function PluginConfigPage(
document.body.style.overflow = '';
}
console.log('[Dialog Debug] After cleanup - body.style.pointerEvents:', document.body.style.pointerEvents);
console.log('[Dialog Debug] After cleanup - body.style.overflow:', document.body.style.overflow);
console.log(
'[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);
console.log('[Dialog Debug] Computed pointerEvents:', computedStyle.pointerEvents);
console.log(
'[Dialog Debug] Computed pointerEvents:',
computedStyle.pointerEvents,
);
};
// 多次清理以确保覆盖 Radix 的设置
@@ -280,7 +284,9 @@ export default function PluginConfigPage(
const interval = setInterval(() => {
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
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.pointerEvents = '';
}
@@ -294,10 +300,15 @@ export default function PluginConfigPage(
useEffect(() => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'style'
) {
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
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.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>) {
@@ -389,22 +455,30 @@ export default function PluginConfigPage(
timeout: value.timeout,
};
await httpClient.createMCPServer(serverConfig);
toast.success(t('mcp.createSuccess'));
if (isEditMode && editingServerName) {
// 编辑模式:更新服务器
await httpClient.updateMCPServer(editingServerName, serverConfig);
toast.success(t('mcp.updateSuccess'));
} else {
// 创建模式:新建服务器
await httpClient.createMCPServer(serverConfig);
toast.success(t('mcp.createSuccess'));
}
// 只有在异步操作成功后才关闭对话框
setMcpSSEModalOpen(false);
// 重置表单
// 重置表单和状态
form.reset();
setExtraArgs([]);
setEditingServerName(null);
setIsEditMode(false);
// 调用回调通知父组件刷新
onFormSubmit?.();
// 刷新服务器列表
setRefreshKey((prev) => prev + 1);
} catch (error) {
console.error('Failed to create MCP server:', error);
toast.error(t('mcp.createFailed'));
console.error('Failed to save MCP server:', error);
toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
}
}
@@ -422,9 +496,9 @@ export default function PluginConfigPage(
extraArgsObj[arg.key] = arg.value;
}
});
httpClient.testMCPServer(
form.getValues('name'),
).then((res) => {
httpClient
.testMCPServer(form.getValues('name'))
.then((res) => {
console.log(res);
toast.success(t('models.testSuccess'));
})
@@ -573,7 +647,6 @@ export default function PluginConfigPage(
return renderPluginConnectionErrorState();
}
return (
<div
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
@@ -599,9 +672,12 @@ export default function PluginConfigPage(
{t('plugins.marketplace')}
</TabsTrigger>
)}
<TabsTrigger value="mcp-servers" className="px-6 py-4 cursor-pointer">
{t('mcp.title')}
</TabsTrigger>
<TabsTrigger
value="mcp-servers"
className="px-6 py-4 cursor-pointer"
>
{t('mcp.title')}
</TabsTrigger>
</TabsList>
<div className="flex flex-row justify-end items-center">
@@ -618,7 +694,9 @@ export default function PluginConfigPage(
<DropdownMenuTrigger asChild>
<Button variant="default" className="px-6 py-4 cursor-pointer">
<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" />
</Button>
</DropdownMenuTrigger>
@@ -637,9 +715,13 @@ export default function PluginConfigPage(
<PlusIcon className="w-4 h-4" />
{t('mcp.installFromGithub')}
</DropdownMenuItem> */}
<DropdownMenuItem
<DropdownMenuItem
onClick={() => {
setActiveTab('mcp-servers');
setIsEditMode(false);
setEditingServerName(null);
form.reset();
setExtraArgs([]);
setMcpSSEModalOpen(true);
}}
>
@@ -691,11 +773,9 @@ export default function PluginConfigPage(
</TabsContent>
<TabsContent value="mcp-servers">
<MCPMarketComponent
askInstallServer={(githubURL) => {
setMcpGithubURL(githubURL);
setMcpMarketInstallModalOpen(true);
// setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null);
key={refreshKey}
onEditServer={(serverName) => {
loadServerForEdit(serverName);
}}
/>
</TabsContent>
@@ -781,229 +861,222 @@ export default function PluginConfigPage(
open={showDeleteConfirmModal}
onOpenChange={setShowDeleteConfirmModal}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
</DialogHeader>
<DialogDescription>
{t('plugins.deleteConfirmation')}
</DialogDescription>
<DialogFooter>
<Button
variant='destructive'
onClick={() => {
deleteMCPServer();
setShowDeleteConfirmModal(false);
}}
>
{t('common.confirm')}
</Button>
</DialogFooter>
</DialogContent>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
</DialogHeader>
<DialogDescription>
{t('plugins.deleteConfirmation')}
</DialogDescription>
<DialogFooter>
<Button
variant="destructive"
onClick={() => {
deleteMCPServer();
setShowDeleteConfirmModal(false);
}}
>
{t('common.confirm')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
open={mcpSSEModalOpen}
onOpenChange={setMcpSSEModalOpen}
onOpenChange={(open) => {
setMcpSSEModalOpen(open);
if (!open) {
// 关闭对话框时重置编辑状态
setIsEditMode(false);
setEditingServerName(null);
form.reset();
setExtraArgs([]);
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('mcp.createServer')}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleFormSubmit)}
className='space-y-4'
>
<div className='space-y-4'>
<FormField
control={form.control}
name='name'
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) =>
(
<DialogContent>
<DialogHeader>
<DialogTitle>
{isEditMode ? t('mcp.editServer') : t('mcp.createServer')}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleFormSubmit)}
className="space-y-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('mcp.ssereadtimeout')}
</FormLabel>
<FormLabel>{t('mcp.name')}</FormLabel>
<FormControl>
<Input
placeholder={t('mcp.sseTimeout')}
{...field}
/>
<Input {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)
}
)}
/>
<FormItem>
<FormLabel>{t('models.extraParameters')}</FormLabel>
<div className="space-y-2">
{extraArgs.map((arg, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder={t('models.keyName')}
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"
<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>
<FormLabel>{t('mcp.ssereadtimeout')}</FormLabel>
<FormControl>
<Input placeholder={t('mcp.sseTimeout')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormItem>
<FormLabel>{t('models.extraParameters')}</FormLabel>
<div className="space-y-2">
{extraArgs.map((arg, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder={t('models.keyName')}
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>
</svg>
</button>
</div>
))}
<Button type="button" variant="outline" onClick={addExtraArg}>
{t('models.addParameter')}
</Button>
</div>
<FormDescription>
{t('llm.extraParametersDescription')}
</FormDescription>
<FormMessage />
</FormItem>
{t('models.addParameter')}
</Button>
</div>
<FormDescription>
{t('llm.extraParametersDescription')}
</FormDescription>
<FormMessage />
</FormItem>
<DialogFooter>
{editMode && (
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)}
>
{t('common.delete')}
</Button>
)}
<DialogFooter>
{isEditMode && (
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)}
>
{t('common.delete')}
</Button>
)}
<Button type="submit">
{editMode ? t('common.save') : t('common.submit')}
</Button>
<Button type="submit">
{isEditMode ? t('common.save') : t('common.submit')}
</Button>
<Button
type="button"
variant="outline"
onClick={() => testMcp()}
disabled={mcpTesting}
>
{t('common.test')}
</Button>
<Button
type="button"
variant="outline"
onClick={() => testMcp()}
disabled={mcpTesting}
>
{t('common.test')}
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
setMcpSSEModalOpen(false);
form.reset();
setExtraArgs([]);
onFormCancel?.();
}}
>
{t('common.cancel')}
</Button>
</DialogFooter>
</div>
</form>
</Form>
</DialogContent>
<Button
type="button"
variant="outline"
onClick={() => {
setMcpSSEModalOpen(false);
form.reset();
setExtraArgs([]);
setIsEditMode(false);
setEditingServerName(null);
}}
>
{t('common.cancel')}
</Button>
</DialogFooter>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</div>
);
}

View File

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

View File

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

View File

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