@kaddo/mcp 3.20.0 → 3.21.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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +164 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,8 @@ directory set via the `KADDO_PROJECT_DIR` environment variable).
49
49
  | `kaddo://roadmap` | `knowledge/delivery/roadmap.md` | delivery roadmap |
50
50
  | `kaddo://capsules` | `.kaddo/external.yml` + `external/` | external Knowledge Capsules |
51
51
  | `kaddo://agents` | `knowledge/agents/` | installed agent prompts |
52
- | `kaddo://skills` | `knowledge/skills/` | installed skills (empty if none) |
52
+ | `kaddo://skills` | `knowledge/skills/` | installed reusable skills (empty if none) |
53
+ | `kaddo://skills/<id>` | `knowledge/skills/<id>/skill.md` | one reusable skill |
53
54
 
54
55
  ## Tools (read-only)
55
56
 
@@ -58,6 +59,7 @@ directory set via the `KADDO_PROJECT_DIR` environment variable).
58
59
  - `kaddo_get_work_item` — a Work Item by `id` (summary + full markdown).
59
60
  - `kaddo_list_capsules` / `kaddo_get_capsule` — external Knowledge Capsules.
60
61
  - `kaddo_list_agents` / `kaddo_get_agent_prompt` — installed agent prompts.
62
+ - `kaddo_list_skills` / `kaddo_get_skill` — installed reusable skills.
61
63
  - `kaddo_list_graph_hints` — graph hints, filter by `artifact_type` / `severity` / `active_only`.
62
64
 
63
65
  ## Derived tools (write only under `.kaddo/`)
package/dist/index.js CHANGED
@@ -207,8 +207,50 @@ function getAgentPrompt(root, name) {
207
207
  return { ...info, content };
208
208
  }
209
209
 
210
- // src/workitems.ts
210
+ // src/skills.ts
211
211
  import matter2 from "gray-matter";
212
+ function firstHeadingOrLine2(md) {
213
+ for (const line of md.split(/\r?\n/)) {
214
+ const t = line.trim();
215
+ if (!t || t.startsWith("#") || t.startsWith("---")) continue;
216
+ return t.replace(/\s+/g, " ").slice(0, 200);
217
+ }
218
+ return "";
219
+ }
220
+ function listSkills(root) {
221
+ const files = listFiles(root, "knowledge/skills", "skill.md");
222
+ const out = [];
223
+ for (const rel of files) {
224
+ const raw = readText(root, rel);
225
+ if (raw === null) continue;
226
+ let data = {};
227
+ try {
228
+ data = matter2(raw).data;
229
+ } catch {
230
+ }
231
+ if (data.type && String(data.type) !== "skill") continue;
232
+ const folder = rel.split("/").slice(-2)[0];
233
+ out.push({
234
+ id: String(data.id ?? folder),
235
+ title: String(data.title ?? folder),
236
+ group: String(data.group ?? "unknown"),
237
+ applies_to: Array.isArray(data.applies_to) ? data.applies_to.map(String).filter(Boolean) : [],
238
+ path: rel
239
+ });
240
+ }
241
+ return out.sort((a, b) => a.id.localeCompare(b.id));
242
+ }
243
+ function getSkill(root, id) {
244
+ const want = id.replace(/\.md$/, "");
245
+ const info = listSkills(root).find((s) => s.id === want);
246
+ if (!info) return null;
247
+ const content = readText(root, info.path);
248
+ if (content === null) return null;
249
+ return { ...info, content, description: firstHeadingOrLine2(content) || info.title };
250
+ }
251
+
252
+ // src/workitems.ts
253
+ import matter3 from "gray-matter";
212
254
  var ACTIVE_STATES = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
