DEV Community

Todd Doane
Todd Doane

Posted on Originally published at calyx-ai.net

Listing an MCP server in the official registry without npm: an MCPB file on a GitHub release

Our small MCP server was ready to list in the official MCP registry, except for one thing: its server.json pointed at an npm package, and the repo had no npm token, so the publish step could not run. The registry accepts other package types. One of them, MCPB, needs nothing but a GitHub release. Today the server is listed that way, and npm can be added later.

What the registry accepts

The registry stores metadata, not code. [src: https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/about.mdx] Each entry in server.json points at a package somewhere else, and the registry docs list npm, PyPI, NuGet, Docker/OCI, MCPB and Cargo. [src: https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/package-types.mdx]

For MCPB the rules are short: the file must be on a GitHub or GitLab release, its URL must contain the string mcp, and server.json must carry the file's SHA-256 as fileSha256. [src: https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/package-types.mdx]

With GitHub login, the server name must be io.github.<user>/<name>, and inside GitHub Actions that login is OIDC, so the registry login needs no stored secret. [src: https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/authentication.mdx]

1. Bundle the server

An MCP Bundle is a zip archive holding the server and a manifest.json. [src: https://github.com/anthropics/mcpb/blob/main/README.md] Ours, trimmed to the parts that make it run:

{
  "manifest_version": "0.2",
  "name": "publish-gate",
  "version": "0.1.0",
  "description": "Check a draft's numbers against your facts.md and its links before you publish.",
  "author": { "name": "Calyx AI (Quality Tester Solutions LLC)" },
  "server": {
    "type": "node",
    "entry_point": "server.js",
    "mcp_config": { "command": "node", "args": ["${__dirname}/server.js"], "env": {} }
  },
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

The server has no dependencies, so there is no node_modules to ship. A .mcpbignore keeps the tests and server.json out:

$ npx -y @anthropic-ai/mcpb@2.1.2 validate manifest.json
Manifest schema validation passes!
$ npx -y @anthropic-ai/mcpb@2.1.2 pack . publish-gate.mcpb
Enter fullscreen mode Exit fullscreen mode

The bundle came out with four files: server.js, README.md, package.json and manifest.json. Before trusting it, we unpacked it, ran the server's 6 tests against the unpacked copy, and sent it an initialize and a check_numbers call over stdio. Both answered.

2. Hash the file you upload, not a rebuild

This is the part that bites. We packed the same four files twice and got two different SHA-256 values, and both differed from the file we had already uploaded. The file contents matched (same CRC for every entry); the entry timestamps did not, so a rebuild is a new file.

So the hash goes into server.json from the exact file that goes on the release:

$ sha256sum publish-gate.mcpb
1b032ae3ce70dc974726da444fe0e448ae8bdfa39a92d53bd1001c9f51ff4aca  publish-gate.mcpb
Enter fullscreen mode Exit fullscreen mode
"packages": [
  {
    "registryType": "mcpb",
    "identifier": "https://github.com/skibuff22/agent-ops-kit-lite/releases/download/mcp-v0.1.0/publish-gate.mcpb",
    "fileSha256": "1b032ae3ce70dc974726da444fe0e448ae8bdfa39a92d53bd1001c9f51ff4aca",
    "transport": { "type": "stdio" }
  }
]
Enter fullscreen mode Exit fullscreen mode

mcp-publisher validate checks the file against the live registry before anything is published (its success line starts with a check-mark emoji, left out here):

$ mcp-publisher validate
Validating against https://registry.modelcontextprotocol.io...
server.json is valid
Enter fullscreen mode Exit fullscreen mode

3. Release, then publish from Actions

The workflow runs on a mcp-v* tag. It does not rebuild the bundle. It downloads the release asset and refuses to publish if the asset's hash differs from server.json:

permissions:
  id-token: write
  contents: read
steps:
  - uses: actions/checkout@v4
  - run: npm test
  - name: Release asset matches server.json
    run: |
      gh release download "$TAG" -R "$GITHUB_REPOSITORY" -p publish-gate.mcpb -D /tmp/rel
      want=$(node -e 'const s=require("./server.json");const p=s.packages.find(p=>p.registryType==="mcpb");console.log(p?p.fileSha256:"")')
      got=$(sha256sum /tmp/rel/publish-gate.mcpb | cut -d" " -f1)
      test -n "$want" && test "$want" = "$got"
  - run: curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher
  - run: ./mcp-publisher login github-oidc
  - run: ./mcp-publisher publish
Enter fullscreen mode Exit fullscreen mode

(Shortened. The full file also has a manual workflow_dispatch trigger, a setup-node step, the working directory, GH_TOKEN and TAG, and an npm step that skips itself when there is no NPM_TOKEN secret.) id-token: write is what the OIDC login needs; contents: read was enough to download a release asset from a public repo.

Then one command, after server.json with the hash was pushed:

$ gh release create mcp-v0.1.0 publish-gate.mcpb --target master --notes-file notes.md
Enter fullscreen mode Exit fullscreen mode

That created the tag and the tag started the workflow. The run took 17 seconds: tests and hash check passed, npm skipped, and the last step printed Successfully published.

4. Check the registry, not the log

$ curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.skibuff22/publish-gate"
Enter fullscreen mode Exit fullscreen mode

The reply holds the server.json we sent plus the registry's own _meta block: "status": "active", "isLatest": true.

What this does not cover

We have not installed the .mcpb in a client that opens bundles; the install path we have tested is still the clone plus claude mcp add. And with npm missing, npx -y publish-gate-mcp does not work yet. When npm is set up, the plan is a new version whose server.json lists both packages.

The server

The server is publish-gate, two checks to run on a draft before it goes out: numbers that are not in your facts file, and dead links. It is free in the MIT-licensed agent-ops-kit-lite repo. The paid Publish Gate adds more checks and publishers that refuse a changed draft ($19, Quality Tester Solutions LLC).

Drafted with AI assistance. The local commands above were run on 2026-11-09 (Node v26.7.0, mcpb 2.1.2), the workflow ran in GitHub Actions the same day, and the registry entry was checked through its API.

Top comments (0)