Large Language ModelsGenerate imagesGenerate videos

MCP Registry: GitHub, server.json and How to List Your Server

The MCP Registry is where clients and marketplaces find your server, and getting listed takes one server.json, one verified namespace, and one command. This article follows the GitHub login, package ownership checks, remote servers, and a tagged release workflow.

MCP Registry: GitHub, server.json and How to List Your Server
Cristian Da Conceicao
Founder of Picasso IA

A good MCP server that nobody can find might as well not exist. The official MCP Registry fixes that with one JSON file, one verified name, and one command, and GitHub shows up at three separate points along the way: the login, the namespace, and the release automation. This article follows the exact files and commands from the registry docs, so your server lands in the registry on the first attempt instead of the fifth.

💡 Quick answer: write a server.json, prove you own the package it points to, run mcp-publisher login github, then run mcp-publisher publish. Everything below explains why each step exists and what breaks when you skip it.

What the MCP Registry Actually Is

The MCP Registry is the official, centralized metadata repository for publicly accessible MCP servers, backed by Anthropic, GitHub, PulseMCP and Microsoft. It opened in preview in September 2025, the API has been frozen at v0.1 since late October 2025, and the docs still carry a preview banner, so expect small changes. The live service sits at registry.modelcontextprotocol.io.

Metadata, Not Code

Aerial view of a container port where stacked shipping containers stand in neat grids at golden hour

The registry never stores your code. It stores a record that points to a package on npm, PyPI, NuGet, crates.io, a container registry, or a GitHub release. Think of a container port: the manifest says what sits in each box and where it came from, while the cargo lives somewhere else. That is why the order matters. You publish the package first, and only then the registry entry.

Where GitHub Fits In

GitHub touches the process in three places:

  • Identity. Log in with GitHub and your server name must start with io.github.username/, or your organization's name in place of the username.
  • Metadata. server.json carries a repository object with "source": "github" and the repo URL.
  • Automation. GitHub Actions can authenticate to the registry through OIDC, with no stored secret.

There is also a separate storefront. GitHub runs its own MCP Registry at github.com/mcp, and GitHub announced that servers self-published to the open source community registry "will automatically appear" there. Treat that as a bonus, not a promise: after you publish, check the GitHub listing yourself.

Who Can List a Server

Open source and closed source servers are both welcome, with one condition: the server must be publicly reachable. That means a public package (an npm package, a Docker image on a public registry) or a remote endpoint that is not locked inside a private network. Servers on an internal host like mcp.acme-corp.internal, or behind a private package registry, are out of scope. For those, run your own private registry.

Also worth knowing: host apps are not meant to read the official registry directly. Marketplaces and aggregators pull from it on a regular schedule, for example once an hour, and add their own curation and ratings on top. Your listing travels through them.

Pick Your Namespace First

The name field in server.json is your server's permanent identity, and your login method decides which names you are allowed to use.

