@withpica/mcp-server-directory 1.0.0 → 1.1.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 (62) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +1 -0
  4. package/dist/client.js.map +1 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/prompts/index.d.ts +6 -5
  11. package/dist/prompts/index.d.ts.map +1 -1
  12. package/dist/prompts/index.js +136 -96
  13. package/dist/prompts/index.js.map +1 -1
  14. package/dist/resources/llms-primer.d.ts.map +1 -1
  15. package/dist/resources/llms-primer.js +1 -0
  16. package/dist/resources/llms-primer.js.map +1 -1
  17. package/dist/server.d.ts.map +1 -1
  18. package/dist/server.js +1 -0
  19. package/dist/server.js.map +1 -1
  20. package/dist/tools/index.d.ts.map +1 -1
  21. package/dist/tools/index.js +1 -0
  22. package/dist/tools/index.js.map +1 -1
  23. package/dist/tools/people.d.ts.map +1 -1
  24. package/dist/tools/people.js +1 -0
  25. package/dist/tools/people.js.map +1 -1
  26. package/dist/tools/recordings.d.ts.map +1 -1
  27. package/dist/tools/recordings.js +1 -0
  28. package/dist/tools/recordings.js.map +1 -1
  29. package/dist/tools/search.d.ts.map +1 -1
  30. package/dist/tools/search.js +1 -0
  31. package/dist/tools/search.js.map +1 -1
  32. package/dist/tools/works.d.ts.map +1 -1
  33. package/dist/tools/works.js +1 -0
  34. package/dist/tools/works.js.map +1 -1
  35. package/dist/utils/errors.d.ts.map +1 -1
  36. package/dist/utils/errors.js +1 -0
  37. package/dist/utils/errors.js.map +1 -1
  38. package/dist/utils/formatting.d.ts.map +1 -1
  39. package/dist/utils/formatting.js +1 -0
  40. package/dist/utils/formatting.js.map +1 -1
  41. package/jest.config.js +31 -0
  42. package/package.json +1 -1
  43. package/src/__tests__/prompts/index.test.ts +145 -0
  44. package/src/__tests__/prompts/prompt-eval-harness.test.ts +251 -0
  45. package/src/__tests__/tools/composability-chains.test.ts +98 -0
  46. package/src/__tests__/tools/people.test.ts +106 -0
  47. package/src/__tests__/tools/search.test.ts +94 -0
  48. package/src/__tests__/tools/works.test.ts +148 -0
  49. package/src/client.ts +128 -0
  50. package/src/config.ts +23 -0
  51. package/src/index.ts +36 -0
  52. package/src/prompts/index.ts +250 -0
  53. package/src/resources/llms-primer.ts +35 -0
  54. package/src/server.ts +134 -0
  55. package/src/tools/index.ts +71 -0
  56. package/src/tools/people.ts +215 -0
  57. package/src/tools/recordings.ts +145 -0
  58. package/src/tools/search.ts +63 -0
  59. package/src/tools/works.ts +273 -0
  60. package/src/utils/errors.ts +64 -0
  61. package/src/utils/formatting.ts +28 -0
  62. package/tsconfig.json +22 -0
