@cyanheads/mcp-ts-core 0.9.8 → 0.9.10
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.
- package/CLAUDE.md +2 -1
- package/README.md +4 -4
- package/changelog/0.9.x/0.9.10.md +37 -0
- package/changelog/0.9.x/0.9.9.md +20 -0
- package/changelog/template.md +8 -0
- package/dist/logs/combined.log +4 -0
- package/dist/logs/error.log +2 -0
- package/dist/logs/interactions.log +0 -0
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +27 -6
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +29 -4
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/testing/fuzz.d.ts +6 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +93 -49
- package/dist/testing/fuzz.js.map +1 -1
- package/package.json +11 -9
- package/scripts/check-framework-antipatterns.ts +8 -4
- package/skills/design-mcp-server/SKILL.md +72 -2
- package/skills/git-wrapup/SKILL.md +1 -0
- package/skills/maintenance/SKILL.md +2 -1
- package/skills/polish-docs-meta/SKILL.md +25 -5
- package/skills/release-and-publish/SKILL.md +7 -6
- package/templates/.claude-plugin/plugin.json +20 -0
- package/templates/.codex-plugin/mcp.json +9 -0
- package/templates/.codex-plugin/plugin.json +25 -0
- package/templates/AGENTS.md +5 -0
- package/templates/CLAUDE.md +6 -0
- package/templates/changelog/template.md +8 -0
- package/scripts/split-changelog.ts +0 -133
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Design the tool surface, resources, and service layer for a new MCP server. Use when starting a new server, planning a major feature expansion, or when the user describes a domain/API they want to expose via MCP. Produces a design doc at docs/design.md that drives implementation.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.12"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -29,6 +29,23 @@ Gather before designing. Ask the user if not obvious from context:
|
|
|
29
29
|
|
|
30
30
|
If the domain has a public API, read its docs before designing. For internal-only servers, skip API research and go straight to user goals. Don't design from vibes either way.
|
|
31
31
|
|
|
32
|
+
### Server scope and audience
|
|
33
|
+
|
|
34
|
+
Before committing to a server boundary, answer: **what workflow does this server serve, and who is the audience?**
|
|
35
|
+
|
|
36
|
+
The unit of a server is a *user workflow*, not an API. A single rich API can earn its own server when the audience is large and the API surface supports a full workflow (PubMed for literature research, SEC EDGAR for financial analysis, Shodan for internet-wide device intelligence). Multiple APIs should collapse into one server when they serve the same workflow from different angles — a "threat intelligence" server that aggregates VirusTotal, AbuseIPDB, and GreyNoise is more useful than three separate servers because the user's goal is "assess this indicator," not "query VirusTotal."
|
|
37
|
+
|
|
38
|
+
**Don't default to one-API-one-server.** That's the right call when the API is deep enough and the audience is large enough, but it's not the starting point. The starting point is the workflow:
|
|
39
|
+
|
|
40
|
+
| Signal | Server boundary |
|
|
41
|
+
|:-------|:----------------|
|
|
42
|
+
| Single API with rich surface, large audience | Standalone server named for the platform (`pubmed-mcp-server`, `secedgar-mcp-server`) |
|
|
43
|
+
| Multiple APIs serving the same workflow | One server named for the workflow (`threat-intel-mcp-server`), APIs are internal sources |
|
|
44
|
+
| Domain with distinct sub-audiences | Consider splitting — a pentester and a SOC analyst have different workflows even in the same domain |
|
|
45
|
+
| Pure computation, no external deps | Standalone server named for the capability (`calculator-mcp-server`, `redteam-mcp-server`) |
|
|
46
|
+
|
|
47
|
+
When multiple APIs collapse into one server, the tool surface is organized around what the user is doing, not which API gets called. The agent says "investigate this domain" and the server routes to the best available source internally. Individual APIs become service-layer implementation details, not tool-surface identities.
|
|
48
|
+
|
|
32
49
|
## Server Naming
|
|
33
50
|
|
|
34
51
|
The server name (repo name, npm package, public identity) must communicate what it does at a glance. The test: can a human or agent scanning a server list tell what this server does from the name alone?
|
|
@@ -141,6 +158,56 @@ const findStudies = tool('clinicaltrials_find_studies', {
|
|
|
141
158
|
|
|
142
159
|
> **Tip — mode consolidation.** When a tool has several related operations on the same noun, you can consolidate them under one tool with a `mode`/`operation` enum. This affects both naming (noun-led, e.g., `github_pull_request`) and handler design (dispatch by mode). Use when it tightens the surface; skip when ops diverge enough to warrant separate tools.
|
|
143
160
|
|
|
161
|
+
#### Multi-source tools and fallback chains
|
|
162
|
+
|
|
163
|
+
**Applies when:** a server aggregates multiple data sources for the same workflow, and the "best" source varies by input type, availability, or coverage. Skip for single-API servers.
|
|
164
|
+
|
|
165
|
+
When a tool's goal can be served by multiple sources, design it as a **multi-source tool** — the agent calls one tool, the handler routes to the best source (or fans out to several) internally. This is the difference between a "PubMed wrapper" and a "literature research server": `pubmed_search_articles` tries PubMed first, falls back to EuropePMC for broader coverage, then Unpaywall for open access. The agent doesn't choose which API to hit — the server makes that decision based on what works.
|
|
166
|
+
|
|
167
|
+
Two patterns:
|
|
168
|
+
|
|
169
|
+
**Source fallback chains** — try sources in priority order, fall through on failure or empty results. Best when sources cover the same data with different depth or availability. The output should indicate which source provided the data so the agent (and human) can assess provenance.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Handler pseudocode — not a real implementation
|
|
173
|
+
async handler(input, ctx) {
|
|
174
|
+
// Primary: PubMed E-utilities (authoritative, best metadata)
|
|
175
|
+
const result = await pubmedService.search(input.query);
|
|
176
|
+
if (result.items.length > 0) return { ...result, source: 'pubmed' };
|
|
177
|
+
|
|
178
|
+
// Fallback: EuropePMC (broader coverage, includes preprints)
|
|
179
|
+
const epmcResult = await epmcService.search(input.query);
|
|
180
|
+
if (epmcResult.items.length > 0) return { ...epmcResult, source: 'europepmc' };
|
|
181
|
+
|
|
182
|
+
return { items: [], source: 'none', message: 'No results from any source.' };
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Multi-source fan-out** — query multiple sources in parallel, merge results. Best when sources provide complementary data about the same entity. Use `Promise.allSettled` so one failing source doesn't tank the whole call.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// Handler pseudocode — indicator enrichment across threat intel sources
|
|
190
|
+
async handler(input, ctx) {
|
|
191
|
+
const [vt, abuse, greynoise] = await Promise.allSettled([
|
|
192
|
+
vtService.lookup(input.indicator),
|
|
193
|
+
abuseIpService.check(input.indicator),
|
|
194
|
+
greynoiseService.query(input.indicator),
|
|
195
|
+
]);
|
|
196
|
+
return {
|
|
197
|
+
indicator: input.indicator,
|
|
198
|
+
sources: {
|
|
199
|
+
virustotal: vt.status === 'fulfilled' ? vt.value : { error: vt.reason.message },
|
|
200
|
+
abuseipdb: abuse.status === 'fulfilled' ? abuse.value : { error: abuse.reason.message },
|
|
201
|
+
greynoise: greynoise.status === 'fulfilled' ? greynoise.value : { error: greynoise.reason.message },
|
|
202
|
+
},
|
|
203
|
+
// Server synthesizes a verdict from available data — the agent gets a conclusion, not raw API dumps
|
|
204
|
+
assessment: synthesizeVerdict(vt, abuse, greynoise),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
In both patterns, the tool surface is organized around what the user is doing. Sources are service-layer details — the agent sees `threat_enrich_indicator`, not `virustotal_lookup` + `abuseipdb_check` + `greynoise_query`. Mode-based dispatch by input type (e.g., `indicator_type: 'ip' | 'domain' | 'hash'`) naturally routes to different source chains per mode, since different sources cover different indicator types.
|
|
210
|
+
|
|
144
211
|
There is no fixed ceiling on tool count — tools need to earn their keep, but don't artificially limit the surface. If the domain genuinely has 20 distinct workflows, expose 20 tools.
|
|
145
212
|
|
|
146
213
|
#### Cut the surface
|
|
@@ -416,7 +483,7 @@ Skip for purely data/action-oriented servers.
|
|
|
416
483
|
|
|
417
484
|
### 7. Plan Services and Config
|
|
418
485
|
|
|
419
|
-
**Services** — one per external dependency. Init/accessor pattern. Skip if all tools are thin wrappers with no shared state.
|
|
486
|
+
**Services** — one per external dependency (or per source, for multi-source servers). Init/accessor pattern. Skip if all tools are thin wrappers with no shared state. For multi-source servers, each upstream API gets its own service with its own auth, rate limits, and retry config — tools compose across services internally, agents never see the service boundary.
|
|
420
487
|
|
|
421
488
|
**Server-as-service.** When the server IS the source of truth (knowledge graph, in-memory task tracker, local scratchpad, embedded inference wrapper), the resilience table below doesn't apply — there's no upstream to retry. The design questions shift to state management: what's tenant-scoped vs. global, what TTLs apply, what survives a restart, what the storage backend is. Plan persistence via `ctx.state` for tenant-scoped KV (auto-namespaced by `tenantId`), or use a `StorageService` provider directly when data must cross tenants. Service init still happens in `setup()`, accessed via `getMyService()` at request time. Calls within the server are local and synchronous-ish — the API-efficiency table below also doesn't apply.
|
|
422
489
|
|
|
@@ -539,6 +606,8 @@ Execute the plan using the scaffolding skills:
|
|
|
539
606
|
|
|
540
607
|
Items without an `If …:` prefix apply to every design. Conditional items only apply when the trigger fires — otherwise skip them.
|
|
541
608
|
|
|
609
|
+
- [ ] Server scope decided — workflow identified, audience sized, boundary drawn (standalone single-API vs. multi-source aggregation vs. internal-only)
|
|
610
|
+
- [ ] **If multi-source:** tool surface organized around user workflows, not API identity. Sources are service-layer details.
|
|
542
611
|
- [ ] External APIs/dependencies researched and verified (docs fetched, SDKs identified)
|
|
543
612
|
- [ ] **If wrapping an external API:** live API probed (at minimum: one list/search, one single-item GET, one error case)
|
|
544
613
|
- [ ] User goals enumerated first (3–10 outcomes agents will accomplish, scaled to domain size), then domain operations mapped as raw material
|
|
@@ -567,5 +636,6 @@ Items without an `If …:` prefix apply to every design. Conditional items only
|
|
|
567
636
|
- [ ] **If the server is itself the source of truth (no external API):** state lifecycle planned — tenant-scoped vs. global, TTLs, what survives restart, storage backend chosen
|
|
568
637
|
- [ ] **If the server has external deps or shared state:** service layer planned (or explicitly skipped with reasoning)
|
|
569
638
|
- [ ] **If services wrap external APIs:** resilience planned (retry boundary, backoff, parse classification)
|
|
639
|
+
- [ ] **If multi-source server:** each source has its own service with independent auth/retry/rate-limit config. Fallback chains or fan-out strategy documented per tool. Output includes source provenance.
|
|
570
640
|
- [ ] **If exposing a SQL/analytical workspace over tabular data is in scope:** DataCanvas considered (`api-canvas` skill) as one option before designing custom analytical state — register / query / export tools accepting an optional `canvas_id`, with `ctx.core.canvas?` reads
|
|
571
641
|
- [ ] **If the server needs runtime config:** env vars identified in `server-config.ts`
|
|
@@ -180,6 +180,7 @@ Dependency bumps:
|
|
|
180
180
|
- Not a CHANGELOG copy — terse, scannable
|
|
181
181
|
- No marketing adjectives
|
|
182
182
|
- Length is earned — two-line tags are fine for small patches
|
|
183
|
+
- **Issue backlinks:** when changes address GitHub issues, include `(#N)` references in the relevant bullets — same as the changelog entry. The backlinks render as clickable links in the GitHub Release body.
|
|
183
184
|
|
|
184
185
|
### 9. Verify end state
|
|
185
186
|
|
|
@@ -78,7 +78,8 @@ Scan specifically for:
|
|
|
78
78
|
| Config changes | New env vars, renamed keys, changed defaults |
|
|
79
79
|
| Linter rules | New definition-lint rules that may now flag existing tools/resources |
|
|
80
80
|
| New or materially-changed skills | Note new skills or workflow changes (renamed steps, new checklist items) worth surfacing at end-of-run. Don't auto-invoke — some skills (e.g. `security-pass`) are user-triggered. The per-version changelog entries (e.g. 0.6.14 calling out `skills/security-pass/ (v1.0)`) name what changed. |
|
|
81
|
-
| New template-scaffolded files | Compare `templates/` in the package against the project root. Files that `init` would create for a new project but don't exist in this project are adoption candidates — create them with project-specific values (version, name, description, env vars from `server.json`). Examples: `manifest.json`, `.mcpbignore`. Skip files the project has intentionally opted out of (documented in CLAUDE.md or a code comment). |
|
|
81
|
+
| New template-scaffolded files | Compare `templates/` in the package against the project root. Files that `init` would create for a new project but don't exist in this project are adoption candidates — create them with project-specific values (version, name, description, env vars from `server.json`). Examples: `manifest.json`, `.mcpbignore`, `.codex-plugin/`, `.claude-plugin/`. Skip files the project has intentionally opted out of (documented in CLAUDE.md or a code comment). |
|
|
82
|
+
| Changelog `agent-notes` | Read `agent-notes` frontmatter from each new per-version changelog file — these carry release-specific adoption instructions for downstream consumers (new files to create, fields to populate, one-time migration steps). Apply them alongside other adoption work in Step 6. |
|
|
82
83
|
|
|
83
84
|
Cross-reference each finding against the server's code. Collect adoption opportunities for Step 6.
|
|
84
85
|
|
|
@@ -165,7 +165,24 @@ Never hand-edit `CHANGELOG.md` when using this pattern — it's a build artifact
|
|
|
165
165
|
|
|
166
166
|
**Monolithic** — maintain `CHANGELOG.md` directly in [Keep a Changelog](https://keepachangelog.com/) format. To collapse from the template default: delete the `changelog/` directory, remove `changelog:build` and `changelog:check` from `package.json` scripts (and from `devcheck.config.json` if referenced), and drop `"changelog/"` from the `files` array. The `release` skill's directory-specific steps then don't apply — just edit `CHANGELOG.md` and bump version at release time.
|
|
167
167
|
|
|
168
|
-
### 10.
|
|
168
|
+
### 10. Plugin Metadata (Codex / Claude Code)
|
|
169
|
+
|
|
170
|
+
If `.codex-plugin/plugin.json` exists, verify it's populated and in sync with `package.json` and `server.json`:
|
|
171
|
+
|
|
172
|
+
- `name` matches `package.json` `name`
|
|
173
|
+
- `version` matches `package.json` `version`
|
|
174
|
+
- `description` matches `package.json` `description`
|
|
175
|
+
- `repository` matches `package.json` `repository.url`
|
|
176
|
+
- `license` matches `package.json` `license`
|
|
177
|
+
- `interface.displayName` = `package.json` `name`
|
|
178
|
+
- `interface.shortDescription` matches `package.json` `description`
|
|
179
|
+
- `interface.category` is set to a meaningful category
|
|
180
|
+
|
|
181
|
+
If `.codex-plugin/mcp.json` exists, verify the server name key matches `package.json` `name` and env vars include any required API keys from the server config schema.
|
|
182
|
+
|
|
183
|
+
If `.claude-plugin/plugin.json` exists, apply the same checks: `name`, `version`, `description`, `repository`, `license` from `package.json`. Verify the inline `mcpServers` entry key matches `package.json` `name` and env vars include any required API keys.
|
|
184
|
+
|
|
185
|
+
### 11. MCPB Bundling Artifacts
|
|
169
186
|
|
|
170
187
|
If the project ships as an `.mcpb` bundle for Claude Desktop (check for `manifest.json` at the project root), verify the full artifact set is present and consistent. If the project doesn't ship `.mcpb` bundles, skip this step.
|
|
171
188
|
|
|
@@ -196,11 +213,11 @@ If the project ships as an `.mcpb` bundle for Claude Desktop (check for `manifes
|
|
|
196
213
|
- See `references/readme.md` for badge format and config generation commands
|
|
197
214
|
- See the **Bundling** section of `templates/CLAUDE.md` for `base64` / `encodeURIComponent` generation
|
|
198
215
|
|
|
199
|
-
###
|
|
216
|
+
### 12. `LICENSE`
|
|
200
217
|
|
|
201
218
|
Confirm a license file exists. If not, ask the user which license to use (default: Apache-2.0, matching the scaffolded `package.json`). Create the file.
|
|
202
219
|
|
|
203
|
-
###
|
|
220
|
+
### 13. `Dockerfile`
|
|
204
221
|
|
|
205
222
|
If a `Dockerfile` exists, verify the OCI labels and runtime config match the actual server:
|
|
206
223
|
|
|
@@ -211,7 +228,7 @@ If a `Dockerfile` exists, verify the OCI labels and runtime config match the act
|
|
|
211
228
|
|
|
212
229
|
If no `Dockerfile` exists and the server is deployed via HTTP transport, consider scaffolding one — the template is available via `npx @cyanheads/mcp-ts-core init`.
|
|
213
230
|
|
|
214
|
-
###
|
|
231
|
+
### 14. `docs/tree.md`
|
|
215
232
|
|
|
216
233
|
Regenerate the directory structure:
|
|
217
234
|
|
|
@@ -221,7 +238,7 @@ bun run tree
|
|
|
221
238
|
|
|
222
239
|
Review the output for anything unexpected (leftover files, missing directories).
|
|
223
240
|
|
|
224
|
-
###
|
|
241
|
+
### 15. Final Verification
|
|
225
242
|
|
|
226
243
|
Run the full check suite one last time:
|
|
227
244
|
|
|
@@ -243,6 +260,9 @@ Both must pass clean.
|
|
|
243
260
|
- [ ] GitHub repo description matches `package.json` description; topics ↔ keywords in sync
|
|
244
261
|
- [ ] `bunfig.toml` present
|
|
245
262
|
- [ ] Changelog current — either monolithic `CHANGELOG.md` (hand-edited, Keep a Changelog) or directory-based (`changelog/<minor>.x/<version>.md` + rollup regenerated and in sync)
|
|
263
|
+
- [ ] `.codex-plugin/plugin.json` populated and in sync with `package.json` (if present)
|
|
264
|
+
- [ ] `.codex-plugin/mcp.json` server name and env vars current (if present)
|
|
265
|
+
- [ ] `.claude-plugin/plugin.json` populated and in sync with `package.json` (if present)
|
|
246
266
|
- [ ] MCPB artifacts consistent (if `manifest.json` present) — version synced, env vars match `server.json`, `bundle` + `lint:packaging` scripts exist, README install badges present
|
|
247
267
|
- [ ] `LICENSE` file present
|
|
248
268
|
- [ ] `Dockerfile` OCI labels and runtime config accurate (if present)
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Ship a release end-to-end across every registry the project targets (npm, MCP Registry, GitHub Releases for `.mcpb` bundles, GHCR). Runs the final verification gate, pushes commits and tags, then publishes to each applicable destination. Assumes git wrapup (version bumps, changelog, commit, annotated tag) is already complete — this skill is the post-wrapup publish workflow. Retries transient network failures on publish steps; halts with a partial-state report when retries are exhausted or the failure is terminal.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.7"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -131,11 +131,12 @@ Halt on any publisher error other than "cannot publish duplicate version".
|
|
|
131
131
|
|
|
132
132
|
Only if `manifest.json` exists at the repo root (otherwise skip).
|
|
133
133
|
|
|
134
|
-
Build the bundle, then create a Release on the existing annotated tag and attach the `.mcpb`. The Release sits on top of the tag from wrapup — `--verify-tag` enforces that the tag already exists on the remote and prevents `gh` from creating a lightweight tag that would shadow the annotated one. `--notes-from-tag` pulls release notes
|
|
134
|
+
Build the bundle, then create a Release on the existing annotated tag and attach the `.mcpb`. The Release sits on top of the tag from wrapup — `--verify-tag` enforces that the tag already exists on the remote and prevents `gh` from creating a lightweight tag that would shadow the annotated one. `--notes-from-tag` pulls the tag annotation body as release notes. `--title` sets the release title from the tag subject — `--notes-from-tag` alone does NOT set the title (it defaults to the bare tag name, e.g. "v0.1.8" with no theme). The tag subject already omits the version number per the git-wrapup skill, so prepending `v<VERSION>:` produces the correct display title.
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
137
|
bun run bundle # produces dist/<name>.mcpb (stable filename, no version)
|
|
138
|
-
|
|
138
|
+
SUBJECT=$(git tag -l --format='%(contents:subject)' v<VERSION>)
|
|
139
|
+
gh release create v<VERSION> --verify-tag --notes-from-tag --title "v<VERSION>: $SUBJECT" dist/*.mcpb
|
|
139
140
|
```
|
|
140
141
|
|
|
141
142
|
The stable filename matters: it lets the README "Install in Claude Desktop" badge point at `releases/latest/download/<name>.mcpb` and always resolve to the most recent release. The `bundle` script in the templates outputs `dist/{{PACKAGE_NAME}}.mcpb` for this reason.
|
|
@@ -178,7 +179,7 @@ If the project uses a non-GHCR registry or a custom image name, respect the proj
|
|
|
178
179
|
Print clickable URLs for every destination that succeeded:
|
|
179
180
|
|
|
180
181
|
- npm: `https://www.npmjs.com/package/<package.json#name>/v/<version>`
|
|
181
|
-
- MCP Registry: `https://registry.modelcontextprotocol.io/v0/servers
|
|
182
|
+
- MCP Registry: `https://registry.modelcontextprotocol.io/v0.1/servers/<mcpName>/versions/<version>` — `mcpName` is the `name` field from `server.json` (URL-encode the `/` as `%2F`)
|
|
182
183
|
- GitHub Release: `https://github.com/<OWNER>/<REPO>/releases/tag/v<VERSION>` (with `.mcpb` asset attached)
|
|
183
184
|
- GHCR: `ghcr.io/<OWNER>/<REPO>:<VERSION>`
|
|
184
185
|
|
|
@@ -189,7 +190,7 @@ Skip any destination that was skipped in its step.
|
|
|
189
190
|
Confirm each published artifact is actually live — don't rely on a successful push exit code alone. For each destination that succeeded:
|
|
190
191
|
|
|
191
192
|
- **npm**: `npm view <package.json#name>@<version> version` — must return the version string
|
|
192
|
-
- **MCP Registry**: `curl -s "https://registry.modelcontextprotocol.io/v0/servers
|
|
193
|
+
- **MCP Registry**: `curl -s "https://registry.modelcontextprotocol.io/v0.1/servers/<mcpName>/versions/<version>"` — must return HTTP 200 with `server.version` matching `<version>` (`mcpName` is the `name` field from `server.json`; URL-encode `/` as `%2F`). The search endpoint (`/v0.1/servers?search=`) paginates and may not include the latest version for packages with many releases — always use the direct version lookup.
|
|
193
194
|
- **GitHub Release**: `gh release view v<VERSION> -R <OWNER>/<REPO> --json assets --jq '.assets[].name'` — must list the `.mcpb` file
|
|
194
195
|
- **GHCR**: fetch an anonymous bearer token, then `curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" "https://ghcr.io/v2/<OWNER>/<REPO>/manifests/<VERSION>"` — must return HTTP 200
|
|
195
196
|
|
|
@@ -205,7 +206,7 @@ If any check fails, halt and report which destination is unreachable. A successf
|
|
|
205
206
|
- [ ] Tags pushed to origin
|
|
206
207
|
- [ ] `bun publish --access public` succeeds
|
|
207
208
|
- [ ] `bun run publish-mcp` succeeds (if `server.json` present)
|
|
208
|
-
- [ ] `bun run bundle` + `gh release create --verify-tag --notes-from-tag` succeeds (if `manifest.json` present)
|
|
209
|
+
- [ ] `bun run bundle` + `gh release create --verify-tag --notes-from-tag --title` succeeds (if `manifest.json` present)
|
|
209
210
|
- [ ] Docker buildx multi-arch push succeeds (if `Dockerfile` present)
|
|
210
211
|
- [ ] All published artifacts verified reachable (npm, MCP Registry, GH Release asset, GHCR manifest)
|
|
211
212
|
- [ ] On re-invocation: idempotent-success signals recognized for already-published destinations
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": ""
|
|
7
|
+
},
|
|
8
|
+
"homepage": "",
|
|
9
|
+
"repository": "",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"{{PACKAGE_NAME}}": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "{{PACKAGE_NAME}}"],
|
|
15
|
+
"env": {
|
|
16
|
+
"MCP_TRANSPORT_TYPE": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "",
|
|
7
|
+
"email": "",
|
|
8
|
+
"url": ""
|
|
9
|
+
},
|
|
10
|
+
"homepage": "",
|
|
11
|
+
"repository": "",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"keywords": ["mcp", "mcp-server", "model-context-protocol"],
|
|
14
|
+
"mcpServers": "./.codex-plugin/mcp.json",
|
|
15
|
+
"interface": {
|
|
16
|
+
"displayName": "{{PACKAGE_NAME}}",
|
|
17
|
+
"shortDescription": "",
|
|
18
|
+
"longDescription": "",
|
|
19
|
+
"developerName": "",
|
|
20
|
+
"category": "",
|
|
21
|
+
"capabilities": ["Read"],
|
|
22
|
+
"websiteURL": "",
|
|
23
|
+
"defaultPrompt": []
|
|
24
|
+
}
|
|
25
|
+
}
|
package/templates/AGENTS.md
CHANGED
|
@@ -369,6 +369,8 @@ security: false # optional — true flags security fi
|
|
|
369
369
|
|
|
370
370
|
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
371
371
|
|
|
372
|
+
`agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
|
|
373
|
+
|
|
372
374
|
**Section order** (Keep a Changelog): Added, Changed, Deprecated, Removed, Fixed, Security. Include only sections with entries — don't ship empty headers.
|
|
373
375
|
|
|
374
376
|
**Tag annotations** render as GitHub Release bodies via `--notes-from-tag`. They must be structured markdown — never a flat comma-separated string. Subject omits the version number (GitHub prepends it). See `changelog/template.md` for the full format reference.
|
|
@@ -401,4 +403,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
401
403
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
402
404
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
403
405
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
406
|
+
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
|
|
407
|
+
- [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
|
|
408
|
+
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
|
|
404
409
|
- [ ] `npm run devcheck` passes
|
package/templates/CLAUDE.md
CHANGED
|
@@ -50,6 +50,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
50
50
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
51
51
|
- **Check `ctx.elicit` / `ctx.sample`** for presence before calling.
|
|
52
52
|
- **Secrets in env vars only** — never hardcoded.
|
|
53
|
+
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
53
54
|
|
|
54
55
|
---
|
|
55
56
|
|
|
@@ -369,6 +370,8 @@ security: false # optional — true flags security fi
|
|
|
369
370
|
|
|
370
371
|
`breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
|
|
371
372
|
|
|
373
|
+
`agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
|
|
374
|
+
|
|
372
375
|
**Section order** (Keep a Changelog): Added, Changed, Deprecated, Removed, Fixed, Security. Include only sections with entries — don't ship empty headers.
|
|
373
376
|
|
|
374
377
|
**Tag annotations** render as GitHub Release bodies via `--notes-from-tag`. They must be structured markdown — never a flat comma-separated string. Subject omits the version number (GitHub prepends it). See `changelog/template.md` for the full format reference.
|
|
@@ -401,4 +404,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
401
404
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
402
405
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
403
406
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
407
|
+
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = package name; `interface.shortDescription` from `package.json` description
|
|
408
|
+
- [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
|
|
409
|
+
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
|
|
404
410
|
- [ ] `npm run devcheck` passes
|
|
@@ -19,6 +19,14 @@ breaking: false
|
|
|
19
19
|
# `## Security` section below. Flagged as `Security` in the rollup so
|
|
20
20
|
# users can triage upgrade urgency at a glance.
|
|
21
21
|
security: false
|
|
22
|
+
|
|
23
|
+
# Optional free-form notes for maintenance agents processing this release.
|
|
24
|
+
# Not rendered in CHANGELOG — consumed by agents running `maintenance` on
|
|
25
|
+
# downstream servers. Use for adoption instructions that don't fit the
|
|
26
|
+
# human-facing sections: new files to create, fields to populate, one-time
|
|
27
|
+
# migration steps. Omit the field entirely when there's nothing to say.
|
|
28
|
+
# agent-notes: |
|
|
29
|
+
# <instructions for downstream maintenance agents>
|
|
22
30
|
---
|
|
23
31
|
|
|
24
32
|
# <version> — YYYY-MM-DD
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview One-shot migration: split the monolithic CHANGELOG.md into
|
|
4
|
-
* per-version files under `changelog/<major.minor>.x/<version>.md`.
|
|
5
|
-
*
|
|
6
|
-
* After the initial migration, this script is no longer needed — the
|
|
7
|
-
* per-version files become the source of truth and `scripts/build-changelog.ts`
|
|
8
|
-
* regenerates CHANGELOG.md from them. Keep this around only long enough to
|
|
9
|
-
* verify the round-trip (split → build → diff).
|
|
10
|
-
*
|
|
11
|
-
* Behavior:
|
|
12
|
-
* • Reads CHANGELOG.md, splits on `## [X.Y.Z] - YYYY-MM-DD` headers
|
|
13
|
-
* • Groups by minor series: 0.1.4 → changelog/0.1.x/0.1.4.md
|
|
14
|
-
* • Writes each version block with an H1 heading
|
|
15
|
-
* • Creates changelog/template.md template if missing
|
|
16
|
-
* • Overwrites existing per-version files (idempotent)
|
|
17
|
-
* • Drops the preamble (title + description) — that's handled by the build script
|
|
18
|
-
*
|
|
19
|
-
* Usage: bun run scripts/split-changelog.ts
|
|
20
|
-
*
|
|
21
|
-
* @module scripts/split-changelog
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
-
import { resolve } from 'node:path';
|
|
26
|
-
|
|
27
|
-
const CHANGELOG_PATH = resolve('CHANGELOG.md');
|
|
28
|
-
const CHANGELOG_DIR = resolve('changelog');
|
|
29
|
-
const TEMPLATE_PATH = resolve(CHANGELOG_DIR, 'template.md');
|
|
30
|
-
|
|
31
|
-
const TEMPLATE_CONTENT = `# <version> — YYYY-MM-DD
|
|
32
|
-
|
|
33
|
-
<!-- Brief summary of the upcoming release — delete this comment when filled in. -->
|
|
34
|
-
|
|
35
|
-
## Added
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
## Changed
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
## Fixed
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
`;
|
|
47
|
-
|
|
48
|
-
interface Section {
|
|
49
|
-
body: string;
|
|
50
|
-
date: string;
|
|
51
|
-
version: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function extractSections(content: string): Section[] {
|
|
55
|
-
const regex = /^## \[([^\]]+)\] - (\d{4}-\d{2}-\d{2})$/gm;
|
|
56
|
-
const matches = [...content.matchAll(regex)];
|
|
57
|
-
if (matches.length === 0) {
|
|
58
|
-
throw new Error('No version sections found in CHANGELOG.md (expected ## [X.Y.Z] - YYYY-MM-DD)');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const sections: Section[] = [];
|
|
62
|
-
for (let i = 0; i < matches.length; i++) {
|
|
63
|
-
const match = matches[i] as RegExpMatchArray & { index: number };
|
|
64
|
-
const next = matches[i + 1] as (RegExpMatchArray & { index: number }) | undefined;
|
|
65
|
-
const start = match.index + match[0].length;
|
|
66
|
-
const end = next ? next.index : content.length;
|
|
67
|
-
|
|
68
|
-
let body = content.slice(start, end);
|
|
69
|
-
body = body.replace(/^\n+/, '');
|
|
70
|
-
body = body.replace(/\n+---\n*$/, '\n');
|
|
71
|
-
body = body.replace(/\n*$/, '\n');
|
|
72
|
-
|
|
73
|
-
sections.push({
|
|
74
|
-
version: match[1] as string,
|
|
75
|
-
date: match[2] as string,
|
|
76
|
-
body,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return sections;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Promote heading levels by stripping one `#` from H2+ lines. Assumes no H1
|
|
84
|
-
* or H2 in section bodies (the version H2 was already stripped); the existing
|
|
85
|
-
* CHANGELOG.md uses H3 for Added/Changed/Fixed etc., which become H2 here.
|
|
86
|
-
*/
|
|
87
|
-
function promoteHeadings(body: string): string {
|
|
88
|
-
return body.replace(/^(#{2,6}) /gm, (_, hashes: string) => `${hashes.slice(1)} `);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function toPerVersionFile(section: Section): string {
|
|
92
|
-
return `# ${section.version} — ${section.date}\n\n${promoteHeadings(section.body)}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Extract the `major.minor.x` series directory for a version string. */
|
|
96
|
-
function seriesOf(version: string): string {
|
|
97
|
-
const [major, minor] = version.split('.');
|
|
98
|
-
if (!major || !minor) throw new Error(`Cannot derive series from version: ${version}`);
|
|
99
|
-
return `${major}.${minor}.x`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function main(): void {
|
|
103
|
-
const content = readFileSync(CHANGELOG_PATH, 'utf-8');
|
|
104
|
-
const sections = extractSections(content);
|
|
105
|
-
|
|
106
|
-
mkdirSync(CHANGELOG_DIR, { recursive: true });
|
|
107
|
-
|
|
108
|
-
const seriesCounts = new Map<string, number>();
|
|
109
|
-
|
|
110
|
-
for (const section of sections) {
|
|
111
|
-
const series = seriesOf(section.version);
|
|
112
|
-
const seriesDir = resolve(CHANGELOG_DIR, series);
|
|
113
|
-
mkdirSync(seriesDir, { recursive: true });
|
|
114
|
-
const path = resolve(seriesDir, `${section.version}.md`);
|
|
115
|
-
writeFileSync(path, toPerVersionFile(section));
|
|
116
|
-
seriesCounts.set(series, (seriesCounts.get(series) ?? 0) + 1);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const [series, count] of [...seriesCounts.entries()].sort()) {
|
|
120
|
-
console.log(` + changelog/${series}/ (${count} file${count === 1 ? '' : 's'})`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!existsSync(TEMPLATE_PATH)) {
|
|
124
|
-
writeFileSync(TEMPLATE_PATH, TEMPLATE_CONTENT);
|
|
125
|
-
console.log(' + changelog/template.md (format reference)');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
console.log(`\nSplit ${sections.length} versions into ${seriesCounts.size} series.`);
|
|
129
|
-
console.log('Next: `bun run scripts/build-changelog.ts` to regenerate CHANGELOG.md,');
|
|
130
|
-
console.log(' then `diff` against the original to verify byte-equality.');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
main();
|