@systemdox/mcp-server 0.1.12 → 0.1.14

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 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: doc.content }]
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
- (d) => `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})` + (d.snippet ? `
500
- > ${d.snippet}` : "")
501
- ).join("\n");
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 retrieve full content.`
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 {
@@ -931,13 +976,19 @@ async function hydrate(api, item, meta) {
931
976
  }
932
977
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
933
978
  const budget = maxTokens ?? DEFAULT_BUDGET;
934
- const [guardrails, specs, docs, stack, rules] = await Promise.all([
979
+ const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
935
980
  api.get("/v1/guardrails").catch(() => []),
936
981
  api.get("/v1/specs").catch(() => []),
937
982
  api.get("/v1/docs").catch(() => ({ items: [] })),
938
983
  api.get("/v1/ai/settings/stack").catch(() => []),
939
- api.get("/v1/standards/rules").catch(() => [])
984
+ api.get("/v1/standards/rules").catch(() => []),
985
+ // Capability states — FAIL-OPEN: an unreadable config (older backend, a
986
+ // blip) gates nothing, it can never hide working context.
987
+ api.get("/v1/doc-repos").catch(() => null)
940
988
  ]);
989
+ const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
990
+ const guardrails = disabled("fitness") ? [] : rawGuardrails;
991
+ const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
941
992
  const refs = /* @__PURE__ */ new Map();
942
993
  const key = (type, id) => `${type}:${id}`;
943
994
  const scorable = [
@@ -967,12 +1018,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
967
1018
  };
968
1019
  }),
969
1020
  ...docs.items.map((d) => {
1021
+ const section = selectSection(d.outline, taskDescription);
1022
+ const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
970
1023
  refs.set(key("doc", d.id), {
971
1024
  type: "doc",
972
1025
  id: d.id,
973
1026
  title: d.title,
974
- listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)`,
975
- fetchHint: `get_docs(doc_id="${d.id}")`
1027
+ section,
1028
+ pointer,
1029
+ listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
1030
+ \u21B3 ${pointer}` : ""),
1031
+ fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
976
1032
  });
977
1033
  return {
978
1034
  type: "doc",
@@ -1045,6 +1101,8 @@ ${formatStack(stack)}`;
1045
1101
  const blocks = [];
