@withpica/mcp-server-directory 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/lib/changelog.d.ts +16 -0
- package/dist/lib/changelog.d.ts.map +1 -0
- package/dist/lib/changelog.generated.d.ts +4 -0
- package/dist/lib/changelog.generated.d.ts.map +1 -0
- package/dist/lib/changelog.generated.js +6 -0
- package/dist/lib/changelog.generated.js.map +1 -0
- package/dist/lib/changelog.js +84 -0
- package/dist/lib/changelog.js.map +1 -0
- package/dist/prompts/public-question-atlas.d.ts.map +1 -1
- package/dist/prompts/public-question-atlas.js +30 -0
- package/dist/prompts/public-question-atlas.js.map +1 -1
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +129 -10
- package/dist/server.js.map +1 -1
- package/dist/skills/index.d.ts +32 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +50 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skills.generated.d.ts +18 -0
- package/dist/skills/skills.generated.d.ts.map +1 -0
- package/dist/skills/skills.generated.js +14 -0
- package/dist/skills/skills.generated.js.map +1 -0
- package/dist/tools/__tests__/release-notes.test.d.ts +2 -0
- package/dist/tools/__tests__/release-notes.test.d.ts.map +1 -0
- package/dist/tools/__tests__/release-notes.test.js +49 -0
- package/dist/tools/__tests__/release-notes.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/release-notes.d.ts +22 -0
- package/dist/tools/release-notes.d.ts.map +1 -0
- package/dist/tools/release-notes.js +102 -0
- package/dist/tools/release-notes.js.map +1 -0
- package/dist/tools/skills.d.ts +19 -0
- package/dist/tools/skills.d.ts.map +1 -0
- package/dist/tools/skills.js +127 -0
- package/dist/tools/skills.js.map +1 -0
- package/package.json +4 -2
- package/scripts/build-changelog.ts +25 -0
- package/scripts/build-skills.ts +192 -0
- package/src/__tests__/skills/skills-registry.test.ts +63 -0
- package/src/__tests__/skills/skills-tools.test.ts +74 -0
- package/src/lib/changelog.ts +119 -0
- package/src/prompts/public-question-atlas.ts +36 -10
- package/src/server.ts +142 -11
- package/src/skills/find-music-for-sync-brief/SKILL.md +89 -0
- package/src/skills/index.ts +63 -0
- package/src/skills/skills.generated.ts +33 -0
- package/src/tools/__tests__/release-notes.test.ts +62 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/release-notes.ts +148 -0
- package/src/tools/skills.ts +146 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ADR-140 Phase 2b — generator for `skills.generated.ts` (directory MCP).
|
|
5
|
+
*
|
|
6
|
+
* Sister of mcp-server/scripts/build-skills.ts. Same shape, scoped to the
|
|
7
|
+
* directory MCP. Public/anonymous surface — every skill body must pass the
|
|
8
|
+
* IP-protection lint (scripts/lint-mcp-tools.ts Rule 14) before publishing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const ROOT = join(__dirname, "..");
|
|
18
|
+
const SKILLS_ROOT = join(ROOT, "src/skills");
|
|
19
|
+
const OUTPUT_PATH = join(SKILLS_ROOT, "skills.generated.ts");
|
|
20
|
+
|
|
21
|
+
interface SkillFrontmatter {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
triggers: string[];
|
|
25
|
+
audience: string;
|
|
26
|
+
tools_required: string[];
|
|
27
|
+
output: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ParsedSkill extends SkillFrontmatter {
|
|
31
|
+
body: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseFrontmatter(src: string, sourceFile: string): ParsedSkill {
|
|
35
|
+
let i = 0;
|
|
36
|
+
if (src.startsWith("<!--")) {
|
|
37
|
+
const end = src.indexOf("-->");
|
|
38
|
+
if (end === -1) {
|
|
39
|
+
throw new Error(`${sourceFile}: unterminated HTML comment header`);
|
|
40
|
+
}
|
|
41
|
+
i = end + 3;
|
|
42
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
43
|
+
}
|
|
44
|
+
if (src.slice(i, i + 3) !== "---") {
|
|
45
|
+
throw new Error(`${sourceFile}: missing --- frontmatter opener`);
|
|
46
|
+
}
|
|
47
|
+
i += 3;
|
|
48
|
+
while (i < src.length && src[i] !== "\n") i++;
|
|
49
|
+
i++;
|
|
50
|
+
const closeIdx = src.indexOf("\n---", i);
|
|
51
|
+
if (closeIdx === -1) {
|
|
52
|
+
throw new Error(`${sourceFile}: missing --- frontmatter closer`);
|
|
53
|
+
}
|
|
54
|
+
const frontmatterText = src.slice(i, closeIdx);
|
|
55
|
+
const bodyStart = closeIdx + 4;
|
|
56
|
+
const body = src.slice(bodyStart).replace(/^\n+/, "").trimEnd() + "\n";
|
|
57
|
+
|
|
58
|
+
const fm: Record<string, string | string[]> = {};
|
|
59
|
+
const lines = frontmatterText.split("\n");
|
|
60
|
+
let currentListKey: string | null = null;
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.trim() === "") {
|
|
63
|
+
currentListKey = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const listItemMatch = line.match(/^\s+-\s+(.+)$/);
|
|
67
|
+
if (listItemMatch && currentListKey) {
|
|
68
|
+
(fm[currentListKey] as string[]).push(listItemMatch[1].trim());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const kvMatch = line.match(/^([a-z_][a-z0-9_]*)\s*:\s*(.*)$/i);
|
|
72
|
+
if (!kvMatch) continue;
|
|
73
|
+
const key = kvMatch[1];
|
|
74
|
+
const value = kvMatch[2].trim();
|
|
75
|
+
if (value === "") {
|
|
76
|
+
fm[key] = [];
|
|
77
|
+
currentListKey = key;
|
|
78
|
+
} else {
|
|
79
|
+
fm[key] = value;
|
|
80
|
+
currentListKey = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const required = [
|
|
85
|
+
"name",
|
|
86
|
+
"description",
|
|
87
|
+
"triggers",
|
|
88
|
+
"audience",
|
|
89
|
+
"tools_required",
|
|
90
|
+
"output",
|
|
91
|
+
];
|
|
92
|
+
for (const field of required) {
|
|
93
|
+
if (!(field in fm)) {
|
|
94
|
+
throw new Error(`${sourceFile}: missing required frontmatter field "${field}"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(fm.triggers) || fm.triggers.length === 0) {
|
|
98
|
+
throw new Error(`${sourceFile}: triggers must be a non-empty list`);
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(fm.tools_required) || fm.tools_required.length === 0) {
|
|
101
|
+
throw new Error(`${sourceFile}: tools_required must be a non-empty list`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
name: fm.name as string,
|
|
106
|
+
description: fm.description as string,
|
|
107
|
+
triggers: fm.triggers as string[],
|
|
108
|
+
audience: fm.audience as string,
|
|
109
|
+
tools_required: fm.tools_required as string[],
|
|
110
|
+
output: fm.output as string,
|
|
111
|
+
body,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function discoverSkills(): ParsedSkill[] {
|
|
116
|
+
if (!existsSync(SKILLS_ROOT)) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const entries = readdirSync(SKILLS_ROOT, { withFileTypes: true });
|
|
120
|
+
const skills: ParsedSkill[] = [];
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.isDirectory()) continue;
|
|
123
|
+
const skillFile = join(SKILLS_ROOT, entry.name, "SKILL.md");
|
|
124
|
+
if (!existsSync(skillFile)) continue;
|
|
125
|
+
const src = readFileSync(skillFile, "utf-8");
|
|
126
|
+
const parsed = parseFrontmatter(src, `src/skills/${entry.name}/SKILL.md`);
|
|
127
|
+
if (parsed.name !== entry.name) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`src/skills/${entry.name}/SKILL.md: frontmatter name "${parsed.name}" must match directory name "${entry.name}"`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
skills.push(parsed);
|
|
133
|
+
}
|
|
134
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
return skills;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function emit(skills: ParsedSkill[]): string {
|
|
139
|
+
const header = `// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* GENERATED FILE — DO NOT EDIT BY HAND.
|
|
143
|
+
*
|
|
144
|
+
* Regenerated by \`scripts/build-skills.ts\` from \`src/skills/<name>/SKILL.md\`.
|
|
145
|
+
* Runs in \`prebuild\`. See ADR-140 Phase 2b.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
export interface Skill {
|
|
149
|
+
name: string;
|
|
150
|
+
description: string;
|
|
151
|
+
triggers: string[];
|
|
152
|
+
audience: string;
|
|
153
|
+
tools_required: string[];
|
|
154
|
+
output: string;
|
|
155
|
+
body: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
`;
|
|
159
|
+
const lines: string[] = [header, "export const SKILLS: Record<string, Skill> = {"];
|
|
160
|
+
for (const skill of skills) {
|
|
161
|
+
lines.push(` ${JSON.stringify(skill.name)}: {`);
|
|
162
|
+
lines.push(` name: ${JSON.stringify(skill.name)},`);
|
|
163
|
+
lines.push(` description: ${JSON.stringify(skill.description)},`);
|
|
164
|
+
lines.push(` triggers: ${JSON.stringify(skill.triggers)},`);
|
|
165
|
+
lines.push(` audience: ${JSON.stringify(skill.audience)},`);
|
|
166
|
+
lines.push(` tools_required: ${JSON.stringify(skill.tools_required)},`);
|
|
167
|
+
lines.push(` output: ${JSON.stringify(skill.output)},`);
|
|
168
|
+
lines.push(` body: ${JSON.stringify(skill.body)},`);
|
|
169
|
+
lines.push(` },`);
|
|
170
|
+
}
|
|
171
|
+
lines.push("};");
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(
|
|
174
|
+
`export const SKILL_NAMES: ReadonlyArray<string> = ${JSON.stringify(skills.map((s) => s.name))};`,
|
|
175
|
+
);
|
|
176
|
+
lines.push("");
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function main(): void {
|
|
181
|
+
const skills = discoverSkills();
|
|
182
|
+
const output = emit(skills);
|
|
183
|
+
writeFileSync(OUTPUT_PATH, output, "utf-8");
|
|
184
|
+
console.log(
|
|
185
|
+
`[build-skills/directory] Wrote ${skills.length} skill(s) to src/skills/skills.generated.ts`,
|
|
186
|
+
);
|
|
187
|
+
for (const skill of skills) {
|
|
188
|
+
console.log(` - ${skill.name} (${skill.body.length} bytes body)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory SkillsRegistry test suite — ADR-140 Phase 2b
|
|
5
|
+
*
|
|
6
|
+
* Sister of mcp-server's skills-registry.test.ts. Scoped to the directory
|
|
7
|
+
* MCP's single Stage 1 skill (find-music-for-sync-brief) and its public-skill
|
|
8
|
+
* IP-exposure profile (lower bar — methodology body must be IP-safe).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "@jest/globals";
|
|
12
|
+
import {
|
|
13
|
+
SkillsRegistry,
|
|
14
|
+
SkillNotFoundError,
|
|
15
|
+
} from "../../skills/index.js";
|
|
16
|
+
|
|
17
|
+
describe("Directory SkillsRegistry", () => {
|
|
18
|
+
const registry = new SkillsRegistry();
|
|
19
|
+
|
|
20
|
+
it("registers the Stage 1 directory skill", () => {
|
|
21
|
+
expect(registry.has("find-music-for-sync-brief")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("listSkills returns metadata-only summaries", () => {
|
|
25
|
+
for (const summary of registry.listSkills()) {
|
|
26
|
+
expect(summary).toHaveProperty("triggers");
|
|
27
|
+
expect(summary).not.toHaveProperty("body");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("getSkill returns the methodology body", () => {
|
|
32
|
+
const skill = registry.getSkill("find-music-for-sync-brief");
|
|
33
|
+
expect(skill.body.length).toBeGreaterThan(100);
|
|
34
|
+
expect(skill.body).toContain("# find-music-for-sync-brief");
|
|
35
|
+
expect(skill.tools_required).toContain("directory_search_recordings");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("getSkill throws SkillNotFoundError for unknown name", () => {
|
|
39
|
+
expect(() => registry.getSkill("not-a-real-skill")).toThrow(
|
|
40
|
+
SkillNotFoundError,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("body contains no trade-secret path fragments (directory MCP is public)", () => {
|
|
45
|
+
const FORBIDDEN = [
|
|
46
|
+
"lib/services/chain",
|
|
47
|
+
"lib/services/enrichment",
|
|
48
|
+
"lib/services/pica-export",
|
|
49
|
+
"lib/services/global-identity",
|
|
50
|
+
"lib/services/identity-inheritance",
|
|
51
|
+
"lib/services/machine-payments",
|
|
52
|
+
"lib/services/credit-gating",
|
|
53
|
+
"lib/services/mlc-import",
|
|
54
|
+
"lib/integrations/",
|
|
55
|
+
];
|
|
56
|
+
for (const summary of registry.listSkills()) {
|
|
57
|
+
const skill = registry.getSkill(summary.name);
|
|
58
|
+
for (const fragment of FORBIDDEN) {
|
|
59
|
+
expect(skill.body).not.toContain(fragment);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory skills tools test suite — ADR-140 Phase 2b
|
|
5
|
+
*
|
|
6
|
+
* Covers directory_skill_list + directory_skill_get on the public directory
|
|
7
|
+
* MCP. Same meta-tool shape as the creator MCP, but tools_required names
|
|
8
|
+
* follow the `directory_*` prefix convention.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "@jest/globals";
|
|
12
|
+
import { DirectorySkillsTools } from "../../tools/skills.js";
|
|
13
|
+
|
|
14
|
+
describe("DirectorySkillsTools", () => {
|
|
15
|
+
const skillsTools = new DirectorySkillsTools();
|
|
16
|
+
const tools = skillsTools.getTools();
|
|
17
|
+
|
|
18
|
+
it("registers exactly 2 meta-tools", () => {
|
|
19
|
+
expect(tools.length).toBe(2);
|
|
20
|
+
expect(tools.map((t) => t.definition.name).sort()).toEqual([
|
|
21
|
+
"directory_skill_get",
|
|
22
|
+
"directory_skill_list",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("both tools are tier:read", () => {
|
|
27
|
+
for (const tool of tools) {
|
|
28
|
+
expect(tool.definition.tier).toBe("read");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("directory_skill_list returns the Stage 1 skill", async () => {
|
|
33
|
+
const list = tools.find(
|
|
34
|
+
(t) => t.definition.name === "directory_skill_list",
|
|
35
|
+
)!;
|
|
36
|
+
const result = await list.executor({});
|
|
37
|
+
expect(result.isError).toBeFalsy();
|
|
38
|
+
const summaries = result.structuredContent!.skills as Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
}>;
|
|
41
|
+
expect(summaries.map((s) => s.name)).toContain(
|
|
42
|
+
"find-music-for-sync-brief",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("directory_skill_get returns the methodology body", async () => {
|
|
47
|
+
const get = tools.find(
|
|
48
|
+
(t) => t.definition.name === "directory_skill_get",
|
|
49
|
+
)!;
|
|
50
|
+
const result = await get.executor({ name: "find-music-for-sync-brief" });
|
|
51
|
+
expect(result.isError).toBeFalsy();
|
|
52
|
+
const textBlock = result.content.find((c: any) => c.type === "text") as
|
|
53
|
+
| { type: "text"; text: string }
|
|
54
|
+
| undefined;
|
|
55
|
+
expect(textBlock?.text).toContain("# find-music-for-sync-brief");
|
|
56
|
+
expect(textBlock?.text).toContain("directory_search_recordings");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("directory_skill_get returns isError for unknown skill", async () => {
|
|
60
|
+
const get = tools.find(
|
|
61
|
+
(t) => t.definition.name === "directory_skill_get",
|
|
62
|
+
)!;
|
|
63
|
+
const result = await get.executor({ name: "not-a-real-skill" });
|
|
64
|
+
expect(result.isError).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
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
|
+
)!;
|
|
71
|
+
const result = await get.executor({});
|
|
72
|
+
expect(result.isError).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runtime CHANGELOG parser. Mirrors scripts/parse-changelog.ts but takes
|
|
5
|
+
* the markdown body as a string (no fs dependency) so the MCP runtime
|
|
6
|
+
* can read from a build-time embedded constant. Sister of
|
|
7
|
+
* mcp-server/src/lib/changelog.ts and team-mcp-server/src/lib/changelog.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ChangelogSectionName =
|
|
11
|
+
| "Added"
|
|
12
|
+
| "Changed"
|
|
13
|
+
| "Fixed"
|
|
14
|
+
| "Removed"
|
|
15
|
+
| "Security"
|
|
16
|
+
| "Deprecated";
|
|
17
|
+
|
|
18
|
+
export const CHANGELOG_SECTION_NAMES: ChangelogSectionName[] = [
|
|
19
|
+
"Added",
|
|
20
|
+
"Changed",
|
|
21
|
+
"Fixed",
|
|
22
|
+
"Removed",
|
|
23
|
+
"Security",
|
|
24
|
+
"Deprecated",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export interface ChangelogEntry {
|
|
28
|
+
version: string;
|
|
29
|
+
date: string | null;
|
|
30
|
+
sections: Partial<Record<ChangelogSectionName, string[]>>;
|
|
31
|
+
markdown: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const HEADING_RE = /^##\s+\[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?\s*$/;
|
|
35
|
+
const SECTION_RE = /^###\s+(\w+)\s*$/;
|
|
36
|
+
const BULLET_RE = /^-\s+(.*)$/;
|
|
37
|
+
|
|
38
|
+
export function parseChangelogText(text: string): ChangelogEntry[] {
|
|
39
|
+
const lines = text.split("\n");
|
|
40
|
+
|
|
41
|
+
const entries: ChangelogEntry[] = [];
|
|
42
|
+
let current: (ChangelogEntry & { _start: number }) | null = null;
|
|
43
|
+
let currentSection: ChangelogSectionName | null = null;
|
|
44
|
+
let currentBullet: {
|
|
45
|
+
entry: ChangelogEntry;
|
|
46
|
+
section: ChangelogSectionName;
|
|
47
|
+
index: number;
|
|
48
|
+
} | null = null;
|
|
49
|
+
|
|
50
|
+
const flush = (endLine: number) => {
|
|
51
|
+
if (current && current.version !== "Unreleased") {
|
|
52
|
+
current.markdown = lines
|
|
53
|
+
.slice(current._start, endLine)
|
|
54
|
+
.join("\n")
|
|
55
|
+
.trimEnd();
|
|
56
|
+
const { _start: _start, ...rest } = current;
|
|
57
|
+
entries.push(rest);
|
|
58
|
+
}
|
|
59
|
+
current = null;
|
|
60
|
+
currentSection = null;
|
|
61
|
+
currentBullet = null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
const headingMatch = line.match(HEADING_RE);
|
|
67
|
+
if (headingMatch) {
|
|
68
|
+
flush(i);
|
|
69
|
+
current = {
|
|
70
|
+
version: headingMatch[1],
|
|
71
|
+
date: headingMatch[2] ?? null,
|
|
72
|
+
sections: {},
|
|
73
|
+
markdown: "",
|
|
74
|
+
_start: i,
|
|
75
|
+
};
|
|
76
|
+
currentSection = null;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!current) continue;
|
|
81
|
+
|
|
82
|
+
const sectionMatch = line.match(SECTION_RE);
|
|
83
|
+
if (sectionMatch) {
|
|
84
|
+
const name = sectionMatch[1] as ChangelogSectionName;
|
|
85
|
+
if (CHANGELOG_SECTION_NAMES.includes(name)) {
|
|
86
|
+
currentSection = name;
|
|
87
|
+
if (!current.sections[name]) current.sections[name] = [];
|
|
88
|
+
} else {
|
|
89
|
+
currentSection = null;
|
|
90
|
+
}
|
|
91
|
+
currentBullet = null;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const bulletMatch = line.match(BULLET_RE);
|
|
96
|
+
if (bulletMatch && currentSection) {
|
|
97
|
+
const arr = current.sections[currentSection]!;
|
|
98
|
+
arr.push(bulletMatch[1].trim());
|
|
99
|
+
currentBullet = {
|
|
100
|
+
entry: current,
|
|
101
|
+
section: currentSection,
|
|
102
|
+
index: arr.length - 1,
|
|
103
|
+
};
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (currentBullet && line.startsWith(" ") && line.trim().length > 0) {
|
|
108
|
+
const arr = currentBullet.entry.sections[currentBullet.section]!;
|
|
109
|
+
arr[currentBullet.index] = `${arr[currentBullet.index]} ${line.trim()}`;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
currentBullet = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
flush(lines.length);
|
|
117
|
+
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
@@ -145,8 +145,7 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
|
|
|
145
145
|
"No work matched that title in our index. The work may not be " +
|
|
146
146
|
"registered with a society we mirror, or the title may be " +
|
|
147
147
|
"ambiguous — try a writer name or year of release to narrow.",
|
|
148
|
-
suggest_query_shape:
|
|
149
|
-
"title + writer | title + year | ISWC if known",
|
|
148
|
+
suggest_query_shape: "title + writer | title + year | ISWC if known",
|
|
150
149
|
},
|
|
151
150
|
},
|
|
152
151
|
{
|
|
@@ -230,8 +229,7 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
|
|
|
230
229
|
"No work matched that title. The song may not be in our index " +
|
|
231
230
|
"yet, or the title may be ambiguous (many works share the same " +
|
|
232
231
|
"title) — try a writer name or year of release.",
|
|
233
|
-
suggest_query_shape:
|
|
234
|
-
"title + writer | title + year | ISWC if known",
|
|
232
|
+
suggest_query_shape: "title + writer | title + year | ISWC if known",
|
|
235
233
|
},
|
|
236
234
|
},
|
|
237
235
|
{
|
|
@@ -242,8 +240,7 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
|
|
|
242
240
|
message:
|
|
243
241
|
"No work matched that title in our index. Try refining with the " +
|
|
244
242
|
"writer's name, year of release, or the ISWC if you have it.",
|
|
245
|
-
suggest_query_shape:
|
|
246
|
-
"title + writer | title + year | ISWC if known",
|
|
243
|
+
suggest_query_shape: "title + writer | title + year | ISWC if known",
|
|
247
244
|
},
|
|
248
245
|
},
|
|
249
246
|
{
|
|
@@ -510,8 +507,7 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
|
|
|
510
507
|
"No matches in our public directory across works or creators. " +
|
|
511
508
|
"Try narrowing to one entity type (work title or creator name), " +
|
|
512
509
|
"or supply an identifier if you have one.",
|
|
513
|
-
suggest_query_shape:
|
|
514
|
-
"title + writer | creator name + role | identifier",
|
|
510
|
+
suggest_query_shape: "title + writer | creator name + role | identifier",
|
|
515
511
|
},
|
|
516
512
|
},
|
|
517
513
|
|
|
@@ -533,8 +529,38 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
|
|
|
533
529
|
"unregistered, registered with a society we don't mirror, or the " +
|
|
534
530
|
"title is ambiguous. Try a writer name or year to narrow, or " +
|
|
535
531
|
"look up by ISWC if you have one.",
|
|
536
|
-
suggest_query_shape:
|
|
537
|
-
"title + writer | title + year | ISWC if known",
|
|
532
|
+
suggest_query_shape: "title + writer | title + year | ISWC if known",
|
|
538
533
|
},
|
|
539
534
|
},
|
|
535
|
+
{
|
|
536
|
+
// ADR-140 Phase 2b — skill discovery meta-questions.
|
|
537
|
+
// Routes to directory_skill_list so an external LLM connecting to the
|
|
538
|
+
// directory MCP via Anthropic's MCP Directory or ChatGPT Apps catalogue
|
|
539
|
+
// can ask "what can this server do?" and get a list of named
|
|
540
|
+
// methodologies (find-music-for-sync-brief, etc.) rather than having
|
|
541
|
+
// to read the full 9-tool surface description-by-description.
|
|
542
|
+
question: "what skills do you have?",
|
|
543
|
+
synonyms: [
|
|
544
|
+
"what can you do?",
|
|
545
|
+
"show me your methodologies",
|
|
546
|
+
"how do I find music here?",
|
|
547
|
+
"how do I use this directory?",
|
|
548
|
+
"what workflows are available?",
|
|
549
|
+
],
|
|
550
|
+
resolves_to: { kind: "tool", name: "directory_skill_list" },
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
// ─────────────────────────────────────────────────────────────────
|
|
554
|
+
// Directory-surface self-introspection — what shipped in this MCP
|
|
555
|
+
// ─────────────────────────────────────────────────────────────────
|
|
556
|
+
{
|
|
557
|
+
question: "what's new in the directory mcp",
|
|
558
|
+
synonyms: [
|
|
559
|
+
"what changed in the directory",
|
|
560
|
+
"directory release notes",
|
|
561
|
+
"what version is the directory on",
|
|
562
|
+
"directory changelog",
|
|
563
|
+
],
|
|
564
|
+
resolves_to: { kind: "tool", name: "directory_release_notes" },
|
|
565
|
+
},
|
|
540
566
|
];
|