mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-30 13:39:20 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9cbc5381a | ||
|
|
56c6447469 | ||
|
|
f8149c4b0b | ||
|
|
e259f30539 |
@@ -20,6 +20,6 @@
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-undef": "off",
|
||||
"no-undef": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import { MarketServer, MarketServerInstallation } from '@/types';
|
||||
import ServerForm from './ServerForm';
|
||||
|
||||
import { ServerConfig } from '@/types';
|
||||
|
||||
interface MarketServerDetailProps {
|
||||
server: MarketServer;
|
||||
onBack: () => void;
|
||||
onInstall: (server: MarketServer) => void;
|
||||
onInstall: (server: MarketServer, config: ServerConfig) => void;
|
||||
installing?: boolean;
|
||||
isInstalled?: boolean;
|
||||
}
|
||||
@@ -83,8 +85,8 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
setError(null);
|
||||
// Pass the server object to the parent component for installation
|
||||
onInstall(server);
|
||||
// Pass the server object and the payload (includes env changes) for installation
|
||||
onInstall(server, payload.config);
|
||||
setModalVisible(false);
|
||||
} catch (err) {
|
||||
console.error('Error installing server:', err);
|
||||
@@ -294,4 +296,4 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketServerDetail;
|
||||
export default MarketServerDetail;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MarketServer, ApiResponse } from '@/types';
|
||||
import { MarketServer, ApiResponse, ServerConfig } from '@/types';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
export const useMarketData = () => {
|
||||
@@ -347,7 +347,7 @@ export const useMarketData = () => {
|
||||
|
||||
// Install server to the local environment
|
||||
const installServer = useCallback(
|
||||
async (server: MarketServer) => {
|
||||
async (server: MarketServer, customConfig: ServerConfig) => {
|
||||
try {
|
||||
const installType = server.installations?.npm
|
||||
? 'npm'
|
||||
@@ -362,13 +362,13 @@ export const useMarketData = () => {
|
||||
|
||||
const installation = server.installations[installType];
|
||||
|
||||
// Prepare server configuration
|
||||
// Prepare server configuration, merging with customConfig
|
||||
const serverConfig = {
|
||||
name: server.name,
|
||||
config: {
|
||||
command: installation.command,
|
||||
args: installation.args,
|
||||
env: installation.env || {},
|
||||
command: customConfig.command || installation.command || '',
|
||||
args: customConfig.args || installation.args || [],
|
||||
env: { ...installation.env, ...customConfig.env },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { MarketServer } from '@/types';
|
||||
import { MarketServer, ServerConfig } from '@/types';
|
||||
import { useMarketData } from '@/hooks/useMarketData';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import MarketServerCard from '@/components/MarketServerCard';
|
||||
@@ -90,10 +90,11 @@ const MarketPage: React.FC = () => {
|
||||
navigate('/market');
|
||||
};
|
||||
|
||||
const handleInstall = async (server: MarketServer) => {
|
||||
const handleInstall = async (server: MarketServer, config: ServerConfig) => {
|
||||
try {
|
||||
setInstalling(true);
|
||||
const success = await installServer(server);
|
||||
// Pass the server object and the config to the installServer function
|
||||
const success = await installServer(server, config);
|
||||
if (success) {
|
||||
// Show success message using toast instead of alert
|
||||
showToast(t('market.installSuccess', { serverName: server.display_name }), 'success');
|
||||
@@ -353,4 +354,4 @@ const MarketPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketPage;
|
||||
export default MarketPage;
|
||||
|
||||
@@ -15,18 +15,37 @@ const defaultConfig = {
|
||||
mcpHubVersion: getPackageVersion(),
|
||||
};
|
||||
|
||||
// Settings cache
|
||||
let settingsCache: McpSettings | null = null;
|
||||
|
||||
export const getSettingsPath = (): string => {
|
||||
return getConfigFilePath('mcp_settings.json', 'Settings');
|
||||
};
|
||||
|
||||
export const loadSettings = (): McpSettings => {
|
||||
// If cache exists, return cached data directly
|
||||
if (settingsCache) {
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
const settingsPath = getSettingsPath();
|
||||
try {
|
||||
const settingsData = fs.readFileSync(settingsPath, 'utf8');
|
||||
return JSON.parse(settingsData);
|
||||
const settings = JSON.parse(settingsData);
|
||||
|
||||
// Update cache
|
||||
settingsCache = settings;
|
||||
|
||||
console.log(`Loaded settings from ${settingsPath}:`, settings);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load settings from ${settingsPath}:`, error);
|
||||
return { mcpServers: {}, users: [] };
|
||||
const defaultSettings = { mcpServers: {}, users: [] };
|
||||
|
||||
// Cache default settings
|
||||
settingsCache = defaultSettings;
|
||||
|
||||
return defaultSettings;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,6 +53,10 @@ export const saveSettings = (settings: McpSettings): boolean => {
|
||||
const settingsPath = getSettingsPath();
|
||||
try {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
||||
|
||||
// Update cache after successful save
|
||||
settingsCache = settings;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to save settings to ${settingsPath}:`, error);
|
||||
@@ -41,6 +64,22 @@ export const saveSettings = (settings: McpSettings): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear settings cache, force next loadSettings call to re-read from file
|
||||
*/
|
||||
export const clearSettingsCache = (): void => {
|
||||
settingsCache = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current cache status (for debugging)
|
||||
*/
|
||||
export const getSettingsCacheInfo = (): { hasCache: boolean } => {
|
||||
return {
|
||||
hasCache: settingsCache !== null,
|
||||
};
|
||||
};
|
||||
|
||||
export const replaceEnvVars = (env: Record<string, any>): Record<string, any> => {
|
||||
const res: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
|
||||
@@ -107,6 +107,11 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default keep-alive interval for SSE servers if not specified
|
||||
if ((config.type === 'sse' || (!config.type && config.url)) && !config.keepAliveInterval) {
|
||||
config.keepAliveInterval = 60000; // Default 60 seconds for SSE servers
|
||||
}
|
||||
|
||||
const result = await addServer(name, config);
|
||||
if (result.success) {
|
||||
notifyToolChanged();
|
||||
@@ -224,6 +229,11 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default keep-alive interval for SSE servers if not specified
|
||||
if ((config.type === 'sse' || (!config.type && config.url)) && !config.keepAliveInterval) {
|
||||
config.keepAliveInterval = 60000; // Default 60 seconds for SSE servers
|
||||
}
|
||||
|
||||
const result = await updateMcpServer(name, config);
|
||||
if (result.success) {
|
||||
notifyToolChanged();
|
||||
|
||||
@@ -13,6 +13,38 @@ import { saveToolsAsVectorEmbeddings, searchToolsByVector } from './vectorSearch
|
||||
|
||||
const servers: { [sessionId: string]: Server } = {};
|
||||
|
||||
// Helper function to set up keep-alive ping for SSE connections
|
||||
const setupKeepAlive = (serverInfo: ServerInfo, serverConfig: ServerConfig): void => {
|
||||
// Only set up keep-alive for SSE connections
|
||||
if (!(serverInfo.transport instanceof SSEClientTransport)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing interval first
|
||||
if (serverInfo.keepAliveIntervalId) {
|
||||
clearInterval(serverInfo.keepAliveIntervalId);
|
||||
}
|
||||
|
||||
// Use configured interval or default to 60 seconds for SSE
|
||||
const interval = serverConfig.keepAliveInterval || 60000;
|
||||
|
||||
serverInfo.keepAliveIntervalId = setInterval(async () => {
|
||||
try {
|
||||
if (serverInfo.client && serverInfo.status === 'connected') {
|
||||
await serverInfo.client.ping();
|
||||
console.log(`Keep-alive ping successful for server: ${serverInfo.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Keep-alive ping failed for server ${serverInfo.name}:`, error);
|
||||
// TODO Consider handling reconnection logic here if needed
|
||||
}
|
||||
}, interval);
|
||||
|
||||
console.log(
|
||||
`Keep-alive ping set up for server ${serverInfo.name} with interval ${interval / 1000} seconds`,
|
||||
);
|
||||
};
|
||||
|
||||
export const initUpstreamServers = async (): Promise<void> => {
|
||||
await registerAllTools(true);
|
||||
};
|
||||
@@ -210,6 +242,9 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
serverInfo.status = 'connected';
|
||||
serverInfo.error = null;
|
||||
|
||||
// Set up keep-alive ping for SSE connections
|
||||
setupKeepAlive(serverInfo, conf);
|
||||
|
||||
// Save tools as vector embeddings for search
|
||||
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
||||
})
|
||||
@@ -389,6 +424,13 @@ export const updateMcpServer = async (
|
||||
function closeServer(name: string) {
|
||||
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
|
||||
if (serverInfo && serverInfo.client && serverInfo.transport) {
|
||||
// Clear keep-alive interval if exists
|
||||
if (serverInfo.keepAliveIntervalId) {
|
||||
clearInterval(serverInfo.keepAliveIntervalId);
|
||||
serverInfo.keepAliveIntervalId = undefined;
|
||||
console.log(`Cleared keep-alive interval for server: ${serverInfo.name}`);
|
||||
}
|
||||
|
||||
serverInfo.client.close();
|
||||
serverInfo.transport.close();
|
||||
console.log(`Closed client and transport for server: ${serverInfo.name}`);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
||||
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { deleteMcpServer, getMcpServer } from './mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
import config from '../config/index.js';
|
||||
|
||||
const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
|
||||
|
||||
@@ -58,7 +59,7 @@ export const handleSseConnection = async (req: Request, res: Response): Promise<
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
const transport = new SSEServerTransport(`${config.basePath}/messages`, res);
|
||||
transports[transport.sessionId] = { transport, group: group };
|
||||
|
||||
res.on('close', () => {
|
||||
@@ -108,7 +109,10 @@ export const handleSseMessage = async (req: Request, res: Response): Promise<voi
|
||||
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
const group = req.params.group;
|
||||
console.log(`Handling MCP post request for sessionId: ${sessionId} and group: ${group}`);
|
||||
const body = req.body;
|
||||
console.log(
|
||||
`Handling MCP post request for sessionId: ${sessionId} and group: ${group} with body: ${JSON.stringify(body)}`,
|
||||
);
|
||||
// Check bearer auth
|
||||
if (!validateBearerAuth(req)) {
|
||||
res.status(401).send('Bearer authentication required or invalid token');
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface ServerConfig {
|
||||
env?: Record<string, string>; // Environment variables
|
||||
headers?: Record<string, string>; // HTTP headers for SSE/streamable-http servers
|
||||
enabled?: boolean; // Flag to enable/disable the server
|
||||
keepAliveInterval?: number; // Keep-alive ping interval in milliseconds (default: 60000ms for SSE servers)
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
}
|
||||
|
||||
@@ -118,6 +119,7 @@ export interface ServerInfo {
|
||||
transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used
|
||||
createTime: number; // Timestamp of when the server was created
|
||||
enabled?: boolean; // Flag to indicate if the server is enabled
|
||||
keepAliveIntervalId?: NodeJS.Timeout; // Timer ID for keep-alive ping interval
|
||||
}
|
||||
|
||||
// Details about a tool available on the server
|
||||
|
||||
Reference in New Issue
Block a user