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.