refactor: mcp server datastructure

This commit is contained in:
Junyan Qin
2025-11-04 16:13:03 +08:00
parent bc1fbfa190
commit c1c03f11b4
13 changed files with 155 additions and 208 deletions
@@ -1,7 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import styles from '@/app/home/plugins/plugins.module.css';
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';
@@ -55,16 +54,14 @@ export default function MCPComponent({
}
return (
<div className={`${styles.marketComponentBody}`}>
<div className="w-full h-full">
{/* 已安装的服务器列表 */}
<div className="mb-6">
<div className={`${styles.pluginListContainer}`}>
<div className="mb-[2rem]">
<div className="w-full px-[0.8rem] pt-[2rem] grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
{t('mcp.loading')}
</div>
<div className="text-center p-[2rem]">{t('mcp.loading')}</div>
) : installedServers.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
<div className="text-center p-[2rem]">
{t('mcp.noServerInstalled')}
</div>
) : (
@@ -6,6 +6,7 @@ import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Wrench } from 'lucide-react';
export default function MCPCardComponent({
cardVO,
@@ -36,36 +37,6 @@ export default function MCPCardComponent({
setEnabled(cardVO.enable);
}, [cardVO.name, cardVO.status, cardVO.error, cardVO.tools, cardVO.enable]);
function getStatusColor(): string {
switch (status) {
case 'connected':
return 'text-green-600';
case 'disconnected':
return 'text-gray-500';
case 'error':
return 'text-red-600';
case 'disabled':
return 'text-gray-400';
default:
return 'text-gray-500';
}
}
function getStatusIcon(): string {
switch (status) {
case 'connected':
return 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disconnected':
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'error':
return 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disabled':
return 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636';
default:
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
}
}
function handleEnable(checked: boolean) {
setSwitchEnable(false);
httpClient
@@ -154,55 +125,32 @@ export default function MCPCardComponent({
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] cursor-pointer"
className="w-[100%] h-[10rem] bg-white dark:bg-[#1f1f22] rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] dark:shadow-none p-[1.2rem] cursor-pointer transition-all duration-200 hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.1)] dark:hover:shadow-none"
onClick={onCardClick}
>
<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"
width="64"
height="64"
fill="rgba(70,146,221,1)"
>
<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>
<path d="M17.6567 14.8284L16.2425 13.4142L17.6567 12C19.2188 10.4379 19.2188 7.90524 17.6567 6.34314C16.0946 4.78105 13.5619 4.78105 11.9998 6.34314L10.5856 7.75736L9.17139 6.34314L10.5856 4.92893C12.9287 2.58578 16.7277 2.58578 19.0709 4.92893C21.414 7.27208 21.414 11.0711 19.0709 13.4142L17.6567 14.8284ZM14.8282 17.6569L13.414 19.0711C11.0709 21.4142 7.27189 21.4142 4.92875 19.0711C2.5856 16.7279 2.5856 12.9289 4.92875 10.5858L6.34296 9.17157L7.75717 10.5858L6.34296 12C4.78086 13.5621 4.78086 16.0948 6.34296 17.6569C7.90506 19.2189 10.4377 19.2189 11.9998 17.6569L13.414 16.2426L14.8282 17.6569ZM14.8282 7.75736L16.2425 9.17157L9.17139 16.2426L7.75717 14.8284L14.8282 7.75736Z"></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="flex flex-row items-center justify-start gap-[0.4rem]">
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
<Badge variant="outline" className="text-[0.7rem]">
{cardVO.mode.toUpperCase()}
</Badge>
</div>
</div>
<div className="flex flex-row items-center justify-start gap-[0.4rem] mt-1">
<svg
className={`w-4 h-4 ${getStatusColor()}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d={getStatusIcon()}
/>
</svg>
<div className={`text-[0.8rem] ${getStatusColor()}`}>
{status === 'connected' && t('mcp.statusConnected')}
{status === 'disconnected' && t('mcp.statusDisconnected')}
{status === 'error' && t('mcp.statusError')}
{status === 'disabled' && t('mcp.statusDisabled')}
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] font-medium">
{cardVO.name}
</div>
</div>
</div>
{error && (
<div className="text-[0.7rem] text-red-500 line-clamp-2 mt-1">
<div className="text-[0.7rem] text-red-500 dark:text-red-400 line-clamp-2 mt-1">
{error}
</div>
)}
@@ -210,15 +158,8 @@ export default function MCPCardComponent({
<div className="w-full flex flex-row items-start justify-start 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-black"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z" />
</svg>
<div className="text-base text-black font-medium">
<Wrench className="w-5 h-5" />
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
{t('mcp.toolCount', { count: toolsCount })}
</div>
</div>
@@ -246,22 +187,7 @@ export default function MCPCardComponent({
onClick={(e) => handleTest(e)}
disabled={testing}
>
<svg
className={`w-4 h-4 text-gray-600 ${
testing ? 'animate-spin' : ''
}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<RefreshCcw className="w-4 h-4" />
</Button>
</div>
</div>
+23 -21
View File
@@ -396,25 +396,20 @@ export default function PluginConfigPage() {
const server = resp.server ?? resp;
console.log('Loaded server for edit:', server);
const extraArgs = server.extra_args as
| Record<string, unknown>
| undefined;
const extraArgs = server.extra_args;
form.setValue('name', server.name);
form.setValue('url', (extraArgs?.url as string) || '');
form.setValue('timeout', (extraArgs?.timeout as number) || 30);
form.setValue(
'ssereadtimeout',
(extraArgs?.ssereadtimeout as number) || 300,
);
form.setValue('url', extraArgs.url);
form.setValue('timeout', extraArgs.timeout);
form.setValue('ssereadtimeout', extraArgs.ssereadtimeout);
if (extraArgs?.headers) {
const headers = Object.entries(
extraArgs.headers as Record<string, unknown>,
).map(([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}));
if (extraArgs.headers) {
const headers = Object.entries(extraArgs.headers).map(
([key, value]) => ({
key,
type: 'string' as const,
value: String(value),
}),
);
setExtraArgs(headers);
form.setValue('extra_args', headers);
}
@@ -569,7 +564,17 @@ export default function PluginConfigPage() {
await httpClient.updateMCPServer(editingServerName, serverConfig);
toast.success(t('mcp.updateSuccess'));
} else {
await httpClient.createMCPServer(serverConfig);
await httpClient.createMCPServer({
extra_args: {
url: value.url,
headers: extraArgsObj as Record<string, string>,
timeout: value.timeout,
ssereadtimeout: value.ssereadtimeout,
},
name: value.name,
mode: 'sse' as const,
enable: true,
});
toast.success(t('mcp.createSuccess'));
}
@@ -927,9 +932,6 @@ export default function PluginConfigPage() {
}}
/>
</TabsContent>
{/* <TabsContent value="mcp">
<MCPComponent ref={mcpComponentRef} />
</TabsContent> */}
<TabsContent value="mcp-servers">
<MCPServerComponent
key={refreshKey}
+17 -18
View File
@@ -318,29 +318,28 @@ export interface ApiRespMCPServer {
server: MCPServer;
}
export interface MCPServer {
extra_args: Record<string, unknown>;
name: string;
mode: 'stdio' | 'sse';
enable: boolean;
config: MCPServerConfig;
status: 'connected' | 'disconnected' | 'error' | 'disabled';
tools: MCPTool[];
error?: string;
export interface MCPServerExtraArgsSSE {
url: string;
headers: Record<string, string>;
timeout: number;
ssereadtimeout: number;
}
export interface MCPServerConfig {
export interface MCPServerRuntimeInfo {
connected: boolean;
error_message: string;
tool_count: number;
}
export interface MCPServer {
uuid?: string;
name: string;
mode: 'stdio' | 'sse';
enable: boolean;
// stdio mode
command?: string;
args?: string[];
env?: Record<string, string>;
// sse mode
url?: string;
headers?: Record<string, string>;
timeout?: number;
extra_args: MCPServerExtraArgsSSE;
runtime_info?: MCPServerRuntimeInfo;
created_at?: string;
updated_at?: string;
}
export interface MCPTool {
+4 -6
View File
@@ -35,7 +35,7 @@ import {
ApiRespPluginSystemStatus,
ApiRespMCPServers,
ApiRespMCPServer,
MCPServerConfig,
MCPServer,
} from '@/app/infra/entities/api';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
@@ -500,15 +500,13 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/mcp/servers/${serverName}`);
}
public createMCPServer(
server: MCPServerConfig,
): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', { source: server });
public createMCPServer(server: MCPServer): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', server);
}
public updateMCPServer(
serverName: string,
server: Partial<MCPServerConfig>,
server: Partial<MCPServer>,
): Promise<AsyncTaskCreatedResp> {
return this.put(`/api/v1/mcp/servers/${serverName}`, server);
}