pattern-mcp 0.13.0 → 0.14.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.
package/README.md CHANGED
@@ -15,12 +15,17 @@ design reference.
15
15
 
16
16
  [Website](https://usepattern.sh) · [npm](https://www.npmjs.com/package/pattern-mcp) · [Report an issue](https://github.com/donaldrichard19-LVD/pattern-mcp/issues/new/choose)
17
17
 
18
- **Current release: v0.13.0** — `npx pattern-mcp init` now sets up the
19
- connection to your MCP client for you (Claude Code, Claude Desktop,
20
- Cursor detected and configured automatically; Codex CLI gets manual
21
- instructions). Running `npx pattern-mcp` bare in your own terminal also
22
- now tells you it needs a client connected, instead of silently sitting
23
- there. See
18
+ **Current release: v0.14.0** — a crash on startup is now diagnosable
19
+ instead of silent (a new, coarse `pattern_cli_exited` telemetry event),
20
+ Pattern warns at startup if `ANTHROPIC_API_KEY` is missing or clearly
21
+ malformed instead of only surfacing a raw 401 mid-call, and a 429 from
22
+ the Anthropic API now gets one respectful retry (honoring `Retry-After`)
23
+ before it's raised. Previously: v0.13.0 added `npx pattern-mcp init`,
24
+ which sets up the connection to your MCP client for you (Claude Code,
25
+ Claude Desktop, Cursor detected and configured automatically; Codex CLI
26
+ gets manual instructions). Running `npx pattern-mcp` bare in your own
27
+ terminal also tells you it needs a client connected, instead of silently
28
+ sitting there. See
24
29
  [Connect Pattern to your MCP client](#connect-pattern-to-your-mcp-client)
25
30
  for more details.
26
31
 
@@ -1859,6 +1864,14 @@ are a biased, tiny sample of everyone who installs.
1859
1864
  neither `recommend_component` counts nor `@posthog/mcp`'s handshake
1860
1865
  event below can answer, since both require getting further than a
1861
1866
  bare `npx pattern-mcp` run.
1867
+ - On process exit, as of v0.14.0: a single `pattern_cli_exited` event
1868
+ carrying only a coarse reason (`sigint`, `sigterm`,
1869
+ `uncaught_exception`, `unhandled_rejection`, or
1870
+ `fatal_startup_error`) and, for the two exception cases, the thrown
1871
+ value's constructor name (e.g. `TypeError`) -- never the error
1872
+ message or stack trace. Paired with `pattern_cli_started` so a start
1873
+ with no matching MCP handshake is diagnosable as a crash instead of
1874
+ silent.
1862
1875
  2. Standard MCP tool-call analytics, via
1863
1876
  [`@posthog/mcp`](https://posthog.com/docs/mcp-analytics): which tool
1864
1877
  was called, call duration, and success/failure, so unique installs and
package/dist/index.js CHANGED
@@ -37,13 +37,33 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statS
37
37
  import { homedir } from "node:os";
38
38
  import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
39
39
  import { fileURLToPath } from "node:url";
40
- import { captureApiError, captureCliStarted, captureRecommendation, getClient as getPostHogClient, installId, printTelemetryNoticeOnce, shutdownTelemetry, TELEMETRY_ENABLED, } from "./telemetry.js";
40
+ import { captureApiError, captureCliExited, captureCliStarted, captureRecommendation, getClient as getPostHogClient, installId, printTelemetryNoticeOnce, shutdownTelemetry, TELEMETRY_ENABLED, } from "./telemetry.js";
41
41
  import { offerEnforcementSetupOnce } from "./init-enforcement.js";
42
42
  import { connectInstructionsText, offerClientConnectSetupOnce, runConnect } from "./client-connect.js";
43
43
  export const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
44
44
  // Only required for org-scoped keys (not tied to one workspace); unset for
45
45
  // legacy workspace-scoped keys, which don't need it.
46
46
  export const ANTHROPIC_WORKSPACE_ID = process.env.ANTHROPIC_WORKSPACE_ID;
47
+ // Cheap, no-network sanity check on the key's shape, run once at startup.
48
+ // Deliberately NOT a real auth ping against the Anthropic API -- that would
49
+ // spend a real request on every single server boot (every MCP client
50
+ // launch), which is exactly the kind of always-pay-the-API cost this
51
+ // project avoids elsewhere (see the skip-list and ledger-cache-hit designs).
52
+ // This only catches the cheap, common misconfigurations -- unset, empty, or
53
+ // a value that's obviously not an Anthropic key (wrong var pasted, stray
54
+ // quotes) -- surfaced at startup instead of only on the first real tool
55
+ // call's 401. Never blocks startup; recommend_component/extract_requirements
56
+ // still fail with their own clear message if this warning goes unheeded.
57
+ function warnIfAnthropicKeyLooksWrong() {
58
+ if (!ANTHROPIC_API_KEY) {
59
+ console.error("Pattern: ANTHROPIC_API_KEY is not set. recommend_component and extract_requirements will fail until it is.");
60
+ return;
61
+ }
62
+ if (!/^sk-ant-/.test(ANTHROPIC_API_KEY)) {
63
+ console.error("Pattern: ANTHROPIC_API_KEY is set but doesn't look like a real Anthropic key (expected it to start with " +
64
+ "\"sk-ant-\"). If a tool call fails with a 401, check this value first.");
65
+ }
66
+ }
47
67
  // Configurable so Sonnet vs. Haiku can be A/B tested without a code change.
48
68
  // Defaults to Sonnet 5. Try MODEL=claude-haiku-4-5-20251001 to test the
49
69
  // cheaper tier -- re-run the 5 validated test cases from the product brief
@@ -395,9 +415,15 @@ function buildMeta(timings, usage) {
395
415
  // in a new dependency for what's a small, stable, well-documented event
396
416
  // shape (message_start/content_block_start/_delta/_stop/message_delta/
397
417
  // message_stop).
398
- async function streamAnthropicMessage(body) {
399
- const requestStartMs = Date.now();
400
- const response = await fetch("https://api.anthropic.com/v1/messages", {
418
+ // One retry, not a real backoff loop -- deliberately cost-conscious (see
419
+ // this project's skip-list/ledger-cache-hit reasoning): a 429 that's still
420
+ // rate-limited after respecting the API's own Retry-After is treated as a
421
+ // real failure to surface, not something worth spending a second wait on.
422
+ const RATE_LIMIT_MAX_RETRIES = 1;
423
+ // Fallback only for the rare case the API doesn't send Retry-After at all.
424
+ const RATE_LIMIT_DEFAULT_BACKOFF_MS = 3000;
425
+ function postAnthropicMessages(body) {
426
+ return fetch("https://api.anthropic.com/v1/messages", {
401
427
  method: "POST",
402
428
  headers: {
403
429
  "content-type": "application/json",
@@ -407,11 +433,28 @@ async function streamAnthropicMessage(body) {
407
433
  },
408
434
  body: JSON.stringify({ ...body, stream: true }),
409
435
  });
436
+ }
437
+ async function streamAnthropicMessage(body) {
438
+ const requestStartMs = Date.now();
439
+ let response = await postAnthropicMessages(body);
440
+ for (let attempt = 0; response.status === 429 && attempt < RATE_LIMIT_MAX_RETRIES; attempt++) {
441
+ const retryAfterHeader = response.headers.get("retry-after");
442
+ const retryAfterSeconds = retryAfterHeader ? Number.parseFloat(retryAfterHeader) : NaN;
443
+ const waitMs = Number.isFinite(retryAfterSeconds)
444
+ ? Math.max(0, retryAfterSeconds * 1000)
445
+ : RATE_LIMIT_DEFAULT_BACKOFF_MS;
446
+ console.error(`Pattern: rate limited by the Anthropic API, retrying in ${(waitMs / 1000).toFixed(1)}s ` +
447
+ `(${retryAfterHeader ? "per Retry-After" : "default backoff, no Retry-After header"})...`);
448
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
449
+ response = await postAnthropicMessages(body);
450
+ }
410
451
  if (!response.ok) {
411
452
  const errText = await response.text();
412
453
  const hint = response.status === 401
413
454
  ? " -- check that ANTHROPIC_API_KEY is set to a valid, active key in the environment running this MCP server."
414
- : "";
455
+ : response.status === 429
456
+ ? " -- still rate limited after retrying once; the caller should wait longer before trying this request again."
457
+ : "";
415
458
  throw new Error(`Anthropic API error ${response.status}: ${errText}${hint}`);
416
459
  }
417
460
  if (!response.body) {
@@ -3895,6 +3938,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3895
3938
  // they're still looking at the terminal, not five minutes after they
3896
3939
  // alt-tabbed away.
3897
3940
  const IDLE_CONNECT_NUDGE_MS = 20_000;
3941
+ // Registered once, at module load, so it covers the entire process
3942
+ // lifetime -- including a throw during main() itself, before the server
3943
+ // ever connects. Without this, a crash-on-start left no telemetry trace at
3944
+ // all: captureCliStarted fires, the process dies, and nothing explains why
3945
+ // (see project_pattern_activation_funnel memory -- the incident this
3946
+ // exists to make diagnosable next time). Both handlers exit(1) after
3947
+ // capturing: Node considers the process's state undefined past an uncaught
3948
+ // exception, so continuing to run is the wrong default regardless of
3949
+ // telemetry.
3950
+ let exitTelemetryCaptured = false;
3951
+ function captureExitOnce(reason, err) {
3952
+ if (exitTelemetryCaptured)
3953
+ return;
3954
+ exitTelemetryCaptured = true;
3955
+ captureCliExited(reason, err);
3956
+ }
3957
+ process.on("uncaughtException", async (err) => {
3958
+ console.error("Pattern: uncaught exception, exiting.", err);
3959
+ captureExitOnce("uncaught_exception", err);
3960
+ await shutdownTelemetry();
3961
+ process.exit(1);
3962
+ });
3963
+ process.on("unhandledRejection", async (reason) => {
3964
+ console.error("Pattern: unhandled rejection, exiting.", reason);
3965
+ captureExitOnce("unhandled_rejection", reason);
3966
+ await shutdownTelemetry();
3967
+ process.exit(1);
3968
+ });
3898
3969
  async function main() {
3899
3970
  // `npx pattern-mcp init` -- the connect wizard -- exits without ever
3900
3971
  // starting the server. Checked before anything else so it can't be
@@ -3912,6 +3983,7 @@ async function main() {
3912
3983
  process.exit(0);
3913
3984
  }
3914
3985
  captureCliStarted("server");
3986
+ warnIfAnthropicKeyLooksWrong();
3915
3987
  printTelemetryNoticeOnce();
3916
3988
  // Piggybacks on this same first-run moment (Option B, see
3917
3989
  // init-enforcement.ts) -- always prints a one-time, non-blocking mention;
@@ -3940,11 +4012,15 @@ async function main() {
3940
4012
  }
3941
4013
  await server.connect(transport);
3942
4014
  // Best-effort telemetry drain on clean shutdown -- no-op when telemetry
3943
- // was never enabled (see src/telemetry.ts).
4015
+ // was never enabled (see src/telemetry.ts). Also captures which signal
4016
+ // ended the process: a real client's normal disconnect looks the same as
4017
+ // a supervisor repeatedly killing-and-restarting a failing process, and
4018
+ // this is what tells the two apart in the starts-vs-exits comparison.
3944
4019
  for (const signal of ["SIGINT", "SIGTERM"]) {
3945
4020
  process.on(signal, async () => {
3946
4021
  if (idleNudgeTimer)
3947
4022
  clearTimeout(idleNudgeTimer);
4023
+ captureExitOnce(signal === "SIGINT" ? "sigint" : "sigterm");
3948
4024
  await shutdownTelemetry();
3949
4025
  process.exit(0);
3950
4026
  });
@@ -3957,8 +4033,10 @@ async function main() {
3957
4033
  // entry point, `npx pattern-mcp`) never sets this, so autostart is
3958
4034
  // unaffected.
3959
4035
  if (!process.env.PATTERN_NO_AUTOSTART) {
3960
- main().catch((err) => {
4036
+ main().catch(async (err) => {
3961
4037
  console.error("Fatal error starting pattern-mcp:", err);
4038
+ captureExitOnce("fatal_startup_error", err);
4039
+ await shutdownTelemetry();
3962
4040
  process.exit(1);
3963
4041
  });
3964
4042
  }
package/dist/telemetry.js CHANGED
@@ -36,6 +36,15 @@
36
36
  * itself are never sent, by either half. See SECURITY.md and README.md for
37
37
  * the full disclosure and how to opt out.
38
38
  *
39
+ * Manual/ad-hoc test sessions (a one-off MCP client run by hand while
40
+ * debugging, not a checked-in script) should set PATTERN_TELEMETRY=0 before
41
+ * connecting -- there's no way for this file to distinguish that from real
42
+ * usage on its own, and self-testing was previously showing up as if it
43
+ * were adoption (see project_pattern_activation_funnel memory: roughly half
44
+ * of all recorded handshakes turned out to be internal test/smoke-test
45
+ * clients). Checked-in test scripts (e.g. scripts/test-client.mjs) default
46
+ * this off already.
47
+ *
39
48
  * Reuses Pattern's existing PostHog project (the same one the marketing
40
49
  * site sends browser events to) with its public, write-only project key --
41
50
  * safe to embed in a distributed package the same way that key is already
@@ -235,6 +244,20 @@ export function captureRecommendation(args) {
235
244
  export function captureCliStarted(mode) {
236
245
  capture("pattern_cli_started", { mode });
237
246
  }
247
+ // Paired with captureCliStarted so a start with no matching handshake is
248
+ // diagnosable instead of silent -- added after a 2026-09-13 incident where
249
+ // 33 starts in one hour produced exactly 1 successful handshake, and
250
+ // telemetry had no way to say why the other 32 processes ended (see
251
+ // project_pattern_activation_funnel memory). Only a coarse reason and the
252
+ // thrown value's constructor name travel -- never the error message or
253
+ // stack, which could contain a file path, a stray argument value, or other
254
+ // local detail never sent by design (see this file's header).
255
+ export function captureCliExited(reason, err) {
256
+ capture("pattern_cli_exited", {
257
+ exit_reason: reason,
258
+ error_name: err instanceof Error ? err.name : null,
259
+ });
260
+ }
238
261
  export function captureApiError(args) {
239
262
  const { type, status } = classifyApiError(args.message);
240
263
  capture("pattern_cli_api_error", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pattern-mcp",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "MCP server that turns your design guidance into a checkable process -- evaluates UI components from external libraries (shadcn/ui, 21st.dev, ReUI) or your own registered design system against a requirements checklist, then tells the agent whether to reuse an existing component or build one from a concrete design reference.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",