diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 879bce0..463fd5f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Wrong or outdated claim -about: A claim, step or link in the skill is wrong, or the audit script misbehaves +about: A claim, step or link in the skill is wrong, or the audit checks misbehave labels: bug --- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 98b9a6b..5e2f0a1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,7 @@ Link or dated observation for any new claim, and its label (verified, observed o ## Checklist -- [ ] `python3 scripts/validate_skill.py` prints `ok` +- [ ] CI is green - [ ] Evals updated if trigger behaviour changed - [ ] `CHANGELOG.md` updated - [ ] No client names, domains or private data diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 270f470..818b0f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,61 @@ jobs: with: python-version: "3.13" - name: Validate skill, evals, manifests and links - run: python3 scripts/validate_skill.py - - name: Compile the audit script - run: python3 -m py_compile skills/github-seo/scripts/check_repo_seo.py + # Inline, because the plugin directory refuses script files from this repo. + run: | + python3 - <<'EOF' + import json, re, sys + from pathlib import Path + + ROOT = Path(".") + errors = [] + for skill_md in sorted(ROOT.glob("skills/*/SKILL.md")): + d = skill_md.parent + m = re.match(r"^---\n(.*?)\n---\n", skill_md.read_text(), flags=re.S) + if not m: + errors.append(f"{skill_md}: missing frontmatter") + continue + fm = dict( + line.split(":", 1) for line in m.group(1).splitlines() if ":" in line and not line.startswith(" ") + ) + fm = {k.strip(): v.strip() for k, v in fm.items()} + name = fm.get("name", "") + if name != d.name: + errors.append(f"{skill_md}: name '{name}' must equal directory '{d.name}'") + if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name): + errors.append(f"{skill_md}: name must be lowercase-hyphenated") + desc = fm.get("description", "") + if not desc: + errors.append(f"{skill_md}: description is empty") + elif len(desc) > 1024: + errors.append(f"{skill_md}: description is {len(desc)} chars, limit 1024") + evals = d / "evals" / "evals.json" + if not evals.exists(): + errors.append(f"{d}: missing evals/evals.json") + else: + cases = json.loads(evals.read_text()).get("cases", []) + kinds = {c.get("type") for c in cases} + for needed in ("should_trigger", "should_not_trigger", "quality"): + if needed not in kinds: + errors.append(f"{evals}: no '{needed}' cases") + for c in cases: + if c.get("type") == "quality" and not c.get("expected_behavior"): + errors.append(f"{evals}: quality case without expected_behavior: {c.get('prompt')}") + for manifest in (".claude-plugin/plugin.json", ".claude-plugin/marketplace.json"): + try: + json.loads((ROOT / manifest).read_text()) + except Exception as e: + errors.append(f"{manifest}: {e}") + link = re.compile(r"\]\((?!https?:|#|mailto:)([^)#\s]+)") + for md in ROOT.rglob("*.md"): + if ".git" in md.parts: + continue + for target in link.findall(md.read_text()): + if not (md.parent / target).exists(): + errors.append(f"{md}: broken link '{target}'") + print("\n".join(errors) or "ok") + sys.exit(1 if errors else 0) + EOF skills-sh: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abf192..a209fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes are listed here. The format follows [Keep a Changelog](https ## [Unreleased] +### Changed +- MCP distribution facts (2026-11-02): Official MCP Registry `_meta` 4 KB limit and npm version timing, Glama listing claim, awesome-mcp-servers requirements, Claude plugin directory rules. +- MCP distribution facts (2026-11-03): Glama build config and stale listings, awesome-mcp-servers bot and entry rules, Claude plugin preparation. +- Retired platforms (2026-11-05): one table in `references/mcp-registry-checklist.md`; guidance for retired platforms removed. +- Claude plugin directory (2026-11-11): the file types and components the directory accepts from an organisation that is not Verified, and commands that list the files the directory is likely to refuse. +- Claude plugin directory (2026-11-05): a "Decide first" check for plugins that return third-party personal data, keep plugin files when not submitting, and two more documented holds (brand name, MCP server command). + +### Added +- `references/audit-checks.md`: the repo audit and the listing checks as read-only GitHub CLI commands and tables. The audit flags files and README lines that name a retired platform. The listing checks cover the Official MCP Registry entry, its npm or Docker Hub package, Glama and awesome-mcp-servers. + +### Removed +- `skills/github-seo/scripts/check_repo_seo.py`, `scripts/validate_skill.py` and the SVG assets. The Claude plugin directory accepts only Markdown, text, JSON, YAML and PNG, JPEG, GIF, WebP or ICO files from an organisation that is not Verified. CI runs the validator checks inline. + ## [0.2.0] - 2026-11-02 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74b9dd0..cc44a31 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Thanks for helping. The most valuable contribution is a correction or addition b 1. Fork and branch from `main`. 2. Make the change. -3. Run `python3 scripts/validate_skill.py`. It must print `ok`. +3. CI checks the skill frontmatter, the evals, the manifests and the internal links on each pull request. 4. If you changed when the skill should trigger, update `skills/github-seo/evals/evals.json`. 5. Add a line to `CHANGELOG.md` under an Unreleased heading. 6. Open a pull request and fill in the template. diff --git a/README.md b/README.md index 81fba51..75bd22c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ GitHub SEO Skill is an Agent Skill for Claude Code that gets open-source GitHub repos found on Google and GitHub search. It audits a repo's name, description, topics and README, writes a search-ready README, runs a pre-publish secret and client-data check, and walks MCP servers -through registry submissions such as the Official MCP Registry, Smithery, Glama and mcp.so. +through registry submissions such as the Official MCP Registry, Glama and mcp.so. It works with any tool that supports the [Agent Skills](https://agentskills.io) format. @@ -44,7 +44,7 @@ Then ask in plain words, for example "audit github.com/you/your-repo for search | Stage | What you get | |---|---| | Safety pass | Full-history secret scan and client-data check before anything goes public | -| Audit | A read-only script scores the description, topics, homepage, license, README shape and community files | +| Audit | Read-only GitHub CLI checks score the description, topics, homepage, license, README shape and community files | | README | An eight-section template aimed at the search snippet, 400 to 700 words | | Metadata | Repo slug, About text, topics, Website field and social preview guidance | | Launch | A tested order: repo, write-up, community, wider channels, template galleries | @@ -52,19 +52,17 @@ Then ask in plain words, for example "audit github.com/you/your-repo for search ## Run the audit yourself -Needs the [GitHub CLI](https://cli.github.com) logged in. The script changes nothing. +Needs the [GitHub CLI](https://cli.github.com) logged in. The checks change nothing. -```bash -python3 skills/github-seo/scripts/check_repo_seo.py owner/repo -``` - -It prints `PASS`, `WARN` or `FAIL` per check and exits non-zero on any `FAIL`. The social preview -image cannot be read or set through the CLI, so it is listed as a manual step. +Follow [audit-checks.md](skills/github-seo/references/audit-checks.md): four `gh` commands read the +repo, and a table gives pass, warn or fail for each check. The social preview image cannot be read +or set through the CLI, so it is listed as a manual step. ## Network access and data -This repo collects no data and runs no server. The audit script only calls the GitHub CLI (`gh`) -you have logged in, using read-only requests. When the skill guides an MCP server submission, it +This repo collects no data and runs no server. The audit only calls the GitHub CLI (`gh`) +you have logged in, using read-only requests. The listing checks read public pages of the Official +MCP Registry, npm, Docker Hub and Glama. When the skill guides an MCP server submission, it tells you to look up a server on the Official MCP Registry (`https://registry.modelcontextprotocol.io`), and the publishing steps use the `mcp-publisher` tool, which sends your server metadata to that registry. Nothing is sent unless you run those steps. @@ -82,9 +80,8 @@ The skill is built to repeat those labels when it reports back, instead of prese ## Repository layout ``` -skills/github-seo/ # the skill: SKILL.md, references/, scripts/, evals/ +skills/github-seo/ # the skill: SKILL.md, references/, evals/ .claude-plugin/ # plugin and marketplace manifests -scripts/validate_skill.py # checks frontmatter, evals, manifests and links (runs in CI) ``` ## Related projects diff --git a/SECURITY.md b/SECURITY.md index f952944..2bd5e14 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,10 +5,10 @@ Please do not open a public issue. Use GitHub's private reporting instead: [Report a vulnerability](https://github.com/devkindhq/github-seo-skill/security/advisories/new). -Relevant reports include the audit script running unintended commands, or skill instructions that +Relevant reports include the audit checks running unintended commands, or skill instructions that could lead an agent to leak secrets or publish private data. ## Scope -This project contains documentation and one read-only Python script that calls the GitHub CLI. +This project contains documentation and read-only commands that call the GitHub CLI. It has no runtime service and stores no data. diff --git a/assets/icon.svg b/assets/icon.svg deleted file mode 100644 index 6546089..0000000 --- a/assets/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - SEO - GITHUB - diff --git a/assets/social-preview.svg b/assets/social-preview.svg deleted file mode 100644 index bd2fb06..0000000 --- a/assets/social-preview.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - AGENT SKILL FOR CLAUDE CODE - GitHub SEO Skill - Get your open-source repo found, safely. - - Repo audit - Search-ready README - Secret scan - MCP registries - - - github.com/devkindhq/github-seo-skill - Devkind - diff --git a/scripts/validate_skill.py b/scripts/validate_skill.py deleted file mode 100644 index 10345e5..0000000 --- a/scripts/validate_skill.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the skill, its evals, the plugin manifests and internal markdown links. Stdlib only.""" -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -errors = [] - - -def err(msg): - errors.append(msg) - - -for skill_md in sorted(ROOT.glob("skills/*/SKILL.md")): - d = skill_md.parent - m = re.match(r"^---\n(.*?)\n---\n", skill_md.read_text(), flags=re.S) - if not m: - err(f"{skill_md}: missing frontmatter") - continue - fm = dict( - line.split(":", 1) for line in m.group(1).splitlines() if ":" in line and not line.startswith(" ") - ) - fm = {k.strip(): v.strip() for k, v in fm.items()} - name = fm.get("name", "") - if name != d.name: - err(f"{skill_md}: name '{name}' must equal directory '{d.name}'") - if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name): - err(f"{skill_md}: name must be lowercase-hyphenated") - desc = fm.get("description", "") - if not desc: - err(f"{skill_md}: description is empty") - elif len(desc) > 1024: - err(f"{skill_md}: description is {len(desc)} chars, limit 1024") - - evals = d / "evals" / "evals.json" - if not evals.exists(): - err(f"{d}: missing evals/evals.json") - else: - cases = json.loads(evals.read_text()).get("cases", []) - kinds = {c.get("type") for c in cases} - for needed in ("should_trigger", "should_not_trigger", "quality"): - if needed not in kinds: - err(f"{evals}: no '{needed}' cases") - for c in cases: - if c.get("type") == "quality" and not c.get("expected_behavior"): - err(f"{evals}: quality case without expected_behavior: {c.get('prompt')}") - -for manifest in (".claude-plugin/plugin.json", ".claude-plugin/marketplace.json"): - try: - json.loads((ROOT / manifest).read_text()) - except Exception as e: # noqa: BLE001 - err(f"{manifest}: {e}") - -link = re.compile(r"\]\((?!https?:|#|mailto:)([^)#\s]+)") -for md in ROOT.rglob("*.md"): - if ".git" in md.parts: - continue - for target in link.findall(md.read_text()): - if not (md.parent / target).exists(): - err(f"{md.relative_to(ROOT)}: broken link '{target}'") - -if errors: - print("\n".join(errors)) - sys.exit(1) -print("ok") diff --git a/skills/github-seo/SKILL.md b/skills/github-seo/SKILL.md index 542046d..79cb144 100644 --- a/skills/github-seo/SKILL.md +++ b/skills/github-seo/SKILL.md @@ -1,6 +1,6 @@ --- name: github-seo -description: Makes an open-source GitHub repo discoverable and safe to publish. Audits the repo name, description, topics and README for search, writes a search-ready README, runs a pre-publish secret and client-data check, and guides the launch sequence. Covers MCP server registry submissions (Official MCP Registry, Smithery, Glama, mcp.so). Use when the user wants more stars, traffic or search visibility for a repo, is about to open-source a project, asks for GitHub SEO or repo SEO, or wants to publish an MCP server. Not for general website SEO. +description: Makes an open-source GitHub repo discoverable and safe to publish. Audits the repo name, description, topics and README for search, writes a search-ready README, runs a pre-publish secret and client-data check, and guides the launch sequence. Covers MCP server registry submissions (Official MCP Registry, Glama, mcp.so) and the Claude plugin directory. Use when the user wants more stars, traffic or search visibility for a repo, is about to open-source a project, asks for GitHub SEO or repo SEO, or wants to publish an MCP server. Not for general website SEO. license: MIT --- @@ -44,14 +44,10 @@ Details and the publish-or-not framework: `references/publish-safety.md`. ## Step 2: audit what exists -Run the read-only audit. It needs the GitHub CLI (`gh`) logged in. +Run the read-only repo audit in `references/audit-checks.md`. It needs the GitHub CLI (`gh`) logged in. -```bash -python3 scripts/check_repo_seo.py owner/repo -``` - -It reports pass, warn or fail for the description, topics, homepage, license, README shape, -and community files, and prints the one lever it cannot check (the social preview image). +It gives pass, warn or fail for the description, topics, homepage, license, README shape, +community files and retired-platform leftovers, and names the one lever no command can check (the social preview image). Fix failures first, then warnings. ## Step A: plain repo checklist @@ -78,19 +74,21 @@ Fix failures first, then warnings. produces the artifact the next needs. 9. **Upstream galleries and awesome-lists** only once the repo is complete and has real stars. Maintainers reject cold, thin submissions. -10. **Claude plugin directory.** If the repo ships a Claude plugin, follow - `references/claude-plugin-directory.md` for the submission portal, Validate findings and - the other plugin distribution routes. +10. **Claude plugin directory.** If the repo ships or could ship a Claude plugin, start with + "Decide first" in `references/claude-plugin-directory.md`. It says whether to submit, then + covers the submission portal, Validate findings and the other plugin distribution routes. ## Step B: MCP server checklist (after Step A) 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 +plus the `mcp-publisher` CLI), 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). Pitfalls from real runs (org namespace 403, OCI +account, the Glama build config, publish order, domain presence). Pitfalls from real runs (org namespace 403, OCI build and label, leaked-history scrub) are in the install reference. +Submit only to the registries in that checklist, never to one in its "Retired platforms" table. +After you submit, confirm each listing with the listing checks in `references/audit-checks.md`. ## What not to do diff --git a/skills/github-seo/evals/evals.json b/skills/github-seo/evals/evals.json index 574f8bd..d2d1734 100644 --- a/skills/github-seo/evals/evals.json +++ b/skills/github-seo/evals/evals.json @@ -9,7 +9,7 @@ { "type": "should_trigger", "prompt": "what topics and description should I put on this repo so people find it" }, { "type": "should_trigger", "prompt": "plan the launch for our new open source tool, reddit, hn, the lot" }, { "type": "should_trigger", "prompt": "is it safe to make this private repo public" }, - { "type": "should_trigger", "prompt": "submit my mcp server to smithery and glama" }, + { "type": "should_trigger", "prompt": "submit my mcp server to glama and mcp.so" }, { "type": "should_trigger", "prompt": "audit the discoverability of github.com/acme/widget-cli" }, { "type": "should_not_trigger", "prompt": "improve the SEO of my marketing website" }, { "type": "should_not_trigger", "prompt": "write a blog post about our product" }, @@ -34,7 +34,7 @@ "type": "quality", "prompt": "Audit github.com/acme/widget-cli for search visibility", "expected_behavior": [ - "runs scripts/check_repo_seo.py rather than guessing at the repo state", + "runs the repo audit in references/audit-checks.md rather than guessing at the repo state", "reports failures before warnings", "tells the owner the social preview image must be uploaded manually in Settings", "does not add inaccurate topics or keyword-stuff the description" diff --git a/skills/github-seo/references/audit-checks.md b/skills/github-seo/references/audit-checks.md new file mode 100644 index 0000000..3d2f7bb --- /dev/null +++ b/skills/github-seo/references/audit-checks.md @@ -0,0 +1,81 @@ +# Audit checks + +Read-only checks for Step 2 (repo audit) and for after a registry submission (listing checks). +Each `gh` command needs the GitHub CLI logged in. Nothing here changes the repo. + +Report each check as pass, warn or fail. + +## Repo audit + +Read the facts for `owner/repo`: + +```bash +gh repo view owner/repo --json nameWithOwner,name,description,homepageUrl,licenseInfo,repositoryTopics,isPrivate +gh api repos/owner/repo/readme -H "Accept: application/vnd.github.raw" +gh api repos/owner/repo/contents/ --jq '.[].name' +gh api "repos/owner/repo/git/trees/HEAD?recursive=1" --jq '.tree[] | select(.type=="blob") | .path' +``` + +| Check | Fail | Warn | +|---|---|---| +| Description | Empty, or over 350 characters. It is the About text and the search snippet. | Over 120 characters. It can truncate in results. | +| Topics | None set. | Fewer than 5. Add accurate ones. | +| Homepage | | Empty. Point it at the canonical write-up. | +| License | None, or GitHub reports the key `other`. It reads as "do not use this". | | +| README | Missing. | Under 150 words (too thin to rank) or over 700 words (move depth to a write-up). Do not count words in code blocks. | +| README H1 | No H1. | The H1 lacks one or more words of the repo name. Split the name on `-` and `_`, ignore words of 1 or 2 letters, and ignore case. | +| First paragraph | | None after the H1, or over 320 characters. It acts as the search snippet. Skip badges, headings, tables, lists and code. | +| Community files | | `CONTRIBUTING.md` or `SECURITY.md` is missing in the repo root. | +| Retired platforms | A file path or a README line names a platform in the "Retired platforms" table of `mcp-registry-checklist.md`. In a clone, `git grep -n -i ` finds the other mentions. | | + +Two points no command can check: + +- The social preview image cannot be read or set by CLI. Tell the owner: Settings, General, + Social preview (1280 by 640). +- If `isPrivate` is true, the repo is invisible to search until the owner makes it public. + +### When the repo root has a `server.json` + +```bash +gh api repos/owner/repo/contents/server.json -H "Accept: application/vnd.github.raw" +``` + +| Check | Fail | Warn | +|---|---|---| +| `server.json` | Not valid JSON. | | +| `name` | | Does not start with `io.github./`. This is fine only with domain auth. | +| `description` | Empty, or over 100 characters. | | +| `websiteUrl` | | Empty. Set the official homepage field. | +| OCI package, when `packages` has one | The root `Dockerfile` is missing, or lacks `LABEL io.modelcontextprotocol.server.name` with the same value as `name`. | | +| npm package, when `packages` has one | `mcpName` in the root `package.json` is missing or differs from `name`. | | + +## Listing checks + +Run these after a registry submission. Read `name` from `server.json` first. A repo with no +`server.json` has no MCP listing to check. + +Fetch each URL and read the HTTP status and the JSON body. A status that the table does not name, +or no response, is a fail: + +| Listing | URL | Pass | Otherwise | +|---|---|---|---| +| Official MCP Registry | `https://registry.modelcontextprotocol.io/v0/servers//versions/latest` | 200. Note `server.version` and `server.packages`. | Fail: not found. | +| npm package (for each npm entry in `server.packages`) | `https://registry.npmjs.org//latest` | 200, and `version` equals the version of that package entry, or `server.version` when the entry has none. | Warn: the versions differ. Fail: not found. | +| Docker Hub image (for each OCI entry on Docker Hub) | `https://hub.docker.com/v2/repositories//tags/` | 200. Take `` and `` from the identifier without the `docker.io/` prefix, split at `:`. With no tag, use `latest`. With no `/` in the image, put `library/` before it. | Fail: the tag does not exist. Warn: the image is on another host (the first part of the image name has a dot), so check it by hand. | +| Glama | `https://glama.ai/mcp/servers/owner/repo` | 200. | Warn on 404: Glama can list a server under another name, so search glama.ai by hand. | + +Check the registry packages, not the `server.json` in the repo, so that the result matches what +clients install. + +awesome-mcp-servers: + +```bash +gh api repos/punkpeye/awesome-mcp-servers/contents/README.md -H "Accept: application/vnd.github.raw" | grep -i -c "\[owner/repo\]" +gh pr list --repo punkpeye/awesome-mcp-servers --state all --search "owner/repo" --json number,state +``` + +A count of 1 or more means the list has the repo. If the count is 0, report it as a warn with the +state of each pull request found. + +Directories that need a login or have no API stay a manual check: the Claude plugin directory, +mcp.so, mcpservers.org and Cursor Directory. diff --git a/skills/github-seo/references/claude-plugin-directory.md b/skills/github-seo/references/claude-plugin-directory.md index 193027b..f63f070 100644 --- a/skills/github-seo/references/claude-plugin-directory.md +++ b/skills/github-seo/references/claude-plugin-directory.md @@ -4,17 +4,93 @@ Generalised from one real submission at `claude.ai/directory/manage` on 2026-11- [observed, 2026-11-02]. The portal and its checks change, so re-check each step before repeating this. Anything not seen during the run is marked [unverified]. +## Decide first + +Answer three questions before any plugin work. Questions 1 and 2 are from the Anthropic Software +Directory Policy 1D and 3A [verified, 2026-11-05]. Question 3 is from the portal [observed, 2026-11-11]: + +1. Does the plugin collect user data or connect to a remote service? Then it needs "a clear, + accessible privacy policy link". Without one, do not submit yet. +2. Does it collect or return personal data about people other than the user, such as emails or + profiles? Then tell the owner it may be refused even with a privacy policy, because software + must protect "the privacy interests of third parties". +3. Is the submitting organisation Verified? If not, the directory refuses each file in the plugin + folder that is not Markdown, text, JSON, YAML or an image, and each file that starts with `#!`. + Read "Organisations that are not Verified" before you prepare the plugin. + +Submit or skip is the owner's call. + +If the answer is skip, change no files: the directory reads only what you submit. The plugin files +(`.claude-plugin/`, `.mcp.json`, plugin skills) still serve everyone who installs from the repo, +through its own `marketplace.json` or `/plugin marketplace add owner/repo`. Keep them unless the +owner wants the plugin gone everywhere. To find other marketplaces that list the repo: + +```bash +gh search code "owner/repo" --filename marketplace.json +``` + +The search finds only marketplaces in repos you can read, so zero results do not prove that +nothing depends on the files. + +## Organisations that are not Verified + +The portal shows these rules. The public docs do not state them, so re-check them before each submission. +Unless a line has its own label, the source is one refused version and one draft validation +[observed, 2026-11-11]. + +- **Refused now:** a plugin folder with a file that, "whatever its name", is not Markdown, text, + JSON, YAML, or a PNG, JPEG, GIF, WebP or ICO image. Also refused: a file that starts with `#!`, + a package manager configuration file, and a text file that is not UTF-8 or has NUL bytes. A + license, a readme, `.gitignore` and the icon can stay if they are UTF-8 text or an allowed + image and do not start with `#!`. +- SVG is not on the portal's list. The public checklist permits SVG [verified, 2026-11-11, + claude.com/docs/plugins/pre-submission-checklist.md]. Use a PNG [inference]. +- **Marked "not refused yet":** a local MCP server, and `package.json` fields outside the + portal's list, such as `mcpName`. For a mod, the portal says that later only Verified + organisations can submit one. +- Other teams report that the directory already refuses local MCP servers, mods and command hooks + [observed, 2026-11-11, third-party reports in + [anthropics/claude-plugins-official#7024](https://github.com/anthropics/claude-plugins-official/issues/7024)]. +- The portal says "Verified means Anthropic knows your organization is a real company". No way to + apply was found, and the teams in that issue report none [observed, 2026-11-11]. +- Validate on a draft can report a rule as "not refused yet" while the scan after submission + refuses another plugin for that rule. Do not treat a clean Validate as proof [inference]. + +List the files the directory is likely to refuse. Run this in the plugin folder: + +```bash +git ls-files | grep -v -i -E '\.(md|txt|json|ya?ml|png|jpe?g|gif|webp|ico)$' | grep -v -E '(^|/)(LICENSE|\.gitignore)$' +git ls-files -z | xargs -0 awk 'FNR==1 && /^#!/ {print FILENAME}' +``` + +The commands go by file name and first line. They do not check package manager configuration +files, text encoding or NUL bytes. + +If a script is only for CI, move its checks into the workflow file. If a skill runs a script, +write the checks as commands in a reference file. For a plugin in a subfolder, the directory +"reads and scans only that folder" [verified, 2026-11-11, claude.com/docs/plugins/submit.md]. A +few validation checks still read the whole repo, such as the size limits and `.gitattributes` +[verified, 2026-11-11, claude.com/docs/plugins/pre-submission-checklist.md]. + ## Prerequisites -- A paid claude.ai plan [observed, 2026-11-02]. -- A public GitHub repo with `.claude-plugin/plugin.json` [observed, 2026-11-02]. -- A GitHub account with push access to the repo, used by the submitter [observed, 2026-11-02]. +- A paid claude.ai plan [observed, 2026-11-02]. On Team and Enterprise, the submitter needs the + Owner role or, on Enterprise, a custom role with the Directory permission [verified, 2026-11-02, + claude.com/docs/directory/publish.md]. +- A GitHub repo with `.claude-plugin/plugin.json` [observed, 2026-11-02]. The repo can stay + private while you validate and submit, but must be public before the listing goes live + [verified, 2026-11-02, claude.com/docs/plugins/submit.md]. +- A GitHub account with push access to the repo, used by the submitter [observed, 2026-11-02], + and connected on claude.ai in the submitting organisation [verified, 2026-11-02, same page]. +- The listing belongs to the organisation you submit from. Submit from the organisation that + should own it long term [verified, 2026-11-02, claude.com/docs/directory/publish.md]. - Limits: at most 10 submissions per organisation per 24 hours (drafts and withdrawn submissions count), and one submission per repo and folder [observed, 2026-11-02]. ## The five steps -1. **Source.** Point the portal at the repo and folder. +1. **Source.** In the portal, select Submit new, then Plugin bundle [verified, 2026-11-02, + claude.com/docs/plugins/submit.md]. Point the portal at the repo and folder. 2. **Listing details.** Name, description, icon and related listing fields. 3. **Data handling.** Questions on personal data, whether the plugin sends data to other services (declare this in the README), retention, and users under 18 [observed, 2026-11-02]. @@ -24,21 +100,61 @@ repeating this. Anything not seen during the run is marked [unverified]. 5. **Review and submit.** An automated scan runs first. Held items go to a human reviewer. Approval is not guaranteed, and no review-time estimate was shown [observed, 2026-11-02]. +## Prepare the plugin + +- The plugin folder is the folder that holds `.claude-plugin/plugin.json`, and installers get that + whole folder [verified, 2026-11-03, claude.com/docs/plugins/pre-submission-checklist.md]. If it + is the root of a repo that also holds the server's `package.json` and `yarn.lock`, every install + shows a note that the listed packages were not installed, because plugin installs do not support + yarn or pnpm lockfiles [observed, 2026-11-03, `claude plugin list`]. A server that runs from + npm through `npx` never uses those packages, so the note does not affect it [general]. +- Set `displayName`, and set `icon` to a path inside the plugin, such as ./assets/icon.png. The + directory reads `icon`, `documentationUrl`, `supportUrl`, `privacyPolicyUrl` and + `termsOfServiceUrl` from `plugin.json` only [verified, 2026-11-03, + code.claude.com/docs/en/plugins-reference]. +- Add a README "Network access and data" section: what the plugin downloads, which hosts it calls, + what each request carries and what it stores. The checklist asks the README to describe + everything the plugin runs, sends or fetches [verified, 2026-11-03, pre-submission checklist]. +- Run `claude plugin validate .` in the plugin folder before you push. It checks syntax and schema + only, so the portal's Validate still runs [verified, 2026-11-03, pre-submission checklist]. + ## Validate Validate runs 7 checks and reports three kinds of result: Blocking findings, policy holds and -warnings [observed, 2026-11-02]. It is commit-specific, so re-run it after every push that +warnings [observed, 2026-11-02]. After submission, the scan can also refuse a version by directory +policy (see "Organisations that are not Verified"). Validate is commit-specific, so re-run it after every push that touches a fix [observed, 2026-11-02]. | Trigger seen | Result | Fix that cleared it | |---|---|---| -| A doc telling users to run `$(gh auth token)` | Policy hold, "Uses a credential from the user's machine" | Tell users to supply their own token | +| A doc that told users to reuse the token of their GitHub CLI login in a command | Policy hold, "Uses a credential from the user's machine" | Tell users to supply their own token | | An `npx ... add` install line in the README | Non-blocking "download-and-execute" warning (cause inferred, [unverified]) | Add a README "Network access and data" section | -| Missing `.claude-plugin/icon.png` | Warning | Add the icon | +| No PNG icon in `.claude-plugin/` | Warning | Add the icon | | No display name or marketplace description | Listing fields incomplete | Add `displayName` in `plugin.json` and `description` in `marketplace.json` | All rows [observed, 2026-11-02]. +### Documented rules for packages and files + +From claude.com/docs/plugins/pre-submission-checklist.md [verified, 2026-11-02; the brand row +2026-11-05; the MCP server command row 2026-11-09]. The launcher rule lists `npx`, `bunx`, `pnpm dlx`, `yarn dlx`, +`uvx`, `pipx run` and `uv run` only. + +| Rule | Result if broken | +|---|---| +| Pin every package a launcher runs to an exact version, such as `npx @1.2.3` | Blocks, "Unpinned npx launcher" | +| A launcher with an exact pin | Still held for a reviewer, "Runs a pinned npx or uvx package" | +| `.npmrc` or another file that sets a registry or package source, in a plugin with a launcher | Blocks | +| `package.json` beside `package-lock.json`, `npm-shrinkwrap.json`, `bun.lock` or `bun.lockb` in the plugin folder root | Held for a reviewer, "Dependencies install from a lockfile". `yarn.lock` is not on the list | +| `name`, `displayName` or `author.name` could be mistaken for a well-known brand that is not yours | Held for a reviewer, "Name matches a known brand" | +| Start each local MCP server by running a file in the plugin with plain arguments, not through a shell or a package-manager script such as `npm run` | Held for a reviewer, "MCP server command wasn't read" | +| Keep every file that is not an image or font under 256 KiB | Held for a reviewer | +| Keep the plugin to 512 files or fewer | Held for a reviewer | +| Do not write the path of a bundled image or font in backticks or a code block, and do not refer to it from a command, hook or script | Held for a reviewer, "Image or font file that the plugin's code could run" [verified, 2026-11-11, same page] | + +The checklist does not name `docker run`. A `docker run` command does not run a file in the plugin, +so expect the same "MCP server command wasn't read" hold [inference, 2026-11-09]. + ### Scanner false positives (second submission, 2026-11-02) Two more plugins went through Validate on 2026-11-02. The scanner reads text in skills and @@ -46,7 +162,7 @@ READMEs, not only code, so documentation can trigger holds [observed, 2026-11-02 | Trigger seen | Result | What happened | |---|---|---| -| A skill doc example reading `process.env.X_API_KEY` next to a `fetch()` | Two policy holds, "Uses a credential from the user's machine" | Cleared after the example read was rewritten as pseudocode and the variable renamed [observed, 2026-11-02] | +| One skill doc example that read an API key from an environment variable, next to a network call | Two policy holds, "Uses a credential from the user's machine" | Cleared after the example read was rewritten as pseudocode and the variable renamed [observed, 2026-11-02] | | The English word "pass" in skill prose, such as "a later pass can reference" | Same credential hold, worded as reading the installer's "pass" (the `pass` password manager), paired with a documented `curl` upload | Rewording one instance surfaced the next. The scanner reports one hit per run, and the word appears in dozens of skill bodies. Left for reviewer confirmation [observed, 2026-11-02] | | Many example images (66 files) in the repo | Policy hold, "Image or font file that the plugin's code could run" | Not fixable without removing or relocating the images, so the plugin goes to a reviewer [observed, 2026-11-02] | | Docs saying to run a `curl` command that a tool returns, and some PNG files | Non-blocking "download-and-run" warning | Doc-only mentions need no change per the portal. Only PNGs were flagged, not the WebP files. Converting five flagged PNGs to WebP was tried; whether that clears the warning is [unverified] | @@ -73,3 +189,9 @@ The review step offers an auto-publish toggle. Two update modes were shown [obse - `anthropics/claude-plugins-official` is by partner contact only [observed, 2026-11-02]. - Your own `marketplace.json` lets users run `/plugin marketplace add /` [general]. - A skills.sh listing is automatic, built from `npx skills add` telemetry [observed, 2026-11-02]. +- `/plugin install --marketplace /` adds the marketplace and installs in one + step. It needs Claude Code v2.1.275 or later [verified, 2026-11-02, + code.claude.com/docs/en/plugins/publish.md]. +- The directory no longer accepts desktop extension (MCPB) listings. Ship a local MCP server + inside a plugin bundle instead [verified, 2026-11-02, claude.com/docs/directory/publish.md]. For an + organisation that is not Verified, read "Organisations that are not Verified" first. diff --git a/skills/github-seo/references/mcp-install-and-metadata.md b/skills/github-seo/references/mcp-install-and-metadata.md index d85fbb7..25eb1ed 100644 --- a/skills/github-seo/references/mcp-install-and-metadata.md +++ b/skills/github-seo/references/mcp-install-and-metadata.md @@ -20,6 +20,13 @@ mismatch is the most common publish failure. | 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) | +npm timing: the registry checks that the exact version exists on npm. Right after `npm publish` +it can reject the version with "NPM package '...' exists, but version 'X' was not found (status: +404). A newly published release can take a moment to appear..." [verified, 2026-11-02, registry +`internal/validators/registries/npm.go`]. Publish to the registry only after +`npm view @` returns the version. The version became visible about one minute +after publish [observed on a Devkind npm MCP server, 2026-11-02]. + 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 @@ -71,11 +78,18 @@ Schema `2025-12-11` [verified]: | `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`. | +| `_meta` | Vendor extension | Only `io.modelcontextprotocol.registry/publisher-provided` is kept, max 4096 bytes of JSON. Put author 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. +`_meta` limit [verified, 2026-11-02, registry `internal/validators/validators.go` +`validatePublisherExtensions` and `docs/reference/server-json/official-registry-requirements.md`]: +the registry drops every other `_meta` key, and fails the publish when the publisher-provided +object exceeds 4 KB, with "publisher-provided extension exceeds 4KB limit (N bytes)". A full +tool-name list can pass that limit. A 125-tool server failed at 4474 bytes [observed on a Devkind +npm MCP server, 2026-11-02]. Keep the tool list out of `_meta` for a large server. + Environment variables go in `packages[].environmentVariables` with `name`, `description`, `isRequired` and `isSecret`. Names only, never values. @@ -125,20 +139,16 @@ server needs them. | 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. | -`smithery.yaml` with a docker `commandFunction` was copied from another published server README and is -[unverified] against Smithery's current docs. Check them before reusing it. - 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`, +- Same organisation name and the same homepage URL in `server.json` (`websiteUrl`), `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. @@ -190,4 +200,4 @@ directory that renders the README. Keep it specific to the server's audience, no - One link to a specific page on a domain you own (a services, consulting or product page, not only the home page) plus a contact address. - Request each linked URL first and confirm it returns 200. -- Use the same organisation name and homepage as in `server.json`, `smithery.yaml` and the About box. +- Use the same organisation name and homepage as in `server.json` and the About box. diff --git a/skills/github-seo/references/mcp-registry-checklist.md b/skills/github-seo/references/mcp-registry-checklist.md index 2125fee..3cd6663 100644 --- a/skills/github-seo/references/mcp-registry-checklist.md +++ b/skills/github-seo/references/mcp-registry-checklist.md @@ -32,32 +32,107 @@ Write the fields every listing asks for, then reuse them verbatim so listings st |---|---|---| | **Official MCP Registry** | A `server.json` in your project, published with the `mcp-publisher` CLI. Names take the form `io.github./` and you must authenticate with access to that namespace [verified, 2026-10-29, registry README]. For an org namespace the device flow is not enough, see `mcp-install-and-metadata.md` section 2 [verified, 2026-11-02]. A pull request to the registry repo does not publish a server [verified, 2026-10-29]. | Direct. Do this first, because other registries ingest from it. | | **Docker MCP Registry** | PR adding `servers//server.yaml`, Docker team review [verified, 2026-11-02]. | Optional second route, see `mcp-install-and-metadata.md` section 8. | -| **Smithery** | Listed under an organisation namespace. Shows a quality score that depends on the live tool surface [observed]. | Direct. Check for stale listings under a personal account. | | **PulseMCP** | Ingests from the Official Registry [observed]. | None once the Official entry is live. | | **mcpbench.ai** | Indexes from the Official Registry, can lag [observed]. | Verify it appears. | -| **Glama** | Manual submission, goes to review [observed]. | Direct, then wait. | +| **Glama** | Manual submission, goes to review [observed]. A listing can exist before you submit [observed, 2026-11-02]. Approval comes by email [observed, 2026-11-03]. The listing gets tools and a score only after a Glama build passes [observed, 2026-11-03]. | Search Glama first. Claim an existing listing (see Gotchas), else submit with Add Server and wait for the email. Then set the build (see Gotchas). | | **mcp.so** | Manual submission with name, icon and server config, human approval [observed]. | Direct, then wait. | | **mcpservers.org** | Free manual listing, category required, email approval [observed]. | Direct, then wait. | | **Cursor Directory** | Manual submission with an icon upload [observed]. | Direct. | | **MCPMarket** | Observed to index without a submission form [observed]. | Verify the listing and description. | | **Community lists** (selective, unclear criteria) | Small subsets of servers, links may be nofollow [observed]. | Low priority. | +## Retired platforms + +Do not submit to these. Remove their config files, scripts and doc mentions, but keep released +changelog entries. The Step 2 audit +(`audit-checks.md`) uses this table and flags any file path or README line that names +a platform here. To retire a platform, add a row. + +| Platform | Status | +|---|---| +| **Smithery** | Out of service. Its hosting, API, accounts, CLI and SDKs shut down on 31 Oct 2026. Only a read-only directory remains, with nothing to publish [verified, 2026-11-05, smithery.ai/future]. | + ## 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. + GitHub account before the org account existed, that listing can persist on other registries. Find it + and have it removed, through the registry's support if there is no self-service delete. +- **Unclaimed Glama listing.** An npm-published server with a private repo already had an + unclaimed Glama page with an older README [observed on a Devkind npm MCP server, 2026-11-02]. + The claim box on a Glama server page says access comes from the repo, with no form + [observed, 2026-11-03, Glama server page claim box]: + - The owner of a personal repo signs in as that GitHub account. The page becomes theirs at once. + - For an org-owned repo, add `glama.json` at the repo root with your GitHub username under + `maintainers`, then sign in. Glama recognises you on the next sync. + + ```json + {"$schema": "https://glama.ai/mcp/schemas/server.json", "maintainers": ["github-username"]} + ``` + + After you change `glama.json`, do the Claim flow again [verified, 2026-11-02, + https://glama.ai/blog/2025-07-08-what-is-glamajson]. +- **Glama build config.** Glama builds its own image from the settings on the server page under + Admin, Dockerfile. The repo needs no Dockerfile for this [observed, 2026-11-03]. Glama generated + `pnpm install` build steps for a repo that pins `"packageManager": "yarn@1.22.22"`, and the build + failed with "This project is configured to use yarn" [observed, 2026-11-03]. Set the build steps + for the repo's own package manager, for example + `["npm install -g yarn@1.22.22", "yarn install --frozen-lockfile", "yarn build"]`, and the CMD + to the stdio entry point, for example `["mcp-proxy", "--", "node", "dist/index.js"]`. Check the + generated environment schema as well: one set `TRANSPORT` to `http` for a server whose default + is stdio [observed, 2026-11-03]. Use placeholder values only, never real keys. +- **Stale Glama listing.** The summary, tool list and score come from the latest Glama release, + not from the repo [observed, 2026-11-03]. An old README or tool count after a sync usually means + the build fails. Fix the build, then run Build & Release. Auto-Release then builds on every + GitHub release [observed, 2026-11-03]. Glama numbers its releases on its own, so they do not + match npm versions [observed, 2026-11-03]. - **Publish order.** Merge pull requests that change the tool surface before publishing to the - Official Registry. The `server.json` tool list must match what ships, and some registries + Official Registry. If `server.json` lists tools, that list must match what ships, and some registries recompute a quality score from the live tool count. - **Domain presence compounds** [general]. Several distinct root domains linking to the project is likely a stronger signal than one domain repeating. If you control more than one property, mention the server on each. +## awesome-mcp-servers + +`punkpeye/awesome-mcp-servers` needs a Glama listing first. Its CI +(`.github/workflows/check-glama.yml`) and `CONTRIBUTING.md` require the following +[verified, 2026-11-02, commit 29f2d49]: + +- A public GitHub repo. A remote-only server goes to `punkpeye/awesome-remote-mcp-servers`. +- The entry name in `owner/repo` form, linked to the GitHub repo. The CI message: "The entry + name must use the full `owner/repo` format (not just the repo name)." +- A Glama score badge in the entry. Without it, CI adds the label `missing-glama`: + `[![owner/repo MCP server](https://glama.ai/mcp/servers/owner/repo/badges/score.svg)](https://glama.ai/mcp/servers/owner/repo)` +- At least one permitted emoji and no other emoji. 🎖️ marks an official implementation only. +- Alphabetical order within the category. + +The list's bot and recent merges add these rules: + +- The Glama listing must pass Glama's checks, not only show a badge. The bot asks that the server + "passes all checks" and that it "start and respond to introspection requests", which is the Glama + build above [verified, 2026-11-03, bot comment on a closed PR]. +- One server per PR. Nearly every merged PR adds one line [observed, 2026-11-03]. +- The scope emoji follows the note under the README legend: ☁️ "when MCP server is talking to + remote APIs", 🏠 "when MCP server is talking to a locally installed software" [verified, + 2026-11-03, README legend note]. +- Put the badge right after the repo link, as the existing entries do [observed, 2026-11-03]. +- `CONTRIBUTING.md` sets no star minimum. The Glama checks are the gate for this list + [verified, 2026-11-03, CONTRIBUTING.md]. + +Order: make the repo public, get the Glama listing, then open the awesome-list PR. + ## After submission Most manual registries have a review queue and no way to speed it up. Track pending reviews in a note and check back, rather than resubmitting. + +Check the listings with the listing checks in `audit-checks.md`. They need `gh` logged in and read `server.json` from the repo. + +They find the latest Official MCP Registry entry and check the package it names: the npm version +matches, or the OCI image tag exists on Docker Hub. They also check that the Glama page exists (a 404 +is a warning, because Glama can list a server under another name), and whether awesome-mcp-servers +lists the repo or has a pull request for it. Directories that need a login, such as the Claude +plugin directory, stay a manual check. diff --git a/skills/github-seo/scripts/check_repo_seo.py b/skills/github-seo/scripts/check_repo_seo.py deleted file mode 100644 index e65178d..0000000 --- a/skills/github-seo/scripts/check_repo_seo.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -"""Read-only GitHub repo SEO audit. Usage: check_repo_seo.py [owner/repo] - -Needs the GitHub CLI (`gh`) logged in. Makes no changes. Exits 1 if any check fails. -""" -import base64 -import json -import re -import subprocess -import sys - -results = [] - - -def gh(*args): - out = subprocess.run(["gh", *args], capture_output=True, text=True) - if out.returncode != 0: - sys.exit(f"gh {' '.join(args)} failed: {out.stderr.strip()}") - return out.stdout - - -def record(level, name, detail): - results.append((level, name, detail)) - - -target = sys.argv[1:2] -meta = json.loads( - gh( - "repo", "view", *target, "--json", - "nameWithOwner,name,description,homepageUrl,licenseInfo,repositoryTopics,isPrivate", - ) -) -slug = meta["nameWithOwner"] -name = meta["name"] - -# Description -desc = (meta["description"] or "").strip() -if not desc: - record("FAIL", "description", "empty. This is the About text and the search snippet.") -elif len(desc) > 350: - record("FAIL", "description", f"{len(desc)} chars, over the 350 limit") -elif len(desc) > 120: - record("WARN", "description", f"{len(desc)} chars, may truncate in results (aim for 120 or fewer)") -else: - record("PASS", "description", f"{len(desc)} chars") - -# Topics -topics = [t["name"] for t in (meta["repositoryTopics"] or [])] -if not topics: - record("FAIL", "topics", "none set") -elif len(topics) < 5: - record("WARN", "topics", f"{len(topics)} set ({', '.join(topics)}). Add accurate ones.") -else: - record("PASS", "topics", f"{len(topics)} set") - -# Homepage and license -if meta["homepageUrl"]: - record("PASS", "homepage", meta["homepageUrl"]) -else: - record("WARN", "homepage", "empty. Point it at the canonical write-up.") -lic = meta["licenseInfo"] -if lic and lic.get("key") != "other": - record("PASS", "license", lic["name"]) -else: - record("FAIL", "license", "no recognised license. It reads as 'do not use this'.") - -# README shape -try: - readme = base64.b64decode(json.loads(gh("api", f"repos/{slug}/readme"))["content"]).decode() -except SystemExit: - readme = "" -if not readme: - record("FAIL", "readme", "missing") -else: - words = len(re.findall(r"\w+", re.sub(r"```.*?```", "", readme, flags=re.S))) - if words > 700: - record("WARN", "readme length", f"{words} words. Target 400 to 700; move depth to a write-up.") - elif words < 150: - record("WARN", "readme length", f"{words} words. Probably too thin to rank.") - else: - record("PASS", "readme length", f"{words} words") - - body = re.sub(r"<[^>]+>|!?\[[^\]]*\]\([^)]*\)|\[!?\[.*?\n", "", readme) - h1 = re.search(r"^# (.+)$", body, flags=re.M) - tokens = [t for t in re.split(r"[-_]", name.lower()) if len(t) > 2] - if not h1: - record("FAIL", "readme H1", "no H1 found") - elif tokens and not all(t in h1.group(1).lower() for t in tokens): - record("WARN", "readme H1", f'"{h1.group(1)}" does not contain all words of "{name}"') - else: - record("PASS", "readme H1", h1.group(1)) - - after = body[h1.end():] if h1 else body - first = next((p.strip() for p in re.split(r"\n\s*\n", after) if p.strip() and not p.lstrip().startswith(("#", "|", "```", "-", "[!"))), "") - if not first: - record("WARN", "first paragraph", "none found. It acts as the search snippet.") - elif len(first) > 320: - record("WARN", "first paragraph", f"{len(first)} chars. Keep it to 2 or 3 sentences.") - else: - record("PASS", "first paragraph", f"{len(first)} chars") - -# Community files -tree = json.loads(gh("api", f"repos/{slug}/contents/")) -files = {f["name"].lower() for f in tree} -for label, candidates in [ - ("CONTRIBUTING", {"contributing.md"}), - ("SECURITY", {"security.md"}), -]: - 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}") -print(f"\nMANUAL social preview: cannot be read or set by CLI. Settings > General > Social preview (1280x640) for {slug}.") -if meta["isPrivate"]: - print("NOTE repo is private, so it is invisible to search until it is made public.") -sys.exit(1 if any(l == "FAIL" for l, _, _ in results) else 0)