@@ -0,0 +1,250 @@
1
+ // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
+
3
+ /**
4
+ * Skill Registry for Directory MCP Server
5
+ * Workflow-level skills for music discovery and creator research.
6
+ * Deployed as upgraded prompts; will convert to MCP skills format
7
+ * when the spec extension ships (ADR-171).
8
+ */
9
+
10
+ export interface PromptDefinition {
11
+ name: string;
12
+ description: string;
13
+ arguments?: Array<{
14
+ name: string;
15
+ description: string;
16
+ required?: boolean;
17
+ }>;
18
+ }
19
+
20
+ export interface PromptMessage {
21
+ role: "user" | "assistant";
22
+ content: {
23
+ type: string;
24
+ text: string;
25
+ };
26
+ }
27
+
28
+ export interface PromptResult {
29
+ messages: PromptMessage[];
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ const SHARED_PREAMBLE = `You are guiding a music seeker through the PICA public directory — a discovery layer over verified music catalogs.
34
+
35
+ Tone: knowledgeable friend. You know what's available and how to find it. Straightforward, never salesy.
36
+
37
+ Discovery principle: every creator's work has equal discoverability. Catalog quality determines visibility, not fame or follower count. Results are sorted by musical fit, not popularity. An unknown songwriter with analysed audio ranks the same as a major artist.
38
+
39
+ Before starting this workflow:
40
+ 1. Read the llms://primer resource to understand what's available in the directory
41
+ 2. This is a read-only public directory — you can search and look up, but you can't create or modify anything
42
+ 3. Only works from organisations that opted into the directory are visible
43
+
44
+ After completing this workflow:
45
+ - Summarise what you found in one short paragraph
46
+ - Suggest the natural next step
47
+ - If they say no, that's fine. Don't ask twice.`;
48
+
49
+ export class PromptRegistry {
50
+ listPrompts(): PromptDefinition[] {
51
+ return [
52
+ {
53
+ name: "discover-music",
54
+ description:
55
+ "Find music for a sync brief, playlist, or project — search by mood, BPM, key, energy, or description. Equal discovery: results sorted by musical fit, not popularity",
56
+ arguments: [
57
+ {
58
+ name: "brief",
59
+ description:
60
+ "What you're looking for — a mood, scene description, or audio characteristics",
61
+ required: false,
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ name: "research-creator",
67
+ description:
68
+ "Research a songwriter, composer, or performer — full catalog, identifiers, collaborator network, rights landscape, and similarity search",
69
+ arguments: [
70
+ {
71
+ name: "name_or_id",
72
+ description: "Creator name, IPI number, ISNI, or MusicBrainz ID",
73
+ required: false,
74
+ },
75
+ ],
76
+ },
77
+ ];
78
+ }
79
+
80
+ async getPrompt(
81
+ name: string,
82
+ args?: Record<string, any>,
83
+ ): Promise<PromptResult> {
84
+ switch (name) {
85
+ case "discover-music":
86
+ return this.getDiscoverMusicSkill(args?.brief);
87
+ case "research-creator":
88
+ return this.getResearchCreatorSkill(args?.name_or_id);
89
+ default:
90
+ throw new Error(`Prompt not found: ${name}`);
91
+ }
92
+ }
93
+
94
+ private getDiscoverMusicSkill(brief?: string): PromptResult {
95
+ const briefInstruction = brief
96
+ ? `The user is looking for: "${brief}". Translate this into audio search parameters.`
97
+ : `Ask what they're looking for — a mood, a scene, a vibe, or specific audio characteristics (BPM, key, energy).`;
98
+
99
+ return {
100
+ messages: [
101
+ {
102
+ role: "user",
103
+ content: {
104
+ type: "text",
105
+ text: `${SHARED_PREAMBLE}
106
+
107
+ --- SKILL: discover-music ---
108
+
109
+ Help find music by what it sounds like, not who made it. Equal discovery — every well-catalogued work has the same chance.
110
+
111
+ ${briefInstruction}
112
+
113
+ Sync brief / mood-based search:
114
+ → Extract parameters from natural language:
115
+ mood, BPM range, key, energy, instrumentation, vocal/instrumental, duration constraints
116
+ → Translate mood descriptions to search parameters for directory_search_recordings:
117
+ - "Upbeat" → min_energy: 0.6, min_danceability: 0.5
118
+ - "Dark/moody" → max_energy: 0.4, key_mode: minor
119
+ - "Chill" → max_energy: 0.4, max_bpm: 100
120
+ - Specific requests → use BPM, key, duration filters directly
121
+ → Run directory_search_recordings with the parameters
122
+ → Present results by musical fit, not popularity:
123
+ "here are [N] tracks that match. sorted by how closely they fit your brief, not by who made them."
124
+ → Per result: title, creator, BPM, key, mood tags, duration.
125
+ No follower counts. No stream numbers.
126
+
127
+ For each promising result, use directory_lookup_work_full to get:
128
+ - Who wrote it (credits with IPI numbers)
129
+ - Whether credits are attested (verified ownership)
130
+ - DSP links (Spotify, Apple Music — so the user can listen)
131
+ - Registration score (higher = cleaner rights chain)
132
+
133
+ Present as a shortlist with:
134
+ - Title, artist, BPM, key, energy
135
+ - Credits summary (who to contact for licensing)
136
+ - Any flags (unattested credits, low registration score, missing ISRC)
137
+
138
+ Reference track search:
139
+ → "got a reference track? tell me what you like about it."
140
+ → Parse what they value: tempo, mood, instrumentation, vocal style
141
+ → Search by those parameters
142
+
143
+ Similarity exploration:
144
+ → "want to find music similar to [track] by someone you've never heard of?"
145
+ → Search by the sound, not the name — this is where the discovery principle lives
146
+
147
+ If they like one:
148
+ → "want to hear more from this creator, or find similar tracks?"
149
+ → Similar → search with same parameters
150
+ → More from creator → pivot to research-creator skill
151
+
152
+ Rights check:
153
+ → For any track: "want to check the rights situation?"
154
+ → Show ownership, publisher, contact path via directory_lookup_work_full
155
+ → If enquiry → guide through submission
156
+
157
+ If no results match, suggest broadening the search (wider BPM range, drop key filter, etc.).
158
+
159
+ If the catalog is small, say so: "the directory currently has [N] works — it's growing. here's what matches from what's available."
160
+
161
+ Domain knowledge:
162
+ - Results sorted by audio-parameter match, not streams or followers
163
+ - Reference track searching works by extracting musical qualities, not artist similarity
164
+ - Rights information is always available — the directory doesn't hide who owns what
165
+ - Sync supervisors think in briefs (mood + tempo + energy + duration), not artist names
166
+ - Similarity search by audio surfaces genuinely similar music the seeker might never have found
167
+
168
+ Tools: directory_search_recordings, directory_search, directory_lookup_work, directory_lookup_work_full, directory_lookup_person, directory_lookup_person_full`,
169
+ },
170
+ },
171
+ ],
172
+ };
173
+ }
174
+
175
+ private getResearchCreatorSkill(nameOrId?: string): PromptResult {
176
+ const searchInstruction = nameOrId
177
+ ? `Look up: "${nameOrId}". Try directory_lookup_person_full first (it accepts name, IPI, ISNI, or MusicBrainz ID). If that doesn't match, use directory_list_people with a text search.`
178
+ : `Ask for the creator's name, IPI number, ISNI, or MusicBrainz ID.`;
179
+
180
+ return {
181
+ messages: [
182
+ {
183
+ role: "user",
184
+ content: {
185
+ type: "text",
186
+ text: `${SHARED_PREAMBLE}
187
+
188
+ --- SKILL: research-creator ---
189
+
190
+ Deep-dive on a specific creator. Full catalog, identifiers, collaborator network, rights landscape.
191
+
192
+ Also handles routing that was previously in directory-autopilot:
193
+ - If the user has a specific identifier to resolve (ISRC, ISWC, IPI, ISNI):
194
+ → ISRC (e.g. USABC1234567) → directory_lookup_isrc → then directory_lookup_work_full
195
+ → ISWC (e.g. T-123.456.789-0) → directory_lookup_work_full
196
+ → IPI or ISNI → directory_lookup_person_full
197
+ → MusicBrainz ID → directory_lookup_person_full
198
+ - If the user wants to browse → use directory_search with a broad query, or directory_list_works / directory_list_people
199
+
200
+ "who do you want to learn about?"
201
+
202
+ ${searchInstruction}
203
+
204
+ Lookup by name, IPI, ISNI, or ISRC:
205
+ → Find the person using directory_lookup_person_full
206
+ → If multiple matches: present with distinguishing works/roles
207
+
208
+ Creator profile:
209
+ → Full catalog: all works, roles, co-writers
210
+ → Identifiers: IPI, ISNI, MusicBrainz, IPN
211
+ → Collaborator network: "frequently works with [names]"
212
+ → Genre/mood footprint: based on analysed audio across catalog
213
+
214
+ For their most notable works (up to 5), use directory_lookup_work_full to get:
215
+ - Full credits and splits (are they the sole writer or one of many?)
216
+ - Recordings with ISRCs
217
+ - Audio analysis (BPM, key, energy)
218
+ - DSP links and registration status
219
+
220
+ Cross-reference:
221
+ → "want to verify their identifiers against ISNI or MusicBrainz?"
222
+ → Flag discrepancies
223
+
224
+ Similarity exploration:
225
+ → "want to find other creators with a similar sound?"
226
+ → Use directory_search_recordings by audio characteristics of their catalog
227
+ → Equal discovery: results by sound, not fame
228
+
229
+ Rights landscape:
230
+ → "want to know who controls their catalog?"
231
+ → Publishers, agreements, licensing paths
232
+
233
+ After research:
234
+ → Summary: creator profile, catalog overview, notable works, gaps
235
+ → "want to submit a licensing enquiry, or explore more creators?"
236
+
237
+ Domain knowledge:
238
+ - Creator research often starts from one work and expands via collaborator network — adjacent talent discovery
239
+ - Identifier verification across sources catches discrepancies that matter for rights
240
+ - Similarity search by audio surfaces genuinely similar music through non-traditional channels
241
+ - The collaborator network is how you discover creators no algorithm would surface
242
+ - This is useful for: rights research, due diligence before licensing, publisher evaluation, or catalog acquisition
243
+
244
+ Tools: directory_lookup_person_full, directory_lookup_work_full, directory_search_recordings, directory_search, directory_lookup_isrc, directory_list_works, directory_list_people`,
245
+ },
246
+ },
247
+ ],
248
+ };
249
+ }
250
+ }
@@ -0,0 +1,35 @@
1
+ // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
+
3
+ export const DIRECTORY_PRIMER = `# PICA Directory — Public Music Catalog Search
4
+
5
+ Read-only access to published music catalog data. No authentication required.
6
+ Works and people are only visible if the owning organisation has opted into
7
+ the directory.
8
+
9
+ ## First Connection
10
+
11
+ If you're not sure where to start, invoke the **directory-autopilot** prompt.
12
+ It asks what you need and routes to the right workflow: sync search, rights
13
+ research, or identifier lookup.
14
+
15
+ ## What You Can Do
16
+ - Search works by title, ISWC, publisher, label
17
+ - Search people by name, ISNI, IPI
18
+ - Look up works by recording ISRC (directory_lookup_isrc)
19
+ - Search recordings by audio characteristics (BPM, key, mood, energy)
20
+
21
+ ## What You Cannot Do
22
+ - Create, update, or delete anything (read-only)
23
+ - Access unpublished works or private catalog data
24
+ - See financial data, agreements, or internal metadata
25
+
26
+ ## Entity Model
27
+ - **Work**: composition with title, ISWC, credits, recordings
28
+ - **Person**: writer/composer/performer with IPI, ISNI identifiers
29
+ - **Recording**: audio capture with ISRC, linked to one work
30
+
31
+ ## Recommended Flow
32
+ 1. directory_search (broad text search across works + people)
33
+ 2. directory_lookup_work or directory_lookup_person (detailed view)
34
+ 3. directory_search_recordings (audio characteristic search for sync)
35
+ `;
package/src/server.ts ADDED
@@ -0,0 +1,134 @@
1
+ // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ ListPromptsRequestSchema,
11
+ GetPromptRequestSchema,
12
+ } from "@modelcontextprotocol/sdk/types.js";
13
+ import { DirectoryClient } from "./client.js";
14
+ import { ServerConfig } from "./config.js";
15
+ import { ToolRegistry } from "./tools/index.js";
16
+ import { PromptRegistry } from "./prompts/index.js";
17
+ import { logError } from "./utils/errors.js";
18
+ import { DIRECTORY_PRIMER } from "./resources/llms-primer.js";
19
+
20
+ export class DirectoryMcpServer {
21
+ private server: Server;
22
+ private client: DirectoryClient;
23
+ private toolRegistry: ToolRegistry;
24
+ private promptRegistry: PromptRegistry;
25
+ private config: ServerConfig;
26
+
27
+ constructor(config: ServerConfig) {
28
+ this.config = config;
29
+
30
+ this.client = new DirectoryClient({
31
+ baseUrl: config.directoryUrl,
32
+ debug: config.debug,
33
+ });
34
+
35
+ this.server = new Server(
36
+ {
37
+ name: config.serverName,
38
+ version: config.version,
39
+ },
40
+ {
41
+ capabilities: {
42
+ tools: {},
43
+ resources: {},
44
+ prompts: {},
45
+ },
46
+ },
47
+ );
48
+
49
+ this.toolRegistry = new ToolRegistry(this.client);
50
+ this.promptRegistry = new PromptRegistry();
51
+ this.setupHandlers();
52
+ }
53
+
54
+ private setupHandlers(): void {
55
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
56
+ if (this.config.debug) {
57
+ console.error("[Directory MCP] Listing tools");
58
+ }
59
+ return { tools: this.toolRegistry.listTools() };
60
+ });
61
+
62
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
63
+ return {
64
+ resources: [
65
+ {
66
+ uri: "llms://primer",
67
+ name: "PICA Domain Primer",
68
+ description:
69
+ "Plain-language description of PICA's domain model, entity relationships, and recommended agent workflows. Read this first.",
70
+ mimeType: "text/markdown",
71
+ },
72
+ ],
73
+ };
74
+ });
75
+
76
+ this.server.setRequestHandler(
77
+ ReadResourceRequestSchema,
78
+ async (request) => {
79
+ const { uri } = request.params;
80
+ if (uri === "llms://primer") {
81
+ return {
82
+ contents: [
83
+ {
84
+ uri: "llms://primer",
85
+ mimeType: "text/markdown",
86
+ text: DIRECTORY_PRIMER,
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ throw new Error(`Resource not found: ${uri}`);
92
+ },
93
+ );
94
+
95
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
96
+ return { prompts: this.promptRegistry.listPrompts() };
97
+ });
98
+
99
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
100
+ const { name, arguments: args } = request.params;
101
+ return await this.promptRegistry.getPrompt(name, args);
102
+ });
103
+
104
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
105
+ const { name, arguments: args } = request.params;
106
+
107
+ if (this.config.debug) {
108
+ console.error(`[Directory MCP] Executing tool: ${name}`, args);
109
+ }
110
+
111
+ try {
112
+ return await this.toolRegistry.executeTool(name, args || {});
113
+ } catch (error) {
114
+ logError(`Tool execution: ${name}`, error);
115
+ throw error;
116
+ }
117
+ });
118
+ }
119
+
120
+ async start(): Promise<void> {
121
+ const transport = new StdioServerTransport();
122
+ await this.server.connect(transport);
123
+
124
+ console.error("[Directory MCP] PICA Directory MCP Server started");
125
+ console.error(`[Directory MCP] Version: ${this.config.version}`);
126
+ console.error(`[Directory MCP] API URL: ${this.config.directoryUrl}`);
127
+ console.error("[Directory MCP] Ready to accept connections");
128
+ }
129
+
130
+ async stop(): Promise<void> {
131
+ await this.server.close();
132
+ console.error("[Directory MCP] Server stopped");
133
+ }
134
+ }
@@ -0,0 +1,71 @@
1
+ // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
+
3
+ import { DirectoryClient } from "../client.js";
4
+ import { DirectoryWorksTools } from "./works.js";
5
+ import { DirectoryPeopleTools } from "./people.js";
6
+ import { DirectorySearchTools } from "./search.js";
7
+ import { DirectoryRecordingsTools } from "./recordings.js";
8
+ import { formatError, logError, ToolExecutionError } from "../utils/errors.js";
9
+
10
+ export interface ToolDefinition {
11
+ name: string;
12
+ description: string;
13
+ inputSchema: {
14
+ type: string;
15
+ properties: Record<string, any>;
16
+ required?: string[];
17
+ };
18
+ }
19
+
20
+ export interface ToolResult {
21
+ content: Array<{ type: string; text: string }>;
22
+ structuredContent?: Record<string, unknown>;
23
+ isError?: boolean;
24
+ }
25
+
26
+ export type ToolExecutor = (args: Record<string, any>) => Promise<any>;
27
+
28
+ export class ToolRegistry {
29
+ private tools: Map<
30
+ string,
31
+ { definition: ToolDefinition; executor: ToolExecutor }
32
+ >;
33
+
34
+ constructor(client: DirectoryClient) {
35
+ this.tools = new Map();
36
+
37
+ const groups = [
38
+ new DirectoryWorksTools(client),
39
+ new DirectoryPeopleTools(client),
40
+ new DirectorySearchTools(client),
41
+ new DirectoryRecordingsTools(client),
42
+ ];
43
+
44
+ for (const group of groups) {
45
+ for (const tool of group.getTools()) {
46
+ this.tools.set(tool.definition.name, tool);
47
+ }
48
+ }
49
+ }
50
+
51
+ listTools(): ToolDefinition[] {
52
+ return Array.from(this.tools.values()).map((t) => t.definition);
53
+ }
54
+
55
+ async executeTool(name: string, args: Record<string, any>): Promise<any> {
56
+ const tool = this.tools.get(name);
57
+ if (!tool) {
58
+ throw new ToolExecutionError(`Tool not found: ${name}`);
59
+ }
60
+
61
+ try {
62
+ return await tool.executor(args);
63
+ } catch (error) {
64
+ logError(`Tool execution: ${name}`, error);
65
+ return {
66
+ content: [formatError(error)],
67
+ isError: true,
68
+ };
69
+ }
70
+ }
71
+ }