@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 +1 -1
- package/dist/index.mjs +39 -13
- 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.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
|
|
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)
|
|
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(
|
|
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
|
|
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}).`);
|
|
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
|
|
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.
|
|
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",
|