pattern-mcp 0.14.0 → 0.15.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,17 +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.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
18
+ **Current release: v0.15.0** — `npx pattern-mcp init` is now the first
19
+ and only command shown for getting started, everywhere (this README
20
+ and the website); bare `npx pattern-mcp` is documented as the server
21
+ command your client uses, not something to run yourself. The connect
22
+ wizard also no longer asks just once: if you run Pattern bare in your
23
+ own terminal and skip or miss it, it keeps offering on every later run
24
+ until it can actually confirm a client is connected, instead of going
25
+ silent forever after the first try. Previously: v0.14.1 made the
26
+ crash/exit telemetry added in v0.14.0 (`pattern_cli_exited`) register
27
+ before any of this file's own module-level code runs, and tagged both
28
+ it and `pattern_cli_started` with the running package version, so a
29
+ crash right around a release can be tied to the old or new binary. Before
30
+ that: v0.14.0 made a crash on startup diagnosable instead of silent,
31
+ warned at startup if `ANTHROPIC_API_KEY` is missing or clearly
32
+ malformed instead of only surfacing a raw 401 mid-call, and added one
33
+ respectful retry on a 429 (honoring `Retry-After`). See
29
34
  [Connect Pattern to your MCP client](#connect-pattern-to-your-mcp-client)
30
35
  for more details.
31
36
 
@@ -45,11 +50,22 @@ for more details.
45
50
  ## Install
46
51
 
47
52
  ```bash
48
- npx pattern-mcp
53
+ npx pattern-mcp init
49
54
  ```
50
55
 
51
- See [Quick Start](#quick-start) below to add your Anthropic API key and connect
52
- Pattern to your MCP client.
56
+ This is the only command you run yourself. It downloads Pattern,
57
+ detects which MCP client(s) you have (Claude Code, Claude Desktop,
58
+ Cursor, or Codex CLI), connects each one for you, and offers to add
59
+ your Anthropic API key. See [Quick Start](#quick-start) below for what it
60
+ does step by step, or
61
+ [Connect Pattern to your MCP client](#connect-pattern-to-your-mcp-client)
62
+ if you'd rather connect a client by hand.
63
+
64
+ `npx pattern-mcp` on its own (no `init`) is the server command your MCP
65
+ client will use once connected -- running it yourself in a bare
66
+ terminal starts a real process that just sits there waiting for a
67
+ client, since nothing has told it to connect to one yet. Prefer `init`
68
+ above for getting started.
53
69
 
54
70
  ## What Pattern Does
55
71
 
@@ -219,14 +235,40 @@ threshold.
219
235
 
220
236
  ## Quick Start
221
237
 
222
- ### 1. Install
238
+ ### 1. Install and connect
223
239
 
224
240
  ```bash
225
- npx pattern-mcp
241
+ npx pattern-mcp init
226
242
  ```
227
243
 
228
- `npx` runs the `pattern-mcp` command on demand without a separate install
229
- step, used in the client configs below.
244
+ This is the only command you need to run yourself -- `npx` downloads
245
+ `pattern-mcp` on demand, then `init` detects which clients you have
246
+ installed and offers to connect each one:
247
+
248
+ - **Claude Code** -- runs `claude mcp add` for you (asks whether to make
249
+ Pattern available in every project or just this one); skips if already
250
+ connected (`claude mcp list` already shows it).
251
+ - **Claude Desktop** and **Cursor** -- merges a `pattern` entry into the
252
+ client's own config file, showing the exact change before writing it
253
+ and never touching any other server already configured there.
254
+ - **Codex CLI** -- prints the config snippet to add by hand (Codex's
255
+ config is TOML; this doesn't auto-edit it).
256
+
257
+ Optionally pastes your `ANTHROPIC_API_KEY` into whichever configs you set
258
+ up (visible in plain text as you type it, and in the files it writes) --
259
+ press Enter to skip and add it yourself later, see
260
+ [step 2](#2-add-your-anthropic-api-key) below. Run non-interactively with
261
+ `--yes` (skips the API key prompt entirely, accepts every detected
262
+ client).
263
+
264
+ If `init` doesn't detect your client, or you'd rather set it up by
265
+ hand, see [Connect Pattern to your MCP client](#connect-pattern-to-your-mcp-client)
266
+ below for the same configs, per client, done manually. The server
267
+ command either way is `npx pattern-mcp` -- this is what your client's
268
+ config launches; you shouldn't need to run it yourself. If you do run
269
+ it bare in your own terminal (e.g. to double check the install), it
270
+ will just sit there waiting for a client and periodically remind you
271
+ to run `init` -- that's expected, not a hang.
230
272
 
231
273
  <details>
232
274
  <summary>Build from source instead</summary>
@@ -238,14 +280,15 @@ npm install
238
280
  npm run build
239
281
  ```
240
282
 
241
- Use `node /absolute/path/to/pattern-mcp/dist/index.js` as the server
242
- command in place of `npx pattern-mcp` in the examples below.
283
+ Use `node /absolute/path/to/pattern-mcp/dist/index.js` in place of
284
+ `npx pattern-mcp` everywhere in this README, including inside `init`'s
285
+ own generated client configs.
243
286
 
244
287
  </details>
245
288
 
246
289
  ### 2. Add your Anthropic API key
247
290
 
248
- Pattern requires:
291
+ Skipped it above, or want to change it? Pattern requires:
249
292
 
250
293
  ```
251
294
  ANTHROPIC_API_KEY
@@ -260,40 +303,10 @@ Claude Pro or Max subscription does not include API usage.
260
303
 
261
304
  ### Connect Pattern to your MCP client
262
305
 
263
- Pattern is a standard MCP server, so it works with MCP-compatible
264
- clients.
265
-
266
- The server command is:
267
-
268
- ```
269
- npx pattern-mcp
270
- ```
271
-
272
- #### Automatic setup
273
-
274
- ```bash
275
- npx pattern-mcp init
276
- ```
277
-
278
- Detects which clients are installed and offers to connect each one:
279
-
280
- - **Claude Code** -- runs `claude mcp add` for you (asks whether to make
281
- Pattern available in every project or just this one); skips if already
282
- connected (`claude mcp list` already shows it).
283
- - **Claude Desktop** and **Cursor** -- merges a `pattern` entry into the
284
- client's own config file, showing the exact change before writing it
285
- and never touching any other server already configured there.
286
- - **Codex CLI** -- prints the config snippet to add by hand (Codex's
287
- config is TOML; this doesn't auto-edit it).
288
-
289
- Optionally pastes your `ANTHROPIC_API_KEY` into whichever configs you set
290
- up (visible in plain text as you type it, and in the files it writes) --
291
- press Enter to skip and add it yourself later instead. Run
292
- non-interactively with `--yes` (skips the API key prompt entirely,
293
- accepts every detected client).
294
-
295
- If you'd rather do it by hand, or `init` didn't detect your client, the
296
- per-client instructions below cover the same configs manually.
306
+ [Step 1](#1-install-and-connect) above (`npx pattern-mcp init`) does
307
+ this automatically for every client it detects -- the sections below
308
+ are the same configs done by hand, for a client `init` didn't detect,
309
+ or if you'd simply rather edit the config yourself.
297
310
 
298
311
  #### Claude Code
299
312
 
@@ -127,6 +127,28 @@ async function mergeServerConfig(label, path, apiKey, options) {
127
127
  writeFileSync(path, JSON.stringify(merged, null, 2) + "\n", "utf8");
128
128
  console.log(`Written. Restart ${label} to pick it up.`);
129
129
  }
130
+ function clientConfigHasPattern(path) {
131
+ if (!path)
132
+ return false;
133
+ const { config } = readJsonConfig(path);
134
+ return Boolean(config.mcpServers?.pattern);
135
+ }
136
+ // Best-effort, read-only check across every client Pattern knows how to
137
+ // detect a prior successful setup for -- used to decide whether to keep
138
+ // offering the connect wizard on a later bare run (see
139
+ // offerClientConnectSetupOnce below), instead of asking only once ever
140
+ // regardless of outcome. Codex is deliberately excluded: its config is
141
+ // TOML, which this project never parses or writes (see
142
+ // offerCodexInstructions), so there's no way to confirm a Codex-only
143
+ // setup from here. That means a Codex-only user keeps getting offered
144
+ // the wizard -- a false negative, which is the safe failure mode (asks
145
+ // again when already connected) rather than a false positive (goes quiet
146
+ // when it isn't).
147
+ export function isAnyClientConnected(root) {
148
+ return (claudeCodeAlreadyConnected() ||
149
+ clientConfigHasPattern(claudeDesktopConfigPath()) ||
150
+ clientConfigHasPattern(join(root, ".cursor", "mcp.json")));
151
+ }
130
152
  // Claude Desktop isn't shipped on Linux -- there's no config path to
131
153
  // even guess at there, so this target is simply not offered on that
132
154
  // platform rather than writing a file no client will ever read.
@@ -225,36 +247,53 @@ export async function runConnect(root, options) {
225
247
  // Option 1/#1 from the activation-funnel discussion: piggybacks on the
226
248
  // same first-run moment as the telemetry and enforcement-boundary
227
249
  // notices (see telemetry.ts's printTelemetryNoticeOnce, which states the
228
- // stdin constraint first). Always prints a one-time, non-blocking
229
- // mention -- including when a real MCP client has spawned this as a
230
- // subprocess, where it's genuinely irrelevant but harmless, since the
231
- // notice is gated on a marker file the same as the others. Only offers
232
- // the actual interactive "set it up now?" prompt when stdin is a real
233
- // TTY, i.e. a human ran `npx pattern-mcp` bare in their own shell.
250
+ // stdin constraint first). Always prints the full notice once, ever --
251
+ // including when a real MCP client has spawned this as a subprocess,
252
+ // where it's genuinely irrelevant but harmless, since the notice is
253
+ // gated on a marker file the same as the others.
254
+ //
255
+ // The interactive "set it up now?" prompt is different: it used to be
256
+ // gated on that same one-time marker, so a human who ignored or missed
257
+ // it on the very first bare run never saw it again -- a permanent
258
+ // drop-off with no second chance, found while mapping the new-install
259
+ // journey (see project_pattern_activation_funnel memory). It now keeps
260
+ // reappearing on every bare TTY run -- a human running `npx pattern-mcp`
261
+ // in their own shell, never a real client's spawned subprocess -- for as
262
+ // long as isAnyClientConnected() can't confirm a real connection exists
263
+ // yet. This is the concrete fix for "don't rely on the user to figure
264
+ // out how to connect": Pattern keeps offering, not just once, until it
265
+ // can verify success, or until PATTERN_NO_CONNECT_NOTICE opts out.
234
266
  const CONNECT_NOTICE_PATH = process.env.PATTERN_CONNECT_NOTICE_PATH ?? join(homedir(), ".pattern", "connect_notice_shown");
235
267
  export async function offerClientConnectSetupOnce(root) {
236
268
  if (process.env.PATTERN_NO_CONNECT_NOTICE)
237
269
  return;
270
+ let noticeAlreadyShown = true;
238
271
  try {
239
272
  readFileSync(CONNECT_NOTICE_PATH, "utf8");
240
- return;
241
273
  }
242
274
  catch {
243
- // No marker yet -- fall through and show it.
275
+ noticeAlreadyShown = false;
244
276
  }
245
- console.error(["", "Pattern -- one-time setup notice (this will not print again)", connectInstructionsText(), ""].join("\n"));
246
- try {
247
- mkdirSync(dirname(CONNECT_NOTICE_PATH), { recursive: true });
248
- writeFileSync(CONNECT_NOTICE_PATH, new Date().toISOString(), "utf8");
249
- }
250
- catch {
251
- // Couldn't persist the marker -- worst case this prints again next
252
- // run. Never blocks startup over it, same as the other notices.
277
+ if (!noticeAlreadyShown) {
278
+ console.error(["", "Pattern -- one-time setup notice (this will not print again)", connectInstructionsText(), ""].join("\n"));
279
+ try {
280
+ mkdirSync(dirname(CONNECT_NOTICE_PATH), { recursive: true });
281
+ writeFileSync(CONNECT_NOTICE_PATH, new Date().toISOString(), "utf8");
282
+ }
283
+ catch {
284
+ // Couldn't persist the marker -- worst case this prints again next
285
+ // run. Never blocks startup over it, same as the other notices.
286
+ }
253
287
  }
254
288
  if (!process.stdin.isTTY)
255
289
  return;
290
+ if (isAnyClientConnected(root))
291
+ return;
256
292
  try {
257
- const setUpNow = await confirm("Run the connect wizard now?", { yes: false }, true);
293
+ const question = noticeAlreadyShown
294
+ ? "No MCP client is connected to Pattern yet -- run the connect wizard now?"
295
+ : "Run the connect wizard now?";
296
+ const setUpNow = await confirm(question, { yes: false }, true);
258
297
  if (setUpNow) {
259
298
  await runConnect(root, { yes: false }); // closes the shared readline itself
260
299
  }
package/dist/index.js CHANGED
@@ -40,6 +40,53 @@ import { fileURLToPath } from "node:url";
40
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.
@@ -3414,11 +3461,8 @@ export function extractJson(text) {
3414
3461
  return text;
3415
3462
  return text.slice(start, end + 1);
3416
3463
  }
3417
- // Read from package.json rather than hardcoding, so $mcp_server_version in
3418
- // PostHog's MCP tool-call analytics (and any client that reads the MCP
3419
- // initialize response) reflects the version actually installed instead of
3420
- // staying frozen at whatever it was when this line was first written.
3421
- 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).
3422
3466
  const server = new Server({ name: "pattern-mcp", version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
3423
3467
  // Standard MCP tool-call analytics (tool name, duration, success/failure,
3424
3468
  // unique installs/sessions) via PostHog's own MCP SDK -- separate from
@@ -3938,41 +3982,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3938
3982
  // they're still looking at the terminal, not five minutes after they
3939
3983
  // alt-tabbed away.
3940
3984
  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
- });
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.
3969
3988
  async function main() {
3970
3989
  // `npx pattern-mcp init` -- the connect wizard -- exits without ever
3971
3990
  // starting the server. Checked before anything else so it can't be
3972
3991
  // shadowed by a tool name collision later.
3973
3992
  const argv = process.argv.slice(2);
3974
3993
  if (argv[0] === "init") {
3975
- captureCliStarted("init");
3994
+ captureCliStarted("init", PACKAGE_VERSION);
3976
3995
  await runConnect(PROJECT_ROOT, { yes: argv.includes("--yes") });
3977
3996
  await shutdownTelemetry();
3978
3997
  // Explicit exit, not a bare return -- shutdownTelemetry races a
@@ -3982,7 +4001,7 @@ async function main() {
3982
4001
  // event loop to drain on its own if some other handle is lingering.
3983
4002
  process.exit(0);
3984
4003
  }
3985
- captureCliStarted("server");
4004
+ captureCliStarted("server", PACKAGE_VERSION);
3986
4005
  warnIfAnthropicKeyLooksWrong();
3987
4006
  printTelemetryNoticeOnce();
3988
4007
  // Piggybacks on this same first-run moment (Option B, see
package/dist/telemetry.js CHANGED
@@ -241,21 +241,30 @@ export function captureRecommendation(args) {
241
241
  // project_pattern_reddit_launch_spike memory for why that gap mattered:
242
242
  // a 2026-09-11 download spike showed almost no matching $mcp_initialize
243
243
  // growth, and there was no signal at all for the step in between.
244
- export function captureCliStarted(mode) {
245
- 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 });
246
253
  }
247
254
  // Paired with captureCliStarted so a start with no matching handshake is
248
255
  // diagnosable instead of silent -- added after a 2026-09-13 incident where
249
256
  // 33 starts in one hour produced exactly 1 successful handshake, and
250
257
  // 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) {
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) {
256
264
  capture("pattern_cli_exited", {
257
265
  exit_reason: reason,
258
266
  error_name: err instanceof Error ? err.name : null,
267
+ version,
259
268
  });
260
269
  }
261
270
  export function captureApiError(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pattern-mcp",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",