@withpica/mcp-server-directory 1.4.1 → 1.4.2

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 (77) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +62 -0
  3. package/dist/client.js +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/changelog.generated.d.ts +2 -2
  8. package/dist/lib/changelog.generated.d.ts.map +1 -1
  9. package/dist/lib/changelog.generated.js +3 -3
  10. package/dist/lib/changelog.generated.js.map +1 -1
  11. package/dist/lib/changelog.js +1 -1
  12. package/dist/prompts/index.js +1 -1
  13. package/dist/prompts/public-question-atlas.js +1 -1
  14. package/dist/resources/llms-primer.js +1 -1
  15. package/dist/server.js +1 -1
  16. package/dist/skills/index.js +1 -1
  17. package/dist/skills/skills.generated.js +1 -1
  18. package/dist/tools/chain.js +1 -1
  19. package/dist/tools/index.js +1 -1
  20. package/dist/tools/people.js +1 -1
  21. package/dist/tools/recordings.js +1 -1
  22. package/dist/tools/release-notes.d.ts.map +1 -1
  23. package/dist/tools/release-notes.js +3 -2
  24. package/dist/tools/release-notes.js.map +1 -1
  25. package/dist/tools/search.js +1 -1
  26. package/dist/tools/skills.d.ts.map +1 -1
  27. package/dist/tools/skills.js +5 -3
  28. package/dist/tools/skills.js.map +1 -1
  29. package/dist/tools/works.js +1 -1
  30. package/dist/utils/errors.d.ts.map +1 -1
  31. package/dist/utils/errors.js +57 -4
  32. package/dist/utils/errors.js.map +1 -1
  33. package/dist/utils/formatting.d.ts.map +1 -1
  34. package/dist/utils/formatting.js +9 -3
  35. package/dist/utils/formatting.js.map +1 -1
  36. package/jest.config.js +4 -1
  37. package/package.json +8 -4
  38. package/scripts/build-changelog.ts +2 -2
  39. package/scripts/build-skills.ts +9 -4
  40. package/src/__tests__/prompts/index.test.ts +1 -1
  41. package/src/__tests__/prompts/prompt-eval-harness.test.ts +1 -1
  42. package/src/__tests__/skills/skills-registry.test.ts +2 -5
  43. package/src/__tests__/skills/skills-tools.test.ts +5 -13
  44. package/src/__tests__/tools/chain.test.ts +1 -1
  45. package/src/__tests__/tools/composability-chains.test.ts +15 -26
  46. package/src/__tests__/tools/people.test.ts +1 -1
  47. package/src/__tests__/tools/search.test.ts +1 -1
  48. package/src/__tests__/tools/tool-count-parity.test.ts +63 -0
  49. package/src/__tests__/tools/works.test.ts +1 -1
  50. package/src/__tests__/utils/errors.test.ts +68 -0
  51. package/src/client.ts +1 -1
  52. package/src/config.ts +1 -1
  53. package/src/index.ts +1 -2
  54. package/src/lib/changelog.ts +1 -1
  55. package/src/prompts/index.ts +1 -1
  56. package/src/prompts/public-question-atlas.ts +1 -1
  57. package/src/resources/llms-primer.ts +1 -1
  58. package/src/server.ts +1 -1
  59. package/src/skills/find-music-for-sync-brief/SKILL.md +1 -1
  60. package/src/skills/index.ts +1 -1
  61. package/src/skills/skills.generated.ts +1 -1
  62. package/src/tools/__tests__/release-notes.test.ts +1 -1
  63. package/src/tools/chain.ts +1 -1
  64. package/src/tools/index.ts +1 -1
  65. package/src/tools/people.ts +1 -1
  66. package/src/tools/recordings.ts +1 -1
  67. package/src/tools/release-notes.ts +3 -2
  68. package/src/tools/search.ts +1 -1
  69. package/src/tools/skills.ts +5 -3
  70. package/src/tools/works.ts +1 -1
  71. package/src/utils/errors.ts +72 -4
  72. package/src/utils/formatting.ts +9 -3
  73. package/tsconfig.build.json +10 -0
  74. package/dist/tools/__tests__/release-notes.test.d.ts +0 -2
  75. package/dist/tools/__tests__/release-notes.test.d.ts.map +0 -1
  76. package/dist/tools/__tests__/release-notes.test.js +0 -49
  77. package/dist/tools/__tests__/release-notes.test.js.map +0 -1
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * ADR-140 Phase 2b — generator for `skills.generated.ts` (directory MCP).
@@ -91,7 +91,9 @@ function parseFrontmatter(src: string, sourceFile: string): ParsedSkill {
91
91
  ];
