Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
3d667e10f8 feat: add tool call activity backend infrastructure for DB mode
- Add ToolCallActivity TypeORM entity with indexes for efficient querying
- Add ToolCallActivityRepository with pagination, search, and statistics
- Add ToolCallActivityDao interface and DB implementation
- Update DaoFactory and DatabaseDaoFactory to support activity DAO
- Add IToolCallActivity interfaces to types

Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-12-31 16:30:55 +00:00
copilot-swe-agent[bot]
c06f9ed916 Initial plan 2025-12-31 15:34:07 +00:00
25 changed files with 578 additions and 702 deletions

View File

@@ -259,92 +259,6 @@ MCPHub supports environment variable substitution using `${VAR_NAME}` syntax:
}
```
### Proxy Configuration (proxychains4)
MCPHub supports routing STDIO server network traffic through a proxy using **proxychains4**. This feature is available on **Linux and macOS only** (Windows is not supported).
<Note>
To use this feature, you must have `proxychains4` installed on your system:
- **Debian/Ubuntu**: `apt install proxychains4`
- **macOS**: `brew install proxychains-ng`
- **Arch Linux**: `pacman -S proxychains-ng`
</Note>
#### Basic Proxy Configuration
```json
{
"mcpServers": {
"fetch-via-proxy": {
"command": "uvx",
"args": ["mcp-server-fetch"],
"proxy": {
"enabled": true,
"type": "socks5",
"host": "127.0.0.1",
"port": 1080
}
}
}
}
```
#### Proxy Configuration Options
| Field | Type | Default | Description |
| ------------ | ------- | --------- | ------------------------------------------------ |
| `enabled` | boolean | `false` | Enable/disable proxy routing |
| `type` | string | `socks5` | Proxy protocol: `socks4`, `socks5`, or `http` |
| `host` | string | - | Proxy server hostname or IP address |
| `port` | number | - | Proxy server port |
| `username` | string | - | Proxy authentication username (optional) |
| `password` | string | - | Proxy authentication password (optional) |
| `configPath` | string | - | Path to custom proxychains4 config file |
#### Proxy with Authentication
```json
{
"mcpServers": {
"secure-server": {
"command": "npx",
"args": ["-y", "@example/mcp-server"],
"proxy": {
"enabled": true,
"type": "http",
"host": "proxy.example.com",
"port": 8080,
"username": "${PROXY_USER}",
"password": "${PROXY_PASSWORD}"
}
}
}
}
```
#### Using Custom proxychains4 Configuration
For advanced use cases, you can provide your own proxychains4 configuration file:
```json
{
"mcpServers": {
"custom-proxy-server": {
"command": "python",
"args": ["-m", "custom_mcp_server"],
"proxy": {
"enabled": true,
"configPath": "/etc/proxychains4/custom.conf"
}
}
}
}
```
<Tip>
When `configPath` is specified, all other proxy settings (`type`, `host`, `port`, etc.) are ignored, and the custom configuration file is used directly.
</Tip>
{/* ### Custom Server Scripts
#### Local Python Server

View File

@@ -31,47 +31,6 @@
"DATABASE_URL": "${DATABASE_URL}"
}
},
"example-stdio-with-proxy": {
"type": "stdio",
"command": "uvx",
"args": [
"mcp-server-fetch"
],
"proxy": {
"enabled": true,
"type": "socks5",
"host": "${PROXY_HOST}",
"port": 1080
}
},
"example-stdio-with-auth-proxy": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@example/mcp-server"
],
"proxy": {
"enabled": true,
"type": "http",
"host": "${HTTP_PROXY_HOST}",
"port": 8080,
"username": "${PROXY_USERNAME}",
"password": "${PROXY_PASSWORD}"
}
},
"example-stdio-with-custom-proxy-config": {
"type": "stdio",
"command": "python",
"args": [
"-m",
"custom_mcp_server"
],
"proxy": {
"enabled": true,
"configPath": "/etc/proxychains4/custom.conf"
}
},
"example-openapi-server": {
"type": "openapi",
"openapi": {

View File

@@ -1,20 +1,16 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
disabled?: boolean;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
disabled = false
onPageChange
}) => {
const { t } = useTranslation();
// Generate page buttons
const getPageButtons = () => {
const buttons = [];
@@ -99,26 +95,26 @@ const Pagination: React.FC<PaginationProps> = ({
<div className="flex justify-center items-center my-6">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={disabled || currentPage === 1}
className={`px-3 py-1 rounded mr-2 ${disabled || currentPage === 1
disabled={currentPage === 1}
className={`px-3 py-1 rounded mr-2 ${currentPage === 1
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
}`}
>
&laquo; {t('common.previous')}
&laquo; Prev
</button>
<div className="flex">{getPageButtons()}</div>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={disabled || currentPage === totalPages}
className={`px-3 py-1 rounded ml-2 ${disabled || currentPage === totalPages
disabled={currentPage === totalPages}
className={`px-3 py-1 rounded ml-2 ${currentPage === totalPages
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
}`}
>
{t('common.next')} &raquo;
Next &raquo;
</button>
</div>
);

View File

@@ -17,16 +17,6 @@ const CONFIG = {
},
};
// Pagination info type
interface PaginationInfo {
page: number;
limit: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
}
// Context type definition
interface ServerContextType {
servers: Server[];
@@ -34,11 +24,6 @@ interface ServerContextType {
setError: (error: string | null) => void;
isLoading: boolean;
fetchAttempts: number;
pagination: PaginationInfo | null;
currentPage: number;
serversPerPage: number;
setCurrentPage: (page: number) => void;
setServersPerPage: (limit: number) => void;
triggerRefresh: () => void;
refreshIfNeeded: () => void; // Smart refresh with debounce
handleServerAdd: () => void;
@@ -60,9 +45,6 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const [refreshKey, setRefreshKey] = useState(0);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [fetchAttempts, setFetchAttempts] = useState(0);
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [serversPerPage, setServersPerPage] = useState(10);
// Timer reference for polling
const intervalRef = useRef<NodeJS.Timeout | null>(null);
@@ -91,31 +73,18 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const fetchServers = async () => {
try {
console.log('[ServerContext] Fetching servers from API...');
// Build query parameters for pagination
const params = new URLSearchParams();
params.append('page', currentPage.toString());
params.append('limit', serversPerPage.toString());
const data = await apiGet(`/servers?${params.toString()}`);
const data = await apiGet('/servers');
// Update last fetch time
lastFetchTimeRef.current = Date.now();
if (data && data.success && Array.isArray(data.data)) {
setServers(data.data);
// Update pagination info if available
if (data.pagination) {
setPagination(data.pagination);
} else {
setPagination(null);
}
} else if (data && Array.isArray(data)) {
// Compatibility handling for non-paginated responses
setServers(data);
setPagination(null);
} else {
console.error('Invalid server data format:', data);
setServers([]);
setPagination(null);
}
// Reset error state
@@ -145,7 +114,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// Set up regular polling
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
},
[t, currentPage, serversPerPage],
[t],
);
// Watch for authentication status changes
@@ -181,11 +150,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const fetchInitialData = async () => {
try {
console.log('[ServerContext] Initial fetch - attempt', attemptsRef.current + 1);
// Build query parameters for pagination
const params = new URLSearchParams();
params.append('page', currentPage.toString());
params.append('limit', serversPerPage.toString());
const data = await apiGet(`/servers?${params.toString()}`);
const data = await apiGet('/servers');
// Update last fetch time
lastFetchTimeRef.current = Date.now();
@@ -193,12 +158,6 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// Handle API response wrapper object, extract data field
if (data && data.success && Array.isArray(data.data)) {
setServers(data.data);
// Update pagination info if available
if (data.pagination) {
setPagination(data.pagination);
} else {
setPagination(null);
}
setIsInitialLoading(false);
// Initialization successful, start normal polling (skip immediate to avoid duplicate fetch)
startNormalPolling({ immediate: false });
@@ -206,7 +165,6 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
} else if (data && Array.isArray(data)) {
// Compatibility handling, if API directly returns array
setServers(data);
setPagination(null);
setIsInitialLoading(false);
// Initialization successful, start normal polling (skip immediate to avoid duplicate fetch)
startNormalPolling({ immediate: false });
@@ -215,7 +173,6 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// If data format is not as expected, set to empty array
console.error('Invalid server data format:', data);
setServers([]);
setPagination(null);
setIsInitialLoading(false);
// Initialization successful but data is empty, start normal polling (skip immediate)
startNormalPolling({ immediate: false });
@@ -270,7 +227,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
return () => {
clearTimer();
};
}, [refreshKey, t, isInitialLoading, startNormalPolling, currentPage, serversPerPage]);
}, [refreshKey, t, isInitialLoading, startNormalPolling]);
// Manually trigger refresh (always refreshes)
const triggerRefresh = useCallback(() => {
@@ -426,28 +383,12 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
[t, triggerRefresh],
);
// Handle page change
const handlePageChange = useCallback((page: number) => {
setCurrentPage(page);
}, []);
// Handle servers per page change
const handleServersPerPageChange = useCallback((limit: number) => {
setServersPerPage(limit);
setCurrentPage(1); // Reset to first page when changing page size
}, []);
const value: ServerContextType = {
servers,
error,
setError,
isLoading: isInitialLoading,
fetchAttempts,
pagination,
currentPage,
serversPerPage,
setCurrentPage: handlePageChange,
setServersPerPage: handleServersPerPageChange,
triggerRefresh,
refreshIfNeeded,
handleServerAdd,

View File

@@ -8,7 +8,6 @@ import EditServerForm from '@/components/EditServerForm';
import { useServerData } from '@/hooks/useServerData';
import DxtUploadForm from '@/components/DxtUploadForm';
import JSONImportForm from '@/components/JSONImportForm';
import Pagination from '@/components/ui/Pagination';
const ServersPage: React.FC = () => {
const { t } = useTranslation();
@@ -18,11 +17,6 @@ const ServersPage: React.FC = () => {
error,
setError,
isLoading,
pagination,
currentPage,
serversPerPage,
setCurrentPage,
setServersPerPage,
handleServerAdd,
handleServerEdit,
handleServerRemove,
@@ -157,66 +151,19 @@ const ServersPage: React.FC = () => {
<p className="text-gray-600">{t('app.noServers')}</p>
</div>
) : (
<>
<div className="space-y-6">
{servers.map((server, index) => (
<ServerCard
key={index}
server={server}
onRemove={handleServerRemove}
onEdit={handleEditClick}
onToggle={handleServerToggle}
onRefresh={triggerRefresh}
onReload={handleServerReload}
/>
))}
</div>
<div className="flex items-center mb-4">
<div className="flex-[2] text-sm text-gray-500">
{pagination ? (
t('common.showing', {
start: (pagination.page - 1) * pagination.limit + 1,
end: Math.min(pagination.page * pagination.limit, pagination.total),
total: pagination.total
})
) : (
t('common.showing', {
start: 1,
end: servers.length,
total: servers.length
})
)}
</div>
<div className="flex-[4] flex justify-center">
{pagination && pagination.totalPages > 1 && (
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
onPageChange={setCurrentPage}
disabled={isLoading}
/>
)}
</div>
<div className="flex-[2] flex items-center justify-end space-x-2">
<label htmlFor="perPage" className="text-sm text-gray-600">
{t('common.itemsPerPage')}:
</label>
<select
id="perPage"
value={serversPerPage}
onChange={(e) => setServersPerPage(Number(e.target.value))}
disabled={isLoading}
className="border rounded p-1 text-sm btn-secondary outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</div>
</>
<div className="space-y-6">
{servers.map((server, index) => (
<ServerCard
key={index}
server={server}
onRemove={handleServerRemove}
onEdit={handleEditClick}
onToggle={handleServerToggle}
onRefresh={triggerRefresh}
onReload={handleServerReload}
/>
))}
</div>
)}
{editingServer && (

View File

@@ -105,17 +105,6 @@ export interface Prompt {
enabled?: boolean;
}
// Proxychains4 configuration for STDIO servers (Linux/macOS only)
export interface ProxychainsConfig {
enabled?: boolean; // Enable/disable proxychains4 proxy routing
type?: 'socks4' | 'socks5' | 'http'; // Proxy protocol type
host?: string; // Proxy server hostname or IP address
port?: number; // Proxy server port
username?: string; // Proxy authentication username (optional)
password?: string; // Proxy authentication password (optional)
configPath?: string; // Path to custom proxychains4 configuration file (optional)
}
// Server config types
export interface ServerConfig {
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi';
@@ -134,8 +123,6 @@ export interface ServerConfig {
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
maxTotalTimeout?: number; // Maximum total timeout in milliseconds
}; // MCP request options configuration
// Proxychains4 proxy configuration for STDIO servers (Linux/macOS only, Windows not supported)
proxy?: ProxychainsConfig;
// OAuth authentication for upstream MCP servers
oauth?: {
clientId?: string; // OAuth client ID

View File

@@ -248,10 +248,6 @@
"wechat": "WeChat",
"discord": "Discord",
"required": "Required",
"itemsPerPage": "Items per page",
"showing": "Showing {{start}}-{{end}} of {{total}}",
"previous": "Previous",
"next": "Next",
"secret": "Secret",
"default": "Default",
"value": "Value",

View File

@@ -248,10 +248,6 @@
"github": "GitHub",
"wechat": "WeChat",
"discord": "Discord",
"itemsPerPage": "Éléments par page",
"showing": "Affichage de {{start}}-{{end}} sur {{total}}",
"previous": "Précédent",
"next": "Suivant",
"required": "Requis",
"secret": "Secret",
"default": "Défaut",

View File

@@ -248,10 +248,6 @@
"github": "GitHub",
"wechat": "WeChat",
"discord": "Discord",
"itemsPerPage": "Sayfa başına öğe",
"showing": "{{total}} öğeden {{start}}-{{end}} gösteriliyor",
"previous": "Önceki",
"next": "Sonraki",
"required": "Gerekli",
"secret": "Gizli",
"default": "Varsayılan",

View File

@@ -248,10 +248,6 @@
"dismiss": "忽略",
"github": "GitHub",
"wechat": "微信",
"itemsPerPage": "每页显示",
"showing": "显示第 {{start}}-{{end}} 条,共 {{total}} 条",
"previous": "上一页",
"next": "下一页",
"discord": "Discord",
"required": "必填",
"secret": "敏感",

View File

@@ -7,7 +7,6 @@ import {
BatchCreateServersResponse,
BatchServerResult,
ServerConfig,
ServerInfo,
} from '../types/index.js';
import {
getServersInfo,
@@ -25,66 +24,13 @@ import { createSafeJSON } from '../utils/serialization.js';
import { cloneDefaultOAuthServerConfig } from '../constants/oauthServerDefaults.js';
import { getServerDao, getGroupDao, getSystemConfigDao } from '../dao/DaoFactory.js';
import { getBearerKeyDao } from '../dao/DaoFactory.js';
import { UserContextService } from '../services/userContextService.js';
export const getAllServers = async (req: Request, res: Response): Promise<void> => {
export const getAllServers = async (_: Request, res: Response): Promise<void> => {
try {
// Parse pagination parameters from query string
const page = req.query.page ? parseInt(req.query.page as string, 10) : 1;
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
// Validate pagination parameters
if (page < 1) {
res.status(400).json({
success: false,
message: 'Page number must be greater than 0',
});
return;
}
if (limit !== undefined && (limit < 1 || limit > 1000)) {
res.status(400).json({
success: false,
message: 'Limit must be between 1 and 1000',
});
return;
}
// Get current user for filtering
const currentUser = UserContextService.getInstance().getCurrentUser();
const isAdmin = !currentUser || currentUser.isAdmin;
// Get servers info with pagination if limit is specified
let serversInfo: Omit<ServerInfo, 'client' | 'transport'>[];
let pagination = undefined;
if (limit !== undefined) {
// Use DAO layer pagination with proper filtering
const serverDao = getServerDao();
const paginatedResult = isAdmin
? await serverDao.findAllPaginated(page, limit)
: await serverDao.findByOwnerPaginated(currentUser!.username, page, limit);
// Get runtime info for paginated servers
serversInfo = await getServersInfo(page, limit, currentUser);
pagination = {
page: paginatedResult.page,
limit: paginatedResult.limit,
total: paginatedResult.total,
totalPages: paginatedResult.totalPages,
hasNextPage: paginatedResult.page < paginatedResult.totalPages,
hasPrevPage: paginatedResult.page > 1,
};
} else {
// No pagination, get all servers (will be filtered by mcpService)
serversInfo = await getServersInfo();
}
const serversInfo = await getServersInfo();
const response: ApiResponse = {
success: true,
data: createSafeJSON(serversInfo),
...(pagination && { pagination }),
};
res.json(response);
} catch (error) {
@@ -618,9 +564,10 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
});
}
} catch (error) {
console.error('Failed to update server:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
message: error instanceof Error ? error.message : 'Internal server error',
});
}
};

View File

@@ -6,6 +6,7 @@ import { UserConfigDao, UserConfigDaoImpl } from './UserConfigDao.js';
import { OAuthClientDao, OAuthClientDaoImpl } from './OAuthClientDao.js';
import { OAuthTokenDao, OAuthTokenDaoImpl } from './OAuthTokenDao.js';
import { BearerKeyDao, BearerKeyDaoImpl } from './BearerKeyDao.js';
import { ToolCallActivityDao } from './ToolCallActivityDao.js';
/**
* DAO Factory interface for creating DAO instances
@@ -19,6 +20,7 @@ export interface DaoFactory {
getOAuthClientDao(): OAuthClientDao;
getOAuthTokenDao(): OAuthTokenDao;
getBearerKeyDao(): BearerKeyDao;
getToolCallActivityDao(): ToolCallActivityDao | null; // Only available in DB mode
}
/**
@@ -106,6 +108,11 @@ export class JsonFileDaoFactory implements DaoFactory {
return this.bearerKeyDao;
}
getToolCallActivityDao(): ToolCallActivityDao | null {
// Tool call activity is only available in DB mode
return null;
}
/**
* Reset all cached DAO instances (useful for testing)
*/
@@ -194,3 +201,14 @@ export function getOAuthTokenDao(): OAuthTokenDao {
export function getBearerKeyDao(): BearerKeyDao {
return getDaoFactory().getBearerKeyDao();
}
export function getToolCallActivityDao(): ToolCallActivityDao | null {
return getDaoFactory().getToolCallActivityDao();
}
/**
* Check if the application is using database mode
*/
export function isUsingDatabase(): boolean {
return getDaoFactory().getToolCallActivityDao() !== null;
}

View File

@@ -8,6 +8,7 @@ import {
OAuthClientDao,
OAuthTokenDao,
BearerKeyDao,
ToolCallActivityDao,
} from './index.js';
import { UserDaoDbImpl } from './UserDaoDbImpl.js';
import { ServerDaoDbImpl } from './ServerDaoDbImpl.js';
@@ -17,6 +18,7 @@ import { UserConfigDaoDbImpl } from './UserConfigDaoDbImpl.js';
import { OAuthClientDaoDbImpl } from './OAuthClientDaoDbImpl.js';
import { OAuthTokenDaoDbImpl } from './OAuthTokenDaoDbImpl.js';
import { BearerKeyDaoDbImpl } from './BearerKeyDaoDbImpl.js';
import { ToolCallActivityDaoDbImpl } from './ToolCallActivityDao.js';
/**
* Database-backed DAO factory implementation
@@ -32,6 +34,7 @@ export class DatabaseDaoFactory implements DaoFactory {
private oauthClientDao: OAuthClientDao | null = null;
private oauthTokenDao: OAuthTokenDao | null = null;
private bearerKeyDao: BearerKeyDao | null = null;
private toolCallActivityDao: ToolCallActivityDao | null = null;
/**
* Get singleton instance
@@ -103,6 +106,13 @@ export class DatabaseDaoFactory implements DaoFactory {
return this.bearerKeyDao!;
}
getToolCallActivityDao(): ToolCallActivityDao | null {
if (!this.toolCallActivityDao) {
this.toolCallActivityDao = new ToolCallActivityDaoDbImpl();
}
return this.toolCallActivityDao;
}
/**
* Reset all cached DAO instances (useful for testing)
*/
@@ -115,5 +125,6 @@ export class DatabaseDaoFactory implements DaoFactory {
this.oauthClientDao = null;
this.oauthTokenDao = null;
this.bearerKeyDao = null;
this.toolCallActivityDao = null;
}
}

View File

@@ -2,31 +2,10 @@ import { ServerConfig } from '../types/index.js';
import { BaseDao } from './base/BaseDao.js';
import { JsonFileBaseDao } from './base/JsonFileBaseDao.js';
/**
* Pagination result interface
*/
export interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
/**
* Server DAO interface with server-specific operations
*/
export interface ServerDao extends BaseDao<ServerConfigWithName, string> {
/**
* Find all servers with pagination
*/
findAllPaginated(page: number, limit: number): Promise<PaginatedResult<ServerConfigWithName>>;
/**
* Find servers by owner with pagination
*/
findByOwnerPaginated(owner: string, page: number, limit: number): Promise<PaginatedResult<ServerConfigWithName>>;
/**
* Find servers by owner
*/
@@ -197,61 +176,6 @@ export class ServerDaoImpl extends JsonFileBaseDao implements ServerDao {
return servers.length;
}
async findAllPaginated(page: number, limit: number): Promise<PaginatedResult<ServerConfigWithName>> {
const allServers = await this.getAll();
// Sort: enabled servers first, then by creation time
const sortedServers = allServers.sort((a, b) => {
const aEnabled = a.enabled !== false;
const bEnabled = b.enabled !== false;
if (aEnabled !== bEnabled) {
return aEnabled ? -1 : 1;
}
return 0; // Keep original order for same enabled status
});
const total = sortedServers.length;
const totalPages = Math.ceil(total / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const data = sortedServers.slice(startIndex, endIndex);
return {
data,
total,
page,
limit,
totalPages,
};
}
async findByOwnerPaginated(owner: string, page: number, limit: number): Promise<PaginatedResult<ServerConfigWithName>> {
const allServers = await this.getAll();
const filteredServers = allServers.filter((server) => server.owner === owner);
// Sort: enabled servers first, then by creation time
const sortedServers = filteredServers.sort((a, b) => {
const aEnabled = a.enabled !== false;
const bEnabled = b.enabled !== false;
if (aEnabled !== bEnabled) {
return aEnabled ? -1 : 1;
}
return 0; // Keep original order for same enabled status
});
const total = sortedServers.length;
const totalPages = Math.ceil(total / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const data = sortedServers.slice(startIndex, endIndex);
return {
data,
total,
page,
limit,
totalPages,
};
}
async findByOwner(owner: string): Promise<ServerConfigWithName[]> {
const servers = await this.getAll();
return servers.filter((server) => server.owner === owner);

View File

@@ -1,4 +1,4 @@
import { ServerDao, ServerConfigWithName, PaginatedResult } from './index.js';
import { ServerDao, ServerConfigWithName } from './index.js';
import { ServerRepository } from '../db/repositories/ServerRepository.js';
/**
@@ -16,32 +16,6 @@ export class ServerDaoDbImpl implements ServerDao {
return servers.map((s) => this.mapToServerConfig(s));
}
async findAllPaginated(page: number, limit: number): Promise<PaginatedResult<ServerConfigWithName>> {
const { data, total } = await this.repository.findAllPaginated(page, limit);
const totalPages = Math.ceil(total / limit);
return {
data: data.map((s) => this.mapToServerConfig(s)),
total,
page,
limit,
totalPages,
};
}
async findByOwnerPaginated(owner: string, page: number, limit: number): Promise<PaginatedResult<ServerConfigWithName>> {
const { data, total } = await this.repository.findByOwnerPaginated(owner, page, limit);
const totalPages = Math.ceil(total / limit);
return {
data: data.map((s) => this.mapToServerConfig(s)),
total,
page,
limit,
totalPages,
};
}
async findById(name: string): Promise<ServerConfigWithName | null> {
const server = await this.repository.findByName(name);
return server ? this.mapToServerConfig(server) : null;
@@ -64,7 +38,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts: entity.prompts,
options: entity.options,
oauth: entity.oauth,
proxy: entity.proxy,
openapi: entity.openapi,
});
return this.mapToServerConfig(server);
@@ -89,7 +62,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts: entity.prompts,
options: entity.options,
oauth: entity.oauth,
proxy: entity.proxy,
openapi: entity.openapi,
});
return server ? this.mapToServerConfig(server) : null;
@@ -168,7 +140,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts?: Record<string, { enabled: boolean; description?: string }>;
options?: Record<string, any>;
oauth?: Record<string, any>;
proxy?: Record<string, any>;
openapi?: Record<string, any>;
}): ServerConfigWithName {
return {
@@ -187,7 +158,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts: server.prompts,
options: server.options,
oauth: server.oauth,
proxy: server.proxy,
openapi: server.openapi,
};
}

View File

@@ -0,0 +1,186 @@
import {
IToolCallActivity,
IToolCallActivitySearchParams,
IToolCallActivityPage,
IToolCallActivityStats,
} from '../types/index.js';
import { ToolCallActivityRepository } from '../db/repositories/ToolCallActivityRepository.js';
/**
* Tool Call Activity DAO interface (DB mode only)
*/
export interface ToolCallActivityDao {
/**
* Create a new tool call activity
*/
create(activity: Omit<IToolCallActivity, 'id' | 'createdAt'>): Promise<IToolCallActivity>;
/**
* Find activity by ID
*/
findById(id: string): Promise<IToolCallActivity | null>;
/**
* Update an existing activity
*/
update(id: string, updates: Partial<IToolCallActivity>): Promise<IToolCallActivity | null>;
/**
* Delete an activity
*/
delete(id: string): Promise<boolean>;
/**
* Find activities with pagination and filtering
*/
findWithPagination(
page: number,
pageSize: number,
params?: IToolCallActivitySearchParams,
): Promise<IToolCallActivityPage>;
/**
* Get recent activities
*/
findRecent(limit: number): Promise<IToolCallActivity[]>;
/**
* Get activity statistics
*/
getStats(): Promise<IToolCallActivityStats>;
/**
* Delete old activities (cleanup)
*/
deleteOlderThan(date: Date): Promise<number>;
/**
* Count total activities
*/
count(): Promise<number>;
}
/**
* Database-backed implementation of ToolCallActivityDao
*/
export class ToolCallActivityDaoDbImpl implements ToolCallActivityDao {
private repository: ToolCallActivityRepository;
constructor() {
this.repository = new ToolCallActivityRepository();
}
async create(activity: Omit<IToolCallActivity, 'id' | 'createdAt'>): Promise<IToolCallActivity> {
const created = await this.repository.create({
serverName: activity.serverName,
toolName: activity.toolName,
keyId: activity.keyId,
keyName: activity.keyName,
status: activity.status,
request: activity.request,
response: activity.response,
errorMessage: activity.errorMessage,
durationMs: activity.durationMs,
clientIp: activity.clientIp,
sessionId: activity.sessionId,
groupName: activity.groupName,
});
return this.mapToInterface(created);
}
async findById(id: string): Promise<IToolCallActivity | null> {
const activity = await this.repository.findById(id);
return activity ? this.mapToInterface(activity) : null;
}
async update(
id: string,
updates: Partial<IToolCallActivity>,
): Promise<IToolCallActivity | null> {
const updated = await this.repository.update(id, {
serverName: updates.serverName,
toolName: updates.toolName,
keyId: updates.keyId,
keyName: updates.keyName,
status: updates.status,
request: updates.request,
response: updates.response,
errorMessage: updates.errorMessage,
durationMs: updates.durationMs,
clientIp: updates.clientIp,
sessionId: updates.sessionId,
groupName: updates.groupName,
});
return updated ? this.mapToInterface(updated) : null;
}
async delete(id: string): Promise<boolean> {
return await this.repository.delete(id);
}
async findWithPagination(
page: number = 1,
pageSize: number = 20,
params: IToolCallActivitySearchParams = {},
): Promise<IToolCallActivityPage> {
const result = await this.repository.findWithPagination(page, pageSize, params);
return {
items: result.items.map((item) => this.mapToInterface(item)),
total: result.total,
page: result.page,
pageSize: result.pageSize,
totalPages: result.totalPages,
};
}
async findRecent(limit: number = 10): Promise<IToolCallActivity[]> {
const activities = await this.repository.findRecent(limit);
return activities.map((activity) => this.mapToInterface(activity));
}
async getStats(): Promise<IToolCallActivityStats> {
return await this.repository.getStats();
}
async deleteOlderThan(date: Date): Promise<number> {
return await this.repository.deleteOlderThan(date);
}
async count(): Promise<number> {
return await this.repository.count();
}
private mapToInterface(activity: {
id: string;
serverName: string;
toolName: string;
keyId?: string;
keyName?: string;
status: 'pending' | 'success' | 'error';
request?: string;
response?: string;
errorMessage?: string;
durationMs?: number;
clientIp?: string;
sessionId?: string;
groupName?: string;
createdAt: Date;
}): IToolCallActivity {
return {
id: activity.id,
serverName: activity.serverName,
toolName: activity.toolName,
keyId: activity.keyId,
keyName: activity.keyName,
status: activity.status,
request: activity.request,
response: activity.response,
errorMessage: activity.errorMessage,
durationMs: activity.durationMs,
clientIp: activity.clientIp,
sessionId: activity.sessionId,
groupName: activity.groupName,
createdAt: activity.createdAt,
};
}
}

View File

@@ -9,6 +9,7 @@ export * from './UserConfigDao.js';
export * from './OAuthClientDao.js';
export * from './OAuthTokenDao.js';
export * from './BearerKeyDao.js';
export * from './ToolCallActivityDao.js';
// Export database implementations
export * from './UserDaoDbImpl.js';

View File

@@ -59,9 +59,6 @@ export class Server {
@Column({ type: 'simple-json', nullable: true })
oauth?: Record<string, any>;
@Column({ type: 'simple-json', nullable: true })
proxy?: Record<string, any>;
@Column({ type: 'simple-json', nullable: true })
openapi?: Record<string, any>;

View File

@@ -0,0 +1,62 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
Index,
} from 'typeorm';
/**
* Tool call activity entity for logging tool invocations (DB mode only)
*/
@Entity({ name: 'tool_call_activities' })
export class ToolCallActivity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ type: 'varchar', length: 255, name: 'server_name' })
serverName: string;
@Index()
@Column({ type: 'varchar', length: 255, name: 'tool_name' })
toolName: string;
@Index()
@Column({ type: 'varchar', length: 255, name: 'key_id', nullable: true })
keyId?: string;
@Column({ type: 'varchar', length: 255, name: 'key_name', nullable: true })
keyName?: string;
@Index()
@Column({ type: 'varchar', length: 50, default: 'pending' })
status: 'pending' | 'success' | 'error';
@Column({ type: 'text', nullable: true })
request?: string;
@Column({ type: 'text', nullable: true })
response?: string;
@Column({ type: 'text', name: 'error_message', nullable: true })
errorMessage?: string;
@Column({ type: 'int', name: 'duration_ms', nullable: true })
durationMs?: number;
@Column({ type: 'varchar', length: 100, name: 'client_ip', nullable: true })
clientIp?: string;
@Column({ type: 'varchar', length: 255, name: 'session_id', nullable: true })
sessionId?: string;
@Column({ type: 'varchar', length: 255, name: 'group_name', nullable: true })
groupName?: string;
@Index()
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt: Date;
}
export default ToolCallActivity;

View File

@@ -7,6 +7,7 @@ import UserConfig from './UserConfig.js';
import OAuthClient from './OAuthClient.js';
import OAuthToken from './OAuthToken.js';
import BearerKey from './BearerKey.js';
import ToolCallActivity from './ToolCallActivity.js';
// Export all entities
export default [
@@ -19,6 +20,7 @@ export default [
OAuthClient,
OAuthToken,
BearerKey,
ToolCallActivity,
];
// Export individual entities for direct use
@@ -32,4 +34,5 @@ export {
OAuthClient,
OAuthToken,
BearerKey,
ToolCallActivity,
};

View File

@@ -69,41 +69,6 @@ export class ServerRepository {
return await this.repository.count();
}
/**
* Find servers with pagination
*/
async findAllPaginated(page: number, limit: number): Promise<{ data: Server[]; total: number }> {
const skip = (page - 1) * limit;
const [data, total] = await this.repository.findAndCount({
order: {
enabled: 'DESC', // Enabled servers first
createdAt: 'ASC' // Then by creation time
},
skip,
take: limit,
});
return { data, total };
}
/**
* Find servers by owner with pagination
*/
async findByOwnerPaginated(owner: string, page: number, limit: number): Promise<{ data: Server[]; total: number }> {
const skip = (page - 1) * limit;
const [data, total] = await this.repository.findAndCount({
where: { owner },
order: {
enabled: 'DESC', // Enabled servers first
createdAt: 'ASC' // Then by creation time
},
skip,
take: limit,
});
return { data, total };
}
/**
* Find servers by owner
*/

View File

@@ -0,0 +1,200 @@
import { Repository, FindOptionsWhere, ILike, Between } from 'typeorm';
import { ToolCallActivity } from '../entities/ToolCallActivity.js';
import { getAppDataSource } from '../connection.js';
/**
* Search parameters for filtering tool call activities
*/
export interface ToolCallActivitySearchParams {
serverName?: string;
toolName?: string;
keyId?: string;
status?: 'pending' | 'success' | 'error';
groupName?: string;
startDate?: Date;
endDate?: Date;
searchQuery?: string;
}
/**
* Pagination result for tool call activities
*/
export interface ToolCallActivityPage {
items: ToolCallActivity[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/**
* Repository for ToolCallActivity entity
*/
export class ToolCallActivityRepository {
private repository: Repository<ToolCallActivity>;
constructor() {
this.repository = getAppDataSource().getRepository(ToolCallActivity);
}
/**
* Create a new tool call activity
*/
async create(
activity: Omit<ToolCallActivity, 'id' | 'createdAt'>,
): Promise<ToolCallActivity> {
const newActivity = this.repository.create(activity);
return await this.repository.save(newActivity);
}
/**
* Find activity by ID
*/
async findById(id: string): Promise<ToolCallActivity | null> {
return await this.repository.findOne({ where: { id } });
}
/**
* Update an existing activity
*/
async update(
id: string,
updates: Partial<ToolCallActivity>,
): Promise<ToolCallActivity | null> {
const activity = await this.findById(id);
if (!activity) {
return null;
}
const updated = this.repository.merge(activity, updates);
return await this.repository.save(updated);
}
/**
* Delete an activity
*/
async delete(id: string): Promise<boolean> {
const result = await this.repository.delete({ id });
return (result.affected ?? 0) > 0;
}
/**
* Find activities with pagination and filtering
*/
async findWithPagination(
page: number = 1,
pageSize: number = 20,
params: ToolCallActivitySearchParams = {},
): Promise<ToolCallActivityPage> {
const where: FindOptionsWhere<ToolCallActivity>[] = [];
const baseWhere: FindOptionsWhere<ToolCallActivity> = {};
// Add filters
if (params.serverName) {
baseWhere.serverName = params.serverName;
}
if (params.toolName) {
baseWhere.toolName = params.toolName;
}
if (params.keyId) {
baseWhere.keyId = params.keyId;
}
if (params.status) {
baseWhere.status = params.status;
}
if (params.groupName) {
baseWhere.groupName = params.groupName;
}
if (params.startDate && params.endDate) {
baseWhere.createdAt = Between(params.startDate, params.endDate);
}
// Handle search query - search across multiple fields
if (params.searchQuery) {
const searchPattern = `%${params.searchQuery}%`;
where.push(
{ ...baseWhere, serverName: ILike(searchPattern) },
{ ...baseWhere, toolName: ILike(searchPattern) },
{ ...baseWhere, keyName: ILike(searchPattern) },
{ ...baseWhere, groupName: ILike(searchPattern) },
);
} else {
where.push(baseWhere);
}
const [items, total] = await this.repository.findAndCount({
where: where.length > 0 ? where : undefined,
order: { createdAt: 'DESC' },
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
/**
* Get recent activities
*/
async findRecent(limit: number = 10): Promise<ToolCallActivity[]> {
return await this.repository.find({
order: { createdAt: 'DESC' },
take: limit,
});
}
/**
* Get activity statistics
*/
async getStats(): Promise<{
total: number;
success: number;
error: number;
pending: number;
avgDurationMs: number;
}> {
const stats = await this.repository
.createQueryBuilder('activity')
.select([
'COUNT(*) as total',
'SUM(CASE WHEN status = \'success\' THEN 1 ELSE 0 END) as success',
'SUM(CASE WHEN status = \'error\' THEN 1 ELSE 0 END) as error',
'SUM(CASE WHEN status = \'pending\' THEN 1 ELSE 0 END) as pending',
'AVG(duration_ms) as avgDurationMs',
])
.getRawOne();
return {
total: parseInt(stats?.total || '0', 10),
success: parseInt(stats?.success || '0', 10),
error: parseInt(stats?.error || '0', 10),
pending: parseInt(stats?.pending || '0', 10),
avgDurationMs: parseFloat(stats?.avgDurationMs || '0'),
};
}
/**
* Delete old activities (cleanup)
*/
async deleteOlderThan(date: Date): Promise<number> {
const result = await this.repository
.createQueryBuilder()
.delete()
.where('created_at < :date', { date })
.execute();
return result.affected ?? 0;
}
/**
* Count total activities
*/
async count(): Promise<number> {
return await this.repository.count();
}
}
export default ToolCallActivityRepository;

View File

@@ -7,6 +7,7 @@ import { UserConfigRepository } from './UserConfigRepository.js';
import { OAuthClientRepository } from './OAuthClientRepository.js';
import { OAuthTokenRepository } from './OAuthTokenRepository.js';
import { BearerKeyRepository } from './BearerKeyRepository.js';
import { ToolCallActivityRepository } from './ToolCallActivityRepository.js';
// Export all repositories
export {
@@ -19,4 +20,5 @@ export {
OAuthClientRepository,
OAuthTokenRepository,
BearerKeyRepository,
ToolCallActivityRepository,
};

View File

@@ -1,6 +1,4 @@
import os from 'os';
import path from 'path';
import fs from 'fs';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
@@ -17,7 +15,7 @@ import {
StreamableHTTPClientTransportOptions,
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { createFetchWithProxy, getProxyConfigFromEnv } from './proxy.js';
import { ServerInfo, ServerConfig, Tool, ProxychainsConfig } from '../types/index.js';
import { ServerInfo, ServerConfig, Tool } from '../types/index.js';
import { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js';
import config from '../config/index.js';
import { getGroup } from './sseService.js';
@@ -34,150 +32,6 @@ const servers: { [sessionId: string]: Server } = {};
import { setupClientKeepAlive } from './keepAliveService.js';
/**
* Check if proxychains4 is available on the system (Linux/macOS only).
* Returns the path to proxychains4 if found, null otherwise.
*/
const findProxychains4 = (): string | null => {
// Windows is not supported
if (process.platform === 'win32') {
return null;
}
// Common proxychains4 binary paths
const possiblePaths = [
'/usr/bin/proxychains4',
'/usr/local/bin/proxychains4',
'/opt/homebrew/bin/proxychains4', // macOS Homebrew ARM
'/usr/local/Cellar/proxychains-ng/*/bin/proxychains4', // macOS Homebrew Intel
];
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
return p;
}
}
// Try to find in PATH
const pathEnv = process.env.PATH || '';
const pathDirs = pathEnv.split(path.delimiter);
for (const dir of pathDirs) {
const fullPath = path.join(dir, 'proxychains4');
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
};
/**
* Generate a temporary proxychains4 configuration file.
* Returns the path to the generated config file.
*/
const generateProxychainsConfig = (
serverName: string,
proxyConfig: ProxychainsConfig,
): string | null => {
// If a custom config path is provided, use it directly
if (proxyConfig.configPath) {
if (fs.existsSync(proxyConfig.configPath)) {
return proxyConfig.configPath;
}
console.warn(
`[${serverName}] Custom proxychains config not found: ${proxyConfig.configPath}`,
);
return null;
}
// Validate required fields
if (!proxyConfig.host || !proxyConfig.port) {
console.warn(`[${serverName}] Proxy host and port are required for proxychains4`);
return null;
}
const proxyType = proxyConfig.type || 'socks5';
const proxyLine = proxyConfig.username && proxyConfig.password
? `${proxyType} ${proxyConfig.host} ${proxyConfig.port} ${proxyConfig.username} ${proxyConfig.password}`
: `${proxyType} ${proxyConfig.host} ${proxyConfig.port}`;
const configContent = `# Proxychains4 configuration for MCP server: ${serverName}
# Generated by MCPHub
strict_chain
proxy_dns
remote_dns_subnet 224
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
${proxyLine}
`;
// Create temp directory if needed
const tempDir = path.join(os.tmpdir(), 'mcphub-proxychains');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Write config file
const configPath = path.join(tempDir, `${serverName.replace(/[^a-zA-Z0-9-_]/g, '_')}.conf`);
fs.writeFileSync(configPath, configContent, 'utf-8');
console.log(`[${serverName}] Generated proxychains4 config: ${configPath}`);
return configPath;
};
/**
* Wrap a command with proxychains4 if proxy is configured and available.
* Returns modified command and args if proxychains4 is used, original values otherwise.
*/
const wrapWithProxychains = (
serverName: string,
command: string,
args: string[],
proxyConfig?: ProxychainsConfig,
): { command: string; args: string[] } => {
// Skip if proxy is not enabled or not configured
if (!proxyConfig?.enabled) {
return { command, args };
}
// Check platform - Windows is not supported
if (process.platform === 'win32') {
console.warn(
`[${serverName}] proxychains4 proxy is not supported on Windows, ignoring proxy configuration`,
);
return { command, args };
}
// Find proxychains4 binary
const proxychains4Path = findProxychains4();
if (!proxychains4Path) {
console.warn(
`[${serverName}] proxychains4 not found on system, install it with: apt install proxychains4 (Debian/Ubuntu) or brew install proxychains-ng (macOS)`,
);
return { command, args };
}
// Generate or get config file
const configPath = generateProxychainsConfig(serverName, proxyConfig);
if (!configPath) {
console.warn(`[${serverName}] Failed to setup proxychains4 configuration, skipping proxy`);
return { command, args };
}
// Wrap command with proxychains4
console.log(
`[${serverName}] Using proxychains4 proxy: ${proxyConfig.type || 'socks5'}://${proxyConfig.host}:${proxyConfig.port}`,
);
return {
command: proxychains4Path,
args: ['-f', configPath, command, ...args],
};
};
export const initUpstreamServers = async (): Promise<void> => {
// Initialize OAuth clients for servers with dynamic registration
await initializeAllOAuthClients();
@@ -355,19 +209,11 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
env['npm_config_registry'] = systemConfig.install.npmRegistry;
}
// Apply proxychains4 wrapper if proxy is configured (Linux/macOS only)
const { command: finalCommand, args: finalArgs } = wrapWithProxychains(
name,
conf.command,
replaceEnvVars(conf.args) as string[],
conf.proxy,
);
// Create STDIO transport with potentially wrapped command
// Expand environment variables in command
transport = new StdioClientTransport({
cwd: os.homedir(),
command: finalCommand,
args: finalArgs,
command: conf.command,
args: replaceEnvVars(conf.args) as string[],
env: env,
stderr: 'pipe',
});
@@ -772,19 +618,9 @@ export const registerAllTools = async (isInit: boolean, serverName?: string): Pr
};
// Get all server information
export const getServersInfo = async (
page?: number,
limit?: number,
user?: any,
): Promise<Omit<ServerInfo, 'client' | 'transport'>[]> => {
export const getServersInfo = async (): Promise<Omit<ServerInfo, 'client' | 'transport'>[]> => {
const allServers: ServerConfigWithName[] = await getServerDao().findAll();
const dataService = getDataService();
// Get paginated or all server configurations from DAO
// If pagination is used with a non-admin user, filtering is already done at DAO level
const isPaginated = limit !== undefined && page !== undefined;
const allServers: ServerConfigWithName[] = isPaginated
? (await getServerDao().findAllPaginated(page, limit)).data
: await getServerDao().findAll();
// Ensure that servers recently added via DAO but not yet initialized in serverInfos
// are still visible in the servers list. This avoids a race condition where
@@ -793,19 +629,10 @@ export const getServersInfo = async (
const combinedServerInfos: ServerInfo[] = [...serverInfos];
const existingNames = new Set(combinedServerInfos.map((s) => s.name));
// Create a set of server names we're interested in (for pagination)
const requestedServerNames = new Set(allServers.map((s) => s.name));
// Filter serverInfos to only include requested servers if pagination is used
const filteredServerInfos = isPaginated
? combinedServerInfos.filter((s) => requestedServerNames.has(s.name))
: combinedServerInfos;
// Add servers from DAO that don't have runtime info yet
for (const server of allServers) {
if (!existingNames.has(server.name)) {
const isEnabled = server.enabled === undefined ? true : server.enabled;
filteredServerInfos.push({
combinedServerInfos.push({
name: server.name,
owner: server.owner,
// Newly created servers that are enabled should appear as "connecting"
@@ -821,16 +648,12 @@ export const getServersInfo = async (
}
}
// Apply user filtering only when NOT using pagination (pagination already filtered at DAO level)
// Or when no pagination parameters provided (backward compatibility)
const shouldApplyUserFilter = !isPaginated;
const filterServerInfos: ServerInfo[] = shouldApplyUserFilter && dataService.filterData
? dataService.filterData(filteredServerInfos, user)
: filteredServerInfos;
const filterServerInfos: ServerInfo[] = dataService.filterData
? dataService.filterData(combinedServerInfos)
: combinedServerInfos;
const infos = filterServerInfos
.filter((info) => requestedServerNames.has(info.name)) // Only include requested servers
.map(({ name, status, tools, prompts, createTime, error, oauth }) => {
const infos = filterServerInfos.map(
({ name, status, tools, prompts, createTime, error, oauth }) => {
const serverConfig = allServers.find((server) => server.name === name);
const enabled = serverConfig ? serverConfig.enabled !== false : true;
@@ -869,8 +692,12 @@ export const getServersInfo = async (
}
: undefined,
};
});
// Sorting is now handled at DAO layer for consistent pagination results
},
);
infos.sort((a, b) => {
if (a.enabled === b.enabled) return 0;
return a.enabled ? -1 : 1;
});
return infos;
};

View File

@@ -270,17 +270,6 @@ export interface McpSettings {
bearerKeys?: BearerKey[]; // Bearer authentication keys (multi-key configuration)
}
// Proxychains4 configuration for STDIO servers (Linux/macOS only)
export interface ProxychainsConfig {
enabled?: boolean; // Enable/disable proxychains4 proxy routing
type?: 'socks4' | 'socks5' | 'http'; // Proxy protocol type
host?: string; // Proxy server hostname or IP address
port?: number; // Proxy server port
username?: string; // Proxy authentication username (optional)
password?: string; // Proxy authentication password (optional)
configPath?: string; // Path to custom proxychains4 configuration file (optional, overrides above settings)
}
// Configuration details for an individual server
export interface ServerConfig {
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi'; // Type of server
@@ -296,8 +285,6 @@ export interface ServerConfig {
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
prompts?: Record<string, { enabled: boolean; description?: string }>; // Prompt-specific configurations with enable/disable state and custom descriptions
options?: Partial<Pick<RequestOptions, 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout'>>; // MCP request options configuration
// Proxychains4 proxy configuration for STDIO servers (Linux/macOS only, Windows not supported)
proxy?: ProxychainsConfig;
// OAuth authentication for upstream MCP servers
oauth?: {
// Static client configuration (traditional OAuth flow)
@@ -494,3 +481,51 @@ export interface BatchCreateGroupsResponse {
failureCount: number; // Number of groups that failed
results: BatchGroupResult[]; // Detailed results for each group
}
// Tool call activity interface for logging tool invocations (DB mode only)
export interface IToolCallActivity {
id?: string;
serverName: string;
toolName: string;
keyId?: string;
keyName?: string;
status: 'pending' | 'success' | 'error';
request?: string;
response?: string;
errorMessage?: string;
durationMs?: number;
clientIp?: string;
sessionId?: string;
groupName?: string;
createdAt?: Date;
}
// Tool call activity search parameters
export interface IToolCallActivitySearchParams {
serverName?: string;
toolName?: string;
keyId?: string;
status?: 'pending' | 'success' | 'error';
groupName?: string;
startDate?: Date;
endDate?: Date;
searchQuery?: string;
}
// Tool call activity pagination result
export interface IToolCallActivityPage {
items: IToolCallActivity[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Tool call activity statistics
export interface IToolCallActivityStats {
total: number;
success: number;
error: number;
pending: number;
avgDurationMs: number;
}