213
255
  function strArray(v) {
214
256
  return Array.isArray(v) ? v.map((x) => String(x)).filter(Boolean) : [];
@@ -235,7 +277,7 @@ function listWorkItems(root) {
235
277
  if (raw === null) continue;
236
278
  let parsed;
237
279
  try {
238
- parsed = matter2(raw);
280
+ parsed = matter3(raw);
239
281
  } catch {
240
282
  continue;
241
283
  }
@@ -396,18 +438,15 @@ var RESOURCES = [
396
438
  {
397
439
  uri: "kaddo://skills",
398
440
  name: "Kaddo skills",
399
- description: "Installed skills from knowledge/skills/ (empty if none).",
441
+ description: "Installed reusable skills from knowledge/skills/ (empty if none).",
400
442
  mimeType: "application/json",
401
- read: (root) => {
402
- const files = listFiles(root, "knowledge/skills", ".md");
403
- return [
404
- {
405
- uri: "kaddo://skills",
406
- text: JSON.stringify({ skills: files }, null, 2),
407
- mimeType: "application/json"
408
- }
409
- ];
410
- }
443
+ read: (root) => [
444
+ {
445
+ uri: "kaddo://skills",
446
+ text: JSON.stringify({ skills: listSkills(root) }, null, 2),
447
+ mimeType: "application/json"
448
+ }
449
+ ]
411
450
  }
412
451
  ];
413
452
 
@@ -499,6 +538,14 @@ function getAgentPromptTool(root, name) {
499
538
  if (!agent) return fail(`Agent "${name}" is not installed. Run \`kaddo add agents\` first.`);
500
539
  return ok(agent);
501
540
  }
541
+ function listSkillsTool(root) {
542
+ return ok(listSkills(root));
543
+ }
544
+ function getSkillTool(root, id) {
545
+ const skill = getSkill(root, id);
546
+ if (!skill) return fail(`Skill "${id}" is not installed. Run \`kaddo add skills\` first.`);
547
+ return ok(skill);
548
+ }
502
549
  function listGraphHints(root, filter = {}) {
503
550
  const report = readJson(root, ".kaddo/graph-hints.json");
504
551
  if (!report) return fail("Graph hints not found. Run `kaddo graph export` first.");
@@ -528,6 +575,9 @@ function readDir(dirPath) {
528
575
  function isFile(p) {
529
576
  return exists(p) && fs2.statSync(p).isFile();
530
577
  }
578
+ function isDir(p) {
579
+ return exists(p) && fs2.statSync(p).isDirectory();
580
+ }
531
581
  function join(...parts) {
532
582
  return path2.join(...parts);
533
583
  }
@@ -619,13 +669,13 @@ ${issues}`);
619
669
  }
620
670
 
621
671
  // ../cli/src/core/context-pack.ts
622
- import matter5 from "gray-matter";
672
+ import matter7 from "gray-matter";
623
673
 
624
674
  // ../cli/src/services/artifact-reader.ts
625
- import matter3 from "gray-matter";
675
+ import matter4 from "gray-matter";
626
676
  function parseArtifact(filePath, raw) {
627
677
  try {
628
- const { data } = matter3(raw);
678
+ const { data } = matter4(raw);
629
679
  const globs = Array.isArray(data.code) ? data.code.filter(Boolean) : [];
630
680
  return {
631
681
  filePath,
@@ -1283,7 +1333,7 @@ function assessPhase(input) {
1283
1333
  }
1284
1334
 
1285
1335
  // ../cli/src/core/capsule.ts
1286
- import matter4 from "gray-matter";
1336
+ import matter5 from "gray-matter";
1287
1337
  import { parse as parseYaml5, stringify as stringifyYaml } from "yaml";
1288
1338
 
1289
1339
  // ../cli/src/services/owners.ts
@@ -1436,7 +1486,7 @@ function sectionParagraph2(md, title) {
1436
1486
  return "";
1437
1487
  }
1438
1488
  function parseCapsule(id, path3, md) {
1439
- const { data } = matter4(md);
1489
+ const { data } = matter5(md);
1440
1490
  const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
1441
1491
  let ageDays = null;
1442
1492
  if (updatedAt) {
@@ -1877,6 +1927,38 @@ function loadGraphHints(dir) {
1877
1927
  }
1878
1928
  }
1879
1929
 
1930
+ // ../cli/src/services/installed-skills.ts
1931
+ import matter6 from "gray-matter";
1932
+ var SKILLS_DIR = join("knowledge", "skills");
1933
+ function discoverInstalledSkills(dir) {
1934
+ const base = join(dir, SKILLS_DIR);
1935
+ if (!exists(base) || !isDir(base)) return [];
1936
+ const out = [];
1937
+ for (const entry of readDir(base)) {
1938
+ const skillFile = join(base, entry, "skill.md");
1939
+ if (!isFile(skillFile)) continue;
1940
+ try {
1941
+ const { data } = matter6(readFile(skillFile));
1942
+ if (data.type && String(data.type) !== "skill") continue;
1943
+ const id = String(data.id ?? entry);
1944
+ out.push({
1945
+ id,
1946
+ title: String(data.title ?? id),
1947
+ group: String(data.group ?? "unknown"),
1948
+ appliesTo: Array.isArray(data.applies_to) ? data.applies_to.map(String).filter(Boolean) : [],
1949
+ relPath: `${SKILLS_DIR.replace(/\\/g, "/")}/${entry}/skill.md`
1950
+ });
1951
+ } catch {
1952
+ }
1953
+ }
1954
+ return out.sort((a, b) => a.id.localeCompare(b.id));
1955
+ }
1956
+ function skillGroupCounts(skills) {
1957
+ const counts = {};
1958
+ for (const s of skills) counts[s.group] = (counts[s.group] ?? 0) + 1;
1959
+ return counts;
1960
+ }
1961
+
1880
1962
  // ../cli/src/core/context-pack.ts
1881
1963
  var CONTEXT_PACK_VERSION = "1";
1882
1964
  var ARCH_DIR2 = "knowledge";
@@ -1890,7 +1972,7 @@ function readScanJson(dir) {
1890
1972
  }
1891
1973
  }
1892
1974
  function firstParagraph3(markdown) {
1893
- const body = matter5(markdown).content.trim();
1975
+ const body = matter7(markdown).content.trim();
1894
1976
  const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
1895
1977
  return para ?? "";
1896
1978
  }
@@ -2063,6 +2145,7 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
2063
2145
  external: loadExternalCapsules(dir),
2064
2146
  graph: loadGraphSummary(dir),
2065
2147
  graphHints: loadGraphHints(dir),
2148
+ skills: discoverInstalledSkills(dir).map((s) => s.id),
2066
2149
  mappedModules,
2067
2150
  missing,
2068
2151
  // VS-052: the handoff is driven by the REAL phase, not project.state, so the pack never
@@ -2256,6 +2339,12 @@ function renderContextPack(pack) {
2256
2339
  `);
2257
2340
  }
2258
2341
  }
2342
+ if (pack.skills.length > 0) {
2343
+ parts.push("## Skills\n");
2344
+ parts.push("Available reusable skills (agents apply these; content is not inlined):\n");
2345
+ parts.push(pack.skills.map((s) => `- ${s}`).join("\n") + "\n");
2346
+ parts.push("Read full skill definitions in `knowledge/skills/` or via the Kaddo MCP server (`kaddo://skills`).\n");
2347
+ }
2259
2348
  parts.push("## Missing Context\n");
2260
2349
  if (missing.length > 0) {
2261
2350
  parts.push(missing.map((m) => `- ${m}`).join("\n") + "\n");
@@ -2475,6 +2564,10 @@ function buildProjectExplanation(dir) {
2475
2564
  externalCapsules: loadExternalCapsules(dir),
2476
2565
  graph: loadGraphSummary(dir),
2477
2566
  graphHints: loadGraphHints(dir),
2567
+ skills: (() => {
2568
+ const installed = discoverInstalledSkills(dir);
2569
+ return { total: installed.length, byGroup: skillGroupCounts(installed) };
2570
+ })(),
2478
2571
  layers,
2479
2572
  roadmap,
2480
2573
  mappedModules,
@@ -2625,6 +2718,15 @@ function renderExplanationHuman(exp) {
2625
2718
  if (exp.graph.generatedAt) lines.push(`- Last exported: ${exp.graph.generatedAt}`);
2626
2719
  lines.push("");
2627
2720
  }
2721
+ if (exp.skills.total > 0) {
2722
+ lines.push(`## Skills installed: ${exp.skills.total}`);
2723
+ const groups = Object.entries(exp.skills.byGroup).sort((a, b) => a[0].localeCompare(b[0]));
2724
+ if (groups.length > 0) {
2725
+ lines.push("Groups:");
2726
+ for (const [g, n] of groups) lines.push(`- ${g}: ${n}`);
2727
+ }
2728
+ lines.push("");
2729
+ }
2628
2730
  if (exp.missingKnowledge.length > 0) {
2629
2731
  lines.push("## Missing Knowledge");
2630
2732
  for (const m of exp.missingKnowledge) lines.push(`- ${m}`);
@@ -3021,6 +3123,38 @@ function createServer(root) {
3021
3123
  },
3022
3124
  async (args) => toolText(guarded(root, () => listGraphHints(root, args)))
3023
3125
  );
3126
+ server.registerTool(
3127
+ "kaddo_list_skills",
3128
+ { title: "List skills", description: "List installed reusable skills.", inputSchema: {} },
3129
+ async () => toolText(guarded(root, () => listSkillsTool(root)))
3130
+ );
3131
+ server.registerTool(
3132
+ "kaddo_get_skill",
3133
+ { title: "Get skill", description: "Get an installed reusable skill by id.", inputSchema: { id: z2.string() } },
3134
+ async (args) => toolText(guarded(root, () => getSkillTool(root, args.id)))
3135
+ );
3136
+ let installedSkills = [];
3137
+ try {
3138
+ assertKaddoProject(root);
3139
+ installedSkills = listSkills(root);
3140
+ } catch {
3141
+ installedSkills = [];
3142
+ }
3143
+ for (const s of installedSkills) {
3144
+ server.registerResource(
3145
+ `skill: ${s.id}`,
3146
+ `kaddo://skills/${s.id}`,
3147
+ { title: s.title, description: `Reusable skill "${s.id}" (${s.group}).`, mimeType: "text/markdown" },
3148
+ async (uri) => {
3149
+ const full = getSkill(root, s.id);
3150
+ return {
3151
+ contents: [
3152
+ { uri: uri.href, text: full?.content ?? `Skill "${s.id}" not found.`, mimeType: "text/markdown" }
3153
+ ]
3154
+ };
3155
+ }
3156
+ );
3157
+ }
3024
3158
  server.registerTool(
3025
3159
  "kaddo_generate_context",
3026
3160
  { title: "Generate context pack", description: `Regenerate .kaddo/context-pack.md + .json. ${DERIVED_NOTE}`, inputSchema: {} },
@@ -3064,6 +3198,17 @@ function createServer(root) {
3064
3198
  }
3065
3199
  );
3066
3200
  }
3201
+ for (const s of installedSkills) {
3202
+ server.registerPrompt(
3203
+ `skill-${s.id}`,
3204
+ { title: s.title, description: `Reusable skill "${s.id}" (${s.group}).` },
3205
+ async () => {
3206
+ const full = getSkill(root, s.id);
3207
+ const content = full?.content ?? `Skill "${s.id}" is not installed.`;
3208
+ return { messages: [{ role: "user", content: { type: "text", text: content } }] };
3209
+ }
3210
+ );
3211
+ }
3067
3212
  return server;
3068
3213
  }
3069
3214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/mcp",
3
- "version": "3.20.0",
3
+ "version": "3.21.0",
4
4
  "description": "MCP server for Kaddo project knowledge: read-only resources/tools/prompts plus safe derived generation under .kaddo/.",
5
5
  "license": "MIT",
6
6
  "repository": {