bc-observatory 0.3.0 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/server.js +367 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,4 +38,12 @@ model, search is keyword-only and says so.
38
38
  `get_object` takes an id, a reference (`t36`, `36-be`), a name or a caption, and a `country`; `whats_new` lists nothing
39
39
  dated after today unless `include_future: true`.
40
40
 
41
+ `cat`, `get_object` and `localization` return a page as it is when it fits 30,000 characters. A longer page
42
+ (Codeunit 80 "Sales-Post" is 177,746) comes back as a compact view: the frontmatter with long lists cut, the title
43
+ block, the section keys with their row counts and the first 5 rows of each section. `section` reads one section
44
+ (`fields`, `events`, `procedures`, `calls`, ..., or `frontmatter`), `offset` and `limit` page through it by rows, and
45
+ no answer exceeds 30,000 characters. A symbol's path from `search` (`objects/codeunit/80#event-OnAfterPostSalesDoc`)
46
+ returns just that member. `BC_OBSERVATORY_MAX_CHARS` changes the limit (`0` returns every page whole, as before
47
+ 0.4.0).
48
+
41
49
  MIT. Content: see the repository's CONTENT-NOTICE.md (Microsoft content attributed; community content derived only).
package/dist/server.js CHANGED
@@ -891,10 +891,363 @@ function dot(a, b, bOffset = 0) {
891
891
  return s;
892
892
  }
893
893
 
