@withpica/mcp-server-directory 1.4.2 → 1.6.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 (85) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +4 -3
  3. package/dist/client.d.ts +30 -1
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +28 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/config.d.ts +6 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +8 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/lib/changelog.generated.d.ts +2 -2
  12. package/dist/lib/changelog.generated.d.ts.map +1 -1
  13. package/dist/lib/changelog.generated.js +2 -2
  14. package/dist/lib/changelog.generated.js.map +1 -1
  15. package/dist/prompts/index.js +1 -1
  16. package/dist/prompts/index.js.map +1 -1
  17. package/dist/prompts/public-question-atlas.d.ts.map +1 -1
  18. package/dist/prompts/public-question-atlas.js +17 -0
  19. package/dist/prompts/public-question-atlas.js.map +1 -1
  20. package/dist/resources/llms-primer.d.ts +1 -1
  21. package/dist/resources/llms-primer.d.ts.map +1 -1
  22. package/dist/resources/llms-primer.js +6 -2
  23. package/dist/resources/llms-primer.js.map +1 -1
  24. package/dist/server.d.ts +9 -1
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +15 -5
  27. package/dist/server.js.map +1 -1
  28. package/dist/skills/skills.generated.js +1 -1
  29. package/dist/skills/skills.generated.js.map +1 -1
  30. package/dist/tools/chain.d.ts +2 -2
  31. package/dist/tools/chain.d.ts.map +1 -1
  32. package/dist/tools/chain.js +15 -0
  33. package/dist/tools/chain.js.map +1 -1
  34. package/dist/tools/enquiries.d.ts +19 -0
  35. package/dist/tools/enquiries.d.ts.map +1 -0
  36. package/dist/tools/enquiries.js +190 -0
  37. package/dist/tools/enquiries.js.map +1 -0
  38. package/dist/tools/index.d.ts +6 -5
  39. package/dist/tools/index.d.ts.map +1 -1
  40. package/dist/tools/index.js +2 -0
  41. package/dist/tools/index.js.map +1 -1
  42. package/dist/tools/people.d.ts +2 -2
  43. package/dist/tools/people.d.ts.map +1 -1
  44. package/dist/tools/people.js.map +1 -1
  45. package/dist/tools/recordings.d.ts +2 -2
  46. package/dist/tools/recordings.d.ts.map +1 -1
  47. package/dist/tools/recordings.js.map +1 -1
  48. package/dist/tools/release-notes.d.ts +2 -2
  49. package/dist/tools/release-notes.d.ts.map +1 -1
  50. package/dist/tools/release-notes.js.map +1 -1
  51. package/dist/tools/search.d.ts +2 -2
  52. package/dist/tools/search.d.ts.map +1 -1
  53. package/dist/tools/search.js.map +1 -1
  54. package/dist/tools/works.d.ts +2 -2
  55. package/dist/tools/works.d.ts.map +1 -1
  56. package/dist/tools/works.js +17 -1
  57. package/dist/tools/works.js.map +1 -1
  58. package/dist/utils/errors.d.ts +10 -0
  59. package/dist/utils/errors.d.ts.map +1 -1
  60. package/dist/utils/errors.js +33 -0
  61. package/dist/utils/errors.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/__tests__/prompts/prompt-eval-harness.test.ts +3 -2
  64. package/src/__tests__/tools/chain.test.ts +34 -0
  65. package/src/__tests__/tools/composability-chains.test.ts +2 -2
  66. package/src/__tests__/tools/enquiries.test.ts +135 -0
  67. package/src/__tests__/tools/works.test.ts +3 -0
  68. package/src/__tests__/utils/errors.test.ts +13 -0
  69. package/src/client.ts +53 -2
  70. package/src/config.ts +12 -1
  71. package/src/prompts/index.ts +1 -1
  72. package/src/prompts/public-question-atlas.ts +18 -0
  73. package/src/resources/llms-primer.ts +6 -2
  74. package/src/server.ts +19 -7
  75. package/src/skills/find-music-for-sync-brief/SKILL.md +1 -1
  76. package/src/skills/skills.generated.ts +1 -1
  77. package/src/tools/chain.ts +18 -3
  78. package/src/tools/enquiries.ts +210 -0
  79. package/src/tools/index.ts +8 -5
  80. package/src/tools/people.ts +3 -3
  81. package/src/tools/recordings.ts +3 -3
  82. package/src/tools/release-notes.ts +2 -2
  83. package/src/tools/search.ts +3 -3
  84. package/src/tools/works.ts +22 -4
  85. package/src/utils/errors.ts +40 -0
