@systemdox/mcp-server 0.1.42 → 0.1.44

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/dist/http.js CHANGED
@@ -63,6 +63,16 @@ var SystemDoxApiClient = class {
63
63
  this.cache.set(cacheKey, data);
64
64
  return data;
65
65
  }
66
+ /** A read that skips the cache: an install must take the version that is
67
+ * latest now, not the one cached before someone published another. */
68
+ async getFresh(path) {
69
+ const res = await this.fetchRaw(path);
70
+ if (!res.ok) {
71
+ const body = await res.text();
72
+ throw new Error(`SystemDox API error ${res.status}: ${body}`);
73
+ }
74
+ return await res.json();
75
+ }
66
76
  async post(path, body) {
67
77
  return this.mutate("POST", path, body);
68
78
  }
@@ -83,8 +93,8 @@ var SystemDoxApiClient = class {
83
93
  body: JSON.stringify(body)
84
94
  });
85
95
  if (!res.ok) {
86
- const text = await res.text();
87
- throw new Error(`SystemDox API error ${res.status}: ${text}`);
96
+ const text2 = await res.text();
97
+ throw new Error(`SystemDox API error ${res.status}: ${text2}`);
88
98
  }
89
99
  return await res.json();
90
100
  }
@@ -303,12 +313,12 @@ function registerGetGuardrails(server, apiClient) {
303
313
  );
304
314
  }
305
315
  if (filtered.length === 0) {
306
- const text2 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
316
+ const text3 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
307
317
  return {
308
- content: [{ type: "text", text: text2 }]
318
+ content: [{ type: "text", text: text3 }]
309
319
  };
310
320
  }
311
- const text = filtered.map(
321
+ const text2 = filtered.map(
312
322
  (g) => `## ${g.title} [${g.severity}]
313
323
 
314
324
  ${g.description}
@@ -332,7 +342,7 @@ ${g.fix}
332
342
  type: "text",
333
343
  text: `Found ${filtered.length} check(s):
334
344
 
335
- ${text}`
345
+ ${text2}`
336
346
  }
337
347
  ]
338
348
  };
@@ -422,7 +432,7 @@ ${spec.taskContent}`);
422
432
  ]
423
433
  };
424
434
  }
425
- const text = filtered.map(
435
+ const text2 = filtered.map(
426
436
  (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
427
437
  ).join("\n");
428
438
  return {
@@ -431,7 +441,7 @@ ${spec.taskContent}`);
431
441
  type: "text",
432
442
  text: `Found ${filtered.length} spec(s):
433
443
 
434
- ${text}`
444
+ ${text2}`
435
445
  }
436
446
  ]
437
447
  };
@@ -484,18 +494,18 @@ function registerGetDocs(server, apiClient) {
484
494
  isError: true
485
495
  };
486
496
  }
487
- let text2 = doc.content;
497
+ let text3 = doc.content;
488
498
  if (anchor && doc.section_resolved === false) {
489
- text2 = `_Section "${anchor}" not found \u2014 returning the full document._
499
+ text3 = `_Section "${anchor}" not found \u2014 returning the full document._
490
500
 
491
- ` + text2;
501
+ ` + text3;
492
502
  } else if (anchor && doc.heading) {
493
- text2 = `## ${doc.heading}
503
+ text3 = `## ${doc.heading}
494
504
 
495
505
  ${doc.content}`;
496
506
  }
497
507
  return {
498
- content: [{ type: "text", text: text2 }]
508
+ content: [{ type: "text", text: text3 }]
499
509
  };
500
510
  }
501
511
  const params = {};
@@ -519,7 +529,7 @@ ${doc.content}`;
519
529
  ]
520
530
  };
521
531
  }
522
- const text = items.map((d) => {
532
+ const text2 = items.map((d) => {
523
533
  const sections = (d.outline ?? []).slice(0, 8);
524
534
  const sectionHint = sections.length > 0 ? `
525
535
  sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
@@ -533,7 +543,7 @@ ${doc.content}`;
533
543
  type: "text",
534
544
  text: `Found ${items.length} document(s) (${response.total} total):
535
545
 
536
- ${text}
546
+ ${text2}
537
547
 
