@systemdox/mcp-server 0.1.12 → 0.1.13
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 +93 -19
- package/dist/http.js +92 -18
- package/dist/index.js +92 -18
- package/dist/lib/context-builder.d.ts +9 -0
- package/dist/lib/context-builder.js +61 -9
- package/dist/tools/get-docs.js +108 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -444,20 +444,28 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
444
444
|
"get_docs",
|
|
445
445
|
{
|
|
446
446
|
title: "Get Docs",
|
|
447
|
-
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search.",
|
|
447
|
+
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search. Pass doc_id with section (a heading anchor) or chunk_id to retrieve just that section instead of the whole document.",
|
|
448
448
|
inputSchema: {
|
|
449
449
|
repo: z3.string().optional().describe("Filter by repo (owner/repo format)"),
|
|
450
450
|
path: z3.string().optional().describe("File path glob pattern (e.g., docs/adr/*.md)"),
|
|
451
451
|
search: z3.string().optional().describe("Keyword search across document content"),
|
|
452
452
|
doc_id: z3.string().optional().describe(
|
|
453
453
|
"Specific document ID to retrieve full content"
|
|
454
|
+
),
|
|
455
|
+
section: z3.string().optional().describe(
|
|
456
|
+
"Heading anchor (e.g. 'rollback-strategy') \u2014 with doc_id, returns just that section"
|
|
457
|
+
),
|
|
458
|
+
chunk_id: z3.string().optional().describe(
|
|
459
|
+
"Section chunk id ('{path_hash}#{anchor}') from get_context \u2014 with doc_id, returns just that section"
|
|
454
460
|
)
|
|
455
461
|
}
|
|
456
462
|
},
|
|
457
|
-
async ({ repo, path, search, doc_id }) => {
|
|
463
|
+
async ({ repo, path, search, doc_id, section, chunk_id }) => {
|
|
458
464
|
if (doc_id) {
|
|
465
|
+
const anchor = section ?? (chunk_id ? chunk_id.split("#").pop() : void 0);
|
|
459
466
|
const doc = await apiClient2.get(
|
|
460
|
-
`/v1/docs/${encodeURIComponent(doc_id)}/content
|
|
467
|
+
`/v1/docs/${encodeURIComponent(doc_id)}/content`,
|
|
468
|
+
anchor ? { section: anchor } : void 0
|
|
461
469
|
);
|
|
462
470
|
if (doc.error) {
|
|
463
471
|
return {
|
|
@@ -470,8 +478,18 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
470
478
|
isError: true
|
|
471
479
|
};
|
|
472
480
|
}
|
|
481
|
+
let text2 = doc.content;
|
|
482
|
+
if (anchor && doc.section_resolved === false) {
|
|
483
|
+
text2 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
484
|
+
|
|
485
|
+
` + text2;
|
|
486
|
+
} else if (anchor && doc.heading) {
|
|
487
|
+
text2 = `## ${doc.heading}
|
|
488
|
+
|
|
489
|
+
${doc.content}`;
|
|
490
|
+
}
|
|
473
491
|
return {
|
|
474
|
-
content: [{ type: "text", text:
|
|
492
|
+
content: [{ type: "text", text: text2 }]
|
|
475
493
|
};
|
|
476
494
|
}
|
|
477
495
|
const params = {};
|
|
@@ -495,10 +513,14 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
495
513
|
]
|
|
496
514
|
};
|
|
497
515
|
}
|
|
498
|
-
const text = items.map(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
516
|
+
const text = items.map((d) => {
|
|
517
|
+
const sections = (d.outline ?? []).slice(0, 8);
|
|
518
|
+
const sectionHint = sections.length > 0 ? `
|
|
519
|
+
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
520
|
+
return `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})
|
|
521
|
+
doc_id: \`${d.id}\`` + (d.snippet ? `
|
|
522
|
+
> ${d.snippet}` : "") + sectionHint;
|
|
523
|
+
}).join("\n");
|
|
502
524
|
return {
|
|
503
525
|
content: [
|
|
504
526
|
{
|
|
@@ -507,7 +529,7 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
507
529
|
|
|
508
530
|
${text}
|
|
509
531
|
|
|
510
|
-
Use doc_id to
|
|
532
|
+
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
511
533
|
}
|
|
512
534
|
]
|
|
513
535
|
};
|
|
@@ -908,18 +930,41 @@ ${spec.taskContent}`);
|
|
|
908
930
|
if (!body) throw new Error("spec has no content");
|
|
909
931
|
return body;
|
|
910
932
|
}
|
|
911
|
-
async function fetchDocBody(api, docId) {
|
|
933
|
+
async function fetchDocBody(api, docId, anchor) {
|
|
912
934
|
const doc = await api.get(
|
|
913
|
-
`/v1/docs/${encodeURIComponent(docId)}/content
|
|
935
|
+
`/v1/docs/${encodeURIComponent(docId)}/content`,
|
|
936
|
+
anchor ? { section: anchor } : void 0
|
|
914
937
|
);
|
|
915
938
|
if (doc.error) throw new Error(doc.error);
|
|
916
939
|
const body = (doc.content ?? "").trim();
|
|
917
940
|
if (!body) throw new Error("document is empty");
|
|
918
941
|
return body;
|
|
919
942
|
}
|
|
943
|
+
function githubDeepLink(repoFullName, path, anchor) {
|
|
944
|
+
return `https://github.com/${repoFullName}/blob/HEAD/${path}#${anchor}`;
|
|
945
|
+
}
|
|
946
|
+
function formatSectionPointer(repoFullName, path, s) {
|
|
947
|
+
return `\`${path}#${s.anchor}\` \xB7 \`chunk_id: ${s.chunkId}\` \xB7 [${s.heading}](${githubDeepLink(repoFullName, path, s.anchor)})`;
|
|
948
|
+
}
|
|
949
|
+
function selectSection(outline, taskDescription) {
|
|
950
|
+
if (!outline || outline.length === 0) return void 0;
|
|
951
|
+
const keywords = taskDescription.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !SECTION_STOP_WORDS.has(w));
|
|
952
|
+
let best = outline[0];
|
|
953
|
+
let bestScore = -1;
|
|
954
|
+
for (const s of outline) {
|
|
955
|
+
const heading = s.heading.toLowerCase();
|
|
956
|
+
let score = 0;
|
|
957
|
+
for (const kw of keywords) if (heading.includes(kw)) score++;
|
|
958
|
+
if (score > bestScore) {
|
|
959
|
+
best = s;
|
|
960
|
+
bestScore = score;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return best;
|
|
964
|
+
}
|
|
920
965
|
async function hydrate(api, item, meta) {
|
|
921
966
|
try {
|
|
922
|
-
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id);
|
|
967
|
+
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id, meta.section?.anchor);
|
|
923
968
|
return { item, meta, body };
|
|
924
969
|
} catch (err) {
|
|
925
970
|
return {
|
|
@@ -967,12 +1012,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
967
1012
|
};
|
|
968
1013
|
}),
|
|
969
1014
|
...docs.items.map((d) => {
|
|
1015
|
+
const section = selectSection(d.outline, taskDescription);
|
|
1016
|
+
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
970
1017
|
refs.set(key("doc", d.id), {
|
|
971
1018
|
type: "doc",
|
|
972
1019
|
id: d.id,
|
|
973
1020
|
title: d.title,
|
|
974
|
-
|
|
975
|
-
|
|
1021
|
+
section,
|
|
1022
|
+
pointer,
|
|
1023
|
+
listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
|
|
1024
|
+
\u21B3 ${pointer}` : ""),
|
|
1025
|
+
fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
|
|
976
1026
|
});
|
|
977
1027
|
return {
|
|
978
1028
|
type: "doc",
|
|
@@ -1045,6 +1095,8 @@ ${formatStack(stack)}`;
|
|
|
1045
1095
|
const blocks = [];
|
|
1046
1096
|
for (const h of hydrated) {
|
|
1047
1097
|
itemsLeft--;
|
|
1098
|
+
const headline = h.meta.pointer ? `${h.item.content}
|
|
1099
|
+
${h.meta.pointer}` : h.item.content;
|
|
1048
1100
|
const emit = (block) => {
|
|
1049
1101
|
bodyBudget -= Math.max(
|
|
1050
1102
|
0,
|
|
@@ -1055,7 +1107,7 @@ ${formatStack(stack)}`;
|
|
|
1055
1107
|
if (h.error !== void 0) {
|
|
1056
1108
|
bodyFailures++;
|
|
1057
1109
|
emit(
|
|
1058
|
-
`${
|
|
1110
|
+
`${headline}
|
|
1059
1111
|
|
|
1060
1112
|
_Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
1061
1113
|
);
|
|
@@ -1068,7 +1120,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
|
1068
1120
|
);
|
|
1069
1121
|
if (allowance < MIN_BODY_TOKENS) {
|
|
1070
1122
|
emit(
|
|
1071
|
-
`${
|
|
1123
|
+
`${headline}
|
|
1072
1124
|
|
|
1073
1125
|
_Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
|
|
1074
1126
|
);
|
|
@@ -1080,7 +1132,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
|
|
|
1080
1132
|
);
|
|
1081
1133
|
itemsWithBody++;
|
|
1082
1134
|
emit(
|
|
1083
|
-
`${
|
|
1135
|
+
`${headline}
|
|
1084
1136
|
|
|
1085
1137
|
${body2}` + (truncated ? `
|
|
1086
1138
|
|
|
@@ -1099,7 +1151,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
|
|
|
1099
1151
|
sections.push(
|
|
1100
1152
|
`## Relevant Specs & Docs (${blocks.length})
|
|
1101
1153
|
|
|
1102
|
-
|
|
1154
|
+
_Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
|
|
1103
1155
|
|
|
1104
1156
|
` + blocks.join("\n\n---\n\n")
|
|
1105
1157
|
);
|
|
@@ -1142,7 +1194,7 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
|
1142
1194
|
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
1143
1195
|
return { text: body + footer, stats };
|
|
1144
1196
|
}
|
|
1145
|
-
var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS;
|
|
1197
|
+
var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, SECTION_STOP_WORDS;
|
|
1146
1198
|
var init_context_builder = __esm({
|
|
1147
1199
|
"src/lib/context-builder.ts"() {
|
|
1148
1200
|
"use strict";
|
|
@@ -1156,6 +1208,28 @@ var init_context_builder = __esm({
|
|
|
1156
1208
|
MAX_TAIL = 10;
|
|
1157
1209
|
TAIL_SHARE = 0.15;
|
|
1158
1210
|
TRUNCATION_NOTICE_TOKENS = 30;
|
|
1211
|
+
SECTION_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1212
|
+
"the",
|
|
1213
|
+
"a",
|
|
1214
|
+
"an",
|
|
1215
|
+
"of",
|
|
1216
|
+
"to",
|
|
1217
|
+
"and",
|
|
1218
|
+
"or",
|
|
1219
|
+
"for",
|
|
1220
|
+
"in",
|
|
1221
|
+
"on",
|
|
1222
|
+
"with",
|
|
1223
|
+
"how",
|
|
1224
|
+
"why",
|
|
1225
|
+
"what",
|
|
1226
|
+
"add",
|
|
1227
|
+
"new",
|
|
1228
|
+
"use",
|
|
1229
|
+
"using",
|
|
1230
|
+
"update",
|
|
1231
|
+
"fix"
|
|
1232
|
+
]);
|
|
1159
1233
|
}
|
|
1160
1234
|
});
|
|
1161
1235
|
|
package/dist/http.js
CHANGED
|
@@ -384,20 +384,28 @@ function registerGetDocs(server, apiClient) {
|
|
|
384
384
|
"get_docs",
|
|
385
385
|
{
|
|
386
386
|
title: "Get Docs",
|
|
387
|
-
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search.",
|
|
387
|
+
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search. Pass doc_id with section (a heading anchor) or chunk_id to retrieve just that section instead of the whole document.",
|
|
388
388
|
inputSchema: {
|
|
389
389
|
repo: z3.string().optional().describe("Filter by repo (owner/repo format)"),
|
|
390
390
|
path: z3.string().optional().describe("File path glob pattern (e.g., docs/adr/*.md)"),
|
|
391
391
|
search: z3.string().optional().describe("Keyword search across document content"),
|
|
392
392
|
doc_id: z3.string().optional().describe(
|
|
393
393
|
"Specific document ID to retrieve full content"
|
|
394
|
+
),
|
|
395
|
+
section: z3.string().optional().describe(
|
|
396
|
+
"Heading anchor (e.g. 'rollback-strategy') \u2014 with doc_id, returns just that section"
|
|
397
|
+
),
|
|
398
|
+
chunk_id: z3.string().optional().describe(
|
|
399
|
+
"Section chunk id ('{path_hash}#{anchor}') from get_context \u2014 with doc_id, returns just that section"
|
|
394
400
|
)
|
|
395
401
|
}
|
|
396
402
|
},
|
|
397
|
-
async ({ repo, path, search, doc_id }) => {
|
|
403
|
+
async ({ repo, path, search, doc_id, section, chunk_id }) => {
|
|
398
404
|
if (doc_id) {
|
|
405
|
+
const anchor = section ?? (chunk_id ? chunk_id.split("#").pop() : void 0);
|
|
399
406
|
const doc = await apiClient.get(
|
|
400
|
-
`/v1/docs/${encodeURIComponent(doc_id)}/content
|
|
407
|
+
`/v1/docs/${encodeURIComponent(doc_id)}/content`,
|
|
408
|
+
anchor ? { section: anchor } : void 0
|
|
401
409
|
);
|
|
402
410
|
if (doc.error) {
|
|
403
411
|
return {
|
|
@@ -410,8 +418,18 @@ function registerGetDocs(server, apiClient) {
|
|
|
410
418
|
isError: true
|
|
411
419
|
};
|
|
412
420
|
}
|
|
421
|
+
let text2 = doc.content;
|
|
422
|
+
if (anchor && doc.section_resolved === false) {
|
|
423
|
+
text2 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
424
|
+
|
|
425
|
+
` + text2;
|
|
426
|
+
} else if (anchor && doc.heading) {
|
|
427
|
+
text2 = `## ${doc.heading}
|
|
428
|
+
|
|
429
|
+
${doc.content}`;
|
|
430
|
+
}
|
|
413
431
|
return {
|
|
414
|
-
content: [{ type: "text", text:
|
|
432
|
+
content: [{ type: "text", text: text2 }]
|
|
415
433
|
};
|
|
416
434
|
}
|
|
417
435
|
const params = {};
|
|
@@ -435,10 +453,14 @@ function registerGetDocs(server, apiClient) {
|
|
|
435
453
|
]
|
|
436
454
|
};
|
|
437
455
|
}
|
|
438
|
-
const text = items.map(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
456
|
+
const text = items.map((d) => {
|
|
457
|
+
const sections = (d.outline ?? []).slice(0, 8);
|
|
458
|
+
const sectionHint = sections.length > 0 ? `
|
|
459
|
+
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
460
|
+
return `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})
|
|
461
|
+
doc_id: \`${d.id}\`` + (d.snippet ? `
|
|
462
|
+
> ${d.snippet}` : "") + sectionHint;
|
|
463
|
+
}).join("\n");
|
|
442
464
|
return {
|
|
443
465
|
content: [
|
|
444
466
|
{
|
|
@@ -447,7 +469,7 @@ function registerGetDocs(server, apiClient) {
|
|
|
447
469
|
|
|
448
470
|
${text}
|
|
449
471
|
|
|
450
|
-
Use doc_id to
|
|
472
|
+
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
451
473
|
}
|
|
452
474
|
]
|
|
453
475
|
};
|
|
@@ -837,18 +859,63 @@ ${spec.taskContent}`);
|
|
|
837
859
|
if (!body) throw new Error("spec has no content");
|
|
838
860
|
return body;
|
|
839
861
|
}
|
|
840
|
-
async function fetchDocBody(api, docId) {
|
|
862
|
+
async function fetchDocBody(api, docId, anchor) {
|
|
841
863
|
const doc = await api.get(
|
|
842
|
-
`/v1/docs/${encodeURIComponent(docId)}/content
|
|
864
|
+
`/v1/docs/${encodeURIComponent(docId)}/content`,
|
|
865
|
+
anchor ? { section: anchor } : void 0
|
|
843
866
|
);
|
|
844
867
|
if (doc.error) throw new Error(doc.error);
|
|
845
868
|
const body = (doc.content ?? "").trim();
|
|
846
869
|
if (!body) throw new Error("document is empty");
|
|
847
870
|
return body;
|
|
848
871
|
}
|
|
872
|
+
function githubDeepLink(repoFullName, path, anchor) {
|
|
873
|
+
return `https://github.com/${repoFullName}/blob/HEAD/${path}#${anchor}`;
|
|
874
|
+
}
|
|
875
|
+
function formatSectionPointer(repoFullName, path, s) {
|
|
876
|
+
return `\`${path}#${s.anchor}\` \xB7 \`chunk_id: ${s.chunkId}\` \xB7 [${s.heading}](${githubDeepLink(repoFullName, path, s.anchor)})`;
|
|
877
|
+
}
|
|
878
|
+
var SECTION_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
879
|
+
"the",
|
|
880
|
+
"a",
|
|
881
|
+
"an",
|
|
882
|
+
"of",
|
|
883
|
+
"to",
|
|
884
|
+
"and",
|
|
885
|
+
"or",
|
|
886
|
+
"for",
|
|
887
|
+
"in",
|
|
888
|
+
"on",
|
|
889
|
+
"with",
|
|
890
|
+
"how",
|
|
891
|
+
"why",
|
|
892
|
+
"what",
|
|
893
|
+
"add",
|
|
894
|
+
"new",
|
|
895
|
+
"use",
|
|
896
|
+
"using",
|
|
897
|
+
"update",
|
|
898
|
+
"fix"
|
|
899
|
+
]);
|
|
900
|
+
function selectSection(outline, taskDescription) {
|
|
901
|
+
if (!outline || outline.length === 0) return void 0;
|
|
902
|
+
const keywords = taskDescription.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !SECTION_STOP_WORDS.has(w));
|
|
903
|
+
let best = outline[0];
|
|
904
|
+
let bestScore = -1;
|
|
905
|
+
for (const s of outline) {
|
|
906
|
+
const heading = s.heading.toLowerCase();
|
|
907
|
+
let score = 0;
|
|
908
|
+
for (const kw of keywords) if (heading.includes(kw)) score++;
|
|
909
|
+
if (score > bestScore) {
|
|
910
|
+
best = s;
|
|
911
|
+
bestScore = score;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return best;
|
|
915
|
+
}
|
|
849
916
|
async function hydrate(api, item, meta) {
|
|
850
917
|
try {
|
|
851
|
-
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id);
|
|
918
|
+
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id, meta.section?.anchor);
|
|
852
919
|
return { item, meta, body };
|
|
853
920
|
} catch (err) {
|
|
854
921
|
return {
|
|
@@ -896,12 +963,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
896
963
|
};
|
|
897
964
|
}),
|
|
898
965
|
...docs.items.map((d) => {
|
|
966
|
+
const section = selectSection(d.outline, taskDescription);
|
|
967
|
+
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
899
968
|
refs.set(key("doc", d.id), {
|
|
900
969
|
type: "doc",
|
|
901
970
|
id: d.id,
|
|
902
971
|
title: d.title,
|
|
903
|
-
|
|
904
|
-
|
|
972
|
+
section,
|
|
973
|
+
pointer,
|
|
974
|
+
listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
|
|
975
|
+
\u21B3 ${pointer}` : ""),
|
|
976
|
+
fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
|
|
905
977
|
});
|
|
906
978
|
return {
|
|
907
979
|
type: "doc",
|
|
@@ -974,6 +1046,8 @@ ${formatStack(stack)}`;
|
|
|
974
1046
|
const blocks = [];
|
|
975
1047
|
for (const h of hydrated) {
|
|
976
1048
|
itemsLeft--;
|
|
1049
|
+
const headline = h.meta.pointer ? `${h.item.content}
|
|
1050
|
+
${h.meta.pointer}` : h.item.content;
|
|
977
1051
|
const emit = (block) => {
|
|
978
1052
|
bodyBudget -= Math.max(
|
|
979
1053
|
0,
|
|
@@ -984,7 +1058,7 @@ ${formatStack(stack)}`;
|
|
|
984
1058
|
if (h.error !== void 0) {
|
|
985
1059
|
bodyFailures++;
|
|
986
1060
|
emit(
|
|
987
|
-
`${
|
|
1061
|
+
`${headline}
|
|
988
1062
|
|
|
989
1063
|
_Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
990
1064
|
);
|
|
@@ -997,7 +1071,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
|
997
1071
|
);
|
|
998
1072
|
if (allowance < MIN_BODY_TOKENS) {
|
|
999
1073
|
emit(
|
|
1000
|
-
`${
|
|
1074
|
+
`${headline}
|
|
1001
1075
|
|
|
1002
1076
|
_Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
|
|
1003
1077
|
);
|
|
@@ -1009,7 +1083,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
|
|
|
1009
1083
|
);
|
|
1010
1084
|
itemsWithBody++;
|
|
1011
1085
|
emit(
|
|
1012
|
-
`${
|
|
1086
|
+
`${headline}
|
|
1013
1087
|
|
|
1014
1088
|
${body2}` + (truncated ? `
|
|
1015
1089
|
|
|
@@ -1028,7 +1102,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
|
|
|
1028
1102
|
sections.push(
|
|
1029
1103
|
`## Relevant Specs & Docs (${blocks.length})
|
|
1030
1104
|
|
|
1031
|
-
|
|
1105
|
+
_Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
|
|
1032
1106
|
|
|
1033
1107
|
` + blocks.join("\n\n---\n\n")
|
|
1034
1108
|
);
|
package/dist/index.js
CHANGED
|
@@ -400,20 +400,28 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
400
400
|
"get_docs",
|
|
401
401
|
{
|
|
402
402
|
title: "Get Docs",
|
|
403
|
-
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search.",
|
|
403
|
+
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search. Pass doc_id with section (a heading anchor) or chunk_id to retrieve just that section instead of the whole document.",
|
|
404
404
|
inputSchema: {
|
|
405
405
|
repo: z3.string().optional().describe("Filter by repo (owner/repo format)"),
|
|
406
406
|
path: z3.string().optional().describe("File path glob pattern (e.g., docs/adr/*.md)"),
|
|
407
407
|
search: z3.string().optional().describe("Keyword search across document content"),
|
|
408
408
|
doc_id: z3.string().optional().describe(
|
|
409
409
|
"Specific document ID to retrieve full content"
|
|
410
|
+
),
|
|
411
|
+
section: z3.string().optional().describe(
|
|
412
|
+
"Heading anchor (e.g. 'rollback-strategy') \u2014 with doc_id, returns just that section"
|
|
413
|
+
),
|
|
414
|
+
chunk_id: z3.string().optional().describe(
|
|
415
|
+
"Section chunk id ('{path_hash}#{anchor}') from get_context \u2014 with doc_id, returns just that section"
|
|
410
416
|
)
|
|
411
417
|
}
|
|
412
418
|
},
|
|
413
|
-
async ({ repo, path, search, doc_id }) => {
|
|
419
|
+
async ({ repo, path, search, doc_id, section, chunk_id }) => {
|
|
414
420
|
if (doc_id) {
|
|
421
|
+
const anchor = section ?? (chunk_id ? chunk_id.split("#").pop() : void 0);
|
|
415
422
|
const doc = await apiClient2.get(
|
|
416
|
-
`/v1/docs/${encodeURIComponent(doc_id)}/content
|
|
423
|
+
`/v1/docs/${encodeURIComponent(doc_id)}/content`,
|
|
424
|
+
anchor ? { section: anchor } : void 0
|
|
417
425
|
);
|
|
418
426
|
if (doc.error) {
|
|
419
427
|
return {
|
|
@@ -426,8 +434,18 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
426
434
|
isError: true
|
|
427
435
|
};
|
|
428
436
|
}
|
|
437
|
+
let text2 = doc.content;
|
|
438
|
+
if (anchor && doc.section_resolved === false) {
|
|
439
|
+
text2 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
440
|
+
|
|
441
|
+
` + text2;
|
|
442
|
+
} else if (anchor && doc.heading) {
|
|
443
|
+
text2 = `## ${doc.heading}
|
|
444
|
+
|
|
445
|
+
${doc.content}`;
|
|
446
|
+
}
|
|
429
447
|
return {
|
|
430
|
-
content: [{ type: "text", text:
|
|
448
|
+
content: [{ type: "text", text: text2 }]
|
|
431
449
|
};
|
|
432
450
|
}
|
|
433
451
|
const params = {};
|
|
@@ -451,10 +469,14 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
451
469
|
]
|
|
452
470
|
};
|
|
453
471
|
}
|
|
454
|
-
const text = items.map(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
472
|
+
const text = items.map((d) => {
|
|
473
|
+
const sections = (d.outline ?? []).slice(0, 8);
|
|
474
|
+
const sectionHint = sections.length > 0 ? `
|
|
475
|
+
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
476
|
+
return `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})
|
|
477
|
+
doc_id: \`${d.id}\`` + (d.snippet ? `
|
|
478
|
+
> ${d.snippet}` : "") + sectionHint;
|
|
479
|
+
}).join("\n");
|
|
458
480
|
return {
|
|
459
481
|
content: [
|
|
460
482
|
{
|
|
@@ -463,7 +485,7 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
463
485
|
|
|
464
486
|
${text}
|
|
465
487
|
|
|
466
|
-
Use doc_id to
|
|
488
|
+
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
467
489
|
}
|
|
468
490
|
]
|
|
469
491
|
};
|
|
@@ -853,18 +875,63 @@ ${spec.taskContent}`);
|
|
|
853
875
|
if (!body) throw new Error("spec has no content");
|
|
854
876
|
return body;
|
|
855
877
|
}
|
|
856
|
-
async function fetchDocBody(api, docId) {
|
|
878
|
+
async function fetchDocBody(api, docId, anchor) {
|
|
857
879
|
const doc = await api.get(
|
|
858
|
-
`/v1/docs/${encodeURIComponent(docId)}/content
|
|
880
|
+
`/v1/docs/${encodeURIComponent(docId)}/content`,
|
|
881
|
+
anchor ? { section: anchor } : void 0
|
|
859
882
|
);
|
|
860
883
|
if (doc.error) throw new Error(doc.error);
|
|
861
884
|
const body = (doc.content ?? "").trim();
|
|
862
885
|
if (!body) throw new Error("document is empty");
|
|
863
886
|
return body;
|
|
864
887
|
}
|
|
888
|
+
function githubDeepLink(repoFullName, path, anchor) {
|
|
889
|
+
return `https://github.com/${repoFullName}/blob/HEAD/${path}#${anchor}`;
|
|
890
|
+
}
|
|
891
|
+
function formatSectionPointer(repoFullName, path, s) {
|
|
892
|
+
return `\`${path}#${s.anchor}\` \xB7 \`chunk_id: ${s.chunkId}\` \xB7 [${s.heading}](${githubDeepLink(repoFullName, path, s.anchor)})`;
|
|
893
|
+
}
|
|
894
|
+
var SECTION_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
895
|
+
"the",
|
|
896
|
+
"a",
|
|
897
|
+
"an",
|
|
898
|
+
"of",
|
|
899
|
+
"to",
|
|
900
|
+
"and",
|
|
901
|
+
"or",
|
|
902
|
+
"for",
|
|
903
|
+
"in",
|
|
904
|
+
"on",
|
|
905
|
+
"with",
|
|
906
|
+
"how",
|
|
907
|
+
"why",
|
|
908
|
+
"what",
|
|
909
|
+
"add",
|
|
910
|
+
"new",
|
|
911
|
+
"use",
|
|
912
|
+
"using",
|
|
913
|
+
"update",
|
|
914
|
+
"fix"
|
|
915
|
+
]);
|
|
916
|
+
function selectSection(outline, taskDescription) {
|
|
917
|
+
if (!outline || outline.length === 0) return void 0;
|
|
918
|
+
const keywords = taskDescription.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !SECTION_STOP_WORDS.has(w));
|
|
919
|
+
let best = outline[0];
|
|
920
|
+
let bestScore = -1;
|
|
921
|
+
for (const s of outline) {
|
|
922
|
+
const heading = s.heading.toLowerCase();
|
|
923
|
+
let score = 0;
|
|
924
|
+
for (const kw of keywords) if (heading.includes(kw)) score++;
|
|
925
|
+
if (score > bestScore) {
|
|
926
|
+
best = s;
|
|
927
|
+
bestScore = score;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return best;
|
|
931
|
+
}
|
|
865
932
|
async function hydrate(api, item, meta) {
|
|
866
933
|
try {
|
|
867
|
-
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id);
|
|
934
|
+
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id, meta.section?.anchor);
|
|
868
935
|
return { item, meta, body };
|
|
869
936
|
} catch (err) {
|
|
870
937
|
return {
|
|
@@ -912,12 +979,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
912
979
|
};
|
|
913
980
|
}),
|
|
914
981
|
...docs.items.map((d) => {
|
|
982
|
+
const section = selectSection(d.outline, taskDescription);
|
|
983
|
+
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
915
984
|
refs.set(key("doc", d.id), {
|
|
916
985
|
type: "doc",
|
|
917
986
|
id: d.id,
|
|
918
987
|
title: d.title,
|
|
919
|
-
|
|
920
|
-
|
|
988
|
+
section,
|
|
989
|
+
pointer,
|
|
990
|
+
listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
|
|
991
|
+
\u21B3 ${pointer}` : ""),
|
|
992
|
+
fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
|
|
921
993
|
});
|
|
922
994
|
return {
|
|
923
995
|
type: "doc",
|
|
@@ -990,6 +1062,8 @@ ${formatStack(stack)}`;
|
|
|
990
1062
|
const blocks = [];
|
|
991
1063
|
for (const h of hydrated) {
|
|
992
1064
|
itemsLeft--;
|
|
1065
|
+
const headline = h.meta.pointer ? `${h.item.content}
|
|
1066
|
+
${h.meta.pointer}` : h.item.content;
|
|
993
1067
|
const emit = (block) => {
|
|
994
1068
|
bodyBudget -= Math.max(
|
|
995
1069
|
0,
|
|
@@ -1000,7 +1074,7 @@ ${formatStack(stack)}`;
|
|
|
1000
1074
|
if (h.error !== void 0) {
|
|
1001
1075
|
bodyFailures++;
|
|
1002
1076
|
emit(
|
|
1003
|
-
`${
|
|
1077
|
+
`${headline}
|
|
1004
1078
|
|
|
1005
1079
|
_Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
1006
1080
|
);
|
|
@@ -1013,7 +1087,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
|
1013
1087
|
);
|
|
1014
1088
|
if (allowance < MIN_BODY_TOKENS) {
|
|
1015
1089
|
emit(
|
|
1016
|
-
`${
|
|
1090
|
+
`${headline}
|
|
1017
1091
|
|
|
1018
1092
|
_Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
|
|
1019
1093
|
);
|
|
@@ -1025,7 +1099,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
|
|
|
1025
1099
|
);
|
|
1026
1100
|
itemsWithBody++;
|
|
1027
1101
|
emit(
|
|
1028
|
-
`${
|
|
1102
|
+
`${headline}
|
|
1029
1103
|
|
|
1030
1104
|
${body2}` + (truncated ? `
|
|
1031
1105
|
|
|
@@ -1044,7 +1118,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
|
|
|
1044
1118
|
sections.push(
|
|
1045
1119
|
`## Relevant Specs & Docs (${blocks.length})
|
|
1046
1120
|
|
|
1047
|
-
|
|
1121
|
+
_Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
|
|
1048
1122
|
|
|
1049
1123
|
` + blocks.join("\n\n---\n\n")
|
|
1050
1124
|
);
|
|
@@ -23,6 +23,13 @@ export interface SpecSummary {
|
|
|
23
23
|
requirementCount: number;
|
|
24
24
|
updatedAt: string;
|
|
25
25
|
}
|
|
26
|
+
/** One heading-scoped section of a document (#865). */
|
|
27
|
+
export interface DocSection {
|
|
28
|
+
heading: string;
|
|
29
|
+
level: number;
|
|
30
|
+
anchor: string;
|
|
31
|
+
chunkId: string;
|
|
32
|
+
}
|
|
26
33
|
export interface DocListResponse {
|
|
27
34
|
items: Array<{
|
|
28
35
|
id: string;
|
|
@@ -32,6 +39,8 @@ export interface DocListResponse {
|
|
|
32
39
|
doc_type: string;
|
|
33
40
|
size: number;
|
|
34
41
|
synced_at: string;
|
|
42
|
+
/** Structured section outline, when the doc has been re-indexed (#865). */
|
|
43
|
+
outline?: DocSection[];
|
|
35
44
|
}>;
|
|
36
45
|
}
|
|
37
46
|
export interface TechStackEntry {
|
|
@@ -335,18 +335,63 @@ ${spec.taskContent}`);
|
|
|
335
335
|
if (!body) throw new Error("spec has no content");
|
|
336
336
|
return body;
|
|
337
337
|
}
|
|
338
|
-
async function fetchDocBody(api, docId) {
|
|
338
|
+
async function fetchDocBody(api, docId, anchor) {
|
|
339
339
|
const doc = await api.get(
|
|
340
|
-
`/v1/docs/${encodeURIComponent(docId)}/content
|
|
340
|
+
`/v1/docs/${encodeURIComponent(docId)}/content`,
|
|
341
|
+
anchor ? { section: anchor } : void 0
|
|
341
342
|
);
|
|
342
343
|
if (doc.error) throw new Error(doc.error);
|
|
343
344
|
const body = (doc.content ?? "").trim();
|
|
344
345
|
if (!body) throw new Error("document is empty");
|
|
345
346
|
return body;
|
|
346
347
|
}
|
|
348
|
+
function githubDeepLink(repoFullName, path, anchor) {
|
|
349
|
+
return `https://github.com/${repoFullName}/blob/HEAD/${path}#${anchor}`;
|
|
350
|
+
}
|
|
351
|
+
function formatSectionPointer(repoFullName, path, s) {
|
|
352
|
+
return `\`${path}#${s.anchor}\` \xB7 \`chunk_id: ${s.chunkId}\` \xB7 [${s.heading}](${githubDeepLink(repoFullName, path, s.anchor)})`;
|
|
353
|
+
}
|
|
354
|
+
var SECTION_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
355
|
+
"the",
|
|
356
|
+
"a",
|
|
357
|
+
"an",
|
|
358
|
+
"of",
|
|
359
|
+
"to",
|
|
360
|
+
"and",
|
|
361
|
+
"or",
|
|
362
|
+
"for",
|
|
363
|
+
"in",
|
|
364
|
+
"on",
|
|
365
|
+
"with",
|
|
366
|
+
"how",
|
|
367
|
+
"why",
|
|
368
|
+
"what",
|
|
369
|
+
"add",
|
|
370
|
+
"new",
|
|
371
|
+
"use",
|
|
372
|
+
"using",
|
|
373
|
+
"update",
|
|
374
|
+
"fix"
|
|
375
|
+
]);
|
|
376
|
+
function selectSection(outline, taskDescription) {
|
|
377
|
+
if (!outline || outline.length === 0) return void 0;
|
|
378
|
+
const keywords = taskDescription.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !SECTION_STOP_WORDS.has(w));
|
|
379
|
+
let best = outline[0];
|
|
380
|
+
let bestScore = -1;
|
|
381
|
+
for (const s of outline) {
|
|
382
|
+
const heading = s.heading.toLowerCase();
|
|
383
|
+
let score = 0;
|
|
384
|
+
for (const kw of keywords) if (heading.includes(kw)) score++;
|
|
385
|
+
if (score > bestScore) {
|
|
386
|
+
best = s;
|
|
387
|
+
bestScore = score;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return best;
|
|
391
|
+
}
|
|
347
392
|
async function hydrate(api, item, meta) {
|
|
348
393
|
try {
|
|
349
|
-
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id);
|
|
394
|
+
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id, meta.section?.anchor);
|
|
350
395
|
return { item, meta, body };
|
|
351
396
|
} catch (err) {
|
|
352
397
|
return {
|
|
@@ -394,12 +439,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
394
439
|
};
|
|
395
440
|
}),
|
|
396
441
|
...docs.items.map((d) => {
|
|
442
|
+
const section = selectSection(d.outline, taskDescription);
|
|
443
|
+
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
397
444
|
refs.set(key("doc", d.id), {
|
|
398
445
|
type: "doc",
|
|
399
446
|
id: d.id,
|
|
400
447
|
title: d.title,
|
|
401
|
-
|
|
402
|
-
|
|
448
|
+
section,
|
|
449
|
+
pointer,
|
|
450
|
+
listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
|
|
451
|
+
\u21B3 ${pointer}` : ""),
|
|
452
|
+
fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
|
|
403
453
|
});
|
|
404
454
|
return {
|
|
405
455
|
type: "doc",
|
|
@@ -472,6 +522,8 @@ ${formatStack(stack)}`;
|
|
|
472
522
|
const blocks = [];
|
|
473
523
|
for (const h of hydrated) {
|
|
474
524
|
itemsLeft--;
|
|
525
|
+
const headline = h.meta.pointer ? `${h.item.content}
|
|
526
|
+
${h.meta.pointer}` : h.item.content;
|
|
475
527
|
const emit = (block) => {
|
|
476
528
|
bodyBudget -= Math.max(
|
|
477
529
|
0,
|
|
@@ -482,7 +534,7 @@ ${formatStack(stack)}`;
|
|
|
482
534
|
if (h.error !== void 0) {
|
|
483
535
|
bodyFailures++;
|
|
484
536
|
emit(
|
|
485
|
-
`${
|
|
537
|
+
`${headline}
|
|
486
538
|
|
|
487
539
|
_Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
488
540
|
);
|
|
@@ -495,7 +547,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
|
495
547
|
);
|
|
496
548
|
if (allowance < MIN_BODY_TOKENS) {
|
|
497
549
|
emit(
|
|
498
|
-
`${
|
|
550
|
+
`${headline}
|
|
499
551
|
|
|
500
552
|
_Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
|
|
501
553
|
);
|
|
@@ -507,7 +559,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
|
|
|
507
559
|
);
|
|
508
560
|
itemsWithBody++;
|
|
509
561
|
emit(
|
|
510
|
-
`${
|
|
562
|
+
`${headline}
|
|
511
563
|
|
|
512
564
|
${body2}` + (truncated ? `
|
|
513
565
|
|
|
@@ -526,7 +578,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
|
|
|
526
578
|
sections.push(
|
|
527
579
|
`## Relevant Specs & Docs (${blocks.length})
|
|
528
580
|
|
|
529
|
-
|
|
581
|
+
_Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
|
|
530
582
|
|
|
531
583
|
` + blocks.join("\n\n---\n\n")
|
|
532
584
|
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/tools/get-docs.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
function matchGlob(path, pattern) {
|
|
6
|
+
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
7
|
+
return new RegExp(`^${regex}$`).test(path);
|
|
8
|
+
}
|
|
9
|
+
function registerGetDocs(server, apiClient) {
|
|
10
|
+
server.registerTool(
|
|
11
|
+
"get_docs",
|
|
12
|
+
{
|
|
13
|
+
title: "Get Docs",
|
|
14
|
+
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search. Pass doc_id with section (a heading anchor) or chunk_id to retrieve just that section instead of the whole document.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
repo: z.string().optional().describe("Filter by repo (owner/repo format)"),
|
|
17
|
+
path: z.string().optional().describe("File path glob pattern (e.g., docs/adr/*.md)"),
|
|
18
|
+
search: z.string().optional().describe("Keyword search across document content"),
|
|
19
|
+
doc_id: z.string().optional().describe(
|
|
20
|
+
"Specific document ID to retrieve full content"
|
|
21
|
+
),
|
|
22
|
+
section: z.string().optional().describe(
|
|
23
|
+
"Heading anchor (e.g. 'rollback-strategy') \u2014 with doc_id, returns just that section"
|
|
24
|
+
),
|
|
25
|
+
chunk_id: z.string().optional().describe(
|
|
26
|
+
"Section chunk id ('{path_hash}#{anchor}') from get_context \u2014 with doc_id, returns just that section"
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async ({ repo, path, search, doc_id, section, chunk_id }) => {
|
|
31
|
+
if (doc_id) {
|
|
32
|
+
const anchor = section ?? (chunk_id ? chunk_id.split("#").pop() : void 0);
|
|
33
|
+
const doc = await apiClient.get(
|
|
34
|
+
`/v1/docs/${encodeURIComponent(doc_id)}/content`,
|
|
35
|
+
anchor ? { section: anchor } : void 0
|
|
36
|
+
);
|
|
37
|
+
if (doc.error) {
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `Error retrieving document: ${doc.error}`
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
isError: true
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
let text2 = doc.content;
|
|
49
|
+
if (anchor && doc.section_resolved === false) {
|
|
50
|
+
text2 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
51
|
+
|
|
52
|
+
` + text2;
|
|
53
|
+
} else if (anchor && doc.heading) {
|
|
54
|
+
text2 = `## ${doc.heading}
|
|
55
|
+
|
|
56
|
+
${doc.content}`;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: text2 }]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const params = {};
|
|
63
|
+
if (repo) params.repo = repo;
|
|
64
|
+
if (search) params.q = search;
|
|
65
|
+
const response = await apiClient.get(
|
|
66
|
+
"/v1/docs",
|
|
67
|
+
params
|
|
68
|
+
);
|
|
69
|
+
let items = response.items;
|
|
70
|
+
if (path) {
|
|
71
|
+
items = items.filter((item) => matchGlob(item.path, path));
|
|
72
|
+
}
|
|
73
|
+
if (items.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `No documents found${repo ? ` in ${repo}` : ""}${path ? ` matching "${path}"` : ""}${search ? ` for "${search}"` : ""}.`
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const text = items.map((d) => {
|
|
84
|
+
const sections = (d.outline ?? []).slice(0, 8);
|
|
85
|
+
const sectionHint = sections.length > 0 ? `
|
|
86
|
+
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
87
|
+
return `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})
|
|
88
|
+
doc_id: \`${d.id}\`` + (d.snippet ? `
|
|
89
|
+
> ${d.snippet}` : "") + sectionHint;
|
|
90
|
+
}).join("\n");
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: `Found ${items.length} document(s) (${response.total} total):
|
|
96
|
+
|
|
97
|
+
${text}
|
|
98
|
+
|
|
99
|
+
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
registerGetDocs
|
|
108
|
+
};
|
package/package.json
CHANGED