diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d7032..9714878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/skills/github-seo/SKILL.md b/skills/github-seo/SKILL.md index 46f174a..a969210 100644 --- a/skills/github-seo/SKILL.md +++ b/skills/github-seo/SKILL.md @@ -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). diff --git a/skills/github-seo/evals/evals.json b/skills/github-seo/evals/evals.json index 9aa910a..574f8bd 100644 --- a/skills/github-seo/evals/evals.json +++ b/skills/github-seo/evals/evals.json @@ -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" + ] } ] } diff --git a/skills/github-seo/references/mcp-install-and-metadata.md b/skills/github-seo/references/mcp-install-and-metadata.md new file mode 100644 index 0000000..447b386 --- /dev/null +++ b/skills/github-seo/references/mcp-install-and-metadata.md @@ -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: ` string in the package README (a comment is fine) | +| NuGet | `nuget` | `mcp-name: ` string in the package README | +| Cargo | `cargo` | `mcp-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./`. 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.*/`. +- `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=" +``` + +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 ` for npm, `uvx ` for PyPI, `docker run --rm -i ` + for OCI). Use placeholder values for secrets. +3. **One `
` 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 ''`. + - 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",":"]}`. 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 " 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. diff --git a/skills/github-seo/references/mcp-registry-checklist.md b/skills/github-seo/references/mcp-registry-checklist.md index 00f7822..76a3407 100644 --- a/skills/github-seo/references/mcp-registry-checklist.md +++ b/skills/github-seo/references/mcp-registry-checklist.md @@ -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 @@ -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. diff --git a/skills/github-seo/scripts/check_repo_seo.py b/skills/github-seo/scripts/check_repo_seo.py index cc39165..e65178d 100644 --- a/skills/github-seo/scripts/check_repo_seo.py +++ b/skills/github-seo/scripts/check_repo_seo.py @@ -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}")