@cyanheads/mcp-ts-core 0.7.6 → 0.8.1

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 (107) hide show
  1. package/CLAUDE.md +22 -7
  2. package/README.md +2 -2
  3. package/changelog/0.8.x/0.8.0.md +33 -0
  4. package/changelog/0.8.x/0.8.1.md +17 -0
  5. package/changelog/template.md +13 -0
  6. package/dist/core/context.d.ts +67 -0
  7. package/dist/core/context.d.ts.map +1 -1
  8. package/dist/core/context.js +46 -1
  9. package/dist/core/context.js.map +1 -1
  10. package/dist/core/index.d.ts +2 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +1 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/linter/rules/error-contract-rules.d.ts +45 -0
  15. package/dist/linter/rules/error-contract-rules.d.ts.map +1 -0
  16. package/dist/linter/rules/error-contract-rules.js +321 -0
  17. package/dist/linter/rules/error-contract-rules.js.map +1 -0
  18. package/dist/linter/rules/handler-body-rules.d.ts +18 -0
  19. package/dist/linter/rules/handler-body-rules.d.ts.map +1 -0
  20. package/dist/linter/rules/handler-body-rules.js +134 -0
  21. package/dist/linter/rules/handler-body-rules.js.map +1 -0
  22. package/dist/linter/rules/index.d.ts +2 -0
  23. package/dist/linter/rules/index.d.ts.map +1 -1
  24. package/dist/linter/rules/index.js +2 -0
  25. package/dist/linter/rules/index.js.map +1 -1
  26. package/dist/linter/rules/resource-rules.d.ts.map +1 -1
  27. package/dist/linter/rules/resource-rules.js +9 -0
  28. package/dist/linter/rules/resource-rules.js.map +1 -1
  29. package/dist/linter/rules/source-text.d.ts +19 -0
  30. package/dist/linter/rules/source-text.d.ts.map +1 -0
  31. package/dist/linter/rules/source-text.js +96 -0
  32. package/dist/linter/rules/source-text.js.map +1 -0
  33. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  34. package/dist/linter/rules/tool-rules.js +9 -0
  35. package/dist/linter/rules/tool-rules.js.map +1 -1
  36. package/dist/logs/combined.log +4 -4
  37. package/dist/logs/error.log +4 -4
  38. package/dist/mcp-server/apps/appBuilders.d.ts +9 -4
  39. package/dist/mcp-server/apps/appBuilders.d.ts.map +1 -1
  40. package/dist/mcp-server/apps/appBuilders.js +4 -0
  41. package/dist/mcp-server/apps/appBuilders.js.map +1 -1
  42. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  43. package/dist/mcp-server/resources/resource-registration.js +3 -2
  44. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  45. package/dist/mcp-server/resources/utils/resourceDefinition.d.ts +13 -5
  46. package/dist/mcp-server/resources/utils/resourceDefinition.d.ts.map +1 -1
  47. package/dist/mcp-server/resources/utils/resourceDefinition.js.map +1 -1
  48. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  49. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +5 -4
  50. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  51. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  52. package/dist/mcp-server/tools/tool-registration.js +13 -7
  53. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  54. package/dist/mcp-server/tools/utils/toolDefinition.d.ts +64 -16
  55. package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
  56. package/dist/mcp-server/tools/utils/toolDefinition.js +25 -11
  57. package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
  58. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  59. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +6 -4
  60. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  61. package/dist/testing/index.d.ts +8 -0
  62. package/dist/testing/index.d.ts.map +1 -1
  63. package/dist/testing/index.js +5 -1
  64. package/dist/testing/index.js.map +1 -1
  65. package/dist/types-global/errors.d.ts +82 -0
  66. package/dist/types-global/errors.d.ts.map +1 -1
  67. package/dist/types-global/errors.js +25 -0
  68. package/dist/types-global/errors.js.map +1 -1
  69. package/dist/utils/formatting/index.d.ts +1 -0
  70. package/dist/utils/formatting/index.d.ts.map +1 -1
  71. package/dist/utils/formatting/index.js +1 -0
  72. package/dist/utils/formatting/index.js.map +1 -1
  73. package/dist/utils/formatting/partialResult.d.ts +145 -0
  74. package/dist/utils/formatting/partialResult.d.ts.map +1 -0
  75. package/dist/utils/formatting/partialResult.js +145 -0
  76. package/dist/utils/formatting/partialResult.js.map +1 -0
  77. package/dist/utils/index.d.ts +2 -1
  78. package/dist/utils/index.d.ts.map +1 -1
  79. package/dist/utils/index.js +2 -1
  80. package/dist/utils/index.js.map +1 -1
  81. package/dist/utils/network/httpError.d.ts +112 -0
  82. package/dist/utils/network/httpError.d.ts.map +1 -0
  83. package/dist/utils/network/httpError.js +153 -0
  84. package/dist/utils/network/httpError.js.map +1 -0
  85. package/dist/utils/network/retry.d.ts.map +1 -1
  86. package/dist/utils/network/retry.js +0 -1
  87. package/dist/utils/network/retry.js.map +1 -1
  88. package/package.json +5 -4
  89. package/scripts/split-changelog.ts +133 -0
  90. package/skills/add-app-tool/SKILL.md +12 -0
  91. package/skills/add-resource/SKILL.md +40 -0
  92. package/skills/add-service/SKILL.md +54 -1
  93. package/skills/add-test/SKILL.md +39 -0
  94. package/skills/add-tool/SKILL.md +42 -5
  95. package/skills/api-context/SKILL.md +75 -1
  96. package/skills/api-errors/SKILL.md +183 -5
  97. package/skills/api-linter/SKILL.md +223 -3
  98. package/skills/api-testing/SKILL.md +79 -4
  99. package/skills/api-utils/SKILL.md +4 -2
  100. package/skills/design-mcp-server/SKILL.md +13 -10
  101. package/skills/field-test/SKILL.md +81 -15
  102. package/skills/maintenance/SKILL.md +5 -2
  103. package/skills/report-issue-framework/SKILL.md +2 -2
  104. package/skills/security-pass/SKILL.md +6 -5
  105. package/templates/AGENTS.md +23 -8
  106. package/templates/CLAUDE.md +23 -8
  107. package/templates/changelog/template.md +18 -5
