@withpica/mcp-server-directory 1.4.0 → 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.
- package/CHANGELOG.md +30 -0
- package/README.md +62 -0
- package/dist/client.js +1 -1
- package/dist/config.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/changelog.generated.d.ts +2 -2
- package/dist/lib/changelog.generated.d.ts.map +1 -1
- package/dist/lib/changelog.generated.js +3 -3
- package/dist/lib/changelog.generated.js.map +1 -1
- package/dist/lib/changelog.js +1 -1
- package/dist/prompts/index.js +1 -1
- package/dist/prompts/public-question-atlas.js +1 -1
- package/dist/resources/llms-primer.js +1 -1
- package/dist/server.js +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/skills.generated.js +1 -1
- package/dist/tools/chain.js +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/people.js +1 -1
- package/dist/tools/recordings.js +1 -1
- package/dist/tools/release-notes.d.ts.map +1 -1
- package/dist/tools/release-notes.js +3 -2
- package/dist/tools/release-notes.js.map +1 -1
- package/dist/tools/search.js +1 -1
- package/dist/tools/skills.d.ts.map +1 -1
- package/dist/tools/skills.js +5 -3
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/works.js +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +57 -4
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/formatting.d.ts.map +1 -1
- package/dist/utils/formatting.js +9 -3
- package/dist/utils/formatting.js.map +1 -1
- package/jest.config.js +4 -1
- package/package.json +10 -6
- package/scripts/build-changelog.ts +2 -2
- package/scripts/build-skills.ts +9 -4
- package/src/__tests__/prompts/index.test.ts +1 -1
- package/src/__tests__/prompts/prompt-eval-harness.test.ts +1 -1
- package/src/__tests__/skills/skills-registry.test.ts +2 -5
- package/src/__tests__/skills/skills-tools.test.ts +5 -13
- package/src/__tests__/tools/chain.test.ts +1 -1
- package/src/__tests__/tools/composability-chains.test.ts +15 -26
- package/src/__tests__/tools/people.test.ts +1 -1
- package/src/__tests__/tools/search.test.ts +1 -1
- package/src/__tests__/tools/tool-count-parity.test.ts +63 -0
- package/src/__tests__/tools/works.test.ts +1 -1
- package/src/__tests__/utils/errors.test.ts +68 -0
- package/src/client.ts +1 -1
- package/src/config.ts +1 -1
- package/src/index.ts +1 -2
- package/src/lib/changelog.ts +1 -1
- package/src/prompts/index.ts +1 -1
- package/src/prompts/public-question-atlas.ts +1 -1
- package/src/resources/llms-primer.ts +1 -1
- package/src/server.ts +1 -1
- package/src/skills/find-music-for-sync-brief/SKILL.md +1 -1
- package/src/skills/index.ts +1 -1
- package/src/skills/skills.generated.ts +1 -1
- package/src/tools/__tests__/release-notes.test.ts +1 -1
- package/src/tools/chain.ts +1 -1
- package/src/tools/index.ts +1 -1
- package/src/tools/people.ts +1 -1
- package/src/tools/recordings.ts +1 -1
- package/src/tools/release-notes.ts +3 -2
- package/src/tools/search.ts +1 -1
- package/src/tools/skills.ts +5 -3
- package/src/tools/works.ts +1 -1
- package/src/utils/errors.ts +72 -4
- package/src/utils/formatting.ts +9 -3
- package/tsconfig.build.json +10 -0
- package/dist/tools/__tests__/release-notes.test.d.ts +0 -2
- package/dist/tools/__tests__/release-notes.test.d.ts.map +0 -1
- package/dist/tools/__tests__/release-notes.test.js +0 -49
- package/dist/tools/__tests__/release-notes.test.js.map +0 -1
package/scripts/build-skills.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
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(
|
|
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)
|
|
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[] = [
|
|
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)
|
|
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)
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
54
|
-
expect(allTools).toHaveLength(
|
|
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", () => {
|
|
@@ -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
|
+
});
|
|
@@ -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
package/src/config.ts
CHANGED
package/src/index.ts
CHANGED
package/src/lib/changelog.ts
CHANGED
package/src/prompts/index.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
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)
|
|
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.
|
package/src/skills/index.ts
CHANGED
package/src/tools/chain.ts
CHANGED
package/src/tools/index.ts
CHANGED
package/src/tools/people.ts
CHANGED
package/src/tools/recordings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
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: {
|
package/src/tools/search.ts
CHANGED
package/src/tools/skills.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
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.
|
|
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: {
|
package/src/tools/works.ts
CHANGED
package/src/utils/errors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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",
|
package/src/utils/formatting.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c)
|
|
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(
|
|
11
|
-
structuredContent:
|
|
16
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
|
|
17
|
+
structuredContent: envelope as Record<string, unknown>,
|
|
12
18
|
};
|
|
13
19
|
}
|
|
14
20
|
|