GitHub Packages vs npm โ€” should you publish to both?

We just shipped meridian-skills-mcp v2.0.0 to both the public npm registry (as meridian-skills-mcp) and GitHub Packages (as @luuow/meridian-skills-mcp) from a single git tag v2.0.0 && git push origin v2.0.0. Whole thing is one workflow file, one pre-existing NPM_TOKEN secret, and the automatic GITHUB_TOKEN.

Worth noting because the conventional wisdom is "just publish to npm." That conventional wisdom is correct as far as it goes โ€” but GitHub Packages has earned its slot in our build, and the cost of dual-publishing is small enough that not doing it is the questionable choice now. Here's the case.

The publish workflow, in full

One file, two registries, three jobs:

# .github/workflows/publish.yml
name: Publish package
on:
  push:
    tags: ['v*']
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: npm }
      - run: npm ci
      - run: node --test tests/*.test.mjs

  npm:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci --omit=dev
      - run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  ghp:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://npm.pkg.github.com'
          scope: '@luuow'
      - run: npm ci --omit=dev
      - run: npm pkg set name=@luuow/meridian-skills-mcp
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Notice the npm pkg set name=@luuow/meridian-skills-mcp line in the GitHub Packages job. That's the only piece of friction. GitHub Packages requires scoped names, and the scope must match the GitHub user/org. So we keep the canonical package.json with the unscoped public name (meridian-skills-mcp โ€” what's already on npm) and just rewrite the name field for one job.

Where GitHub Packages actually wins

1. The auth surface is the same as your code's

GITHUB_TOKEN is the token your CI already has. The token your repo settings already manage. The token your branch protection already respects. Publishing to GitHub Packages reuses it. No npm account, no separate NPM_TOKEN rotation, no second dashboard.

For internal packages especially, this is huge. Your "who can publish" question collapses into "who can push to this repo's main." That's it. Same answer for source code, same answer for releases.

2. Provenance is automatic, not opt-in

Every package on GitHub Packages is linked to the exact commit SHA, exact workflow run, and exact runner that built it. The package page shows them. You click through to the build log from the version. The version is signed by the GitHub OIDC token of the runner.

npm gained provenance attestations in 2023, but they require explicit npm publish --provenance + a publishConfig in package.json + the package name to start with a scope. GitHub Packages just does it.

3. One registry for npm, Docker, Maven, NuGet, RubyGems

ghcr.io/luuow/meridian-mcp (Docker image) and npm.pkg.github.com/@luuow/meridian-skills-mcp (npm package) are the same account, same auth, same UI. If you ship a CLI as both an npm package and a Docker image โ€” increasingly common โ€” having them under one roof means one auth dance, not two.

4. Private packages are free

GitHub Packages includes private packages on the free tier (within standard storage/bandwidth quotas). The npm equivalent โ€” npm Pro โ€” is $7/seat/month. For an internal-only package shared across a few repos, "publish to GitHub Packages, period" is the right answer. The minute the package is internal, npm has no advantage.

Where npm still wins

1. Anonymous install

This is the big one and it's not close. npm install meridian-skills-mcp Just Works for anyone, anywhere, with zero auth. npm install @luuow/meridian-skills-mcp from GitHub Packages requires every consumer to have a .npmrc with @luuow:registry=https://npm.pkg.github.com AND an auth token โ€” even for public packages. That's a long-standing GitHub Packages design choice and it's the single biggest reason it hasn't displaced npm for public distribution.

Practically: if anyone you don't know is going to install your package, keep publishing to npm. Don't make a stranger configure registry settings to try your tool.

2. Default-tooling integration

Every package manager front-end, every CI cache, every "search npm" surface, every npx invocation, every codesandbox dependency resolver โ€” they all default to registry.npmjs.org. If you only publish to GitHub Packages you're invisible to all of that.

3. Discoverability

npmjs.com's search is mediocre but it exists. GitHub Packages doesn't have a global search; you find a package by knowing the @owner in advance. For an open-source library you want people to find, npm is the only choice.

So: publish to both

The honest conclusion isn't "switch to GitHub Packages." It's "the marginal cost of also publishing to GitHub Packages is one npm pkg set name line and one extra job in your existing release workflow."

npm publicGitHub Packages
Anonymous installโœ…โŒ (auth required even for public)
Auth shared with codeโŒโœ…
Provenance (linked to SHA + run)opt-in (--provenance)automatic
Private packages on free tierโŒ ($7/seat/mo)โœ…
Same registry as Docker imagesโŒโœ… (ghcr.io)
Naming: scope requiredoptionalrequired, must match owner
Default for npm installโœ…โŒ

For a tool you want strangers to npm install: npm is required. GitHub Packages is a great second location โ€” it gives you provenance, cleaner auth for CI consumers inside your org, and a path to private without paying.

The npm side is also doing a thing now

Worth flagging: npm now has provenance statements via Sigstore, npm Trusted Publishing (OIDC-based, no long-lived tokens), and tightened scope-by-default defaults. The gap to GitHub Packages on attestation is closing. The gap on "public install requires no auth" is not โ€” and probably never will, because that's GitHub's billing posture more than a technical decision.

So the publishing playbook for 2026, in our opinion:

  1. Public open-source npm package? Publish to both. Use npm Trusted Publishing for the npm half (no NPM_TOKEN secret to rotate). The dual-publish workflow above is your starting point.
  2. Private internal package? GitHub Packages only. No reason to maintain an npm Pro subscription anymore.
  3. Hybrid (CLI + Docker)? Both registries for the npm half, ghcr.io for the Docker half. Single auth surface for consumers who use both.

One last note: the rename trick

The reason we keep meridian-skills-mcp (unscoped) on npm and @luuow/meridian-skills-mcp on GitHub Packages โ€” instead of forcing the scoped name everywhere โ€” is install ergonomics. Most users will install via:

npm install -g meridian-skills-mcp
claude mcp add meridian meridian-mcp

Renaming to @luuow/meridian-skills-mcp would break that one-liner for everyone who isn't already configured for GitHub Packages. The npm pkg set name=... rewrite at publish time avoids that โ€” both registries get the build artefact you want, with the name each registry's consumers expect.

It's three lines of YAML for an entire second registry. Hard to argue with that.

Want to ship your own MCP server end-to-end?

The full guide ships every line of code that powers Meridian's routing pipeline โ€” including the dual-registry release workflow above and the NPM_TOKEN / GITHUB_TOKEN setup that goes with it.

Build Your Own MCP Server โ€” $29 โ†’

Both v2.0.0 packages went live on 2026-05-06 from this workflow run. Public on npmjs.com and GitHub Packages.