mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: add request options configuration to server form (#171)
This commit is contained in:
@@ -41,7 +41,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
args: (initialData && initialData.config && initialData.config.args) || [],
|
||||
type: getInitialServerType(), // Initialize the type field
|
||||
env: [],
|
||||
headers: []
|
||||
headers: [],
|
||||
options: {
|
||||
timeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.timeout) || 60000,
|
||||
resetTimeoutOnProgress: (initialData && initialData.config && initialData.config.options && initialData.config.options.resetTimeoutOnProgress) || false,
|
||||
maxTotalTimeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.maxTotalTimeout) || undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
||||
@@ -56,6 +61,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
: [],
|
||||
)
|
||||
|
||||
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const isEdit = !!initialData
|
||||
|
||||
@@ -66,7 +72,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
|
||||
// Transform space-separated arguments string into array
|
||||
const handleArgsChange = (value: string) => {
|
||||
let args = value.split(' ').filter((arg) => arg.trim() !== '')
|
||||
const args = value.split(' ').filter((arg) => arg.trim() !== '')
|
||||
setFormData({ ...formData, arguments: value, args })
|
||||
}
|
||||
|
||||
@@ -107,6 +113,17 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
setHeaderVars(newHeaderVars)
|
||||
}
|
||||
|
||||
// Handle options changes
|
||||
const handleOptionsChange = (field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout', value: number | boolean | undefined) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
options: {
|
||||
...prev.options,
|
||||
[field]: value
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Submit handler for server configuration
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -127,6 +144,18 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
}
|
||||
})
|
||||
|
||||
// Prepare options object, only include defined values
|
||||
const options: any = {}
|
||||
if (formData.options?.timeout && formData.options.timeout !== 60000) {
|
||||
options.timeout = formData.options.timeout
|
||||
}
|
||||
if (formData.options?.resetTimeoutOnProgress) {
|
||||
options.resetTimeoutOnProgress = formData.options.resetTimeoutOnProgress
|
||||
}
|
||||
if (formData.options?.maxTotalTimeout) {
|
||||
options.maxTotalTimeout = formData.options.maxTotalTimeout
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
config: {
|
||||
@@ -141,7 +170,8 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
args: formData.args,
|
||||
env: Object.keys(env).length > 0 ? env : undefined,
|
||||
}
|
||||
)
|
||||
),
|
||||
...(Object.keys(options).length > 0 ? { options } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +395,75 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Request Options Configuration */}
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer bg-gray-50 hover:bg-gray-100 p-3 rounded border"
|
||||
onClick={() => setIsRequestOptionsExpanded(!isRequestOptionsExpanded)}
|
||||
>
|
||||
<label className="text-gray-700 text-sm font-bold">
|
||||
{t('server.requestOptions')}
|
||||
</label>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{isRequestOptionsExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isRequestOptionsExpanded && (
|
||||
<div className="border rounded-b p-4 bg-gray-50 border-t-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="timeout">
|
||||
{t('server.timeout')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="timeout"
|
||||
value={formData.options?.timeout || 60000}
|
||||
onChange={(e) => handleOptionsChange('timeout', parseInt(e.target.value) || 60000)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="30000"
|
||||
min="1000"
|
||||
max="300000"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.timeoutDescription')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="maxTotalTimeout">
|
||||
{t('server.maxTotalTimeout')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxTotalTimeout"
|
||||
value={formData.options?.maxTotalTimeout || ''}
|
||||
onChange={(e) => handleOptionsChange('maxTotalTimeout', e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder="Optional"
|
||||
min="1000"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.maxTotalTimeoutDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.options?.resetTimeoutOnProgress || false}
|
||||
onChange={(e) => handleOptionsChange('resetTimeoutOnProgress', e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-gray-600 text-sm">{t('server.resetTimeoutOnProgress')}</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-6">
|
||||
{t('server.resetTimeoutOnProgressDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"enabled": "Enabled",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"requestOptions": "Configuration",
|
||||
"timeout": "Request Timeout",
|
||||
"timeoutDescription": "Timeout for requests to the MCP server (ms)",
|
||||
"maxTotalTimeout": "Maximum Total Timeout",
|
||||
"maxTotalTimeoutDescription": "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||||
"resetTimeoutOnProgress": "Reset Timeout on Progress",
|
||||
"resetTimeoutOnProgressDescription": "Reset timeout on progress notifications",
|
||||
"remove": "Remove",
|
||||
"toggleError": "Failed to toggle server {{serverName}}",
|
||||
"alreadyExists": "Server {{serverName}} already exists",
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
"enabled": "已启用",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"requestOptions": "配置",
|
||||
"timeout": "请求超时",
|
||||
"timeoutDescription": "请求超时时间(毫秒)",
|
||||
"maxTotalTimeout": "最大总超时",
|
||||
"maxTotalTimeoutDescription": "无论是否有进度通知的最大总超时时间(毫秒)",
|
||||
"resetTimeoutOnProgress": "收到进度通知时重置超时",
|
||||
"resetTimeoutOnProgressDescription": "适用于发送周期性进度更新的长时间运行操作",
|
||||
"remove": "移除",
|
||||
"toggleError": "切换服务器 {{serverName}} 状态失败",
|
||||
"alreadyExists": "服务器 {{serverName}} 已经存在",
|
||||
|
||||
@@ -80,6 +80,11 @@ export interface ServerConfig {
|
||||
headers?: Record<string, string>;
|
||||
enabled?: boolean;
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
options?: {
|
||||
timeout?: number; // Request timeout in milliseconds
|
||||
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
|
||||
maxTotalTimeout?: number; // Maximum total timeout in milliseconds
|
||||
}; // MCP request options configuration
|
||||
}
|
||||
|
||||
// Server types
|
||||
@@ -116,6 +121,11 @@ export interface ServerFormData {
|
||||
type?: 'stdio' | 'sse' | 'streamable-http'; // Added type field
|
||||
env: EnvVar[];
|
||||
headers: EnvVar[];
|
||||
options?: {
|
||||
timeout?: number;
|
||||
resetTimeoutOnProgress?: boolean;
|
||||
maxTotalTimeout?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Group form data types
|
||||
|
||||
@@ -36,7 +36,7 @@ export const loadSettings = (): McpSettings => {
|
||||
// Update cache
|
||||
settingsCache = settings;
|
||||
|
||||
console.log(`Loaded settings from ${settingsPath}:`, settings);
|
||||
console.log(`Loaded settings from ${settingsPath}`);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load settings from ${settingsPath}:`, error);
|
||||
|
||||
@@ -1,37 +1,8 @@
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import fs from 'fs';
|
||||
import { auth } from './auth.js';
|
||||
import { initializeDefaultUser } from '../models/User.js';
|
||||
import config from '../config/index.js';
|
||||
|
||||
// Create __dirname equivalent for ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Try to find the correct frontend file path
|
||||
const findFrontendPath = (): string => {
|
||||
// First try development environment path
|
||||
const devPath = path.join(dirname(__dirname), 'frontend', 'dist', 'index.html');
|
||||
if (fs.existsSync(devPath)) {
|
||||
return path.join(dirname(__dirname), 'frontend', 'dist');
|
||||
}
|
||||
|
||||
// Try npm/npx installed path (remove /dist directory)
|
||||
const npmPath = path.join(dirname(dirname(__dirname)), 'frontend', 'dist', 'index.html');
|
||||
if (fs.existsSync(npmPath)) {
|
||||
return path.join(dirname(dirname(__dirname)), 'frontend', 'dist');
|
||||
}
|
||||
|
||||
// If none of the above paths exist, return the most reasonable default path and log a warning
|
||||
console.warn('Warning: Could not locate frontend files. Using default path.');
|
||||
return path.join(dirname(__dirname), 'frontend', 'dist');
|
||||
};
|
||||
|
||||
const frontendPath = findFrontendPath();
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
_req: Request,
|
||||
@@ -52,6 +23,7 @@ export const initMiddlewares = (app: express.Application): void => {
|
||||
app.use((req, res, next) => {
|
||||
const basePath = config.basePath;
|
||||
// Only apply JSON parsing for API and auth routes, not for SSE or message endpoints
|
||||
// TODO exclude sse responses by mcp endpoint
|
||||
if (
|
||||
req.path !== `${basePath}/sse` &&
|
||||
!req.path.startsWith(`${basePath}/sse/`) &&
|
||||
|
||||
@@ -218,14 +218,27 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
},
|
||||
},
|
||||
);
|
||||
const timeout = isInit ? Number(config.initTimeout) : Number(config.timeout);
|
||||
|
||||
const initRequestOptions = isInit
|
||||
? {
|
||||
timeout: Number(config.initTimeout) || 60000,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Get request options from server configuration, with fallbacks
|
||||
const serverRequestOptions = conf.options || {};
|
||||
const requestOptions = {
|
||||
timeout: serverRequestOptions.timeout || 60000,
|
||||
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
|
||||
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
||||
};
|
||||
|
||||
client
|
||||
.connect(transport, { timeout: timeout })
|
||||
.connect(transport, initRequestOptions || requestOptions)
|
||||
.then(() => {
|
||||
console.log(`Successfully connected client for server: ${name}`);
|
||||
|
||||
client
|
||||
.listTools({}, { timeout: timeout })
|
||||
.listTools({}, initRequestOptions || requestOptions)
|
||||
.then((tools) => {
|
||||
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
||||
const serverInfo = getServerByName(name);
|
||||
@@ -276,6 +289,7 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
tools: [],
|
||||
client,
|
||||
transport,
|
||||
options: requestOptions,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
console.log(`Initialized client for server: ${name}`);
|
||||
@@ -696,14 +710,12 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
|
||||
// Special handling for call_tool
|
||||
if (request.params.name === 'call_tool') {
|
||||
let { toolName, arguments: toolArgs = {} } = request.params.arguments || {};
|
||||
|
||||
let { toolName } = request.params.arguments || {};
|
||||
if (!toolName) {
|
||||
throw new Error('toolName parameter is required');
|
||||
}
|
||||
|
||||
// arguments parameter is now optional
|
||||
|
||||
const { arguments: toolArgs = {} } = request.params.arguments || {};
|
||||
let targetServerInfo: ServerInfo | undefined;
|
||||
if (extra && extra.server) {
|
||||
targetServerInfo = getServerByName(extra.server);
|
||||
@@ -744,10 +756,14 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
toolName = toolName.startsWith(`${targetServerInfo.name}-`)
|
||||
? toolName.replace(`${targetServerInfo.name}-`, '')
|
||||
: toolName;
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: finalArgs,
|
||||
});
|
||||
const result = await client.callTool(
|
||||
{
|
||||
name: toolName,
|
||||
arguments: finalArgs,
|
||||
},
|
||||
undefined,
|
||||
targetServerInfo.options || {},
|
||||
);
|
||||
|
||||
console.log(`Tool invocation result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
@@ -766,7 +782,7 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
request.params.name = request.params.name.startsWith(`${serverInfo.name}-`)
|
||||
? request.params.name.replace(`${serverInfo.name}-`, '')
|
||||
: request.params.name;
|
||||
const result = await client.callTool(request.params);
|
||||
const result = await client.callTool(request.params, undefined, serverInfo.options || {});
|
||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
||||
import { SmartRoutingConfig } from '../utils/smartRouting.js';
|
||||
|
||||
// User interface
|
||||
@@ -107,6 +108,7 @@ export interface ServerConfig {
|
||||
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
|
||||
options?: Partial<Pick<RequestOptions, 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout'>>; // MCP request options configuration
|
||||
}
|
||||
|
||||
// Information about a server's status and tools
|
||||
@@ -117,6 +119,7 @@ export interface ServerInfo {
|
||||
tools: ToolInfo[]; // List of tools available on the server
|
||||
client?: Client; // Client instance for communication
|
||||
transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used
|
||||
options?: RequestOptions; // Options for requests
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user