1046
1102
  for (const h of hydrated) {
1047
1103
  itemsLeft--;
1104
+ const headline = h.meta.pointer ? `${h.item.content}
1105
+ ${h.meta.pointer}` : h.item.content;
1048
1106
  const emit = (block) => {
1049
1107
  bodyBudget -= Math.max(
1050
1108
  0,
@@ -1055,7 +1113,7 @@ ${formatStack(stack)}`;
1055
1113
  if (h.error !== void 0) {
1056
1114
  bodyFailures++;
1057
1115
  emit(
1058
- `${h.item.content}
1116
+ `${headline}
1059
1117
 
1060
1118
  _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
1061
1119
  );
@@ -1068,7 +1126,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
1068
1126
  );
1069
1127
  if (allowance < MIN_BODY_TOKENS) {
1070
1128
  emit(
1071
- `${h.item.content}
1129
+ `${headline}
1072
1130
 
1073
1131
  _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
1074
1132
  );
@@ -1080,7 +1138,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
1080
1138
  );
1081
1139
  itemsWithBody++;
1082
1140
  emit(
1083
- `${h.item.content}
1141
+ `${headline}
1084
1142
 
1085
1143
  ${body2}` + (truncated ? `
1086
1144
 
@@ -1099,7 +1157,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
1099
1157
  sections.push(
1100
1158
  `## Relevant Specs & Docs (${blocks.length})
1101
1159
 
1102
- _Full text of the highest-ranked items. Every item carries the ID to fetch it in full._
1160
+ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
1103
1161
 
1104
1162
  ` + blocks.join("\n\n---\n\n")
1105
1163
  );
@@ -1142,7 +1200,7 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
1142
1200
  _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
1143
1201
  return { text: body + footer, stats };
1144
1202
  }
1145
- var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS;
1203
+ var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, SECTION_STOP_WORDS;
1146
1204
  var init_context_builder = __esm({
1147
1205
  "src/lib/context-builder.ts"() {
1148
1206
  "use strict";
@@ -1156,6 +1214,28 @@ var init_context_builder = __esm({
1156
1214
  MAX_TAIL = 10;
1157
1215
  TAIL_SHARE = 0.15;
1158
1216
  TRUNCATION_NOTICE_TOKENS = 30;
1217
+ SECTION_STOP_WORDS = /* @__PURE__ */ new Set([
1218
+ "the",
1219
+ "a",
1220
+ "an",
1221
+ "of",
1222
+ "to",
1223
+ "and",
1224
+ "or",
1225
+ "for",
1226
+ "in",
1227
+ "on",
1228
+ "with",
1229
+ "how",
1230
+ "why",
1231
+ "what",
1232
+ "add",
1233
+ "new",
1234
+ "use",
1235
+ "using",
1236
+ "update",
1237
+ "fix"
1238
+ ]);
1159
1239
  }
1160
1240
  });
1161
1241
 
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: doc.content }]
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
- (d) => `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})` + (d.snippet ? `
440
- > ${d.snippet}` : "")
441
- ).join("\n");
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 retrieve full content.`
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 {
@@ -860,13 +927,19 @@ async function hydrate(api, item, meta) {
860
927
  }
861
928
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
862
929
  const budget = maxTokens ?? DEFAULT_BUDGET;
863
- const [guardrails, specs, docs, stack, rules] = await Promise.all([
930
+ const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
864
931
  api.get("/v1/guardrails").catch(() => []),
865
932
  api.get("/v1/specs").catch(() => []),
866
933
  api.get("/v1/docs").catch(() => ({ items: [] })),
867
934
  api.get("/v1/ai/settings/stack").catch(() => []),
868
- api.get("/v1/standards/rules").catch(() => [])
935
+ api.get("/v1/standards/rules").catch(() => []),
936
+ // Capability states — FAIL-OPEN: an unreadable config (older backend, a
937
+ // blip) gates nothing, it can never hide working context.
938
+ api.get("/v1/doc-repos").catch(() => null)
869
939
  ]);
940
+ const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
941
+ const guardrails = disabled("fitness") ? [] : rawGuardrails;
942
+ const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
870
943
  const refs = /* @__PURE__ */ new Map();
871
944
  const key = (type, id) => `${type}:${id}`;
872
945
  const scorable = [
@@ -896,12 +969,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
896
969
  };
897
970
  }),
898
971
  ...docs.items.map((d) => {
972
+ const section = selectSection(d.outline, taskDescription);
973
+ const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
899
974
  refs.set(key("doc", d.id), {
900
975
  type: "doc",
901
976
  id: d.id,
902
977
  title: d.title,
903
- listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)`,
904
- fetchHint: `get_docs(doc_id="${d.id}")`
978
+ section,
979
+ pointer,
980
+ listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
981
+ \u21B3 ${pointer}` : ""),
982
+ fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
905
983
  });
906
984
  return {
907
985
  type: "doc",
@@ -974,6 +1052,8 @@ ${formatStack(stack)}`;
974
1052
  const blocks = [];
975
1053
  for (const h of hydrated) {
976
1054
  itemsLeft--;
1055
+ const headline = h.meta.pointer ? `${h.item.content}
1056
+ ${h.meta.pointer}` : h.item.content;
977
1057
  const emit = (block) => {
978
1058
  bodyBudget -= Math.max(
979
1059
  0,
@@ -984,7 +1064,7 @@ ${formatStack(stack)}`;
984
1064
  if (h.error !== void 0) {
985
1065
  bodyFailures++;
986
1066
  emit(
987
- `${h.item.content}
1067
+ `${headline}
988
1068
 
989
1069
  _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
990
1070
  );
@@ -997,7 +1077,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
997
1077
  );
998
1078
  if (allowance < MIN_BODY_TOKENS) {
999
1079
  emit(
1000
- `${h.item.content}
1080
+ `${headline}
1001
1081
 
1002
1082
  _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
1003
1083
  );
@@ -1009,7 +1089,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
1009
1089
  );
1010
1090
  itemsWithBody++;
1011
1091
  emit(
1012
- `${h.item.content}
1092
+ `${headline}
1013
1093
 
1014
1094
  ${body2}` + (truncated ? `
1015
1095
 
@@ -1028,7 +1108,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
1028
1108
  sections.push(
1029
1109
  `## Relevant Specs & Docs (${blocks.length})
1030
1110
 
1031
- _Full text of the highest-ranked items. Every item carries the ID to fetch it in full._
1111
+ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
1032
1112
 
1033
1113
  ` + blocks.join("\n\n---\n\n")
1034
1114
  );
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: doc.content }]
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
- (d) => `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})` + (d.snippet ? `
456
- > ${d.snippet}` : "")
457
- ).join("\n");
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 retrieve full content.`
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 {
@@ -876,13 +943,19 @@ async function hydrate(api, item, meta) {
876
943
  }
877
944
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
878
945
  const budget = maxTokens ?? DEFAULT_BUDGET;
879
- const [guardrails, specs, docs, stack, rules] = await Promise.all([
946
+ const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
880
947
  api.get("/v1/guardrails").catch(() => []),
881
948
  api.get("/v1/specs").catch(() => []),
882
949
  api.get("/v1/docs").catch(() => ({ items: [] })),
883
950
  api.get("/v1/ai/settings/stack").catch(() => []),
884
- api.get("/v1/standards/rules").catch(() => [])
951
+ api.get("/v1/standards/rules").catch(() => []),
952
+ // Capability states — FAIL-OPEN: an unreadable config (older backend, a
953
+ // blip) gates nothing, it can never hide working context.
954
+ api.get("/v1/doc-repos").catch(() => null)
885
955
  ]);
956
+ const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
957
+ const guardrails = disabled("fitness") ? [] : rawGuardrails;
958
+ const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
886
959
  const refs = /* @__PURE__ */ new Map();
887
960
  const key = (type, id) => `${type}:${id}`;
888
961
  const scorable = [
@@ -912,12 +985,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
912
985
  };
913
986
  }),
914
987
  ...docs.items.map((d) => {
988
+ const section = selectSection(d.outline, taskDescription);
989
+ const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
915
990
  refs.set(key("doc", d.id), {
916
991
  type: "doc",
917
992
  id: d.id,
918
993
  title: d.title,
919
- listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)`,
920
- fetchHint: `get_docs(doc_id="${d.id}")`
994
+ section,
995
+ pointer,
996
+ listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
997
+ \u21B3 ${pointer}` : ""),
998
+ fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
921
999
  });
922
1000
  return {
923
1001
  type: "doc",
@@ -990,6 +1068,8 @@ ${formatStack(stack)}`;
990
1068
  const blocks = [];
