MCP Registry: how to publish an AI governance server
Publishing an MCP server takes twenty minutes. Publishing a governance server without lying about what it does takes one extra step — the one this guide puts first.
The official Model Context Protocol registry is the directory MCP clients read to discover servers. Being listed means being findable by someone searching for “compliance” or “policy” who has never heard your name. Not being listed means existing only for the people you sent a URL to.
This is the procedure as it actually went for a governance server — including the two places where it stopped dead.
Step 0 — Do not publish until the promise is true
Before touching the registry, four checks against production, not against your laptop. They take a minute and prevent the one unrecoverable failure in this procedure: a listed server that does not live up to its own description.
# 1. the site answers
curl -s -o /dev/null -w "%{http_code}\n" https://your-domain.tld/
# 2. signing is ACTIVE — blocking if the manifest says "signed evidence"
curl -s https://your-domain.tld/.well-known/your-authority.json | grep -o '"status":"[a-z]*"'
# 3. the MCP endpoint answers and lists the advertised tool
curl -s https://your-domain.tld/api/mcp-http \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# 4. the manifest is served as JSON (and not as a redirect body)
curl -sL -o /dev/null -w "%{http_code} %{size_download}\n" \
https://your-domain.tld/.well-known/mcp-server.jsonStep 1 — Choose the namespace (effectively irreversible)
The namespace determines the authentication method and how the market reads you. Changing it after publication creates a duplicate, and the name is part of the identity clients memorise.
| Namespace | Server name | Authentication | How the market reads it |
|---|---|---|---|
| Domain | com.example/mcp | DNS TXT (Ed25519) or HTTP file | company product |
| GitHub | io.github.handle/server | mcp-publisher login github | personal project |
For a governance server the domain is not an aesthetic choice: proving domain ownership is the identity claim that trust in your decisions rests on. A server that claims to arbitrate compliance questions from behind a GitHub handle is asking a lot of its callers.
Step 2 — Write server.json
The minimum viable manifest for a remote server. Two constraints catch nearly everyone: the name must be reverse-DNS, and the description is capped at 100 characters.
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "ca.structureclerk/mcp",
"title": "StructureClerk",
"description": "AI agent policy decisions: ALLOW, DENY, APPROVE, ESCALATE. Signed evidence.",
"version": "2.0.0",
"websiteUrl": "https://structureclerk.ca/authority",
"remotes": [
{ "type": "streamable-http", "url": "https://structureclerk.ca/api/mcp-http" }
]
}A maintenance note: keep this file in the repository (say, at public/.well-known/mcp-server.json), served publicly, and copy it to server.json when publishing. A manifest that exists only on the machine of whoever published becomes wrong at the first change, and nobody notices.
Step 3 — The part that blocks: the Ed25519 key
The documentation says “prove domain ownership via DNS”. You picture a token printed by the tool, to be pasted into your DNS zone. It is not that. You generate an Ed25519 keypair: the public key goes into the TXT record, the private key is handed to the tool as hex.
Running mcp-publisher login dns --domain example.tld without a key gets you exactly this, and nothing else:
Error: ed25519 private key (hex) is requiredThe three commands that unblock it:
# 1. an Ed25519 keypair (the private key never leaves your machine)
openssl genpkey -algorithm Ed25519 -out key.pem
# 2. the PUBLIC key, base64, for the DNS record
PUBLIC_KEY="$(openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64)"
echo "v=MCPv1; k=ed25519; p=${PUBLIC_KEY}"
# 3. the PRIVATE key, hex — this is what the CLI asks for
PRIVATE_KEY="$(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')"- The TXT record goes on the domain apex (
example.tld, not_mcp.example.tld), with the value exactlyv=MCPv1; k=ed25519; p=<base64 public key>. - On macOS,
opensslis LibreSSL by default and does not implement Ed25519: install OpenSSL 3 (Homebrew) and call that binary. - On Windows,
cmd.exeexpands neither$HOMEnor$(...). Use the Git Bash terminal shipped with Git for Windows — these commands work there as written. - The private key goes nowhere else: not in the repository, not in the manifest, not in CI. It is what proves you are the domain.
Wait for DNS propagation before continuing. dig TXT example.tld must return your record; allow 5 to 30 minutes depending on the registrar. And leave the record in place afterwards — the registry may re-verify.
Step 4 — Publish
- 1
Install the tool
mcp-publisherships as a release binary. On Windows the downloaded binary is not on yourPATH: call it by full path, or add its folder toPATH.brew install mcp-publisher # macOS # Linux / Windows: release binary from modelcontextprotocol/registry - 2
Authenticate against the registry
With the hex private key from step 3, and the domain whose TXT record has propagated.
mcp-publisher login dns --domain example.tld --private-key "${PRIVATE_KEY}" - 3
Publish from the folder holding server.json
The tool validates locally before sending. If it refuses, the message names the offending field: fix it in the repository's source file, not only in the throwaway copy.
cp public/.well-known/mcp-server.json ./server.json mcp-publisher publish - 4
Verify the listing
Then the only test that really counts: a real user journey, in an MCP client, searching for your server by keyword, adding it, and asking it a question from their own job.
curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=example"
What publishing a governance server demands on top
A server that converts currencies only has to be correct. A server answering “is this agent allowed?” commits its caller in front of a third party: a regulator, a customer, an auditor. Five requirements follow, none of them imposed by the registry — they are imposed by the nature of the answer.
- Determinism. The same question must return the same answer. A governance engine that samples cannot be audited; ours is a pure rule table with no LLM in the decision path.
- Citations at the right level. Citing a framework is verifiable; citing a specific section as though it were legal advice is a promise an engine cannot keep.
- Published freshness. Every citation carries its last-verified date, and the response carries the oldest of them. A compliance product that hides the age of its own data is asking to be believed rather than checked.
- Evidence verifiable without you. Digest, signature, chaining, third-party timestamp — and free verification, no account. Evidence you must pay to check is not evidence.
- Advisory status stated. Manifest, response and documentation must say the same thing: the server decides, the caller's infrastructure enforces.
Declare which of your tools are metered, too. An agent that discovers your server in the registry has no way to guess that a call costs money, and an undeclared metered tool is the fastest way to lose an integrator's trust in a single invoice.
Troubleshooting — errors actually encountered
| Symptom | Cause | Fix |
|---|---|---|
Error: ed25519 private key (hex) is required | login dns expects a key you generate yourself | Step 3, then --private-key <hex> |
mcp-publisher: command not recognised | Binary downloaded but not on PATH | Call it by full path, or add the folder to PATH |
| The downloaded manifest is 15 bytes | curl without -L saved a redirect body | curl -L, then check the file actually contains JSON |
openssl genpkey -algorithm Ed25519 fails (macOS) | LibreSSL, shipped by default, does not implement Ed25519 | Install OpenSSL 3 and call that binary explicitly |
login dns keeps failing | TXT record not propagated yet | dig TXT example.tld, wait, retry |
| Description rejected | Longer than 100 characters | Shorten it in the source file, republish |
| Server appears, tools fail | Production out of date | The registry lists, it does not proxy: redeploy |
After publication
- Keep the DNS TXT record permanently.
- Bump
versionin the manifest on every change, then republish with the same command. - Treat the registry description as a public claim: the day it stops being true, it gets corrected.
- Record the publication date somewhere in the repository — it is exactly the kind of fact you think you will remember and do not.
+Do I need a domain to publish to the MCP registry?
No — GitHub authentication lets you publish under io.github.<handle>/<server>. But for a governance server, domain ownership is part of the trust argument, and the namespace is very hard to change afterwards.
+Where does the TXT record go?
On the domain apex, with the value v=MCPv1; k=ed25519; p=<base64 public key>. Not on a subdomain, not on a selector. It must stay in place after publication, since the registry may re-verify ownership.
+Do I need to keep the private key?
Yes, in your secret manager: it is required for every republication. It does not go in the repository, in the manifest, or in a shared CI variable.
+Does the registry run my server?
No. It lists its name, description and URL. If your production goes down or regresses, the listing stays intact and points at a broken server — hence step 0.
+How do I update an already-published server?
Bump version in the manifest and run mcp-publisher publish again with the same authentication. The name never changes: it is the identity clients memorised.
The server described here is real: ca.structureclerk/mcp exposes an authority decision tool for AI agents. Its specification and evidence format are public on the Authority page.