wolfpack-mcp 1.0.78 → 1.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/apiClient.js CHANGED
@@ -1,4 +1,4 @@
1
- import fetch from 'node-fetch';
1
+ import { fetch } from './proxyFetch.js';
2
2
  import { config } from './config.js';
3
3
  export class ApiError extends Error {
4
4
  status;
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { AGENT_BUILDER_TOOLS, handleAgentBuilderTool } from './agentBuilderTools
13
13
  import { PROCEDURE_TOOLS, handleProcedureTool } from './procedureTools.js';
14
14
  import { AGENT_SELF_TOOLS, handleAgentSelfTool } from './agentSelfTools.js';
15
15
  import { resolveRadarItemId } from './resolveRadarItemId.js';
16
+ import { fetch as proxyFetch } from './proxyFetch.js';
16
17
  // Get current package version
17
18
  const require = createRequire(import.meta.url);
18
19
  const packageJson = require('../package.json');
@@ -21,7 +22,10 @@ const PACKAGE_NAME = packageJson.name;
21
22
  // Check for updates from npm registry
22
23
  async function checkForUpdates() {
23
24
  try {
24
- const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
25
+ // proxyFetch, not global fetch: in a restricted container the registry is
26
+ // only reachable through the proxy, and a direct attempt just stalls until
27
+ // the timeout on every start (#1793).
28
+ const response = await proxyFetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
25
29
  headers: { Accept: 'application/json' },
26
30
  signal: AbortSignal.timeout(5000), // 5 second timeout
27
31
  });
@@ -0,0 +1,61 @@
1
+ import nodeFetch from 'node-fetch';
2
+ import { HttpProxyAgent } from 'http-proxy-agent';
3
+ import { HttpsProxyAgent } from 'https-proxy-agent';
4
+ /**
5
+ * Whether a host is exempted from proxying by NO_PROXY. Entries match the host
6
+ * exactly or as a domain suffix (`.example.com`, or bare `example.com` for its
7
+ * subdomains); `*` exempts everything.
8
+ */
9
+ export function bypassesProxy(hostname, noProxy) {
10
+ if (!noProxy)
11
+ return false;
12
+ const host = hostname.toLowerCase();
13
+ return noProxy
14
+ .split(',')
15
+ .map((entry) => entry.trim().toLowerCase())
16
+ .filter(Boolean)
17
+ .some((entry) => {
18
+ if (entry === '*')
19
+ return true;
20
+ const suffix = entry.startsWith('.') ? entry : `.${entry}`;
21
+ return host === entry.replace(/^\./, '') || host.endsWith(suffix);
22
+ });
23
+ }
24
+ /** The proxy URL that applies to a target, or null to connect directly. */
25
+ export function proxyForUrl(target, env = process.env) {
26
+ if (bypassesProxy(target.hostname, env.NO_PROXY ?? env.no_proxy))
27
+ return null;
28
+ const proxy = target.protocol === 'https:'
29
+ ? (env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy)
30
+ : (env.HTTP_PROXY ?? env.http_proxy);
31
+ return proxy || null;
32
+ }
33
+ const agents = new Map();
34
+ function agentFor(target) {
35
+ const proxy = proxyForUrl(target);
36
+ if (!proxy)
37
+ return undefined;
38
+ const key = `${target.protocol}${proxy}`;
39
+ let agent = agents.get(key);
40
+ if (!agent) {
41
+ agent = target.protocol === 'https:' ? new HttpsProxyAgent(proxy) : new HttpProxyAgent(proxy);
42
+ agents.set(key, agent);
43
+ }
44
+ return agent;
45
+ }
46
+ /**
47
+ * `node-fetch` with the proxy from the environment applied.
48
+ *
49
+ * node-fetch does not read HTTP_PROXY/HTTPS_PROXY on its own, so inside a
50
+ * restricted agent container this client connected direct to the backend —
51
+ * which has no route out and no DNS, the container's only permitted egress
52
+ * being the Squid proxy (#1793). Agents are cached per origin, and requests
53
+ * are unaffected when no proxy variables are set.
54
+ */
55
+ export function fetch(url, init = {}) {
56
+ if (init.agent !== undefined)
57
+ return nodeFetch(url, init);
58
+ const target = new URL(typeof url === 'string' ? url : url.url);
59
+ return nodeFetch(url, { ...init, agent: agentFor(target) });
60
+ }
61
+ export default fetch;
@@ -0,0 +1,67 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { createServer } from 'node:http';
3
+ import { once } from 'node:events';
4
+ import { bypassesProxy, fetch, proxyForUrl } from './proxyFetch.js';
5
+ const port = (server) => server.address().port;
6
+ describe('bypassesProxy', () => {
7
+ it('exempts an exact host and its subdomains, on a dot boundary only', () => {
8
+ expect(bypassesProxy('localhost', 'localhost,127.0.0.1')).toBe(true);
9
+ expect(bypassesProxy('api.example.com', '.example.com')).toBe(true);
10
+ expect(bypassesProxy('example.com', 'example.com')).toBe(true);
11
+ expect(bypassesProxy('notexample.com', 'example.com')).toBe(false);
12
+ expect(bypassesProxy('wolfpacks.work', 'localhost,127.0.0.1')).toBe(false);
13
+ });
14
+ it('treats * as exempting everything, and no NO_PROXY as exempting nothing', () => {
15
+ expect(bypassesProxy('wolfpacks.work', '*')).toBe(true);
16
+ expect(bypassesProxy('wolfpacks.work', undefined)).toBe(false);
17
+ });
18
+ });
19
+ describe('proxyForUrl', () => {
20
+ it('prefers HTTPS_PROXY for https and falls back to HTTP_PROXY', () => {
21
+ const env = { HTTPS_PROXY: 'http://s:1', HTTP_PROXY: 'http://p:2' };
22
+ expect(proxyForUrl(new URL('https://wolfpacks.work/api'), env)).toBe('http://s:1');
23
+ expect(proxyForUrl(new URL('http://wolfpacks.work/api'), env)).toBe('http://p:2');
24
+ expect(proxyForUrl(new URL('https://wolfpacks.work/api'), { HTTP_PROXY: 'http://p:2' })).toBe('http://p:2');
25
+ });
26
+ it('returns null for a NO_PROXY host and when no proxy is configured', () => {
27
+ expect(proxyForUrl(new URL('http://localhost:3001/api'), {
28
+ NO_PROXY: 'localhost',
29
+ HTTP_PROXY: 'http://p:1',
30
+ })).toBeNull();
31
+ expect(proxyForUrl(new URL('https://wolfpacks.work/api'), {})).toBeNull();
32
+ });
33
+ });
34
+ describe('fetch', () => {
35
+ let proxy;
36
+ let seen;
37
+ beforeEach(async () => {
38
+ seen = [];
39
+ proxy = createServer((req, res) => {
40
+ seen.push(req.url ?? '');
41
+ res.writeHead(200, { 'content-type': 'application/json' });
42
+ res.end('{"proxied":true}');
43
+ });
44
+ proxy.listen(0);
45
+ await once(proxy, 'listening');
46
+ });
47
+ afterEach(() => {
48
+ proxy.close();
49
+ delete process.env.HTTP_PROXY;
50
+ delete process.env.NO_PROXY;
51
+ });
52
+ // Regression test for #1793: node-fetch does not read the proxy variables,
53
+ // so inside a restricted container this client connected direct to a backend
54
+ // it had neither a route to nor DNS for.
55
+ it('sends the request to the proxy, for a host it cannot itself resolve', async () => {
56
+ process.env.HTTP_PROXY = `http://127.0.0.1:${port(proxy)}`;
57
+ const res = await fetch('http://nowhere.invalid/api/mcp/work-items');
58
+ expect(res.status).toBe(200);
59
+ expect(seen).toEqual(['http://nowhere.invalid/api/mcp/work-items']);
60
+ });
61
+ it('connects direct when no proxy is configured', async () => {
62
+ const res = await fetch(`http://127.0.0.1:${port(proxy)}/direct`);
63
+ expect(res.status).toBe(200);
64
+ // Reached the server as an origin request, not as a proxy request
65
+ expect(seen).toEqual(['/direct']);
66
+ });
67
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.78",
3
+ "version": "1.0.79",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -35,6 +35,8 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/sdk": "^1.26.0",
38
+ "http-proxy-agent": "^7.0.2",
39
+ "https-proxy-agent": "^7.0.6",
38
40
  "node-fetch": "^3.3.2",
39
41
  "yaml": "^2.8.0",
40
42
  "zod": "^3.24.1"