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 +2 -1
- package/dist/apiClient.js +1 -1
- package/dist/index.js +36 -7
- package/dist/proxyFetch.js +61 -0
- package/dist/proxyFetch.test.js +67 -0
- package/dist/workItemReminders.js +3 -0
- package/dist/workItemReminders.test.js +6 -1
- package/package.json +3 -1
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
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
|
-
|
|
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,
|
|
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. ``) 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" (
|
|
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
|
-
//
|
|
2140
|
-
|
|
2141
|
-
|
|
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 +=
|
|
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.
|
|
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"
|