wolfpack-mcp 1.0.78 → 1.0.80
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/agentBuilderTools.js +12 -5
- package/dist/agentMemoryTools.test.js +19 -0
- package/dist/agentSelfTools.js +6 -0
- package/dist/apiClient.js +1 -1
- package/dist/client.js +6 -2
- package/dist/index.js +18 -2
- package/dist/proxyFetch.js +61 -0
- package/dist/proxyFetch.test.js +67 -0
- package/package.json +3 -1
|
@@ -116,7 +116,8 @@ export const AGENT_BUILDER_TOOLS = [
|
|
|
116
116
|
description: 'Create an alias of an agent, so the same definition can run work in parallel. ' +
|
|
117
117
|
'An agent runs one session at a time, so N concurrent workers means N aliases. ' +
|
|
118
118
|
'The alias live-tracks the source for container image, prompts, instructions, LLM, tools, skills and tasks — edit those once on the source. ' +
|
|
119
|
-
'It does NOT inherit project assignment, API key permissions
|
|
119
|
+
'It does NOT inherit project assignment, API key permissions or schedules: set those on the alias afterwards, or it will start with a read-only key and no repository. ' +
|
|
120
|
+
'Secrets are inherited only when marked shared_with_aliases on the source (same-org only); an alias-local secret of the same name overrides. ' +
|
|
120
121
|
'Aliases are auto-named from the source (e.g. "Coder (2)"). You cannot alias an alias.',
|
|
121
122
|
inputSchema: {
|
|
122
123
|
type: 'object',
|
|
@@ -714,16 +715,21 @@ export const AGENT_BUILDER_TOOLS = [
|
|
|
714
715
|
{
|
|
715
716
|
name: 'set_agent_secret',
|
|
716
717
|
description: 'Create or update a secret for an agent. ' +
|
|
717
|
-
'Name must be uppercase letters, digits, and underscores (e.g. MY_API_KEY).'
|
|
718
|
+
'Name must be uppercase letters, digits, and underscores (e.g. MY_API_KEY). ' +
|
|
719
|
+
'Omit value to change only shared_with_aliases on an existing secret.',
|
|
718
720
|
inputSchema: {
|
|
719
721
|
type: 'object',
|
|
720
722
|
properties: {
|
|
721
723
|
agent_id: { type: 'string', description: 'Agent profile ID' },
|
|
722
724
|
name: { type: 'string', description: 'Secret name (e.g. MY_API_KEY)' },
|
|
723
725
|
value: { type: 'string', description: 'Secret value (encrypted at rest)' },
|
|
726
|
+
shared_with_aliases: {
|
|
727
|
+
type: 'boolean',
|
|
728
|
+
description: 'Share with same-org aliases of this agent (default false)',
|
|
729
|
+
},
|
|
724
730
|
...ORG_SLUG_PROP,
|
|
725
731
|
},
|
|
726
|
-
required: ['agent_id', 'name'
|
|
732
|
+
required: ['agent_id', 'name'],
|
|
727
733
|
},
|
|
728
734
|
},
|
|
729
735
|
// ─── Group 7: Discovery ───────────────────────────────────────────────────
|
|
@@ -1315,11 +1321,12 @@ export async function handleAgentBuilderTool(name, args, client) {
|
|
|
1315
1321
|
.object({
|
|
1316
1322
|
agent_id: z.string(),
|
|
1317
1323
|
name: z.string(),
|
|
1318
|
-
value: z.string(),
|
|
1324
|
+
value: z.string().optional(),
|
|
1325
|
+
shared_with_aliases: z.boolean().optional(),
|
|
1319
1326
|
org_slug: orgSlugField,
|
|
1320
1327
|
})
|
|
1321
1328
|
.parse(args);
|
|
1322
|
-
const secret = await client.setAgentSecret(parsed.agent_id, parsed.name, parsed.value, resolveOrg(parsed));
|
|
1329
|
+
const secret = await client.setAgentSecret(parsed.agent_id, parsed.name, parsed.value, parsed.shared_with_aliases, resolveOrg(parsed));
|
|
1323
1330
|
return { content: [{ type: 'text', text: `Set secret "${secret.name}"` }] };
|
|
1324
1331
|
}
|
|
1325
1332
|
// ─── Discovery ────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS } from './agentSelfTools.js';
|
|
3
|
+
const names = (tools) => tools.map((t) => t.name);
|
|
4
|
+
describe('agent self and memory tool split (#1776)', () => {
|
|
5
|
+
it('keeps the memory tools out of the self tools, so they can be withheld separately', () => {
|
|
6
|
+
expect(names(AGENT_SELF_TOOLS)).toEqual(['get_self', 'get_own_sessions']);
|
|
7
|
+
});
|
|
8
|
+
it('groups every memory tool under the separately gated set', () => {
|
|
9
|
+
expect(names(AGENT_MEMORY_TOOLS).sort()).toEqual([
|
|
10
|
+
'get_memory',
|
|
11
|
+
'list_memories',
|
|
12
|
+
'save_memory',
|
|
13
|
+
]);
|
|
14
|
+
});
|
|
15
|
+
it('never lists the same tool twice', () => {
|
|
16
|
+
const all = [...names(AGENT_SELF_TOOLS), ...names(AGENT_MEMORY_TOOLS)];
|
|
17
|
+
expect(new Set(all).size).toBe(all.length);
|
|
18
|
+
});
|
|
19
|
+
});
|
package/dist/agentSelfTools.js
CHANGED
|
@@ -23,6 +23,12 @@ export const AGENT_SELF_TOOLS = [
|
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Memory tools, gated by the `agent_memory` capability so an agent whose
|
|
29
|
+
* memory is disabled (#1776) is never offered them.
|
|
30
|
+
*/
|
|
31
|
+
export const AGENT_MEMORY_TOOLS = [
|
|
26
32
|
{
|
|
27
33
|
name: 'list_memories',
|
|
28
34
|
description: 'List all your persistent memory entries. Memory persists across sessions and is scoped to you. ' +
|
package/dist/apiClient.js
CHANGED
package/dist/client.js
CHANGED
|
@@ -762,8 +762,12 @@ export class WolfpackClient {
|
|
|
762
762
|
async listAgentSecrets(agentId, orgSlug) {
|
|
763
763
|
return this.api.get(this.withOrgSlug(`/agents/${agentId}/secrets`, orgSlug));
|
|
764
764
|
}
|
|
765
|
-
async setAgentSecret(agentId, name, value, orgSlug) {
|
|
766
|
-
return this.api.post(this.withOrgSlug(`/agents/${agentId}/secrets`, orgSlug), {
|
|
765
|
+
async setAgentSecret(agentId, name, value, sharedWithAliases, orgSlug) {
|
|
766
|
+
return this.api.post(this.withOrgSlug(`/agents/${agentId}/secrets`, orgSlug), {
|
|
767
|
+
name,
|
|
768
|
+
value,
|
|
769
|
+
sharedWithAliases,
|
|
770
|
+
});
|
|
767
771
|
}
|
|
768
772
|
// ─── Agent Builder: Skills (write) ────────────────────────────────────────
|
|
769
773
|
async createSkill(body, orgSlug) {
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,9 @@ import { allTasksChecked, getWorkItemReminders } from './workItemReminders.js';
|
|
|
11
11
|
import { validateConfig, config } from './config.js';
|
|
12
12
|
import { AGENT_BUILDER_TOOLS, handleAgentBuilderTool } from './agentBuilderTools.js';
|
|
13
13
|
import { PROCEDURE_TOOLS, handleProcedureTool } from './procedureTools.js';
|
|
14
|
-
import { AGENT_SELF_TOOLS, handleAgentSelfTool } from './agentSelfTools.js';
|
|
14
|
+
import { AGENT_SELF_TOOLS, AGENT_MEMORY_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
|
});
|
|
@@ -754,6 +758,8 @@ class WolfpackMCPServer {
|
|
|
754
758
|
'AGENTS: unless granted the mcp:work_items:read_all permission, your results are always ' +
|
|
755
759
|
'narrowed to items assigned to you or routed to a work pool you belong to, whatever ' +
|
|
756
760
|
'assigned_to_id you pass; the response says so when this applies. ' +
|
|
761
|
+
'AGENTS: when more than one item is claimable, the response also states the order to take ' +
|
|
762
|
+
'them in (bug fixes first, then higher priority, then oldest) and which to pull next. ' +
|
|
757
763
|
'TERMINOLOGY: "board" and "kanban" are synonymous - both refer to the Kanban board of work items. ' +
|
|
758
764
|
'The board has columns: "new" (to do), "doing" (in progress), "review" (pending review), "ready" (code done, awaiting deployment), "blocked", "completed" (deployed). ' +
|
|
759
765
|
'The "backlog" or "pending" status represents items not yet on the board. ' +
|
|
@@ -982,6 +988,8 @@ class WolfpackMCPServer {
|
|
|
982
988
|
'agent and will return 403: ask a human to route it to your pool or assign it to you. ' +
|
|
983
989
|
'AGENTS: pulling puts the item straight into "doing" — you are taking it to start on it ' +
|
|
984
990
|
'now — so there is no separate "move it to doing" step. Move it to "review" when done. ' +
|
|
991
|
+
'AGENTS: with more than one item claimable, take them in the order list_work_items gives ' +
|
|
992
|
+
'you: bug fixes first, then higher priority, then oldest. ' +
|
|
985
993
|
'If no assignee is specified, assigns to the API key owner. ' +
|
|
986
994
|
'In personal projects, items are always assigned to the owner.',
|
|
987
995
|
inputSchema: {
|
|
@@ -2036,6 +2044,7 @@ class WolfpackMCPServer {
|
|
|
2036
2044
|
},
|
|
2037
2045
|
...(this.capabilities.includes('procedures') ? PROCEDURE_TOOLS : []),
|
|
2038
2046
|
...(this.capabilities.includes('agent_self') ? AGENT_SELF_TOOLS : []),
|
|
2047
|
+
...(this.capabilities.includes('agent_memory') ? AGENT_MEMORY_TOOLS : []),
|
|
2039
2048
|
...(this.capabilities.includes('agent_builder') ? AGENT_BUILDER_TOOLS : []),
|
|
2040
2049
|
],
|
|
2041
2050
|
};
|
|
@@ -2961,6 +2970,13 @@ class WolfpackMCPServer {
|
|
|
2961
2970
|
return handleAgentSelfTool(name, args, this.client);
|
|
2962
2971
|
}
|
|
2963
2972
|
}
|
|
2973
|
+
// Check memory tools (separately gated — #1776)
|
|
2974
|
+
if (this.capabilities.includes('agent_memory')) {
|
|
2975
|
+
const memoryToolNames = AGENT_MEMORY_TOOLS.map((t) => t.name);
|
|
2976
|
+
if (memoryToolNames.includes(name)) {
|
|
2977
|
+
return handleAgentSelfTool(name, args, this.client);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2964
2980
|
// Check agent builder tools
|
|
2965
2981
|
if (this.capabilities.includes('agent_builder')) {
|
|
2966
2982
|
return handleAgentBuilderTool(name, args, this.client);
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.80",
|
|
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"
|