991
1069
  for (const h of hydrated) {
992
1070
  itemsLeft--;
1071
+ const headline = h.meta.pointer ? `${h.item.content}
1072
+ ${h.meta.pointer}` : h.item.content;
993
1073
  const emit = (block) => {
994
1074
  bodyBudget -= Math.max(
995
1075
  0,
@@ -1000,7 +1080,7 @@ ${formatStack(stack)}`;
1000
1080
  if (h.error !== void 0) {
1001
1081
  bodyFailures++;
1002
1082
  emit(
1003
- `${h.item.content}
1083
+ `${headline}
1004
1084
 
1005
1085
  _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
1006
1086
  );
@@ -1013,7 +1093,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
1013
1093
  );
1014
1094
  if (allowance < MIN_BODY_TOKENS) {
1015
1095
  emit(
1016
- `${h.item.content}
1096
+ `${headline}
1017
1097
 
1018
1098
  _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
1019
1099
  );
@@ -1025,7 +1105,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
1025
1105
  );
1026
1106
  itemsWithBody++;
1027
1107
  emit(
1028
- `${h.item.content}
1108
+ `${headline}
1029
1109
 
1030
1110
  ${body2}` + (truncated ? `
1031
1111
 
@@ -1044,7 +1124,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
1044
1124
  sections.push(
1045
1125
  `## Relevant Specs & Docs (${blocks.length})
1046
1126
 
1047
- _Full text of the highest-ranked items. Every item carries the ID to fetch it in full._
1127
+ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
1048
1128
 
1049
1129
  ` + blocks.join("\n\n---\n\n")
1050
1130
  );
@@ -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 {
@@ -358,13 +403,19 @@ async function hydrate(api, item, meta) {
358
403
  }
359
404
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
360
405
  const budget = maxTokens ?? DEFAULT_BUDGET;
361
- const [guardrails, specs, docs, stack, rules] = await Promise.all([
406
+ const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
362
407
  api.get("/v1/guardrails").catch(() => []),
363
408
  api.get("/v1/specs").catch(() => []),
364
409
  api.get("/v1/docs").catch(() => ({ items: [] })),
365
410
  api.get("/v1/ai/settings/stack").catch(() => []),
366
- api.get("/v1/standards/rules").catch(() => [])
411
+ api.get("/v1/standards/rules").catch(() => []),
412
+ // Capability states — FAIL-OPEN: an unreadable config (older backend, a
413
+ // blip) gates nothing, it can never hide working context.
414
+ api.get("/v1/doc-repos").catch(() => null)
367
415
  ]);
416
+ const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
417
+ const guardrails = disabled("fitness") ? [] : rawGuardrails;
418
+ const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
368
419
  const refs = /* @__PURE__ */ new Map();
369
420
  const key = (type, id) => `${type}:${id}`;
370
421
  const scorable = [
@@ -394,12 +445,17 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
394
445
  };
395
446
  }),
396
447
  ...docs.items.map((d) => {
448
+ const section = selectSection(d.outline, taskDescription);
449
+ const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
397
450
  refs.set(key("doc", d.id), {
398
451
  type: "doc",
399
452
  id: d.id,
400
453
  title: d.title,
401
- listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)`,
402
- fetchHint: `get_docs(doc_id="${d.id}")`
454
+ section,
455
+ pointer,
456
+ listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)` + (pointer ? `
457
+ \u21B3 ${pointer}` : ""),
458
+ fetchHint: section ? `get_docs(doc_id="${d.id}", section="${section.anchor}")` : `get_docs(doc_id="${d.id}")`
403
459
  });
404
460
  return {
405
461
  type: "doc",
@@ -472,6 +528,8 @@ ${formatStack(stack)}`;
472
528
  const blocks = [];
473
529
  for (const h of hydrated) {
474
530
  itemsLeft--;
531
+ const headline = h.meta.pointer ? `${h.item.content}
532
+ ${h.meta.pointer}` : h.item.content;
475
533
  const emit = (block) => {
476
534
  bodyBudget -= Math.max(
477
535
  0,
@@ -482,7 +540,7 @@ ${formatStack(stack)}`;
482
540
  if (h.error !== void 0) {
483
541
  bodyFailures++;
484
542
  emit(
485
- `${h.item.content}
543
+ `${headline}
486
544
 
487
545
  _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
488
546
  );
@@ -495,7 +553,7 @@ _Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
495
553
  );
496
554
  if (allowance < MIN_BODY_TOKENS) {
497
555
  emit(
498
- `${h.item.content}
556
+ `${headline}
499
557
 
500
558
  _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
501
559
  );
@@ -507,7 +565,7 @@ _Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`
507
565
  );
508
566
  itemsWithBody++;
509
567
  emit(
510
- `${h.item.content}
568
+ `${headline}
511
569
 
512
570
  ${body2}` + (truncated ? `
513
571
 
@@ -526,7 +584,7 @@ _[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`
526
584
  sections.push(
527
585
  `## Relevant Specs & Docs (${blocks.length})
528
586
 
529
- _Full text of the highest-ranked items. Every item carries the ID to fetch it in full._
587
+ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Each carries the ID / \`chunk_id\` to fetch more._
530
588
 
531
589
  ` + blocks.join("\n\n---\n\n")
532
590
  );
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemdox/mcp-server",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "description": "MCP server that exposes SystemDox checks, specs, docs, and tech stack to AI coding tools",
6
6
  "main": "./dist/index.js",