Login methodName formatExample
GitHubio.github.username/* or io.github.orgname/*io.github.alice/weather-server
Domain (DNS or HTTP)Reverse form of your domaincom.example/acme-analytics

GitHub Names for Quick Wins

Close-up of vintage brass mailboxes with small paper name labels in an old apartment lobby

Choose the GitHub route when you are an individual developer or an open source project. The CLI runs an OAuth device flow, you approve it in the browser, and you are done in a couple of minutes. No DNS panel, no files to host. The tradeoff is the name: io.github.alice/weather-server reads fine for a side project, but a company brand usually wants its own domain.

Domain Names With DNS or HTTP

Domain-based names use the reverse form of a domain you control, such as com.example/acme-analytics. You prove control one of two ways:

  1. DNS. Generate an Ed25519 (or ECDSA P-384) pair with openssl, then publish the public half as a TXT record in the form example.com. IN TXT "v=MCPv1; k=ed25519; p=<base64>". Allow several minutes for propagation.
  2. HTTP. Host the same v=MCPv1; ... line as a file at https://example.com/.well-known/mcp-registry-auth.

Then log in with mcp-publisher login dns --domain example.com or mcp-publisher login http --domain example.com, adding the private half of your pair as shown in the authentication docs. Teams that prefer not to keep a private file on a laptop can sign through Google or Azure cloud signing services instead.

Write server.json Step by Step

Generate the Skeleton

Install the publisher with Homebrew (brew install mcp-publisher) or download a binary from the registry's GitHub releases. Then, inside your server project:

mcp-publisher --help
mcp-publisher init

The init command writes a server.json template and fills in what it can from your project.

A Minimal Working File

Developer in a gray hoodie typing at a standing desk beside a notebook checklist and a small succulent

Here is the shape the docs use for a local npm server:

{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "io.github.my-username/weather",
  "description": "An MCP server for weather information.",
  "repository": {
    "url": "https://github.com/my-username/mcp-weather-server",
    "source": "github"
  },
  "version": "1.0.1",
  "packages": [
    {
      "registryType": "npm",
      "identifier": "@my-username/mcp-weather-server",
      "version": "1.0.1",
      "transport": {
        "type": "stdio"
      }
    }
  ]
}

Keep the $schema line that init generates, since the schema date moves over time. Three fields cause most of the trouble. The name must match the ownership proof inside your package (more on that below). The packages[].identifier must point at something already published. And transport.type tells clients how to talk to the server, with stdio meaning a local process.

Need environment variables? Add them to the package entry with isRequired and isSecret flags, so clients prompt for them and mask the input.

Version Rules That Bite

Every publication needs a unique version, and once it is published, that version and its metadata cannot change. Semantic versioning is recommended, though any string is accepted. Version ranges are rejected on purpose.

Version stringStatus
1.0.0, 1.0.0-beta.1, 3.0.0-rc.2Recommended
2025-06-18, v1.0Allowed
^1.2.3, ~1.2.3, >=1.2.3, 1.xProhibited

Two habits keep you out of trouble. First, align the server version with the package version, so 1.2.3 in server.json matches 1.2.3 on npm. Second, if you only need to fix registry metadata without touching the package, publish a prerelease such as 1.2.3-1. Watch the catch: semver sorts a prerelease before its regular version, so publishing 1.2.3-1 after 1.2.3 will not be marked as the latest.

Remote Servers With remotes

Low-angle view down a quiet data center aisle lined with tall black server racks

Hosted servers use a remotes array instead of, or next to, packages:

{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "com.example/acme-analytics",
  "description": "Real-time business intelligence and reporting platform",
  "version": "2.0.0",
  "remotes": [
    {
      "type": "streamable-http",
      "url": "https://analytics.example.com/mcp",
      "headers": [
        {
          "name": "Authorization",
          "description": "Bearer token for your account",
          "isRequired": true,
          "isSecret": true
        }
      ]
    }
  ]
}

A remote must be publicly accessible at its URL. Prefer Streamable HTTP; the SSE transport is deprecated, so add an "sse" remote only for existing clients. Multi-tenant setups can use URL variables such as https://{tenant_id}.analytics.example.com/mcp, each described with isRequired, default or choices. And if you ship both a package and a remote, list both: the host app picks the install method it prefers.

Prove You Own the Package

The registry checks that the package really belongs to the name you are claiming. Skip this and publishing fails with "Registry validation failed for package". Each package type has its own proof.

One Check Per Package Type

Macro shot of a brass notary stamp pressing a fresh ink impression onto thick cream paper

Package typeregistryTypeOwnership proof
npmnpmmcpName in package.json equals the server name
PyPIpypimcp-name: <server name> in the README, hidden comment allowed
NuGetnugetmcp-name: <server name> in the README, hidden comment allowed
Cargo (crates.io)cargomcp-name: <server name> as visible README text
Docker or OCI imageociLABEL io.modelcontextprotocol.server.name="<server name>"
MCPB filemcpbURL contains "mcp", plus a fileSha256 hash in server.json

For npm, that looks like this in package.json:

{
  "name": "@my-username/mcp-weather-server",
  "version": "1.0.1",
  "mcpName": "io.github.my-username/weather"
}

A few details trip people up. The npm check uses the public npm registry only, and PyPI and NuGet are likewise limited to their official registries. crates.io strips HTML comments, so the Cargo token has to be visible text, not a hidden comment. For container images, the identifier follows registry/namespace/repository:tag, and the supported hosts are Docker Hub, GitHub Container Registry (ghcr.io), Google Artifact Registry, Azure Container Registry and Microsoft Container Registry. For MCPB files hosted on GitHub or GitLab releases, compute the hash with openssl dgst -sha256 your-file.mcpb. The registry does not verify that hash, but clients do before installing.

💡 Tip: the server name in server.json and the proof inside the package must match character for character. A stray capital letter is enough to fail validation.

Publish From Your Terminal

Log In With GitHub

Woman in a denim jacket at a rainy cafe window table holding a phone beside an open laptop

Run the login from your project folder:

mcp-publisher login github

The CLI prints a one-time code and a URL:

To authenticate, please:
1. Go to: https://github.com/login/device
2. Enter code: ABCD-1234
3. Authorize this application
Waiting for authorization...

Open the link, paste the code, approve, and the terminal confirms the login. If you later see "Invalid or expired Registry JWT token", the session lapsed. Run the login again.

Publish and Verify

Eye-level view of a bookshop window at golden hour with one new hardback book on a wooden display stand

With the package live on npm and server.json saved, publish:

mcp-publisher publish

A healthy run prints the registry URL and your server name with its version. Confirm it through the public API:

curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.my-username/weather"

Your server's metadata should appear in the JSON that comes back. Downstream marketplaces refresh on their own schedule, so give them a while before you expect to see the listing there, and look for it on github.com/mcp too.

Updates follow the same path. Bump the package version, publish it to npm, bump server.json to match, and run mcp-publisher publish again. Each publication is its own immutable version, and the registry marks the newest semantic version as the latest, so clients that ask for the current release get the right one.

Ship Releases With GitHub Actions

A Tagged Release Workflow

High-angle view of cardboard parcels riding a conveyor belt through a sorting hall

Once the manual run works, move it into CI so every version tag publishes the package and the registry entry together. This workflow uses GitHub OIDC, the method the docs recommend:

name: Publish to MCP Registry

on:
  push:
    tags: ["v*"]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Set up Node.js
        uses: actions/setup-node@v5
        with:
          node-version: "lts/*"

      - name: Install dependencies
        run: npm ci

      - name: Build package
        run: npm run build --if-present

      - name: Publish package to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Install mcp-publisher
        run: |
          curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher

      - name: Authenticate to MCP Registry
        run: ./mcp-publisher login github-oidc

      - name: Publish server to MCP Registry
        run: ./mcp-publisher publish

Release with two commands: git tag v1.0.0 and git push origin v1.0.0. One thing the template leaves optional is the version bump. If server.json holds a hardcoded version, the registry will reject a repeat publication, so set it from the tag before the publish step. For a single-package server, this jq line updates both version fields:

VERSION=${GITHUB_REF#refs/tags/v}
jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.tmp && mv server.tmp server.json

Which Secrets You Need

  • GitHub OIDC: no registry secret. You only need the id-token: write permission.
  • GitHub personal access token: store it as a secret and run mcp-publisher login github --token, with the read:org and read:user scopes.
  • DNS login: store the private half of your Ed25519 pair as a secret and pass it to mcp-publisher login dns.
  • Package registry: the workflow above also needs an NPM_TOKEN secret for npm publish.

Fix Errors Before Users See Them

Overhead view of a desk with a printed page marked in red pencil, a magnifying glass and sticky notes

Most failed publishes come down to five messages:

Error messageLikely fix
"Registry validation failed for package"The package lacks its ownership proof, such as mcpName in package.json.
"Invalid or expired Registry JWT token"Log in again with mcp-publisher login github.
"You do not have permission to publish this server"Your login method does not match the name prefix. GitHub login needs io.github.your-username/.
"Authentication failed"In Actions, confirm id-token: write is set, or check your secrets.
"Package validation failed"The package is not on its registry yet, or it lacks the ownership proof.

Before every release, run through this short list:

  • The name in server.json equals the mcpName (or README token, or image label).
  • The package version in server.json already exists on npm, PyPI or your container host.
  • The server version has never been published before, and it is not a range.
  • Any remote URL answers from the public internet, not just your office network.
  • The server is meant for the public. Private servers belong in a private registry.

Draft and Illustrate With PicassoIA

An LLM is a fast first-draft machine for server.json, as long as the registry does the judging. PicassoIA hosts large language models you can use straight from the browser, including Claude Sonnet 5, GPT 5.6 Sol and Gemini 3.5 Flash.

How to Use Sonnet 5 on PicassoIA

  1. Open the Claude Sonnet 5 page on PicassoIA and start a new chat.
  2. Paste your package.json fields (name, version, description, repository) plus the $schema line that mcp-publisher init produced.
  3. Ask for JSON only, tell the model to leave $schema untouched, and forbid invented fields.
  4. Copy the result into server.json, then check by eye that name equals your mcpName.
  5. Run mcp-publisher publish. If validation complains, paste the exact error back into the chat.

Short prompts with pasted source beat long prompts with descriptions, because models invent plausible fields when they are not shown the real file. Want the Actions workflow scaffolded too? GPT 5.6 Sol is a good second opinion for that.

PicassoIA also runs a developer API, and it makes a handy example of what environmentVariables are for. The API lives at https://api.picassoia.com/v1 and works like other prediction APIs: create a prediction, poll it, then fetch the result, with up to 5 concurrent predictions per account (as of early October 2026). A hypothetical wrapper server would ask each user for their own credential once, through an entry like this inside its package:

"environmentVariables": [
  {
    "name": "PICASSOIA_TOKEN",
    "description": "Bearer credential for api.picassoia.com/v1",
    "isRequired": true,
    "isSecret": true,
    "format": "string"
  }
]

A registry entry is only text, but the README, the social card and the launch post all need pictures. Open PicassoIA, pick an image or video model, and generate a hero image or a short demo clip for your server. The full model catalog is at picassoia.com/en/all-models. Try three prompts, keep the best one, and ship it next to your first mcp-publisher publish.

Share this article