@withpica/mcp-server-directory 1.0.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 +56 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +9 -12
- 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/resources/llms-primer.d.ts.map +1 -1
- package/dist/resources/llms-primer.js +1 -0
- package/dist/resources/llms-primer.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- 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 +3 -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 +24 -36
- package/dist/tools/people.js.map +1 -1
- package/dist/tools/recordings.d.ts.map +1 -1
- package/dist/tools/recordings.js +8 -3
- package/dist/tools/recordings.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +8 -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 +42 -42
- package/dist/tools/works.js.map +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +1 -0
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/formatting.d.ts.map +1 -1
- package/dist/utils/formatting.js +1 -0
- package/dist/utils/formatting.js.map +1 -1
- package/jest.config.js +31 -0
- package/package.json +3 -2
- package/src/__tests__/prompts/index.test.ts +128 -0
- package/src/__tests__/prompts/prompt-eval-harness.test.ts +282 -0
- package/src/__tests__/tools/chain.test.ts +122 -0
- package/src/__tests__/tools/composability-chains.test.ts +100 -0
- package/src/__tests__/tools/people.test.ts +112 -0
- package/src/__tests__/tools/search.test.ts +94 -0
- package/src/__tests__/tools/works.test.ts +177 -0
- package/src/client.ts +128 -0
- package/src/config.ts +23 -0
- package/src/index.ts +36 -0
- package/src/prompts/index.ts +206 -0
- package/src/prompts/public-question-atlas.ts +540 -0
- package/src/resources/llms-primer.ts +35 -0
- package/src/server.ts +134 -0
- package/src/tools/chain.ts +118 -0
- package/src/tools/index.ts +83 -0
- package/src/tools/people.ts +196 -0
- package/src/tools/recordings.ts +149 -0
- package/src/tools/search.ts +66 -0
- package/src/tools/works.ts +266 -0
- package/src/utils/errors.ts +64 -0
- package/src/utils/formatting.ts +28 -0
- package/tsconfig.json +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpica/mcp-server-directory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP Server for PICA Public Directory — enables AI assistants to search verified works and creators",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
15
15
|
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
16
|
-
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage"
|
|
16
|
+
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
17
18
|
},
|
|
18
19
|
"author": "PICA Platform",
|
|
19
20
|
"license": "MIT",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory Prompt Registry Tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeAll } from "@jest/globals";
|
|
8
|
+
import { PromptRegistry } from "../../prompts/index";
|
|
9
|
+
|
|
10
|
+
describe("Directory Prompt Registry", () => {
|
|
11
|
+
const registry = new PromptRegistry();
|
|
12
|
+
|
|
13
|
+
describe("listPrompts", () => {
|
|
14
|
+
it("returns 3 prompts", () => {
|
|
15
|
+
const prompts = registry.listPrompts();
|
|
16
|
+
expect(prompts).toHaveLength(3);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("includes all expected prompts", () => {
|
|
20
|
+
const names = registry.listPrompts().map((p) => p.name);
|
|
21
|
+
expect(names).toContain("find-music");
|
|
22
|
+
expect(names).toContain("research-creator");
|
|
23
|
+
expect(names).toContain("directory-autopilot");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("prompt names are unique and kebab-case", () => {
|
|
27
|
+
const prompts = registry.listPrompts();
|
|
28
|
+
const names = prompts.map((p) => p.name);
|
|
29
|
+
expect(new Set(names).size).toBe(names.length);
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
expect(name).toMatch(/^[a-z][a-z0-9-]*$/);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("getPrompt", () => {
|
|
37
|
+
it("resolves all prompts without error", async () => {
|
|
38
|
+
const prompts = registry.listPrompts();
|
|
39
|
+
for (const prompt of prompts) {
|
|
40
|
+
const result = await registry.getPrompt(prompt.name);
|
|
41
|
+
expect(result.messages).toBeInstanceOf(Array);
|
|
42
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
43
|
+
expect(result.messages[0].content.type).toBe("text");
|
|
44
|
+
expect(result.messages[0].content.text).toBeTruthy();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("throws on unknown prompt", async () => {
|
|
49
|
+
await expect(registry.getPrompt("nonexistent")).rejects.toThrow(
|
|
50
|
+
"Prompt not found",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
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");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("find-music workflow", () => {
|
|
88
|
+
it("references audio search tool", async () => {
|
|
89
|
+
const result = await registry.getPrompt("find-music");
|
|
90
|
+
const text = result.messages[0].content.text;
|
|
91
|
+
expect(text).toContain("directory_search_recordings");
|
|
92
|
+
expect(text).toContain("directory_lookup_work");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("includes mood-to-parameter translation guide", async () => {
|
|
96
|
+
const result = await registry.getPrompt("find-music");
|
|
97
|
+
const text = result.messages[0].content.text;
|
|
98
|
+
expect(text).toMatch(/upbeat|dark|chill/i);
|
|
99
|
+
expect(text).toContain("energy");
|
|
100
|
+
expect(text).toContain("BPM");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("accepts optional brief argument", async () => {
|
|
104
|
+
const result = await registry.getPrompt("find-music", {
|
|
105
|
+
brief: "dark moody piano",
|
|
106
|
+
});
|
|
107
|
+
const text = result.messages[0].content.text;
|
|
108
|
+
expect(text).toContain("dark moody piano");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("research-creator workflow", () => {
|
|
113
|
+
it("references person lookup tools", async () => {
|
|
114
|
+
const result = await registry.getPrompt("research-creator");
|
|
115
|
+
const text = result.messages[0].content.text;
|
|
116
|
+
expect(text).toContain("directory_lookup_person");
|
|
117
|
+
expect(text).toContain("directory_lookup_work");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("accepts optional name argument", async () => {
|
|
121
|
+
const result = await registry.getPrompt("research-creator", {
|
|
122
|
+
name_or_id: "Max Martin",
|
|
123
|
+
});
|
|
124
|
+
const text = result.messages[0].content.text;
|
|
125
|
+
expect(text).toContain("Max Martin");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory Prompt Eval Harness — ADR-140 Phase 3
|
|
5
|
+
*
|
|
6
|
+
* Deterministic test suite that validates directory prompt text produces
|
|
7
|
+
* correct workflow routing. Parses prompt text for tool references,
|
|
8
|
+
* ordering, and routing conditions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll } from "@jest/globals";
|
|
12
|
+
import { PromptRegistry } from "../../prompts/index";
|
|
13
|
+
|
|
14
|
+
const registry = new PromptRegistry();
|
|
15
|
+
|
|
16
|
+
/** Extract all directory_* tool references from text in order of appearance */
|
|
17
|
+
function extractToolRefs(text: string): string[] {
|
|
18
|
+
return [...text.matchAll(/directory_\w+/g)].map((m) => m[0]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Check if tool A appears before tool B in the text */
|
|
22
|
+
function appearsBeforeInText(
|
|
23
|
+
text: string,
|
|
24
|
+
toolA: string,
|
|
25
|
+
toolB: string,
|
|
26
|
+
): boolean {
|
|
27
|
+
const indexA = text.indexOf(toolA);
|
|
28
|
+
const indexB = text.indexOf(toolB);
|
|
29
|
+
return indexA !== -1 && indexB !== -1 && indexA < indexB;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Prompt Eval Harness — Directory MCP", () => {
|
|
33
|
+
describe("Scenario 1: directory-autopilot routes sync searches to find-music", () => {
|
|
34
|
+
let text: string;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
38
|
+
text = result.messages[0].content.text;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reads llms://primer first for orientation", () => {
|
|
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);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("uses directory_search_recordings as the primary search tool", () => {
|
|
130
|
+
expect(tools).toContain("directory_search_recordings");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("chains to directory_lookup_work for detailed results", () => {
|
|
134
|
+
expect(tools).toContain("directory_lookup_work");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("search comes before lookup (correct order)", () => {
|
|
138
|
+
expect(
|
|
139
|
+
appearsBeforeInText(
|
|
140
|
+
text,
|
|
141
|
+
"directory_search_recordings",
|
|
142
|
+
"directory_lookup_work",
|
|
143
|
+
),
|
|
144
|
+
).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("includes mood-to-parameter translation guide", () => {
|
|
148
|
+
// The prompt should help agents translate natural language to search params
|
|
149
|
+
expect(text).toMatch(/energy/i);
|
|
150
|
+
expect(text).toMatch(/bpm/i);
|
|
151
|
+
expect(text).toMatch(/danceability/i);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("covers all key audio search dimensions", () => {
|
|
155
|
+
const dimensions = [/energy/i, /bpm/i, /key/i, /mood/i, /duration/i];
|
|
156
|
+
for (const dim of dimensions) {
|
|
157
|
+
expect(text).toMatch(dim);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("includes result presentation format with credits", () => {
|
|
162
|
+
expect(text).toMatch(/credits|who to contact/i);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("handles no-results case with broadening suggestions", () => {
|
|
166
|
+
expect(text).toMatch(/no results|broaden/i);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("Scenario 5: research-creator references person then work lookup", () => {
|
|
171
|
+
let text: string;
|
|
172
|
+
let tools: string[];
|
|
173
|
+
|
|
174
|
+
beforeAll(async () => {
|
|
175
|
+
const result = await registry.getPrompt("research-creator");
|
|
176
|
+
text = result.messages[0].content.text;
|
|
177
|
+
tools = extractToolRefs(text);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("starts with directory_lookup_person", () => {
|
|
181
|
+
expect(tools[0]).toBe("directory_lookup_person");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("chains to directory_lookup_work for notable works", () => {
|
|
185
|
+
expect(tools).toContain("directory_lookup_work");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("person lookup comes before work lookup", () => {
|
|
189
|
+
expect(
|
|
190
|
+
appearsBeforeInText(
|
|
191
|
+
text,
|
|
192
|
+
"directory_lookup_person",
|
|
193
|
+
"directory_lookup_work",
|
|
194
|
+
),
|
|
195
|
+
).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("checks identifiers (IPI, ISNI, MusicBrainz)", () => {
|
|
199
|
+
expect(text).toMatch(/ipi/i);
|
|
200
|
+
expect(text).toMatch(/isni/i);
|
|
201
|
+
expect(text).toMatch(/musicbrainz/i);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("examines collaborator network", () => {
|
|
205
|
+
expect(text).toMatch(/collaborator/i);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("includes use cases for the research output", () => {
|
|
209
|
+
expect(text).toMatch(
|
|
210
|
+
/rights research|due diligence|licensing|publisher|acquisition/i,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("accepts name_or_id argument and includes it in search", async () => {
|
|
215
|
+
const result = await registry.getPrompt("research-creator", {
|
|
216
|
+
name_or_id: "Diane Warren",
|
|
217
|
+
});
|
|
218
|
+
const argText = result.messages[0].content.text;
|
|
219
|
+
expect(argText).toContain("Diane Warren");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
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");
|
|
229
|
+
});
|
|
230
|
+
|
|
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");
|
|
236
|
+
});
|
|
237
|
+
|
|
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;
|
|
241
|
+
|
|
242
|
+
const conditions = [
|
|
243
|
+
/music for a project/i,
|
|
244
|
+
/information about a creator/i,
|
|
245
|
+
/specific identifier/i,
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
for (const cond of conditions) {
|
|
249
|
+
expect(text).toMatch(cond);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
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);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("all prompts reference at least one directory_ tool", async () => {
|
|
260
|
+
const prompts = registry.listPrompts();
|
|
261
|
+
const noTools: string[] = [];
|
|
262
|
+
|
|
263
|
+
for (const prompt of prompts) {
|
|
264
|
+
const result = await registry.getPrompt(prompt.name);
|
|
265
|
+
const text = result.messages[0].content.text;
|
|
266
|
+
const tools = extractToolRefs(text);
|
|
267
|
+
|
|
268
|
+
if (tools.length === 0) {
|
|
269
|
+
noTools.push(prompt.name);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expect(noTools).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("directory-autopilot is read-only (no create/modify language)", async () => {
|
|
277
|
+
const result = await registry.getPrompt("directory-autopilot");
|
|
278
|
+
const text = result.messages[0].content.text;
|
|
279
|
+
expect(text).toMatch(/read-only/i);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory MCP — Composability Chain Tests
|
|
5
|
+
* Verifies "→ then:" chains point to valid tools
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll } from "@jest/globals";
|
|
9
|
+
import { DirectoryWorksTools } from "../../tools/works";
|
|
10
|
+
import { DirectoryPeopleTools } from "../../tools/people";
|
|
11
|
+
import { DirectorySearchTools } from "../../tools/search";
|
|
12
|
+
import { DirectoryRecordingsTools } from "../../tools/recordings";
|
|
13
|
+
import { DirectoryChainTools } from "../../tools/chain";
|
|
14
|
+
|
|
15
|
+
const nullClient = null as any;
|
|
16
|
+
|
|
17
|
+
function getAllTools() {
|
|
18
|
+
const classes = [
|
|
19
|
+
new DirectoryWorksTools(nullClient),
|
|
20
|
+
new DirectoryPeopleTools(nullClient),
|
|
21
|
+
new DirectorySearchTools(nullClient),
|
|
22
|
+
new DirectoryRecordingsTools(nullClient),
|
|
23
|
+
new DirectoryChainTools(nullClient),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const tools: Array<{ name: string; description: string }> = [];
|
|
27
|
+
for (const cls of classes) {
|
|
28
|
+
for (const tool of cls.getTools()) {
|
|
29
|
+
tools.push({
|
|
30
|
+
name: tool.definition.name,
|
|
31
|
+
description: tool.definition.description,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return tools;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseChains(description: string): string[] {
|
|
39
|
+
const match = description.match(/→ then: (.+)$/);
|
|
40
|
+
if (!match) return [];
|
|
41
|
+
return [...match[1].matchAll(/directory_\w+/g)].map((m) => m[0]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("Directory Composability Chains", () => {
|
|
45
|
+
let allTools: Array<{ name: string; description: string }>;
|
|
46
|
+
let allNames: Set<string>;
|
|
47
|
+
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
allTools = getAllTools();
|
|
50
|
+
allNames = new Set(allTools.map((t) => t.name));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("all 8 tools are registered", () => {
|
|
54
|
+
expect(allTools).toHaveLength(8);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("all tools have composability chains", () => {
|
|
58
|
+
const missing = allTools.filter((t) => !t.description.includes("→ then:"));
|
|
59
|
+
expect(missing.map((t) => t.name)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("all chain references point to existing tools", () => {
|
|
63
|
+
const broken: string[] = [];
|
|
64
|
+
for (const tool of allTools) {
|
|
65
|
+
const refs = parseChains(tool.description);
|
|
66
|
+
for (const ref of refs) {
|
|
67
|
+
if (!allNames.has(ref)) {
|
|
68
|
+
broken.push(`${tool.name} → ${ref} (not found)`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
expect(broken).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("no tool chains reference itself", () => {
|
|
76
|
+
const selfRefs = allTools.filter((t) =>
|
|
77
|
+
parseChains(t.description).includes(t.name),
|
|
78
|
+
);
|
|
79
|
+
expect(selfRefs.map((t) => t.name)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("every chain includes a reason hint", () => {
|
|
83
|
+
const missingReasons: string[] = [];
|
|
84
|
+
for (const tool of allTools) {
|
|
85
|
+
if (!tool.description.includes("→ then:")) continue;
|
|
86
|
+
const chainPart = tool.description.split("→ then:")[1];
|
|
87
|
+
const refs = [...chainPart.matchAll(/directory_\w+/g)];
|
|
88
|
+
for (const ref of refs) {
|
|
89
|
+
const afterRef = chainPart.slice(
|
|
90
|
+
(ref.index ?? 0) + ref[0].length,
|
|
91
|
+
(ref.index ?? 0) + ref[0].length + 50,
|
|
92
|
+
);
|
|
93
|
+
if (!afterRef.match(/^\s*\(/)) {
|
|
94
|
+
missingReasons.push(`${tool.name} → ${ref[0]} missing reason`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
expect(missingReasons).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
});
|