@cyanheads/mcp-ts-core 0.7.6 → 0.8.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 (104) hide show
  1. package/CLAUDE.md +22 -7
  2. package/README.md +2 -2
  3. package/changelog/0.8.x/0.8.0.md +31 -0
  4. package/dist/core/context.d.ts +67 -0
  5. package/dist/core/context.d.ts.map +1 -1
  6. package/dist/core/context.js +46 -1
  7. package/dist/core/context.js.map +1 -1
  8. package/dist/core/index.d.ts +2 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +1 -0
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/linter/rules/error-contract-rules.d.ts +45 -0
  13. package/dist/linter/rules/error-contract-rules.d.ts.map +1 -0
  14. package/dist/linter/rules/error-contract-rules.js +321 -0
  15. package/dist/linter/rules/error-contract-rules.js.map +1 -0
  16. package/dist/linter/rules/handler-body-rules.d.ts +18 -0
  17. package/dist/linter/rules/handler-body-rules.d.ts.map +1 -0
  18. package/dist/linter/rules/handler-body-rules.js +134 -0
  19. package/dist/linter/rules/handler-body-rules.js.map +1 -0
  20. package/dist/linter/rules/index.d.ts +2 -0
  21. package/dist/linter/rules/index.d.ts.map +1 -1
  22. package/dist/linter/rules/index.js +2 -0
  23. package/dist/linter/rules/index.js.map +1 -1
  24. package/dist/linter/rules/resource-rules.d.ts.map +1 -1
  25. package/dist/linter/rules/resource-rules.js +9 -0
  26. package/dist/linter/rules/resource-rules.js.map +1 -1
  27. package/dist/linter/rules/source-text.d.ts +19 -0
  28. package/dist/linter/rules/source-text.d.ts.map +1 -0
  29. package/dist/linter/rules/source-text.js +96 -0
  30. package/dist/linter/rules/source-text.js.map +1 -0
  31. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  32. package/dist/linter/rules/tool-rules.js +9 -0
  33. package/dist/linter/rules/tool-rules.js.map +1 -1
  34. package/dist/logs/combined.log +4 -4
  35. package/dist/logs/error.log +4 -4
  36. package/dist/mcp-server/apps/appBuilders.d.ts +9 -4
  37. package/dist/mcp-server/apps/appBuilders.d.ts.map +1 -1
  38. package/dist/mcp-server/apps/appBuilders.js +4 -0
  39. package/dist/mcp-server/apps/appBuilders.js.map +1 -1
  40. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  41. package/dist/mcp-server/resources/resource-registration.js +3 -2
  42. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  43. package/dist/mcp-server/resources/utils/resourceDefinition.d.ts +13 -5
  44. package/dist/mcp-server/resources/utils/resourceDefinition.d.ts.map +1 -1
  45. package/dist/mcp-server/resources/utils/resourceDefinition.js.map +1 -1
  46. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  47. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +5 -4
  48. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  49. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/tool-registration.js +13 -7
  51. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  52. package/dist/mcp-server/tools/utils/toolDefinition.d.ts +64 -16
  53. package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
  54. package/dist/mcp-server/tools/utils/toolDefinition.js +25 -11
  55. package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
  56. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  57. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +6 -4
  58. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  59. package/dist/testing/index.d.ts +8 -0
  60. package/dist/testing/index.d.ts.map +1 -1
  61. package/dist/testing/index.js +5 -1
  62. package/dist/testing/index.js.map +1 -1
  63. package/dist/types-global/errors.d.ts +82 -0
  64. package/dist/types-global/errors.d.ts.map +1 -1
  65. package/dist/types-global/errors.js +25 -0
  66. package/dist/types-global/errors.js.map +1 -1
  67. package/dist/utils/formatting/index.d.ts +1 -0
  68. package/dist/utils/formatting/index.d.ts.map +1 -1
  69. package/dist/utils/formatting/index.js +1 -0
  70. package/dist/utils/formatting/index.js.map +1 -1
  71. package/dist/utils/formatting/partialResult.d.ts +145 -0
  72. package/dist/utils/formatting/partialResult.d.ts.map +1 -0
  73. package/dist/utils/formatting/partialResult.js +145 -0
  74. package/dist/utils/formatting/partialResult.js.map +1 -0
  75. package/dist/utils/index.d.ts +2 -1
  76. package/dist/utils/index.d.ts.map +1 -1
  77. package/dist/utils/index.js +2 -1
  78. package/dist/utils/index.js.map +1 -1
  79. package/dist/utils/network/httpError.d.ts +112 -0
  80. package/dist/utils/network/httpError.d.ts.map +1 -0
  81. package/dist/utils/network/httpError.js +153 -0
  82. package/dist/utils/network/httpError.js.map +1 -0
  83. package/dist/utils/network/retry.d.ts.map +1 -1
  84. package/dist/utils/network/retry.js +0 -1
  85. package/dist/utils/network/retry.js.map +1 -1
  86. package/package.json +5 -4
  87. package/scripts/split-changelog.ts +133 -0
  88. package/skills/add-app-tool/SKILL.md +12 -0
  89. package/skills/add-resource/SKILL.md +40 -0
  90. package/skills/add-service/SKILL.md +47 -0
  91. package/skills/add-test/SKILL.md +39 -0
  92. package/skills/add-tool/SKILL.md +39 -4
  93. package/skills/api-context/SKILL.md +75 -1
  94. package/skills/api-errors/SKILL.md +162 -4
  95. package/skills/api-linter/SKILL.md +223 -3
  96. package/skills/api-testing/SKILL.md +79 -4
  97. package/skills/api-utils/SKILL.md +4 -2
  98. package/skills/design-mcp-server/SKILL.md +13 -10
  99. package/skills/field-test/SKILL.md +8 -2
  100. package/skills/maintenance/SKILL.md +2 -2
  101. package/skills/report-issue-framework/SKILL.md +2 -2
  102. package/skills/security-pass/SKILL.md +6 -5
  103. package/templates/AGENTS.md +23 -8
  104. package/templates/CLAUDE.md +23 -8
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @fileoverview Helpers for converting HTTP `Response` objects into properly
3
+ * classified `McpError` instances. Replaces the hand-rolled status → code ladder
4
+ * that consumer servers tend to write (and get wrong, especially for 401/403/408).
5
+ * @module src/utils/network/httpError
6
+ */
7
+ import { JsonRpcErrorCode, McpError } from '../../types-global/errors.js';
8
+ /**
9
+ * Maps an HTTP status code to a `JsonRpcErrorCode`. Covers the full client/server
10
+ * 4xx/5xx range, with specific mappings for the codes most upstream APIs use to
11
+ * signal authoritative outcomes (auth failures, conflicts, validation, rate limits).
12
+ *
13
+ * Returns `undefined` when the status is in the 1xx/2xx/3xx range — those are not
14
+ * errors and the caller should not be invoking error mapping on them.
15
+ *
16
+ * | Status | Code |
17
+ * |:-------|:-----|
18
+ * | 400 | `InvalidParams` |
19
+ * | 401 | `Unauthorized` |
20
+ * | 402 | `Forbidden` (payment-required, treated as access denial) |
21
+ * | 403 | `Forbidden` |
22
+ * | 404 | `NotFound` |
23
+ * | 405, 406, 410, 415 | `InvalidRequest` |
24
+ * | 408 | `Timeout` |
25
+ * | 409 | `Conflict` |
26
+ * | 412, 416, 417 | `InvalidRequest` |
27
+ * | 422 | `ValidationError` |
28
+ * | 423, 424 | `Conflict` |
29
+ * | 425 | `Timeout` |
30
+ * | 428 | `InvalidRequest` |
31
+ * | 429 | `RateLimited` |
32
+ * | 431, 451 | `InvalidRequest` |
33
+ * | 4xx (other) | `InvalidRequest` |
34
+ * | 500, 501 | `InternalError` |
35
+ * | 502, 503 | `ServiceUnavailable` |
36
+ * | 504 | `Timeout` |
37
+ * | 5xx (other) | `ServiceUnavailable` |
38
+ */
39
+ export function httpStatusToErrorCode(status) {
40
+ if (status < 400)
41
+ return;
42
+ switch (status) {
43
+ case 400:
44
+ return JsonRpcErrorCode.InvalidParams;
45
+ case 401:
46
+ return JsonRpcErrorCode.Unauthorized;
47
+ case 402:
48
+ case 403:
49
+ return JsonRpcErrorCode.Forbidden;
50
+ case 404:
51
+ return JsonRpcErrorCode.NotFound;
52
+ case 408:
53
+ return JsonRpcErrorCode.Timeout;
54
+ case 409:
55
+ return JsonRpcErrorCode.Conflict;
56
+ case 422:
57
+ return JsonRpcErrorCode.ValidationError;
58
+ case 423:
59
+ case 424:
60
+ return JsonRpcErrorCode.Conflict;
61
+ case 425:
62
+ return JsonRpcErrorCode.Timeout;
63
+ case 429:
64
+ return JsonRpcErrorCode.RateLimited;
65
+ case 500:
66
+ case 501:
67
+ return JsonRpcErrorCode.InternalError;
68
+ case 502:
69
+ case 503:
70
+ return JsonRpcErrorCode.ServiceUnavailable;
71
+ case 504:
72
+ return JsonRpcErrorCode.Timeout;
73
+ default:
74
+ return status >= 500 ? JsonRpcErrorCode.ServiceUnavailable : JsonRpcErrorCode.InvalidRequest;
75
+ }
76
+ }
77
+ const DEFAULT_BODY_LIMIT = 500;
78
+ /**
79
+ * Builds an `McpError` from an HTTP `Response`, with status-aware classification
80
+ * and optional body capture.
81
+ *
82
+ * Reads the response body (consuming it) when `captureBody` is true, so callers
83
+ * must `response.clone()` first if they intend to read the body elsewhere.
84
+ *
85
+ * Always returns an `McpError` even for 1xx/2xx/3xx — the caller is expected to
86
+ * have verified `!response.ok` first, but the helper falls back to a sensible
87
+ * code (`InternalError`) instead of silently producing nothing.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const response = await fetch(url);
92
+ * if (!response.ok) {
93
+ * throw await httpErrorFromResponse(response, {
94
+ * service: 'NCBI',
95
+ * data: { endpoint: 'esearch' },
96
+ * });
97
+ * }
98
+ * ```
99
+ *
100
+ * @example Wrapping a network failure
101
+ * ```ts
102
+ * try {
103
+ * const response = await fetch(url);
104
+ * if (!response.ok) {
105
+ * throw await httpErrorFromResponse(response, { service: 'NCBI' });
106
+ * }
107
+ * return await response.text();
108
+ * } catch (error) {
109
+ * if (error instanceof McpError) throw error;
110
+ * throw new McpError(JsonRpcErrorCode.ServiceUnavailable, 'NCBI request failed', { url }, { cause: error });
111
+ * }
112
+ * ```
113
+ */
114
+ export async function httpErrorFromResponse(response, options = {}) {
115
+ const { captureBody = true, bodyLimit = DEFAULT_BODY_LIMIT, service, data: extraData, cause, codeOverride, } = options;
116
+ const code = codeOverride?.(response.status) ??
117
+ httpStatusToErrorCode(response.status) ??
118
+ JsonRpcErrorCode.InternalError;
119
+ const subject = service ?? safeHost(response.url) ?? 'Upstream';
120
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
121
+ let body;
122
+ if (captureBody) {
123
+ try {
124
+ const raw = await response.text();
125
+ body = raw.length > bodyLimit ? `${raw.slice(0, bodyLimit)}…` : raw;
126
+ }
127
+ catch {
128
+ /* body unreadable (already consumed, network error mid-stream) */
129
+ }
130
+ }
131
+ const retryAfter = response.headers.get('retry-after') ?? undefined;
132
+ const data = {
133
+ url: response.url || undefined,
134
+ status: response.status,
135
+ statusText: response.statusText || undefined,
136
+ ...(body !== undefined && { body }),
137
+ ...(retryAfter !== undefined && { retryAfter }),
138
+ ...extraData,
139
+ };
140
+ return new McpError(code, `${subject} returned HTTP ${response.status}${statusText}.`, data, cause !== undefined ? { cause } : undefined);
141
+ }
142
+ /** Returns the hostname from a URL string, or `undefined` if it can't be parsed. */
143
+ function safeHost(url) {
144
+ if (!url)
145
+ return;
146
+ try {
147
+ return new URL(url).host;
148
+ }
149
+ catch {
150
+ return;
151
+ }
152
+ }
153
+ //# sourceMappingURL=httpError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpError.js","sourceRoot":"","sources":["../../../src/utils/network/httpError.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/utils/network/retry.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAYzE,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE1C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA6DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC,CAiD/F"}
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/utils/network/retry.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAYzE,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE1C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA6DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC,CAiD/F"}
@@ -3,7 +3,6 @@
3
3
  * may fail transiently. Designed so the retry boundary covers the full pipeline
