@runpod/mcp-server 1.2.0 → 1.4.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/dist/stdio.cjs ADDED
@@ -0,0 +1,1639 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // src/install/clients.ts
34
+ function localServerConfig(apiKey, serverProperty) {
35
+ const config = {
36
+ command: "npx",
37
+ args: ["-y", NPM_PACKAGE],
38
+ env: { RUNPOD_API_KEY: apiKey }
39
+ };
40
+ return serverProperty === "servers" ? { type: "stdio", ...config } : config;
41
+ }
42
+ function hostedServerConfig(strategy, serverProperty, url) {
43
+ if (strategy === "mcp-remote") {
44
+ return { command: "npx", args: ["-y", "mcp-remote", url] };
45
+ }
46
+ return serverProperty === "servers" ? { type: "http", url } : { url };
47
+ }
48
+ function upsertJsonServer(configPath, serverProperty, value) {
49
+ try {
50
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
51
+ const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
52
+ const edits = jsonc.modify(content, [serverProperty, SERVER_NAME2], value, {
53
+ formattingOptions: { tabSize: 2, insertSpaces: true }
54
+ });
55
+ fs.writeFileSync(configPath, jsonc.applyEdits(content, edits), "utf8");
56
+ return { success: true };
57
+ } catch (error) {
58
+ return { success: false, message: errMessage(error) };
59
+ }
60
+ }
61
+ function removeJsonServer(configPath, serverProperty) {
62
+ try {
63
+ if (!fs.existsSync(configPath)) {
64
+ return { success: true, message: "nothing to remove" };
65
+ }
66
+ const content = fs.readFileSync(configPath, "utf8");
67
+ const edits = jsonc.modify(
68
+ content,
69
+ [serverProperty, SERVER_NAME2],
70
+ void 0,
71
+ { formattingOptions: { tabSize: 2, insertSpaces: true } }
72
+ );
73
+ fs.writeFileSync(configPath, jsonc.applyEdits(content, edits), "utf8");
74
+ return { success: true };
75
+ } catch (error) {
76
+ return { success: false, message: errMessage(error) };
77
+ }
78
+ }
79
+ function errMessage(error) {
80
+ return error instanceof Error ? error.message : String(error);
81
+ }
82
+ function exists(p2) {
83
+ try {
84
+ return fs.existsSync(p2);
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function jsonClient(opts) {
90
+ const serverProperty = opts.serverProperty ?? "mcpServers";
91
+ const hostedStrategy = opts.hostedStrategy ?? "http";
92
+ return {
93
+ id: opts.id,
94
+ name: opts.name,
95
+ detect: () => Promise.resolve(
96
+ (opts.detectPaths?.() ?? [path.dirname(opts.configPath())]).some(exists)
97
+ ),
98
+ add: (mode) => {
99
+ const value = mode.kind === "local" ? localServerConfig(mode.apiKey, serverProperty) : hostedServerConfig(hostedStrategy, serverProperty, mode.url);
100
+ return Promise.resolve(
101
+ upsertJsonServer(opts.configPath(), serverProperty, value)
102
+ );
103
+ },
104
+ remove: () => Promise.resolve(removeJsonServer(opts.configPath(), serverProperty)),
105
+ describeTarget: () => opts.configPath()
106
+ };
107
+ }
108
+ function findClaudeBinary() {
109
+ const candidates = [
110
+ path.join(os.homedir(), ".claude", "local", "claude"),
111
+ "/usr/local/bin/claude",
112
+ "/opt/homebrew/bin/claude"
113
+ ];
114
+ for (const candidate of candidates) {
115
+ if (exists(candidate)) return candidate;
116
+ }
117
+ try {
118
+ (0, import_child_process.execSync)("command -v claude", { stdio: "pipe" });
119
+ return "claude";
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ function runClaude(binary, args) {
125
+ try {
126
+ (0, import_child_process.execFileSync)(binary, args, { stdio: "pipe" });
127
+ return { success: true };
128
+ } catch (error) {
129
+ const message = errMessage(error);
130
+ if (message.includes("already exists")) {
131
+ return { success: true, message: "already configured" };
132
+ }
133
+ return { success: false, message };
134
+ }
135
+ }
136
+ function claudeDesktopConfigPath() {
137
+ if (process.platform === "win32") {
138
+ return path.join(appData, "Claude", "claude_desktop_config.json");
139
+ }
140
+ return path.join(
141
+ home,
142
+ "Library",
143
+ "Application Support",
144
+ "Claude",
145
+ "claude_desktop_config.json"
146
+ );
147
+ }
148
+ function vsCodeConfigPath() {
149
+ if (process.platform === "win32") {
150
+ return path.join(appData, "Code", "User", "mcp.json");
151
+ }
152
+ if (process.platform === "darwin") {
153
+ return path.join(
154
+ home,
155
+ "Library",
156
+ "Application Support",
157
+ "Code",
158
+ "User",
159
+ "mcp.json"
160
+ );
161
+ }
162
+ return path.join(home, ".config", "Code", "User", "mcp.json");
163
+ }
164
+ var fs, os, path, import_child_process, jsonc, SERVER_NAME2, NPM_PACKAGE, HOSTED_URL, claudeCodeClient, home, appData, CLIENTS;
165
+ var init_clients = __esm({
166
+ "src/install/clients.ts"() {
167
+ "use strict";
168
+ fs = __toESM(require("fs"), 1);
169
+ os = __toESM(require("os"), 1);
170
+ path = __toESM(require("path"), 1);
171
+ import_child_process = require("child_process");
172
+ jsonc = __toESM(require("jsonc-parser"), 1);
173
+ SERVER_NAME2 = "runpod";
174
+ NPM_PACKAGE = "@runpod/mcp-server@latest";
175
+ HOSTED_URL = process.env.RUNPOD_MCP_URL ?? "https://mcp.getrunpod.io/";
176
+ claudeCodeClient = {
177
+ id: "claude-code",
178
+ name: "Claude Code",
179
+ detect: () => Promise.resolve(findClaudeBinary() !== null),
180
+ add: (mode) => {
181
+ const binary = findClaudeBinary();
182
+ if (!binary) {
183
+ return Promise.resolve({
184
+ success: false,
185
+ message: "claude CLI not found"
186
+ });
187
+ }
188
+ const args = mode.kind === "local" ? [
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
+ };
231
+ home = os.homedir();
232
+ appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
233
+ CLIENTS = [
234
+ claudeCodeClient,
235
+ jsonClient({
236
+ id: "cursor",
237
+ name: "Cursor",
238
+ configPath: () => path.join(home, ".cursor", "mcp.json")
239
+ }),
240
+ jsonClient({
241
+ id: "windsurf",
242
+ name: "Windsurf",
243
+ // Windsurf is stdio-only, so it reaches the hosted server via mcp-remote.
244
+ hostedStrategy: "mcp-remote",
245
+ configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json")
246
+ }),
247
+ jsonClient({
248
+ id: "claude-desktop",
249
+ name: "Claude Desktop",
250
+ // Claude Desktop config files are stdio-only; use the mcp-remote bridge.
251
+ hostedStrategy: "mcp-remote",
252
+ configPath: claudeDesktopConfigPath,
253
+ // Detect by the application/support directory, which exists once the app runs.
254
+ detectPaths: () => [path.dirname(claudeDesktopConfigPath())]
255
+ }),
256
+ jsonClient({
257
+ id: "vscode",
258
+ name: "Visual Studio Code",
259
+ serverProperty: "servers",
260
+ configPath: vsCodeConfigPath,
261
+ detectPaths: () => [path.dirname(vsCodeConfigPath())]
262
+ })
263
+ ];
264
+ }
265
+ });
266
+
267
+ // src/install/wizard.ts
268
+ var wizard_exports = {};
269
+ __export(wizard_exports, {
270
+ runWizard: () => runWizard
271
+ });
272
+ function openBrowser(url) {
273
+ try {
274
+ if (process.platform === "darwin") {
275
+ (0, import_child_process2.execFileSync)("open", [url], { stdio: "ignore" });
276
+ } else if (process.platform === "win32") {
277
+ (0, import_child_process2.execFileSync)("cmd", ["/c", "start", "", url], { stdio: "ignore" });
278
+ } else {
279
+ (0, import_child_process2.execFileSync)("xdg-open", [url], { stdio: "ignore" });
280
+ }
281
+ } catch {
282
+ }
283
+ }
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
+ function bail(message = "Cancelled.") {
298
+ p.cancel(message);
299
+ process.exit(0);
300
+ }
301
+ async function promptForApiKey() {
302
+ const fromEnv = process.env.RUNPOD_API_KEY;
303
+ if (fromEnv) {
304
+ const useEnv = await p.confirm({
305
+ message: "Use the RUNPOD_API_KEY from your environment?"
306
+ });
307
+ if (p.isCancel(useEnv)) bail();
308
+ if (useEnv) return fromEnv;
309
+ }
310
+ const create = await p.confirm({
311
+ message: "Open the Runpod console to create or copy an API key?"
312
+ });
313
+ if (p.isCancel(create)) bail();
314
+ if (create) {
315
+ openBrowser(API_KEYS_URL);
316
+ p.log.info(`Opening ${API_KEYS_URL}`);
317
+ }
318
+ const apiKey = await p.password({
319
+ message: "Paste your Runpod API key",
320
+ validate: (value) => !value || value.trim().length === 0 ? "API key cannot be empty" : void 0
321
+ });
322
+ if (p.isCancel(apiKey)) bail();
323
+ return apiKey.trim();
324
+ }
325
+ async function selectClients(detected, verb) {
326
+ const options = CLIENTS.map((client) => ({
327
+ value: client.id,
328
+ label: client.name,
329
+ hint: detected.get(client.id) ? "detected" : void 0
330
+ }));
331
+ const selected = await p.multiselect({
332
+ message: `Which agents should we ${verb}?`,
333
+ options,
334
+ // Pre-select detected clients so the common case is a single Enter press.
335
+ initialValues: CLIENTS.filter((c) => detected.get(c.id)).map((c) => c.id),
336
+ required: true
337
+ });
338
+ if (p.isCancel(selected)) bail();
339
+ return CLIENTS.filter((c) => selected.includes(c.id));
340
+ }
341
+ async function detectAll() {
342
+ const entries = await Promise.all(
343
+ CLIENTS.map(async (c) => [c.id, await c.detect()])
344
+ );
345
+ return new Map(entries);
346
+ }
347
+ async function selectMode() {
348
+ const kind = await p.select({
349
+ message: "How do you want to connect to Runpod?",
350
+ options: [
351
+ {
352
+ value: "hosted",
353
+ label: "Sign in with Runpod (recommended)",
354
+ hint: "hosted, OAuth, no API key stored"
355
+ },
356
+ {
357
+ value: "local",
358
+ label: "Run locally with an API key",
359
+ hint: "runs on this machine via npx"
360
+ }
361
+ ],
362
+ initialValue: "hosted"
363
+ });
364
+ if (p.isCancel(kind)) bail();
365
+ if (kind === "hosted") {
366
+ return { kind: "hosted", url: HOSTED_URL };
367
+ }
368
+ const apiKey = await promptForApiKey();
369
+ const verifySpinner = p.spinner();
370
+ verifySpinner.start("Verifying API key");
371
+ const valid = await verifyApiKey(apiKey);
372
+ if (valid === true) {
373
+ verifySpinner.stop("API key verified.");
374
+ } else if (valid === false) {
375
+ verifySpinner.stop("API key rejected by Runpod.");
376
+ const proceed = await p.confirm({
377
+ message: "The key did not authenticate. Continue anyway?",
378
+ initialValue: false
379
+ });
380
+ if (p.isCancel(proceed) || !proceed) bail();
381
+ } else {
382
+ verifySpinner.stop("Could not verify the key (offline?). Continuing.");
383
+ }
384
+ return { kind: "local", apiKey };
385
+ }
386
+ async function runAdd() {
387
+ const detected = await detectAll();
388
+ const clients = await selectClients(detected, "set up");
389
+ const mode = await selectMode();
390
+ const results = [];
391
+ for (const client of clients) {
392
+ const spinner2 = p.spinner();
393
+ spinner2.start(`Configuring ${client.name}`);
394
+ const result = await client.add(mode);
395
+ if (result.success) {
396
+ spinner2.stop(`${client.name} configured.`);
397
+ results.push(`\u2713 ${client.name} \u2014 ${client.describeTarget()}`);
398
+ } else {
399
+ spinner2.stop(`${client.name} failed.`);
400
+ results.push(`\u2717 ${client.name} \u2014 ${result.message ?? "unknown error"}`);
401
+ }
402
+ }
403
+ p.note(results.join("\n"), "Results");
404
+ p.outro(
405
+ 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."
406
+ );
407
+ }
408
+ async function runRemove() {
409
+ const detected = await detectAll();
410
+ const clients = await selectClients(detected, "remove Runpod from");
411
+ const results = [];
412
+ for (const client of clients) {
413
+ const spinner2 = p.spinner();
414
+ spinner2.start(`Removing from ${client.name}`);
415
+ const result = await client.remove();
416
+ spinner2.stop(
417
+ result.success ? `${client.name} cleaned up.` : `${client.name} failed.`
418
+ );
419
+ results.push(
420
+ `${result.success ? "\u2713" : "\u2717"} ${client.name}${result.message ? ` \u2014 ${result.message}` : ""}`
421
+ );
422
+ }
423
+ p.note(results.join("\n"), "Results");
424
+ p.outro("Done.");
425
+ }
426
+ async function runWizard(subcommand2) {
427
+ p.intro("Runpod MCP server installer");
428
+ if (subcommand2 === "remove") {
429
+ await runRemove();
430
+ } else {
431
+ await runAdd();
432
+ }
433
+ }
434
+ var import_child_process2, p, API_KEYS_URL;
435
+ var init_wizard = __esm({
436
+ "src/install/wizard.ts"() {
437
+ "use strict";
438
+ import_child_process2 = require("child_process");
439
+ p = __toESM(require("@clack/prompts"), 1);
440
+ init_clients();
441
+ API_KEYS_URL = "https://www.runpod.io/console/user/settings";
442
+ }
443
+ });
444
+
445
+ // src/stdio.ts
446
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
447
+
448
+ // src/server.ts
449
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
450
+ var SERVER_NAME = "Runpod API Server";
451
+ var SERVER_VERSION = true ? "1.4.0" : "dev";
452
+ function createServer(version = SERVER_VERSION) {
453
+ return new import_mcp.McpServer({
454
+ name: SERVER_NAME,
455
+ version,
456
+ capabilities: {
457
+ resources: {},
458
+ tools: {}
459
+ }
460
+ });
461
+ }
462
+
463
+ // src/tools.ts
464
+ var import_zod2 = require("zod");
465
+ var import_node_fetch = __toESM(require("node-fetch"), 1);
466
+ var import_node_crypto = require("crypto");
467
+
468
+ // src/pagination.ts
469
+ var import_zod = require("zod");
470
+ var DEFAULT_LIST_LIMIT = 20;
471
+ var MAX_LIST_LIMIT = 100;
472
+ var listPaginationParams = {
473
+ limit: import_zod.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
474
+ `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
475
+ ),
476
+ cursor: import_zod.z.string().optional().describe(
477
+ "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
478
+ )
479
+ };
480
+ function decodeCursorOffset(cursor) {
481
+ if (!cursor) return 0;
482
+ try {
483
+ const decoded = Buffer.from(cursor, "base64").toString("utf8");
484
+ const offset = Number.parseInt(decoded, 10);
485
+ return Number.isFinite(offset) && offset >= 0 ? offset : 0;
486
+ } catch {
487
+ return 0;
488
+ }
489
+ }
490
+ function encodeCursorOffset(offset) {
491
+ return Buffer.from(String(offset), "utf8").toString("base64");
492
+ }
493
+ function capListResult(result, options) {
494
+ const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
495
+ const offset = decodeCursorOffset(options.cursor);
496
+ if (!Array.isArray(result)) {
497
+ return {
498
+ content: [
499
+ { type: "text", text: JSON.stringify(result, null, 2) }
500
+ ]
501
+ };
502
+ }
503
+ const total = result.length;
504
+ const page = result.slice(offset, offset + limit);
505
+ const nextOffset = offset + page.length;
506
+ const hasMore = nextOffset < total;
507
+ const envelope = {
508
+ items: page,
509
+ pagination: {
510
+ total,
511
+ returned: page.length,
512
+ offset,
513
+ truncated: hasMore,
514
+ nextCursor: hasMore ? encodeCursorOffset(nextOffset) : null,
515
+ note: hasMore ? `Showing ${page.length} of ${total}. Pass cursor=nextCursor to fetch more, or narrow the query with filters.` : void 0
516
+ }
517
+ };
518
+ return {
519
+ content: [
520
+ { type: "text", text: JSON.stringify(envelope, null, 2) }
521
+ ]
522
+ };
523
+ }
524
+
525
+ // src/tools.ts
526
+ var API_BASE_URL = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
527
+ var SERVERLESS_API_BASE_URL = process.env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
528
+ var GRAPHQL_URL = process.env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
529
+ var MCP_SERVER_VERSION = true ? "1.4.0" : "dev";
530
+ var SESSION_ID = (0, import_node_crypto.randomUUID)();
531
+ function sanitizeUaToken(value) {
532
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
533
+ }
534
+ function trackingHeaders(server, ctx) {
535
+ const info = server.server.getClientVersion();
536
+ const fallbackName = ctx.clientUserAgent || "unknown";
537
+ const name = sanitizeUaToken(info?.name || fallbackName);
538
+ const version = sanitizeUaToken(info?.version || "unknown");
539
+ const serverVersion = ctx.serverVersion || MCP_SERVER_VERSION;
540
+ const userAgent = `runpod-mcp-server/${serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${ctx.transport})`;
541
+ return {
542
+ "User-Agent": userAgent,
543
+ "X-Runpod-Session-Id": SESSION_ID
544
+ };
545
+ }
546
+ async function graphqlRequest(query, tracking) {
547
+ const response = await (0, import_node_fetch.default)(GRAPHQL_URL, {
548
+ method: "POST",
549
+ headers: {
550
+ "Content-Type": "application/json",
551
+ ...tracking()
552
+ },
553
+ body: JSON.stringify({ query })
554
+ });
555
+ const result = await response.json();
556
+ if (result.errors && result.errors.length > 0) {
557
+ throw new Error(
558
+ `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
559
+ );
560
+ }
561
+ if (!result.data) {
562
+ throw new Error("No data returned from GraphQL query");
563
+ }
564
+ return result.data;
565
+ }
566
+ function createRunpodRequest(apiKey, tracking) {
567
+ return async function runpodRequest(endpoint, method = "GET", body) {
568
+ const url = `${API_BASE_URL}${endpoint}`;
569
+ const headers = {
570
+ Authorization: `Bearer ${apiKey}`,
571
+ "Content-Type": "application/json",
572
+ ...tracking()
573
+ };
574
+ const options = {
575
+ method,
576
+ headers
577
+ };
578
+ if (body && (method === "POST" || method === "PATCH")) {
579
+ options.body = JSON.stringify(body);
580
+ }
581
+ try {
582
+ const response = await (0, import_node_fetch.default)(url, options);
583
+ if (!response.ok) {
584
+ const errorText = await response.text();
585
+ throw new Error(`Runpod API Error: ${response.status} - ${errorText}`);
586
+ }
587
+ const contentType = response.headers.get("content-type");
588
+ if (contentType && contentType.includes("application/json")) {
589
+ return await response.json();
590
+ }
591
+ return { success: true, status: response.status };
592
+ } catch (error) {
593
+ console.error("Error calling Runpod API:", error);
594
+ throw error;
595
+ }
596
+ };
597
+ }
598
+ function createServerlessRequest(apiKey, tracking) {
599
+ return async function serverlessRequest(endpointId, path2, method = "GET", body) {
600
+ const url = `${SERVERLESS_API_BASE_URL}/${endpointId}${path2}`;
601
+ const headers = {
602
+ Authorization: `Bearer ${apiKey}`,
603
+ "Content-Type": "application/json",
604
+ ...tracking()
605
+ };
606
+ const options = {
607
+ method,
608
+ headers
609
+ };
610
+ if (body && (method === "POST" || method === "PATCH")) {
611
+ options.body = JSON.stringify(body);
612
+ }
613
+ try {
614
+ const response = await (0, import_node_fetch.default)(url, options);
615
+ if (!response.ok) {
616
+ const errorText = await response.text();
617
+ throw new Error(
618
+ `Runpod Serverless API Error: ${response.status} - ${errorText}`
619
+ );
620
+ }
621
+ const contentType = response.headers.get("content-type");
622
+ if (contentType && contentType.includes("application/json")) {
623
+ return await response.json();
624
+ }
625
+ return { success: true, status: response.status };
626
+ } catch (error) {
627
+ console.error("Error calling Runpod Serverless API:", error);
628
+ throw error;
629
+ }
630
+ };
631
+ }
632
+ function registerTools(server, ctx) {
633
+ const tracking = () => trackingHeaders(server, ctx);
634
+ const runpodRequest = createRunpodRequest(ctx.apiKey, tracking);
635
+ const serverlessRequest = createServerlessRequest(ctx.apiKey, tracking);
636
+ server.tool(
637
+ "list-gpu-types",
638
+ {
639
+ minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
640
+ secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
641
+ communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
642
+ searchTerm: import_zod2.z.string().optional().describe(
643
+ "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
644
+ ),
645
+ includeUnavailable: import_zod2.z.boolean().optional().describe(
646
+ "Include GPUs that are currently out of stock. Default is false"
647
+ )
648
+ },
649
+ async (params) => {
650
+ const data = await graphqlRequest(
651
+ `
652
+ query {
653
+ gpuTypes {
654
+ id
655
+ displayName
656
+ memoryInGb
657
+ secureCloud
658
+ communityCloud
659
+ lowestPrice(input: { gpuCount: 1 }) {
660
+ stockStatus
661
+ }
662
+ }
663
+ }
664
+ `,
665
+ tracking
666
+ );
667
+ const stockPriority = {
668
+ High: 3,
669
+ Medium: 2,
670
+ Low: 1
671
+ };
672
+ const isAvailable = (gpu) => {
673
+ const status = gpu.lowestPrice?.stockStatus;
674
+ return !!status && status !== "Out";
675
+ };
676
+ let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
677
+ if (!params.includeUnavailable) {
678
+ gpuTypes = gpuTypes.filter(isAvailable);
679
+ }
680
+ if (params.minMemoryGb) {
681
+ gpuTypes = gpuTypes.filter(
682
+ (gpu) => gpu.memoryInGb >= params.minMemoryGb
683
+ );
684
+ }
685
+ if (params.secureCloudOnly) {
686
+ gpuTypes = gpuTypes.filter((gpu) => gpu.secureCloud);
687
+ }
688
+ if (params.communityCloudOnly) {
689
+ gpuTypes = gpuTypes.filter((gpu) => gpu.communityCloud);
690
+ }
691
+ if (params.searchTerm) {
692
+ const term = params.searchTerm.toLowerCase();
693
+ gpuTypes = gpuTypes.filter(
694
+ (gpu) => gpu.id.toLowerCase().includes(term) || gpu.displayName.toLowerCase().includes(term)
695
+ );
696
+ }
697
+ gpuTypes.sort((a, b) => {
698
+ const aP = stockPriority[a.lowestPrice?.stockStatus || ""] || 0;
699
+ const bP = stockPriority[b.lowestPrice?.stockStatus || ""] || 0;
700
+ if (bP !== aP) return bP - aP;
701
+ return b.memoryInGb - a.memoryInGb;
702
+ });
703
+ const result = gpuTypes.map((gpu) => ({
704
+ id: gpu.id,
705
+ displayName: gpu.displayName,
706
+ memoryGb: gpu.memoryInGb,
707
+ secureCloud: gpu.secureCloud,
708
+ communityCloud: gpu.communityCloud,
709
+ stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
710
+ available: isAvailable(gpu)
711
+ }));
712
+ return {
713
+ content: [
714
+ {
715
+ type: "text",
716
+ text: JSON.stringify(result, null, 2)
717
+ }
718
+ ]
719
+ };
720
+ }
721
+ );
722
+ server.tool(
723
+ "list-data-centers",
724
+ {
725
+ region: import_zod2.z.string().optional().describe(
726
+ "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
727
+ )
728
+ },
729
+ async (params) => {
730
+ const data = await graphqlRequest(
731
+ `
732
+ query {
733
+ dataCenters {
734
+ id
735
+ name
736
+ location
737
+ }
738
+ }
739
+ `,
740
+ tracking
741
+ );
742
+ let dataCenters = data.dataCenters;
743
+ if (params.region) {
744
+ const term = params.region.toLowerCase();
745
+ dataCenters = dataCenters.filter(
746
+ (dc) => dc.location.toLowerCase().includes(term)
747
+ );
748
+ }
749
+ return {
750
+ content: [
751
+ {
752
+ type: "text",
753
+ text: JSON.stringify(dataCenters, null, 2)
754
+ }
755
+ ]
756
+ };
757
+ }
758
+ );
759
+ server.tool(
760
+ "list-pods",
761
+ {
762
+ ...listPaginationParams,
763
+ computeType: import_zod2.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
764
+ gpuTypeId: import_zod2.z.array(import_zod2.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
765
+ dataCenterId: import_zod2.z.array(import_zod2.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
766
+ name: import_zod2.z.string().optional().describe("Filter to Pods with the provided name"),
767
+ includeMachine: import_zod2.z.boolean().optional().describe("Include information about the machine"),
768
+ includeNetworkVolume: import_zod2.z.boolean().optional().describe("Include information about attached network volumes")
769
+ },
770
+ async (params) => {
771
+ const queryParams = new URLSearchParams();
772
+ if (params.computeType)
773
+ queryParams.append("computeType", params.computeType);
774
+ if (params.gpuTypeId)
775
+ params.gpuTypeId.forEach(
776
+ (type) => queryParams.append("gpuTypeId", type)
777
+ );
778
+ if (params.dataCenterId)
779
+ params.dataCenterId.forEach(
780
+ (dc) => queryParams.append("dataCenterId", dc)
781
+ );
782
+ if (params.name) queryParams.append("name", params.name);
783
+ if (params.includeMachine)
784
+ queryParams.append("includeMachine", params.includeMachine.toString());
785
+ if (params.includeNetworkVolume)
786
+ queryParams.append(
787
+ "includeNetworkVolume",
788
+ params.includeNetworkVolume.toString()
789
+ );
790
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
791
+ const result = await runpodRequest(`/pods${queryString}`);
792
+ return capListResult(result, {
793
+ limit: params.limit,
794
+ cursor: params.cursor
795
+ });
796
+ }
797
+ );
798
+ server.tool(
799
+ "get-pod",
800
+ {
801
+ podId: import_zod2.z.string().describe("ID of the pod to retrieve"),
802
+ includeMachine: import_zod2.z.boolean().optional().describe("Include information about the machine"),
803
+ includeNetworkVolume: import_zod2.z.boolean().optional().describe("Include information about attached network volumes")
804
+ },
805
+ async (params) => {
806
+ const queryParams = new URLSearchParams();
807
+ if (params.includeMachine)
808
+ queryParams.append("includeMachine", params.includeMachine.toString());
809
+ if (params.includeNetworkVolume)
810
+ queryParams.append(
811
+ "includeNetworkVolume",
812
+ params.includeNetworkVolume.toString()
813
+ );
814
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
815
+ const result = await runpodRequest(`/pods/${params.podId}${queryString}`);
816
+ return {
817
+ content: [
818
+ {
819
+ type: "text",
820
+ text: JSON.stringify(result, null, 2)
821
+ }
822
+ ]
823
+ };
824
+ }
825
+ );
826
+ server.tool(
827
+ "create-pod",
828
+ 'Create a new GPU/CPU pod on Runpod. If the user does not specify an image, 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.',
829
+ {
830
+ name: import_zod2.z.string().optional().describe("Name for the pod"),
831
+ imageName: import_zod2.z.string().describe("Docker image to use"),
832
+ cloudType: import_zod2.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
833
+ gpuTypeIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of acceptable GPU types"),
834
+ gpuCount: import_zod2.z.number().optional().describe("Number of GPUs"),
835
+ containerDiskInGb: import_zod2.z.number().optional().describe("Container disk size in GB"),
836
+ volumeInGb: import_zod2.z.number().optional().describe("Volume size in GB"),
837
+ volumeMountPath: import_zod2.z.string().optional().describe("Path to mount the volume"),
838
+ ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
839
+ env: import_zod2.z.record(import_zod2.z.string()).optional().describe("Environment variables"),
840
+ dataCenterIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of data centers")
841
+ },
842
+ async (params) => {
843
+ const result = await runpodRequest("/pods", "POST", params);
844
+ return {
845
+ content: [
846
+ {
847
+ type: "text",
848
+ text: JSON.stringify(result, null, 2)
849
+ }
850
+ ]
851
+ };
852
+ }
853
+ );
854
+ server.tool(
855
+ "update-pod",
856
+ {
857
+ podId: import_zod2.z.string().describe("ID of the pod to update"),
858
+ name: import_zod2.z.string().optional().describe("New name for the pod"),
859
+ imageName: import_zod2.z.string().optional().describe("New Docker image"),
860
+ containerDiskInGb: import_zod2.z.number().optional().describe("New container disk size in GB"),
861
+ volumeInGb: import_zod2.z.number().optional().describe("New volume size in GB"),
862
+ volumeMountPath: import_zod2.z.string().optional().describe("New path to mount the volume"),
863
+ ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("New ports to expose"),
864
+ env: import_zod2.z.record(import_zod2.z.string()).optional().describe("New environment variables")
865
+ },
866
+ async (params) => {
867
+ const { podId, ...updateParams } = params;
868
+ const result = await runpodRequest(
869
+ `/pods/${podId}`,
870
+ "PATCH",
871
+ updateParams
872
+ );
873
+ return {
874
+ content: [
875
+ {
876
+ type: "text",
877
+ text: JSON.stringify(result, null, 2)
878
+ }
879
+ ]
880
+ };
881
+ }
882
+ );
883
+ server.tool(
884
+ "start-pod",
885
+ {
886
+ podId: import_zod2.z.string().describe("ID of the pod to start")
887
+ },
888
+ async (params) => {
889
+ const result = await runpodRequest(`/pods/${params.podId}/start`, "POST");
890
+ return {
891
+ content: [
892
+ {
893
+ type: "text",
894
+ text: JSON.stringify(result, null, 2)
895
+ }
896
+ ]
897
+ };
898
+ }
899
+ );
900
+ server.tool(
901
+ "stop-pod",
902
+ {
903
+ podId: import_zod2.z.string().describe("ID of the pod to stop")
904
+ },
905
+ async (params) => {
906
+ const result = await runpodRequest(`/pods/${params.podId}/stop`, "POST");
907
+ return {
908
+ content: [
909
+ {
910
+ type: "text",
911
+ text: JSON.stringify(result, null, 2)
912
+ }
913
+ ]
914
+ };
915
+ }
916
+ );
917
+ server.tool(
918
+ "delete-pod",
919
+ {
920
+ podId: import_zod2.z.string().describe("ID of the pod to delete")
921
+ },
922
+ async (params) => {
923
+ const result = await runpodRequest(`/pods/${params.podId}`, "DELETE");
924
+ return {
925
+ content: [
926
+ {
927
+ type: "text",
928
+ text: JSON.stringify(result, null, 2)
929
+ }
930
+ ]
931
+ };
932
+ }
933
+ );
934
+ server.tool(
935
+ "list-endpoints",
936
+ {
937
+ ...listPaginationParams,
938
+ includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
939
+ includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
940
+ },
941
+ async (params) => {
942
+ const queryParams = new URLSearchParams();
943
+ if (params.includeTemplate)
944
+ queryParams.append(
945
+ "includeTemplate",
946
+ params.includeTemplate.toString()
947
+ );
948
+ if (params.includeWorkers)
949
+ queryParams.append("includeWorkers", params.includeWorkers.toString());
950
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
951
+ const result = await runpodRequest(`/endpoints${queryString}`);
952
+ return capListResult(result, {
953
+ limit: params.limit,
954
+ cursor: params.cursor
955
+ });
956
+ }
957
+ );
958
+ server.tool(
959
+ "get-endpoint",
960
+ {
961
+ endpointId: import_zod2.z.string().describe("ID of the endpoint to retrieve"),
962
+ includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
963
+ includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
964
+ },
965
+ async (params) => {
966
+ const queryParams = new URLSearchParams();
967
+ if (params.includeTemplate)
968
+ queryParams.append(
969
+ "includeTemplate",
970
+ params.includeTemplate.toString()
971
+ );
972
+ if (params.includeWorkers)
973
+ queryParams.append("includeWorkers", params.includeWorkers.toString());
974
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
975
+ const result = await runpodRequest(
976
+ `/endpoints/${params.endpointId}${queryString}`
977
+ );
978
+ return {
979
+ content: [
980
+ {
981
+ type: "text",
982
+ text: JSON.stringify(result, null, 2)
983
+ }
984
+ ]
985
+ };
986
+ }
987
+ );
988
+ server.tool(
989
+ "create-endpoint",
990
+ {
991
+ name: import_zod2.z.string().optional().describe("Name for the endpoint"),
992
+ templateId: import_zod2.z.string().describe("Template ID to use"),
993
+ computeType: import_zod2.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint"),
994
+ gpuTypeIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of acceptable GPU types"),
995
+ gpuCount: import_zod2.z.number().optional().describe("Number of GPUs per worker"),
996
+ workersMin: import_zod2.z.number().optional().describe("Minimum number of workers"),
997
+ workersMax: import_zod2.z.number().optional().describe("Maximum number of workers"),
998
+ dataCenterIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of data centers")
999
+ },
1000
+ async (params) => {
1001
+ const result = await runpodRequest("/endpoints", "POST", params);
1002
+ return {
1003
+ content: [
1004
+ {
1005
+ type: "text",
1006
+ text: JSON.stringify(result, null, 2)
1007
+ }
1008
+ ]
1009
+ };
1010
+ }
1011
+ );
1012
+ server.tool(
1013
+ "update-endpoint",
1014
+ {
1015
+ endpointId: import_zod2.z.string().describe("ID of the endpoint to update"),
1016
+ name: import_zod2.z.string().optional().describe("New name for the endpoint"),
1017
+ workersMin: import_zod2.z.number().optional().describe("New minimum number of workers"),
1018
+ workersMax: import_zod2.z.number().optional().describe("New maximum number of workers"),
1019
+ idleTimeout: import_zod2.z.number().optional().describe("New idle timeout in seconds"),
1020
+ scalerType: import_zod2.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1021
+ scalerValue: import_zod2.z.number().optional().describe("Scaler value")
1022
+ },
1023
+ async (params) => {
1024
+ const { endpointId, ...updateParams } = params;
1025
+ const result = await runpodRequest(
1026
+ `/endpoints/${endpointId}`,
1027
+ "PATCH",
1028
+ updateParams
1029
+ );
1030
+ return {
1031
+ content: [
1032
+ {
1033
+ type: "text",
1034
+ text: JSON.stringify(result, null, 2)
1035
+ }
1036
+ ]
1037
+ };
1038
+ }
1039
+ );
1040
+ server.tool(
1041
+ "delete-endpoint",
1042
+ {
1043
+ endpointId: import_zod2.z.string().describe("ID of the endpoint to delete")
1044
+ },
1045
+ async (params) => {
1046
+ const result = await runpodRequest(
1047
+ `/endpoints/${params.endpointId}`,
1048
+ "DELETE"
1049
+ );
1050
+ return {
1051
+ content: [
1052
+ {
1053
+ type: "text",
1054
+ text: JSON.stringify(result, null, 2)
1055
+ }
1056
+ ]
1057
+ };
1058
+ }
1059
+ );
1060
+ const endpointIdSchema = import_zod2.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1061
+ const jobIdSchema = import_zod2.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1062
+ const inputSchema = import_zod2.z.record(import_zod2.z.unknown()).describe(
1063
+ "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
1064
+ );
1065
+ const webhookSchema = import_zod2.z.string().url().optional().describe(
1066
+ "Webhook URL to receive job completion notifications instead of polling"
1067
+ );
1068
+ const policySchema = import_zod2.z.object({
1069
+ executionTimeout: import_zod2.z.number().optional().describe("Maximum execution time in milliseconds"),
1070
+ lowPriority: import_zod2.z.boolean().optional().describe("Submit as a low-priority job"),
1071
+ ttl: import_zod2.z.number().optional().describe("Time-to-live for the job result in milliseconds")
1072
+ }).optional().describe("Execution policy options");
1073
+ const s3ConfigSchema = import_zod2.z.object({
1074
+ accessId: import_zod2.z.string().describe("S3 access key ID"),
1075
+ accessSecret: import_zod2.z.string().describe("S3 secret access key"),
1076
+ bucketName: import_zod2.z.string().describe("S3 bucket name"),
1077
+ endpointUrl: import_zod2.z.string().describe("S3 endpoint URL")
1078
+ }).optional().describe("S3-compatible storage config for large outputs");
1079
+ server.tool(
1080
+ "run-endpoint",
1081
+ "Submit an asynchronous job to a Serverless endpoint. Returns a job ID immediately \u2014 use get-job-status to poll for results. Async results are available for 30 minutes after completion.",
1082
+ {
1083
+ endpointId: endpointIdSchema.describe(
1084
+ "ID of the Serverless endpoint to run"
1085
+ ),
1086
+ input: inputSchema,
1087
+ webhook: webhookSchema,
1088
+ policy: policySchema,
1089
+ s3Config: s3ConfigSchema
1090
+ },
1091
+ async (params) => {
1092
+ const { endpointId, ...body } = params;
1093
+ const result = await serverlessRequest(
1094
+ endpointId,
1095
+ "/run",
1096
+ "POST",
1097
+ body
1098
+ );
1099
+ return {
1100
+ content: [
1101
+ {
1102
+ type: "text",
1103
+ text: JSON.stringify(result, null, 2)
1104
+ }
1105
+ ]
1106
+ };
1107
+ }
1108
+ );
1109
+ server.tool(
1110
+ "runsync-endpoint",
1111
+ "Submit a synchronous job to a Serverless endpoint and wait for the result. Best for tasks completing within 90 seconds. If processing exceeds 90 seconds, the response returns a job ID to poll with get-job-status. Max payload: 20 MB. Results expire after 1 minute. Use the wait parameter to extend the server-side wait up to 5 minutes (300000 ms).",
1112
+ {
1113
+ endpointId: endpointIdSchema.describe(
1114
+ "ID of the Serverless endpoint to run synchronously"
1115
+ ),
1116
+ input: inputSchema,
1117
+ wait: import_zod2.z.number().min(1e3).max(3e5).optional().describe(
1118
+ "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)."
1119
+ ),
1120
+ webhook: webhookSchema,
1121
+ policy: policySchema,
1122
+ s3Config: s3ConfigSchema
1123
+ },
1124
+ async (params) => {
1125
+ const { endpointId, wait, ...body } = params;
1126
+ const path2 = wait ? `/runsync?wait=${wait}` : "/runsync";
1127
+ const result = await serverlessRequest(
1128
+ endpointId,
1129
+ path2,
1130
+ "POST",
1131
+ body
1132
+ );
1133
+ return {
1134
+ content: [
1135
+ {
1136
+ type: "text",
1137
+ text: JSON.stringify(result, null, 2)
1138
+ }
1139
+ ]
1140
+ };
1141
+ }
1142
+ );
1143
+ server.tool(
1144
+ "get-job-status",
1145
+ "Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT.",
1146
+ {
1147
+ endpointId: endpointIdSchema.describe(
1148
+ "ID of the Serverless endpoint the job belongs to"
1149
+ ),
1150
+ jobId: jobIdSchema.describe("ID of the job to check")
1151
+ },
1152
+ async (params) => {
1153
+ const result = await serverlessRequest(
1154
+ params.endpointId,
1155
+ `/status/${params.jobId}`
1156
+ );
1157
+ return {
1158
+ content: [
1159
+ {
1160
+ type: "text",
1161
+ text: JSON.stringify(result, null, 2)
1162
+ }
1163
+ ]
1164
+ };
1165
+ }
1166
+ );
1167
+ server.tool(
1168
+ "stream-job",
1169
+ "Retrieve all streaming output from a Serverless job by polling until the job reaches a terminal state. The worker must support streaming output. Polls /stream/{jobId} repeatedly and collects every chunk until status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT.",
1170
+ {
1171
+ endpointId: endpointIdSchema.describe(
1172
+ "ID of the Serverless endpoint the job belongs to"
1173
+ ),
1174
+ jobId: jobIdSchema.describe("ID of the job to stream results from")
1175
+ },
1176
+ async (params) => {
1177
+ const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1178
+ "COMPLETED",
1179
+ "FAILED",
1180
+ "CANCELLED",
1181
+ "TIMED_OUT"
1182
+ ]);
1183
+ const MAX_POLL_TIME_MS = 5 * 60 * 1e3;
1184
+ const POLL_INTERVAL_MS = 1e3;
1185
+ const MAX_CONSECUTIVE_ERRORS = 5;
1186
+ const allChunks = [];
1187
+ let finalResult = {};
1188
+ let consecutiveErrors = 0;
1189
+ let lastError;
1190
+ const startTime = Date.now();
1191
+ while (true) {
1192
+ try {
1193
+ const result = await serverlessRequest(
1194
+ params.endpointId,
1195
+ `/stream/${params.jobId}`
1196
+ );
1197
+ consecutiveErrors = 0;
1198
+ if (Array.isArray(result.stream)) {
1199
+ allChunks.push(...result.stream);
1200
+ }
1201
+ finalResult = result;
1202
+ if (TERMINAL_STATUSES.has(result.status)) {
1203
+ break;
1204
+ }
1205
+ } catch (error) {
1206
+ consecutiveErrors++;
1207
+ lastError = error instanceof Error ? error.message : String(error);
1208
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1209
+ finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
1210
+ break;
1211
+ }
1212
+ }
1213
+ if (Date.now() - startTime > MAX_POLL_TIME_MS) {
1214
+ finalResult.pollingTimedOut = true;
1215
+ finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
1216
+ if (lastError) finalResult.lastError = lastError;
1217
+ break;
1218
+ }
1219
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1220
+ }
1221
+ return {
1222
+ content: [
1223
+ {
1224
+ type: "text",
1225
+ text: JSON.stringify(
1226
+ { ...finalResult, stream: allChunks },
1227
+ null,
1228
+ 2
1229
+ )
1230
+ }
1231
+ ]
1232
+ };
1233
+ }
1234
+ );
1235
+ server.tool(
1236
+ "cancel-job",
1237
+ "Cancel a Serverless job that is queued or in progress.",
1238
+ {
1239
+ endpointId: endpointIdSchema.describe(
1240
+ "ID of the Serverless endpoint the job belongs to"
1241
+ ),
1242
+ jobId: jobIdSchema.describe("ID of the job to cancel")
1243
+ },
1244
+ async (params) => {
1245
+ const result = await serverlessRequest(
1246
+ params.endpointId,
1247
+ `/cancel/${params.jobId}`,
1248
+ "POST"
1249
+ );
1250
+ return {
1251
+ content: [
1252
+ {
1253
+ type: "text",
1254
+ text: JSON.stringify(result, null, 2)
1255
+ }
1256
+ ]
1257
+ };
1258
+ }
1259
+ );
1260
+ server.tool(
1261
+ "retry-job",
1262
+ "Retry a failed or timed-out Serverless job. Only works for jobs with FAILED or TIMED_OUT status. The previous output is removed and the job is requeued.",
1263
+ {
1264
+ endpointId: endpointIdSchema.describe(
1265
+ "ID of the Serverless endpoint the job belongs to"
1266
+ ),
1267
+ jobId: jobIdSchema.describe("ID of the job to retry")
1268
+ },
1269
+ async (params) => {
1270
+ const result = await serverlessRequest(
1271
+ params.endpointId,
1272
+ `/retry/${params.jobId}`,
1273
+ "POST"
1274
+ );
1275
+ return {
1276
+ content: [
1277
+ {
1278
+ type: "text",
1279
+ text: JSON.stringify(result, null, 2)
1280
+ }
1281
+ ]
1282
+ };
1283
+ }
1284
+ );
1285
+ server.tool(
1286
+ "endpoint-health",
1287
+ "Get the health and operational status of a Serverless endpoint, including worker counts and job statistics.",
1288
+ {
1289
+ endpointId: endpointIdSchema.describe(
1290
+ "ID of the Serverless endpoint to check health for"
1291
+ )
1292
+ },
1293
+ async (params) => {
1294
+ const result = await serverlessRequest(params.endpointId, "/health");
1295
+ return {
1296
+ content: [
1297
+ {
1298
+ type: "text",
1299
+ text: JSON.stringify(result, null, 2)
1300
+ }
1301
+ ]
1302
+ };
1303
+ }
1304
+ );
1305
+ server.tool(
1306
+ "purge-endpoint-queue",
1307
+ "Remove all pending jobs from a Serverless endpoint queue. Only affects queued jobs \u2014 in-progress jobs continue running. Use this for error recovery or clearing outdated requests.",
1308
+ {
1309
+ endpointId: endpointIdSchema.describe(
1310
+ "ID of the Serverless endpoint to purge the queue for"
1311
+ )
1312
+ },
1313
+ async (params) => {
1314
+ const result = await serverlessRequest(
1315
+ params.endpointId,
1316
+ "/purge-queue",
1317
+ "POST"
1318
+ );
1319
+ return {
1320
+ content: [
1321
+ {
1322
+ type: "text",
1323
+ text: JSON.stringify(result, null, 2)
1324
+ }
1325
+ ]
1326
+ };
1327
+ }
1328
+ );
1329
+ server.tool(
1330
+ "list-templates",
1331
+ `List available templates. By default returns only the user's own templates. Use includeRunpodTemplates to also include official Runpod templates. The recommended default template for new pods is "Runpod Pytorch 2.8.0" (ID: runpod-torch-v280) \u2014 it has the latest CUDA and PyTorch versions.`,
1332
+ {
1333
+ ...listPaginationParams,
1334
+ includeRunpodTemplates: import_zod2.z.boolean().optional().describe("Include official Runpod templates in the response"),
1335
+ includePublicTemplates: import_zod2.z.boolean().optional().describe("Include community-made public templates in the response"),
1336
+ includeEndpointBoundTemplates: import_zod2.z.boolean().optional().describe(
1337
+ "Include templates bound to Serverless endpoints in the response"
1338
+ )
1339
+ },
1340
+ async (params) => {
1341
+ const queryParams = new URLSearchParams();
1342
+ if (params.includeRunpodTemplates)
1343
+ queryParams.set("includeRunpodTemplates", "true");
1344
+ if (params.includePublicTemplates)
1345
+ queryParams.set("includePublicTemplates", "true");
1346
+ if (params.includeEndpointBoundTemplates)
1347
+ queryParams.set("includeEndpointBoundTemplates", "true");
1348
+ const query = queryParams.toString();
1349
+ const result = await runpodRequest(
1350
+ `/templates${query ? `?${query}` : ""}`
1351
+ );
1352
+ return capListResult(result, {
1353
+ limit: params.limit,
1354
+ cursor: params.cursor
1355
+ });
1356
+ }
1357
+ );
1358
+ server.tool(
1359
+ "get-template",
1360
+ {
1361
+ templateId: import_zod2.z.string().describe("ID of the template to retrieve")
1362
+ },
1363
+ async (params) => {
1364
+ const result = await runpodRequest(`/templates/${params.templateId}`);
1365
+ return {
1366
+ content: [
1367
+ {
1368
+ type: "text",
1369
+ text: JSON.stringify(result, null, 2)
1370
+ }
1371
+ ]
1372
+ };
1373
+ }
1374
+ );
1375
+ server.tool(
1376
+ "create-template",
1377
+ {
1378
+ name: import_zod2.z.string().describe("Name for the template"),
1379
+ imageName: import_zod2.z.string().describe("Docker image to use"),
1380
+ isServerless: import_zod2.z.boolean().optional().describe("Is this a serverless template"),
1381
+ ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("Ports to expose"),
1382
+ dockerEntrypoint: import_zod2.z.array(import_zod2.z.string()).optional().describe("Docker entrypoint commands"),
1383
+ dockerStartCmd: import_zod2.z.array(import_zod2.z.string()).optional().describe("Docker start commands"),
1384
+ env: import_zod2.z.record(import_zod2.z.string()).optional().describe("Environment variables"),
1385
+ containerDiskInGb: import_zod2.z.number().optional().describe("Container disk size in GB"),
1386
+ volumeInGb: import_zod2.z.number().optional().describe("Volume size in GB"),
1387
+ volumeMountPath: import_zod2.z.string().optional().describe("Path to mount the volume"),
1388
+ readme: import_zod2.z.string().optional().describe("README content in markdown format"),
1389
+ containerRegistryAuthId: import_zod2.z.string().optional().describe(
1390
+ "ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Required when imageName points at a private registry; without it serverless workers fail the image pull."
1391
+ )
1392
+ },
1393
+ async (params) => {
1394
+ const result = await runpodRequest("/templates", "POST", params);
1395
+ return {
1396
+ content: [
1397
+ {
1398
+ type: "text",
1399
+ text: JSON.stringify(result, null, 2)
1400
+ }
1401
+ ]
1402
+ };
1403
+ }
1404
+ );
1405
+ server.tool(
1406
+ "update-template",
1407
+ {
1408
+ templateId: import_zod2.z.string().describe("ID of the template to update"),
1409
+ name: import_zod2.z.string().optional().describe("New name for the template"),
1410
+ imageName: import_zod2.z.string().optional().describe("New Docker image"),
1411
+ ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("New ports to expose"),
1412
+ env: import_zod2.z.record(import_zod2.z.string()).optional().describe("New environment variables"),
1413
+ readme: import_zod2.z.string().optional().describe("New README content in markdown format"),
1414
+ containerRegistryAuthId: import_zod2.z.string().optional().describe(
1415
+ "ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Set this to attach a credential to a template referencing a private registry."
1416
+ )
1417
+ },
1418
+ async (params) => {
1419
+ const { templateId, ...updateParams } = params;
1420
+ const result = await runpodRequest(
1421
+ `/templates/${templateId}`,
1422
+ "PATCH",
1423
+ updateParams
1424
+ );
1425
+ return {
1426
+ content: [
1427
+ {
1428
+ type: "text",
1429
+ text: JSON.stringify(result, null, 2)
1430
+ }
1431
+ ]
1432
+ };
1433
+ }
1434
+ );
1435
+ server.tool(
1436
+ "delete-template",
1437
+ {
1438
+ templateId: import_zod2.z.string().describe("ID of the template to delete")
1439
+ },
1440
+ async (params) => {
1441
+ const result = await runpodRequest(
1442
+ `/templates/${params.templateId}`,
1443
+ "DELETE"
1444
+ );
1445
+ return {
1446
+ content: [
1447
+ {
1448
+ type: "text",
1449
+ text: JSON.stringify(result, null, 2)
1450
+ }
1451
+ ]
1452
+ };
1453
+ }
1454
+ );
1455
+ server.tool("list-network-volumes", listPaginationParams, async (params) => {
1456
+ const result = await runpodRequest("/networkvolumes");
1457
+ return capListResult(result, {
1458
+ limit: params.limit,
1459
+ cursor: params.cursor
1460
+ });
1461
+ });
1462
+ server.tool(
1463
+ "get-network-volume",
1464
+ {
1465
+ networkVolumeId: import_zod2.z.string().describe("ID of the network volume to retrieve")
1466
+ },
1467
+ async (params) => {
1468
+ const result = await runpodRequest(
1469
+ `/networkvolumes/${params.networkVolumeId}`
1470
+ );
1471
+ return {
1472
+ content: [
1473
+ {
1474
+ type: "text",
1475
+ text: JSON.stringify(result, null, 2)
1476
+ }
1477
+ ]
1478
+ };
1479
+ }
1480
+ );
1481
+ server.tool(
1482
+ "create-network-volume",
1483
+ {
1484
+ name: import_zod2.z.string().describe("Name for the network volume"),
1485
+ size: import_zod2.z.number().describe("Size in GB (1-4000)"),
1486
+ dataCenterId: import_zod2.z.string().describe("Data center ID")
1487
+ },
1488
+ async (params) => {
1489
+ const result = await runpodRequest("/networkvolumes", "POST", params);
1490
+ return {
1491
+ content: [
1492
+ {
1493
+ type: "text",
1494
+ text: JSON.stringify(result, null, 2)
1495
+ }
1496
+ ]
1497
+ };
1498
+ }
1499
+ );
1500
+ server.tool(
1501
+ "update-network-volume",
1502
+ {
1503
+ networkVolumeId: import_zod2.z.string().describe("ID of the network volume to update"),
1504
+ name: import_zod2.z.string().optional().describe("New name for the network volume"),
1505
+ size: import_zod2.z.number().optional().describe("New size in GB (must be larger than current)")
1506
+ },
1507
+ async (params) => {
1508
+ const { networkVolumeId, ...updateParams } = params;
1509
+ const result = await runpodRequest(
1510
+ `/networkvolumes/${networkVolumeId}`,
1511
+ "PATCH",
1512
+ updateParams
1513
+ );
1514
+ return {
1515
+ content: [
1516
+ {
1517
+ type: "text",
1518
+ text: JSON.stringify(result, null, 2)
1519
+ }
1520
+ ]
1521
+ };
1522
+ }
1523
+ );
1524
+ server.tool(
1525
+ "delete-network-volume",
1526
+ {
1527
+ networkVolumeId: import_zod2.z.string().describe("ID of the network volume to delete")
1528
+ },
1529
+ async (params) => {
1530
+ const result = await runpodRequest(
1531
+ `/networkvolumes/${params.networkVolumeId}`,
1532
+ "DELETE"
1533
+ );
1534
+ return {
1535
+ content: [
1536
+ {
1537
+ type: "text",
1538
+ text: JSON.stringify(result, null, 2)
1539
+ }
1540
+ ]
1541
+ };
1542
+ }
1543
+ );
1544
+ server.tool(
1545
+ "list-container-registry-auths",
1546
+ listPaginationParams,
1547
+ async (params) => {
1548
+ const result = await runpodRequest("/containerregistryauth");
1549
+ return capListResult(result, {
1550
+ limit: params.limit,
1551
+ cursor: params.cursor
1552
+ });
1553
+ }
1554
+ );
1555
+ server.tool(
1556
+ "get-container-registry-auth",
1557
+ {
1558
+ containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to retrieve")
1559
+ },
1560
+ async (params) => {
1561
+ const result = await runpodRequest(
1562
+ `/containerregistryauth/${params.containerRegistryAuthId}`
1563
+ );
1564
+ return {
1565
+ content: [
1566
+ {
1567
+ type: "text",
1568
+ text: JSON.stringify(result, null, 2)
1569
+ }
1570
+ ]
1571
+ };
1572
+ }
1573
+ );
1574
+ server.tool(
1575
+ "create-container-registry-auth",
1576
+ {
1577
+ name: import_zod2.z.string().describe("Name for the container registry auth"),
1578
+ username: import_zod2.z.string().describe("Registry username"),
1579
+ password: import_zod2.z.string().describe("Registry password")
1580
+ },
1581
+ async (params) => {
1582
+ const result = await runpodRequest(
1583
+ "/containerregistryauth",
1584
+ "POST",
1585
+ params
1586
+ );
1587
+ return {
1588
+ content: [
1589
+ {
1590
+ type: "text",
1591
+ text: JSON.stringify(result, null, 2)
1592
+ }
1593
+ ]
1594
+ };
1595
+ }
1596
+ );
1597
+ server.tool(
1598
+ "delete-container-registry-auth",
1599
+ {
1600
+ containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to delete")
1601
+ },
1602
+ async (params) => {
1603
+ const result = await runpodRequest(
1604
+ `/containerregistryauth/${params.containerRegistryAuthId}`,
1605
+ "DELETE"
1606
+ );
1607
+ return {
1608
+ content: [
1609
+ {
1610
+ type: "text",
1611
+ text: JSON.stringify(result, null, 2)
1612
+ }
1613
+ ]
1614
+ };
1615
+ }
1616
+ );
1617
+ }
1618
+
1619
+ // src/stdio.ts
1620
+ var WIZARD_SUBCOMMANDS = ["add", "remove"];
1621
+ var subcommand = process.argv[2];
1622
+ var isWizardMode = WIZARD_SUBCOMMANDS.includes(subcommand);
1623
+ if (isWizardMode) {
1624
+ Promise.resolve().then(() => (init_wizard(), wizard_exports)).then(({ runWizard: runWizard2 }) => runWizard2(subcommand)).catch((error) => {
1625
+ console.error(error instanceof Error ? error.message : String(error));
1626
+ process.exit(1);
1627
+ });
1628
+ } else {
1629
+ const API_KEY = process.env.RUNPOD_API_KEY;
1630
+ if (!API_KEY) {
1631
+ console.error("RUNPOD_API_KEY environment variable is required");
1632
+ process.exit(1);
1633
+ }
1634
+ const server = createServer();
1635
+ registerTools(server, { apiKey: API_KEY, transport: "stdio" });
1636
+ const transport = new import_stdio.StdioServerTransport();
1637
+ server.connect(transport);
1638
+ }
1639
+ //# sourceMappingURL=stdio.cjs.map