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 +24 -6
- package/dist/index.js +111 -14
- package/dist/telemetry.js +34 -2
- package/package.json +1 -1
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.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
now
|
|
23
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
//
|
|
3375
|
-
//
|
|
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
|
-
|
|
236
|
-
|
|
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.
|
|
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",
|