From c673afb97e5ff1cf3148975d9accd15c23b46eaa Mon Sep 17 00:00:00 2001 From: samanhappy Date: Sun, 14 Dec 2025 15:44:44 +0800 Subject: [PATCH] Add HTTP/HTTPS proxy configuration and environment variable support (#506) --- frontend/src/components/ServerForm.tsx | 44 +++++++ package.json | 1 + pnpm-lock.yaml | 9 ++ src/services/mcpService.ts | 13 +- src/services/proxy.ts | 167 +++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 src/services/proxy.ts diff --git a/frontend/src/components/ServerForm.tsx b/frontend/src/components/ServerForm.tsx index 28f04b5..f44dc44 100644 --- a/frontend/src/components/ServerForm.tsx +++ b/frontend/src/components/ServerForm.tsx @@ -375,6 +375,7 @@ const ServerForm = ({ ? { url: formData.url, ...(Object.keys(headers).length > 0 ? { headers } : {}), + ...(Object.keys(env).length > 0 ? { env } : {}), ...(oauthConfig ? { oauth: oauthConfig } : {}), } : { @@ -978,6 +979,49 @@ const ServerForm = ({ ))} +
+
+ + +
+ {envVars.map((envVar, index) => ( +
+
+ handleEnvVarChange(index, 'key', e.target.value)} + className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input" + placeholder={t('server.key')} + /> + : + handleEnvVarChange(index, 'value', e.target.value)} + className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input" + placeholder={t('server.value')} + /> +
+ +
+ ))} +
+
=20.18.1'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -8946,6 +8953,8 @@ snapshots: undici-types@7.13.0: {} + undici@7.16.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 2f25a4d..1cfdea4 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -14,6 +14,7 @@ import { StreamableHTTPClientTransport, StreamableHTTPClientTransportOptions, } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { createFetchWithProxy, getProxyConfigFromEnv } from './proxy.js'; import { ServerInfo, ServerConfig, Tool } from '../types/index.js'; import { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js'; import config from '../config/index.js'; @@ -134,6 +135,10 @@ export const cleanupAllServers = (): void => { // Helper function to create transport based on server configuration export const createTransportFromConfig = async (name: string, conf: ServerConfig): Promise => { let transport; + const env: Record = { + ...(process.env as Record), + ...replaceEnvVars(conf.env || {}), + }; if (conf.type === 'streamable-http') { const options: StreamableHTTPClientTransportOptions = {}; @@ -152,6 +157,8 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig console.log(`OAuth provider configured for server: ${name}`); } + options.fetch = createFetchWithProxy(getProxyConfigFromEnv(env)); + transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options); } else if (conf.url) { // SSE transport @@ -174,13 +181,11 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig console.log(`OAuth provider configured for server: ${name}`); } + options.fetch = createFetchWithProxy(getProxyConfigFromEnv(env)); + transport = new SSEClientTransport(new URL(conf.url), options); } else if (conf.command && conf.args) { // Stdio transport - const env: Record = { - ...(process.env as Record), - ...replaceEnvVars(conf.env || {}), - }; env['PATH'] = expandEnvVars(process.env.PATH as string) || ''; const systemConfigDao = getSystemConfigDao(); diff --git a/src/services/proxy.ts b/src/services/proxy.ts new file mode 100644 index 0000000..2f4578c --- /dev/null +++ b/src/services/proxy.ts @@ -0,0 +1,167 @@ +/** + * HTTP/HTTPS proxy configuration utilities for MCP client transports. + * + * This module provides utilities to configure HTTP and HTTPS proxies when + * connecting to MCP servers. Proxies are configured by providing a custom + * fetch implementation that uses Node.js http/https agents with proxy support. + * + */ + +import { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'; + +/** + * Configuration options for HTTP/HTTPS proxy settings. + */ +export interface ProxyConfig { + /** + * HTTP proxy URL (e.g., 'http://proxy.example.com:8080') + * Can include authentication: 'http://user:pass@proxy.example.com:8080' + */ + httpProxy?: string; + + /** + * HTTPS proxy URL (e.g., 'https://proxy.example.com:8443') + * Can include authentication: 'https://user:pass@proxy.example.com:8443' + */ + httpsProxy?: string; + + /** + * Comma-separated list of hosts that should bypass the proxy + * (e.g., 'localhost,127.0.0.1,.example.com') + */ + noProxy?: string; +} + +/** + * Creates a fetch function that uses the specified proxy configuration. + * + * This function returns a fetch implementation that routes requests through + * the configured HTTP/HTTPS proxies using undici's ProxyAgent. + * + * Note: This function requires the 'undici' package to be installed. + * Install it with: npm install undici + * + * @param config - Proxy configuration options + * @returns A fetch-compatible function configured to use the specified proxies + * + */ +export function createFetchWithProxy(config: ProxyConfig): FetchLike { + // If no proxy is configured, return the default fetch + if (!config.httpProxy && !config.httpsProxy) { + return fetch; + } + + // Parse no_proxy list + const noProxyList = parseNoProxy(config.noProxy); + + return async (url: string | URL, init?: RequestInit): Promise => { + const targetUrl = typeof url === 'string' ? new URL(url) : url; + + // Check if host should bypass proxy + if (shouldBypassProxy(targetUrl.hostname, noProxyList)) { + return fetch(url, init); + } + + // Determine which proxy to use based on protocol + const proxyUrl = targetUrl.protocol === 'https:' ? config.httpsProxy : config.httpProxy; + + if (!proxyUrl) { + // No proxy configured for this protocol + return fetch(url, init); + } + + // Use undici for proxy support if available + try { + // Dynamic import - undici is an optional peer dependency + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const undici = await import('undici' as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ProxyAgent = (undici as any).ProxyAgent; + const dispatcher = new ProxyAgent(proxyUrl); + + return fetch(url, { + ...init, + // @ts-expect-error - dispatcher is undici-specific + dispatcher, + }); + } catch (error) { + // undici not available - throw error requiring installation + throw new Error( + 'Proxy support requires the "undici" package. ' + + 'Install it with: npm install undici\n' + + `Original error: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }; +} + +/** + * Parses a NO_PROXY environment variable value into a list of patterns. + */ +function parseNoProxy(noProxy?: string): string[] { + if (!noProxy) { + return []; + } + + return noProxy + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0); +} + +/** + * Checks if a hostname should bypass the proxy based on NO_PROXY patterns. + */ +function shouldBypassProxy(hostname: string, noProxyList: string[]): boolean { + if (noProxyList.length === 0) { + return false; + } + + const hostnameLower = hostname.toLowerCase(); + + for (const pattern of noProxyList) { + const patternLower = pattern.toLowerCase(); + + // Exact match + if (hostnameLower === patternLower) { + return true; + } + + // Domain suffix match (e.g., .example.com matches sub.example.com) + if (patternLower.startsWith('.') && hostnameLower.endsWith(patternLower)) { + return true; + } + + // Domain suffix match without leading dot + if (!patternLower.startsWith('.') && hostnameLower.endsWith('.' + patternLower)) { + return true; + } + + // Special case: "*" matches everything + if (patternLower === '*') { + return true; + } + } + + return false; +} + +/** + * Creates a ProxyConfig from environment variables. + * + * This function reads standard proxy environment variables: + * - HTTP_PROXY, http_proxy + * - HTTPS_PROXY, https_proxy + * - NO_PROXY, no_proxy + * + * Lowercase versions take precedence over uppercase versions. + * + * @returns A ProxyConfig object populated from environment variables + */ +export function getProxyConfigFromEnv(env: Record): ProxyConfig { + return { + httpProxy: env.http_proxy || env.HTTP_PROXY, + httpsProxy: env.https_proxy || env.HTTPS_PROXY, + noProxy: env.no_proxy || env.NO_PROXY, + }; +}