4
4
  * (HTTP fetch + response parsing/validation), not just the network call.
5
5
  * @module src/utils/network/retry
6
- * @see docs/service-resilience.md
7
6
  */
8
7
  import { JsonRpcErrorCode, McpError } from '../../types-global/errors.js';
9
8
  import { logger } from '../../utils/internal/logger.js';
@@ -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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
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
  "main": "dist/core/index.js",
@@ -16,6 +16,7 @@
16
16
  "scripts/clean.ts",
17
17
  "scripts/devcheck.ts",
18
18
  "scripts/lint-mcp.ts",
19
+ "scripts/split-changelog.ts",
19
20
  "scripts/tree.ts",
20
21
  "skills/",
21
22
  "templates/",
@@ -173,7 +174,7 @@
173
174
  "@opentelemetry/sdk-node": "^0.215.0",
174
175
  "@opentelemetry/sdk-trace-node": "^2.7.0",
175
176
  "@opentelemetry/semantic-conventions": "^1.40.0",
176
- "@supabase/supabase-js": "^2.105.0",
177
+ "@supabase/supabase-js": "^2.105.1",
177
178
  "@types/bun": "^1.3.13",
178
179
  "@types/js-yaml": "^4.0.9",
179
180
  "@types/node": "^25.6.0",
