wolfpack-mcp 1.0.77 → 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/README.md CHANGED
@@ -232,7 +232,8 @@ Get a specific issue.
232
232
 
233
233
  #### `create_issue`
234
234
 
235
- Create a new issue.
235
+ Create a new issue. Human callers only — issues are how the people using the product report problems
236
+ they hit, so an agent files what it finds with `create_work_item` instead.
236
237
 
237
238
  - `title` (required): Issue title
238
239
  - `description`, `severity`, `type`, `assigned_to_id`, `environment`, `affected_version`, `reproducible`,
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
  });
@@ -828,7 +832,8 @@ class WolfpackMCPServer {
828
832
  'Returns full details including description (markdown notes). ' +
829
833
  'Call this before updating to see current content. ' +
830
834
  'WORKFLOW: When asked to work on an item, check its status and follow the required state transitions ' +
831
- '(pending→pull first, new→doing, blocked/ready/completed/closednew→doing, then review when done). ' +
835
+ '(pending→pull first, new→doing, review→doing when you are picking the work back up, ' +
836
+ 'blocked/ready/completed/closed→new→doing, then review when done). ' +
832
837
  'PLANNING: Check if the description contains a plan (markdown checklist). If not, APPEND one using update_work_progress - preserve all original description text and add your plan below a "---" separator. ' +
833
838
  'FORMS: Procedure-created work items may include formDefinition (field definitions with name, label, type, required, options) and formValues (current values). Use submit_work_item_form to fill in form values. ' +
834
839
  'IMAGES: Image references in the description (e.g. `![alt](/api/files/...)`) can be viewed using the download_image tool.',
@@ -850,7 +855,7 @@ class WolfpackMCPServer {
850
855
  {
851
856
  name: 'update_work_progress',
852
857
  description: 'Update work item description/notes with your progress. WORKFLOW: 1) get_work_item to read current notes 2) Modify the markdown description with new findings/progress 3) Call this with the complete updated description. The full description replaces the existing one. ' +
853
- 'STATUS: Items in "new" are auto-advanced to "doing" (updating progress means work has started). Completing the work is NOT auto-detected: when done, set status to "review" via update_work_item and add a completion comment. ' +
858
+ 'STATUS: Items in "new" — and items in "review" you are picking back up — are auto-advanced to "doing" (recording progress means work is under way). Completing the work is NOT auto-detected: when done, set status to "review" via update_work_item and add a completion comment. ' +
854
859
  'CRITICAL: NEVER overwrite or remove the original description text. Preserve all existing content exactly as-is. Append your plan below a "---" separator. You may only modify content you previously added (your plan section). ' +
855
860
  'BEST PRACTICE: Append a plan with markdown checkboxes (- [ ] task). Check off completed tasks (- [x] task) as you progress. Include sections for: Plan (checklist), Approach (strategy), and Notes (discoveries/decisions). ' +
856
861
  CONTENT_LINKING_HELP,
@@ -881,6 +886,7 @@ class WolfpackMCPServer {
881
886
  'Take it with pull_work_item, which assigns it to you and puts it in "doing"; that is ' +
882
887
  'what makes the board show who is working on what. ' +
883
888
  'When moving to "review", add a completion comment via create_work_item_comment. When moving to "blocked", add a comment explaining the blocker. ' +
889
+ 'When you take an item back out of "review" to rework it, set it to "doing" first — an item being changed is not an item waiting to be reviewed. ' +
884
890
  'closing_comment is only accepted together with status "closed" — never use it for progress or completion notes.',
885
891
  inputSchema: {
886
892
  type: 'object',
@@ -1271,6 +1277,7 @@ class WolfpackMCPServer {
1271
1277
  {
1272
1278
  name: 'create_issue',
1273
1279
  description: 'Create a new issue in your current project (auto-selected for single-project users, or use list_projects first for multi-project). Requires mcp:issues:create permission. ' +
1280
+ 'Agents: not for you. Issues are how the people using the product report problems they hit — file what you found with create_work_item instead. ' +
1274
1281
  CONTENT_LINKING_HELP,
1275
1282
  inputSchema: {
1276
1283
  type: 'object',
@@ -2136,12 +2143,16 @@ class WolfpackMCPServer {
2136
2143
  let workItem = await this.client.updateWorkProgress(parsed.work_item_id, parsed.description, teamSlug);
2137
2144
  if (workItem) {
2138
2145
  let summary = `Updated description on: ${workItem.title}`;
2139
- // Progress on a "new" item means work has started advance it to "doing"
2140
- if (workItem.status === 'new') {
2141
- const advanced = await this.client.updateWorkItem(parsed.work_item_id, { status: 'doing' }, teamSlug);
2146
+ // Recording progress means the work is under way: a "new" item starts, and an
2147
+ // item in "review" comes back to "doing" because its developer is working on it
2148
+ // again (#1787) otherwise the board keeps showing handed-over work that is
2149
+ // still being changed.
2150
+ const previousStatus = workItem.status;
2151
+ if (previousStatus === 'new' || previousStatus === 'review') {
2152
+ const advanced = await this.moveToDoing(parsed.work_item_id, teamSlug);
2142
2153
  if (advanced) {
2143
2154
  workItem = advanced;
2144
- summary += ' (status auto-advanced new → doing)';
2155
+ summary += ` (status auto-advanced ${previousStatus} → doing)`;
2145
2156
  }
2146
2157
  }
2147
2158
  let text = `${summary}\n\n${JSON.stringify(stripUuids(workItem), null, 2)}`;
@@ -2971,6 +2982,24 @@ class WolfpackMCPServer {
2971
2982
  }
2972
2983
  });
2973
2984
  }
2985
+ /**
2986
+ * Put a work item in "doing" after progress was recorded on it, and report whether that
2987
+ * stuck. An agent whose work pool may not set "doing" — a reviewer annotating an item in
2988
+ * "review" rather than the developer resuming it — is refused by the backend status
2989
+ * policy; that refusal is the right answer, so it leaves the saved note alone instead of
2990
+ * failing the call the agent actually made.
2991
+ */
2992
+ async moveToDoing(workItemId, teamSlug) {
2993
+ try {
2994
+ return await this.client.updateWorkItem(workItemId, { status: 'doing' }, teamSlug);
2995
+ }
2996
+ catch (error) {
2997
+ if (error && typeof error === 'object' && 'status' in error && error.status === 403) {
2998
+ return null;
2999
+ }
3000
+ throw error;
3001
+ }
3002
+ }
2974
3003
  isImageBuffer(buffer) {
2975
3004
  if (buffer.length < 4)
2976
3005
  return false;
@@ -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
+ });
@@ -25,6 +25,9 @@ const STATUS_REMINDERS = {
25
25
  'BEFORE starting work.',
26
26
  doing: 'REMINDER: This work item is in "doing". The moment the work is complete, set status to "review" ' +
27
27
  '(update_work_item) and add a completion comment — do not leave it sitting in "doing".',
28
+ review: 'REMINDER: This work item is in "review" — the work was handed over. If you are picking it back ' +
29
+ 'up (rework, review feedback, a follow-up change), set status to "doing" (update_work_item) BEFORE ' +
30
+ 'starting, so the board shows the work is under way again. If you are reviewing it, leave it where it is.',
28
31
  };
29
32
  // Reminders to prepend to a get_work_item response, most urgent first.
30
33
  export function getWorkItemReminders(status, description, approved) {
@@ -50,13 +50,18 @@ describe('getWorkItemReminders', () => {
50
50
  expect.stringContaining('set status to "review"'),
51
51
  ]);
52
52
  });
53
+ it('tells agents picking a reviewed item back up to move it to doing', () => {
54
+ const reminders = getWorkItemReminders('review', PLAN);
55
+ expect(reminders).toEqual([expect.stringContaining('set status to "doing"')]);
56
+ expect(reminders[0]).toContain('If you are reviewing it, leave it where it is');
57
+ });
53
58
  it('adds the no-plan reminder when the description has no plan', () => {
54
59
  const reminders = getWorkItemReminders('new', 'Just a description');
55
60
  expect(reminders).toHaveLength(2);
56
61
  expect(reminders[1]).toContain('no plan');
57
62
  });
58
63
  it('stays quiet for statuses with no required action', () => {
59
- expect(getWorkItemReminders('review', PLAN)).toEqual([]);
60
64
  expect(getWorkItemReminders('completed', PLAN)).toEqual([]);
65
+ expect(getWorkItemReminders('ready', PLAN)).toEqual([]);
61
66
  });
62
67
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.77",
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"