@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.
- package/CHANGELOG.md +43 -26
- package/dist/prompts/index.d.ts +5 -6
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +92 -135
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/public-question-atlas.d.ts +121 -0
- package/dist/prompts/public-question-atlas.d.ts.map +1 -0
- package/dist/prompts/public-question-atlas.js +404 -0
- package/dist/prompts/public-question-atlas.js.map +1 -0
- package/dist/tools/chain.d.ts +12 -0
- package/dist/tools/chain.d.ts.map +1 -0
- package/dist/tools/chain.js +109 -0
- package/dist/tools/chain.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/people.d.ts +0 -1
- package/dist/tools/people.d.ts.map +1 -1
- package/dist/tools/people.js +23 -36
- package/dist/tools/people.js.map +1 -1
- package/dist/tools/recordings.d.ts.map +1 -1
- package/dist/tools/recordings.js +7 -3
- package/dist/tools/recordings.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +7 -4
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/works.d.ts +0 -1
- package/dist/tools/works.d.ts.map +1 -1
- package/dist/tools/works.js +41 -42
- package/dist/tools/works.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/prompts/index.test.ts +47 -64
- package/src/__tests__/prompts/prompt-eval-harness.test.ts +135 -104
- 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 +540 -0
- package/src/tools/chain.ts +118 -0
- package/src/tools/index.ts +12 -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/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
|
|
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
|
|
10
|
+
describe("Directory Prompt Registry", () => {
|
|
11
11
|
const registry = new PromptRegistry();
|
|
12
12
|
|
|
13
13
|
describe("listPrompts", () => {
|
|
14
|
-
it("returns
|
|
14
|
+
it("returns 3 prompts", () => {
|
|
15
15
|
const prompts = registry.listPrompts();
|
|
16
|
-
expect(prompts).toHaveLength(
|
|
16
|
+
expect(prompts).toHaveLength(3);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it("includes all expected
|
|
19
|
+
it("includes all expected prompts", () => {
|
|
20
20
|
const names = registry.listPrompts().map((p) => p.name);
|
|
21
|
-
expect(names).toContain("
|
|
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("
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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("
|
|
87
|
+
describe("find-music workflow", () => {
|
|
65
88
|
it("references audio search tool", async () => {
|
|
66
|
-
const result = await registry.getPrompt("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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("
|
|
113
|
-
expect(text).toContain("
|
|
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
|
|
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,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", () => {
|