Skip to content

Command · version 0.4.0

Test your own MCP server

Run the same checks we use against a server you run yourself: started on your machine, or at a URL you prove you control. The command runs on your machine. It prints what it finds and a plain “N of M checks passed” count. Results stay on your computer — nothing is sent to us, and there is no account.

Who it is for

Developers who run an MCP server: either one started by a command on their own machine (a stdio server), or, from version 0.2.0, their own Streamable HTTP server at a URL, after a one-time proof that they control that exact URL (below). Without the proof it does not test a URL. In one check on 50 random registry packages (Linux, Node 20), it produced a full result for 32; that is one sample on one kind of machine, not a measure of every server. We ran that check once, in our own cloud sandbox, to see whether the command works beyond our own test servers; we publish no result for any of those servers (more on the security page).

Run it

npx protocolprobe-selftest --i-own-this-server --stdio node ./your-server.js
npx protocolprobe-selftest --i-own-this-server --out report.json --stdio node ./your-server.js

Needs Node.js 18 or newer. Everything after --stdio is the command that starts your server. --i-own-this-server is required on every run.

Python servers

npx protocolprobe-selftest --i-own-this-server --stdio uv run --directory . server.py
npx protocolprobe-selftest --i-own-this-server --stdio uvx your-package

These need uv. Run the first from your project folder. Keep --directory .: the command starts your server in a temporary folder, and a plain uv run server.py then does not find your project. The second runs a package that declares a command with the same name as the package; if the command has another name, use uvx --from your-package your-command.

Test a server at a URL

  1. Run npx protocolprobe-selftest --i-own-this-server --url https://your.server/mcp. It connects to nothing and prints a line protocolprobe-proof=pp1-….
  2. Add that line, on a line of its own, to your server’s MCP instructions (the text it returns when a client connects; in the TypeScript SDK, the instructions option of new McpServer), and deploy.
  3. Run the same command again within 30 minutes. It reads the line on the same connection it then tests with. The value works once; remove the line afterwards.

Rules: the value works once and only for the exact URL (scheme, host, port, path and query); no redirect is followed; the host name is resolved once and every connection goes to that address; https for any host, http only for your own machine; no token or header is sent unless you supply one (below); Streamable HTTP only. Exit code 3 means “add the value and run again”, 4 means the proof was refused (the reason is printed).

Over HTTP fewer checks apply: checks 4 and 11 (secret and log leaks) need a server process this command starts, so they are N-A and never counted; checks 5 and 10 need your server on this machine, so for a server elsewhere they are N-A too. OAuth metadata is read only on your server’s own address, and check 16 never probes your authorization server.

The proof stops accidents and casual misuse; it is not enforcement. The command is open source and can be changed, and the proof shows that you control what the server at that URL answers, not the platform it runs on. Use it only for servers you operate or are authorised to test. On 2026-10-19 we ran this flow against our own test server on Vercel over https: the proof and the checks worked, the used-up value and another address each asked for a new value, and we then deleted that server.

A server that needs a credential (0.3.0)

If your server needs a token, give it your own from an environment variable: --bearer-env MY_TOKEN sends Authorization: Bearer with the value of MY_TOKEN, and --header-env X-Api-Key=MY_KEY sends another header (you can repeat it). There is no option that takes the value itself, because your shell history and the process list would keep it. The proof is still required: a token shows you use a service, not that you run it.

The credential is sent only to the exact URL you proved, with every request of the test session. It is not sent on a redirect (none is followed), not to OAuth metadata (even on the same host), and not to one extra connection the command makes without it, to see whether your server also opens a session without a credential; the result states that as a fact. It is never printed, never written to the report and never stored: its exact value is replaced by [redacted] everywhere, even when your server repeats it back. A value in another form, such as base64, is not recognised.

If your server refuses it, nothing is tested and the command says “your server refused the credential you supplied” (exit code 4). With a credential, checks 14 (token passthrough) and 15 (token audience) are listed as not testable, with what is missing: an upstream service we can watch, and a second token for another audience. The command has neither and makes neither up. Check 16 is never run.

Through a proxy

