pattern-mcp 0.13.0 → 0.14.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.
package/README.md CHANGED
@@ -15,12 +15,22 @@ 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.1** — the crash/exit telemetry added in
19
+ v0.14.0 (`pattern_cli_exited`) is now registered before any of this
20
+ file's own module-level code runs, instead of near `main()`, so it
21
+ catches more of what can go wrong on startup; both `pattern_cli_started`
22
+ and `pattern_cli_exited` now also carry the running package version, so
23
+ a crash right around a release can be tied to the old or new binary
24
+ instead of staying ambiguous. Previously: v0.14.0 made a crash on
25
+ startup diagnosable instead of silent, warned at startup if
26
+ `ANTHROPIC_API_KEY` is missing or clearly malformed instead of only
27
+ surfacing a raw 401 mid-call, and added one respectful retry on a 429
28
+ (honoring `Retry-After`). Before that: v0.13.0 added `npx pattern-mcp init`,
29
+ which sets up the connection to your MCP client for you (Claude Code,
30
+ Claude Desktop, Cursor detected and configured automatically; Codex CLI
31
+ gets manual instructions). Running `npx pattern-mcp` bare in your own
32
+ terminal also tells you it needs a client connected, instead of silently
33
+ sitting there. See
24
34
  [Connect Pattern to your MCP client](#connect-pattern-to-your-mcp-client)
25
35
  for more details.
26
36
 
@@ -1859,6 +1869,14 @@ are a biased, tiny sample of everyone who installs.
1859
1869
  neither `recommend_component` counts nor `@posthog/mcp`'s handshake
1860
1870
  event below can answer, since both require getting further than a
1861
1871
  bare `npx pattern-mcp` run.
1872
+ - On process exit, as of v0.14.0: a single `pattern_cli_exited` event
1873
+ carrying only a coarse reason (`sigint`, `sigterm`,
1874
+ `uncaught_exception`, `unhandled_rejection`, or
1875
+ `fatal_startup_error`) and, for the two exception cases, the thrown
1876
+ value's constructor name (e.g. `TypeError`) -- never the error
1877
+ message or stack trace. Paired with `pattern_cli_started` so a start
1878
+ with no matching MCP handshake is diagnosable as a crash instead of
1879
+ silent.
1862
1880
  2. Standard MCP tool-call analytics, via
1863
1881
  [`@posthog/mcp`](https://posthog.com/docs/mcp-analytics): which tool
1864
1882
  was called, call duration, and success/failure, so unique installs and
package/dist/index.js CHANGED
@@ -37,13 +37,80 @@ 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
+ // Read as early as possible in this file's own module-level code, wrapped
44
+ // so a missing/corrupt package.json can't itself become a new, unguarded
45
+ // crash source -- this is read before the crash handlers below exist to
46
+ // catch anything. Moved up from where it used to live (just above the
47
+ // `server` construction, far later in this file) after a 2026-09-14
48
+ // incident where a caller crashed on every single launch, ~29 times in 4
49
+ // minutes, immediately after a version bump published -- telemetry had no
50
+ // way to say whether the crashing process was the old or new version (see
51
+ // project_pattern_activation_funnel memory). Every pattern_cli_started/
52
+ // pattern_cli_exited event now carries this, closing that gap for next
53
+ // time.
54
+ const PACKAGE_VERSION = (() => {
55
+ try {
56
+ return JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
57
+ }
58
+ catch {
59
+ return "unknown";
60
+ }
61
+ })();
62
+ // Registered before anything else in this file runs (all the way up here,
63
+ // not down by main() where it used to be) so a throw anywhere in this
64
+ // file's own module-level code -- not just inside main() -- is captured
65
+ // instead of dying silently before these handlers would otherwise have
66
+ // existed. Can't cover a throw during the import statements above this
67
+ // line (nothing can run before those resolve), but this closes the much
68
+ // larger window between "imports finished" and "main() starts," which is
69
+ // most of this file's ~4700 lines of function/constant definitions and
70
+ // tool registrations.
71
+ let exitTelemetryCaptured = false;
72
+ function captureExitOnce(reason, err) {
73
+ if (exitTelemetryCaptured)
74
+ return;
75
+ exitTelemetryCaptured = true;
76
+ captureCliExited(reason, err, PACKAGE_VERSION);
77
+ }
78
+ process.on("uncaughtException", async (err) => {
79
+ console.error("Pattern: uncaught exception, exiting.", err);
80
+ captureExitOnce("uncaught_exception", err);
81
+ await shutdownTelemetry();
82
+ process.exit(1);
83
+ });
84
+ process.on("unhandledRejection", async (reason) => {
85
+ console.error("Pattern: unhandled rejection, exiting.", reason);
86
+ captureExitOnce("unhandled_rejection", reason);
87
+ await shutdownTelemetry();
88
+ process.exit(1);
89
+ });
43
90
  export const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
44
91
  // Only required for org-scoped keys (not tied to one workspace); unset for
45
92
  // legacy workspace-scoped keys, which don't need it.
46
93
  export const ANTHROPIC_WORKSPACE_ID = process.env.ANTHROPIC_WORKSPACE_ID;
94
+ // Cheap, no-network sanity check on the key's shape, run once at startup.
95
+ // Deliberately NOT a real auth ping against the Anthropic API -- that would
96
+ // spend a real request on every single server boot (every MCP client
97
+ // launch), which is exactly the kind of always-pay-the-API cost this
98
+ // project avoids elsewhere (see the skip-list and ledger-cache-hit designs).
99
+ // This only catches the cheap, common misconfigurations -- unset, empty, or
100
+ // a value that's obviously not an Anthropic key (wrong var pasted, stray
101
+ // quotes) -- surfaced at startup instead of only on the first real tool
102
+ // call's 401. Never blocks startup; recommend_component/extract_requirements
103
+ // still fail with their own clear message if this warning goes unheeded.
104
+ function warnIfAnthropicKeyLooksWrong() {
105
+ if (!ANTHROPIC_API_KEY) {
106
+ console.error("Pattern: ANTHROPIC_API_KEY is not set. recommend_component and extract_requirements will fail until it is.");
107
+ return;
108
+ }
109
+ if (!/^sk-ant-/.test(ANTHROPIC_API_KEY)) {
110
+ console.error("Pattern: ANTHROPIC_API_KEY is set but doesn't look like a real Anthropic key (expected it to start with " +
111
+ "\"sk-ant-\"). If a tool call fails with a 401, check this value first.");
112
+ }
113
+ }
47
114
  // Configurable so Sonnet vs. Haiku can be A/B tested without a code change.
48
115
  // Defaults to Sonnet 5. Try MODEL=claude-haiku-4-5-20251001 to test the
49
116
  // cheaper tier -- re-run the 5 validated test cases from the product brief
@@ -395,9 +462,15 @@ function buildMeta(timings, usage) {
395
462
  // in a new dependency for what's a small, stable, well-documented event
396
463
  // shape (message_start/content_block_start/_delta/_stop/message_delta/
397
464
  // message_stop).
398
- async function streamAnthropicMessage(body) {
399
- const requestStartMs = Date.now();
400
- const response = await fetch("https://api.anthropic.com/v1/messages", {
465
+ // One retry, not a real backoff loop -- deliberately cost-conscious (see
466
+ // this project's skip-list/ledger-cache-hit reasoning): a 429 that's still
467
+ // rate-limited after respecting the API's own Retry-After is treated as a
468
+ // real failure to surface, not something worth spending a second wait on.
469
+ const RATE_LIMIT_MAX_RETRIES = 1;
470
+ // Fallback only for the rare case the API doesn't send Retry-After at all.
471
+ const RATE_LIMIT_DEFAULT_BACKOFF_MS = 3000;
472
+ function postAnthropicMessages(body) {
473
+ return fetch("https://api.anthropic.com/v1/messages", {
401
474
  method: "POST",
402
475
  headers: {
403
476
  "content-type": "application/json",
@@ -407,11 +480,28 @@ async function streamAnthropicMessage(body) {
407
480
  },
408
481
  body: JSON.stringify({ ...body, stream: true }),
409
482
  });
483
+ }
484
+ async function streamAnthropicMessage(body) {
485
+ const requestStartMs = Date.now();
486
+ let response = await postAnthropicMessages(body);
487
+ for (let attempt = 0; response.status === 429 && attempt < RATE_LIMIT_MAX_RETRIES; attempt++) {
488
+ const retryAfterHeader = response.headers.get("retry-after");
489
+ const retryAfterSeconds = retryAfterHeader ? Number.parseFloat(retryAfterHeader) : NaN;
490
+ const waitMs = Number.isFinite(retryAfterSeconds)
491
+ ? Math.max(0, retryAfterSeconds * 1000)
492
+ : RATE_LIMIT_DEFAULT_BACKOFF_MS;
493
+ console.error(`Pattern: rate limited by the Anthropic API, retrying in ${(waitMs / 1000).toFixed(1)}s ` +
494
+ `(${retryAfterHeader ? "per Retry-After" : "default backoff, no Retry-After header"})...`);
495
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
496
+ response = await postAnthropicMessages(body);
497
+ }
410
498
  if (!response.ok) {
411
499
  const errText = await response.text();
412
500
  const hint = response.status === 401
413
501
  ? " -- check that ANTHROPIC_API_KEY is set to a valid, active key in the environment running this MCP server."
414
- : "";
502
+ : response.status === 429
503
+ ? " -- still rate limited after retrying once; the caller should wait longer before trying this request again."
504
+ : "";
415
505
  throw new Error(`Anthropic API error ${response.status}: ${errText}${hint}`);
416
506
  }
417
507
  if (!response.body) {
@@ -3371,11 +3461,8 @@ export function extractJson(text) {
3371
3461
  return text;
3372
3462
  return text.slice(start, end + 1);
3373
3463
  }
3374
- // Read from package.json rather than hardcoding, so $mcp_server_version in
3375
- // PostHog's MCP tool-call analytics (and any client that reads the MCP
3376
- // initialize response) reflects the version actually installed instead of
3377
- // staying frozen at whatever it was when this line was first written.
3378
- const PACKAGE_VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
3464
+ // PACKAGE_VERSION is defined near the top of this file now (read as early
3465
+ // as possible, before the crash handlers -- see the comment there).
3379
3466
  const server = new Server({ name: "pattern-mcp", version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
3380
3467
  // Standard MCP tool-call analytics (tool name, duration, success/failure,
3381
3468
  // unique installs/sessions) via PostHog's own MCP SDK -- separate from
@@ -3895,13 +3982,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3895
3982
  // they're still looking at the terminal, not five minutes after they
3896
3983
  // alt-tabbed away.
3897
3984
  const IDLE_CONNECT_NUDGE_MS = 20_000;
3985
+ // captureExitOnce and the uncaughtException/unhandledRejection handlers
3986
+ // are registered near the top of this file now, before PACKAGE_VERSION's
3987
+ // definition -- see the comment there for why.
3898
3988
  async function main() {
3899
3989
  // `npx pattern-mcp init` -- the connect wizard -- exits without ever
3900
3990
  // starting the server. Checked before anything else so it can't be
3901
3991
  // shadowed by a tool name collision later.
3902
3992
  const argv = process.argv.slice(2);
3903
3993
  if (argv[0] === "init") {
3904
- captureCliStarted("init");
3994
+ captureCliStarted("init", PACKAGE_VERSION);
3905
3995
  await runConnect(PROJECT_ROOT, { yes: argv.includes("--yes") });
3906
3996
  await shutdownTelemetry();
3907
3997
  // Explicit exit, not a bare return -- shutdownTelemetry races a
@@ -3911,7 +4001,8 @@ async function main() {
3911
4001
  // event loop to drain on its own if some other handle is lingering.
3912
4002
  process.exit(0);
3913
4003
  }
3914
- captureCliStarted("server");
4004
+ captureCliStarted("server", PACKAGE_VERSION);
4005
+ warnIfAnthropicKeyLooksWrong();
3915
4006
  printTelemetryNoticeOnce();
3916
4007
  // Piggybacks on this same first-run moment (Option B, see
3917
4008
  // init-enforcement.ts) -- always prints a one-time, non-blocking mention;
@@ -3940,11 +4031,15 @@ async function main() {
3940
4031
  }
3941
4032
  await server.connect(transport);
3942
4033
  // Best-effort telemetry drain on clean shutdown -- no-op when telemetry
3943
- // was never enabled (see src/telemetry.ts).
4034
+ // was never enabled (see src/telemetry.ts). Also captures which signal
4035
+ // ended the process: a real client's normal disconnect looks the same as
4036
+ // a supervisor repeatedly killing-and-restarting a failing process, and
4037
+ // this is what tells the two apart in the starts-vs-exits comparison.
3944
4038
  for (const signal of ["SIGINT", "SIGTERM"]) {
3945
4039
  process.on(signal, async () => {
3946
4040
  if (idleNudgeTimer)
3947
4041
  clearTimeout(idleNudgeTimer);
4042
+ captureExitOnce(signal === "SIGINT" ? "sigint" : "sigterm");
3948
4043
  await shutdownTelemetry();
3949
4044
  process.exit(0);
3950
4045
  });
@@ -3957,8 +4052,10 @@ async function main() {
3957
4052
  // entry point, `npx pattern-mcp`) never sets this, so autostart is
3958
4053
  // unaffected.
3959
4054
  if (!process.env.PATTERN_NO_AUTOSTART) {
3960
- main().catch((err) => {
4055
+ main().catch(async (err) => {
3961
4056
  console.error("Fatal error starting pattern-mcp:", err);
4057
+ captureExitOnce("fatal_startup_error", err);
4058
+ await shutdownTelemetry();
3962
4059
  process.exit(1);
3963
4060
  });
3964
4061
  }
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
@@ -232,8 +241,31 @@ export function captureRecommendation(args) {
232
241
  // project_pattern_reddit_launch_spike memory for why that gap mattered:
233
242
  // a 2026-09-11 download spike showed almost no matching $mcp_initialize
234
243
  // growth, and there was no signal at all for the step in between.
235
- export function captureCliStarted(mode) {
236
- capture("pattern_cli_started", { mode });
244
+ // `version` is the running package.json version (see index.ts's
245
+ // PACKAGE_VERSION) -- added 2026-09-14 after an incident where a caller
246
+ // crashed on every launch attempt (~29 times in 4 minutes) in the few
247
+ // minutes right after a version bump published, and telemetry had no way
248
+ // to say whether the crashing process was the old or new version. Without
249
+ // it, "did the fix actually ship before this happened" is unanswerable
250
+ // from telemetry alone.
251
+ export function captureCliStarted(mode, version) {
252
+ capture("pattern_cli_started", { mode, version });
253
+ }
254
+ // Paired with captureCliStarted so a start with no matching handshake is
255
+ // diagnosable instead of silent -- added after a 2026-09-13 incident where
256
+ // 33 starts in one hour produced exactly 1 successful handshake, and
257
+ // telemetry had no way to say why the other 32 processes ended (see
258
+ // project_pattern_activation_funnel memory). Only a coarse reason, the
259
+ // thrown value's constructor name, and the running version travel --
260
+ // never the error message or stack, which could contain a file path, a
261
+ // stray argument value, or other local detail never sent by design (see
262
+ // this file's header).
263
+ export function captureCliExited(reason, err, version) {
264
+ capture("pattern_cli_exited", {
265
+ exit_reason: reason,
266
+ error_name: err instanceof Error ? err.name : null,
267
+ version,
268
+ });
237
269
  }
238
270
  export function captureApiError(args) {
239
271
  const { type, status } = classifyApiError(args.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pattern-mcp",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
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",