@runpod/mcp-server 1.4.0 → 3.0.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/tools.cjs CHANGED
@@ -33,19 +33,685 @@ __export(tools_exports, {
33
33
  registerTools: () => registerTools
34
34
  });
35
35
  module.exports = __toCommonJS(tools_exports);
36
- var import_zod2 = require("zod");
36
+
37
+ // src/tools/runtime.ts
37
38
  var import_node_fetch = __toESM(require("node-fetch"), 1);
38
39
  var import_node_crypto = require("crypto");
39
40
 
40
- // src/pagination.ts
41
+ // src/_shared/http.ts
42
+ var HttpError = class extends Error {
43
+ constructor(prefix, status, body) {
44
+ const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
45
+ super(`${prefix}: ${status} - ${body}${hint}`);
46
+ this.name = "HttpError";
47
+ this.status = status;
48
+ this.body = body;
49
+ }
50
+ };
51
+ function isJsonContentType(contentType) {
52
+ if (!contentType) return false;
53
+ const ct = contentType.toLowerCase();
54
+ return ct.includes("application/json") || ct.includes("+json");
55
+ }
56
+ function methodSendsBody(method) {
57
+ return method === "POST" || method === "PATCH" || method === "PUT";
58
+ }
59
+ function buildRequestHeaders(apiKey, tracking) {
60
+ return {
61
+ Authorization: `Bearer ${apiKey}`,
62
+ "Content-Type": "application/json",
63
+ ...tracking()
64
+ };
65
+ }
66
+ function createHttpClient(deps) {
67
+ return async function request(url, method = "GET", body) {
68
+ const init = {
69
+ method,
70
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
71
+ };
72
+ if (body && methodSendsBody(method)) {
73
+ init.body = JSON.stringify(body);
74
+ }
75
+ const response = await deps.fetch(url, init);
76
+ if (!response.ok) {
77
+ throw new HttpError(
78
+ deps.errorPrefix,
79
+ response.status,
80
+ await response.text()
81
+ );
82
+ }
83
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
84
+ };
85
+ }
86
+
87
+ // src/_shared/tracking.ts
88
+ function sanitizeUaToken(value) {
89
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
90
+ }
91
+ function buildTrackingHeaders(input) {
92
+ const name = sanitizeUaToken(input.clientName || "unknown");
93
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
94
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
95
+ return {
96
+ "User-Agent": userAgent,
97
+ "X-Runpod-Session-Id": input.sessionId
98
+ };
99
+ }
100
+
101
+ // src/tools/logs.ts
41
102
  var import_zod = require("zod");
