=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,
+ };
+}