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 +19 -6
- package/dist/index.js +85 -7
- package/dist/telemetry.js +23 -0
- package/package.json +1 -1
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.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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.
|
|
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",
|