diff --git a/docs/configuration/mcp-settings.mdx b/docs/configuration/mcp-settings.mdx index 49cc9c6..7426a0e 100644 --- a/docs/configuration/mcp-settings.mdx +++ b/docs/configuration/mcp-settings.mdx @@ -259,6 +259,92 @@ 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). + + +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` + + +#### 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" + } + } + } +} +``` + + +When `configPath` is specified, all other proxy settings (`type`, `host`, `port`, etc.) are ignored, and the custom configuration file is used directly. + + {/* ### Custom Server Scripts #### Local Python Server diff --git a/examples/mcp_settings_with_env_vars.json b/examples/mcp_settings_with_env_vars.json index 6a5701c..c36d4c6 100644 --- a/examples/mcp_settings_with_env_vars.json +++ b/examples/mcp_settings_with_env_vars.json @@ -31,6 +31,47 @@ "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": { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index cfef4c8..ea68a7e 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -105,6 +105,17 @@ 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'; @@ -123,6 +134,8 @@ 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 diff --git a/src/dao/ServerDaoDbImpl.ts b/src/dao/ServerDaoDbImpl.ts index 12ce3d8..d24c9a2 100644 --- a/src/dao/ServerDaoDbImpl.ts +++ b/src/dao/ServerDaoDbImpl.ts @@ -38,6 +38,7 @@ export class ServerDaoDbImpl implements ServerDao { prompts: entity.prompts, options: entity.options, oauth: entity.oauth, + proxy: entity.proxy, openapi: entity.openapi, }); return this.mapToServerConfig(server); @@ -62,6 +63,7 @@ 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; @@ -140,6 +142,7 @@ export class ServerDaoDbImpl implements ServerDao { prompts?: Record; options?: Record; oauth?: Record; + proxy?: Record; openapi?: Record; }): ServerConfigWithName { return { @@ -158,6 +161,7 @@ export class ServerDaoDbImpl implements ServerDao { prompts: server.prompts, options: server.options, oauth: server.oauth, + proxy: server.proxy, openapi: server.openapi, }; } diff --git a/src/db/entities/Server.ts b/src/db/entities/Server.ts index bfdbb0c..2022c0d 100644 --- a/src/db/entities/Server.ts +++ b/src/db/entities/Server.ts @@ -59,6 +59,9 @@ export class Server { @Column({ type: 'simple-json', nullable: true }) oauth?: Record; + @Column({ type: 'simple-json', nullable: true }) + proxy?: Record; + @Column({ type: 'simple-json', nullable: true }) openapi?: Record; diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 967193f..bd0a261 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -1,4 +1,6 @@ import os from 'os'; +import path from 'path'; +import fs from 'fs'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, @@ -15,7 +17,7 @@ import { StreamableHTTPClientTransportOptions, } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { createFetchWithProxy, getProxyConfigFromEnv } from './proxy.js'; -import { ServerInfo, ServerConfig, Tool } from '../types/index.js'; +import { ServerInfo, ServerConfig, Tool, ProxychainsConfig } from '../types/index.js'; import { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js'; import config from '../config/index.js'; import { getGroup } from './sseService.js'; @@ -32,6 +34,150 @@ 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 => { // Initialize OAuth clients for servers with dynamic registration await initializeAllOAuthClients(); @@ -209,11 +355,19 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig env['npm_config_registry'] = systemConfig.install.npmRegistry; } - // Expand environment variables in command + // 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 transport = new StdioClientTransport({ cwd: os.homedir(), - command: conf.command, - args: replaceEnvVars(conf.args) as string[], + command: finalCommand, + args: finalArgs, env: env, stderr: 'pipe', }); diff --git a/src/types/index.ts b/src/types/index.ts index e5e8186..e64a7af 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -270,6 +270,17 @@ 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 @@ -285,6 +296,8 @@ export interface ServerConfig { tools?: Record; // Tool-specific configurations with enable/disable state and custom descriptions prompts?: Record; // Prompt-specific configurations with enable/disable state and custom descriptions options?: Partial>; // 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)