894
+ // src/sections.ts
895
+ var DEFAULT_BUDGET = 3e4;
896
+ var MIN_BUDGET = 5e3;
897
+ var PREVIEW_ROWS = 5;
898
+ var ROW_CLIP = 200;
899
+ var PROSE_CLIP = 800;
900
+ var HEAD_LINE_MAX = 120;
901
+ var FM_LIST_CHARS = 600;
902
+ var FM_LIST_MIN = 3;
903
+ var RESERVE = 300;
904
+ var OBJECT_KEYS = {
905
+ "Properties": "properties",
906
+ "Fields": "fields",
907
+ "Fields on this page": "controls",
908
+ "Actions": "actions",
909
+ "Keys": "keys",
910
+ "Values": "values",
911
+ "Events published": "events",
912
+ "Trigger event subscribers": "trigger_subscribers",
913
+ "Event subscriptions": "subscriptions",
914
+ "Procedures": "procedures",
915
+ "Triggers": "triggers",
916
+ "Relations": "relations",
917
+ "Referenced by": "referenced_by",
918
+ "Pages and codeunits on this table": "pages",
919
+ "Extended by": "extended_by",
920
+ "Calls": "calls",
921
+ "Called by": "called_by",
922
+ "Implements": "implements",
923
+ "Implemented by": "implemented_by",
924
+ "Recent changes": "changes",
925
+ "Across versions": "versions",
926
+ "Countries that replace it": "countries",
927
+ "Documented on Microsoft Learn": "learn",
928
+ "Deprecations": "deprecations",
929
+ "Ask your agent": "ask"
930
+ };
931
+ var snake = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
932
+ var blank = (l) => l.trim() === "";
933
+ var fmt = (n) => n.toLocaleString("en-US");
934
+ var FENCE = /^\s*```/;
935
+ var SEPARATOR = /^\|\s*:?-/;
936
+ var CAP_NOTE = /^- and \d+ more/;
937
+ function budgetOf(env) {
938
+ const n = env === void 0 || env.trim() === "" ? NaN : Number(env);
939
+ if (!Number.isFinite(n) || n < 0) return DEFAULT_BUDGET;
940
+ if (n === 0) return Infinity;
941
+ return Math.max(MIN_BUDGET, Math.floor(n));
942
+ }
943
+ function sectionKey(heading) {
944
+ return OBJECT_KEYS[heading] ?? snake(heading);
945
+ }
946
+ function splitSection(lines) {
947
+ const head = [], rows = [];
948
+ let pending = [];
949
+ let table2 = null, group = null;
950
+ let inTable = false, afterRow = false, fence = null;
951
+ const other = (line2, kind = "text") => {
952
+ if (rows.length) pending.push({ line: line2, kind });
953
+ else head.push(line2);
954
+ };
955
+ const addRow = (first) => {
956
+ if (rows.length) {
957
+ const prev = rows[rows.length - 1];
958
+ for (const p of pending) {
959
+ if (p.kind === "group" && p.line === group) continue;
960
+ if (p.kind === "table" && table2 && (p.line === table2[0] || p.line === table2[1])) continue;
961
+ if (!blank(p.line)) prev.lines.push(p.line);
962
+ }
963
+ }
964
+ pending = [];
965
+ rows.push({ lines: [first], table: table2, group });
966
+ afterRow = true;
967
+ };
968
+ for (let i = 0; i < lines.length; i++) {
969
+ const l = lines[i];
970
+ if (fence) {
971
+ if (fence === "row") rows[rows.length - 1].lines.push(l);
972
+ else other(l);
973
+ if (FENCE.test(l)) fence = null;
974
+ continue;
975
+ }
976
+ if (FENCE.test(l)) {
977
+ if (afterRow && /^ {2,}/.test(l)) {
978
+ rows[rows.length - 1].lines.push(l);
979
+ fence = "row";
980
+ } else {
981
+ other(l);
982
+ fence = "text";
983
+ afterRow = false;
984
+ inTable = false;
985
+ table2 = null;
986
+ }
987
+ continue;
988
+ }
989
+ if (afterRow && /^ {2,}\S/.test(l)) {
990
+ rows[rows.length - 1].lines.push(l);
991
+ continue;
992
+ }
993
+ if (l.startsWith("|")) {
994
+ if (!inTable && i + 1 < lines.length && SEPARATOR.test(lines[i + 1])) {
995
+ table2 = [l, lines[i + 1]];
996
+ inTable = true;
997
+ afterRow = false;
998
+ other(l, "table");
999
+ other(lines[++i], "table");
1000
+ continue;
1001
+ }
1002
+ if (inTable) {
1003
+ addRow(l);
1004
+ continue;
1005
+ }
1006
+ } else if (inTable) {
1007
+ inTable = false;
1008
+ table2 = null;
1009
+ }
1010
+ if (l.startsWith("- ") && !CAP_NOTE.test(l)) {
1011
+ addRow(l);
1012
+ continue;
1013
+ }
1014
+ afterRow = false;
1015
+ if (l.startsWith("### ")) {
1016
+ group = l;
1017
+ other(l, "group");
1018
+ continue;
1019
+ }
1020
+ other(l);
1021
+ }
1022
+ const tail = pending.map((p) => p.line);
1023
+ return { head, rows, tail };
1024
+ }
1025
+ var memo = null;
1026
+ function parsePage(md) {
1027
+ if (memo && memo.md === md) return memo.page;
1028
+ const all = md.split("\n");
1029
+ let i = 0, frontmatter = "";
1030
+ if (all[0] === "---") {
1031
+ const end = all.indexOf("---", 1);
1032
+ if (end > 0) {
1033
+ frontmatter = all.slice(1, end).join("\n");
1034
+ i = end + 1;
1035
+ }
1036
+ }
1037
+ const pre = [], chunks = [];
1038
+ let fence = false;
1039
+ for (; i < all.length; i++) {
1040
+ const l = all[i];
1041
+ if (!fence && l.startsWith("## ")) {
1042
+ chunks.push({ heading: l.slice(3).trim(), lines: [] });
1043
+ continue;
1044
+ }
1045
+ if (FENCE.test(l)) fence = !fence;
1046
+ (chunks.length ? chunks[chunks.length - 1].lines : pre).push(l);
1047
+ }
1048
+ const seen = /* @__PURE__ */ new Map();
1049
+ const sections = chunks.map(({ heading, lines }) => {
1050
+ while (lines.length && blank(lines[lines.length - 1])) lines.pop();
1051
+ const base = sectionKey(heading), n = (seen.get(base) ?? 0) + 1;
1052
+ seen.set(base, n);
1053
+ return { heading, key: n === 1 ? base : `${base}_${n}`, lines, ...splitSection(lines) };
1054
+ });
1055
+ const preamble = pre.join("\n").replace(/^\s*\n/, "").replace(/\s+$/, "");
1056
+ const page = { frontmatter, preamble, sections };
1057
+ memo = { md, page };
1058
+ return page;
1059
+ }
1060
+ function frontmatterSection(p) {
1061
+ const lines = p.frontmatter ? p.frontmatter.split("\n") : [];
1062
+ return { heading: "", key: "frontmatter", lines, head: [], rows: lines.map((l) => ({ lines: [l], table: null, group: null })), tail: [] };
1063
+ }
1064
+ function findSection(p, want) {
1065
+ const w = want.trim().toLowerCase(), ws = snake(want);
1066
+ if (w === "frontmatter") return frontmatterSection(p);
1067
+ return p.sections.find((s) => s.key === w) ?? p.sections.find((s) => s.heading.toLowerCase() === w) ?? p.sections.find((s) => ws !== "" && snake(s.heading) === ws) ?? null;
1068
+ }
1069
+ var keyList = (p, counts = false) => [...p.sections.map((s) => counts && s.rows.length ? `${s.key} (${s.rows.length})` : s.key), "frontmatter"].join(", ");
1070
+ var titleOf = (p, path) => p.preamble.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() || path;
1071
+ function clip(s, n, mark = "\u2026") {
1072
+ if (s.length <= n) return s;
1073
+ const end = /[\uD800-\uDBFF]/.test(s[n - 2]) ? n - 2 : n - 1;
1074
+ return `${s.slice(0, end)}${mark}`;
1075
+ }
1076
+ var indentOf = (l) => l.length - l.trimStart().length;
1077
+ var BLOCK_SCALAR = /(?:^|:|-)\s*[|>][-+0-9]*\s*$/;
1078
+ var LIST_ITEM = /^( *)-(?: |$)/;
1079
+ function cutLists(lines) {
1080
+ const out = [];
1081
+ let i = 0;
1082
+ while (i < lines.length) {
1083
+ const l = lines[i], m = LIST_ITEM.exec(l);
1084
+ if (!m) {
1085
+ out.push(l);
1086
+ i++;
1087
+ if (BLOCK_SCALAR.test(l)) {
1088
+ const ind2 = indentOf(l);
1089
+ while (i < lines.length && (blank(lines[i]) || indentOf(lines[i]) > ind2)) out.push(lines[i++]);
1090
+ }
1091
+ continue;
1092
+ }
1093
+ const ind = m[1].length, items = [];
1094
+ while (i < lines.length && indentOf(lines[i]) === ind && LIST_ITEM.test(lines[i])) {
1095
+ const item = [lines[i++]];
1096
+ while (i < lines.length && (indentOf(lines[i]) > ind || blank(lines[i]) && i + 1 < lines.length && indentOf(lines[i + 1]) > ind)) item.push(lines[i++]);
1097
+ items.push(BLOCK_SCALAR.test(item[0]) ? item : [item[0], ...cutLists(item.slice(1))]);
1098
+ }
1099
+ const size = (k2) => items.slice(0, k2).map((it) => it.join("\n")).join("\n").length;
1100
+ if (size(items.length) <= FM_LIST_CHARS) {
1101
+ out.push(...items.flat());
1102
+ continue;
1103
+ }
1104
+ let k = 0;
1105
+ while (k < items.length && size(k + 1) <= FM_LIST_CHARS) k++;
1106
+ k = Math.max(k, FM_LIST_MIN);
1107
+ out.push(...items.slice(0, k).flat());
1108
+ if (k < items.length) out.push(`${" ".repeat(ind)}# \u2026 ${items.length - k} more; section: "frontmatter"`);
1109
+ }
1110
+ return out;
1111
+ }
1112
+ function compactFrontmatter(yaml) {
1113
+ const lines = yaml.split("\n"), kept = [];
1114
+ for (let i = 0; i < lines.length; i++) {
1115
+ if (/^generated:/.test(lines[i])) {
1116
+ while (i + 1 < lines.length && (indentOf(lines[i + 1]) > 0 || blank(lines[i + 1]))) i++;
1117
+ continue;
1118
+ }
1119
+ kept.push(lines[i]);
1120
+ }
1121
+ return cutLists(kept).join("\n");
1122
+ }
1123
+ function compactView(md, path, budget) {
1124
+ const p = parsePage(md);
1125
+ const out = [];
1126
+ if (md.startsWith("---\n") && p.frontmatter !== "") out.push("---", compactFrontmatter(p.frontmatter), "---", "");
1127
+ if (p.preamble) out.push(p.preamble, "");
1128
+ out.push(`Compact view: this page is ${fmt(md.length)} characters and one answer holds ${fmt(budget)}, so each section shows its first ${PREVIEW_ROWS} rows. Sections: ${keyList(p, true)}. Read one with cat("${path}", section: "<key>"); offset and limit page through a long one.`, "");
1129
+ for (const s of p.sections) {
1130
+ out.push(`## ${s.heading}`);
1131
+ const more = `\u2026 (section: "${s.key}")`;
1132
+ if (!s.rows.length) {
1133
+ const text2 = s.head.filter((l) => !blank(l)).join("\n");
1134
+ if (text2) out.push(clip(text2, PROSE_CLIP, more));
1135
+ } else {
1136
+ const lead = s.head.find((l) => !blank(l) && !l.startsWith("|") && !l.startsWith("### "));
1137
+ if (lead && lead.length <= HEAD_LINE_MAX) out.push(lead);
1138
+ let group = null, table2 = null;
1139
+ for (const r of s.rows.slice(0, PREVIEW_ROWS)) {
1140
+ if (r.group && r.group !== group) {
1141
+ out.push(r.group);
1142
+ group = r.group;
1143
+ }
1144
+ const t = r.table ? r.table.join("\n") : null;
1145
+ if (r.table && t !== table2) out.push(...r.table);
1146
+ table2 = t;
1147
+ out.push(clip(r.lines[0], ROW_CLIP));
1148
+ }
1149
+ if (s.rows.length > PREVIEW_ROWS) out.push(`\u2192 ${s.rows.length - PREVIEW_ROWS} more of ${s.rows.length}: cat("${path}", section: "${s.key}", offset: ${PREVIEW_ROWS})`);
1150
+ else {
1151
+ const text2 = s.tail.filter((l) => !blank(l)).join("\n");
1152
+ if (text2) out.push(clip(text2, PROSE_CLIP, more));
1153
+ }
1154
+ }
1155
+ out.push("");
1156
+ }
1157
+ return `${out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "")}
1158
+ `;
1159
+ }
1160
+ var dropLeadingBlank = (lines) => {
1161
+ let i = 0;
1162
+ while (i < lines.length && blank(lines[i])) i++;
1163
+ return lines.slice(i);
1164
+ };
1165
+ function sectionView(md, path, want, offset, limit, budget, site) {
1166
+ const p = parsePage(md);
1167
+ const nl = (lines) => `${lines.join("\n")}
1168
+ `;
1169
+ const s = findSection(p, want);
1170
+ if (!s) return `No section "${want}" on ${path}. Sections: ${keyList(p)}.`;
1171
+ const fm = s.key === "frontmatter" && s.heading === "";
1172
+ const first = `${titleOf(p, path)}, section "${s.key}": ${site}${path}/`;
1173
+ const heading = fm ? [] : [`## ${s.heading}`];
1174
+ const n = s.rows.length, off = Math.max(0, Math.floor(offset ?? 0));
1175
+ const verbatim = nl(fm ? [first, "", "---", ...s.lines, "---"] : [first, "", ...heading, ...s.lines]);
1176
+ if (!n || off === 0 && limit === void 0 && verbatim.length <= budget) return verbatim;
1177
+ const tail = dropLeadingBlank(s.tail);
1178
+ if (off >= n) return nl([first, "", ...heading, "", ...tail.length ? tail : [`No rows after ${n} in section "${s.key}" of ${path}.`]]);
1179
+ const out = [first, "", ...heading, ...fm ? [] : [""]];
1180
+ let group = null, table2 = null;
1181
+ if (off === 0) {
1182
+ out.push(...dropLeadingBlank(s.head));
1183
+ group = s.rows[0].group;
1184
+ table2 = s.rows[0].table ? s.rows[0].table.join("\n") : null;
1185
+ }
1186
+ let len = out.join("\n").length, b = off;
1187
+ const end = limit === void 0 ? n : Math.min(n, off + Math.max(1, Math.floor(limit)));
1188
+ for (let r = off; r < end; r++) {
1189
+ const row = s.rows[r], add = [];
1190
+ let g = group, t = table2;
1191
+ if (row.group && row.group !== g) {
1192
+ add.push(...out.length && !blank(out[out.length - 1]) ? [""] : [], row.group, "");
1193
+ g = row.group;
1194
+ t = null;
1195
+ }
1196
+ const rt = row.table ? row.table.join("\n") : null;
1197
+ if (row.table && rt !== t) add.push(...row.table);
1198
+ t = rt;
1199
+ add.push(...row.lines);
1200
+ const size = add.join("\n").length + 1;
1201
+ if (r > off && len + size > budget - RESERVE) break;
1202
+ out.push(...add);
1203
+ len += size;
1204
+ group = g;
1205
+ table2 = t;
1206
+ b = r + 1;
1207
+ }
1208
+ const rows = `Rows ${off + 1}-${b} of ${n}.`;
1209
+ if (b < n) return nl([...out, "", `${rows} Next: cat("${path}", section: "${s.key}", offset: ${b})`]);
1210
+ if (!tail.length) return nl([...out, "", rows]);
1211
+ const withTail = nl([...out, "", ...tail, "", rows]);
1212
+ if (limit === void 0 && withTail.length <= budget) return withTail;
1213
+ return nl([...out, "", `${rows} The section's closing text: cat("${path}", section: "${s.key}", offset: ${n})`]);
1214
+ }
1215
+ var ANCHOR_SECTION = { field: ["fields", "field"], value: ["values", "value"], event: ["events", "event"], proc: ["procedures", "procedure"] };
1216
+ function anchorView(md, path, anchor, site) {
1217
+ const a = anchor.replace(/^#/, ""), m = /^(field|value|event|proc)-(.+)$/.exec(a);
1218
+ if (!m) return null;
1219
+ const [key, label] = ANCHOR_SECTION[m[1]];
1220
+ const p = parsePage(md);
1221
+ const s = p.sections.find((x) => x.key === key);
1222
+ if (!s) return null;
1223
+ const match = m[1] === "field" || m[1] === "value" ? (l) => l.startsWith(`| ${m[2]} |`) : (l) => l.startsWith(`- \`${m[2]}(`);
1224
+ const rows = s.rows.filter((r) => match(r.lines[0]));
1225
+ if (!rows.length) return null;
1226
+ return `${[`${titleOf(p, path)}, ${label} ${m[2]}: ${site}${path}/#${a}`, "", `## ${s.heading}`, ...rows[0].table ?? [], ...rows.flatMap((r) => r.lines)].join("\n")}
1227
+ `;
1228
+ }
1229
+ function pageView(md, path, a, budget, site) {
1230
+ const hash = path.indexOf("#");
1231
+ if (hash >= 0) {
1232
+ const clean = path.slice(0, hash), anchor = path.slice(hash);
1233
+ return anchorView(md, clean, anchor, site) ?? `(No row for ${anchor} on this page.)
1234
+
1235
+ ${pageView(md, clean, {}, budget, site)}`;
1236
+ }
1237
+ if (a.section !== void 0 && a.section.trim() !== "") return sectionView(md, path, a.section, a.offset, a.limit, budget, site);
1238
+ if (a.offset !== void 0 || a.limit !== void 0) {
1239
+ return `offset and limit read one section: cat("${path}", section: "<key>", offset: ${a.offset ?? 0}). Sections: ${keyList(parsePage(md))}.`;
1240
+ }
1241
+ if (md.length <= budget) return md;
1242
+ return compactView(md, path, budget);
1243
+ }
1244
+
894
1245
  // src/server.ts
895
- var VERSION = "0.3.0";
1246
+ var VERSION = "0.4.0";
896
1247
  var SITE = (process.env.BC_OBSERVATORY_SITE ?? "https://waldo1001.github.io/waldo.BCObservatory/").replace(/\/?$/, "/");
897
1248
  var LOCAL = process.env.BC_OBSERVATORY_LOCAL ? resolve(process.env.BC_OBSERVATORY_LOCAL) : null;
1249
+ var BUDGET = budgetOf(process.env.BC_OBSERVATORY_MAX_CHARS);
1250
+ var B = (Number.isFinite(BUDGET) ? BUDGET : DEFAULT_BUDGET).toLocaleString("en-US");
898
1251
  var REPO = "https://github.com/waldo1001/waldo.BCObservatory";
899
1252
  var DAY = 864e5;
900
1253
  var cacheDir = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "bc-observatory", createHash("sha256").update(SITE).digest("hex").slice(0, 12));
@@ -1082,7 +1435,9 @@ async function toolLs(a) {
1082
1435
  ... and ${files.length - MAX} more pages; use search() to narrow down` : ""}`;
1083
1436
  }
1084
1437
  async function toolCat(a) {
1085
- return pageMarkdown(a.path.replace(/#.*$/, ""));
1438
+ const [base, anchor] = a.path.split(/(?=#)/);
1439
+ const clean = `${base.replace(/^\/+|\/+$/g, "").replace(/\.md$/, "")}${anchor ?? ""}`;
1440
+ return pageView(await pageMarkdown(clean.replace(/#.*$/, "")), clean, a, BUDGET, SITE);
1086
1441
  }
1087
1442
  async function toolGetObject(a) {
1088
1443
  await loadIndex();
@@ -1123,7 +1478,7 @@ async function toolGetObject(a) {
1123
1478
  const also = !country && twins.length ? `
1124
1479
 
1125
1480
  (Also in ${twins.join(", ")}: get_object("${type}", "${pick[0].objectId ?? pick[0].name}", country).)` : "";
1126
- return `${note}${await pageMarkdown(pick[0].id)}${also}`;
1481
+ return `${note}${pageView(await pageMarkdown(pick[0].id), pick[0].id, a, BUDGET, SITE)}${a.section ? "" : also}`;
1127
1482
  }
1128
1483
  if (pick.length > 1) return `${pick.length} ${type} objects match "${a.idOrName}"; read one with cat(path):
1129
1484
  ${pick.map((r) => `- ${r.title} path=${r.id}`).join("\n")}`;
@@ -1144,7 +1499,8 @@ async function toolDiffObject(a) {
1144
1499
  ${JSON.stringify(o, null, 1)}`;
1145
1500
  }