@@ -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.0"
7
+ version: "2.1"
8
8
  audience: external
9
9
  type: debug
10
10
  ---
@@ -27,14 +27,20 @@ Write the helper to `/tmp/mcp-field-test.sh` once, then source it in every subse
27
27
  cat > /tmp/mcp-field-test.sh <<'HELPER_EOF'
28
28
  #!/bin/bash
29
29
  # Field-test helper: manage an MCP HTTP server + JSON-RPC session across shell calls.
30
+ # Surfaces failures aggressively — field test is for finding things that fail,
31
+ # so the helper auto-tails logs and prints HTTP status/body on errors instead
32
+ # of swallowing them.
30
33
  STATE_FILE="/tmp/mcp-field-test.env"
31
34
  [ -f "$STATE_FILE" ] && . "$STATE_FILE"
32
35
 
33
36
  mcp_start() {
34
37
  local dir="${1:-$PWD}"
35
38
  echo "building $dir ..."
36
- (cd "$dir" && bun run rebuild) >/tmp/mcp-build.log 2>&1 \
37
- || { echo "BUILD FAILED — see /tmp/mcp-build.log"; return 1; }
39
+ if ! (cd "$dir" && bun run rebuild) >/tmp/mcp-build.log 2>&1; then
40
+ echo "BUILD FAILED — last 30 lines of /tmp/mcp-build.log:"
41
+ tail -30 /tmp/mcp-build.log
42
+ return 1
43
+ fi
38
44
  echo "starting server ..."
39
45
  (cd "$dir" && bun run start:http) >/tmp/mcp-server.log 2>&1 &
40
46
  local pid=$!
@@ -45,7 +51,8 @@ mcp_start() {
45
51
  sleep 0.25
46
52
  done
47
53
  if [ -z "$line" ]; then
48
- echo "server failed to start — see /tmp/mcp-server.log"
54
+ echo "server failed to start within 10s last 30 lines of /tmp/mcp-server.log:"
55
+ tail -30 /tmp/mcp-server.log
49
56
  kill "$pid" 2>/dev/null
50
57
  return 1
51
58
  fi
@@ -63,12 +70,21 @@ EOF
63
70
  mcp_init() {
64
71
  [ -z "$MCP_URL" ] && { echo "run mcp_start first"; return 1; }
65
72
  local hdr="/tmp/mcp-init-headers.txt"
66
- curl -sS -D "$hdr" -X POST "$MCP_URL" \
73
+ local body_file="/tmp/mcp-init-body.txt"
74
+ local status
75
+ status=$(curl -sS -D "$hdr" -o "$body_file" -w '%{http_code}' -X POST "$MCP_URL" \
67
76
  -H "Content-Type: application/json" \
68
77
  -H "Accept: application/json, text/event-stream" \
69
- -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"field-test","version":"2.0"}}}' >/dev/null
78
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"field-test","version":"2.1"}}}')
70
79
  local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
