@bicon/mcp-composer
v0.3.5
Published
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).
Maintainers
Readme
BICon Initiative Composer (MCP server)
An MCP server that helps an AI client — e.g. Claude Desktop — set up a portfolio initiative conversationally. Creating an initiative by hand means filling ~15 metadata fields and rating 30 sub-dimensions across four scoring dimensions (plus optional economics and a large EU-AI-Act block); this server lets an assistant run that interview and produce a complete, valid initiative.
Draft-first by design — no auth, no API calls
The server holds no credentials and never talks to the BICon API. Its tools assemble and validate an initiative and hand back an import file; you load that file through the app's existing, authenticated import UI. The "Add as new → Confirm" step in the app is the human-in-the-loop — nothing reaches your organisation until you confirm it in the browser.
Why: the API only accepts short-lived browser session tokens (there is no API key), and a draft-first flow adds zero new attack surface. Live create/update via API keys is a separate, security-reviewed phase.
Tools & resource
| Name | What it does |
|---|---|
| resource initiative://schema / tool get_initiative_schema | The field catalog: required fields, enum values, the four dimensions with their named sub-dimensions (EN/DE labels + help text, plus the wording the app uses in digital / innovation portfolios), the exact shape of every optional section, the EU-AI-Act vocabularies, and the handoff steps. Read this first. |
| assemble_initiative | Compact spec (metadata + named sub-ratings 0–100 + confidence per dimension + optional sections, optionally the organisation's context) → fully-formed initiative validated against the API contract (CreateInitiativeBody from @workspace/api-zod). Mints the initiative id and every nested row id (decision-log entries, status reports, incidents — a client never invents ids), derives the four dimension scores (the app's own scoring function), accepts dates as dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd, returns {ok, initiative, derivedScores, warnings, errors, contextId}. Pass id (an existing initiative's) only to update that initiative. |
| 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. |
| 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) and non-archived initiatives (id + name), 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. |
| composer_version | Returns {composerVersion, capabilities}. Every successful tool result also carries composerVersion. |
The portfolio context — how the Composer learns an organisation's settings
The Composer holds no credentials and calls no API, so on its own it cannot
know how an organisation configured its portfolio. The app therefore exports
a small portfolio-context JSON (Import/Export → the AI-assistant card →
Download portfolio context): the active portfolio mode, which evaluation
sliders are switched off, the organisation's own slider wording and rating
notes, business units, categories, tags, the skill roster and the live
initiatives (id + name), currency, discount rate and the AI-Act module switch.
The user hands that file to the assistant at the start; the assistant calls
read_portfolio_context once and then passes the returned contextId
(ctx_<16 hex>, a content hash — or the whole file again) as context to
assemble_initiative / export_initiative_file, so a 5–40 KB document is
not re-emitted three times per initiative. The id resolves against a
per-process content cache; an unknown id (server restarted) is a clear error
naming the fix, never a wrong answer. Effects:
- the interview uses the organisation's wording (org label › mode overlay › base) and skips switched-off sub-dimensions;
- switched-off sub-dimensions are optional in the spec, stored as 0 (the app's
blank default — a value sent anyway is zeroed with a warning) and
excluded from the dimension score — the app's own derivation, so the
score does not change on the first save in Evaluate; a dimension with every
sub-dimension off needs only
{ confidence }; businessUnitandaiCategorymust be on the organisation's lists (no inventing — matched case-insensitively and normalised to the organisation's spelling; new ones are created in the app's Settings);dependsOn,requiredSkills[].skillIdand an updateidmust be ids that exist (any id format; Retired initiatives count, archived ones do not);- ratings must be on the organisation's slider step (Settings › Slider granularity: 5, 10, 20 or 25) — the plan states it, a value off the grid is rejected with the step in the message (files exported before 2026-08-21 carry no step and are not checked);
- every EU-AI-Act field the app strips is warned about while the organisation's AI-Act module is off; the export file defaults to the context's mode and language; a file newer than the Composer knows is flagged.
Without the file everything falls back to the built-in catalog: business unit and category are free text, dependency / update ids must at least be UUID-shaped (zod's rule, the same the API uses), and scores are derived over all 30 sub-dimensions. Export the file again after changing settings — the plan reports its export date.
How validation works (and why it lives at the wire)
The MCP SDK validates every call with the registered schema and hands the
handler the parsed value — whatever the schema strips, the server never sees.
So the spec schema is registered as a full strict object, with every
optional section a deep-strict copy of the API contract: an unknown key
anywhere is rejected by the SDK with its exact path
(Unrecognized key(s) in object: 'literacy' at aiAct), enum values, dates
(dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd — real calendar dates, normalised to
yyyy-mm-dd) and blank names are caught with field paths, and the advertised JSON Schema
(additionalProperties: false, no $ref pointers) means what it says. A
section may also be sent as a JSON string (clients working from a stale
tool descriptor did that); it is parsed and validated identically. What a JSON
Schema cannot express — the 30 named ratings, the AI-Act controlled
vocabularies (aiActVocabularies in the descriptor; the app drops unknown
obligation keys and never shows unknown artifact keys), the score derivation —
is checked in the assembler, which reports all problems of a call at once
with the allowed values in the message (id shapes included). Row ids of
decision-log entries, status reports and incidents are minted by the
Composer — a client-supplied UUID is kept, anything else replaced — because
the app keys those rows per initiative and a re-used id from another
initiative is exactly what collided on import once.
Errors the SDK raises before a handler runs (Input validation error: …) carry
no composerVersion — that is normal. A successful result without it means an
old build is answering (see next section).
Stale instances (reinstall is not enough — quit the app)
Claude Desktop runs an extension in two places: a chat-UI instance, which
every reinstall restarts, and a session-facing instance
(LocalMcpServerManager in Desktop's main.log) that serves Claude Code,
Cowork and remote claude.ai chats reaching the device. The session-facing
instance is started once at app launch and is only replaced on a full app
quit — reinstalling or disabling/enabling the extension leaves it running
the old code. Observed on 2026-08-18: five reinstalls, the chat-UI copy was
current every time, and every session still got the original build.
Tell: a successful tool result without composerVersion, or
composer_version missing from the tool list. Fix: install the new .mcpb,
then quit Claude Desktop completely (tray icon → Quit — closing the window
is not enough), reopen, and check with composer_version.
One source for the domain
The Composer imports the app's own domain leaf, @workspace/initiative-model
(types and value lists incl. the AI-Act vocabularies, the score derivation, the
portfolio-mode wording overlays, the backup-format version) — the same package
the web app renders and validates with — and derives its enum lists from the
generated API contract. The 30 sub-dimension keys are the shared leaf's too;
the only hand-listed part is their base wording, and catalog-drift.test.ts
pins the keys to the contract in both directions. server.test.ts drives the real server through
the real SDK client over an in-memory transport, so what is asserted is what a
client sees.
Install for users
Claude Desktop — the extension (recommended, no Node needed)
Download bicon-composer.mcpb from the BICon app (Import/Export → Set
up initiatives with an AI assistant, or directly at
https://www.bicon.digital/downloads/bicon-composer.mcpb) and open it —
double-click, or Claude Desktop → Settings → Extensions → Install. Claude
Desktop runs the server on its own built-in Node runtime, so there is nothing
else to install and no configuration file to edit. Then quit Claude Desktop
completely and reopen (see Stale instances), start a new chat and ask:
"Use the BICon Initiative Composer to help me set up a new initiative."
Why the extension is primary: current Claude Desktop builds manage local MCP
servers as Desktop Extensions and treat claude_desktop_config.json as their
own preferences store — a hand-pasted mcpServers block there is silently
overwritten. There is no supported config-file route for Claude Desktop.
Other MCP clients — the npm package
@bicon/mcp-composer on npm is a single self-contained bundle whose only
runtime dependency is the MCP SDK. Any MCP-capable client with Node ≥ 20 can
run it; for a client that reads a mcpServers config, add this and restart
the client:
{
"mcpServers": {
"bicon-composer": {
"command": "npx",
"args": ["-y", "@bicon/mcp-composer@latest"]
}
}
}@latest matters: an unpinned npx spec keeps serving whatever it cached
first. Check what you got with the composer_version tool — the registry
version and the extension version are released together (see Publishing);
if npm is behind, use the extension.
Prerequisite: Node.js ≥ 20 on the same operating system as the client.
On Windows with Node only inside WSL, launch the server through WSL:
"command": "wsl.exe", "args": ["-d", "Ubuntu", "--", "/path/to/npx", "-y",
"@bicon/mcp-composer@latest"] (replace /path/to/npx with the output of
which npx in WSL). Restart the client afterwards.
Build & run from source (contributors)
pnpm --filter @bicon/mcp-composer run build # → dist/index.mjs (npm bundle) + dist/mcpb/composer-<v>.mjs (extension bundle)
pnpm --filter @bicon/mcp-composer run test # vitest, incl. the SDK-boundary suite (builds the bundles itself)
pnpm --filter @bicon/mcp-composer run build:mcpb # → dist/bicon-composer.mcpb, copied to the web app's public/downloads
node lib/integrations/mcp/dist/index.mjs # stdio server (used by a client, not by hand)For a client that reads a mcpServers config, point it at the local build
with an absolute path ("command": "node", "args": ["/ABSOLUTE/PATH/TO/lib/integrations/mcp/dist/index.mjs"];
on Windows with the repo in WSL, run through wsl.exe with the WSL node).
For Claude Desktop, build and install the extension instead.
build:mcpb stages the manifest (version, entry point and launch args written
from package.json — the checked-in mcpb/manifest.json carries
placeholders), adds the single self-contained bundle, and — before zipping —
boots the staged server and drives it through the protocol: initialize
(version), tools/list (five tools, no $ref), composer_version,
resources/list, and an assemble_initiative call with an unknown AI-Act key
that must be rejected by name. It then copies the .mcpb to
artifacts/bicon-portfolio/public/downloads/ and writes
src/lib/composer-extension.json (version + path), which the Import/Export
card imports. Requires python3 for the zip container.
Using it (the interview → import flow)
- (Recommended) In the app, Import/Export → Download portfolio context, and give that file to Claude first — it then interviews with your organisation's own wording, skips switched-off sliders and only accepts your business units, categories, skills and initiatives.
- Ask Claude: "Help me set up a new initiative in BICon." It reads
initiative://schema(and your context) and interviews you — metadata first, then the sub-dimensions with the app's help text, then a confidence per dimension. Dates are dd/mm/yyyy in conversation. - Claude calls
assemble_initiativeand shows you the derived scores and any warnings as a sanity check (fix and re-run if anything looks off). - Claude calls
export_initiative_filewith the same spec → save the returned JSON under the suggested filename (initiative-<name>-<date>.json). To update an existing initiative, name it — Claude passes its id from the context and the app offers "Update"; a re-export without the id is recognised by name and offered as an update too. - In the app (signed in to the target organisation): if the initiative carries
EU-AI-Act data, make sure the organisation's EU-AI-Act module is switched
on (Settings › AI Act — the app strips AI-Act fields on import while it is
off; the file's
sourceModeis informational and cannot override that). Then Import/Export → Single initiative card → drop the file → the preview should show 0 warnings → Add as new (or Update, which keeps the existing initiative's id, creation date and links) → Confirm. - The initiative appears in the Inventory with exactly the scores Claude reported.
Publishing (maintainers)
Release = extension + npm together, from one version:
cd lib/integrations/mcp
npm version patch|minor # bumps package.json — the ONLY place the version is written
pnpm run test # incl. the SDK-boundary suite
pnpm run build:mcpb # bundles, ship-gate smoke, copies the .mcpb + version JSON into the web app
npm login # once, as the BICon npm account (business identity, 2FA on)
npm publish # prepack builds; publishConfig.access is publicThen commit (the .mcpb and composer-extension.json are part of the
release commit) and verify from a clean machine:
npx -y @bicon/mcp-composer@latest starts and composer_version reports the
new version. The bundle inlines zod, @workspace/api-zod and
@workspace/initiative-model (workspace-only packages the registry cannot
resolve), so what ships is dist/index.mjs + this README with
@modelcontextprotocol/sdk as the sole runtime dependency —
publish-shape.test.ts guards exactly that.
Not in scope (Phase 2)
No authentication, no live POST /initiatives, no read/list/update tools, no
hosted/HTTP transport. Those require an API-key auth path in the API server that
doesn't exist yet — deliberately deferred to a separate, security-reviewed
phase after the production Clerk split.
