@systemdox/mcp-server 0.1.51 → 0.1.53

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/server.js CHANGED
@@ -188,6 +188,17 @@ ${text3}`
188
188
 
189
189
  // src/tools/get-specs.ts
190
190
  import { z as z2 } from "zod";
191
+
192
+ // src/lib/spec-label.ts
193
+ var DECISION_PREFIX = "ADR: ";
194
+ function isDecisionDraft(name) {
195
+ return name.startsWith(DECISION_PREFIX);
196
+ }
197
+ function specLabel(spec) {
198
+ return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
199
+ }
200
+
201
+ // src/tools/get-specs.ts
191
202
  function registerGetSpecs(server, apiClient) {
192
203
  server.registerTool(
193
204
  "get_specs",
@@ -252,7 +263,7 @@ ${spec.taskContent}`);
252
263
  };
253
264
  }
254
265
  const text3 = filtered.map(
255
- (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
266
+ (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
256
267
  ).join("\n");
257
268
  return {
258
269
  content: [
@@ -270,6 +281,11 @@ ${text3}`
270
281
 
271
282
  // src/tools/get-docs.ts
272
283
  import { z as z3 } from "zod";
284
+ var PATH_FILTER_LIMIT = "1000";
285
+ function literalPrefix(pattern) {
286
+ const wildcard = pattern.indexOf("*");
287
+ return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
288
+ }
273
289
  function matchGlob(path, pattern) {
274
290
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
275
291
  return new RegExp(`^${regex}$`).test(path);
@@ -330,11 +346,17 @@ ${doc.content}`;
330
346
  const params = {};
331
347
  if (repo) params.repo = repo;
332
348
  if (search) params.q = search;
349
+ if (path) {
350
+ params.limit = PATH_FILTER_LIMIT;
351
+ const prefix = literalPrefix(path);
352
+ if (prefix) params.pathPrefix = prefix;
353
+ }
333
354
  const response = await apiClient.get(
334
355
  "/v1/docs",
335
356
  params
336
357
  );
337
358
  let items = response.items;
359
+ const unread = path ? response.total - response.items.length : 0;
338
360
  if (path) {
339
361
  items = items.filter((item) => matchGlob(item.path, path));
340
362
  }
@@ -364,7 +386,9 @@ ${doc.content}`;
364
386
 
365
387
  ${text3}
366
388
 
367
- Use doc_id for full content, or doc_id + section to fetch one section.`
389
+ ` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
390
+
391
+ ` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
368
392
  }
369
393
  ]
370
394
  };
@@ -465,7 +489,9 @@ ${deps}`);
465
489
  }
466
490
  if (ctx.adrs.length > 0) {
467
491
  const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
468
- sections.push(`## Architecture Decision Records (${ctx.adrs.length})
492
+ const total = ctx.adrTotal ?? ctx.adrs.length;
493
+ const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
494
+ sections.push(`## Architecture Decision Records (${count})
469
495
 
470
496
  ${adrs}`);
471
497
  }
@@ -837,6 +863,9 @@ var SEVERITY_SCORES = {
837
863
  medium: 0.5,
838
864
  low: 0.25
839
865
  };
866
+ var MIN_TITLE_SHARE = 1 / 3;
867
+ var RECORD_PATH = /(^|\/)changelog\//;
868
+ var RECORD_FACTOR = 0.5;
840
869
  var CHARS_PER_TOKEN = 4;