1146
1501
  async function toolLocalization(a) {
1147
- return pageMarkdown(`localizations/${a.country.toLowerCase()}`);
1502
+ const p = `localizations/${a.country.toLowerCase()}`;
1503
+ return pageView(await pageMarkdown(p), p, a, BUDGET, SITE);
1148
1504
  }
1149
1505
  async function toolWhatsNew(a, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
1150
1506
  await loadIndex();
@@ -1176,6 +1532,7 @@ ${a.message}
1176
1532
  (sent from the bc-observatory MCP server ${VERSION})`;
1177
1533
  return `Open this link to file the issue (no sign-in through the agent): ${REPO}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}&labels=feedback`;
1178
1534
  }
1535
+ var VIEW = { section: z.string().optional(), offset: z.number().int().min(0).max(1e5).optional(), limit: z.number().int().min(1).max(5e3).optional() };
1179
1536
  function createServer() {
1180
1537
  const server = new McpServer({ name: "bc-observatory", version: VERSION }, {
1181
1538
  instructions: "BC Observatory: an agent-first knowledge base of Microsoft Dynamics 365 Business Central (Learn hubs, AL objects from the code for BC28-30, localizations, roadmap features, videos, community posts, code changes: merged pull requests of Microsoft's Business Central repositories, BCApps joined to the AL objects they changed, under changes/, and AL Language extension releases (the marketplace changelog, under releases/)). Every page carries a trust tier (official = Microsoft, community = everyone else) and a review state: say which tier a claim comes from. Never invent AL object ids or version numbers: look them up with get_object. Start with search(), read pages with cat(path), browse with ls(path)."
@@ -1184,7 +1541,7 @@ function createServer() {
1184
1541
  "search",
1185
1542
  {
1186
1543
  title: "Search the knowledge base",
1187
- description: "Search every page (title, AL object name and caption, summary, tags) with the site's own scorer: per query word title or caption 3, tags 2, summary 1, through the exact word, a prefix, the plural, a synonym (client = customer, G/L = general ledger) or one typo; a name equal to the query +10; an object reference ('t36', 'cu 80', 'table 11300 BE') or a bare id ('36') goes straight to the object; base app before first-party app before country layer, then an object's importance (what references, calls and documents it); hubs by size and review state. Hybrid by default: the exact band (references and exact names) stays on top and the rest is fused with a meaning ranking, so a paraphrase still finds the page; each hit says [keyword], [meaning] or [both]. mode 'keyword' for the scorer alone, 'semantic' for meaning only. Fields, published events, global procedures and enum values (W1 and apps, the major the object pages are built from) are searchable too ('OnAfterPostSalesDoc', 'Posting Date', or a prefix: 'event: OnAfterPost', 'field: Posting Date', 'proc: CopyToTempLines'); a symbol hit names its object and its path ends in #event-<Name>, #field-<id>, #proc-<Name> or #value-<ordinal>. Filters: type (topic, app, feature, object, localization, video, post, change, source, digest, release), tier (official, community), system (finance, sales, ... or an alias such as 'g/l'), country ('BE'), app ('Subscription Billing'), object_type ('table'), kind (page, field, event, proc, value).",
1544
+ description: "Search every page, and the fields, published events, global procedures and enum values of AL objects, ranked like the site's search. Takes names ('Sales Header'), references ('t36', 'cu 80', 'table 11300 BE'), bare ids ('36') and symbol prefixes ('event: OnAfterPost', 'field: Posting Date', 'proc: CopyToTempLines'); a symbol hit's path ends in its anchor (#event-<Name>, #field-<id>, #proc-<Name>, #value-<ordinal>); cat(path) reads that member alone. Hybrid by default: exact matches stay on top and the rest is fused with a meaning ranking; each hit says [keyword], [meaning] or [both]; mode 'keyword' or 'semantic' for one list. Filters: type (topic, app, feature, object, localization, video, post, change, source, digest, release), tier (official, community), system (finance, sales, ... or an alias such as 'g/l'), country ('BE'), app ('Subscription Billing'), object_type ('table'), kind (page, field, event, proc, value).",
1188
1545
  inputSchema: { query: z.string(), type: z.string().optional(), tier: z.string().optional(), system: z.string().optional(), country: z.string().optional(), app: z.string().optional(), object_type: z.string().optional(), kind: z.enum(["page", "field", "event", "proc", "value"]).optional(), limit: z.number().int().min(1).max(50).optional(), mode: z.enum(["hybrid", "keyword", "semantic"]).optional() }
1189
1546
  },
1190
1547
  async (a) => text(await toolSearch(a))
@@ -1196,13 +1553,13 @@ function createServer() {
1196
1553
  );
1197
1554
  server.registerTool(
1198
1555
  "cat",
1199
- { title: "Read a page", description: "The full markdown of a page (frontmatter with evidence and links, then the body), by its path from search or ls.", inputSchema: { path: z.string() } },
1556
+ { title: "Read a page", description: `A page's markdown by its path from search or ls: frontmatter with evidence and links, then the body. A page over ${B} characters comes back compact with its section keys; section (or 'frontmatter'), offset and limit read one part. A symbol path ('objects/codeunit/80#event-OnAfterPostSalesDoc') returns that member's row.`, inputSchema: { path: z.string(), ...VIEW } },
1200
1557
  async (a) => text(await toolCat(a))
