@cyanheads/mcp-ts-core 0.10.13 → 0.10.15

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 (71) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +10 -6
  4. package/biome.json +1 -1
  5. package/changelog/0.10.x/0.10.13.md +2 -2
  6. package/changelog/0.10.x/0.10.14.md +22 -0
  7. package/changelog/0.10.x/0.10.15.md +57 -0
  8. package/dist/linter/rules/definition-rules.d.ts +23 -0
  9. package/dist/linter/rules/definition-rules.d.ts.map +1 -0
  10. package/dist/linter/rules/definition-rules.js +35 -0
  11. package/dist/linter/rules/definition-rules.js.map +1 -0
  12. package/dist/linter/rules/enrichment-rules.d.ts +2 -2
  13. package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
  14. package/dist/linter/rules/enrichment-rules.js +5 -0
  15. package/dist/linter/rules/enrichment-rules.js.map +1 -1
  16. package/dist/linter/rules/handler-body-rules.d.ts +1 -1
  17. package/dist/linter/rules/handler-body-rules.d.ts.map +1 -1
  18. package/dist/linter/rules/handler-body-rules.js +3 -0
  19. package/dist/linter/rules/handler-body-rules.js.map +1 -1
  20. package/dist/linter/rules/prompt-rules.d.ts.map +1 -1
  21. package/dist/linter/rules/prompt-rules.js +3 -0
  22. package/dist/linter/rules/prompt-rules.js.map +1 -1
  23. package/dist/linter/rules/resource-rules.d.ts.map +1 -1
  24. package/dist/linter/rules/resource-rules.js +3 -0
  25. package/dist/linter/rules/resource-rules.js.map +1 -1
  26. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  27. package/dist/linter/rules/tool-rules.js +3 -0
  28. package/dist/linter/rules/tool-rules.js.map +1 -1
  29. package/dist/linter/validate.d.ts.map +1 -1
  30. package/dist/linter/validate.js +16 -1
  31. package/dist/linter/validate.js.map +1 -1
  32. package/dist/logs/combined.log +8 -0
  33. package/dist/logs/error.log +4 -0
  34. package/dist/logs/interactions.log +0 -0
  35. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  36. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +17 -2
  37. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  38. package/dist/utils/network/fetchWithTimeout.d.ts +14 -1
  39. package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
  40. package/dist/utils/network/fetchWithTimeout.js +28 -5
  41. package/dist/utils/network/fetchWithTimeout.js.map +1 -1
  42. package/dist/utils/network/httpError.d.ts +4 -1
  43. package/dist/utils/network/httpError.d.ts.map +1 -1
  44. package/dist/utils/network/httpError.js +6 -0
  45. package/dist/utils/network/httpError.js.map +1 -1
  46. package/dist/utils/network/retry.d.ts +12 -0
  47. package/dist/utils/network/retry.d.ts.map +1 -1
  48. package/dist/utils/network/retry.js +58 -3
  49. package/dist/utils/network/retry.js.map +1 -1
  50. package/dist/utils/overflow/outlineOnOverflow.d.ts +24 -11
  51. package/dist/utils/overflow/outlineOnOverflow.d.ts.map +1 -1
  52. package/dist/utils/overflow/outlineOnOverflow.js +24 -11
  53. package/dist/utils/overflow/outlineOnOverflow.js.map +1 -1
  54. package/package.json +25 -25
  55. package/skills/add-tool/SKILL.md +2 -2
  56. package/skills/api-canvas/SKILL.md +2 -2
  57. package/skills/api-linter/SKILL.md +14 -1
  58. package/skills/api-utils/SKILL.md +4 -4
  59. package/skills/design-mcp-server/SKILL.md +2 -2
  60. package/skills/field-test/SKILL.md +5 -4
  61. package/skills/git-wrapup/SKILL.md +12 -24
  62. package/skills/orchestrations/SKILL.md +2 -2
  63. package/skills/orchestrations/workflows/field-test-fix.md +1 -1
  64. package/skills/orchestrations/workflows/fix-wrapup-release.md +2 -2
  65. package/skills/orchestrations/workflows/maintenance-release.md +2 -2
  66. package/skills/release-and-publish/SKILL.md +2 -2
  67. package/skills/techniques/SKILL.md +2 -2
  68. package/skills/techniques/references/outline-on-overflow.md +15 -9
  69. package/skills/tool-defs-analysis/SKILL.md +63 -22
  70. package/templates/Dockerfile +1 -1
  71. package/templates/package.json +2 -2
@@ -31,6 +31,43 @@ function computeDelay(attempt, baseDelayMs, maxDelayMs, jitter) {
31
31
  const jitterRange = exponential * jitter;
32
32
  return exponential - jitterRange + Math.random() * jitterRange * 2;
33
33
  }
