Developer ResourcesSeptember 11, 2026 · 8 min read

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.

Koray Köylü
Koray Köylü

Founder, ibanchecker.cash

pypinpmpackagistmcptrusted-publishingoidc
Share

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/v1

Create 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 Request

A 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-publish

One 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 publish

Those 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 validate

The 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  <- truth

Use the JSON endpoint, or /simple/<name>/. Both answer honestly.

How do the four compare?

RegistryFirst publishLater releasesSecret stored
PyPIOIDC, via a pending publisherOIDCNone
npmManual, OIDC cannotOIDC once trust is setNone after setup
PackagistSubmit the repo URLAny v* tagNone
MCP Registrymcp-publisherSameNone

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

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.