71
- [ -z "$sid" ] && { echo "no session id returned"; return 1; }
80
+ if [ -z "$sid" ]; then
81
+ echo "init failed — HTTP $status, no Mcp-Session-Id header returned"
82
+ echo "--- response body ---"
83
+ cat "$body_file"
84
+ echo "--- response headers ---"
85
+ cat "$hdr"
86
+ return 1
87
+ fi
72
88
  cat > "$STATE_FILE" <<EOF
73
89
  export MCP_PID=$MCP_PID
74
90
  export MCP_URL=$MCP_URL
@@ -81,11 +97,13 @@ EOF
81
97
  -H "Accept: application/json, text/event-stream" \
82
98
  -H "Mcp-Session-Id: $sid" \
83
99
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
84
- echo "session=$sid"
100
+ echo "session=$sid (HTTP $status)"
85
101
  }
86
102
 
87
103
  # Usage: mcp_call METHOD [JSON_PARAMS]
88
- # Prints the JSON-RPC response (SSE framing stripped). Pipe to `jq`.
104
+ # Prints the JSON-RPC response. SSE framing is stripped when present; on
105
+ # non-SSE responses the raw body is printed instead so plain-JSON error
106
+ # replies (HTTP 4xx/5xx) still surface. Pipe to `jq`.
89
107
  mcp_call() {
90
108
  [ -z "$MCP_SID" ] && { echo "run mcp_init first"; return 1; }
91
109
  local method="$1"; local params="${2:-}"
@@ -95,17 +113,57 @@ mcp_call() {
95
113
  else
96
114
  body=$(printf '{"jsonrpc":"2.0","id":%d,"method":"%s","params":%s}' "$RANDOM" "$method" "$params")
97
115
  fi
98
- curl -sS -X POST "$MCP_URL" \
116
+ local resp_file="/tmp/mcp-call-body.txt"
117
+ local status
118
+ status=$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST "$MCP_URL" \
99
119
  -H "Content-Type: application/json" \
100
120
  -H "Accept: application/json, text/event-stream" \
101
121
  -H "Mcp-Session-Id: $MCP_SID" \
102
- -d "$body" | sed -n 's/^data: //p'
122
+ -d "$body")
123
+ if [ "$status" -ge 400 ]; then
124
+ echo "HTTP $status from $method — response:" >&2
125
+ cat "$resp_file" >&2
126
+ return 1
127
+ fi
128
+ local sse; sse=$(sed -n 's/^data: //p' "$resp_file")
129
+ if [ -n "$sse" ]; then
130
+ printf '%s\n' "$sse"
131
+ else
132
+ cat "$resp_file"
133
+ fi
134
+ }
135
+
136
+ # Tail the server log. Useful when a call surprises you — pino startup banner,
137
+ # definition lint diagnostics, request handler errors, upstream calls, and
138
+ # rate-limit warnings live in /tmp/mcp-server.log.
139
+ # Usage: mcp_log [N] (default: 50 lines)
140
+ mcp_log() {
141
+ local n="${1:-50}"
142
+ tail -n "$n" /tmp/mcp-server.log
103
143
  }
