Step 1: Audit a Python file
Start with a small server. Save it asserver.py:
server.py
.py file runs with mcpscore’s own Python, and a .js file runs with
node. mcpscore’s Python has mcpscore’s own dependencies installed, mcp
among them, and none of your project’s. Add one import from your own
project and the server cannot start:
server.py
2: mcpscore could not connect. Lines that start with
server stderr: are your server’s own output, relayed so you can see why it
stopped.
Step 2: Run it in its own environment
--stdio takes the command you already use to start your server. Put it
where the file name was:
uv run starts the server inside your project’s environment, so every
dependency in your pyproject.toml is there. The score is the same as in
Step 1, because the server is the same.
--stdio works for any language. It runs the command directly, with no
shell in between:
--stdio consumes the rest of the command line, including your server’s own
flags. Every mcpscore option goes before it.
Step 3: Pass the server its secrets
Most servers read an API key at startup. Add that toserver.py:
server.py
HOME, LOGNAME, PATH,
SHELL, TERM, and USER on macOS and Linux. Your shell’s secrets stay
out of a process you have not vouched for.
Hand the key over by name with --env:
--env NAME copies the value from mcpscore’s own environment, so it never
appears on the command line, in shell history, or in the report. Repeat the
flag for each variable. --env NAME=VALUE sets a value inline, which is
fine for non-secret settings like --env LOG_LEVEL=debug.
Step 4: Fail when the score drops
Add--fail-under with the lowest percentage you accept:
3. 105 of 126 rounds to 83%, below the 90% you asked
for. Lower the bar to where the server is today:
Technical details: how mcpscore launches your server
Technical details: how mcpscore launches your server
- A
.pytarget runs with the interpreter mcpscore itself runs on (sys.executable). A.jstarget runs withnodefromPATH. --stdioruns the first word as the executable and passes the rest as arguments. There is no shell, so quoting, pipes, and$VARSin the server command are not interpreted. A script name on its own is not an executable:--stdio server.pyfails with a hint to use--stdio python server.py.- The server gets the MCP Python SDK’s default environment (
HOME,LOGNAME,PATH,SHELL,TERM,USERon POSIX) plus each--env.--envvalues are never logged and never written to the report. - The command line, as typed after
--stdio, becomes the report’stargetfield and is visible in the process list. That is why secrets go in with--env NAMEand never as arguments. - Your server’s stderr is relayed line by line with a
server stderr:prefix. Its stdout carries the MCP protocol and is never printed. - mcpscore opens more than one connection during an audit, for example to check that the tool catalog is the same on a second connection, so your server starts more than once.
When it fails
More failures and their fixes are in Troubleshooting.
Recap
- Audit a Python or JavaScript file with
mcpscore server.py. - Run the server in its own environment with
--stdio uv run server.py, or any command in any language. - Pass secrets with
--env NAME, before--stdio. - Fail the run below a score with
--fail-under 80.
What’s next
- Improve your score: the findings most servers fail, and the fix for each.
- Smoke mode: call the server’s tools after the audit and check they behave.
- GitHub Action or other CI: run the Step 4 command on every pull request.