@@ -6,6 +6,16 @@ export declare class McpServerError extends Error {
6
6
  export declare class ToolExecutionError extends McpServerError {
7
7
  constructor(message: string, details?: any);
8
8
  }
9
+ /**
10
+ * Status → code for a route refusal that carried no code of its own.
11
+ *
12
+ * ⚠️ A port of `STATUS_REFUSAL_CODES` in
13
+ * `mcp-server-shared/mcp-utils/src/errors.ts`. This package does not depend on
14
+ * `@withpica/mcp-utils` (it publishes on its own), so it cannot import the
15
+ * shared formatter; `scripts/__tests__/format-error-status-parity.test.ts`
16
+ * fails if the two tables disagree.
17
+ */
18
+ export declare const STATUS_REFUSAL_CODES: Record<number, string>;
9
19
  export declare function formatError(error: any): {
10
20
  type: string;
11
21
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAEA,qBAAa,cAAe,SAAQ,KAAK;IAG9B,IAAI,EAAE,MAAM;IACZ,OAAO,CAAC,EAAE,GAAG;gBAFpB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,GAAG,YAAA;CAMvB;AAED,qBAAa,kBAAmB,SAAQ,cAAc;gBACxC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;CAI3C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAuEtE;AA8BD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI,CAS1D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAEA,qBAAa,cAAe,SAAQ,KAAK;IAG9B,IAAI,EAAE,MAAM;IACZ,OAAO,CAAC,EAAE,GAAG;gBAFpB,OAAO,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,GAAG,YAAA;CAMvB;AAED,qBAAa,kBAAmB,SAAQ,cAAc;gBACxC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;CAI3C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOvD,CAAC;AAEF,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA6FtE;AA8BD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI,CAS1D"}
@@ -16,6 +16,23 @@ export class ToolExecutionError extends McpServerError {
16
16
  this.name = "ToolExecutionError";
17
17
  }
18
18
  }
19
+ /**
20
+ * Status → code for a route refusal that carried no code of its own.
21
+ *
22
+ * ⚠️ A port of `STATUS_REFUSAL_CODES` in
23
+ * `mcp-server-shared/mcp-utils/src/errors.ts`. This package does not depend on
24
+ * `@withpica/mcp-utils` (it publishes on its own), so it cannot import the
25
+ * shared formatter; `scripts/__tests__/format-error-status-parity.test.ts`
26
+ * fails if the two tables disagree.
27
+ */
28
+ export const STATUS_REFUSAL_CODES = {
29
+ 400: "VALIDATION_ERROR",
30
+ 404: "NOT_FOUND",
31
+ 409: "CONFLICT",
32
+ 413: "PAYLOAD_TOO_LARGE",
33
+ 415: "UNSUPPORTED_MEDIA_TYPE",
34
+ 422: "VALIDATION_ERROR",
35
+ };
19
36
  export function formatError(error) {
20
37
  if (error instanceof McpServerError) {
21
38
  return {
@@ -56,6 +73,22 @@ export function formatError(error) {
56
73
  }, null, 2),
57
74
  };
58
75
  }
76
+ // A code-less 4xx is still a refusal; its status names the kind. Without
77
+ // this it read as UNKNOWN_ERROR, the bucket for an unclassified crash (ops
78
+ // issue 55510879 — the same defect in the shared formatter).
79
+ const status = error?.status;
80
+ const statusCode = typeof status === "number" ? STATUS_REFUSAL_CODES[status] : undefined;
81
+ if (statusCode) {
82
+ return {
83
+ type: "text",
84
+ text: JSON.stringify({
85
+ error: statusCode,
86
+ message: bodyMessage || rawMessage,
87
+ status,
88
+ ...(bodyDetails !== undefined ? { details: bodyDetails } : {}),
89
+ }, null, 2),
90
+ };
91
+ }
59
92
  return {
60
93
  type: "text",
61
94
  text: JSON.stringify({
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpica/mcp-server-directory",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "description": "MCP Server for the withPICA Public Directory — enables AI assistants to search verified works and creators",
5
5
  "homepage": "https://withpica.com/connect",
6
6
  "bugs": {
@@ -9,7 +9,7 @@
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
11
11
  "bin": {
12
- "pica-directory-mcp": "./dist/index.js"
12
+ "pica-directory-mcp": "dist/index.js"
13
13
  },
14
14
  "scripts": {
15
15
  "prebuild": "tsx scripts/build-changelog.ts && tsx scripts/build-skills.ts",
@@ -273,10 +273,11 @@ describe("Prompt Eval Harness — Directory MCP", () => {
273
273
  expect(noTools).toEqual([]);
274
274
  });
275
275
 
276
- it("directory-autopilot is read-only (no create/modify language)", async () => {
276
+ it("directory-autopilot says the catalogue is read-only and the one write needs the user's say-so", async () => {
277
277
  const result = await registry.getPrompt("directory-autopilot");
278
278
  const text = result.messages[0].content.text;
279
- expect(text).toMatch(/read-only/i);
279
+ expect(text).toMatch(/catalogue is read-only/i);
280
+ expect(text).toMatch(/only if you ask/i);
280
281
  });
281
282
  });
282
283
  });
@@ -96,6 +96,40 @@ describe("DirectoryChainTools", () => {
96
96
  });
97
97
  });
98
98
 
99
+ it("forwards the ADR-321 rights filters only when true", async () => {
100
+ mockClient.request.mockResolvedValue({
101
+ success: true,
102
+ results: [],
103
+ total: 0,
104
+ });
105
+ const tool = chainTools
106
+ .getTools()
107
+ .find((t) => t.definition.name === "directory_chain")!;
108
+
109
+ await tool.executor({ programmable_only: true, ready_to_license: true });
110
+ expect(mockClient.request).toHaveBeenLastCalledWith("/chain", {
111
+ programmable_only: "true",
112
+ ready_to_license: "true",
113
+ });
114
+
115
+ // control: false (or a truthy non-boolean) is not forwarded
116
+ await tool.executor({
117
+ q: "song",
118
+ programmable_only: false,
119
+ ready_to_license: "yes",
120
+ });
121
+ expect(mockClient.request).toHaveBeenLastCalledWith("/chain", {
122
+ q: "song",
123
+ });
124
+ });
125
+
126
+ it("declares both rights filters as booleans in the schema", () => {
127
+ const props = chainTools.getTools()[0].definition.inputSchema
128
+ .properties as Record<string, { type: string }>;
129
+ expect(props.programmable_only.type).toBe("boolean");
130
+ expect(props.ready_to_license.type).toBe("boolean");
131
+ });
132
+
99
133
  it("returns a human summary when no results", async () => {
100
134
  mockClient.request.mockResolvedValue({
101
135
  success: true,
@@ -39,8 +39,8 @@ describe("Directory Composability Chains", () => {
39
39
  allNames = new Set(allTools.map((t) => t.name));
40
40
  });
41
41
 
42
- it("all 11 tools are registered (parity with lib/constants/mcp-surface.ts is enforced by tool-count-parity.test.ts)", () => {
43
- expect(allTools).toHaveLength(11);
42
+ it("all 12 tools are registered (parity with lib/constants/mcp-surface.ts is enforced by tool-count-parity.test.ts)", () => {
43
+ expect(allTools).toHaveLength(12);
44
44
  });
45
45
 
46
46
  it("all tools have composability chains", () => {
@@ -0,0 +1,135 @@
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
+
3
+ /**
4
+ * directory_send_licensing_enquiry — agents browsing the public directory had
5
+ * no way to reach an owner; the sync skill told them to "contact the rights
6
+ * holder via the credits summary" (brief 2026-09-25).
7
+ */
8
+
9
+ import { jest, describe, it, expect, beforeEach } from "@jest/globals";
10
+ import { DirectoryEnquiryTools } from "../../tools/enquiries";
11
+ import { DirectoryClient } from "../../client";
12
+
13
+ const WORK = "11111111-2222-4333-8444-555555555555";
14
+
15
+ describe("directory_send_licensing_enquiry", () => {
16
+ let client: jest.Mocked<DirectoryClient>;
17
+ let tool: ReturnType<DirectoryEnquiryTools["getTools"]>[number];
18
+
19
+ beforeEach(() => {
20
+ client = {
21
+ sendLicensingEnquiry: jest.fn(),
22
+ workPageUrl: jest.fn(
23
+ (id: string) => `https://withpica.com/directory/works/${id}`,
24
+ ),
25
+ } as any;
26
+ tool = new DirectoryEnquiryTools(client).getTools()[0];
27
+ });
28
+
29
+ const args = {
30
+ work_id: WORK,
31
+ contact_name: "Jane Doe",
32
+ contact_email: "jane@films.test",
33
+ project_type: "film",
34
+ project_description: "opening titles",
35
+ };
36
+
37
+ it("is the one write-tier directory tool and requires the user's own details", () => {
38
+ expect(tool.definition.tier).toBe("write");
39
+ expect(tool.definition.inputSchema.required).toEqual([
40
+ "work_id",
41
+ "contact_name",
42
+ "contact_email",
43
+ "project_type",
44
+ "project_description",
45
+ ]);
46
+ expect(tool.definition.inputSchema.required).not.toContain("budget_amount");
47
+ expect(tool.definition.description).toMatch(/ONLY call after the user/);
48
+ });
49
+
50
+ it("maps to the public enquiry body, says 'not specified' for what the user didn't give, and returns the work page", async () => {
51
+ client.sendLicensingEnquiry.mockResolvedValue({
52
+ status: 200,
53
+ json: { success: true, data: { id: "enq-1", status: "pending" } },
54
+ });
55
+
56
+ const result = await tool.executor(args);
57
+
58
+ expect(client.sendLicensingEnquiry).toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ workId: WORK,
61
+ contactName: "Jane Doe",
62
+ contactEmail: "jane@films.test",
63
+ territory: "not specified",
64
+ duration: "not specified",
65
+ proposedBudgetAmount: null,
66
+ proposedBudgetCurrency: "GBP",
67
+ }),
68
+ );
69
+ expect(result.isError).toBeUndefined();
70
+ expect(result.structuredContent).toEqual({
71
+ sent: true,
72
+ enquiry_id: "enq-1",
73
+ status: "pending",
74
+ work_page_url: `https://withpica.com/directory/works/${WORK}`,
75
+ });
76
+ expect(result.content[0].text).toContain("nothing has been agreed");
77
+ });
78
+
79
+ it.each([
80
+ [403, /not taking licensing enquiries/],
81
+ [404, /not listed in the public directory/],
82
+ [429, /Too many enquiries/],
83
+ ])("reports HTTP %i as not sent, with the reason", async (status, text) => {
84
+ client.sendLicensingEnquiry.mockResolvedValue({
85
+ status,
86
+ json: { success: false, error: { code: "X", message: "x" } },
87
+ });
88
+ const result = await tool.executor(args);
89
+ expect(result.isError).toBe(true);
90
+ expect(result.structuredContent).toMatchObject({
91
+ sent: false,
92
+ http_status: status,
93
+ });
94
+ expect(result.content[0].text).toMatch(text);
95
+ });
96
+
97
+ // Review finding: the route stores, THEN emails. A 5xx or a lost answer can
98
+ // follow a stored enquiry, so "not sent" would invite a duplicate.
99
+ it.each([500, 502])(
100
+ "reports HTTP %i as an unknown outcome, not 'not sent'",
101
+ async (status) => {
102
+ client.sendLicensingEnquiry.mockResolvedValue({ status, json: null });
103
+ const result = await tool.executor(args);
104
+ expect(result.structuredContent).toMatchObject({
105
+ sent: null,
106
+ outcome: "unknown",
107
+ });
108
+ expect(result.content[0].text).toMatch(/Do not resend/);
109
+ expect(result.content[0].text).not.toMatch(/^Not sent/);
110
+ },
111
+ );
112
+
113
+ it("reports a timeout as an unknown outcome", async () => {
114
+ client.sendLicensingEnquiry.mockRejectedValue(
115
+ new Error("Request timed out after 15000ms: POST x"),
116
+ );
117
+ const result = await tool.executor(args);
118
+ expect(result.structuredContent).toMatchObject({ outcome: "unknown" });
119
+ });
120
+
121
+ it("passes a 400's own message through", async () => {
122
+ client.sendLicensingEnquiry.mockResolvedValue({
123
+ status: 400,
124
+ json: {
125
+ success: false,
126
+ error: {
127
+ code: "ENQUIRY_EMAIL_INVALID",
128
+ message: "Invalid email format",
129
+ },
130
+ },
131
+ });
132
+ const result = await tool.executor(args);
133
+ expect(result.content[0].text).toContain("Invalid email format");
134
+ });
135
+ });
@@ -18,6 +18,9 @@ describe("DirectoryWorksTools", () => {
18
18
  beforeEach(() => {
19
19
  mockClient = {
20
20
  request: jest.fn(),
21
+ workPageUrl: jest.fn(
22
+ (id: string) => `https://withpica.com/directory/works/${id}`,
23
+ ),
21
24
  } as any;
22
25
 
23
26
  worksTools = new DirectoryWorksTools(mockClient);
@@ -46,6 +46,19 @@ describe("directory-mcp formatError structured-body decode", () => {
46
46
  expect(parsed.message).toBe("nope");
47
47
  });
48
48
 
49
+ // Ops issue 55510879: a code-less 4xx is a refusal; its status names it.
50
+ it("classifies a code-less 400 as VALIDATION_ERROR by status", () => {
51
+ const error = {
52
+ status: 400,
53
+ message:
54
+ "API request failed: 400 " +
55
+ JSON.stringify({ success: false, error: "name required" }),
56
+ };
57
+ const parsed = JSON.parse(formatError(error).text);
58
+ expect(parsed.error).toBe("VALIDATION_ERROR");
59
+ expect(parsed.message).toBe("name required");
60
+ });
61
+
49
62
  it("recovers the real message from a coded-less body (no UNKNOWN noise)", () => {
50
63
  const error = {
51
64
  message: JSON.stringify({
package/src/client.ts CHANGED
@@ -17,7 +17,22 @@ export interface DirectoryClientConfig {
17
17
  debug?: boolean;
18
18
  }
19
19
 
20
- export class DirectoryClient {
20
+ /**
21
+ * What the directory tools need from the API. `DirectoryClient` below talks to
22
+ * the public API over HTTP (the npm/stdio server). The hosted endpoint
23
+ * (app/api/mcp/directory in the pica app) supplies its own implementation that
24
+ * calls the same route handlers in-process, so the caller's IP still reaches
25
+ * every rate limit.
26
+ */
27
+ export interface DirectoryApi {
28
+ request<T>(path: string, params?: Record<string, string>): Promise<T>;
29
+ workPageUrl(workId: string): string;
30
+ sendLicensingEnquiry(
31
+ body: Record<string, unknown>,
32
+ ): Promise<{ status: number; json: any }>;
33
+ }
34
+
35
+ export class DirectoryClient implements DirectoryApi {
21
36
  private baseUrl: string;
22
37
  private debug: boolean;
23
38
 
@@ -37,7 +52,9 @@ export class DirectoryClient {
37
52
  return await fetch(url, { ...init, signal: controller.signal });
38
53
  } catch (err: any) {
39
54
  if (err.name === "AbortError") {
40
- throw new Error(`Request timed out after ${timeoutMs}ms: GET ${url}`);
55
+ throw new Error(
56
+ `Request timed out after ${timeoutMs}ms: ${init.method ?? "GET"} ${url}`,
57
+ );
41
58
  }
42
59
  throw err;
43
60
  } finally {
@@ -45,6 +62,40 @@ export class DirectoryClient {
45
62
  }
46
63
  }
47
64
 
65
+ /** The public page for a directory work, for a human to open. */
66
+ workPageUrl(workId: string): string {
67
+ return `${this.baseUrl}/directory/works/${encodeURIComponent(workId)}`;
68
+ }
69
+
70
+ /**
71
+ * Send a licensing enquiry through the public enquiry route (the same one
72
+ * the directory's web form uses). ONE attempt, never retried: a retry after
73
+ * a timeout or a 5xx could store the enquiry twice and email the owner
74
+ * twice. Resolves with the status and parsed body for every HTTP answer,
75
+ * so the caller can tell a refusal (400/403/404/429) from a failure;
76
+ * throws only when no answer arrived.
77
+ */
78
+ async sendLicensingEnquiry(
79
+ body: Record<string, unknown>,
80
+ ): Promise<{ status: number; json: any }> {
81
+ const url = `${this.baseUrl}/api/shop/license-enquiries`;
82
+ if (this.debug) console.error(`[Directory MCP] POST ${url}`);
83
+ const response = await this.fetchWithTimeout(
84
+ url,
85
+ {
86
+ method: "POST",
87
+ headers: {
88
+ Accept: "application/json",
89
+ "Content-Type": "application/json",
90
+ },
91
+ body: JSON.stringify(body),
92
+ },
93
+ DEFAULT_TIMEOUT_MS,
94
+ );
95
+ const json: any = await response.json().catch(() => null);
96
+ return { status: response.status, json };
97
+ }
98
+
48
99
  async request<T>(path: string, params?: Record<string, string>): Promise<T> {
49
100
  const url = new URL(`${this.baseUrl}/api/public/directory${path}`);
50
101
  if (params) {
package/src/config.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
+ import { createRequire } from "node:module";
4
+
5
+ /**
6
+ * The package's own version, read from package.json (src/ and dist/ both sit
7
+ * one level below it). It was hardcoded "1.0.0", so serverInfo never said
8
+ * which release an agent was talking to.
9
+ */
10
+ export const PACKAGE_VERSION: string = createRequire(import.meta.url)(
11
+ "../package.json",
12
+ ).version;
13
+
3
14
  export interface ServerConfig {
4
15
  directoryUrl: string;
5
16
  serverName: string;
@@ -11,7 +22,7 @@ export function loadConfig(): ServerConfig {
11
22
  return {
12
23
  directoryUrl: process.env.PICA_DIRECTORY_URL || "https://withpica.com",
13
24
  serverName: "pica-directory-mcp-server",
14
- version: "1.0.0",
25
+ version: PACKAGE_VERSION,
15
26
  debug: process.env.DEBUG === "true" || process.env.DEBUG === "1",
16
27
  };
17
28
  }
@@ -197,7 +197,7 @@ If I want to BROWSE what's in the directory:
197
197
 
198
198
  Tell me which workflow you chose and why, in one sentence. Offer alternatives.
199
199
 
200
- Important: This is a read-only public directory. I can search and look up, but I can't create or modify anything. Only works from organisations that opted into the directory are visible.`,
200
+ Important: The directory's catalogue is read-only: I can search and look up, but I can't create or modify anything in it. The one thing I can send, and only if you ask, is a licensing enquiry to a work's owner, with your own name and email. Only works from organisations that opted into the directory are visible.`,
201
201
  },
202
202
  },
203
203
  ],
@@ -563,4 +563,22 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
563
563
  ],
564
564
  resolves_to: { kind: "tool", name: "directory_release_notes" },
565
565
  },
566
+
567
+ // ─────────────────────────────────────────────────────────────────
568
+ // Reaching an owner — the directory's one write
569
+ // ─────────────────────────────────────────────────────────────────
570
+ {
571
+ question: "can I license this song?",
572
+ synonyms: [
573
+ "ask the owner if I can use this",
574
+ "send a licensing enquiry",
575
+ "contact the rights holder",
576
+ "who do I ask to license this?",
577
+ "I want to use this track in my project",
578
+ "ask the owner if I can license this",
579
+ "send a licensing enquiry for X",
580
+ "contact the rights holder about using this song",
581
+ ],
582
+ resolves_to: { kind: "tool", name: "directory_send_licensing_enquiry" },
583
+ },
566
584
  ];
@@ -2,7 +2,9 @@
2
2
 
3
3
  export const DIRECTORY_PRIMER = `# PICA Directory — Public Music Catalog Search
4
4
 
5
- Read-only access to published music catalog data. No authentication required.
5
+ Read access to published music catalog data, plus one write: sending a
6
+ licensing enquiry to a work's owner when the user asks. No authentication
7
+ required.
6
8
  Works and people are only visible if the owning organisation has opted into
7
9
  the directory.
8
10
 
@@ -17,9 +19,11 @@ research, or identifier lookup.
17
19
  - Search people by name, ISNI, IPI
18
20
  - Look up works by recording ISRC (directory_lookup_isrc)
19
21
  - Search recordings by audio characteristics (BPM, key, mood, energy)
22
+ - Send a licensing enquiry to a work's owner (directory_send_licensing_enquiry),
23
+ only when the user asks and gives their own name, email and project
20
24
 
21
25
  ## What You Cannot Do
22
- - Create, update, or delete anything (read-only)
26
+ - Create, update, or delete catalogue data (the catalogue is read-only)
23
27
  - Access unpublished works or private catalog data
24
28
  - See financial data, agreements, or internal metadata
25
29
 
package/src/server.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
5
6
  import {
6
7
  CallToolRequestSchema,
7
8
  ListToolsRequestSchema,
@@ -10,7 +11,7 @@ import {
10
11
  ListPromptsRequestSchema,
11
12
  GetPromptRequestSchema,
12
13
  } from "@modelcontextprotocol/sdk/types.js";
13
- import { DirectoryClient } from "./client.js";
14
+ import { DirectoryClient, type DirectoryApi } from "./client.js";
14
15
  import { ServerConfig } from "./config.js";
15
16
  import { ToolRegistry } from "./tools/index.js";
16
17
  import { PromptRegistry } from "./prompts/index.js";
@@ -21,7 +22,7 @@ import { SkillsRegistry } from "./skills/index.js";
21
22
 
22
23
  export class DirectoryMcpServer {
23
24
  private server: Server;
24
- private client: DirectoryClient;
25
+ private client: DirectoryApi;
25
26
  private toolRegistry: ToolRegistry;
26
27
  private promptRegistry: PromptRegistry;
27
28
  private skillsRegistry: SkillsRegistry;
@@ -32,13 +33,19 @@ export class DirectoryMcpServer {
32
33
  // pipeline today; this is provenance-only.
33
34
  private clientAttributionLogged = false;
34
35
 
35
- constructor(config: ServerConfig) {
36
+ /**
37
+ * `client` defaults to the HTTP client for the public API (the npm/stdio
38
+ * server). The hosted endpoint passes an in-process one.
39
+ */
40
+ constructor(config: ServerConfig, client?: DirectoryApi) {
36
41
  this.config = config;
37
42
 
38
- this.client = new DirectoryClient({
39
- baseUrl: config.directoryUrl,
40
- debug: config.debug,
41
- });
43
+ this.client =
44
+ client ??
45
+ new DirectoryClient({
46
+ baseUrl: config.directoryUrl,
47
+ debug: config.debug,
48
+ });
42
49
 
43
50
  this.server = new Server(
44
51
  {
@@ -248,6 +255,11 @@ export class DirectoryMcpServer {
248
255
  };
249
256
  }
250
257
 
258
+ /** Serve over any MCP transport (the hosted endpoint uses Streamable HTTP). */
259
+ async connect(transport: Transport): Promise<void> {
260
+ await this.server.connect(transport);
261
+ }
262
+
251
263
  async start(): Promise<void> {
252
264
  const transport = new StdioServerTransport();
253
265
  await this.server.connect(transport);
@@ -79,7 +79,7 @@ Be honest if the directory is small for a specific style — PICA's directory gr
79
79
 
80
80
  ## What not to do
81
81
 
82
- - Don't claim to license tracks — this skill returns a shortlist, not a license. The user contacts the rights holder via the credits summary.
82
+ - Don't claim to license tracks — this skill returns a shortlist, not a license. When the user wants to approach an owner, offer `directory_send_licensing_enquiry`, and send it only once they say yes and give their own name, email and project description. Never invent those details.
83
83
  - Don't speculate about pricing — sync fees are negotiated per-placement
84
84
  - Don't return results from outside the directory (no general web search, no Spotify metadata directly) — the directory is the source of truth for this skill
85
85
  - Don't return tracks with no attested credits without flagging them as "rights chain unverified"
@@ -26,7 +26,7 @@ export const SKILLS: Record<string, Skill> = {
26
26
  audience: "sync supervisor / music library / ad agency / film & TV music team",
27
27
  tools_required: ["directory_search_recordings","directory_lookup_work","directory_lookup_person"],
28
28
  output: "A rights-aware shortlist — each result with title, artist, audio characteristics (BPM, key, energy), credits summary for licensing contact, and any flags (unattested credits, low registration score, missing ISRC).",
29
- body: "# find-music-for-sync-brief\n\nHelp the user find music in PICA's public directory for a sync brief. Translate their description (mood, vibe, scene, era, vocal style) into search parameters, run the search, and return a shortlist that includes the information they need to license — not just the track.\n\nThis is a public skill — any AI agent connected to the directory MCP can use it. The directory only contains works from organisations that have opted in.\n\n## Step 1 — Translate the brief\n\nMost sync briefs are described in natural language. Translate before searching:\n\n| User says | Search parameters |\n|---|---|\n| upbeat / uplifting / driving | `min_energy: 0.6`, `min_danceability: 0.5` |\n| dark / moody / brooding | `max_energy: 0.4`, `key_mode: minor` |\n| chill / ambient / background | `max_energy: 0.4`, `max_bpm: 100` |\n| epic / cinematic | `min_energy: 0.7`, often longer durations |\n| specific BPM (\"around 120bpm\") | `min_bpm: 115`, `max_bpm: 125` |\n| acoustic / sparse | typically lower `danceability` and `energy` |\n| vocal / instrumental | filter on `has_vocals` if available |\n\nIf the user gives an artist or song reference (\"something like X\"), look up that reference first via `directory_lookup_work` or `directory_search_recordings` by title — then mimic its audio profile in the new search.\n\n## Step 2 — Search\n\nCall `directory_search_recordings` with the parameters from step 1. Default limit is fine for an initial scan.\n\nIf the brief is vague, do two searches with slightly different parameters and compare. If too many results, narrow on BPM or key. If too few, widen the BPM range first, then drop key_mode.\n\n## Step 3 — Lookup full details\n\nFor the top 5-10 promising results, call `directory_lookup_work` to get:\n\n- **Who wrote it** — credits with IPI numbers (essential for the licensing contact path)\n- **Whether credits are attested** — an unattested credit means the ownership claim hasn't been verified; flag this\n- **DSP links** (Spotify, Apple Music) — the user wants to listen before shortlisting\n- **Registration score** — higher score = cleaner rights chain = easier to license\n\nFor high-value matches where the user wants to know more about the creator, call `directory_lookup_person` with the IPI to see the writer's broader catalog and collaborator network.\n\n## Step 4 — Present the shortlist\n\nPresent each shortlisted track with:\n\n- **Title, artist** — what the user listens for\n- **Audio profile** — BPM, key, energy (one line, the supervisor's quick filter)\n- **Credits summary** — writers with IPI, publisher if known (this is the licensing path)\n- **DSP links** — so the user can listen\n- **Flags** — unattested credits, low registration score, missing ISRC, multiple writers (= multi-party clearance needed)\n\nIf a track has all green flags (attested credits, high registration score, ISRC present, single or low-count writers), call it out as \"clean to clear.\"\n\n## Step 5 — If no results\n\nIf the search returns nothing, walk the user back through the brief:\n- Widen the BPM range first (most supervisors don't actually mind ±10 BPM)\n- Drop the key filter (key matching matters less than mood for most placements)\n- Drop the energy bounds and search by genre/mood text only\n- Suggest the user expand to adjacent moods (\"uplifting\" → also try \"warm\" or \"hopeful\")\n\nBe honest if the directory is small for a specific style — PICA's directory grows with org opt-ins; some niches are sparse.\n\n## What not to do\n\n- Don't claim to license tracks — this skill returns a shortlist, not a license. The user contacts the rights holder via the credits summary.\n- Don't speculate about pricing — sync fees are negotiated per-placement\n- Don't return results from outside the directory (no general web search, no Spotify metadata directly) — the directory is the source of truth for this skill\n- Don't return tracks with no attested credits without flagging them as \"rights chain unverified\"\n\n## Follow-on skills\n\n- `due-diligence-on-creator` (Stage 2 candidate) — for deeper research on a specific writer before reaching out\n",
29
+ body: "# find-music-for-sync-brief\n\nHelp the user find music in PICA's public directory for a sync brief. Translate their description (mood, vibe, scene, era, vocal style) into search parameters, run the search, and return a shortlist that includes the information they need to license — not just the track.\n\nThis is a public skill — any AI agent connected to the directory MCP can use it. The directory only contains works from organisations that have opted in.\n\n## Step 1 — Translate the brief\n\nMost sync briefs are described in natural language. Translate before searching:\n\n| User says | Search parameters |\n|---|---|\n| upbeat / uplifting / driving | `min_energy: 0.6`, `min_danceability: 0.5` |\n| dark / moody / brooding | `max_energy: 0.4`, `key_mode: minor` |\n| chill / ambient / background | `max_energy: 0.4`, `max_bpm: 100` |\n| epic / cinematic | `min_energy: 0.7`, often longer durations |\n| specific BPM (\"around 120bpm\") | `min_bpm: 115`, `max_bpm: 125` |\n| acoustic / sparse | typically lower `danceability` and `energy` |\n| vocal / instrumental | filter on `has_vocals` if available |\n\nIf the user gives an artist or song reference (\"something like X\"), look up that reference first via `directory_lookup_work` or `directory_search_recordings` by title — then mimic its audio profile in the new search.\n\n## Step 2 — Search\n\nCall `directory_search_recordings` with the parameters from step 1. Default limit is fine for an initial scan.\n\nIf the brief is vague, do two searches with slightly different parameters and compare. If too many results, narrow on BPM or key. If too few, widen the BPM range first, then drop key_mode.\n\n## Step 3 — Lookup full details\n\nFor the top 5-10 promising results, call `directory_lookup_work` to get:\n\n- **Who wrote it** — credits with IPI numbers (essential for the licensing contact path)\n- **Whether credits are attested** — an unattested credit means the ownership claim hasn't been verified; flag this\n- **DSP links** (Spotify, Apple Music) — the user wants to listen before shortlisting\n- **Registration score** — higher score = cleaner rights chain = easier to license\n\nFor high-value matches where the user wants to know more about the creator, call `directory_lookup_person` with the IPI to see the writer's broader catalog and collaborator network.\n\n## Step 4 — Present the shortlist\n\nPresent each shortlisted track with:\n\n- **Title, artist** — what the user listens for\n- **Audio profile** — BPM, key, energy (one line, the supervisor's quick filter)\n- **Credits summary** — writers with IPI, publisher if known (this is the licensing path)\n- **DSP links** — so the user can listen\n- **Flags** — unattested credits, low registration score, missing ISRC, multiple writers (= multi-party clearance needed)\n\nIf a track has all green flags (attested credits, high registration score, ISRC present, single or low-count writers), call it out as \"clean to clear.\"\n\n## Step 5 — If no results\n\nIf the search returns nothing, walk the user back through the brief:\n- Widen the BPM range first (most supervisors don't actually mind ±10 BPM)\n- Drop the key filter (key matching matters less than mood for most placements)\n- Drop the energy bounds and search by genre/mood text only\n- Suggest the user expand to adjacent moods (\"uplifting\" → also try \"warm\" or \"hopeful\")\n\nBe honest if the directory is small for a specific style — PICA's directory grows with org opt-ins; some niches are sparse.\n\n## What not to do\n\n- Don't claim to license tracks — this skill returns a shortlist, not a license. When the user wants to approach an owner, offer `directory_send_licensing_enquiry`, and send it only once they say yes and give their own name, email and project description. Never invent those details.\n- Don't speculate about pricing — sync fees are negotiated per-placement\n- Don't return results from outside the directory (no general web search, no Spotify metadata directly) — the directory is the source of truth for this skill\n- Don't return tracks with no attested credits without flagging them as \"rights chain unverified\"\n\n## Follow-on skills\n\n- `due-diligence-on-creator` (Stage 2 candidate) — for deeper research on a specific writer before reaching out\n",
30
30
  },
31
31
  };
32
32
 
@@ -1,12 +1,12 @@
1
1
  // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
- import { DirectoryClient } from "../client.js";
3
+ import type { DirectoryApi } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
5
5
 
6
6
  export class DirectoryChainTools {
7
- private client: DirectoryClient;
7
+ private client: DirectoryApi;
8
8
 
9
- constructor(client: DirectoryClient) {
9
+ constructor(client: DirectoryApi) {
10
10
  this.client = client;
11
11
  }
12
12
 
@@ -26,6 +26,9 @@ export class DirectoryChainTools {
26
26
  "Prefer over stitching list_works + lookup_work + lookup_person when you want the whole graph " +
27
27
  "around a track in a single round-trip. " +
28
28
  "Supports audio-only queries (min_bpm, key, mood, etc.) when you don't have a text query. " +
29
+ "Work results carry programmable_since (every owner confirmed a locked split) and " +
30
+ "ready_to_license (composition and a master confirmed, with audio, ISRC and ISWC); " +
31
+ "filter to either with programmable_only / ready_to_license. " +
29
32
  "→ then: directory_lookup_work (deeper work detail), directory_lookup_person (research a named writer)",
30
33
  inputSchema: {
31
34
  type: "object",
@@ -62,6 +65,16 @@ export class DirectoryChainTools {
62
65
  type: "string",
63
66
  description: "Mood filter (exact match)",
64
67
  },
68
+ programmable_only: {
69
+ type: "boolean",
70
+ description:
71
+ "Only programmable songs — every owner confirmed a locked split, so a licence fee is paid to each without chasing sign-off. Drops people from text results.",
72
+ },
73
+ ready_to_license: {
74
+ type: "boolean",
75
+ description:
76
+ "Only songs ready to license — composition and a master confirmed by every owner, that recording has audio and an ISRC, and the song has an ISWC. Audio results are the ready recordings themselves. Drops people from text results.",
77
+ },
65
78
  limit: {
66
79
  type: "number",
67
80
  description: "Max results (default 10, max 20)",
@@ -94,6 +107,8 @@ export class DirectoryChainTools {
94
107
  if (args.key) params.key = args.key;
95
108
  if (args.key_mode) params.key_mode = args.key_mode;
96
109
  if (args.mood) params.mood = args.mood;
110
+ if (args.programmable_only === true) params.programmable_only = "true";
111
+ if (args.ready_to_license === true) params.ready_to_license = "true";
97
112
 
98
113
  const response: any = await this.client.request("/chain", params);
99
114