@bicon/mcp-composer 0.11.0 → 0.13.1

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/README.md CHANGED
@@ -28,7 +28,7 @@ via API keys is a separate, security-reviewed phase.
28
28
  | `export_initiative_file` | Takes the **same compact spec**, assembles it and wraps the result in the app's single-initiative JSON envelope; returns `{ok, filename, json, importInstructions, derivedScores, warnings}`. There is one pipeline — a hand-edited initiative object is not an input, so client-supplied scores or unknown keys can never bypass assembly. |
29
29
  | `read_portfolio_context` | Takes the organisation's **portfolio-context file** (see below) and returns the interview plan for that organisation — its own labels, help texts and rating notes per sub-dimension, which sub-dimensions are switched off, its business units, categories, tags, skills (id + name), non-archived initiatives (id + name) and the capability map's capabilities (id, name, domain, importance, current → target maturity; context version 2), currency, discount rate, whether the EU-AI-Act module is on — plus a short **contextId** that stands in for the file in the two other tools. |
30
30
  | `composer_version` | Returns `{composerVersion, capabilities}`. Every *successful* tool result also carries `composerVersion`. |
31
- | `load_portfolio` | Reads the organisation's **full backup** (Import/Export → Backup → Export, a `bicon-backup-….json`, format version 10 or newer) from a local `path` (inline `backup` JSON only for small files), runs it through the app's own sanitiser, strips author fields, and returns a short **portfolioId** plus a summary (counts by status, business unit and driver, skills, capabilities, saved portfolios, snapshots, currency, budget cap, export date). |
31
+ | `load_portfolio` | Reads the organisation's **full backup** (Import/Export → Backup → Export, a `bicon-portfolio-backup-YYYY-MM-DD.json`, format version 10 or newer) from a local `path` (inline `backup` JSON only for small files), runs it through the app's own sanitiser, strips author fields, and returns a short **portfolioId** plus a summary (counts by status, business unit and driver, skills, capabilities, saved portfolios, snapshots, currency, budget cap, export date). |
32
32
  | `query_initiatives` | Filters the loaded portfolio (status, business unit, category, driver, tags, saved portfolio, capability, skill, score ranges, overdue milestones / follow-ups, severe risks) and returns chosen columns, sorted, plus aggregates over the whole scope. At most 200 rows per call. |
33
33
  | `portfolio_readings` | Computes portfolio readings with the app's own libs: KPIs, concentration bars and HHI, the seven operating-model readings, capability impact, the risk register, early-warning signals and Board Actions. |