1201
1558
  );
1202
1559
  server.registerTool("get_object", {
1203
1560
  title: "Look up an AL object",
1204
- description: "An AL object of W1, a Microsoft first-party app or a country layer by type and id, reference or name, e.g. ('table', '18'), ('table', 't36'), ('table', '36-be'), ('codeunit', 'Sales-Post'), ('page', 'Customers') by its caption, ('table', 'VAT VIES Correction') or with country 'BE': fields, keys, events, public procedures, obsolete state, versions, countries that replace it, Learn pages. A W1 object wins over its country twins unless a country is given; two candidates of the same standing are listed with their paths.",
1205
- inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional() }
1561
+ description: `An AL object of W1, a Microsoft first-party app or a country layer by type and id, reference, name or caption: ('table', '18'), ('table', 't36'), ('table', '36-be'), ('codeunit', 'Sales-Post'), ('page', 'Customers'), or with country 'BE'. Returns its page: fields, keys, events, procedures, calls, obsolete state, versions, countries, Learn pages; a W1 object wins over its country twins unless a country is given, and two candidates are listed with their paths. A page over ${B} characters comes back compact (frontmatter, the first 5 rows of each section, the section keys): pass section ('fields', 'events', 'procedures', 'calls', ...) with offset and limit to read one. To check that a field, event or procedure exists, search('event: <Name>') answers without the page.`,
1562
+ inputSchema: { type: z.string(), idOrName: z.string(), country: z.string().length(2).optional(), ...VIEW }
1206
1563
  }, async (a) => text(await toolGetObject(a)));
