@withpica/mcp-server-directory 1.1.0 → 1.2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +43 -26
  2. package/dist/prompts/index.d.ts +5 -6
  3. package/dist/prompts/index.d.ts.map +1 -1
  4. package/dist/prompts/index.js +92 -135
  5. package/dist/prompts/index.js.map +1 -1
  6. package/dist/prompts/public-question-atlas.d.ts +121 -0
  7. package/dist/prompts/public-question-atlas.d.ts.map +1 -0
  8. package/dist/prompts/public-question-atlas.js +404 -0
  9. package/dist/prompts/public-question-atlas.js.map +1 -0
  10. package/dist/tools/chain.d.ts +12 -0
  11. package/dist/tools/chain.d.ts.map +1 -0
  12. package/dist/tools/chain.js +109 -0
  13. package/dist/tools/chain.js.map +1 -0
  14. package/dist/tools/index.d.ts +9 -0
  15. package/dist/tools/index.d.ts.map +1 -1
  16. package/dist/tools/index.js +2 -0
  17. package/dist/tools/index.js.map +1 -1
  18. package/dist/tools/people.d.ts +0 -1
  19. package/dist/tools/people.d.ts.map +1 -1
  20. package/dist/tools/people.js +23 -36
  21. package/dist/tools/people.js.map +1 -1
  22. package/dist/tools/recordings.d.ts.map +1 -1
  23. package/dist/tools/recordings.js +7 -3
  24. package/dist/tools/recordings.js.map +1 -1
  25. package/dist/tools/search.d.ts.map +1 -1
  26. package/dist/tools/search.js +7 -4
  27. package/dist/tools/search.js.map +1 -1
  28. package/dist/tools/works.d.ts +0 -1
  29. package/dist/tools/works.d.ts.map +1 -1
  30. package/dist/tools/works.js +41 -42
  31. package/dist/tools/works.js.map +1 -1
  32. package/package.json +3 -2
  33. package/src/__tests__/prompts/index.test.ts +47 -64
  34. package/src/__tests__/prompts/prompt-eval-harness.test.ts +135 -104
  35. package/src/__tests__/tools/chain.test.ts +122 -0
  36. package/src/__tests__/tools/composability-chains.test.ts +4 -2
  37. package/src/__tests__/tools/people.test.ts +9 -3
  38. package/src/__tests__/tools/works.test.ts +32 -3
  39. package/src/prompts/index.ts +97 -141
  40. package/src/prompts/public-question-atlas.ts +540 -0
  41. package/src/tools/chain.ts +118 -0
  42. package/src/tools/index.ts +12 -0
  43. package/src/tools/people.ts +22 -41
  44. package/src/tools/recordings.ts +7 -3
  45. package/src/tools/search.ts +7 -4
  46. package/src/tools/works.ts +39 -46
@@ -1,28 +1,29 @@
1
1
  // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
- * Directory Skill Registry Tests
4
+ * Directory Prompt Registry Tests
5
5
  */
6
6
 
7
7
  import { describe, it, expect, beforeAll } from "@jest/globals";
8
8
  import { PromptRegistry } from "../../prompts/index";
9
9
 
