@runpod/mcp-server 3.1.0 → 3.3.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.
@@ -0,0 +1,901 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/install/wizard.ts
4
+ import { execFileSync } from "child_process";
5
+ import * as p from "@clack/prompts";
6
+
7
+ // src/install/clients.ts
8
+ import * as fs from "fs";
9
+ import * as os from "os";
10
+ import * as path from "path";
11
+ import { execSync, spawnSync } from "child_process";
12
+ import * as jsonc from "jsonc-parser";
13
+ var SERVER_NAME = "runpod";
14
+ var NPM_PACKAGE = "@runpod/mcp-server@latest";
15
+ var HOSTED_URL = process.env.RUNPOD_MCP_URL ?? "https://mcp.getrunpod.io/";
16
+ function localServerConfig(apiKey, serverProperty) {
17
+ const config = {
18
+ command: "npx",
19
+ args: ["-y", NPM_PACKAGE],
20
+ env: { RUNPOD_API_KEY: apiKey }
21
+ };
22
+ return serverProperty === "servers" ? { type: "stdio", ...config } : config;
23
+ }
24
+ function hostedServerConfig(strategy, serverProperty, url) {
25
+ if (strategy === "mcp-remote") {
26
+ return { command: "npx", args: ["-y", "mcp-remote", url] };
27
+ }
28
+ return serverProperty === "servers" ? { type: "http", url } : { url };
29
+ }
30
+ function describeDialect(dialect) {
31
+ return dialect === "json" ? "JSON" : "JSONC";
32
+ }
33
+ function parseFailure(content, dialect) {
34
+ if (dialect === "json") {
35
+ try {
36
+ JSON.parse(content);
37
+ return void 0;
38
+ } catch (error) {
39
+ return errMessage(error);
40
+ }
41
+ }
42
+ const errors = [];
43
+ jsonc.parse(content, errors, { allowTrailingComma: true });
44
+ return errors.length > 0 ? `JSONC parse error at offset ${errors[0].offset}` : void 0;
45
+ }
46
+ function parseAsClient(content, dialect) {
47
+ if (dialect !== "json") {
48
+ return jsonc.parse(content, [], { allowTrailingComma: true });
49
+ }
50
+ try {
51
+ return JSON.parse(content);
52
+ } catch {
53
+ return void 0;
54
+ }
55
+ }
56
+ function readBackEntry(content, serverProperty, dialect) {
57
+ const parsed = parseAsClient(content, dialect);
58
+ if (typeof parsed !== "object" || parsed === null) return void 0;
59
+ const servers = parsed[serverProperty];
60
+ if (typeof servers !== "object" || servers === null) return void 0;
61
+ return servers[SERVER_NAME];
62
+ }
63
+ function reliesOnLeniency(content) {
64
+ if (content.trim() === "") return false;
65
+ try {
66
+ JSON.parse(content);
67
+ return false;
68
+ } catch {
69
+ return parseFailure(content, "jsonc") === void 0;
70
+ }
71
+ }
72
+ function upsertJsonServer(configPath, serverProperty, value, dialect) {
73
+ if (!path.isAbsolute(configPath)) {
74
+ return {
75
+ success: false,
76
+ message: `refusing to write "${configPath}": not an absolute path, so it would resolve against the current directory. This usually means an environment variable (APPDATA, XDG_CONFIG_HOME) is set to an empty or relative value.`
77
+ };
78
+ }
79
+ try {
80
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
81
+ const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
82
+ const edits = jsonc.modify(content, [serverProperty, SERVER_NAME], value, {
83
+ formattingOptions: { tabSize: 2, insertSpaces: true }
84
+ });
85
+ const updated = jsonc.applyEdits(content, edits).replace(/^\uFEFF/, "");
86
+ const failure = parseFailure(updated, dialect);
87
+ if (failure) {
88
+ const wasAlreadyBroken = content.trim() !== "" && parseFailure(content, dialect) !== void 0;
89
+ return {
90
+ success: false,
91
+ message: wasAlreadyBroken ? `${configPath} could not be parsed as ${describeDialect(dialect)} before this change either (${failure}), so it was left untouched \u2014 this client would not load the entry. Fix the file (or move it aside) and re-run.` : `writing to ${configPath} would leave it unparseable (${failure}), so it was left untouched.`
92
+ };
93
+ }
94
+ const landed = readBackEntry(updated, serverProperty, dialect);
95
+ if (JSON.stringify(landed) !== JSON.stringify(value)) {
96
+ return {
97
+ success: false,
98
+ message: `${configPath} was left untouched: the entry would have been written where this client does not read it, so it would have had no effect. This usually means "${serverProperty}" appears more than once in the file \u2014 the last one wins, so merge them into a single block and re-run.`
99
+ };
100
+ }
101
+ fs.writeFileSync(configPath, updated, { encoding: "utf8", mode: 384 });
102
+ if (dialect === "unverified" && reliesOnLeniency(updated)) {
103
+ return {
104
+ success: true,
105
+ message: "written, but this config uses comments or trailing commas and whether this client accepts them is unverified \u2014 if the Runpod server does not appear, remove them"
106
+ };
107
+ }
108
+ return { success: true };
109
+ } catch (error) {
110
+ return { success: false, message: errMessage(error) };
111
+ }
112
+ }
113
+ function removeJsonServer(configPath, serverProperty, dialect) {
114
+ if (!path.isAbsolute(configPath)) {
115
+ return {
116
+ success: false,
117
+ message: `refusing to touch "${configPath}": not an absolute path. Check APPDATA / XDG_CONFIG_HOME.`
118
+ };
119
+ }
120
+ try {
121
+ if (!fs.existsSync(configPath)) {
122
+ return { success: true, message: "nothing to remove" };
123
+ }
124
+ const content = fs.readFileSync(configPath, "utf8");
125
+ if (content.trim() === "") {
126
+ return { success: true, message: "nothing to remove" };
127
+ }
128
+ const existingFailure = parseFailure(content, dialect);
129
+ if (existingFailure !== void 0) {
130
+ return {
131
+ success: false,
132
+ message: `could not safely remove the Runpod entry from ${configPath}: the existing config is not valid ${describeDialect(dialect)} (${existingFailure}). The original file was left unchanged; repair it or delete the entry by hand.`
133
+ };
134
+ }
135
+ let edits;
136
+ try {
137
+ edits = jsonc.modify(content, [serverProperty, SERVER_NAME], void 0, {
138
+ formattingOptions: { tabSize: 2, insertSpaces: true }
139
+ });
140
+ } catch (error) {
141
+ if (/empty document/i.test(errMessage(error))) {
142
+ return { success: true, message: "nothing to remove" };
143
+ }
144
+ throw error;
145
+ }
146
+ if (edits.length === 0) {
147
+ return readBackEntry(content, serverProperty, dialect) === void 0 ? { success: true, message: "nothing to remove" } : {
148
+ success: false,
149
+ message: `the Runpod entry is still present in ${configPath} and could not be removed automatically \u2014 "${serverProperty}" appears more than once, so the edit targeted a block this client does not read. Delete the entry by hand.`
150
+ };
151
+ }
152
+ const updated = jsonc.applyEdits(content, edits);
153
+ const brokeIt = parseFailure(updated, dialect) !== void 0;
154
+ const stillThere = readBackEntry(updated, serverProperty, dialect) !== void 0;
155
+ if (brokeIt || stillThere) {
156
+ return {
157
+ success: false,
158
+ message: `could not remove the Runpod entry from ${configPath} without ${brokeIt ? "making the config unparseable" : "leaving the entry present"}. The original file was left unchanged so its comments and formatting are preserved; delete the Runpod entry by hand.`
159
+ };
160
+ }
161
+ fs.writeFileSync(configPath, updated, "utf8");
162
+ return { success: true };
163
+ } catch (error) {
164
+ return { success: false, message: errMessage(error) };
165
+ }
166
+ }
167
+ function errMessage(error) {
168
+ return error instanceof Error ? error.message : String(error);
169
+ }
170
+ function exists(p2) {
171
+ try {
172
+ return fs.existsSync(p2);
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ function jsonClient(opts) {
178
+ const serverProperty = opts.serverProperty ?? "mcpServers";
179
+ const hostedStrategy = opts.hostedStrategy ?? "http";
180
+ return {
181
+ id: opts.id,
182
+ name: opts.name,
183
+ detect: () => Promise.resolve(
184
+ (opts.detectPaths?.() ?? [path.dirname(opts.configPath())]).some(exists)
185
+ ),
186
+ add: (mode) => {
187
+ const value = mode.kind === "local" ? localServerConfig(mode.apiKey, serverProperty) : hostedServerConfig(hostedStrategy, serverProperty, mode.url);
188
+ return Promise.resolve(
189
+ upsertJsonServer(opts.configPath(), serverProperty, value, opts.dialect)
190
+ );
191
+ },
192
+ dialect: opts.dialect,
193
+ remove: () => Promise.resolve(
194
+ removeJsonServer(opts.configPath(), serverProperty, opts.dialect)
195
+ ),
196
+ describeTarget: () => opts.configPath()
197
+ };
198
+ }
199
+ function claudeCandidates(platform, homedir2, appdata) {
200
+ if (platform === "win32") {
201
+ const win = path.win32;
202
+ const defaultAppdata = win.join(homedir2, "AppData", "Roaming");
203
+ return [
204
+ {
205
+ path: win.join(homedir2, ".local", "bin", "claude.exe"),
206
+ trustedRoot: homedir2
207
+ },
208
+ // npm -g installs a .cmd shim. runClaude bypasses it and resolves the real
209
+ // native or JavaScript target from the installed package manifest.
210
+ //
211
+ // absoluteEnvDir, not `?? `: an empty or relative APPDATA yielded the relative
212
+ // `npm\claude.cmd`. Candidates are probed before any lookup, so a relative
213
+ // candidate would resolve against the current directory and receive the live
214
+ // API key.
215
+ {
216
+ path: win.join(
217
+ absoluteEnvDir(appdata, () => defaultAppdata),
218
+ "npm",
219
+ "claude.cmd"
220
+ ),
221
+ trustedRoot: defaultAppdata
222
+ }
223
+ ];
224
+ }
225
+ return [
226
+ // Where the current native installer puts it (verified on a real install:
227
+ // ~/.local/bin/claude -> versions/2.1.220). Same reasoning as the Windows
228
+ // .local\bin\claude.exe entry — it also covers an install whose PATH entry is
229
+ // missing, which the lookup can never find. Without it, POSIX detection depended
230
+ // entirely on PATH.
231
+ {
232
+ path: path.posix.join(homedir2, ".local", "bin", "claude"),
233
+ trustedRoot: homedir2
234
+ },
235
+ {
236
+ path: path.posix.join(homedir2, ".claude", "local", "claude"),
237
+ trustedRoot: homedir2
238
+ },
239
+ { path: "/usr/local/bin/claude", trustedRoot: "/usr/local" },
240
+ { path: "/opt/homebrew/bin/claude", trustedRoot: "/opt/homebrew" }
241
+ ];
242
+ }
243
+ function pathIsInside(candidate, directory, pathApi) {
244
+ const relative = pathApi.relative(
245
+ pathApi.resolve(directory),
246
+ pathApi.resolve(candidate)
247
+ );
248
+ return relative === "" || !relative.startsWith(`..${pathApi.sep}`) && relative !== ".." && !pathApi.isAbsolute(relative);
249
+ }
250
+ function isNodeModulesBin(candidate) {
251
+ return /(^|[\\/])node_modules[\\/]\.bin([\\/]|$)/i.test(candidate);
252
+ }
253
+ function pickClaudeBinary(stdout, platform, cwd) {
254
+ if (platform === "win32") return null;
255
+ const hits = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
256
+ return hits.find(
257
+ (hit) => path.posix.isAbsolute(hit) && !isNodeModulesBin(hit) && (cwd === void 0 || !pathIsInside(hit, cwd, path.posix))
258
+ ) ?? null;
259
+ }
260
+ var PROJECT_BOUNDARY_MARKERS = [
261
+ ".git",
262
+ "package.json",
263
+ "pnpm-workspace.yaml",
264
+ "pnpm-lock.yaml",
265
+ "package-lock.json",
266
+ "yarn.lock",
267
+ "bun.lock",
268
+ "bun.lockb"
269
+ ];
270
+ function projectBoundary(cwd, home2) {
271
+ let current = cwd;
272
+ let boundary = null;
273
+ while (true) {
274
+ if (current === home2) return boundary;
275
+ if (PROJECT_BOUNDARY_MARKERS.some(
276
+ (marker) => exists(path.join(current, marker))
277
+ )) {
278
+ boundary = current;
279
+ }
280
+ const parent = path.dirname(current);
281
+ if (parent === current) return boundary;
282
+ current = parent;
283
+ }
284
+ }
285
+ function trustedClaudeBinary(binary, cwd, home2, requiredRoot) {
286
+ try {
287
+ const canonicalBinary = fs.realpathSync.native(binary);
288
+ const canonicalCwd = fs.realpathSync.native(cwd);
289
+ let canonicalHome;
290
+ if (home2 !== void 0) {
291
+ try {
292
+ canonicalHome = fs.realpathSync.native(home2);
293
+ } catch {
294
+ canonicalHome = void 0;
295
+ }
296
+ }
297
+ const projectRoot = projectBoundary(canonicalCwd, canonicalHome);
298
+ const canonicalRequiredRoot = requiredRoot === void 0 ? void 0 : fs.realpathSync.native(requiredRoot);
299
+ if (isNodeModulesBin(canonicalBinary) || canonicalRequiredRoot !== void 0 && !pathIsInside(canonicalBinary, canonicalRequiredRoot, path) || projectRoot !== null && pathIsInside(canonicalBinary, projectRoot, path)) {
300
+ return null;
301
+ }
302
+ return canonicalBinary;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+ var CMD_META = /[()\][%!^"`<>&|;, *?\t\n\v\r]/;
308
+ function findClaudeBinary(options = {}) {
309
+ const platform = options.platform ?? process.platform;
310
+ let homedir2 = options.homedir;
311
+ if (homedir2 === void 0) {
312
+ try {
313
+ homedir2 = os.userInfo().homedir;
314
+ } catch {
315
+ homedir2 = null;
316
+ }
317
+ }
318
+ const appdata = options.appdata ?? process.env.APPDATA;
319
+ const cwd = options.cwd ?? process.cwd();
320
+ const candidates = homedir2 == null ? [] : claudeCandidates(platform, homedir2, appdata);
321
+ for (const candidate of candidates) {
322
+ if (!path.isAbsolute(candidate.path) && !path.win32.isAbsolute(candidate.path))
323
+ continue;
324
+ if (exists(candidate.path)) {
325
+ const trusted = trustedClaudeBinary(
326
+ candidate.path,
327
+ cwd,
328
+ homedir2 ?? void 0,
329
+ candidate.trustedRoot
330
+ );
331
+ if (trusted !== null) return trusted;
332
+ }
333
+ }
334
+ try {
335
+ if (platform === "win32") return null;
336
+ const out = execSync("command -v claude", {
337
+ stdio: ["ignore", "pipe", "ignore"],
338
+ encoding: "utf8"
339
+ });
340
+ const binary = pickClaudeBinary(out, platform);
341
+ return binary === null ? null : trustedClaudeBinary(binary, cwd, homedir2 ?? void 0);
342
+ } catch {
343
+ return null;
344
+ }
345
+ }
346
+ function describeCommand(binary, args) {
347
+ const quote = (s) => /\s/.test(s) || CMD_META.test(s) ? `"${s}"` : s;
348
+ return [binary, ...args].map(quote).join(" ");
349
+ }
350
+ var KEY_PLACEHOLDER = "YOUR_RUNPOD_API_KEY";
351
+ function describeCommandRedacted(binary, args) {
352
+ return describeCommand(
353
+ binary,
354
+ args.map(
355
+ (arg) => arg.startsWith("RUNPOD_API_KEY=") ? `RUNPOD_API_KEY=${KEY_PLACEHOLDER}` : arg
356
+ )
357
+ );
358
+ }
359
+ function redactSecret(text, secret) {
360
+ if (!secret) return text;
361
+ return text.split(secret).join(KEY_PLACEHOLDER);
362
+ }
363
+ function sanitizedClaudeEnv() {
364
+ const env = { ...process.env };
365
+ for (const key of Object.keys(env)) {
366
+ if (["path", "comspec", "pathext", "node_options", "node_path"].includes(
367
+ key.toLowerCase()
368
+ )) {
369
+ delete env[key];
370
+ }
371
+ }
372
+ if (process.platform === "win32") {
373
+ env.Path = path.dirname(process.execPath);
374
+ } else {
375
+ env.PATH = [
376
+ path.dirname(process.execPath),
377
+ "/usr/local/bin",
378
+ "/opt/homebrew/bin",
379
+ "/usr/bin",
380
+ "/bin"
381
+ ].join(path.delimiter);
382
+ }
383
+ return env;
384
+ }
385
+ function windowsNpmClaudeTarget(binary) {
386
+ if (process.platform !== "win32" || !binary.toLowerCase().endsWith(".cmd")) {
387
+ return null;
388
+ }
389
+ try {
390
+ const npmRoot = fs.realpathSync.native(path.dirname(binary));
391
+ const packageRoot = fs.realpathSync.native(
392
+ path.join(npmRoot, "node_modules", "@anthropic-ai", "claude-code")
393
+ );
394
+ if (!pathIsInside(packageRoot, npmRoot, path)) return null;
395
+ const manifestPath = fs.realpathSync.native(
396
+ path.join(packageRoot, "package.json")
397
+ );
398
+ if (!pathIsInside(manifestPath, packageRoot, path)) return null;
399
+ const manifest = JSON.parse(
400
+ fs.readFileSync(manifestPath, "utf8")
401
+ );
402
+ if (manifest === null || typeof manifest !== "object") return null;
403
+ const bin = manifest.bin;
404
+ const target = typeof bin === "string" ? bin : bin !== null && typeof bin === "object" ? bin.claude : void 0;
405
+ if (typeof target !== "string" || target.trim() === "") return null;
406
+ const entrypoint = fs.realpathSync.native(
407
+ path.resolve(packageRoot, target)
408
+ );
409
+ if (!pathIsInside(entrypoint, packageRoot, path)) return null;
410
+ if (/\.(?:exe|com)$/i.test(entrypoint)) {
411
+ return { command: entrypoint, argsPrefix: [] };
412
+ }
413
+ if (/\.(?:c?js|mjs)$/i.test(entrypoint)) {
414
+ return { command: process.execPath, argsPrefix: [entrypoint] };
415
+ }
416
+ return null;
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ function runClaude(binary, args, secret) {
422
+ const isWindowsCmd = process.platform === "win32" && binary.toLowerCase().endsWith(".cmd");
423
+ const npmTarget = windowsNpmClaudeTarget(binary);
424
+ if (isWindowsCmd && npmTarget === null) {
425
+ return {
426
+ success: false,
427
+ refused: true,
428
+ message: `could not locate a trusted Claude Code npm entrypoint beside ${binary}. Run the shim yourself, substituting your key:
429
+ ${describeCommandRedacted(binary, args)}`
430
+ };
431
+ }
432
+ const command = npmTarget?.command ?? binary;
433
+ const commandArgs = [...npmTarget?.argsPrefix ?? [], ...args];
434
+ const result = spawnSync(command, commandArgs, {
435
+ stdio: "pipe",
436
+ encoding: "utf8",
437
+ env: sanitizedClaudeEnv()
438
+ });
439
+ if (result.error)
440
+ return {
441
+ success: false,
442
+ message: redactSecret(errMessage(result.error), secret)
443
+ };
444
+ const stdio = `${result.stdout ?? ""}${result.stderr ?? ""}`;
445
+ if (result.status === 0) return { success: true, output: stdio };
446
+ if (result.status === null) {
447
+ return { success: false, message: `killed by ${result.signal}` };
448
+ }
449
+ if (stdio.includes("already exists")) {
450
+ return {
451
+ success: true,
452
+ // Deliberately not "to replace the API key": this branch is also reached in
453
+ // hosted mode, where there is no key — and the pre-existing entry may be of the
454
+ // other kind entirely, which is exactly what the user needs to know.
455
+ message: `already configured, existing entry left unchanged \u2014 it was NOT updated to what you just chose (to replace it, run \`${describeCommand(binary, ["mcp", "remove", SERVER_NAME, "--scope", "user"])}\` first, then re-run this wizard)`
456
+ };
457
+ }
458
+ return {
459
+ success: false,
460
+ message: redactSecret(
461
+ stdio.trim() || `${path.basename(binary)} exited with code ${result.status}`,
462
+ secret
463
+ )
464
+ };
465
+ }
466
+ function absoluteEnvDir(value, fallback) {
467
+ if (!value || !value.trim()) return fallback();
468
+ if (!path.isAbsolute(value) && !path.win32.isAbsolute(value))
469
+ return fallback();
470
+ return value;
471
+ }
472
+ function claudeUserConfigPath() {
473
+ const configured = process.env.CLAUDE_CONFIG_DIR;
474
+ return path.join(
475
+ configured ? path.resolve(configured) : os.homedir(),
476
+ ".claude.json"
477
+ );
478
+ }
479
+ function readClaudeConfigState(configPath) {
480
+ const unknown = {
481
+ configPath,
482
+ userScope: void 0,
483
+ localScopeDirs: []
484
+ };
485
+ if (!path.isAbsolute(configPath)) return unknown;
486
+ let raw;
487
+ try {
488
+ if (!fs.existsSync(configPath)) {
489
+ return { configPath, userScope: false, localScopeDirs: [] };
490
+ }
491
+ raw = fs.readFileSync(configPath, "utf8");
492
+ } catch {
493
+ return unknown;
494
+ }
495
+ const errors = [];
496
+ const parsed = jsonc.parse(raw.replace(/^\uFEFF/, ""), errors, {
497
+ allowTrailingComma: true
498
+ });
499
+ if (errors.length > 0 || typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
500
+ return unknown;
501
+ }
502
+ const has = (servers) => typeof servers === "object" && servers !== null && Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
503
+ const own = (key) => Object.prototype.hasOwnProperty.call(parsed, key) ? parsed[key] : void 0;
504
+ const projects = own("projects");
505
+ const localScopeDirs = typeof projects === "object" && projects !== null && !Array.isArray(projects) ? Object.entries(projects).filter(([, project]) => has(project?.mcpServers)).map(([dir]) => dir) : [];
506
+ return { configPath, userScope: has(own("mcpServers")), localScopeDirs };
507
+ }
508
+ function describeDirs(dirs) {
509
+ if (dirs.length === 1) return dirs[0];
510
+ return `${dirs.length} project directories (${dirs.slice(0, 3).join(", ")}${dirs.length > 3 ? ", \u2026" : ""})`;
511
+ }
512
+ function interpretRemoveResult(result, state, binary = "claude") {
513
+ if (result.refused) return result;
514
+ const alsoElsewhere = state.localScopeDirs.length > 0 ? ` A separate local-scope ${SERVER_NAME} entry (which may carry its own API key) also exists in ${describeDirs(state.localScopeDirs)} \u2014 this wizard does not touch those. Remove with \`${describeCommand(binary, ["mcp", "remove", SERVER_NAME, "--scope", "local"])}\` from each.` : "";
515
+ if (state.userScope === true) {
516
+ return {
517
+ success: false,
518
+ message: `the ${SERVER_NAME} entry is STILL in Claude Code's user config (${state.configPath}), so your API key is still on disk. ${result.success ? "Most likely that file is not writable \u2014 the CLI reports success anyway." : `The removal command itself failed: ${result.message ?? "no output"}`}${alsoElsewhere}`
519
+ };
520
+ }
521
+ if (state.userScope === false) {
522
+ return {
523
+ success: true,
524
+ message: (result.success ? "removed from your user config" : "nothing to remove in your user config") + alsoElsewhere
525
+ };
526
+ }
527
+ if (result.success) {
528
+ return {
529
+ success: true,
530
+ message: `removal reported success, but ${state.configPath} could not be read to confirm it`
531
+ };
532
+ }
533
+ return { success: false, message: result.message };
534
+ }
535
+ function interpretAddResult(result, state, binary) {
536
+ if (result.refused || !result.success) return result;
537
+ if (state.userScope === false) {
538
+ return {
539
+ success: false,
540
+ message: `Claude Code reported success but no ${SERVER_NAME} entry is in its user config afterwards (${state.configPath}) \u2014 that file is most likely not writable. Nothing was configured.`
541
+ };
542
+ }
543
+ if (state.userScope === true && state.localScopeDirs.length > 0) {
544
+ return {
545
+ success: true,
546
+ message: `${result.message ? `${result.message}; ` : ""}note a local-scope ${SERVER_NAME} entry also exists in ${describeDirs(state.localScopeDirs)} and takes precedence over the user config there, so Claude Code will keep using it in those projects \u2014 remove it with \`${describeCommand(binary, ["mcp", "remove", SERVER_NAME, "--scope", "local"])}\` run from each`
547
+ };
548
+ }
549
+ if (state.userScope === void 0) {
550
+ return {
551
+ success: true,
552
+ message: `${result.message ? `${result.message}; ` : ""}could not read ${state.configPath} to confirm the entry landed`
553
+ };
554
+ }
555
+ return { success: true, message: result.message };
556
+ }
557
+ function createClaudeCodeClient(resolveBinary, readState = () => readClaudeConfigState(claudeUserConfigPath())) {
558
+ return {
559
+ id: "claude-code",
560
+ name: "Claude Code",
561
+ detect: () => Promise.resolve(resolveBinary() !== null),
562
+ add: (mode) => {
563
+ const args = mode.kind === "local" ? [
564
+ "mcp",
565
+ "add",
566
+ SERVER_NAME,
567
+ "--scope",
568
+ "user",
569
+ "-e",
570
+ `RUNPOD_API_KEY=${mode.apiKey}`,
571
+ "--",
572
+ "npx",
573
+ "-y",
574
+ NPM_PACKAGE
575
+ ] : [
576
+ "mcp",
577
+ "add",
578
+ "--transport",
579
+ "http",
580
+ "--scope",
581
+ "user",
582
+ SERVER_NAME,
583
+ mode.url
584
+ ];
585
+ const binary = resolveBinary();
586
+ if (!binary) {
587
+ return Promise.resolve({
588
+ success: false,
589
+ message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, register it yourself${mode.kind === "local" ? ", substituting your key" : ""}:
590
+ ${describeCommandRedacted("claude", args)}`
591
+ });
592
+ }
593
+ const result = runClaude(
594
+ binary,
595
+ args,
596
+ mode.kind === "local" ? mode.apiKey : void 0
597
+ );
598
+ if (!result.success) return Promise.resolve(result);
599
+ return Promise.resolve(interpretAddResult(result, readState(), binary));
600
+ },
601
+ remove: () => {
602
+ const binary = resolveBinary();
603
+ if (!binary) {
604
+ return Promise.resolve({
605
+ success: false,
606
+ message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, remove the entry yourself:
607
+ ${describeCommand("claude", ["mcp", "remove", SERVER_NAME, "--scope", "user"])}`
608
+ });
609
+ }
610
+ const result = runClaude(binary, [
611
+ "mcp",
612
+ "remove",
613
+ "--scope",
614
+ "user",
615
+ SERVER_NAME
616
+ ]);
617
+ if (result.refused) return Promise.resolve(result);
618
+ return Promise.resolve(
619
+ interpretRemoveResult(result, readState(), binary)
620
+ );
621
+ },
622
+ describeTarget: () => "Claude Code user config (via `claude mcp` CLI)"
623
+ };
624
+ }
625
+ var claudeCodeClient = createClaudeCodeClient(findClaudeBinary);
626
+ var home = os.homedir();
627
+ function appDataDir() {
628
+ return absoluteEnvDir(
629
+ process.env.APPDATA,
630
+ () => path.join(home, "AppData", "Roaming")
631
+ );
632
+ }
633
+ function claudeDesktopConfigPath() {
634
+ if (process.platform === "win32") {
635
+ return path.join(appDataDir(), "Claude", "claude_desktop_config.json");
636
+ }
637
+ if (process.platform === "darwin") {
638
+ return path.join(
639
+ home,
640
+ "Library",
641
+ "Application Support",
642
+ "Claude",
643
+ "claude_desktop_config.json"
644
+ );
645
+ }
646
+ return path.join(
647
+ absoluteEnvDir(
648
+ process.env.XDG_CONFIG_HOME,
649
+ () => path.join(home, ".config")
650
+ ),
651
+ "Claude",
652
+ "claude_desktop_config.json"
653
+ );
654
+ }
655
+ function vsCodeConfigPath() {
656
+ if (process.platform === "win32") {
657
+ return path.join(appDataDir(), "Code", "User", "mcp.json");
658
+ }
659
+ if (process.platform === "darwin") {
660
+ return path.join(
661
+ home,
662
+ "Library",
663
+ "Application Support",
664
+ "Code",
665
+ "User",
666
+ "mcp.json"
667
+ );
668
+ }
669
+ return path.join(
670
+ absoluteEnvDir(
671
+ process.env.XDG_CONFIG_HOME,
672
+ () => path.join(home, ".config")
673
+ ),
674
+ "Code",
675
+ "User",
676
+ "mcp.json"
677
+ );
678
+ }
679
+ var CLIENTS = [
680
+ claudeCodeClient,
681
+ jsonClient({
682
+ id: "cursor",
683
+ name: "Cursor",
684
+ // A VS Code fork, and its MCP reader keeps VS Code's tolerance — comments and
685
+ // trailing commas both parse. Read out of the shipped bundle, not assumed from
686
+ // "Electron app"; see ConfigDialect.
687
+ dialect: "jsonc",
688
+ configPath: () => path.join(home, ".cursor", "mcp.json")
689
+ }),
690
+ jsonClient({
691
+ id: "windsurf",
692
+ name: "Windsurf",
693
+ // Not installed on any machine this was checked on, so its parser was never read.
694
+ // Lenient-and-disclosed rather than a guess in either direction.
695
+ dialect: "unverified",
696
+ // Windsurf is stdio-only, so it reaches the hosted server via mcp-remote.
697
+ hostedStrategy: "mcp-remote",
698
+ configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json")
699
+ }),
700
+ jsonClient({
701
+ id: "claude-desktop",
702
+ name: "Claude Desktop",
703
+ // Genuinely strict: its reader is a bare `JSON.parse(readFileSync(...))` with no
704
+ // preprocessing and no retry, so a comment or trailing comma is fatal.
705
+ dialect: "json",
706
+ // Claude Desktop config files are stdio-only; use the mcp-remote bridge.
707
+ hostedStrategy: "mcp-remote",
708
+ configPath: claudeDesktopConfigPath,
709
+ // Detect by the application/support directory, which exists once the app runs.
710
+ detectPaths: () => [path.dirname(claudeDesktopConfigPath())]
711
+ }),
712
+ jsonClient({
713
+ id: "vscode",
714
+ name: "Visual Studio Code",
715
+ serverProperty: "servers",
716
+ // The one client that genuinely reads JSONC — comments and trailing commas in
717
+ // mcp.json are supported and common.
718
+ dialect: "jsonc",
719
+ configPath: vsCodeConfigPath,
720
+ detectPaths: () => [path.dirname(vsCodeConfigPath())]
721
+ })
722
+ ];
723
+
724
+ // src/install/verify-key.ts
725
+ var VERIFY_API_KEY_TIMEOUT_MS = 1e4;
726
+ async function verifyApiKey(apiKey, timeoutMs = VERIFY_API_KEY_TIMEOUT_MS) {
727
+ try {
728
+ const base = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
729
+ const response = await fetch(`${base}/pods`, {
730
+ headers: { Authorization: `Bearer ${apiKey}` },
731
+ // Without this the wizard sits at "Verifying…" forever against a host
732
+ // that accepts the connection and goes quiet, with no way out but ^C.
733
+ // An abort lands in the catch below and is reported as "check failed",
734
+ // which is exactly what it is.
735
+ signal: AbortSignal.timeout(timeoutMs)
736
+ });
737
+ if (response.ok) return true;
738
+ if (response.status === 401 || response.status === 403) return false;
739
+ return null;
740
+ } catch {
741
+ return null;
742
+ }
743
+ }
744
+
745
+ // src/install/wizard.ts
746
+ var API_KEYS_URL = "https://www.runpod.io/console/user/settings";
747
+ function openBrowser(url) {
748
+ try {
749
+ if (process.platform === "darwin") {
750
+ execFileSync("open", [url], { stdio: "ignore" });
751
+ } else if (process.platform === "win32") {
752
+ execFileSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
753
+ } else {
754
+ execFileSync("xdg-open", [url], { stdio: "ignore" });
755
+ }
756
+ } catch {
757
+ }
758
+ }
759
+ function bail(message = "Cancelled.") {
760
+ p.cancel(message);
761
+ process.exit(0);
762
+ }
763
+ async function promptForApiKey() {
764
+ const fromEnv = process.env.RUNPOD_API_KEY?.trim();
765
+ if (fromEnv) {
766
+ const useEnv = await p.confirm({
767
+ message: "Use the RUNPOD_API_KEY from your environment?"
768
+ });
769
+ if (p.isCancel(useEnv)) bail();
770
+ if (useEnv) return fromEnv;
771
+ }
772
+ const create = await p.confirm({
773
+ message: "Open the Runpod console to create or copy an API key?"
774
+ });
775
+ if (p.isCancel(create)) bail();
776
+ if (create) {
777
+ openBrowser(API_KEYS_URL);
778
+ p.log.info(`Opening ${API_KEYS_URL}`);
779
+ }
780
+ const apiKey = await p.password({
781
+ message: "Paste your Runpod API key",
782
+ validate: (value) => !value || value.trim().length === 0 ? "API key cannot be empty" : void 0
783
+ });
784
+ if (p.isCancel(apiKey)) bail();
785
+ return apiKey.trim();
786
+ }
787
+ async function selectClients(detected, verb) {
788
+ const options = CLIENTS.map((client) => ({
789
+ value: client.id,
790
+ label: client.name,
791
+ hint: detected.get(client.id) ? "detected" : void 0
792
+ }));
793
+ const selected = await p.multiselect({
794
+ message: `Which agents should we ${verb}?`,
795
+ options,
796
+ // Pre-select detected clients so the common case is a single Enter press.
797
+ initialValues: CLIENTS.filter((c) => detected.get(c.id)).map((c) => c.id),
798
+ required: true
799
+ });
800
+ if (p.isCancel(selected)) bail();
801
+ return CLIENTS.filter((c) => selected.includes(c.id));
802
+ }
803
+ async function detectAll() {
804
+ const entries = await Promise.all(
805
+ CLIENTS.map(async (c) => [c.id, await c.detect()])
806
+ );
807
+ return new Map(entries);
808
+ }
809
+ async function selectMode() {
810
+ const kind = await p.select({
811
+ message: "How do you want to connect to Runpod?",
812
+ options: [
813
+ {
814
+ value: "hosted",
815
+ label: "Sign in with Runpod (recommended)",
816
+ hint: "hosted, OAuth, no API key stored"
817
+ },
818
+ {
819
+ value: "local",
820
+ label: "Run locally with an API key",
821
+ hint: "runs on this machine via npx"
822
+ }
823
+ ],
824
+ initialValue: "hosted"
825
+ });
826
+ if (p.isCancel(kind)) bail();
827
+ if (kind === "hosted") {
828
+ return { kind: "hosted", url: HOSTED_URL };
829
+ }
830
+ const apiKey = await promptForApiKey();
831
+ const verifySpinner = p.spinner();
832
+ verifySpinner.start("Verifying API key");
833
+ const valid = await verifyApiKey(apiKey);
834
+ if (valid === true) {
835
+ verifySpinner.stop("API key verified.");
836
+ } else if (valid === false) {
837
+ verifySpinner.stop("API key rejected by Runpod.");
838
+ const proceed = await p.confirm({
839
+ message: "The key did not authenticate. Continue anyway?",
840
+ initialValue: false
841
+ });
842
+ if (p.isCancel(proceed) || !proceed) bail();
843
+ } else {
844
+ verifySpinner.stop("Could not verify the key (offline?). Continuing.");
845
+ }
846
+ return { kind: "local", apiKey };
847
+ }
848
+ async function runAdd() {
849
+ const detected = await detectAll();
850
+ const clients = await selectClients(detected, "set up");
851
+ const mode = await selectMode();
852
+ const results = [];
853
+ for (const client of clients) {
854
+ const spinner2 = p.spinner();
855
+ spinner2.start(`Configuring ${client.name}`);
856
+ const result = await client.add(mode);
857
+ if (result.success) {
858
+ spinner2.stop(`${client.name} configured.`);
859
+ results.push(
860
+ `\u2713 ${client.name} \u2014 ${result.message ? `${result.message}; ` : ""}${client.describeTarget()}`
861
+ );
862
+ } else {
863
+ spinner2.stop(`${client.name} failed.`);
864
+ results.push(`\u2717 ${client.name} \u2014 ${result.message ?? "unknown error"}`);
865
+ }
866
+ }
867
+ p.note(results.join("\n"), "Results");
868
+ p.outro(
869
+ mode.kind === "hosted" ? 'Restart your agent, then complete "Sign in with Runpod" when it connects.' : "Restart your agent (or reconnect MCP) to load the Runpod tools."
870
+ );
871
+ }
872
+ async function runRemove() {
873
+ const detected = await detectAll();
874
+ const clients = await selectClients(detected, "remove Runpod from");
875
+ const results = [];
876
+ for (const client of clients) {
877
+ const spinner2 = p.spinner();
878
+ spinner2.start(`Removing from ${client.name}`);
879
+ const result = await client.remove();
880
+ spinner2.stop(
881
+ result.success ? `${client.name} cleaned up.` : `${client.name} failed.`
882
+ );
883
+ results.push(
884
+ `${result.success ? "\u2713" : "\u2717"} ${client.name}${result.message ? ` \u2014 ${result.message}` : ""}`
885
+ );
886
+ }
887
+ p.note(results.join("\n"), "Results");
888
+ p.outro("Done.");
889
+ }
890
+ async function runWizard(subcommand) {
891
+ p.intro("Runpod MCP server installer");
892
+ if (subcommand === "remove") {
893
+ await runRemove();
894
+ } else {
895
+ await runAdd();
896
+ }
897
+ }
898
+ export {
899
+ runWizard
900
+ };
901
+ //# sourceMappingURL=wizard-FBLOZ6YM.mjs.map