@withpica/mcp-server 2.50.0 → 2.51.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 +16 -0
- package/dist/prompts/creator-question-atlas.d.ts.map +1 -1
- package/dist/prompts/creator-question-atlas.js +17 -0
- package/dist/prompts/creator-question-atlas.js.map +1 -1
- package/dist/resources/index.d.ts +10 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +134 -1
- package/dist/resources/index.js.map +1 -1
- package/dist/server-instructions.d.ts +4 -3
- package/dist/server-instructions.d.ts.map +1 -1
- package/dist/server-instructions.js +4 -1
- package/dist/server-instructions.js.map +1 -1
- package/dist/skills/index.d.ts +42 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +59 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skills.generated.d.ts +25 -0
- package/dist/skills/skills.generated.d.ts.map +1 -0
- package/dist/skills/skills.generated.js +86 -0
- package/dist/skills/skills.generated.js.map +1 -0
- package/dist/tools/discovery.d.ts.map +1 -1
- package/dist/tools/discovery.js +8 -0
- package/dist/tools/discovery.js.map +1 -1
- package/dist/tools/index.d.ts +43 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +87 -10
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/metadata.d.ts.map +1 -1
- package/dist/tools/metadata.js +13 -0
- package/dist/tools/metadata.js.map +1 -1
- package/dist/tools/skills.d.ts +25 -0
- package/dist/tools/skills.d.ts.map +1 -0
- package/dist/tools/skills.js +144 -0
- package/dist/tools/skills.js.map +1 -0
- package/package.json +6 -5
- package/scripts/build-skills.ts +229 -0
- package/server.json +2 -2
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ADR-140 Phase 2b — generator for `skills.generated.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Reads `src/skills/<name>/SKILL.md` files, parses YAML frontmatter, and emits
|
|
7
|
+
* a TypeScript constant consumed by SkillsRegistry. Generation at build time
|
|
8
|
+
* (not runtime FS reads) so npm-published `dist/` ships bundled bytes — `.md`
|
|
9
|
+
* files are excluded by `.npmignore`.
|
|
10
|
+
*
|
|
11
|
+
* Frontmatter shape:
|
|
12
|
+
* ---
|
|
13
|
+
* name: kebab-case-name
|
|
14
|
+
* description: one-line
|
|
15
|
+
* triggers:
|
|
16
|
+
* - phrase one
|
|
17
|
+
* - phrase two
|
|
18
|
+
* audience: manager / artist / etc
|
|
19
|
+
* tools_required:
|
|
20
|
+
* - tool_a
|
|
21
|
+
* - tool_b
|
|
22
|
+
* output: one-line description
|
|
23
|
+
* ---
|
|
24
|
+
*
|
|
25
|
+
* Hand-rolled YAML parser (frontmatter only — no nested objects, no anchors).
|
|
26
|
+
* Adding a `js-yaml` dep would be overkill for the constrained shape.
|
|
27
|
+
*
|
|
28
|
+
* Re-runs are deterministic: same .md files produce a byte-identical output.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = dirname(__filename);
|
|
37
|
+
const MCP_SERVER_ROOT = join(__dirname, "..");
|
|
38
|
+
const SKILLS_ROOT = join(MCP_SERVER_ROOT, "src/skills");
|
|
39
|
+
const OUTPUT_PATH = join(SKILLS_ROOT, "skills.generated.ts");
|
|
40
|
+
|
|
41
|
+
interface SkillFrontmatter {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
triggers: string[];
|
|
45
|
+
audience: string;
|
|
46
|
+
tools_required: string[];
|
|
47
|
+
output: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ParsedSkill extends SkillFrontmatter {
|
|
51
|
+
body: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseFrontmatter(src: string, sourceFile: string): ParsedSkill {
|
|
55
|
+
// Strip leading HTML comment (copyright header) if present, then expect ---
|
|
56
|
+
let i = 0;
|
|
57
|
+
if (src.startsWith("<!--")) {
|
|
58
|
+
const end = src.indexOf("-->");
|
|
59
|
+
if (end === -1) {
|
|
60
|
+
throw new Error(`${sourceFile}: unterminated HTML comment header`);
|
|
61
|
+
}
|
|
62
|
+
i = end + 3;
|
|
63
|
+
// Skip whitespace/newlines
|
|
64
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (src.slice(i, i + 3) !== "---") {
|
|
68
|
+
throw new Error(`${sourceFile}: missing --- frontmatter opener`);
|
|
69
|
+
}
|
|
70
|
+
i += 3;
|
|
71
|
+
// Skip newline after opener
|
|
72
|
+
while (i < src.length && src[i] !== "\n") i++;
|
|
73
|
+
i++;
|
|
74
|
+
|
|
75
|
+
const closeIdx = src.indexOf("\n---", i);
|
|
76
|
+
if (closeIdx === -1) {
|
|
77
|
+
throw new Error(`${sourceFile}: missing --- frontmatter closer`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const frontmatterText = src.slice(i, closeIdx);
|
|
81
|
+
const bodyStart = closeIdx + 4; // skip \n---
|
|
82
|
+
const body = src.slice(bodyStart).replace(/^\n+/, "").trimEnd() + "\n";
|
|
83
|
+
|
|
84
|
+
// Parse: key: value OR key: (list follows)
|
|
85
|
+
const fm: Record<string, string | string[]> = {};
|
|
86
|
+
const lines = frontmatterText.split("\n");
|
|
87
|
+
let currentListKey: string | null = null;
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (line.trim() === "") {
|
|
91
|
+
currentListKey = null;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const listItemMatch = line.match(/^\s+-\s+(.+)$/);
|
|
95
|
+
if (listItemMatch && currentListKey) {
|
|
96
|
+
(fm[currentListKey] as string[]).push(listItemMatch[1].trim());
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const kvMatch = line.match(/^([a-z_][a-z0-9_]*)\s*:\s*(.*)$/i);
|
|
100
|
+
if (!kvMatch) continue;
|
|
101
|
+
const key = kvMatch[1];
|
|
102
|
+
const value = kvMatch[2].trim();
|
|
103
|
+
if (value === "") {
|
|
104
|
+
// List follows
|
|
105
|
+
fm[key] = [];
|
|
106
|
+
currentListKey = key;
|
|
107
|
+
} else {
|
|
108
|
+
fm[key] = value;
|
|
109
|
+
currentListKey = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate required fields
|
|
114
|
+
const required = [
|
|
115
|
+
"name",
|
|
116
|
+
"description",
|
|
117
|
+
"triggers",
|
|
118
|
+
"audience",
|
|
119
|
+
"tools_required",
|
|
120
|
+
"output",
|
|
121
|
+
];
|
|
122
|
+
for (const field of required) {
|
|
123
|
+
if (!(field in fm)) {
|
|
124
|
+
throw new Error(`${sourceFile}: missing required frontmatter field "${field}"`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(fm.triggers) || fm.triggers.length === 0) {
|
|
128
|
+
throw new Error(`${sourceFile}: triggers must be a non-empty list`);
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(fm.tools_required) || fm.tools_required.length === 0) {
|
|
131
|
+
throw new Error(`${sourceFile}: tools_required must be a non-empty list`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
name: fm.name as string,
|
|
136
|
+
description: fm.description as string,
|
|
137
|
+
triggers: fm.triggers as string[],
|
|
138
|
+
audience: fm.audience as string,
|
|
139
|
+
tools_required: fm.tools_required as string[],
|
|
140
|
+
output: fm.output as string,
|
|
141
|
+
body,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function discoverSkills(): ParsedSkill[] {
|
|
146
|
+
if (!existsSync(SKILLS_ROOT)) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const entries = readdirSync(SKILLS_ROOT, { withFileTypes: true });
|
|
150
|
+
const skills: ParsedSkill[] = [];
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isDirectory()) continue;
|
|
153
|
+
const skillFile = join(SKILLS_ROOT, entry.name, "SKILL.md");
|
|
154
|
+
if (!existsSync(skillFile)) continue;
|
|
155
|
+
const src = readFileSync(skillFile, "utf-8");
|
|
156
|
+
const parsed = parseFrontmatter(src, `src/skills/${entry.name}/SKILL.md`);
|
|
157
|
+
if (parsed.name !== entry.name) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`src/skills/${entry.name}/SKILL.md: frontmatter name "${parsed.name}" must match directory name "${entry.name}"`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
skills.push(parsed);
|
|
163
|
+
}
|
|
164
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
165
|
+
return skills;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function emit(skills: ParsedSkill[]): string {
|
|
169
|
+
const header = `// Copyright (c) 2024-2026 Withpica Ltd. All rights reserved.
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* GENERATED FILE — DO NOT EDIT BY HAND.
|
|
173
|
+
*
|
|
174
|
+
* Regenerated by \`scripts/build-skills.ts\` from \`src/skills/<name>/SKILL.md\`.
|
|
175
|
+
* Runs in \`prebuild\` and \`prepublishOnly\`. See ADR-140 Phase 2b.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
export interface Skill {
|
|
179
|
+
/** Stable kebab-case identifier matching the directory name. */
|
|
180
|
+
name: string;
|
|
181
|
+
/** One-line description shown in pica_skill_list. */
|
|
182
|
+
description: string;
|
|
183
|
+
/** Trigger phrases an agent should recognise as invoking this skill. */
|
|
184
|
+
triggers: string[];
|
|
185
|
+
/** Audience tag (manager / artist / sync supervisor / etc). */
|
|
186
|
+
audience: string;
|
|
187
|
+
/** Tools the skill body chains. Lint-checkable. */
|
|
188
|
+
tools_required: string[];
|
|
189
|
+
/** One-line output shape description. */
|
|
190
|
+
output: string;
|
|
191
|
+
/** Full skill methodology body — markdown, exposed by pica_skill_get. */
|
|
192
|
+
body: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
`;
|
|
196
|
+
const lines: string[] = [header, "export const SKILLS: Record<string, Skill> = {"];
|
|
197
|
+
for (const skill of skills) {
|
|
198
|
+
lines.push(` ${JSON.stringify(skill.name)}: {`);
|
|
199
|
+
lines.push(` name: ${JSON.stringify(skill.name)},`);
|
|
200
|
+
lines.push(` description: ${JSON.stringify(skill.description)},`);
|
|
201
|
+
lines.push(` triggers: ${JSON.stringify(skill.triggers)},`);
|
|
202
|
+
lines.push(` audience: ${JSON.stringify(skill.audience)},`);
|
|
203
|
+
lines.push(` tools_required: ${JSON.stringify(skill.tools_required)},`);
|
|
204
|
+
lines.push(` output: ${JSON.stringify(skill.output)},`);
|
|
205
|
+
lines.push(` body: ${JSON.stringify(skill.body)},`);
|
|
206
|
+
lines.push(` },`);
|
|
207
|
+
}
|
|
208
|
+
lines.push("};");
|
|
209
|
+
lines.push("");
|
|
210
|
+
lines.push(
|
|
211
|
+
`export const SKILL_NAMES: ReadonlyArray<string> = ${JSON.stringify(skills.map((s) => s.name))};`,
|
|
212
|
+
);
|
|
213
|
+
lines.push("");
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function main(): void {
|
|
218
|
+
const skills = discoverSkills();
|
|
219
|
+
const output = emit(skills);
|
|
220
|
+
writeFileSync(OUTPUT_PATH, output, "utf-8");
|
|
221
|
+
console.log(
|
|
222
|
+
`[build-skills] Wrote ${skills.length} skill(s) to src/skills/skills.generated.ts`,
|
|
223
|
+
);
|
|
224
|
+
for (const skill of skills) {
|
|
225
|
+
console.log(` - ${skill.name} (${skill.body.length} bytes body)`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main();
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/withpica/pica",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.51.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@withpica/mcp-server",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.51.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|