34
+ /**
35
+ * Parses an upstream `Retry-After` hint into milliseconds. The two HTTP helpers
36
+ * (`fetchWithTimeout`, `httpErrorFromResponse`) capture the raw header value into
37
+ * `error.data.retryAfter`; this reads it back so the retry delay can honor the
38
+ * wait the upstream explicitly asked for instead of blind exponential backoff.
39
+ *
40
+ * Handles both RFC 9110 §10.2.3 forms:
41
+ * - **delta-seconds** — a bare non-negative integer (`"30"` → `30_000`).
42
+ * - **HTTP-date** — an absolute instant, converted to a wait from now and
43
+ * clamped at `0` (a past date means "retry now").
44
+ *
45
+ * A numeric `data.retryAfter` is also accepted and interpreted as delta-seconds,
46
+ * matching the header's units. Returns `undefined` when the error carries no
47
+ * parseable hint, so callers fall back to exponential backoff.
48
+ */
49
+ function parseRetryAfterMs(error) {
50
+ if (!(error instanceof McpError))
51
+ return;
52
+ const raw = error.data?.retryAfter;
53
+ if (typeof raw === 'number') {
54
+ return Number.isFinite(raw) && raw >= 0 ? raw * 1000 : undefined;
55
+ }
56
+ if (typeof raw !== 'string')
57
+ return;
58
+ const trimmed = raw.trim();
59
+ if (trimmed === '')
60
+ return;
61
+ // delta-seconds: a bare non-negative integer. Checked before Date.parse so a
62
+ // value like "120" is never misread as a calendar year.
63
+ if (/^\d+$/.test(trimmed))
64
+ return Number(trimmed) * 1000;
65
+ // HTTP-date: absolute instant → wait from now, clamped at 0.
66
+ const dateMs = Date.parse(trimmed);
67
+ if (Number.isNaN(dateMs))
68
+ return;
69
+ return Math.max(0, dateMs - Date.now());
70
+ }
34
71
  /**
35
72
  * Default transient check: `McpError` with a transient code, or any non-McpError
36
73
  * (network failures, unexpected throws) which are assumed transient.
@@ -92,6 +129,14 @@ function enrichExhaustedError(error, totalAttempts, operation) {
92
129
  * via `ctx.fail`) for failures that can never succeed on retry (oversized query,
93
130
  * malformed request surfaced as HTTP 200 + error body, etc.).
94
131
  *
132
+ * **Retry-After honoring.** When a transient error carries `data.retryAfter`
133
+ * (captured by `fetchWithTimeout` / `httpErrorFromResponse` from the upstream
134
+ * header), the retry delay honors it — parsing both delta-seconds and HTTP-date
135
+ * forms (RFC 9110 §10.2.3) — instead of the exponential value. If the requested
136
+ * wait exceeds `maxDelayMs`, the error is treated as non-transient and fails
137
+ * fast: a window that won't clear within the retry budget is surfaced to the
138
+ * caller immediately rather than burning attempts that cannot succeed.
139
+ *
95
140
  * When retries exhaust, the final error is enriched with attempt count in both
96
141
  * the message and structured data, so callers know retries were already attempted.
97
142
  *
@@ -133,13 +178,23 @@ export async function withRetry(fn, options = {}) {
133
178
  if (!isTransient(error)) {
134
179
  throw error;
135
180
  }
181
+ // Honor an upstream Retry-After hint over blind exponential backoff.
182
+ const retryAfterMs = parseRetryAfterMs(error);
183
+ // A requested wait longer than the cap can't clear within the retry budget —
184
+ // surface the limit immediately instead of burning an attempt on a window
185
+ // that won't open in time.
186
+ if (retryAfterMs !== undefined && retryAfterMs > maxDelayMs) {
187
+ logger.debug(`Retry-After ${Math.round(retryAfterMs)}ms exceeds maxDelayMs ${maxDelayMs}ms for ${operation ?? 'operation'} — failing fast`, context);
188
+ throw error;
189
+ }
136
190
  if (isLastAttempt) {
137
191
  throw enrichExhaustedError(error, totalAttempts, operation);
138
192
  }
139
- // Log and backoff
140
- const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, jitter);
193
+ // Log and backoff — the honored Retry-After wins over the exponential value.
194
+ const delay = retryAfterMs ?? computeDelay(attempt, baseDelayMs, maxDelayMs, jitter);
141
195
  const errorMessage = error instanceof Error ? error.message : String(error);
142
- logger.debug(`Retry ${attempt + 1}/${maxRetries} for ${operation ?? 'operation'}: ${errorMessage} — waiting ${Math.round(delay)}ms`, context);
196
+ const delaySource = retryAfterMs === undefined ? '' : ' (Retry-After)';
197
+ logger.debug(`Retry ${attempt + 1}/${maxRetries} for ${operation ?? 'operation'}: ${errorMessage} — waiting ${Math.round(delay)}ms${delaySource}`, context);
143
198
  await sleep(delay, signal);
144
199
  }
145
200
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../../src/utils/network/retry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../../src/utils/network/retry.ts"],"names":[],"mappings":"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"}
@@ -24,14 +24,19 @@ export interface SectionMeta {
24
24
  name: string;
25
25
  }
26
26
  /**
27
- * Reusable outline arm for a tool's discriminated-union `output`. Pair it with
28
- * the tool's full-payload schema so the parity linter validates each branch:
27
+ * Reusable outline arm for a tool's `output`. A tool's `output` must be a flat
28
+ * `z.object` — `tool()` rejects a `z.discriminatedUnion` (the `schema-is-object`
29
+ * lint rule and the enrichment `.extend()` are both `ZodObject`-only). Model the
30
+ * two modes as one object with a `kind` discriminator and presence-based optional
31
+ * arms, folding this schema's `sections` / `notice` shape into the outline arm:
29
32
  *
30
33
  * ```ts
31
- * output: z.discriminatedUnion('kind', [
32
- * FullLabel.extend({ kind: z.literal('full') }),
33
- * OUTLINE_VARIANT,
34
- * ]),
34
+ * output: z.object({
35
+ * kind: z.enum(['full', 'outline']),
36
+ * // full-mode arms (present when kind === 'full') — each .optional()
37
+ * sections: OUTLINE_VARIANT.shape.sections.optional(), // outline-mode arms
38
+ * notice: OUTLINE_VARIANT.shape.notice.optional(),
39
+ * }),
35
40
  * ```
36
41
  */