103
+ function parseLogSse(raw) {
104
+ const items = [];
105
+ for (const block of raw.split(/\r?\n\r?\n/)) {
106
+ const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
107
+ if (!dataLines.length) continue;
108
+ const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
109
+ if (!payload.trim()) continue;
110
+ try {
111
+ const parsed = JSON.parse(payload);
112
+ items.push(
113
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
114
+ );
115
+ } catch {
116
+ items.push({ raw: payload });
117
+ }
118
+ }
119
+ return items;
120
+ }
121
+ var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
122
+ var LOG_STREAM_MAX_BYTES = 256 * 1024;
123
+ async function readSseSnapshot(fetchImpl, url, headers, opts) {
124
+ const controller = new AbortController();
125
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
126
+ const chunks = [];
127
+ let bytes = 0;
128
+ let truncated = false;
129
+ try {
130
+ const res = await fetchImpl(url, {
131
+ method: "GET",
132
+ headers,
133
+ signal: controller.signal
134
+ });
135
+ if (!res.ok) {
136
+ throw new HttpError(
137
+ "Runpod API Error",
138
+ res.status,
139
+ await res.text().catch(() => "")
140
+ );
141
+ }
142
+ for await (const chunk of res.body ?? []) {
143
+ chunks.push(chunk);
144
+ bytes += chunk.length;
145
+ if (bytes >= opts.maxBytes) {
146
+ truncated = true;
147
+ controller.abort();
148
+ break;
149
+ }
150
+ }
151
+ } catch (err) {
152
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
153
+ } finally {
154
+ clearTimeout(timer);
155
+ }
156
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
157
+ }
158
+ var logStreamParams = {
159
+ source: import_zod.z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
160
+ tail: import_zod.z.number().int().min(0).max(5e3).optional().describe(
161
+ "Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
162
+ ),
163
+ since: import_zod.z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
164
+ maxWaitMs: import_zod.z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
165
+ };
166
+ async function collectLogSnapshot(streamSse, logsUrl, params) {
167
+ const qs = new URLSearchParams();
168
+ if (params.source && params.source !== "both")
169
+ qs.append("source", params.source);
170
+ if (params.tail !== void 0) qs.append("tail", String(params.tail));
171
+ if (params.since) qs.append("since", params.since);
172
+ const query = qs.toString() ? `?${qs}` : "";
173
+ const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
174
+ maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
175
+ maxBytes: LOG_STREAM_MAX_BYTES
176
+ });
177
+ const items = parseLogSse(raw);
178
+ if (truncated) items.pop();
179
+ return { items, count: items.length, truncated };
180
+ }
181
+ async function streamLogsReply(rt, tool, params) {
182
+ const backend = rt.backendFor(tool.resource);
183
+ if (backend.version === "v1") {
184
+ return rt.jsonReply({
185
+ error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
186
+ status: 501
187
+ });
188
+ }
189
+ try {
190
+ return rt.jsonReply(
191
+ await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
192
+ );
193
+ } catch (error) {
194
+ if (error instanceof HttpError) {
195
+ return rt.jsonReply({ error: error.message, status: error.status });
196
+ }
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ // src/_shared/mappers.ts
202
+ function compact(obj) {
203
+ const out = {};
204
+ for (const [k, v] of Object.entries(obj)) {
205
+ if (v !== void 0) out[k] = v;
206
+ }
207
+ return out;
208
+ }
209
+ function persistentMount(size, path) {
210
+ if (size === void 0 || path === void 0) return void 0;
211
+ return { persistent: { size, path } };
212
+ }
213
+ function joinStartCmd(cmd) {
214
+ return cmd && cmd.length ? cmd.join(" ") : void 0;
215
+ }
216
+ function containerConfigToV2(p) {
217
+ return compact({
218
+ image: p.imageName,
219
+ disk: p.containerDiskInGb,
220
+ ports: p.ports,
221
+ env: p.env,
222
+ mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
223
+ });
224
+ }
225
+ function gpuConfigToV2(gpuTypeIds, gpuCount) {
226
+ const id = gpuTypeIds?.[0];
227
+ if (id === void 0) return void 0;
228
+ return compact({ id, count: gpuCount });
229
+ }
230
+ function mapPodCreateToV2(params) {
231
+ return compact({
232
+ ...containerConfigToV2(params),
233
+ name: params.name,
234
+ // Private-image registry credential (v2 ContainerConfig `registry`). When
235
+ // deploying from a template, this spreads over the template's registry, so
236
+ // passing it overrides the inherited credential. An empty string is an
237
+ // explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
238
+ // template's inherited credential. Undefined is dropped by compact, so an
239
+ // omitted param leaves the template's registry in place.
240
+ registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
241
+ cloud: params.cloudType,
242
+ // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
243
+ // centers; omit/empty = scheduler chooses). Pass the v1 list straight
244
+ // through — the earlier singular `dataCenter` key did not exist on the pod
245
+ // schema, so placement was being silently dropped. GPU, by contrast, IS
246
+ // singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
247
+ // create-pod handler warns when more than one gpuTypeId was supplied.
248
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
249
+ gpu: gpuConfigToV2(params.gpuTypeIds, params.gpuCount)
250
+ });
251
+ }
252
+ function mapPodUpdateToV2(params) {
253
+ return {
254
+ ...containerConfigToV2(params),
255
+ ...compact({ name: params.name })
256
+ };
257
+ }
258
+ function nestedOrUndefined(obj) {
259
+ const c = compact(obj);
260
+ return Object.keys(c).length ? c : void 0;
261
+ }
262
+ function endpointGpuConfig(pools, count) {
263
+ if (!pools?.length) return void 0;
264
+ return compact({ pools, count });
265
+ }
266
+ function mapEndpointToV2(params) {
267
+ return compact({
268
+ name: params.name,
269
+ image: params.imageName,
270
+ args: params.args,
271
+ disk: params.containerDiskInGb,
272
+ ports: params.ports,
273
+ env: params.env,
274
+ registry: params.containerRegistryAuthId,
275
+ gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
276
+ workers: nestedOrUndefined({
277
+ min: params.workersMin,
278
+ max: params.workersMax
279
+ }),
280
+ scaling: nestedOrUndefined({
281
+ type: params.scalerType,
282
+ value: params.scalerValue,
283
+ idleTimeout: params.idleTimeout
284
+ }),
285
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
286
+ networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
287
+ timeout: params.executionTimeoutMs,
288
+ flashboot: params.flashboot
289
+ });
290
+ }
291
+ var mapEndpointCreateToV2 = mapEndpointToV2;
292
+ var mapEndpointUpdateToV2 = mapEndpointToV2;
293
+ function mapNetworkVolumeCreateToV2(params) {
294
+ return compact({
295
+ name: params.name,
296
+ size: params.size,
297
+ dataCenter: params.dataCenterId,
298
+ type: params.volumeType
299
+ });
300
+ }
301
+ function mapTemplateCreateToV2(params) {
302
+ return {
303
+ ...containerConfigToV2(params),
304
+ ...compact({
305
+ name: params.name,
306
+ serverless: params.isServerless,
307
+ registry: params.containerRegistryAuthId,
308
+ args: joinStartCmd(params.dockerStartCmd),
309
+ // No longer required by v2 (it defaults to NVIDIA server-side), so an
310
+ // unset category is dropped by compact instead of being forced here.
311
+ category: params.category
312
+ })
313
+ };
314
+ }
315
+ function podBodyFromTemplate(template) {
316
+ const out = {};
317
+ const keys = [
318
+ "name",
319
+ "image",
320
+ "args",
321
+ "disk",
322
+ "ports",
323
+ "env",
324
+ "mounts",
325
+ "registry"
326
+ ];
327
+ for (const key of keys) {
328
+ const value = template[key];
329
+ if (value !== void 0 && value !== null) out[key] = value;
330
+ }
331
+ return out;
332
+ }
333
+ function mapTemplateUpdateToV2(params) {
334
+ return {
335
+ ...containerConfigToV2(params),
336
+ ...compact({
337
+ name: params.name,
338
+ registry: params.containerRegistryAuthId,
339
+ args: joinStartCmd(params.dockerStartCmd)
340
+ })
341
+ };
342
+ }
343
+
344
+ // src/_shared/backend.ts
345
+ var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
346
+ var V2_UNWRAP_KEY = {
347
+ pods: "pods",
348
+ templates: "templates",
349
+ networkVolumes: "networkVolumes",
350
+ registries: "registries",
351
+ gpus: "gpus",
352
+ cpus: "cpus",
353
+ dataCenters: "dataCenters",
354
+ endpoints: "endpoints",
355
+ jobs: "jobs",
356
+ workers: "workers",
357
+ billing: "records"
358
+ };
359
+ var RESOURCE_ENV_KEY = {
360
+ pods: "PODS",
361
+ templates: "TEMPLATES",
362
+ networkVolumes: "NETWORK_VOLUMES",
363
+ registries: "REGISTRIES",
364
+ gpus: "GPUS",
365
+ cpus: "CPUS",
366
+ dataCenters: "DATA_CENTERS",
367
+ endpoints: "ENDPOINTS",
368
+ jobs: "JOBS",
369
+ workers: "WORKERS",
370
+ billing: "BILLING"
371
+ };
372
+ function restV1Base(env) {
373
+ return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
374
+ }
375
+ function restV2Base(env) {
376
+ return env.RUNPOD_REST_V2_API_URL ?? "https://v2-rest.runpod.io/v2";
377
+ }
378
+ function serverlessBase(env) {
379
+ return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
380
+ }
381
+ function publicGraphqlBase(env) {
382
+ return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
383
+ }
384
+ function authedGraphqlBase(env) {
385
+ return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
386
+ }
387
+ function unwrapList(resource, version, raw) {
388
+ if (version === "v1") return Array.isArray(raw) ? raw : [];
389
+ if (raw === null || typeof raw !== "object") return [];
390
+ const arr = raw[V2_UNWRAP_KEY[resource]];
391
+ return Array.isArray(arr) ? arr : [];
392
+ }
393
+ function resolveVersion(opts) {
394
+ const { resource, env, ctx, v2Available } = opts;
395
+ if (V1_ONLY.has(resource)) return "v1";
396
+ const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
397
+ const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
398
+ if (setting === "v2") return "v2";
399
+ if (setting === "auto") {
400
+ if (ctx.transport === "http") return "v1";
401
+ return v2Available === true ? "v2" : "v1";
402
+ }
403
+ return "v1";
404
+ }
405
+ var identity = (body) => body;
406
+ var V1_REST_PATHS = {
407
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
408
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
409
+ networkVolumes: {
410
+ list: "/networkvolumes",
411
+ get: (id) => `/networkvolumes/${id}`
412
+ },
413
+ registries: {
414
+ list: "/containerregistryauth",
415
+ get: (id) => `/containerregistryauth/${id}`
416
+ },
417
+ endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
418
+ };
419
+ var CATALOG = /* @__PURE__ */ new Set([
420
+ "gpus",
421
+ "cpus",
422
+ "dataCenters"
423
+ ]);
424
+ var V2_ONLY = /* @__PURE__ */ new Set([
425
+ "workers",
426
+ "billing"
427
+ ]);
428
+ var V2_REST_PATHS = {
429
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
430
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
431
+ networkVolumes: {
432
+ list: "/network-volumes",
433
+ get: (id) => `/network-volumes/${id}`
434
+ },
435
+ registries: { list: "/registries", get: (id) => `/registries/${id}` },
436
+ gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
437
+ cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
438
+ dataCenters: {
439
+ list: "/catalog/datacenters",
440
+ get: (id) => `/catalog/datacenters/${id}`
441
+ },
442
+ // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
443
+ // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
444
+ // unwrap key stays correct.
445
+ endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
446
+ // workers/billing build their own sub-paths in the tool from `base`; the
447
+ // list entry just satisfies buildV2Backend (which requires a paths entry).
448
+ workers: { list: "/serverless" },
449
+ billing: { list: "/billing" }
450
+ };
451
+ var V2_MAPPERS = {
452
+ pods: {
453
+ create: (b) => mapPodCreateToV2(b),
454
+ update: (b) => mapPodUpdateToV2(b)
455
+ },
456
+ templates: {
457
+ create: (b) => mapTemplateCreateToV2(b),
458
+ update: (b) => mapTemplateUpdateToV2(b)
459
+ },
460
+ networkVolumes: {
461
+ create: (b) => mapNetworkVolumeCreateToV2(
462
+ b
463
+ ),
464
+ update: identity
465
+ },
466
+ registries: { create: identity, update: identity },
467
+ endpoints: {
468
+ create: (b) => mapEndpointCreateToV2(b),
469
+ update: (b) => mapEndpointUpdateToV2(b)
470
+ }
471
+ };
472
+ function buildV2Backend(resource, env, unwrap) {
473
+ const paths = V2_REST_PATHS[resource];
474
+ if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
475
+ const mappers = V2_MAPPERS[resource];
476
+ return {
477
+ kind: "rest",
478
+ version: "v2",
479
+ base: restV2Base(env),
480
+ list: paths.list,
481
+ get: paths.get,
482
+ unwrap,
483
+ mapCreate: mappers?.create ?? identity,
484
+ mapUpdate: mappers?.update ?? identity
485
+ };
486
+ }
487
+ function buildV1Backend(resource, env, unwrap) {
488
+ if (resource === "jobs") {
489
+ return {
490
+ kind: "rest",
491
+ version: "v1",
492
+ base: serverlessBase(env),
493
+ unwrap,
494
+ mapCreate: identity,
495
+ mapUpdate: identity
496
+ };
497
+ }
498
+ if (V2_ONLY.has(resource)) {
499
+ return {
500
+ kind: "rest",
501
+ version: "v1",
502
+ base: restV1Base(env),
503
+ unwrap,
504
+ mapCreate: identity,
505
+ mapUpdate: identity
506
+ };
507
+ }
508
+ if (CATALOG.has(resource)) {
509
+ return {
510
+ kind: "graphql",
511
+ version: "v1",
512
+ base: publicGraphqlBase(env),
513
+ unwrap,
514
+ mapCreate: identity,
515
+ mapUpdate: identity
516
+ };
517
+ }
518
+ const paths = V1_REST_PATHS[resource];
519
+ if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
520
+ return {
521
+ kind: "rest",
522
+ version: "v1",
523
+ base: restV1Base(env),
524
+ list: paths.list,
525
+ get: paths.get,
526
+ unwrap,
527
+ mapCreate: identity,
528
+ mapUpdate: identity
529
+ };
530
+ }
531
+ function resolveBackend(opts) {
532
+ const { resource, env } = opts;
533
+ const version = resolveVersion(opts);
534
+ const unwrap = (raw) => unwrapList(resource, version, raw);
535
+ return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
536
+ }
537
+
538
+ // src/tools/runtime.ts
539
+ var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
540
+ var SESSION_ID = (0, import_node_crypto.randomUUID)();
541
+ function trackingHeaders(server, ctx) {
542
+ const info = server.server.getClientVersion();
543
+ return buildTrackingHeaders({
544
+ clientName: info?.name || ctx.clientUserAgent || "unknown",
545
+ clientVersion: info?.version || "unknown",
546
+ transport: ctx.transport,
547
+ // Prefer a server version supplied by the caller (the hosted entrypoint reads
548
+ // it from package.json at runtime, since tsup's build-time define does not
549
+ // run when Vercel compiles api/index.ts). Fall back to the build-time value.
550
+ serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
551
+ sessionId: SESSION_ID
552
+ });
553
+ }
554
+ var defaultFetch = import_node_fetch.default;
555
+ async function withApiErrorLog(label, call) {
556
+ try {
557
+ return await call();
558
+ } catch (error) {
559
+ console.error(label, error);
560
+ throw error;
561
+ }
562
+ }
563
+ function jsonReply(result) {
564
+ return {
565
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
566
+ };
567
+ }
568
+ var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
569
+ var WRITE = { readOnlyHint: false, openWorldHint: true };
570
+ var DESTRUCTIVE = {
571
+ readOnlyHint: false,
572
+ destructiveHint: true,
573
+ idempotentHint: true,
574
+ openWorldHint: true
575
+ };
576
+ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
577
+ const response = await fetchImpl(url, {
578
+ method: "POST",
579
+ headers: {
580
+ "Content-Type": "application/json",
581
+ ...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
582
+ ...tracking()
583
+ },
584
+ body: JSON.stringify({
585
+ query,
586
+ ...options?.variables ? { variables: options.variables } : {}
587
+ })
588
+ });
589
+ const result = await response.json();
590
+ if (result.errors && result.errors.length > 0) {
591
+ throw new Error(
592
+ `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
593
+ );
594
+ }
595
+ if (!result.data) {
596
+ throw new Error("No data returned from GraphQL query");
597
+ }
598
+ return result.data;
599
+ }
600
+ function createToolRuntime(server, ctx, deps = {}) {
601
+ const tracking = () => trackingHeaders(server, ctx);
602
+ function observeUnauthorized(fn) {
603
+ const notify = ctx.onUnauthorized;
604
+ if (!notify) return fn;
605
+ return async (...args) => {
606
+ const response = await fn(...args);
607
+ if (response.status === 401) notify();
608
+ return response;
609
+ };
610
+ }
611
+ const rawFetch = deps.fetch ?? defaultFetch;
612
+ const httpFetch = observeUnauthorized(rawFetch);
613
+ const v1Client = createHttpClient({
614
+ apiKey: ctx.apiKey,
615
+ fetch: httpFetch,
616
+ tracking,
617
+ errorPrefix: "Runpod API Error"
618
+ });
619
+ const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
620
+ "Error calling Runpod API:",
621
+ () => v1Client(`${restV1Base(process.env)}${endpoint}`, method, body)
622
+ );
623
+ const serverlessClient = createHttpClient({
624
+ apiKey: ctx.apiKey,
625
+ fetch: httpFetch,
626
+ tracking,
627
+ errorPrefix: "Runpod Serverless API Error"
628
+ });
629
+ const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
630
+ "Error calling Runpod Serverless API:",
631
+ () => serverlessClient(
632
+ `${serverlessBase(process.env)}/${endpointId}${path}`,
633
+ method,
634
+ body
635
+ )
636
+ );
637
+ const restClient = createHttpClient({
638
+ apiKey: ctx.apiKey,
639
+ fetch: httpFetch,
640
+ tracking,
641
+ errorPrefix: "Runpod API Error"
642
+ });
643
+ const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
644
+ "Error calling Runpod API:",
645
+ () => restClient(url, method, body)
646
+ );
647
+ const sseFetch = observeUnauthorized(
648
+ deps.sseFetch ?? import_node_fetch.default
649
+ );
650
+ const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
651
+ sseFetch,
652
+ url,
653
+ {
654
+ Authorization: `Bearer ${ctx.apiKey}`,
655
+ Accept: "text/event-stream",
656
+ ...tracking()
657
+ },
658
+ opts
659
+ ));
660
+ const backendFor = (resource) => resolveBackend({
661
+ resource,
662
+ env: process.env,
663
+ ctx: { transport: ctx.transport },
664
+ v2Available: deps.v2Available
665
+ });
666
+ const podAction = async (podId, action) => {
667
+ const backend = backendFor("pods");
668
+ const podPath = backend.get(podId);
669
+ if (backend.version === "v2") {
670
+ return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
671
+ action
672
+ });
673
+ }
674
+ return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
675
+ };
676
+ return {
677
+ jsonReply,
678
+ graphql: (query) => graphqlRequest(
679
+ query,
680
+ tracking,
681
+ // rawFetch, not httpFetch: this call carries no credential (see above).
682
+ rawFetch,
683
+ publicGraphqlBase(process.env)
684
+ ),
685
+ graphqlAuthed: (query, variables) => graphqlRequest(
686
+ query,
687
+ tracking,
688
+ httpFetch,
689
+ // NOT publicGraphqlBase: this call carries the caller's API key.
690
+ authedGraphqlBase(process.env),
691
+ { variables, apiKey: ctx.apiKey }
692
+ ),
693
+ runpodRequest,
694
+ serverlessRequest,
695
+ callRestUrl,
696
+ backendFor,
697
+ streamSse,
698
+ podAction,
699
+ env: process.env
700
+ };
701
+ }
702
+
703
+ // src/tools/catalog.ts
704
+ var import_zod3 = require("zod");
705
+
706
+ // src/pagination.ts
707
+ var import_zod2 = require("zod");
42
708
  var DEFAULT_LIST_LIMIT = 20;
43
709
  var MAX_LIST_LIMIT = 100;
44
710
  var listPaginationParams = {
45
- limit: import_zod.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
711
+ limit: import_zod2.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
46
712
  `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
47
713
  ),
48
- cursor: import_zod.z.string().optional().describe(
714
+ cursor: import_zod2.z.string().optional().describe(
49
715
  "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
50
716
  )
51
717
  };
@@ -62,7 +728,7 @@ function decodeCursorOffset(cursor) {
62
728
  function encodeCursorOffset(offset) {
63
729
  return Buffer.from(String(offset), "utf8").toString("base64");
64
730
  }
65
- function capListResult(result, options) {
731
+ function capListResult(result, options, extra) {
66
732
  const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
67
733
  const offset = decodeCursorOffset(options.cursor);
68
734
  if (!Array.isArray(result)) {
@@ -76,7 +742,11 @@ function capListResult(result, options) {
76
742
  const page = result.slice(offset, offset + limit);
77
743
  const nextOffset = offset + page.length;
78
744
  const hasMore = nextOffset < total;
745
+ const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
746
+ void _i;
747
+ void _p;
79
748
  const envelope = {
749
+ ...safeExtra,
80
750
  items: page,
81
751
  pagination: {
82
752
  total,
@@ -94,133 +764,67 @@ function capListResult(result, options) {
94
764
  };
95
765
  }
96
766
 
97
- // src/tools.ts
98
- var API_BASE_URL = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
99
- var SERVERLESS_API_BASE_URL = process.env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
100
- var GRAPHQL_URL = process.env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
101
- var MCP_SERVER_VERSION = true ? "1.4.0" : "dev";
102
- var SESSION_ID = (0, import_node_crypto.randomUUID)();
103
- function sanitizeUaToken(value) {
104
- return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
105
- }
106
- function trackingHeaders(server, ctx) {
107
- const info = server.server.getClientVersion();
108
- const fallbackName = ctx.clientUserAgent || "unknown";
109
- const name = sanitizeUaToken(info?.name || fallbackName);
110
- const version = sanitizeUaToken(info?.version || "unknown");
111
- const serverVersion = ctx.serverVersion || MCP_SERVER_VERSION;
112
- const userAgent = `runpod-mcp-server/${serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${ctx.transport})`;
113
- return {
114
- "User-Agent": userAgent,
115
- "X-Runpod-Session-Id": SESSION_ID
116
- };
117
- }
118
- async function graphqlRequest(query, tracking) {
119
- const response = await (0, import_node_fetch.default)(GRAPHQL_URL, {
120
- method: "POST",
121
- headers: {
122
- "Content-Type": "application/json",
123
- ...tracking()
124
- },
125
- body: JSON.stringify({ query })
126
- });
127
- const result = await response.json();
128
- if (result.errors && result.errors.length > 0) {
129
- throw new Error(
130
- `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
131
- );
132
- }
133
- if (!result.data) {
134
- throw new Error("No data returned from GraphQL query");
135
- }
136
- return result.data;
137
- }
138
- function createRunpodRequest(apiKey, tracking) {
139
- return async function runpodRequest(endpoint, method = "GET", body) {
140
- const url = `${API_BASE_URL}${endpoint}`;
141
- const headers = {
142
- Authorization: `Bearer ${apiKey}`,
143
- "Content-Type": "application/json",
144
- ...tracking()
145
- };
146
- const options = {
147
- method,
148
- headers
149
- };
150
- if (body && (method === "POST" || method === "PATCH")) {
151
- options.body = JSON.stringify(body);
152
- }
153
- try {
154
- const response = await (0, import_node_fetch.default)(url, options);
155
- if (!response.ok) {
156
- const errorText = await response.text();
157
- throw new Error(`Runpod API Error: ${response.status} - ${errorText}`);
158
- }
159
- const contentType = response.headers.get("content-type");
160
- if (contentType && contentType.includes("application/json")) {
161
- return await response.json();
162
- }
163
- return { success: true, status: response.status };
164
- } catch (error) {
165
- console.error("Error calling Runpod API:", error);
166
- throw error;
167
- }
168
- };
169
- }
170
- function createServerlessRequest(apiKey, tracking) {
171
- return async function serverlessRequest(endpointId, path, method = "GET", body) {
172
- const url = `${SERVERLESS_API_BASE_URL}/${endpointId}${path}`;
173
- const headers = {
174
- Authorization: `Bearer ${apiKey}`,
175
- "Content-Type": "application/json",
176
- ...tracking()
177
- };
178
- const options = {
179
- method,
180
- headers
181
- };
182
- if (body && (method === "POST" || method === "PATCH")) {
183
- options.body = JSON.stringify(body);
184
- }
185
- try {
186
- const response = await (0, import_node_fetch.default)(url, options);
187
- if (!response.ok) {
188
- const errorText = await response.text();
189
- throw new Error(
190
- `Runpod Serverless API Error: ${response.status} - ${errorText}`
191
- );
192
- }
193
- const contentType = response.headers.get("content-type");
194
- if (contentType && contentType.includes("application/json")) {
195
- return await response.json();
196
- }
197
- return { success: true, status: response.status };
198
- } catch (error) {
199
- console.error("Error calling Runpod Serverless API:", error);
200
- throw error;
201
- }
202
- };
203
- }
204
- function registerTools(server, ctx) {
205
- const tracking = () => trackingHeaders(server, ctx);
206
- const runpodRequest = createRunpodRequest(ctx.apiKey, tracking);
207
- const serverlessRequest = createServerlessRequest(ctx.apiKey, tracking);
767
+ // src/tools/catalog.ts
768
+ function registerCatalogTools(server, rt) {
769
+ const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
208
770
  server.tool(
209
771
  "list-gpu-types",
772
+ "List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. By default the full catalog is returned: each result includes an `availability` summary (HIGH/MEDIUM/LOW/NONE) and results are sorted with the most-available GPUs first, but nothing is hidden. Set includeUnavailable:false to drop out-of-stock GPUs and list only deployable ones (set includeAvailability:false to skip the stock lookup entirely). For per-datacenter availability (to pick a dataCenterIds), use get-gpu-type.",
210
773
  {
211
- minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
212
- secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
213
- communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
214
- searchTerm: import_zod2.z.string().optional().describe(
774
+ ...listPaginationParams,
775
+ minMemoryGb: import_zod3.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
776
+ secureCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
777
+ communityCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
778
+ searchTerm: import_zod3.z.string().optional().describe(
215
779
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
216
780
  ),
217
- includeUnavailable: import_zod2.z.boolean().optional().describe(
218
- "Include GPUs that are currently out of stock. Default is false"
781
+ includeUnavailable: import_zod3.z.boolean().optional().describe(
782
+ "Out-of-stock GPUs are included by default (annotated availability:NONE and sorted last). Set false to hide them and list only currently-deployable GPUs."
783
+ ),
784
+ includeAvailability: import_zod3.z.boolean().optional().describe(
785
+ "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."
219
786
  )
220
787
  },
788
+ { title: "List GPU types", ...READ_ONLY },
221
789
  async (params) => {
222
- const data = await graphqlRequest(
223
- `
790
+ const backend = backendFor("gpus");
791
+ if (backend.version === "v2") {
792
+ const wantAvailability = params.includeAvailability !== false;
793
+ const raw = await callRestUrl(
794
+ `${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
795
+ );
796
+ let gpus = backend.unwrap(raw);
797
+ gpus = gpus.filter((g) => g.id !== "unknown");
798
+ if (params.minMemoryGb !== void 0)
799
+ gpus = gpus.filter(
800
+ (g) => Number(g.memory ?? 0) >= params.minMemoryGb
801
+ );
802
+ if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
803
+ if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
804
+ if (params.searchTerm) {
805
+ const t = params.searchTerm.toLowerCase();
806
+ gpus = gpus.filter(
807
+ (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
808
+ );
809
+ }
810
+ if (params.includeUnavailable === false)
811
+ gpus = gpus.filter((g) => g.availability !== "NONE");
812
+ const rank = {
813
+ HIGH: 3,
814
+ MEDIUM: 2,
815
+ LOW: 1,
816
+ NONE: 0
817
+ };
818
+ gpus.sort(
819
+ (a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
820
+ );
821
+ gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
822
+ return capListResult(gpus, {
823
+ limit: params.limit,
824
+ cursor: params.cursor
825
+ });
826
+ }
827
+ const data = await graphql(`
224
828
  query {
225
829
  gpuTypes {
226
830
  id
@@ -233,9 +837,7 @@ function registerTools(server, ctx) {
233
837
  }
234
838
  }
235
839
  }
236
- `,
237
- tracking
238
- );
840
+ `);
239
841
  const stockPriority = {
240
842
  High: 3,
241
843
  Medium: 2,
@@ -246,7 +848,7 @@ function registerTools(server, ctx) {
246
848
  return !!status && status !== "Out";
247
849
  };
248
850
  let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
249
- if (!params.includeUnavailable) {
851
+ if (params.includeUnavailable === false) {
250
852
  gpuTypes = gpuTypes.filter(isAvailable);
251
853
  }
252
854
  if (params.minMemoryGb) {
@@ -281,26 +883,39 @@ function registerTools(server, ctx) {
281
883
  stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
282
884
  available: isAvailable(gpu)
283
885
  }));
284
- return {
285
- content: [
286
- {
287
- type: "text",
288
- text: JSON.stringify(result, null, 2)
289
- }
290
- ]
291
- };
886
+ return capListResult(result, {
887
+ limit: params.limit,
888
+ cursor: params.cursor
889
+ });
292
890
  }
293
891
  );
294
892
  server.tool(
295
893
  "list-data-centers",
894
+ "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
296
895
  {
297
- region: import_zod2.z.string().optional().describe(
896
+ ...listPaginationParams,
897
+ region: import_zod3.z.string().optional().describe(
298
898
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
299
899
  )
300
900
  },
901
+ { title: "List data centers", ...READ_ONLY },
301
902
  async (params) => {
302
- const data = await graphqlRequest(
303
- `
903
+ const backend = backendFor("dataCenters");
904
+ if (backend.version === "v2") {
905
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
906
+ let dcs = backend.unwrap(raw);
907
+ if (params.region) {
908
+ const t = params.region.toLowerCase();
909
+ dcs = dcs.filter(
910
+ (dc) => String(dc.region ?? "").toLowerCase().includes(t)
911
+ );
912
+ }
913
+ return capListResult(dcs, {
914
+ limit: params.limit,
915
+ cursor: params.cursor
916
+ });
917
+ }
918
+ const data = await graphql(`
304
919
  query {
305
920
  dataCenters {
306
921
  id
@@ -308,9 +923,7 @@ function registerTools(server, ctx) {
308
923
  location
309
924
  }
310
925
  }
311
- `,
312
- tracking
313
- );
926
+ `);
314
927
  let dataCenters = data.dataCenters;
315
928
  if (params.region) {
316
929
  const term = params.region.toLowerCase();
@@ -318,50 +931,720 @@ function registerTools(server, ctx) {
318
931
  (dc) => dc.location.toLowerCase().includes(term)
319
932
  );
320
933
  }
321
- return {
322
- content: [
323
- {
324
- type: "text",
325
- text: JSON.stringify(dataCenters, null, 2)
934
+ return capListResult(dataCenters, {
935
+ limit: params.limit,
936
+ cursor: params.cursor
937
+ });
938
+ }
939
+ );
940
+ server.tool(
941
+ "list-cpu-types",
942
+ "List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
943
+ { ...listPaginationParams },
944
+ { title: "List CPU types", ...READ_ONLY },
945
+ async (params) => {
946
+ const backend = backendFor("cpus");
947
+ if (backend.version === "v1") {
948
+ return jsonReply2({
949
+ error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
950
+ status: 501
951
+ });
952
+ }
953
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
954
+ return capListResult(backend.unwrap(raw), {
955
+ limit: params.limit,
956
+ cursor: params.cursor
957
+ });
958
+ }
959
+ );
960
+ server.tool(
961
+ "get-gpu-type",
962
+ "Get details for a single GPU type by id, including per-datacenter availability. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there). Use the returned dataCenters[].availability to pick a dataCenterIds with stock before creating a pod.",
963
+ {
964
+ gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
965
+ includeAvailability: import_zod3.z.boolean().optional().describe(
966
+ "Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
967
+ )
968
+ },
969
+ { title: "Get GPU type", ...READ_ONLY },
970
+ async (params) => {
971
+ const backend = backendFor("gpus");
972
+ if (backend.version === "v1") {
973
+ return jsonReply2({
974
+ error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
975
+ status: 501
976
+ });
977
+ }
978
+ const path = backend.get(encodeURIComponent(params.gpuTypeId));
979
+ const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
980
+ const result = await callRestUrl(`${backend.base}${path}${query}`);
981
+ return jsonReply2(result);
982
+ }
983
+ );
984
+ server.tool(
985
+ "get-cpu-type",
986
+ "Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
987
+ {
988
+ cpuTypeId: import_zod3.z.string().describe("ID of the CPU type to retrieve")
989
+ },
990
+ { title: "Get CPU type", ...READ_ONLY },
991
+ async (params) => {
992
+ const backend = backendFor("cpus");
993
+ if (backend.version === "v1") {
994
+ return jsonReply2({
995
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
996
+ status: 501
997
+ });
998
+ }
999
+ const result = await callRestUrl(
1000
+ `${backend.base}${backend.get(params.cpuTypeId)}`
1001
+ );
1002
+ return jsonReply2(result);
1003
+ }
1004
+ );
1005
+ server.tool(
1006
+ "get-data-center",
1007
+ "Get details for a single data center by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-data-centers there).",
1008
+ {
1009
+ dataCenterId: import_zod3.z.string().describe("ID of the data center to retrieve")
1010
+ },
1011
+ { title: "Get data center", ...READ_ONLY },
1012
+ async (params) => {
1013
+ const backend = backendFor("dataCenters");
1014
+ if (backend.version === "v1") {
1015
+ return jsonReply2({
1016
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1017
+ status: 501
1018
+ });
1019
+ }
1020
+ const result = await callRestUrl(
1021
+ `${backend.base}${backend.get(params.dataCenterId)}`
1022
+ );
1023
+ return jsonReply2(result);
1024
+ }
1025
+ );
1026
+ }
1027
+
1028
+ // src/tools/hub.ts
1029
+ var import_zod4 = require("zod");
1030
+ function parseReleaseConfig(config) {
1031
+ if (!config) return void 0;
1032
+ try {
1033
+ return JSON.parse(config);
1034
+ } catch {
1035
+ return config;
1036
+ }
1037
+ }
1038
+ function listingsQuery(withConfig) {
1039
+ return `
1040
+ query {
1041
+ listings(input: {}) {
1042
+ id
1043
+ repoId
1044
+ title
1045
+ description
1046
+ repoName
1047
+ repoOwner
1048
+ createdAt
1049
+ updatedAt
1050
+ views
1051
+ stars
1052
+ deploys
1053
+ language
1054
+ category
1055
+ tags
1056
+ type
1057
+ listedRelease {
1058
+ id
1059
+ name
1060
+ tagName
1061
+ createdAt${withConfig ? "\n config" : ""}
1062
+ build {
1063
+ id
1064
+ imageName
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1069
+ `;
1070
+ }
1071
+ function serializeBoolean(value, input) {
1072
+ if (value === "true") return input.trueValue ?? "true";
1073
+ if (value === "false") return input.falseValue ?? "false";
1074
+ return value;
1075
+ }
1076
+ function buildHubEnv(config, overrides) {
1077
+ const env = [];
1078
+ const missingRequired = [];
1079
+ const remaining = { ...overrides };
1080
+ for (const entry of config.env ?? []) {
1081
+ const input = entry.input ?? {};
1082
+ const override = entry.key in remaining ? remaining[entry.key] : void 0;
1083
+ delete remaining[entry.key];
1084
+ if (override !== void 0 && override !== "") {
1085
+ env.push({
1086
+ key: entry.key,
1087
+ value: input.type === "boolean" ? serializeBoolean(override, input) : override
1088
+ });
1089
+ continue;
1090
+ }
1091
+ if (input.default !== void 0 && input.default !== null) {
1092
+ env.push({
1093
+ key: entry.key,
1094
+ value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
1095
+ });
1096
+ continue;
1097
+ }
1098
+ if (input.required) {
1099
+ missingRequired.push(entry.key);
1100
+ env.push({ key: entry.key, value: "" });
1101
+ continue;
1102
+ }
1103
+ }
1104
+ for (const [key, value] of Object.entries(remaining)) {
1105
+ env.push({ key, value });
1106
+ }
1107
+ return { env, missingRequired };
1108
+ }
1109
+ function minCudaVersion(versions) {
1110
+ if (!versions || versions.length === 0) return null;
1111
+ const compare = (a, b) => {
1112
+ const pa = a.split(".").map(Number);
1113
+ const pb = b.split(".").map(Number);
1114
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1115
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1116
+ if (d !== 0) return d;
1117
+ }
1118
+ return 0;
1119
+ };
1120
+ return [...versions].sort(compare)[0];
1121
+ }
1122
+ function randomSuffix() {
1123
+ return Math.random().toString(36).slice(2, 8);
1124
+ }
1125
+ function registerHubTools(server, rt) {
1126
+ const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
1127
+ server.tool(
1128
+ "list-hub-repos",
1129
+ "List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
1130
+ {
1131
+ ...listPaginationParams,
1132
+ searchTerm: import_zod4.z.string().optional().describe(
1133
+ "Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
1134
+ ),
1135
+ category: import_zod4.z.string().optional().describe(
1136
+ "Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
1137
+ ),
1138
+ type: import_zod4.z.enum(["SERVERLESS", "POD"]).optional().describe(
1139
+ "Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
1140
+ ),
1141
+ repoOwner: import_zod4.z.string().optional().describe(
1142
+ "Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
1143
+ ),
1144
+ includeConfig: import_zod4.z.boolean().optional().describe(
1145
+ "Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
1146
+ )
1147
+ },
1148
+ { title: "List Hub repos", ...READ_ONLY },
1149
+ async (params) => {
1150
+ const data = await graphql(
1151
+ listingsQuery(params.includeConfig === true)
1152
+ );
1153
+ let listings = data.listings;
1154
+ if (params.type) {
1155
+ listings = listings.filter(
1156
+ (l) => (l.type ?? "").toUpperCase() === params.type
1157
+ );
1158
+ }
1159
+ if (params.category) {
1160
+ const term = params.category.toLowerCase();
1161
+ listings = listings.filter(
1162
+ (l) => (l.category ?? "").toLowerCase() === term
1163
+ );
1164
+ }
1165
+ if (params.repoOwner) {
1166
+ const term = params.repoOwner.toLowerCase();
1167
+ listings = listings.filter(
1168
+ (l) => (l.repoOwner ?? "").toLowerCase() === term
1169
+ );
1170
+ }
1171
+ if (params.searchTerm) {
1172
+ const term = params.searchTerm.toLowerCase();
1173
+ listings = listings.filter(
1174
+ (l) => [
1175
+ l.title,
1176
+ l.description,
1177
+ l.repoName,
1178
+ l.repoOwner,
1179
+ ...l.tags ?? []
1180
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1181
+ );
1182
+ }
1183
+ listings = [...listings].sort(
1184
+ (a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
1185
+ );
1186
+ const result = listings.map((l) => ({
1187
+ id: l.id,
1188
+ repo: `${l.repoOwner}/${l.repoName}`,
1189
+ title: l.title,
1190
+ description: l.description,
1191
+ type: l.type,
1192
+ category: l.category,
1193
+ tags: l.tags,
1194
+ language: l.language,
1195
+ stars: l.stars,
1196
+ deploys: l.deploys,
1197
+ views: l.views,
1198
+ githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
1199
+ hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
1200
+ createdAt: l.createdAt,
1201
+ updatedAt: l.updatedAt,
1202
+ listedRelease: l.listedRelease ? {
1203
+ hubReleaseId: l.listedRelease.id,
1204
+ name: l.listedRelease.name,
1205
+ tagName: l.listedRelease.tagName,
1206
+ createdAt: l.listedRelease.createdAt,
1207
+ imageName: l.listedRelease.build?.imageName,
1208
+ ...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
1209
+ } : null
1210
+ }));
1211
+ return capListResult(result, {
1212
+ limit: params.limit,
1213
+ cursor: params.cursor
1214
+ });
1215
+ }
1216
+ );
1217
+ server.tool(
1218
+ "deploy-hub-repo",
1219
+ "Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet), so it works the same on v1 and v2.",
1220
+ {
1221
+ repo: import_zod4.z.string().optional().describe(
1222
+ `The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
1223
+ ),
1224
+ hubReleaseId: import_zod4.z.string().optional().describe(
1225
+ "The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
1226
+ ),
1227
+ name: import_zod4.z.string().optional().describe(
1228
+ "Name for the new endpoint. Defaults to '<listing title> <release tag>'."
1229
+ ),
1230
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe(
1231
+ "Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
1232
+ ),
1233
+ gpuIds: import_zod4.z.string().optional().describe(
1234
+ "Comma-separated GPU pool names for workers (e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). To pin specific GPU SKUs within a pool, append exclusions prefixed with '-' using GPU type ids from list-gpu-types (e.g. 'AMPERE_16,-NVIDIA RTX 2000 Ada Generation,-NVIDIA RTX A4500' allows only the pool's remaining SKUs). Defaults to the release config's gpuIds; required when the config does not specify one."
1235
+ ),
1236
+ gpuCount: import_zod4.z.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
1237
+ containerDiskInGb: import_zod4.z.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
1238
+ workersMin: import_zod4.z.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
1239
+ workersMax: import_zod4.z.number().int().positive().optional().describe("Maximum workers (default: account default)."),
1240
+ idleTimeout: import_zod4.z.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
1241
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
1242
+ scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value (default 4)."),
1243
+ executionTimeoutMs: import_zod4.z.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
1244
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
1245
+ },
1246
+ { title: "Deploy Hub repo", ...WRITE },
1247
+ async (params) => {
1248
+ if (!params.repo && !params.hubReleaseId) {
1249
+ return jsonReply2({
1250
+ error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
1251
+ });
1252
+ }
1253
+ const catalog = await graphql(listingsQuery(true));
1254
+ let listing;
1255
+ if (params.hubReleaseId) {
1256
+ listing = catalog.listings.find(
1257
+ (l) => l.listedRelease?.id === params.hubReleaseId
1258
+ );
1259
+ } else {
1260
+ const repoKey = params.repo.toLowerCase();
1261
+ listing = catalog.listings.find(
1262
+ (l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
1263
+ );
1264
+ }
1265
+ if (!listing) {
1266
+ return jsonReply2({
1267
+ error: `No Hub listing found for ${params.hubReleaseId ? `hubReleaseId "${params.hubReleaseId}" (only each repo's currently listed release is deployable)` : `repo "${params.repo}"`}. Use list-hub-repos to see the catalog.`
1268
+ });
1269
+ }
1270
+ const release = listing.listedRelease;
1271
+ if (!release?.build?.imageName) {
1272
+ return jsonReply2({
1273
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
1274
+ });
1275
+ }
1276
+ if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
1277
+ return jsonReply2({
1278
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
1279
+ });
1280
+ }
1281
+ const config = parseReleaseConfig(release.config) ?? {};
1282
+ const gpuIds = params.gpuIds ?? config.gpuIds;
1283
+ if (!gpuIds) {
1284
+ return jsonReply2({
1285
+ error: 'This release does not specify a GPU pool \u2014 pass gpuIds (comma-separated pool names, e.g. "ADA_80_PRO,AMPERE_80"; see the pool field on list-gpu-types).'
1286
+ });
1287
+ }
1288
+ const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
1289
+ if (missingRequired.length > 0) {
1290
+ return jsonReply2({
1291
+ error: `Missing required environment variables for this release: ${missingRequired.join(
1292
+ ", "
1293
+ )}. Pass them via the env parameter.`
1294
+ });
1295
+ }
1296
+ const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
1297
+ const minCuda = minCudaVersion(config.allowedCudaVersions);
1298
+ const input = {
1299
+ name: endpointName,
1300
+ hubReleaseId: release.id,
1301
+ type: "QB",
1302
+ gpuIds,
1303
+ gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
1304
+ workersMin: params.workersMin ?? 0,
1305
+ workersMax: params.workersMax ?? null,
1306
+ idleTimeout: params.idleTimeout ?? 5,
1307
+ scalerType: params.scalerType ?? "QUEUE_DELAY",
1308
+ scalerValue: params.scalerValue ?? 4,
1309
+ executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
1310
+ flashBootType: params.flashboot ?? "FLASHBOOT",
1311
+ locations: null,
1312
+ networkVolumeIds: null,
1313
+ compliance: [],
1314
+ modelReferences: [],
1315
+ ...minCuda ? {
1316
+ minCudaVersion: minCuda,
1317
+ allowedCudaVersions: config.allowedCudaVersions.join(",")
1318
+ } : {},
1319
+ template: {
1320
+ name: `${endpointName}__template__${randomSuffix()}`,
1321
+ imageName: release.build.imageName,
1322
+ containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
1323
+ containerRegistryAuthId: "",
1324
+ dockerArgs: "",
1325
+ startScript: "",
1326
+ ports: "",
1327
+ env
1328
+ }
1329
+ };
1330
+ const data = await graphqlAuthed(
1331
+ `
1332
+ mutation saveEndpoint($input: EndpointInput!) {
1333
+ saveEndpoint(input: $input) {
1334
+ id
1335
+ name
1336
+ gpuIds
1337
+ gpuCount
1338
+ workersMin
1339
+ workersMax
1340
+ idleTimeout
1341
+ scalerType
1342
+ scalerValue
1343
+ flashBootType
1344
+ templateId
1345
+ }
1346
+ }
1347
+ `,
1348
+ { input }
1349
+ );
1350
+ return jsonReply2({
1351
+ endpoint: data.saveEndpoint,
1352
+ deployed: {
1353
+ repo: `${listing.repoOwner}/${listing.repoName}`,
1354
+ release: release.tagName,
1355
+ hubReleaseId: release.id,
1356
+ imageName: release.build.imageName
1357
+ },
1358
+ note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
1359
+ });
1360
+ }
1361
+ );
1362
+ }
1363
+
1364
+ // src/tools/public-endpoints.ts
1365
+ var import_zod5 = require("zod");
1366
+ function parseMetadata(metadata) {
1367
+ if (!metadata) return {};
1368
+ try {
1369
+ const parsed = JSON.parse(metadata);
1370
+ return parsed && typeof parsed === "object" ? parsed : {};
1371
+ } catch {
1372
+ return {};
1373
+ }
1374
+ }
1375
+ function registerPublicEndpointTools(server, rt) {
1376
+ const { graphql } = rt;
1377
+ server.tool(
1378
+ "list-public-endpoints",
1379
+ "List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
1380
+ {
1381
+ ...listPaginationParams,
1382
+ searchTerm: import_zod5.z.string().optional().describe(
1383
+ "Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
1384
+ ),
1385
+ modality: import_zod5.z.string().optional().describe(
1386
+ "Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
1387
+ ),
1388
+ owner: import_zod5.z.string().optional().describe(
1389
+ "Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
1390
+ ),
1391
+ includeOffline: import_zod5.z.boolean().optional().describe(
1392
+ "Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
1393
+ )
1394
+ },
1395
+ { title: "List public endpoints", ...READ_ONLY },
1396
+ async (params) => {
1397
+ const data = await graphql(`
1398
+ query {
1399
+ allAiApiPublicConfigs {
1400
+ id
1401
+ aiApiId
1402
+ modelName
1403
+ displayName
1404
+ description
1405
+ metadata
1406
+ isLive
1407
+ createdAt
1408
+ updatedAt
1409
+ }
1410
+ }
1411
+ `);
1412
+ let configs = data.allAiApiPublicConfigs.map((c) => ({
1413
+ config: c,
1414
+ meta: parseMetadata(c.metadata)
1415
+ }));
1416
+ if (!params.includeOffline) {
1417
+ configs = configs.filter(({ config }) => config.isLive);
1418
+ }
1419
+ if (params.modality) {
1420
+ const term = params.modality.toLowerCase();
1421
+ configs = configs.filter(
1422
+ ({ meta }) => (meta.source ?? "").toLowerCase() === term
1423
+ );
1424
+ }
1425
+ if (params.owner) {
1426
+ const term = params.owner.toLowerCase();
1427
+ configs = configs.filter(
1428
+ ({ meta }) => (meta.owner ?? "").toLowerCase() === term
1429
+ );
1430
+ }
1431
+ if (params.searchTerm) {
1432
+ const term = params.searchTerm.toLowerCase();
1433
+ configs = configs.filter(
1434
+ ({ config, meta }) => [
1435
+ config.displayName,
1436
+ config.modelName,
1437
+ config.aiApiId,
1438
+ config.description,
1439
+ meta.owner,
1440
+ meta.tag,
1441
+ meta.source
1442
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1443
+ );
1444
+ }
1445
+ configs = [...configs].sort(
1446
+ (a, b) => a.config.displayName.localeCompare(b.config.displayName)
1447
+ );
1448
+ const result = configs.map(({ config, meta }) => ({
1449
+ endpointId: config.aiApiId,
1450
+ displayName: config.displayName,
1451
+ modelName: config.modelName,
1452
+ description: config.description,
1453
+ modality: meta.source,
1454
+ tag: meta.tag,
1455
+ owner: meta.owner,
1456
+ pricing: meta.priceString,
1457
+ isLive: config.isLive,
1458
+ baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
1459
+ createdAt: config.createdAt,
1460
+ updatedAt: config.updatedAt
1461
+ }));
1462
+ return capListResult(result, {
1463
+ limit: params.limit,
1464
+ cursor: params.cursor
1465
+ });
1466
+ }
1467
+ );
1468
+ }
1469
+
1470
+ // src/tools/endpoint-gpus.ts
1471
+ var import_zod6 = require("zod");
1472
+ function registerEndpointGpuTools(server, rt) {
1473
+ const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
1474
+ server.tool(
1475
+ "set-endpoint-gpus",
1476
+ "Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs, which create-endpoint/update-endpoint cannot express. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly (e.g. pools:['AMPERE_16'], excludeGpuTypeIds:['NVIDIA RTX 2000 Ada Generation','NVIDIA RTX 4000 Ada Generation','NVIDIA RTX A4500'] pins RTX A4000). All other endpoint settings (workers, scaling, timeouts, template) are read first and preserved. Works on any Serverless endpoint via the authenticated GraphQL API.",
1477
+ {
1478
+ endpointId: import_zod6.z.string().describe("ID of the Serverless endpoint to update"),
1479
+ gpuIds: import_zod6.z.string().optional().describe(
1480
+ "Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
1481
+ ),
1482
+ pools: import_zod6.z.array(import_zod6.z.string()).optional().describe(
1483
+ "GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
1484
+ ),
1485
+ excludeGpuTypeIds: import_zod6.z.array(import_zod6.z.string()).optional().describe(
1486
+ "GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
1487
+ ),
1488
+ gpuCount: import_zod6.z.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
1489
+ minCudaVersion: import_zod6.z.string().optional().describe(
1490
+ "Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
1491
+ ),
1492
+ allowedCudaVersions: import_zod6.z.string().optional().describe(
1493
+ "Comma-separated allowed host CUDA versions (e.g. '12.8,12.7,12.6'). Omit to keep the current value. CUDA compatibility is part of GPU selection \u2014 a narrow list can leave an endpoint unable to schedule workers."
1494
+ )
1495
+ },
1496
+ { title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
1497
+ async (params) => {
1498
+ const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
1499
+ ...params.pools,
1500
+ ...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
1501
+ ].join(",") : void 0);
1502
+ if (!gpuIds) {
1503
+ return jsonReply2({
1504
+ error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
1505
+ });
1506
+ }
1507
+ const data = await graphqlAuthed(`
1508
+ query {
1509
+ myself {
1510
+ endpoints {
1511
+ id
1512
+ name
1513
+ gpuIds
1514
+ gpuCount
1515
+ workersMin
1516
+ workersMax
1517
+ idleTimeout
1518
+ scalerType
1519
+ scalerValue
1520
+ executionTimeoutMs
1521
+ flashBootType
1522
+ type
1523
+ locations
1524
+ templateId
1525
+ allowedCudaVersions
1526
+ minCudaVersion
1527
+ compliance
1528
+ modelReferences
1529
+ networkVolumeIds {
1530
+ networkVolumeId
1531
+ dataCenterId
1532
+ }
1533
+ }
326
1534
  }
327
- ]
1535
+ }
1536
+ `);
1537
+ const current = data.myself.endpoints.find(
1538
+ (e) => e.id === params.endpointId
1539
+ );
1540
+ if (!current) {
1541
+ return jsonReply2({
1542
+ error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
1543
+ });
1544
+ }
1545
+ const input = {
1546
+ id: current.id,
1547
+ name: current.name,
1548
+ gpuIds,
1549
+ gpuCount: params.gpuCount ?? current.gpuCount,
1550
+ workersMin: current.workersMin,
1551
+ workersMax: current.workersMax,
1552
+ idleTimeout: current.idleTimeout,
1553
+ scalerType: current.scalerType,
1554
+ scalerValue: current.scalerValue,
1555
+ executionTimeoutMs: current.executionTimeoutMs,
1556
+ flashBootType: current.flashBootType,
1557
+ type: current.type,
1558
+ locations: current.locations,
1559
+ networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
1560
+ // Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
1561
+ // ONLY, and the read shape is rejected outright.
1562
+ current.networkVolumeIds.map((v) => ({
1563
+ networkVolumeId: v.networkVolumeId
1564
+ }))
1565
+ ) : null
328
1566
  };
1567
+ for (const [key, value] of Object.entries({
1568
+ templateId: current.templateId,
1569
+ allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
1570
+ minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
1571
+ compliance: current.compliance,
1572
+ modelReferences: current.modelReferences
1573
+ })) {
1574
+ if (value !== null && value !== void 0) input[key] = value;
1575
+ }
1576
+ const result = await graphqlAuthed(
1577
+ `
1578
+ mutation saveEndpoint($input: EndpointInput!) {
1579
+ saveEndpoint(input: $input) {
1580
+ id
1581
+ name
1582
+ gpuIds
1583
+ gpuCount
1584
+ workersMin
1585
+ workersMax
1586
+ }
1587
+ }
1588
+ `,
1589
+ { input }
1590
+ );
1591
+ return jsonReply2({
1592
+ endpoint: result.saveEndpoint,
1593
+ previousGpuIds: current.gpuIds
1594
+ });
329
1595
  }
330
1596
  );
1597
+ }
1598
+
1599
+ // src/tools/pods.ts
1600
+ var import_zod7 = require("zod");
1601
+ function registerPodTools(server, rt) {
1602
+ const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
331
1603
  server.tool(
332
1604
  "list-pods",
1605
+ "List your pods (running and stopped) with optional filters \u2014 compute type, GPU type, data center, name \u2014 plus machine/network-volume expansion. Paginated via limit/cursor.",
333
1606
  {
334
1607
  ...listPaginationParams,
335
- computeType: import_zod2.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
336
- gpuTypeId: import_zod2.z.array(import_zod2.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
337
- dataCenterId: import_zod2.z.array(import_zod2.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
338
- name: import_zod2.z.string().optional().describe("Filter to Pods with the provided name"),
339
- includeMachine: import_zod2.z.boolean().optional().describe("Include information about the machine"),
340
- includeNetworkVolume: import_zod2.z.boolean().optional().describe("Include information about attached network volumes")
1608
+ computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
1609
+ gpuTypeId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
1610
+ dataCenterId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
1611
+ name: import_zod7.z.string().optional().describe("Filter to Pods with the provided name"),
1612
+ includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
1613
+ includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
341
1614
  },
1615
+ { title: "List pods", ...READ_ONLY },
342
1616
  async (params) => {
343
- const queryParams = new URLSearchParams();
344
- if (params.computeType)
345
- queryParams.append("computeType", params.computeType);
346
- if (params.gpuTypeId)
347
- params.gpuTypeId.forEach(
348
- (type) => queryParams.append("gpuTypeId", type)
349
- );
350
- if (params.dataCenterId)
351
- params.dataCenterId.forEach(
352
- (dc) => queryParams.append("dataCenterId", dc)
353
- );
354
- if (params.name) queryParams.append("name", params.name);
355
- if (params.includeMachine)
356
- queryParams.append("includeMachine", params.includeMachine.toString());
357
- if (params.includeNetworkVolume)
358
- queryParams.append(
359
- "includeNetworkVolume",
360
- params.includeNetworkVolume.toString()
361
- );
362
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
363
- const result = await runpodRequest(`/pods${queryString}`);
364
- return capListResult(result, {
1617
+ const backend = backendFor("pods");
1618
+ let queryString = "";
1619
+ if (backend.version === "v1") {
1620
+ const queryParams = new URLSearchParams();
1621
+ if (params.computeType)
1622
+ queryParams.append("computeType", params.computeType);
1623
+ if (params.gpuTypeId)
1624
+ params.gpuTypeId.forEach(
1625
+ (type) => queryParams.append("gpuTypeId", type)
1626
+ );
1627
+ if (params.dataCenterId)
1628
+ params.dataCenterId.forEach(
1629
+ (dc) => queryParams.append("dataCenterId", dc)
1630
+ );
1631
+ if (params.name) queryParams.append("name", params.name);
1632
+ if (params.includeMachine)
1633
+ queryParams.append(
1634
+ "includeMachine",
1635
+ params.includeMachine.toString()
1636
+ );
1637
+ if (params.includeNetworkVolume)
1638
+ queryParams.append(
1639
+ "includeNetworkVolume",
1640
+ params.includeNetworkVolume.toString()
1641
+ );
1642
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1643
+ }
1644
+ const result = await callRestUrl(
1645
+ `${backend.base}${backend.list}${queryString}`
1646
+ );
1647
+ return capListResult(backend.unwrap(result), {
365
1648
  limit: params.limit,
366
1649
  cursor: params.cursor
367
1650
  });
@@ -369,159 +1652,308 @@ function registerTools(server, ctx) {
369
1652
  );
370
1653
  server.tool(
371
1654
  "get-pod",
1655
+ "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
372
1656
  {
373
- podId: import_zod2.z.string().describe("ID of the pod to retrieve"),
374
- includeMachine: import_zod2.z.boolean().optional().describe("Include information about the machine"),
375
- includeNetworkVolume: import_zod2.z.boolean().optional().describe("Include information about attached network volumes")
1657
+ podId: import_zod7.z.string().describe("ID of the pod to retrieve"),
1658
+ includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
1659
+ includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
376
1660
  },
1661
+ { title: "Get pod", ...READ_ONLY },
377
1662
  async (params) => {
378
- const queryParams = new URLSearchParams();
379
- if (params.includeMachine)
380
- queryParams.append("includeMachine", params.includeMachine.toString());
381
- if (params.includeNetworkVolume)
382
- queryParams.append(
383
- "includeNetworkVolume",
384
- params.includeNetworkVolume.toString()
385
- );
386
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
387
- const result = await runpodRequest(`/pods/${params.podId}${queryString}`);
388
- return {
389
- content: [
390
- {
391
- type: "text",
392
- text: JSON.stringify(result, null, 2)
393
- }
394
- ]
395
- };
1663
+ const backend = backendFor("pods");
1664
+ let queryString = "";
1665
+ if (backend.version === "v1") {
1666
+ const queryParams = new URLSearchParams();
1667
+ if (params.includeMachine)
1668
+ queryParams.append(
1669
+ "includeMachine",
1670
+ params.includeMachine.toString()
1671
+ );
1672
+ if (params.includeNetworkVolume)
1673
+ queryParams.append(
1674
+ "includeNetworkVolume",
1675
+ params.includeNetworkVolume.toString()
1676
+ );
1677
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1678
+ }
1679
+ const result = await callRestUrl(
1680
+ `${backend.base}${backend.get(params.podId)}${queryString}`
1681
+ );
1682
+ return jsonReply2(result);
396
1683
  }
397
1684
  );
398
1685
  server.tool(
399
1686
  "create-pod",
400
- '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.',
1687
+ '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.',
401
1688
  {
402
- name: import_zod2.z.string().optional().describe("Name for the pod"),
403
- imageName: import_zod2.z.string().describe("Docker image to use"),
404
- cloudType: import_zod2.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
405
- gpuTypeIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of acceptable GPU types"),
406
- gpuCount: import_zod2.z.number().optional().describe("Number of GPUs"),
407
- containerDiskInGb: import_zod2.z.number().optional().describe("Container disk size in GB"),
408
- volumeInGb: import_zod2.z.number().optional().describe("Volume size in GB"),
409
- volumeMountPath: import_zod2.z.string().optional().describe("Path to mount the volume"),
410
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
411
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("Environment variables"),
412
- dataCenterIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of data centers")
1689
+ name: import_zod7.z.string().optional().describe("Name for the pod"),
1690
+ imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
1691
+ templateId: import_zod7.z.string().optional().describe(
1692
+ "Deploy from an existing template (see list-templates / get-template). The template supplies name, image, start command, ports, env, disk, volume, and registry credential as defaults; any field you also pass replaces the template value for that field (whole-field, not a merge). Override the registry credential with containerRegistryAuthId. Requires the v2 REST API. Still pass gpuTypeIds or computeType to choose the compute \u2014 a template does not pin the pod compute."
1693
+ ),
1694
+ computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe(
1695
+ "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
1696
+ ),
1697
+ cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1698
+ gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
1699
+ gpuCount: import_zod7.z.number().optional().describe("Number of GPUs"),
1700
+ containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
1701
+ volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
1702
+ volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
1703
+ ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1704
+ env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
1705
+ containerRegistryAuthId: import_zod7.z.string().optional().describe(
1706
+ "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)."
1707
+ ),
1708
+ dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
413
1709
  },
1710
+ { title: "Create pod", ...WRITE },
414
1711
  async (params) => {
415
- const result = await runpodRequest("/pods", "POST", params);
416
- return {
417
- content: [
418
- {
419
- type: "text",
420
- text: JSON.stringify(result, null, 2)
1712
+ const backend = backendFor("pods");
1713
+ if (params.templateId && backend.version !== "v2") {
1714
+ return jsonReply2({
1715
+ error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
1716
+ status: 501
1717
+ });
1718
+ }
1719
+ if (!params.imageName && !params.templateId) {
1720
+ return jsonReply2({
1721
+ error: "Provide imageName, or templateId to deploy from an existing template.",
1722
+ status: 400
1723
+ });
1724
+ }
1725
+ const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
1726
+ const hasGpuCount = (params.gpuCount ?? 0) > 0;
1727
+ const wantsCpu = params.computeType === "CPU";
1728
+ const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
1729
+ if (backend.version === "v2") {
1730
+ if (wantsGpu && wantsCpu) {
1731
+ return jsonReply2({
1732
+ error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
1733
+ status: 400
1734
+ });
1735
+ }
1736
+ if (!wantsGpu && !wantsCpu) {
1737
+ return jsonReply2({
1738
+ error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
1739
+ status: 400
1740
+ });
1741
+ }
1742
+ if (wantsGpu && !hasGpuTypes) {
1743
+ return jsonReply2({
1744
+ error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
1745
+ status: 400
1746
+ });
1747
+ }
1748
+ if (wantsCpu) {
1749
+ if (params.templateId) {
1750
+ return jsonReply2({
1751
+ error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
1752
+ status: 400
1753
+ });
421
1754
  }
422
- ]
423
- };
1755
+ try {
1756
+ const result = await callRestUrl(
1757
+ `${restV1Base(env)}/pods`,
1758
+ "POST",
1759
+ params
1760
+ );
1761
+ return jsonReply2({
1762
+ ...result,
1763
+ _servedBy: "v1",
1764
+ _note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
1765
+ });
1766
+ } catch (error) {
1767
+ if (error instanceof HttpError) {
1768
+ return jsonReply2({
1769
+ error: error.message,
1770
+ status: error.status,
1771
+ _servedBy: "v1",
1772
+ _note: "CPU pod create was routed to the v1 API and failed."
1773
+ });
1774
+ }
1775
+ throw error;
1776
+ }
1777
+ }
1778
+ }
1779
+ let body = backend.mapCreate(params);
1780
+ if (params.templateId) {
1781
+ let template;
1782
+ try {
1783
+ template = await callRestUrl(
1784
+ `${restV2Base(env)}/templates/${params.templateId}`
1785
+ );
1786
+ } catch (error) {
1787
+ if (error instanceof HttpError) {
1788
+ return jsonReply2({
1789
+ error: `Failed to load template ${params.templateId}: ${error.message}`,
1790
+ status: error.status
1791
+ });
1792
+ }
1793
+ throw error;
1794
+ }
1795
+ body = { ...podBodyFromTemplate(template), ...body };
1796
+ }
1797
+ try {
1798
+ const result = await callRestUrl(
1799
+ `${backend.base}${backend.list}`,
1800
+ "POST",
1801
+ body
1802
+ );
1803
+ if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
1804
+ return jsonReply2({
1805
+ ...result,
1806
+ _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(", ")}.`
1807
+ });
1808
+ }
1809
+ return jsonReply2(result);
1810
+ } catch (error) {
1811
+ if (error instanceof HttpError) {
1812
+ return jsonReply2({ error: error.message, status: error.status });
1813
+ }
1814
+ throw error;
1815
+ }
424
1816
  }
425
1817
  );
426
1818
  server.tool(
427
1819
  "update-pod",
1820
+ "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
428
1821
  {
429
- podId: import_zod2.z.string().describe("ID of the pod to update"),
430
- name: import_zod2.z.string().optional().describe("New name for the pod"),
431
- imageName: import_zod2.z.string().optional().describe("New Docker image"),
432
- containerDiskInGb: import_zod2.z.number().optional().describe("New container disk size in GB"),
433
- volumeInGb: import_zod2.z.number().optional().describe("New volume size in GB"),
434
- volumeMountPath: import_zod2.z.string().optional().describe("New path to mount the volume"),
435
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("New ports to expose"),
436
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("New environment variables")
1822
+ podId: import_zod7.z.string().describe("ID of the pod to update"),
1823
+ name: import_zod7.z.string().optional().describe("New name for the pod"),
1824
+ imageName: import_zod7.z.string().optional().describe("New Docker image"),
1825
+ containerDiskInGb: import_zod7.z.number().optional().describe("New container disk size in GB"),
1826
+ volumeInGb: import_zod7.z.number().optional().describe("New volume size in GB"),
1827
+ volumeMountPath: import_zod7.z.string().optional().describe("New path to mount the volume"),
1828
+ ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("New ports to expose"),
1829
+ env: import_zod7.z.record(import_zod7.z.string()).optional().describe("New environment variables")
437
1830
  },
1831
+ { title: "Update pod", ...WRITE, idempotentHint: true },
438
1832
  async (params) => {
439
1833
  const { podId, ...updateParams } = params;
440
- const result = await runpodRequest(
441
- `/pods/${podId}`,
1834
+ const backend = backendFor("pods");
1835
+ const body = backend.mapUpdate(updateParams);
1836
+ const result = await callRestUrl(
1837
+ `${backend.base}${backend.get(podId)}`,
442
1838
  "PATCH",
443
- updateParams
1839
+ body
444
1840
  );
445
- return {
446
- content: [
447
- {
448
- type: "text",
449
- text: JSON.stringify(result, null, 2)
450
- }
451
- ]
452
- };
1841
+ return jsonReply2(result);
453
1842
  }
454
1843
  );
455
1844
  server.tool(
456
1845
  "start-pod",
1846
+ "Start a stopped pod (resumes the same pod and its billing).",
457
1847
  {
458
- podId: import_zod2.z.string().describe("ID of the pod to start")
1848
+ podId: import_zod7.z.string().describe("ID of the pod to start")
459
1849
  },
1850
+ { title: "Start pod", ...WRITE, idempotentHint: true },
460
1851
  async (params) => {
461
- const result = await runpodRequest(`/pods/${params.podId}/start`, "POST");
462
- return {
463
- content: [
464
- {
465
- type: "text",
466
- text: JSON.stringify(result, null, 2)
467
- }
468
- ]
469
- };
1852
+ const result = await podAction(params.podId, "start");
1853
+ return jsonReply2(result);
470
1854
  }
471
1855
  );
472
1856
  server.tool(
473
1857
  "stop-pod",
1858
+ "Stop a running pod. The pod and its disk persist; GPU billing stops.",
474
1859
  {
475
- podId: import_zod2.z.string().describe("ID of the pod to stop")
1860
+ podId: import_zod7.z.string().describe("ID of the pod to stop")
476
1861
  },
1862
+ { title: "Stop pod", ...WRITE, idempotentHint: true },
477
1863
  async (params) => {
478
- const result = await runpodRequest(`/pods/${params.podId}/stop`, "POST");
479
- return {
480
- content: [
481
- {
482
- type: "text",
483
- text: JSON.stringify(result, null, 2)
484
- }
485
- ]
486
- };
1864
+ const result = await podAction(params.podId, "stop");
1865
+ return jsonReply2(result);
1866
+ }
1867
+ );
1868
+ server.tool(
1869
+ "restart-pod",
1870
+ "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1871
+ {
1872
+ podId: import_zod7.z.string().describe("ID of the pod to restart")
1873
+ },
1874
+ { title: "Restart pod", ...WRITE },
1875
+ async (params) => {
1876
+ const backend = backendFor("pods");
1877
+ if (backend.version === "v1") {
1878
+ return jsonReply2({
1879
+ error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
1880
+ status: 501
1881
+ });
1882
+ }
1883
+ const result = await podAction(params.podId, "restart");
1884
+ return jsonReply2(result);
487
1885
  }
488
1886
  );
1887
+ server.tool(
1888
+ "stream-pod-logs",
1889
+ "Fetch a bounded snapshot of a pod's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
1890
+ {
1891
+ podId: import_zod7.z.string().describe("ID of the pod whose logs to stream"),
1892
+ ...logStreamParams
1893
+ },
1894
+ { title: "Stream pod logs", ...READ_ONLY },
1895
+ (params) => streamLogsReply(
1896
+ rt,
1897
+ {
1898
+ name: "stream-pod-logs",
1899
+ resource: "pods",
1900
+ logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
1901
+ },
1902
+ params
1903
+ )
1904
+ );
489
1905
  server.tool(
490
1906
  "delete-pod",
1907
+ "Permanently delete a pod and its data. This cannot be undone.",
491
1908
  {
492
- podId: import_zod2.z.string().describe("ID of the pod to delete")
1909
+ podId: import_zod7.z.string().describe("ID of the pod to delete")
493
1910
  },
1911
+ { title: "Delete pod", ...DESTRUCTIVE },
494
1912
  async (params) => {
495
- const result = await runpodRequest(`/pods/${params.podId}`, "DELETE");
496
- return {
497
- content: [
498
- {
499
- type: "text",
500
- text: JSON.stringify(result, null, 2)
501
- }
502
- ]
503
- };
1913
+ const backend = backendFor("pods");
1914
+ const result = await callRestUrl(
1915
+ `${backend.base}${backend.get(params.podId)}`,
1916
+ "DELETE"
1917
+ );
1918
+ return jsonReply2(result);
504
1919
  }
505
1920
  );
1921
+ }
1922
+
1923
+ // src/tools/endpoints.ts
1924
+ var import_zod8 = require("zod");
1925
+ function registerEndpointTools(server, rt) {
1926
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
506
1927
  server.tool(
507
1928
  "list-endpoints",
1929
+ "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
508
1930
  {
509
1931
  ...listPaginationParams,
510
- includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
511
- includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
1932
+ includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
1933
+ includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
512
1934
  },
1935
+ { title: "List endpoints", ...READ_ONLY },
513
1936
  async (params) => {
514
- const queryParams = new URLSearchParams();
515
- if (params.includeTemplate)
516
- queryParams.append(
517
- "includeTemplate",
518
- params.includeTemplate.toString()
519
- );
520
- if (params.includeWorkers)
521
- queryParams.append("includeWorkers", params.includeWorkers.toString());
522
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
523
- const result = await runpodRequest(`/endpoints${queryString}`);
524
- return capListResult(result, {
1937
+ const backend = backendFor("endpoints");
1938
+ let queryString = "";
1939
+ if (backend.version === "v1") {
1940
+ const queryParams = new URLSearchParams();
1941
+ if (params.includeTemplate)
1942
+ queryParams.append(
1943
+ "includeTemplate",
1944
+ params.includeTemplate.toString()
1945
+ );
1946
+ if (params.includeWorkers)
1947
+ queryParams.append(
1948
+ "includeWorkers",
1949
+ params.includeWorkers.toString()
1950
+ );
1951
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1952
+ }
1953
+ const result = await callRestUrl(
1954
+ `${backend.base}${backend.list}${queryString}`
1955
+ );
1956
+ return capListResult(backend.unwrap(result), {
525
1957
  limit: params.limit,
526
1958
  cursor: params.cursor
527
1959
  });
@@ -529,124 +1961,275 @@ function registerTools(server, ctx) {
529
1961
  );
530
1962
  server.tool(
531
1963
  "get-endpoint",
1964
+ "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
532
1965
  {
533
- endpointId: import_zod2.z.string().describe("ID of the endpoint to retrieve"),
534
- includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
535
- includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
1966
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
1967
+ includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
1968
+ includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
536
1969
  },
1970
+ { title: "Get endpoint", ...READ_ONLY },
537
1971
  async (params) => {
538
- const queryParams = new URLSearchParams();
539
- if (params.includeTemplate)
540
- queryParams.append(
541
- "includeTemplate",
542
- params.includeTemplate.toString()
543
- );
544
- if (params.includeWorkers)
545
- queryParams.append("includeWorkers", params.includeWorkers.toString());
546
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
547
- const result = await runpodRequest(
548
- `/endpoints/${params.endpointId}${queryString}`
1972
+ const backend = backendFor("endpoints");
1973
+ let queryString = "";
1974
+ if (backend.version === "v1") {
1975
+ const queryParams = new URLSearchParams();
1976
+ if (params.includeTemplate)
1977
+ queryParams.append(
1978
+ "includeTemplate",
1979
+ params.includeTemplate.toString()
1980
+ );
1981
+ if (params.includeWorkers)
1982
+ queryParams.append(
1983
+ "includeWorkers",
1984
+ params.includeWorkers.toString()
1985
+ );
1986
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1987
+ }
1988
+ const result = await callRestUrl(
1989
+ `${backend.base}${backend.get(params.endpointId)}${queryString}`
549
1990
  );
550
- return {
551
- content: [
552
- {
553
- type: "text",
554
- text: JSON.stringify(result, null, 2)
555
- }
556
- ]
557
- };
1991
+ return jsonReply2(result);
558
1992
  }
559
1993
  );
560
1994
  server.tool(
561
1995
  "create-endpoint",
1996
+ "Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero).",
562
1997
  {
563
- name: import_zod2.z.string().optional().describe("Name for the endpoint"),
564
- templateId: import_zod2.z.string().describe("Template ID to use"),
565
- computeType: import_zod2.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint"),
566
- gpuTypeIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of acceptable GPU types"),
567
- gpuCount: import_zod2.z.number().optional().describe("Number of GPUs per worker"),
568
- workersMin: import_zod2.z.number().optional().describe("Minimum number of workers"),
569
- workersMax: import_zod2.z.number().optional().describe("Maximum number of workers"),
570
- dataCenterIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of data centers")
1998
+ name: import_zod8.z.string().optional().describe("Name for the endpoint"),
1999
+ // --- v2 inline-config fields ---
2000
+ imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
2001
+ gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
2002
+ 'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
2003
+ ),
2004
+ gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
2005
+ args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
2006
+ containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
2007
+ ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
2008
+ env: import_zod8.z.record(import_zod8.z.string()).optional().describe("Environment variables (v2)"),
2009
+ containerRegistryAuthId: import_zod8.z.string().optional().describe("Container registry auth id for a private image (v2)"),
2010
+ networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("Network volume ids to attach (v2)"),
2011
+ executionTimeoutMs: import_zod8.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
2012
+ flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
2013
+ // --- v1 template-based fields ---
2014
+ templateId: import_zod8.z.string().optional().describe("Template ID (v1). Required on v1."),
2015
+ computeType: import_zod8.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
2016
+ gpuTypeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of acceptable GPU types (v1)"),
2017
+ // --- shared autoscaling fields ---
2018
+ workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
2019
+ workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
2020
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
2021
+ scalerValue: import_zod8.z.number().optional().describe("Autoscaler target value"),
2022
+ idleTimeout: import_zod8.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
2023
+ dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
571
2024
  },
2025
+ { title: "Create endpoint", ...WRITE },
572
2026
  async (params) => {
573
- const result = await runpodRequest("/endpoints", "POST", params);
574
- return {
575
- content: [
576
- {
577
- type: "text",
578
- text: JSON.stringify(result, null, 2)
579
- }
580
- ]
581
- };
2027
+ const backend = backendFor("endpoints");
2028
+ if (backend.version === "v2") {
2029
+ if (!params.name) {
2030
+ return jsonReply2({
2031
+ error: "On the v2 REST API, create-endpoint requires a name.",
2032
+ status: 400
2033
+ });
2034
+ }
2035
+ if (!params.imageName) {
2036
+ return jsonReply2({
2037
+ error: "On the v2 REST API, create-endpoint needs an imageName (v2 endpoints are image-based, not template-based). To use a templateId, set RUNPOD_REST_VERSION=v1.",
2038
+ status: 400
2039
+ });
2040
+ }
2041
+ if (!params.gpuPoolIds?.length) {
2042
+ return jsonReply2({
2043
+ error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
2044
+ status: 400
2045
+ });
2046
+ }
2047
+ const body = backend.mapCreate(params);
2048
+ const result2 = await callRestUrl(
2049
+ `${backend.base}${backend.list}`,
2050
+ "POST",
2051
+ body
2052
+ );
2053
+ return jsonReply2(result2);
2054
+ }
2055
+ if (!params.templateId) {
2056
+ return jsonReply2({
2057
+ error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
2058
+ status: 400
2059
+ });
2060
+ }
2061
+ const v1Body = {};
2062
+ for (const k of [
2063
+ "templateId",
2064
+ "name",
2065
+ "computeType",
2066
+ "gpuTypeIds",
2067
+ "gpuCount",
2068
+ "workersMin",
2069
+ "workersMax",
2070
+ "dataCenterIds"
2071
+ ]) {
2072
+ if (params[k] !== void 0) v1Body[k] = params[k];
2073
+ }
2074
+ const result = await callRestUrl(
2075
+ `${backend.base}${backend.list}`,
2076
+ "POST",
2077
+ v1Body
2078
+ );
2079
+ return jsonReply2(result);
582
2080
  }
583
2081
  );
584
2082
  server.tool(
585
2083
  "update-endpoint",
2084
+ "Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change.",
586
2085
  {
587
- endpointId: import_zod2.z.string().describe("ID of the endpoint to update"),
588
- name: import_zod2.z.string().optional().describe("New name for the endpoint"),
589
- workersMin: import_zod2.z.number().optional().describe("New minimum number of workers"),
590
- workersMax: import_zod2.z.number().optional().describe("New maximum number of workers"),
591
- idleTimeout: import_zod2.z.number().optional().describe("New idle timeout in seconds"),
592
- scalerType: import_zod2.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
593
- scalerValue: import_zod2.z.number().optional().describe("Scaler value")
2086
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
2087
+ name: import_zod8.z.string().optional().describe("New name for the endpoint"),
2088
+ workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
2089
+ workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
2090
+ idleTimeout: import_zod8.z.number().optional().describe("New idle timeout in seconds"),
2091
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2092
+ scalerValue: import_zod8.z.number().optional().describe("Scaler value"),
2093
+ // --- v2-only inline-config fields ---
2094
+ imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
2095
+ gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
2096
+ gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
2097
+ args: import_zod8.z.string().optional().describe("New container args (v2)"),
2098
+ containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
2099
+ ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
2100
+ env: import_zod8.z.record(import_zod8.z.string()).optional().describe("New environment variables (v2)"),
2101
+ containerRegistryAuthId: import_zod8.z.string().optional().describe("New container registry auth id (v2)"),
2102
+ networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("New network volume ids (v2)"),
2103
+ executionTimeoutMs: import_zod8.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
2104
+ flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
594
2105
  },
2106
+ { title: "Update endpoint", ...WRITE, idempotentHint: true },
595
2107
  async (params) => {
596
2108
  const { endpointId, ...updateParams } = params;
597
- const result = await runpodRequest(
598
- `/endpoints/${endpointId}`,
2109
+ const backend = backendFor("endpoints");
2110
+ const body = backend.mapUpdate(updateParams);
2111
+ const result = await callRestUrl(
2112
+ `${backend.base}${backend.get(endpointId)}`,
599
2113
  "PATCH",
600
- updateParams
2114
+ body
601
2115
  );
602
- return {
603
- content: [
604
- {
605
- type: "text",
606
- text: JSON.stringify(result, null, 2)
607
- }
608
- ]
609
- };
2116
+ return jsonReply2(result);
610
2117
  }
611
2118
  );
612
2119
  server.tool(
613
2120
  "delete-endpoint",
2121
+ "Permanently delete a Serverless endpoint. This cannot be undone.",
614
2122
  {
615
- endpointId: import_zod2.z.string().describe("ID of the endpoint to delete")
2123
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to delete")
616
2124
  },
2125
+ { title: "Delete endpoint", ...DESTRUCTIVE },
617
2126
  async (params) => {
618
- const result = await runpodRequest(
619
- `/endpoints/${params.endpointId}`,
2127
+ const backend = backendFor("endpoints");
2128
+ const result = await callRestUrl(
2129
+ `${backend.base}${backend.get(params.endpointId)}`,
620
2130
  "DELETE"
621
2131
  );
622
- return {
623
- content: [
624
- {
625
- type: "text",
626
- text: JSON.stringify(result, null, 2)
627
- }
628
- ]
629
- };
2132
+ return jsonReply2(result);
2133
+ }
2134
+ );
2135
+ server.tool(
2136
+ "list-endpoint-releases",
2137
+ "List a Serverless endpoint's release history and current rollout status (workers on the latest version). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2138
+ {
2139
+ ...listPaginationParams,
2140
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose releases to list")
2141
+ },
2142
+ { title: "List endpoint releases", ...READ_ONLY },
2143
+ async (params) => {
2144
+ const backend = backendFor("endpoints");
2145
+ if (backend.version === "v1") {
2146
+ return jsonReply2({
2147
+ error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2148
+ status: 501
2149
+ });
2150
+ }
2151
+ const raw = await callRestUrl(
2152
+ `${backend.base}/serverless/${params.endpointId}/releases`
2153
+ );
2154
+ const releases = Array.isArray(raw?.releases) ? raw.releases : [];
2155
+ return capListResult(
2156
+ releases,
2157
+ { limit: params.limit, cursor: params.cursor },
2158
+ { rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
2159
+ );
2160
+ }
2161
+ );
2162
+ server.tool(
2163
+ "list-endpoint-workers",
2164
+ "List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2165
+ {
2166
+ ...listPaginationParams,
2167
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
2168
+ },
2169
+ { title: "List endpoint workers", ...READ_ONLY },
2170
+ async (params) => {
2171
+ const backend = backendFor("workers");
2172
+ if (backend.version === "v1") {
2173
+ return jsonReply2({
2174
+ error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
2175
+ status: 501
2176
+ });
2177
+ }
2178
+ const raw = await callRestUrl(
2179
+ `${backend.base}/serverless/${params.endpointId}/workers`
2180
+ );
2181
+ return capListResult(
2182
+ backend.unwrap(raw),
2183
+ { limit: params.limit, cursor: params.cursor },
2184
+ { summary: raw?.summary, endpointVersion: raw?.endpointVersion }
2185
+ );
630
2186
  }
631
2187
  );
632
- const endpointIdSchema = import_zod2.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
633
- const jobIdSchema = import_zod2.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
634
- const inputSchema = import_zod2.z.record(import_zod2.z.unknown()).describe(
2188
+ server.tool(
2189
+ "stream-worker-logs",
2190
+ "Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
2191
+ {
2192
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
2193
+ workerId: import_zod8.z.string().describe(
2194
+ "ID of the worker whose logs to stream (from list-endpoint-workers)"
2195
+ ),
2196
+ ...logStreamParams
2197
+ },
2198
+ { title: "Stream worker logs", ...READ_ONLY },
2199
+ (params) => streamLogsReply(
2200
+ rt,
2201
+ {
2202
+ name: "stream-worker-logs",
2203
+ resource: "workers",
2204
+ logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
2205
+ },
2206
+ params
2207
+ )
2208
+ );
2209
+ }
2210
+
2211
+ // src/tools/jobs.ts
2212
+ var import_zod9 = require("zod");
2213
+ function registerJobTools(server, rt) {
2214
+ const { jsonReply: jsonReply2, serverlessRequest } = rt;
2215
+ const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2216
+ const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2217
+ const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
635
2218
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
636
2219
  );
637
- const webhookSchema = import_zod2.z.string().url().optional().describe(
2220
+ const webhookSchema = import_zod9.z.string().url().optional().describe(
638
2221
  "Webhook URL to receive job completion notifications instead of polling"
639
2222
  );
640
- const policySchema = import_zod2.z.object({
641
- executionTimeout: import_zod2.z.number().optional().describe("Maximum execution time in milliseconds"),
642
- lowPriority: import_zod2.z.boolean().optional().describe("Submit as a low-priority job"),
643
- ttl: import_zod2.z.number().optional().describe("Time-to-live for the job result in milliseconds")
2223
+ const policySchema = import_zod9.z.object({
2224
+ executionTimeout: import_zod9.z.number().optional().describe("Maximum execution time in milliseconds"),
2225
+ lowPriority: import_zod9.z.boolean().optional().describe("Submit as a low-priority job"),
2226
+ ttl: import_zod9.z.number().optional().describe("Time-to-live for the job result in milliseconds")
644
2227
  }).optional().describe("Execution policy options");
645
- const s3ConfigSchema = import_zod2.z.object({
646
- accessId: import_zod2.z.string().describe("S3 access key ID"),
647
- accessSecret: import_zod2.z.string().describe("S3 secret access key"),
648
- bucketName: import_zod2.z.string().describe("S3 bucket name"),
649
- endpointUrl: import_zod2.z.string().describe("S3 endpoint URL")
2228
+ const s3ConfigSchema = import_zod9.z.object({
2229
+ accessId: import_zod9.z.string().describe("S3 access key ID"),
2230
+ accessSecret: import_zod9.z.string().describe("S3 secret access key"),
2231
+ bucketName: import_zod9.z.string().describe("S3 bucket name"),
2232
+ endpointUrl: import_zod9.z.string().describe("S3 endpoint URL")
650
2233
  }).optional().describe("S3-compatible storage config for large outputs");
651
2234
  server.tool(
652
2235
  "run-endpoint",
@@ -660,6 +2243,7 @@ function registerTools(server, ctx) {
660
2243
  policy: policySchema,
661
2244
  s3Config: s3ConfigSchema
662
2245
  },
2246
+ { title: "Run endpoint (async)", ...WRITE },
663
2247
  async (params) => {
664
2248
  const { endpointId, ...body } = params;
665
2249
  const result = await serverlessRequest(
@@ -668,14 +2252,7 @@ function registerTools(server, ctx) {
668
2252
  "POST",
669
2253
  body
670
2254
  );
671
- return {
672
- content: [
673
- {
674
- type: "text",
675
- text: JSON.stringify(result, null, 2)
676
- }
677
- ]
678
- };
2255
+ return jsonReply2(result);
679
2256
  }
680
2257
  );
681
2258
  server.tool(
@@ -686,13 +2263,14 @@ function registerTools(server, ctx) {
686
2263
  "ID of the Serverless endpoint to run synchronously"
687
2264
  ),
688
2265
  input: inputSchema,
689
- wait: import_zod2.z.number().min(1e3).max(3e5).optional().describe(
2266
+ wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
690
2267
  "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)."
691
2268
  ),
692
2269
  webhook: webhookSchema,
693
2270
  policy: policySchema,
694
2271
  s3Config: s3ConfigSchema
695
2272
  },
2273
+ { title: "Run endpoint (sync)", ...WRITE },
696
2274
  async (params) => {
697
2275
  const { endpointId, wait, ...body } = params;
698
2276
  const path = wait ? `/runsync?wait=${wait}` : "/runsync";
@@ -702,14 +2280,7 @@ function registerTools(server, ctx) {
702
2280
  "POST",
703
2281
  body
704
2282
  );
705
- return {
706
- content: [
707
- {
708
- type: "text",
709
- text: JSON.stringify(result, null, 2)
710
- }
711
- ]
712
- };
2283
+ return jsonReply2(result);
713
2284
  }
714
2285
  );
715
2286
  server.tool(
@@ -721,19 +2292,13 @@ function registerTools(server, ctx) {
721
2292
  ),
722
2293
  jobId: jobIdSchema.describe("ID of the job to check")
723
2294
  },
2295
+ { title: "Get job status", ...READ_ONLY },
724
2296
  async (params) => {
725
2297
  const result = await serverlessRequest(
726
2298
  params.endpointId,
727
2299
  `/status/${params.jobId}`
728
2300
  );
729
- return {
730
- content: [
731
- {
732
- type: "text",
733
- text: JSON.stringify(result, null, 2)
734
- }
735
- ]
736
- };
2301
+ return jsonReply2(result);
737
2302
  }
738
2303
  );
739
2304
  server.tool(
@@ -745,6 +2310,7 @@ function registerTools(server, ctx) {
745
2310
  ),
746
2311
  jobId: jobIdSchema.describe("ID of the job to stream results from")
747
2312
  },
2313
+ { title: "Stream job", ...READ_ONLY },
748
2314
  async (params) => {
749
2315
  const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
750
2316
  "COMPLETED",
@@ -790,18 +2356,7 @@ function registerTools(server, ctx) {
790
2356
  }
791
2357
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
792
2358
  }
793
- return {
794
- content: [
795
- {
796
- type: "text",
797
- text: JSON.stringify(
798
- { ...finalResult, stream: allChunks },
799
- null,
800
- 2
801
- )
802
- }
803
- ]
804
- };
2359
+ return jsonReply2({ ...finalResult, stream: allChunks });
805
2360
  }
806
2361
  );
807
2362
  server.tool(
@@ -813,20 +2368,14 @@ function registerTools(server, ctx) {
813
2368
  ),
814
2369
  jobId: jobIdSchema.describe("ID of the job to cancel")
815
2370
  },
2371
+ { title: "Cancel job", ...WRITE, idempotentHint: true },
816
2372
  async (params) => {
817
2373
  const result = await serverlessRequest(
818
2374
  params.endpointId,
819
2375
  `/cancel/${params.jobId}`,
820
2376
  "POST"
821
2377
  );
822
- return {
823
- content: [
824
- {
825
- type: "text",
826
- text: JSON.stringify(result, null, 2)
827
- }
828
- ]
829
- };
2378
+ return jsonReply2(result);
830
2379
  }
831
2380
  );
832
2381
  server.tool(
@@ -838,20 +2387,14 @@ function registerTools(server, ctx) {
838
2387
  ),
839
2388
  jobId: jobIdSchema.describe("ID of the job to retry")
840
2389
  },
2390
+ { title: "Retry job", ...WRITE },
841
2391
  async (params) => {
842
2392
  const result = await serverlessRequest(
843
2393
  params.endpointId,
844
2394
  `/retry/${params.jobId}`,
845
2395
  "POST"
846
2396
  );
847
- return {
848
- content: [
849
- {
850
- type: "text",
851
- text: JSON.stringify(result, null, 2)
852
- }
853
- ]
854
- };
2397
+ return jsonReply2(result);
855
2398
  }
856
2399
  );
857
2400
  server.tool(
@@ -862,16 +2405,10 @@ function registerTools(server, ctx) {
862
2405
  "ID of the Serverless endpoint to check health for"
863
2406
  )
864
2407
  },
2408
+ { title: "Endpoint health", ...READ_ONLY },
865
2409
  async (params) => {
866
2410
  const result = await serverlessRequest(params.endpointId, "/health");
867
- return {
868
- content: [
869
- {
870
- type: "text",
871
- text: JSON.stringify(result, null, 2)
872
- }
873
- ]
874
- };
2411
+ return jsonReply2(result);
875
2412
  }
876
2413
  );
877
2414
  server.tool(
@@ -882,46 +2419,51 @@ function registerTools(server, ctx) {
882
2419
  "ID of the Serverless endpoint to purge the queue for"
883
2420
  )
884
2421
  },
2422
+ { title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
885
2423
  async (params) => {
886
2424
  const result = await serverlessRequest(
887
2425
  params.endpointId,
888
2426
  "/purge-queue",
889
2427
  "POST"
890
2428
  );
891
- return {
892
- content: [
893
- {
894
- type: "text",
895
- text: JSON.stringify(result, null, 2)
896
- }
897
- ]
898
- };
2429
+ return jsonReply2(result);
899
2430
  }
900
2431
  );
2432
+ }
2433
+
2434
+ // src/tools/templates.ts
2435
+ var import_zod10 = require("zod");
2436
+ function registerTemplateTools(server, rt) {
2437
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
901
2438
  server.tool(
902
2439
  "list-templates",
903
2440
  `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.`,
904
2441
  {
905
2442
  ...listPaginationParams,
906
- includeRunpodTemplates: import_zod2.z.boolean().optional().describe("Include official Runpod templates in the response"),
907
- includePublicTemplates: import_zod2.z.boolean().optional().describe("Include community-made public templates in the response"),
908
- includeEndpointBoundTemplates: import_zod2.z.boolean().optional().describe(
2443
+ includeRunpodTemplates: import_zod10.z.boolean().optional().describe("Include official Runpod templates in the response"),
2444
+ includePublicTemplates: import_zod10.z.boolean().optional().describe("Include community-made public templates in the response"),
2445
+ includeEndpointBoundTemplates: import_zod10.z.boolean().optional().describe(
909
2446
  "Include templates bound to Serverless endpoints in the response"
910
2447
  )
911
2448
  },
2449
+ { title: "List templates", ...READ_ONLY },
912
2450
  async (params) => {
913
- const queryParams = new URLSearchParams();
914
- if (params.includeRunpodTemplates)
915
- queryParams.set("includeRunpodTemplates", "true");
916
- if (params.includePublicTemplates)
917
- queryParams.set("includePublicTemplates", "true");
918
- if (params.includeEndpointBoundTemplates)
919
- queryParams.set("includeEndpointBoundTemplates", "true");
920
- const query = queryParams.toString();
921
- const result = await runpodRequest(
922
- `/templates${query ? `?${query}` : ""}`
2451
+ const backend = backendFor("templates");
2452
+ let query = "";
2453
+ if (backend.version === "v1") {
2454
+ const queryParams = new URLSearchParams();
2455
+ if (params.includeRunpodTemplates)
2456
+ queryParams.set("includeRunpodTemplates", "true");
2457
+ if (params.includePublicTemplates)
2458
+ queryParams.set("includePublicTemplates", "true");
2459
+ if (params.includeEndpointBoundTemplates)
2460
+ queryParams.set("includeEndpointBoundTemplates", "true");
2461
+ query = queryParams.toString() ? `?${queryParams.toString()}` : "";
2462
+ }
2463
+ const result = await callRestUrl(
2464
+ `${backend.base}${backend.list}${query}`
923
2465
  );
924
- return capListResult(result, {
2466
+ return capListResult(backend.unwrap(result), {
925
2467
  limit: params.limit,
926
2468
  cursor: params.cursor
927
2469
  });
@@ -929,196 +2471,223 @@ function registerTools(server, ctx) {
929
2471
  );
930
2472
  server.tool(
931
2473
  "get-template",
2474
+ "Get one template by id (image, env, ports, disk/volume config).",
932
2475
  {
933
- templateId: import_zod2.z.string().describe("ID of the template to retrieve")
2476
+ templateId: import_zod10.z.string().describe("ID of the template to retrieve")
934
2477
  },
2478
+ { title: "Get template", ...READ_ONLY },
935
2479
  async (params) => {
936
- const result = await runpodRequest(`/templates/${params.templateId}`);
937
- return {
938
- content: [
939
- {
940
- type: "text",
941
- text: JSON.stringify(result, null, 2)
942
- }
943
- ]
944
- };
2480
+ const backend = backendFor("templates");
2481
+ const result = await callRestUrl(
2482
+ `${backend.base}${backend.get(params.templateId)}`
2483
+ );
2484
+ return jsonReply2(result);
945
2485
  }
946
2486
  );
947
2487
  server.tool(
948
2488
  "create-template",
2489
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
949
2490
  {
950
- name: import_zod2.z.string().describe("Name for the template"),
951
- imageName: import_zod2.z.string().describe("Docker image to use"),
952
- isServerless: import_zod2.z.boolean().optional().describe("Is this a serverless template"),
953
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("Ports to expose"),
954
- dockerEntrypoint: import_zod2.z.array(import_zod2.z.string()).optional().describe("Docker entrypoint commands"),
955
- dockerStartCmd: import_zod2.z.array(import_zod2.z.string()).optional().describe("Docker start commands"),
956
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("Environment variables"),
957
- containerDiskInGb: import_zod2.z.number().optional().describe("Container disk size in GB"),
958
- volumeInGb: import_zod2.z.number().optional().describe("Volume size in GB"),
959
- volumeMountPath: import_zod2.z.string().optional().describe("Path to mount the volume"),
960
- readme: import_zod2.z.string().optional().describe("README content in markdown format"),
961
- containerRegistryAuthId: import_zod2.z.string().optional().describe(
2491
+ name: import_zod10.z.string().describe("Name for the template"),
2492
+ imageName: import_zod10.z.string().describe("Docker image to use"),
2493
+ isServerless: import_zod10.z.boolean().optional().describe("Is this a serverless template"),
2494
+ ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("Ports to expose"),
2495
+ dockerEntrypoint: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2496
+ "Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
2497
+ ),
2498
+ dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2499
+ 'Container start command. On v2 this is stored as the template\'s `args` string; a multi-element array is joined with spaces (e.g. ["python","-u","app.py"] \u2192 "python -u app.py"). The join is lossy: an element that itself contains spaces is NOT preserved as one argument \u2014 e.g. ["sh","-c","echo hi"] becomes "sh -c echo hi", so pass a shell command as a single pre-quoted element if that distinction matters. Omit or pass an empty array to leave the command unset (never overwrites with "").'
2500
+ ),
2501
+ env: import_zod10.z.record(import_zod10.z.string()).optional().describe("Environment variables"),
2502
+ containerDiskInGb: import_zod10.z.number().optional().describe("Container disk size in GB"),
2503
+ volumeInGb: import_zod10.z.number().optional().describe("Volume size in GB"),
2504
+ volumeMountPath: import_zod10.z.string().optional().describe("Path to mount the volume"),
2505
+ readme: import_zod10.z.string().optional().describe("README content in markdown format"),
2506
+ category: import_zod10.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2507
+ "How the template is grouped and filtered in the Runpod console \u2014 it does NOT affect hardware selection, scheduling, or billing. Optional; defaults to NVIDIA. Set CPU or AMD only to file the template correctly in the console."
2508
+ ),
2509
+ containerRegistryAuthId: import_zod10.z.string().optional().describe(
962
2510
  "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."
963
2511
  )
964
2512
  },
2513
+ { title: "Create template", ...WRITE },
965
2514
  async (params) => {
966
- const result = await runpodRequest("/templates", "POST", params);
967
- return {
968
- content: [
969
- {
970
- type: "text",
971
- text: JSON.stringify(result, null, 2)
972
- }
973
- ]
974
- };
2515
+ const backend = backendFor("templates");
2516
+ const body = backend.mapCreate(params);
2517
+ const result = await callRestUrl(
2518
+ `${backend.base}${backend.list}`,
2519
+ "POST",
2520
+ body
2521
+ );
2522
+ return jsonReply2(result);
975
2523
  }
976
2524
  );
977
2525
  server.tool(
978
2526
  "update-template",
2527
+ "Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
979
2528
  {
980
- templateId: import_zod2.z.string().describe("ID of the template to update"),
981
- name: import_zod2.z.string().optional().describe("New name for the template"),
982
- imageName: import_zod2.z.string().optional().describe("New Docker image"),
983
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("New ports to expose"),
984
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("New environment variables"),
985
- readme: import_zod2.z.string().optional().describe("New README content in markdown format"),
986
- containerRegistryAuthId: import_zod2.z.string().optional().describe(
2529
+ templateId: import_zod10.z.string().describe("ID of the template to update"),
2530
+ name: import_zod10.z.string().optional().describe("New name for the template"),
2531
+ imageName: import_zod10.z.string().optional().describe("New Docker image"),
2532
+ dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2533
+ "New container start command. On v2 stored as the template's `args` string; a multi-element array is joined with spaces (lossy \u2014 an element containing spaces is split into separate tokens). Note: this can only set a command, not clear one \u2014 an empty or absent array is ignored so the existing command is preserved."
2534
+ ),
2535
+ ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("New ports to expose"),
2536
+ env: import_zod10.z.record(import_zod10.z.string()).optional().describe("New environment variables"),
2537
+ readme: import_zod10.z.string().optional().describe("New README content in markdown format"),
2538
+ containerRegistryAuthId: import_zod10.z.string().optional().describe(
987
2539
  "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."
988
2540
  )
989
2541
  },
2542
+ { title: "Update template", ...WRITE, idempotentHint: true },
990
2543
  async (params) => {
991
2544
  const { templateId, ...updateParams } = params;
992
- const result = await runpodRequest(
993
- `/templates/${templateId}`,
2545
+ const backend = backendFor("templates");
2546
+ const body = backend.mapUpdate(updateParams);
2547
+ const result = await callRestUrl(
2548
+ `${backend.base}${backend.get(templateId)}`,
994
2549
  "PATCH",
995
- updateParams
2550
+ body
996
2551
  );
997
- return {
998
- content: [
999
- {
1000
- type: "text",
1001
- text: JSON.stringify(result, null, 2)
1002
- }
1003
- ]
1004
- };
2552
+ return jsonReply2(result);
1005
2553
  }
1006
2554
  );
1007
2555
  server.tool(
1008
2556
  "delete-template",
2557
+ "Permanently delete a template. This cannot be undone.",
1009
2558
  {
1010
- templateId: import_zod2.z.string().describe("ID of the template to delete")
2559
+ templateId: import_zod10.z.string().describe("ID of the template to delete")
1011
2560
  },
2561
+ { title: "Delete template", ...DESTRUCTIVE },
1012
2562
  async (params) => {
1013
- const result = await runpodRequest(
1014
- `/templates/${params.templateId}`,
2563
+ const backend = backendFor("templates");
2564
+ const result = await callRestUrl(
2565
+ `${backend.base}${backend.get(params.templateId)}`,
1015
2566
  "DELETE"
1016
2567
  );
1017
- return {
1018
- content: [
1019
- {
1020
- type: "text",
1021
- text: JSON.stringify(result, null, 2)
1022
- }
1023
- ]
1024
- };
2568
+ return jsonReply2(result);
2569
+ }
2570
+ );
2571
+ }
2572
+
2573
+ // src/tools/network-volumes.ts
2574
+ var import_zod11 = require("zod");
2575
+ function registerNetworkVolumeTools(server, rt) {
2576
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2577
+ server.tool(
2578
+ "list-network-volumes",
2579
+ "List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
2580
+ listPaginationParams,
2581
+ { title: "List network volumes", ...READ_ONLY },
2582
+ async (params) => {
2583
+ const backend = backendFor("networkVolumes");
2584
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
2585
+ return capListResult(backend.unwrap(result), {
2586
+ limit: params.limit,
2587
+ cursor: params.cursor
2588
+ });
1025
2589
  }
1026
2590
  );
1027
- server.tool("list-network-volumes", listPaginationParams, async (params) => {
1028
- const result = await runpodRequest("/networkvolumes");
1029
- return capListResult(result, {
1030
- limit: params.limit,
1031
- cursor: params.cursor
1032
- });
1033
- });
1034
2591
  server.tool(
1035
2592
  "get-network-volume",
2593
+ "Get one network volume by id (name, size, data center).",
1036
2594
  {
1037
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to retrieve")
2595
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to retrieve")
1038
2596
  },
2597
+ { title: "Get network volume", ...READ_ONLY },
1039
2598
  async (params) => {
1040
- const result = await runpodRequest(
1041
- `/networkvolumes/${params.networkVolumeId}`
2599
+ const backend = backendFor("networkVolumes");
2600
+ const result = await callRestUrl(
2601
+ `${backend.base}${backend.get(params.networkVolumeId)}`
1042
2602
  );
1043
- return {
1044
- content: [
1045
- {
1046
- type: "text",
1047
- text: JSON.stringify(result, null, 2)
1048
- }
1049
- ]
1050
- };
2603
+ return jsonReply2(result);
1051
2604
  }
1052
2605
  );
1053
2606
  server.tool(
1054
2607
  "create-network-volume",
2608
+ "Create a persistent network volume in a data center (size 10\u20134096 GB) that pods can mount. Pass volumeType to pick a storage tier; omit it to get the data center default.",
1055
2609
  {
1056
- name: import_zod2.z.string().describe("Name for the network volume"),
1057
- size: import_zod2.z.number().describe("Size in GB (1-4000)"),
1058
- dataCenterId: import_zod2.z.string().describe("Data center ID")
2610
+ name: import_zod11.z.string().describe("Name for the network volume"),
2611
+ // Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
2612
+ // here as well as described, so an out-of-range value fails locally
2613
+ // instead of costing an API roundtrip.
2614
+ size: import_zod11.z.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
2615
+ dataCenterId: import_zod11.z.string().describe("Data center ID (see list-data-centers)"),
2616
+ volumeType: import_zod11.z.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
2617
+ "Storage tier. Omit for the data center's default (primary) tier. HIGH_PERFORMANCE provisions an HPS volume. Immutable after creation \u2014 update-network-volume cannot change it."
2618
+ )
1059
2619
  },
2620
+ { title: "Create network volume", ...WRITE },
1060
2621
  async (params) => {
1061
- const result = await runpodRequest("/networkvolumes", "POST", params);
1062
- return {
1063
- content: [
1064
- {
1065
- type: "text",
1066
- text: JSON.stringify(result, null, 2)
1067
- }
1068
- ]
1069
- };
2622
+ const backend = backendFor("networkVolumes");
2623
+ const body = backend.mapCreate(params);
2624
+ const result = await callRestUrl(
2625
+ `${backend.base}${backend.list}`,
2626
+ "POST",
2627
+ body
2628
+ );
2629
+ return jsonReply2(result);
1070
2630
  }
1071
2631
  );
1072
2632
  server.tool(
1073
2633
  "update-network-volume",
2634
+ "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1074
2635
  {
1075
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to update"),
1076
- name: import_zod2.z.string().optional().describe("New name for the network volume"),
1077
- size: import_zod2.z.number().optional().describe("New size in GB (must be larger than current)")
2636
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to update"),
2637
+ name: import_zod11.z.string().optional().describe("New name for the network volume"),
2638
+ // Same absolute bounds as create. "Larger than current" stays server-side —
2639
+ // we do not hold the current size here, so only the range is checked.
2640
+ size: import_zod11.z.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
1078
2641
  },
2642
+ { title: "Update network volume", ...WRITE, idempotentHint: true },
1079
2643
  async (params) => {
1080
2644
  const { networkVolumeId, ...updateParams } = params;
1081
- const result = await runpodRequest(
1082
- `/networkvolumes/${networkVolumeId}`,
2645
+ const backend = backendFor("networkVolumes");
2646
+ const body = backend.mapUpdate(updateParams);
2647
+ const result = await callRestUrl(
2648
+ `${backend.base}${backend.get(networkVolumeId)}`,
1083
2649
  "PATCH",
1084
- updateParams
2650
+ body
1085
2651
  );
1086
- return {
1087
- content: [
1088
- {
1089
- type: "text",
1090
- text: JSON.stringify(result, null, 2)
1091
- }
1092
- ]
1093
- };
2652
+ return jsonReply2(result);
1094
2653
  }
1095
2654
  );
1096
2655
  server.tool(
1097
2656
  "delete-network-volume",
2657
+ "Permanently delete a network volume and its data. This cannot be undone.",
1098
2658
  {
1099
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to delete")
2659
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to delete")
1100
2660
  },
2661
+ { title: "Delete network volume", ...DESTRUCTIVE },
1101
2662
  async (params) => {
1102
- const result = await runpodRequest(
1103
- `/networkvolumes/${params.networkVolumeId}`,
2663
+ const backend = backendFor("networkVolumes");
2664
+ const result = await callRestUrl(
2665
+ `${backend.base}${backend.get(params.networkVolumeId)}`,
1104
2666
  "DELETE"
1105
2667
  );
1106
- return {
1107
- content: [
1108
- {
1109
- type: "text",
1110
- text: JSON.stringify(result, null, 2)
1111
- }
1112
- ]
1113
- };
2668
+ return jsonReply2(result);
1114
2669
  }
1115
2670
  );
2671
+ }
2672
+
2673
+ // src/tools/registries.ts
2674
+ var import_zod12 = require("zod");
2675
+ var DELEGATIONS = "/delegations";
2676
+ function registerRegistryTools(server, rt) {
2677
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2678
+ const delegationsV2OnlyNotice = (tool) => ({
2679
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
2680
+ status: 501
2681
+ });
1116
2682
  server.tool(
1117
2683
  "list-container-registry-auths",
2684
+ "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
1118
2685
  listPaginationParams,
2686
+ { title: "List container registry auths", ...READ_ONLY },
1119
2687
  async (params) => {
1120
- const result = await runpodRequest("/containerregistryauth");
1121
- return capListResult(result, {
2688
+ const backend = backendFor("registries");
2689
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
2690
+ return capListResult(backend.unwrap(result), {
1122
2691
  limit: params.limit,
1123
2692
  cursor: params.cursor
1124
2693
  });
@@ -1126,67 +2695,205 @@ function registerTools(server, ctx) {
1126
2695
  );
1127
2696
  server.tool(
1128
2697
  "get-container-registry-auth",
2698
+ "Get one saved container-registry credential by id (the secret/password is not returned).",
1129
2699
  {
1130
- containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to retrieve")
2700
+ containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to retrieve")
1131
2701
  },
2702
+ { title: "Get container registry auth", ...READ_ONLY },
1132
2703
  async (params) => {
1133
- const result = await runpodRequest(
1134
- `/containerregistryauth/${params.containerRegistryAuthId}`
2704
+ const backend = backendFor("registries");
2705
+ const result = await callRestUrl(
2706
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`
1135
2707
  );
1136
- return {
1137
- content: [
1138
- {
1139
- type: "text",
1140
- text: JSON.stringify(result, null, 2)
1141
- }
1142
- ]
1143
- };
2708
+ return jsonReply2(result);
1144
2709
  }
1145
2710
  );
1146
2711
  server.tool(
1147
2712
  "create-container-registry-auth",
2713
+ "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1148
2714
  {
1149
- name: import_zod2.z.string().describe("Name for the container registry auth"),
1150
- username: import_zod2.z.string().describe("Registry username"),
1151
- password: import_zod2.z.string().describe("Registry password")
2715
+ name: import_zod12.z.string().describe("Name for the container registry auth"),
2716
+ username: import_zod12.z.string().describe("Registry username"),
2717
+ password: import_zod12.z.string().describe("Registry password or access token (stored as a secret)")
1152
2718
  },
2719
+ { title: "Create container registry auth", ...WRITE },
1153
2720
  async (params) => {
1154
- const result = await runpodRequest(
1155
- "/containerregistryauth",
2721
+ const backend = backendFor("registries");
2722
+ const body = backend.mapCreate(params);
2723
+ const result = await callRestUrl(
2724
+ `${backend.base}${backend.list}`,
1156
2725
  "POST",
1157
- params
2726
+ body
1158
2727
  );
1159
- return {
1160
- content: [
1161
- {
1162
- type: "text",
1163
- text: JSON.stringify(result, null, 2)
1164
- }
1165
- ]
1166
- };
2728
+ return jsonReply2(result);
1167
2729
  }
1168
2730
  );
1169
2731
  server.tool(
1170
2732
  "delete-container-registry-auth",
2733
+ "Permanently delete a saved container-registry credential. This cannot be undone.",
1171
2734
  {
1172
- containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to delete")
2735
+ containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to delete")
1173
2736
  },
2737
+ { title: "Delete container registry auth", ...DESTRUCTIVE },
1174
2738
  async (params) => {
1175
- const result = await runpodRequest(
1176
- `/containerregistryauth/${params.containerRegistryAuthId}`,
2739
+ const backend = backendFor("registries");
2740
+ const result = await callRestUrl(
2741
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`,
1177
2742
  "DELETE"
1178
2743
  );
1179
- return {
1180
- content: [
1181
- {
1182
- type: "text",
1183
- text: JSON.stringify(result, null, 2)
1184
- }
1185
- ]
1186
- };
2744
+ return jsonReply2(result);
2745
+ }
2746
+ );
2747
+ server.tool(
2748
+ "list-registry-delegations",
2749
+ "List your AWS ECR delegations \u2014 ECR repositories Runpod has been granted scoped pull access to, so private ECR images can be pulled without storing AWS credentials. Each entry includes the `dockerRegistryUri` to pull from. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2750
+ listPaginationParams,
2751
+ { title: "List registry delegations", ...READ_ONLY },
2752
+ async (params) => {
2753
+ const backend = backendFor("registries");
2754
+ if (backend.version === "v1") {
2755
+ return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
2756
+ }
2757
+ const raw = await callRestUrl(
2758
+ `${backend.base}${backend.list}${DELEGATIONS}`
2759
+ );
2760
+ const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
2761
+ return capListResult(delegations, {
2762
+ limit: params.limit,
2763
+ cursor: params.cursor
2764
+ });
2765
+ }
2766
+ );
2767
+ server.tool(
2768
+ "create-registry-delegation",
2769
+ "Register an AWS ECR repository so Runpod can pull private images from it without stored credentials. Pass the repository ARN; the reply carries the `dockerRegistryUri`, resolved repository/tag, and AWS region. v2-only \u2014 returns a 501 notice on the v1 API. For a non-ECR private registry use create-container-registry-auth instead.",
2770
+ {
2771
+ resource: import_zod12.z.string().min(1).describe(
2772
+ 'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
2773
+ ),
2774
+ name: import_zod12.z.string().optional().describe("Optional human-readable name for the delegation")
2775
+ },
2776
+ { title: "Create registry delegation", ...WRITE },
2777
+ async (params) => {
2778
+ const backend = backendFor("registries");
2779
+ if (backend.version === "v1") {
2780
+ return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
2781
+ }
2782
+ const body = { resource: params.resource };
2783
+ if (params.name !== void 0) body.name = params.name;
2784
+ const result = await callRestUrl(
2785
+ `${backend.base}${backend.list}${DELEGATIONS}`,
2786
+ "POST",
2787
+ body
2788
+ );
2789
+ return jsonReply2(result);
2790
+ }
2791
+ );
2792
+ server.tool(
2793
+ "delete-registry-delegation",
2794
+ "Revoke an AWS ECR delegation, withdrawing Runpod's pull access to that repository. Existing workers keep running; future pulls fail. v2-only \u2014 returns a 501 notice on the v1 API. This cannot be undone.",
2795
+ {
2796
+ delegationId: import_zod12.z.string().describe(
2797
+ "ID of the delegation to revoke (from list-registry-delegations)"
2798
+ )
2799
+ },
2800
+ { title: "Revoke registry delegation", ...DESTRUCTIVE },
2801
+ async (params) => {
2802
+ const backend = backendFor("registries");
2803
+ if (backend.version === "v1") {
2804
+ return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
2805
+ }
2806
+ const result = await callRestUrl(
2807
+ `${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
2808
+ params.delegationId
2809
+ )}`,
2810
+ "DELETE"
2811
+ );
2812
+ return jsonReply2(result);
2813
+ }
2814
+ );
2815
+ }
2816
+
2817
+ // src/tools/billing.ts
2818
+ var import_zod13 = require("zod");
2819
+ var BILLING_SCOPE_PATH = {
2820
+ all: "/billing",
2821
+ pods: "/billing/pods",
2822
+ serverless: "/billing/serverless",
2823
+ endpoints: "/billing/endpoints",
2824
+ networkvolumes: "/billing/networkvolumes",
2825
+ clusters: "/billing/clusters"
2826
+ };
2827
+ function registerBillingTools(server, rt) {
2828
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2829
+ server.tool(
2830
+ "get-billing",
2831
+ "Get time-bucketed Runpod spend history, either aggregated across all resources or broken down by resource type (pods, serverless, endpoints, network volumes, clusters). v2-only \u2014 returns a 501 notice on the v1 API. Records are capped via limit/cursor; narrow the window with startTime/endTime or lastN.",
2832
+ {
2833
+ ...listPaginationParams,
2834
+ scope: import_zod13.z.enum([
2835
+ "all",
2836
+ "pods",
2837
+ "serverless",
2838
+ "endpoints",
2839
+ "networkvolumes",
2840
+ "clusters"
2841
+ ]).optional().describe(
2842
+ 'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2843
+ ),
2844
+ startTime: import_zod13.z.string().optional().describe(
2845
+ "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2846
+ ),
2847
+ endTime: import_zod13.z.string().optional().describe(
2848
+ "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2849
+ ),
2850
+ bucketSize: import_zod13.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2851
+ lastN: import_zod13.z.number().int().positive().optional().describe(
2852
+ "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2853
+ )
2854
+ },
2855
+ { title: "Get billing history", ...READ_ONLY },
2856
+ async (params) => {
2857
+ const backend = backendFor("billing");
2858
+ if (backend.version === "v1") {
2859
+ return jsonReply2({
2860
+ error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2861
+ status: 501
2862
+ });
2863
+ }
2864
+ const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
2865
+ const query = new URLSearchParams();
2866
+ if (params.startTime) query.set("startTime", params.startTime);
2867
+ if (params.endTime) query.set("endTime", params.endTime);
2868
+ if (params.bucketSize) query.set("bucketSize", params.bucketSize);
2869
+ if (params.lastN !== void 0 && !params.startTime && !params.endTime)
2870
+ query.set("lastN", String(params.lastN));
2871
+ const qs = query.toString() ? `?${query.toString()}` : "";
2872
+ const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
2873
+ return capListResult(
2874
+ backend.unwrap(raw),
2875
+ { limit: params.limit, cursor: params.cursor },
2876
+ { metadata: raw?.metadata }
2877
+ );
1187
2878
  }
1188
2879
  );
1189
2880
  }
2881
+
2882
+ // src/tools.ts
2883
+ function registerTools(server, ctx, deps = {}) {
2884
+ const rt = createToolRuntime(server, ctx, deps);
2885
+ registerCatalogTools(server, rt);
2886
+ registerHubTools(server, rt);
2887
+ registerPublicEndpointTools(server, rt);
2888
+ registerPodTools(server, rt);
2889
+ registerEndpointTools(server, rt);
2890
+ registerEndpointGpuTools(server, rt);
2891
+ registerJobTools(server, rt);
2892
+ registerTemplateTools(server, rt);
2893
+ registerNetworkVolumeTools(server, rt);
2894
+ registerRegistryTools(server, rt);
2895
+ registerBillingTools(server, rt);
2896
+ }
1190
2897
  // Annotate the CommonJS export names for ESM import in node:
1191
2898
  0 && (module.exports = {
1192
2899
  registerTools