34
34
  | `rank_initiatives` | Runs the Prioritisation ranking and cut line (the organisation's rule book, budget envelope, exclusions and skill caps), with what-if inputs: cap, budget field, trade-off percent, skill FTE deltas, promoted / excluded ids, queue statuses, regulatory-first switch. |
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
 
14
14
  // src/version.ts
15
- var COMPOSER_VERSION = true ? "0.11.0" : readPackageVersion();
15
+ var COMPOSER_VERSION = true ? "0.13.1" : readPackageVersion();
16
16
 
17
17
  // ../../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/external.js
18
18
  var external_exports = {};
@@ -29869,7 +29869,7 @@ function expandPath(p) {
29869
29869
  function readBackupFile(p) {
29870
29870
  const full = expandPath(p);
29871
29871
  if (path.extname(full).toLowerCase() !== ".json") {
29872
- return { ok: false, error: `path: "${full}" is not a .json file. Pass the backup the app downloads (Import/Export \u2192 Backup \u2192 Export), named like bicon-backup-\u2026.json.` };
29872
+ return { ok: false, error: `path: "${full}" is not a .json file. Pass the backup the app downloads (Import/Export \u2192 Backup \u2192 Export), named like bicon-portfolio-backup-YYYY-MM-DD.json.` };
29873
29873
  }
29874
29874
  let size;
29875
29875
  try {
@@ -31268,6 +31268,17 @@ function unrecordedSignals(i, ctx, t) {
31268
31268
  });
31269
31269
  }
31270
31270
 
31271
+ // ../../initiative-model/src/decision-log.ts
31272
+ function sortDecisionLog(entries) {
31273
+ return entries.map((entry, index) => ({ entry, index })).sort((a, b) => {
31274
+ if (a.entry.date !== b.entry.date) return a.entry.date < b.entry.date ? 1 : -1;
31275
+ const at = a.entry.createdAt ?? "";
31276
+ const bt = b.entry.createdAt ?? "";
31277
+ if (at !== bt) return at < bt ? 1 : -1;
31278
+ return b.index - a.index;
31279
+ }).map(({ entry }) => entry);
31280
+ }
31281
+
31271
31282
  // ../../initiative-model/src/skill-demand.ts
31272
31283
  var round12 = (n) => Math.round(n * 10) / 10;
31273
31284
  var MAX_DEMAND_MONTHS = 48;
@@ -31288,7 +31299,6 @@ function indexToKey(idx) {
31288
31299
  }
31289
31300
  function skillDemandContributors(initiatives, opts) {
31290
31301
  const openEnded = opts?.openEndedInProduction === true;
31291
- const productionRunOnly = opts?.productionRunOnly === true;
31292
31302
  const now = opts?.now ?? /* @__PURE__ */ new Date();
31293
31303
  const todayIdx = now.getFullYear() * 12 + now.getMonth();
31294
31304
  const contributors2 = [];
@@ -31299,20 +31309,6 @@ function skillDemandContributors(initiatives, opts) {
31299
31309
  const skills = raw.filter((rs) => rs.fte > 0);
31300
31310
  const startKey = monthKey(i.timelineStart);
31301
31311
  const endKey = monthKey(i.timelineEnd);
31302
- if (productionRunOnly && i.status === "In Production") {
31303
- const runSkills = raw.map((rs) => ({ skillId: rs.skillId, fte: rs.runFte ?? rs.fte })).filter((rs) => rs.fte > 0);
31304
- if (runSkills.length > 0) {
31305
- contributors2.push({
31306
- initiativeId: i.id,
31307
- phase: "run",
31308
- start: startKey ? monthIndex(startKey) : todayIdx,
31309
- end: Number.POSITIVE_INFINITY,
31310
- axisEnd: null,
31311
- skills: runSkills
31312
- });
31313
- }
31314
- continue;
31315
- }
31316
31312
  if (openEnded && i.status === "In Production") {
31317
31313
  const runSkills = raw.map((rs) => ({ skillId: rs.skillId, fte: rs.runFte ?? rs.fte })).filter((rs) => rs.fte > 0);
31318
31314
  const startIdx = startKey ? monthIndex(startKey) : null;
@@ -31613,13 +31609,19 @@ function toRow(i, fields, c) {
31613
31609
  for (const f of fields) row[f] = fieldValue(i, f, c);
31614
31610
  return row;
31615
31611
  }
31616
- var SIZE_NOTE = "Answer cut to stay within about 60 KB: long texts are shortened (\u2026) and long lists keep their first entries. Ask a narrower question or use query_initiatives with fewer fields.";
31612
+ var SIZE_NOTE = "Answer cut to stay within about 60 KB: long texts are shortened (\u2026) and long lists and maps (such as counts by business unit) keep their first entries. Ask a narrower question or use query_initiatives with fewer fields.";
31613
+ var MIN_KEYS = 60;
31617
31614
  function shrink(v, maxStr, maxArr) {
31618
31615
  if (typeof v === "string") return v.length > maxStr ? `${v.slice(0, maxStr)}\u2026` : v;
31619
31616
  if (Array.isArray(v)) return v.slice(0, maxArr).map((x) => shrink(x, maxStr, maxArr));
31620
31617
  if (v && typeof v === "object") {
31621
31618
  const out = {};
31622
- for (const [k, x] of Object.entries(v)) out[k] = shrink(x, maxStr, maxArr);
31619
+ for (const [k, x] of Object.entries(v).slice(0, Math.max(maxArr, MIN_KEYS))) {
31620
+ const short = shrink(k, maxStr, maxArr);
31621
+ let key = short;
31622
+ for (let n = 2; key in out; n++) key = `${short} (${n})`;
31623
+ out[key] = shrink(x, maxStr, maxArr);
31624
+ }
31623
31625
  return out;
31624
31626
  }
31625
31627
  return v;
@@ -31918,7 +31920,8 @@ function rankPortfolio(p, q, now = Date.now()) {
31918
31920
  for (const id of ids ?? []) if (!known.has(id)) errors.push(`${field}: "${id}" is not an initiative id of this backup.`);
31919
31921
  }
31920
31922
  const skillIds = new Set(p.skills.map((s) => s.id));
31921
- for (const id of Object.keys(q.skillFteDeltas ?? {})) if (!skillIds.has(id)) errors.push(`skillFteDeltas: "${id}" is not a skill id of this backup.`);
31923
+ for (const id of Object.keys(q.skillFteDeltas ?? {})) if (!skillIds.has(id)) errors.push(`skillFteDeltas: "${id}" is not a skill id of this backup (the key is the skill id, not its name).`);
31924
+ errors.push(...capDeltaErrors(p, q.skillFteDeltas));
31922
31925
  if (errors.length) return { ok: false, errors };
31923
31926
  const base = baselineScenario(p.budgetField, p.exclusions);
31924
31927
  const scenario = {
@@ -32027,7 +32030,7 @@ function initiativeDetail(p, ref, now = Date.now()) {
32027
32030
  followUpsList: sortFollowUps(i.followUps ?? []).map((f) => ({ title: f.title, due: f.dueDate, done: f.doneDate ?? null, state: followUpState(f, now), assignee: f.assigneeName ?? null })),
32028
32031
  // The six latest by reporting date, newest first, as the Status Report page.
32029
32032
  statusReports: sortStatusReports(i.statusReports ?? []).slice(0, 6),
32030
- decisionLog: (i.decisionLog ?? []).slice(-10).map((d) => ({ date: d.date, kind: d.kind, owner: d.owner ?? null, rationale: d.rationale ?? null })),
32033
+ decisionLog: sortDecisionLog(i.decisionLog ?? []).slice(0, 10).map((d) => ({ date: d.date, kind: d.kind, owner: d.owner ?? null, rationale: d.rationale ?? null })),
32031
32034
  capabilityImpacts: (i.capabilityImpacts ?? []).map((ci) => ({ capability: linkedCapabilityPaths({ ...i, capabilityImpacts: [ci] }, p.capabilities)[0] ?? ci.capabilityId, kind: ci.kind, maturityTo: ci.maturityTo ?? null })),
32032
32035
  dependents: p.initiatives.filter((x) => x.dependsOn.includes(i.id)).map((x) => x.name)
32033
32036
  }
@@ -32309,18 +32312,26 @@ function boardSlices(p, q, now = Date.now()) {
32309
32312
  };
32310
32313
  }
32311
32314
  var SKILL_MONTHS_DEFAULT = 24;
32315
+ function capDeltaErrors(p, deltas) {
32316
+ const errors = [];
32317
+ for (const [id, d] of Object.entries(deltas ?? {})) {
32318
+ const s = p.skills.find((x) => x.id === id);
32319
+ if (!s) continue;
32320
+ if (s.fteCap <= 0 && d !== 0) errors.push(`skillFteDeltas: ${s.name} has no FTE cap (0 = no limit), so a delta would create one. Set a cap in the app (Settings \u2192 Taxonomy \u2192 Skills) to test a limit.`);
32321
+ else if (s.fteCap > 0 && Math.round((s.fteCap + d) * 10) / 10 <= 0) errors.push(`skillFteDeltas: ${d} takes the cap of ${s.name} (${s.fteCap} FTE) to 0, which the cut line reads as no limit at all. The smallest cap is 0.1 FTE (a delta of ${Math.round((0.1 - s.fteCap) * 10) / 10}).`);
32322
+ }
32323
+ return errors;
32324
+ }
32312
32325
  function skillAllocation(p, q, now = Date.now()) {
32313
32326
  const errors = [];
32314
32327
  if (p.skills.length === 0) return { ok: false, errors: ["The backup has no skill roster (Settings \u2192 Taxonomy \u2192 Skills in the app)."] };
32315
32328
  const byKey = (ref) => p.skills.find((s) => s.id === ref) ?? p.skills.find((s) => lc(s.name) === lc(ref));
32316
32329
  const chosen = q.skills?.length ? q.skills.map((r) => [r, byKey(r)]) : p.skills.map((s) => [s.id, s]);
32317
32330
  for (const [r, s] of chosen) if (!s) errors.push(`skills: "${r}" is not a skill of this backup (have: ${p.skills.map((x) => x.name).join(" | ")}).`);
32318
- for (const [id, d] of Object.entries(q.skillFteDeltas ?? {})) {
32319
- const s = p.skills.find((x) => x.id === id);
32320
- if (!s) errors.push(`skillFteDeltas: "${id}" is not a skill id of this backup (the key is the skill id, not its name).`);
32321
- else if (s.fteCap <= 0 && d !== 0) errors.push(`skillFteDeltas: ${s.name} has no FTE cap (0 = no limit), so a delta would create one. Set a cap in the app (Settings \u2192 Taxonomy \u2192 Skills) to test a limit.`);
32322
- else if (s.fteCap > 0 && Math.round((s.fteCap + d) * 10) / 10 <= 0) errors.push(`skillFteDeltas: ${d} takes the cap of ${s.name} (${s.fteCap} FTE) to 0, which the cut line reads as no limit at all. The smallest cap is 0.1 FTE (a delta of ${Math.round((0.1 - s.fteCap) * 10) / 10}).`);
32331
+ for (const id of Object.keys(q.skillFteDeltas ?? {})) {
32332
+ if (!p.skills.some((x) => x.id === id)) errors.push(`skillFteDeltas: "${id}" is not a skill id of this backup (the key is the skill id, not its name).`);
32323
32333
  }
32334
+ errors.push(...capDeltaErrors(p, q.skillFteDeltas));
32324
32335
  const known = new Set(p.initiatives.map((i) => i.id));
32325
32336
  for (const id of q.excludedIds ?? []) if (!known.has(id)) errors.push(`excludedIds: "${id}" is not an initiative id of this backup.`);
32326
32337
  if (q.fromMonth && !/^\d{4}-(0[1-9]|1[0-2])$/.test(q.fromMonth)) errors.push(`fromMonth: "${q.fromMonth}" is not a yyyy-mm month.`);
@@ -32429,7 +32440,7 @@ function skillAllocation(p, q, now = Date.now()) {
32429
32440
  ...list.length < perSkill.length ? { skillsReturned: list.length, skillCount: perSkill.length, cutForSize: `Cut to the first ${list.length} of ${perSkill.length} skills to fit; pass skills to see the others.` } : {},
32430
32441
  ...rowCap2 < limit && perSkill.some((s) => s.initiatives.length > rowCap2) ? { rowsCutForSize: `Initiative rows cut to ${rowCap2} per skill to fit (initiativeCount gives the full number); ask for fewer skills to see more.` } : {},
32431
32442
  ...whatIf ? { whatIf } : {},
32432
- note: "pipelineDemand = in-flight work not yet in production (the capacity Board Action's rule); headroom = cap minus pipelineDemand (run teams not counted, as that rule); runFte = In Production run teams organisation-wide; fundedUse = the baseline cut line's use of a capped skill; the monthly series is the Roadmap's skill demand in whole calendar months: a timeline's start month and end month count in full at build FTE, and an In Production run team (includeRunTeams) starts the month after the end. Quote these figures rather than recomputing them."
32443
+ note: "pipelineDemand = in-flight work not yet in production (the capacity Board Action's rule); headroom = cap minus pipelineDemand (run teams not counted, as that rule); runFte = In Production run teams organisation-wide; fundedUse = the baseline cut line's use of a capped skill; the monthly series is the Roadmap's skill demand in whole calendar months: a timeline's start month and end month count in full at build FTE, and an In Production run team (includeRunTeams) starts the month after the end at its run FTE (no run FTE set: the build FTE; a start without an end: the build FTE runs on from the start; no usable start or an inverted timeline: the run FTE from the current month). Quote these figures rather than recomputing them."
32433
32444
  });
32434
32445
  let rowCap = limit;
32435
32446
  while (rowCap > 5 && JSON.stringify(build(rowCap, perSkill)).length > OUTPUT_BUDGET_CHARS) rowCap = Math.max(5, Math.floor(rowCap / 2));
@@ -32634,7 +32645,7 @@ function createServer() {
32634
32645
  "load_portfolio",
32635
32646
  {
32636
32647
  title: "Load portfolio backup",
32637
- description: "Loads the organisation's FULL BACKUP (BICon app: Import/Export \u2192 Backup \u2192 Export, a bicon-backup-\u2026.json) for analysis and returns a portfolioId plus a summary: counts by status, business unit and driver, the skills, capabilities, saved portfolios and snapshots, currency, budget cap. Pass the file's full local `path` (ask the user where it is, often Downloads); inline `backup` JSON only for small files. Read-only: nothing is sent anywhere, author fields are removed on load. Call once, then pass `portfolio: \"<portfolioId>\"` to the other analysis tools. The data is as of the export date in the summary.",
32648
+ description: "Loads the organisation's FULL BACKUP (BICon app: Import/Export \u2192 Backup \u2192 Export, a bicon-portfolio-backup-YYYY-MM-DD.json) for analysis and returns a portfolioId plus a summary: counts by status, business unit and driver, the skills, capabilities, saved portfolios and snapshots, currency, budget cap. Pass the file's full local `path` (ask the user where it is, often Downloads); inline `backup` JSON only for small files. Read-only: nothing is sent anywhere, author fields are removed on load. Call once, then pass `portfolio: \"<portfolioId>\"` to the other analysis tools. The data is as of the export date in the summary.",
32638
32649
  inputSchema: loadPortfolioInputSchema
32639
32650
  },
32640
32651
  async (input2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicon/mcp-composer",
3
- "version": "0.11.0",
3
+ "version": "0.13.1",
4
4
  "description": "MCP server that helps an AI client compose a valid BICon portfolio initiative and hand it back as an import file (draft-first, no auth, no API calls).",
5
5
  "license": "UNLICENSED",
6
6
  "author": "BICon Establishment",