@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 +1 -1
- package/dist/index.mjs +39 -28
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
|
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)
|
|
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(
|
|
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
|
|
32319
|
-
|
|
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
|
|
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.
|
|
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",
|