@cyanheads/mcp-ts-core 0.5.3 → 0.6.0

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.
Files changed (143) hide show
  1. package/CLAUDE.md +41 -2
  2. package/README.md +1 -1
  3. package/changelog/0.1.x/0.1.0.md +78 -0
  4. package/changelog/0.1.x/0.1.1.md +28 -0
  5. package/changelog/0.1.x/0.1.10.md +32 -0
  6. package/changelog/0.1.x/0.1.11.md +51 -0
  7. package/changelog/0.1.x/0.1.12.md +21 -0
  8. package/changelog/0.1.x/0.1.13.md +16 -0
  9. package/changelog/0.1.x/0.1.14.md +20 -0
  10. package/changelog/0.1.x/0.1.15.md +24 -0
  11. package/changelog/0.1.x/0.1.16.md +17 -0
  12. package/changelog/0.1.x/0.1.17.md +14 -0
  13. package/changelog/0.1.x/0.1.18.md +18 -0
  14. package/changelog/0.1.x/0.1.19.md +19 -0
  15. package/changelog/0.1.x/0.1.2.md +25 -0
  16. package/changelog/0.1.x/0.1.20.md +21 -0
  17. package/changelog/0.1.x/0.1.21.md +17 -0
  18. package/changelog/0.1.x/0.1.22.md +28 -0
  19. package/changelog/0.1.x/0.1.23.md +23 -0
  20. package/changelog/0.1.x/0.1.24.md +17 -0
  21. package/changelog/0.1.x/0.1.25.md +16 -0
  22. package/changelog/0.1.x/0.1.26.md +22 -0
  23. package/changelog/0.1.x/0.1.27.md +30 -0
  24. package/changelog/0.1.x/0.1.28.md +16 -0
  25. package/changelog/0.1.x/0.1.29.md +19 -0
  26. package/changelog/0.1.x/0.1.3.md +22 -0
  27. package/changelog/0.1.x/0.1.4.md +17 -0
  28. package/changelog/0.1.x/0.1.5.md +25 -0
  29. package/changelog/0.1.x/0.1.6.md +26 -0
  30. package/changelog/0.1.x/0.1.7.md +29 -0
  31. package/changelog/0.1.x/0.1.8.md +33 -0
  32. package/changelog/0.1.x/0.1.9.md +19 -0
  33. package/changelog/0.2.x/0.2.0.md +32 -0
  34. package/changelog/0.2.x/0.2.1.md +12 -0
  35. package/changelog/0.2.x/0.2.10.md +38 -0
  36. package/changelog/0.2.x/0.2.11.md +29 -0
  37. package/changelog/0.2.x/0.2.12.md +31 -0
  38. package/changelog/0.2.x/0.2.2.md +19 -0
  39. package/changelog/0.2.x/0.2.3.md +15 -0
  40. package/changelog/0.2.x/0.2.4.md +24 -0
  41. package/changelog/0.2.x/0.2.5.md +27 -0
  42. package/changelog/0.2.x/0.2.6.md +23 -0
  43. package/changelog/0.2.x/0.2.7.md +23 -0
  44. package/changelog/0.2.x/0.2.8.md +12 -0
  45. package/changelog/0.2.x/0.2.9.md +25 -0
  46. package/changelog/0.3.x/0.3.0.md +45 -0
  47. package/changelog/0.3.x/0.3.1.md +16 -0
  48. package/changelog/0.3.x/0.3.2.md +24 -0
  49. package/changelog/0.3.x/0.3.3.md +31 -0
  50. package/changelog/0.3.x/0.3.4.md +31 -0
  51. package/changelog/0.3.x/0.3.5.md +32 -0
  52. package/changelog/0.3.x/0.3.6.md +48 -0
  53. package/changelog/0.3.x/0.3.7.md +23 -0
  54. package/changelog/0.3.x/0.3.8.md +21 -0
  55. package/changelog/0.4.x/0.4.0.md +38 -0
  56. package/changelog/0.4.x/0.4.1.md +31 -0
  57. package/changelog/0.5.x/0.5.0.md +29 -0
  58. package/changelog/0.5.x/0.5.1.md +18 -0
  59. package/changelog/0.5.x/0.5.2.md +38 -0
  60. package/changelog/0.5.x/0.5.3.md +26 -0
  61. package/changelog/0.5.x/0.5.4.md +29 -0
  62. package/changelog/0.6.x/0.6.0.md +39 -0
  63. package/changelog/unreleased.md +40 -0
  64. package/dist/cli/init.js +1 -0
  65. package/dist/cli/init.js.map +1 -1
  66. package/dist/core/app.d.ts +13 -3
  67. package/dist/core/app.d.ts.map +1 -1
  68. package/dist/core/app.js +20 -13
  69. package/dist/core/app.js.map +1 -1
  70. package/dist/core/index.d.ts +1 -0
  71. package/dist/core/index.d.ts.map +1 -1
  72. package/dist/core/index.js.map +1 -1
  73. package/dist/core/serverManifest.d.ts +237 -0
  74. package/dist/core/serverManifest.d.ts.map +1 -0
  75. package/dist/core/serverManifest.js +310 -0
  76. package/dist/core/serverManifest.js.map +1 -0
  77. package/dist/core/worker.d.ts.map +1 -1
  78. package/dist/core/worker.js +2 -2
  79. package/dist/core/worker.js.map +1 -1
  80. package/dist/linter/rules/landing-rules.d.ts +15 -0
  81. package/dist/linter/rules/landing-rules.d.ts.map +1 -0
  82. package/dist/linter/rules/landing-rules.js +125 -0
  83. package/dist/linter/rules/landing-rules.js.map +1 -0
  84. package/dist/linter/types.d.ts +5 -2
  85. package/dist/linter/types.d.ts.map +1 -1
  86. package/dist/linter/validate.d.ts.map +1 -1
  87. package/dist/linter/validate.js +26 -2
  88. package/dist/linter/validate.js.map +1 -1
  89. package/dist/mcp-server/transports/http/httpTransport.d.ts +4 -3
  90. package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
  91. package/dist/mcp-server/transports/http/httpTransport.js +47 -26
  92. package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
  93. package/dist/mcp-server/transports/http/httpTypes.d.ts +0 -12
  94. package/dist/mcp-server/transports/http/httpTypes.d.ts.map +1 -1
  95. package/dist/mcp-server/transports/http/landing-page.d.ts +48 -0
  96. package/dist/mcp-server/transports/http/landing-page.d.ts.map +1 -0
  97. package/dist/mcp-server/transports/http/landing-page.js +912 -0
  98. package/dist/mcp-server/transports/http/landing-page.js.map +1 -0
  99. package/dist/mcp-server/transports/http/serverCard.d.ts +67 -0
  100. package/dist/mcp-server/transports/http/serverCard.d.ts.map +1 -0
  101. package/dist/mcp-server/transports/http/serverCard.js +91 -0
  102. package/dist/mcp-server/transports/http/serverCard.js.map +1 -0
  103. package/dist/mcp-server/transports/manager.d.ts +3 -3
  104. package/dist/mcp-server/transports/manager.d.ts.map +1 -1
  105. package/dist/mcp-server/transports/manager.js +4 -4
  106. package/dist/mcp-server/transports/manager.js.map +1 -1
  107. package/dist/utils/formatting/html.d.ts +76 -0
  108. package/dist/utils/formatting/html.d.ts.map +1 -0
  109. package/dist/utils/formatting/html.js +111 -0
  110. package/dist/utils/formatting/html.js.map +1 -0
  111. package/dist/utils/formatting/index.d.ts +1 -0
  112. package/dist/utils/formatting/index.d.ts.map +1 -1
  113. package/dist/utils/formatting/index.js +1 -0
  114. package/dist/utils/formatting/index.js.map +1 -1
  115. package/dist/utils/index.d.ts +1 -1
  116. package/dist/utils/index.d.ts.map +1 -1
  117. package/dist/utils/index.js +1 -1
  118. package/dist/utils/index.js.map +1 -1
  119. package/package.json +5 -1
  120. package/scripts/build-changelog.ts +222 -0
  121. package/scripts/devcheck.ts +19 -4
  122. package/scripts/tree.ts +3 -0
  123. package/skills/add-app-tool/SKILL.md +2 -4
  124. package/skills/add-prompt/SKILL.md +2 -4
  125. package/skills/add-resource/SKILL.md +2 -4
  126. package/skills/add-service/SKILL.md +2 -4
  127. package/skills/add-tool/SKILL.md +6 -5
  128. package/skills/api-context/SKILL.md +2 -2
  129. package/skills/api-linter/SKILL.md +391 -0
  130. package/skills/api-services/SKILL.md +1 -1
  131. package/skills/api-services/references/graph.md +1 -1
  132. package/skills/api-utils/SKILL.md +1 -1
  133. package/skills/api-utils/references/parsing.md +1 -1
  134. package/skills/api-utils/references/security.md +1 -1
  135. package/skills/design-mcp-server/SKILL.md +2 -2
  136. package/skills/maintenance/SKILL.md +12 -11
  137. package/skills/polish-docs-meta/SKILL.md +24 -9
  138. package/skills/release/SKILL.md +21 -7
  139. package/skills/setup/SKILL.md +4 -8
  140. package/templates/AGENTS.md +23 -1
  141. package/templates/CLAUDE.md +23 -1
  142. package/templates/changelog/unreleased.md +40 -0
  143. package/templates/package.json +3 -0