104
144
 
105
145
  mcp_stop() {
106
- [ -n "$MCP_PID" ] && kill "$MCP_PID" 2>/dev/null
146
+ if [ -z "$MCP_PID" ]; then
147
+ rm -f "$STATE_FILE"
148
+ echo "no PID to stop"
149
+ return 0
150
+ fi
151
+ kill "$MCP_PID" 2>/dev/null
152
+ for _ in $(seq 1 12); do
153
+ kill -0 "$MCP_PID" 2>/dev/null || break
154
+ sleep 0.25
155
+ done
156
+ if kill -0 "$MCP_PID" 2>/dev/null; then
157
+ echo "PID $MCP_PID didn't exit on SIGTERM — sending SIGKILL"
158
+ kill -9 "$MCP_PID" 2>/dev/null
159
+ sleep 0.5
160
+ fi
161
+ if kill -0 "$MCP_PID" 2>/dev/null; then
162
+ echo "WARNING: PID $MCP_PID still alive after SIGKILL"
163
+ else
164
+ echo "stopped pid=$MCP_PID"
165
+ fi
107
166
  rm -f "$STATE_FILE"
108
- echo "stopped"
109
167
  }
110
168
  HELPER_EOF
111
169
 
@@ -132,8 +190,8 @@ Runs `initialize`, captures the session id, sends `notifications/initialized`.
132
190
 
133
191
  ```bash
134
192
  . /tmp/mcp-field-test.sh
135
- mcp_call tools/list | jq '.result.tools[] | {name, description, inputSchema, outputSchema}'
136
- mcp_call resources/list | jq '.result.resources[] | {uri, name, mimeType}'
193
+ mcp_call tools/list | jq '.result.tools[] | {name, description, inputSchema, outputSchema, errors: ._meta["mcp-ts-core/errors"]}'
194
+ mcp_call resources/list | jq '.result.resources[] | {uri, name, mimeType, errors: ._meta["mcp-ts-core/errors"]}'
137
195
  mcp_call prompts/list | jq '.result.prompts[] | {name, description, arguments}'
138
196
  ```
139
197
 
@@ -171,6 +229,8 @@ Treat any hit as a `ux` finding in the report. The authoring rule lives under *T
171
229
  | Hits external API / live upstream | One call that exercises upstream; note rate-limit / timeout / transient-failure behavior |
172
230
  | Chained with other tools (search → detail → act) | Run one representative chain end-to-end; does each step return the IDs/cursors the next needs? |
173
231
  | `cursor` / `offset` / `limit` params | Pagination: second page, end-of-list |
