DEV Community

iamthat
iamthat

Posted on

Nine reasons your MCP server never shows up in the directories

Nine reasons your MCP server never shows up in the directories

I spent one night making three MCP servers visible to the directories, and
almost every hour of it went into things that have nothing to do with how good
your server is. This is the list I wish I had before I started, with the exact
evidence for each item.

Nothing here is theory. Every item below is something that was broken in a
real server, and I can show you the request that broke it.


1. The description limit is 100 characters, and it fails silently

The official MCP Registry validates server.json before publishing. If
description is longer than 100 characters, the publish call returns:

422 Unprocessable Entity
{"errors":[{"message":"expected length <= 100","location":"body.description"}]}
Enter fullscreen mode Exit fullscreen mode

That is the friendly one. The unfriendly case is a description that is 101
characters and passes local validation, because you never run the registry's
validator before you publish. Run it:

mcp-publisher validate
Enter fullscreen mode Exit fullscreen mode

2. resources/list and prompts/list must answer with empty lists

If you have no resources and no prompts, the correct answer is an empty list.
The wrong answer is "method not found".

Directory scanners treat a JSON-RPC error as a broken server and stop there. A
one-line server that returns {"resources": []} passes every scan. It costs
three lines and it is the difference between "listed" and "not listed".

if method in ("resources/list", "prompts/list"):
    return _result(request_id, {"resources": [], "prompts": []})
Enter fullscreen mode Exit fullscreen mode

3. Tool annotations are not decoration

Every tool needs title, readOnlyHint, destructiveHint and openWorldHint.
Some directories reject the listing outright without them; the rest render them
as badges, which is the only place a buyer sees that your tool does not write
anything.

"annotations": {
  "title": "Verify an AI agent artifact",
  "readOnlyHint": true,
  "destructiveHint": false,
  "idempotentHint": true,
  "openWorldHint": false
}
Enter fullscreen mode Exit fullscreen mode

4. Return failures as tool results, not as protocol errors

A reviewer will call your tool. If the tool fails and you answer with a
JSON-RPC -32603, the reviewer sees an opaque "Internal error" and marks the
server as broken. Answer with a result that carries isError: true and the
real message. The tool failed — say so, in words, in the place the tool is read.

return {
    "content": [{"type": "text", "text": f"{verdict} · {reason}"}],
    "structuredContent": payload,
    "isError": verdict == "reject",
}
Enter fullscreen mode Exit fullscreen mode

5. llms-install.md with a placeholder is worse than no file

Cline, and other agents, install servers by reading the README and
llms-install.md. Mine said:

claude mcp add --transport http attest https://REPLACE_WITH_DOMAIN/mcp
Enter fullscreen mode Exit fullscreen mode

An agent that follows that literally installs nothing, and the failure looks
like the agent's problem, not yours. Put the real hostname in.

6. A 404 on your privacy policy is a rejection, not a detail

At least one major directory requires a working privacy policy URL and treats
its absence as an automatic rejection. /privacy returning 404 is enough.
What it should say is short: what you store (counters, feedback, requests), what
you do not (artifacts, prompts, IP addresses), and how to ask for deletion.

7. Point your Gemini extension at the MCP endpoint, not at your home page

Gemini CLI picks up gemini-extension.json from the repository root and
indexes it within a day. Mine pointed at the site root:

{ "name": "attest", "httpUrl": "https://attest.rta.lat" }
Enter fullscreen mode Exit fullscreen mode

That is a web page, not an MCP server. The gallery would have listed a server
that cannot answer tools/list. It should be:

{
  "name": "attest",
  "mcpServers": { "attest": { "httpUrl": "https://attest.rta.lat/mcp" } }
}
Enter fullscreen mode Exit fullscreen mode

Add the gemini-cli-extension topic to the repository, or the crawler does not
look.

8. Dead endpoints in your spec make the whole listing look broken

Our OpenAPI spec still advertised two endpoints that had been removed months
earlier. A buyer who read the spec and called them got a 404 — and concluded
the server was abandoned. The spec is the product page for anyone who reads it
before connecting. Keep it equal to what the server actually answers:

curl -s -X POST https://your.host/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Enter fullscreen mode Exit fullscreen mode

Every path in the spec should appear in that answer, and nothing should appear
in the spec that does not.

9. One remote URL is one registry entry

The official registry accepts io.github.user/name (GitHub authentication) or
com.example.name (domain authentication). I tried to give my servers a
namespace of their own while keeping the URLs I already had:

400 Bad Request
{"message":"remote URL https://attest.rta.lat/mcp is already used by server io.github.mrpkk/attest"}
Enter fullscreen mode Exit fullscreen mode

The registry keys entries by remote URL. One endpoint, one entry, one name. If
you want a different namespace, you have to give up the URL first.

Domain authentication has a second trap: the TXT record goes on the apex
of the domain, not on a selector. _mcp-registry.example.com will not be seen.
It follows SPF placement, not DKIM.

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}\""
Enter fullscreen mode Exit fullscreen mode

