Skip to content

Commit 8d968d7

Browse files
Merge pull request #81 from codacy/proxy-support
feat: add proxy support for mcp server CF-2438
2 parents 12e1ba5 + 951b71e commit 8d968d7

3 files changed

Lines changed: 88 additions & 2 deletions

File tree

index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import * as Tools from './src/tools/index.js';
1010
import type { ToolKeys } from './src/schemas.js';
1111
import * as Handlers from './src/handlers/index.js';
1212
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13+
import { applyProxyConfig } from './src/proxy.js';
14+
15+
// Apply proxy / SSL configuration before any outbound requests are made.
16+
applyProxyConfig();
1317

1418
// Check for API key
1519
const CODACY_ACCOUNT_TOKEN = process.env.CODACY_ACCOUNT_TOKEN;

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@
3434
"dependencies": {
3535
"@modelcontextprotocol/sdk": "1.25.2",
3636
"@types/node": "^22",
37-
"@types/node-fetch": "^2.6.12",
3837
"@types/sarif": "2.1.7",
39-
"node-fetch": "^3.3.2",
38+
"undici": "^6.0.0",
4039
"universal-user-agent": "^7.0.2"
4140
},
4241
"devDependencies": {

src/proxy.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
2+
import type { Dispatcher } from 'undici';
3+
4+
// Matches a request hostname against the NO_PROXY / no_proxy list.
5+
// Handles exact matches and suffix matches (e.g. "example.com" also covers "sub.example.com").
6+
function matchesNoProxy(hostname: string): boolean {
7+
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy;
8+
if (!noProxy) return false;
9+
const lower = hostname.toLowerCase();
10+
return noProxy
11+
.split(',')
12+
.map(h => h.trim().toLowerCase().replace(/^\./, ''))
13+
.filter(Boolean)
14+
.some(entry => entry === '*' || lower === entry || lower.endsWith('.' + entry));
15+
}
16+
17+
// Ensures a proxy URL has a protocol prefix. A bare host:port (e.g. "localhost:8080")
18+
// is treated as HTTP, matching the convention used by curl and most proxy tools.
19+
function normalizeProxyUrl(url: string): string {
20+
return /^https?:\/\//i.test(url) ? url : `http://${url}`;
21+
}
22+
23+
// Routes each fetch() call through HTTPS_PROXY or HTTP_PROXY based on the request
24+
// protocol, bypassing the proxy for any host that matches NO_PROXY.
25+
// Extends Agent so all Dispatcher methods are already implemented.
26+
class ProxyRoutingDispatcher extends Agent {
27+
private readonly _httpsProxy: ProxyAgent | undefined;
28+
private readonly _httpProxy: ProxyAgent | undefined;
29+
30+
constructor(httpProxy: string | undefined, httpsProxy: string | undefined, disableSSL: boolean) {
31+
super();
32+
// requestTls — TLS to the target server through the CONNECT tunnel.
33+
// proxyTls — TLS to the proxy itself (relevant when proxy URL is HTTPS).
34+
const tlsOpts = disableSSL ? { rejectUnauthorized: false } : undefined;
35+
const proxyOpts = tlsOpts ? { requestTls: tlsOpts, proxyTls: tlsOpts } : {};
36+
this._httpsProxy = httpsProxy
37+
? new ProxyAgent({ uri: normalizeProxyUrl(httpsProxy), ...proxyOpts })
38+
: undefined;
39+
this._httpProxy = httpProxy
40+
? new ProxyAgent({ uri: normalizeProxyUrl(httpProxy), ...proxyOpts })
41+
: undefined;
42+
}
43+
44+
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean {
45+
const origin = options.origin instanceof URL ? options.origin : new URL(String(options.origin));
46+
47+
if (!matchesNoProxy(origin.hostname)) {
48+
const proxy =
49+
origin.protocol === 'https:'
50+
? (this._httpsProxy ?? this._httpProxy)
51+
: (this._httpProxy ?? this._httpsProxy);
52+
if (proxy) return proxy.dispatch(options, handler);
53+
}
54+
55+
return super.dispatch(options, handler);
56+
}
57+
}
58+
59+
/**
60+
* Reads proxy configuration from environment variables and patches the global
61+
* fetch dispatcher used by Node.js native fetch.
62+
*
63+
* Supported env vars:
64+
* HTTPS_PROXY / https_proxy — proxy for HTTPS requests
65+
* HTTP_PROXY / http_proxy — proxy for HTTP requests
66+
* NO_PROXY / no_proxy — comma-separated list of hosts to bypass
67+
* NODE_TLS_REJECT_UNAUTHORIZED=0 — disable TLS certificate verification
68+
*
69+
* For corporate environments that use SSL inspection (MITM proxies), prefer adding the
70+
* corporate CA certificate to Node's trust store rather than disabling verification:
71+
* NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem
72+
* Node reads this at startup and it applies to all TLS connections, including
73+
* the inner tunnel established through a CONNECT proxy.
74+
*/
75+
export function applyProxyConfig(): void {
76+
const httpsProxy = process.env.HTTPS_PROXY ?? process.env.https_proxy;
77+
const httpProxy = process.env.HTTP_PROXY ?? process.env.http_proxy;
78+
79+
if (!httpsProxy && !httpProxy) return;
80+
81+
const disableSSL = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
82+
setGlobalDispatcher(new ProxyRoutingDispatcher(httpProxy, httpsProxy, disableSSL));
83+
}

0 commit comments

Comments
 (0)