538
548
  Use doc_id for full content, or doc_id + section to fetch one section.`
539
549
  }
@@ -868,13 +878,13 @@ var SEVERITY_SCORES = {
868
878
  low: 0.25
869
879
  };
870
880
  var CHARS_PER_TOKEN = 4;
871
- function estimateTokens(text) {
872
- return Math.ceil(text.length / CHARS_PER_TOKEN);
881
+ function estimateTokens(text2) {
882
+ return Math.ceil(text2.length / CHARS_PER_TOKEN);
873
883
  }
874
- function truncateToTokens(text, maxTokens) {
884
+ function truncateToTokens(text2, maxTokens) {
875
885
  const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
876
- if (text.length <= maxChars) return { text, truncated: false };
877
- const head = text.slice(0, maxChars);
886
+ if (text2.length <= maxChars) return { text: text2, truncated: false };
887
+ const head = text2.slice(0, maxChars);
878
888
  const floor = Math.floor(maxChars * 0.75);
879
889
  const paragraph = head.lastIndexOf("\n\n");
880
890
  const line = head.lastIndexOf("\n");
@@ -919,7 +929,7 @@ function scoreSeverity(item) {
919
929
  if (!item.severity) return 0.5;
920
930
  return SEVERITY_SCORES[item.severity] ?? 0.5;
921
931
  }
922
- function extractKeywords(text) {
932
+ function extractKeywords(text2) {
923
933
  const stopWords = /* @__PURE__ */ new Set([
924
934
  "the",
925
935
  "a",
@@ -1004,7 +1014,7 @@ function extractKeywords(text) {
1004
1014
  "implement",
1005
1015
  "create"
1006
1016
  ]);
1007
- return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1017
+ return text2.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1008
1018
  }
1009
1019
  function scoreItems(items, taskDescription, filePaths) {
1010
1020
  const languages = detectLanguages(filePaths);
@@ -1047,20 +1057,20 @@ var MAX_TAIL = 10;
1047
1057
  var TAIL_SHARE = 0.15;
1048
1058
  var TRUNCATION_NOTICE_TOKENS = 30;
1049
1059
  function formatGuardrail(g) {
1050
- let text = `## ${g.title} [${g.severity}]
1060
+ let text2 = `## ${g.title} [${g.severity}]
1051
1061
 
1052
1062
  ${g.description}`;
1053
- if (g.pattern) text += `
1063
+ if (g.pattern) text2 += `
1054
1064
 
1055
1065
  **Bad:** \`\`\`
1056
1066
  ${g.pattern}
1057
1067
  \`\`\``;
1058
- if (g.fix) text += `
1068
+ if (g.fix) text2 += `
1059
1069
 
1060
1070
  **Good:** \`\`\`
1061
1071
  ${g.fix}
1062
1072
  \`\`\``;
1063
- return text;
1073
+ return text2;
1064
1074
  }
