npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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):

  1. The agent doors — the SAME routes the cockpit CLI and the browser call, authenticated with this machine's collector device token (~/.config/bli-cockpit/session.json, written by cockpit login). This is the live, actively-developed half: docs_*/msg_* (BLI-3706), work_* (BLI-3716) and jarvis_* (BLI-3732).
  2. emit_event/get_ticket_timeline/get_active_tickets — the legacy event-stream write path, authenticated with BLI_OPERATOR_TOKEN or the bli-event session 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  FAILED

degraded 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. subject changes WHOSE page is asked for, never who is asking. A delivery reason other than delivered (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 own scope/degradedBecause reason 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_read has a narrower audience than slack_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 as admin only, not as an error; settings_list returns 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_rewrite QUEUES 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_correct refuses 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: true resolves 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_revoke looks the device up and names it back to you first; without confirm: true that 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:

  1. BLI_OPERATOR_TOKEN (verbatim, no refresh) — CI path.
  2. Session helper — shells out to node scripts/bli-event-session.mjs get-token, which reads ~/.config/bli-event/session.json and refreshes the access_token when near expiry. Run bli-event login once 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/mcp

The 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.mdtick19.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 --json

It 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                      6483

A 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