pattern-mcp 0.12.0 → 0.13.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 +294 -241
- package/dist/client-connect.js +265 -0
- package/dist/index.js +308 -240
- package/dist/init-enforcement.js +1 -60
- package/dist/prompt.js +65 -0
- package/dist/telemetry.js +40 -7
- package/package.json +1 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// `pattern-mcp init` -- closes the gap between "downloaded Pattern" and
|
|
2
|
+
// "actually connected it to an MCP client," which is the step where
|
|
3
|
+
// someone can first see real value (a real recommend_component call in
|
|
4
|
+
// their own agent). See project_pattern_reddit_launch_spike memory: a
|
|
5
|
+
// 2026-09-11 download spike showed almost no matching growth in real MCP
|
|
6
|
+
// handshakes, and the most likely reason is that `npx pattern-mcp` run
|
|
7
|
+
// bare in a terminal (the natural first thing a curious downloader does)
|
|
8
|
+
// never gets any further than a process sitting on stdin waiting for a
|
|
9
|
+
// client that was never configured to connect to it.
|
|
10
|
+
//
|
|
11
|
+
// Same shape as init-enforcement.ts's `pattern-check-gate init`
|
|
12
|
+
// (detect state, show what would change, confirm each step
|
|
13
|
+
// independently, never silently overwrite) and shares its prompt
|
|
14
|
+
// plumbing (prompt.ts) rather than reimplementing it.
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { homedir, platform } from "node:os";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { askLine, closeRl, confirm } from "./prompt.js";
|
|
20
|
+
const SERVER_COMMAND = "npx";
|
|
21
|
+
const SERVER_ARGS = ["pattern-mcp"];
|
|
22
|
+
// The same plain-language pointer shown in three places: the TTY-only
|
|
23
|
+
// one-time notice, the idle nudge if no client ever connects, and the
|
|
24
|
+
// init wizard's fallback when no supported client was detected at all.
|
|
25
|
+
export function connectInstructionsText() {
|
|
26
|
+
return [
|
|
27
|
+
"Pattern only does something once it's connected to an MCP client --",
|
|
28
|
+
"on its own it's just a process waiting on stdin. Two ways to fix that:",
|
|
29
|
+
"",
|
|
30
|
+
" 1. Run the setup wizard: npx pattern-mcp init",
|
|
31
|
+
" 2. Or connect it by hand -- see",
|
|
32
|
+
" https://github.com/donaldrichard19-LVD/pattern-mcp#connect-pattern-to-your-mcp-client",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
function hasCommand(cmd, versionFlag = "--version") {
|
|
36
|
+
try {
|
|
37
|
+
execFileSync(cmd, [versionFlag], { stdio: "ignore", timeout: 5000 });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function claudeCodeAlreadyConnected() {
|
|
45
|
+
try {
|
|
46
|
+
const out = execFileSync("claude", ["mcp", "list"], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
49
|
+
timeout: 10000,
|
|
50
|
+
});
|
|
51
|
+
return /^pattern\b/m.test(out);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function setupClaudeCode(apiKey, options) {
|
|
58
|
+
if (!hasCommand("claude"))
|
|
59
|
+
return;
|
|
60
|
+
console.log("\nClaude Code detected (`claude` on PATH).");
|
|
61
|
+
if (claudeCodeAlreadyConnected()) {
|
|
62
|
+
console.log(" Already connected (`claude mcp list` shows pattern) -- skipping.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const everywhere = await confirm(" Make Pattern available in every Claude Code project, not just this one?", options, true);
|
|
66
|
+
const args = ["mcp", "add", "pattern"];
|
|
67
|
+
if (apiKey)
|
|
68
|
+
args.push("-e", `ANTHROPIC_API_KEY=${apiKey}`);
|
|
69
|
+
if (everywhere)
|
|
70
|
+
args.push("--scope", "user");
|
|
71
|
+
args.push("--", SERVER_COMMAND, ...SERVER_ARGS);
|
|
72
|
+
console.log(` Running: claude ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`);
|
|
73
|
+
const proceed = await confirm(" Proceed?", options, true);
|
|
74
|
+
if (!proceed) {
|
|
75
|
+
console.log(" Skipped.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
execFileSync("claude", args, { stdio: "inherit", timeout: 15000 });
|
|
80
|
+
console.log(" Connected. Verify with `claude mcp list`.");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
console.log(" `claude mcp add` failed -- see output above, or add it manually (README's Claude Code section).");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function readJsonConfig(path) {
|
|
87
|
+
if (!existsSync(path))
|
|
88
|
+
return { config: {}, existed: false, valid: true };
|
|
89
|
+
try {
|
|
90
|
+
return { config: JSON.parse(readFileSync(path, "utf8")), existed: true, valid: true };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { config: {}, existed: true, valid: false };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Shared by Claude Desktop and Cursor -- both use the same
|
|
97
|
+
// `{"mcpServers": {"<name>": {command, args, env?}}}` shape. Never
|
|
98
|
+
// touches any other key in the file, and never overwrites an existing
|
|
99
|
+
// "pattern" entry without an explicit confirm, same discipline as
|
|
100
|
+
// init-enforcement.ts's setupClaudeSettings.
|
|
101
|
+
async function mergeServerConfig(label, path, apiKey, options) {
|
|
102
|
+
const { config, existed, valid } = readJsonConfig(path);
|
|
103
|
+
if (existed && !valid) {
|
|
104
|
+
console.log(`\n${label}: ${path} exists but isn't valid JSON -- skipping, fix it manually first.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const servers = { ...(config.mcpServers ?? {}) };
|
|
108
|
+
if (servers.pattern) {
|
|
109
|
+
console.log(`\n${label}: pattern already configured in ${path} -- skipping.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const entry = { command: SERVER_COMMAND, args: [...SERVER_ARGS] };
|
|
113
|
+
if (apiKey)
|
|
114
|
+
entry.env = { ANTHROPIC_API_KEY: apiKey };
|
|
115
|
+
console.log(`\n${existed ? "Merging into" : "Creating"} ${label} config at ${path}:`);
|
|
116
|
+
console.log(JSON.stringify({ pattern: entry }, null, 2)
|
|
117
|
+
.split("\n")
|
|
118
|
+
.map((l) => ` ${l}`)
|
|
119
|
+
.join("\n"));
|
|
120
|
+
const proceed = await confirm("Write this?", options, true);
|
|
121
|
+
if (!proceed) {
|
|
122
|
+
console.log("Skipped.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const merged = { ...config, mcpServers: { ...servers, pattern: entry } };
|
|
126
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
127
|
+
writeFileSync(path, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
128
|
+
console.log(`Written. Restart ${label} to pick it up.`);
|
|
129
|
+
}
|
|
130
|
+
// Claude Desktop isn't shipped on Linux -- there's no config path to
|
|
131
|
+
// even guess at there, so this target is simply not offered on that
|
|
132
|
+
// platform rather than writing a file no client will ever read.
|
|
133
|
+
function claudeDesktopConfigPath() {
|
|
134
|
+
const home = homedir();
|
|
135
|
+
switch (platform()) {
|
|
136
|
+
case "darwin":
|
|
137
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
138
|
+
case "win32":
|
|
139
|
+
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
140
|
+
default:
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function setupClaudeDesktop(apiKey, options) {
|
|
145
|
+
const path = claudeDesktopConfigPath();
|
|
146
|
+
if (!path)
|
|
147
|
+
return;
|
|
148
|
+
// Only offer this if Claude Desktop's own config directory already
|
|
149
|
+
// exists -- the strongest available signal it's actually installed,
|
|
150
|
+
// without a CLI to query directly (unlike Claude Code's `claude
|
|
151
|
+
// --version`).
|
|
152
|
+
if (!existsSync(dirname(path)))
|
|
153
|
+
return;
|
|
154
|
+
await mergeServerConfig("Claude Desktop", path, apiKey, options);
|
|
155
|
+
}
|
|
156
|
+
// Cursor has no CLI to query and no reliable global config location
|
|
157
|
+
// across platforms, so this uses the same detection init-enforcement.ts
|
|
158
|
+
// uses for "has this repo used X before": a project-level `.cursor/`
|
|
159
|
+
// directory already present is real evidence Cursor has opened this
|
|
160
|
+
// project, without guessing at a global install marker that doesn't
|
|
161
|
+
// exist. Project-scoped `.cursor/mcp.json`, matching the README.
|
|
162
|
+
async function setupCursor(root, apiKey, options) {
|
|
163
|
+
if (!existsSync(join(root, ".cursor")))
|
|
164
|
+
return;
|
|
165
|
+
await mergeServerConfig("Cursor", join(root, ".cursor", "mcp.json"), apiKey, options);
|
|
166
|
+
}
|
|
167
|
+
// Codex CLI's global config is TOML (~/.codex/config.toml), which this
|
|
168
|
+
// deliberately does not hand-edit -- no TOML writer is already a
|
|
169
|
+
// dependency here, and getting a partial/lossy rewrite wrong on someone's
|
|
170
|
+
// existing config is worse than just telling them what to add. Detected
|
|
171
|
+
// the same way as the other targets (evidence it's actually used), but
|
|
172
|
+
// only ever prints instructions.
|
|
173
|
+
function offerCodexInstructions() {
|
|
174
|
+
if (!existsSync(join(homedir(), ".codex")))
|
|
175
|
+
return;
|
|
176
|
+
console.log([
|
|
177
|
+
"\nCodex CLI detected (~/.codex exists). Pattern doesn't auto-write Codex's",
|
|
178
|
+
"TOML config -- add this to ~/.codex/config.toml (or .codex/config.json for",
|
|
179
|
+
"this project only):",
|
|
180
|
+
"",
|
|
181
|
+
' [mcp_servers.pattern]',
|
|
182
|
+
' command = "npx"',
|
|
183
|
+
' args = ["pattern-mcp"]',
|
|
184
|
+
].join("\n"));
|
|
185
|
+
}
|
|
186
|
+
async function promptApiKey(options) {
|
|
187
|
+
if (options.yes)
|
|
188
|
+
return null;
|
|
189
|
+
console.log("\nYour ANTHROPIC_API_KEY can be written into whichever client configs you set up\n" +
|
|
190
|
+
"below (visible in plain text there and as you type it now), or you can skip\n" +
|
|
191
|
+
"and add it yourself later -- see README's \"Add your Anthropic API key\".");
|
|
192
|
+
const answer = (await askLine("Paste your ANTHROPIC_API_KEY now, or press Enter to skip: ")).trim();
|
|
193
|
+
return answer || null;
|
|
194
|
+
}
|
|
195
|
+
export async function runConnect(root, options) {
|
|
196
|
+
console.log("Connecting Pattern to your MCP client(s)...");
|
|
197
|
+
try {
|
|
198
|
+
const apiKey = await promptApiKey(options);
|
|
199
|
+
let anyDetected = false;
|
|
200
|
+
if (hasCommand("claude")) {
|
|
201
|
+
anyDetected = true;
|
|
202
|
+
await setupClaudeCode(apiKey, options);
|
|
203
|
+
}
|
|
204
|
+
if (claudeDesktopConfigPath() && existsSync(dirname(claudeDesktopConfigPath()))) {
|
|
205
|
+
anyDetected = true;
|
|
206
|
+
await setupClaudeDesktop(apiKey, options);
|
|
207
|
+
}
|
|
208
|
+
if (existsSync(join(root, ".cursor"))) {
|
|
209
|
+
anyDetected = true;
|
|
210
|
+
await setupCursor(root, apiKey, options);
|
|
211
|
+
}
|
|
212
|
+
if (existsSync(join(homedir(), ".codex"))) {
|
|
213
|
+
anyDetected = true;
|
|
214
|
+
offerCodexInstructions();
|
|
215
|
+
}
|
|
216
|
+
if (!anyDetected) {
|
|
217
|
+
console.log("\nNo supported MCP client was detected on this machine automatically.\n" + connectInstructionsText());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
closeRl();
|
|
222
|
+
}
|
|
223
|
+
console.log("\nDone. Ask your agent to list its MCP tools and look for recommend_component.");
|
|
224
|
+
}
|
|
225
|
+
// Option 1/#1 from the activation-funnel discussion: piggybacks on the
|
|
226
|
+
// same first-run moment as the telemetry and enforcement-boundary
|
|
227
|
+
// 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.
|
|
234
|
+
const CONNECT_NOTICE_PATH = process.env.PATTERN_CONNECT_NOTICE_PATH ?? join(homedir(), ".pattern", "connect_notice_shown");
|
|
235
|
+
export async function offerClientConnectSetupOnce(root) {
|
|
236
|
+
if (process.env.PATTERN_NO_CONNECT_NOTICE)
|
|
237
|
+
return;
|
|
238
|
+
try {
|
|
239
|
+
readFileSync(CONNECT_NOTICE_PATH, "utf8");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// No marker yet -- fall through and show it.
|
|
244
|
+
}
|
|
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.
|
|
253
|
+
}
|
|
254
|
+
if (!process.stdin.isTTY)
|
|
255
|
+
return;
|
|
256
|
+
try {
|
|
257
|
+
const setUpNow = await confirm("Run the connect wizard now?", { yes: false }, true);
|
|
258
|
+
if (setUpNow) {
|
|
259
|
+
await runConnect(root, { yes: false }); // closes the shared readline itself
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
closeRl();
|
|
264
|
+
}
|
|
265
|
+
}
|