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.
- shipit_skill-0.1.0/CHANGELOG.md +29 -0
- shipit_skill-0.1.0/LICENSE +21 -0
- shipit_skill-0.1.0/PKG-INFO +78 -0
- shipit_skill-0.1.0/README.md +64 -0
- shipit_skill-0.1.0/SKILL.md +158 -0
- shipit_skill-0.1.0/examples/mcp-server-launch.md +80 -0
- shipit_skill-0.1.0/pyproject.toml +37 -0
- shipit_skill-0.1.0/scripts/awesome_pr.py +70 -0
- shipit_skill-0.1.0/scripts/ci.py +108 -0
- shipit_skill-0.1.0/scripts/glama.py +59 -0
- shipit_skill-0.1.0/scripts/mcp_smoke.py +39 -0
- shipit_skill-0.1.0/scripts/promo_check.py +66 -0
- shipit_skill-0.1.0/scripts/publish.py +58 -0
- shipit_skill-0.1.0/setup.cfg +4 -0
- shipit_skill-0.1.0/shipit_skill.egg-info/PKG-INFO +78 -0
- shipit_skill-0.1.0/shipit_skill.egg-info/SOURCES.txt +18 -0
- shipit_skill-0.1.0/shipit_skill.egg-info/dependency_links.txt +1 -0
- shipit_skill-0.1.0/shipit_skill.egg-info/requires.txt +5 -0
- shipit_skill-0.1.0/shipit_skill.egg-info/top_level.txt +1 -0
- shipit_skill-0.1.0/tests/test_scripts.py +105 -0
|
@@ -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
|
+
[](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml)
|
|
18
|
+
[](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
|
+
[](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml)
|
|
4
|
+
[](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
|
+
`[](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
|
+
[](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,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
|
+
[](https://github.com/skyzhao1223/shipit-skill/actions/workflows/ci.yml)
|
|
18
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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
|