feat: completely remove the fucking mcp market components and refs

This commit is contained in:
Junyan Qin
2025-11-03 20:23:53 +08:00
parent f3199dda20
commit bc1fbfa190
10 changed files with 26 additions and 930 deletions

View File

@@ -2,13 +2,13 @@
import { useEffect, useState } from 'react';
import styles from '@/app/home/plugins/plugins.module.css';
import MCPCardComponent from '@/app/home/plugins/mcp/mcp-card/MCPCardComponent';
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO';
import MCPCardComponent from '@/app/home/plugins/mcp-server/mcp-card/MCPCardComponent';
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useTranslation } from 'react-i18next';
import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPMarketComponent({
export default function MCPComponent({
onEditServer,
toolsCountCache = {},
}: {
@@ -20,7 +20,6 @@ export default function MCPMarketComponent({
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
initData();
}, []);
@@ -55,8 +54,6 @@ export default function MCPMarketComponent({
});
}
return (
<div className={`${styles.marketComponentBody}`}>
{/* 已安装的服务器列表 */}

View File

@@ -1,4 +1,4 @@
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO';
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useState, useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from '@/components/ui/badge';

View File

@@ -1,87 +0,0 @@
import { MCPMarketCardVO } from '@/app/home/plugins/mcp-server/mcp-server-card/MCPServerCardVO';
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
export default function MCPMarketCardComponent({
cardVO,
installServer,
}: {
cardVO: MCPMarketCardVO;
installServer: (serverURL: string) => void;
}) {
const { t } = useTranslation();
function handleInstallClick(serverURL: string) {
installServer(serverURL);
}
return (
<div className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem]">
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
<svg
className="w-16 h-16 text-[#2288ee]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M13.5 2C13.5 2.82843 14.1716 3.5 15 3.5C15.8284 3.5 16.5 2.82843 16.5 2C16.5 1.17157 15.8284 0.5 15 0.5C14.1716 0.5 13.5 1.17157 13.5 2ZM8.5 8C8.5 8.82843 9.17157 9.5 10 9.5C10.8284 9.5 11.5 8.82843 11.5 8C11.5 7.17157 10.8284 6.5 10 6.5C9.17157 6.5 8.5 7.17157 8.5 8ZM1.5 14C1.5 14.8284 2.17157 15.5 3 15.5C3.82843 15.5 4.5 14.8284 4.5 14C4.5 13.1716 3.82843 12.5 3 12.5C2.17157 12.5 1.5 13.1716 1.5 14ZM19.5 14C19.5 14.8284 20.1716 15.5 21 15.5C21.8284 15.5 22.5 14.8284 22.5 14C22.5 13.1716 21.8284 12.5 21 12.5C20.1716 12.5 19.5 13.1716 19.5 14ZM8.5 20C8.5 20.8284 9.17157 21.5 10 21.5C10.8284 21.5 11.5 20.8284 11.5 20C11.5 19.1716 10.8284 19 10 19C9.17157 19 8.5 19.1716 8.5 20ZM2.5 8L6.5 8L6.5 10L2.5 10L2.5 8ZM13.5 8L17.5 8L17.5 10L13.5 10L13.5 8ZM8.5 2L8.5 6L10.5 6L10.5 2L8.5 2ZM8.5 14L8.5 18L10.5 18L10.5 14L8.5 14ZM2.5 14L6.5 14L6.5 16L2.5 16L2.5 14ZM13.5 14L17.5 14L17.5 16L13.5 16L13.5 14Z"></path>
</svg>
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
<div className="flex flex-col items-start justify-start">
<div className="flex flex-col items-start justify-start">
<div className="text-[0.7rem] text-[#666]">
{cardVO.author} /{' '}
</div>
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
</div>
</div>
<div className="text-[0.8rem] text-[#666] line-clamp-2">
{cardVO.description}
</div>
</div>
<div className="w-full flex flex-row items-start justify-between gap-[0.6rem]">
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
<svg
className="w-[1.2rem] h-[1.2rem] text-[#ffcd27]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path>
</svg>
<div className="text-base text-[#ffcd27] font-medium">
{t('mcp.starCount', { count: cardVO.starCount })}
</div>
</div>
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
<svg
className="w-[1.4rem] h-[1.4rem] text-black cursor-pointer"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
onClick={() => window.open(cardVO.githubURL, '_blank')}
>
<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"></path>
</svg>
<Button
variant="default"
size="sm"
onClick={() => {
handleInstallClick(cardVO.githubURL);
}}
className="cursor-pointer"
>
{t('mcp.install')}
</Button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,29 +0,0 @@
export interface IMCPMarketCardVO {
serverId: string;
author: string;
name: string;
description: string;
starCount: number;
githubURL: string;
version: string;
}
export class MCPMarketCardVO implements IMCPMarketCardVO {
serverId: string;
description: string;
name: string;
author: string;
githubURL: string;
starCount: number;
version: string;
constructor(prop: IMCPMarketCardVO) {
this.description = prop.description;
this.name = prop.name;
this.author = prop.author;
this.githubURL = prop.githubURL;
this.starCount = prop.starCount;
this.serverId = prop.serverId;
this.version = prop.version;
}
}

View File

@@ -1,324 +0,0 @@
'use client';
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO';
import MCPCardComponent from '@/app/home/plugins/mcp/mcp-card/MCPCardComponent';
import MCPForm from '@/app/home/plugins/mcp/mcp-form/MCPForm';
import styles from '@/app/home/plugins/plugins.module.css';
import { httpClient } from '@/app/infra/http/HttpClient';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
export interface MCPComponentRef {
refreshServerList: () => void;
createServer: () => void;
}
const MCPComponent = forwardRef<MCPComponentRef>((_props, ref) => {
const { t } = useTranslation();
const [serverList, setServerList] = useState<MCPCardVO[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [selectedServer, setSelectedServer] = useState<MCPCardVO | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
const [serverToDelete, setServerToDelete] = useState<MCPCardVO | null>(null);
const [deleting, setDeleting] = useState<boolean>(false);
const [autoTestTriggered, setAutoTestTriggered] = useState<boolean>(false);
const [testingServers, setTestingServers] = useState<Set<string>>(new Set());
useEffect(() => {
initData();
}, []);
function initData() {
getServerList(true);
}
function getServerList(shouldAutoTest: boolean = false) {
console.log('[MCP] Fetching server list...');
httpClient
.getMCPServers()
.then((value) => {
const servers = value.servers.map((server) => new MCPCardVO(server));
console.log(
'[MCP] Server list updated:',
servers.map((s) => ({
name: s.name,
status: s.status,
tools: s.tools,
})),
);
setServerList(servers);
// 自动测试:仅在初始加载且还未触发过自动测试时执行
if (shouldAutoTest && !autoTestTriggered && servers.length > 0) {
setAutoTestTriggered(true);
testAllServers(servers);
}
})
.catch((error) => {
toast.error(t('mcp.getServerListError') + error.message);
});
}
async function testAllServers(servers: MCPCardVO[]) {
// 为每个服务器启动测试
console.log('[MCP] Starting tests for all servers:', servers.length);
const testPromises = servers.map((server) => testServer(server.name));
// 等待所有测试完成
try {
await Promise.all(testPromises);
console.log('[MCP] All tests completed, refreshing server list...');
// 所有测试完成后延迟1秒再刷新确保后端状态已更新
setTimeout(() => {
console.log('[MCP] Refreshing server list after tests');
getServerList(false);
}, 1000);
} catch (err) {
console.error('[MCP] Some tests failed:', err);
// 即使有失败,也要刷新列表
setTimeout(() => {
console.log('[MCP] Refreshing server list after test failures');
getServerList(false);
}, 1000);
}
}
function testServer(serverName: string): Promise<void> {
return new Promise((resolve, reject) => {
// 标记为正在测试
console.log(`[MCP] Starting test for server: ${serverName}`);
setTestingServers((prev) => new Set(prev).add(serverName));
httpClient
.testMCPServer(serverName)
.then((resp) => {
const taskId = resp.task_id;
console.log(
`[MCP] Test task created for ${serverName}, task_id: ${taskId}`,
);
// 监控任务状态
const interval = setInterval(() => {
httpClient
.getAsyncTask(taskId)
.then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
// 标记测试完成
setTestingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
if (taskResp.runtime.exception) {
console.error(
`[MCP] Test failed for ${serverName}:`,
taskResp.runtime.exception,
);
reject(new Error(taskResp.runtime.exception));
} else {
console.log(
`[MCP] Test completed successfully for ${serverName}`,
);
resolve();
}
}
})
.catch((err) => {
clearInterval(interval);
setTestingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
console.error(
`[MCP] Error monitoring task for ${serverName}:`,
err,
);
reject(err);
});
}, 1000);
})
.catch((err) => {
console.error(`[MCP] Failed to start test for ${serverName}:`, err);
setTestingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
reject(err);
});
});
}
useImperativeHandle(ref, () => ({
refreshServerList: () => getServerList(false),
createServer: () => {
setSelectedServer(null);
setModalOpen(true);
},
}));
function handleServerClick(server: MCPCardVO) {
setSelectedServer(server);
setModalOpen(true);
}
function handleDeleteClick(server: MCPCardVO, e: React.MouseEvent) {
e.stopPropagation();
setServerToDelete(server);
setDeleteDialogOpen(true);
}
async function confirmDelete() {
if (!serverToDelete) return;
setDeleting(true);
try {
const response = await httpClient.deleteMCPServer(serverToDelete.name);
const taskId = response.task_id;
// 监控任务状态
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
setDeleting(false);
setDeleteDialogOpen(false);
if (taskResp.runtime.exception) {
toast.error(t('mcp.deleteError') + taskResp.runtime.exception);
} else {
toast.success(t('mcp.deleteSuccess'));
getServerList(false);
}
}
});
}, 1000);
} catch (error: unknown) {
setDeleting(false);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(t('mcp.deleteError') + errorMessage);
}
}
return (
<>
{serverList.length === 0 ? (
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
<svg
className="h-[3rem] w-[3rem]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 18V14.3C4 13.4716 3.32843 12.8 2.5 12.8H2V11.2H2.5C3.32843 11.2 4 10.5284 4 9.7V6C4 4.34315 5.34315 3 7 3H8V5H7C6.44772 5 6 5.44772 6 6V10.1C6 10.9858 5.42408 11.7372 4.62623 12C5.42408 12.2628 6 13.0142 6 13.9V18C6 18.5523 6.44772 19 7 19H8V21H7C5.34315 21 4 19.6569 4 18ZM20 14.3V18C20 19.6569 18.6569 21 17 21H16V19H17C17.5523 19 18 18.5523 18 18V13.9C18 13.0142 18.5759 12.2628 19.3738 12C18.5759 11.7372 18 10.9858 18 10.1V6C18 5.44772 17.5523 5 17 5H16V3H17C18.6569 3 20 4.34315 20 6V9.7C20 10.5284 20.6716 11.2 21.5 11.2H22V12.8H21.5C20.6716 12.8 20 13.4716 20 14.3Z"></path>
</svg>
<div className="text-lg mb-2">{t('mcp.noServerInstalled')}</div>
</div>
) : (
<div className={`${styles.pluginListContainer}`}>
{serverList.map((vo) => {
return (
<div key={vo.name} className="relative group">
<MCPCardComponent
cardVO={vo}
onCardClick={() => handleServerClick(vo)}
onRefresh={() => getServerList(false)}
/>
{/* 删除按钮 */}
<button
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-red-500 hover:bg-red-600 text-white rounded-full p-1"
onClick={(e) => handleDeleteClick(vo, e)}
>
<svg
className="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
);
})}
</div>
)}
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
<DialogHeader className="px-6 pt-6 pb-2">
<DialogTitle>
{selectedServer ? t('mcp.editServer') : t('mcp.createServer')}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 pb-6">
<MCPForm
serverName={selectedServer?.name}
isEdit={!!selectedServer}
onFormSubmit={() => {
setModalOpen(false);
getServerList(false);
}}
onFormCancel={() => {
setModalOpen(false);
}}
/>
</div>
</DialogContent>
</Dialog>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('mcp.deleteServer')}</AlertDialogTitle>
<AlertDialogDescription>
{t('mcp.confirmDeleteServer', { name: serverToDelete?.name })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}>
{t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={deleting}
className="bg-red-600 hover:bg-red-700"
>
{deleting ? t('plugins.deleting') : t('common.delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
});
export default MCPComponent;

View File

@@ -1,408 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { httpClient } from '@/app/infra/http/HttpClient';
import { MCPServerConfig } from '@/app/infra/entities/api';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { PlusIcon, TrashIcon } from 'lucide-react';
interface MCPFormProps {
serverName?: string;
isEdit?: boolean;
onFormSubmit: () => void;
onFormCancel: () => void;
}
export default function MCPForm({
serverName,
isEdit = false,
onFormSubmit,
onFormCancel,
}: MCPFormProps) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<MCPServerConfig>({
name: '',
mode: 'stdio',
enable: true,
command: '',
args: [],
env: {},
url: '',
headers: {},
timeout: 10,
});
useEffect(() => {
if (isEdit && serverName) {
loadServerConfig();
}
}, [isEdit, serverName]);
async function loadServerConfig() {
try {
const response = await httpClient.getMCPServer(serverName!);
setFormData(response.server.config);
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(t('mcp.getServerListError') + errorMessage);
}
}
function handleInputChange(field: keyof MCPServerConfig, value: unknown) {
setFormData((prev) => ({
...prev,
[field]: value,
}));
}
function addArrayItem(field: 'args', value: string = '') {
const currentArray = formData[field] as string[];
handleInputChange(field, [...currentArray, value]);
}
function updateArrayItem(field: 'args', index: number, value: string) {
const currentArray = formData[field] as string[];
const newArray = [...currentArray];
newArray[index] = value;
handleInputChange(field, newArray);
}
function removeArrayItem(field: 'args', index: number) {
const currentArray = formData[field] as string[];
const newArray = currentArray.filter((_, i) => i !== index);
handleInputChange(field, newArray);
}
function addObjectItem(
field: 'env' | 'headers',
key: string = '',
value: string = '',
) {
const currentObj = formData[field] as Record<string, string>;
handleInputChange(field, {
...currentObj,
[key]: value,
});
}
function updateObjectItem(
field: 'env' | 'headers',
oldKey: string,
newKey: string,
value: string,
) {
const currentObj = formData[field] as Record<string, string>;
const newObj = { ...currentObj };
if (oldKey !== newKey) {
delete newObj[oldKey];
}
newObj[newKey] = value;
handleInputChange(field, newObj);
}
function removeObjectItem(field: 'env' | 'headers', key: string) {
const currentObj = formData[field] as Record<string, string>;
const newObj = { ...currentObj };
delete newObj[key];
handleInputChange(field, newObj);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// 验证表单
if (!formData.name.trim()) {
toast.error(t('mcp.serverNameRequired'));
return;
}
if (formData.mode === 'stdio' && !formData.command?.trim()) {
toast.error(t('mcp.commandRequired'));
return;
}
if (formData.mode === 'sse' && !formData.url?.trim()) {
toast.error(t('mcp.urlRequired'));
return;
}
setLoading(true);
try {
let taskId: number;
if (isEdit) {
const response = await httpClient.updateMCPServer(
serverName!,
formData,
);
taskId = response.task_id;
} else {
const response = await httpClient.createMCPServer(formData);
taskId = response.task_id;
}
// 监控任务状态
const interval = setInterval(() => {
httpClient.getAsyncTask(taskId).then((taskResp) => {
if (taskResp.runtime.done) {
clearInterval(interval);
setLoading(false);
if (taskResp.runtime.exception) {
toast.error(
(isEdit ? t('mcp.saveError') : t('mcp.createError')) +
taskResp.runtime.exception,
);
} else {
toast.success(
isEdit ? t('mcp.saveSuccess') : t('mcp.createSuccess'),
);
onFormSubmit();
}
}
});
}, 1000);
} catch (error: unknown) {
setLoading(false);
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(
(isEdit ? t('mcp.saveError') : t('mcp.createError')) + errorMessage,
);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* 基础配置 */}
<div className="space-y-4">
<div>
<Label htmlFor="name">{t('mcp.serverName')}</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
disabled={isEdit}
placeholder={t('mcp.serverName')}
/>
</div>
<div>
<Label htmlFor="enable">{t('common.enable')}</Label>
<div className="flex items-center space-x-2 mt-2">
<Switch
id="enable"
checked={formData.enable}
onCheckedChange={(checked) =>
handleInputChange('enable', checked)
}
/>
</div>
</div>
<div>
<Label>{t('mcp.serverMode')}</Label>
<Tabs
value={formData.mode}
onValueChange={(value) =>
handleInputChange('mode', value as 'stdio' | 'sse')
}
className="mt-2"
>
<TabsList>
<TabsTrigger value="stdio">{t('mcp.stdio')}</TabsTrigger>
<TabsTrigger value="sse">{t('mcp.sse')}</TabsTrigger>
</TabsList>
<TabsContent value="stdio" className="space-y-4 mt-4">
<div>
<Label htmlFor="command">{t('mcp.command')}</Label>
<Input
id="command"
value={formData.command || ''}
onChange={(e) => handleInputChange('command', e.target.value)}
placeholder="python -m your_mcp_server"
/>
</div>
<div>
<Label>{t('mcp.args')}</Label>
<div className="space-y-2 mt-2">
{(formData.args || []).map((arg, index) => (
<div key={index} className="flex items-center space-x-2">
<Input
value={arg}
onChange={(e) =>
updateArrayItem('args', index, e.target.value)
}
placeholder="参数"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => removeArrayItem('args', index)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => addArrayItem('args')}
>
<PlusIcon className="h-4 w-4 mr-2" />
{t('mcp.addArgument')}
</Button>
</div>
</div>
<div>
<Label>{t('mcp.env')}</Label>
<div className="space-y-2 mt-2">
{Object.entries(formData.env || {}).map(([key, value]) => (
<div key={key} className="flex items-center space-x-2">
<Input
value={key}
onChange={(e) =>
updateObjectItem('env', key, e.target.value, value)
}
placeholder={t('mcp.keyName')}
className="flex-1"
/>
<Input
value={value}
onChange={(e) =>
updateObjectItem('env', key, key, e.target.value)
}
placeholder={t('mcp.value')}
className="flex-1"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => removeObjectItem('env', key)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => addObjectItem('env')}
>
<PlusIcon className="h-4 w-4 mr-2" />
{t('mcp.addEnvVar')}
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="sse" className="space-y-4 mt-4">
<div>
<Label htmlFor="url">{t('mcp.url')}</Label>
<Input
id="url"
value={formData.url || ''}
onChange={(e) => handleInputChange('url', e.target.value)}
placeholder="http://localhost:3000/sse"
/>
</div>
<div>
<Label htmlFor="timeout">{t('mcp.timeout')}</Label>
<Input
id="timeout"
type="number"
value={formData.timeout || 10}
onChange={(e) =>
handleInputChange('timeout', parseInt(e.target.value) || 10)
}
placeholder="10"
/>
</div>
<div>
<Label>{t('mcp.headers')}</Label>
<div className="space-y-2 mt-2">
{Object.entries(formData.headers || {}).map(
([key, value]) => (
<div key={key} className="flex items-center space-x-2">
<Input
value={key}
onChange={(e) =>
updateObjectItem(
'headers',
key,
e.target.value,
value,
)
}
placeholder={t('mcp.keyName')}
className="flex-1"
/>
<Input
value={value}
onChange={(e) =>
updateObjectItem(
'headers',
key,
key,
e.target.value,
)
}
placeholder={t('mcp.value')}
className="flex-1"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => removeObjectItem('headers', key)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
),
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => addObjectItem('headers')}
>
<PlusIcon className="h-4 w-4 mr-2" />
{t('mcp.addHeader')}
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onFormCancel}>
{t('common.cancel')}
</Button>
<Button type="submit" disabled={loading}>
{loading ? t('common.saving') : t('common.save')}
</Button>
</div>
</form>
);
}

View File

@@ -4,9 +4,7 @@ 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 MCPComponent, {
MCPComponentRef,
} from '@/app/home/plugins/mcp/MCPComponent';
import MCPServerComponent from '@/app/home/plugins/mcp-server/MCPServerComponent';
import styles from './plugins.module.css';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -200,9 +198,7 @@ export default function PluginConfigPage() {
}, 1000);
}
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
const mcpComponentRef = useRef<MCPComponentRef>(null);
const [mcpTesting, setMcpTesting] = useState(false);
const [editingServerName, setEditingServerName] = useState<string | null>(
null,
@@ -656,14 +652,20 @@ export default function PluginConfigPage() {
t('mcp.toolsFound'),
);
} else {
toast.error(t('mcp.testError') + ': ' + t('mcp.noToolsFound'));
toast.error(
t('mcp.testError') + ': ' + t('mcp.noToolsFound'),
);
}
} catch (parseError) {
console.error('Failed to parse test result:', parseError);
toast.error(t('mcp.testError') + ': ' + t('mcp.parseResultFailed'));
toast.error(
t('mcp.testError') + ': ' + t('mcp.parseResultFailed'),
);
}
} else {
toast.error(t('mcp.testError') + ': ' + t('mcp.noResultReturned'));
toast.error(
t('mcp.testError') + ': ' + t('mcp.noResultReturned'),
);
}
}
})
@@ -671,7 +673,11 @@ export default function PluginConfigPage() {
console.error('获取测试任务状态失败:', err);
clearInterval(interval);
setMcpTesting(false);
toast.error(t('mcp.testError') + ': ' + (err.message || t('mcp.getTaskFailed')));
toast.error(
t('mcp.testError') +
': ' +
(err.message || t('mcp.getTaskFailed')),
);
});
}, 1000);
} else {
@@ -682,7 +688,9 @@ export default function PluginConfigPage() {
.catch((err) => {
console.error('启动测试失败:', err);
setMcpTesting(false);
toast.error(t('mcp.testError') + ': ' + (err.message || t('mcp.unknownError')));
toast.error(
t('mcp.testError') + ': ' + (err.message || t('mcp.unknownError')),
);
});
}
@@ -853,15 +861,6 @@ export default function PluginConfigPage() {
</TabsList>
<div className="flex flex-row justify-end items-center">
{/* <Button
variant="outline"
className="px-6 py-4 cursor-pointer mr-2"
onClick={() => {
// setSortModalOpen(true);
}}
>
{t('plugins.arrange')}
</Button> */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="px-6 py-4 cursor-pointer">
@@ -875,18 +874,6 @@ export default function PluginConfigPage() {
<DropdownMenuContent align="end">
{activeTab === 'mcp-servers' ? (
<>
{/* <DropdownMenuItem
onClick={() => {
setActiveTab('mcp-market');
setMcpMarketInstallModalOpen(true);
setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null);
setMcpGithubURL('');
}}
>
<PlusIcon className="w-4 h-4" />
{t('mcp.installFromGithub')}
</DropdownMenuItem> */}
<DropdownMenuItem
onClick={() => {
setActiveTab('mcp-servers');
@@ -940,9 +927,9 @@ export default function PluginConfigPage() {
}}
/>
</TabsContent>
<TabsContent value="mcp">
{/* <TabsContent value="mcp">
<MCPComponent ref={mcpComponentRef} />
</TabsContent>
</TabsContent> */}
<TabsContent value="mcp-servers">
<MCPServerComponent
key={refreshKey}

View File

@@ -348,27 +348,3 @@ export interface MCPTool {
description: string;
parameters: object;
}
// MCP Market
export interface MCPMarketResponse {
servers: MCPMarketServer[];
total: number;
}
export interface MCPMarketServer {
ID: number;
CreatedAt: string; // ISO 8601 格式日期
UpdatedAt: string;
DeletedAt: string | null;
name: string;
author: string;
description: string;
repository: string; // GitHub 仓库路径
artifacts_path: string;
stars: number;
downloads: number;
status: 'initialized' | 'mounted';
synced_at: string;
pushed_at: string; // 最后一次代码推送时间
version?: string;
}

View File

@@ -530,22 +530,6 @@ export class BackendClient extends BaseHttpClient {
return this.post(`/api/v1/mcp/servers/${serverName}/test`);
}
// public getMCPMarketServers(
// page: number,
// page_size: number,
// query: string,
// sort_by: string = 'stars',
// sort_order: string = 'DESC',
// ): Promise<MCPMarketResponse> {
// return this.post(`/api/v1/market/mcp`, {
// page,
// page_size,
// query,
// sort_by,
// sort_order,
// });
// }
public installMCPServerFromGithub(
source: string,
): Promise<AsyncTaskCreatedResp> {