โ† All posts
ENGINEERING September 7, 2026 ยท 4 min read

The install instruction that only worked for us

SR
Sentrinel Team
Product & engineering

The MCP server shipped a week ago with setup instructions that looked like this:

claude mcp add sentrinel \
  --env SENTRINEL_API_URL=https://api.sentrinel.dev \
  --env SENTRINEL_API_KEY=snt_mcp_โ€ฆ \
  -- bun run /path/to/elysia-monitoring/packages/mcp/src/server.ts

Read that path. It is a checkout of a repository nobody outside the company has. The instruction was written by someone who already had one, tested by someone who already had one, and works for exactly that population.

Fixing it turned out to be less about packaging than about where the key lives.

Not npm, not a clone

The obvious move is to publish to npm and write bunx @sentrinel/mcp. Our packages are consumed as git dependencies from a public mirror rather than from npm, and the MCP server has no reason to be the exception that starts a publishing pipeline. The next move is git clone && bun install, which works and which nobody does โ€” it is three commands, one of which fails differently on every machine depending on what package manager is installed.

We already had the answer in the repo. The Postgres collector installs with:

curl -fsSL https://sentrinel.dev/install-collector.sh | sudo bash

That works because the collector is not distributed as a package at all. It is built into a single file at site-build time, published next to the marketing site, and downloaded by a shell script. There is no dependency resolution on the user's machine because there are no dependencies left by the time it gets there.

The MCP server is now built the same way โ€” bun build --target=bun --minify, two bundles, server and CLI, 712KB together โ€” and published alongside the site. The installer needs no sudo, because unlike the collector this is a developer tool on a laptop, not a service on a database host: everything lands in ~/.sentrinel and ~/.local/bin.

Where the key goes

The first version of the installer registered the server the obvious way:

claude mcp add sentrinel --env SENTRINEL_API_KEY="$API_KEY" -- sentrinel-mcp

Our own documentation says not to do that. The security section of the guide is explicit that the key goes in the environment and never on a command line, because ps shows argv to every process on the machine โ€” it is why neither the server nor the CLI accepts a --key flag. Handing the key to claude mcp add as an argument puts it in exactly the place we tell people to keep it out of, and then writes it into a JSON config file as well.

So the key does not go through the agent at all. The installer writes it to ~/.sentrinel/env at mode 0600, and the commands it puts on your PATH are launchers that read that file before exec'ing the bundle:

#!/usr/bin/env bash
if [ -f "$HOME/.sentrinel/env" ]; then
  while IFS='=' read -r k v; do
    case "$k" in ''|'#'*) continue ;; esac
    [ -n "${!k:-}" ] || export "$k=$v"
  done < "$HOME/.sentrinel/env"
fi
exec bun run "$HOME/.sentrinel/sentrinel-mcp.js" "$@"

Two consequences fall out of that, and both are worth more than the tidiness.

The agent's config holds no secret. claude mcp add sentrinel -- sentrinel-mcp is the whole registration. You can commit an mcp.json containing it. Rotating the key is one file, not one file per agent you have configured.

Already-set variables win. The launcher only exports what is missing, so SENTRINEL_API_KEY=โ€ฆ sentrinel issues still runs a one-off against another app without touching the file. That is the behaviour you want from a config file and almost never get from a script that sources one.

One line, including the part everyone forgets

The collector's installer takes its configuration inline and starts the service immediately, so installing and configuring are one step. The agent installer now does the same, and goes one further: if the claude CLI is present, it registers the server itself.

curl -fsSL https://sentrinel.dev/install-mcp.sh | SENTRINEL_API_KEY=snt_mcp_โ€ฆ bash

Download, install Bun if missing, write the key, register with Claude Code. The output ends with the sentence to type in a session, because at that point there is nothing else to do.

A run with no key installs the binaries and writes nothing โ€” an upgrade must never quietly unconfigure a working install โ€” and prints the line to run when you have one. Re-running with a key rewrites only the key.

The failure we designed for

An installer that downloads bundles has one interesting failure: a CDN that serves the marketing site's 404 page for a missing file. curl -fsSL follows that happily, and you end up with an HTML document named sentrinel-mcp.js that an agent will dutifully try to execute, producing a syntax error from a file the user never wrote. The installer checks the first 200 bytes for <!doctype or <html> and refuses, which the collector's installer taught us to do.

The other one has no code fix, only a note: a GUI app started from the Dock does not read your shell profile, so ~/.local/bin may not be on its PATH and a bare sentrinel-mcp will not resolve. Claude Code in a terminal is fine. Claude Desktop and Cursor may need the absolute path, and the troubleshooting table says so.

The rule this came from

The instruction was not wrong when it was written. It was written from inside the repository, and everything it named was true from there.

The check is cheap: run your own install instructions in an environment that has none of your context โ€” a temp HOME, an empty PATH, no checkout โ€” and see what happens. We did that for this one, against the deployed URL, before publishing it. Setup, the security model, and a worked example from empty machine to merged fix are in the guide.