One Client, Four Registries: PyPI, npm, Packagist and MCP All Disagree
Why OIDC cannot do npm's first publish, why Packagist needs no workflow, and the PyPI name check that returns 200 for names nobody owns.
Founder, ibanchecker.cash
We ship one small API client in several languages. The code is nearly identical each time: five methods, typed models, no runtime dependencies. Publishing it, though, was different in every ecosystem, and not in the ways the documentation prepares you for. PyPI, npm, Packagist and the MCP Registry each have a different idea of what it means to prove a release came from you, and two of them have a trap that will cost you an afternoon.
This is what actually happened, with the version numbers and error strings, so you can skip the parts that cost us time.
Why does PyPI make this easiest?
Because PyPI lets you describe the publisher before the package exists. You register a pending publisher against a repository, a workflow filename and an environment, and the first release from that workflow creates the project. No token is ever generated, stored or rotated.
name: publish
on:
release:
types: [published]
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # Trusted Publishing (OIDC), no token stored
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- run: python -m pip install --upgrade build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1Create the GitHub environment named in environment: before you cut the release, or the job fails on a missing environment rather than on anything to do with publishing.
Why can npm not do the same thing?
npm supports OIDC, but it has no equivalent of a pending publisher, so OIDC cannot perform a package's first publish. Version 0.1.0 has to go out with a manual npm publish, and only then can the trust configuration attach to something that exists. This is a CLI constraint, tracked as npm/cli issue #8544, and the published documentation does not mention it.
Configuring trust afterwards has its own edge. On npm 11.11.0 the command returns a bare 400 Bad Request with no error body:
npm trust github --file publish.yml --repository owner/repo --environment npm
# 400 Bad RequestA registry rule introduced on 20 May 2026 requires the trust configuration to name an allowed action, and that npm version predates the flag that expresses it. Newer versions say so in plain words. If you cannot upgrade globally, run the newer CLI directly:
npx -y [email protected] trust github \
--file publish.yml --repository owner/repo \
--environment npm --allow-publishOne more practical detail: the npm CLI redacts two-factor approval URLs when it does not think it is attached to a terminal, so in CI-style output you get a censored link you cannot open. Allocate a pty and the real URL appears:
printf '\n' | script -q /dev/null npm publishThose links expire within minutes, so only run commands that trigger them when you are actually at the keyboard.
Why does Packagist need no workflow at all?
Because it does not publish anything. You submit the repository URL once, and from then on every tag matching v* becomes a version. There is no OIDC, no token, no publish.yml.
The consequence is a rule that catches people coming from npm: your composer.json must not contain a version field. The tag is the single source of truth, and a hardcoded version will fight it. Ours has none:
{
"name": "ibanchecker/client",
"require": { "php": "^8.0", "ext-curl": "*", "ext-json": "*" }
}We expected to have to install a webhook manually because the account was registered by email rather than by connecting GitHub. Packagist installed it itself, and the package page reads "auto-updated" without anyone touching it.
What is different about the MCP Registry?
The MCP Registry is young enough that its manifest schema still moves. A server.json that validated last month can be rejected this month for a field that was renamed. Validate before you publish rather than after:
mcp-publisher validateThe payoff is that registry listings propagate. A single MCP Registry entry is picked up by the directories that index it, which is how a small server ends up discoverable in several places from one publish.
The trap that wastes the most time: checking whether a name is free
Before any of this you need a package name, and the obvious way to check PyPI is to open the project page. Do not. That URL returns 200 OK for names that do not exist, because an unauthenticated request can be served a bot challenge with a success status. You will conclude the name is taken and pick a worse one.
Compare a name we do own with one nobody does:
# a name that exists
curl -s -o /dev/null -w '%{http_code}\n' https://pypi.org/project/ibanchecker/ # 200
curl -s -o /dev/null -w '%{http_code}\n' https://pypi.org/pypi/ibanchecker/json # 200
# a name that does not exist
curl -s -o /dev/null -w '%{http_code}\n' https://pypi.org/project/zzz-not-taken-xyz/ # 200 <- lies
curl -s -o /dev/null -w '%{http_code}\n' https://pypi.org/pypi/zzz-not-taken-xyz/json # 404 <- truthUse the JSON endpoint, or /simple/<name>/. Both answer honestly.
How do the four compare?
| Registry | First publish | Later releases | Secret stored |
|---|---|---|---|
| PyPI | OIDC, via a pending publisher | OIDC | None |
| npm | Manual, OIDC cannot | OIDC once trust is set | None after setup |
| Packagist | Submit the repo URL | Any v* tag | None |
| MCP Registry | mcp-publisher | Same | None |
The through-line is that none of the four needs a long-lived token any more, and every one of them gets there by a different route. If you are shipping to more than one, budget the time for npm and leave PyPI until last, because it is the one that will just work.
What we published
The worked example is the client for our own IBAN validation API, if you want to read a small, boring, dependency-free implementation of the same thing four times: ibanchecker on PyPI, ibanchecker/client on Packagist, @ibanchecker/client on npm, and @ibanchecker/mcp for the MCP server. The API they call is documented at ibanchecker.cash/api-docs, and the MCP setup is on our integrations page.
Sources & References
- PyPI Trusted Publishers: official documentation, including pending publishers
- npm Trusted Publishing: official documentation
- npm/cli issue 8544: OIDC and the first publish of a package
- Packagist: how submission and tag-based versioning work
- MCP Registry: the publisher tool and the server.json schema
Last updated: September 2026
Integrate IBAN validation via API
REST API with 1,000 free validations/month. JSON response with bank name, SEPA status, and BBAN breakdown.
Introducing Northern Cyprus UBAN Support: Validator and Bank Directory
A validator for the 28-character Northern Cyprus UBAN, plus a directory of all 23 registered banks. What UBAN is, and wh...
npm trust github Fails with 400 Bad Request: The Missing Permission Flag
A bare 400 with no error body, caused by a 20 May 2026 registry rule. The exact npm version boundary, the one flag that ...
How to Validate IBANs in JavaScript (API & Manual)
Two approaches: call the ibanchecker.cash API for full bank data, or implement MOD-97 validation yourself. Includes work...
IBAN Validation in Python: Complete Implementation with Regex and Check Digits
Two production-ready approaches: call the API for bank metadata, or implement MOD-97 yourself. Includes working code and...