@withpica/mcp-server-directory 1.3.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/client.d.ts +12 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +93 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/config.d.ts +9 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +15 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +28 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/lib/changelog.d.ts +16 -0
  15. package/dist/lib/changelog.d.ts.map +1 -0
  16. package/dist/lib/changelog.generated.d.ts +4 -0
  17. package/dist/lib/changelog.generated.d.ts.map +1 -0
  18. package/dist/lib/changelog.generated.js +6 -0
  19. package/dist/lib/changelog.generated.js.map +1 -0
  20. package/dist/lib/changelog.js +84 -0
  21. package/dist/lib/changelog.js.map +1 -0
  22. package/dist/prompts/index.d.ts +32 -0
  23. package/dist/prompts/index.d.ts.map +1 -0
  24. package/dist/prompts/index.js +165 -0
  25. package/dist/prompts/index.js.map +1 -0
  26. package/dist/prompts/public-question-atlas.d.ts +121 -0
  27. package/dist/prompts/public-question-atlas.d.ts.map +1 -0
  28. package/dist/prompts/public-question-atlas.js +434 -0
  29. package/dist/prompts/public-question-atlas.js.map +1 -0
  30. package/dist/resources/llms-primer.d.ts +2 -0
  31. package/dist/resources/llms-primer.d.ts.map +1 -0
  32. package/dist/resources/llms-primer.js +35 -0
  33. package/dist/resources/llms-primer.js.map +1 -0
  34. package/dist/server.d.ts +33 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +223 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/skills/index.d.ts +32 -0
  39. package/dist/skills/index.d.ts.map +1 -0
  40. package/dist/skills/index.js +50 -0
  41. package/dist/skills/index.js.map +1 -0
  42. package/dist/skills/skills.generated.d.ts +18 -0
  43. package/dist/skills/skills.generated.d.ts.map +1 -0
  44. package/dist/skills/skills.generated.js +14 -0
  45. package/dist/skills/skills.generated.js.map +1 -0
  46. package/dist/tools/__tests__/release-notes.test.d.ts +2 -0
  47. package/dist/tools/__tests__/release-notes.test.d.ts.map +1 -0
  48. package/dist/tools/__tests__/release-notes.test.js +49 -0
  49. package/dist/tools/__tests__/release-notes.test.js.map +1 -0
  50. package/dist/tools/chain.d.ts +12 -0
  51. package/dist/tools/chain.d.ts.map +1 -0
  52. package/dist/tools/chain.js +109 -0
  53. package/dist/tools/chain.js.map +1 -0
  54. package/dist/tools/index.d.ts +36 -0
  55. package/dist/tools/index.d.ts.map +1 -0
  56. package/dist/tools/index.js +49 -0
  57. package/dist/tools/index.js.map +1 -0
  58. package/dist/tools/people.d.ts +13 -0
  59. package/dist/tools/people.d.ts.map +1 -0
  60. package/dist/tools/people.js +177 -0
  61. package/dist/tools/people.js.map +1 -0
  62. package/dist/tools/recordings.d.ts +12 -0
  63. package/dist/tools/recordings.d.ts.map +1 -0
  64. package/dist/tools/recordings.js +145 -0
  65. package/dist/tools/recordings.js.map +1 -0
  66. package/dist/tools/release-notes.d.ts +22 -0
  67. package/dist/tools/release-notes.d.ts.map +1 -0
  68. package/dist/tools/release-notes.js +102 -0
  69. package/dist/tools/release-notes.js.map +1 -0
  70. package/dist/tools/search.d.ts +12 -0
  71. package/dist/tools/search.d.ts.map +1 -0
  72. package/dist/tools/search.js +56 -0
  73. package/dist/tools/search.js.map +1 -0
  74. package/dist/tools/skills.d.ts +19 -0
  75. package/dist/tools/skills.d.ts.map +1 -0
  76. package/dist/tools/skills.js +127 -0
  77. package/dist/tools/skills.js.map +1 -0
  78. package/dist/tools/works.d.ts +14 -0
  79. package/dist/tools/works.d.ts.map +1 -0
  80. package/dist/tools/works.js +248 -0
  81. package/dist/tools/works.js.map +1 -0
  82. package/dist/utils/errors.d.ts +14 -0
  83. package/dist/utils/errors.d.ts.map +1 -0
  84. package/dist/utils/errors.js +48 -0
  85. package/dist/utils/errors.js.map +1 -0
  86. package/dist/utils/formatting.d.ts +10 -0
  87. package/dist/utils/formatting.d.ts.map +1 -0
  88. package/dist/utils/formatting.js +16 -0
  89. package/dist/utils/formatting.js.map +1 -0
  90. package/package.json +4 -3
  91. package/scripts/build-changelog.ts +25 -0
  92. package/src/lib/changelog.ts +119 -0
  93. package/src/prompts/public-question-atlas.ts +19 -10
  94. package/src/server.ts +19 -0
  95. package/src/tools/__tests__/release-notes.test.ts +62 -0
  96. package/src/tools/index.ts +2 -0
  97. package/src/tools/release-notes.ts +148 -0
