@systemdox/mcp-server 0.1.49 → 0.1.51

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/setup-mcp.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/setup-mcp.ts
4
4
  import { existsSync as existsSync3, readFileSync } from "fs";
5
+ import { mkdir as mkdir4 } from "fs/promises";
5
6
  import { join as join4 } from "path";
6
7
 
7
8
  // src/lib/cache.ts
@@ -64,14 +65,6 @@ var SystemDoxApiClient = class {
64
65
  this.config = config;
65
66
  this.cache = new ResponseCache(config.cacheTtlMs);
66
67
  }
67
- async validate() {
68
- const res = await this.fetchRaw("/v1/health");
69
- if (!res.ok) {
70
- throw new Error(
71
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
72
- );
73
- }
74
- }
75
68
  async get(path, params) {
76
69
  const url = this.buildUrl(path, params);
77
70
  const cacheKey = url.toString();
@@ -85,8 +78,8 @@ var SystemDoxApiClient = class {
85
78
  }
86
79
  /** A read that skips the cache: an install must take the version that is
87
80
  * latest now, not the one cached before someone published another. */
88
- async getFresh(path) {
89
- const res = await this.fetchRaw(path);
81
+ async getFresh(path, signal) {
82
+ const res = await this.fetchRaw(path, void 0, signal);
90
83
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
91
84
  return await res.json();
92
85
  }
@@ -116,8 +109,8 @@ var SystemDoxApiClient = class {
116
109
  // One method per operationId. Every read skips the cache: setup, sync and
117
110
  // plugin-path act on what the workspace holds now.
118
111
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
119
- getOwnTeamSetup() {
120
- return this.getFresh(TEAM_SETUP);
112
+ getOwnTeamSetup(signal) {
113
+ return this.getFresh(TEAM_SETUP, signal);
121
114
  }
122
115
  getTeamSetup(workspace) {
123
116
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -221,14 +214,15 @@ var SystemDoxApiClient = class {
221
214
  }
222
215
  return url;
223
216
  }
224
- async fetchRaw(path, params) {
217
+ async fetchRaw(path, params, signal) {
225
218
  const url = this.buildUrl(path, params);
226
219
  return fetch(url, {
227
220
  headers: {
228
221
  Authorization: `Bearer ${this.config.apiKey}`,
229
222
  "X-API-Key": this.config.apiKey,
230
223
  Accept: "application/json"
231
- }
224
+ },
225
+ signal
232
226
  });
233
227
  }
234
228
  };
@@ -598,6 +592,7 @@ async function setupMcpServer(opts) {
598
592
  }
599
593
  try {
600
594
  if ((await readCredentials())[workspace]?.apiKey !== apiKey) await writeCredentials(workspace, apiKey);
595
+ await mkdir4(join4(homeDir(), ".claude", "skills"), { recursive: true });
601
596
  const several = Object.keys(await readCredentials()).length > 1;
602
597
  for (const target of selected) {
603
598
  const removed = await writeServerEntry(target.path, apiUrl, several ? workspace : void 0);
@@ -615,9 +610,10 @@ async function setupMcpServer(opts) {
615
610
  Your key for ${workspace} is in ${credentialsPath()} (readable by you alone), not in these configs.
616
611
 
617
612
  Done! Restart your AI coding tool to activate the SystemDox MCP server.
618
- The server provides 21 tools: get_context, get_checks,
613
+ The server provides 25 tools: get_context, get_team_setup, get_checks,
619
614
  get_project_rules, get_prompt_templates, get_specs, get_docs,
620
615
  get_tech_stack, get_planning_context, get_plan_board, get_change,
616
+ get_document_links, trace_document_path, get_graph_stats,
621
617
  create_check, create_project_rule, create_prompt_template,
622
618
  update_spec, report_decision, publish_skill, list_skills,
623
619
  install_skill, publish_setup_part, resolve_person, sync_memory.
@@ -68,14 +68,6 @@ var SystemDoxApiClient = class {
68
68
  this.config = config;
69
69
  this.cache = new ResponseCache(config.cacheTtlMs);
70
70
  }
71
- async validate() {
72
- const res = await this.fetchRaw("/v1/health");
73
- if (!res.ok) {
74
- throw new Error(
75
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
76
- );
77
- }
78
- }
79
71
  async get(path, params) {
80
72
  const url = this.buildUrl(path, params);
81
73
  const cacheKey = url.toString();
@@ -89,8 +81,8 @@ var SystemDoxApiClient = class {
89
81
  }
90
82
  /** A read that skips the cache: an install must take the version that is
91
83
  * latest now, not the one cached before someone published another. */
92
- async getFresh(path) {
93
- const res = await this.fetchRaw(path);
84
+ async getFresh(path, signal) {
85
+ const res = await this.fetchRaw(path, void 0, signal);
94
86
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
95
87
  return await res.json();
96
88
  }
@@ -120,8 +112,8 @@ var SystemDoxApiClient = class {
120
112
  // One method per operationId. Every read skips the cache: setup, sync and
121
113
  // plugin-path act on what the workspace holds now.
122
114
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
123
- getOwnTeamSetup() {
124
- return this.getFresh(TEAM_SETUP);
115
+ getOwnTeamSetup(signal) {
116
+ return this.getFresh(TEAM_SETUP, signal);
125
117
  }
126
118
  getTeamSetup(workspace) {
127
119
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -225,14 +217,15 @@ var SystemDoxApiClient = class {
225
217
  }
226
218
  return url;
227
219
  }
228
- async fetchRaw(path, params) {
220
+ async fetchRaw(path, params, signal) {
229
221
  const url = this.buildUrl(path, params);
230
222
  return fetch(url, {
231
223
  headers: {
232
224
  Authorization: `Bearer ${this.config.apiKey}`,
233
225
  "X-API-Key": this.config.apiKey,
234
226
  Accept: "application/json"
235
- }
227
+ },
228
+ signal
236
229
  });
237
230
  }
238
231
  };
@@ -65,14 +65,6 @@ var SystemDoxApiClient = class {
65
65
  this.config = config;
66
66
  this.cache = new ResponseCache(config.cacheTtlMs);
67
67
  }
68
- async validate() {
69
- const res = await this.fetchRaw("/v1/health");
70
- if (!res.ok) {
71
- throw new Error(
72
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
73
- );
74
- }
75
- }
76
68
  async get(path, params) {
77
69
  const url = this.buildUrl(path, params);
78
70
  const cacheKey = url.toString();
@@ -86,8 +78,8 @@ var SystemDoxApiClient = class {
86
78
  }
87
79
  /** A read that skips the cache: an install must take the version that is
88
80
  * latest now, not the one cached before someone published another. */
89
- async getFresh(path) {
90
- const res = await this.fetchRaw(path);
81
+ async getFresh(path, signal) {
82
+ const res = await this.fetchRaw(path, void 0, signal);
91
83
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
92
84
  return await res.json();
93
85
  }
@@ -117,8 +109,8 @@ var SystemDoxApiClient = class {
117
109
  // One method per operationId. Every read skips the cache: setup, sync and
118
110
  // plugin-path act on what the workspace holds now.
119
111
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
120
- getOwnTeamSetup() {
121
- return this.getFresh(TEAM_SETUP);
112
+ getOwnTeamSetup(signal) {
113
+ return this.getFresh(TEAM_SETUP, signal);
122
114
  }
123
115
  getTeamSetup(workspace) {
124
116
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -222,14 +214,15 @@ var SystemDoxApiClient = class {
222
214
  }
223
215
  return url;
224
216
  }
225
- async fetchRaw(path, params) {
217
+ async fetchRaw(path, params, signal) {
226
218
  const url = this.buildUrl(path, params);
227
219
  return fetch(url, {
228
220
  headers: {
229
221
  Authorization: `Bearer ${this.config.apiKey}`,
230
222
  "X-API-Key": this.config.apiKey,
231
223
  Accept: "application/json"
232
- }
224
+ },
225
+ signal
233
226
  });
234
227
  }
235
228
  };
@@ -0,0 +1,87 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SystemDoxApiClient } from "../lib/api-client.js";
3
+ /**
4
+ * The document graph over MCP (ADR-224, specs/changes/document-graph/contract.md §5).
5
+ * Three read-only tools on the hosted and the local server. Every answer says
6
+ * how each link is known, and one that was cut says so.
7
+ */
8
+ interface GraphNode {
9
+ id: string;
10
+ label: string;
11
+ type: string;
12
+ repo: string;
13
+ path: string;
14
+ }
15
+ interface NeighbourLink {
16
+ direction: "out" | "in";
17
+ relation: string;
18
+ provenance: string;
19
+ confidence: number;
20
+ file: string;
21
+ line: number;
22
+ other: GraphNode;
23
+ }
24
+ interface AmbiguousLink {
25
+ reference: string;
26
+ file: string;
27
+ line: number;
28
+ candidates: GraphNode[];
29
+ }
30
+ interface Neighbours {
31
+ doc: GraphNode;
32
+ links: NeighbourLink[];
33
+ ambiguous: AmbiguousLink[];
34
+ total: number;
35
+ truncated: boolean;
36
+ }
37
+ interface PathStep {
38
+ from: GraphNode;
39
+ to: GraphNode;
40
+ relation: string;
41
+ provenance: string;
42
+ confidence: number;
43
+ file: string;
44
+ line: number;
45
+ reversed: boolean;
46
+ }
47
+ interface GraphPath {
48
+ found: boolean;
49
+ hops: number;
50
+ confidence: number;
51
+ steps: PathStep[];
52
+ }
53
+ interface RepoFreshness {
54
+ repo: string;
55
+ documents: number;
56
+ links: number;
57
+ indexedCommit: string;
58
+ indexedAt: string;
59
+ headCommit: string;
60
+ pushedAt: string;
61
+ behind: boolean;
62
+ }
63
+ interface GraphStats {
64
+ nodeCount: number;
65
+ edgeCount: number;
66
+ links: {
67
+ declared: number;
68
+ extracted: number;
69
+ inferred: number;
70
+ ambiguous: number;
71
+ };
72
+ repos: RepoFreshness[];
73
+ }
74
+ interface RepoLink {
75
+ source: string;
76
+ target: string;
77
+ relation: string;
78
+ provenance: string;
79
+ confidence: number;
80
+ file: string;
81
+ line: number;
82
+ }
83
+ export declare function formatNeighbours(answer: Neighbours): string;
84
+ export declare function formatPath(answer: GraphPath, maxHops: number): string;
85
+ export declare function formatStats(stats: GraphStats, repoLinks: RepoLink[]): string;
86
+ export declare function registerDocumentGraph(server: McpServer, apiClient: SystemDoxApiClient): void;
87
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SystemDoxApiClient } from "../lib/api-client.js";
3
+ export declare function registerGetTeamSetup(server: McpServer, apiClient: SystemDoxApiClient, { localFiles, keyProblem }: {
4
+ localFiles: boolean;
5
+ keyProblem?: string;
6
+ }): void;
@@ -29,6 +29,7 @@ import { homedir } from "os";
29
29
  import { dirname, join, relative, resolve, sep } from "path";
30
30
  var MANIFEST_FILE = ".systemdox-skill.json";
31
31
  var ENTRY_FILE = "SKILL.md";
32
+ var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
32
33
  var MAX_FILES = 100;
33
34
  var MAX_FILE_BYTES = 256 * 1024;
34
35
  var MAX_TOTAL_BYTES = 1024 * 1024;
@@ -53,7 +54,7 @@ function isValidName(name) {
53
54
  return name.length <= 64 && NAME_RE.test(name);
54
55
  }
55
56
  function isValidPath(path) {
56
- return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
57
+ return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
57
58
  }
58
59
  function frontmatterName(skillMd) {
59
60
  const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
@@ -202,6 +203,9 @@ function fenceFor(text2) {
202
203
  const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
203
204
  return "`".repeat(Math.max(3, longest + 1));
204
205
  }
206
+ function afterInstall(skillMd) {
207
+ return `No restart is needed: a running Claude Code session picks it up. If one does not list it yet, read ${skillMd} and follow it, or run /reload-skills.`;
208
+ }
205
209
  function describeInstall(name, r) {
206
210
  const lines = [
207
211
  `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
@@ -211,7 +215,7 @@ function describeInstall(name, r) {
211
215
  lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
212
216
  }
213
217
  if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
214
- lines.push("Start a new Claude Code session to load it.");
218
+ lines.push(afterInstall(join(r.dest, "SKILL.md")));
215
219
  return lines.join("\n");
216
220
  }
217
221
 
@@ -341,7 +345,7 @@ ${fence}
341
345
  ${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
342
346
  }).join("\n\n");
343
347
  return text(
344
- `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.
348
+ `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. ${afterInstall(`${dest}/SKILL.md`)}
345
349
 
346
350
  ### ${MANIFEST_FILE}
347
351
 
@@ -14,13 +14,14 @@ import { existsSync } from "fs";
14
14
  import { homedir } from "os";
15
15
  import { dirname, join, relative, resolve, sep } from "path";
16
16
  var MANIFEST_FILE = ".systemdox-skill.json";
17
+ var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
17
18
  var MAX_FILES = 100;
18
19
  var MAX_FILE_BYTES = 256 * 1024;
19
20
  var MAX_TOTAL_BYTES = 1024 * 1024;
20
21
  var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
21
22
  var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
22
23
  function isValidPath(path) {
23
- return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
24
+ return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
24
25
  }
25
26
  async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
26
27
  for (const entry of await readdir(dir, { withFileTypes: true })) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemdox/mcp-server",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "mcpName": "io.github.puglieseweb/systemdox",
5
5
  "type": "module",
6
6
  "description": "Give AI coding agents your architecture context over MCP — checks, specs, ADRs, project rules, and docs from SystemDox, delivered at write time",