37
42
  export declare const OUTLINE_VARIANT: z.ZodObject<{
@@ -71,8 +76,10 @@ export interface OutlineOptions<T> {
71
76
  }
72
77
  /**
73
78
  * Returns the document whole when it fits the budget, or a section outline when
74
- * it overflows. The caller spreads the result into a discriminated-union `output`
75
- * keyed on `kind` ({@link OUTLINE_VARIANT} supplies the outline arm).
79
+ * it overflows. Declare the tool's `output` as a flat `z.object` with a `kind`
80
+ * discriminator and presence-based optional arms — `tool()` rejects a
81
+ * `z.discriminatedUnion` output ({@link OUTLINE_VARIANT} supplies the outline
82
+ * arm's `sections` / `notice`).
76
83
  *
77
84
  * Single-entry short-circuit: a document with fewer than two sections is returned
78
85
  * whole even when over budget — an outline of one section would cost a round-trip
@@ -108,11 +115,17 @@ export declare function selectSections<T extends Record<string, unknown>>(doc: T
108
115
  }): Partial<T>;
109
116
  /**
110
117
  * Renders an outline payload to MCP `content[]` — the markdown twin of the
111
- * outline's `structuredContent`, so `format()`-parity holds. Drop into a tool's
112
- * `format`:
118
+ * outline's `structuredContent`, so `format()`-parity holds. The flat-object
119
+ * `output` carries both modes as optional fields, so render each arm on field
120
+ * presence, independently — never branch on `kind`. Parity injects one synthetic
121
+ * sample with every optional field populated at once, so a mutually-exclusive
122
+ * `kind` branch would leave the untaken arm's fields unrendered:
113
123
  *
114
124
  * ```ts
115
- * format: (r) => (r.kind === 'outline' ? formatOutline(r) : renderFull(r)),
125
+ * format: (r) => [
126
+ * ...(r.id ? renderFull(r) : []), // full arm — key on a full-only field
127
+ * ...(r.sections ? formatOutline({ kind: 'outline', sections: r.sections, notice: r.notice ?? '' }) : []),
128
+ * ],
116
129
  * ```
117
130
  */
118
131
  export declare function formatOutline(outline: OutlinePayload): ContentBlock[];
@@ -1 +1 @@
1
- {"version":3,"file":"outlineOnOverflow.d.ts","sourceRoot":"","sources":["../../../src/utils/overflow/outlineOnOverflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,qFAAqF;AACrF,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe;;;;;;;iBAW1B,CAAC;AAEH,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE7D,mFAAmF;AACnF,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,cAAc,CAAC;AAEvE,6CAA6C;AAC7C,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,WAAW,EAAE,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;CAC9C;AAmBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,GAAG,EAAE,CAAC,EACN,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAC1B,aAAa,CAAC,CAAC,CAAC,CAgBlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,GAAG,EAAE,CAAC,EACN,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAClC,OAAO,CAAC,CAAC,CAAC,CAGZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,YAAY,EAAE,CASrE"}
1
+ {"version":3,"file":"outlineOnOverflow.d.ts","sourceRoot":"","sources":["../../../src/utils/overflow/outlineOnOverflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,qFAAqF;AACrF,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe;;;;;;;iBAW1B,CAAC;AAEH,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE7D,mFAAmF;AACnF,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,cAAc,CAAC;AAEvE,6CAA6C;AAC7C,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,WAAW,EAAE,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;CAC9C;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,GAAG,EAAE,CAAC,EACN,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAC1B,aAAa,CAAC,CAAC,CAAC,CAgBlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9D,GAAG,EAAE,CAAC,EACN,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAClC,OAAO,CAAC,CAAC,CAAC,CAGZ;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,YAAY,EAAE,CASrE"}
@@ -2,14 +2,19 @@ import { z } from 'zod';
2
2
  /** Default serialized-byte budget. Over this, a document overflows to an outline. */
3
3
  export const DEFAULT_OUTLINE_BUDGET_BYTES = 24_000;
4
4
  /**
5
- * Reusable outline arm for a tool's discriminated-union `output`. Pair it with
6
- * the tool's full-payload schema so the parity linter validates each branch:
5
+ * Reusable outline arm for a tool's `output`. A tool's `output` must be a flat
6
+ * `z.object` — `tool()` rejects a `z.discriminatedUnion` (the `schema-is-object`
7
+ * lint rule and the enrichment `.extend()` are both `ZodObject`-only). Model the
8
+ * two modes as one object with a `kind` discriminator and presence-based optional
9
+ * arms, folding this schema's `sections` / `notice` shape into the outline arm:
7
10
  *
8
11
  * ```ts
9
- * output: z.discriminatedUnion('kind', [
10
- * FullLabel.extend({ kind: z.literal('full') }),
11
- * OUTLINE_VARIANT,
12
- * ]),
12
+ * output: z.object({
13
+ * kind: z.enum(['full', 'outline']),
14
+ * // full-mode arms (present when kind === 'full') — each .optional()
15
+ * sections: OUTLINE_VARIANT.shape.sections.optional(), // outline-mode arms
16
+ * notice: OUTLINE_VARIANT.shape.notice.optional(),
17
+ * }),
13
18
  * ```
14
19
  */
15
20
  export const OUTLINE_VARIANT = z.object({
@@ -39,8 +44,10 @@ function defaultNotice(sections) {
39
44
  }
40
45
  /**
41
46
  * Returns the document whole when it fits the budget, or a section outline when
42
- * it overflows. The caller spreads the result into a discriminated-union `output`
43
- * keyed on `kind` ({@link OUTLINE_VARIANT} supplies the outline arm).
47
+ * it overflows. Declare the tool's `output` as a flat `z.object` with a `kind`
48
+ * discriminator and presence-based optional arms — `tool()` rejects a
49
+ * `z.discriminatedUnion` output ({@link OUTLINE_VARIANT} supplies the outline
50
+ * arm's `sections` / `notice`).
44
51
  *
45
52
  * Single-entry short-circuit: a document with fewer than two sections is returned
46
53
  * whole even when over budget — an outline of one section would cost a round-trip
@@ -89,11 +96,17 @@ export function selectSections(doc, want, options) {
89
96
  }
90
97
  /**
91
98
  * Renders an outline payload to MCP `content[]` — the markdown twin of the
92
- * outline's `structuredContent`, so `format()`-parity holds. Drop into a tool's
93
- * `format`:
99
+ * outline's `structuredContent`, so `format()`-parity holds. The flat-object
100
+ * `output` carries both modes as optional fields, so render each arm on field
101
+ * presence, independently — never branch on `kind`. Parity injects one synthetic
102
+ * sample with every optional field populated at once, so a mutually-exclusive
103
+ * `kind` branch would leave the untaken arm's fields unrendered:
94
104
  *
95
105
  * ```ts
96
- * format: (r) => (r.kind === 'outline' ? formatOutline(r) : renderFull(r)),
106
+ * format: (r) => [
107
+ * ...(r.id ? renderFull(r) : []), // full arm — key on a full-only field
108
+ * ...(r.sections ? formatOutline({ kind: 'outline', sections: r.sections, notice: r.notice ?? '' }) : []),
109
+ * ],
97
110
  * ```
98
111
  */
99
112
  export function formatOutline(outline) {
@@ -1 +1 @@
1
- {"version":3,"file":"outlineOnOverflow.js","sourceRoot":"","sources":["../../../src/utils/overflow/outlineOnOverflow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outlineOnOverflow.js","sourceRoot":"","sources":["../../../src/utils/overflow/outlineOnOverflow.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.10.13",
3
+ "version": "0.10.15",
4
4
  "mcpName": "io.github.cyanheads/mcp-ts-core",
5
5
  "description": "Agent-native TypeScript framework for building MCP servers. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.",
6
6
  "files": [
@@ -193,9 +193,9 @@
193
193
  "publish-mcp": "mcp-publisher login github -token \"$(security find-generic-password -a \"$USER\" -s mcp-publisher-github-pat -w)\" && mcp-publisher publish"
194
194
  },
195
195
  "devDependencies": {
196
- "@biomejs/biome": "2.5.2",
197
- "@cloudflare/vitest-pool-workers": "^0.18.0",
198
- "@cloudflare/workers-types": "5.20260705.1",
196
+ "@biomejs/biome": "2.5.4",
197
+ "@cloudflare/vitest-pool-workers": "^0.18.4",
198
+ "@cloudflare/workers-types": "5.20260706.1",
199
199
  "@duckdb/node-api": "^1.5.4-r.1",
200
200
  "@hono/otel": "^1.1.2",
201
201
  "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0",
@@ -206,44 +206,44 @@
206
206
  "@opentelemetry/sdk-metrics": "^2.9.0",
207
207
  "@opentelemetry/sdk-node": "^0.220.0",
208
208
  "@opentelemetry/sdk-trace-node": "^2.9.0",
209
- "@opentelemetry/semantic-conventions": "^1.41.1",
209
+ "@opentelemetry/semantic-conventions": "^1.43.0",
210
210
  "@socketsecurity/bun-security-scanner": "^1.1.2",
211
- "@supabase/supabase-js": "^2.110.0",
211
+ "@supabase/supabase-js": "^2.110.6",
212
212
  "@types/bun": "^1.3.14",
213
- "@types/node": "26.1.0",
213
+ "@types/node": "26.1.1",
214
214
  "@types/papaparse": "^5.5.2",
215
215
  "@types/sanitize-html": "^2.16.1",
216
216
  "@types/validator": "^13.15.10",
217
- "@vitest/coverage-istanbul": "4.1.9",
218
- "@vitest/ui": "4.1.9",
217
+ "@vitest/coverage-istanbul": "4.1.10",
218
+ "@vitest/ui": "4.1.10",
219
219
  "better-sqlite3": "^12.11.1",
220
220
  "bun-types": "^1.3.14",
221
- "chrono-node": "^2.9.1",
221
+ "chrono-node": "^2.10.0",
222
222
  "clipboardy": "^5.3.1",
223
223
  "defuddle": "^0.19.1",
224
224
  "depcheck": "^1.4.7",
225
225
  "diff": "^9.0.0",
226
226
  "execa": "^9.6.1",
227
- "fast-check": "^4.8.0",
228
- "fast-xml-parser": "^5.9.3",
229
- "ignore": "^7.0.5",
227
+ "fast-check": "^4.9.0",
228
+ "fast-xml-parser": "^5.10.1",
229
+ "ignore": "^7.0.6",
230
230
  "js-yaml": "^5.2.1",
231
- "linkedom": "^0.18.12",
232
- "node-cron": "^4.5.0",
233
- "openai": "^6.45.0",
231
+ "linkedom": "^0.18.13",
232
+ "node-cron": "^4.6.0",
233
+ "openai": "^6.46.0",
234
234
  "papaparse": "^5.5.4",
235
235
  "partial-json": "^0.1.7",
236
236
  "pdf-lib": "^1.17.1",
237
237
  "pino-pretty": "^13.1.3",
238
- "repomix": "^1.16.0",
239
- "sanitize-html": "^2.17.5",
240
- "tsc-alias": "^1.9.0",
241
- "typedoc": "^0.28.19",
238
+ "repomix": "^1.16.1",
239
+ "sanitize-html": "^2.17.6",
240
+ "tsc-alias": "^1.9.1",
241
+ "typedoc": "^0.28.20",
242
242
  "typescript": "^6.0.3",
243
243
  "unpdf": "^1.6.2",
244
244
  "validator": "^13.15.35",
245
- "vite": "8.1.3",
246
- "vitest": "^4.1.9"
245
+ "vite": "8.1.5",
246
+ "vitest": "^4.1.10"
247
247
  },
248
248
  "keywords": [
249
249
  "agent",
@@ -295,13 +295,13 @@
295
295
  "access": "public"
296
296
  },
297
297
  "dependencies": {
298
- "@hono/mcp": "^0.3.0",
299
- "@hono/node-server": "^2.0.8",
298
+ "@hono/mcp": "^0.3.1",
299
+ "@hono/node-server": "^2.0.10",
300
300
  "@modelcontextprotocol/ext-apps": "^1.7.4",
301
301
  "@modelcontextprotocol/sdk": "^1.29.0",
302
302
  "@opentelemetry/api": "^1.9.1",
303
303
  "dotenv": "^17.4.2",
304
- "hono": "^4.12.27",
304
+ "hono": "^4.12.30",
305
305
  "jose": "^6.2.3",
306
306
  "pino": "^10.3.1",
307
307
  "zod": "^4.4.3"
@@ -4,7 +4,7 @@ description: >
4
4
  Scaffold a new MCP tool definition. Use when the user asks to add a tool, create a new tool, or implement a new capability for the server.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.15"
7
+ version: "2.16"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -626,7 +626,7 @@ Large payloads burn the agent's context window. Default to curated summaries; of
626
626
  - **Large objects**: Return key fields by default; accept a `fields` or `verbose` parameter for full data
627
627
  - **Binary/blob content**: Return metadata and a reference, not the raw content
628
628
  - **Analytical working sets**: When upstream returns more *analytical* rows (data an agent would SQL — aggregate, group, join) than fit in context, `DataCanvas` (`ctx.core.canvas?`, Tier 3 — opt-in via `CANVAS_PROVIDER_TYPE=duckdb`) lets you register the rows and return the `canvas_id` plus a preview so the agent can run SQL to slice down without a re-fetch. The `spillover()` helper (`@cyanheads/mcp-ts-core/canvas`) automates the overflow case: drain rows up to a character budget for the inline preview, auto-register the full source on overflow, return both as a discriminated union. **Two gates:** it must be analytical, not a discovery/search surface of categorical metadata (those don't earn a canvas regardless of row count — use MCP-side list filtering or pagination); and a tool emitting a `canvas_id` MUST be paired with a registered `dataframe_query` tool, or the handle is unreachable. Compute distributions or refinement hints across the full result — not the preview — so the agent gets honest aggregate signal on the rows it didn't read. See `api-canvas` for the register / query / export pattern and the spillover flow.
629
- - **One large document**: When a single call returns one document-shaped record (not a row set) that can overflow context, return a section *outline* — top-level keys + per-section byte size — and let the agent re-call with `sections: [...]` for only what it needs, instead of truncating one surface. `outlineOnOverflow()` with `OUTLINE_VARIANT` / `selectSections()` / `formatOutline()` (`@cyanheads/mcp-ts-core/utils`) measures the payload and returns a `full | outline` discriminated-union `output`; declare `OUTLINE_VARIANT` as a branch so `format()`-parity holds per arm. Pure measure + key-slice — Workers-portable, unlike canvas `spillover()`. Use for one fat record; use `spillover()` for a row collection. See the `techniques` skill's `outline-on-overflow` reference.
629
+ - **One large document**: When a single call returns one document-shaped record (not a row set) that can overflow context, return a section *outline* — top-level keys + per-section byte size — and let the agent re-call with `sections: [...]` for only what it needs, instead of truncating one surface. `outlineOnOverflow()` with `OUTLINE_VARIANT` / `selectSections()` / `formatOutline()` (`@cyanheads/mcp-ts-core/utils`) measures the payload and returns a `full | outline` result. Declare the tool's `output` as a flat `z.object` with a `kind` discriminator and presence-based optional arms (fold in `OUTLINE_VARIANT.shape.sections` / `.notice`) — `tool()` rejects a `z.discriminatedUnion` output — and render each arm on field presence in `format()` so parity holds. Pure measure + key-slice — Workers-portable, unlike canvas `spillover()`. Use for one fat record; use `spillover()` for a row collection. See the `techniques` skill's `outline-on-overflow` reference.
630
630
 
631
631
  ## MCP-side list filtering
632
632
 
@@ -4,7 +4,7 @@ description: >
4
4
  DataCanvas primitive reference — a Tier 3 SQL/analytical workspace for tabular MCP servers, backed by DuckDB. Use when registering tables from upstream APIs, running ad-hoc SQL across them, and exporting results. Covers the acquire → register → query → export flow, per-table TTL, the token-sharing pattern for multi-agent collaboration, env config, and Cloudflare Workers fail-closed behavior.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.8"
7
+ version: "1.9"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -212,7 +212,7 @@ const imported = await target.importFrom(source.canvasId, 'orders', { asName: 'o
212
212
  // { tableName: 'orders_copy', rowCount: 2, columns: [...] }
213
213
  ```
214
214
 
215
- Idempotent on re-import (drop + create on the target). `asName` defaults to `sourceTableName`. Throws `validationError({ reason: 'import_same_canvas' })` if source and target are the same canvas — use `query({ registerAs })` to materialize within a single canvas. Throws `notFound` if the source table is missing; `validationError({ reason: 'import_view_clash' })` if the target name collides with an existing view.
215
+ Idempotent on re-import (drop + create on the target). `asName` defaults to `sourceTableName`. Throws `validationError({ reason: 'import_same_canvas' })` if source and target are the same canvas — use `query({ registerAs })` to materialize within a single canvas. Throws `notFound({ reason: 'missing_table' })` if the source table is missing; `validationError({ reason: 'import_view_clash' })` if the target name collides with an existing view.
216
216
 
217
217
  ### `instance.export(tableName, target, options?)`
218
218
 
@@ -4,7 +4,7 @@ description: >
4
4
  MCP definition linter rules reference. Use when `bun run lint:mcp` or `bun run devcheck` 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
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.7"
7
+ version: "1.8"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -42,6 +42,7 @@ Grouped by family. Jump to any rule ID via its anchor.
42
42
 
43
43
  | Family | Rules | Section |
44
44
  |:-------|:------|:--------|
45
+ | Definition | `definition-invalid` | [Definition rules](#definition-rules) |
45
46
  | Format parity | `format-parity`, `format-parity-threw`, `format-parity-walk-failed` | [Format parity](#format-parity) |
46
47
  | Schema | `schema-is-object`, `describe-on-fields`, `schema-serializable` | [Schema rules](#schema-rules) |
47
48
  | Portability | `schema-format-portability`, `schema-anyof-needs-type`, `schema-no-discriminator-keyword`, `schema-no-defs`, `schema-dialect-tag` | [Portability rules](#portability-rules) |
@@ -58,6 +59,18 @@ Grouped by family. Jump to any rule ID via its anchor.
58
59
 
59
60
  ---
60
61
 
62
+ ## Definition rules
63
+
64
+ ### definition-invalid
65
+
66
+ **Severity:** error
67
+
68
+ Fires when a `tools`, `resources`, or `prompts` array passed to `validateDefinitions()` contains a `null`/`undefined` entry (or any non-object value) instead of a definition object — e.g. a stray import or a conditional that yields `undefined`/`false`. The bad entry is reported as this diagnostic and skipped, rather than crashing the whole lint run.
69
+
70
+ **Fix:** remove the empty slot, or ensure every element of the array is a real definition object (e.g. `[makeFooTool(), enabled ? makeBarTool() : null].filter(Boolean)`).
71
+
72
+ ---
73
+
61
74
  ## Format parity
62
75
 
63
76
  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.
@@ -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.4"
7
+ version: "2.5"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -29,9 +29,9 @@ Utility exports from `@cyanheads/mcp-ts-core/utils`. Utilities with complex APIs
29
29
 
30
30
  | Export | API | Notes |
31
31
  |:-------|:----|:------|
32
- | `fetchWithTimeout` | `(url, timeoutMs, context: RequestContext, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean` and `signal?: AbortSignal` (external cancellation). SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. **Error/log redaction:** URLs written into thrown errors and log lines are reduced to `origin + pathname` — the query string (where API keys commonly ride: `?api-key=…`, `?api_key=…`) never reaches the client or the logs. The actual request still uses the full URL. |
33
- | `withRetry` | `<T>(fn: () => Promise<T>, options?: RetryOptions) -> Promise<T>` | Executes `fn` with exponential backoff. Retries on transient errors (`ServiceUnavailable`, `Timeout`, `RateLimited`); non-transient errors fail immediately. On exhaustion, enriches the final error with attempt count in message and `data.retryAttempts`. **Place the retry boundary around the full pipeline** (fetch + parse), not just the network call. `RetryOptions`: `maxRetries` (default `3`), `baseDelayMs` (default `1000`), `maxDelayMs` (default `30000`), `jitter` (default `0.25`), `operation` (log label), `context` (RequestContext), `signal` (AbortSignal), `isTransient` (custom predicate). |
34
- | `httpErrorFromResponse` | `(response: Response, options?: HttpErrorFromResponseOptions) -> Promise<McpError>` | Maps an HTTP `Response` to a properly classified `McpError` — full status table including 401/403/408/422/429/5xx, body capture (truncated), `retry-after` header, optional `cause`. Use this instead of hand-rolling `if (status === 429) ...` ladders. Reads the response body — `clone()` first if you need it elsewhere. `HttpErrorFromResponseOptions`: `service?` (logical name in message, e.g. `'NCBI'`), `captureBody?` (default `true`), `bodyLimit?` (default `500`), `data?` (extra fields merged into `error.data`), `cause?`, `codeOverride?` (per-status mapping override). Pairs naturally with `withRetry` — both classify codes the same way. |
32
+ | `fetchWithTimeout` | `(url, timeoutMs, context: RequestContext, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean`, `expectedStatuses?: number[]` (listed non-2xx statuses logged at `debug` not `error`, still thrown), and `signal?: AbortSignal` (external cancellation). On a non-2xx, `error.data` carries `status`/`body` plus the legacy `statusCode`/`responseBody` aliases (identical values; consolidating in a future major). SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. **Error/log redaction:** URLs written into thrown errors and log lines are reduced to `origin + pathname` — the query string (where API keys commonly ride: `?api-key=…`, `?api_key=…`) never reaches the client or the logs. The actual request still uses the full URL. |
33
+ | `withRetry` | `<T>(fn: () => Promise<T>, options?: RetryOptions) -> Promise<T>` | Executes `fn` with exponential backoff. Retries on transient errors (`ServiceUnavailable`, `Timeout`, `RateLimited`); non-transient errors fail immediately. Honors an upstream `Retry-After` on `data.retryAfter` (delta-seconds or HTTP-date) over exponential backoff, capped at `maxDelayMs`; a requested wait beyond the cap fails fast rather than sleeping. On exhaustion, enriches the final error with attempt count in message and `data.retryAttempts`. **Place the retry boundary around the full pipeline** (fetch + parse), not just the network call. `RetryOptions`: `maxRetries` (default `3`), `baseDelayMs` (default `1000`), `maxDelayMs` (default `30000`), `jitter` (default `0.25`), `operation` (log label), `context` (RequestContext), `signal` (AbortSignal), `isTransient` (custom predicate). |
34
+ | `httpErrorFromResponse` | `(response: Response, options?: HttpErrorFromResponseOptions) -> Promise<McpError>` | Maps an HTTP `Response` to a properly classified `McpError` — full status table including 401/403/408/422/429/5xx, body capture (truncated), `retry-after` header, optional `cause`. `error.data` carries `status`/`body` plus the legacy `statusCode`/`responseBody` aliases (identical values), so a consumer can classify either helper's error without knowing which raised it. Use this instead of hand-rolling `if (status === 429) ...` ladders. Reads the response body — `clone()` first if you need it elsewhere. `HttpErrorFromResponseOptions`: `service?` (logical name in message, e.g. `'NCBI'`), `captureBody?` (default `true`), `bodyLimit?` (default `500`), `data?` (extra fields merged into `error.data`), `cause?`, `codeOverride?` (per-status mapping override). Pairs naturally with `withRetry` — both classify codes the same way. |
35
35
  | `httpStatusToErrorCode` | `(status: number) -> JsonRpcErrorCode \| undefined` | Sync status → code lookup. Returns `undefined` for 1xx/2xx/3xx. Use when you need just the code without a `Response` object handy. |
36
36
 
37
37
  ---
@@ -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.19"
7
+ version: "2.20"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -361,7 +361,7 @@ output: z.object({
361
361
  - **Capped lists disclose truncation.** When a tool accepts a cap-like input (`limit`, `per_page`, `page_size`, `max_results`, `max_items`) and returns an array, the handler must disclose when the cap was hit. Standard fields: `truncated: true`, `shown`, `cap` in the `enrichment` block via `ctx.enrich.truncated({ shown, cap })`. `ctx.enrich.total(n)` (writes `totalCount`) is also recognized. Silent caps leave the agent treating a partial set as complete. The `capped-list-no-truncation` lint rule enforces this; see `api-linter` and `api-context`'s `ctx.enrich.truncated()` section.
362
362
  - **Truncate large output with counts.** When a list exceeds a reasonable display size, show the top N and append "...and X more". Don't silently drop results.
363
363
  - **Spill big *analytical* results to a queryable surface.** When a tool's row set is something an agent would run SQL over (aggregate, group, join) *and* can exceed any reasonable context budget — paginated APIs, streamed exports, big query results — pair an inline preview with a `DataCanvas` table holding the full set. **Two rules gate this:** (1) it must earn its keep on *shape, not size* — a discovery/search surface of categorical metadata (titles, IDs) is not analytical and doesn't get a canvas regardless of row count; for name→ID resolution over a bounded list use [MCP-side list filtering](#mcp-side-list-filtering); (2) the `canvas_id` is reachable only if the same server **also exposes a `dataframe_query` tool** — emit one without the other and the handle is dead output. Compute distributions or refinement hints across the full result, not the preview, so aggregate signal stays honest. See `api-canvas` for the `spillover()` helper and both rules in full.
364
- - **Outline one large *document* into sections.** When a single tool call returns one document-shaped record (not many rows) that can exceed context — a ~130KB FDA drug label, a big API entity dominated by a few fat fields — return a section *outline* (top-level keys + per-section byte size) instead of truncating, and let the agent re-call with `sections: [...]` to pull only what it needs. The `outlineOnOverflow()` helper (`@cyanheads/mcp-ts-core/utils`) measures the payload and returns a `full | outline` discriminated union; declare its `OUTLINE_VARIANT` as a branch of the tool's `output` so `format()`-parity is enforced per branch. Pure measure + key-slice — Workers-portable, unlike canvas-bound `spillover()`. Distinct from spillover on *shape*: spillover splits a row collection, this outlines one fat record. See the `techniques` skill's `outline-on-overflow` reference.
364
+ - **Outline one large *document* into sections.** When a single tool call returns one document-shaped record (not many rows) that can exceed context — a ~130KB FDA drug label, a big API entity dominated by a few fat fields — return a section *outline* (top-level keys + per-section byte size) instead of truncating, and let the agent re-call with `sections: [...]` to pull only what it needs. The `outlineOnOverflow()` helper (`@cyanheads/mcp-ts-core/utils`) measures the payload and returns a `full | outline` result; declare the tool's `output` as a flat `z.object` with a `kind` discriminator and presence-based optional arms (folding in `OUTLINE_VARIANT.shape.sections` / `.notice`) — `tool()` rejects a `z.discriminatedUnion` output — so `format()`-parity holds when each arm renders on field presence. Pure measure + key-slice — Workers-portable, unlike canvas-bound `spillover()`. Distinct from spillover on *shape*: spillover splits a row collection, this outlines one fat record. See the `techniques` skill's `outline-on-overflow` reference.
365
365
  - **Mirror a bulk upstream instead of paginating it live.** When the server wraps a large or slow API whose corpus is queried far more than it changes, sync it once into a persistent local index and query that as the primary data path — not the live API per request. Match the backend to corpus size: ≲ tens of thousands of rows → an in-memory index (server-level, no primitive); ~10⁴–10⁷ → the `MirrorService` (embedded SQLite + FTS5; declare a schema + a `sync` ingester via `defineMirror`/`sqliteMirrorStore`, then `runSync`/`query`, see `api-mirror`); ≳ 10⁸ → an external store. Distinct lifecycle from DataCanvas: a mirror is long-lived and cross-session, refreshed on a schedule; canvas is ephemeral and per-session.
366
366
  - **`format()` is the markdown twin of `structuredContent` — make both content-complete.** Different MCP clients forward different surfaces to the model: some (e.g., Claude Code) read `structuredContent` from `output`, others (e.g., Claude Desktop) read `content[]` from `format()`. Both must carry the same data so every client sees the same picture — `format()` just dresses it up with markdown. A thin `format()` that returns only a count or title leaves `content[]`-only clients blind to data that `structuredContent` clients can see. Render all fields the LLM needs, with structured markdown (headers, bold labels, lists) for readability.
367
367
  - **Agent-facing context must reach both client surfaces — put it in `enrichment`.** `structuredContent` (from `output`) and `content[]` (from `format()`) are read by different clients. Empty-result notices, the query/filter as the server parsed it, and pagination totals — the context the agent *reasons with*, distinct from the domain payload — reach only `content[]` if hand-authored into `format()` text alone, leaving `structuredContent`-only clients (Claude Code) blind. (The reverse can't happen: `format-parity` drags every `output` field into `format()`, so `output`-authored context already reaches both.) An `enrichment` block — the success-path counterpart to `errors[]`, populated via `ctx.enrich(...)` — reaches both automatically: merged into `structuredContent`, advertised as `output.extend(enrichment)`, mirrored into a `content[]` trailer, no `format()` entry needed. How each field renders in that trailer is a per-tool call — a kind-tag (`notice`/`total`/`echo`/`delta`) when a canonical form fits, a domain key like `totalFound` otherwise, and an `enrichmentTrailer.render` for any structured (object/array) field so it doesn't ship as a JSON blob. See `add-tool`'s **Tool Response Design**.
@@ -4,7 +4,7 @@ description: >
4
4
  Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.6"
7
+ version: "2.7"
8
8
  audience: external
9
9
  type: debug
10
10
  ---
@@ -232,7 +232,7 @@ Treat any hit as a `ux` finding in the report. The authoring rule lives under *T
232
232
  | Category | What to verify |
233
233
  |:---------|:---------------|
234
234
  | Happy path | One realistic input. Output shape matches schema. `content[]` text reads clearly to a human. |
235
- | `structuredContent` ↔ `content[]` parity | Every field in `structuredContent` is surfaced in the text. Parity gap = client-specific blindness. |
235
+ | `structuredContent` ↔ `content[]` parity | Dump the whole array (`jq '.result.content'`) and check every `structuredContent` field is surfaced *somewhere* in it — enrichment lands in its own trailing block, not in `content[0]`. Parity gap = client-specific blindness. |
236
236
  | Input error | One invalid input (wrong type or missing required). Error text says *what*, *why*, *how to fix*. |
237
237
 
238
238
  **Situational — add only when triggered**
@@ -271,6 +271,7 @@ When a call surprises you — slow, hangs, returns terse output, surfaces an unh
271
271
 
272
272
  **Interpreting responses**
273
273
 
274
+ - **`content[]` is an array of blocks — read all of them, never just `content[0]`.** A success result is assembled as `[...ctx.content media blocks, ...the format()/JSON domain render, ...the enrichment trailer]`. Everything the handler put on `ctx.enrich` — empty-result notices, totals, query echoes, truncation disclosure — renders in that trailer, a **separate trailing block**, not inside the `format()` block. Quoting `content[0].text` and reporting those fields as absent from `content[]` is a false parity gap; the suggested fix (render them in `format()` too) would double-render them. Dump `.result.content` in full before claiming drift.
274
275
  - Tool domain errors return `{result: {content: [...], isError: true}}` — they live in `result`, not `error`. Check `isError`, not the JSON-RPC error field.
275
276
  - **Tool error code/reason** rides on `result.structuredContent.error.{code, message, data?.reason}` — inspect that, not just the text. `data` is only spread when the handler threw an `McpError` (or `ZodError`); plain `throw new Error(...)` won't populate `data.reason`. Use `ctx.fail`-thrown errors when the contract reason matters. The text in `result.content[0].text` mirrors the message and includes `Recovery: <hint>` when `data.recovery.hint` is present.
276
277
  - **Resource errors** are JSON-RPC-level — they appear in the top-level `error.{code, data.reason}` field, not inside `result`. Resource handlers re-throw rather than producing an `isError` envelope.
@@ -297,7 +298,7 @@ One paragraph. How many definitions exercised, how many passed clean, how many h
297
298
 
298
299
  #### Findings
299
300
 
300
- Only include definitions with issues. Group by severity. Each finding is 2–4 lines unless it genuinely needs more.
301
+ Only include definitions with issues. Group by severity. Each finding is 2–4 lines unless it genuinely needs more. A parity finding cites the full `content[]` dump as its evidence — a quote from one index doesn't establish drift.
301
302
 
302
303
  | Severity | Meaning |
303
304
  |:---------|:--------|
@@ -336,7 +337,7 @@ End with:
336
337
  - [ ] HTTP server built and started; real port parsed from log
337
338
  - [ ] Session initialized; `notifications/initialized` sent
338
339
  - [ ] Catalog surfaced and presented; descriptions audited for leaks (implementation details, meta-coaching, consumer-aware phrasing)
339
- - [ ] Universal battery run on every definition (happy path, parity, input error)
340
+ - [ ] Universal battery run on every definition (happy path, parity against the full `content[]` array, input error)
340
341
  - [ ] Situational categories applied only when triggered
341
342
  - [ ] **If >15 tools:** sampled 30–40% for situational testing; skipped definitions listed in report
342
343
  - [ ] **If a tool declared an `errors: [...]` contract:** ≥1 declared failure mode triggered; `result.structuredContent.error.code` and `data.reason` verified against the contract entry
@@ -4,7 +4,7 @@ description: >
4
4
  Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts) and an annotated tag. Verify, commit, tag. Stops at "committed and tagged locally" — no push, no publish. The release-and-publish skill picks up from here. Distilled from the git_wrapup_instructions protocol.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.7"
7
+ version: "1.8"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -160,40 +160,28 @@ Use `-m` with embedded newlines in the string (the commit `-m`-only constraint a
160
160
 
161
161
  `--cleanup=whitespace` is load-bearing. The default cleanup (`strip`) deletes `#`-leading lines as comments, so markdown headers silently vanish from the tag body. `--cleanup=verbatim` is worse: it skips end-of-message normalization, so with tag signing enabled the signature is appended flush against the message's last character — git then can't parse its own signature (the tag reads as unsigned) and the whole `-----BEGIN SSH SIGNATURE-----` block publishes verbatim into the GitHub Release body.
162
162
 
163
- Format:
163
+ Format — a **headline digest**, never a section-by-section changelog mirror:
164
164
 
165
165
  ```
166
166
  <theme — omit version number, GitHub prepends v<VERSION>:>
167
167
 
168
- <optional context — one concise line, two max>
169
-
170
- <Sections — Keep a Changelog names, only those with entries>
171
-
172
- Added:
173
-
174
- - <bullet>
175
-
176
- Changed:
177
-
178
- - <bullet>
179
-
180
- <dep arrows if applicable>
181
-
182
- Dependency bumps:
183
-
184
- - `pkg` ^old → ^new
185
-
186
- <N> tests pass; `bun run devcheck` clean.
168
+ - <notable user-facing change> (#N)
169
+ - <notable user-facing change> (#N)
170
+ - <ONE compact grouped line for the minor/internal changes — build config, repo hygiene, metadata>
171
+ - deps: `@cyanheads/mcp-ts-core` ^0.10.6 → ^0.10.14 (+ dev-dep bumps)
187
172
 
188
173
  [CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)
189
174
  ```
190
175
 
191
176
  **Rules:**
192
177
  - Subject line omits the version number (GitHub prepends `v<VERSION>:` to the release title)
193
- - **No narrative preamble** — context under the subject is one concise line, two max; never paragraph blocks. Detail belongs in the bullets
194
- - Not a CHANGELOG copy — terse, scannable
178
+ - **Flat bullets only — never Keep-a-Changelog section headers.** `Added:`/`Changed:`/`Fixed:`/`Dependency bumps:` belong in the changelog file; a tag that mirrors the changelog's structure is wrong even when every line is accurate
179
+ - **Complete at headline granularity** — every changelog-worthy change stays visible: notable changes get their own bullet, minor/internal items (build config, repo hygiene, metadata) share ONE grouped compact bullet. Nothing silently dropped, nothing expanded — the changelog carries the depth, the tag carries the existence
180
+ - **Deps: one line max**, naming only what earns it (the framework bump, a major); per-package arrows for the rest live in the changelog entry only
181
+ - **No gates line** — test counts and devcheck status are changelog detail, not release-body material
182
+ - No narrative preamble — bullets under the subject, no paragraph blocks
195
183
  - No marketing adjectives
196
- - Length is earned — two-line tags are fine for small patches
184
+ - Length is earned — a subject + two bullets + changelog link is a fine tag for a small patch
197
185
  - **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.
198
186
  - **Changelog link (final line):** end the tag body with a Markdown link to this version's changelog file, so the GitHub Release offers a one-click jump to the full entry — `[CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)`. Derive `<OWNER>/<REPO>` from the origin remote; the path mirrors the file authored in step 4 (e.g. `changelog/0.10.x/0.10.12.md`). Keep the blank line above it so it renders as its own paragraph, not appended to the gates line.
199
187