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
15 changed files with 535 additions and 318 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 {/* ### Custom Server Scripts
#### Local Python Server #### Local Python Server

View File

@@ -31,47 +31,6 @@
"DATABASE_URL": "${DATABASE_URL}" "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": { "example-openapi-server": {
"type": "openapi", "type": "openapi",
"openapi": { "openapi": {

View File

@@ -105,17 +105,6 @@ export interface Prompt {
enabled?: boolean; 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 // Server config types
export interface ServerConfig { export interface ServerConfig {
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi'; type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi';
@@ -134,8 +123,6 @@ export interface ServerConfig {
resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications resetTimeoutOnProgress?: boolean; // Reset timeout on progress notifications
maxTotalTimeout?: number; // Maximum total timeout in milliseconds maxTotalTimeout?: number; // Maximum total timeout in milliseconds
}; // MCP request options configuration }; // 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 authentication for upstream MCP servers
oauth?: { oauth?: {
clientId?: string; // OAuth client ID clientId?: string; // OAuth client ID

View File

@@ -6,6 +6,7 @@ import { UserConfigDao, UserConfigDaoImpl } from './UserConfigDao.js';
import { OAuthClientDao, OAuthClientDaoImpl } from './OAuthClientDao.js'; import { OAuthClientDao, OAuthClientDaoImpl } from './OAuthClientDao.js';
import { OAuthTokenDao, OAuthTokenDaoImpl } from './OAuthTokenDao.js'; import { OAuthTokenDao, OAuthTokenDaoImpl } from './OAuthTokenDao.js';
import { BearerKeyDao, BearerKeyDaoImpl } from './BearerKeyDao.js'; import { BearerKeyDao, BearerKeyDaoImpl } from './BearerKeyDao.js';
import { ToolCallActivityDao } from './ToolCallActivityDao.js';
/** /**
* DAO Factory interface for creating DAO instances * DAO Factory interface for creating DAO instances
@@ -19,6 +20,7 @@ export interface DaoFactory {
getOAuthClientDao(): OAuthClientDao; getOAuthClientDao(): OAuthClientDao;
getOAuthTokenDao(): OAuthTokenDao; getOAuthTokenDao(): OAuthTokenDao;
getBearerKeyDao(): BearerKeyDao; getBearerKeyDao(): BearerKeyDao;
getToolCallActivityDao(): ToolCallActivityDao | null; // Only available in DB mode
} }
/** /**
@@ -106,6 +108,11 @@ export class JsonFileDaoFactory implements DaoFactory {
return this.bearerKeyDao; 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) * Reset all cached DAO instances (useful for testing)
*/ */
@@ -194,3 +201,14 @@ export function getOAuthTokenDao(): OAuthTokenDao {
export function getBearerKeyDao(): BearerKeyDao { export function getBearerKeyDao(): BearerKeyDao {
return getDaoFactory().getBearerKeyDao(); 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, OAuthClientDao,
OAuthTokenDao, OAuthTokenDao,
BearerKeyDao, BearerKeyDao,
ToolCallActivityDao,
} from './index.js'; } from './index.js';
import { UserDaoDbImpl } from './UserDaoDbImpl.js'; import { UserDaoDbImpl } from './UserDaoDbImpl.js';
import { ServerDaoDbImpl } from './ServerDaoDbImpl.js'; import { ServerDaoDbImpl } from './ServerDaoDbImpl.js';
@@ -17,6 +18,7 @@ import { UserConfigDaoDbImpl } from './UserConfigDaoDbImpl.js';
import { OAuthClientDaoDbImpl } from './OAuthClientDaoDbImpl.js'; import { OAuthClientDaoDbImpl } from './OAuthClientDaoDbImpl.js';
import { OAuthTokenDaoDbImpl } from './OAuthTokenDaoDbImpl.js'; import { OAuthTokenDaoDbImpl } from './OAuthTokenDaoDbImpl.js';
import { BearerKeyDaoDbImpl } from './BearerKeyDaoDbImpl.js'; import { BearerKeyDaoDbImpl } from './BearerKeyDaoDbImpl.js';
import { ToolCallActivityDaoDbImpl } from './ToolCallActivityDao.js';
/** /**
* Database-backed DAO factory implementation * Database-backed DAO factory implementation
@@ -32,6 +34,7 @@ export class DatabaseDaoFactory implements DaoFactory {
private oauthClientDao: OAuthClientDao | null = null; private oauthClientDao: OAuthClientDao | null = null;
private oauthTokenDao: OAuthTokenDao | null = null; private oauthTokenDao: OAuthTokenDao | null = null;
private bearerKeyDao: BearerKeyDao | null = null; private bearerKeyDao: BearerKeyDao | null = null;
private toolCallActivityDao: ToolCallActivityDao | null = null;
/** /**
* Get singleton instance * Get singleton instance
@@ -103,6 +106,13 @@ export class DatabaseDaoFactory implements DaoFactory {
return this.bearerKeyDao!; return this.bearerKeyDao!;
} }
getToolCallActivityDao(): ToolCallActivityDao | null {
if (!this.toolCallActivityDao) {
this.toolCallActivityDao = new ToolCallActivityDaoDbImpl();
}
return this.toolCallActivityDao;
}
/** /**
* Reset all cached DAO instances (useful for testing) * Reset all cached DAO instances (useful for testing)
*/ */
@@ -115,5 +125,6 @@ export class DatabaseDaoFactory implements DaoFactory {
this.oauthClientDao = null; this.oauthClientDao = null;
this.oauthTokenDao = null; this.oauthTokenDao = null;
this.bearerKeyDao = null; this.bearerKeyDao = null;
this.toolCallActivityDao = null;
} }
} }