@@ -194,7 +195,7 @@
194
195
  "js-yaml": "^4.1.1",
195
196
  "linkedom": "^0.18.12",
196
197
  "node-cron": "^4.2.1",
197
- "openai": "^6.34.0",
198
+ "openai": "^6.35.0",
198
199
  "papaparse": "^5.5.3",
199
200
  "partial-json": "^0.1.7",
200
201
  "pdf-lib": "^1.17.1",
@@ -204,7 +205,7 @@
204
205
  "tsc-alias": "^1.8.16",
205
206
  "typedoc": "^0.28.19",
206
207
  "typescript": "^6.0.3",
207
- "unpdf": "^1.6.0",
208
+ "unpdf": "^1.6.1",
208
209
  "validator": "^13.15.35",
209
210
  "vite": "8.0.10",
210
211
  "vitest": "^4.1.5"
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview One-shot migration: split the monolithic CHANGELOG.md into
4
+ * per-version files under `changelog/<major.minor>.x/<version>.md`.
5
+ *
6
+ * After the initial migration, this script is no longer needed — the
7
+ * per-version files become the source of truth and `scripts/build-changelog.ts`
8
+ * regenerates CHANGELOG.md from them. Keep this around only long enough to
9
+ * verify the round-trip (split → build → diff).
10
+ *
11
+ * Behavior:
12
+ * • Reads CHANGELOG.md, splits on `## [X.Y.Z] - YYYY-MM-DD` headers
13
+ * • Groups by minor series: 0.1.4 → changelog/0.1.x/0.1.4.md
14
+ * • Writes each version block with an H1 heading
15
+ * • Creates changelog/template.md template if missing
16
+ * • Overwrites existing per-version files (idempotent)
17
+ * • Drops the preamble (title + description) — that's handled by the build script
18
+ *
19
+ * Usage: bun run scripts/split-changelog.ts
20
+ *
21
+ * @module scripts/split-changelog
22
+ */
23
+
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
25
+ import { resolve } from 'node:path';
26
+
27
+ const CHANGELOG_PATH = resolve('CHANGELOG.md');
28
+ const CHANGELOG_DIR = resolve('changelog');
29
+ const TEMPLATE_PATH = resolve(CHANGELOG_DIR, 'template.md');
30
+
31
+ const TEMPLATE_CONTENT = `# <version> — YYYY-MM-DD
32
+
33
+ <!-- Brief summary of the upcoming release — delete this comment when filled in. -->
34
+
35
+ ## Added
36
+
37
+ -
38
+
39
+ ## Changed
40
+
41
+ -
42
+
43
+ ## Fixed
44
+
45
+ -
46
+ `;
47
+
48
+ interface Section {
49
+ body: string;
50
+ date: string;
51
+ version: string;
52
+ }
53
+
54
+ function extractSections(content: string): Section[] {
55
+ const regex = /^## \[([^\]]+)\] - (\d{4}-\d{2}-\d{2})$/gm;
56
+ const matches = [...content.matchAll(regex)];
57
+ if (matches.length === 0) {
58
+ throw new Error('No version sections found in CHANGELOG.md (expected ## [X.Y.Z] - YYYY-MM-DD)');
59
+ }
60
+
61
+ const sections: Section[] = [];
62
+ for (let i = 0; i < matches.length; i++) {
63
+ const match = matches[i] as RegExpMatchArray & { index: number };
64
+ const next = matches[i + 1] as (RegExpMatchArray & { index: number }) | undefined;
65
+ const start = match.index + match[0].length;
66
+ const end = next ? next.index : content.length;
67
+
68
+ let body = content.slice(start, end);
69
+ body = body.replace(/^\n+/, '');
70
+ body = body.replace(/\n+---\n*$/, '\n');
71
+ body = body.replace(/\n*$/, '\n');
72
+
73
+ sections.push({
74
+ version: match[1] as string,
75
+ date: match[2] as string,
76
+ body,
77
+ });
78
+ }
79
+ return sections;
80
+ }
81
+
82
+ /**
83
+ * Promote heading levels by stripping one `#` from H2+ lines. Assumes no H1
84
+ * or H2 in section bodies (the version H2 was already stripped); the existing
85
+ * CHANGELOG.md uses H3 for Added/Changed/Fixed etc., which become H2 here.
86
+ */
87
+ function promoteHeadings(body: string): string {
88
+ return body.replace(/^(#{2,6}) /gm, (_, hashes: string) => `${hashes.slice(1)} `);
89
+ }
90
+
91
+ function toPerVersionFile(section: Section): string {
92
+ return `# ${section.version} — ${section.date}\n\n${promoteHeadings(section.body)}`;
93
+ }
94
+
95
+ /** Extract the `major.minor.x` series directory for a version string. */
96
+ function seriesOf(version: string): string {
97
+ const [major, minor] = version.split('.');
98
+ if (!major || !minor) throw new Error(`Cannot derive series from version: ${version}`);
99
+ return `${major}.${minor}.x`;
100
+ }
101
+
102
+ function main(): void {
103
+ const content = readFileSync(CHANGELOG_PATH, 'utf-8');
104
+ const sections = extractSections(content);
105
+
106
+ mkdirSync(CHANGELOG_DIR, { recursive: true });
107
+
108
+ const seriesCounts = new Map<string, number>();
109
+
110
+ for (const section of sections) {
111
+ const series = seriesOf(section.version);
112
+ const seriesDir = resolve(CHANGELOG_DIR, series);
113
+ mkdirSync(seriesDir, { recursive: true });
114
+ const path = resolve(seriesDir, `${section.version}.md`);
115
+ writeFileSync(path, toPerVersionFile(section));
116
+ seriesCounts.set(series, (seriesCounts.get(series) ?? 0) + 1);
117
+ }
118
+
119
+ for (const [series, count] of [...seriesCounts.entries()].sort()) {
120
+ console.log(` + changelog/${series}/ (${count} file${count === 1 ? '' : 's'})`);
121
+ }
122
+
123
+ if (!existsSync(TEMPLATE_PATH)) {
124
+ writeFileSync(TEMPLATE_PATH, TEMPLATE_CONTENT);
125
+ console.log(' + changelog/template.md (format reference)');
126
+ }
127
+
128
+ console.log(`\nSplit ${sections.length} versions into ${seriesCounts.size} series.`);
129
+ console.log('Next: `bun run scripts/build-changelog.ts` to regenerate CHANGELOG.md,');
130
+ console.log(' then `diff` against the original to verify byte-equality.');
131
+ }
132
+
133
+ main();
@@ -9,6 +9,18 @@ metadata:
9
9
  type: reference
10
10
  ---
11
11
 
12
+ ## When to Use
13
+
14
+ App tools are **rarely the right choice**. Reach for one only when all of the following hold:
15
+
16
+ 1. A *human* will actively interact with the result in real time — not just an LLM consuming text.
17
+ 2. The target deployment runs in a client that supports MCP Apps. Many clients (Claude Code, Cursor, most chat UIs) are tool-only and will only ever see the `format()` text fallback you have to maintain anyway.
18
+ 3. The interaction the UI enables — scrubbing a dense table, approving a multi-step plan, filling a structured form — is core to the workflow, not nice-to-have rendering.
19
+
20
+ App tools cost more than standard tools: an iframe + CSP setup, `app.ontoolresult` / `callServerTool` plumbing, host-context wiring (theme, fonts, styles), and a `format()` text path that has to be content-complete because most clients see only that. Two surfaces to keep in sync, two failure modes per change.
21
+
22
+ Default to `add-tool`. This skill is the how-to once that bar is cleared — the "whether to" decision belongs in `design-mcp-server`.
23
+
12
24
  ## Context
13
25
 
14
26
  MCP Apps extend the standard tool pattern with an interactive HTML UI rendered in a sandboxed iframe by the host. Each MCP App consists of two definitions:
@@ -107,6 +107,46 @@ await createApp({
107
107
 
108
108
  If the repo already uses `src/mcp-server/resources/definitions/index.ts`, update that barrel instead of changing the registration style.
109
109
 
110
+ ### Optional: declarative `errors[]` contract
111
+
112
+ Resources can opt into the same typed error contract as tools — surfaced in `resources/list` under `_meta['mcp-ts-core/errors']` and bound to a typed `ctx.fail(reason, …)` keyed by the declared reason union:
113
+
114
+ ```typescript
115
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
116
+
117
+ export const articleResource = resource('article://{pmid}', {
118
+ description: 'Read an article by PMID.',
119
+ errors: [
120
+ { reason: 'no_pmid_match', code: JsonRpcErrorCode.NotFound,
121
+ when: 'PMID not found in the index.' },
122
+ { reason: 'withdrawn', code: JsonRpcErrorCode.NotFound,
123
+ when: 'Article was withdrawn upstream.' },
124
+ { reason: 'upstream_throttled', code: JsonRpcErrorCode.RateLimited,
125
+ when: 'Upstream PubMed quota hit.', retryable: true },
126
+ ],
127
+ params: z.object({ pmid: z.string().describe('PubMed ID') }),
128
+ async handler(params, ctx) {
129
+ const article = await fetchOne(params.pmid);
130
+ if (!article) throw ctx.fail('no_pmid_match', `PMID ${params.pmid} not indexed`);
131
+ if (article.withdrawn) throw ctx.fail('withdrawn');
132
+ return article;
133
+ },
134
+ });
135
+ ```
136
+
137
+ Without `errors[]`, the handler receives plain `Context` (no `fail` method) and throws via error factories (`notFound`, `serviceUnavailable`, …) directly. The contract is opt-in. See `skills/api-errors/SKILL.md` for the full pattern, baseline codes, and conformance rules.
138
+
139
+ ### Other `resource()` options
140
+
141
+ Beyond `description`, `params`, `handler`, and `list`, the builder also supports:
142
+
143
+ | Field | Purpose |
144
+ |:------|:--------|
145
+ | `output` | Optional Zod schema for runtime validation of the handler return value (parity with `tool()`'s `output`). |
146
+ | `format` | Optional formatter mapping the handler's return to the `ReadResourceResult.contents[]` shape. Default: string passthrough; objects serialized to JSON. Override when you need to attach permissions, custom encodings, or split into multiple content items. |
147
+ | `annotations` | Resource annotations (e.g., `audience`, `priority`) — see `ResourceAnnotations`. |
148
+ | `title` | Human-readable display title (defaults to `name`). |
149
+
110
150
  ## Checklist
111
151
 
112
152
  - [ ] File created at `src/mcp-server/resources/definitions/{{resource-name}}.resource.ts`
@@ -115,6 +115,7 @@ async fetchItem(id: string, ctx: Context): Promise<Item> {
115
115
  `${this.baseUrl}/items/${id}`,
116
116
  10_000,
117
117
  ctx,
118
+ { signal: ctx.signal },
118
119
  );
119
120
  const text = await response.text();
120
121
  return this.parseResponse<Item>(text);
@@ -136,9 +137,37 @@ async fetchItem(id: string, ctx: Context): Promise<Item> {
136
137
  3. **Classify parse failures by content.** If the upstream returns HTTP 200 with an HTML error page, detect it and throw `ServiceUnavailable` (transient) instead of `SerializationError` (non-transient).
137
138
  4. **Exhausted retries say so.** `withRetry` automatically enriches the final error with attempt count — callers know retries were already attempted.
138
139
 
140
+ ### When you need finer-grained HTTP error classification
141
+
142
+ `fetchWithTimeout` collapses every non-2xx into `ServiceUnavailable`. That's the safe default but it isn't always right — a `401` should be `Unauthorized`, a `429` should be `RateLimited` (and is retryable), a `408` should be `Timeout` (and is retryable). When you need the nuance, drop down to raw `fetch` + `httpErrorFromResponse`:
143
+
144
+ ```typescript
145
+ import { httpErrorFromResponse, withRetry } from '@cyanheads/mcp-ts-core/utils';
146
+
147
+ async fetchItem(id: string, ctx: Context): Promise<Item> {
148
+ return withRetry(
149
+ async () => {
150
+ const response = await fetch(`${this.baseUrl}/items/${id}`, { signal: ctx.signal });
151
+ if (!response.ok) {
152
+ throw await httpErrorFromResponse(response, {
153
+ service: 'MyAPI',
154
+ data: { itemId: id },
155
+ });
156
+ }
157
+ return this.parseResponse<Item>(await response.text());
158
+ },
159
+ { operation: 'fetchItem', context: ctx, signal: ctx.signal },
160
+ );
161
+ }
162
+ ```
163
+
164
+ `httpErrorFromResponse` maps the full status table (401/403/408/422/429/5xx) to the appropriate `JsonRpcErrorCode`, captures the response body (truncated), and forwards `Retry-After` headers into `error.data.retryAfter`. The codes it produces line up with `withRetry`'s transient-code set, so retryable HTTP failures (429, 503, 504) are retried automatically and non-retryable ones (401, 404, 422) fail immediately.
165
+
139
166
  ### Response handler pattern
140
167
 
141
168
  ```typescript
169
+ import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
170
+
142
171
  parseResponse<T>(text: string): T {
143
172
  // Detect HTML error pages masquerading as successful responses
144
173
  if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
@@ -188,6 +217,24 @@ function normalizeRepo(raw: RawRepo): Repo {
188
217
  }
189
218
  ```
190
219
 
220
+ ## Error Handling in Services
221
+
222
+ Services don't declare `errors: [...]` contracts and don't have `ctx.fail` — that contract surface is tool/resource-only. Inside services:
223
+
224
+ - **Throw via factories** when a specific code matters: `throw notFound(...)`, `throw rateLimited(...)`, `throw serviceUnavailable(...)`. The framework's auto-classifier catches anything else.
225
+ - **Wrap risky pipelines in `ErrorHandler.tryCatch`** when you want structured logging + auto-classification without writing try/catch boilerplate. It always rethrows — never swallows. Useful for parsing untrusted input (JSON, config) or third-party SDK calls whose error types you don't control:
226
+
227
+ ```ts
228
+ import { ErrorHandler } from '@cyanheads/mcp-ts-core/utils';
229
+
230
+ const parsed = await ErrorHandler.tryCatch(
231
+ () => JSON.parse(rawConfig),
232
+ { operation: 'MyService.parseConfig', errorCode: JsonRpcErrorCode.ConfigurationError },
233
+ );
234
+ ```
235
+
236
+ - **Tool/resource handlers bubble service errors unchanged** — the contract advertises the *advertised* failure surface, and any code thrown from a service still reaches the client correctly via the auto-classifier. The conformance lint scans handler source text only, so service-thrown codes aren't flagged.
237
+
191
238
  ## API Efficiency
192
239
 
193
240
  When a service wraps an external API, design methods to minimize upstream calls. These patterns compound — a tool calling 3 service methods that each make N requests is 3N calls; batching drops it to 3.
@@ -38,6 +38,7 @@ Read the handler and identify:
38
38
  | **`ctx.state` usage** | Use `createMockContext({ tenantId: 'test' })` to enable storage |
39
39
  | **`ctx.elicit` / `ctx.sample`** | Mock with `vi.fn()`, also test the absent case (undefined) |
40
40
  | **`ctx.progress`** | Use `createMockContext({ progress: true })` for task tools |
41
+ | **`ctx.fail` (typed contract)** | Definitions with `errors[]` need `fail` attached to the mock ctx — `createMockContext({ errors: myTool.errors })` does it for you. Assert on `data.reason` (stable per-contract entry), not just `code`. |
41
42
  | **`format` function** | Test separately if defined — it's pure, no ctx needed. Verify it renders the IDs and fields the model needs, not just a count or title. For projection-style tools, test non-default field selections. |
42
43
  | **Sparse upstream payloads** | For third-party API integrations, build a fixture with omitted fields. Assert normalized output still validates and `format()` preserves unknown values instead of inventing facts. |
43
44
  | **Auth scopes** | Not tested at handler level (framework enforces) — skip |
@@ -76,6 +77,17 @@ describe('{{TOOL_EXPORT}}', () => {
76
77
  await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toThrow();
77
78
  });
78
79
 
80
+ // Only when the tool declares `errors: [...]`. Drop this block otherwise.
81
+ it('throws ctx.fail("{{REASON}}") for the declared failure mode', async () => {
82
+ const ctx = createMockContext({ errors: {{TOOL_EXPORT}}.errors });
83
+ const input = {{TOOL_EXPORT}}.input.parse({
84
+ // input that triggers the declared failure mode
85
+ });
86
+ await expect({{TOOL_EXPORT}}.handler(input, ctx)).rejects.toMatchObject({
87
+ data: { reason: '{{REASON}}' },
88
+ });
89
+ });
90
+
79
91
  it('formats output completely', () => {
80
92
  const output = { /* mock output matching the output schema */ };
81
93
  const blocks = {{TOOL_EXPORT}}.format!(output);
@@ -115,6 +127,13 @@ describe('{{RESOURCE_EXPORT}}', () => {
115
127
  await expect({{RESOURCE_EXPORT}}.handler(params, ctx)).rejects.toThrow();
116
128
  });
117
129
 
130
+ // For resources that declare an `errors: [...]` contract, pass the contract via
131
+ // `createMockContext` so the typed `ctx.fail` is wired automatically:
132
+ // const ctx = createMockContext({ errors: {{RESOURCE_EXPORT}}.errors });
133
+ // const err = await {{RESOURCE_EXPORT}}.handler(params, ctx).catch((e) => e);
134
+ // expect(err.code).toBe(JsonRpcErrorCode.NotFound);
135
+ // expect(err.data.reason).toBe('no_match');
136
+
118
137
  it('lists available resources', async () => {
119
138
  const listing = await {{RESOURCE_EXPORT}}.list!();
120
139
  expect(listing.resources).toBeInstanceOf(Array);
@@ -139,9 +158,12 @@ import { beforeEach, describe, expect, it } from 'vitest';
139
158
  import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
140
159
  import { get{{ServiceClass}}, init{{ServiceClass}} } from '@/services/{{domain}}/{{domain}}-service.js';
141
160
 
161
+ import { createInMemoryStorage } from '@cyanheads/mcp-ts-core/testing';
162
+
142
163
  describe('{{ServiceClass}}', () => {
143
164
  beforeEach(() => {
144
165
  // Re-initialize with fresh config/storage for each test
166
+ const mockStorage = createInMemoryStorage();
145
167
  init{{ServiceClass}}(mockConfig, mockStorage);
146
168
  });
147
169
 
@@ -189,6 +211,23 @@ it('respects cancellation', async () => {
189
211
  });
190
212
  ```
191
213
 
214
+ ## Fuzz Testing
215
+
216
+ For schema-heavy or input-validation-critical handlers, the framework ships fuzz helpers that generate valid + adversarial inputs from your Zod schemas via `fast-check` and assert handler invariants (no crashes, no prototype pollution, no stack-trace leaks):
217
+
218
+ ```typescript
219
+ import { fuzzTool } from '@cyanheads/mcp-ts-core/testing/fuzz';
220
+
221
+ it('survives fuzz testing', async () => {
222
+ const report = await fuzzTool({{TOOL_EXPORT}}, { numRuns: 100 });
223
+ expect(report.crashes).toHaveLength(0);
224
+ expect(report.leaks).toHaveLength(0);
225
+ expect(report.prototypePollution).toBe(false);
226
+ });
227
+ ```
228
+
229
+ Available helpers from `@cyanheads/mcp-ts-core/testing/fuzz`: `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary` (custom property-based tests), `adversarialArbitrary` and `ADVERSARIAL_STRINGS` (targeted injection sets). Returns a `FuzzReport` you can assert against. Options: `numRuns`, `numAdversarial`, `seed` (reproducibility), `timeout`, `ctx` (`MockContextOptions` for stateful handlers).
230
+
192
231
  ## Generating Tests from Schemas
193
232
 
194
233
  When scaffolding tests for an existing handler, use the Zod schemas to generate meaningful test cases:
@@ -58,10 +58,16 @@ export const {{TOOL_EXPORT}} = tool('{{tool_name}}', {
58
58
  // All fields need .describe(). Only JSON-Schema-serializable Zod types allowed.
59
59
  }),
60
60
  // auth: ['tool:{{tool_name}}:read'],
61
+ // errors: [
62
+ // { reason: 'no_match', code: JsonRpcErrorCode.NotFound, when: 'No items matched the query.' },
63
+ // { reason: 'queue_full', code: JsonRpcErrorCode.RateLimited, when: 'Local queue at capacity.', retryable: true },
64
+ // ],
61
65
 
62
66
  async handler(input, ctx) {
63
67
  ctx.log.info('Processing', { /* relevant input fields */ });
64
- // Pure logic — throw on failure, no try/catch
68
+ // Pure logic — throw on failure, no try/catch.
69
+ // With an `errors[]` contract: `throw ctx.fail('reason_id', message?, data?)`.
70
+ // Without: throw via factories (`notFound`, `validationError`, …) or plain `Error`.
65
71
  return { /* output */ };
66
72
  },
67
73
 
@@ -233,9 +239,37 @@ format: (result) => [{
233
239
 
234
240
  ### Error classification and messaging
235
241
 
236
- The framework auto-classifies many errors at runtime (HTTP status codes, JS error types, common patterns). Use explicit error factories when you want a specific code and clear recovery guidance; plain `throw new Error()` when auto-classification is sufficient.
242
+ **Recommended: declare an `errors[]` contract.** A typed contract surfaces in `tools/list` and gives the handler a typed `ctx.fail(reason, …)` keyed by the declared reason union TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated and tamper-proof, and the linter enforces conformance against the handler body.
237
243
 
238
- **Classify by origin** — different sources need different codes:
244
+ ```typescript
245
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
246
+
247
+ export const fetchArticles = tool('fetch_articles', {
248
+ description: 'Fetch articles by PMID.',
249
+ errors: [
250
+ { reason: 'no_pmid_match', code: JsonRpcErrorCode.NotFound,
251
+ when: 'None of the requested PMIDs returned data.' },
252
+ { reason: 'queue_full', code: JsonRpcErrorCode.RateLimited,
253
+ when: 'Local request queue at capacity.', retryable: true },
254
+ ],
255
+ input: z.object({ pmids: z.array(z.string()).describe('PMIDs to fetch') }),
256
+ output: z.object({ articles: z.array(ArticleSchema).describe('Resolved articles') }),
257
+ async handler(input, ctx) {
258
+ if (queue.full()) throw ctx.fail('queue_full');
259
+ const articles = await fetch(input.pmids);
260
+ if (articles.length === 0) {
261
+ throw ctx.fail('no_pmid_match', `No data for ${input.pmids.length} PMIDs`, { pmids: input.pmids });
262
+ }
263
+ return { articles };
264
+ },
265
+ });
266
+ ```
267
+
268
+ **Baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring. Omit the contract only for throwaway prototypes — declare it everywhere else. Wire-level behavior is identical when omitted, but you lose the type-checked `ctx.fail`, the `tools/list` advertisement, and conformance lint coverage.
269
+
270
+ `ctx.fail` accepts an optional 4th `options` argument for ES2022 cause chaining: `throw ctx.fail('upstream_error', 'Upstream returned 500', { url }, { cause: e })`.
271
+
272
+ **Fallback: error factories.** Use when no contract entry fits — ad-hoc throws, prototype tools, or service-layer code. The framework also auto-classifies plain `throw new Error()` from message patterns as a last resort.
239
273
 
240
274
  ```typescript
241
275
  // Client input error — agent can fix and retry
@@ -259,7 +293,7 @@ throw invalidParams(
259
293
  );
260
294
  ```
261
295
 
262
- **Error messages are recovery instructions.** Name what went wrong, why, and what action to take. The message is the agent's only signal — a bare "Not found" is a dead end.
296
+ **Error messages are recovery instructions.** Name what went wrong, why, and what action to take. The message is the agent's only signal — a bare "Not found" is a dead end. See `skills/api-errors/SKILL.md` for the full contract pattern, factories list, and auto-classification table.
263
297
 
264
298
  ### Include operational metadata
265
299
 
@@ -329,6 +363,7 @@ Large payloads burn the agent's context window. Default to curated summaries; of
329
363
  - [ ] `format()` renders every field in the output schema — enforced at lint time via sentinel injection, startup fails with `format-parity` errors otherwise. Different clients forward different surfaces (Claude Code → `structuredContent`, Claude Desktop → `content[]`); both must carry the same data. Primary fix: render the missing field in `format()` (use `z.discriminatedUnion` for list/detail variants). Escape hatch: if the output schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) rather than maintaining aspirational typing
330
364
  - [ ] If wrapping external API: output schema and `format()` preserve uncertainty from sparse upstream payloads instead of inventing concrete values
331
365
  - [ ] `auth` scopes declared if the tool needs authorization
366
+ - [ ] `errors: [...]` contract declared for known domain failure modes (recommended; omit only for throwaway prototypes)
332
367
  - [ ] `task: true` added if the tool is long-running
333
368
  - [ ] Registered in the project's existing `createApp()` tool list (directly or via barrel)
334
369
  - [ ] `bun run devcheck` passes