mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-10 21:10:59 +00:00
perf: mcp server status checking logic
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
import uuid
|
import uuid
|
||||||
import traceback
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from ....core import app
|
from ....core import app
|
||||||
from ....entity.persistence import mcp as persistence_mcp
|
from ....entity.persistence import mcp as persistence_mcp
|
||||||
@@ -124,7 +125,6 @@ class MCPService:
|
|||||||
|
|
||||||
async def create_mcp_server(self, server_data: dict) -> str:
|
async def create_mcp_server(self, server_data: dict) -> str:
|
||||||
server_data['uuid'] = str(uuid.uuid4())
|
server_data['uuid'] = str(uuid.uuid4())
|
||||||
print('server_data:', server_data)
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data))
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
@@ -134,7 +134,8 @@ class MCPService:
|
|||||||
if server_entity:
|
if server_entity:
|
||||||
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server_entity)
|
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server_entity)
|
||||||
if self.ap.tool_mgr.mcp_tool_loader:
|
if self.ap.tool_mgr.mcp_tool_loader:
|
||||||
await self.ap.tool_mgr.mcp_tool_loader.load_mcp_server(server_config)
|
task = asyncio.create_task(self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config))
|
||||||
|
self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(task)
|
||||||
|
|
||||||
return server_data['uuid']
|
return server_data['uuid']
|
||||||
|
|
||||||
@@ -175,7 +176,9 @@ class MCPService:
|
|||||||
if updated_server:
|
if updated_server:
|
||||||
# convert entity to config dict
|
# convert entity to config dict
|
||||||
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, updated_server)
|
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, updated_server)
|
||||||
await self.ap.tool_mgr.mcp_tool_loader.load_mcp_server(server_config)
|
# await self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config)
|
||||||
|
task = asyncio.create_task(self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config))
|
||||||
|
self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(task)
|
||||||
|
|
||||||
async def delete_mcp_server(self, server_uuid: str) -> None:
|
async def delete_mcp_server(self, server_uuid: str) -> None:
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
import typing
|
import typing
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
import traceback
|
import traceback
|
||||||
@@ -16,6 +17,12 @@ import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
|||||||
from ....entity.persistence import mcp as persistence_mcp
|
from ....entity.persistence import mcp as persistence_mcp
|
||||||
|
|
||||||
|
|
||||||
|
class MCPSessionStatus(enum.Enum):
|
||||||
|
CONNECTING = 'connecting'
|
||||||
|
CONNECTED = 'connected'
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
|
||||||
class RuntimeMCPSession:
|
class RuntimeMCPSession:
|
||||||
"""运行时 MCP 会话"""
|
"""运行时 MCP 会话"""
|
||||||
|
|
||||||
@@ -33,7 +40,8 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
enable: bool
|
enable: bool
|
||||||
|
|
||||||
connected: bool
|
# connected: bool
|
||||||
|
status: MCPSessionStatus
|
||||||
|
|
||||||
last_test_error_message: str
|
last_test_error_message: str
|
||||||
|
|
||||||
@@ -47,7 +55,7 @@ class RuntimeMCPSession:
|
|||||||
self.exit_stack = AsyncExitStack()
|
self.exit_stack = AsyncExitStack()
|
||||||
self.functions = []
|
self.functions = []
|
||||||
|
|
||||||
self.connected = False
|
self.status = MCPSessionStatus.CONNECTING
|
||||||
self.last_test_error_message = ''
|
self.last_test_error_message = ''
|
||||||
|
|
||||||
async def _init_stdio_python_server(self):
|
async def _init_stdio_python_server(self):
|
||||||
@@ -117,10 +125,10 @@ class RuntimeMCPSession:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.connected = True
|
self.status = MCPSessionStatus.CONNECTED
|
||||||
self.last_test_error_message = ''
|
self.last_test_error_message = ''
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.connected = False
|
self.status = MCPSessionStatus.ERROR
|
||||||
self.last_test_error_message = str(e)
|
self.last_test_error_message = str(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@@ -129,7 +137,7 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
def get_runtime_info_dict(self) -> dict:
|
def get_runtime_info_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'connected': self.connected,
|
'status': self.status.value,
|
||||||
'error_message': self.last_test_error_message,
|
'error_message': self.last_test_error_message,
|
||||||
'tool_count': len(self.get_tools()),
|
'tool_count': len(self.get_tools()),
|
||||||
'tools': [
|
'tools': [
|
||||||
@@ -163,13 +171,13 @@ class MCPLoader(loader.ToolLoader):
|
|||||||
|
|
||||||
_last_listed_functions: list[resource_tool.LLMTool]
|
_last_listed_functions: list[resource_tool.LLMTool]
|
||||||
|
|
||||||
_startup_load_tasks: list[asyncio.Task]
|
_hosted_mcp_tasks: list[asyncio.Task]
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
def __init__(self, ap: app.Application):
|
||||||
super().__init__(ap)
|
super().__init__(ap)
|
||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
self._last_listed_functions = []
|
self._last_listed_functions = []
|
||||||
self._startup_load_tasks = []
|
self._hosted_mcp_tasks = []
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
await self.load_mcp_servers_from_db()
|
await self.load_mcp_servers_from_db()
|
||||||
@@ -185,30 +193,30 @@ class MCPLoader(loader.ToolLoader):
|
|||||||
for server in servers:
|
for server in servers:
|
||||||
config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server)
|
config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server)
|
||||||
|
|
||||||
async def load_mcp_server_task(server_config: dict):
|
task = asyncio.create_task(self.host_mcp_server(config))
|
||||||
self.ap.logger.debug(f'Loading MCP server {server_config}')
|
self._hosted_mcp_tasks.append(task)
|
||||||
try:
|
|
||||||
session = await self.load_mcp_server(server_config)
|
|
||||||
self.sessions[server_config['name']] = session
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(
|
|
||||||
f'Failed to load MCP server from db: {server_config["name"]}({server_config["uuid"]}): {e}\n{traceback.format_exc()}'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.ap.logger.debug(f'Starting MCP server {server_config["name"]}({server_config["uuid"]})')
|
async def host_mcp_server(self, server_config: dict):
|
||||||
try:
|
self.ap.logger.debug(f'Loading MCP server {server_config}')
|
||||||
await session.start()
|
try:
|
||||||
except Exception as e:
|
session = await self.load_mcp_server(server_config)
|
||||||
self.ap.logger.error(
|
self.sessions[server_config['name']] = session
|
||||||
f'Failed to start MCP server {server_config["name"]}({server_config["uuid"]}): {e}\n{traceback.format_exc()}'
|
except Exception as e:
|
||||||
)
|
self.ap.logger.error(
|
||||||
return
|
f'Failed to load MCP server from db: {server_config["name"]}({server_config["uuid"]}): {e}\n{traceback.format_exc()}'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
self.ap.logger.debug(f'Started MCP server {server_config["name"]}({server_config["uuid"]})')
|
self.ap.logger.debug(f'Starting MCP server {server_config["name"]}({server_config["uuid"]})')
|
||||||
|
try:
|
||||||
|
await session.start()
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.error(
|
||||||
|
f'Failed to start MCP server {server_config["name"]}({server_config["uuid"]}): {e}\n{traceback.format_exc()}'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
task = asyncio.create_task(load_mcp_server_task(config))
|
self.ap.logger.debug(f'Started MCP server {server_config["name"]}({server_config["uuid"]})')
|
||||||
self._startup_load_tasks.append(task)
|
|
||||||
|
|
||||||
async def load_mcp_server(self, server_config: dict) -> RuntimeMCPSession:
|
async def load_mcp_server(self, server_config: dict) -> RuntimeMCPSession:
|
||||||
"""加载 MCP 服务器到运行时
|
"""加载 MCP 服务器到运行时
|
||||||
@@ -271,20 +279,6 @@ class MCPLoader(loader.ToolLoader):
|
|||||||
|
|
||||||
raise ValueError(f'Tool not found: {name}')
|
raise ValueError(f'Tool not found: {name}')
|
||||||
|
|
||||||
async def reload_mcp_server(self, server_config: dict):
|
|
||||||
"""重新加载 MCP 服务器(先移除再加载)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
server_config: 服务器配置字典,必须包含 name 字段
|
|
||||||
"""
|
|
||||||
server_name = server_config['name']
|
|
||||||
|
|
||||||
if server_name in self.sessions:
|
|
||||||
await self.remove_mcp_server(server_name)
|
|
||||||
|
|
||||||
# 重新加载
|
|
||||||
await self.load_mcp_server(server_config)
|
|
||||||
|
|
||||||
async def remove_mcp_server(self, server_name: str):
|
async def remove_mcp_server(self, server_name: str):
|
||||||
"""移除 MCP 服务器"""
|
"""移除 MCP 服务器"""
|
||||||
if server_name not in self.sessions:
|
if server_name not in self.sessions:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { MCPServer } from '@/app/infra/entities/api';
|
import { MCPServer, MCPSessionStatus } from '@/app/infra/entities/api';
|
||||||
|
|
||||||
export class MCPCardVO {
|
export class MCPCardVO {
|
||||||
name: string;
|
name: string;
|
||||||
mode: 'stdio' | 'sse';
|
mode: 'stdio' | 'sse';
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
status: 'connected' | 'disconnected' | 'error' | 'disabled';
|
status: MCPSessionStatus;
|
||||||
tools: number;
|
tools: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
||||||
@@ -15,45 +15,15 @@ export class MCPCardVO {
|
|||||||
|
|
||||||
// Determine status from runtime_info
|
// Determine status from runtime_info
|
||||||
if (!data.runtime_info) {
|
if (!data.runtime_info) {
|
||||||
this.status = 'disconnected';
|
this.status = MCPSessionStatus.ERROR;
|
||||||
this.tools = 0;
|
this.tools = 0;
|
||||||
} else if (data.runtime_info.connected) {
|
} else if (data.runtime_info.status === MCPSessionStatus.CONNECTED) {
|
||||||
this.status = 'connected';
|
this.status = data.runtime_info.status;
|
||||||
this.tools = data.runtime_info.tool_count || 0;
|
this.tools = data.runtime_info.tool_count || 0;
|
||||||
} else {
|
} else {
|
||||||
this.status = 'error';
|
this.status = data.runtime_info.status;
|
||||||
this.tools = 0;
|
this.tools = 0;
|
||||||
this.error = data.runtime_info.error_message;
|
this.error = data.runtime_info.error_message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatusColor(): string {
|
|
||||||
switch (this.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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatusIcon(): string {
|
|
||||||
switch (this.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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import MCPCardComponent from '@/app/home/plugins/mcp-server/mcp-card/MCPCardComponent';
|
import MCPCardComponent from '@/app/home/plugins/mcp-server/mcp-card/MCPCardComponent';
|
||||||
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
|
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { MCPSessionStatus } from '@/app/infra/entities/api';
|
||||||
|
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
|
||||||
@@ -16,11 +17,44 @@ export default function MCPComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
|
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInstalledServers();
|
fetchInstalledServers();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup: clear polling interval when component unmounts
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check if any server is connecting and start/stop polling accordingly
|
||||||
|
useEffect(() => {
|
||||||
|
const hasConnecting = installedServers.some(
|
||||||
|
(server) => server.status === MCPSessionStatus.CONNECTING,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasConnecting && !pollingIntervalRef.current) {
|
||||||
|
// Start polling every 3 seconds
|
||||||
|
pollingIntervalRef.current = setInterval(() => {
|
||||||
|
fetchInstalledServers();
|
||||||
|
}, 3000);
|
||||||
|
} else if (!hasConnecting && pollingIntervalRef.current) {
|
||||||
|
// Stop polling when no server is connecting
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [installedServers]);
|
||||||
|
|
||||||
function fetchInstalledServers() {
|
function fetchInstalledServers() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
httpClient
|
httpClient
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Switch } from '@/components/ui/switch';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RefreshCcw, Wrench, Ban, AlertCircle } from 'lucide-react';
|
import { RefreshCcw, Wrench, Ban, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
|
import { MCPSessionStatus } from '@/app/infra/entities/api';
|
||||||
|
|
||||||
export default function MCPCardComponent({
|
export default function MCPCardComponent({
|
||||||
cardVO,
|
cardVO,
|
||||||
@@ -110,7 +111,7 @@ export default function MCPCardComponent({
|
|||||||
{t('mcp.statusDisabled')}
|
{t('mcp.statusDisabled')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : status === 'connected' ? (
|
) : status === MCPSessionStatus.CONNECTED ? (
|
||||||
// 连接成功 - 显示工具数量
|
// 连接成功 - 显示工具数量
|
||||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||||
<Wrench className="w-5 h-5" />
|
<Wrench className="w-5 h-5" />
|
||||||
@@ -118,6 +119,14 @@ export default function MCPCardComponent({
|
|||||||
{t('mcp.toolCount', { count: toolsCount })}
|
{t('mcp.toolCount', { count: toolsCount })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : status === MCPSessionStatus.CONNECTING ? (
|
||||||
|
// 连接中 - 蓝色加载
|
||||||
|
<div className="flex flex-row items-center gap-[0.4rem]">
|
||||||
|
<Loader2 className="w-4 h-4 text-blue-500 dark:text-blue-400 animate-spin" />
|
||||||
|
<div className="text-sm text-blue-500 dark:text-blue-400 font-medium">
|
||||||
|
{t('mcp.connecting')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 连接失败 - 红色
|
// 连接失败 - 红色
|
||||||
<div className="flex flex-row items-center gap-[0.4rem]">
|
<div className="flex flex-row items-center gap-[0.4rem]">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
import { Resolver, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -42,9 +42,10 @@ import {
|
|||||||
MCPServerRuntimeInfo,
|
MCPServerRuntimeInfo,
|
||||||
MCPTool,
|
MCPTool,
|
||||||
MCPServer,
|
MCPServer,
|
||||||
|
MCPSessionStatus,
|
||||||
} from '@/app/infra/entities/api';
|
} from '@/app/infra/entities/api';
|
||||||
|
|
||||||
// Status Display Component - 只在测试中或连接失败时使用
|
// Status Display Component - 在测试中、连接中或连接失败时使用
|
||||||
function StatusDisplay({
|
function StatusDisplay({
|
||||||
testing,
|
testing,
|
||||||
runtimeInfo,
|
runtimeInfo,
|
||||||
@@ -82,6 +83,35 @@ function StatusDisplay({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 连接中
|
||||||
|
if (runtimeInfo.status === MCPSessionStatus.CONNECTING) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-blue-600">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 animate-spin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">{t('mcp.connecting')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 连接失败
|
// 连接失败
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -201,6 +231,7 @@ export default function MCPFormDialog({
|
|||||||
const [runtimeInfo, setRuntimeInfo] = useState<MCPServerRuntimeInfo | null>(
|
const [runtimeInfo, setRuntimeInfo] = useState<MCPServerRuntimeInfo | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Load server data when editing
|
// Load server data when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -212,8 +243,48 @@ export default function MCPFormDialog({
|
|||||||
setExtraArgs([]);
|
setExtraArgs([]);
|
||||||
setRuntimeInfo(null);
|
setRuntimeInfo(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup polling interval when dialog closes
|
||||||
|
return () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [open, isEditMode, serverName]);
|
}, [open, isEditMode, serverName]);
|
||||||
|
|
||||||
|
// Poll for updates when runtime_info status is CONNECTING
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!open ||
|
||||||
|
!isEditMode ||
|
||||||
|
!serverName ||
|
||||||
|
!runtimeInfo ||
|
||||||
|
runtimeInfo.status !== MCPSessionStatus.CONNECTING
|
||||||
|
) {
|
||||||
|
// Stop polling if conditions are not met
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling if not already running
|
||||||
|
if (!pollingIntervalRef.current) {
|
||||||
|
pollingIntervalRef.current = setInterval(() => {
|
||||||
|
loadServerForEdit(serverName);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [open, isEditMode, serverName, runtimeInfo?.status]);
|
||||||
|
|
||||||
async function loadServerForEdit(serverName: string) {
|
async function loadServerForEdit(serverName: string) {
|
||||||
try {
|
try {
|
||||||
const resp = await httpClient.getMCPServer(serverName);
|
const resp = await httpClient.getMCPServer(serverName);
|
||||||
@@ -312,7 +383,7 @@ export default function MCPFormDialog({
|
|||||||
taskResp.runtime.exception || t('mcp.unknownError');
|
taskResp.runtime.exception || t('mcp.unknownError');
|
||||||
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
|
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
|
||||||
setRuntimeInfo({
|
setRuntimeInfo({
|
||||||
connected: false,
|
status: MCPSessionStatus.ERROR,
|
||||||
error_message: errorMsg,
|
error_message: errorMsg,
|
||||||
tool_count: 0,
|
tool_count: 0,
|
||||||
tools: [],
|
tools: [],
|
||||||
@@ -387,7 +458,8 @@ export default function MCPFormDialog({
|
|||||||
{isEditMode && runtimeInfo && (
|
{isEditMode && runtimeInfo && (
|
||||||
<div className="mb-4 space-y-3">
|
<div className="mb-4 space-y-3">
|
||||||
{/* 测试中或连接失败时显示状态 */}
|
{/* 测试中或连接失败时显示状态 */}
|
||||||
{(mcpTesting || !runtimeInfo.connected) && (
|
{(mcpTesting ||
|
||||||
|
runtimeInfo.status !== MCPSessionStatus.CONNECTED) && (
|
||||||
<div className="p-3 rounded-lg border">
|
<div className="p-3 rounded-lg border">
|
||||||
<StatusDisplay
|
<StatusDisplay
|
||||||
testing={mcpTesting}
|
testing={mcpTesting}
|
||||||
@@ -399,7 +471,7 @@ export default function MCPFormDialog({
|
|||||||
|
|
||||||
{/* 连接成功时只显示工具列表 */}
|
{/* 连接成功时只显示工具列表 */}
|
||||||
{!mcpTesting &&
|
{!mcpTesting &&
|
||||||
runtimeInfo.connected &&
|
runtimeInfo.status === MCPSessionStatus.CONNECTED &&
|
||||||
runtimeInfo.tools?.length > 0 && (
|
runtimeInfo.tools?.length > 0 && (
|
||||||
<ToolsList tools={runtimeInfo.tools} />
|
<ToolsList tools={runtimeInfo.tools} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -325,8 +325,14 @@ export interface MCPServerExtraArgsSSE {
|
|||||||
ssereadtimeout: number;
|
ssereadtimeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MCPSessionStatus {
|
||||||
|
CONNECTING = 'connecting',
|
||||||
|
CONNECTED = 'connected',
|
||||||
|
ERROR = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
export interface MCPServerRuntimeInfo {
|
export interface MCPServerRuntimeInfo {
|
||||||
connected: boolean;
|
status: MCPSessionStatus;
|
||||||
error_message: string;
|
error_message: string;
|
||||||
tool_count: number;
|
tool_count: number;
|
||||||
tools: MCPTool[];
|
tools: MCPTool[];
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ const enUS = {
|
|||||||
keyName: 'Key Name',
|
keyName: 'Key Name',
|
||||||
value: 'Value',
|
value: 'Value',
|
||||||
testing: 'Testing...',
|
testing: 'Testing...',
|
||||||
|
connecting: 'Connecting...',
|
||||||
testSuccess: 'Connection test successful',
|
testSuccess: 'Connection test successful',
|
||||||
testFailed: 'Connection test failed: ',
|
testFailed: 'Connection test failed: ',
|
||||||
testError: 'Connection test error',
|
testError: 'Connection test error',
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ const jaJP = {
|
|||||||
keyName: 'キー名',
|
keyName: 'キー名',
|
||||||
value: '値',
|
value: '値',
|
||||||
testing: 'テスト中...',
|
testing: 'テスト中...',
|
||||||
|
connecting: '接続中...',
|
||||||
testSuccess: '接続テストに成功しました',
|
testSuccess: '接続テストに成功しました',
|
||||||
testFailed: '接続テストに失敗しました:',
|
testFailed: '接続テストに失敗しました:',
|
||||||
testError: '接続テストエラー',
|
testError: '接続テストエラー',
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ const zhHans = {
|
|||||||
keyName: '键名',
|
keyName: '键名',
|
||||||
value: '值',
|
value: '值',
|
||||||
testing: '测试中...',
|
testing: '测试中...',
|
||||||
|
connecting: '连接中...',
|
||||||
testSuccess: '连接测试成功',
|
testSuccess: '连接测试成功',
|
||||||
testFailed: '连接测试失败:',
|
testFailed: '连接测试失败:',
|
||||||
testError: '连接测试出错',
|
testError: '连接测试出错',
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ const zhHant = {
|
|||||||
keyName: '鍵名',
|
keyName: '鍵名',
|
||||||
value: '值',
|
value: '值',
|
||||||
testing: '測試中...',
|
testing: '測試中...',
|
||||||
|
connecting: '連接中...',
|
||||||
testSuccess: '連接測試成功',
|
testSuccess: '連接測試成功',
|
||||||
testFailed: '連接測試失敗:',
|
testFailed: '連接測試失敗:',
|
||||||
testError: '連接測試出錯',
|
testError: '連接測試出錯',
|
||||||
|
|||||||
Reference in New Issue
Block a user