X190
Does this MCP endpoint enforce authentication?
That is the whole tool. It sends initialize, replays tools/list with no
token, and records what came back. If the server hands its tool list to an
unauthenticated caller, that is the finding. If it refuses, the refusal is
checked for an RFC 9728 challenge a client can actually follow.
Single file, Python 3 stdlib only, zero dependencies.
Why only that
We probed the 74 HTTP endpoints among the first 100 servers in the official MCP registry on 2026-09-10. Every fault observed was auth absence (F1) or a broken OAuth resource-server posture (F2). Nothing else was observed, so nothing else is checked.
That run's headline rate stays suspended — the probe of the day treated HTTP 200 as proof the tool list was served, and JSON-RPC routinely refuses inside a 200. Its receipts recorded no bodies, so it cannot be recovered.
It has been re-measured instead. On 2026-09-11, 100 endpoints drawn from the registry at most one per publisher domain, probed once each with v0.9.1:
94 conclusive, 6 inconclusive
45 served the tool list with no token 45.0%
40 refused with RFC 9728 metadata that resolved
9 refused with a broken, absent or unfetchable challenge
45% of the sample handed its tool list to an unauthenticated caller. That is a new measurement, not a recovery of the old one, and the sampling limits are real — one endpoint per publisher deliberately stops one operator with 763 listed servers from setting the rate, but it over-weights small publishers as a result. Whether a given open endpoint is a fault or an intentionally public service is not a judgement this tool makes. See EVIDENCE.md.
F1 auth-absence tools/list returned a JSON-RPC result with no token
F2 incomplete-oauth-rs refused, but with no RFC 9728 challenge a client could
follow, or with metadata that does not resolve
Install
uvx x190 check https://host/mcp
That runs it without installing anything. To keep it around:
pip install x190
Either way you get an X190 command. No dependencies, so it works in any
Python 3.9+ environment. If you would rather not use a package manager at all,
the tool is one file: download X190.py from the latest
release and run it with
python3 X190.py.
Usage
X190 check https://host/mcp # probe -> signed receipt
X190 check http://127.0.0.1:3000/mcp
X190 verify-receipt x.receipt.json
Exit codes: 0 = pass, or inconclusive only; 1 = at least one fault;
2 = the target is not an http(s) URL.
Servers distributed for stdio
A package that declares stdio transport can still ship an HTTP mode. In our measurement of the registry's local-transport population, 3 of 23 npm packages shipped a network-listening MCP surface, and all three could serve MCP without authentication in a documented configuration — while the registry listed no endpoint to probe.
If you ship or run such a server, start its HTTP mode and probe that URL like any other. The fault is the same fault; only the discovery is different.
Receipts
A receipt records the target, the checks, the raw probe evidence, and a timestamp — including what could not be determined.
hmac_sha256 signs the body with $X190_KEY, falling back to a
per-machine key at ~/.X190-key (created 0600).
What a receipt proves, exactly: that someone holding the key produced this byte-for-byte content. Nothing else. The scheme is symmetric, so a party who can verify a receipt can also mint one — which means a receipt is tamper evidence for your own archive, not an attestation you can hand to a third party as proof. Two unrelated organisations cannot use it to distinguish an authentic result from a fabricated one, because doing so would require sharing the secret that lets either of them fabricate. If you need transferable proof, this design cannot give it to you; that would need public-key signatures and a key you publish.
Receipts signed with the published demo key are marked "demo_key": true and
the verifier warns about them, because that key is in this README and anyone
can sign anything with it. Set X190_KEY to a real secret anywhere you
intend to verify receipts later, including CI.
The receipts under demo/ were produced against loopback servers in this
repository's own tests and are signed with a published constant, so anyone can
verify them:
X190_KEY=X190-demo-key-not-a-secret python3 X190.py verify-receipt demo/open-server.receipt.json
X190_KEY=X190-demo-key-not-a-secret python3 X190.py verify-receipt demo/tampered.receipt.json # -> false, exit 1
That key is a demo constant, not a secret.
GitHub Action
On the GitHub Marketplace.
- uses: unempyd/X190@v0.9.2
with:
target: https://your-host/mcp
gate-key: ${{ secrets.X190_KEY }}
The step fails the build when a fault is found. Read
steps.<id>.outputs.failures to gate on the count yourself instead.
After a finding
On an interactive terminal, a run that finds something prints one short note to
stderr saying what the probe does not claim. stdout stays pure JSON, so
piping it into a parser is unaffected, and nothing is printed in CI or when the
output is redirected. --quiet turns it off entirely.
Tests
python3 -m unittest discover -s tests
Stdlib only, no outbound network: the tests stand up loopback servers.
What a finding asserts
AUTH-OPEN is an observation, not a verdict: at this timestamp, from this
network position, the endpoint returned a JSON-RPC result for tools/list
sent with no credentials, and the receipt says how many tools came back.
It does not assert that this is a mistake. A deliberately public MCP server is a legitimate design. The receipt is evidence of what the endpoint did; whether that is a fault is the operator's call.
What this is NOT
Not a gateway, agent runtime, identity provider, or hosted platform. Not a source-code scanner — earlier versions shipped one, and the measurement above is why it was removed: every fault in the population was observable at the endpoint, and none of the affected servers published source to scan. Not a replacement for the official conformance suite.
Support
Something X190 got wrong, or missed: open an issue. Anything that would let X190 mislead a reader or reach somewhere it should not: security advisory, private until published. See SECURITY.md and CONTRIBUTING.md.
The tool is MIT licensed, which already permits commercial use. There is nothing to buy.
Methodology honesty
Probes are single-shot and chained (initialize, then a tokenless
tools/list). They establish posture at one timestamp from one network
position — not full OAuth conformance.
- A
passmeans the endpoint refused an unauthenticated tool call then, from here, and published resolvable metadata. It is not an audit, and it says nothing about token validation, scopes, or authorisation once a token is actually presented. - Bot walls, dead endpoints and unexpected protocol shapes report
inconclusive. The tool never claims a fault it did not observe. A bot wall must look like one: a 403 carrying a JSON body no longer counts, so an MCP server cannot buy an inconclusive by putting "access denied" in its JSON. - The handshake is completed before anything is asked for —
initialize, thennotifications/initialized, thentools/listcarrying the protocol version the server negotiated. A spec-strict server that would otherwise reject the request is measured rather than filed as inconclusive. - The answer is read the way a client reads it, not the way the status line
suggests. A tool list served over an event stream is parsed with real SSE
framing — consecutive
data:fields joined with a newline, as the spec requires — a JSON-RPCresultis proof under any 2xx rather than only a literal 200, and a body that arrives compressed is decompressed (bounded by the same read cap, because a small compressed body can expand without limit). All three were false negatives: endpoints the official MCP SDK lists tools from were reportedinconclusive. What counts as proof is unchanged — a JSON-RPCresult— so this cannot accuse a server that refused. - Where the two differ, this reads more leniently than a strict client. A result delivered in a shape the reference client rejects — an unterminated event stream, a single-element batch, a status it will not read a body from — is still reported, because the tool list did leave the server to an unauthenticated caller and a less fussy client would take it. The finding records the status and encoding actually observed, so the reader can judge.
- Every request this tool makes — the probe itself and the metadata fetch — goes through one guarded opener that re-validates each redirect hop. Neither will follow a redirect to a private or link-local address, so a scanned server cannot use this tool to reach into the network of whoever runs it. This does not survive DNS rebinding between the check and the connection.
- The probe additionally refuses to leave the origin you named. A finding is a
claim about a specific endpoint, so a redirect to a different host or port
reports
REDIRECT-OFF-TARGET/ inconclusive rather than quietly measuring something else and filing it under your target. Same-origin redirects are followed and recorded. - Receipts are HMAC-signed, not PKI. Anyone holding the key can mint one; they are tamper-evidence for an archive, not third-party attestation.
- A receipt has no freshness. It carries a timestamp and nothing binds it to
now, so a genuine passing receipt can be presented long after the posture
changed.
verify-receiptreportsage_seconds; decide your own staleness policy. Nothing here proves an endpoint is currently closed. - An endpoint can single out this prober. The probe sends a
X190/...User-Agent from one IP; a server that returns a clean 401 to it and its tool list to everyone else passes. This is reproduced in our own testing and is inherent to remote black-box probing — a receipt records what the endpoint returned to us, then, not what it returns to everyone. - An endpoint can pass CI by redirecting away. A 302 to a different host or
port reports
REDIRECT-OFF-TARGET/ inconclusive, which does not fail the gate. That verdict is deliberate — a finding cannot be attributed to an origin the caller did not name, and legitimate deployments redirecthost/mcptomcp.host/mcp, so failing here would manufacture false positives. The refusal and the URL are recorded in the receipt; re-run against the destination if you meant to probe it. - An endpoint can force an inconclusive result. A response padded past the
5 MB read cap is reported
RESPONSE-TRUNCATED/ inconclusive, and inconclusive findings do not fail the gate. The tool will not claim a fault it could not observe, so a server that refuses to be readable is recorded as unread rather than as safe. Read the findings, not just the exit code. - Probing sends unauthenticated requests to whatever URL you pass. Only probe endpoints you are authorised to probe.
Hands-on work
The tool is free and the MIT licence already grants every right you need; there is no second licence to buy. What is not free is the judgement the probe refuses to make — whether a token is actually validated, whether scopes are enforced, what to change to make a broken RFC 9728 challenge followable — and continuity, because a receipt has no freshness and posture decays. Both are priced at unempyd.github.io/X190-site.
A receipt is HMAC, therefore symmetric. It is tamper evidence for your own archive and is not sold, or sellable, as third-party attestation.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file x190-0.9.2.tar.gz.
File metadata
- Download URL: x190-0.9.2.tar.gz
- Upload date:
- Size: 31.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77a34ca42cb9f70e42f65b3a9ab474dd296787d6ae8b6128d73d3df96c0464c3
|
|
| MD5 |
02f9190c88ba57b338517aa8052aeb95
|
|
| BLAKE2b-256 |
c43be57aa796f67960d6bc55e1d6b6d44e58421d54e83b06bdc2450937c5862b
|
File details
Details for the file x190-0.9.2-py3-none-any.whl.
File metadata
- Download URL: x190-0.9.2-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92e33c51e721bdc4095f816b5d751017363e06ff124e820a474fdd8c89c4b168
|
|
| MD5 |
d0f2c4dbd6100b522de69041cd803670
|
|
| BLAKE2b-256 |
2986bbfe912c3580d6b696c12b56f857136c137f335c61a2f8c1cfae313c6366
|