@withpica/mcp-server-directory 1.1.0 → 1.3.0
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/CHANGELOG.md +43 -26
- package/package.json +4 -2
- package/scripts/build-skills.ts +192 -0
- package/src/__tests__/prompts/index.test.ts +47 -64
- package/src/__tests__/prompts/prompt-eval-harness.test.ts +135 -104
- package/src/__tests__/skills/skills-registry.test.ts +63 -0
- package/src/__tests__/skills/skills-tools.test.ts +74 -0
- package/src/__tests__/tools/chain.test.ts +122 -0
- package/src/__tests__/tools/composability-chains.test.ts +4 -2
- package/src/__tests__/tools/people.test.ts +9 -3
- package/src/__tests__/tools/works.test.ts +32 -3
- package/src/prompts/index.ts +97 -141
- package/src/prompts/public-question-atlas.ts +557 -0
- package/src/server.ts +123 -11
- package/src/skills/find-music-for-sync-brief/SKILL.md +89 -0
- package/src/skills/index.ts +63 -0
- package/src/skills/skills.generated.ts +33 -0
- package/src/tools/chain.ts +118 -0
- package/src/tools/index.ts +15 -0
- package/src/tools/people.ts +22 -41
- package/src/tools/recordings.ts +7 -3
- package/src/tools/search.ts +7 -4
- package/src/tools/skills.ts +146 -0
- package/src/tools/works.ts +39 -46
- package/dist/client.d.ts +0 -12
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -93
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -9
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -15
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -28
- package/dist/index.js.map +0 -1
- package/dist/prompts/index.d.ts +0 -33
- package/dist/prompts/index.d.ts.map +0 -1
- package/dist/prompts/index.js +0 -208
- package/dist/prompts/index.js.map +0 -1
- package/dist/resources/llms-primer.d.ts +0 -2
- package/dist/resources/llms-primer.d.ts.map +0 -1
- package/dist/resources/llms-primer.js +0 -35
- package/dist/resources/llms-primer.js.map +0 -1
- package/dist/server.d.ts +0 -13
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -104
- package/dist/server.js.map +0 -1
- package/dist/tools/index.d.ts +0 -26
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -43
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/people.d.ts +0 -14
- package/dist/tools/people.d.ts.map +0 -1
- package/dist/tools/people.js +0 -190
- package/dist/tools/people.js.map +0 -1
- package/dist/tools/recordings.d.ts +0 -12
- package/dist/tools/recordings.d.ts.map +0 -1
- package/dist/tools/recordings.js +0 -141
- package/dist/tools/recordings.js.map +0 -1
- package/dist/tools/search.d.ts +0 -12
- package/dist/tools/search.d.ts.map +0 -1
- package/dist/tools/search.js +0 -53
- package/dist/tools/search.js.map +0 -1
- package/dist/tools/works.d.ts +0 -15
- package/dist/tools/works.d.ts.map +0 -1
- package/dist/tools/works.js +0 -249
- package/dist/tools/works.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -14
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -48
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/formatting.d.ts +0 -10
- package/dist/utils/formatting.d.ts.map +0 -1
- package/dist/utils/formatting.js +0 -16
- package/dist/utils/formatting.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Directory
|
|
4
|
+
* Directory Prompt Eval Harness — ADR-140 Phase 3
|
|
5
5
|
*
|
|
6
|
-
* Deterministic test suite that validates directory
|
|
7
|
-
* correct workflow routing. Parses
|
|
6
|
+
* Deterministic test suite that validates directory prompt text produces
|
|
7
|
+
* correct workflow routing. Parses prompt text for tool references,
|
|
8
8
|
* ordering, and routing conditions.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -29,27 +29,109 @@ function appearsBeforeInText(
|
|
|
29
29
|
return indexA !== -1 && indexB !== -1 && indexA < indexB;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
describe("
|
|
33
|
-
describe("Scenario 1:
|
|
32
|
+
describe("Prompt Eval Harness — Directory MCP", () => {
|
|
33
|
+
describe("Scenario 1: directory-autopilot routes sync searches to find-music", () => {
|
|
34
34
|
let text: string;
|
|
35
|
-
let tools: string[];
|
|
36
35
|
|
|
37
36
|
beforeAll(async () => {
|
|
38
|
-
const result = await registry.getPrompt("
|
|
37
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
39
38
|
text = result.messages[0].content.text;
|
|
40
|
-
tools = extractToolRefs(text);
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
it("reads llms://primer
|
|
41
|
+
it("reads llms://primer first for orientation", () => {
|
|
44
42
|
expect(text).toContain("llms://primer");
|
|
43
|
+
const primerIdx = text.indexOf("llms://primer");
|
|
44
|
+
const firstRouteIdx = text.search(/if i'm looking for/i);
|
|
45
|
+
expect(primerIdx).toBeLessThan(firstRouteIdx);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("routes music/sync/playlist requests to find-music workflow", () => {
|
|
49
|
+
// Find the sync search routing block
|
|
50
|
+
const syncBlock = text.slice(
|
|
51
|
+
text.search(/music for a project|sync|playlist/i),
|
|
52
|
+
text.search(/music for a project|sync|playlist/i) + 300,
|
|
53
|
+
);
|
|
54
|
+
expect(syncBlock.toLowerCase()).toContain("find-music");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("mentions sync brief, playlist, and mood as triggers for find-music", () => {
|
|
58
|
+
expect(text).toMatch(/sync/i);
|
|
59
|
+
expect(text).toMatch(/playlist/i);
|
|
60
|
+
expect(text).toMatch(/mood/i);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("Scenario 2: directory-autopilot routes creator research to person lookup", () => {
|
|
65
|
+
let text: string;
|
|
66
|
+
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
69
|
+
text = result.messages[0].content.text;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("routes creator/songwriter/performer requests to research-creator", () => {
|
|
73
|
+
expect(text).toMatch(/songwriter|composer|performer|creator/i);
|
|
74
|
+
expect(text).toContain("research-creator");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("references directory_lookup_person as the primary lookup tool", () => {
|
|
78
|
+
expect(text).toContain("directory_lookup_person");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("Scenario 3: directory-autopilot routes identifier resolution to correct tools", () => {
|
|
83
|
+
let text: string;
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
87
|
+
text = result.messages[0].content.text;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("routes ISRC to directory_lookup_isrc", () => {
|
|
91
|
+
expect(text).toMatch(/isrc/i);
|
|
92
|
+
expect(text).toContain("directory_lookup_isrc");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("chains ISRC lookup to directory_lookup_work", () => {
|
|
96
|
+
// After ISRC lookup, should chain to work details
|
|
97
|
+
const isrcBlock = text.slice(
|
|
98
|
+
text.search(/isrc/i),
|
|
99
|
+
text.search(/isrc/i) + 200,
|
|
100
|
+
);
|
|
101
|
+
expect(isrcBlock).toContain("directory_lookup_work");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("routes IPI/ISNI to directory_lookup_person", () => {
|
|
105
|
+
expect(text).toMatch(/ipi|isni/i);
|
|
106
|
+
expect(text).toContain("directory_lookup_person");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("routes ISWC to directory_lookup_work", () => {
|
|
110
|
+
expect(text).toMatch(/iswc/i);
|
|
111
|
+
expect(text).toContain("directory_lookup_work");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("handles MusicBrainz IDs", () => {
|
|
115
|
+
expect(text).toMatch(/musicbrainz/i);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("Scenario 4: find-music references search then lookup workflow", () => {
|
|
120
|
+
let text: string;
|
|
121
|
+
let tools: string[];
|
|
122
|
+
|
|
123
|
+
beforeAll(async () => {
|
|
124
|
+
const result = await registry.getPrompt("find-music");
|
|
125
|
+
text = result.messages[0].content.text;
|
|
126
|
+
tools = extractToolRefs(text);
|
|
45
127
|
});
|
|
46
128
|
|
|
47
129
|
it("uses directory_search_recordings as the primary search tool", () => {
|
|
48
130
|
expect(tools).toContain("directory_search_recordings");
|
|
49
131
|
});
|
|
50
132
|
|
|
51
|
-
it("chains to
|
|
52
|
-
expect(tools).toContain("
|
|
133
|
+
it("chains to directory_lookup_work for detailed results", () => {
|
|
134
|
+
expect(tools).toContain("directory_lookup_work");
|
|
53
135
|
});
|
|
54
136
|
|
|
55
137
|
it("search comes before lookup (correct order)", () => {
|
|
@@ -57,12 +139,13 @@ describe("Skill Eval Harness — Directory MCP", () => {
|
|
|
57
139
|
appearsBeforeInText(
|
|
58
140
|
text,
|
|
59
141
|
"directory_search_recordings",
|
|
60
|
-
"
|
|
142
|
+
"directory_lookup_work",
|
|
61
143
|
),
|
|
62
144
|
).toBe(true);
|
|
63
145
|
});
|
|
64
146
|
|
|
65
147
|
it("includes mood-to-parameter translation guide", () => {
|
|
148
|
+
// The prompt should help agents translate natural language to search params
|
|
66
149
|
expect(text).toMatch(/energy/i);
|
|
67
150
|
expect(text).toMatch(/bpm/i);
|
|
68
151
|
expect(text).toMatch(/danceability/i);
|
|
@@ -82,30 +165,9 @@ describe("Skill Eval Harness — Directory MCP", () => {
|
|
|
82
165
|
it("handles no-results case with broadening suggestions", () => {
|
|
83
166
|
expect(text).toMatch(/no results|broaden/i);
|
|
84
167
|
});
|
|
85
|
-
|
|
86
|
-
it("emphasises equal discovery principle", () => {
|
|
87
|
-
expect(text).toMatch(/equal discovery/i);
|
|
88
|
-
expect(text).toMatch(/not.*popularity|not.*fame/i);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("includes reference track search path", () => {
|
|
92
|
-
expect(text).toMatch(/reference track/i);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("includes similarity exploration path", () => {
|
|
96
|
-
expect(text).toMatch(/similar/i);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("mentions sync brief and playlist as use cases", () => {
|
|
100
|
-
expect(text).toMatch(/sync/i);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("includes rights check path", () => {
|
|
104
|
-
expect(text).toMatch(/rights/i);
|
|
105
|
-
});
|
|
106
168
|
});
|
|
107
169
|
|
|
108
|
-
describe("Scenario
|
|
170
|
+
describe("Scenario 5: research-creator references person then work lookup", () => {
|
|
109
171
|
let text: string;
|
|
110
172
|
let tools: string[];
|
|
111
173
|
|
|
@@ -115,23 +177,20 @@ describe("Skill Eval Harness — Directory MCP", () => {
|
|
|
115
177
|
tools = extractToolRefs(text);
|
|
116
178
|
});
|
|
117
179
|
|
|
118
|
-
it("
|
|
119
|
-
expect(tools).
|
|
180
|
+
it("starts with directory_lookup_person", () => {
|
|
181
|
+
expect(tools[0]).toBe("directory_lookup_person");
|
|
120
182
|
});
|
|
121
183
|
|
|
122
|
-
it("chains to
|
|
123
|
-
expect(tools).toContain("
|
|
184
|
+
it("chains to directory_lookup_work for notable works", () => {
|
|
185
|
+
expect(tools).toContain("directory_lookup_work");
|
|
124
186
|
});
|
|
125
187
|
|
|
126
|
-
it("person lookup comes before work lookup
|
|
127
|
-
// After the routing section, lookup_person_full should appear before lookup_work_full
|
|
128
|
-
const mainFlowStart = text.indexOf("Lookup by name");
|
|
129
|
-
const subText = text.slice(mainFlowStart);
|
|
188
|
+
it("person lookup comes before work lookup", () => {
|
|
130
189
|
expect(
|
|
131
190
|
appearsBeforeInText(
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
"
|
|
191
|
+
text,
|
|
192
|
+
"directory_lookup_person",
|
|
193
|
+
"directory_lookup_work",
|
|
135
194
|
),
|
|
136
195
|
).toBe(true);
|
|
137
196
|
});
|
|
@@ -143,7 +202,7 @@ describe("Skill Eval Harness — Directory MCP", () => {
|
|
|
143
202
|
});
|
|
144
203
|
|
|
145
204
|
it("examines collaborator network", () => {
|
|
146
|
-
expect(text).toMatch(/collaborator
|
|
205
|
+
expect(text).toMatch(/collaborator/i);
|
|
147
206
|
});
|
|
148
207
|
|
|
149
208
|
it("includes use cases for the research output", () => {
|
|
@@ -161,50 +220,43 @@ describe("Skill Eval Harness — Directory MCP", () => {
|
|
|
161
220
|
});
|
|
162
221
|
});
|
|
163
222
|
|
|
164
|
-
describe("
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
text
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("routes ISRC to directory_lookup_isrc", () => {
|
|
173
|
-
expect(text).toMatch(/isrc/i);
|
|
174
|
-
expect(text).toContain("directory_lookup_isrc");
|
|
223
|
+
describe("Cross-prompt routing integrity", () => {
|
|
224
|
+
it("directory-autopilot references both sub-workflow prompts", async () => {
|
|
225
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
226
|
+
const text = result.messages[0].content.text;
|
|
227
|
+
expect(text).toContain("find-music");
|
|
228
|
+
expect(text).toContain("research-creator");
|
|
175
229
|
});
|
|
176
230
|
|
|
177
|
-
it("
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
);
|
|
182
|
-
expect(isrcBlock).toContain("directory_lookup_work_full");
|
|
231
|
+
it("every sub-workflow referenced by autopilot exists as a prompt", () => {
|
|
232
|
+
const promptNames = registry.listPrompts().map((p) => p.name);
|
|
233
|
+
expect(promptNames).toContain("find-music");
|
|
234
|
+
expect(promptNames).toContain("research-creator");
|
|
235
|
+
expect(promptNames).toContain("directory-autopilot");
|
|
183
236
|
});
|
|
184
237
|
|
|
185
|
-
it("
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
238
|
+
it("autopilot has at least 3 routing conditions", async () => {
|
|
239
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
240
|
+
const text = result.messages[0].content.text;
|
|
189
241
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
const conditions = [
|
|
243
|
+
/music for a project/i,
|
|
244
|
+
/information about a creator/i,
|
|
245
|
+
/specific identifier/i,
|
|
246
|
+
];
|
|
194
247
|
|
|
195
|
-
|
|
196
|
-
|
|
248
|
+
for (const cond of conditions) {
|
|
249
|
+
expect(text).toMatch(cond);
|
|
250
|
+
}
|
|
197
251
|
});
|
|
198
252
|
|
|
199
|
-
it("
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
expect(text).
|
|
253
|
+
it("autopilot explains routing decision", async () => {
|
|
254
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
255
|
+
const text = result.messages[0].content.text;
|
|
256
|
+
expect(text).toMatch(/tell me which|explain|why/i);
|
|
203
257
|
});
|
|
204
|
-
});
|
|
205
258
|
|
|
206
|
-
|
|
207
|
-
it("all skills reference at least one directory_ tool", async () => {
|
|
259
|
+
it("all prompts reference at least one directory_ tool", async () => {
|
|
208
260
|
const prompts = registry.listPrompts();
|
|
209
261
|
const noTools: string[] = [];
|
|
210
262
|
|
|
@@ -221,31 +273,10 @@ describe("Skill Eval Harness — Directory MCP", () => {
|
|
|
221
273
|
expect(noTools).toEqual([]);
|
|
222
274
|
});
|
|
223
275
|
|
|
224
|
-
it("
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
for (const prompt of prompts) {
|
|
228
|
-
const result = await registry.getPrompt(prompt.name);
|
|
229
|
-
const text = result.messages[0].content.text;
|
|
230
|
-
expect(text).toContain("Discovery principle");
|
|
231
|
-
expect(text).toContain("llms://primer");
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("all skills are read-only (no create/modify tools)", async () => {
|
|
236
|
-
const prompts = registry.listPrompts();
|
|
237
|
-
|
|
238
|
-
for (const prompt of prompts) {
|
|
239
|
-
const result = await registry.getPrompt(prompt.name);
|
|
240
|
-
const text = result.messages[0].content.text;
|
|
241
|
-
expect(text).toMatch(/read-only/i);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("discover-music includes similarity path to research-creator", async () => {
|
|
246
|
-
const result = await registry.getPrompt("discover-music");
|
|
276
|
+
it("directory-autopilot is read-only (no create/modify language)", async () => {
|
|
277
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
247
278
|
const text = result.messages[0].content.text;
|
|
248
|
-
expect(text).
|
|
279
|
+
expect(text).toMatch(/read-only/i);
|
|
249
280
|
});
|
|
250
281
|
});
|
|
251
282
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory SkillsRegistry test suite — ADR-140 Phase 2b
|
|
5
|
+
*
|
|
6
|
+
* Sister of mcp-server's skills-registry.test.ts. Scoped to the directory
|
|
7
|
+
* MCP's single Stage 1 skill (find-music-for-sync-brief) and its public-skill
|
|
8
|
+
* IP-exposure profile (lower bar — methodology body must be IP-safe).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "@jest/globals";
|
|
12
|
+
import {
|
|
13
|
+
SkillsRegistry,
|
|
14
|
+
SkillNotFoundError,
|
|
15
|
+
} from "../../skills/index.js";
|
|
16
|
+
|
|
17
|
+
describe("Directory SkillsRegistry", () => {
|
|
18
|
+
const registry = new SkillsRegistry();
|
|
19
|
+
|
|
20
|
+
it("registers the Stage 1 directory skill", () => {
|
|
21
|
+
expect(registry.has("find-music-for-sync-brief")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("listSkills returns metadata-only summaries", () => {
|
|
25
|
+
for (const summary of registry.listSkills()) {
|
|
26
|
+
expect(summary).toHaveProperty("triggers");
|
|
27
|
+
expect(summary).not.toHaveProperty("body");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("getSkill returns the methodology body", () => {
|
|
32
|
+
const skill = registry.getSkill("find-music-for-sync-brief");
|
|
33
|
+
expect(skill.body.length).toBeGreaterThan(100);
|
|
34
|
+
expect(skill.body).toContain("# find-music-for-sync-brief");
|
|
35
|
+
expect(skill.tools_required).toContain("directory_search_recordings");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("getSkill throws SkillNotFoundError for unknown name", () => {
|
|
39
|
+
expect(() => registry.getSkill("not-a-real-skill")).toThrow(
|
|
40
|
+
SkillNotFoundError,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("body contains no trade-secret path fragments (directory MCP is public)", () => {
|
|
45
|
+
const FORBIDDEN = [
|
|
46
|
+
"lib/services/chain",
|
|
47
|
+
"lib/services/enrichment",
|
|
48
|
+
"lib/services/pica-export",
|
|
49
|
+
"lib/services/global-identity",
|
|
50
|
+
"lib/services/identity-inheritance",
|
|
51
|
+
"lib/services/machine-payments",
|
|
52
|
+
"lib/services/credit-gating",
|
|
53
|
+
"lib/services/mlc-import",
|
|
54
|
+
"lib/integrations/",
|
|
55
|
+
];
|
|
56
|
+
for (const summary of registry.listSkills()) {
|
|
57
|
+
const skill = registry.getSkill(summary.name);
|
|
58
|
+
for (const fragment of FORBIDDEN) {
|
|
59
|
+
expect(skill.body).not.toContain(fragment);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory skills tools test suite — ADR-140 Phase 2b
|
|
5
|
+
*
|
|
6
|
+
* Covers directory_skill_list + directory_skill_get on the public directory
|
|
7
|
+
* MCP. Same meta-tool shape as the creator MCP, but tools_required names
|
|
8
|
+
* follow the `directory_*` prefix convention.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "@jest/globals";
|
|
12
|
+
import { DirectorySkillsTools } from "../../tools/skills.js";
|
|
13
|
+
|
|
14
|
+
describe("DirectorySkillsTools", () => {
|
|
15
|
+
const skillsTools = new DirectorySkillsTools();
|
|
16
|
+
const tools = skillsTools.getTools();
|
|
17
|
+
|
|
18
|
+
it("registers exactly 2 meta-tools", () => {
|
|
19
|
+
expect(tools.length).toBe(2);
|
|
20
|
+
expect(tools.map((t) => t.definition.name).sort()).toEqual([
|
|
21
|
+
"directory_skill_get",
|
|
22
|
+
"directory_skill_list",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("both tools are tier:read", () => {
|
|
27
|
+
for (const tool of tools) {
|
|
28
|
+
expect(tool.definition.tier).toBe("read");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("directory_skill_list returns the Stage 1 skill", async () => {
|
|
33
|
+
const list = tools.find(
|
|
34
|
+
(t) => t.definition.name === "directory_skill_list",
|
|
35
|
+
)!;
|
|
36
|
+
const result = await list.executor({});
|
|
37
|
+
expect(result.isError).toBeFalsy();
|
|
38
|
+
const summaries = result.structuredContent!.skills as Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
}>;
|
|
41
|
+
expect(summaries.map((s) => s.name)).toContain(
|
|
42
|
+
"find-music-for-sync-brief",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("directory_skill_get returns the methodology body", async () => {
|
|
47
|
+
const get = tools.find(
|
|
48
|
+
(t) => t.definition.name === "directory_skill_get",
|
|
49
|
+
)!;
|
|
50
|
+
const result = await get.executor({ name: "find-music-for-sync-brief" });
|
|
51
|
+
expect(result.isError).toBeFalsy();
|
|
52
|
+
const textBlock = result.content.find((c: any) => c.type === "text") as
|
|
53
|
+
| { type: "text"; text: string }
|
|
54
|
+
| undefined;
|
|
55
|
+
expect(textBlock?.text).toContain("# find-music-for-sync-brief");
|
|
56
|
+
expect(textBlock?.text).toContain("directory_search_recordings");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("directory_skill_get returns isError for unknown skill", async () => {
|
|
60
|
+
const get = tools.find(
|
|
61
|
+
(t) => t.definition.name === "directory_skill_get",
|
|
62
|
+
)!;
|
|
63
|
+
const result = await get.executor({ name: "not-a-real-skill" });
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("directory_skill_get returns isError when name is missing", async () => {
|
|
68
|
+
const get = tools.find(
|
|
69
|
+
(t) => t.definition.name === "directory_skill_get",
|
|
70
|
+
)!;
|
|
71
|
+
const result = await get.executor({});
|
|
72
|
+
expect(result.isError).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
jest,
|
|
5
|
+
describe,
|
|
6
|
+
it,
|
|
7
|
+
expect,
|
|
8
|
+
beforeEach,
|
|
9
|
+
afterEach,
|
|
10
|
+
} from "@jest/globals";
|
|
11
|
+
import { DirectoryChainTools } from "../../tools/chain";
|
|
12
|
+
import { DirectoryClient } from "../../client";
|
|
13
|
+
|
|
14
|
+
describe("DirectoryChainTools", () => {
|
|
15
|
+
let chainTools: DirectoryChainTools;
|
|
16
|
+
let mockClient: jest.Mocked<DirectoryClient>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockClient = { request: jest.fn() } as any;
|
|
20
|
+
chainTools = new DirectoryChainTools(mockClient);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("registers 1 tool", () => {
|
|
28
|
+
const tools = chainTools.getTools();
|
|
29
|
+
expect(tools).toHaveLength(1);
|
|
30
|
+
expect(tools[0].definition.name).toBe("directory_chain");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("directory_chain", () => {
|
|
34
|
+
it("passes free-text query to /chain", async () => {
|
|
35
|
+
mockClient.request.mockResolvedValue({
|
|
36
|
+
success: true,
|
|
37
|
+
results: [{ match_type: "work", work: { title: "Test" } }],
|
|
38
|
+
total: 1,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const tool = chainTools
|
|
42
|
+
.getTools()
|
|
43
|
+
.find((t) => t.definition.name === "directory_chain")!;
|
|
44
|
+
const result = await tool.executor({ q: "test query" });
|
|
45
|
+
|
|
46
|
+
expect(mockClient.request).toHaveBeenCalledWith("/chain", {
|
|
47
|
+
q: "test query",
|
|
48
|
+
});
|
|
49
|
+
expect(result.structuredContent.results).toHaveLength(1);
|
|
50
|
+
expect(result.structuredContent.total).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("passes identifier for direct ISWC/ISRC lookup", async () => {
|
|
54
|
+
mockClient.request.mockResolvedValue({
|
|
55
|
+
success: true,
|
|
56
|
+
results: [],
|
|
57
|
+
total: 0,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const tool = chainTools
|
|
61
|
+
.getTools()
|
|
62
|
+
.find((t) => t.definition.name === "directory_chain")!;
|
|
63
|
+
await tool.executor({ identifier: "T-123.456.789-0" });
|
|
64
|
+
|
|
65
|
+
expect(mockClient.request).toHaveBeenCalledWith("/chain", {
|
|
66
|
+
identifier: "T-123.456.789-0",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("forwards audio filters", async () => {
|
|
71
|
+
mockClient.request.mockResolvedValue({
|
|
72
|
+
success: true,
|
|
73
|
+
results: [],
|
|
74
|
+
total: 0,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const tool = chainTools
|
|
78
|
+
.getTools()
|
|
79
|
+
.find((t) => t.definition.name === "directory_chain")!;
|
|
80
|
+
await tool.executor({
|
|
81
|
+
min_bpm: 100,
|
|
82
|
+
max_bpm: 140,
|
|
83
|
+
key: "C",
|
|
84
|
+
key_mode: "minor",
|
|
85
|
+
mood: "dark",
|
|
86
|
+
limit: 15,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(mockClient.request).toHaveBeenCalledWith("/chain", {
|
|
90
|
+
min_bpm: "100",
|
|
91
|
+
max_bpm: "140",
|
|
92
|
+
key: "C",
|
|
93
|
+
key_mode: "minor",
|
|
94
|
+
mood: "dark",
|
|
95
|
+
limit: "15",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns a human summary when no results", async () => {
|
|
100
|
+
mockClient.request.mockResolvedValue({
|
|
101
|
+
success: true,
|
|
102
|
+
results: [],
|
|
103
|
+
total: 0,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const tool = chainTools
|
|
107
|
+
.getTools()
|
|
108
|
+
.find((t) => t.definition.name === "directory_chain")!;
|
|
109
|
+
const result = await tool.executor({ q: "no-match" });
|
|
110
|
+
|
|
111
|
+
expect(result.content[0].text).toContain("No chain results");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("throws on API failure", async () => {
|
|
115
|
+
mockClient.request.mockRejectedValue(new Error("Network error"));
|
|
116
|
+
const tool = chainTools
|
|
117
|
+
.getTools()
|
|
118
|
+
.find((t) => t.definition.name === "directory_chain")!;
|
|
119
|
+
await expect(tool.executor({ q: "x" })).rejects.toThrow("Network error");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -10,6 +10,7 @@ import { DirectoryWorksTools } from "../../tools/works";
|
|
|
10
10
|
import { DirectoryPeopleTools } from "../../tools/people";
|
|
11
11
|
import { DirectorySearchTools } from "../../tools/search";
|
|
12
12
|
import { DirectoryRecordingsTools } from "../../tools/recordings";
|
|
13
|
+
import { DirectoryChainTools } from "../../tools/chain";
|
|
13
14
|
|
|
14
15
|
const nullClient = null as any;
|
|
15
16
|
|
|
@@ -19,6 +20,7 @@ function getAllTools() {
|
|
|
19
20
|
new DirectoryPeopleTools(nullClient),
|
|
20
21
|
new DirectorySearchTools(nullClient),
|
|
21
22
|
new DirectoryRecordingsTools(nullClient),
|
|
23
|
+
new DirectoryChainTools(nullClient),
|
|
22
24
|
];
|
|
23
25
|
|
|
24
26
|
const tools: Array<{ name: string; description: string }> = [];
|
|
@@ -48,8 +50,8 @@ describe("Directory Composability Chains", () => {
|
|
|
48
50
|
allNames = new Set(allTools.map((t) => t.name));
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
it("all
|
|
52
|
-
expect(allTools).toHaveLength(
|
|
53
|
+
it("all 8 tools are registered", () => {
|
|
54
|
+
expect(allTools).toHaveLength(8);
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
it("all tools have composability chains", () => {
|
|
@@ -24,13 +24,19 @@ describe("DirectoryPeopleTools", () => {
|
|
|
24
24
|
jest.clearAllMocks();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it("registers
|
|
27
|
+
it("registers 2 tools", () => {
|
|
28
28
|
const tools = peopleTools.getTools();
|
|
29
|
-
expect(tools).toHaveLength(
|
|
29
|
+
expect(tools).toHaveLength(2);
|
|
30
30
|
const names = tools.map((t) => t.definition.name);
|
|
31
31
|
expect(names).toContain("directory_list_people");
|
|
32
32
|
expect(names).toContain("directory_lookup_person");
|
|
33
|
-
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("exposes the letter filter on list_people", () => {
|
|
36
|
+
const tool = peopleTools
|
|
37
|
+
.getTools()
|
|
38
|
+
.find((t) => t.definition.name === "directory_list_people")!;
|
|
39
|
+
expect(tool.definition.inputSchema.properties.letter).toBeDefined();
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
describe("directory_list_people", () => {
|