The MCP Registry is the official, open catalog of MCP servers, run by the Model Context Protocol project at registry.modelcontextprotocol.io. It doesn't host code or run servers. It stores one small JSON record per server version, called server.json, that says what the server is called, who owns that name, and where to get it: an npm, PyPI, NuGet, Cargo, Docker or MCPB package, a remote URL, or both. Marketplaces and other directories, and through them MCP clients, read that record through a public API.
Publishing takes four things: a server.json, a marker in your package proving it belongs to that name (if you ship a package), a login that proves you own the namespace, and the mcp-publisher CLI. The official quickstart walks the simplest case, an npm package under a GitHub-username namespace. This guide covers a common company setup: a hosted endpoint plus a package, published under your own domain. We published DialMCP as com.dialmcp/dialmcp on July 27, 2026, with a hosted Streamable HTTP endpoint and an npm package in the same record, under our own domain. Every file and command below is the real one, checked against the registry docs as of October 13, 2026.
What the MCP Registry is, and what it isn't
Think of it as a card catalog, not a library. The registry holds metadata and points elsewhere for the actual thing you run. A few properties shape everything that follows:
- It's still labelled preview. It launched in preview on September 8, 2025, and its v0.1 API was frozen on October 24, 2025. Every page of the publishing guide still warns that "breaking changes or data resets may occur before general availability", so keep your
server.jsonin your repo as the source of truth. - Published versions are immutable. To change a description, you publish a new version. Only a version's status can change later.
- It proves who owns the name, not who owns the URL. Login proves control of the namespace. For a remote server, the registry does not check that the endpoint belongs to you. It does insist that each remote URL is used by only one server name.
- It's built to be copied. The read API needs no authentication. Downstream "aggregators" are expected to pull from it on a schedule (the docs suggest about hourly) and keep their own copy. GitHub's MCP Registry is one of them.
That last point is why publishing is worth an afternoon. You write one record, and many of the directories that sync from it list you without a form.
Step 0: choose your namespace
The name field is a reverse-DNS namespace, a slash, and a server name. Which namespace you can use depends on how you log in:
| Login method | Name you can publish | What it proves |
|---|---|---|
login github (device code) | io.github.alice/weather | You control that GitHub account. Org namespaces like io.github.acme/* need you to be an org Owner; plain membership no longer counts. |
login github-oidc (Actions only) | io.github.alice/weather | A workflow running in a repository owned by that account or org. It grants io.github.<repo-owner>/* with no stored secret. |
login dns | com.example/weather | You can set a TXT record on example.com. |
login http | com.example/weather | You can serve a file at https://example.com/.well-known/mcp-registry-auth. |
The GitHub route is the quickest. If your remote endpoint lives on your own domain, the docs suggest domain login instead, so users can see that the name and the URL belong to the same publisher. That's why we picked com.dialmcp/dialmcp over an io.github name. Our endpoint is mcp.dialmcp.com, and "com.dialmcp" says who runs it. Pick carefully: the name is the identity every downstream directory keys on, and published versions can't be renamed.
Step 1: write server.json
Run mcp-publisher init in your project for a template, or write the file by hand. Here is ours, field for field (whitespace condensed), from the public connector repo:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "com.dialmcp/dialmcp",
"title": "DialMCP",
"description": "Let AI agents place real phone calls from your verified number, with transcripts and recordings.",
"version": "0.1.0",
"websiteUrl": "https://dialmcp.com",
"repository": {
"url": "https://github.com/SkillfulAgents/dialmcp-connector",
"source": "github",
"id": "1314191579"
},
"icons": [
{
"src": "https://raw.githubusercontent.com/SkillfulAgents/dialmcp-connector/main/assets/logo.png",
"mimeType": "image/png",
"sizes": ["512x512"]
}
],
"remotes": [
{ "type": "streamable-http", "url": "https://mcp.dialmcp.com/mcp" }
],
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "dialmcp-connector",
"version": "0.1.0",
"transport": { "type": "stdio" }
}
]
}
The fields that trip people up:
descriptionhas a hard cap of 100 characters. Longer ones fail validation. Ours is 96. This one line also becomes your listing copy in most directories that import you, so write it like an ad, not a README.$schemais versioned by date. The current docs use2025-12-11. An older date on its own only triggers a warning. What breaks is copying a pre-September-2025 example (schema2025-07-09) whose snake_case fields, such asregistry_type, the current schema spells in camelCase (registryType). The registry rejects those outright.versionmust be unique per publish. Semver is recommended, and ranges like^1.2.3are rejected. For a package-based server, keep it equal to the package version.remotesandpackagescan coexist. We list both, so a client can connect to the hosted endpoint or run the npm bridge locally. A remote-only server dropspackagesentirely. Note thatinitgenerates apackagesentry even then, so delete it.- Use
streamable-httpfor remotes.sseis still accepted, but the transport is deprecated, so list it only to support older clients. If you're unsure which you serve, our Streamable HTTP vs SSE explainer covers the difference. Remotes can also declare requiredheaders(for an API key) or URL template variables such as{tenant_id}.
Step 2: prove the package is yours
The registry won't take your word that a package belongs to your server name. For npm, PyPI, NuGet, Cargo and Docker, the published artifact must carry a marker naming the server, and the registry checks it. So publish the package first, with the marker inside, or the registry has nothing to check.
| Package type | Ownership marker |
|---|---|
| npm | "mcpName": "com.example/weather" in package.json. Public npmjs.org registry only. |
| PyPI, NuGet | mcp-name: com.example/weather in the package README, visible or inside an HTML comment |
| Cargo | The same mcp-name: line, but as visible text, because crates.io strips HTML comments |
| Docker / OCI | LABEL io.modelcontextprotocol.server.name="com.example/weather" |
| MCPB | No name marker. The download URL must be a GitHub or GitLab release and contain "mcp", and server.json must include the artifact's fileSha256. Clients check that hash; the registry doesn't. |
| Remote only | Nothing to prove; the URL just has to be publicly reachable |
For us that's one line in the connector's package.json: "mcpName": "com.dialmcp/dialmcp". The marker is read from the published version, not your working copy. If you add it after publishing, you need a new package version before the registry will accept the server. For PyPI and NuGet, don't glue the name to a full stop at the end of a sentence; the docs say the token needs a boundary after it, so a trailing "." breaks the match.
Step 3: prove the namespace is yours
With a GitHub namespace this is mcp-publisher login github. It prints a device code, you enter it at github.com/login/device, and you're done. For a domain namespace you generate a key pair, publish the public half, and sign in with the private half. The DNS version from the docs:
# Ed25519 needs OpenSSL 3; macOS's built-in LibreSSL can't generate it
openssl genpkey -algorithm Ed25519 -out key.pem
PUBLIC_KEY="$(openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64)"
echo "example.com. IN TXT \"v=MCPv1; k=ed25519; p=${PUBLIC_KEY}\""
# after adding the TXT record and letting it propagate:
PRIVATE_KEY="$(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')"
mcp-publisher login dns --domain example.com --private-key "${PRIVATE_KEY}"
Our record is public, like any TXT record. dig TXT dialmcp.com returns it next to our SPF and Google verification entries:
"v=MCPv1; k=ed25519; p=JGLzQ7cEnGdgtNcC9XGsSPYgizRpt5TX3QSS7tGv9PA="
Three things the docs warn about, and that we'd underline:
- Put the TXT record on the apex (
dialmcp.com), not on a selector like_mcp-auth.dialmcp.com. The registry only looks at the apex. Current versions tell you if they find the record under_mcp-author_mcp-registry; anywhere else you just get "no MCP public key found in DNS TXT records". - When you rotate keys, delete the old record. If the new record hasn't propagated yet, login fails with "signature verification failed (tried published key …)". The error lists the keys it found, and a leftover record makes that list harder to read.
- Guard the private key like a deploy key for the whole domain. It can publish new versions of, deprecate, or delete any server under
com.dialmcp/*(and subdomain namespaces likecom.dialmcp.mcp/*), not just this one. Keep it out of shell history and plain repo secrets (more on that in the CI section).
If you can't touch DNS, HTTP login is the fallback. Serve the v=MCPv1; k=ed25519; p=… line as a plain file at https://example.com/.well-known/mcp-registry-auth and run mcp-publisher login http --domain example.com --private-key .... Unlike DNS, it covers only com.example/*, not subdomain namespaces. If you'd rather not hold a raw key at all, both methods also accept signing keys kept in Google Cloud KMS or Azure Key Vault.
Step 4: validate, publish, and check the record
Install the CLI with brew install mcp-publisher or the release binary from the registry's GitHub releases. Then:
mcp-publisher validate # schema and field checks, publishes nothing
mcp-publisher publish
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=com.dialmcp"
Run validate every time. It needs no login and catches schema and field errors, but namespace and package-ownership problems only show up at publish. The registry then adds its own block to what you sent. This is what came back for us today:
"_meta": {
"io.modelcontextprotocol.registry/official": {
"status": "active",
"statusChangedAt": "2026-07-27T21:27:26.974526Z",
"publishedAt": "2026-07-27T21:27:26.974526Z",
"updatedAt": "2026-07-27T21:27:26.974526Z",
"isLatest": true
}
}
That block, not a confirmation email, is your receipt. Directories that sync from the registry rely on it, especially isLatest and status. Every timestamp in our record is still the original publish time, because version 0.1.0 has never changed.
Updating, deprecating and deleting
Since a published version can't be edited, every change is a new publish with a new version. Two rules from the versioning docs are worth knowing before you need them:
- Metadata-only fixes get a prerelease suffix. To fix a typo without shipping a new package, publish server version
1.2.3-1that still points at package1.2.3. Semver sorts1.2.3-1before1.2.3, so a prerelease published after its regular version won't be marked latest. - "Latest" follows semver, not the clock. If you switch from semver to something unparseable, the odd version gets marked latest regardless.
To retire a server, run mcp-publisher status --status deprecated --message "Use 2.0.0" com.example/weather 1.0.0 (or --status deleted). Drop the version and add --all-versions to change every version at once, and set --status active to undo it. Deleted servers disappear from default listings but stay retrievable with include_deleted=true. The FAQ says metadata is never permanently removed, and preview data can still be reset, so plan as if anything you publish stays on record.
Automating it in GitHub Actions
The registry's own guide shows a tag-triggered workflow that publishes the package, then the server. For io.github names, mcp-publisher login github-oidc needs only id-token: write and no stored secret. Domain namespaces need the private key as a secret, and that's where our release workflow adds three things the basic example leaves out (excerpt, lightly condensed):
registry:
needs: publish
if: github.event_name == 'release'
runs-on: ubuntu-latest
environment: release
steps:
# ...checkout, install mcp-publisher...
- name: Wait for npm to propagate
run: |
VERSION="$(node -p "require('./package.json').version")"
for i in $(seq 1 10); do
curl -sfo /dev/null "https://registry.npmjs.org/dialmcp-connector/$VERSION" && exit 0
sleep 15
done
exit 1
- name: Publish to the MCP Registry
run: |
./mcp-publisher login dns --domain dialmcp.com --private-key "$MCP_REGISTRY_PRIVATE_KEY"
./mcp-publisher publish
env:
MCP_REGISTRY_PRIVATE_KEY: ${{ secrets.MCP_REGISTRY_PRIVATE_KEY }}
- The key sits behind a tag-restricted environment. It's stored as an environment secret on
release, and that environment's deployment policy allows onlyv*tags. A plain repository secret would be readable by any workflow a repo writer can trigger, and this key controls the whole namespace. The registry's GitHub Actions guide recommends the same pattern, and calls adding a required reviewer the strongest option. - It waits for npm. The registry checks
mcpNameon the freshly published npm version. If you callpublisha second afternpm publish, the version may not be visible yet. So the job polls npm before publishing instead of failing the release. - Dry runs still validate. The first job typechecks, runs a small script that checks the required fields, the 100-character description, the schema date, and that
mcpName, the npm identifier and both versions all agree, then builds and smoke-tests the package and finally runsmcp-publisher validate. That job also runs on manual dry runs, which never see the key. A tag that doesn't matchpackage.jsonstops the release before anything is published.
One confession: our first publish was done by hand, about two minutes before the commit that added the registry step to our release workflow. We haven't cut a tagged release since, so the workflow gets its first real run on our next release. The docs add two warnings for any setup: never publish from a pull_request_target workflow that checks out PR code, and avoid self-hosted runners for the publish job on public repos.
What happened after we published
This is the part the quickstarts don't cover. We never filled in a form for any of these. When we checked between October 5 and 11, each listing below existed, generated from the registry record:
| Where | What we found |
|---|---|
| GitHub MCP Registry | A page at github.com/mcp/com.dialmcp/dialmcp with our logo and README |
| MCPI (mcpi.app) | Correct endpoint, plus its own uptime checks against mcp.dialmcp.com/mcp |
| MCPNav, Agents Junction, MCP Server Index, allmcps and several smaller directories | Imported listings keyed on com.dialmcp/dialmcp |
Three lessons from those copies. First, your 100 characters are your marketing everywhere. Directories reuse the description and pick their own category. One filed us under "AI Agents" rather than communication, and one sample config showed only the npm bridge, not the hosted URL. Second, propagation isn't uniform. Some mirrors picked us up and others hadn't by mid-October. PulseMCP, for one, hadn't listed us yet; its site said new submissions were paused during an intake rework. GitHub doesn't document how its MCP Registry picks servers from the official one either, so treat our GitHub page as what happened to us, not a guarantee. Third, a mirror is not a backlink strategy. Several of these pages link to your repository or endpoint rather than your website, and a few mark links nofollow. Treat them as discoverability, not SEO.
Common errors and fixes
| Error | Usual cause |
|---|---|
| "You do not have permission to publish this server" | Login method and namespace don't match: a GitHub login with a com.* name, or an org name when you aren't an org Owner |
| "Registry validation failed for package" / "Package validation failed" | Missing or mismatched mcpName / mcp-name, or the package version isn't on the public registry yet |
| "no MCPv1 TXT record at …, but one was found at …" / "no MCP public key found in DNS TXT records" | TXT record under a selector instead of the apex, or not propagated yet |
| "signature verification failed (tried published key …)" | The published key isn't the one you're signing with: stale record, wrong key, or the new record hasn't propagated |
Algorithm Ed25519 not found | macOS LibreSSL; install openssl@3 and call it by full path |
| "Invalid or expired Registry JWT token" | Session expired; run mcp-publisher login again |
| "invalid audience" in CI | An old mcp-publisher binary; download the latest release |
| Publish fails on a remote URL | Another server name already uses that URL, or the endpoint isn't publicly reachable |
expected length <= 100 at body.description | Description over 100 characters |
If you're publishing a server you're still building, test it first. MCP Inspector will tell you whether a remote endpoint answers initialize and tools/list correctly before anyone else finds it through the registry. For the server itself, how to build an MCP server covers both SDKs, and remote vs local MCP servers covers what changes once yours has a public URL.
Further reading
- DialMCP hosted endpoint reference
- MCP authorization and OAuth for remote servers
- The official MCP Registry
- MCP Registry docs on GitHub (quickstart, authentication, package types, versioning)
- SkillfulAgents/dialmcp-connector: our server.json and release workflow
See a published remote server in action. DialMCP is com.dialmcp/dialmcp in the MCP Registry. It lets Claude, ChatGPT, Codex, Cursor or VS Code place real phone calls from your own verified number and hand back a transcript and a structured outcome. Add https://mcp.dialmcp.com/mcp as a connector and sign in. US and Canada, free during launch.
FAQ
What is the MCP Registry?
The official MCP Registry, at registry.modelcontextprotocol.io, is an open catalog of public MCP servers run by the Model Context Protocol project. It stores a server.json metadata record for each server version, pointing to a package (npm, PyPI, NuGet, Cargo, OCI or MCPB), a remote URL, or both. It is still labelled preview.
Does the MCP Registry host my server or package?
No. It only stores metadata. Publish your package to its own registry first, or run your remote server yourself; the registry record points to it.
Is the official MCP Registry the same as the GitHub MCP Registry?
No. The official registry is the upstream catalog. GitHub's MCP Registry at github.com/mcp is a separate catalog that shows servers from it. DialMCP's GitHub page appeared without us submitting anything, but GitHub does not document exactly how it selects servers, so don't count on it.
Can I publish a private or internal MCP server?
No. The official registry is for publicly accessible servers only. Remote URLs must be publicly reachable and packages must be on public registries such as npmjs.org.
How do I update or remove my listing?
Publish a new server.json with a new version string, because published versions can't be edited. To retire a server, set its status to deprecated or deleted with mcp-publisher status. Deleted servers are hidden from default listings, but the record is never permanently removed.