Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import * as Tools from './src/tools/index.js';
import type { ToolKeys } from './src/schemas.js';
import * as Handlers from './src/handlers/index.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { applyProxyConfig } from './src/proxy.js';

// Apply proxy / SSL configuration before any outbound requests are made.
applyProxyConfig();
Comment thread
nedaKaighobadi marked this conversation as resolved.

// Check for API key
const CODACY_ACCOUNT_TOKEN = process.env.CODACY_ACCOUNT_TOKEN;
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@
"dependencies": {
"@modelcontextprotocol/sdk": "1.25.2",
"@types/node": "^22",
"@types/node-fetch": "^2.6.12",
"@types/sarif": "2.1.7",
"node-fetch": "^3.3.2",
"undici": "^6.0.0",
"universal-user-agent": "^7.0.2"
},
"devDependencies": {
Expand Down
83 changes: 83 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
import type { Dispatcher } from 'undici';

// Matches a request hostname against the NO_PROXY / no_proxy list.
// Handles exact matches and suffix matches (e.g. "example.com" also covers "sub.example.com").
function matchesNoProxy(hostname: string): boolean {
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy;
if (!noProxy) return false;
const lower = hostname.toLowerCase();
return noProxy
.split(',')
.map(h => h.trim().toLowerCase().replace(/^\./, ''))
.filter(Boolean)
.some(entry => entry === '*' || lower === entry || lower.endsWith('.' + entry));
}

// Ensures a proxy URL has a protocol prefix. A bare host:port (e.g. "localhost:8080")
// is treated as HTTP, matching the convention used by curl and most proxy tools.
function normalizeProxyUrl(url: string): string {
return /^https?:\/\//i.test(url) ? url : `http://${url}`;
}

// Routes each fetch() call through HTTPS_PROXY or HTTP_PROXY based on the request
// protocol, bypassing the proxy for any host that matches NO_PROXY.
// Extends Agent so all Dispatcher methods are already implemented.
class ProxyRoutingDispatcher extends Agent {
private readonly _httpsProxy: ProxyAgent | undefined;
private readonly _httpProxy: ProxyAgent | undefined;

constructor(httpProxy: string | undefined, httpsProxy: string | undefined, disableSSL: boolean) {
super();
// requestTls — TLS to the target server through the CONNECT tunnel.
// proxyTls — TLS to the proxy itself (relevant when proxy URL is HTTPS).
const tlsOpts = disableSSL ? { rejectUnauthorized: false } : undefined;

Check failure on line 34 in src/proxy.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/proxy.ts#L34

Checks for setting the environment variable NODE_TLS_REJECT_UNAUTHORIZED to 0, which disables TLS verification.
const proxyOpts = tlsOpts ? { requestTls: tlsOpts, proxyTls: tlsOpts } : {};
this._httpsProxy = httpsProxy
? new ProxyAgent({ uri: normalizeProxyUrl(httpsProxy), ...proxyOpts })
: undefined;
this._httpProxy = httpProxy
? new ProxyAgent({ uri: normalizeProxyUrl(httpProxy), ...proxyOpts })
: undefined;
}

dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean {
Comment thread
nedaKaighobadi marked this conversation as resolved.
const origin = options.origin instanceof URL ? options.origin : new URL(String(options.origin));
Comment thread
nedaKaighobadi marked this conversation as resolved.

if (!matchesNoProxy(origin.hostname)) {
const proxy =
origin.protocol === 'https:'
? (this._httpsProxy ?? this._httpProxy)
: (this._httpProxy ?? this._httpsProxy);
if (proxy) return proxy.dispatch(options, handler);
}

return super.dispatch(options, handler);
}
Comment thread
nedaKaighobadi marked this conversation as resolved.
}

/**
* Reads proxy configuration from environment variables and patches the global
* fetch dispatcher used by Node.js native fetch.
*
* Supported env vars:
* HTTPS_PROXY / https_proxy — proxy for HTTPS requests
* HTTP_PROXY / http_proxy — proxy for HTTP requests
* NO_PROXY / no_proxy — comma-separated list of hosts to bypass
* NODE_TLS_REJECT_UNAUTHORIZED=0 — disable TLS certificate verification
*
* For corporate environments that use SSL inspection (MITM proxies), prefer adding the
* corporate CA certificate to Node's trust store rather than disabling verification:
* NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem
* Node reads this at startup and it applies to all TLS connections, including
* the inner tunnel established through a CONNECT proxy.
*/
export function applyProxyConfig(): void {
Comment thread
nedaKaighobadi marked this conversation as resolved.
const httpsProxy = process.env.HTTPS_PROXY ?? process.env.https_proxy;
const httpProxy = process.env.HTTP_PROXY ?? process.env.http_proxy;

if (!httpsProxy && !httpProxy) return;

const disableSSL = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
setGlobalDispatcher(new ProxyRoutingDispatcher(httpProxy, httpsProxy, disableSSL));
}