View File

@@ -38,7 +38,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts: entity.prompts, prompts: entity.prompts,
options: entity.options, options: entity.options,
oauth: entity.oauth, oauth: entity.oauth,
proxy: entity.proxy,
openapi: entity.openapi, openapi: entity.openapi,
}); });
return this.mapToServerConfig(server); return this.mapToServerConfig(server);
@@ -63,7 +62,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts: entity.prompts, prompts: entity.prompts,
options: entity.options, options: entity.options,
oauth: entity.oauth, oauth: entity.oauth,
proxy: entity.proxy,
openapi: entity.openapi, openapi: entity.openapi,
}); });
return server ? this.mapToServerConfig(server) : null; return server ? this.mapToServerConfig(server) : null;
@@ -142,7 +140,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts?: Record<string, { enabled: boolean; description?: string }>; prompts?: Record<string, { enabled: boolean; description?: string }>;
options?: Record<string, any>; options?: Record<string, any>;
oauth?: Record<string, any>; oauth?: Record<string, any>;
proxy?: Record<string, any>;
openapi?: Record<string, any>; openapi?: Record<string, any>;
}): ServerConfigWithName { }): ServerConfigWithName {
return { return {
@@ -161,7 +158,6 @@ export class ServerDaoDbImpl implements ServerDao {
prompts: server.prompts, prompts: server.prompts,
options: server.options, options: server.options,
oauth: server.oauth, oauth: server.oauth,
proxy: server.proxy,
openapi: server.openapi, 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 './OAuthClientDao.js';
export * from './OAuthTokenDao.js'; export * from './OAuthTokenDao.js';
export * from './BearerKeyDao.js'; export * from './BearerKeyDao.js';
export * from './ToolCallActivityDao.js';
// Export database implementations // Export database implementations
export * from './UserDaoDbImpl.js'; export * from './UserDaoDbImpl.js';

View File

@@ -59,9 +59,6 @@ export class Server {
@Column({ type: 'simple-json', nullable: true }) @Column({ type: 'simple-json', nullable: true })
oauth?: Record<string, any>; oauth?: Record<string, any>;
@Column({ type: 'simple-json', nullable: true })
proxy?: Record<string, any>;
@Column({ type: 'simple-json', nullable: true }) @Column({ type: 'simple-json', nullable: true })
openapi?: Record<string, any>; 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 OAuthClient from './OAuthClient.js';
import OAuthToken from './OAuthToken.js'; import OAuthToken from './OAuthToken.js';
import BearerKey from './BearerKey.js'; import BearerKey from './BearerKey.js';
import ToolCallActivity from './ToolCallActivity.js';
// Export all entities // Export all entities
export default [ export default [
@@ -19,6 +20,7 @@ export default [
OAuthClient, OAuthClient,
OAuthToken, OAuthToken,
BearerKey, BearerKey,
ToolCallActivity,
]; ];
// Export individual entities for direct use // Export individual entities for direct use
@@ -32,4 +34,5 @@ export {
OAuthClient, OAuthClient,
OAuthToken, OAuthToken,
BearerKey, BearerKey,
ToolCallActivity,
}; };

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 { OAuthClientRepository } from './OAuthClientRepository.js';
import { OAuthTokenRepository } from './OAuthTokenRepository.js'; import { OAuthTokenRepository } from './OAuthTokenRepository.js';
import { BearerKeyRepository } from './BearerKeyRepository.js'; import { BearerKeyRepository } from './BearerKeyRepository.js';
import { ToolCallActivityRepository } from './ToolCallActivityRepository.js';
// Export all repositories // Export all repositories
export { export {
@@ -19,4 +20,5 @@ export {
OAuthClientRepository, OAuthClientRepository,
OAuthTokenRepository, OAuthTokenRepository,
BearerKeyRepository, BearerKeyRepository,
ToolCallActivityRepository,
}; };

View File

@@ -1,6 +1,4 @@
import os from 'os'; import os from 'os';
import path from 'path';
import fs from 'fs';
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { import {
CallToolRequestSchema, CallToolRequestSchema,
@@ -17,7 +15,7 @@ import {
StreamableHTTPClientTransportOptions, StreamableHTTPClientTransportOptions,
} from '@modelcontextprotocol/sdk/client/streamableHttp.js'; } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { createFetchWithProxy, getProxyConfigFromEnv } from './proxy.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 { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js';
import config from '../config/index.js'; import config from '../config/index.js';
import { getGroup } from './sseService.js'; import { getGroup } from './sseService.js';
@@ -34,150 +32,6 @@ const servers: { [sessionId: string]: Server } = {};
import { setupClientKeepAlive } from './keepAliveService.js'; 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> => { export const initUpstreamServers = async (): Promise<void> => {
// Initialize OAuth clients for servers with dynamic registration // Initialize OAuth clients for servers with dynamic registration
await initializeAllOAuthClients(); await initializeAllOAuthClients();
@@ -355,19 +209,11 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
env['npm_config_registry'] = systemConfig.install.npmRegistry; env['npm_config_registry'] = systemConfig.install.npmRegistry;
} }
// Apply proxychains4 wrapper if proxy is configured (Linux/macOS only) // Expand environment variables in command
const { command: finalCommand, args: finalArgs } = wrapWithProxychains(
name,
conf.command,
replaceEnvVars(conf.args) as string[],
conf.proxy,
);
// Create STDIO transport with potentially wrapped command
transport = new StdioClientTransport({ transport = new StdioClientTransport({
cwd: os.homedir(), cwd: os.homedir(),
command: finalCommand, command: conf.command,
args: finalArgs, args: replaceEnvVars(conf.args) as string[],
env: env, env: env,
stderr: 'pipe', stderr: 'pipe',
}); });

View File

@@ -270,17 +270,6 @@ export interface McpSettings {
bearerKeys?: BearerKey[]; // Bearer authentication keys (multi-key configuration) 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 // Configuration details for an individual server
export interface ServerConfig { export interface ServerConfig {
type?: 'stdio' | 'sse' | 'streamable-http' | 'openapi'; // Type of server 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 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 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 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 authentication for upstream MCP servers
oauth?: { oauth?: {
// Static client configuration (traditional OAuth flow) // Static client configuration (traditional OAuth flow)
@@ -494,3 +481,51 @@ export interface BatchCreateGroupsResponse {
failureCount: number; // Number of groups that failed failureCount: number; // Number of groups that failed
results: BatchGroupResult[]; // Detailed results for each group 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;
}