Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- `references/mcp-install-and-metadata.md`: package types and ownership checks, `server.json` fields, README install routes per client, listing files, org visibility.
- Audit checks for `server.json` (description length, `websiteUrl`, namespace) and the OCI label.
- Docker/OCI registry quality eval.

## [0.1.0] - 2026-10-29

### Added
Expand Down
4 changes: 3 additions & 1 deletion skills/github-seo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ Fix failures first, then warnings.

## Step B: MCP server checklist (after Step A)

Use `references/mcp-registry-checklist.md`. It covers the Official MCP Registry (`server.json`
Start with `references/mcp-install-and-metadata.md`: choose the package type (npm, PyPI, OCI and
others each have their own ownership check), complete `server.json` including `websiteUrl`, and
lay out the README install routes per client. Then use `references/mcp-registry-checklist.md`. It covers the Official MCP Registry (`server.json`
plus the `mcp-publisher` CLI), Smithery, Glama, mcp.so, mcpservers.org, Cursor Directory, which
registries auto-ingest from the Official one, and common gotchas (stale listings under a personal
account, publish order, domain presence).
Expand Down
11 changes: 11 additions & 0 deletions skills/github-seo/evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@
"prepares one set of submission copy and reuses it for every listing",
"flags that manual registries have review queues and should not be resubmitted"
]
},
{
"type": "quality",
"prompt": "My MCP server ships only as a Docker image. How do I publish it to the official registry?",
"expected_behavior": [
"uses registryType oci with an identifier of the form registry/namespace/repository:tag",
"adds the io.modelcontextprotocol.server.name label to the image and matches it to the server.json name",
"fills websiteUrl and keeps the description at 100 characters or fewer",
"tells the owner that mcp-publisher login is interactive and theirs to run",
"labels registry facts with a verification date rather than stating them from memory"
]
}
]
}
119 changes: 119 additions & 0 deletions skills/github-seo/references/mcp-install-and-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# MCP server install routes and listing metadata

How to make an MCP server easy to install from the README and complete in every registry entry.
Labels follow `SKILL.md`. Registry facts were checked against the official docs on 2026-10-31
[verified]. The install-route patterns come from a published, live MCP server README
[observed]; the per-client steps in them were not re-checked against each vendor's docs.

## 1. Pick the package type first

The Official MCP Registry stores metadata only. The package must already exist on a supported
host before you publish the entry [verified]. Each type has its own ownership check, and a
mismatch is the most common publish failure.

| Package type | `registryType` | Ownership check [verified] |
|---|---|---|
| npm (public registry only) | `npm` | `mcpName` in `package.json` equals the `server.json` `name` |
| PyPI | `pypi` | `mcp-name: <name>` string in the package README (a comment is fine) |
| NuGet | `nuget` | `mcp-name: <name>` string in the package README |
| Cargo | `cargo` | `mcp-name: <name>` as visible text, because crates.io strips HTML comments |
| Docker/OCI | `oci` | Image label `io.modelcontextprotocol.server.name` equals the `server.json` `name` |
| MCPB bundle | `mcpb` | URL contains "mcp", and `fileSha256` is set (clients check the hash) |

OCI details [verified]: supported hosts are Docker Hub, `ghcr.io`, Google Artifact Registry,
`*.azurecr.io` and `mcr.microsoft.com`. The `identifier` is `registry/namespace/repository:tag`
(a digest also works). Pin a version tag, not `latest`. Put the `LABEL` in the Dockerfile and
rebuild before the final push, because the label has to be in the pushed image.

## 2. Namespace and auth [verified]

- GitHub login gives `io.github.<user-or-org>/<name>`. A live org-owned entry exists
(`io.github.devkindhq/swell`) [observed], so org namespaces work in practice.
- Domain auth (DNS TXT record or an HTTP file at `/.well-known/mcp-registry-auth`) gives a
reverse-DNS name such as `com.example.*/<name>`.
- `mcp-publisher login github` is an interactive device-code flow. The repo owner runs it.

## 3. `server.json` fields that matter for discovery

Schema `2025-12-11` [verified]:

| Field | Rule | Why it matters |
|---|---|---|
| `name` | Required, 3 to 200 chars, exactly one `/` | The registry identity. Must match the package ownership check. |
| `description` | Required, **max 100 chars** | Shown in listings. Capability, not implementation. |
| `title` | Optional, max 100 | Display name. Use the product name. |
| `version` | Required, no ranges | Match the package version. |
| `websiteUrl` | Optional, URI | The official homepage field. **Set it.** |
| `repository` | `url` and `source` | Source link for inspection. |
| `icons` | Optional | PNG or JPEG for UI display. |
| `packages` | Array | One entry per install route (see section 1). |
| `_meta` | Vendor extension | Free-form. Put author, tool list and website here as a bonus, not instead of `websiteUrl`. |

Observed gap: a live entry that put the website only under `_meta` and left `websiteUrl` empty
[observed, 2026-10-31]. Whether downstream directories display `_meta` is unknown, so fill both.

Environment variables go in `packages[].environmentVariables` with `name`, `description`,
`isRequired` and `isSecret`. Names only, never values.

Verify after publishing:

```bash
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=<your-server-name>"
```

The registry is in preview, so breaking changes or data resets can happen [verified].

## 4. README install routes

Readers arrive with different clients. One "Standard config" block, then one collapsible block per
client, is the pattern used by a live server README [observed]:

1. **Requirements** line: runtime version, supported clients, which credentials are needed.
2. **Standard config**: the JSON that works in most clients. Show the launch command exactly as the
package type dictates (`npx <pkg>` for npm, `uvx <pkg>` for PyPI, `docker run --rm -i <image>`
for OCI). Use placeholder values for secrets.
3. **One `<details>` per client**, each with the shortest working path:
- Claude Desktop: paste into `claude_desktop_config.json`.
- Cursor: an install deeplink button plus the manual Settings path.
- VS Code: the settings route plus `code --add-mcp '<json>'`.
- Windsurf, Goose, LM Studio, Warp: the settings menu path, then reuse the standard config.
- Claude Code: `claude mcp add` [general, confirm the flag syntax against current docs].
4. **Credentials**: where to find each key, the environment variable names, and a debug flag.
5. **Transports**: say which of stdio and HTTP the server supports.

Why it helps: each client name is a searchable phrase, the first copy-paste works for most
readers, and the collapsed blocks keep the README short [general]. Keep to the 400 to 700 word
target by linking out to the write-up for anything long, and cap the client list at the clients
you have actually tested.

Docker-only servers: the standard config is
`{"command":"docker","args":["run","--rm","-i","<image>:<tag>"]}`. Add resource limits only if the
server needs them.

## 5. Repo files that feed listings

| File | Feeds | Notes |
|---|---|---|
| `server.json` | Official MCP Registry | Section 3. Commit it to the repo root. |
| `smithery.yaml` | Smithery | `name`, `description`, `vendor`, `homepage`, `license`, `startCommand` with a config schema [observed]. |
| `manifest.json` | MCPB bundle, desktop installs | Carries `author` with name and URL, `repository`, and `user_config` prompts for credentials [observed]. |
| README badges | Humans | "Built by <org>" badge linking to the homepage. |

Keep one source of truth for the description, tool list and homepage, and copy it into each file.
A version drift between `package.json`, `server.json` and `manifest.json` is easy to introduce
[observed: one repo had `1.1.0` in two files and `1.0.9` in the third].

## 6. Make the org visible in every listing

- Same organisation name and the same homepage URL in `server.json` (`websiteUrl`), `smithery.yaml`,
`manifest.json`, the README byline and the repo About box.
- Link to a page on a domain you own that is specific to the server or its category, not only the
home page.
- Whether a given directory shows the website or follows the link must be checked per directory
[unverified]. Open the live listing and look.

## 7. Automate the release [general]

A manual publish drifts out of date. The registry documents a GitHub Actions route using OIDC
[verified: documented, not tested here]. Wire `mcp-publisher publish` into the release job so the
registry version tracks the package version.
7 changes: 6 additions & 1 deletion skills/github-seo/references/mcp-registry-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Generalised from a real MCP server submission run across these registries on 202
## Prerequisite

Finish Step A in `SKILL.md` first (README, description, topics, license). A registry listing
points back at the repo, and a thin repo makes a thin listing.
points back at the repo, and a thin repo makes a thin listing. Then work through
`mcp-install-and-metadata.md` for the package type, `server.json` fields and README install routes.

## Prepare the submission copy once

Expand Down Expand Up @@ -42,6 +43,10 @@ Write the fields every listing asks for, then reuse them verbatim so listings st

## Gotchas

- **Empty `websiteUrl`.** Put the homepage in the official `websiteUrl` field, not only under
`_meta`. See `mcp-install-and-metadata.md`.
- **Version drift.** Keep `package.json`, `server.json` and `manifest.json` on the same version.

- **Stale listings under a personal account.** If the server was ever published from a personal
GitHub account before the org account existed, that listing can persist on registries such as
Smithery. Find it and have it removed, through the registry's support if there is no self-service delete.
Expand Down
27 changes: 27 additions & 0 deletions skills/github-seo/scripts/check_repo_seo.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,33 @@ def record(level, name, detail):
ok = bool(files & candidates)
record("PASS" if ok else "WARN", label, "present" if ok else "missing in repo root")

# MCP server metadata (only when a server.json is present)
if "server.json" in files:
try:
sj = json.loads(base64.b64decode(json.loads(gh("api", f"repos/{slug}/contents/server.json"))["content"]))
except (SystemExit, ValueError):
sj = None
record("FAIL", "server.json", "present but not valid JSON")
if sj is not None:
owner = slug.split("/")[0].lower()
sname = sj.get("name", "")
if sname.lower().startswith(f"io.github.{owner}/"):
record("PASS", "server.json name", sname)
else:
record("WARN", "server.json name", f'"{sname}" is not io.github.{owner}/*. Fine only with domain auth.')
sdesc = sj.get("description", "")
record("PASS" if 0 < len(sdesc) <= 100 else "FAIL", "server.json description", f"{len(sdesc)} chars, max 100")
record("PASS" if sj.get("websiteUrl") else "WARN", "server.json websiteUrl", sj.get("websiteUrl") or "empty. Set the official homepage field.")
pkgs = sj.get("packages") or []
if any(p.get("registryType") == "oci" for p in pkgs):
df = ""
if "dockerfile" in files:
df = base64.b64decode(json.loads(gh("api", f"repos/{slug}/contents/Dockerfile"))["content"]).decode()
has = "io.modelcontextprotocol.server.name" in df and sname in df
record("PASS" if has else "FAIL", "OCI label", "matches server.json name" if has else "Dockerfile lacks LABEL io.modelcontextprotocol.server.name matching the name")
elif any(p.get("registryType") == "npm" for p in pkgs):
record("PASS", "npm mcpName", "check package.json mcpName equals server.json name (not read by this script)")

width = max(len(n) for _, n, _ in results)
for level, n, d in results:
print(f"{level:<5} {n:<{width}} {d}")
Expand Down
Loading