@@ -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,7 @@ 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
  },
540
535
  {
@@ -554,4 +549,18 @@ export const PUBLIC_ATLAS: AtlasEntry[] = [
554
549
  ],
555
550
  resolves_to: { kind: "tool", name: "directory_skill_list" },
556
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
+ },
557
566
  ];
package/src/server.ts CHANGED
@@ -16,6 +16,7 @@ import { ToolRegistry } from "./tools/index.js";
16
16
  import { PromptRegistry } from "./prompts/index.js";
17
17
  import { logError } from "./utils/errors.js";
18
18
  import { DIRECTORY_PRIMER } from "./resources/llms-primer.js";
19
+ import { CHANGELOG_MD } from "./lib/changelog.generated.js";
19
20
  import { SkillsRegistry } from "./skills/index.js";
20
21
 
21
22
  export class DirectoryMcpServer {
@@ -88,6 +89,13 @@ export class DirectoryMcpServer {
88
89
  "Enumeration of downloadable directory skill methodologies (SEP-2640 bridge). Each entry maps a kebab-case name to its skill:// URI, description, and trigger phrases.",
89
90
  mimeType: "application/json",
90
91
  },
92
+ {
93
+ uri: "release-notes://withpica/mcp-server-directory/latest",
94
+ name: "Release Notes — @withpica/mcp-server-directory",
95
+ description:
96
+ "Full CHANGELOG.md for the PICA directory MCP server. For just the last few releases, agents can call directory_release_notes.",
97
+ mimeType: "text/markdown",
98
+ },
91
99
  ];
