@bicon/mcp-composer 0.11.0 → 0.12.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.
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.12.0" : 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;
@@ -31613,13 +31624,19 @@ function toRow(i, fields, c) {
31613
31624
  for (const f of fields) row[f] = fieldValue(i, f, c);
31614
31625
  return row;
31615
31626
  }
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.";
31627
+ 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.";
31628
+ var MIN_KEYS = 60;
31617
31629
  function shrink(v, maxStr, maxArr) {
31618
31630
  if (typeof v === "string") return v.length > maxStr ? `${v.slice(0, maxStr)}\u2026` : v;
31619
31631
  if (Array.isArray(v)) return v.slice(0, maxArr).map((x) => shrink(x, maxStr, maxArr));
31620
31632
  if (v && typeof v === "object") {
31621
31633
  const out = {};
31622
- for (const [k, x] of Object.entries(v)) out[k] = shrink(x, maxStr, maxArr);
31634
+ for (const [k, x] of Object.entries(v).slice(0, Math.max(maxArr, MIN_KEYS))) {
31635
+ const short = shrink(k, maxStr, maxArr);
31636
+ let key = short;
31637
+ for (let n = 2; key in out; n++) key = `${short} (${n})`;
31638
+ out[key] = shrink(x, maxStr, maxArr);
31639
+ }
31623
31640
  return out;
31624
31641
  }
31625
31642
  return v;
@@ -31918,7 +31935,8 @@ function rankPortfolio(p, q, now = Date.now()) {
31918
31935
  for (const id of ids ?? []) if (!known.has(id)) errors.push(`${field}: "${id}" is not an initiative id of this backup.`);
31919
31936
  }
31920
31937
  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.`);
31938
+ 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).`);
31939
+ errors.push(...capDeltaErrors(p, q.skillFteDeltas));
31922
31940
  if (errors.length) return { ok: false, errors };
31923
31941
  const base = baselineScenario(p.budgetField, p.exclusions);
31924
31942
  const scenario = {
@@ -32027,7 +32045,7 @@ function initiativeDetail(p, ref, now = Date.now()) {
32027
32045
  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
32046
  // The six latest by reporting date, newest first, as the Status Report page.
32029
32047
  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 })),
32048
+ decisionLog: sortDecisionLog(i.decisionLog ?? []).slice(0, 10).map((d) => ({ date: d.date, kind: d.kind, owner: d.owner ?? null, rationale: d.rationale ?? null })),
32031
32049
  capabilityImpacts: (i.capabilityImpacts ?? []).map((ci) => ({ capability: linkedCapabilityPaths({ ...i, capabilityImpacts: [ci] }, p.capabilities)[0] ?? ci.capabilityId, kind: ci.kind, maturityTo: ci.maturityTo ?? null })),
32032
32050
  dependents: p.initiatives.filter((x) => x.dependsOn.includes(i.id)).map((x) => x.name)
32033
32051
  }
@@ -32309,18 +32327,26 @@ function boardSlices(p, q, now = Date.now()) {
32309
32327
  };
32310
32328
  }
32311
32329
  var SKILL_MONTHS_DEFAULT = 24;
32330
+ function capDeltaErrors(p, deltas) {
32331
+ const errors = [];
32332
+ for (const [id, d] of Object.entries(deltas ?? {})) {
32333
+ const s = p.skills.find((x) => x.id === id);
32334
+ if (!s) continue;
32335
+ 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.`);
32336
+ 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}).`);
32337
+ }
32338
+ return errors;
32339
+ }
32312
32340
  function skillAllocation(p, q, now = Date.now()) {
32313
32341
  const errors = [];
32314
32342
  if (p.skills.length === 0) return { ok: false, errors: ["The backup has no skill roster (Settings \u2192 Taxonomy \u2192 Skills in the app)."] };
32315
32343
  const byKey = (ref) => p.skills.find((s) => s.id === ref) ?? p.skills.find((s) => lc(s.name) === lc(ref));
32316
32344
  const chosen = q.skills?.length ? q.skills.map((r) => [r, byKey(r)]) : p.skills.map((s) => [s.id, s]);
32317
32345
  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}).`);
32346
+ for (const id of Object.keys(q.skillFteDeltas ?? {})) {
32347
+ 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
32348
  }
32349
+ errors.push(...capDeltaErrors(p, q.skillFteDeltas));
32324
32350
  const known = new Set(p.initiatives.map((i) => i.id));
32325
32351
  for (const id of q.excludedIds ?? []) if (!known.has(id)) errors.push(`excludedIds: "${id}" is not an initiative id of this backup.`);
32326
32352
  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 +32455,7 @@ function skillAllocation(p, q, now = Date.now()) {
32429
32455
  ...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
32456
  ...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
32457
  ...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."
32458
+ 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
32459
  });
32434
32460
  let rowCap = limit;
32435
32461
  while (rowCap > 5 && JSON.stringify(build(rowCap, perSkill)).length > OUTPUT_BUDGET_CHARS) rowCap = Math.max(5, Math.floor(rowCap / 2));
@@ -32634,7 +32660,7 @@ function createServer() {
32634
32660
  "load_portfolio",
32635
32661
  {
32636
32662
  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.",
32663
+ 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
32664
  inputSchema: loadPortfolioInputSchema
32639
32665
  },
32640
32666
  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.12.0",
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",