1065
1075
  function formatSpecHeadline(s) {
1066
1076
  return `### ${s.name}
@@ -1082,19 +1092,19 @@ function formatRules(rules) {
1082
1092
  if (!byCategory.has(cat)) byCategory.set(cat, []);
1083
1093
  byCategory.get(cat).push(r);
1084
1094
  }
1085
- let text = "";
1095
+ let text2 = "";
1086
1096
  for (const [cat, catRules] of byCategory) {
1087
- text += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1097
+ text2 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1088
1098
  `;
1089
1099
  for (const r of catRules) {
1090
- text += `- ${r.text}
1100
+ text2 += `- ${r.text}
1091
1101
  `;
1092
1102
  }
1093
- text += "\n";
1103
+ text2 += "\n";
1094
1104
  }
1095
1105
  return `## Project Rules (${rules.length})
1096
1106
 
1097
- ${text.trim()}`;
1107
+ ${text2.trim()}`;
1098
1108
  }
1099
1109
  async function fetchSpecBody(api, draftId) {
1100
1110
  const spec = await api.get(
@@ -1431,13 +1441,13 @@ function registerGetContext(server, apiClient) {
1431
1441
  }
1432
1442
  },
1433
1443
  async ({ task_description, file_paths, max_tokens }) => {
1434
- const { text } = await buildContext(apiClient, {
1444
+ const { text: text2 } = await buildContext(apiClient, {
1435
1445
  taskDescription: task_description,
1436
1446
  filePaths: file_paths,
1437
1447
  maxTokens: max_tokens
1438
1448
  });
1439
1449
  return {
1440
- content: [{ type: "text", text }]
1450
+ content: [{ type: "text", text: text2 }]
1441
1451
  };
1442
1452
  }
1443
1453
  );
@@ -1685,9 +1695,9 @@ function registerGetProjectRules(server, apiClient) {
1685
1695
  );
1686
1696
  }
1687
1697
  if (filtered.length === 0) {
1688
- const text2 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1698
+ const text3 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1689
1699
  return {
1690
- content: [{ type: "text", text: text2 }]
1700
+ content: [{ type: "text", text: text3 }]
1691
1701
  };
1692
1702
  }
1693
1703
  const grouped = /* @__PURE__ */ new Map();
@@ -1696,25 +1706,25 @@ function registerGetProjectRules(server, apiClient) {
1696
1706
  if (!grouped.has(cat)) grouped.set(cat, []);
1697
1707
  grouped.get(cat).push(rule);
1698
1708
  }
1699
- let text = `Found ${filtered.length} project rule(s):
1709
+ let text2 = `Found ${filtered.length} project rule(s):
1700
1710
 
1701
1711
  `;
1702
1712
  for (const [cat, catRules] of grouped) {
1703
1713
  const label = CATEGORY_LABELS[cat] ?? cat;
1704
- text += `## ${label}
1714
+ text2 += `## ${label}
1705
1715
 
1706
1716
  `;
1707
1717
  for (const rule of catRules) {
1708
- text += `- ${rule.text}
1718
+ text2 += `- ${rule.text}
1709
1719
  `;
1710
1720
  }
1711
- text += "\n";
1721
+ text2 += "\n";
1712
1722
  }
1713
1723
  return {
1714
1724
  content: [
1715
1725
  {
1716
1726
  type: "text",
1717
- text
1727
+ text: text2
1718
1728
  }
1719
1729
  ]
1720
1730
  };
@@ -1740,10 +1750,10 @@ function registerCreateProjectRule(server, apiClient) {
1740
1750
  readOnlyHint: false
1741
1751
  }
1742
1752
  },
1743
- async ({ text, category }) => {
1753
+ async ({ text: text2, category }) => {
1744
1754
  const saved = await apiClient.post(
1745
1755
  "/v1/standards/rules",
1746
- { text, category }
1756
+ { text: text2, category }
1747
1757
  );
1748
1758
  apiClient.invalidateCache("/v1/standards/rules");
1749
1759
  return {
@@ -1781,7 +1791,7 @@ function registerGetPromptTemplates(server, apiClient) {
1781
1791
  ]
1782
1792
  };
1783
1793
  }
1784
- const text = templates.map(
1794
+ const text2 = templates.map(
1785
1795
  (t) => `## ${t.name}
1786
1796
 
1787
1797
  ` + (t.description ? `*${t.description}*
@@ -1796,7 +1806,7 @@ ${t.prompt}
1796
1806
  type: "text",
1797
1807
  text: `Found ${templates.length} prompt template(s):
1798
1808
 
1799
- ${text}`
1809
+ ${text2}`
1800
1810
  }
1801
1811
  ]
1802
1812
  };
@@ -1843,9 +1853,345 @@ function registerCreatePromptTemplate(server, apiClient) {
1843
1853
  );
1844
1854
  }
1845
1855
 
1856
+ // src/tools/skills.ts
1857
+ import { z as z13 } from "zod";
1858
+ import { homedir as homedir2 } from "os";
1859
+ import { basename, resolve as resolve2 } from "path";
1860
+
1861
+ // src/lib/skills.ts
1862
+ import { createHash } from "crypto";
1863
+ import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
1864
+ import { existsSync } from "fs";
1865
+ import { homedir } from "os";
1866
+ import { dirname, join, relative, resolve, sep } from "path";
1867
+ var MANIFEST_FILE = ".systemdox-skill.json";
1868
+ var ENTRY_FILE = "SKILL.md";
1869
+ var MAX_FILES = 100;
1870
+ var MAX_FILE_BYTES = 256 * 1024;
1871
+ var MAX_TOTAL_BYTES = 1024 * 1024;
1872
+ var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1873
+ var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
1874
+ var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
1875
+ var SkillInstallRefused = class extends Error {
1876
+ constructor(dest, edited) {
1877
+ super(
1878
+ `${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
1879
+ );
1880
+ this.dest = dest;
1881
+ this.edited = edited;
1882
+ }
1883
+ dest;
1884
+ edited;
1885
+ };
1886
+ function sha256(content) {
1887
+ return createHash("sha256").update(content, "utf8").digest("hex");
1888
+ }
1889
+ function isValidName(name) {
1890
+ return name.length <= 64 && NAME_RE.test(name);
1891
+ }
1892
+ function isValidPath(path) {
1893
+ return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
1894
+ }
1895
+ function frontmatterName(skillMd) {
1896
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
1897
+ if (!block) return void 0;
1898
+ const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
1899
+ return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
1900
+ }
1901
+ function defaultSkillDir(name) {
1902
+ return join(homedir(), ".claude", "skills", name);
1903
+ }
1904
+ async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
1905
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
1906
+ if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
1907
+ const full = join(dir, entry.name);
1908
+ const rel = relative(root, full).split(sep).join("/");
1909
+ if (entry.isSymbolicLink()) out.links.push(rel);
1910
+ else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
1911
+ else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
1912
+ else if (entry.isFile()) out.files.push(rel);
1913
+ }
1914
+ out.files.sort();
1915
+ return out;
1916
+ }
1917
+ async function readSkillDirectory(directory) {
1918
+ const root = resolve(directory);
1919
+ if (!existsSync(join(root, ENTRY_FILE))) {
1920
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
1921
+ }
1922
+ const walked = await walk(root, { skipHidden: true });
1923
+ if (walked.links.length) {
1924
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
1925
+ }
1926
+ const paths = walked.files;
1927
+ if (paths.length > MAX_FILES) {
1928
+ throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
1929
+ }
1930
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1931
+ const files = [];
1932
+ let total = 0;
1933
+ for (const path of paths) {
1934
+ if (!isValidPath(path)) {
1935
+ throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
1936
+ }
1937
+ const raw = await readFile(join(root, ...path.split("/")));
1938
+ if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
1939
+ if (raw.length > MAX_FILE_BYTES) {
1940
+ throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
1941
+ }
1942
+ total += raw.length;
1943
+ if (total > MAX_TOTAL_BYTES) {
1944
+ throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
1945
+ }
1946
+ let content;
1947
+ try {
1948
+ content = decoder.decode(raw);
1949
+ } catch {
1950
+ throw new Error(`${path} is not UTF-8 text`);
1951
+ }
1952
+ files.push({ path, content });
1953
+ }
1954
+ const skillMd = files.find((f) => f.path === ENTRY_FILE);
1955
+ const name = frontmatterName(skillMd.content);
1956
+ if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
1957
+ return { name, files, skipped: walked.hidden };
1958
+ }
1959
+ function within(dest, path) {
1960
+ if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
1961
+ const full = resolve(dest, ...path.split("/"));
1962
+ if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
1963
+ return full;
1964
+ }
1965
+ async function readManifest(dest) {
1966
+ try {
1967
+ return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
1968
+ } catch {
1969
+ return void 0;
1970
+ }
1971
+ }
1972
+ async function installSkill(version, dest, { overwrite = false } = {}) {
1973
+ if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
1974
+ const incoming = new Map(version.files.map((f) => [f.path, f]));
1975
+ for (const f of version.files) within(dest, f.path);
1976
+ if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
1977
+ throw new Error(`${dest} is a link: install into a real folder`);
1978
+ }
1979
+ const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
1980
+ if (walked.links.length) {
1981
+ throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
1982
+ }
1983
+ const present = walked.files;
1984
+ const manifest = await readManifest(dest);
1985
+ const known = Object.fromEntries(
1986
+ Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
1987
+ );
1988
+ const edited = [];
1989
+ const untracked = [];
1990
+ for (const path of present) {
1991
+ const content = await readFile(join(dest, ...path.split("/")), "utf8");
1992
+ if (Object.hasOwn(known, path)) {
1993
+ if (sha256(content) !== known[path]) edited.push(path);
1994
+ } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
1995
+ if (incoming.has(path) || !manifest) edited.push(path);
1996
+ else untracked.push(path);
1997
+ }
1998
+ }
1999
+ const blocking = edited.filter((p) => incoming.has(p) || !manifest);
2000
+ if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
2001
+ const written = [];
2002
+ for (const f of version.files) {
2003
+ const full = within(dest, f.path);
2004
+ await mkdir(dirname(full), { recursive: true });
2005
+ await writeFile(full, f.content, "utf8");
2006
+ written.push(f.path);
2007
+ }
2008
+ const removed = [];
2009
+ const keptEdited = [];
2010
+ for (const path of Object.keys(known)) {
2011
+ if (incoming.has(path) || !present.includes(path)) continue;
2012
+ if (edited.includes(path)) {
2013
+ keptEdited.push(path);
2014
+ continue;
2015
+ }
2016
+ await rm(within(dest, path));
2017
+ removed.push(path);
2018
+ }
2019
+ const next = {
2020
+ name: version.name,
2021
+ version: version.version,
2022
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2023
+ files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
2024
+ };
2025
+ await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
2026
+ return {
2027
+ dest,
2028
+ version: version.version,
2029
+ written,
2030
+ removed,
2031
+ keptUntracked: untracked.filter((p) => !incoming.has(p)),
2032
+ keptEdited
2033
+ };
2034
+ }
2035
+ function fenceFor(text2) {
2036
+ const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
2037
+ return "`".repeat(Math.max(3, longest + 1));
2038
+ }
2039
+ function describeInstall(name, r) {
2040
+ const lines = [
2041
+ `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
2042
+ ];
2043
+ if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
2044
+ if (r.keptEdited.length) {
2045
+ lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
2046
+ }
2047
+ if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
2048
+ lines.push("Start a new Claude Code session to load it.");
2049
+ return lines.join("\n");
2050
+ }
2051
+
2052
+ // src/tools/skills.ts
2053
+ var SKILLS = "/v1/standards/skills";
2054
+ function expandHome(path) {
2055
+ return path.replace(/^~(?=$|[\\/])/, homedir2());
2056
+ }
2057
+ function text(t) {
2058
+ return { content: [{ type: "text", text: t }] };
2059
+ }
2060
+ function registerSkillTools(server, apiClient, { localFiles }) {
2061
+ const fileSchema = z13.object({
2062
+ path: z13.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2063
+ content: z13.string().describe("The file's full text, exactly as it is")
2064
+ });
2065
+ server.registerTool(
2066
+ "publish_skill",
2067
+ {
2068
+ title: "Publish Skill",
2069
+ description: localFiles ? "Publish a Claude Code skill folder (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass `directory` (e.g. ~/.claude/skills/<name>): the server reads the files itself, byte for byte. Publishing a name again makes a new version. Never retype the files into `files` when the folder is on this computer." : "Publish a Claude Code skill (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass every file of the folder in `files`, exactly as it is. Publishing a name again makes a new version.",
2070
+ inputSchema: {
2071
+ ...localFiles ? {
2072
+ directory: z13.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2073
+ } : {},
2074
+ files: z13.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2075
+ name: z13.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2076
+ spaceId: z13.string().optional().describe("Space for a NEW skill (default: your default space)"),
2077
+ note: z13.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
2078
+ baseVersion: z13.number().int().positive().optional().describe(
2079
+ "The version these files were edited from. When given, the publish is refused (SYD-K017) if someone published a newer version since, instead of replacing their changes"
2080
+ )
2081
+ },
2082
+ annotations: { readOnlyHint: false }
2083
+ },
2084
+ async (args) => {
2085
+ let files;
2086
+ let name;
2087
+ let skipped = [];
2088
+ if (localFiles && args.directory) {
2089
+ ({ name, files, skipped } = await readSkillDirectory(expandHome(args.directory)));
2090
+ } else if (args.files?.length) {
2091
+ files = args.files;
2092
+ const entry = files.find((f) => f.path === ENTRY_FILE);
2093
+ name = entry ? frontmatterName(entry.content) : void 0;
2094
+ } else {
2095
+ throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
2096
+ }
2097
+ name = args.name ?? name;
2098
+ if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name; pass \`name\`.`);
2099
+ const saved = await apiClient.post(
2100
+ `${SKILLS}/${encodeURIComponent(name)}/versions`,
2101
+ {
2102
+ files,
2103
+ ...args.spaceId ? { spaceId: args.spaceId } : {},
2104
+ ...args.note ? { note: args.note } : {},
2105
+ ...args.baseVersion ? { baseVersion: args.baseVersion } : {}
2106
+ }
2107
+ );
2108
+ apiClient.invalidateCache(SKILLS);
2109
+ return text(
2110
+ `Published **${saved.name}** version ${saved.version} (${saved.fileCount} file(s), ${saved.totalBytes} bytes).
2111
+ Install it anywhere with: npx -y @systemdox/mcp-server install-skill ${saved.name}` + (skipped.length ? `
2112
+ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2113
+ );
2114
+ }
2115
+ );
2116
+ server.registerTool(
2117
+ "list_skills",
2118
+ {
2119
+ title: "List Skills",
2120
+ description: "List the Claude Code skills published to SystemDox that you can install: name, description, latest version. Use install_skill to install one.",
2121
+ inputSchema: {},
2122
+ annotations: { readOnlyHint: true }
2123
+ },
2124
+ async () => {
2125
+ const { skills } = await apiClient.get(SKILLS);
2126
+ if (!skills.length) {
2127
+ return text(
2128
+ `No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient))}`
2129
+ );
2130
+ }
2131
+ return text(
2132
+ `${skills.length} skill(s):
2133
+
2134
+ ` + skills.map((s) => `- **${s.name}** (version ${s.latestVersion}, updated ${s.updatedAt.slice(0, 10)}): ${s.description}`).join("\n")
2135
+ );
2136
+ }
2137
+ );
2138
+ server.registerTool(
2139
+ "install_skill",
2140
+ {
2141
+ title: "Install Skill",
2142
+ description: localFiles ? "Install a skill published to SystemDox onto this computer (default ~/.claude/skills/<name>/). Refuses, naming the files, when a file there was edited since it was installed, unless overwrite is true. Never runs anything." : "Fetch a skill published to SystemDox so you can install it: returns every file and the folder to write them into (default ~/.claude/skills/<name>/). Write each file exactly, then the manifest it shows.",
2143
+ inputSchema: {
2144
+ name: z13.string().describe("The skill's name"),
2145
+ version: z13.number().int().positive().optional().describe("A version number (default: the latest)"),
2146
+ target: z13.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2147
+ ...localFiles ? { overwrite: z13.boolean().optional().describe("Replace files edited since they were installed") } : {}
2148
+ },
2149
+ annotations: { readOnlyHint: !localFiles }
2150
+ },
2151
+ async (args) => {
2152
+ const version = await apiClient.getFresh(
2153
+ `${SKILLS}/${encodeURIComponent(args.name)}/versions/${args.version ?? "latest"}`
2154
+ );
2155
+ if (localFiles) {
2156
+ const dest2 = args.target ? expandHome(args.target) : defaultSkillDir(version.name);
2157
+ if (basename(resolve2(dest2)) !== version.name) {
2158
+ throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
2159
+ }
2160
+ const result = await installSkill(version, dest2, { overwrite: args.overwrite });
2161
+ return text(describeInstall(version.name, result));
2162
+ }
2163
+ const dest = args.target ?? `~/.claude/skills/${version.name}`;
2164
+ const manifest = {
2165
+ name: version.name,
2166
+ version: version.version,
2167
+ installedAt: "<now, ISO 8601>",
2168
+ files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
2169
+ };
2170
+ const body = version.files.map((f) => {
2171
+ const fence = fenceFor(f.content);
2172
+ return `### ${f.path}
2173
+
2174
+ ${fence}
2175
+ ${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
2176
+ }).join("\n\n");
2177
+ return text(
2178
+ `Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content, and tell the user to start a new Claude Code session.
2179
+
2180
+ ### ${MANIFEST_FILE}
2181
+
2182
+ \`\`\`json
2183
+ ${JSON.stringify(manifest, null, 2)}
2184
+ \`\`\`
2185
+
2186
+ ${body}`
2187
+ );
2188
+ }
2189
+ );
2190
+ }
2191
+
1846
2192
  // src/server.ts
1847
- var SERVER_VERSION = true ? "0.1.42" : "0.0.0-dev";
1848
- function createServer(apiClient) {
2193
+ var SERVER_VERSION = true ? "0.1.44" : "0.0.0-dev";
2194
+ function createServer(apiClient, { localFiles = false } = {}) {
1849
2195
  const server = new McpServer({
1850
2196
  name: "systemdox-mcp-server",
1851
2197
  version: SERVER_VERSION
@@ -1864,6 +2210,7 @@ function createServer(apiClient) {
1864
2210
  registerReportDecision(server, apiClient);
1865
2211
  registerCreateProjectRule(server, apiClient);
1866
2212
  registerCreatePromptTemplate(server, apiClient);
2213
+ registerSkillTools(server, apiClient, { localFiles });
1867
2214
  return server;
1868
2215
  }
1869
2216