92
100
  for (const summary of this.skillsRegistry.listSkills()) {
93
101
  resources.push({
@@ -115,6 +123,17 @@ export class DirectoryMcpServer {
115
123
  ],
116
124
  };
117
125
  }
126
+ if (uri === "release-notes://withpica/mcp-server-directory/latest") {
127
+ return {
128
+ contents: [
129
+ {
130
+ uri,
131
+ mimeType: "text/markdown",
132
+ text: CHANGELOG_MD,
133
+ },
134
+ ],
135
+ };
136
+ }
118
137
  if (uri.startsWith("skill://withpica/")) {
119
138
  return this.readSkillResource(uri);
120
139
  }
@@ -0,0 +1,62 @@
1
+ // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
+
3
+ import { DirectoryReleaseNotesTools } from "../release-notes.js";
4
+ import type { DirectoryClient } from "../../client.js";
5
+
6
+ describe("DirectoryReleaseNotesTools.directory_release_notes", () => {
7
+ const client = {} as DirectoryClient;
8
+ const tools = new DirectoryReleaseNotesTools(client).getTools();
9
+ const tool = tools.find(
10
+ (t: { definition: { name: string } }) =>
11
+ t.definition.name === "directory_release_notes",
12
+ )!;
13
+
14
+ it("registers as a single tool named directory_release_notes", () => {
15
+ expect(tools).toHaveLength(1);
16
+ expect(tool).toBeDefined();
17
+ });
18
+
19
+ it("returns the last 3 releases by default", async () => {
20
+ const result = await tool.executor({});
21
+ const data = JSON.parse(result.content[0].text);
22
+ expect(data.package).toBe("@withpica/mcp-server-directory");
23
+ expect(data.current_version).toMatch(/^\d+\.\d+\.\d+$/);
24
+ expect(Array.isArray(data.releases)).toBe(true);
25
+ expect(data.releases.length).toBeLessThanOrEqual(3);
26
+ expect(data.releases.length).toBeGreaterThan(0);
27
+ expect(data.resource_uri).toBe(
28
+ "release-notes://withpica/mcp-server-directory/latest",
29
+ );
30
+ });
31
+
32
+ it("respects since_version (strictly greater)", async () => {
33
+ const result = await tool.executor({ since_version: "0.0.1" });
34
+ const data = JSON.parse(result.content[0].text);
35
+ expect(data.releases.length).toBeGreaterThan(0);
36
+ for (const r of data.releases) {
37
+ expect(r.version).not.toBe("0.0.1");
38
+ }
39
+ });
40
+
41
+ it("filters to specified sections", async () => {
42
+ const result = await tool.executor({ sections: ["Added"] });
43
+ const data = JSON.parse(result.content[0].text);
44
+ for (const r of data.releases) {
45
+ for (const k of Object.keys(r.sections)) {
46
+ expect(k).toBe("Added");
47
+ }
48
+ }
49
+ });
50
+
51
+ it("returns empty releases array when since_version exceeds current_version", async () => {
52
+ const result = await tool.executor({ since_version: "99.0.0" });
53
+ const data = JSON.parse(result.content[0].text);
54
+ expect(data.releases).toEqual([]);
55
+ });
56
+
57
+ it("clamps limit to max 20", async () => {
58
+ const result = await tool.executor({ limit: 9999 });
59
+ const data = JSON.parse(result.content[0].text);
60
+ expect(data.releases.length).toBeLessThanOrEqual(20);
61
+ });
62
+ });
@@ -7,6 +7,7 @@ import { DirectorySearchTools } from "./search.js";
7
7
  import { DirectoryRecordingsTools } from "./recordings.js";
8
8
  import { DirectoryChainTools } from "./chain.js";
9
9
  import { DirectorySkillsTools } from "./skills.js";
10
+ import { DirectoryReleaseNotesTools } from "./release-notes.js";
10
11
  import { formatError, logError, ToolExecutionError } from "../utils/errors.js";
11
12
 
12
13
  /**
@@ -54,6 +55,7 @@ export class ToolRegistry {
54
55
  new DirectoryRecordingsTools(client),
55
56
  new DirectoryChainTools(client),
56
57
  new DirectorySkillsTools(),
58
+ new DirectoryReleaseNotesTools(client),
57
59
  ];
58
60
 
59
61
  for (const group of groups) {
@@ -0,0 +1,148 @@
1
+ // Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
2
+
3
+ /* eslint-disable @typescript-eslint/no-explicit-any -- ToolExecutor signature uses Record<string, any> */
4
+
5
+ /**
6
+ * directory_release_notes — what shipped in the PICA directory MCP.
7
+ *
8
+ * Reads CHANGELOG.md embedded at build time via scripts/build-changelog.ts.
9
+ * Sister of pica_release_notes (creator MCP) and team_release_notes (team MCP).
10
+ *
11
+ * Public-facing MCP — no trade-secret content in the description (per
12
+ * .claude/rules/ip-protection.md). The CHANGELOG body is the same file
13
+ * shipped in the npm tarball, already vetted as public.
14
+ */
15
+
16
+ import { DirectoryClient } from "../client.js";
17
+ import { ToolDefinition, ToolExecutor, ToolResult } from "./index.js";
18
+ import {
19
+ parseChangelogText,
20
+ type ChangelogSectionName,
21
+ CHANGELOG_SECTION_NAMES,
22
+ } from "../lib/changelog.js";
23
+ import {
24
+ CHANGELOG_MD,
25
+ PACKAGE_NAME,
26
+ PACKAGE_VERSION,
27
+ } from "../lib/changelog.generated.js";
28
+
29
+ const RESOURCE_URI = "release-notes://withpica/mcp-server-directory/latest";
30
+ const DEFAULT_LIMIT = 3;
31
+ const MAX_LIMIT = 20;
32
+
33
+ function compareSemver(a: string, b: string): number {
34
+ const parse = (v: string) => v.split(".").map((n) => parseInt(n, 10) || 0);
35
+ const [a1, a2, a3] = parse(a);
36
+ const [b1, b2, b3] = parse(b);
37
+ if (a1 !== b1) return a1 - b1;
38
+ if (a2 !== b2) return a2 - b2;
39
+ return a3 - b3;
40
+ }
41
+
42
+ export class DirectoryReleaseNotesTools {
43
+ constructor(private client: DirectoryClient) {}
44
+
45
+ getTools(): Array<{ definition: ToolDefinition; executor: ToolExecutor }> {
46
+ return [
47
+ {
48
+ definition: {
49
+ name: "directory_release_notes",
50
+ tier: "read",
51
+ description:
52
+ "Use when the user asks: 'what's new in the directory mcp', " +
53
+ "'what changed in the directory', 'directory release notes', " +
54
+ "'what version is the directory on'. " +
55
+ "Returns recent releases of the PICA directory MCP server — " +
56
+ "what shipped in the public catalogue surface itself. " +
57
+ "NOT for catalogue content searches (use directory_search / " +
58
+ "directory_list_works / directory_chain for those). " +
59
+ "Returns { package, current_version, releases: [{ version, date, sections, markdown }], resource_uri }. " +
60
+ "Defaults to the last 3 releases; use `since_version` to bound the output " +
61
+ "or `sections` to filter to Added/Fixed/etc.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ since_version: {
66
+ type: "string",
67
+ description:
68
+ "If set, returns only releases strictly greater than this version " +
69
+ "(semver-compared). Versions equal to since_version are excluded.",
70
+ },
71
+ limit: {
72
+ type: "number",
73
+ description: `Maximum number of releases to return. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`,
74
+ },
75
+ sections: {
76
+ type: "array",
77
+ items: { type: "string", enum: [...CHANGELOG_SECTION_NAMES] },
78
+ description:
79
+ "Filter each release's sections to only these names " +
80
+ "(e.g. ['Added', 'Fixed']). Sections not in the list are omitted from output.",
81
+ },
82
+ },
83
+ },
84
+ },
85
+ executor: this.releaseNotes.bind(this),
86
+ },
87
+ ];
88
+ }
89
+
90
+ private releaseNotes: ToolExecutor = async (
91
+ args: Record<string, any>,
92
+ ): Promise<ToolResult> => {
93
+ const sinceVersion =
94
+ typeof args.since_version === "string" ? args.since_version : null;
95
+ const rawLimit =
96
+ typeof args.limit === "number" ? args.limit : DEFAULT_LIMIT;
97
+ const limit = Math.max(1, Math.min(MAX_LIMIT, Math.floor(rawLimit)));
98
+ const sectionFilter: ChangelogSectionName[] | null = Array.isArray(
99
+ args.sections,
100
+ )
101
+ ? args.sections.filter((s: unknown): s is ChangelogSectionName =>
102
+ CHANGELOG_SECTION_NAMES.includes(s as ChangelogSectionName),
103
+ )
104
+ : null;
105
+
106
+ const all = parseChangelogText(CHANGELOG_MD);
107
+
108
+ let filtered = all;
109
+ if (sinceVersion) {
110
+ filtered = all.filter((e) => compareSemver(e.version, sinceVersion) > 0);
111
+ }
112
+ filtered = filtered.slice(0, limit);
113
+
114
+ const releases = filtered.map((e) => {
115
+ const sections = sectionFilter
116
+ ? Object.fromEntries(
117
+ Object.entries(e.sections).filter(([k]) =>
118
+ sectionFilter.includes(k as ChangelogSectionName),
119
+ ),
120
+ )
121
+ : e.sections;
122
+ return {
123
+ version: e.version,
124
+ date: e.date,
125
+ sections,
126
+ markdown: e.markdown,
127
+ };
128
+ });
129
+
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: JSON.stringify(
135
+ {
136
+ package: PACKAGE_NAME,
137
+ current_version: PACKAGE_VERSION,
138
+ releases,
139
+ resource_uri: RESOURCE_URI,
140
+ },
141
+ null,
142
+ 2,
143
+ ),
144
+ },
145
+ ],
146
+ };
147
+ };
148
+ }