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. [source] Each entry in server.json points at a package somewhere else, and the registry docs list npm, PyPI, NuGet, Docker/OCI, MCPB and Cargo. [source]
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. [source]
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. [source]
1. Bundle the server
An MCP Bundle is a zip archive holding the server and a manifest.json. [source] 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"
}
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
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
"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" }
}
]
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
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
(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
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"
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).
Want the full gate?
The free MCP server has two of the Publish Gate's checks (numbers, links). The paid Publish Gate also checks front matter, banned phrases, emoji, exclamation-mark count, em-dash density and rhetorical openers, and its publishers refuse a draft whose body changed after it passed. $19 one-time.
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.