841
870
  function estimateTokens(text3) {
842
871
  return Math.ceil(text3.length / CHARS_PER_TOKEN);
@@ -889,6 +918,32 @@ function scoreSeverity(item) {
889
918
  if (!item.severity) return 0.5;
890
919
  return SEVERITY_SCORES[item.severity] ?? 0.5;
891
920
  }
921
+ function stem(word) {
922
+ return word.length > 3 ? word.replace(/s$/, "") : word;
923
+ }
924
+ function stems(text3) {
925
+ return new Set(
926
+ text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
927
+ );
928
+ }
929
+ function titleShare(title, keywords) {
930
+ const wanted = stems(keywords.join(" "));
931
+ if (wanted.size === 0) return 0;
932
+ const held = stems(title);
933
+ let matches = 0;
934
+ for (const word of wanted) {
935
+ if (held.has(word)) matches++;
936
+ }
937
+ return matches / wanted.size;
938
+ }
939
+ function scoreReference(item, keywords) {
940
+ const share = titleShare(item.title, keywords);
941
+ const score = Math.max(
942
+ item.relevance ?? 0,
943
+ share >= MIN_TITLE_SHARE ? share : 0
944
+ );
945
+ return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
946
+ }
892
947
  function extractKeywords(text3) {
893
948
  const stopWords = /* @__PURE__ */ new Set([
894
949
  "the",
@@ -985,7 +1040,8 @@ function scoreItems(items, taskDescription, filePaths) {
985
1040
  const keywordScore = scoreKeywordMatch(item, keywords);
986
1041
  const recencyScore = scoreRecency(item);
987
1042
  const severityScore = scoreSeverity(item);
988
- const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
1043
+ const isReference = item.type === "spec" || item.type === "doc";
1044
+ const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
989
1045
  return {
990
1046
  type: item.type,
991
1047
  id: item.id,
@@ -994,7 +1050,7 @@ function scoreItems(items, taskDescription, filePaths) {
994
1050
  score,
995
1051
  tokenEstimate: estimateTokens(item.content)
996
1052
  };
997
- }).sort((a, b) => b.score - a.score);
1053
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
998
1054
  }
999
1055
  function selectWithinBudget(items, maxTokens) {
1000
1056
  const selected = [];
@@ -1016,6 +1072,10 @@ var MAX_HYDRATED = 8;
1016
1072
  var MAX_TAIL = 10;
1017
1073
  var TAIL_SHARE = 0.15;
1018
1074
  var TRUNCATION_NOTICE_TOKENS = 30;
1075
+ var STACK_SHARE = 0.1;
1076
+ var MIN_REFERENCES = 3;
1077
+ var SEARCH_RESULTS = 30;
1078
+ var MAX_QUERY_CHARS = 300;
1019
1079
  function formatGuardrail(g) {
1020
1080
  let text3 = `## ${g.title} [${g.severity}]
1021
1081
 
@@ -1034,7 +1094,7 @@ ${g.fix}
1034
1094
  }
1035
1095
  function formatSpecHeadline(s) {
1036
1096
  return `### ${s.name}
1037
- \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
1097
+ \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
1038
1098
  }
1039
1099
  function formatDocHeadline(d) {
1040
1100
  return `### ${d.title}
@@ -1045,6 +1105,21 @@ function formatStack(entries2) {
1045
1105
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
1046
1106
  ).join("\n");
1047
1107
  }
1108
+ function selectStack(entries2, words, maxTokens) {
1109
+ const named = (e) => {
1110
+ const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
1111
+ return words.some((w) => entry.includes(w));
1112
+ };
1113
+ const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
1114
+ const shown = [];
1115
+ for (const entry of ordered) {
1116
+ if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
1117
+ break;
1118
+ }
1119
+ shown.push(entry);
1120
+ }
1121
+ return { text: formatStack(shown), hidden: entries2.length - shown.length };
1122
+ }
1048
1123
  function formatRules(rules) {
1049
1124
  const byCategory = /* @__PURE__ */ new Map();
1050
1125
  for (const r of rules) {
@@ -1066,6 +1141,21 @@ function formatRules(rules) {
1066
1141
 
1067
1142
  ${text3.trim()}`;
1068
1143
  }
1144
+ async function findDocs(api, taskDescription) {
1145
+ const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
1146
+ if (query) {
1147
+ try {
1148
+ const found = await api.get("/v1/docs", {
1149
+ q: query,
1150
+ limit: String(SEARCH_RESULTS)
1151
+ });
1152
+ if (found.items.length > 0) return { ...found, searched: true };
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ const listed = await api.get("/v1/docs");
1157
+ return { ...listed, searched: false };
1158
+ }
1069
1159
  async function fetchSpecBody(api, draftId) {
1070
1160
  const spec = await api.get(
1071
1161
  `/v1/specs/${encodeURIComponent(draftId)}`
@@ -1166,7 +1256,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1166
1256
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
1167
1257
  api.get("/v1/guardrails").catch(orEmpty([])),
1168
1258
  api.get("/v1/specs").catch(orEmpty([])),
1169
- api.get("/v1/docs").catch(orEmpty({ items: [] })),
1259
+ findDocs(api, taskDescription).catch(
1260
+ orEmpty({ items: [], searched: false })
1261
+ ),
1170
1262
  api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1171
1263
  api.get("/v1/standards/rules").catch(orEmpty([])),
1172
1264
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
@@ -1179,6 +1271,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1179
1271
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
1180
1272
  const refs = /* @__PURE__ */ new Map();
1181
1273
  const key = (type, id) => `${type}:${id}`;
1274
+ const docTitles = new Set(docs.items.map((d) => d.title));
1275
+ const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
1182
1276
  const scorable = [
1183
1277
  ...guardrails.map((g) => ({
1184
1278
  type: "guardrail",
@@ -1189,12 +1283,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1189
1283
  severity: g.severity,
1190
1284
  updatedAt: g.updatedAt
1191
1285
  })),
1192
- ...specs.filter((s) => s.status !== "archived").map((s) => {
1286
+ ...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
1193
1287
  refs.set(key("spec", s.draftId), {
1194
1288
  type: "spec",
1195
1289
  id: s.draftId,
1196
1290
  title: s.name,
1197
- listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s.requirementCount} requirements)`,
1291
+ listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
1198
1292
  fetchHint: `get_specs(draft_id="${s.draftId}")`
1199
1293
  });
1200
1294
  return {
@@ -1205,7 +1299,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1205
1299
  updatedAt: s.updatedAt
1206
1300
  };
1207
1301
  }),
1208
- ...docs.items.map((d) => {
1302
+ ...docs.items.map((d, position) => {
1209
1303
  const section = selectSection(d.outline, taskDescription);
1210
1304
  const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
1211
1305
  refs.set(key("doc", d.id), {
@@ -1224,7 +1318,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1224
1318
  title: d.title,
1225
1319
  content: formatDocHeadline(d),
1226
1320
  category: d.doc_type === "adr" ? "architecture" : void 0,
1227
- updatedAt: d.synced_at
1321
+ updatedAt: d.synced_at,
1322
+ path: d.path,
1323
+ relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
1228
1324
  };
1229
1325
  })
1230
1326
  ];
@@ -1232,9 +1328,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1232
1328
  const sections = [];
1233
1329
  let overhead = 0;
1234
1330
  if (stack.length > 0) {
1331
+ const { text: text3, hidden } = selectStack(
1332
+ stack,
1333
+ [...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
1334
+ Math.floor(budget * STACK_SHARE)
1335
+ );
1235
1336
  const section = `## Tech Stack
1236
1337
 
1237
- ${formatStack(stack)}`;
1338
+ ${text3}` + (hidden > 0 ? `
1339
+
1340
+ _${hidden} more with \`get_tech_stack\`._` : "");
1238
1341
  sections.push(section);
1239
1342
  overhead += estimateTokens(section);
1240
1343
  }
@@ -1353,9 +1456,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
1353
1456
  const tailLines = [];
1354
1457
  for (const meta of tailCandidates) {
1355
1458
  const cost = estimateTokens(meta.listLine);
1356
- if (cost > remaining) break;
1459
+ const named = blocks.length + tailLines.length;
1460
+ if (cost > remaining && named >= MIN_REFERENCES) break;
1357
1461
  tailLines.push(meta.listLine);
1358
- remaining -= cost;
1462
+ remaining = Math.max(0, remaining - cost);
1359
1463
  }
1360
1464
  if (tailLines.length > 0) {
1361
1465
  sections.push(
@@ -2658,9 +2762,9 @@ function localDate(now) {
2658
2762
  return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
2659
2763
  }
2660
2764
  function conflictCopyName(path, date, taken) {
2661
- const stem = path.replace(/\.md$/, "");
2765
+ const stem2 = path.replace(/\.md$/, "");
2662
2766
  for (let n = 1; ; n++) {
2663
- const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
2767
+ const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
2664
2768
  if (!taken.has(name.toLowerCase())) return name;
2665
2769
  }
2666
2770
  }
@@ -3547,7 +3651,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3547
3651
  }
3548
3652
 
3549
3653
  // src/server.ts
3550
- var SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
3654
+ var SERVER_VERSION = true ? "0.1.53" : "0.0.0-dev";
3551
3655
  function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
3552
3656
  const server = new McpServer(
3553
3657
  {
@@ -2,6 +2,11 @@
2
2
 
3
3
  // src/tools/get-docs.ts
4
4
  import { z } from "zod";
5
+ var PATH_FILTER_LIMIT = "1000";
6
+ function literalPrefix(pattern) {
7
+ const wildcard = pattern.indexOf("*");
8
+ return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
9
+ }
5
10
  function matchGlob(path, pattern) {
6
11
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
7
12
  return new RegExp(`^${regex}$`).test(path);
@@ -62,11 +67,17 @@ ${doc.content}`;
62
67
  const params = {};
63
68
  if (repo) params.repo = repo;
64
69
  if (search) params.q = search;
70
+ if (path) {
71
+ params.limit = PATH_FILTER_LIMIT;
72
+ const prefix = literalPrefix(path);
73
+ if (prefix) params.pathPrefix = prefix;
74
+ }
65
75
  const response = await apiClient.get(
66
76
  "/v1/docs",
67
77
  params
68
78
  );
69
79
  let items = response.items;
80
+ const unread = path ? response.total - response.items.length : 0;
70
81
  if (path) {
71
82
  items = items.filter((item) => matchGlob(item.path, path));
72
83
  }
@@ -96,7 +107,9 @@ ${doc.content}`;
96
107
 
97
108
  ${text}
98
109
 
99
- Use doc_id for full content, or doc_id + section to fetch one section.`
110
+ ` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
111
+
112
+ ` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
100
113
  }
101
114
  ]
102
115
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemdox/mcp-server",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
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",