@@ -0,0 +1,391 @@
1
+ ---
2
+ name: api-linter
3
+ description: >
4
+ MCP definition linter rules reference. Use when `bun run lint:mcp`, `bun run devcheck`, or `createApp()` startup reports a lint error or warning (`format-parity`, `schema-is-object`, `name-format`, `server-json-*`, etc.) and you need to understand the rule, its severity, and how to fix it. Every rule ID the linter emits has an entry in this doc.
5
+ metadata:
6
+ author: cyanheads
7
+ version: "1.0"
8
+ audience: external
9
+ type: reference
10
+ ---
11
+
12
+ ## Overview
13
+
14
+ The linter validates tool, resource, and prompt definitions against the MCP spec and framework conventions. It runs in three places:
15
+
16
+ | Entry point | When | On failure |
17
+ |:------------|:-----|:-----------|
18
+ | `createApp()` / `createWorkerHandler()` | Every startup | Throws `ConfigurationError`; process exits with a formatted banner. Warnings are logged and startup continues. |
19
+ | `bun run lint:mcp` | Manual or CI | Prints errors + warnings, exits non-zero on errors. |
20
+ | `bun run devcheck` | Pre-commit workflow | Wraps `lint:mcp` alongside typecheck, format, `bun audit`, `bun outdated`. |
21
+
22
+ All three surface the same `LintReport` from `validateDefinitions()` (exported from `@cyanheads/mcp-ts-core/linter`). Each diagnostic has a stable `rule` ID — that's the anchor you land on via the `See: skills/api-linter/SKILL.md#<rule>` breadcrumb appended to every message.
23
+
24
+ **Severity:**
25
+ - **error** — MUST-level spec violation; blocks startup.
26
+ - **warning** — SHOULD-level or quality issue; logged but startup continues.
27
+
28
+ **Imports (if you need to run the linter programmatically):**
29
+
30
+ ```ts
31
+ import { validateDefinitions } from '@cyanheads/mcp-ts-core/linter';
32
+ import type { LintReport, LintDiagnostic } from '@cyanheads/mcp-ts-core/linter';
33
+
34
+ const report = validateDefinitions({ tools, resources, prompts, serverJson, packageJson });
35
+ if (!report.passed) process.exit(1);
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Rule index
41
+
42
+ Grouped by family. Jump to any rule ID via its anchor.
43
+
44
+ | Family | Rules | Section |
45
+ |:-------|:------|:--------|
46
+ | Format parity | `format-parity`, `format-parity-threw`, `format-parity-walk-failed` | [Format parity](#format-parity) |
47
+ | Schema | `schema-is-object`, `describe-on-fields`, `schema-serializable` | [Schema rules](#schema-rules) |
48
+ | Names | `name-required`, `name-format`, `name-unique` | [Name rules](#name-rules) |
49
+ | Tools | `description-required`, `handler-required`, `auth-type`, `auth-scope-format`, `annotation-type`, `annotation-coherence`, `meta-ui-type`, `meta-ui-resource-uri-required`, `meta-ui-resource-uri-scheme`, `app-tool-resource-pairing` | [Tool rules](#tool-rules) |
50
+ | Resources | `uri-template-required`, `uri-template-valid`, `resource-name-not-uri`, `template-params-align` | [Resource rules](#resource-rules) |
51
+ | Prompts | `generate-required` | [Prompt rules](#prompt-rules) |
52
+ | server.json | ~40 rules prefixed `server-json-*` | [server.json rules](#server-json-rules) |
53
+
54
+ ---
55
+
56
+ ## Format parity
57
+
58
+ Why this family exists: different MCP clients forward different surfaces of a tool response to the model. Claude Code reads `structuredContent` (from your handler's return value, typed by `output`). Claude Desktop reads `content[]` (from your `format()` function). Every field must be visible on both surfaces or one class of client sees less than another. The linter enforces this by synthesizing a sample value where every leaf is a uniquely identifiable sentinel, calling `format()` once, then verifying each sentinel (or its key name, for permissive types like booleans) appears in the rendered text.
59
+
60
+ ### format-parity
61
+
62
+ **Severity:** error
63
+
64
+ Fires when `format()` does not render a field present in `output`. Emitted once per missing field; large schemas can produce many `format-parity` diagnostics from a single tool.
65
+
66
+ **Primary fix:** render the missing field in `format()`. For tools that return either a summary list or a detail view, use `z.discriminatedUnion` so each branch is walked separately:
67
+
68
+ ```ts
69
+ output: z.discriminatedUnion('mode', [
70
+ z.object({ mode: z.literal('list'), items: z.array(ItemSchema) }),
71
+ z.object({ mode: z.literal('detail'), item: ItemSchema, history: z.array(HistoryEntry) }),
72
+ ]),
73
+
74
+ format: (result) => {
75
+ if (result.mode === 'list') return renderList(result.items);
76
+ return renderDetail(result.item, result.history);
77
+ }
78
+ ```
79
+
80
+ **Escape hatch:** if the output schema was over-typed for a genuinely dynamic upstream API (e.g., a third-party JSON blob whose shape you can't nail down), relax it:
81
+
82
+ ```ts
83
+ output: z.object({}).passthrough()
84
+ ```
85
+
86
+ `passthrough()` still flows the full payload to `structuredContent` without declaring each field, so the linter has nothing to check against and you're not maintaining aspirational typing.
87
+
88
+ **Anti-pattern:** summary-only `format()` like `return [{ type: 'text', text: \`Found ${n} items\` }]`. The sentinel walk will flag every field in the items array. Don't "fix" this by removing fields from `output` — that makes `structuredContent` clients blind too.
89
+
90
+ ### format-parity-threw
91
+
92
+ **Severity:** warning
93
+
94
+ Fires when `format()` throws while being called with a synthetic sample. The linter cannot verify parity because your formatter crashed before producing output.
95
+
96
+ **Fix:** `format()` must be **total** — render any valid value of the output schema without throwing. Common causes:
97
+
98
+ - Assuming an optional array is always present (`result.items.map(...)` when `items` could be `undefined`)
99
+ - Dereferencing a discriminated-union branch without checking the discriminator
100
+ - Calling `toFixed()` or `toISOString()` on a value that could legitimately be any number/string
101
+
102
+ Add narrow guards. The linter feeds a synthetic but schema-valid value; if your formatter can't handle it, real inputs will eventually hit the same path.
103
+
104
+ ### format-parity-walk-failed
105
+
106
+ **Severity:** warning
107
+
108
+ Fires when the linter cannot walk the output schema to build a synthetic sample (usually because the schema uses an unusual composition the walker doesn't recognize). Parity is not verified for that tool — nothing is broken at runtime, but the check is silently disabled.
109
+
110
+ **Fix:** inspect the walker error message in the diagnostic. Usually caused by very deep recursion, custom Zod extensions, or mixing Zod 3 and 4 schema internals. File an issue against `@cyanheads/mcp-ts-core` with the schema shape — this is a linter gap, not user error.
111
+
112
+ ---
113
+
114
+ ## Schema rules
115
+
116
+ ### schema-is-object
117
+
118
+ **Severity:** error
119
+
120
+ Tool `input`/`output` and prompt `args` must be `z.object({...})` at the top level (not `z.string()`, `z.array(...)`, etc.). The MCP spec requires a keyed structure at the schema root.
121
+
122
+ **Fix:** wrap whatever you had in a single-key object:
123
+
124
+ ```ts
125
+ // Wrong
126
+ input: z.array(z.string())
127
+ // Right
128
+ input: z.object({ items: z.array(z.string()).describe('List of items') })
129
+ ```
130
+
131
+ ### describe-on-fields
132
+
133
+ **Severity:** warning
134
+
135
+ Every field in `input`, `output`, `params`, or `args` needs a `.describe('...')` call. Descriptions ship to the client and the LLM — missing ones make tools harder to use correctly.
136
+
137
+ **Fix:** add `.describe('...')` to every leaf. The linter reports which path is missing a description (e.g., `input.filters.status`), so it's a mechanical fix.
138
+
139
+ ### schema-serializable
140
+
141
+ **Severity:** error
142
+
143
+ Input/output schemas must use JSON-Schema-serializable Zod types only. The MCP SDK converts schemas to JSON Schema for `tools/list`; non-serializable types cause a hard runtime failure.
144
+
145
+ **Disallowed:** `z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`.
146
+
147
+ **Fix:** use structural equivalents. Most common swap:
148
+
149
+ ```ts
150
+ // Wrong
151
+ z.date()
152
+ // Right
153
+ z.string().describe('ISO 8601 timestamp, e.g., 2026-04-20T12:00:00Z')
154
+ ```
155
+
156
+ Parse the string to a `Date` inside the handler if you need one.
157
+
158
+ ---
159
+
160
+ ## Name rules
161
+
162
+ ### name-required
163
+
164
+ **Severity:** error
165
+
166
+ Every tool, resource, and prompt definition needs a non-empty `name` string. For resources, an empty `name` also falls back to the URI template (see `resource-name-not-uri`).
167
+
168
+ ### name-format
169
+
170
+ **Severity:** error
171
+
172
+ Names must match `^[a-zA-Z0-9._-]+$` (alphanumerics, dots, hyphens, underscores). Tools conventionally use `snake_case`, resources and prompts use `kebab-case` or `snake_case`.
173
+
174
+ **Fix:** rename to a valid identifier. If the legacy name is user-facing, keep `title` as the display string and use a valid `name` internally.
175
+
176
+ ### name-unique
177
+
178
+ **Severity:** error
179
+
180
+ Tool names, resource names, and prompt names must each be unique within their type. Duplicates would cause the client to see only one.
181
+
182
+ **Fix:** rename one, or consolidate into a single definition if they're actually the same tool.
183
+
184
+ ---
185
+
186
+ ## Tool rules
187
+
188
+ ### description-required
189
+
190
+ **Severity:** warning
191
+
192
+ Every tool, resource, and prompt needs a non-empty `description`. This is what the client shows the LLM to decide whether to call the definition. A missing description dramatically hurts selection accuracy.
193
+
194
+ Also applies to resources and prompts (same rule ID, different `definitionType`).
195
+
196
+ **Fix:** write a single cohesive paragraph. Prose, not bullet lists. Descriptions render inline in most clients.
197
+
198
+ ### handler-required
199
+
200
+ **Severity:** error
201
+
202
+ Every tool must have a `handler` function (or `taskHandlers` object for task tools). Every resource must have a `handler`. Definitions without handlers can't do anything at runtime.
203
+
204
+ Also applies to resources (same rule ID, different `definitionType`).
205
+
206
+ ### auth-type
207
+
208
+ **Severity:** error
209
+
210
+ `auth` must be an array of strings. A single string or other shape is rejected.
211
+
212
+ ```ts
213
+ // Wrong
214
+ auth: 'tool:my_tool:read'
215
+ // Right
216
+ auth: ['tool:my_tool:read']
217
+ ```
218
+
219
+ ### auth-scope-format
220
+
221
+ **Severity:** error
222
+
223
+ Every element in `auth` must be a non-empty string. Empty strings in the array are rejected — they'd match anything.
224
+
225
+ ### annotation-type
226
+
227
+ **Severity:** warning
228
+
229
+ `annotations` hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) must be booleans. Strings like `'yes'` or numbers are rejected — the MCP spec defines these as booleans and clients may type-check.
230
+
231
+ ### annotation-coherence
232
+
233
+ **Severity:** warning
234
+
235
+ Contradictory annotation combinations. The canonical case: `readOnlyHint: true` with `destructiveHint: true` — a read-only tool cannot be destructive. `idempotentHint: true` alongside `readOnlyHint: true` is fine (explicit redundancy is allowed).
236
+
237
+ ### meta-ui-type
238
+
239
+ **Severity:** error (MCP Apps tools only)
240
+
241
+ When a tool declares `_meta.ui`, that field must be an object. `null`, arrays, or primitives are rejected.
242
+
243
+ ### meta-ui-resource-uri-required
244
+
245
+ **Severity:** error (MCP Apps tools only)
246
+
247
+ `_meta.ui.resourceUri` must be a non-empty string. This is the URI the client resolves to load the app UI.
248
+
249
+ ### meta-ui-resource-uri-scheme
250
+
251
+ **Severity:** warning (MCP Apps tools only)
252
+
253
+ `_meta.ui.resourceUri` should use the `ui://` scheme. Other schemes (like `https://`) work but are discouraged — the `ui://` convention signals the resource is meant to be hosted by the MCP server, not fetched externally.
254
+
255
+ ### app-tool-resource-pairing
256
+
257
+ **Severity:** warning (MCP Apps tools only)
258
+
259
+ An app tool's `_meta.ui.resourceUri` must match the `uriTemplate` of a registered resource. This catches the common mistake of renaming one side of the pair and forgetting the other.
260
+
261
+ **Fix:** either correct the `resourceUri` to match an existing resource, or register the resource it references. Use the `add-app-tool` skill's paired scaffold to avoid this.
262
+
263
+ ---
264
+
265
+ ## Resource rules
266
+
267
+ ### uri-template-required
268
+
269
+ **Severity:** error
270
+
271
+ Every resource needs a non-empty `uriTemplate` string. The URI template is the resource's primary identifier.
272
+
273
+ ### uri-template-valid
274
+
275
+ **Severity:** error
276
+
277
+ `uriTemplate` must be syntactically valid per RFC 6570: balanced braces, non-empty variable names. `test://{id/data` (unbalanced) and `test://{}/data` (empty variable) are rejected.
278
+
279
+ ### resource-name-not-uri
280
+
281
+ **Severity:** warning
282
+
283
+ Warns when the resource's `name` defaults to the URI template because no explicit name was provided. URIs make poor display names — clients often show them verbatim.
284
+
285
+ **Fix:** add a short `name` field:
286
+
287
+ ```ts
288
+ resource('myscheme://{id}/data', {
289
+ name: 'Item data', // <-- add this
290
+ // ...
291
+ })
292
+ ```
293
+
294
+ ### template-params-align
295
+
296
+ **Severity:** error
297
+
298
+ Every variable in the URI template must appear as a key in the `params` schema, and vice versa. `test://{itemId}/data` with `params: z.object({ item_id: ... })` is rejected — casing mismatches count.
299
+
300
+ **Fix:** rename one side so they match exactly. The error message names which variables are on which side.
301
+
302
+ ---
303
+
304
+ ## Prompt rules
305
+
306
+ ### generate-required
307
+
308
+ **Severity:** error
309
+
310
+ Every prompt needs a `generate` function that returns the message array. Prompts without `generate` have nothing to produce.
311
+
312
+ (Prompts also share `name-*` and `description-required` rules from their respective families.)
313
+
314
+ ---
315
+
316
+ ## server.json rules
317
+
318
+ Validates the `server.json` manifest at project root against the [MCP server manifest spec](https://modelcontextprotocol.io/specification). Every rule below fires only when a `server.json` is present.
319
+
320
+ | Rule ID | Severity | What it checks |
321
+ |:--------|:---------|:---------------|
322
+ | `server-json-type` | error | `server.json` must be a JSON object, not an array or primitive |
323
+ | `server-json-name-required` | error | `name` must be present and non-empty |
324
+ | `server-json-name-length` | error | `name` length 3–200 characters |
325
+ | `server-json-name-format` | error | `name` must match reverse-DNS pattern `owner/project` |
326
+ | `server-json-description-required` | error | `description` must be present and non-empty |
327
+ | `server-json-description-length` | warning | `description` > 100 chars — some registries truncate |
328
+ | `server-json-version-required` | error | `version` must be present |
329
+ | `server-json-version-length` | error | `version` length ≤ 255 |
330
+ | `server-json-version-no-range` | error | `version` must be a specific version, not a range (`^`, `~`, `>=`, etc.) |
331
+ | `server-json-version-semver` | warning | `version` should be valid semver (`major.minor.patch`) |
332
+ | `server-json-version-sync` | warning | `server.json` `version` should match `package.json` `version` |
333
+ | `server-json-repository-type` | error | `repository` must be an object |
334
+ | `server-json-repository-url` | error | `repository.url` is required when `repository` is present |
335
+ | `server-json-repository-source` | error | `repository.source` is required when `repository` is present |
336
+ | `server-json-packages-type` | error | `packages` must be an array |
337
+ | `server-json-package-type` | error | Each `packages[i]` must be an object |
338
+ | `server-json-package-registry` | error | `packages[i].registryType` is required |
339
+ | `server-json-package-identifier` | error | `packages[i].identifier` is required |
340
+ | `server-json-package-transport` | error | `packages[i].transport` is required |
341
+ | `server-json-package-no-latest` | error | `packages[i].version` must not be `"latest"` — pin a specific version |
342
+ | `server-json-package-version-sync` | warning | `packages[i].version` should match root `version` |
343
+ | `server-json-package-args-type` | error | `packages[i].packageArguments` must be an array |
344
+ | `server-json-runtime-args-type` | error | `packages[i].runtimeArguments` must be an array |
345
+ | `server-json-env-vars-type` | error | `packages[i].environmentVariables` must be an array |
346
+ | `server-json-remotes-type` | error | `remotes` must be an array |
347
+ | `server-json-remote-type` | error | Each `remotes[i]` must be an object |
348
+ | `server-json-remote-transport-type` | error | `remotes[i].type` is required |
349
+ | `server-json-remote-no-stdio` | error | `remotes[i].type` must be `streamable-http` or `sse` — `stdio` is not valid for remotes |
350
+ | `server-json-transport-type` | error | `transport` must be an object |
351
+ | `server-json-transport-type-value` | error | `transport.type` must be one of `stdio`, `streamable-http`, `sse` |
352
+ | `server-json-transport-url-required` | error | `transport.url` required for `streamable-http` and `sse` |
353
+ | `server-json-transport-url-format` | warning | `transport.url` should be `http://` or `https://` |
354
+ | `server-json-argument-type` | error | Each argument must be an object |
355
+ | `server-json-argument-type-value` | error | `argument.type` must be `positional` or `named` |
356
+ | `server-json-argument-name` | error | Named arguments require `name` |
357
+ | `server-json-argument-value` | error | Positional arguments require `value` or `valueHint` |
358
+ | `server-json-input-format` | warning | `format` should be `string`, `number`, `boolean`, or `filepath` |
359
+ | `server-json-env-var-type` | error | Each environment variable must be an object |
360
+ | `server-json-env-var-name` | error | Environment variable `name` is required |
361
+ | `server-json-env-var-description` | warning | Environment variables should have a `description` |
362
+
363
+ Most of these are mechanical — fix the manifest field named in the diagnostic's `message`. The registry spec is the source of truth; this linter just surfaces violations before you submit.
364
+
365
+ ---
366
+
367
+ ## Escape hatches
368
+
369
+ ### Dynamic upstream data
370
+
371
+ If `output` wraps a third-party API whose shape you can't pin down, prefer `z.object({}).passthrough()` over aspirational typing. The linter skips `format-parity` for passthrough schemas, and `structuredContent` still receives the full payload.
372
+
373
+ ### Temporarily suppress a warning
374
+
375
+ Warnings don't block startup, so you can ship with them logged. If one is genuinely wrong (rather than the rule being wrong for your case), file an issue against `@cyanheads/mcp-ts-core` with the repro — the linter rules are still maturing.
376
+
377
+ ### Escape isn't "make it pass"
378
+
379
+ Don't remove fields from `output` to silence `format-parity` — that makes the data invisible to `structuredContent` clients too. Don't rename `description` to something else to silence `describe-on-fields`. The right fix is either to render the field (format-parity) or accept the warning (description-required).
380
+
381
+ ---
382
+
383
+ ## Adding a new rule
384
+
385
+ If you're extending `@cyanheads/mcp-ts-core` with a new lint rule:
386
+
387
+ 1. Add the rule to `src/linter/rules/<family>-rules.ts`. Return `LintDiagnostic` objects with a stable `rule` ID.
388
+ 2. Wire it into `validateDefinitions()` in `src/linter/validate.ts` if it's a new family.
389
+ 3. Add tests in `tests/unit/linter/`.
390
+ 4. **Document the rule in this file.** Add it to the rule index, write a section under the matching family, and bump `metadata.version` in the frontmatter.
391
+ 5. The breadcrumb mapping in `validateDefinitions()` is family-prefix-based (`server-json-*` → `#server-json-rules`, etc.), so rules in existing families pick up the right anchor automatically.
@@ -4,7 +4,7 @@ description: >
4
4
  API reference for built-in service providers (LLM, Speech, Graph). Use when looking up service interfaces, provider capabilities, or integration patterns.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.2"
7
+ version: "1.3"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -111,7 +111,7 @@ const path = await graphService.shortestPath('user:alice', 'user:charlie', conte
111
111
  algorithm: 'bfs',
112
112
  maxLength: 4,
113
113
  });
114
- if (path) console.log(`${path.vertices.length} hops`);
114
+ if (path) context.log.info(`${path.vertices.length} hops`);
115
115
 
116
116
  // Check reachability
117
117
  const connected = await graphService.pathExists('user:alice', 'user:charlie', context, 3);
@@ -4,7 +4,7 @@ description: >
4
4
  API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.0"
7
+ version: "2.1"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -249,7 +249,7 @@ Matches `--- ... ---` at the very start of the document. An empty `---\n---` blo
249
249
  ```ts
250
250
  const { frontmatter, content, hasFrontmatter } = await frontmatterParser.parse<SkillMeta>(markdown);
251
251
  if (hasFrontmatter) {
252
- console.log(frontmatter.name, frontmatter.version);
252
+ logger.info({ name: frontmatter.name, version: frontmatter.version }, 'parsed frontmatter');
253
253
  }
254
254
  ```
255
255
 
@@ -147,7 +147,7 @@ rateLimiter.check(`api:${ctx.tenantId}`, ctx);
147
147
  // Check status without consuming a request
148
148
  const status = rateLimiter.getStatus('api:tenant-123');
149
149
  if (status) {
150
- console.log(`${status.remaining} requests left, resets at ${new Date(status.resetTime)}`);
150
+ logger.info(`${status.remaining} requests left, resets at ${new Date(status.resetTime)}`);
151
151
  }
152
152
 
153
153
  // Runtime reconfiguration
@@ -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.4"
7
+ version: "2.5"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -271,7 +271,7 @@ throw new Error('Not found');
271
271
  "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first."
272
272
 
273
273
  // Good — structured hint in error data
274
- throw new McpError(JsonRpcErrorCode.Forbidden,
274
+ throw forbidden(
275
275
  "Cannot perform 'reset --hard' on protected branch 'main' without explicit confirmation.",
276
276
  { branch: 'main', operation: 'reset --hard', hint: 'Set the confirmed parameter to true to proceed.' },
277
277
  );
@@ -4,7 +4,7 @@ description: >
4
4
  Investigate, adopt, and verify dependency updates — with special handling for `@cyanheads/mcp-ts-core`. Captures what changed, understands why, cross-references against the codebase, adopts framework improvements, syncs project skills, and runs final checks. Supports two entry modes: run the full flow end-to-end, or review updates you already applied.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.3"
7
+ version: "1.4"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -50,13 +50,18 @@ Do not redo this investigation inline — the `changelog` skill handles tag-form
50
50
 
51
51
  ### 4. Framework review (`@cyanheads/mcp-ts-core`)
52
52
 
53
- If `@cyanheads/mcp-ts-core` was updated, do a deeper pass beyond what the `changelog` skill covers. Read:
53
+ If `@cyanheads/mcp-ts-core` was updated, do a deeper pass beyond what the `changelog` skill covers. The framework ships a **directory-based changelog** grouped by minor series (`.x` semver-wildcard convention) — one file per released version at `node_modules/@cyanheads/mcp-ts-core/changelog/<major.minor>.x/<version>.md`. Read only the files between old and new rather than scanning a monolithic file.
54
54
 
55
- ```bash
56
- node_modules/@cyanheads/mcp-ts-core/CHANGELOG.md
57
- ```
55
+ Example — `0.5.2 → 0.5.4` means reading two new version files:
56
+
57
+ - `node_modules/@cyanheads/mcp-ts-core/changelog/0.5.x/0.5.3.md`
58
+ - `node_modules/@cyanheads/mcp-ts-core/changelog/0.5.x/0.5.4.md`
59
+
60
+ Cross-series updates span multiple directories — e.g., `0.4.1 → 0.5.2` reads `0.5.x/0.5.0.md`, `0.5.x/0.5.1.md`, `0.5.x/0.5.2.md`. Enumerate the series directories under `node_modules/@cyanheads/mcp-ts-core/changelog/` to find the relevant files.
58
61
 
59
- Extract entries between the old and new version. Scan specifically for:
62
+ If the per-version directory isn't present (pre-0.5.5 releases, or downstream package that hasn't adopted the convention), fall back to the monolithic rollup at `node_modules/@cyanheads/mcp-ts-core/CHANGELOG.md` and extract the relevant sections manually.
63
+
64
+ Scan specifically for:
60
65
 
61
66
  | Area | Adoption Check |
62
67
  |:-----|:---------------|
@@ -70,11 +75,7 @@ Extract entries between the old and new version. Scan specifically for:
70
75
 
71
76
  Cross-reference each finding against the server's code. Collect adoption opportunities for Step 6.
72
77
 
73
- **Template review.** The framework also ships `templates/CLAUDE.md` and `templates/AGENTS.md` as scaffolding for consumer agent protocol files. The consumer's `CLAUDE.md`/`AGENTS.md` was copied at init time and has since diverged (local customizations, echo replacements, server-specific sections). Read the upstream template fresh:
74
-
75
- ```bash
76
- node_modules/@cyanheads/mcp-ts-core/templates/CLAUDE.md
77
- ```
78
+ **Template review.** The framework also ships `templates/CLAUDE.md` and `templates/AGENTS.md` as scaffolding for consumer agent protocol files. The consumer's `CLAUDE.md`/`AGENTS.md` was copied at init time and has since diverged (local customizations, echo replacements, server-specific sections). Read the upstream template fresh at `node_modules/@cyanheads/mcp-ts-core/templates/CLAUDE.md`.
78
79
 
79
80
  Skip the mechanical diff — consumer customizations create too much noise to filter. Instead, read end-to-end with fresh eyes, mentally comparing against the current `CLAUDE.md`. Look for: new conventions, updated skill references, expanded checklists, new callouts, clearer explanations, restructured sections. Present findings; let the user cherry-pick what to adopt. Never auto-merge — the consumer's file is theirs.
80
81
 
@@ -4,7 +4,7 @@ description: >
4
4
  Finalize documentation and project metadata for a ship-ready MCP server. Use after implementation is complete, tests pass, and devcheck is clean. Safe to run at any stage — each step checks current state and only acts on what still needs work.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.4"
7
+ version: "1.5"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -118,22 +118,37 @@ frozenLockfile = false
118
118
  bun = true
119
119
  ```
120
120
 
121
- ### 9. `CHANGELOG.md`
121
+ ### 9. Changelog
122
122
 
123
- If `CHANGELOG.md` doesn't exist, create it with an initial entry. If it exists, verify the latest entry reflects the current state:
123
+ Directory-based, grouped by minor series using the `.x` semver-wildcard convention: per-version files live in `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.1.x/0.1.0.md`), work-in-progress in `changelog/unreleased.md` at the top level, and `CHANGELOG.md` is a rollup artifact regenerated by `bun run changelog:build`. Devcheck's `Changelog Sync` step enforces that `CHANGELOG.md` stays in sync.
124
+
125
+ If `changelog/` doesn't exist, create the structure:
126
+
127
+ 1. Make the `changelog/` directory
128
+ 2. Create `changelog/unreleased.md` from the template (H1 `# Unreleased` + empty Added/Changed/Fixed sections)
129
+ 3. If the server already has a shipped version (e.g. 0.1.0), create the series directory and initial entry: `changelog/0.1.x/0.1.0.md` with H1 `# 0.1.0 — YYYY-MM-DD`, concrete version and date
130
+ 4. Run `bun run changelog:build` to generate `CHANGELOG.md`
131
+
132
+ Per-version file format:
124
133
 
125
134
  ```markdown
126
- # Changelog
135
+ ---
136
+ summary: One-line headline for the rollup index — ≤250 chars, no markdown
137
+ breaking: false
138
+ ---
139
+
140
+ # 0.1.0 — YYYY-MM-DD
127
141
 
128
- ## 0.1.0 YYYY-MM-DD
142
+ Optional narrative intro (1-3 sentences).
129
143
 
130
- Initial release.
144
+ ## Added
131
145
 
132
- ### Added
133
146
  - [list tools, resources, prompts, key capabilities]
134
147
  ```
135
148
 
136
- Use a concrete version and date. Never `[Unreleased]`.
149
+ **Frontmatter:** `summary` is required (powers the CHANGELOG.md index), `breaking` is optional and defaults to `false` (set `true` for releases requiring consumer code changes).
150
+
151
+ Never hand-edit `CHANGELOG.md` — it's a build artifact. Never use `[Unreleased]` as a version header in a released file.
137
152
 
138
153
  ### 10. `LICENSE`
139
154
 
@@ -181,7 +196,7 @@ Both must pass clean.
181
196
  - [ ] `server.json` matches official MCP schema, versions synced, env vars current
182
197
  - [ ] GitHub repo description matches `package.json` description; topics ↔ keywords in sync
183
198
  - [ ] `bunfig.toml` present
184
- - [ ] `CHANGELOG.md` exists with current entry
199
+ - [ ] `changelog/` directory present with per-version files; `CHANGELOG.md` rollup regenerated and in sync
185
200
  - [ ] `LICENSE` file present
186
201
  - [ ] `Dockerfile` OCI labels and runtime config accurate (if present)
187
202
  - [ ] `docs/tree.md` regenerated
@@ -4,7 +4,7 @@ description: >
4
4
  Verify release readiness and publish. The git wrapup protocol handles version bumps, changelog, README, commits, and tagging during the coding session. This skill verifies nothing was missed, runs final checks, and presents the irreversible publish commands.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.2"
7
+ version: "1.4"
8
8
  audience: internal
9
9
  type: workflow
10
10
  ---
@@ -34,13 +34,27 @@ The wrapup protocol bumps versions, but sometimes a file gets missed. **Search f
34
34
 
35
35
  Fix any mismatches. A grep for the **old** version is the fastest way to find stragglers.
36
36
 
37
- ### 3. Verify CHANGELOG.md
37
+ ### 3. Release-ize the Changelog Entry
38
38
 
39
- Confirm the changelog entry:
39
+ The changelog is directory-based, grouped by minor series using the `.x` semver-wildcard convention: per-version files live at `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.5.x/0.5.4.md`), work-in-progress entries go in `changelog/unreleased.md` at the top level, and `CHANGELOG.md` is an auto-generated rollup (`bun run changelog:build`).
40
40
 
41
- - Uses a **concrete version number and date** (never `[Unreleased]`)
42
- - Groups changes correctly: Added, Changed, Fixed, Removed
43
- - Accurately reflects what actually shipped cross-reference with `git log` since the last tag
41
+ If the wrapup already converted `unreleased.md` to the version file, verify it. Otherwise release-ize it now:
42
+
43
+ 1. Determine the series: `0.5.5` `0.5.x/`. Create the directory if it doesn't exist: `mkdir -p changelog/<series>`
44
+ 2. `git mv changelog/unreleased.md changelog/<series>/<version>.md`
45
+ 3. Update the H1: `# Unreleased` → `# <version> — <date>` (em-dash, ISO date)
46
+ 4. **Fill in the frontmatter** at the top of the file:
47
+ - `summary:` — one-line headline, ≤250 chars, no markdown. Write it like a GitHub Release title. Required.
48
+ - `breaking:` — set to `true` if this release requires consumer code changes (API removal, signature change, config rename). Defaults to `false`. Renders `· ⚠️ Breaking` in the rollup when true.
49
+ 5. Verify content:
50
+ - Sections grouped correctly (Added / Changed / Fixed / Removed)
51
+ - Accurately reflects what shipped — cross-reference with `git log` since the last tag
52
+ - If this release absorbed pre-release versions (e.g., `0.6.0-beta.1`), confirm their sub-headers are preserved in the final file
53
+ - **Issue/PR references use full URLs**, not bare `#NN`. GitHub's auto-link only renders inside its own UI; these files are read from `node_modules` too, where bare `#NN` is dead text. Use `[#38](https://github.com/<owner>/<repo>/issues/38)` (or `/pull/NN` for PRs). Only link numbers verified via `gh issue view NN` / `gh pr view NN` — never speculate on future numbers, since GitHub will happily resolve `#42` to whatever unrelated item already owns 42 and pull its title into timeline previews.
54
+ 6. Create a fresh empty `changelog/unreleased.md` from the template (frontmatter stub with empty `summary`, `breaking: false`)
55
+ 7. Regenerate the rollup: `bun run changelog:build` — warnings about missing summaries are expected during the legacy-file backfill period but should not include this release
56
+
57
+ Never hand-edit `CHANGELOG.md` — it's a build artifact. Devcheck's `Changelog Sync` step will fail if it drifts.
44
58
 
45
59
  ### 4. Verify README.md
46
60
 
@@ -117,7 +131,7 @@ mcp-publisher publish
117
131
 
118
132
  - [ ] Version consistent across all files (package.json, server.json ×3, CLAUDE.md, README.md, templates)
119
133
  - [ ] No stale old-version references found in repo
120
- - [ ] CHANGELOG.md has concrete version and date, content matches actual changes
134
+ - [ ] `changelog/<major.minor>.x/<version>.md` has concrete version and date; `changelog/unreleased.md` reset; `CHANGELOG.md` regenerated via `bun run changelog:build`
121
135
  - [ ] README.md current — feature counts, badges, descriptions, examples
122
136
  - [ ] Modified skill versions bumped in YAML frontmatter
123
137
  - [ ] `docs/tree.md` current (if structure changed)
@@ -4,7 +4,7 @@ description: >
4
4
  Post-init orientation for an MCP server built on @cyanheads/mcp-ts-core. Use after running `@cyanheads/mcp-ts-core init` to understand the project structure, conventions, and skill sync model. Also use when onboarding to an existing project for the first time.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.3"
7
+ version: "1.4"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -22,11 +22,7 @@ The init CLI generates both `CLAUDE.md` and `AGENTS.md` with the same purpose. K
22
22
 
23
23
  Both files serve the same purpose: project-specific agent instructions. Prefer committing one authoritative copy rather than trying to keep both in sync by hand.
24
24
 
25
- For the full framework API, read:
26
-
27
- node_modules/@cyanheads/mcp-ts-core/CLAUDE.md
28
-
29
- Read that file once per session. It contains the exports catalog, tool/resource/prompt contracts, error codes, context API, and common import patterns.
25
+ For the full framework API, read `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` once per session. It contains the exports catalog, tool/resource/prompt contracts, error codes, context API, and common import patterns.
30
26
 
31
27
  ## Project Structure
32
28
 
@@ -95,9 +91,9 @@ This step is the **bootstrap** — it creates the agent directory. From then on,
95
91
 
96
92
  ## Project Scaffolding
97
93
 
98
- After installing dependencies (prefer `bun install`; `npm install` also works), complete these one-time setup tasks:
94
+ After `bun install`, complete these one-time setup tasks:
99
95
 
100
- 1. **Update dependencies to latest** — `bun update --latest` (or `npx npm-check-updates -u && npm install` if using npm). The scaffolded `package.json` pins minimum versions from when the framework was published; updating ensures you start with the latest compatible releases.
96
+ 1. **Update dependencies to latest** — `bun update --latest`. The scaffolded `package.json` pins minimum versions from when the framework was published; updating ensures you start with the latest compatible releases.
101
97
  2. **Initialize git** — `git init && git add -A && git commit -m "chore: scaffold from @cyanheads/mcp-ts-core"`
102
98
  3. **Verify agent protocol placeholders** — if the `init` CLI was run without a `[name]` argument, `{{PACKAGE_NAME}}` may remain as a literal in `CLAUDE.md`/`AGENTS.md` and `package.json`. Replace it with the actual server name.
103
99