mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a421bc476 | ||
|
|
503b60edb7 | ||
|
|
4039a85ee1 | ||
|
|
3a83b83a9e | ||
|
|
c1621805de |
@@ -11,10 +11,11 @@ interface ServerCardProps {
|
||||
server: Server
|
||||
onRemove: (serverName: string) => void
|
||||
onEdit: (server: Server) => void
|
||||
onToggle?: (server: Server, enabled: boolean) => void
|
||||
onToggle?: (server: Server, enabled: boolean) => Promise<boolean>
|
||||
onRefresh?: () => void
|
||||
}
|
||||
|
||||
const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => {
|
||||
const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { showToast } = useToast()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
@@ -102,6 +103,29 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
|
||||
const handleToolToggle = async (toolName: string, enabled: boolean) => {
|
||||
try {
|
||||
const { toggleTool } = await import('@/services/toolService')
|
||||
const result = await toggleTool(server.name, toolName, enabled)
|
||||
|
||||
if (result.success) {
|
||||
showToast(
|
||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
||||
'success'
|
||||
)
|
||||
// Trigger refresh to update the tool's state in the UI
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling tool:', error)
|
||||
showToast(t('tool.toggleFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`bg-white shadow rounded-lg p-6 mb-6 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
||||
@@ -217,7 +241,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
||||
<div className="space-y-4">
|
||||
{server.tools.map((tool, index) => (
|
||||
<ToolCard key={index} server={server.name} tool={tool} />
|
||||
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Tool } from '@/types'
|
||||
import { ChevronDown, ChevronRight, Play, Loader } from '@/components/icons/LucideIcons'
|
||||
import { callTool, ToolCallResult } from '@/services/toolService'
|
||||
import { ChevronDown, ChevronRight, Play, Loader, Edit, Check } from '@/components/icons/LucideIcons'
|
||||
import { callTool, ToolCallResult, updateToolDescription } from '@/services/toolService'
|
||||
import { Switch } from './ToggleGroup'
|
||||
import DynamicForm from './DynamicForm'
|
||||
import ToolResult from './ToolResult'
|
||||
|
||||
interface ToolCardProps {
|
||||
server: string
|
||||
tool: Tool
|
||||
onToggle?: (toolName: string, enabled: boolean) => void
|
||||
onDescriptionUpdate?: (toolName: string, description: string) => void
|
||||
}
|
||||
|
||||
const ToolCard = ({ tool, server }: ToolCardProps) => {
|
||||
const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [showRunForm, setShowRunForm] = useState(false)
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [result, setResult] = useState<ToolCallResult | null>(null)
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false)
|
||||
const [customDescription, setCustomDescription] = useState(tool.description || '')
|
||||
const descriptionInputRef = useRef<HTMLInputElement>(null)
|
||||
const descriptionTextRef = useRef<HTMLSpanElement>(null)
|
||||
const [textWidth, setTextWidth] = useState<number>(0)
|
||||
|
||||
// Focus the input when editing mode is activated
|
||||
useEffect(() => {
|
||||
if (isEditingDescription && descriptionInputRef.current) {
|
||||
descriptionInputRef.current.focus()
|
||||
// Set input width to match text width
|
||||
if (textWidth > 0) {
|
||||
descriptionInputRef.current.style.width = `${textWidth + 20}px` // Add some padding
|
||||
}
|
||||
}
|
||||
}, [isEditingDescription, textWidth])
|
||||
|
||||
// Measure text width when not editing
|
||||
useEffect(() => {
|
||||
if (!isEditingDescription && descriptionTextRef.current) {
|
||||
setTextWidth(descriptionTextRef.current.offsetWidth)
|
||||
}
|
||||
}, [isEditingDescription, customDescription])
|
||||
|
||||
// Generate a unique key for localStorage based on tool name and server
|
||||
const getStorageKey = useCallback(() => {
|
||||
@@ -28,6 +54,49 @@ const ToolCard = ({ tool, server }: ToolCardProps) => {
|
||||
localStorage.removeItem(getStorageKey())
|
||||
}, [getStorageKey])
|
||||
|
||||
const handleToggle = (enabled: boolean) => {
|
||||
if (onToggle) {
|
||||
onToggle(tool.name, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDescriptionEdit = () => {
|
||||
setIsEditingDescription(true)
|
||||
}
|
||||
|
||||
const handleDescriptionSave = async () => {
|
||||
try {
|
||||
const result = await updateToolDescription(server, tool.name, customDescription)
|
||||
if (result.success) {
|
||||
setIsEditingDescription(false)
|
||||
if (onDescriptionUpdate) {
|
||||
onDescriptionUpdate(tool.name, customDescription)
|
||||
}
|
||||
} else {
|
||||
// Revert on error
|
||||
setCustomDescription(tool.description || '')
|
||||
console.error('Failed to update tool description:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating tool description:', error)
|
||||
setCustomDescription(tool.description || '')
|
||||
setIsEditingDescription(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomDescription(e.target.value)
|
||||
}
|
||||
|
||||
const handleDescriptionKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleDescriptionSave()
|
||||
} else if (e.key === 'Escape') {
|
||||
setCustomDescription(tool.description || '')
|
||||
setIsEditingDescription(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRunTool = async (arguments_: Record<string, any>) => {
|
||||
setIsRunning(true)
|
||||
try {
|
||||
@@ -68,13 +137,61 @@ const ToolCard = ({ tool, server }: ToolCardProps) => {
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{tool.name}
|
||||
<span className="ml-2 text-sm font-normal text-gray-600">
|
||||
{tool.description || t('tool.noDescription')}
|
||||
{tool.name.replace(server + '-', '')}
|
||||
<span className="ml-2 text-sm font-normal text-gray-600 inline-flex items-center">
|
||||
{isEditingDescription ? (
|
||||
<>
|
||||
<input
|
||||
ref={descriptionInputRef}
|
||||
type="text"
|
||||
className="px-2 py-1 border border-blue-300 rounded bg-white text-sm"
|
||||
value={customDescription}
|
||||
onChange={handleDescriptionChange}
|
||||
onKeyDown={handleDescriptionKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
minWidth: '100px',
|
||||
width: textWidth > 0 ? `${textWidth + 20}px` : 'auto'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="ml-2 p-1 text-green-600 hover:text-green-800"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDescriptionSave()
|
||||
}}
|
||||
>
|
||||
<Check size={16} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span ref={descriptionTextRef}>{customDescription || t('tool.noDescription')}</span>
|
||||
<button
|
||||
className="ml-2 p-1 text-gray-500 hover:text-blue-600 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDescriptionEdit()
|
||||
}}
|
||||
>
|
||||
<Edit size={14} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Switch
|
||||
checked={tool.enabled ?? true}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -82,7 +199,7 @@ const ToolCard = ({ tool, server }: ToolCardProps) => {
|
||||
setShowRunForm(true)
|
||||
}}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-sm text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
|
||||
disabled={isRunning}
|
||||
disabled={isRunning || !tool.enabled}
|
||||
>
|
||||
{isRunning ? (
|
||||
<Loader size={14} className="animate-spin" />
|
||||
@@ -112,7 +229,7 @@ const ToolCard = ({ tool, server }: ToolCardProps) => {
|
||||
{/* Run Form */}
|
||||
{showRunForm && (
|
||||
<div className="border border-gray-300 rounded-lg p-4 bg-blue-50">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-3">{t('tool.runToolWithName', { name: tool.name })}</h4>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-3">{t('tool.runToolWithName', { name: tool.name.replace(server + '-', '') })}</h4>
|
||||
<DynamicForm
|
||||
schema={tool.inputSchema || { type: 'object' }}
|
||||
onSubmit={handleRunTool}
|
||||
|
||||
@@ -284,7 +284,11 @@
|
||||
"toolResult": "Tool result",
|
||||
"noParameters": "This tool does not require any parameters.",
|
||||
"selectOption": "Select an option",
|
||||
"enterValue": "Enter {{type}} value"
|
||||
"enterValue": "Enter {{type}} value",
|
||||
"enabled": "Enabled",
|
||||
"enableSuccess": "Tool {{name}} enabled successfully",
|
||||
"disableSuccess": "Tool {{name}} disabled successfully",
|
||||
"toggleFailed": "Failed to toggle tool status"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "Enable Global Route",
|
||||
|
||||
@@ -285,7 +285,11 @@
|
||||
"toolResult": "工具结果",
|
||||
"noParameters": "此工具不需要任何参数。",
|
||||
"selectOption": "选择一个选项",
|
||||
"enterValue": "输入{{type}}值"
|
||||
"enterValue": "输入{{type}}值",
|
||||
"enabled": "已启用",
|
||||
"enableSuccess": "工具 {{name}} 启用成功",
|
||||
"disableSuccess": "工具 {{name}} 禁用成功",
|
||||
"toggleFailed": "切换工具状态失败"
|
||||
},
|
||||
"settings": {
|
||||
"enableGlobalRoute": "启用全局路由",
|
||||
|
||||
@@ -125,6 +125,7 @@ const ServersPage: React.FC = () => {
|
||||
onRemove={handleServerRemove}
|
||||
onEdit={handleEditClick}
|
||||
onToggle={handleServerToggle}
|
||||
onRefresh={triggerRefresh}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -70,3 +70,90 @@ export const callTool = async (
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle a tool's enabled state for a specific server
|
||||
*/
|
||||
export const toggleTool = async (
|
||||
serverName: string,
|
||||
toolName: string,
|
||||
enabled: boolean,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Authentication token not found. Please log in.');
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl(`/servers/${serverName}/tools/${toolName}/toggle`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success,
|
||||
error: data.success ? undefined : data.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error toggling tool:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a tool's description for a specific server
|
||||
*/
|
||||
export const updateToolDescription = async (
|
||||
serverName: string,
|
||||
toolName: string,
|
||||
description: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Authentication token not found. Please log in.');
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
getApiUrl(`/servers/${serverName}/tools/${toolName}/description`),
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ description }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success,
|
||||
error: data.success ? undefined : data.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating tool description:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: ToolInputSchema;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
// Server config types
|
||||
@@ -78,6 +79,7 @@ export interface ServerConfig {
|
||||
env?: Record<string, string>;
|
||||
headers?: Record<string, string>;
|
||||
enabled?: boolean;
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
}
|
||||
|
||||
// Server types
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^8.50.0",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
|
||||
1153
pnpm-lock.yaml
generated
1153
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import {
|
||||
removeServer,
|
||||
updateMcpServer,
|
||||
notifyToolChanged,
|
||||
syncToolEmbedding,
|
||||
toggleServerStatus,
|
||||
} from '../services/mcpService.js';
|
||||
import { loadSettings, saveSettings } from '../config/index.js';
|
||||
@@ -318,6 +319,136 @@ export const toggleServer = async (req: Request, res: Response): Promise<void> =
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle tool status for a specific server
|
||||
export const toggleTool = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { serverName, toolName } = req.params;
|
||||
const { enabled } = req.body;
|
||||
|
||||
if (!serverName || !toolName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name and tool name are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Enabled status must be a boolean',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = loadSettings();
|
||||
if (!settings.mcpServers[serverName]) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Server not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize tools config if it doesn't exist
|
||||
if (!settings.mcpServers[serverName].tools) {
|
||||
settings.mcpServers[serverName].tools = {};
|
||||
}
|
||||
|
||||
// Set the tool's enabled state
|
||||
settings.mcpServers[serverName].tools![toolName] = { enabled };
|
||||
|
||||
if (!saveSettings(settings)) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to save settings',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify that tools have changed
|
||||
notifyToolChanged();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Tool ${toolName} ${enabled ? 'enabled' : 'disabled'} successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update tool description for a specific server
|
||||
export const updateToolDescription = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { serverName, toolName } = req.params;
|
||||
const { description } = req.body;
|
||||
|
||||
if (!serverName || !toolName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Server name and tool name are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof description !== 'string') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Description must be a string',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = loadSettings();
|
||||
if (!settings.mcpServers[serverName]) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Server not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize tools config if it doesn't exist
|
||||
if (!settings.mcpServers[serverName].tools) {
|
||||
settings.mcpServers[serverName].tools = {};
|
||||
}
|
||||
|
||||
// Set the tool's description
|
||||
if (!settings.mcpServers[serverName].tools![toolName]) {
|
||||
settings.mcpServers[serverName].tools![toolName] = { enabled: true };
|
||||
}
|
||||
|
||||
settings.mcpServers[serverName].tools![toolName].description = description;
|
||||
|
||||
if (!saveSettings(settings)) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to save settings',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify that tools have changed
|
||||
notifyToolChanged();
|
||||
|
||||
syncToolEmbedding(serverName, toolName);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Tool ${toolName} description updated successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSystemConfig = (req: Request, res: Response): void => {
|
||||
try {
|
||||
const { routing, install, smartRouting } = req.body;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import 'reflect-metadata'; // Ensure reflect-metadata is imported here too
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import dotenv from 'dotenv';
|
||||
import dotenvExpand from 'dotenv-expand';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import entities from './entities/index.js';
|
||||
import { registerPostgresVectorType } from './types/postgresVectorType.js';
|
||||
import { VectorEmbeddingSubscriber } from './subscribers/VectorEmbeddingSubscriber.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
import { getSmartRoutingConfig } from '../utils/smartRouting.js';
|
||||
|
||||
// Helper function to create required PostgreSQL extensions
|
||||
const createRequiredExtensions = async (dataSource: DataSource): Promise<void> => {
|
||||
@@ -30,23 +26,7 @@ const createRequiredExtensions = async (dataSource: DataSource): Promise<void> =
|
||||
|
||||
// Get database URL from smart routing config or fallback to environment variable
|
||||
const getDatabaseUrl = (): string => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const smartRouting = settings.systemConfig?.smartRouting;
|
||||
|
||||
// Use smart routing dbUrl if smart routing is enabled and dbUrl is configured
|
||||
if (smartRouting?.enabled && smartRouting?.dbUrl) {
|
||||
console.log('Using smart routing database URL');
|
||||
return smartRouting.dbUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to load settings for smart routing database URL, falling back to environment variable:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
return getSmartRoutingConfig().dbUrl;
|
||||
};
|
||||
|
||||
// Default database configuration
|
||||
@@ -59,7 +39,10 @@ const defaultConfig: DataSourceOptions = {
|
||||
};
|
||||
|
||||
// AppDataSource is the TypeORM data source
|
||||
let AppDataSource = new DataSource(defaultConfig);
|
||||
let appDataSource = new DataSource(defaultConfig);
|
||||
|
||||
// Global promise to track initialization status
|
||||
let initializationPromise: Promise<DataSource> | null = null;
|
||||
|
||||
// Function to create a new DataSource with updated configuration
|
||||
export const updateDataSourceConfig = (): DataSource => {
|
||||
@@ -69,31 +52,36 @@ export const updateDataSourceConfig = (): DataSource => {
|
||||
};
|
||||
|
||||
// If the configuration has changed, we need to create a new DataSource
|
||||
const currentUrl = (AppDataSource.options as any).url;
|
||||
const currentUrl = (appDataSource.options as any).url;
|
||||
if (currentUrl !== newConfig.url) {
|
||||
console.log('Database URL configuration changed, updating DataSource...');
|
||||
AppDataSource = new DataSource(newConfig);
|
||||
appDataSource = new DataSource(newConfig);
|
||||
// Reset initialization promise when configuration changes
|
||||
initializationPromise = null;
|
||||
}
|
||||
|
||||
return AppDataSource;
|
||||
return appDataSource;
|
||||
};
|
||||
|
||||
// Get the current AppDataSource instance
|
||||
export const getAppDataSource = (): DataSource => {
|
||||
return AppDataSource;
|
||||
return appDataSource;
|
||||
};
|
||||
|
||||
// Reconnect database with updated configuration
|
||||
export const reconnectDatabase = async (): Promise<DataSource> => {
|
||||
try {
|
||||
// Close existing connection if it exists
|
||||
if (AppDataSource.isInitialized) {
|
||||
if (appDataSource.isInitialized) {
|
||||
console.log('Closing existing database connection...');
|
||||
await AppDataSource.destroy();
|
||||
await appDataSource.destroy();
|
||||
}
|
||||
|
||||
// Reset initialization promise to allow fresh initialization
|
||||
initializationPromise = null;
|
||||
|
||||
// Update configuration and reconnect
|
||||
AppDataSource = updateDataSourceConfig();
|
||||
appDataSource = updateDataSourceConfig();
|
||||
return await initializeDatabase();
|
||||
} catch (error) {
|
||||
console.error('Error during database reconnection:', error);
|
||||
@@ -101,26 +89,54 @@ export const reconnectDatabase = async (): Promise<DataSource> => {
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize database connection
|
||||
// Initialize database connection with concurrency control
|
||||
export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
// If initialization is already in progress, wait for it to complete
|
||||
if (initializationPromise) {
|
||||
console.log('Database initialization already in progress, waiting for completion...');
|
||||
return initializationPromise;
|
||||
}
|
||||
|
||||
// If already initialized, return the existing instance
|
||||
if (appDataSource.isInitialized) {
|
||||
console.log('Database already initialized, returning existing instance');
|
||||
return Promise.resolve(appDataSource);
|
||||
}
|
||||
|
||||
// Create a new initialization promise
|
||||
initializationPromise = performDatabaseInitialization();
|
||||
|
||||
try {
|
||||
const result = await initializationPromise;
|
||||
console.log('Database initialization completed successfully');
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Reset the promise on error so initialization can be retried
|
||||
initializationPromise = null;
|
||||
console.error('Database initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Internal function to perform the actual database initialization
|
||||
const performDatabaseInitialization = async (): Promise<DataSource> => {
|
||||
try {
|
||||
// Update configuration before initializing
|
||||
AppDataSource = updateDataSourceConfig();
|
||||
appDataSource = updateDataSourceConfig();
|
||||
|
||||
if (!AppDataSource.isInitialized) {
|
||||
if (!appDataSource.isInitialized) {
|
||||
console.log('Initializing database connection...');
|
||||
// Register the vector type with TypeORM
|
||||
await AppDataSource.initialize();
|
||||
registerPostgresVectorType(AppDataSource);
|
||||
await appDataSource.initialize();
|
||||
registerPostgresVectorType(appDataSource);
|
||||
|
||||
// Create required PostgreSQL extensions
|
||||
await createRequiredExtensions(AppDataSource);
|
||||
await createRequiredExtensions(appDataSource);
|
||||
|
||||
// Set up vector column and index with a more direct approach
|
||||
try {
|
||||
|
||||
// Check if table exists first
|
||||
const tableExists = await AppDataSource.query(`
|
||||
const tableExists = await appDataSource.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
@@ -134,7 +150,7 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
|
||||
// Step 1: Drop any existing index on the column
|
||||
try {
|
||||
await AppDataSource.query(`DROP INDEX IF EXISTS idx_vector_embeddings_embedding;`);
|
||||
await appDataSource.query(`DROP INDEX IF EXISTS idx_vector_embeddings_embedding;`);
|
||||
} catch (dropError: any) {
|
||||
console.warn('Note: Could not drop existing index:', dropError.message);
|
||||
}
|
||||
@@ -142,14 +158,14 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
// Step 2: Alter column type to vector (if it's not already)
|
||||
try {
|
||||
// Check column type first
|
||||
const columnType = await AppDataSource.query(`
|
||||
const columnType = await appDataSource.query(`
|
||||
SELECT data_type FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'vector_embeddings'
|
||||
AND column_name = 'embedding';
|
||||
`);
|
||||
|
||||
if (columnType.length > 0 && columnType[0].data_type !== 'vector') {
|
||||
await AppDataSource.query(`
|
||||
await appDataSource.query(`
|
||||
ALTER TABLE vector_embeddings
|
||||
ALTER COLUMN embedding TYPE vector USING embedding::vector;
|
||||
`);
|
||||
@@ -163,7 +179,7 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
// Step 3: Try to create appropriate indices
|
||||
try {
|
||||
// First, let's check if there are any records to determine the dimensions
|
||||
const records = await AppDataSource.query(`
|
||||
const records = await appDataSource.query(`
|
||||
SELECT dimensions FROM vector_embeddings LIMIT 1;
|
||||
`);
|
||||
|
||||
@@ -177,13 +193,13 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
|
||||
// Set the vector dimensions explicitly only if table has data
|
||||
if (records && records.length > 0) {
|
||||
await AppDataSource.query(`
|
||||
await appDataSource.query(`
|
||||
ALTER TABLE vector_embeddings
|
||||
ALTER COLUMN embedding TYPE vector(${dimensions});
|
||||
`);
|
||||
|
||||
// Now try to create the index
|
||||
await AppDataSource.query(`
|
||||
await appDataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding
|
||||
ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
||||
`);
|
||||
@@ -199,7 +215,7 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
|
||||
try {
|
||||
// Try HNSW index instead
|
||||
await AppDataSource.query(`
|
||||
await appDataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding
|
||||
ON vector_embeddings USING hnsw (embedding vector_cosine_ops);
|
||||
`);
|
||||
@@ -210,7 +226,7 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
|
||||
try {
|
||||
// Create a basic GIN index as last resort
|
||||
await AppDataSource.query(`
|
||||
await appDataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding
|
||||
ON vector_embeddings USING gin (embedding);
|
||||
`);
|
||||
@@ -235,12 +251,11 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
|
||||
// Run one final setup check after schema synchronization is done
|
||||
if (defaultConfig.synchronize) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('Running final vector configuration check...');
|
||||
try {
|
||||
console.log('Running final vector configuration check...');
|
||||
|
||||
// Try setup again with the same code from above
|
||||
const tableExists = await AppDataSource.query(`
|
||||
// Try setup again with the same code from above
|
||||
const tableExists = await appDataSource.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
@@ -248,64 +263,60 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
);
|
||||
`);
|
||||
|
||||
if (tableExists[0].exists) {
|
||||
console.log('Vector embeddings table found, checking configuration...');
|
||||
if (tableExists[0].exists) {
|
||||
console.log('Vector embeddings table found, checking configuration...');
|
||||
|
||||
// Get the dimension size first
|
||||
try {
|
||||
// Try to get dimensions from an existing record
|
||||
const records = await AppDataSource.query(`
|
||||
// Get the dimension size first
|
||||
try {
|
||||
// Try to get dimensions from an existing record
|
||||
const records = await appDataSource.query(`
|
||||
SELECT dimensions FROM vector_embeddings LIMIT 1;
|
||||
`);
|
||||
|
||||
// Only proceed if we have existing data, otherwise let vector service handle it
|
||||
if (records && records.length > 0 && records[0].dimensions) {
|
||||
const dimensions = records[0].dimensions;
|
||||
console.log(`Found vector dimension from database: ${dimensions}`);
|
||||
// Only proceed if we have existing data, otherwise let vector service handle it
|
||||
if (records && records.length > 0 && records[0].dimensions) {
|
||||
const dimensions = records[0].dimensions;
|
||||
console.log(`Found vector dimension from database: ${dimensions}`);
|
||||
|
||||
// Ensure column type is vector with explicit dimensions
|
||||
await AppDataSource.query(`
|
||||
// Ensure column type is vector with explicit dimensions
|
||||
await appDataSource.query(`
|
||||
ALTER TABLE vector_embeddings
|
||||
ALTER COLUMN embedding TYPE vector(${dimensions});
|
||||
`);
|
||||
console.log('Vector embedding column type updated in final check.');
|
||||
console.log('Vector embedding column type updated in final check.');
|
||||
|
||||
// One more attempt at creating the index with dimensions
|
||||
try {
|
||||
// Drop existing index if any
|
||||
await AppDataSource.query(`
|
||||
// One more attempt at creating the index with dimensions
|
||||
try {
|
||||
// Drop existing index if any
|
||||
await appDataSource.query(`
|
||||
DROP INDEX IF EXISTS idx_vector_embeddings_embedding;
|
||||
`);
|
||||
|
||||
// Create new index with proper dimensions
|
||||
await AppDataSource.query(`
|
||||
// Create new index with proper dimensions
|
||||
await appDataSource.query(`
|
||||
CREATE INDEX idx_vector_embeddings_embedding
|
||||
ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
||||
`);
|
||||
console.log('Created IVFFlat index in final check.');
|
||||
} catch (indexError: any) {
|
||||
console.warn(
|
||||
'Final index creation attempt did not succeed:',
|
||||
indexError.message,
|
||||
);
|
||||
console.warn('Using basic lookup without vector index.');
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
'No existing vector data found, vector dimensions will be configured by vector service.',
|
||||
);
|
||||
console.log('Created IVFFlat index in final check.');
|
||||
} catch (indexError: any) {
|
||||
console.warn('Final index creation attempt did not succeed:', indexError.message);
|
||||
console.warn('Using basic lookup without vector index.');
|
||||
}
|
||||
} catch (setupError: any) {
|
||||
console.warn('Vector setup in final check failed:', setupError.message);
|
||||
} else {
|
||||
console.log(
|
||||
'No existing vector data found, vector dimensions will be configured by vector service.',
|
||||
);
|
||||
}
|
||||
} catch (setupError: any) {
|
||||
console.warn('Vector setup in final check failed:', setupError.message);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn('Post-initialization vector setup failed:', error.message);
|
||||
}
|
||||
}, 3000); // Give synchronize some time to complete
|
||||
} catch (error: any) {
|
||||
console.warn('Post-initialization vector setup failed:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return AppDataSource;
|
||||
return appDataSource;
|
||||
} catch (error) {
|
||||
console.error('Error during database initialization:', error);
|
||||
throw error;
|
||||
@@ -314,18 +325,18 @@ export const initializeDatabase = async (): Promise<DataSource> => {
|
||||
|
||||
// Get database connection status
|
||||
export const isDatabaseConnected = (): boolean => {
|
||||
return AppDataSource.isInitialized;
|
||||
return appDataSource.isInitialized;
|
||||
};
|
||||
|
||||
// Close database connection
|
||||
export const closeDatabase = async (): Promise<void> => {
|
||||
if (AppDataSource.isInitialized) {
|
||||
await AppDataSource.destroy();
|
||||
if (appDataSource.isInitialized) {
|
||||
await appDataSource.destroy();
|
||||
console.log('Database connection closed.');
|
||||
}
|
||||
};
|
||||
|
||||
// Export AppDataSource for backward compatibility
|
||||
export { AppDataSource };
|
||||
export const AppDataSource = appDataSource;
|
||||
|
||||
export default getAppDataSource;
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
updateServer,
|
||||
deleteServer,
|
||||
toggleServer,
|
||||
toggleTool,
|
||||
updateToolDescription,
|
||||
updateSystemConfig,
|
||||
} from '../controllers/serverController.js';
|
||||
import {
|
||||
@@ -46,6 +48,8 @@ export const initRoutes = (app: express.Application): void => {
|
||||
router.put('/servers/:name', updateServer);
|
||||
router.delete('/servers/:name', deleteServer);
|
||||
router.post('/servers/:name/toggle', toggleServer);
|
||||
router.post('/servers/:serverName/tools/:toolName/toggle', toggleTool);
|
||||
router.put('/servers/:serverName/tools/:toolName/description', updateToolDescription);
|
||||
router.put('/system-config', updateSystemConfig);
|
||||
|
||||
// Group management routes
|
||||
|
||||
@@ -4,7 +4,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 { ServerInfo, ServerConfig } from '../types/index.js';
|
||||
import { ServerInfo, ServerConfig, ToolInfo } from '../types/index.js';
|
||||
import { loadSettings, saveSettings, expandEnvVars, replaceEnvVars } from '../config/index.js';
|
||||
import config from '../config/index.js';
|
||||
import { getGroup } from './sseService.js';
|
||||
@@ -50,6 +50,21 @@ export const notifyToolChanged = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const syncToolEmbedding = async (serverName: string, toolName: string) => {
|
||||
const serverInfo = getServerByName(serverName);
|
||||
if (!serverInfo) {
|
||||
console.warn(`Server not found: ${serverName}`);
|
||||
return;
|
||||
}
|
||||
const tool = serverInfo.tools.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
console.warn(`Tool not found: ${toolName} on server: ${serverName}`);
|
||||
return;
|
||||
}
|
||||
// Save tool as vector embedding for search
|
||||
saveToolsAsVectorEmbeddings(serverName, [tool]);
|
||||
};
|
||||
|
||||
// Store all server information
|
||||
let serverInfos: ServerInfo[] = [];
|
||||
|
||||
@@ -188,28 +203,15 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
|
||||
}
|
||||
|
||||
serverInfo.tools = tools.tools.map((tool) => ({
|
||||
name: tool.name,
|
||||
name: `${name}-${tool.name}`,
|
||||
description: tool.description || '',
|
||||
inputSchema: tool.inputSchema || {},
|
||||
}));
|
||||
serverInfo.status = 'connected';
|
||||
serverInfo.error = null;
|
||||
|
||||
// Save tools as vector embeddings for search (only when smart routing is enabled)
|
||||
if (serverInfo.tools.length > 0) {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const smartRoutingEnabled = settings.systemConfig?.smartRouting?.enabled || false;
|
||||
if (smartRoutingEnabled) {
|
||||
console.log(
|
||||
`Smart routing enabled - saving vector embeddings for server ${name}`,
|
||||
);
|
||||
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
||||
}
|
||||
} catch (vectorError) {
|
||||
console.warn(`Failed to save vector embeddings for server ${name}:`, vectorError);
|
||||
}
|
||||
}
|
||||
// Save tools as vector embeddings for search
|
||||
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
@@ -258,11 +260,22 @@ export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] =>
|
||||
const infos = serverInfos.map(({ name, status, tools, createTime, error }) => {
|
||||
const serverConfig = settings.mcpServers[name];
|
||||
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
||||
|
||||
// Add enabled status and custom description to each tool
|
||||
const toolsWithEnabled = tools.map((tool) => {
|
||||
const toolConfig = serverConfig?.tools?.[tool.name];
|
||||
return {
|
||||
...tool,
|
||||
description: toolConfig?.description || tool.description, // Use custom description if available
|
||||
enabled: toolConfig?.enabled !== false, // Default to true if not explicitly disabled
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name,
|
||||
status,
|
||||
error,
|
||||
tools,
|
||||
tools: toolsWithEnabled,
|
||||
createTime,
|
||||
enabled,
|
||||
};
|
||||
@@ -279,6 +292,23 @@ const getServerByName = (name: string): ServerInfo | undefined => {
|
||||
return serverInfos.find((serverInfo) => serverInfo.name === name);
|
||||
};
|
||||
|
||||
// Filter tools by server configuration
|
||||
const filterToolsByConfig = (serverName: string, tools: ToolInfo[]): ToolInfo[] => {
|
||||
const settings = loadSettings();
|
||||
const serverConfig = settings.mcpServers[serverName];
|
||||
|
||||
if (!serverConfig || !serverConfig.tools) {
|
||||
// If no tool configuration exists, all tools are enabled by default
|
||||
return tools;
|
||||
}
|
||||
|
||||
return tools.filter((tool) => {
|
||||
const toolConfig = serverConfig.tools?.[tool.name];
|
||||
// If tool is not in config, it's enabled by default
|
||||
return toolConfig?.enabled !== false;
|
||||
});
|
||||
};
|
||||
|
||||
// Get server by tool name
|
||||
const getServerByTool = (toolName: string): ServerInfo | undefined => {
|
||||
return serverInfos.find((serverInfo) => serverInfo.tools.some((tool) => tool.name === toolName));
|
||||
@@ -489,7 +519,21 @@ Available servers: ${serversList}`;
|
||||
const allTools = [];
|
||||
for (const serverInfo of allServerInfos) {
|
||||
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
||||
allTools.push(...serverInfo.tools);
|
||||
// Filter tools based on server configuration and apply custom descriptions
|
||||
const enabledTools = filterToolsByConfig(serverInfo.name, serverInfo.tools);
|
||||
|
||||
// Apply custom descriptions from configuration
|
||||
const settings = loadSettings();
|
||||
const serverConfig = settings.mcpServers[serverInfo.name];
|
||||
const toolsWithCustomDescriptions = enabledTools.map((tool) => {
|
||||
const toolConfig = serverConfig?.tools?.[tool.name];
|
||||
return {
|
||||
...tool,
|
||||
description: toolConfig?.description || tool.description, // Use custom description if available
|
||||
};
|
||||
});
|
||||
|
||||
allTools.push(...toolsWithCustomDescriptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,30 +574,54 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
const searchResults = await searchToolsByVector(query, limitNum, thresholdNum, servers);
|
||||
console.log(`Search results: ${JSON.stringify(searchResults)}`);
|
||||
// Find actual tool information from serverInfos by serverName and toolName
|
||||
const tools = searchResults.map((result) => {
|
||||
// Find the server in serverInfos
|
||||
const server = serverInfos.find(
|
||||
(serverInfo) =>
|
||||
serverInfo.name === result.serverName &&
|
||||
serverInfo.status === 'connected' &&
|
||||
serverInfo.enabled !== false,
|
||||
);
|
||||
if (server && server.tools && server.tools.length > 0) {
|
||||
// Find the tool in server.tools
|
||||
const actualTool = server.tools.find((tool) => tool.name === result.toolName);
|
||||
if (actualTool) {
|
||||
// Return the actual tool info from serverInfos
|
||||
return actualTool;
|
||||
}
|
||||
}
|
||||
const tools = searchResults
|
||||
.map((result) => {
|
||||
// Find the server in serverInfos
|
||||
const server = serverInfos.find(
|
||||
(serverInfo) =>
|
||||
serverInfo.name === result.serverName &&
|
||||
serverInfo.status === 'connected' &&
|
||||
serverInfo.enabled !== false,
|
||||
);
|
||||
if (server && server.tools && server.tools.length > 0) {
|
||||
// Find the tool in server.tools
|
||||
const actualTool = server.tools.find((tool) => tool.name === result.toolName);
|
||||
if (actualTool) {
|
||||
// Check if the tool is enabled in configuration
|
||||
const enabledTools = filterToolsByConfig(server.name, [actualTool]);
|
||||
if (enabledTools.length > 0) {
|
||||
// Apply custom description from configuration
|
||||
const settings = loadSettings();
|
||||
const serverConfig = settings.mcpServers[server.name];
|
||||
const toolConfig = serverConfig?.tools?.[actualTool.name];
|
||||
|
||||
// Fallback to search result if server or tool not found
|
||||
return {
|
||||
name: result.toolName,
|
||||
description: result.description || '',
|
||||
inputSchema: result.inputSchema || {},
|
||||
};
|
||||
});
|
||||
// Return the actual tool info from serverInfos with custom description
|
||||
return {
|
||||
...actualTool,
|
||||
description: toolConfig?.description || actualTool.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to search result if server or tool not found or disabled
|
||||
return {
|
||||
name: result.toolName,
|
||||
description: result.description || '',
|
||||
inputSchema: result.inputSchema || {},
|
||||
};
|
||||
})
|
||||
.filter((tool) => {
|
||||
// Additional filter to remove tools that are disabled
|
||||
if (tool.name) {
|
||||
const serverName = searchResults.find((r) => r.toolName === tool.name)?.serverName;
|
||||
if (serverName) {
|
||||
const enabledTools = filterToolsByConfig(serverName, [tool as ToolInfo]);
|
||||
return enabledTools.length > 0;
|
||||
}
|
||||
}
|
||||
return true; // Keep fallback results
|
||||
});
|
||||
|
||||
// Add usage guidance to the response
|
||||
const response = {
|
||||
@@ -586,7 +654,7 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
|
||||
// Special handling for call_tool
|
||||
if (request.params.name === 'call_tool') {
|
||||
const { toolName, arguments: toolArgs = {} } = request.params.arguments || {};
|
||||
let { toolName, arguments: toolArgs = {} } = request.params.arguments || {};
|
||||
|
||||
if (!toolName) {
|
||||
throw new Error('toolName parameter is required');
|
||||
@@ -631,6 +699,9 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
`Invoking tool '${toolName}' on server '${targetServerInfo.name}' with arguments: ${JSON.stringify(finalArgs)}`,
|
||||
);
|
||||
|
||||
toolName = toolName.startsWith(`${targetServerInfo.name}-`)
|
||||
? toolName.replace(`${targetServerInfo.name}-`, '')
|
||||
: toolName;
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: finalArgs,
|
||||
@@ -649,6 +720,10 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${request.params.name}`);
|
||||
}
|
||||
|
||||
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);
|
||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
|
||||
@@ -2,45 +2,17 @@ import { getRepositoryFactory } from '../db/index.js';
|
||||
import { VectorEmbeddingRepository } from '../db/repositories/index.js';
|
||||
import { ToolInfo } from '../types/index.js';
|
||||
import { getAppDataSource, initializeDatabase } from '../db/connection.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
import { getSmartRoutingConfig } from '../utils/smartRouting.js';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
// Get OpenAI configuration from smartRouting settings or fallback to environment variables
|
||||
const getOpenAIConfig = () => {
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
const smartRouting = settings.systemConfig?.smartRouting;
|
||||
|
||||
return {
|
||||
apiKey: smartRouting?.openaiApiKey || process.env.OPENAI_API_KEY,
|
||||
baseURL:
|
||||
smartRouting?.openaiApiBaseUrl ||
|
||||
process.env.OPENAI_API_BASE_URL ||
|
||||
'https://api.openai.com/v1',
|
||||
embeddingModel:
|
||||
smartRouting?.openaiApiEmbeddingModel ||
|
||||
process.env.OPENAI_API_EMBEDDING_MODEL ||
|
||||
'text-embedding-3-small',
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to load smartRouting settings, falling back to environment variables:',
|
||||
error,
|
||||
);
|
||||
return {
|
||||
apiKey: '',
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
embeddingModel: 'text-embedding-3-small',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Environment variables for embedding configuration
|
||||
const EMBEDDING_ENV = {
|
||||
// The embedding model to use - default to OpenAI but allow BAAI/BGE models
|
||||
MODEL: process.env.EMBEDDING_MODEL || getOpenAIConfig().embeddingModel,
|
||||
// Detect if using a BGE model from the environment variable
|
||||
IS_BGE_MODEL: !!(process.env.EMBEDDING_MODEL && process.env.EMBEDDING_MODEL.includes('bge')),
|
||||
const smartRoutingConfig = getSmartRoutingConfig();
|
||||
return {
|
||||
apiKey: smartRoutingConfig.openaiApiKey,
|
||||
baseURL: smartRoutingConfig.openaiApiBaseUrl,
|
||||
embeddingModel: smartRoutingConfig.openaiApiEmbeddingModel,
|
||||
};
|
||||
};
|
||||
|
||||
// Constants for embedding models
|
||||
@@ -221,6 +193,16 @@ export const saveToolsAsVectorEmbeddings = async (
|
||||
tools: ToolInfo[],
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (tools.length === 0) {
|
||||
console.warn(`No tools to save for server: ${serverName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const smartRoutingConfig = getSmartRoutingConfig();
|
||||
if (!smartRoutingConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getOpenAIConfig();
|
||||
const vectorRepository = getRepositoryFactory(
|
||||
'vectorEmbeddings',
|
||||
|
||||
@@ -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 { SmartRoutingConfig } from '../utils/smartRouting.js';
|
||||
|
||||
// User interface
|
||||
export interface IUser {
|
||||
@@ -90,13 +91,7 @@ export interface McpSettings {
|
||||
pythonIndexUrl?: string; // Python package repository URL (UV_DEFAULT_INDEX)
|
||||
npmRegistry?: string; // NPM registry URL (npm_config_registry)
|
||||
};
|
||||
smartRouting?: {
|
||||
enabled?: boolean; // Controls whether smart routing is enabled
|
||||
dbUrl?: string; // Database URL for smart routing
|
||||
openaiApiBaseUrl?: string; // OpenAI API base URL
|
||||
openaiApiKey?: string; // OpenAI API key
|
||||
openaiApiEmbeddingModel?: string; // OpenAI API embedding model
|
||||
};
|
||||
smartRouting?: SmartRoutingConfig;
|
||||
// Add other system configuration sections here in the future
|
||||
};
|
||||
}
|
||||
@@ -110,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
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
}
|
||||
|
||||
// Information about a server's status and tools
|
||||
@@ -129,6 +125,7 @@ export interface ToolInfo {
|
||||
name: string; // Name of the tool
|
||||
description: string; // Brief description of the tool
|
||||
inputSchema: Record<string, unknown>; // Input schema for the tool
|
||||
enabled?: boolean; // Whether the tool is enabled (optional, defaults to true)
|
||||
}
|
||||
|
||||
// Standardized API response structure
|
||||
|
||||
143
src/utils/smartRouting.ts
Normal file
143
src/utils/smartRouting.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { loadSettings, expandEnvVars } from '../config/index.js';
|
||||
|
||||
/**
|
||||
* Smart routing configuration interface
|
||||
*/
|
||||
export interface SmartRoutingConfig {
|
||||
enabled: boolean;
|
||||
dbUrl: string;
|
||||
openaiApiBaseUrl: string;
|
||||
openaiApiKey: string;
|
||||
openaiApiEmbeddingModel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the complete smart routing configuration from environment variables and settings.
|
||||
*
|
||||
* Priority order for each setting:
|
||||
* 1. Specific environment variables (ENABLE_SMART_ROUTING, SMART_ROUTING_ENABLED, etc.)
|
||||
* 2. Generic environment variables (OPENAI_API_KEY, DATABASE_URL, etc.)
|
||||
* 3. Settings configuration (systemConfig.smartRouting)
|
||||
* 4. Default values
|
||||
*
|
||||
* @returns {SmartRoutingConfig} Complete smart routing configuration
|
||||
*/
|
||||
export function getSmartRoutingConfig(): SmartRoutingConfig {
|
||||
let settings = loadSettings();
|
||||
const smartRoutingSettings: Partial<SmartRoutingConfig> =
|
||||
settings.systemConfig?.smartRouting || {};
|
||||
|
||||
return {
|
||||
// Enabled status - check multiple environment variables
|
||||
enabled: getConfigValue(
|
||||
[process.env.SMART_ROUTING_ENABLED],
|
||||
smartRoutingSettings.enabled,
|
||||
false,
|
||||
parseBooleanEnvVar,
|
||||
),
|
||||
|
||||
// Database configuration
|
||||
dbUrl: getConfigValue([process.env.DB_URL], smartRoutingSettings.dbUrl, '', expandEnvVars),
|
||||
|
||||
// OpenAI API configuration
|
||||
openaiApiBaseUrl: getConfigValue(
|
||||
[process.env.OPENAI_API_BASE_URL],
|
||||
smartRoutingSettings.openaiApiBaseUrl,
|
||||
'https://api.openai.com/v1',
|
||||
expandEnvVars,
|
||||
),
|
||||
|
||||
openaiApiKey: getConfigValue(
|
||||
[process.env.OPENAI_API_KEY],
|
||||
smartRoutingSettings.openaiApiKey,
|
||||
'',
|
||||
expandEnvVars,
|
||||
),
|
||||
|
||||
openaiApiEmbeddingModel: getConfigValue(
|
||||
[process.env.OPENAI_API_EMBEDDING_MODEL],
|
||||
smartRoutingSettings.openaiApiEmbeddingModel,
|
||||
'text-embedding-3-small',
|
||||
expandEnvVars,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a configuration value with priority order: environment variables > settings > default.
|
||||
*
|
||||
* @param {(string | undefined)[]} envVars - Array of environment variable names to check in order
|
||||
* @param {any} settingsValue - Value from settings configuration
|
||||
* @param {any} defaultValue - Default value to use if no other value is found
|
||||
* @param {Function} transformer - Function to transform the final value to the correct type
|
||||
* @returns {any} The configuration value with the appropriate transformation applied
|
||||
*/
|
||||
function getConfigValue<T>(
|
||||
envVars: (string | undefined)[],
|
||||
settingsValue: any,
|
||||
defaultValue: T,
|
||||
transformer: (value: any) => T,
|
||||
): T {
|
||||
// Check environment variables in order
|
||||
for (const envVar of envVars) {
|
||||
if (envVar !== undefined && envVar !== null && envVar !== '') {
|
||||
try {
|
||||
return transformer(envVar);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to transform environment variable "${envVar}":`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check settings value
|
||||
if (settingsValue !== undefined && settingsValue !== null) {
|
||||
try {
|
||||
return transformer(settingsValue);
|
||||
} catch (error) {
|
||||
console.warn('Failed to transform settings value:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Return default value
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string environment variable value to a boolean.
|
||||
* Supports common boolean representations: true/false, 1/0, yes/no, on/off
|
||||
*
|
||||
* @param {string} value - The environment variable value to parse
|
||||
* @returns {boolean} The parsed boolean value
|
||||
*/
|
||||
function parseBooleanEnvVar(value: string): boolean {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalized = value.toLowerCase().trim();
|
||||
|
||||
// Handle common truthy values
|
||||
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle common falsy values
|
||||
if (
|
||||
normalized === 'false' ||
|
||||
normalized === '0' ||
|
||||
normalized === 'no' ||
|
||||
normalized === 'off' ||
|
||||
normalized === ''
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default to false for unrecognized values
|
||||
console.warn(`Unrecognized boolean value for smart routing: "${value}", defaulting to false`);
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user