wolfpack-mcp 1.0.98 → 1.0.100
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/client.js +4 -2
- package/dist/index.js +10 -1731
- package/dist/procedureTools.js +4 -268
- package/dist/toolCatalogue.js +2156 -0
- package/dist/toolCatalogue.test.js +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { TOOL_CATALOGUE, remoteTools, remoteToolPermissions, stdioTools } from './toolCatalogue.js';
|
|
5
|
+
import { BROWSER_TOOLS } from './browserTools.js';
|
|
6
|
+
// @ts-expect-error — a plain .mjs build script, deliberately not part of this program
|
|
7
|
+
import { render, GENERATED_PATH } from '../../../scripts/generate-mcp-catalogue.mjs';
|
|
8
|
+
const repoFile = (path) => readFileSync(join(__dirname, '../../..', path), 'utf8');
|
|
9
|
+
/**
|
|
10
|
+
* The catalogue is the one declaration of a Wolfpack MCP tool, and these are the
|
|
11
|
+
* checks that keep it that way across the package boundary the backend's copy has
|
|
12
|
+
* to cross (#2345). Every one of them failed on `staging` before this item: the
|
|
13
|
+
* two hand-kept lists had drifted to 53 differing descriptions, 55 differing
|
|
14
|
+
* schemas and nine tools only one transport offered.
|
|
15
|
+
*/
|
|
16
|
+
describe('the MCP tool catalogue', () => {
|
|
17
|
+
it("is what the backend's generated copy holds", () => {
|
|
18
|
+
// The one check that makes the other declaration a copy rather than a rival.
|
|
19
|
+
expect(readFileSync(GENERATED_PATH, 'utf8')).toBe(render({ remoteTools, remoteToolPermissions }));
|
|
20
|
+
});
|
|
21
|
+
it('gives every tool a name no other tool has', () => {
|
|
22
|
+
const names = TOOL_CATALOGUE.map((t) => t.name);
|
|
23
|
+
expect(names).toEqual([...new Set(names)]);
|
|
24
|
+
});
|
|
25
|
+
it('decides a scope for every tool', () => {
|
|
26
|
+
// `null` is a decision — "no scope of its own" — and is spelled out with its
|
|
27
|
+
// reason at the entry. `undefined` is a tool nobody scoped.
|
|
28
|
+
const permissions = repoFile('packages/backend/src/lib/mcpPermissions.ts');
|
|
29
|
+
for (const tool of TOOL_CATALOGUE) {
|
|
30
|
+
expect(tool.permission, tool.name).not.toBeUndefined();
|
|
31
|
+
if (tool.permission !== null) {
|
|
32
|
+
expect(permissions, tool.name).toContain(`: '${tool.permission}',`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
it('gives every tool it advertises remotely a handler on the remote server', () => {
|
|
37
|
+
// Advertising a tool the dispatch has no case for is how a catalogue entry
|
|
38
|
+
// becomes a promise the product does not keep.
|
|
39
|
+
const server = repoFile('packages/backend/src/mcp/server.ts');
|
|
40
|
+
for (const tool of remoteTools()) {
|
|
41
|
+
expect(server, tool.name).toContain(`case '${tool.name}':`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
it('forwards every parameter it advertises to the remote dispatch', () => {
|
|
45
|
+
// Having a `case` is not enough: the dispatch has to pass each advertised
|
|
46
|
+
// argument on. Where it does not, a remote client's argument is accepted,
|
|
47
|
+
// ignored, and the call still reports success — the same defect the item
|
|
48
|
+
// was raised on, one level down. Found by review on the first round, when
|
|
49
|
+
// the stdio schema won every merge and seven arguments the remote dispatch
|
|
50
|
+
// had never forwarded became advertised there.
|
|
51
|
+
const server = repoFile('packages/backend/src/mcp/server.ts');
|
|
52
|
+
const dispatchFor = (name) => {
|
|
53
|
+
const block = server.slice(server.indexOf(`\n case '${name}':`) + 1);
|
|
54
|
+
return block.slice(0, block.search(/\n (case '|default:)/));
|
|
55
|
+
};
|
|
56
|
+
const unforwarded = remoteTools().flatMap((tool) => Object.keys(tool.inputSchema.properties ?? {})
|
|
57
|
+
.filter((param) => !dispatchFor(tool.name).includes(`args?.${param}`))
|
|
58
|
+
.map((param) => `${tool.name}.${param}`));
|
|
59
|
+
expect(unforwarded).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
it('offers the procedure tools over stdio only to a key with that capability', () => {
|
|
62
|
+
const withProcedures = stdioTools(['procedures']).map((t) => t.name);
|
|
63
|
+
const without = stdioTools([]).map((t) => t.name);
|
|
64
|
+
expect(withProcedures).toContain('start_case');
|
|
65
|
+
expect(without).not.toContain('start_case');
|
|
66
|
+
// Gating is the only difference: nothing else drops out.
|
|
67
|
+
expect(withProcedures.length - without.length).toBe(TOOL_CATALOGUE.filter((t) => t.capability === 'procedures').length);
|
|
68
|
+
});
|
|
69
|
+
it('takes the remote override only where one is set', () => {
|
|
70
|
+
// upload_image is the single tool whose two clients cannot share a contract.
|
|
71
|
+
const overridden = TOOL_CATALOGUE.filter((t) => t.remote).map((t) => t.name);
|
|
72
|
+
expect(overridden).toEqual(['upload_image']);
|
|
73
|
+
const remote = remoteTools().find((t) => t.name === 'upload_image');
|
|
74
|
+
expect(Object.keys(remote.inputSchema.properties)).toContain('data');
|
|
75
|
+
expect(Object.keys(remote.inputSchema.properties)).not.toContain('file_path');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('the browser-control tool names', () => {
|
|
79
|
+
// #2288 spells these in two more places, neither of which a rename would
|
|
80
|
+
// reach: the backend decides which tools a chat session is served, and the
|
|
81
|
+
// runner tells the agent in prose that they exist.
|
|
82
|
+
const names = BROWSER_TOOLS.map((t) => t.name);
|
|
83
|
+
it('are the ones the backend serves to a chat session', () => {
|
|
84
|
+
const service = repoFile('packages/backend/src/services/agentSessionService.ts');
|
|
85
|
+
const declared = service.slice(service.indexOf('const BROWSER_MCP_TOOLS'));
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
expect(declared.slice(0, declared.indexOf('];')), name).toContain(`'${name}'`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it('are the ones the runner names in the system prompt', () => {
|
|
91
|
+
const section = repoFile('packages/claude-runner/src/env.ts');
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
expect(section, name).toContain(name);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|