232
+ | Tool declared an `errors: [...]` contract | Error contract (tool): trigger ≥1 declared failure mode. Verify `result._meta.error.code` matches the contract entry, `result._meta.error.data.reason` is the declared reason (only present when the handler threw an `McpError` — `ctx.fail` always does, plain `throw new Error(...)` does not), and `content[0].text` is actionable. Reasons declared but unreachable from any input are dead contract entries. |
233
+ | Resource declared an `errors: [...]` contract | Error contract (resource): trigger ≥1 declared failure mode by reading a URI that exercises it. Resources re-throw errors at the JSON-RPC level — verify `error.code` matches the contract entry and `error.data.reason` is the declared reason. (Resources don't use the `result.isError` envelope — they fail the request itself.) |
174
234
 
175
235
  **Resources.** Happy path, not-found URI, `list` if defined, pagination if used.
176
236
  **Prompts.** Happy path, defaults omitted, skim message quality.
@@ -188,9 +248,13 @@ Use `TaskCreate` — one task per definition. Mark complete as you go. Don't bat
188
248
 
189
249
  For each call, capture: input sent, response (trim huge payloads to files), whether `isError: true` appeared, anything surprising (slow response, parity drift, unhelpful text, crash).
190
250
 
251
+ When a call surprises you — slow, hangs, returns terse output, surfaces an unhelpful error — run `. /tmp/mcp-field-test.sh && mcp_log` to tail the server log. The pino startup banner, request handler errors, upstream API call traces, and rate-limit warnings all land in `/tmp/mcp-server.log` rather than coming back through `mcp_call`. Don't guess at runtime behavior from response text alone.
252
+
191
253
  **Interpreting responses**
192
254
 
193
255
  - Tool domain errors return `{result: {content: [...], isError: true}}` — they live in `result`, not `error`. Check `isError`, not the JSON-RPC error field.
256
+ - **Tool error code/reason** rides on `result._meta.error.{code, 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.
257
+ - **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.
194
258
  - JSON-RPC `error` only appears for protocol issues (bad session, malformed envelope, unknown method).
195
259
  - `mcp_call` already strips SSE framing. Pipe to `jq` for readability.
196
260
 
@@ -253,6 +317,8 @@ End with:
253
317
  - [ ] Catalog surfaced and presented; descriptions audited for leaks (implementation details, meta-coaching, consumer-aware phrasing)
254
318
  - [ ] Universal battery run on every definition
255
319
  - [ ] Situational categories applied only when triggered
320
+ - [ ] **If a tool declared an `errors: [...]` contract:** ≥1 declared failure mode triggered; `result._meta.error.code` and `data.reason` verified against the contract entry
321
+ - [ ] **If a resource declared an `errors: [...]` contract:** ≥1 declared failure mode triggered; top-level JSON-RPC `error.code` and `error.data.reason` verified against the contract entry
256
322
  - [ ] External-state / auth-gated tools handled explicitly (run, skip, or confirm)
257
323
  - [ ] Server stopped; state file removed
258
324
  - [ ] Report: summary paragraph → grouped findings → numbered options
@@ -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.7"
7
+ version: "1.9"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -50,6 +50,8 @@ 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
+ **Skill-version paradox.** If `node_modules/@cyanheads/mcp-ts-core/skills/maintenance/SKILL.md`'s `version` exceeds the one running, run Step 5 Phase A first and re-invoke `maintenance` — otherwise feature-adoption rows added in the new version silently don't surface.
54
+
53
55
  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
56
 
55
57
  Example — `0.5.2 → 0.5.4` means reading two new version files:
@@ -65,7 +67,8 @@ Scan specifically for:
65
67
 
66
68
  | Area | Adoption Check |
67
69
  |:-----|:---------------|
68
- | New error factories in `/errors` | Replace ad-hoc `new McpError(...)` with factories where applicable |
70
+ | New `/errors` surface — factories, typed contracts (`errors[]` + `ctx.fail`), `httpErrorFromResponse` | Replace ad-hoc `new McpError(...)` with factories; declare `errors: [...]` on tools that surface domain-specific failure modes; route declared throws through `ctx.fail(reason, …)` so the conformance lint is happy |
71
+ | Existing factory choice — semantic audit | Beyond factory-vs-`new McpError`: audit each `throw factory(...)` against intent. `invalidParams` (-32602) is for malformed JSON-RPC params (wrong-shape post-Zod is rare); semantic post-shape validation should use `validationError` (-32007). `notFound` for missing entities, `conflict` for state collisions, `unauthorized` vs `forbidden` for unauth vs scope-denied. Wrong codes degrade `mcp_error_classified_code` observability and break client retry logic — fix during this pass even if not adopting contracts yet. |
69
72
  | New utilities in `/utils` | Identify any that supersede local helper code |
70
73
  | New context capabilities | Added `ctx.*` methods worth adopting |
71
74
  | Provider/service APIs | Updates to `OpenRouterProvider`, `SpeechService`, `GraphService`, etc. |
@@ -4,7 +4,7 @@ description: >
4
4
  File a bug or feature request against @cyanheads/mcp-ts-core when you hit a framework issue. Use when a builder, utility, context method, or config behaves contrary to the documented API — not for server-specific application bugs.
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
  ---
@@ -141,7 +141,7 @@ Format: `bug(<scope>): concise description`
141
141
  | `prompt` | Prompt builder, generate, args |
142
142
  | `context` | Context, logger, state, progress, elicit, sample |
143
143
  | `config` | AppConfig, parseConfig, env parsing |
144
- | `errors` | McpError, error factories, auto-classification |
144
+ | `errors` | McpError, error factories, typed contracts (`errors[]` / `ctx.fail`), conformance lint, `httpErrorFromResponse`, auto-classification |
145
145
  | `auth` | Auth modes, scope checking, JWT/OAuth |
146
146
  | `storage` | StorageService, providers |
147
147
  | `transport` | stdio/http transport, SSE, session handling |
@@ -4,7 +4,7 @@ description: >
4
4
  Review an MCP server for common security gaps: LLM-facing surfaces as injection vector (tools, resources, prompts, descriptions), scope blast radius, destructive ops without consent, upstream auth shape, input sinks (URL / path / roots / shell / sampling / schema strictness / ReDoS), tenant isolation, leakage through errors and telemetry, unbounded resources, and HTTP-mode deployment surface. Use before a release, after a batch of handler changes, or when the user asks for a security review, audit, or hardening pass. Produces grouped findings and a numbered options list.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.1"
7
+ version: "1.2"
8
8
  audience: external
9
9
  type: audit
10
10
  ---
@@ -203,10 +203,10 @@ grep -rn "^let " src/services/
203
203
 
204
204
  What accidentally reaches the LLM, user, or observability sinks.
205
205
 
206
- **Look in:** `throw new McpError(...)` sites, `McpError.data` fields, output schemas, and every logging / telemetry surface — not just `ctx.log`.
206
+ **Look in:** `throw new McpError(...)` and `ctx.fail(reason, msg, data)` sites, error factory calls (`notFound`, `httpErrorFromResponse`, …), `McpError.data` fields (the `data` arg flows through both paths), output schemas, and every logging / telemetry surface — not just `ctx.log`.
207
207
 
208
208
  ```bash
209
- grep -rn "new McpError" src/
209
+ grep -rnE "new McpError|ctx\.fail\(|httpErrorFromResponse\(" src/
210
210
  grep -rnE "\b(ctx\.log|console\.(log|info|warn|error|debug)|logger\.)" src/
211
211
  grep -rnE "(Sentry\.|captureException|setTag|setContext|addBreadcrumb)" src/
212
212
  grep -rnE "(setAttribute|setAttributes|span\.)" src/ # OpenTelemetry
@@ -214,7 +214,8 @@ grep -rnE "(setAttribute|setAttributes|span\.)" src/ # OpenTelemetry
214
214
 
215
215
  **Check:**
216
216
 
217
- - Error `data` fields carry upstream response bodies, auth headers, stack traces?
217
+ - Error `data` fields (whether passed via `ctx.fail(reason, msg, data)`, `new McpError(code, msg, data)`, or factory calls) carry upstream response bodies, auth headers, stack traces?
218
+ - `httpErrorFromResponse` body capture sweeping in too much (default 500-byte cap is fine for most APIs but consider `captureBody: false` when the upstream returns auth-bearing payloads)?
218
219
  - Output schemas include token prefixes, internal IDs, session identifiers?
219
220
  - `format()` renders fields that shouldn't leave the server?
220
221
  - `ctx.log.info(msg, body)` where `body` is the raw request (may contain secrets)?
@@ -222,7 +223,7 @@ grep -rnE "(setAttribute|setAttributes|span\.)" src/ # OpenTelemetry
222
223
  - OpenTelemetry span attributes / Sentry breadcrumbs carry tokens, PII, or full request bodies?
223
224
  - Secret / token / HMAC comparisons use `===` or `==` instead of constant-time (`timingSafeEqual` / `crypto.timingSafeEqual`) — leaks length and prefix via timing?
224
225
 
225
- **Smell:** `throw new McpError(code, upstream.message, { raw: upstream.body })`. Or: `if (apiKey === expected)` on a request-auth path.
226
+ **Smell:** `throw new McpError(code, upstream.message, { raw: upstream.body })` or `throw ctx.fail('upstream_failed', e.message, { raw: e.response.body })`. Or: `if (apiKey === expected)` on a request-auth path.
226
227
 
227
228
  #### Axis 8 — Resource bounds
228
229
 
@@ -165,24 +165,39 @@ Handlers receive a unified `ctx` object. Key properties:
165
165
 
166
166
  ## Errors
167
167
 
168
- Handlers throw — the framework catches, classifies, and formats. Three escalation levels:
168
+ Handlers throw — the framework catches, classifies, and formats.
169
+
170
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, retryable? }]` on `tool()` / `resource()` to advertise the failure surface in `tools/list` (under `_meta['mcp-ts-core/errors']`) and receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
169
171
 
170
172
  ```ts