Then host the same string at /.well-known/mcp-registry-auth and log in with
mcp-publisher login http --domain example.com --private-key "...". Keep the
private key out of your repository.


What actually moved the numbers

After fixing the list above, one server went from quality C to B in a
directory that grades servers automatically, and all three servers ended up
inside the official registry, which is what makes them appear in three other
directories without a second submission.

The one thing that does not work and never will: paying for placement. The
premium tiers that guarantee a badge cost money, and the badge is not the
product. A directory listing is a distribution channel, not a feature. Fix the
server, describe it honestly, and let the tools answer for themselves.

Check your own server in five commands

curl -s -X POST https://your.host/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'          # tools exist
curl -s -X POST https://your.host/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"resources/list"}'        # empty, not an error
curl -s -X POST https://your.host/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"prompts/list"}'          # empty, not an error
curl -s -o /dev/null -w '%{http_code}\n' https://your.host/privacy
curl -s https://your.host/llms-install.md | head -5             # real hostname
Enter fullscreen mode Exit fullscreen mode

If the first three answer and the last two do not, the directory will not
list you — and it will not tell you why.


I build **attest, one of the servers that went through this list — it checks an
artifact another agent produced before that artifact reaches your context. The
same checklist lives at attest.rta.lat/catalogs,
and there is a free tier with no sign-up:
100 checks for 30 days.

Top comments (5)

Collapse
 
autenai profile image
Auten •

Useful list. Two things from our side that would fit next to it, since we went through the same for a local stdio server (Auten team here, computer-use MCP):

  • Being in the official registry does not get you into GitHub's MCP Registry (github.com/mcp, the one VS Code and Copilot show). It is curated and does not mirror the official one automatically. The only route we found was a nomination by email to [email protected]. Ours was approved about five days later and then showed up within hours, mirroring the official registry entry.
  • For the Claude directory, a local server can't be submitted on its own anymore. It has to live inside a plugin bundle (a plugin/ folder with the manifest), and claude plugin validate catches most of what the review would.

The "fails silently" pattern from #1 matched our experience everywhere: most directories just never list you and never say why, so checking each listing by hand a few days later was the only reliable signal.

Collapse
 
mrpkk profile image
iamthat •

Thank you — the GitHub registry note was the most useful part of this thread. We had assumed the official registry was enough, and you were right that it is not: nominations go to [email protected], so we sent one this morning for attest.

On the Claude directory requiring a plugin bundle: our server is remote (streamable HTTP, nothing to install locally), so I am not sure the requirement applies to us. I will ask rather than assume — but if it does, a plugin/ folder with a manifest plus claude plugin validate is a much cheaper gate than a human review, and I would rather run that validator early than write a plugin folder by hand and find the problem at review time.

And yes on the silent failure. One of our submissions was rejected with a concrete reason; another simply never appeared, with no message anywhere. Checking each listing by hand a few days later is genuinely the only signal that exists — so we now treat "is it listed" as something to verify, not something to assume.

Collapse
 
autenai profile image
Auten •

Glad it helped, and good luck with the nomination. Ours took about five days to come back.

On the Claude directory: as far as we read the docs, the plugin-bundle requirement is about local servers, because a local stdio server can't be listed on its own anymore. A remote streamable HTTP server is the case the connector submission is built for, so you may skip the plugin folder entirely. Asking first is the right call though, the docs move.

"Verify, don't assume" is the rule we ended up with too. A short list of listing URLs you open a few days after each submission catches the silent ones.

Collapse
 
arhancanli profile image
Arhan Canli •

Item 4 is the one I'd add a test for. Returning failures as isError results only works if nothing throws before the handler gets to build the result. Our find_tool crashed on the query "constructor": the lookup used a plain object, so the name resolved to Object's own constructor instead of "not found", and the client saw a protocol error rather than a readable result.

Cheap check before listing: call every tool with names and arguments like constructor, proto, toString, an empty string and a 10,000-character string, and assert each comes back as a normal result (isError or not), never a -32603. We ran 2,980 hostile calls like that against our server and the crashes were all of this kind, not exotic ones.

Do your three servers pass if you send proto as a tool argument?

Collapse
 
mrpkk profile image
iamthat •

Ran it — 45 hostile calls per tool (constructor, proto, toString, an empty string, a 10,000-character string), across all three tools.

And your point found a real defect rather than a hypothetical one. bridge_check was returning -32601 "method not found" when the bridge was not attached. Since the tool is advertised in tools/list, the directory scanner read that as a broken server, and the tool dropped out of the listing entirely. That is exactly the failure you describe: the protocol error arrives instead of a result.

Now every tool-execution failure — bridge not attached, missing argument, artifact over the size limit — comes back as a result with isError: true. Only a genuinely unknown tool name stays a protocol error, because that is the one case where the request could not be understood at all. Fixed in 7e20b7c, with four tests for exactly the cases you listed.

One addition to your checklist: run the same calls through the MCP endpoint, not only the local handler. The distinction that matters is not "does the argument validate" but "does something throw before the result is assembled" — and that boundary only exists at the transport layer.