@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
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
5
- import { formatAsText, formatStructuredList } from "../utils/formatting.js";
5
+ import { formatStructuredList } from "../utils/formatting.js";
6
6
 
7
7
  export class DirectoryPeopleTools {
8
8
  private client: DirectoryClient;
@@ -16,10 +16,14 @@ export class DirectoryPeopleTools {
16
16
  {
17
17
  definition: {
18
18
  name: "directory_list_people",
19
+ tier: "read",
19
20
  description:
21
+ "Use when the user asks: 'list creators in PICA', 'people starting with B', " +
22
+ "'browse the people directory'. " +
20
23
  "Browse and filter creators in the PICA public directory. " +
21
- "Search by name, ISNI, or IPI number. Returns paginated results. " +
22
- " then: directory_lookup_person_full (full profile with works and identifiers)",
24
+ "Filter by free-text (q), ISNI, IPI, or starting letter (A–Z, or '#' for numeric). " +
25
+ "Returns paginated results. " +
26
+ "→ then: directory_lookup_person (full profile with works and identifiers)",
23
27
  inputSchema: {
24
28
  type: "object",
25
29
  properties: {
@@ -35,6 +39,11 @@ export class DirectoryPeopleTools {
35
39
  type: "string",
36
40
  description: "Filter by IPI number",
37
41
  },
42
+ letter: {
43
+ type: "string",
44
+ description:
45
+ "Filter by last name starting letter (A–Z), or '#' for numeric",
46
+ },
38
47
  page: {
39
48
  type: "number",
40
49
  description: "Page number (default: 1, 20 results per page)",
@@ -47,10 +56,16 @@ export class DirectoryPeopleTools {
47
56
  {
48
57
  definition: {
49
58
  name: "directory_lookup_person",
59
+ tier: "read",
50
60
  description:
51
- "Get full details of a single creator by their global creator ID, ISNI, IPI, or MusicBrainz ID. " +
52
- "Returns roles, works, identifiers, and profile information. " +
53
- " then: directory_lookup_person_full (agent-optimised markdown), directory_lookup_work_full (inspect a credited work)",
61
+ "Use when the user asks: 'lookup IPI 12345', 'who is creator X?', " +
62
+ "'tell me about creator X', 'lookup ISNI X'. " +
63
+ "Also call this BEFORE directory_list_works when the input person name is ambiguous " +
64
+ "(e.g. 'Bowie', 'Sade', 'Madonna') to surface candidate persons for disambiguation. " +
65
+ "Get full details of a creator by global creator ID (UUID), ISNI, IPI number, or MusicBrainz ID. " +
66
+ "Returns identifiers, credited works with roles, collaborator network, and verification score " +
67
+ "as a structured markdown summary. " +
68
+ "→ then: directory_lookup_work (inspect a credited work), directory_search_recordings (find their recordings by audio)",
54
69
  inputSchema: {
55
70
  type: "object",
56
71
  properties: {
@@ -65,28 +80,6 @@ export class DirectoryPeopleTools {
65
80
  },
66
81
  executor: this.lookupPerson.bind(this),
67
82
  },
68
- {
69
- definition: {
70
- name: "directory_lookup_person_full",
71
- description:
72
- "Get complete details about a person in the public directory — identifiers (IPI, ISNI), " +
73
- "credited works with roles, collaborator network, and verification score. " +
74
- "Returns a structured markdown summary. " +
75
- "→ then: directory_lookup_work_full (inspect a specific work), directory_search_recordings (find their recordings by audio)",
76
- inputSchema: {
77
- type: "object",
78
- properties: {
79
- id: {
80
- type: "string",
81
- description:
82
- "Global creator ID (UUID), ISNI, IPI number, or MusicBrainz ID",
83
- },
84
- },
85
- required: ["id"],
86
- },
87
- },
88
- executor: this.lookupPersonFull.bind(this),
89
- },
90
83
  ];
91
84
  }
92
85
 
@@ -103,6 +96,7 @@ export class DirectoryPeopleTools {
103
96
  if (args.q) params.q = args.q;
104
97
  if (args.isni) params.isni = args.isni;
105
98
  if (args.ipi) params.ipi = args.ipi;
99
+ if (args.letter) params.letter = args.letter;
106
100
 
107
101
  const response: any = await this.client.request("/people", params);
108
102
 
@@ -118,14 +112,6 @@ export class DirectoryPeopleTools {
118
112
  `/people/${encodeURIComponent(args.id)}`,
119
113
  );
120
114
 
121
- return formatAsText(response.data);
122
- }
123
-
124
- private async lookupPersonFull(args: Record<string, any>): Promise<any> {
125
- const response: any = await this.client.request(
126
- `/people/${encodeURIComponent(args.id)}`,
127
- );
128
-
129
115
  const data = response.data;
130
116
  if (!data) {
131
117
  return {
@@ -136,12 +122,10 @@ export class DirectoryPeopleTools {
136
122
 
137
123
  const lines: string[] = [];
138
124
 
139
- // Name heading
140
125
  const name = data.name || data.full_name || data.display_name || "Unknown";
141
126
  lines.push(`## ${name}`);
142
127
  lines.push("");
143
128
 
144
- // Score / verification
145
129
  if (data.score !== undefined && data.score !== null) {
146
130
  const tier =
147
131
  data.score >= 80
@@ -153,7 +137,6 @@ export class DirectoryPeopleTools {
153
137
  lines.push("");
154
138
  }
155
139
 
156
- // Identifiers
157
140
  const identifiers: Array<{ type: string; value: string }> = [];
158
141
  if (data.ipi || data.ipi_number)
159
142
  identifiers.push({ type: "IPI", value: data.ipi || data.ipi_number });
@@ -174,7 +157,6 @@ export class DirectoryPeopleTools {
174
157
  }
175
158
  lines.push("");
176
159
 
177
- // Works
178
160
  const works: any[] = data.works || data.credited_works || [];
179
161
  lines.push(`### Works (${works.length})`);
180
162
  if (works.length > 0) {
@@ -190,7 +172,6 @@ export class DirectoryPeopleTools {
190
172
  }
191
173
  lines.push("");
192
174
 
193
- // Collaborators
194
175
  const collaborators: any[] = data.collaborators || [];
195
176
  lines.push(`### Collaborators (${collaborators.length})`);
196
177
  if (collaborators.length > 0) {
@@ -16,12 +16,16 @@ export class DirectoryRecordingsTools {
16
16
  {
17
17
  definition: {
18
18
  name: "directory_search_recordings",
19
+ tier: "read",
19
20
  description:
20
- "Search recordings across the PICA directory by audio characteristics. " +
21
+ "Use when the user asks: 'find upbeat tracks around 120 BPM', " +
22
+ "'find instrumental tracks in C minor', 'find energetic music for sync', " +
23
+ "'tracks at X BPM in [key]'. " +
24
+ "AUDIO-FEATURE search only — for name- or title-shaped queries use directory_lookup_work, " +
25
+ "directory_lookup_isrc, or directory_lookup_person, NOT this tool. " +
21
26
  "Find tracks by BPM, key, energy, danceability, duration, and more. " +
22
27
  "Only returns recordings from organisations that have opted into the directory. " +
23
- "Use this for sync licensing searches like 'find upbeat tracks around 120 BPM in a minor key'. " +
24
- "→ then: directory_lookup_work_full (credits and licensing details for a matched track)",
28
+ " then: directory_lookup_work (credits and licensing details for a matched track)",
25
29
  inputSchema: {
26
30
  type: "object",
27
31
  properties: {
@@ -16,11 +16,14 @@ export class DirectorySearchTools {
16
16
  {
17
17
  definition: {
18
18
  name: "directory_search",
19
+ tier: "read",
19
20
  description:
20
- "Search across all verified works and creators in the PICA public directory. " +
21
- "Returns mixed results sorted by relevance. Use this for broad searches; " +
22
- "use list_works or list_people for filtered browsing. " +
23
- " then: directory_lookup_work_full (work details), directory_lookup_person_full (creator details)",
21
+ "Use when the user asks: 'find me X', 'who or what is X?', " +
22
+ "'find any record of X'. " +
23
+ "Use when the entity type is not yet known. " +
24
+ "Convenience fan-out across works and creators runs list_works and list_people with the same query and returns the union. " +
25
+ "No cross-type ranking; prefer list_works or list_people when the entity type is known. " +
26
+ "→ then: directory_lookup_work (work details), directory_lookup_person (creator details)",
24
27
  inputSchema: {
25
28
  type: "object",
26
29
  properties: {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
5
- import { formatAsText, formatStructuredList } from "../utils/formatting.js";
5
+ import { formatStructuredList } from "../utils/formatting.js";
6
6
 
7
7
  export class DirectoryWorksTools {
8
8
  private client: DirectoryClient;
@@ -16,10 +16,14 @@ export class DirectoryWorksTools {
16
16
  {
17
17
  definition: {
18
18
  name: "directory_list_works",
19
+ tier: "read",
19
20
  description:
21
+ "Use when the user asks: 'list works in PICA', 'works starting with B', " +
22
+ "'find works by publisher X', 'browse the catalog'. " +
20
23
  "Browse and filter verified musical works in the PICA public directory. " +
21
- "Search by title, ISWC, publisher, or label. Returns paginated results. " +
22
- " then: directory_lookup_work_full (full details with credits and audio)",
24
+ "Filter by free-text (q), ISWC, ISRC, publisher, label, or starting letter (A–Z, or '#' for numeric). " +
25
+ "Sort by title (default) or recent. Returns paginated results. " +
26
+ "→ then: directory_lookup_work (full details with credits and audio)",
23
27
  inputSchema: {
24
28
  type: "object",
25
29
  properties: {
@@ -31,6 +35,11 @@ export class DirectoryWorksTools {
31
35
  type: "string",
32
36
  description: "Filter by ISWC (e.g. T-123.456.789-0)",
33
37
  },
38
+ isrc: {
39
+ type: "string",
40
+ description:
41
+ "Filter by ISRC (exact match, cross-table recording lookup)",
42
+ },
34
43
  publisher: {
35
44
  type: "string",
36
45
  description: "Filter by publisher name (partial match)",
@@ -39,6 +48,16 @@ export class DirectoryWorksTools {
39
48
  type: "string",
40
49
  description: "Filter by record label (partial match)",
41
50
  },
51
+ letter: {
52
+ type: "string",
53
+ description:
54
+ "Filter by title starting letter (A–Z), or '#' for numeric",
55
+ },
56
+ sort: {
57
+ type: "string",
58
+ enum: ["title", "recent"],
59
+ description: "Sort order (default: title)",
60
+ },
42
61
  page: {
43
62
  type: "number",
44
63
  description: "Page number (default: 1, 20 results per page)",
@@ -51,10 +70,15 @@ export class DirectoryWorksTools {
51
70
  {
52
71
  definition: {
53
72
  name: "directory_lookup_work",
73
+ tier: "read",
54
74
  description:
55
- "Get full details of a single work by ISWC or work ID. " +
56
- "Returns credits, recordings, attestation status, provenance, DSP links, and carbon badge. " +
57
- " then: directory_lookup_work_full (agent-optimised markdown), directory_lookup_person_full (inspect a credited writer)",
75
+ "Use when the user asks: 'what's the ISWC for X?', 'who wrote X?', " +
76
+ "'tell me about song X', 'is this song registered?'. " +
77
+ "Get full details of a single work by ISWC or work UUID. " +
78
+ "Returns credits with IPI numbers and attestation status, recordings with ISRCs, " +
79
+ "audio analysis (BPM, key, energy), DSP links (Spotify, Apple Music), " +
80
+ "AI provenance, and registration score as a structured markdown summary. " +
81
+ "→ then: directory_lookup_person (research a credited writer), directory_search_recordings (find similar tracks by audio)",
58
82
  inputSchema: {
59
83
  type: "object",
60
84
  properties: {
@@ -68,34 +92,16 @@ export class DirectoryWorksTools {
68
92
  },
69
93
  executor: this.lookupWork.bind(this),
70
94
  },
71
- {
72
- definition: {
73
- name: "directory_lookup_work_full",
74
- description:
75
- "Get complete details about a work in the public directory — credits with IPI numbers and attestation status, " +
76
- "recordings with ISRCs, audio analysis (BPM, key, energy), DSP links (Spotify, Apple Music), " +
77
- "AI provenance, and registration score. Returns a structured markdown summary optimised for agent reasoning. " +
78
- "→ then: directory_lookup_person_full (research a credited writer), directory_search_recordings (find similar tracks by audio)",
79
- inputSchema: {
80
- type: "object",
81
- properties: {
82
- identifier: {
83
- type: "string",
84
- description: "ISWC (e.g. T-123.456.789-0) or work UUID",
85
- },
86
- },
87
- required: ["identifier"],
88
- },
89
- },
90
- executor: this.lookupWorkFull.bind(this),
91
- },
92
95
  {
93
96
  definition: {
94
97
  name: "directory_lookup_isrc",
98
+ tier: "read",
95
99
  description:
96
- "Find musical work(s) associated with an ISRC code. " +
97
- "ISRC identifies a specific recording; this returns the underlying work(s). " +
98
- "→ then: directory_lookup_work_full (full details on matched work)",
100
+ "Use when the user asks: 'lookup ISRC X', 'what's the work for ISRC X?', " +
101
+ "'find the work for this recording'. " +
102
+ "Shortcut: look up the work(s) associated with a recording identifier (ISRC). " +
103
+ "Equivalent to directory_list_works with an isrc filter, pinned for recording-first discovery. " +
104
+ "→ then: directory_lookup_work (full details on the matched work)",
99
105
  inputSchema: {
100
106
  type: "object",
101
107
  properties: {
@@ -124,8 +130,11 @@ export class DirectoryWorksTools {
124
130
 
125
131
  if (args.q) params.q = args.q;
126
132
  if (args.iswc) params.iswc = args.iswc;
133
+ if (args.isrc) params.isrc = args.isrc;
127
134
  if (args.publisher) params.publisher = args.publisher;
128
135
  if (args.label) params.label = args.label;
136
+ if (args.letter) params.letter = args.letter;
137
+ if (args.sort) params.sort = args.sort;
129
138
 
130
139
  const response: any = await this.client.request("/works", params);
131
140
 
@@ -141,14 +150,6 @@ export class DirectoryWorksTools {
141
150
  `/works/${encodeURIComponent(args.identifier)}`,
142
151
  );
143
152
 
144
- return formatAsText(response.data);
145
- }
146
-
147
- private async lookupWorkFull(args: Record<string, any>): Promise<any> {
148
- const response: any = await this.client.request(
149
- `/works/${encodeURIComponent(args.identifier)}`,
150
- );
151
-
152
153
  const data = response.data;
153
154
  if (!data) {
154
155
  return {
@@ -159,19 +160,16 @@ export class DirectoryWorksTools {
159
160
 
160
161
  const lines: string[] = [];
161
162
 
162
- // Title + artist
163
163
  const artistName = data.artist_name || data.primary_artist || "";
164
164
  lines.push(`## ${data.title}${artistName ? ` by ${artistName}` : ""}`);
165
165
  lines.push("");
166
166
 
167
- // Key metadata line
168
167
  const metaParts: string[] = [];
169
168
  if (data.iswc) metaParts.push(`**ISWC:** ${data.iswc}`);
170
169
  if (data.organisation_name)
171
170
  metaParts.push(`**Organisation:** ${data.organisation_name}`);
172
171
  if (metaParts.length > 0) lines.push(metaParts.join(" | "));
173
172
 
174
- // Score
175
173
  if (data.score !== undefined && data.score !== null) {
176
174
  const tier =
177
175
  data.score >= 80
@@ -184,7 +182,6 @@ export class DirectoryWorksTools {
184
182
 
185
183
  lines.push("");
186
184
 
187
- // Credits
188
185
  const credits: any[] = data.credits || [];
189
186
  lines.push(`### Credits (${credits.length})`);
190
187
  if (credits.length > 0) {
@@ -202,7 +199,6 @@ export class DirectoryWorksTools {
202
199
  }
203
200
  lines.push("");
204
201
 
205
- // Recordings
206
202
  const recordings: any[] = data.recordings || [];
207
203
  lines.push(`### Recordings (${recordings.length})`);
208
204
  if (recordings.length > 0) {
@@ -218,7 +214,6 @@ export class DirectoryWorksTools {
218
214
  }
219
215
  lines.push("");
220
216
 
221
- // Audio analysis
222
217
  const audio = data.audio_analysis || data.audio || null;
223
218
  if (audio) {
224
219
  lines.push("### Audio");
@@ -234,14 +229,12 @@ export class DirectoryWorksTools {
234
229
  lines.push("");
235
230
  }
236
231
 
237
- // DSP links
238
232
  const dsp = data.dsp_links || data.streaming_links || null;
239
233
  if (dsp && typeof dsp === "object" && Object.keys(dsp).length > 0) {
240
234
  lines.push("### DSP Links");
241
235
  if (dsp.spotify) lines.push(`- Spotify: ${dsp.spotify}`);
242
236
  if (dsp.apple_music) lines.push(`- Apple Music: ${dsp.apple_music}`);
243
237
  if (dsp.youtube) lines.push(`- YouTube: ${dsp.youtube}`);
244
- // catch any other keys
245
238
  for (const [key, val] of Object.entries(dsp)) {
246
239
  if (!["spotify", "apple_music", "youtube"].includes(key) && val) {
247
240
  lines.push(`- ${key}: ${val}`);