A local HTTP server is best tested by starting it as a stdio server, if its framework allows. Through a proxy such as mcp-remote, the process this command starts is the proxy, not your server, so check 4 (leaked environment values) and check 11 (leaks in log messages) cannot work: from version 0.1.2 they show N-Aand are not counted. Version 0.1.1 wrongly showed check 4 as CLEAN, counted as passed: on 2026-10-18, with our own test server, which returns its whole environment, it reported the leak when run directly and “6 of 6 checks passed” through mcp-remote.

The command treats a run as going through a proxy when it names mcp-remote, supergateway,mcp-proxy or @pyroprompts/mcp-stdio-to-streamable-http-adapter, or carries a localhttp(s) address. A proxy not on that list, a wrapper script around one, or a server inside a container or virtual machine (for example docker run -i) is not detected; there, checks 4, 5, 10 and 11 can come out clean without meaning anything.

Time limits

  • --connect-timeout <seconds> — how long your server may take to start and answer. Default 180. This time includes a first download by npx or uvx, so for a big server, run its command once by itself first, or raise the limit.
  • --max-time <seconds> — the whole run stops after this long, your server is stopped and no result is written. Default 600. It must be longer than --connect-timeout.

If your server does not start, the command prints the last lines of your server’s error output, with values that look like secrets hidden, and a hint for a few causes we have reproduced or seen, such as a Python server written for version 1 of the mcp package running with version 2.

What it checks

It connects to your server as a normal MCP client, lists its tools, resources and prompts, calls tools with ordinary inputs, and reports one line per check (the same engine behind this site). Among the checks: tool descriptions that read like injected instructions, a tool that fetches a URL it is handed, whether a tool that changes data completes without asking the client to confirm, and whether a path tool can read outside its root. Checks that do not apply to a local server (the OAuth ones) are marked N-A.

The two off-by-default checks

Two checks behave like a real test, so they are off unless you turn them on:

  • --allow-mutating — also calls tools whose name or annotations suggest they change data. They may really change data, so run it against a test instance, not production.
  • --allow-fuzzing — also sends wrong-type, oversized and missing input to tools.

Without these flags, those two checks are reported NOT-TESTED, never as a pass.

The score

It prints N of M checks passed over the checks that actually ran. A check that did not run is listed under the score and never counted, and the score is marked partial when something did not run. There is no letter grade and no single safety number: a check that did not run says nothing about that behaviour, so this is not a certificate of safety.

In CI (0.4.0)

npx protocolprobe-selftest --i-own-this-server --fail-on-finding --sarif selftest.sarif --stdio node ./dist/server.js
  • --fail-on-finding — exit code 5 when a check needs attention (VULNERABLE, or FOUND, a match to review), so the CI job fails. Without it the exit code is 0 whatever the result, as before; the other exit codes keep their meaning.
  • --sarif <file> — also writes the result as a SARIF 2.1.0 file, the format GitHub code scanning reads. Only checks that need attention become entries; the score and what did not run are in the file too. It is written with the same hiding of secret-looking values as --out, and without your command line.
  • A finding is about the running server, not a line of code, so each entry points at line 1 of one file: --sarif-anchor <file>, or by default the first file in your command. Run it from the repository root. Uploading the file, for example with GitHub’s upload-sarif action, is your own step; give that step if: always(), because a step that exits 5 stops the ones after it.

We checked it on 2026-10-19 with the published 0.4.0 against our own test servers: exit code 5 and four entries for a deliberately leaky one, exit code 0 for a plain one, and the file valid against the SARIF 2.1.0 schema. We have not uploaded one to GitHub.

What it will not do

  • Test a URL without the proof, follow a redirect, or send a token or header you did not supply. After --stdio, a command that carries a non-local address is refused.
  • Send anything anywhere. The report is printed and, with --out or --sarif, written to a file on your machine.
  • Hand your environment variables to the server. It starts the server with a minimal environment plus one planted canary value, which is how it checks for secret leaks.

Limit

The address check reads http(s) addresses in the command line only, so a wrapper script that reaches a different server is not detected. Use it only on servers you own or have permission to test. The package contains its full, readable source and a free (ISC) licence.