mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Enhance routing and settings functionality (#29)
This commit is contained in:
@@ -2,7 +2,7 @@ FROM python:3.13-slim-bookworm AS base
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
RUN apt-get update && apt-get install -y curl gnupg \
|
||||
RUN apt-get update && apt-get install -y curl gnupg git \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -10,37 +10,37 @@ interface MarketServerCardProps {
|
||||
const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 智能计算要显示多少个标签,确保在单行内展示
|
||||
// Intelligently calculate how many tags to display to ensure they fit in a single line
|
||||
const getTagsToDisplay = () => {
|
||||
if (!server.tags || server.tags.length === 0) {
|
||||
return { tagsToShow: [], hasMore: false, moreCount: 0 };
|
||||
}
|
||||
|
||||
// 估计卡片内单行可用宽度(以字符为单位)
|
||||
const estimatedAvailableWidth = 30; // 估计一行可以容纳的字符数
|
||||
// Estimate available width in the card (in characters)
|
||||
const estimatedAvailableWidth = 28; // Estimated number of characters that can fit in one line
|
||||
|
||||
// 计算标签和加号所需的字符空间(包括#号和间距)
|
||||
// Calculate the character space needed for tags and plus sign (including # and spacing)
|
||||
const calculateTagWidth = (tag: string) => tag.length + 3; // +3 for # and spacing
|
||||
|
||||
// 循环确定能显示的最大标签数量
|
||||
// Loop to determine the maximum number of tags that can be displayed
|
||||
let totalWidth = 0;
|
||||
let i = 0;
|
||||
|
||||
// 首先对标签按长度排序,优先显示较短的标签
|
||||
// First, sort tags by length to prioritize displaying shorter tags
|
||||
const sortedTags = [...server.tags].sort((a, b) => a.length - b.length);
|
||||
|
||||
// 计算能够放入的标签数量
|
||||
// Calculate how many tags can fit
|
||||
for (i = 0; i < sortedTags.length; i++) {
|
||||
const tagWidth = calculateTagWidth(sortedTags[i]);
|
||||
|
||||
// 如果这个标签会使总宽度超出可用宽度,停止添加
|
||||
// If this tag would make the total width exceed available width, stop adding
|
||||
if (totalWidth + tagWidth > estimatedAvailableWidth) {
|
||||
break;
|
||||
}
|
||||
|
||||
totalWidth += tagWidth;
|
||||
|
||||
// 如果这是最后一个标签但仍有空间,不需要显示"更多"
|
||||
// If this is the last tag but there's still space, no need to show "more"
|
||||
if (i === sortedTags.length - 1) {
|
||||
return {
|
||||
tagsToShow: sortedTags,
|
||||
@@ -50,16 +50,16 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有足够空间显示任何标签,至少显示一个
|
||||
// If there's not enough space to display any tags, show at least one
|
||||
if (i === 0 && sortedTags.length > 0) {
|
||||
i = 1;
|
||||
}
|
||||
|
||||
// 计算"更多"标签所需的空间
|
||||
// Calculate space needed for the "more" tag
|
||||
const moreCount = sortedTags.length - i;
|
||||
const moreTagWidth = 3 + String(moreCount).length + t('market.moreTags').length;
|
||||
|
||||
// 如果剩余空间足够显示"更多"标签
|
||||
// If there's enough remaining space to display the "more" tag
|
||||
if (totalWidth + moreTagWidth <= estimatedAvailableWidth || i < 1) {
|
||||
return {
|
||||
tagsToShow: sortedTags.slice(0, i),
|
||||
@@ -68,7 +68,7 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
};
|
||||
}
|
||||
|
||||
// 如果连"更多"标签都放不下,减少一个标签以腾出空间
|
||||
// If there's not enough space for even the "more" tag, reduce one tag to make room
|
||||
return {
|
||||
tagsToShow: sortedTags.slice(0, Math.max(1, i - 1)),
|
||||
hasMore: true,
|
||||
|
||||
@@ -16,7 +16,7 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
// 菜单项配置
|
||||
// Menu item configuration
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
path: '/',
|
||||
|
||||
@@ -98,3 +98,37 @@ export const ToggleGroup: React.FC<ToggleGroupProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SwitchProps {
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Switch: React.FC<SwitchProps> = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500",
|
||||
checked ? "bg-blue-600" : "bg-gray-200",
|
||||
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
)}
|
||||
onClick={() => !disabled && onCheckedChange(!checked)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
|
||||
checked ? "translate-x-6" : "translate-x-1"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -2,16 +2,16 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server, ApiResponse } from '@/types';
|
||||
|
||||
// 配置选项
|
||||
// Configuration options
|
||||
const CONFIG = {
|
||||
// 初始化启动阶段的配置
|
||||
// Initialization phase configuration
|
||||
startup: {
|
||||
maxAttempts: 60, // 初始化阶段最大尝试次数
|
||||
pollingInterval: 3000 // 初始阶段轮询间隔 (3秒)
|
||||
maxAttempts: 60, // Maximum number of attempts during initialization
|
||||
pollingInterval: 3000 // Polling interval during initialization (3 seconds)
|
||||
},
|
||||
// 正常运行阶段的配置
|
||||
// Normal operation phase configuration
|
||||
normal: {
|
||||
pollingInterval: 10000 // 正常运行时的轮询间隔 (10秒)
|
||||
pollingInterval: 10000 // Polling interval during normal operation (10 seconds)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,12 +23,12 @@ export const useServerData = () => {
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [fetchAttempts, setFetchAttempts] = useState(0);
|
||||
|
||||
// 轮询定时器引用
|
||||
// Timer reference for polling
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
// 保存当前尝试次数,避免依赖循环
|
||||
// Track current attempt count to avoid dependency cycles
|
||||
const attemptsRef = useRef<number>(0);
|
||||
|
||||
// 清理定时器
|
||||
// Clear the timer
|
||||
const clearTimer = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
@@ -36,9 +36,9 @@ export const useServerData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 开始正常轮询
|
||||
// Start normal polling
|
||||
const startNormalPolling = useCallback(() => {
|
||||
// 确保没有其他定时器在运行
|
||||
// Ensure no other timers are running
|
||||
clearTimer();
|
||||
|
||||
const fetchServers = async () => {
|
||||
@@ -60,12 +60,12 @@ export const useServerData = () => {
|
||||
setServers([]);
|
||||
}
|
||||
|
||||
// 重置错误状态
|
||||
// Reset error state
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Error fetching servers during normal polling:', err);
|
||||
|
||||
// 使用友好的错误消息
|
||||
// Use friendly error message
|
||||
if (!navigator.onLine) {
|
||||
setError(t('errors.network'));
|
||||
} else if (err instanceof TypeError && (
|
||||
@@ -79,21 +79,21 @@ export const useServerData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行一次
|
||||
// Execute immediately
|
||||
fetchServers();
|
||||
|
||||
// 设置定期轮询
|
||||
// Set up regular polling
|
||||
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
// 重置尝试计数
|
||||
// Reset attempt count
|
||||
if (refreshKey > 0) {
|
||||
attemptsRef.current = 0;
|
||||
setFetchAttempts(0);
|
||||
}
|
||||
|
||||
// 初始化加载阶段的请求函数
|
||||
// Initialization phase request function
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
@@ -104,51 +104,51 @@ export const useServerData = () => {
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// 处理API响应中的包装对象,提取data字段
|
||||
// Handle API response wrapper object, extract data field
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setServers(data.data);
|
||||
setIsInitialLoading(false);
|
||||
// 初始化成功,开始正常轮询
|
||||
// Initialization successful, start normal polling
|
||||
startNormalPolling();
|
||||
return true;
|
||||
} else if (data && Array.isArray(data)) {
|
||||
// 兼容性处理,如果API直接返回数组
|
||||
// Compatibility handling, if API directly returns array
|
||||
setServers(data);
|
||||
setIsInitialLoading(false);
|
||||
// 初始化成功,开始正常轮询
|
||||
// Initialization successful, start normal polling
|
||||
startNormalPolling();
|
||||
return true;
|
||||
} else {
|
||||
// 如果数据格式不符合预期,设置为空数组
|
||||
// If data format is not as expected, set to empty array
|
||||
console.error('Invalid server data format:', data);
|
||||
setServers([]);
|
||||
setIsInitialLoading(false);
|
||||
// 初始化成功但数据为空,开始正常轮询
|
||||
// Initialization successful but data is empty, start normal polling
|
||||
startNormalPolling();
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
// 增加尝试次数计数,使用 ref 避免触发 effect 重新运行
|
||||
// Increment attempt count, use ref to avoid triggering effect rerun
|
||||
attemptsRef.current += 1;
|
||||
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
|
||||
|
||||
// 更新状态用于显示
|
||||
// Update state for display
|
||||
setFetchAttempts(attemptsRef.current);
|
||||
|
||||
// 设置适当的错误消息
|
||||
// Set appropriate error message
|
||||
if (!navigator.onLine) {
|
||||
setError(t('errors.network'));
|
||||
} else {
|
||||
setError(t('errors.initialStartup'));
|
||||
}
|
||||
|
||||
// 如果已超过最大尝试次数,放弃初始化并切换到正常轮询
|
||||
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
|
||||
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
||||
console.log('Maximum startup attempts reached, switching to normal polling');
|
||||
setIsInitialLoading(false);
|
||||
// 清除初始化的轮询
|
||||
// Clear initialization polling
|
||||
clearTimer();
|
||||
// 切换到正常轮询模式
|
||||
// Switch to normal polling mode
|
||||
startNormalPolling();
|
||||
}
|
||||
|
||||
@@ -156,45 +156,45 @@ export const useServerData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时,根据当前状态设置适当的轮询
|
||||
// On component mount, set appropriate polling based on current state
|
||||
if (isInitialLoading) {
|
||||
// 确保没有其他定时器在运行
|
||||
// Ensure no other timers are running
|
||||
clearTimer();
|
||||
|
||||
// 立即执行一次初始请求
|
||||
// Execute initial request immediately
|
||||
fetchInitialData();
|
||||
|
||||
// 设置初始阶段的轮询间隔
|
||||
// Set polling interval for initialization phase
|
||||
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
|
||||
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
|
||||
} else {
|
||||
// 已经初始化完成,开始正常轮询
|
||||
// Initialization completed, start normal polling
|
||||
startNormalPolling();
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
// Cleanup function
|
||||
return () => {
|
||||
clearTimer();
|
||||
};
|
||||
}, [refreshKey, t, isInitialLoading, startNormalPolling]);
|
||||
|
||||
// 手动触发刷新
|
||||
// Manually trigger refresh
|
||||
const triggerRefresh = () => {
|
||||
// 清除当前的定时器
|
||||
// Clear current timer
|
||||
clearTimer();
|
||||
|
||||
// 如果在初始化阶段,重置初始化状态
|
||||
// If in initialization phase, reset initialization state
|
||||
if (isInitialLoading) {
|
||||
setIsInitialLoading(true);
|
||||
attemptsRef.current = 0;
|
||||
setFetchAttempts(0);
|
||||
}
|
||||
|
||||
// refreshKey 的改变会触发 useEffect 再次运行
|
||||
// Change in refreshKey will trigger useEffect to run again
|
||||
setRefreshKey(prevKey => prevKey + 1);
|
||||
};
|
||||
|
||||
// 服务器相关操作
|
||||
// Server related operations
|
||||
const handleServerAdd = () => {
|
||||
setRefreshKey(prevKey => prevKey + 1);
|
||||
};
|
||||
|
||||
131
frontend/src/hooks/useSettingsData.ts
Normal file
131
frontend/src/hooks/useSettingsData.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApiResponse } from '@/types';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
|
||||
// Define types for the settings data
|
||||
interface RoutingConfig {
|
||||
enableGlobalRoute: boolean;
|
||||
enableGroupNameRoute: boolean;
|
||||
}
|
||||
|
||||
interface SystemSettings {
|
||||
systemConfig?: {
|
||||
routing?: RoutingConfig;
|
||||
};
|
||||
}
|
||||
|
||||
export const useSettingsData = () => {
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const [routingConfig, setRoutingConfig] = useState<RoutingConfig>({
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true,
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// Trigger a refresh of the settings data
|
||||
const triggerRefresh = useCallback(() => {
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
// Fetch current settings
|
||||
const fetchSettings = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch('/api/settings', {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ApiResponse<SystemSettings> = await response.json();
|
||||
|
||||
if (data.success && data.data?.systemConfig?.routing) {
|
||||
setRoutingConfig({
|
||||
enableGlobalRoute: data.data.systemConfig.routing.enableGlobalRoute ?? true,
|
||||
enableGroupNameRoute: data.data.systemConfig.routing.enableGroupNameRoute ?? true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to fetch settings');
|
||||
showToast(t('errors.failedToFetchSettings'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t, showToast]);
|
||||
|
||||
// Update routing configuration
|
||||
const updateRoutingConfig = async (key: keyof RoutingConfig, value: boolean) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch('/api/system-config', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
routing: {
|
||||
[key]: value,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setRoutingConfig({
|
||||
...routingConfig,
|
||||
[key]: value,
|
||||
});
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update system config:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to update system config');
|
||||
showToast(t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch settings when the component mounts or refreshKey changes
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, [fetchSettings, refreshKey]);
|
||||
|
||||
return {
|
||||
routingConfig,
|
||||
loading,
|
||||
error,
|
||||
setError,
|
||||
triggerRefresh,
|
||||
fetchSettings,
|
||||
updateRoutingConfig,
|
||||
};
|
||||
};
|
||||
@@ -32,8 +32,8 @@ i18n
|
||||
},
|
||||
|
||||
detection: {
|
||||
// Order of detection; we put 'navigator' first to use browser language
|
||||
order: ['navigator', 'localStorage', 'cookie', 'htmlTag'],
|
||||
// Order of detection; prioritize localStorage to respect user language choice
|
||||
order: ['localStorage', 'cookie', 'htmlTag', 'navigator'],
|
||||
// Cache the language in localStorage
|
||||
caches: ['localStorage', 'cookie'],
|
||||
}
|
||||
|
||||
@@ -83,7 +83,9 @@
|
||||
"serverUpdate": "Failed to edit server {{serverName}}. Please check the server status",
|
||||
"serverFetch": "Failed to retrieve server data. Please try again later",
|
||||
"initialStartup": "The server might be starting up. Please wait a moment as this process can take some time on first launch...",
|
||||
"serverInstall": "Failed to install server"
|
||||
"serverInstall": "Failed to install server",
|
||||
"failedToFetchSettings": "Failed to fetch settings",
|
||||
"failedToUpdateRouteConfig": "Failed to update route configuration"
|
||||
},
|
||||
"common": {
|
||||
"processing": "Processing...",
|
||||
@@ -123,7 +125,8 @@
|
||||
"language": "Language",
|
||||
"account": "Account Settings",
|
||||
"password": "Change Password",
|
||||
"appearance": "Appearance"
|
||||
"appearance": "Appearance",
|
||||
"routeConfig": "Route Configuration"
|
||||
},
|
||||
"market": {
|
||||
"title": "Server Market - (Data from mcpm.sh)"
|
||||
@@ -196,5 +199,12 @@
|
||||
"noInstallationMethod": "No installation method available for this server",
|
||||
"showing": "Showing {{from}}-{{to}} of {{total}} servers",
|
||||
"perPage": "Per page"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "Enable Global Route",
|
||||
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
|
||||
"enableGroupNameRoute": "Enable Group Name Route",
|
||||
"enableGroupNameRouteDescription": "Allow connections to /sse endpoint using group names instead of just group IDs",
|
||||
"systemConfigUpdated": "System configuration updated successfully"
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,9 @@
|
||||
"serverUpdate": "编辑服务器 {{serverName}} 失败,请检查服务器状态",
|
||||
"serverFetch": "获取服务器数据失败,请稍后重试",
|
||||
"initialStartup": "服务器可能正在启动中。首次启动可能需要一些时间,请耐心等候...",
|
||||
"serverInstall": "安装服务器失败"
|
||||
"serverInstall": "安装服务器失败",
|
||||
"failedToFetchSettings": "获取设置失败",
|
||||
"failedToUpdateSystemConfig": "更新系统配置失败"
|
||||
},
|
||||
"common": {
|
||||
"processing": "处理中...",
|
||||
@@ -120,7 +122,8 @@
|
||||
"language": "语言",
|
||||
"account": "账户设置",
|
||||
"password": "修改密码",
|
||||
"appearance": "外观"
|
||||
"appearance": "外观",
|
||||
"routeConfig": "路由配置"
|
||||
},
|
||||
"groups": {
|
||||
"title": "分组管理"
|
||||
@@ -196,5 +199,12 @@
|
||||
"noInstallationMethod": "该服务器没有可用的安装方法",
|
||||
"showing": "显示 {{from}}-{{to}}/{{total}} 个服务器",
|
||||
"perPage": "每页显示"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "启用全局路由",
|
||||
"enableGlobalRouteDescription": "允许不指定分组 ID 就连接到 /sse 端点",
|
||||
"enableGroupNameRoute": "启用分组名称路由",
|
||||
"enableGroupNameRouteDescription": "允许使用分组名称而非分组 ID 连接到 /sse 端点",
|
||||
"systemConfigUpdated": "系统配置更新成功"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const DashboardPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { servers, error, setError, isLoading } = useServerData();
|
||||
|
||||
// 计算服务器统计信息
|
||||
// Calculate server statistics
|
||||
const serverStats = {
|
||||
total: servers.length,
|
||||
online: servers.filter(server => server.status === 'connected').length,
|
||||
@@ -22,7 +22,7 @@ const DashboardPage: React.FC = () => {
|
||||
connecting: 'status.connecting'
|
||||
}
|
||||
|
||||
// 计算各状态百分比(用于仪表板展示)
|
||||
// Calculate percentage for each status (for dashboard display)
|
||||
const getStatusPercentage = (status: ServerStatus) => {
|
||||
if (servers.length === 0) return 0;
|
||||
return Math.round((servers.filter(server => server.status === status).length / servers.length) * 100);
|
||||
@@ -64,7 +64,7 @@ const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{/* 服务器总数 */}
|
||||
{/* Total servers */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100 text-blue-800">
|
||||
@@ -79,7 +79,7 @@ const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 在线服务器 */}
|
||||
{/* Online servers */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-green-100 text-green-800">
|
||||
@@ -100,7 +100,7 @@ const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 离线服务器 */}
|
||||
{/* Offline servers */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-red-100 text-red-800">
|
||||
@@ -121,7 +121,7 @@ const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 连接中服务器 */}
|
||||
{/* Connecting servers */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-yellow-100 text-yellow-800">
|
||||
@@ -144,7 +144,7 @@ const DashboardPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 最近活动列表 */}
|
||||
{/* Recent activity list */}
|
||||
{servers.length > 0 && !isLoading && (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('pages.dashboard.recentServers')}</h2>
|
||||
|
||||
@@ -193,7 +193,7 @@ const MarketPage: React.FC = () => {
|
||||
<div className="md:w-48 flex-shrink-0">
|
||||
<div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4">
|
||||
{/* Categories */}
|
||||
{categories.length > 0 && (
|
||||
{categories.length > 0 ? (
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
@@ -218,6 +218,26 @@ const MarketPage: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center py-4">
|
||||
<svg className="animate-spin h-6 w-6 text-blue-500 mb-2" 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"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
<p className="text-sm text-gray-600">{t('app.loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-6">
|
||||
<div className="mb-3">
|
||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
|
||||
@@ -16,9 +16,11 @@ const ServersPage: React.FC = () => {
|
||||
handleServerAdd,
|
||||
handleServerEdit,
|
||||
handleServerRemove,
|
||||
handleServerToggle
|
||||
handleServerToggle,
|
||||
triggerRefresh
|
||||
} = useServerData();
|
||||
const [editingServer, setEditingServer] = useState<Server | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const handleEditClick = async (server: Server) => {
|
||||
const fullServerData = await handleServerEdit(server);
|
||||
@@ -31,6 +33,17 @@ const ServersPage: React.FC = () => {
|
||||
setEditingServer(null);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
triggerRefresh();
|
||||
// Add a slight delay to make the spinner visible
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
@@ -38,12 +51,20 @@ const ServersPage: React.FC = () => {
|
||||
<div className="flex space-x-4">
|
||||
<AddServerForm onAdd={handleServerAdd} />
|
||||
<button
|
||||
onClick={() => handleServerAdd()}
|
||||
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className={`px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center ${isRefreshing ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<svg className="animate-spin h-4 w-4 mr-2" 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"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
{t('common.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,43 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ChangePasswordForm from '@/components/ChangePasswordForm';
|
||||
import { Switch } from '@/components/ui/ToggleGroup';
|
||||
import { useSettingsData } from '@/hooks/useSettingsData';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useToast();
|
||||
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
|
||||
|
||||
// Update current language when it changes
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(i18n.language);
|
||||
}, [i18n.language]);
|
||||
|
||||
const {
|
||||
routingConfig,
|
||||
loading,
|
||||
updateRoutingConfig
|
||||
} = useSettingsData();
|
||||
|
||||
const [sectionsVisible, setSectionsVisible] = useState({
|
||||
routingConfig: false,
|
||||
password: false
|
||||
});
|
||||
|
||||
const toggleSection = (section: 'routingConfig' | 'password') => {
|
||||
setSectionsVisible(prev => ({
|
||||
...prev,
|
||||
[section]: !prev[section]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute', value: boolean) => {
|
||||
await updateRoutingConfig(key, value);
|
||||
};
|
||||
|
||||
const handlePasswordChangeSuccess = () => {
|
||||
setTimeout(() => {
|
||||
@@ -13,42 +45,104 @@ const SettingsPage: React.FC = () => {
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleLanguageChange = (lang: string) => {
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-8">{t('pages.settings.title')}</h1>
|
||||
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('auth.changePassword')}</h2>
|
||||
<div className="max-w-lg">
|
||||
<ChangePasswordForm onSuccess={handlePasswordChangeSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 其他设置可以在这里添加 */}
|
||||
<div className="bg-white shadow rounded-lg p-6 mt-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('pages.settings.language')}</h2>
|
||||
<div className="flex space-x-4">
|
||||
{/* Language Settings */}
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{t('pages.settings.language')}</h2>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200"
|
||||
onClick={() => {
|
||||
localStorage.setItem('i18nextLng', 'en');
|
||||
window.location.reload();
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
|
||||
currentLanguage.startsWith('en')
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
|
||||
}`}
|
||||
onClick={() => handleLanguageChange('en')}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200"
|
||||
onClick={() => {
|
||||
localStorage.setItem('i18nextLng', 'zh');
|
||||
window.location.reload();
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
|
||||
currentLanguage.startsWith('zh')
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
|
||||
}`}
|
||||
onClick={() => handleLanguageChange('zh')}
|
||||
>
|
||||
中文
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Route Configuration Settings */}
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleSection('routingConfig')}
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-800">{t('pages.settings.routeConfig')}</h2>
|
||||
<span className="text-gray-500">
|
||||
{sectionsVisible.routingConfig ? '▼' : '►'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{sectionsVisible.routingConfig && (
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.enableGlobalRouteDescription')}</p>
|
||||
</div>
|
||||
<Switch
|
||||
disabled={loading}
|
||||
checked={routingConfig.enableGlobalRoute}
|
||||
onCheckedChange={(checked) => handleRoutingConfigChange('enableGlobalRoute', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-700">{t('settings.enableGroupNameRoute')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.enableGroupNameRouteDescription')}</p>
|
||||
</div>
|
||||
<Switch
|
||||
disabled={loading}
|
||||
checked={routingConfig.enableGroupNameRoute}
|
||||
onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Change Password */}
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleSection('password')}
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-800">{t('auth.changePassword')}</h2>
|
||||
<span className="text-gray-500">
|
||||
{sectionsVisible.password ? '▼' : '►'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{sectionsVisible.password && (
|
||||
<div className="max-w-lg mt-4">
|
||||
<ChangePasswordForm onSuccess={handlePasswordChangeSuccess} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
import {
|
||||
getAllGroups,
|
||||
getGroupById,
|
||||
getGroupByIdOrName,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
updateGroupServers,
|
||||
@@ -41,7 +41,7 @@ export const getGroup = (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = getGroupById(id);
|
||||
const group = getGroupByIdOrName(id);
|
||||
if (!group) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
@@ -318,7 +318,7 @@ export const getGroupServers = (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = getGroupById(id);
|
||||
const group = getGroupByIdOrName(id);
|
||||
if (!group) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
notifyToolChanged,
|
||||
toggleServerStatus,
|
||||
} from '../services/mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
import { loadSettings, saveSettings } from '../config/index.js';
|
||||
|
||||
export const getAllServers = (_: Request, res: Response): void => {
|
||||
try {
|
||||
@@ -244,3 +244,60 @@ export const toggleServer = async (req: Request, res: Response): Promise<void> =
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSystemConfig = (req: Request, res: Response): void => {
|
||||
try {
|
||||
const { routing } = req.body;
|
||||
|
||||
if (!routing || (typeof routing.enableGlobalRoute !== 'boolean' && typeof routing.enableGroupNameRoute !== 'boolean')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid system configuration provided',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = loadSettings();
|
||||
if (!settings.systemConfig) {
|
||||
settings.systemConfig = {
|
||||
routing: {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!settings.systemConfig.routing) {
|
||||
settings.systemConfig.routing = {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof routing.enableGlobalRoute === 'boolean') {
|
||||
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
|
||||
}
|
||||
|
||||
if (typeof routing.enableGroupNameRoute === 'boolean') {
|
||||
settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute;
|
||||
}
|
||||
|
||||
if (saveSettings(settings)) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings.systemConfig,
|
||||
message: 'System configuration updated successfully',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to save system configuration',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
updateServer,
|
||||
deleteServer,
|
||||
toggleServer,
|
||||
updateSystemConfig
|
||||
} from '../controllers/serverController.js';
|
||||
import {
|
||||
getGroups,
|
||||
@@ -46,6 +47,7 @@ export const initRoutes = (app: express.Application): void => {
|
||||
router.put('/servers/:name', updateServer);
|
||||
router.delete('/servers/:name', deleteServer);
|
||||
router.post('/servers/:name/toggle', toggleServer);
|
||||
router.put('/system-config', updateSystemConfig);
|
||||
|
||||
// Group management routes
|
||||
router.get('/groups', getGroups);
|
||||
|
||||
@@ -32,7 +32,7 @@ export class AppServer {
|
||||
initMcpServer(config.mcpHubName, config.mcpHubVersion)
|
||||
.then(() => {
|
||||
console.log('MCP server initialized successfully');
|
||||
this.app.get('/sse/:groupId?', (req, res) => handleSseConnection(req, res));
|
||||
this.app.get('/sse/:group?', (req, res) => handleSseConnection(req, res));
|
||||
this.app.post('/messages', handleSseMessage);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -9,10 +9,19 @@ export const getAllGroups = (): IGroup[] => {
|
||||
return settings.groups || [];
|
||||
};
|
||||
|
||||
// Get group by ID
|
||||
export const getGroupById = (id: string): IGroup | undefined => {
|
||||
// Get group by ID or name
|
||||
export const getGroupByIdOrName = (key: string): IGroup | undefined => {
|
||||
const settings = loadSettings();
|
||||
const routingConfig = settings.systemConfig?.routing || {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true,
|
||||
};
|
||||
const groups = getAllGroups();
|
||||
return groups.find((group) => group.id === id);
|
||||
return (
|
||||
groups.find(
|
||||
(group) => group.id === key || (group.name === key && routingConfig.enableGroupNameRoute),
|
||||
) || undefined
|
||||
);
|
||||
};
|
||||
|
||||
// Create a new group
|
||||
@@ -218,6 +227,6 @@ export const removeServerFromGroup = (groupId: string, serverName: string): IGro
|
||||
|
||||
// Get all servers in a group
|
||||
export const getServersInGroup = (groupId: string): string[] => {
|
||||
const group = getGroupById(groupId);
|
||||
const group = getGroupByIdOrName(groupId);
|
||||
return group ? group.servers : [];
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ServerInfo, ServerConfig } from '../types/index.js';
|
||||
import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
|
||||
import config from '../config/index.js';
|
||||
import { get } from 'http';
|
||||
import { getGroupId } from './sseService.js';
|
||||
import { getGroup } from './sseService.js';
|
||||
import { getServersInGroup } from './groupService.js';
|
||||
|
||||
let currentServer: Server;
|
||||
@@ -316,12 +316,12 @@ export const createMcpServer = (name: string, version: string): Server => {
|
||||
const server = new Server({ name, version }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(ListToolsRequestSchema, async (_, extra) => {
|
||||
const sessionId = extra.sessionId || '';
|
||||
const groupId = getGroupId(sessionId);
|
||||
console.log(`Handling ListToolsRequest for groupId: ${groupId}`);
|
||||
const group = getGroup(sessionId);
|
||||
console.log(`Handling ListToolsRequest for group: ${group}`);
|
||||
const allServerInfos = serverInfos.filter((serverInfo) => {
|
||||
if (serverInfo.enabled === false) return false;
|
||||
if (!groupId) return true;
|
||||
const serversInGroup = getServersInGroup(groupId);
|
||||
if (!group) return true;
|
||||
const serversInGroup = getServersInGroup(group);
|
||||
return serversInGroup.includes(serverInfo.name);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,33 +1,48 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { getMcpServer } from './mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
|
||||
const transports: { [sessionId: string]: { transport: SSEServerTransport; groupId: string } } = {};
|
||||
const transports: { [sessionId: string]: { transport: SSEServerTransport; group: string } } = {};
|
||||
|
||||
export const getGroupId = (sessionId: string): string => {
|
||||
return transports[sessionId]?.groupId || '';
|
||||
export const getGroup = (sessionId: string): string => {
|
||||
return transports[sessionId]?.group || '';
|
||||
};
|
||||
|
||||
export const handleSseConnection = async (req: Request, res: Response): Promise<void> => {
|
||||
const settings = loadSettings();
|
||||
const routingConfig = settings.systemConfig?.routing || {
|
||||
enableGlobalRoute: true,
|
||||
enableGroupNameRoute: true,
|
||||
};
|
||||
const group = req.params.group;
|
||||
|
||||
// Check if this is a global route (no group) and if it's allowed
|
||||
if (!group && !routingConfig.enableGlobalRoute) {
|
||||
res.status(403).send('Global routes are disabled. Please specify a group ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
const groupId = req.params.groupId;
|
||||
transports[transport.sessionId] = { transport, groupId };
|
||||
transports[transport.sessionId] = { transport, group: group };
|
||||
|
||||
res.on('close', () => {
|
||||
delete transports[transport.sessionId];
|
||||
console.log(`SSE connection closed: ${transport.sessionId}`);
|
||||
});
|
||||
|
||||
console.log(`New SSE connection established: ${transport.sessionId}`);
|
||||
console.log(
|
||||
`New SSE connection established: ${transport.sessionId} with group: ${group || 'global'}`,
|
||||
);
|
||||
await getMcpServer().connect(transport);
|
||||
};
|
||||
|
||||
export const handleSseMessage = async (req: Request, res: Response): Promise<void> => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const { transport, groupId } = transports[sessionId];
|
||||
req.params.groupId = groupId;
|
||||
req.query.groupId = groupId;
|
||||
console.log(`Received message for sessionId: ${sessionId} in groupId: ${groupId}`);
|
||||
const { transport, group } = transports[sessionId];
|
||||
req.params.group = group;
|
||||
req.query.group = group;
|
||||
console.log(`Received message for sessionId: ${sessionId} in group: ${group}`);
|
||||
if (transport) {
|
||||
await transport.handlePostMessage(req, res);
|
||||
} else {
|
||||
|
||||
@@ -78,6 +78,13 @@ export interface McpSettings {
|
||||
[key: string]: ServerConfig; // Key-value pairs of server names and their configurations
|
||||
};
|
||||
groups?: IGroup[]; // Array of server groups
|
||||
systemConfig?: {
|
||||
routing?: {
|
||||
enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled
|
||||
enableGroupNameRoute?: boolean; // Controls whether group routing by name is allowed
|
||||
};
|
||||
// Add other system configuration sections here in the future
|
||||
};
|
||||
}
|
||||
|
||||
// Configuration details for an individual server
|
||||
|
||||
Reference in New Issue
Block a user