10
- describe("Directory Skill Registry", () => {
10
+ describe("Directory Prompt Registry", () => {
11
11
  const registry = new PromptRegistry();
12
12
 
13
13
  describe("listPrompts", () => {
14
- it("returns 2 skills", () => {
14
+ it("returns 3 prompts", () => {
15
15
  const prompts = registry.listPrompts();
16
- expect(prompts).toHaveLength(2);
16
+ expect(prompts).toHaveLength(3);
17
17
  });
18
18
 
19
- it("includes all expected skills", () => {
19
+ it("includes all expected prompts", () => {
20
20
  const names = registry.listPrompts().map((p) => p.name);
21
- expect(names).toContain("discover-music");
21
+ expect(names).toContain("find-music");
22
22
  expect(names).toContain("research-creator");
23
+ expect(names).toContain("directory-autopilot");
23
24
  });
24
25
 
25
- it("skill names are unique and kebab-case", () => {
26
+ it("prompt names are unique and kebab-case", () => {
26
27
  const prompts = registry.listPrompts();
27
28
  const names = prompts.map((p) => p.name);
28
29
  expect(new Set(names).size).toBe(names.length);
@@ -33,7 +34,7 @@ describe("Directory Skill Registry", () => {
33
34
  });
34
35
 
35
36
  describe("getPrompt", () => {
36
- it("resolves all skills without error", async () => {
37
+ it("resolves all prompts without error", async () => {
37
38
  const prompts = registry.listPrompts();
38
39
  for (const prompt of prompts) {
39
40
  const result = await registry.getPrompt(prompt.name);
@@ -49,28 +50,50 @@ describe("Directory Skill Registry", () => {
49
50
  "Prompt not found",
50
51
  );
51
52
  });
53
+ });
52
54
 
53
- it("every skill includes the shared preamble", async () => {
54
- const prompts = registry.listPrompts();
55
- for (const prompt of prompts) {
56
- const result = await registry.getPrompt(prompt.name);
57
- const text = result.messages[0].content.text;
58
- expect(text).toContain("llms://primer");
59
- expect(text).toContain("Discovery principle");
60
- }
55
+ describe("directory-autopilot routing", () => {
56
+ let text: string;
57
+
58
+ beforeAll(async () => {
59
+ const result = await registry.getPrompt("directory-autopilot");
60
+ text = result.messages[0].content.text;
61
+ });
62
+
63
+ it("reads the primer first", () => {
64
+ expect(text).toContain("llms://primer");
65
+ });
66
+
67
+ it("routes sync searches to find-music workflow", () => {
68
+ expect(text).toMatch(/sync|playlist|project/i);
69
+ expect(text).toContain("find-music");
70
+ });
71
+
72
+ it("routes creator research to lookup tools", () => {
73
+ expect(text).toContain("directory_lookup_person");
74
+ });
75
+
76
+ it("routes identifier resolution to specific tools", () => {
77
+ expect(text).toContain("directory_lookup_isrc");
78
+ expect(text).toContain("directory_lookup_work");
79
+ });
80
+
81
+ it("handles browsing requests", () => {
82
+ expect(text).toContain("directory_search");
83
+ expect(text).toContain("directory_list_works");
61
84
  });
62
85
  });
63
86
 
64
- describe("discover-music skill", () => {
87
+ describe("find-music workflow", () => {
65
88
  it("references audio search tool", async () => {
66
- const result = await registry.getPrompt("discover-music");
89
+ const result = await registry.getPrompt("find-music");
67
90
  const text = result.messages[0].content.text;
68
91
  expect(text).toContain("directory_search_recordings");
69
- expect(text).toContain("directory_lookup_work_full");
92
+ expect(text).toContain("directory_lookup_work");
70
93
  });
71
94
 
72
95
  it("includes mood-to-parameter translation guide", async () => {
73
- const result = await registry.getPrompt("discover-music");
96
+ const result = await registry.getPrompt("find-music");
74
97
  const text = result.messages[0].content.text;
75
98
  expect(text).toMatch(/upbeat|dark|chill/i);
76
99
  expect(text).toContain("energy");
@@ -78,39 +101,20 @@ describe("Directory Skill Registry", () => {
78
101
  });
79
102
 
80
103
  it("accepts optional brief argument", async () => {
81
- const result = await registry.getPrompt("discover-music", {
104
+ const result = await registry.getPrompt("find-music", {
82
105
  brief: "dark moody piano",
83
106
  });
84
107
  const text = result.messages[0].content.text;
85
108
  expect(text).toContain("dark moody piano");
86
109
  });
87
-
88
- it("emphasises equal discovery principle", async () => {
89
- const result = await registry.getPrompt("discover-music");
90
- const text = result.messages[0].content.text;
91
- expect(text).toMatch(/equal discovery/i);
92
- expect(text).toMatch(/not.*popularity|not.*fame/i);
93
- });
94
-
95
- it("includes reference track search path", async () => {
96
- const result = await registry.getPrompt("discover-music");
97
- const text = result.messages[0].content.text;
98
- expect(text).toMatch(/reference track/i);
99
- });
100
-
101
- it("includes similarity exploration path", async () => {
102
- const result = await registry.getPrompt("discover-music");
103
- const text = result.messages[0].content.text;
104
- expect(text).toMatch(/similar/i);
105
- });
106
110
  });
107
111
 
108
- describe("research-creator skill", () => {
112
+ describe("research-creator workflow", () => {
109
113
  it("references person lookup tools", async () => {
110
114
  const result = await registry.getPrompt("research-creator");
111
115
  const text = result.messages[0].content.text;
112
- expect(text).toContain("directory_lookup_person_full");
113
- expect(text).toContain("directory_lookup_work_full");
116
+ expect(text).toContain("directory_lookup_person");
117
+ expect(text).toContain("directory_lookup_work");
114
118
  });
115
119
 
116
120
  it("accepts optional name argument", async () => {
@@ -120,26 +124,5 @@ describe("Directory Skill Registry", () => {
120
124
  const text = result.messages[0].content.text;
121
125
  expect(text).toContain("Max Martin");
122
126
  });
123
-
124
- it("handles identifier routing from old directory-autopilot", async () => {
125
- const result = await registry.getPrompt("research-creator");
126
- const text = result.messages[0].content.text;
127
- expect(text).toContain("directory_lookup_isrc");
128
- expect(text).toContain("directory_list_works");
129
- expect(text).toContain("directory_list_people");
130
- });
131
-
132
- it("includes collaborator network exploration", async () => {
133
- const result = await registry.getPrompt("research-creator");
134
- const text = result.messages[0].content.text;
135
- expect(text).toMatch(/collaborator.*network/i);
136
- });
137
-
138
- it("includes similarity search", async () => {
139
- const result = await registry.getPrompt("research-creator");
140
- const text = result.messages[0].content.text;
141
- expect(text).toContain("directory_search_recordings");
142
- expect(text).toMatch(/similar/i);
143
- });
144
127
  });
145
128
  });
@@ -1,10 +1,10 @@
1
1
  // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
- * Directory Skill Eval Harness — ADR-140 Phase 3 + ADR-171 Skills
4
+ * Directory Prompt Eval Harness — ADR-140 Phase 3
5
5
  *
6
- * Deterministic test suite that validates directory skill text produces
7
- * correct workflow routing. Parses skill text for tool references,
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("Skill Eval Harness — Directory MCP", () => {
33
- describe("Scenario 1: discover-music search then lookup workflow", () => {
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("discover-music");
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 in the shared preamble", () => {
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 directory_lookup_work_full for detailed results", () => {
52
- expect(tools).toContain("directory_lookup_work_full");
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
- "directory_lookup_work_full",
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 2: research-creator references person then work lookup", () => {
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("references directory_lookup_person_full as primary lookup", () => {
119
- expect(tools).toContain("directory_lookup_person_full");
180
+ it("starts with directory_lookup_person", () => {
181
+ expect(tools[0]).toBe("directory_lookup_person");
120
182
  });
121
183
 
122
- it("chains to directory_lookup_work_full for notable works", () => {
123
- expect(tools).toContain("directory_lookup_work_full");
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 in the main flow", () => {
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
- subText,
133
- "directory_lookup_person_full",
134
- "directory_lookup_work_full",
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.*network/i);
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("Scenario 3: research-creator handles identifier routing (from old directory-autopilot)", () => {
165
- let text: string;
166
-
167
- beforeAll(async () => {
168
- const result = await registry.getPrompt("research-creator");
169
- text = result.messages[0].content.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("chains ISRC lookup to directory_lookup_work_full", () => {
178
- const isrcBlock = text.slice(
179
- text.search(/ISRC/),
180
- text.search(/ISRC/) + 200,
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("routes IPI/ISNI to directory_lookup_person_full", () => {
186
- expect(text).toMatch(/ipi|isni/i);
187
- expect(text).toContain("directory_lookup_person_full");
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
- it("routes ISWC to directory_lookup_work_full", () => {
191
- expect(text).toMatch(/iswc/i);
192
- expect(text).toContain("directory_lookup_work_full");
193
- });
242
+ const conditions = [
243
+ /music for a project/i,
244
+ /information about a creator/i,
245
+ /specific identifier/i,
246
+ ];
194
247
 
195
- it("handles MusicBrainz IDs", () => {
196
- expect(text).toMatch(/musicbrainz/i);
248
+ for (const cond of conditions) {
249
+ expect(text).toMatch(cond);
250
+ }
197
251
  });
198
252
 
199
- it("supports browsing via directory_search and list tools", () => {
200
- expect(text).toContain("directory_search");
201
- expect(text).toContain("directory_list_works");
202
- expect(text).toContain("directory_list_people");
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
- describe("Cross-skill integrity", () => {
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("all skills include the shared preamble with discovery principle", async () => {
225
- const prompts = registry.listPrompts();
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).toContain("research-creator");
279
+ expect(text).toMatch(/read-only/i);
249
280
  });
250
281
  });
251
282
  });
@@ -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 9 tools are registered", () => {
52
- expect(allTools).toHaveLength(9);
53
+ it("all 8 tools are registered", () => {
54
+ expect(allTools).toHaveLength(8);
53
55
  });
54
56
 
55
57
  it("all tools have composability chains", () => {