171
- // 1. Plain Error — framework auto-classifies from message patterns
172
- throw new Error('Item not found'); // NotFound
173
- throw new Error('Invalid query format'); // → ValidationError
173
+ errors: [
174
+ { reason: 'no_match', code: JsonRpcErrorCode.NotFound, when: 'No item matched the query' },
175
+ ],
176
+ async handler(input, ctx) {
177
+ const item = await db.find(input.id);
178
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
179
+ return item;
180
+ }
181
+ ```
174
182
 
175
- // 2. Error factories explicit code, concise
176
- import { notFound, validationError, forbidden, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
183
+ **Fallback (no contract entry fits):** throw via factories or plain `Error`.
184
+
185
+ ```ts
186
+ // Error factories — explicit code
187
+ import { notFound, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
177
188
  throw notFound('Item not found', { itemId });
178
189
  throw serviceUnavailable('API unavailable', { url }, { cause: err });
179
190
 
180
- // 3. McpErrorfull control over code and data
191
+ // Plain Errorframework auto-classifies from message patterns
192
+ throw new Error('Item not found'); // → NotFound
193
+ throw new Error('Invalid query format'); // → ValidationError
194
+
195
+ // McpError — when no factory exists for the code
181
196
  import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
182
197
  throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
183
198
  ```
184
199
 
185
- Plain `Error` is fine for most cases. Use factories when the error code matters. See framework CLAUDE.md for the full auto-classification table and all available factories.
200
+ See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
186
201
 
187
202
  ---
188
203
 
@@ -165,24 +165,39 @@ Handlers receive a unified `ctx` object. Key properties:
165
165
 
166
166
  ## Errors
167
167
 
168
- Handlers throw — the framework catches, classifies, and formats. Three escalation levels:
168
+ Handlers throw — the framework catches, classifies, and formats.
169
+
170
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, retryable? }]` on `tool()` / `resource()` to advertise the failure surface in `tools/list` (under `_meta['mcp-ts-core/errors']`) and receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
169
171
 
170
172
  ```ts
171
- // 1. Plain Error — framework auto-classifies from message patterns
172
- throw new Error('Item not found'); // NotFound
173
- throw new Error('Invalid query format'); // → ValidationError
173
+ errors: [
174
+ { reason: 'no_match', code: JsonRpcErrorCode.NotFound, when: 'No item matched the query' },
175
+ ],
176
+ async handler(input, ctx) {
177
+ const item = await db.find(input.id);
178
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
179
+ return item;
180
+ }
181
+ ```
174
182
 
175
- // 2. Error factories explicit code, concise
176
- import { notFound, validationError, forbidden, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
183
+ **Fallback (no contract entry fits):** throw via factories or plain `Error`.
184
+
185
+ ```ts
186
+ // Error factories — explicit code
187
+ import { notFound, serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
177
188
  throw notFound('Item not found', { itemId });
178
189
  throw serviceUnavailable('API unavailable', { url }, { cause: err });
179
190
 
180
- // 3. McpErrorfull control over code and data
191
+ // Plain Errorframework auto-classifies from message patterns
192
+ throw new Error('Item not found'); // → NotFound
193
+ throw new Error('Invalid query format'); // → ValidationError
194
+
195
+ // McpError — when no factory exists for the code
181
196
  import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
182
197
  throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection failed', { pool: 'primary' });
183
198
  ```
184
199
 
185
- Plain `Error` is fine for most cases. Use factories when the error code matters. See framework CLAUDE.md for the full auto-classification table and all available factories.
200
+ See framework CLAUDE.md and the `api-errors` skill for the full auto-classification table, all available factories, and the contract reference.
186
201
 
187
202
  ---
188
203
 
@@ -2,7 +2,7 @@
2
2
  # FORMAT REFERENCE — this file is never edited, never moved, never renamed.
3
3
  #
4
4
  # At release time, author a new per-version file at:
5
- # changelog/<major.minor>.x/<version>.md (e.g. changelog/0.1.x/0.1.0.md)
5
+ # changelog/<major.minor>.x/<version>.md (e.g. changelog/0.6.x/0.6.6.md)
6
6
  # using this file's frontmatter and section layout as the starting point.
7
7
  # Set that new file's H1 to `# <version> — YYYY-MM-DD` with a concrete date.
8
8
 
@@ -27,16 +27,29 @@ breaking: false
27
27
 
28
28
  Optional narrative intro — 1-3 sentences framing the release theme. Delete if not needed.
29
29
 
30
+ TONE: terse and fact-dense. 1-2 sentence(s) per bullet where possible —
31
+ name the symbol, state what changed, stop. Drop "explains how it works" prose;
32
+ that belongs in JSDoc, AGENTS.md, or the relevant skill. Drop ceremonial
33
+ framings ("This release introduces…", "fully backwards compatible:" with a
34
+ paragraph of justification). Prefer code/symbol names over English
35
+ re-explanations. If a bullet runs more than ~2 lines, split it or cut it.
36
+
37
+ WHAT TO INCLUDE: every distinct fact a reader needs to adopt or audit the
38
+ release — new exports, signatures, lint rule IDs, env vars, breaking
39
+ changes, version bumps on shipped skills. WHAT TO CUT: mechanism walkthroughs,
40
+ duplicate prose between Added and Changed, file-by-file test enumerations,
41
+ internal implementation notes. Trust the reader to read the code or the docs.
42
+
30
43
  Linking issues/PRs: use full URLs so the link works everywhere (GitHub web UI,
31
44
  npm/node_modules reads, local editors). GitHub's bare `#NN` auto-link only
32
45
  resolves inside its own UI.
33
46
 
34
- [#12](https://github.com/<owner>/<repo>/issues/12) ← issue
35
- [#17](https://github.com/<owner>/<repo>/pull/17) ← PR
47
+ [#38](https://github.com/cyanheads/mcp-ts-core/issues/38) ← issue
48
+ [#42](https://github.com/cyanheads/mcp-ts-core/pull/42) ← PR
36
49
 
37
50
  Only link numbers you've verified exist (via `gh issue view NN` or
38
- `gh pr view NN`). Never speculate on a future number — `#17` for "my
39
- upcoming PR" will quietly resolve to whatever real item already owns 17,
51
+ `gh pr view NN`). Never speculate on a future number — `#42` for "my
52
+ upcoming PR" will quietly resolve to whatever real item already owns 42,
40
53
  and GitHub timeline previews will pull in that unrelated item's title.
41
54
  -->
42
55