@systemdox/mcp-server 0.1.51 → 0.1.52
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/cli.js +131 -19
- package/dist/http.js +121 -17
- package/dist/index.js +121 -17
- package/dist/lib/context-builder.js +98 -11
- package/dist/lib/scoring.d.ts +5 -0
- package/dist/lib/spec-label.d.ts +5 -0
- package/dist/server.js +121 -17
- package/dist/tools/get-docs.js +14 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
1459
|
+
const named = blocks.length + tailLines.length;
|
|
1460
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
1357
1461
|
tailLines.push(meta.listLine);
|
|
1358
|
-
remaining
|
|
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
|
|
2765
|
+
const stem2 = path.replace(/\.md$/, "");
|
|
2662
2766
|
for (let n = 1; ; n++) {
|
|
2663
|
-
const name = `${
|
|
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.
|
|
3654
|
+
var SERVER_VERSION = true ? "0.1.52" : "0.0.0-dev";
|
|
3551
3655
|
function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
|
|
3552
3656
|
const server = new McpServer(
|
|
3553
3657
|
{
|
package/dist/tools/get-docs.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.52",
|
|
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",
|