shipit-skill 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-09-04
4
+
5
+ Initial release.
6
+
7
+ ### Added
8
+
9
+ - **SKILL.md** — one-pass launch pipeline for developer tools: Phase 0 recon,
10
+ Phase 1 engineering baseline, Phase 2 publish, Phase 3 directory listings,
11
+ Phase 4 promo. Includes an automation boundary and a "real-world scars"
12
+ gotcha list.
13
+ - **scripts/**
14
+ - `ci.py` — CI workflow generator (Python with optional MCP-server Docker
15
+ handshake job, TypeScript).
16
+ - `publish.py` — prints build + registry publish commands, runs fresh-install
17
+ verification.
18
+ - `glama.py` — Glama listing/badge check with optional polling.
19
+ - `awesome_pr.py` — recipe for submitting to awesome directories (fork,
20
+ branch, Glama-badge entry, PR).
21
+ - `promo_check.py` — promo freshness check (stale versions / unknown PRs),
22
+ ignores IP-like version fragments.
23
+ - `mcp_smoke.py` — MCP initialize + tools/list smoke test over stdio.
24
+ - **tests/** — 9 pytest cases covering the scripts.
25
+ - **examples/mcp-server-launch.md** — end-to-end walkthrough using a toy MCP
26
+ server.
27
+ - **pyproject.toml** — dev toolchain (pytest, ruff).
28
+ - **.github/workflows/ci.yml** — lint + test.
29
+ - **README.md**, **MIT LICENSE**.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 skyzhao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: shipit-skill
3
+ Version: 0.1.0
4
+ Summary: One-pass launch pipeline Agent Skill for developer tools
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0; extra == "dev"
11
+ Requires-Dist: ruff>=0.4; extra == "dev"
12
+ Requires-Dist: PyYAML>=6.0; extra == "dev"
13
+ Dynamic: license-file
14
+
15
+ # ⚙️ shipit-skill
16
+
17
+ [![CI](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml/badge.svg)](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml)
18
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
19
+
20
+ **Take an existing developer tool from "it works" to "published + listed + marketable" — one pass.**
21
+
22
+ `shipit-skill` is an Agent Skill that runs the full launch pipeline for AI/developer tools
23
+ (MCP servers, CLIs, libraries): engineering baseline → publish → directory listings →
24
+ promo material. It was distilled from shipping [wheel-hub](https://github.com/skyzhao1223/wheel-hub),
25
+ [zspace-cli](https://github.com/skyzhao1223/zspace-cli) and
26
+ [media-manager-skill](https://github.com/skyzhao1223/media-manager-skill) — every gotcha
27
+ below is one that actually bit during those releases.
28
+
29
+ ## Install
30
+
31
+ Copy this folder into your project (or your agent's skills dir):
32
+
33
+ ```bash
34
+ # for the current project
35
+ cp -r shipit-skill/ ~/your-project/.opencode/skills/shipit-skill # opencode
36
+ # cp -r shipit-skill/ ~/your-project/skills/shipit-skill # Claude Code, Cursor, etc.
37
+ ```
38
+
39
+ ## What it does
40
+
41
+ | Phase | Exit criterion | Scripts |
42
+ |-------|----------------|---------|
43
+ | **0. Recon** | gap report (CI? Docker? Release? listed? promo?) | — |
44
+ | **1. Baseline** | CI green, metadata right, Dockerfile present | `scripts/ci.py`, `scripts/mcp_smoke.py` |
45
+ | **2. Publish** | on registry + GitHub Release + clean-env verified | `scripts/publish.py` |
46
+ | **3. Listings** | Glama live + awesome PR open | `scripts/glama.py`, `scripts/awesome_pr.py` |
47
+ | **4. Promo** | promo docs match reality | `scripts/promo_check.py` |
48
+
49
+ See [`SKILL.md`](SKILL.md) for the full agent instructions and the automation
50
+ boundary (what the agent runs vs. what needs a human/credential).
51
+
52
+ ## Example
53
+
54
+ [`examples/mcp-server-launch.md`](examples/mcp-server-launch.md) walks all four
55
+ phases against a toy `hello-mcp` server — reproduce it locally to learn the
56
+ pipeline before applying it to a real project.
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ pip install -e ".[dev]"
62
+ pytest -q # 9 tests
63
+ ruff check . # lint
64
+ ```
65
+
66
+ ## The scars (why this exists)
67
+
68
+ - `mcp>=1.0` + a new SDK → fresh installs crash. Set dependency floors.
69
+ - Python 3.9 can't install `mcp>=2.0` — split the CI matrix.
70
+ - YAML block scalar + inline heredoc = broken workflow. Use a script file.
71
+ - Glama introspection needs stdin held open (`sleep 1`) in smoke tests.
72
+ - PyPI tokens start `pypi-`; UUID-style strings are wrong and 403.
73
+ - npm typosquatting blocks similar names — scoped names dodge it.
74
+ - Glama builds take minutes to hours — poll, don't panic.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,64 @@
1
+ # ⚙️ shipit-skill
2
+
3
+ [![CI](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml/badge.svg)](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ **Take an existing developer tool from "it works" to "published + listed + marketable" — one pass.**
7
+
8
+ `shipit-skill` is an Agent Skill that runs the full launch pipeline for AI/developer tools
9
+ (MCP servers, CLIs, libraries): engineering baseline → publish → directory listings →
10
+ promo material. It was distilled from shipping [wheel-hub](https://github.com/skyzhao1223/wheel-hub),
11
+ [zspace-cli](https://github.com/skyzhao1223/zspace-cli) and
12
+ [media-manager-skill](https://github.com/skyzhao1223/media-manager-skill) — every gotcha
13
+ below is one that actually bit during those releases.
14
+
15
+ ## Install
16
+
17
+ Copy this folder into your project (or your agent's skills dir):
18
+
19
+ ```bash
20
+ # for the current project
21
+ cp -r shipit-skill/ ~/your-project/.opencode/skills/shipit-skill # opencode
22
+ # cp -r shipit-skill/ ~/your-project/skills/shipit-skill # Claude Code, Cursor, etc.
23
+ ```
24
+
25
+ ## What it does
26
+
27
+ | Phase | Exit criterion | Scripts |
28
+ |-------|----------------|---------|
29
+ | **0. Recon** | gap report (CI? Docker? Release? listed? promo?) | — |
30
+ | **1. Baseline** | CI green, metadata right, Dockerfile present | `scripts/ci.py`, `scripts/mcp_smoke.py` |
31
+ | **2. Publish** | on registry + GitHub Release + clean-env verified | `scripts/publish.py` |
32
+ | **3. Listings** | Glama live + awesome PR open | `scripts/glama.py`, `scripts/awesome_pr.py` |
33
+ | **4. Promo** | promo docs match reality | `scripts/promo_check.py` |
34
+
35
+ See [`SKILL.md`](SKILL.md) for the full agent instructions and the automation
36
+ boundary (what the agent runs vs. what needs a human/credential).
37
+
38
+ ## Example
39
+
40
+ [`examples/mcp-server-launch.md`](examples/mcp-server-launch.md) walks all four
41
+ phases against a toy `hello-mcp` server — reproduce it locally to learn the
42
+ pipeline before applying it to a real project.
43
+
44
+ ## Development
45
+
46
+ ```bash
47
+ pip install -e ".[dev]"
48
+ pytest -q # 9 tests
49
+ ruff check . # lint
50
+ ```
51
+
52
+ ## The scars (why this exists)
53
+
54
+ - `mcp>=1.0` + a new SDK → fresh installs crash. Set dependency floors.
55
+ - Python 3.9 can't install `mcp>=2.0` — split the CI matrix.
56
+ - YAML block scalar + inline heredoc = broken workflow. Use a script file.
57
+ - Glama introspection needs stdin held open (`sleep 1`) in smoke tests.
58
+ - PyPI tokens start `pypi-`; UUID-style strings are wrong and 403.
59
+ - npm typosquatting blocks similar names — scoped names dodge it.
60
+ - Glama builds take minutes to hours — poll, don't panic.
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: shipit-skill
3
+ description: >-
4
+ Ship a developer tool (AI agent tool, MCP server, CLI, or library) from
5
+ "it works" to "published + listed + marketable" in one pass. Triggers on
6
+ phrases like 上线 / 发布 / publish / release / 收录 / awesome PR / Glama /
7
+ launch / 宣传 / promo, or when a repo has code+tests but no CI, no package
8
+ release, no directory listing, or no promo material. Covers engineering
9
+ baseline → publish → directory listing → promo across Python/PyPI and
10
+ TypeScript/npm. Use whenever the goal is to take an existing open-source
11
+ project and ship it properly.
12
+ ---
13
+
14
+ # shipit-skill — one-pass launch pipeline for developer tools
15
+
16
+ Take an existing repo from "it works" to **published + listed + marketable**.
17
+ Four phases, each with a hard exit criterion. Skipping a phase is allowed only
18
+ if its exit criterion is already met.
19
+
20
+ > Philosophy: **look before you build.** Check what already exists and what the
21
+ > ecosystem requires *first* — the same way you'd advise any user not to
22
+ > reinvent a wheel.
23
+
24
+ ---
25
+
26
+ ## Phase 0 — Recon (30s, always)
27
+
28
+ Run these checks before touching anything:
29
+
30
+ 1. **Existing repo state** — `git status`, `git remote -v`, recent `git log`.
31
+ 2. **Package registration** — is the name taken? (`pip index versions <name>`,
32
+ `npm view <name>`, registry CDN). Names collide often; scoped names for npm
33
+ (`@org/pkg`) dodge typosquatting rules.
34
+ 3. **Similar projects** — search GitHub for direct competitors. If one exists,
35
+ note the differentiation explicitly; don't build a carbon copy.
36
+ 4. **The "exit criteria" gaps** — CI? Dockerfile? GitHub Release? listed on
37
+ Glama / awesome dirs? promo material current? Build a checklist of what's
38
+ missing. That checklist drives the rest.
39
+
40
+ Output: a short gap report + plan. Get confirmation before Phase 1 if the
41
+ gaps are large.
42
+
43
+ ---
44
+
45
+ ## Phase 1 — Engineering baseline
46
+
47
+ Exit criterion: **CI green on the default branch**, package metadata correct,
48
+ Dockerfile present (if it's an MCP/CLI server).
49
+
50
+ - **CI**: add GitHub Actions. Python: matrix over supported versions, but mind
51
+ extras that require a newer interpreter (e.g. `mcp>=2.0` needs py3.10+ — run
52
+ the base on 3.9, extras on 3.12). Always include lint + tests + a
53
+ **container MCP handshake** job if a server ships.
54
+ - **Dockerfile**: needed for Glama and container users. For stdio MCP servers:
55
+ base image + install the package + `CMD [the-server-command]`. Build **from
56
+ source** (COPY src) unless the published package is verified good — a stale
57
+ PyPI/npm release will silently break introspection.
58
+ - **`.dockerignore`**: keep the image minimal (no `.git`, tests, promo, docs).
59
+ - **Version from metadata**: MCP `serverInfo.version` and CLI `--version` must
60
+ read the package version, not a hardcoded string.
61
+ - **Dependency floor**: if your code targets a new SDK API, set the dependency
62
+ floor (`mcp>=2.0`), don't leave `>=1.0` — pip/npm resolve to latest and break
63
+ fresh installs.
64
+
65
+ Verify: clean `pip install .[extras]` / `npm install` in a temp venv, then run
66
+ the MCP handshake (`initialize` + `tools/list`) against the *container*.
67
+
68
+ ---
69
+
70
+ ## Phase 2 — Publish
71
+
72
+ Exit criterion: **package on the registry**, GitHub Release with tag + notes,
73
+ clean-env install verified.
74
+
75
+ - **Version bump** → build artifacts (`python -m build` / `npm pack`).
76
+ - **Tag + GitHub Release**: title `vX.Y.Z`, notes with Changes / Install /
77
+ What-it-fixes. Let `gh release create` attach from the tag.
78
+ - **Registry publish**:
79
+ - PyPI: needs a token (granular, scoped to the package, **Upload** perm).
80
+ If the token is malformed PyPI returns `403 Invalid or non-existent
81
+ authentication information` — UUID-style strings are wrong; real PyPI
82
+ tokens start `pypi-`. `twine upload` with `--username __token__`.
83
+ - npm: scoped names need the org to exist; granular token with bypass-2FA
84
+ for automation; `--//registry.npmjs.org/:_authToken` inline avoids writing
85
+ creds to disk.
86
+ - **Fresh-install verification** (non-negotiable): create a venv / clean dir,
87
+ install from the *registry*, import the module / run the CLI / do the MCP
88
+ handshake. This catches "works on my machine" packaging bugs.
89
+ - **Gotchas**:
90
+ - npm CDN serves cached 404s for minutes after first publish — don't panic,
91
+ cache-bust with a query param.
92
+ - `File already exists` on retry usually means the first attempt succeeded.
93
+ - Lockfile drift: rename packages → always `npm install` to regen the lock
94
+ or `npm ci` fails in CI.
95
+
96
+ ---
97
+
98
+ ## Phase 3 — Directory listings
99
+
100
+ Exit criterion: **listed on Glama with a score badge** and **awesome PR open
101
+ (or merged)**.
102
+
103
+ - **Glama** (`glama.ai/mcp/servers`): user submits via browser (GitHub OAuth —
104
+ cannot be automated). Before submitting: Dockerfile must exist and pass
105
+ introspection (see Phase 1). After submit: the page takes **minutes to hours**
106
+ to appear — poll `https://glama.ai/mcp/servers/<owner>/<repo>` and
107
+ `/badges/score.svg`. Add the badge to the repo README once live:
108
+ `[![owner/repo MCP server](https://glama.ai/mcp/servers/owner/repo/badges/score.svg)](https://glama.ai/mcp/servers/owner/repo)`
109
+ - **awesome-mcp-servers** (and similar curated lists): PRs are triaged by bots.
110
+ Format per the list's current convention (badge + lang + scope + OS + install
111
+ cmd). Maintainers now require the Glama badge in the entry — submit Glama
112
+ *first*. Add `🤖🤖🤖` to the PR title to opt into the agent fast-track.
113
+ Fork → branch off latest upstream main → add entry → PR. If a previous PR was
114
+ closed for inactivity, force-push the branch or recreate it.
115
+ - **Other directories** (Smithery, PulseMCP, mcp.so…): Smithery reads a
116
+ `smithery.yaml` at repo root (stdio start command + optional env schema).
117
+
118
+ ---
119
+
120
+ ## Phase 4 — Promo material
121
+
122
+ Exit criterion: **promo docs' versions/links/PR numbers match reality**; per-
123
+ platform posts are *ready to paste* (user posts manually — no fake automation).
124
+
125
+ - Maintain `promo/` with per-platform files (V2EX, 知乎, Reddit, X, community).
126
+ - **Freshness check** (run whenever versions/PRs change): grep promo files for
127
+ old version numbers, dead PR links, stale release URLs. Update or mark stale.
128
+ - **Fact discipline**: verify every number/claim before posting — rerun the
129
+ tool and compare output. Wrong claims get torn apart in comments (e.g. a
130
+ "no LICENSE" claim that's actually a GitHub metadata quirk).
131
+ - Keep install commands unpinned (`pip install pkg`) so promo doesn't rot.
132
+
133
+ ---
134
+
135
+ ## Automation boundary
136
+
137
+ | Step | Who runs it |
138
+ |------|-------------|
139
+ | CI files, Dockerfile, metadata, code fixes | Agent |
140
+ | Build artifacts, fresh-install verification | Agent |
141
+ | Registry publish (PyPI/npm) | Agent, **with a user-supplied token** |
142
+ | Glama browser submit | User (OAuth) |
143
+ | awesome/other PR create | Agent |
144
+ | Posting to platforms | User |
145
+
146
+ Never fabricate credentials; ask for the token explicitly and use it inline,
147
+ never write it to a file.
148
+
149
+ ## Real-world scars (from shipping wheel-hub + zspace-cli)
150
+
151
+ 1. `mcp>=1.0` in extras + new SDK → fresh installs crash. Set floors.
152
+ 2. Python 3.9 can't install `mcp>=2.0`; CI matrix must split base vs extras.
153
+ 3. YAML block scalar + inline python heredoc = broken workflow. Use a script file.
154
+ 4. MCP 2.x `Server` object has no `list_tools` — use `@server.tool()` decorators.
155
+ 5. Glama introspection needs stdin held open for a beat (`sleep 1`) in smoke tests.
156
+ 6. PyPI token formats: `pypi-...` real, UUID = wrong.
157
+ 7. npm typosquatting blocks `wheel-hub` next to `wheelhub`; scoped names work.
158
+ 8. `.dockerignore` missing → giant images / secrets in build context.
@@ -0,0 +1,80 @@
1
+ # Example — shipping a minimal MCP server with shipit-skill
2
+
3
+ This walks the 4 phases against a toy `hello-mcp` server so you can see exactly
4
+ what shipit-skill produces at each step. Reproduce it locally to learn the pipeline,
5
+ then apply the same steps to your real project.
6
+
7
+ ```
8
+ hello-mcp/ # your project (already has working code)
9
+ ├── pyproject.toml # name=hello-mcp, extras mcp=["mcp>=2.0; python_version >= '3.10'"]
10
+ ├── src/hello_mcp/ # server.py with @server.tool() handlers
11
+ ├── tests/ # passing tests
12
+ └── scripts/mcp_smoke.py # copy from shipit-skill/scripts/mcp_smoke.py
13
+ ```
14
+
15
+ ## Phase 1 — Baseline
16
+
17
+ ```bash
18
+ # generate CI (python server → includes Docker handshake job)
19
+ python3 shipit-skill/scripts/ci.py --lang python --server hello-mcp --pkg hello-mcp \
20
+ > hello-mcp/.github/workflows/ci.yml
21
+
22
+ # Dockerfile (build from source so the image matches tested code)
23
+ cat > hello-mcp/Dockerfile <<'EOF'
24
+ FROM python:3.12-slim
25
+ WORKDIR /app
26
+ COPY pyproject.toml ./
27
+ COPY src ./src
28
+ RUN pip install --no-cache-dir ".[mcp]"
29
+ CMD ["hello-mcp"]
30
+ EOF
31
+ cat > hello-mcp/.dockerignore <<'EOF'
32
+ .git
33
+ .github
34
+ tests/
35
+ dist/
36
+ EOF
37
+ ```
38
+
39
+ Exit check: CI green, `docker build` passes, handshake OK:
40
+
41
+ ```bash
42
+ cd hello-mcp
43
+ { printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"s","version":"0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n'; sleep 1; } \
44
+ | docker run --rm -i hello-mcp 2>/dev/null | python3 scripts/mcp_smoke.py
45
+ # → initialize OK ... / tools/list OK ...
46
+ ```
47
+
48
+ ## Phase 2 — Publish
49
+
50
+ ```bash
51
+ python3 shipit-skill/scripts/publish.py --lang python --pkg hello-mcp --server hello-mcp --verify
52
+ # prints build + twine commands; PYPI_TOKEN=<pypi-...> twine upload dist/...
53
+ ```
54
+
55
+ ## Phase 3 — Listings
56
+
57
+ ```bash
58
+ python3 shipit-skill/scripts/glama.py --repo <you>/hello-mcp --poll 6
59
+ # → LISTED ✅ once the build queue finishes (minutes to hours)
60
+
61
+ python3 shipit-skill/scripts/awesome_pr.py \
62
+ --upstream punkpeye/awesome-mcp-servers \
63
+ --repo <you>/hello-mcp --fork <you>/awesome-mcp-servers \
64
+ --branch add-hello-mcp --title "Add <you>/hello-mcp to <Category>"
65
+ ```
66
+
67
+ ## Phase 4 — Promo
68
+
69
+ ```bash
70
+ # keep promo/ with per-platform posts; when versions/PRs change:
71
+ python3 shipit-skill/scripts/promo_check.py --dir promo --version 0.1.0 --prs 1234=open
72
+ # → exit 1 lists every stale version / unknown PR before you paste anything
73
+ ```
74
+
75
+ ## Gotchas this example demonstrates
76
+
77
+ - `mcp>=2.0` needs Python 3.10+ → CI matrix splits base (3.9) vs extras (3.12).
78
+ - Dockerfile builds from `src/`, not the PyPI release → no stale-package surprises.
79
+ - Glama needs stdin held open (`sleep 1`) during the handshake smoke test.
80
+ - Promo versions rot fast → always run `promo_check.py` after a release.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "shipit-skill"
3
+ version = "0.1.0"
4
+ description = "One-pass launch pipeline Agent Skill for developer tools"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.9"
8
+
9
+ [project.optional-dependencies]
10
+ dev = [
11
+ "pytest>=7.0",
12
+ "ruff>=0.4",
13
+ "PyYAML>=6.0",
14
+ ]
15
+
16
+ [tool.setuptools]
17
+ # shipit-skill is a skill bundle, not a Python library — ship its files as data.
18
+ include-package-data = false
19
+
20
+ [tool.setuptools.data-files]
21
+ "." = [
22
+ "SKILL.md",
23
+ "README.md",
24
+ "CHANGELOG.md",
25
+ ]
26
+ "scripts" = ["scripts/*.py"]
27
+ "examples" = ["examples/*.md"]
28
+
29
+ [tool.ruff]
30
+ line-length = 100
31
+ target-version = "py39"
32
+
33
+ [tool.ruff.lint]
34
+ select = ["E", "F", "I", "W", "UP"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Helper for submitting to awesome lists (e.g. awesome-mcp-servers).
3
+
4
+ Automates the mechanical parts; keeps the human-verifiable steps explicit.
5
+
6
+ Usage:
7
+ python3 scripts/awesome_pr.py --upstream punkpeye/awesome-mcp-servers \
8
+ --repo skyzhao1223/zspace-cli --fork skyzhao1223/awesome-mcp-servers \
9
+ --branch add-zspace-cli --title "Add skyzhao1223/zspace-cli to File Systems"
10
+
11
+ Steps printed (fork sync, branch, edit README, push, create PR). The README
12
+ edit itself is intentionally manual — every list has its own format.
13
+ """
14
+
15
+ # ruff: noqa: E501
16
+ import argparse
17
+ import subprocess
18
+ import sys
19
+
20
+
21
+ def sh(cmd: list[str]) -> str:
22
+ r = subprocess.run(cmd, capture_output=True, text=True)
23
+ if r.returncode != 0:
24
+ print(r.stderr.strip(), file=sys.stderr)
25
+ return r.stdout.strip()
26
+
27
+
28
+ def main() -> None:
29
+ ap = argparse.ArgumentParser()
30
+ ap.add_argument("--upstream", required=True, help="punkpeye/awesome-mcp-servers")
31
+ ap.add_argument("--repo", required=True, help="your server repo, e.g. skyzhao1223/zspace-cli")
32
+ ap.add_argument("--fork", required=True, help="your fork of upstream")
33
+ ap.add_argument("--branch", required=True, help="branch to create, e.g. add-zspace-cli")
34
+ ap.add_argument("--title", default="", help="PR title; add ' 🤖🤖🤖' to opt into agent fast-track")
35
+ args = ap.parse_args()
36
+
37
+ print(f"""# awesome PR recipe — {args.repo}
38
+
39
+ ## 0. Prereqs
40
+ - Glama listing live (run `python3 scripts/glama.py --repo {args.repo}`) — most
41
+ maintainers now REQUIRE the Glama score badge in the entry. Submit Glama first.
42
+
43
+ ## 1. Sync fork main
44
+ gh api repos/{args.fork}/merge-upstream -X POST -f branch=main
45
+
46
+ ## 2. Branch off latest upstream (in a fresh shallow clone)
47
+ cd /tmp && rm -rf ams
48
+ git clone --depth 1 https://github.com/{args.fork}.git ams && cd ams
49
+ git checkout -q -b {args.branch}
50
+ # if {args.branch} already exists from a closed PR: force push at the end
51
+
52
+ ## 3. Edit README
53
+ # find your category anchor, add the entry after it. Format (typical):
54
+ - [{args.repo}](https://github.com/{args.repo}) \\
55
+ [![{args.repo} MCP server](https://glama.ai/mcp/servers/{args.repo}/badges/score.svg)](https://glama.ai/mcp/servers/{args.repo}) \\
56
+ 🐍 🏠 🍎 - <one-line description> \\`<install cmd>\\`
57
+
58
+ ## 4. Push + PR
59
+ git add README.md
60
+ git commit -m "Add {args.repo} to <Category>"
61
+ git push -f -u origin {args.branch} # -f only if branch existed before
62
+ gh pr create --repo {args.upstream} --head {args.fork.split('/')[0]}:{args.branch} \\
63
+ --title "{args.title} 🤖🤖🤖" --body "<markdown body>"
64
+ """)
65
+
66
+ print("# Commands above are guidance; nothing was executed.")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """Generate a CI workflow for a Python or TypeScript project.
3
+
4
+ Usage:
5
+ python3 scripts/ci.py --lang python [--server zs-mcp] [--pkg zspace-cli]
6
+ python3 scripts/ci.py --lang typescript
7
+
8
+ Prints the YAML to stdout. Pipe to .github/workflows/ci.yml.
9
+ """
10
+
11
+ # ruff: noqa: E501
12
+ import argparse
13
+
14
+ PY_CI = """name: CI
15
+
16
+ on:
17
+ push:
18
+ branches: [main]
19
+ pull_request:
20
+
21
+ jobs:
22
+ test:
23
+ runs-on: ubuntu-latest
24
+ strategy:
25
+ matrix:
26
+ python-version: [{py_versions}]
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{{{ matrix.python-version }}}}
32
+ cache: pip
33
+ - run: pip install -e ".[dev]"
34
+ if: matrix.python-version == '{base_py}'
35
+ - run: pip install -e ".[mcp,dev]"
36
+ if: matrix.python-version != '{base_py}'
37
+ - run: ruff check src tests
38
+ - run: pytest -q
39
+ {server_block}"""
40
+
41
+ PY_SERVER_BLOCK = """
42
+ docker:
43
+ runs-on: ubuntu-latest
44
+ needs: test
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - name: Build image
48
+ run: docker build -t {pkg} .
49
+ - name: MCP handshake (initialize + tools/list)
50
+ run: |
51
+ set +e
52
+ {{
53
+ printf '{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"2024-11-05","capabilities":{{}},"clientInfo":{{"name":"smoke","version":"0"}}}}}}\\n{{"jsonrpc":"2.0","method":"notifications/initialized"}}\\n{{"jsonrpc":"2.0","id":2,"method":"tools/list"}}\\n'
54
+ sleep 1
55
+ }} | docker run --rm -i {pkg} >/tmp/mcp-out.txt 2>/tmp/mcp-err.txt
56
+ rc=$?
57
+ echo "exit=$rc"; echo "--- stderr ---"; cat /tmp/mcp-err.txt
58
+ echo "--- stdout (first 500) ---"; head -c 500 /tmp/mcp-out.txt; echo
59
+ [ $rc -eq 0 ] && python3 scripts/mcp_smoke.py < /tmp/mcp-out.txt
60
+ """
61
+
62
+ TS_CI = """name: CI
63
+
64
+ on:
65
+ push:
66
+ branches: [main]
67
+ pull_request:
68
+
69
+ jobs:
70
+ build:
71
+ runs-on: ubuntu-latest
72
+ steps:
73
+ - uses: actions/checkout@v4
74
+ - uses: actions/setup-node@v4
75
+ with:
76
+ node-version: 24
77
+ cache: npm
78
+ - run: npm ci
79
+ - run: npm run build --workspaces --if-present
80
+ - run: npm test --workspaces --if-present
81
+ """
82
+
83
+
84
+ def main() -> None:
85
+ ap = argparse.ArgumentParser()
86
+ ap.add_argument("--lang", required=True, choices=["python", "typescript"])
87
+ ap.add_argument("--server", help="MCP server console-script name (python only)")
88
+ ap.add_argument("--pkg", default="app", help="Docker image / package name")
89
+ ap.add_argument("--py-versions", default='"3.9", "3.12"', help="comma list")
90
+ ap.add_argument("--base-py", default="3.9", help="base interpreter for extras-free install")
91
+ args = ap.parse_args()
92
+
93
+ if args.lang == "typescript":
94
+ print(TS_CI)
95
+ return
96
+
97
+ server_block = ""
98
+ if args.server:
99
+ server_block = PY_SERVER_BLOCK.format(pkg=args.pkg)
100
+ print(PY_CI.format(
101
+ py_versions=args.py_versions,
102
+ base_py=args.base_py,
103
+ server_block=server_block,
104
+ ))
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Check Glama listing status + badge availability for an MCP server.
3
+
4
+ Usage:
5
+ python3 scripts/glama.py --repo skyzhao1223/zspace-cli [--poll 6] [--wait 40]
6
+
7
+ Prints page status and badge status. With --poll, retries N times waiting
8
+ --wait seconds between tries (Glama builds can take minutes to hours).
9
+ Exit 0 if page is live (200), else 1.
10
+ """
11
+
12
+ import argparse
13
+ import time
14
+ import urllib.request
15
+
16
+
17
+ def status(url: str, ua: bool = True) -> int:
18
+ req = urllib.request.Request(url)
19
+ if ua:
20
+ req.add_header("User-Agent", "Mozilla/5.0")
21
+ try:
22
+ with urllib.request.urlopen(req, timeout=20) as r:
23
+ return r.status
24
+ except Exception:
25
+ return 404
26
+
27
+
28
+ def main() -> None:
29
+ ap = argparse.ArgumentParser()
30
+ ap.add_argument("--repo", required=True, help="owner/repo")
31
+ ap.add_argument("--poll", type=int, default=0, help="poll attempts")
32
+ ap.add_argument("--wait", type=int, default=40, help="seconds between polls")
33
+ args = ap.parse_args()
34
+
35
+ page = f"https://glama.ai/mcp/servers/{args.repo}"
36
+ badge = f"{page}/badges/score.svg"
37
+
38
+ code = status(page)
39
+ print(f"page : {code} {page}")
40
+ print(f"badge: {status(badge)} {badge}")
41
+
42
+ tries = 0
43
+ while code != 200 and tries < args.poll:
44
+ tries += 1
45
+ print(f"waiting {args.wait}s (try {tries}/{args.poll})...")
46
+ time.sleep(args.wait)
47
+ code = status(page)
48
+ print(f"page : {code} {page}")
49
+ if code == 200:
50
+ print(f"badge: {status(badge)} {badge}")
51
+
52
+ if code != 200:
53
+ print("NOT LISTED YET — check the Glama dashboard for build/check status.")
54
+ raise SystemExit(1)
55
+ print("LISTED ✅")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ """MCP smoke test: read initialize + tools/list over stdio and assert basics.
3
+
4
+ Usage:
5
+ printf '<init>\n<notify>\n<tools-list>\n' \
6
+ | <server-cmd> | python3 scripts/mcp_smoke.py
7
+
8
+ Exits 0 if initialize and tools/list were both received and parseable.
9
+ """
10
+
11
+ import json
12
+ import sys
13
+
14
+
15
+ def main() -> None:
16
+ seen_init = False
17
+ seen_tools = False
18
+ for line in sys.stdin:
19
+ line = line.strip()
20
+ if not line:
21
+ continue
22
+ try:
23
+ d = json.loads(line)
24
+ except json.JSONDecodeError:
25
+ continue
26
+ if d.get("id") == 1 and "result" in d:
27
+ assert "serverInfo" in d["result"], d
28
+ print("initialize OK:", d["result"]["serverInfo"])
29
+ seen_init = True
30
+ if d.get("id") == 2 and "result" in d:
31
+ names = [t["name"] for t in d["result"]["tools"]]
32
+ print("tools/list OK:", len(names), "tools:", names[:4], "...")
33
+ seen_tools = True
34
+ assert seen_init, "no initialize response received"
35
+ assert seen_tools, "no tools/list response received"
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """Check promo material freshness: versions, PR numbers, release URLs.
3
+
4
+ Usage:
5
+ python3 scripts/promo_check.py --dir promo --version 0.1.1
6
+ python3 scripts/promo_check.py --dir promo --version 0.1.1 --prs 8262=closed,13600=open
7
+
8
+ Scans *.md in --dir for:
9
+ - version numbers (semver) that differ from --version (reports old ones)
10
+ - GitHub PR/issue links (#NNNNN) not present in --prs
11
+ - github release/tag URLs (reports for manual eyeball)
12
+ Exit code 1 if any stale version or unknown PR is found.
13
+ """
14
+
15
+ import argparse
16
+ import re
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # Match "v0.1.0" / "0.1.1" / " 0.3.0" but NOT "127.0.0.1" (IPs) or "x.y.z".
21
+ SEMVER = re.compile(r"(?<![\d.])(?:v)?0\.\d+\.\d+\b")
22
+ PR_LINK = re.compile(r"#(\d{4,})")
23
+ RELEASE_URL = re.compile(r"releases/tag/([\w.]+)")
24
+
25
+
26
+ def main() -> None:
27
+ ap = argparse.ArgumentParser()
28
+ ap.add_argument("--dir", required=True)
29
+ ap.add_argument("--version", required=True, help="current version, e.g. 0.1.1")
30
+ ap.add_argument("--prs", default="", help="known PRs as id=state,... e.g. 13600=open")
31
+ args = ap.parse_args()
32
+
33
+ known_prs = {k: v for k, v in (kv.split("=") for kv in args.prs.split(",") if kv)}
34
+ target = tuple(int(x) for x in args.version.split("."))
35
+ errors: list[str] = []
36
+
37
+ for path in sorted(Path(args.dir).glob("*.md")):
38
+ text = path.read_text(encoding="utf-8")
39
+ for m in SEMVER.finditer(text):
40
+ v = m.group(0).lstrip("v")
41
+ cur = tuple(int(x) for x in v.split("."))
42
+ if cur != target:
43
+ line = text[: m.start()].count("\n") + 1
44
+ errors.append(f"{path.name}:{line}: stale version {v} (current {args.version})")
45
+ for m in PR_LINK.finditer(text):
46
+ pid = m.group(1)
47
+ if pid not in known_prs:
48
+ line = text[: m.start()].count("\n") + 1
49
+ errors.append(
50
+ f"{path.name}:{line}: unknown PR/issue #{pid} (known: {sorted(known_prs)})"
51
+ )
52
+ for m in RELEASE_URL.finditer(text):
53
+ tag = m.group(1)
54
+ line = text[: m.start()].count("\n") + 1
55
+ print(
56
+ f"{path.name}:{line}: release tag {tag} (verify manually)"
57
+ )
58
+
59
+ if errors:
60
+ print("\n".join(errors))
61
+ sys.exit(1)
62
+ print("OK: no stale versions or unknown PRs.")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ """Publish helper: build, verify fresh install, and emit publish commands.
3
+
4
+ Usage:
5
+ python3 scripts/publish.py --lang python [--pkg zspace-cli] [--server zs-mcp]
6
+ python3 scripts/publish.py --lang typescript [--pkg @org/cli]
7
+
8
+ Prints the exact publish commands (needs a user-supplied token — never stored).
9
+ Also runs a fresh-install verification in a temp venv when --verify.
10
+ """
11
+
12
+ # ruff: noqa: E501
13
+ import argparse
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+
18
+
19
+ def run(cmd: list[str]) -> None:
20
+ subprocess.run(cmd, check=True)
21
+
22
+
23
+ def verify_python(pkg: str, server: str | None, extras: str) -> None:
24
+ tmp = tempfile.mkdtemp(prefix="shipit-")
25
+ run([sys.executable, "-m", "venv", f"{tmp}/venv"])
26
+ pip = f"{tmp}/venv/bin/pip"
27
+ run([pip, "install", "-q", extras])
28
+ importlib = f"{tmp}/venv/bin/python"
29
+ subprocess.run([importlib, "-c", f"import importlib.metadata as m; print('installed', m.version('{pkg}'))"], check=True)
30
+ if server:
31
+ init = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n'
32
+ handshake = f"printf '{init}' | {tmp}/venv/bin/{server} 2>/dev/null | python3 -c \"import sys,json; [print(json.loads(l)['result'].get('serverInfo')) for l in sys.stdin if l.strip()]\""
33
+ subprocess.run(["bash", "-c", handshake], check=True)
34
+ print(f"fresh-install OK (venv: {tmp}/venv)")
35
+
36
+
37
+ def main() -> None:
38
+ ap = argparse.ArgumentParser()
39
+ ap.add_argument("--lang", required=True, choices=["python", "typescript"])
40
+ ap.add_argument("--pkg", required=True, help="package name")
41
+ ap.add_argument("--server", help="MCP console script name (python)")
42
+ ap.add_argument("--verify", action="store_true", help="run fresh-install check")
43
+ args = ap.parse_args()
44
+
45
+ if args.lang == "python":
46
+ print("# Build\npython3 -m build")
47
+ print(f"# Publish (set PYPI_TOKEN=<pypi-... token, Upload scope on {args.pkg}>)\n"
48
+ f"python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ \\\n"
49
+ f" --username __token__ --password \"$PYPI_TOKEN\" dist/{args.pkg}-*.tar.gz dist/{args.pkg}-*.whl")
50
+ if args.verify:
51
+ verify_python(args.pkg, args.server, args.pkg)
52
+ else:
53
+ print("# Publish (set NPM_TOKEN=<granular token, bypass-2fa>)\n"
54
+ "npm publish --//registry.npmjs.org/:_authToken=\"$NPM_TOKEN\" --access public")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: shipit-skill
3
+ Version: 0.1.0
4
+ Summary: One-pass launch pipeline Agent Skill for developer tools
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0; extra == "dev"
11
+ Requires-Dist: ruff>=0.4; extra == "dev"
12
+ Requires-Dist: PyYAML>=6.0; extra == "dev"
13
+ Dynamic: license-file
14
+
15
+ # ⚙️ shipit-skill
16
+
17
+ [![CI](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml/badge.svg)](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml)
18
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
19
+
20
+ **Take an existing developer tool from "it works" to "published + listed + marketable" — one pass.**
21
+
22
+ `shipit-skill` is an Agent Skill that runs the full launch pipeline for AI/developer tools
23
+ (MCP servers, CLIs, libraries): engineering baseline → publish → directory listings →
24
+ promo material. It was distilled from shipping [wheel-hub](https://github.com/skyzhao1223/wheel-hub),
25
+ [zspace-cli](https://github.com/skyzhao1223/zspace-cli) and
26
+ [media-manager-skill](https://github.com/skyzhao1223/media-manager-skill) — every gotcha
27
+ below is one that actually bit during those releases.
28
+
29
+ ## Install
30
+
31
+ Copy this folder into your project (or your agent's skills dir):
32
+
33
+ ```bash
34
+ # for the current project
35
+ cp -r shipit-skill/ ~/your-project/.opencode/skills/shipit-skill # opencode
36
+ # cp -r shipit-skill/ ~/your-project/skills/shipit-skill # Claude Code, Cursor, etc.
37
+ ```
38
+
39
+ ## What it does
40
+
41
+ | Phase | Exit criterion | Scripts |
42
+ |-------|----------------|---------|
43
+ | **0. Recon** | gap report (CI? Docker? Release? listed? promo?) | — |
44
+ | **1. Baseline** | CI green, metadata right, Dockerfile present | `scripts/ci.py`, `scripts/mcp_smoke.py` |
45
+ | **2. Publish** | on registry + GitHub Release + clean-env verified | `scripts/publish.py` |
46
+ | **3. Listings** | Glama live + awesome PR open | `scripts/glama.py`, `scripts/awesome_pr.py` |
47
+ | **4. Promo** | promo docs match reality | `scripts/promo_check.py` |
48
+
49
+ See [`SKILL.md`](SKILL.md) for the full agent instructions and the automation
50
+ boundary (what the agent runs vs. what needs a human/credential).
51
+
52
+ ## Example
53
+
54
+ [`examples/mcp-server-launch.md`](examples/mcp-server-launch.md) walks all four
55
+ phases against a toy `hello-mcp` server — reproduce it locally to learn the
56
+ pipeline before applying it to a real project.
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ pip install -e ".[dev]"
62
+ pytest -q # 9 tests
63
+ ruff check . # lint
64
+ ```
65
+
66
+ ## The scars (why this exists)
67
+
68
+ - `mcp>=1.0` + a new SDK → fresh installs crash. Set dependency floors.
69
+ - Python 3.9 can't install `mcp>=2.0` — split the CI matrix.
70
+ - YAML block scalar + inline heredoc = broken workflow. Use a script file.
71
+ - Glama introspection needs stdin held open (`sleep 1`) in smoke tests.
72
+ - PyPI tokens start `pypi-`; UUID-style strings are wrong and 403.
73
+ - npm typosquatting blocks similar names — scoped names dodge it.
74
+ - Glama builds take minutes to hours — poll, don't panic.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,18 @@
1
+ CHANGELOG.md
2
+ LICENSE
3
+ README.md
4
+ SKILL.md
5
+ pyproject.toml
6
+ examples/mcp-server-launch.md
7
+ scripts/awesome_pr.py
8
+ scripts/ci.py
9
+ scripts/glama.py
10
+ scripts/mcp_smoke.py
11
+ scripts/promo_check.py
12
+ scripts/publish.py
13
+ shipit_skill.egg-info/PKG-INFO
14
+ shipit_skill.egg-info/SOURCES.txt
15
+ shipit_skill.egg-info/dependency_links.txt
16
+ shipit_skill.egg-info/requires.txt
17
+ shipit_skill.egg-info/top_level.txt
18
+ tests/test_scripts.py
@@ -0,0 +1,5 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
4
+ ruff>=0.4
5
+ PyYAML>=6.0
@@ -0,0 +1,105 @@
1
+ """Tests for shipit-skill helper scripts."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ SCRIPTS = ROOT / "scripts"
9
+
10
+
11
+ def run_script(name: str, *args: str) -> subprocess.CompletedProcess[str]:
12
+ return subprocess.run(
13
+ [sys.executable, str(SCRIPTS / name), *args],
14
+ capture_output=True,
15
+ text=True,
16
+ cwd=ROOT,
17
+ )
18
+
19
+
20
+ def test_ci_python_yaml_valid():
21
+ r = run_script("ci.py", "--lang", "python", "--server", "zs-mcp", "--pkg", "app")
22
+ assert r.returncode == 0
23
+ import yaml
24
+
25
+ data = yaml.safe_load(r.stdout)
26
+ assert "test" in data["jobs"]
27
+ assert "docker" in data["jobs"]
28
+ assert "matrix" in data["jobs"]["test"]["strategy"]
29
+
30
+
31
+ def test_ci_python_without_server_has_no_docker_job():
32
+ r = run_script("ci.py", "--lang", "python")
33
+ import yaml
34
+
35
+ data = yaml.safe_load(r.stdout)
36
+ assert "docker" not in data["jobs"]
37
+
38
+
39
+ def test_ci_typescript_yaml_valid():
40
+ r = run_script("ci.py", "--lang", "typescript")
41
+ assert r.returncode == 0
42
+ import yaml
43
+
44
+ data = yaml.safe_load(r.stdout)
45
+ assert "build" in data["jobs"]
46
+
47
+
48
+ def test_promo_check_detects_stale_version(tmp_path: Path):
49
+ (tmp_path / "post.md").write_text(
50
+ "# post\nInstall v0.1.0 and check PR #1234\n", encoding="utf-8"
51
+ )
52
+ r = run_script(
53
+ "promo_check.py", "--dir", str(tmp_path), "--version", "0.1.1", "--prs", "1234=open"
54
+ )
55
+ assert r.returncode == 1
56
+ assert "stale version 0.1.0" in r.stdout
57
+
58
+
59
+ def test_promo_check_ignores_ip_like_versions(tmp_path: Path):
60
+ (tmp_path / "post.md").write_text(
61
+ "proxy at 127.0.0.1:13579 and version 0.1.1\n", encoding="utf-8"
62
+ )
63
+ r = run_script("promo_check.py", "--dir", str(tmp_path), "--version", "0.1.1")
64
+ assert r.returncode == 0
65
+
66
+
67
+ def test_promo_check_unknown_pr(tmp_path: Path):
68
+ (tmp_path / "post.md").write_text("see PR #9999\n", encoding="utf-8")
69
+ r = run_script("promo_check.py", "--dir", str(tmp_path), "--version", "0.1.1")
70
+ assert r.returncode == 1
71
+ assert "unknown PR/issue #9999" in r.stdout
72
+
73
+
74
+ def test_mcp_smoke_ok():
75
+ payload = (
76
+ '{"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"x","version":"1"}}}\n'
77
+ '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"a"}]}}\n'
78
+ )
79
+ r = subprocess.run(
80
+ [sys.executable, str(SCRIPTS / "mcp_smoke.py")],
81
+ input=payload,
82
+ capture_output=True,
83
+ text=True,
84
+ )
85
+ assert r.returncode == 0
86
+ assert "initialize OK" in r.stdout
87
+
88
+
89
+ def test_mcp_smoke_fails_on_missing_tools():
90
+ payload = '{"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"x","version":"1"}}}\n'
91
+ r = subprocess.run(
92
+ [sys.executable, str(SCRIPTS / "mcp_smoke.py")],
93
+ input=payload,
94
+ capture_output=True,
95
+ text=True,
96
+ )
97
+ assert r.returncode != 0
98
+ assert "tools/list" in r.stderr
99
+
100
+
101
+ def test_glama_reports_not_listed():
102
+ # glama.py with a repo guaranteed to not be listed should exit 1.
103
+ r = run_script("glama.py", "--repo", "skyzhao1223/definitely-not-a-repo-xyz")
104
+ assert r.returncode == 1
105
+ assert "NOT LISTED YET" in r.stdout