@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.
- package/CHANGELOG.md +39 -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 +6 -5
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +136 -96
- package/dist/prompts/index.js.map +1 -1
- 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/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/people.d.ts.map +1 -1
- package/dist/tools/people.js +1 -0
- package/dist/tools/people.js.map +1 -1
- package/dist/tools/recordings.d.ts.map +1 -1
- package/dist/tools/recordings.js +1 -0
- package/dist/tools/recordings.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +1 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/works.d.ts.map +1 -1
- package/dist/tools/works.js +1 -0
- 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 +1 -1
- package/src/__tests__/prompts/index.test.ts +145 -0
- package/src/__tests__/prompts/prompt-eval-harness.test.ts +251 -0
- package/src/__tests__/tools/composability-chains.test.ts +98 -0
- package/src/__tests__/tools/people.test.ts +106 -0
- package/src/__tests__/tools/search.test.ts +94 -0
- package/src/__tests__/tools/works.test.ts +148 -0
- package/src/client.ts +128 -0
- package/src/config.ts +23 -0
- package/src/index.ts +36 -0
- package/src/prompts/index.ts +250 -0
- package/src/resources/llms-primer.ts +35 -0
- package/src/server.ts +134 -0
- package/src/tools/index.ts +71 -0
- package/src/tools/people.ts +215 -0
- package/src/tools/recordings.ts +145 -0
- package/src/tools/search.ts +63 -0
- package/src/tools/works.ts +273 -0
- package/src/utils/errors.ts +64 -0
- package/src/utils/formatting.ts +28 -0
- 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
|
+
}
|