92
92
  for (const field of required) {
93
93
  if (!(field in fm)) {
94
- throw new Error(`${sourceFile}: missing required frontmatter field "${field}"`);
94
+ throw new Error(
95
+ `${sourceFile}: missing required frontmatter field "${field}"`,
96
+ );
95
97
  }
96
98
  }
97
99
  if (!Array.isArray(fm.triggers) || fm.triggers.length === 0) {
@@ -136,7 +138,7 @@ function discoverSkills(): ParsedSkill[] {
136
138
  }
137
139
 
138
140
  function emit(skills: ParsedSkill[]): string {
139
- const header = `// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
141
+ const header = `// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
140
142
 
141
143
  /**
142
144
  * GENERATED FILE — DO NOT EDIT BY HAND.
@@ -156,7 +158,10 @@ export interface Skill {
156
158
  }
157
159
 
158
160
  `;
159
- const lines: string[] = [header, "export const SKILLS: Record<string, Skill> = {"];
161
+ const lines: string[] = [
162
+ header,
163
+ "export const SKILLS: Record<string, Skill> = {",
164
+ ];
160
165
  for (const skill of skills) {
161
166
  lines.push(` ${JSON.stringify(skill.name)}: {`);
162
167
  lines.push(` name: ${JSON.stringify(skill.name)},`);
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Directory Prompt Registry Tests
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Directory Prompt Eval Harness — ADR-140 Phase 3
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Directory SkillsRegistry test suite — ADR-140 Phase 2b
@@ -9,10 +9,7 @@
9
9
  */
10
10
 
11
11
  import { describe, it, expect } from "@jest/globals";
12
- import {
13
- SkillsRegistry,
14
- SkillNotFoundError,
15
- } from "../../skills/index.js";
12
+ import { SkillsRegistry, SkillNotFoundError } from "../../skills/index.js";
16
13
 
17
14
  describe("Directory SkillsRegistry", () => {
18
15
  const registry = new SkillsRegistry();
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Directory skills tools test suite — ADR-140 Phase 2b
@@ -38,15 +38,11 @@ describe("DirectorySkillsTools", () => {
38
38
  const summaries = result.structuredContent!.skills as Array<{
39
39
  name: string;
40
40
  }>;
41
- expect(summaries.map((s) => s.name)).toContain(
42
- "find-music-for-sync-brief",
43
- );
41
+ expect(summaries.map((s) => s.name)).toContain("find-music-for-sync-brief");
44
42
  });
45
43
 
46
44
  it("directory_skill_get returns the methodology body", async () => {
47
- const get = tools.find(
48
- (t) => t.definition.name === "directory_skill_get",
49
- )!;
45
+ const get = tools.find((t) => t.definition.name === "directory_skill_get")!;
50
46
  const result = await get.executor({ name: "find-music-for-sync-brief" });
51
47
  expect(result.isError).toBeFalsy();
52
48
  const textBlock = result.content.find((c: any) => c.type === "text") as
@@ -57,17 +53,13 @@ describe("DirectorySkillsTools", () => {
57
53
  });
58
54
 
59
55
  it("directory_skill_get returns isError for unknown skill", async () => {
60
- const get = tools.find(
61
- (t) => t.definition.name === "directory_skill_get",
62
- )!;
56
+ const get = tools.find((t) => t.definition.name === "directory_skill_get")!;
63
57
  const result = await get.executor({ name: "not-a-real-skill" });
64
58
  expect(result.isError).toBe(true);
65
59
  });
66
60
 
67
61
  it("directory_skill_get returns isError when name is missing", async () => {
68
- const get = tools.find(
69
- (t) => t.definition.name === "directory_skill_get",
70
- )!;
62
+ const get = tools.find((t) => t.definition.name === "directory_skill_get")!;
71
63
  const result = await get.executor({});
72
64
  expect(result.isError).toBe(true);
73
65
  });
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import {
4
4
  jest,
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Directory MCP — Composability Chain Tests
@@ -6,33 +6,22 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect, beforeAll } from "@jest/globals";
9
- import { DirectoryWorksTools } from "../../tools/works";
10
- import { DirectoryPeopleTools } from "../../tools/people";
11
- import { DirectorySearchTools } from "../../tools/search";
12
- import { DirectoryRecordingsTools } from "../../tools/recordings";
13
- import { DirectoryChainTools } from "../../tools/chain";
9
+ import { ToolRegistry } from "../../tools/index";
14
10
 
15
11
  const nullClient = null as any;
16
12
 
13
+ /**
14
+ * 2026-08-24 — derived from the REAL registry, not a local class list. The
15
+ * previous fixed five-class list here missed DirectorySkillsTools and
16
+ * DirectoryReleaseNotesTools, so "all 8 tools are registered" passed while
17
+ * the server advertised 11 — a fixed list cannot see the class nobody
18
+ * listed (same failure family as the shows-trust-floor guard).
19
+ */
17
20
  function getAllTools() {
18
- const classes = [
19
- new DirectoryWorksTools(nullClient),
20
- new DirectoryPeopleTools(nullClient),
21
- new DirectorySearchTools(nullClient),
22
- new DirectoryRecordingsTools(nullClient),
23
- new DirectoryChainTools(nullClient),
24
- ];
25
-
26
- const tools: Array<{ name: string; description: string }> = [];
27
- for (const cls of classes) {
28
- for (const tool of cls.getTools()) {
29
- tools.push({
30
- name: tool.definition.name,
31
- description: tool.definition.description,
32
- });
33
- }
34
- }
35
- return tools;
21
+ return new ToolRegistry(nullClient).listTools().map((definition) => ({
22
+ name: definition.name,
23
+ description: definition.description,
24
+ }));
36
25
  }
37
26
 
38
27
  function parseChains(description: string): string[] {
@@ -50,8 +39,8 @@ describe("Directory Composability Chains", () => {
50
39
  allNames = new Set(allTools.map((t) => t.name));
51
40
  });
52
41
 
53
- it("all 8 tools are registered", () => {
54
- expect(allTools).toHaveLength(8);
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);
55
44
  });
56
45
 
57
46
  it("all tools have composability chains", () => {
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import {
4
4
  jest,
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import {
4
4
  jest,
@@ -0,0 +1,63 @@
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
+
3
+ /**
4
+ * Directory tool-count parity tripwire — sister of
5
+ * mcp-server/src/__tests__/tools/tool-count-parity.test.ts.
6
+ *
7
+ * MCP_DIRECTORY_TOOL_COUNT in `lib/constants/mcp-surface.ts` is published
8
+ * live at /.well-known/mcp.json and /.well-known/mcp/server-card.json. It
9
+ * was documented as "small, hand-verified" and drifted exactly as
10
+ * hand-verified numbers do: it read 9 while this registry served 11
11
+ * (directory_search_recordings and directory_release_notes landed without a
12
+ * bump). This test builds the REAL registry and fails when the constant
13
+ * drifts — update the constant in the same PR as any directory tool
14
+ * addition, removal, or retirement.
15
+ *
16
+ * The constant is read from the file as text (not imported) — the app and
17
+ * this package are deliberately isolated builds; a source-text read keeps
18
+ * the tripwire without coupling them.
19
+ */
20
+
21
+ import { describe, it, expect } from "@jest/globals";
22
+ import { readFileSync, existsSync } from "node:fs";
23
+ import { resolve } from "node:path";
24
+ import { ToolRegistry } from "../../tools/index";
25
+
26
+ function readDeclaredCount(): { count: number; source: string } {
27
+ // jest runs with cwd = mcp-server-directory (package script) but tolerate
28
+ // a repo-root cwd too.
29
+ const candidates = [
30
+ resolve(process.cwd(), "..", "lib", "constants", "mcp-surface.ts"),
31
+ resolve(process.cwd(), "lib", "constants", "mcp-surface.ts"),
32
+ ];
33
+ const source = candidates.find((p) => existsSync(p));
34
+ if (!source) {
35
+ throw new Error(
36
+ `lib/constants/mcp-surface.ts not found from cwd=${process.cwd()} — the app-side directory tool-count constant is missing`,
37
+ );
38
+ }
39
+ const text = readFileSync(source, "utf8");
40
+ const match = text.match(/MCP_DIRECTORY_TOOL_COUNT\s*=\s*(\d+)/);
41
+ if (!match) {
42
+ throw new Error(`MCP_DIRECTORY_TOOL_COUNT not declared in ${source}`);
43
+ }
44
+ return { count: Number(match[1]), source };
45
+ }
46
+
47
+ describe("directory tool-count parity — app constant vs real registry", () => {
48
+ it("MCP_DIRECTORY_TOOL_COUNT equals the registry's advertised surface", () => {
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ const registry = new ToolRegistry(null as any);
51
+ const advertised = registry.listTools().length;
52
+ const declared = readDeclaredCount();
53
+
54
+ if (advertised !== declared.count) {
55
+ throw new Error(
56
+ `Directory tool count drifted: registry advertises ${advertised} tools, ` +
57
+ `but MCP_DIRECTORY_TOOL_COUNT in ${declared.source} declares ${declared.count}. ` +
58
+ `Update the constant in the same PR as the tool change.`,
59
+ );
60
+ }
61
+ expect(advertised).toBe(declared.count);
62
+ });
63
+ });
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import {
4
4
  jest,
@@ -0,0 +1,68 @@
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
+
3
+ import { describe, it, expect } from "@jest/globals";
4
+ import { formatError, McpServerError } from "../../utils/errors.js";
5
+
6
+ describe("directory-mcp formatError structured-body decode", () => {
7
+ it("keeps McpServerError code + message", () => {
8
+ const result = formatError(
9
+ new McpServerError("Tool blew up", "TOOL_EXECUTION_ERROR"),
10
+ );
11
+ const parsed = JSON.parse(result.text);
12
+ expect(parsed.error).toBe("TOOL_EXECUTION_ERROR");
13
+ expect(parsed.message).toBe("Tool blew up");
14
+ });
15
+
16
+ // PICA's api-response.ts errorResponse() emits a nested body where `error`
17
+ // is an OBJECT: { success: false, error: { code, message, details } }. The
18
+ // mcp-sdk wraps it as `ApiError("API request failed: <status> <body>")`.
19
+ // Without decoding, the whole thing collapsed to UNKNOWN_ERROR.
20
+ it("surfaces code+message from a wrapped nested { error: { code, message } } body", () => {
21
+ const error = {
22
+ status: 404,
23
+ message:
24
+ "API 404 not found: " +
25
+ JSON.stringify({
26
+ success: false,
27
+ error: {
28
+ code: "WORK_NOT_FOUND",
29
+ message: "no work matches that ISWC",
30
+ },
31
+ }),
32
+ };
33
+ const result = formatError(error);
34
+ const parsed = JSON.parse(result.text);
35
+ expect(parsed.error).toBe("WORK_NOT_FOUND");
36
+ expect(parsed.message).toBe("no work matches that ISWC");
37
+ });
38
+
39
+ it("surfaces code+message from the flat { code, error } convention", () => {
40
+ const error = {
41
+ message: JSON.stringify({ code: "BAD_INPUT", error: "nope" }),
42
+ };
43
+ const result = formatError(error);
44
+ const parsed = JSON.parse(result.text);
45
+ expect(parsed.error).toBe("BAD_INPUT");
46
+ expect(parsed.message).toBe("nope");
47
+ });
48
+
49
+ it("recovers the real message from a coded-less body (no UNKNOWN noise)", () => {
50
+ const error = {
51
+ message: JSON.stringify({
52
+ success: false,
53
+ error: { message: "name required" },
54
+ }),
55
+ };
56
+ const result = formatError(error);
57
+ const parsed = JSON.parse(result.text);
58
+ expect(parsed.error).toBe("UNKNOWN_ERROR");
59
+ expect(parsed.message).toBe("name required");
60
+ });
61
+
62
+ it("falls through to UNKNOWN_ERROR for a plain non-JSON error", () => {
63
+ const result = formatError(new Error("some weird thing"));
64
+ const parsed = JSON.parse(result.text);
65
+ expect(parsed.error).toBe("UNKNOWN_ERROR");
66
+ expect(parsed.message).toBe("some weird thing");
67
+ });
68
+ });
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  const DEFAULT_TIMEOUT_MS = 15_000;
4
4
  const MAX_RETRIES = 2;
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  export interface ServerConfig {
4
4
  directoryUrl: string;
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
3
-
2
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
4
3
 
5
4
  import { loadConfig, validateConfig } from "./config.js";
6
5
  import { DirectoryMcpServer } from "./server.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Runtime CHANGELOG parser. Mirrors scripts/parse-changelog.ts but takes
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Prompt Registry for Directory MCP Server
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Public Question Atlas — ADR-229 Decision 1.
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  export const DIRECTORY_PRIMER = `# PICA Directory — Public Music Catalog Search
4
4
 
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1,4 +1,4 @@
1
- <!-- Copyright (c) 2024-2026 Withpica Ltd. All rights reserved. -->
1
+ <!-- Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. -->
2
2
  ---
3
3
  name: find-music-for-sync-brief
4
4
  description: Find music in the PICA public directory for a sync brief — translate a mood/vibe/scene description into audio search parameters, run the search, and return a rights-aware shortlist with credit details for licensing.
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Skills Registry — directory MCP, ADR-140 Phase 2b
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * GENERATED FILE — DO NOT EDIT BY HAND.
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryReleaseNotesTools } from "../release-notes.js";
4
4
  import type { DirectoryClient } from "../../client.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { DirectoryWorksTools } from "./works.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any -- ToolExecutor signature uses Record<string, any> */
4
4
 
@@ -58,7 +58,8 @@ export class DirectoryReleaseNotesTools {
58
58
  "directory_list_works / directory_chain for those). " +
59
59
  "Returns { package, current_version, releases: [{ version, date, sections, markdown }], resource_uri }. " +
60
60
  "Defaults to the last 3 releases; use `since_version` to bound the output " +
61
- "or `sections` to filter to Added/Fixed/etc.",
61
+ "or `sections` to filter to Added/Fixed/etc. " +
62
+ "→ then: directory_search (explore anything a release note mentions)",
62
63
  inputSchema: {
63
64
  type: "object",
64
65
  properties: {
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  /**
4
4
  * Directory MCP skill tools — ADR-140 Phase 2b
@@ -27,7 +27,8 @@ export class DirectorySkillsTools {
27
27
  description:
28
28
  "Use when the user asks: 'what skills do you have?', 'what can you do?', 'show me your methodologies', 'how do I find music here?'. " +
29
29
  "Returns the list of downloadable directory skill methodologies — name, description, trigger phrases. " +
30
- "Cheap to call. Use directory_skill_get(name) to load a specific skill's full methodology body.",
30
+ "Cheap to call. " +
31
+ "→ then: directory_skill_get (load a specific skill's full methodology body)",
31
32
  inputSchema: {
32
33
  type: "object",
33
34
  properties: {},
@@ -43,7 +44,8 @@ export class DirectorySkillsTools {
43
44
  description:
44
45
  "Use when the user asks: 'what skills do you have?', 'what can you do?', 'show me your methodologies'. " +
45
46
  "Returns the full methodology body for one directory skill — markdown with step-by-step instructions and tool chains. " +
46
- "Call directory_skill_list first to see available skill names, then directory_skill_get to load one. Read once at the start of the workflow; do not re-fetch on every step.",
47
+ "Call directory_skill_list first to see available skill names, then directory_skill_get to load one. Read once at the start of the workflow; do not re-fetch on every step. " +
48
+ "→ then: directory_search (run the loaded methodology against the catalogue)",
47
49
  inputSchema: {
48
50
  type: "object",
49
51
  properties: {
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  import { DirectoryClient } from "../client.js";
4
4
  import { ToolDefinition, ToolExecutor } from "./index.js";
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  export class McpServerError extends Error {
4
4
  constructor(
@@ -31,11 +31,47 @@ export function formatError(error: any): { type: string; text: string } {
31
31
  };
32
32
  }
33
33
 
34
- if (error instanceof Error) {
34
+ // Recover a structured error body so a real domain code surfaces instead of
35
+ // collapsing every route failure to UNKNOWN_ERROR. The mcp-sdk wraps the
36
+ // body inside `ApiError("API request failed: <status> <body>")`, so the JSON
37
+ // is not at the start of the string. PICA's api-response.ts errorResponse()
38
+ // emits { success: false, error: { code, message, details } } — `error` is a
39
+ // nested OBJECT; a few routes emit the flat { code, error } / { error_code,
40
+ // message } conventions. Decode all of them.
41
+ const rawMessage =
42
+ error instanceof Error
43
+ ? error.message
44
+ : (error as any)?.message || String(error);
45
+ const structuredBody = extractEmbeddedJson(rawMessage);
46
+ const nestedError =
47
+ structuredBody?.error && typeof structuredBody.error === "object"
48
+ ? (structuredBody.error as {
49
+ code?: string;
50
+ message?: string;
51
+ details?: unknown;
52
+ })
53
+ : null;
54
+ const bodyCode =
55
+ structuredBody?.error_code ?? structuredBody?.code ?? nestedError?.code;
56
+ const bodyMessage =
57
+ structuredBody?.message ??
58
+ nestedError?.message ??
59
+ (typeof structuredBody?.error === "string"
60
+ ? structuredBody.error
61
+ : undefined);
62
+ const bodyDetails = structuredBody?.details ?? nestedError?.details;
63
+
64
+ // Structured error body from the route — propagate the domain-specific code
65
+ // and message rather than the "API request failed: …" wrapper noise.
66
+ if (bodyCode) {
35
67
  return {
36
68
  type: "text",
37
69
  text: JSON.stringify(
38
- { error: "UNKNOWN_ERROR", message: error.message },
70
+ {
71
+ error: bodyCode,
72
+ message: bodyMessage || rawMessage,
73
+ details: bodyDetails,
74
+ },
39
75
  null,
40
76
  2,
41
77
  ),
@@ -45,13 +81,45 @@ export function formatError(error: any): { type: string; text: string } {
45
81
  return {
46
82
  type: "text",
47
83
  text: JSON.stringify(
48
- { error: "UNKNOWN_ERROR", message: String(error) },
84
+ {
85
+ error: "UNKNOWN_ERROR",
86
+ // A body without a code still carries the real message — surface it.
87
+ message: bodyMessage || rawMessage,
88
+ },
49
89
  null,
50
90
  2,
51
91
  ),
52
92
  };
53
93
  }
54
94
 
95
+ /**
96
+ * Extract a JSON error body from an error message. The body may be the whole
97
+ * string, or — as the mcp-sdk wraps it — embedded after a prefix
98
+ * ("API request failed: 400 {…}"). Slices from the first `{` to the last `}`
99
+ * so a wrapped body is still recovered. Returns null when there is no
100
+ * parseable object.
101
+ */
102
+ function extractEmbeddedJson(raw: unknown): {
103
+ error_code?: string;
104
+ code?: string;
105
+ message?: string;
106
+ // `error` is a string for { code, error: "<message>" }, an object for PICA's
107
+ // nested { error: { code, message, details } } shape.
108
+ error?: string | { code?: string; message?: string; details?: unknown };
109
+ details?: unknown;
110
+ } | null {
111
+ if (typeof raw !== "string") return null;
112
+ const first = raw.indexOf("{");
113
+ const last = raw.lastIndexOf("}");
114
+ if (first === -1 || last <= first) return null;
115
+ try {
116
+ const parsed = JSON.parse(raw.slice(first, last + 1));
117
+ return parsed && typeof parsed === "object" ? parsed : null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
55
123
  export function logError(context: string, error: any): void {
56
124
  const entry: Record<string, unknown> = {
57
125
  level: "error",
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
1
+ // Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
2
2
 
3
3
  export interface FormattedResult {
4
4
  content: Array<{ type: string; text: string }>;
@@ -6,9 +6,15 @@ export interface FormattedResult {
6
6
  }
7
7
 
8
8
  export function formatAsText(data: any): FormattedResult {
9
+ // MCP requires structuredContent to be a RECORD — bare arrays are rejected
10
+ // by the SDK server with -32602. Wrap them in the standard list envelope
11
+ // (mirrors @withpica/mcp-utils formatAsText).
12
+ const envelope = Array.isArray(data)
13
+ ? { items: data, count: data.length }
14
+ : data;
9
15
  return {
10
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
11
- structuredContent: data as Record<string, unknown>,
16
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
17
+ structuredContent: envelope as Record<string, unknown>,
12
18
  };
13
19
  }
14
20
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": [
4
+ "node_modules",
5
+ "dist",
6
+ "**/__tests__/**",
7
+ "**/*.test.ts",
8
+ "**/*.spec.ts"
9
+ ]
10
+ }