@runpod/mcp-server 3.2.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.
- package/CHANGELOG.md +66 -0
- package/dist/{chunk-WGK7D2YR.mjs → chunk-ICUS4KGX.mjs} +345 -98
- package/dist/chunk-ICUS4KGX.mjs.map +1 -0
- package/dist/http.cjs +362 -98
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +19 -2
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +957 -204
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +346 -99
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +344 -97
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +10 -1
- package/dist/tools.d.ts +10 -1
- package/dist/tools.mjs +1 -1
- package/dist/wizard-FBLOZ6YM.mjs +901 -0
- package/dist/wizard-FBLOZ6YM.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-WGK7D2YR.mjs.map +0 -1
- package/dist/wizard-37ZJSVB5.mjs +0 -402
- package/dist/wizard-37ZJSVB5.mjs.map +0 -1
package/dist/stdio.cjs
CHANGED
|
@@ -45,32 +45,138 @@ function hostedServerConfig(strategy, serverProperty, url) {
|
|
|
45
45
|
}
|
|
46
46
|
return serverProperty === "servers" ? { type: "http", url } : { url };
|
|
47
47
|
}
|
|
48
|
-
function
|
|
48
|
+
function describeDialect(dialect) {
|
|
49
|
+
return dialect === "json" ? "JSON" : "JSONC";
|
|
50
|
+
}
|
|
51
|
+
function parseFailure(content, dialect) {
|
|
52
|
+
if (dialect === "json") {
|
|
53
|
+
try {
|
|
54
|
+
JSON.parse(content);
|
|
55
|
+
return void 0;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return errMessage(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const errors = [];
|
|
61
|
+
jsonc.parse(content, errors, { allowTrailingComma: true });
|
|
62
|
+
return errors.length > 0 ? `JSONC parse error at offset ${errors[0].offset}` : void 0;
|
|
63
|
+
}
|
|
64
|
+
function parseAsClient(content, dialect) {
|
|
65
|
+
if (dialect !== "json") {
|
|
66
|
+
return jsonc.parse(content, [], { allowTrailingComma: true });
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(content);
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function readBackEntry(content, serverProperty, dialect) {
|
|
75
|
+
const parsed = parseAsClient(content, dialect);
|
|
76
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
77
|
+
const servers = parsed[serverProperty];
|
|
78
|
+
if (typeof servers !== "object" || servers === null) return void 0;
|
|
79
|
+
return servers[SERVER_NAME2];
|
|
80
|
+
}
|
|
81
|
+
function reliesOnLeniency(content) {
|
|
82
|
+
if (content.trim() === "") return false;
|
|
83
|
+
try {
|
|
84
|
+
JSON.parse(content);
|
|
85
|
+
return false;
|
|
86
|
+
} catch {
|
|
87
|
+
return parseFailure(content, "jsonc") === void 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function upsertJsonServer(configPath, serverProperty, value, dialect) {
|
|
91
|
+
if (!path.isAbsolute(configPath)) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
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.`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
49
97
|
try {
|
|
50
98
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
51
99
|
const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
52
100
|
const edits = jsonc.modify(content, [serverProperty, SERVER_NAME2], value, {
|
|
53
101
|
formattingOptions: { tabSize: 2, insertSpaces: true }
|
|
54
102
|
});
|
|
55
|
-
|
|
103
|
+
const updated = jsonc.applyEdits(content, edits).replace(/^\uFEFF/, "");
|
|
104
|
+
const failure = parseFailure(updated, dialect);
|
|
105
|
+
if (failure) {
|
|
106
|
+
const wasAlreadyBroken = content.trim() !== "" && parseFailure(content, dialect) !== void 0;
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
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.`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const landed = readBackEntry(updated, serverProperty, dialect);
|
|
113
|
+
if (JSON.stringify(landed) !== JSON.stringify(value)) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
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.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
fs.writeFileSync(configPath, updated, { encoding: "utf8", mode: 384 });
|
|
120
|
+
if (dialect === "unverified" && reliesOnLeniency(updated)) {
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
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"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
56
126
|
return { success: true };
|
|
57
127
|
} catch (error) {
|
|
58
128
|
return { success: false, message: errMessage(error) };
|
|
59
129
|
}
|
|
60
130
|
}
|
|
61
|
-
function removeJsonServer(configPath, serverProperty) {
|
|
131
|
+
function removeJsonServer(configPath, serverProperty, dialect) {
|
|
132
|
+
if (!path.isAbsolute(configPath)) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
message: `refusing to touch "${configPath}": not an absolute path. Check APPDATA / XDG_CONFIG_HOME.`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
62
138
|
try {
|
|
63
139
|
if (!fs.existsSync(configPath)) {
|
|
64
140
|
return { success: true, message: "nothing to remove" };
|
|
65
141
|
}
|
|
66
142
|
const content = fs.readFileSync(configPath, "utf8");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
143
|
+
if (content.trim() === "") {
|
|
144
|
+
return { success: true, message: "nothing to remove" };
|
|
145
|
+
}
|
|
146
|
+
const existingFailure = parseFailure(content, dialect);
|
|
147
|
+
if (existingFailure !== void 0) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
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.`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
let edits;
|
|
154
|
+
try {
|
|
155
|
+
edits = jsonc.modify(content, [serverProperty, SERVER_NAME2], void 0, {
|
|
156
|
+
formattingOptions: { tabSize: 2, insertSpaces: true }
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (/empty document/i.test(errMessage(error))) {
|
|
160
|
+
return { success: true, message: "nothing to remove" };
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
if (edits.length === 0) {
|
|
165
|
+
return readBackEntry(content, serverProperty, dialect) === void 0 ? { success: true, message: "nothing to remove" } : {
|
|
166
|
+
success: false,
|
|
167
|
+
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.`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const updated = jsonc.applyEdits(content, edits);
|
|
171
|
+
const brokeIt = parseFailure(updated, dialect) !== void 0;
|
|
172
|
+
const stillThere = readBackEntry(updated, serverProperty, dialect) !== void 0;
|
|
173
|
+
if (brokeIt || stillThere) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
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.`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
fs.writeFileSync(configPath, updated, "utf8");
|
|
74
180
|
return { success: true };
|
|
75
181
|
} catch (error) {
|
|
76
182
|
return { success: false, message: errMessage(error) };
|
|
@@ -98,56 +204,461 @@ function jsonClient(opts) {
|
|
|
98
204
|
add: (mode) => {
|
|
99
205
|
const value = mode.kind === "local" ? localServerConfig(mode.apiKey, serverProperty) : hostedServerConfig(hostedStrategy, serverProperty, mode.url);
|
|
100
206
|
return Promise.resolve(
|
|
101
|
-
upsertJsonServer(opts.configPath(), serverProperty, value)
|
|
207
|
+
upsertJsonServer(opts.configPath(), serverProperty, value, opts.dialect)
|
|
102
208
|
);
|
|
103
209
|
},
|
|
104
|
-
|
|
210
|
+
dialect: opts.dialect,
|
|
211
|
+
remove: () => Promise.resolve(
|
|
212
|
+
removeJsonServer(opts.configPath(), serverProperty, opts.dialect)
|
|
213
|
+
),
|
|
105
214
|
describeTarget: () => opts.configPath()
|
|
106
215
|
};
|
|
107
216
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
|
|
217
|
+
function claudeCandidates(platform, homedir2, appdata) {
|
|
218
|
+
if (platform === "win32") {
|
|
219
|
+
const win = path.win32;
|
|
220
|
+
const defaultAppdata = win.join(homedir2, "AppData", "Roaming");
|
|
221
|
+
return [
|
|
222
|
+
{
|
|
223
|
+
path: win.join(homedir2, ".local", "bin", "claude.exe"),
|
|
224
|
+
trustedRoot: homedir2
|
|
225
|
+
},
|
|
226
|
+
// npm -g installs a .cmd shim. runClaude bypasses it and resolves the real
|
|
227
|
+
// native or JavaScript target from the installed package manifest.
|
|
228
|
+
//
|
|
229
|
+
// absoluteEnvDir, not `?? `: an empty or relative APPDATA yielded the relative
|
|
230
|
+
// `npm\claude.cmd`. Candidates are probed before any lookup, so a relative
|
|
231
|
+
// candidate would resolve against the current directory and receive the live
|
|
232
|
+
// API key.
|
|
233
|
+
{
|
|
234
|
+
path: win.join(
|
|
235
|
+
absoluteEnvDir(appdata, () => defaultAppdata),
|
|
236
|
+
"npm",
|
|
237
|
+
"claude.cmd"
|
|
238
|
+
),
|
|
239
|
+
trustedRoot: defaultAppdata
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
return [
|
|
244
|
+
// Where the current native installer puts it (verified on a real install:
|
|
245
|
+
// ~/.local/bin/claude -> versions/2.1.220). Same reasoning as the Windows
|
|
246
|
+
// .local\bin\claude.exe entry — it also covers an install whose PATH entry is
|
|
247
|
+
// missing, which the lookup can never find. Without it, POSIX detection depended
|
|
248
|
+
// entirely on PATH.
|
|
249
|
+
{
|
|
250
|
+
path: path.posix.join(homedir2, ".local", "bin", "claude"),
|
|
251
|
+
trustedRoot: homedir2
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
path: path.posix.join(homedir2, ".claude", "local", "claude"),
|
|
255
|
+
trustedRoot: homedir2
|
|
256
|
+
},
|
|
257
|
+
{ path: "/usr/local/bin/claude", trustedRoot: "/usr/local" },
|
|
258
|
+
{ path: "/opt/homebrew/bin/claude", trustedRoot: "/opt/homebrew" }
|
|
113
259
|
];
|
|
260
|
+
}
|
|
261
|
+
function pathIsInside(candidate, directory, pathApi) {
|
|
262
|
+
const relative = pathApi.relative(
|
|
263
|
+
pathApi.resolve(directory),
|
|
264
|
+
pathApi.resolve(candidate)
|
|
265
|
+
);
|
|
266
|
+
return relative === "" || !relative.startsWith(`..${pathApi.sep}`) && relative !== ".." && !pathApi.isAbsolute(relative);
|
|
267
|
+
}
|
|
268
|
+
function isNodeModulesBin(candidate) {
|
|
269
|
+
return /(^|[\\/])node_modules[\\/]\.bin([\\/]|$)/i.test(candidate);
|
|
270
|
+
}
|
|
271
|
+
function pickClaudeBinary(stdout, platform, cwd) {
|
|
272
|
+
if (platform === "win32") return null;
|
|
273
|
+
const hits = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
274
|
+
return hits.find(
|
|
275
|
+
(hit) => path.posix.isAbsolute(hit) && !isNodeModulesBin(hit) && (cwd === void 0 || !pathIsInside(hit, cwd, path.posix))
|
|
276
|
+
) ?? null;
|
|
277
|
+
}
|
|
278
|
+
function projectBoundary(cwd, home2) {
|
|
279
|
+
let current = cwd;
|
|
280
|
+
let boundary = null;
|
|
281
|
+
while (true) {
|
|
282
|
+
if (current === home2) return boundary;
|
|
283
|
+
if (PROJECT_BOUNDARY_MARKERS.some(
|
|
284
|
+
(marker) => exists(path.join(current, marker))
|
|
285
|
+
)) {
|
|
286
|
+
boundary = current;
|
|
287
|
+
}
|
|
288
|
+
const parent = path.dirname(current);
|
|
289
|
+
if (parent === current) return boundary;
|
|
290
|
+
current = parent;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function trustedClaudeBinary(binary, cwd, home2, requiredRoot) {
|
|
294
|
+
try {
|
|
295
|
+
const canonicalBinary = fs.realpathSync.native(binary);
|
|
296
|
+
const canonicalCwd = fs.realpathSync.native(cwd);
|
|
297
|
+
let canonicalHome;
|
|
298
|
+
if (home2 !== void 0) {
|
|
299
|
+
try {
|
|
300
|
+
canonicalHome = fs.realpathSync.native(home2);
|
|
301
|
+
} catch {
|
|
302
|
+
canonicalHome = void 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const projectRoot = projectBoundary(canonicalCwd, canonicalHome);
|
|
306
|
+
const canonicalRequiredRoot = requiredRoot === void 0 ? void 0 : fs.realpathSync.native(requiredRoot);
|
|
307
|
+
if (isNodeModulesBin(canonicalBinary) || canonicalRequiredRoot !== void 0 && !pathIsInside(canonicalBinary, canonicalRequiredRoot, path) || projectRoot !== null && pathIsInside(canonicalBinary, projectRoot, path)) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return canonicalBinary;
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function findClaudeBinary(options = {}) {
|
|
316
|
+
const platform = options.platform ?? process.platform;
|
|
317
|
+
let homedir2 = options.homedir;
|
|
318
|
+
if (homedir2 === void 0) {
|
|
319
|
+
try {
|
|
320
|
+
homedir2 = os.userInfo().homedir;
|
|
321
|
+
} catch {
|
|
322
|
+
homedir2 = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const appdata = options.appdata ?? process.env.APPDATA;
|
|
326
|
+
const cwd = options.cwd ?? process.cwd();
|
|
327
|
+
const candidates = homedir2 == null ? [] : claudeCandidates(platform, homedir2, appdata);
|
|
114
328
|
for (const candidate of candidates) {
|
|
115
|
-
if (
|
|
329
|
+
if (!path.isAbsolute(candidate.path) && !path.win32.isAbsolute(candidate.path))
|
|
330
|
+
continue;
|
|
331
|
+
if (exists(candidate.path)) {
|
|
332
|
+
const trusted = trustedClaudeBinary(
|
|
333
|
+
candidate.path,
|
|
334
|
+
cwd,
|
|
335
|
+
homedir2 ?? void 0,
|
|
336
|
+
candidate.trustedRoot
|
|
337
|
+
);
|
|
338
|
+
if (trusted !== null) return trusted;
|
|
339
|
+
}
|
|
116
340
|
}
|
|
117
341
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
342
|
+
if (platform === "win32") return null;
|
|
343
|
+
const out = (0, import_child_process.execSync)("command -v claude", {
|
|
344
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
345
|
+
encoding: "utf8"
|
|
346
|
+
});
|
|
347
|
+
const binary = pickClaudeBinary(out, platform);
|
|
348
|
+
return binary === null ? null : trustedClaudeBinary(binary, cwd, homedir2 ?? void 0);
|
|
120
349
|
} catch {
|
|
121
350
|
return null;
|
|
122
351
|
}
|
|
123
352
|
}
|
|
124
|
-
function
|
|
353
|
+
function describeCommand(binary, args) {
|
|
354
|
+
const quote = (s) => /\s/.test(s) || CMD_META.test(s) ? `"${s}"` : s;
|
|
355
|
+
return [binary, ...args].map(quote).join(" ");
|
|
356
|
+
}
|
|
357
|
+
function describeCommandRedacted(binary, args) {
|
|
358
|
+
return describeCommand(
|
|
359
|
+
binary,
|
|
360
|
+
args.map(
|
|
361
|
+
(arg) => arg.startsWith("RUNPOD_API_KEY=") ? `RUNPOD_API_KEY=${KEY_PLACEHOLDER}` : arg
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
function redactSecret(text, secret) {
|
|
366
|
+
if (!secret) return text;
|
|
367
|
+
return text.split(secret).join(KEY_PLACEHOLDER);
|
|
368
|
+
}
|
|
369
|
+
function sanitizedClaudeEnv() {
|
|
370
|
+
const env = { ...process.env };
|
|
371
|
+
for (const key of Object.keys(env)) {
|
|
372
|
+
if (["path", "comspec", "pathext", "node_options", "node_path"].includes(
|
|
373
|
+
key.toLowerCase()
|
|
374
|
+
)) {
|
|
375
|
+
delete env[key];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (process.platform === "win32") {
|
|
379
|
+
env.Path = path.dirname(process.execPath);
|
|
380
|
+
} else {
|
|
381
|
+
env.PATH = [
|
|
382
|
+
path.dirname(process.execPath),
|
|
383
|
+
"/usr/local/bin",
|
|
384
|
+
"/opt/homebrew/bin",
|
|
385
|
+
"/usr/bin",
|
|
386
|
+
"/bin"
|
|
387
|
+
].join(path.delimiter);
|
|
388
|
+
}
|
|
389
|
+
return env;
|
|
390
|
+
}
|
|
391
|
+
function windowsNpmClaudeTarget(binary) {
|
|
392
|
+
if (process.platform !== "win32" || !binary.toLowerCase().endsWith(".cmd")) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
125
395
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
396
|
+
const npmRoot = fs.realpathSync.native(path.dirname(binary));
|
|
397
|
+
const packageRoot = fs.realpathSync.native(
|
|
398
|
+
path.join(npmRoot, "node_modules", "@anthropic-ai", "claude-code")
|
|
399
|
+
);
|
|
400
|
+
if (!pathIsInside(packageRoot, npmRoot, path)) return null;
|
|
401
|
+
const manifestPath = fs.realpathSync.native(
|
|
402
|
+
path.join(packageRoot, "package.json")
|
|
403
|
+
);
|
|
404
|
+
if (!pathIsInside(manifestPath, packageRoot, path)) return null;
|
|
405
|
+
const manifest = JSON.parse(
|
|
406
|
+
fs.readFileSync(manifestPath, "utf8")
|
|
407
|
+
);
|
|
408
|
+
if (manifest === null || typeof manifest !== "object") return null;
|
|
409
|
+
const bin = manifest.bin;
|
|
410
|
+
const target = typeof bin === "string" ? bin : bin !== null && typeof bin === "object" ? bin.claude : void 0;
|
|
411
|
+
if (typeof target !== "string" || target.trim() === "") return null;
|
|
412
|
+
const entrypoint = fs.realpathSync.native(
|
|
413
|
+
path.resolve(packageRoot, target)
|
|
414
|
+
);
|
|
415
|
+
if (!pathIsInside(entrypoint, packageRoot, path)) return null;
|
|
416
|
+
if (/\.(?:exe|com)$/i.test(entrypoint)) {
|
|
417
|
+
return { command: entrypoint, argsPrefix: [] };
|
|
418
|
+
}
|
|
419
|
+
if (/\.(?:c?js|mjs)$/i.test(entrypoint)) {
|
|
420
|
+
return { command: process.execPath, argsPrefix: [entrypoint] };
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function runClaude(binary, args, secret) {
|
|
428
|
+
const isWindowsCmd = process.platform === "win32" && binary.toLowerCase().endsWith(".cmd");
|
|
429
|
+
const npmTarget = windowsNpmClaudeTarget(binary);
|
|
430
|
+
if (isWindowsCmd && npmTarget === null) {
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
refused: true,
|
|
434
|
+
message: `could not locate a trusted Claude Code npm entrypoint beside ${binary}. Run the shim yourself, substituting your key:
|
|
435
|
+
${describeCommandRedacted(binary, args)}`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const command = npmTarget?.command ?? binary;
|
|
439
|
+
const commandArgs = [...npmTarget?.argsPrefix ?? [], ...args];
|
|
440
|
+
const result = (0, import_child_process.spawnSync)(command, commandArgs, {
|
|
441
|
+
stdio: "pipe",
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
env: sanitizedClaudeEnv()
|
|
444
|
+
});
|
|
445
|
+
if (result.error)
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
message: redactSecret(errMessage(result.error), secret)
|
|
449
|
+
};
|
|
450
|
+
const stdio = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
451
|
+
if (result.status === 0) return { success: true, output: stdio };
|
|
452
|
+
if (result.status === null) {
|
|
453
|
+
return { success: false, message: `killed by ${result.signal}` };
|
|
454
|
+
}
|
|
455
|
+
if (stdio.includes("already exists")) {
|
|
456
|
+
return {
|
|
457
|
+
success: true,
|
|
458
|
+
// Deliberately not "to replace the API key": this branch is also reached in
|
|
459
|
+
// hosted mode, where there is no key — and the pre-existing entry may be of the
|
|
460
|
+
// other kind entirely, which is exactly what the user needs to know.
|
|
461
|
+
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_NAME2, "--scope", "user"])}\` first, then re-run this wizard)`
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
success: false,
|
|
466
|
+
message: redactSecret(
|
|
467
|
+
stdio.trim() || `${path.basename(binary)} exited with code ${result.status}`,
|
|
468
|
+
secret
|
|
469
|
+
)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function absoluteEnvDir(value, fallback) {
|
|
473
|
+
if (!value || !value.trim()) return fallback();
|
|
474
|
+
if (!path.isAbsolute(value) && !path.win32.isAbsolute(value))
|
|
475
|
+
return fallback();
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
function claudeUserConfigPath() {
|
|
479
|
+
const configured = process.env.CLAUDE_CONFIG_DIR;
|
|
480
|
+
return path.join(
|
|
481
|
+
configured ? path.resolve(configured) : os.homedir(),
|
|
482
|
+
".claude.json"
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function readClaudeConfigState(configPath) {
|
|
486
|
+
const unknown = {
|
|
487
|
+
configPath,
|
|
488
|
+
userScope: void 0,
|
|
489
|
+
localScopeDirs: []
|
|
490
|
+
};
|
|
491
|
+
if (!path.isAbsolute(configPath)) return unknown;
|
|
492
|
+
let raw;
|
|
493
|
+
try {
|
|
494
|
+
if (!fs.existsSync(configPath)) {
|
|
495
|
+
return { configPath, userScope: false, localScopeDirs: [] };
|
|
132
496
|
}
|
|
133
|
-
|
|
497
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
498
|
+
} catch {
|
|
499
|
+
return unknown;
|
|
500
|
+
}
|
|
501
|
+
const errors = [];
|
|
502
|
+
const parsed = jsonc.parse(raw.replace(/^\uFEFF/, ""), errors, {
|
|
503
|
+
allowTrailingComma: true
|
|
504
|
+
});
|
|
505
|
+
if (errors.length > 0 || typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
506
|
+
return unknown;
|
|
507
|
+
}
|
|
508
|
+
const has = (servers) => typeof servers === "object" && servers !== null && Object.prototype.hasOwnProperty.call(servers, SERVER_NAME2);
|
|
509
|
+
const own = (key) => Object.prototype.hasOwnProperty.call(parsed, key) ? parsed[key] : void 0;
|
|
510
|
+
const projects = own("projects");
|
|
511
|
+
const localScopeDirs = typeof projects === "object" && projects !== null && !Array.isArray(projects) ? Object.entries(projects).filter(([, project]) => has(project?.mcpServers)).map(([dir]) => dir) : [];
|
|
512
|
+
return { configPath, userScope: has(own("mcpServers")), localScopeDirs };
|
|
513
|
+
}
|
|
514
|
+
function describeDirs(dirs) {
|
|
515
|
+
if (dirs.length === 1) return dirs[0];
|
|
516
|
+
return `${dirs.length} project directories (${dirs.slice(0, 3).join(", ")}${dirs.length > 3 ? ", \u2026" : ""})`;
|
|
517
|
+
}
|
|
518
|
+
function interpretRemoveResult(result, state, binary = "claude") {
|
|
519
|
+
if (result.refused) return result;
|
|
520
|
+
const alsoElsewhere = state.localScopeDirs.length > 0 ? ` A separate local-scope ${SERVER_NAME2} 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_NAME2, "--scope", "local"])}\` from each.` : "";
|
|
521
|
+
if (state.userScope === true) {
|
|
522
|
+
return {
|
|
523
|
+
success: false,
|
|
524
|
+
message: `the ${SERVER_NAME2} 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}`
|
|
525
|
+
};
|
|
134
526
|
}
|
|
527
|
+
if (state.userScope === false) {
|
|
528
|
+
return {
|
|
529
|
+
success: true,
|
|
530
|
+
message: (result.success ? "removed from your user config" : "nothing to remove in your user config") + alsoElsewhere
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (result.success) {
|
|
534
|
+
return {
|
|
535
|
+
success: true,
|
|
536
|
+
message: `removal reported success, but ${state.configPath} could not be read to confirm it`
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return { success: false, message: result.message };
|
|
540
|
+
}
|
|
541
|
+
function interpretAddResult(result, state, binary) {
|
|
542
|
+
if (result.refused || !result.success) return result;
|
|
543
|
+
if (state.userScope === false) {
|
|
544
|
+
return {
|
|
545
|
+
success: false,
|
|
546
|
+
message: `Claude Code reported success but no ${SERVER_NAME2} entry is in its user config afterwards (${state.configPath}) \u2014 that file is most likely not writable. Nothing was configured.`
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (state.userScope === true && state.localScopeDirs.length > 0) {
|
|
550
|
+
return {
|
|
551
|
+
success: true,
|
|
552
|
+
message: `${result.message ? `${result.message}; ` : ""}note a local-scope ${SERVER_NAME2} 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_NAME2, "--scope", "local"])}\` run from each`
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (state.userScope === void 0) {
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
message: `${result.message ? `${result.message}; ` : ""}could not read ${state.configPath} to confirm the entry landed`
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return { success: true, message: result.message };
|
|
562
|
+
}
|
|
563
|
+
function createClaudeCodeClient(resolveBinary, readState = () => readClaudeConfigState(claudeUserConfigPath())) {
|
|
564
|
+
return {
|
|
565
|
+
id: "claude-code",
|
|
566
|
+
name: "Claude Code",
|
|
567
|
+
detect: () => Promise.resolve(resolveBinary() !== null),
|
|
568
|
+
add: (mode) => {
|
|
569
|
+
const args = mode.kind === "local" ? [
|
|
570
|
+
"mcp",
|
|
571
|
+
"add",
|
|
572
|
+
SERVER_NAME2,
|
|
573
|
+
"--scope",
|
|
574
|
+
"user",
|
|
575
|
+
"-e",
|
|
576
|
+
`RUNPOD_API_KEY=${mode.apiKey}`,
|
|
577
|
+
"--",
|
|
578
|
+
"npx",
|
|
579
|
+
"-y",
|
|
580
|
+
NPM_PACKAGE
|
|
581
|
+
] : [
|
|
582
|
+
"mcp",
|
|
583
|
+
"add",
|
|
584
|
+
"--transport",
|
|
585
|
+
"http",
|
|
586
|
+
"--scope",
|
|
587
|
+
"user",
|
|
588
|
+
SERVER_NAME2,
|
|
589
|
+
mode.url
|
|
590
|
+
];
|
|
591
|
+
const binary = resolveBinary();
|
|
592
|
+
if (!binary) {
|
|
593
|
+
return Promise.resolve({
|
|
594
|
+
success: false,
|
|
595
|
+
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" : ""}:
|
|
596
|
+
${describeCommandRedacted("claude", args)}`
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const result = runClaude(
|
|
600
|
+
binary,
|
|
601
|
+
args,
|
|
602
|
+
mode.kind === "local" ? mode.apiKey : void 0
|
|
603
|
+
);
|
|
604
|
+
if (!result.success) return Promise.resolve(result);
|
|
605
|
+
return Promise.resolve(interpretAddResult(result, readState(), binary));
|
|
606
|
+
},
|
|
607
|
+
remove: () => {
|
|
608
|
+
const binary = resolveBinary();
|
|
609
|
+
if (!binary) {
|
|
610
|
+
return Promise.resolve({
|
|
611
|
+
success: false,
|
|
612
|
+
message: `claude CLI not found in the standard install locations. If Claude Code is installed somewhere custom, remove the entry yourself:
|
|
613
|
+
${describeCommand("claude", ["mcp", "remove", SERVER_NAME2, "--scope", "user"])}`
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const result = runClaude(binary, [
|
|
617
|
+
"mcp",
|
|
618
|
+
"remove",
|
|
619
|
+
"--scope",
|
|
620
|
+
"user",
|
|
621
|
+
SERVER_NAME2
|
|
622
|
+
]);
|
|
623
|
+
if (result.refused) return Promise.resolve(result);
|
|
624
|
+
return Promise.resolve(
|
|
625
|
+
interpretRemoveResult(result, readState(), binary)
|
|
626
|
+
);
|
|
627
|
+
},
|
|
628
|
+
describeTarget: () => "Claude Code user config (via `claude mcp` CLI)"
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function appDataDir() {
|
|
632
|
+
return absoluteEnvDir(
|
|
633
|
+
process.env.APPDATA,
|
|
634
|
+
() => path.join(home, "AppData", "Roaming")
|
|
635
|
+
);
|
|
135
636
|
}
|
|
136
637
|
function claudeDesktopConfigPath() {
|
|
137
638
|
if (process.platform === "win32") {
|
|
138
|
-
return path.join(
|
|
639
|
+
return path.join(appDataDir(), "Claude", "claude_desktop_config.json");
|
|
640
|
+
}
|
|
641
|
+
if (process.platform === "darwin") {
|
|
642
|
+
return path.join(
|
|
643
|
+
home,
|
|
644
|
+
"Library",
|
|
645
|
+
"Application Support",
|
|
646
|
+
"Claude",
|
|
647
|
+
"claude_desktop_config.json"
|
|
648
|
+
);
|
|
139
649
|
}
|
|
140
650
|
return path.join(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
651
|
+
absoluteEnvDir(
|
|
652
|
+
process.env.XDG_CONFIG_HOME,
|
|
653
|
+
() => path.join(home, ".config")
|
|
654
|
+
),
|
|
144
655
|
"Claude",
|
|
145
656
|
"claude_desktop_config.json"
|
|
146
657
|
);
|
|
147
658
|
}
|
|
148
659
|
function vsCodeConfigPath() {
|
|
149
660
|
if (process.platform === "win32") {
|
|
150
|
-
return path.join(
|
|
661
|
+
return path.join(appDataDir(), "Code", "User", "mcp.json");
|
|
151
662
|
}
|
|
152
663
|
if (process.platform === "darwin") {
|
|
153
664
|
return path.join(
|
|
@@ -159,9 +670,17 @@ function vsCodeConfigPath() {
|
|
|
159
670
|
"mcp.json"
|
|
160
671
|
);
|
|
161
672
|
}
|
|
162
|
-
return path.join(
|
|
673
|
+
return path.join(
|
|
674
|
+
absoluteEnvDir(
|
|
675
|
+
process.env.XDG_CONFIG_HOME,
|
|
676
|
+
() => path.join(home, ".config")
|
|
677
|
+
),
|
|
678
|
+
"Code",
|
|
679
|
+
"User",
|
|
680
|
+
"mcp.json"
|
|
681
|
+
);
|
|
163
682
|
}
|
|
164
|
-
var fs, os, path, import_child_process, jsonc, SERVER_NAME2, NPM_PACKAGE, HOSTED_URL, claudeCodeClient, home,
|
|
683
|
+
var fs, os, path, import_child_process, jsonc, SERVER_NAME2, NPM_PACKAGE, HOSTED_URL, PROJECT_BOUNDARY_MARKERS, CMD_META, KEY_PLACEHOLDER, claudeCodeClient, home, CLIENTS;
|
|
165
684
|
var init_clients = __esm({
|
|
166
685
|
"src/install/clients.ts"() {
|
|
167
686
|
"use strict";
|
|
@@ -173,73 +692,37 @@ var init_clients = __esm({
|
|
|
173
692
|
SERVER_NAME2 = "runpod";
|
|
174
693
|
NPM_PACKAGE = "@runpod/mcp-server@latest";
|
|
175
694
|
HOSTED_URL = process.env.RUNPOD_MCP_URL ?? "https://mcp.getrunpod.io/";
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"mcp",
|
|
190
|
-
"add",
|
|
191
|
-
SERVER_NAME2,
|
|
192
|
-
"--scope",
|
|
193
|
-
"user",
|
|
194
|
-
"-e",
|
|
195
|
-
`RUNPOD_API_KEY=${mode.apiKey}`,
|
|
196
|
-
"--",
|
|
197
|
-
"npx",
|
|
198
|
-
"-y",
|
|
199
|
-
NPM_PACKAGE
|
|
200
|
-
] : [
|
|
201
|
-
"mcp",
|
|
202
|
-
"add",
|
|
203
|
-
"--transport",
|
|
204
|
-
"http",
|
|
205
|
-
"--scope",
|
|
206
|
-
"user",
|
|
207
|
-
SERVER_NAME2,
|
|
208
|
-
mode.url
|
|
209
|
-
];
|
|
210
|
-
return Promise.resolve(runClaude(binary, args));
|
|
211
|
-
},
|
|
212
|
-
remove: () => {
|
|
213
|
-
const binary = findClaudeBinary();
|
|
214
|
-
if (!binary) {
|
|
215
|
-
return Promise.resolve({
|
|
216
|
-
success: false,
|
|
217
|
-
message: "claude CLI not found"
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
(0, import_child_process.execFileSync)(binary, ["mcp", "remove", "--scope", "user", SERVER_NAME2], {
|
|
222
|
-
stdio: "pipe"
|
|
223
|
-
});
|
|
224
|
-
return Promise.resolve({ success: true });
|
|
225
|
-
} catch (error) {
|
|
226
|
-
return Promise.resolve({ success: true, message: errMessage(error) });
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
describeTarget: () => "Claude Code user config (via `claude mcp` CLI)"
|
|
230
|
-
};
|
|
695
|
+
PROJECT_BOUNDARY_MARKERS = [
|
|
696
|
+
".git",
|
|
697
|
+
"package.json",
|
|
698
|
+
"pnpm-workspace.yaml",
|
|
699
|
+
"pnpm-lock.yaml",
|
|
700
|
+
"package-lock.json",
|
|
701
|
+
"yarn.lock",
|
|
702
|
+
"bun.lock",
|
|
703
|
+
"bun.lockb"
|
|
704
|
+
];
|
|
705
|
+
CMD_META = /[()\][%!^"`<>&|;, *?\t\n\v\r]/;
|
|
706
|
+
KEY_PLACEHOLDER = "YOUR_RUNPOD_API_KEY";
|
|
707
|
+
claudeCodeClient = createClaudeCodeClient(findClaudeBinary);
|
|
231
708
|
home = os.homedir();
|
|
232
|
-
appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
233
709
|
CLIENTS = [
|
|
234
710
|
claudeCodeClient,
|
|
235
711
|
jsonClient({
|
|
236
712
|
id: "cursor",
|
|
237
713
|
name: "Cursor",
|
|
714
|
+
// A VS Code fork, and its MCP reader keeps VS Code's tolerance — comments and
|
|
715
|
+
// trailing commas both parse. Read out of the shipped bundle, not assumed from
|
|
716
|
+
// "Electron app"; see ConfigDialect.
|
|
717
|
+
dialect: "jsonc",
|
|
238
718
|
configPath: () => path.join(home, ".cursor", "mcp.json")
|
|
239
719
|
}),
|
|
240
720
|
jsonClient({
|
|
241
721
|
id: "windsurf",
|
|
242
722
|
name: "Windsurf",
|
|
723
|
+
// Not installed on any machine this was checked on, so its parser was never read.
|
|
724
|
+
// Lenient-and-disclosed rather than a guess in either direction.
|
|
725
|
+
dialect: "unverified",
|
|
243
726
|
// Windsurf is stdio-only, so it reaches the hosted server via mcp-remote.
|
|
244
727
|
hostedStrategy: "mcp-remote",
|
|
245
728
|
configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json")
|
|
@@ -247,6 +730,9 @@ var init_clients = __esm({
|
|
|
247
730
|
jsonClient({
|
|
248
731
|
id: "claude-desktop",
|
|
249
732
|
name: "Claude Desktop",
|
|
733
|
+
// Genuinely strict: its reader is a bare `JSON.parse(readFileSync(...))` with no
|
|
734
|
+
// preprocessing and no retry, so a comment or trailing comma is fatal.
|
|
735
|
+
dialect: "json",
|
|
250
736
|
// Claude Desktop config files are stdio-only; use the mcp-remote bridge.
|
|
251
737
|
hostedStrategy: "mcp-remote",
|
|
252
738
|
configPath: claudeDesktopConfigPath,
|
|
@@ -257,6 +743,9 @@ var init_clients = __esm({
|
|
|
257
743
|
id: "vscode",
|
|
258
744
|
name: "Visual Studio Code",
|
|
259
745
|
serverProperty: "servers",
|
|
746
|
+
// The one client that genuinely reads JSONC — comments and trailing commas in
|
|
747
|
+
// mcp.json are supported and common.
|
|
748
|
+
dialect: "jsonc",
|
|
260
749
|
configPath: vsCodeConfigPath,
|
|
261
750
|
detectPaths: () => [path.dirname(vsCodeConfigPath())]
|
|
262
751
|
})
|
|
@@ -264,6 +753,33 @@ var init_clients = __esm({
|
|
|
264
753
|
}
|
|
265
754
|
});
|
|
266
755
|
|
|
756
|
+
// src/install/verify-key.ts
|
|
757
|
+
async function verifyApiKey(apiKey, timeoutMs = VERIFY_API_KEY_TIMEOUT_MS) {
|
|
758
|
+
try {
|
|
759
|
+
const base = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
|
|
760
|
+
const response = await fetch(`${base}/pods`, {
|
|
761
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
762
|
+
// Without this the wizard sits at "Verifying…" forever against a host
|
|
763
|
+
// that accepts the connection and goes quiet, with no way out but ^C.
|
|
764
|
+
// An abort lands in the catch below and is reported as "check failed",
|
|
765
|
+
// which is exactly what it is.
|
|
766
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
767
|
+
});
|
|
768
|
+
if (response.ok) return true;
|
|
769
|
+
if (response.status === 401 || response.status === 403) return false;
|
|
770
|
+
return null;
|
|
771
|
+
} catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
var VERIFY_API_KEY_TIMEOUT_MS;
|
|
776
|
+
var init_verify_key = __esm({
|
|
777
|
+
"src/install/verify-key.ts"() {
|
|
778
|
+
"use strict";
|
|
779
|
+
VERIFY_API_KEY_TIMEOUT_MS = 1e4;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
267
783
|
// src/install/wizard.ts
|
|
268
784
|
var wizard_exports = {};
|
|
269
785
|
__export(wizard_exports, {
|
|
@@ -281,25 +797,12 @@ function openBrowser(url) {
|
|
|
281
797
|
} catch {
|
|
282
798
|
}
|
|
283
799
|
}
|
|
284
|
-
async function verifyApiKey(apiKey) {
|
|
285
|
-
try {
|
|
286
|
-
const base = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
|
|
287
|
-
const response = await fetch(`${base}/pods`, {
|
|
288
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
289
|
-
});
|
|
290
|
-
if (response.ok) return true;
|
|
291
|
-
if (response.status === 401 || response.status === 403) return false;
|
|
292
|
-
return null;
|
|
293
|
-
} catch {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
800
|
function bail(message = "Cancelled.") {
|
|
298
801
|
p.cancel(message);
|
|
299
802
|
process.exit(0);
|
|
300
803
|
}
|
|
301
804
|
async function promptForApiKey() {
|
|
302
|
-
const fromEnv = process.env.RUNPOD_API_KEY;
|
|
805
|
+
const fromEnv = process.env.RUNPOD_API_KEY?.trim();
|
|
303
806
|
if (fromEnv) {
|
|
304
807
|
const useEnv = await p.confirm({
|
|
305
808
|
message: "Use the RUNPOD_API_KEY from your environment?"
|
|
@@ -394,7 +897,9 @@ async function runAdd() {
|
|
|
394
897
|
const result = await client.add(mode);
|
|
395
898
|
if (result.success) {
|
|
396
899
|
spinner2.stop(`${client.name} configured.`);
|
|
397
|
-
results.push(
|
|
900
|
+
results.push(
|
|
901
|
+
`\u2713 ${client.name} \u2014 ${result.message ? `${result.message}; ` : ""}${client.describeTarget()}`
|
|
902
|
+
);
|
|
398
903
|
} else {
|
|
399
904
|
spinner2.stop(`${client.name} failed.`);
|
|
400
905
|
results.push(`\u2717 ${client.name} \u2014 ${result.message ?? "unknown error"}`);
|
|
@@ -438,6 +943,7 @@ var init_wizard = __esm({
|
|
|
438
943
|
import_child_process2 = require("child_process");
|
|
439
944
|
p = __toESM(require("@clack/prompts"), 1);
|
|
440
945
|
init_clients();
|
|
946
|
+
init_verify_key();
|
|
441
947
|
API_KEYS_URL = "https://www.runpod.io/console/user/settings";
|
|
442
948
|
}
|
|
443
949
|
});
|
|
@@ -476,7 +982,7 @@ function containerConfigToV2(p2) {
|
|
|
476
982
|
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
477
983
|
const id = gpuTypeIds?.[0];
|
|
478
984
|
if (id === void 0) return void 0;
|
|
479
|
-
return
|
|
985
|
+
return { id, count: gpuCount ?? 1 };
|
|
480
986
|
}
|
|
481
987
|
function mapPodCreateToV2(params) {
|
|
482
988
|
return compact({
|
|
@@ -603,6 +1109,42 @@ function podBodyFromTemplate(template) {
|
|
|
603
1109
|
}
|
|
604
1110
|
return out;
|
|
605
1111
|
}
|
|
1112
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
1113
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
1114
|
+
function normalizeSshPublicKey(input) {
|
|
1115
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
1116
|
+
return {
|
|
1117
|
+
ok: false,
|
|
1118
|
+
error: 'sshPublicKey looks like a PRIVATE key. Pass the PUBLIC key instead \u2014 the contents of your ~/.ssh/<key>.pub file (e.g. "ssh-ed25519 AAAA\u2026").'
|
|
1119
|
+
};
|
|
1120
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1121
|
+
if (lines.length === 0)
|
|
1122
|
+
return {
|
|
1123
|
+
ok: false,
|
|
1124
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
1125
|
+
};
|
|
1126
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
1127
|
+
return {
|
|
1128
|
+
ok: false,
|
|
1129
|
+
error: 'sshPublicKey is not an SSH public key. Expected the CONTENTS of your ~/.ssh/<key>.pub file \u2014 a line like "ssh-ed25519 AAAAC3Nza\u2026 you@host" (ssh-*, ecdsa-*, or sk-* followed by the base64 key) \u2014 not a file path, a SHA256 fingerprint, or a bare base64 blob. Newline-separate multiple keys.'
|
|
1130
|
+
};
|
|
1131
|
+
return { ok: true, key: lines.join("\n") };
|
|
1132
|
+
}
|
|
1133
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
1134
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
1135
|
+
const hasSshPort = ports.some((p2) => /^\s*22\/tcp\s*$/i.test(String(p2)));
|
|
1136
|
+
const env = { ...body.env ?? {} };
|
|
1137
|
+
const key = sshPublicKey.trim();
|
|
1138
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
1139
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
1140
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
1141
|
+
${key}`;
|
|
1142
|
+
return {
|
|
1143
|
+
...body,
|
|
1144
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
1145
|
+
env
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
606
1148
|
function mapTemplateUpdateToV2(params) {
|
|
607
1149
|
return {
|
|
608
1150
|
...containerConfigToV2(params),
|
|
@@ -868,7 +1410,7 @@ function resolveBackend(opts) {
|
|
|
868
1410
|
|
|
869
1411
|
// src/server.ts
|
|
870
1412
|
var SERVER_NAME = "Runpod API Server";
|
|
871
|
-
var SERVER_VERSION = true ? "3.
|
|
1413
|
+
var SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
872
1414
|
var SERVER_INSTRUCTIONS = `These tools cover common Runpod operations, but they are curated projections: the underlying APIs expose more fields and parameters than the tools surface.
|
|
873
1415
|
|
|
874
1416
|
When you need data or capability beyond these tools, consult the full API contracts directly. The REST v2 API lives at https://api.runpod.io/v2 \u2014 the same host these tools call \u2014 and is machine-readable at https://api.runpod.io/v2/openapi.json (fetch and search it to enumerate every path, parameter, and response schema). REST v1 at https://rest.runpod.io/v1 is deprecated; prefer v2. The REST version these tools are actually configured for is reported in the serverInfo.version string of the initialize response, e.g. "[RUNPOD_REST_VERSION=v2]". The GraphQL API at https://api.runpod.io/graphql may expose data the REST API does not yet cover; its schema is documented at https://graphql-spec.runpod.io/, and introspection is disabled in production, so use that reference rather than __schema queries. Both APIs take Bearer auth with a Runpod API key: use the key this client is already configured with \u2014 do not search the environment or filesystem for a credential.`;
|
|
@@ -1043,6 +1585,28 @@ var HttpError = class extends Error {
|
|
|
1043
1585
|
this.body = body;
|
|
1044
1586
|
}
|
|
1045
1587
|
};
|
|
1588
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
1589
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
1590
|
+
var RequestTimeoutError = class extends Error {
|
|
1591
|
+
constructor(prefix, timeoutMs, method) {
|
|
1592
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
1593
|
+
super(
|
|
1594
|
+
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
1595
|
+
);
|
|
1596
|
+
this.name = "RequestTimeoutError";
|
|
1597
|
+
this.timeoutMs = timeoutMs;
|
|
1598
|
+
this.method = method;
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
1602
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
1603
|
+
try {
|
|
1604
|
+
return await run(signal);
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
1607
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1046
1610
|
function isJsonContentType(contentType) {
|
|
1047
1611
|
if (!contentType) return false;
|
|
1048
1612
|
const ct = contentType.toLowerCase();
|
|
@@ -1058,28 +1622,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
1058
1622
|
...tracking()
|
|
1059
1623
|
};
|
|
1060
1624
|
}
|
|
1625
|
+
function resolveCeiling(ceiling) {
|
|
1626
|
+
if (ceiling === void 0) return Infinity;
|
|
1627
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
1628
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
1629
|
+
}
|
|
1630
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1631
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
1632
|
+
}
|
|
1061
1633
|
function createHttpClient(deps) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1634
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1635
|
+
return async function request(url, method = "GET", body, options) {
|
|
1636
|
+
const timeoutMs = clampTimeout(
|
|
1637
|
+
options?.timeoutMs,
|
|
1638
|
+
deps.maxTimeoutMs,
|
|
1639
|
+
defaultTimeoutMs
|
|
1640
|
+
);
|
|
1641
|
+
return withRequestTimeout(
|
|
1642
|
+
deps.errorPrefix,
|
|
1643
|
+
timeoutMs,
|
|
1064
1644
|
method,
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
response.
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1645
|
+
async (signal) => {
|
|
1646
|
+
const init = {
|
|
1647
|
+
method,
|
|
1648
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
1649
|
+
signal
|
|
1650
|
+
};
|
|
1651
|
+
if (body && methodSendsBody(method)) {
|
|
1652
|
+
init.body = JSON.stringify(body);
|
|
1653
|
+
}
|
|
1654
|
+
const response = await deps.fetch(url, init);
|
|
1655
|
+
if (!response.ok) {
|
|
1656
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
1657
|
+
response.headers.get("ratelimit"),
|
|
1658
|
+
response.headers.get("retry-after")
|
|
1659
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
1660
|
+
const text = await response.text().catch(
|
|
1661
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
1662
|
+
);
|
|
1663
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
1664
|
+
}
|
|
1665
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
1666
|
+
}
|
|
1667
|
+
);
|
|
1083
1668
|
};
|
|
1084
1669
|
}
|
|
1085
1670
|
|
|
@@ -1206,7 +1791,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
1206
1791
|
}
|
|
1207
1792
|
|
|
1208
1793
|
// src/tools/runtime.ts
|
|
1209
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
1794
|
+
var MCP_SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
1210
1795
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
1211
1796
|
function trackingHeaders(server, ctx) {
|
|
1212
1797
|
const info = server.server.getClientVersion();
|
|
@@ -1221,6 +1806,11 @@ function trackingHeaders(server, ctx) {
|
|
|
1221
1806
|
sessionId: SESSION_ID
|
|
1222
1807
|
});
|
|
1223
1808
|
}
|
|
1809
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
1810
|
+
function invocationBudget(totalMs) {
|
|
1811
|
+
const startedAt = Date.now();
|
|
1812
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
1813
|
+
}
|
|
1224
1814
|
var defaultFetch = import_node_fetch.default;
|
|
1225
1815
|
async function withApiErrorLog(label, call) {
|
|
1226
1816
|
try {
|
|
@@ -1244,6 +1834,23 @@ var DESTRUCTIVE = {
|
|
|
1244
1834
|
openWorldHint: true
|
|
1245
1835
|
};
|
|
1246
1836
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
1837
|
+
return withRequestTimeout(
|
|
1838
|
+
"Runpod GraphQL Error",
|
|
1839
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
1840
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
1841
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
1842
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
1843
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
1844
|
+
// actually says whether anything could have been written.
|
|
1845
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
1846
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
function isGraphqlMutation(query) {
|
|
1850
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
1851
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
1852
|
+
}
|
|
1853
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
1247
1854
|
const response = await fetchImpl(url, {
|
|
1248
1855
|
method: "POST",
|
|
1249
1856
|
headers: {
|
|
@@ -1254,7 +1861,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
1254
1861
|
body: JSON.stringify({
|
|
1255
1862
|
query,
|
|
1256
1863
|
...options?.variables ? { variables: options.variables } : {}
|
|
1257
|
-
})
|
|
1864
|
+
}),
|
|
1865
|
+
signal
|
|
1258
1866
|
});
|
|
1259
1867
|
if (!response.ok) {
|
|
1260
1868
|
const bodyText = await response.text();
|
|
@@ -1310,39 +1918,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
1310
1918
|
}
|
|
1311
1919
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
1312
1920
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
1921
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
1922
|
+
const timeouts = {
|
|
1923
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
1924
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
1925
|
+
};
|
|
1313
1926
|
const v1Client = createHttpClient({
|
|
1314
1927
|
apiKey: ctx.apiKey,
|
|
1315
1928
|
fetch: httpFetch,
|
|
1316
1929
|
tracking,
|
|
1317
|
-
errorPrefix: "Runpod API Error"
|
|
1930
|
+
errorPrefix: "Runpod API Error",
|
|
1931
|
+
...timeouts
|
|
1318
1932
|
});
|
|
1319
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
1933
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
1320
1934
|
"Error calling Runpod API:",
|
|
1321
|
-
() => v1Client(
|
|
1935
|
+
() => v1Client(
|
|
1936
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
1937
|
+
method,
|
|
1938
|
+
body,
|
|
1939
|
+
options
|
|
1940
|
+
)
|
|
1322
1941
|
);
|
|
1323
1942
|
const serverlessClient = createHttpClient({
|
|
1324
1943
|
apiKey: ctx.apiKey,
|
|
1325
1944
|
fetch: httpFetch,
|
|
1326
1945
|
tracking,
|
|
1327
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
1946
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
1947
|
+
...timeouts
|
|
1328
1948
|
});
|
|
1329
|
-
const serverlessRequest = (endpointId, path2, method = "GET", body) => withApiErrorLog(
|
|
1949
|
+
const serverlessRequest = (endpointId, path2, method = "GET", body, options) => withApiErrorLog(
|
|
1330
1950
|
"Error calling Runpod Serverless API:",
|
|
1331
1951
|
() => serverlessClient(
|
|
1332
1952
|
`${serverlessBase(process.env)}/${endpointId}${path2}`,
|
|
1333
1953
|
method,
|
|
1334
|
-
body
|
|
1954
|
+
body,
|
|
1955
|
+
options
|
|
1335
1956
|
)
|
|
1336
1957
|
);
|
|
1337
1958
|
const restClient = createHttpClient({
|
|
1338
1959
|
apiKey: ctx.apiKey,
|
|
1339
1960
|
fetch: httpFetch,
|
|
1340
1961
|
tracking,
|
|
1341
|
-
errorPrefix: "Runpod API Error"
|
|
1962
|
+
errorPrefix: "Runpod API Error",
|
|
1963
|
+
...timeouts
|
|
1342
1964
|
});
|
|
1343
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
1965
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
1344
1966
|
"Error calling Runpod API:",
|
|
1345
|
-
() => restClient(url, method, body)
|
|
1967
|
+
() => restClient(url, method, body, options)
|
|
1346
1968
|
);
|
|
1347
1969
|
const sseFetch = observeUnauthorized(
|
|
1348
1970
|
deps.sseFetch ?? import_node_fetch.default
|
|
@@ -1375,12 +1997,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
1375
1997
|
};
|
|
1376
1998
|
return {
|
|
1377
1999
|
jsonReply,
|
|
2000
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
2001
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
1378
2002
|
graphql: (query) => graphqlRequest(
|
|
1379
2003
|
query,
|
|
1380
2004
|
tracking,
|
|
1381
2005
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
1382
2006
|
rawFetch,
|
|
1383
|
-
publicGraphqlBase(process.env)
|
|
2007
|
+
publicGraphqlBase(process.env),
|
|
2008
|
+
{
|
|
2009
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
2010
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
2011
|
+
}
|
|
1384
2012
|
),
|
|
1385
2013
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
1386
2014
|
query,
|
|
@@ -1388,7 +2016,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
1388
2016
|
httpFetch,
|
|
1389
2017
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
1390
2018
|
authedGraphqlBase(process.env),
|
|
1391
|
-
{
|
|
2019
|
+
{
|
|
2020
|
+
variables,
|
|
2021
|
+
apiKey: ctx.apiKey,
|
|
2022
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
2023
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
2024
|
+
}
|
|
1392
2025
|
),
|
|
1393
2026
|
runpodRequest,
|
|
1394
2027
|
serverlessRequest,
|
|
@@ -1396,7 +2029,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
1396
2029
|
backendFor,
|
|
1397
2030
|
streamSse,
|
|
1398
2031
|
podAction,
|
|
1399
|
-
env: process.env
|
|
2032
|
+
env: process.env,
|
|
2033
|
+
transport: ctx.transport
|
|
1400
2034
|
};
|
|
1401
2035
|
}
|
|
1402
2036
|
|
|
@@ -1465,6 +2099,7 @@ function capListResult(result, options, extra) {
|
|
|
1465
2099
|
}
|
|
1466
2100
|
|
|
1467
2101
|
// src/tools/catalog.ts
|
|
2102
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
1468
2103
|
function registerCatalogTools(server, rt) {
|
|
1469
2104
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1470
2105
|
server.tool(
|
|
@@ -1483,6 +2118,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1483
2118
|
),
|
|
1484
2119
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1485
2120
|
"Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
|
|
2121
|
+
),
|
|
2122
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
2123
|
+
"Product context for the availability lookup \u2014 the same GPU can be scarce for Pods and plentiful for Serverless. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1486
2124
|
)
|
|
1487
2125
|
},
|
|
1488
2126
|
{ title: "List GPU types", ...READ_ONLY },
|
|
@@ -1490,8 +2128,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1490
2128
|
const backend = backendFor("gpus");
|
|
1491
2129
|
if (backend.version === "v2") {
|
|
1492
2130
|
const wantAvailability = params.includeAvailability !== false;
|
|
2131
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1493
2132
|
const raw = await callRestUrl(
|
|
1494
|
-
`${backend.base}${backend.list}${wantAvailability ?
|
|
2133
|
+
`${backend.base}${backend.list}${wantAvailability ? `?include=AVAILABILITY&product=${product}` : ""}`
|
|
1495
2134
|
);
|
|
1496
2135
|
let gpus = backend.unwrap(raw);
|
|
1497
2136
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1664,6 +2303,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1664
2303
|
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1665
2304
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1666
2305
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
2306
|
+
),
|
|
2307
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
2308
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1667
2309
|
)
|
|
1668
2310
|
},
|
|
1669
2311
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1676,7 +2318,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1676
2318
|
});
|
|
1677
2319
|
}
|
|
1678
2320
|
const path2 = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1679
|
-
const
|
|
2321
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
2322
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1680
2323
|
const result = await callRestUrl(`${backend.base}${path2}${query}`);
|
|
1681
2324
|
return jsonReply2(result);
|
|
1682
2325
|
}
|
|
@@ -2556,7 +3199,7 @@ function registerPodTools(server, rt) {
|
|
|
2556
3199
|
);
|
|
2557
3200
|
server.tool(
|
|
2558
3201
|
"create-pod",
|
|
2559
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
3202
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2560
3203
|
{
|
|
2561
3204
|
name: import_zod7.z.string().optional().describe(
|
|
2562
3205
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2570,12 +3213,18 @@ function registerPodTools(server, rt) {
|
|
|
2570
3213
|
),
|
|
2571
3214
|
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2572
3215
|
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2573
|
-
|
|
3216
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
3217
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
3218
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
3219
|
+
gpuCount: import_zod7.z.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2574
3220
|
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2575
3221
|
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2576
3222
|
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2577
3223
|
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2578
3224
|
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
3225
|
+
sshPublicKey: import_zod7.z.string().optional().describe(
|
|
3226
|
+
'SSH PUBLIC key(s) to authorize on the pod \u2014 the CONTENTS of your ~/.ssh/<key>.pub file, i.e. a literal "ssh-ed25519 AAAAC3Nza\u2026 you@host" line, NOT a path to the file, a fingerprint, or the private key; newline-separate multiple keys. Merges the key into the PUBLIC_KEY env var and ensures 22/tcp is exposed, enabling full SSH (SCP/SFTP/rsync) on any image that honors the PUBLIC_KEY convention \u2014 all runpod/* official images do. Extends template ports/env rather than replacing them.'
|
|
3227
|
+
),
|
|
2579
3228
|
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2580
3229
|
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
2581
3230
|
),
|
|
@@ -2584,6 +3233,15 @@ function registerPodTools(server, rt) {
|
|
|
2584
3233
|
{ title: "Create pod", ...WRITE },
|
|
2585
3234
|
async (params) => {
|
|
2586
3235
|
const backend = backendFor("pods");
|
|
3236
|
+
const { sshPublicKey, ...podParams } = params;
|
|
3237
|
+
let sshKey;
|
|
3238
|
+
if (sshPublicKey !== void 0) {
|
|
3239
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
3240
|
+
if (!validated.ok)
|
|
3241
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
3242
|
+
sshKey = validated.key;
|
|
3243
|
+
}
|
|
3244
|
+
const sshNote = "sshPublicKey applied: PUBLIC_KEY env set and 22/tcp exposed. Full SSH (incl. SCP/SFTP/rsync) also requires an image that installs PUBLIC_KEY into authorized_keys and starts sshd (all runpod/* official images do) and a machine with a public IP \u2014 read the pod\u2019s portMappings for port 22 once it is running.";
|
|
2587
3245
|
if (params.templateId && backend.version !== "v2") {
|
|
2588
3246
|
return jsonReply2({
|
|
2589
3247
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2633,13 +3291,15 @@ function registerPodTools(server, rt) {
|
|
|
2633
3291
|
});
|
|
2634
3292
|
}
|
|
2635
3293
|
try {
|
|
3294
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2636
3295
|
const result = await callRestUrl(
|
|
2637
3296
|
`${restV1Base(env)}/pods`,
|
|
2638
3297
|
"POST",
|
|
2639
|
-
|
|
3298
|
+
cpuBody
|
|
2640
3299
|
);
|
|
2641
3300
|
return jsonReply2({
|
|
2642
3301
|
...result,
|
|
3302
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2643
3303
|
_servedBy: "v1",
|
|
2644
3304
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2645
3305
|
});
|
|
@@ -2656,7 +3316,7 @@ function registerPodTools(server, rt) {
|
|
|
2656
3316
|
}
|
|
2657
3317
|
}
|
|
2658
3318
|
}
|
|
2659
|
-
let body = backend.mapCreate(
|
|
3319
|
+
let body = backend.mapCreate(podParams);
|
|
2660
3320
|
if (params.templateId) {
|
|
2661
3321
|
let template;
|
|
2662
3322
|
try {
|
|
@@ -2674,19 +3334,21 @@ function registerPodTools(server, rt) {
|
|
|
2674
3334
|
}
|
|
2675
3335
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2676
3336
|
}
|
|
3337
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2677
3338
|
try {
|
|
2678
3339
|
const result = await callRestUrl(
|
|
2679
3340
|
`${backend.base}${backend.list}`,
|
|
2680
3341
|
"POST",
|
|
2681
3342
|
body
|
|
2682
3343
|
);
|
|
3344
|
+
const extras = {};
|
|
3345
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2683
3346
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2684
|
-
|
|
2685
|
-
...result,
|
|
2686
|
-
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
2687
|
-
});
|
|
3347
|
+
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2688
3348
|
}
|
|
2689
|
-
return jsonReply2(
|
|
3349
|
+
return jsonReply2(
|
|
3350
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
3351
|
+
);
|
|
2690
3352
|
} catch (error) {
|
|
2691
3353
|
if (error instanceof HttpError) {
|
|
2692
3354
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -3147,8 +3809,108 @@ var import_zod9 = require("zod");
|
|
|
3147
3809
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
3148
3810
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
3149
3811
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
3812
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
3813
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
3814
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
3815
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
3816
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
3817
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
3818
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
3819
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
3820
|
+
"COMPLETED",
|
|
3821
|
+
"FAILED",
|
|
3822
|
+
"CANCELLED",
|
|
3823
|
+
"TIMED_OUT"
|
|
3824
|
+
]);
|
|
3825
|
+
function isUnfinishedJobReply(result) {
|
|
3826
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
3827
|
+
}
|
|
3828
|
+
function formatBudget(ms) {
|
|
3829
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
3830
|
+
}
|
|
3831
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
3832
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
3833
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
3834
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
3835
|
+
return Math.max(
|
|
3836
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
3837
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
3838
|
+
);
|
|
3839
|
+
}
|
|
3840
|
+
function sleep(ms) {
|
|
3841
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3842
|
+
}
|
|
3843
|
+
function streamPollPlan(hosted) {
|
|
3844
|
+
return hosted ? {
|
|
3845
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
3846
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
3847
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
3848
|
+
} : {
|
|
3849
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
3850
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3851
|
+
query: ""
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
var RESUME_ADVICE = "Call stream-job again to continue collecting output, get-job-status to check the job without streaming, or stream without a budget by calling the runtime API directly (GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} with a Bearer API key).";
|
|
3855
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
3856
|
+
return {
|
|
3857
|
+
pollingTimedOut: true,
|
|
3858
|
+
note: `Polling stopped after ${formatBudget(
|
|
3859
|
+
budgetMs
|
|
3860
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
3861
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
3862
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
3863
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
3864
|
+
// minutes of healthy streaming ago.
|
|
3865
|
+
...lastError ? { lastError } : {}
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
function errorCapNote(lastError) {
|
|
3869
|
+
return {
|
|
3870
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
3871
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
async function collectJobStream(deps) {
|
|
3875
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
3876
|
+
const startedAt = Date.now();
|
|
3877
|
+
const elapsed = () => Date.now() - startedAt;
|
|
3878
|
+
const chunks = [];
|
|
3879
|
+
let result = {};
|
|
3880
|
+
let consecutiveErrors = 0;
|
|
3881
|
+
let lastError;
|
|
3882
|
+
while (true) {
|
|
3883
|
+
try {
|
|
3884
|
+
const reply = await poll(
|
|
3885
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
3886
|
+
);
|
|
3887
|
+
consecutiveErrors = 0;
|
|
3888
|
+
lastError = void 0;
|
|
3889
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
3890
|
+
result = reply;
|
|
3891
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
3892
|
+
} catch (error) {
|
|
3893
|
+
consecutiveErrors++;
|
|
3894
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3895
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
3896
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
3897
|
+
break;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
if (elapsed() > budgetMs) {
|
|
3901
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
3902
|
+
break;
|
|
3903
|
+
}
|
|
3904
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
3905
|
+
}
|
|
3906
|
+
return { result, chunks };
|
|
3907
|
+
}
|
|
3150
3908
|
function registerJobTools(server, rt) {
|
|
3151
3909
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
3910
|
+
const hosted = rt.transport === "http";
|
|
3911
|
+
const streamBudget = formatBudget(
|
|
3912
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
3913
|
+
);
|
|
3152
3914
|
async function diagnoseQueuedJob(endpointId) {
|
|
3153
3915
|
const cached = diagnosisCache.get(endpointId);
|
|
3154
3916
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -3159,7 +3921,10 @@ function registerJobTools(server, rt) {
|
|
|
3159
3921
|
const backend = backendFor("workers");
|
|
3160
3922
|
if (backend.version !== "v2") return null;
|
|
3161
3923
|
const raw = await callRestUrl(
|
|
3162
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
3924
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
3925
|
+
"GET",
|
|
3926
|
+
void 0,
|
|
3927
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
3163
3928
|
);
|
|
3164
3929
|
const summary = raw?.summary;
|
|
3165
3930
|
const workers = raw?.workers ?? [];
|
|
@@ -3250,14 +4015,14 @@ function registerJobTools(server, rt) {
|
|
|
3250
4015
|
);
|
|
3251
4016
|
server.tool(
|
|
3252
4017
|
"runsync-endpoint",
|
|
3253
|
-
|
|
4018
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
3254
4019
|
{
|
|
3255
4020
|
endpointId: endpointIdSchema.describe(
|
|
3256
4021
|
"ID of the Serverless endpoint to run synchronously"
|
|
3257
4022
|
),
|
|
3258
4023
|
input: inputSchema,
|
|
3259
4024
|
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
3260
|
-
"How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
4025
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
3261
4026
|
),
|
|
3262
4027
|
webhook: webhookSchema,
|
|
3263
4028
|
policy: policySchema,
|
|
@@ -3266,13 +4031,36 @@ function registerJobTools(server, rt) {
|
|
|
3266
4031
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
3267
4032
|
async (params) => {
|
|
3268
4033
|
const { endpointId, wait, ...body } = params;
|
|
3269
|
-
const
|
|
4034
|
+
const effectiveWait = hosted ? Math.min(
|
|
4035
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
4036
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
4037
|
+
) : wait;
|
|
4038
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
4039
|
+
const path2 = `/runsync${waitQuery}`;
|
|
3270
4040
|
const result = await serverlessRequest(
|
|
3271
4041
|
endpointId,
|
|
3272
4042
|
path2,
|
|
3273
4043
|
"POST",
|
|
3274
|
-
body
|
|
4044
|
+
body,
|
|
4045
|
+
{
|
|
4046
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
4047
|
+
// 300s the caller may have asked for.
|
|
4048
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
4049
|
+
}
|
|
3275
4050
|
);
|
|
4051
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
4052
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
4053
|
+
return jsonReply2({
|
|
4054
|
+
...result,
|
|
4055
|
+
waitClamped: {
|
|
4056
|
+
requestedMs: requestedWait,
|
|
4057
|
+
effectiveMs: effectiveWait,
|
|
4058
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
4059
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
4060
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
4061
|
+
}
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
3276
4064
|
return jsonReply2(result);
|
|
3277
4065
|
}
|
|
3278
4066
|
);
|
|
@@ -3306,7 +4094,7 @@ function registerJobTools(server, rt) {
|
|
|
3306
4094
|
);
|
|
3307
4095
|
server.tool(
|
|
3308
4096
|
"stream-job",
|
|
3309
|
-
|
|
4097
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
3310
4098
|
{
|
|
3311
4099
|
endpointId: endpointIdSchema.describe(
|
|
3312
4100
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -3315,51 +4103,16 @@ function registerJobTools(server, rt) {
|
|
|
3315
4103
|
},
|
|
3316
4104
|
{ title: "Stream job", ...READ_ONLY },
|
|
3317
4105
|
async (params) => {
|
|
3318
|
-
const
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
let finalResult = {};
|
|
3329
|
-
let consecutiveErrors = 0;
|
|
3330
|
-
let lastError;
|
|
3331
|
-
const startTime = Date.now();
|
|
3332
|
-
while (true) {
|
|
3333
|
-
try {
|
|
3334
|
-
const result = await serverlessRequest(
|
|
3335
|
-
params.endpointId,
|
|
3336
|
-
`/stream/${params.jobId}`
|
|
3337
|
-
);
|
|
3338
|
-
consecutiveErrors = 0;
|
|
3339
|
-
if (Array.isArray(result.stream)) {
|
|
3340
|
-
allChunks.push(...result.stream);
|
|
3341
|
-
}
|
|
3342
|
-
finalResult = result;
|
|
3343
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
3344
|
-
break;
|
|
3345
|
-
}
|
|
3346
|
-
} catch (error) {
|
|
3347
|
-
consecutiveErrors++;
|
|
3348
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
3349
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
3350
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
3351
|
-
break;
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
3355
|
-
finalResult.pollingTimedOut = true;
|
|
3356
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
3357
|
-
if (lastError) finalResult.lastError = lastError;
|
|
3358
|
-
break;
|
|
3359
|
-
}
|
|
3360
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
3361
|
-
}
|
|
3362
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
4106
|
+
const plan = streamPollPlan(hosted);
|
|
4107
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
4108
|
+
const { result, chunks } = await collectJobStream({
|
|
4109
|
+
budgetMs: plan.budgetMs,
|
|
4110
|
+
holdMs: plan.holdMs,
|
|
4111
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
4112
|
+
timeoutMs
|
|
4113
|
+
})
|
|
4114
|
+
});
|
|
4115
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
3363
4116
|
}
|
|
3364
4117
|
);
|
|
3365
4118
|
server.tool(
|