@bli-cockpit/mcp
v0.1.59
Published
bli-tower: an MCP server over BLI Cockpit's agent doors: JARVIS (jarvis_*), documents (docs_*), channels (msg_*), issues (work_*), the daily page (brief_*), meeting notes (notes_*), the ops board (ops_status/slack_*), settings/team/model, Scout and the wo
Readme
@bli-cockpit/mcp — bli-tower
Two, deliberately separate, tool families over one MCP server (bin
bli-cockpit-mcp, server id bli-tower):
- The agent doors — the SAME routes the
cockpitCLI and the browser call, authenticated with this machine's collector device token (~/.config/bli-cockpit/session.json, written bycockpit login). This is the live, actively-developed half:docs_*/msg_*(BLI-3706),work_*(BLI-3716) andjarvis_*(BLI-3732). emit_event/get_ticket_timeline/get_active_tickets— the legacy event-stream write path, authenticated withBLI_OPERATOR_TOKENor thebli-eventsession helper. See "Legacy event-stream tools" below.
The two halves use DIFFERENT credentials and DIFFERENT base URLs on purpose
(the agent doors talk to the production Tower dashboard; the event-stream
tools talk to BLI_API_BASE_URL, historically a local worker) — see
docs/architecture/cli-caller-identity.md for why they needed no new
server-side door: every one of those routes already accepts a collector device
token.
Every cockpit <noun> <verb> that talks to Tower is meant to have a twin
here (BLI-3706: "a surface an agent cannot reach conveniently is as good as
dead"). src/verb-census.test.ts is that rule as a test — it reads the CLI's
own decision tables and fails when a verb is in neither the twin map, the
terminal-only list, nor the written follow-up queue.
Every Tower verb, and the tool that does the same thing
The three tables below are GENERATED from src/verb-census.ts by
npm run mcp:readme — do not hand-edit them. They are the same three lists
src/verb-census.test.ts polices, so this page and that suite cannot disagree
about what an agent can reach.
107 of 112 Tower verbs have an MCP twin.
Each tool goes through the SAME door its CLI verb calls, with the same
collector device token — never a second route and never a service-role
reader. src/verb-census.test.ts fails when a verb is in none of the
three tables below.
| CLI verb | MCP tool | Door |
| --- | --- | --- |
| cockpit brief done | brief_done | POST /api/jarvis/ticks |
| cockpit brief history | brief_history | GET /api/jarvis/brief?history=1 |
| cockpit brief read | brief_read | GET /api/jarvis/brief |
| cockpit brief rewrite | brief_rewrite | POST /api/jarvis/recompile |
| cockpit brief status | brief_status | GET /api/ops/brief-status |
| cockpit brief undone | brief_undone | POST /api/jarvis/ticks |
| cockpit cal calendars | cal_calendars | GET /api/cal/calendars |
| cockpit cal create | cal_create | POST /api/cal/events |
| cockpit cal detach | cal_detach | DELETE /api/cal/calendars |
| cockpit cal find | cal_find | GET /api/cal/find |
| cockpit cal next | cal_next | GET /api/cal/next |
| cockpit cal share | cal_share | PATCH /api/cal/calendars |
| cockpit cal sync | cal_sync | POST /api/cal/calendars/[id]/sync |
| cockpit cal today | cal_today | GET /api/cal/today |
| cockpit cal week | cal_week | GET /api/cal/week |
| cockpit careers decide | careers_decide | POST /api/careers/applications/[id]/decide |
| cockpit careers grade | careers_grade | POST /api/careers/applications/[id]/grade |
| cockpit careers send-takehome | careers_send_takehome | POST /api/careers/applications/[id]/invite |
| cockpit careers list | careers_list | GET /api/careers/applications |
| cockpit careers rescreen | careers_rescreen | POST /api/careers/applications/[id]/rescreen |
| cockpit careers resume | careers_resume_url | GET /api/careers/applications/[id]/resume-url |
| cockpit careers settings set | careers_settings_set | PUT /api/careers/settings |
| cockpit careers settings show | careers_settings_show | GET /api/careers/settings |
| cockpit careers show | careers_show | GET /api/careers/applications/[id] |
| cockpit careers takehome set | careers_takehome_set | PUT /api/careers/takehomes/[role] |
| cockpit careers takehome show | careers_takehome_show | GET /api/careers/takehomes/[role] |
| cockpit correct | brief_correct | POST /api/jarvis/corrections |
| cockpit docs create | docs_create | POST /api/docs/documents |
| cockpit docs list | docs_list | GET /api/docs/documents |
| cockpit docs read | docs_read | GET /api/docs/documents/[id] |
| cockpit docs tree | docs_tree | GET /api/docs/tree |
| cockpit docs update | docs_update | PATCH /api/docs/documents/[id] |
| cockpit issue comment | work_comment_issue | POST /api/work/issues/[id]/comments |
| cockpit issue create | work_create_issue | POST /api/work/issues |
| cockpit issue history | work_issue_history | GET /api/work/issues/[id]/history |
| cockpit issue list | work_list_issues | GET /api/work/issues |
| cockpit issue move | work_move_issue | POST /api/work/issues/[id]/state |
| cockpit issue show | work_get_issue | GET /api/work/issues/[id] |
| cockpit issue update | work_update_issue | PATCH /api/work/issues/[id] |
| cockpit jarvis | jarvis_ask | POST /api/jarvis/cli |
| cockpit jarvis --trace | jarvis_trace | GET /api/ops/trace/[id] |
| cockpit mail accounts | mail_accounts | GET /api/mail/accounts |
| cockpit mail attachment | mail_attachment | GET /api/mail/attachments/[id]?meta=1 |
| cockpit mail detach | mail_detach | DELETE /api/mail/accounts |
| cockpit mail inbox | mail_inbox | GET /api/mail/inbox |
| cockpit mail read | mail_read | GET /api/mail/threads/[id] |
| cockpit mail search | mail_search | GET /api/mail/search |
| cockpit mail send | mail_send | POST /api/mail/send |
| cockpit mail sync | mail_sync | POST /api/mail/accounts/[id]/sync |
| cockpit memory consolidation | memory_consolidation | GET /api/memory/consolidation |
| cockpit memory forget | forget_memory (bli-memory) | POST /api/memory/forget |
| cockpit memory log | memory_experience | POST /api/memory/experience |
| cockpit memory profile | memory_profile | GET /api/memory/profile |
| cockpit memory requeue | memory_requeue | POST /api/memory/requeue |
| cockpit memory save | save_memory (bli-memory) | POST /api/memory/save |
| cockpit memory search | search_memory (bli-memory) | POST /api/memory/search |
| cockpit memory update | update_memory (bli-memory) | POST /api/memory/update |
| cockpit model set | model_set | POST /api/settings/jarvis-model |
| cockpit model show | model_show | GET /api/settings/jarvis-model |
| cockpit models compare | models_compare | GET /api/models/cards?compare= |
| cockpit models list | models_list | GET /api/models/cards |
| cockpit models show | models_card | GET /api/models/cards?id= |
| cockpit msg channels | msg_channels | GET /api/msg/channels |
| cockpit msg create | msg_create_channel | POST /api/msg/channels |
| cockpit msg dm | msg_dm | POST /api/msg/channels (dm) |
| cockpit msg read | msg_read | GET /api/msg/channels/[id]/messages |
| cockpit msg send | msg_send | POST /api/msg/channels/[id]/messages |
| cockpit msg thread | msg_thread | GET /api/msg/channels/[id]/messages?thread_parent_id= |
| cockpit notes accounts | notes_accounts | GET /api/notes/accounts |
| cockpit notes detach | notes_detach | DELETE /api/notes/accounts |
| cockpit notes folders | notes_folders | GET /api/notes/folders |
| cockpit notes list | notes_list | GET /api/notes/library |
| cockpit notes mkdir | notes_mkdir | POST /api/notes/folders |
| cockpit notes move | notes_move | POST /api/notes/move |
| cockpit notes paste | notes_paste | POST /api/notes/upload (text) |
| cockpit notes place | notes_place | POST /api/notes/place |
| cockpit notes rename | notes_rename | PATCH /api/notes/folders/[id] |
| cockpit notes rmdir | notes_rmdir | DELETE /api/notes/folders/[id] |
| cockpit notes share | notes_share | POST /api/notes/share |
| cockpit notes shelf | notes_shelf | GET /api/notes/shelf |
| cockpit notes shelves | notes_shelves | GET /api/notes/library |
| cockpit notes show | notes_show | GET /api/notes/library/[id] |
| cockpit notes sync | notes_sync | POST /api/notes/accounts/[id]/sync |
| cockpit notes unshare | notes_unshare | POST /api/notes/share (share=false) |
| cockpit notes upload | notes_upload | POST /api/notes/upload |
| cockpit ops recompile | ops_recompile | POST /api/ops/recompile |
| cockpit ops status | ops_status | GET /api/ops/status |
| cockpit project list | work_list_projects | GET /api/work/projects |
| cockpit scout board | scout_board | GET /api/cockpit/scout |
| cockpit scout dismiss | scout_dismiss | POST /api/cockpit/scout (dismiss) |
| cockpit scout start | scout_start | POST /api/cockpit/scout (start) |
| cockpit scout undo | scout_undo | POST /api/cockpit/scout (undo_dismiss) |
| cockpit search | tower_search | GET /api/search |
| cockpit settings env delete | settings_delete | DELETE /api/settings/env-blobs |
| cockpit settings env list | settings_list | GET /api/settings/env-blobs |
| cockpit settings switches set | settings_set | POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor |
| cockpit settings | settings_show | GET /api/settings/* + /api/team/members |
| cockpit slack coverage | slack_coverage | GET /api/ops/slack/coverage |
| cockpit slack read | slack_read | POST /api/ops/slack/read |
| cockpit team device list | team_device_list | GET /api/team/devices |
| cockpit team device revoke | team_device_revoke | POST /api/ambient/devices/[deviceId]/revoke |
| cockpit team invite | team_invite | POST /api/team/invite |
| cockpit team members | team_members | GET /api/team/members |
| cockpit team role | team_role | PATCH /api/team/members/[userId]/role |
| cockpit usage people | usage_people | GET /api/usage/people |
| cockpit usage sessions | usage_sessions | GET /api/usage/sessions |
| cockpit workbook | workbook_read | GET /api/cockpit/workbook |
Owed a twin
This table should only ever shrink.
Nothing. Every Tower verb an agent could want has a door (BLI-3756).
Terminal-only
A claim about the verb's nature, not a backlog.
| CLI verb | Why it can never have a twin |
| --- | --- |
| cockpit brief edit | opens the person's own $EDITOR on this machine and files what they changed; an agent has no editor to open (commands/editor.ts) |
| cockpit cal add-ical | carries a calendar's SECRET iCal ADDRESS — a permanent, unauthenticated, read-anything-on-that-calendar URL. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a calendar stays a thing a person does at a terminal (BLI-3709) |
| cockpit mail add-imap | carries a Google app password. The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so attaching a mailbox stays a thing a person does at a terminal (BLI-3708) |
| cockpit notes connect | carries a notetaker API key (Fellow or Circleback). The CLI reads it from STDIN precisely so it never lands in an argument list; an MCP tool argument travels through a model's context window and whatever transcript store sits behind it, so connecting a notetaker stays a thing a person does at a terminal or in Tower's Settings (BLI-4383) |
| cockpit notes webhook | carries the VENDOR's webhook signing secret, the key an HMAC over every delivered payload is checked against. Exactly the ruling notes connect already carries, for the same credential shape: the CLI reads it from STDIN so it never lands in an argument list, and an MCP tool argument travels through a model's context window and whatever transcript store sits behind it (BLI-4394) |
For memory adoption and ordinary hook measurements, use cockpit ops --memory
or ops_status with memory: true. Both also read GET /api/ops/memory-usage.
jarvis_* tools (BLI-3732)
JARVIS itself — the same assistant Tower web chat, the Slack DM and
cockpit jarvis reach, over the same two doors with the same device token.
| Tool | Wraps | Notes |
| --- | --- | --- |
| jarvis_ask | POST /api/jarvis/cli | One turn. Returns the answer, its Source: lines, turn_id and thread_id. |
| jarvis_trace | GET /api/ops/trace/[id] | That turn's step tree — model steps, tool calls, timings, failures. Metadata only. Takes a turn_id or "last". |
| jarvis_dispatch | POST /api/jarvis/cli | The coding arm (BLI-2981). Two calls: once for the plan, once with the person's approval code. |
| jarvis_check | POST /api/jarvis/cli | Where a dispatched coding task got to. Spends no approval. |
One conversation per call (BLI-3786). A thread is single-writer: two turns
in the same conversation at the same time read each other's messages, and one
can answer the other's question. So every jarvis_* tool sends NO thread
unless the caller named one, the door mints a conversation for that call alone,
and the answer's thread_id is the name it used — pass that back as thread
to continue deliberately. "main" is the shared terminal default nobody chose
and is never sent from here.
jarvis_ask answers with the SAME object cockpit jarvis --json prints —
answer, sources, turn_id, thread_id, trace_thread_id, degraded,
degraded_reasons — so a script and an agent read one contract. A worked
example:
jarvis_ask { "question": "what did Brandon ship yesterday?" }
-> { "answer": "Two pull requests went in…\n\nSource: BLI-3654 — …",
"sources": ["Source: BLI-3654 — …"],
"turn_id": "0f3c…", "thread_id": "agent-6b1e…",
"degraded": false, "degraded_reasons": [] }
jarvis_trace { "turn_id": "0f3c…" } # or "last"
-> Turn 0f3c…
6 steps · 4.2 s · 1 failed
jarvis-chat 4.2s openai:gpt-5.6-terra
readIssues 0.2s
searchMemory 1.1s
readScout 0.3s FAILEDdegraded is never a failure — the answer is still returned. It names how the
turn fell short: model_fallback (a fallback model answered),
answer_revised (the grounding gate took words back), tool_step_failed, or
no_turn_id (no trace id came back, so the tree cannot be opened later).
The coding arm's approval code. jarvis_dispatch takes approval_code as
a parameter and never derives, guesses or fabricates one. Call it once with no
code to get the plan and its 8-character code, show both to the person, then
call it again with the code THEY hand back. The gate's first lock (an HMAC
over the exact plan, this account and a server secret) is untouched by this
surface — this server holds no secret and cannot compute a code. Its second
lock ("the code came from the human's own message") is weaker here than in a
terminal, because nothing on the wire can tell a person-relayed code from one
the model lifted out of the previous answer; src/jarvis-tools.ts says so in
full at the top of the file.
The read tools (BLI-3756 batch 1)
Every Tower READ the CLI already had. Doors and tool names are in the generated table above; what is worth knowing before you call one:
brief_read/brief_history/brief_status— the TODAY page as finished text, the days a person has a page for, and why a brief was or was not delivered.subjectchanges WHOSE page is asked for, never who is asking. A delivery reason other thandelivered(not_monday,no_slack_identity, …) is a real answer and is NOT returned as an error.notes_list/notes_show/notes_shelf/notes_shelves— the meeting-notes library. A device token has no signed-in browser session, so these reads are routinely NARROWER than a person's; every one of them appends the door's ownscope/degradedBecausereason rather than letting an agent conclude a note was never taken.ops_status/slack_coverage/slack_read— the pipeline board (each job judged against its own expected interval, server-side), Slack collection health, and the collected messages themselves.slack_readhas a narrower audience thanslack_coverage, and says so on a 403.settings_show/settings_list/model_show/team_members/team_device_list— settings as this caller may see them. A section you may not see comes back asadmin only, not as an error;settings_listreturns env-file names, sizes and stamps and can never return a value.scout_board/workbook_read— the Scout board (with the page's own standing sentences, and a truncated read saying so) and the per-project document library, rendered by the same walker the page uses.
The write tools (BLI-3756 batch 2)
The seventeen things a person can DO from cockpit and could not do from an
agent. Same doors, same device token, same refusal words — plus one rule the
reads never needed:
An act that cannot be undone is refused without confirm: true. The CLI's
gate is --yes or a person at a keyboard; a client has no keyboard, so the
explicit flag is the whole of it here, and the reason label is the CLI's own
confirmation_required. It guards notes_share (sharing lets everyone signed
in read the note), settings_delete, team_role and team_device_revoke.
Taking a share BACK asks for nothing — nobody needs to be talked out of
narrowing who can read something.
brief_rewrite/brief_correct— ask for a page to be written again, and say that one line on it is wrong.brief_rewriteQUEUES and returns the request id immediately; it does not wait, because an 800-second compile outlives any client deadline and "the tool timed out" would be indistinguishable from "the compile failed".brief_correctrefuses a claim id the page does not carry BEFORE writing, and Tower's push-back ("the record disagrees") is a normal result — the correction is filed either way.notes_upload/notes_paste/notes_share/notes_unshare/notes_move— put a note in from this machine's disk or from text, share it, take it back, move it to another shelf. A path whose NAME looks like a key or credential file is refused without being opened; so is a missing, empty or over-20-MB file. A large note is one model call server-side and can take minutes — your client may give up first, and the upload keeps going.ops_recompile— write one person's page again. It spends a model call;dry_run: trueresolves the person and reads the current page without compiling. If Tower's own 800-second budget runs out the effect is UNKNOWN, not failed, and it says so rather than inviting you to pay twice.scout_start/scout_dismiss/scout_undo— move one card. Each reads the board first, matches the id EXACTLY (no prefixes on this surface — a near-miss would settle a card nobody decided on), and a 403 names the gate without costing you the read.settings_set/settings_delete/model_set— write one settings section, delete an env file, or just change which model answers you. Env content is an argument here rather than stdin (a tool call has no argv, no shell history and no process list), and the rule that survives is the one that mattered: it is never echoed back — not in the answer, not in the structured payload, not in a log.team_invite/team_role/team_device_revoke— invite somebody, change a role, end a machine's pairing. All three relay a super_admin decision made on the server.team_device_revokelooks the device up and names it back to you first; withoutconfirm: truethat naming IS the answer and nothing is revoked.
docs_* / msg_* tools (BLI-3706)
| Tool | Wraps | Notes |
| --- | --- | --- |
| docs_list | GET /api/docs/documents | Every document you may read: id, slug, title, visibility. Never a body. |
| docs_tree | GET /api/docs/tree | The library as its nesting rather than a flat list. Never a body. |
| docs_read | GET /api/docs/documents/[id] | One document's title and full body, by id or slug (resolved against docs_list first). |
| docs_create | POST /api/docs/documents | Creates a document. Omitting body_markdown creates an empty one. |
| docs_update | PATCH /api/docs/documents/[id] | Updates title/body/visibility/parent (a parent change IS a move). Emptying a document that holds text needs allow_empty: true (BLI-3757's guard). |
| msg_channels | GET /api/msg/channels | Every channel you are a member of. |
| msg_read | GET /api/msg/channels/[id]/messages | Recent messages of one channel, newest first. channel is an id or a #name. |
| msg_thread | GET /api/msg/channels/[id]/messages?thread_parent_id= | One thread's replies, oldest first, so it reads as a conversation. |
| msg_send | POST /api/msg/channels/[id]/messages | Posts a message. |
Auth: this machine's collector device token, not BLI_OPERATOR_TOKEN.
Run cockpit login once; the server reads
~/.config/bli-cockpit/session.json fresh on every call (never cached at
startup, so a machine with no pairing yet still serves the event-stream tools
below — only a docs_*/msg_* call on that machine fails, by name). Two
optional env overrides, mirroring @bli-cockpit/memory-mcp's own:
| Var | Required | Purpose |
| --- | --- | --- |
| BLI_COCKPIT_MCP_DEVICE_TOKEN | no | Override the device token (skips the session file). |
| BLI_COCKPIT_MCP_DASHBOARD_URL | no | Override the dashboard base URL (defaults to production, or COCKPIT_DASHBOARD_URL if set). |
A door refusal or a network failure both come back as an MCP isError result
naming the door's own reason label (e.g. document_not_found_or_unreadable)
— never a generic "something went wrong".
Registration on an intern machine
cockpit memory install (run unasked by doctor and the daily sync
tick) registers bli-tower beside bli-memory in the SAME two files —
~/.claude.json's mcpServers and ~/.codex/config.toml's
[mcp_servers.bli-tower] table — resolving this package's own bin the same
way it resolves bli-memory-mcp's (beside the running @bli-cockpit/cli
install, then PATH). bli-tower has no Claude Code hooks and no Codex skill,
so nothing is written to ~/.claude/settings.json for it. See
packages/cockpit-local-collector/src/commands/tower-mcp-install.ts.
Legacy event-stream tools
Thin wrapper over the canonical REST event-stream contracts (see
docs/plans/cockpit-agent-ops-control-plane.md §6) — the FROZEN active-emission
substrate (AGENTS.md "What this repo is"), not where new work goes.
| Tool | Wraps | Notes |
| --- | --- | --- |
| emit_event | POST /api/events/emit | Appends a canonical event. Supports idempotency_key. |
| get_ticket_timeline | GET /api/events/timeline?ticket_id=... | Returns the canonical timeline for a task-visible ticket. |
| get_active_tickets | GET /api/tickets/active?... | Returns active, task-visible tickets derived from orchestration_events. |
emit_event
| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| ticket_id | string | yes | Linear ticket ID, e.g. BUI-412. |
| event_type | string | yes | Canonical event type (e.g. worker_dispatched, note). |
| payload | object | no (default {}) | Event payload. |
| note_md | string | no | Optional markdown note. |
| correlation_id | string | no | E.g. an agent trace ID. |
| idempotency_key | string | no | Replays return status: "replay". |
Returns { event_id, occurred_at, status: "created" | "replay" }.
get_ticket_timeline
| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| ticket_id | string | yes | |
| since | string (ISO-8601) | no | |
| limit | number (1–500) | no (default 100) | |
The API resolves ticket_id through tasks.linear_ticket_id and denies
callers who cannot see that task.
get_active_tickets
All four filters (source, project, lane, assignee) optional strings.
The API derives active work from orchestration_events, resolves tickets back
to tasks.linear_ticket_id, and returns only tasks visible to the caller.
Environment variables (legacy event-stream tools only)
docs_*/msg_*'s own two optional overrides are documented above, beside
those tools — this table is emit_event/get_ticket_timeline/
get_active_tickets's, a separate credential and base URL.
| Var | Required | Default | Purpose |
| --- | --- | --- | --- |
| BLI_OPERATOR_TOKEN | no | — | If set, used verbatim as the bearer token on every request. Intended for CI. When unset, the server falls back to the session helper. |
| BLI_SESSION_HELPER | no | auto-discovered | Absolute path to scripts/bli-event-session.mjs. Only needed if the MCP binary lives outside the bli-cockpit repo layout. |
| BLI_API_BASE_URL | no | http://127.0.0.1:3100 | Cockpit dashboard base URL. |
Authentication (legacy event-stream tools only)
The server resolves an access_token before every authenticated request using the same precedence as the bli-event CLI:
BLI_OPERATOR_TOKEN(verbatim, no refresh) — CI path.- Session helper — shells out to
node scripts/bli-event-session.mjs get-token, which reads~/.config/bli-event/session.jsonand refreshes the access_token when near expiry. Runbli-event loginonce on your laptop to create the session. Tokens are cached in-process for 30s to avoid spawning a subprocess on every tool call.
If both paths are unavailable (no env override, and no session file on disk),
the server still starts and only those three tools refuse, by name, when
called — every device-token tool is unaffected (BLI-3804). It used to exit at
startup, which meant a machine that installed the published package registered
a bli-tower server with NO tools at all: scripts/bli-event-session.mjs
exists in the bli-cockpit repo and nowhere else. src/startup-auth.ts carries
the account.
Install
npm install # from repo root — registers the workspace
npm run build --workspace=@bli-cockpit/mcpThe bin entry exposes a bli-cockpit-mcp CLI that speaks MCP over stdio.
Claude Code (~/.claude/settings.json)
{
"mcpServers": {
"bli-cockpit": {
"command": "node",
"args": [
"/absolute/path/to/bli-cockpit/packages/bli-cockpit-mcp/dist/index.js"
],
"env": {
"BLI_API_BASE_URL": "http://127.0.0.1:3100"
}
}
}
}Run bli-event login once on the host; the MCP auto-resolves the
access_token from ~/.config/bli-event/session.json and refreshes it
transparently.
For CI (or any environment without a session file), set
BLI_OPERATOR_TOKEN to a Supabase access_token in the env block — it
will be used verbatim without refresh.
For deployed cockpits, set BLI_API_BASE_URL to the production URL.
Never commit tokens — keep them in local settings only.
Claude Desktop
Same shape, but the file lives at:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Example invocations
// emit a worker dispatch event
{
"name": "emit_event",
"arguments": {
"ticket_id": "BUI-412",
"event_type": "worker_dispatched",
"payload": { "role": "builder", "model": "sonnet" },
"correlation_id": "trace_abc123"
}
}
// idempotent note from a hook
{
"name": "emit_event",
"arguments": {
"ticket_id": "BUI-412",
"event_type": "note",
"note_md": "ship completed; preview URL https://...",
"idempotency_key": "ship-2026-04-17-1"
}
}Errors
The server surfaces API error codes from the canonical catalog
(docs/architecture/event-stream.md#errors) as MCP tool errors. Common ones:
| Code | Cause |
| --- | --- |
| BLI-E001 | Network / Supabase unreachable. |
| BLI-E002 | Unknown event_type. |
| BLI-E003 | Bad / expired JWT. |
| BLI-E004 | Payload schema validation failed. |
| BLI-E403 | Authenticated caller cannot access the requested ticket. |
| BLI-W003 | Idempotency replay (informational; not an error). |
The smoke — does this server actually answer? (BLI-3804)
The unit suites prove each tool's shape against an injected fetch, and
verb-census.test.ts proves a twin EXISTS for every Tower CLI verb. Neither
starts a server, opens a transport or reaches Tower, which is how five QA ticks
(docs/reports/jarvis-qa-loop/tick15.md … tick19.md) recorded these tools as
untested end to end while the published server was exiting at startup.
npm run mcp:smoke --workspace=@bli-cockpit/mcp # all six families
npm run mcp:smoke --workspace=@bli-cockpit/mcp -- --skip jarvis_ask # no model spend
npm run mcp:smoke --workspace=@bli-cockpit/mcp -- --only docs_list --jsonIt builds the server, spawns dist/index.js over stdio the way an agent host
does, lists the tools, and calls one cheap READ per family — docs_list
(limit 1), msg_channels, work_list_issues (limit 1), cal_calendars,
mail_accounts, jarvis_ask — never a write, never a dispatch. The device
token is never passed to it: the server reads
~/.config/bli-cockpit/session.json itself, so the smoke exercises the real
auth path. Output is one line per tool — name, verdict, the door's own reason
label, milliseconds:
bli-tower smoke — 78 tools registered, 6 called
tool verdict reason ms
docs_list ok answered 719
msg_channels ok answered 795
work_list_issues ok answered 760
cal_calendars ok answered 762
mail_accounts ok answered 478
jarvis_ask ok answered 6483A refusal by name is a pass. The question is whether the server answers,
not whether this account has a mailbox: cal_calendars and mail_accounts
answering "nothing is attached" is a healthy machine. The exit code turns
non-zero only when something never answered — the server would not start, the
transport broke, a call timed out, a tool was not registered, or Tower could
not be reached.
--server <path> points it at an INSTALLED server instead of this checkout's
build, which matters more than it sounds: the repo build resolves a credential
the published install cannot, so a green smoke here has never proven the
fleet's server starts.
npm run mcp:smoke --workspace=@bli-cockpit/mcp -- \
--server "$(npm root -g)/@bli-cockpit/cli/node_modules/@bli-cockpit/mcp/dist/index.js"On a laptop, cockpit doctor's mcp-answers row asks the same first question
of the REGISTERED server every run
(packages/cockpit-local-collector/src/commands/doctor-mcp.ts), so a machine
whose MCP is registered and not answering says so without anyone running this
script.
Development
npm run typecheck --workspace=@bli-cockpit/mcp # also typechecks scripts/
npm test --workspace=@bli-cockpit/mcp
npm run build --workspace=@bli-cockpit/mcp
npm run mcp:smoke --workspace=@bli-cockpit/mcp # needs a paired machine