1207
1564
  server.registerTool("diff_object", {
1208
1565
  title: "What changed in an object",
@@ -1211,12 +1568,12 @@ function createServer() {
1211
1568
  }, async (a) => text(await toolDiffObject(a)));
1212
1569
  server.registerTool(
1213
1570
  "localization",
1214
- { title: "A country localization", description: "What a country layer (BE, NL, DE, ...) adds to or changes in W1, with its Learn local functionality hub.", inputSchema: { country: z.string().length(2) } },
1571
+ { title: "A country localization", description: `What a country layer (BE, NL, DE, ...) adds to or changes in W1, with its Learn local functionality hub. Over ${B} characters it comes back compact; section, offset and limit read one section, as in cat.`, inputSchema: { country: z.string().length(2), ...VIEW } },
1215
1572
  async (a) => text(await toolLocalization(a))
1216
1573
  );
1217
1574
  server.registerTool("whats_new", {
1218
1575
  title: "What is new",
1219
- description: "Videos, posts, roadmap features, AL extension releases (type 'release') and code changes (merged pull requests of BCApps, AL-Go and BCQuality, type 'change') dated on or after a date (YYYY-MM-DD) and up to today, newest first; a roadmap feature's date is its GA month, so features scheduled after today are counted at the end and listed with include_future: true.",
1576
+ description: "Videos, posts, roadmap features, AL extension releases (type 'release') and merged pull requests of BCApps, AL-Go and BCQuality (type 'change') dated on or after a date (YYYY-MM-DD) up to today, newest first. A roadmap feature's date is its GA month: features after today are counted at the end and listed with include_future: true.",
1220
1577
  inputSchema: { since: z.string(), type: z.string().optional(), limit: z.number().int().min(1).max(100).optional(), include_future: z.boolean().optional() }
1221
1578
  }, async (a) => text(await toolWhatsNew(a)));
1222
1579
  server.registerTool("blog_footprint", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bc-observatory",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for BC Observatory: an agent-first, cross-referenced knowledge base of Microsoft Dynamics 365 Business Central (Learn, code, roadmap, videos, community blogs).",
5
5
  "type": "module",
6
6
  "bin": {