@runpod/mcp-server 2.0.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.
@@ -5,7 +5,8 @@ import { randomUUID } from "crypto";
5
5
  // src/_shared/http.ts
6
6
  var HttpError = class extends Error {
7
7
  constructor(prefix, status, body) {
8
- super(`${prefix}: ${status} - ${body}`);
8
+ 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)" : "";
9
+ super(`${prefix}: ${status} - ${body}${hint}`);
9
10
  this.name = "HttpError";
10
11
  this.status = status;
11
12
  this.body = body;
@@ -61,6 +62,106 @@ function buildTrackingHeaders(input) {
61
62
  };
62
63
  }
63
64
 
65
+ // src/tools/logs.ts
66
+ import { z } from "zod";
67
+ function parseLogSse(raw) {
68
+ const items = [];
69
+ for (const block of raw.split(/\r?\n\r?\n/)) {
70
+ const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
71
+ if (!dataLines.length) continue;
72
+ const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
73
+ if (!payload.trim()) continue;
74
+ try {
75
+ const parsed = JSON.parse(payload);
76
+ items.push(
77
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
78
+ );
79
+ } catch {
80
+ items.push({ raw: payload });
81
+ }
82
+ }
83
+ return items;
84
+ }
85
+ var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
86
+ var LOG_STREAM_MAX_BYTES = 256 * 1024;
87
+ async function readSseSnapshot(fetchImpl, url, headers, opts) {
88
+ const controller = new AbortController();
89
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
90
+ const chunks = [];
91
+ let bytes = 0;
92
+ let truncated = false;
93
+ try {
94
+ const res = await fetchImpl(url, {
95
+ method: "GET",
96
+ headers,
97
+ signal: controller.signal
98
+ });
99
+ if (!res.ok) {
100
+ throw new HttpError(
101
+ "Runpod API Error",
102
+ res.status,
103
+ await res.text().catch(() => "")
104
+ );
105
+ }
106
+ for await (const chunk of res.body ?? []) {
107
+ chunks.push(chunk);
108
+ bytes += chunk.length;
109
+ if (bytes >= opts.maxBytes) {
110
+ truncated = true;
111
+ controller.abort();
112
+ break;
113
+ }
114
+ }
115
+ } catch (err) {
116
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
117
+ } finally {
118
+ clearTimeout(timer);
119
+ }
120
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
121
+ }
122
+ var logStreamParams = {
123
+ source: z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
124
+ tail: z.number().int().min(0).max(5e3).optional().describe(
125
+ "Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
126
+ ),
127
+ since: z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
128
+ maxWaitMs: z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
129
+ };
130
+ async function collectLogSnapshot(streamSse, logsUrl, params) {
131
+ const qs = new URLSearchParams();
132
+ if (params.source && params.source !== "both")
133
+ qs.append("source", params.source);
134
+ if (params.tail !== void 0) qs.append("tail", String(params.tail));
135
+ if (params.since) qs.append("since", params.since);
136
+ const query = qs.toString() ? `?${qs}` : "";
137
+ const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
138
+ maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
139
+ maxBytes: LOG_STREAM_MAX_BYTES
140
+ });
141
+ const items = parseLogSse(raw);
142
+ if (truncated) items.pop();
143
+ return { items, count: items.length, truncated };
144
+ }
145
+ async function streamLogsReply(rt, tool, params) {
146
+ const backend = rt.backendFor(tool.resource);
147
+ if (backend.version === "v1") {
148
+ return rt.jsonReply({
149
+ error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
150
+ status: 501
151
+ });
152
+ }
153
+ try {
154
+ return rt.jsonReply(
155
+ await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
156
+ );
157
+ } catch (error) {
158
+ if (error instanceof HttpError) {
159
+ return rt.jsonReply({ error: error.message, status: error.status });
160
+ }
161
+ throw error;
162
+ }
163
+ }
164
+
64
165
  // src/_shared/mappers.ts
65
166
  function compact(obj) {
66
167
  const out = {};
@@ -73,6 +174,9 @@ function persistentMount(size, path) {
73
174
  if (size === void 0 || path === void 0) return void 0;
74
175
  return { persistent: { size, path } };
75
176
  }
177
+ function joinStartCmd(cmd) {
178
+ return cmd && cmd.length ? cmd.join(" ") : void 0;
179
+ }
76
180
  function containerConfigToV2(p) {
77
181
  return compact({
78
182
  image: p.imageName,
@@ -91,6 +195,13 @@ function mapPodCreateToV2(params) {
91
195
  return compact({
92
196
  ...containerConfigToV2(params),
93
197
  name: params.name,
198
+ // Private-image registry credential (v2 ContainerConfig `registry`). When
199
+ // deploying from a template, this spreads over the template's registry, so
200
+ // passing it overrides the inherited credential. An empty string is an
201
+ // explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
202
+ // template's inherited credential. Undefined is dropped by compact, so an
203
+ // omitted param leaves the template's registry in place.
204
+ registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
94
205
  cloud: params.cloudType,
95
206
  // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
96
207
  // centers; omit/empty = scheduler chooses). Pass the v1 list straight
@@ -147,7 +258,8 @@ function mapNetworkVolumeCreateToV2(params) {
147
258
  return compact({
148
259
  name: params.name,
149
260
  size: params.size,
150
- dataCenter: params.dataCenterId
261
+ dataCenter: params.dataCenterId,
262
+ type: params.volumeType
151
263
  });
152
264
  }
153
265
  function mapTemplateCreateToV2(params) {
@@ -156,18 +268,39 @@ function mapTemplateCreateToV2(params) {
156
268
  ...compact({
157
269
  name: params.name,
158
270
  serverless: params.isServerless,
159
- registry: params.containerRegistryAuthId
160
- }),
161
- // Required by v2 — always emit, defaulting to NVIDIA.
162
- category: params.category ?? "NVIDIA"
271
+ registry: params.containerRegistryAuthId,
272
+ args: joinStartCmd(params.dockerStartCmd),
273
+ // No longer required by v2 (it defaults to NVIDIA server-side), so an
274
+ // unset category is dropped by compact instead of being forced here.
275
+ category: params.category
276
+ })
163
277
  };
164
278
  }
279
+ function podBodyFromTemplate(template) {
280
+ const out = {};
281
+ const keys = [
282
+ "name",
283
+ "image",
284
+ "args",
285
+ "disk",
286
+ "ports",
287
+ "env",
288
+ "mounts",
289
+ "registry"
290
+ ];
291
+ for (const key of keys) {
292
+ const value = template[key];
293
+ if (value !== void 0 && value !== null) out[key] = value;
294
+ }
295
+ return out;
296
+ }
165
297
  function mapTemplateUpdateToV2(params) {
166
298
  return {
167
299
  ...containerConfigToV2(params),
168
300
  ...compact({
169
301
  name: params.name,
170
- registry: params.containerRegistryAuthId
302
+ registry: params.containerRegistryAuthId,
303
+ args: joinStartCmd(params.dockerStartCmd)
171
304
  })
172
305
  };
173
306
  }
@@ -184,7 +317,6 @@ var V2_UNWRAP_KEY = {
184
317
  dataCenters: "dataCenters",
185
318
  endpoints: "endpoints",
186
319
  jobs: "jobs",
187
- tags: "tags",
188
320
  workers: "workers",
189
321
  billing: "records"
190
322
  };
@@ -198,7 +330,6 @@ var RESOURCE_ENV_KEY = {
198
330
  dataCenters: "DATA_CENTERS",
199
331
  endpoints: "ENDPOINTS",
200
332
  jobs: "JOBS",
201
- tags: "TAGS",
202
333
  workers: "WORKERS",
203
334
  billing: "BILLING"
204
335
  };
@@ -214,6 +345,9 @@ function serverlessBase(env) {
214
345
  function publicGraphqlBase(env) {
215
346
  return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
216
347
  }
348
+ function authedGraphqlBase(env) {
349
+ return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
350
+ }
217
351
  function unwrapList(resource, version, raw) {
218
352
  if (version === "v1") return Array.isArray(raw) ? raw : [];
219
353
  if (raw === null || typeof raw !== "object") return [];
@@ -232,6 +366,14 @@ function resolveVersion(opts) {
232
366
  }
233
367
  return "v1";
234
368
  }
369
+ function restVersionSummary(env) {
370
+ const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
371
+ const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
372
+ const overrides = Object.keys(env).filter(
373
+ (k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
374
+ ).length;
375
+ return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
376
+ }
235
377
  var identity = (body) => body;
236
378
  var V1_REST_PATHS = {
237
379
  pods: { list: "/pods", get: (id) => `/pods/${id}` },
@@ -252,7 +394,6 @@ var CATALOG = /* @__PURE__ */ new Set([
252
394
  "dataCenters"
253
395
  ]);
254
396
  var V2_ONLY = /* @__PURE__ */ new Set([
255
- "tags",
256
397
  "workers",
257
398
  "billing"
258
399
  ]);
@@ -270,7 +411,6 @@ var V2_REST_PATHS = {
270
411
  list: "/catalog/datacenters",
271
412
  get: (id) => `/catalog/datacenters/${id}`
272
413
  },
273
- tags: { list: "/tags", get: (id) => `/tags/${id}` },
274
414
  // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
275
415
  // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
276
416
  // unwrap key stays correct.
@@ -368,7 +508,7 @@ function resolveBackend(opts) {
368
508
  }
369
509
 
370
510
  // src/tools/runtime.ts
371
- var MCP_SERVER_VERSION = true ? "2.0.0" : "dev";
511
+ var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
372
512
  var SESSION_ID = randomUUID();
373
513
  function trackingHeaders(server, ctx) {
374
514
  const info = server.server.getClientVersion();
@@ -405,14 +545,18 @@ var DESTRUCTIVE = {
405
545
  idempotentHint: true,
406
546
  openWorldHint: true
407
547
  };
408
- async function graphqlRequest(query, tracking, fetchImpl, url) {
548
+ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
409
549
  const response = await fetchImpl(url, {
410
550
  method: "POST",
411
551
  headers: {
412
552
  "Content-Type": "application/json",
553
+ ...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
413
554
  ...tracking()
414
555
  },
415
- body: JSON.stringify({ query })
556
+ body: JSON.stringify({
557
+ query,
558
+ ...options?.variables ? { variables: options.variables } : {}
559
+ })
416
560
  });
417
561
  const result = await response.json();
418
562
  if (result.errors && result.errors.length > 0) {
@@ -427,7 +571,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
427
571
  }
428
572
  function createToolRuntime(server, ctx, deps = {}) {
429
573
  const tracking = () => trackingHeaders(server, ctx);
430
- const httpFetch = deps.fetch ?? defaultFetch;
574
+ function observeUnauthorized(fn) {
575
+ const notify = ctx.onUnauthorized;
576
+ if (!notify) return fn;
577
+ return async (...args) => {
578
+ const response = await fn(...args);
579
+ if (response.status === 401) notify();
580
+ return response;
581
+ };
582
+ }
583
+ const rawFetch = deps.fetch ?? defaultFetch;
584
+ const httpFetch = observeUnauthorized(rawFetch);
431
585
  const v1Client = createHttpClient({
432
586
  apiKey: ctx.apiKey,
433
587
  fetch: httpFetch,
@@ -462,47 +616,19 @@ function createToolRuntime(server, ctx, deps = {}) {
462
616
  "Error calling Runpod API:",
463
617
  () => restClient(url, method, body)
464
618
  );
465
- const streamSse = deps.streamSse ?? (async (url, opts) => {
466
- const controller = new AbortController();
467
- const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
468
- try {
469
- const res = await fetch(url, {
470
- method: "GET",
471
- headers: {
472
- Authorization: `Bearer ${ctx.apiKey}`,
473
- Accept: "text/event-stream",
474
- ...tracking()
475
- },
476
- signal: controller.signal
477
- });
478
- if (!res.ok) {
479
- throw new HttpError(
480
- "Runpod API Error",
481
- res.status,
482
- await res.text().catch(() => "")
483
- );
484
- }
485
- const chunks = [];
486
- let bytes = 0;
487
- let truncated = false;
488
- try {
489
- for await (const chunk of res.body) {
490
- chunks.push(chunk);
491
- bytes += chunk.length;
492
- if (bytes >= opts.maxBytes) {
493
- truncated = true;
494
- controller.abort();
495
- break;
496
- }
497
- }
498
- } catch (err) {
499
- if (!(err instanceof Error && err.name === "AbortError")) throw err;
500
- }
501
- return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
502
- } finally {
503
- clearTimeout(timer);
504
- }
505
- });
619
+ const sseFetch = observeUnauthorized(
620
+ deps.sseFetch ?? fetch
621
+ );
622
+ const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
623
+ sseFetch,
624
+ url,
625
+ {
626
+ Authorization: `Bearer ${ctx.apiKey}`,
627
+ Accept: "text/event-stream",
628
+ ...tracking()
629
+ },
630
+ opts
631
+ ));
506
632
  const backendFor = (resource) => resolveBackend({
507
633
  resource,
508
634
  env: process.env,
@@ -524,9 +650,18 @@ function createToolRuntime(server, ctx, deps = {}) {
524
650
  graphql: (query) => graphqlRequest(
525
651
  query,
526
652
  tracking,
527
- httpFetch,
653
+ // rawFetch, not httpFetch: this call carries no credential (see above).
654
+ rawFetch,
528
655
  publicGraphqlBase(process.env)
529
656
  ),
657
+ graphqlAuthed: (query, variables) => graphqlRequest(
658
+ query,
659
+ tracking,
660
+ httpFetch,
661
+ // NOT publicGraphqlBase: this call carries the caller's API key.
662
+ authedGraphqlBase(process.env),
663
+ { variables, apiKey: ctx.apiKey }
664
+ ),
530
665
  runpodRequest,
531
666
  serverlessRequest,
532
667
  callRestUrl,
@@ -538,17 +673,17 @@ function createToolRuntime(server, ctx, deps = {}) {
538
673
  }
539
674
 
540
675
  // src/tools/catalog.ts
541
- import { z as z2 } from "zod";
676
+ import { z as z3 } from "zod";
542
677
 
543
678
  // src/pagination.ts
544
- import { z } from "zod";
679
+ import { z as z2 } from "zod";
545
680
  var DEFAULT_LIST_LIMIT = 20;
546
681
  var MAX_LIST_LIMIT = 100;
547
682
  var listPaginationParams = {
548
- limit: z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
683
+ limit: z2.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
549
684
  `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
550
685
  ),
551
- cursor: z.string().optional().describe(
686
+ cursor: z2.string().optional().describe(
552
687
  "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
553
688
  )
554
689
  };
@@ -606,25 +741,32 @@ function registerCatalogTools(server, rt) {
606
741
  const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
607
742
  server.tool(
608
743
  "list-gpu-types",
609
- "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. On v2 the realtime stock filter is a no-op.",
744
+ "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.",
610
745
  {
611
746
  ...listPaginationParams,
612
- minMemoryGb: z2.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
613
- secureCloudOnly: z2.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
614
- communityCloudOnly: z2.boolean().optional().describe("Filter to only GPUs available in community cloud"),
615
- searchTerm: z2.string().optional().describe(
747
+ minMemoryGb: z3.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
748
+ secureCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
749
+ communityCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in community cloud"),
750
+ searchTerm: z3.string().optional().describe(
616
751
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
617
752
  ),
618
- includeUnavailable: z2.boolean().optional().describe(
619
- "Include GPUs that are currently out of stock. Default is false"
753
+ includeUnavailable: z3.boolean().optional().describe(
754
+ "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."
755
+ ),
756
+ includeAvailability: z3.boolean().optional().describe(
757
+ "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."
620
758
  )
621
759
  },
622
760
  { title: "List GPU types", ...READ_ONLY },
623
761
  async (params) => {
624
762
  const backend = backendFor("gpus");
625
763
  if (backend.version === "v2") {
626
- const raw = await callRestUrl(`${backend.base}${backend.list}`);
764
+ const wantAvailability = params.includeAvailability !== false;
765
+ const raw = await callRestUrl(
766
+ `${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
767
+ );
627
768
  let gpus = backend.unwrap(raw);
769
+ gpus = gpus.filter((g) => g.id !== "unknown");
628
770
  if (params.minMemoryGb !== void 0)
629
771
  gpus = gpus.filter(
630
772
  (g) => Number(g.memory ?? 0) >= params.minMemoryGb
@@ -637,6 +779,18 @@ function registerCatalogTools(server, rt) {
637
779
  (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
638
780
  );
639
781
  }
782
+ if (params.includeUnavailable === false)
783
+ gpus = gpus.filter((g) => g.availability !== "NONE");
784
+ const rank = {
785
+ HIGH: 3,
786
+ MEDIUM: 2,
787
+ LOW: 1,
788
+ NONE: 0
789
+ };
790
+ gpus.sort(
791
+ (a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
792
+ );
793
+ gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
640
794
  return capListResult(gpus, {
641
795
  limit: params.limit,
642
796
  cursor: params.cursor
@@ -666,7 +820,7 @@ function registerCatalogTools(server, rt) {
666
820
  return !!status && status !== "Out";
667
821
  };
668
822
  let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
669
- if (!params.includeUnavailable) {
823
+ if (params.includeUnavailable === false) {
670
824
  gpuTypes = gpuTypes.filter(isAvailable);
671
825
  }
672
826
  if (params.minMemoryGb) {
@@ -712,7 +866,7 @@ function registerCatalogTools(server, rt) {
712
866
  "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
713
867
  {
714
868
  ...listPaginationParams,
715
- region: z2.string().optional().describe(
869
+ region: z3.string().optional().describe(
716
870
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
717
871
  )
718
872
  },
@@ -777,9 +931,12 @@ function registerCatalogTools(server, rt) {
777
931
  );
778
932
  server.tool(
779
933
  "get-gpu-type",
780
- "Get details for a single GPU type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there).",
934
+ "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.",
781
935
  {
782
- gpuTypeId: z2.string().describe("ID of the GPU type to retrieve")
936
+ gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
937
+ includeAvailability: z3.boolean().optional().describe(
938
+ "Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
939
+ )
783
940
  },
784
941
  { title: "Get GPU type", ...READ_ONLY },
785
942
  async (params) => {
@@ -790,9 +947,9 @@ function registerCatalogTools(server, rt) {
790
947
  status: 501
791
948
  });
792
949
  }
793
- const result = await callRestUrl(
794
- `${backend.base}${backend.get(params.gpuTypeId)}`
795
- );
950
+ const path = backend.get(encodeURIComponent(params.gpuTypeId));
951
+ const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
952
+ const result = await callRestUrl(`${backend.base}${path}${query}`);
796
953
  return jsonReply2(result);
797
954
  }
798
955
  );
@@ -800,7 +957,7 @@ function registerCatalogTools(server, rt) {
800
957
  "get-cpu-type",
801
958
  "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).",
802
959
  {
803
- cpuTypeId: z2.string().describe("ID of the CPU type to retrieve")
960
+ cpuTypeId: z3.string().describe("ID of the CPU type to retrieve")
804
961
  },
805
962
  { title: "Get CPU type", ...READ_ONLY },
806
963
  async (params) => {
@@ -821,7 +978,7 @@ function registerCatalogTools(server, rt) {
821
978
  "get-data-center",
822
979
  "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).",
823
980
  {
824
- dataCenterId: z2.string().describe("ID of the data center to retrieve")
981
+ dataCenterId: z3.string().describe("ID of the data center to retrieve")
825
982
  },
826
983
  { title: "Get data center", ...READ_ONLY },
827
984
  async (params) => {
@@ -840,8 +997,579 @@ function registerCatalogTools(server, rt) {
840
997
  );
841
998
  }
842
999
 
1000
+ // src/tools/hub.ts
1001
+ import { z as z4 } from "zod";
1002
+ function parseReleaseConfig(config) {
1003
+ if (!config) return void 0;
1004
+ try {
1005
+ return JSON.parse(config);
1006
+ } catch {
1007
+ return config;
1008
+ }
1009
+ }
1010
+ function listingsQuery(withConfig) {
1011
+ return `
1012
+ query {
1013
+ listings(input: {}) {
1014
+ id
1015
+ repoId
1016
+ title
1017
+ description
1018
+ repoName
1019
+ repoOwner
1020
+ createdAt
1021
+ updatedAt
1022
+ views
1023
+ stars
1024
+ deploys
1025
+ language
1026
+ category
1027
+ tags
1028
+ type
1029
+ listedRelease {
1030
+ id
1031
+ name
1032
+ tagName
1033
+ createdAt${withConfig ? "\n config" : ""}
1034
+ build {
1035
+ id
1036
+ imageName
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ `;
1042
+ }
1043
+ function serializeBoolean(value, input) {
1044
+ if (value === "true") return input.trueValue ?? "true";
1045
+ if (value === "false") return input.falseValue ?? "false";
1046
+ return value;
1047
+ }
1048
+ function buildHubEnv(config, overrides) {
1049
+ const env = [];
1050
+ const missingRequired = [];
1051
+ const remaining = { ...overrides };
1052
+ for (const entry of config.env ?? []) {
1053
+ const input = entry.input ?? {};
1054
+ const override = entry.key in remaining ? remaining[entry.key] : void 0;
1055
+ delete remaining[entry.key];
1056
+ if (override !== void 0 && override !== "") {
1057
+ env.push({
1058
+ key: entry.key,
1059
+ value: input.type === "boolean" ? serializeBoolean(override, input) : override
1060
+ });
1061
+ continue;
1062
+ }
1063
+ if (input.default !== void 0 && input.default !== null) {
1064
+ env.push({
1065
+ key: entry.key,
1066
+ value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
1067
+ });
1068
+ continue;
1069
+ }
1070
+ if (input.required) {
1071
+ missingRequired.push(entry.key);
1072
+ env.push({ key: entry.key, value: "" });
1073
+ continue;
1074
+ }
1075
+ }
1076
+ for (const [key, value] of Object.entries(remaining)) {
1077
+ env.push({ key, value });
1078
+ }
1079
+ return { env, missingRequired };
1080
+ }
1081
+ function minCudaVersion(versions) {
1082
+ if (!versions || versions.length === 0) return null;
1083
+ const compare = (a, b) => {
1084
+ const pa = a.split(".").map(Number);
1085
+ const pb = b.split(".").map(Number);
1086
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1087
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1088
+ if (d !== 0) return d;
1089
+ }
1090
+ return 0;
1091
+ };
1092
+ return [...versions].sort(compare)[0];
1093
+ }
1094
+ function randomSuffix() {
1095
+ return Math.random().toString(36).slice(2, 8);
1096
+ }
1097
+ function registerHubTools(server, rt) {
1098
+ const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
1099
+ server.tool(
1100
+ "list-hub-repos",
1101
+ "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.",
1102
+ {
1103
+ ...listPaginationParams,
1104
+ searchTerm: z4.string().optional().describe(
1105
+ "Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
1106
+ ),
1107
+ category: z4.string().optional().describe(
1108
+ "Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
1109
+ ),
1110
+ type: z4.enum(["SERVERLESS", "POD"]).optional().describe(
1111
+ "Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
1112
+ ),
1113
+ repoOwner: z4.string().optional().describe(
1114
+ "Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
1115
+ ),
1116
+ includeConfig: z4.boolean().optional().describe(
1117
+ "Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
1118
+ )
1119
+ },
1120
+ { title: "List Hub repos", ...READ_ONLY },
1121
+ async (params) => {
1122
+ const data = await graphql(
1123
+ listingsQuery(params.includeConfig === true)
1124
+ );
1125
+ let listings = data.listings;
1126
+ if (params.type) {
1127
+ listings = listings.filter(
1128
+ (l) => (l.type ?? "").toUpperCase() === params.type
1129
+ );
1130
+ }
1131
+ if (params.category) {
1132
+ const term = params.category.toLowerCase();
1133
+ listings = listings.filter(
1134
+ (l) => (l.category ?? "").toLowerCase() === term
1135
+ );
1136
+ }
1137
+ if (params.repoOwner) {
1138
+ const term = params.repoOwner.toLowerCase();
1139
+ listings = listings.filter(
1140
+ (l) => (l.repoOwner ?? "").toLowerCase() === term
1141
+ );
1142
+ }
1143
+ if (params.searchTerm) {
1144
+ const term = params.searchTerm.toLowerCase();
1145
+ listings = listings.filter(
1146
+ (l) => [
1147
+ l.title,
1148
+ l.description,
1149
+ l.repoName,
1150
+ l.repoOwner,
1151
+ ...l.tags ?? []
1152
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1153
+ );
1154
+ }
1155
+ listings = [...listings].sort(
1156
+ (a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
1157
+ );
1158
+ const result = listings.map((l) => ({
1159
+ id: l.id,
1160
+ repo: `${l.repoOwner}/${l.repoName}`,
1161
+ title: l.title,
1162
+ description: l.description,
1163
+ type: l.type,
1164
+ category: l.category,
1165
+ tags: l.tags,
1166
+ language: l.language,
1167
+ stars: l.stars,
1168
+ deploys: l.deploys,
1169
+ views: l.views,
1170
+ githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
1171
+ hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
1172
+ createdAt: l.createdAt,
1173
+ updatedAt: l.updatedAt,
1174
+ listedRelease: l.listedRelease ? {
1175
+ hubReleaseId: l.listedRelease.id,
1176
+ name: l.listedRelease.name,
1177
+ tagName: l.listedRelease.tagName,
1178
+ createdAt: l.listedRelease.createdAt,
1179
+ imageName: l.listedRelease.build?.imageName,
1180
+ ...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
1181
+ } : null
1182
+ }));
1183
+ return capListResult(result, {
1184
+ limit: params.limit,
1185
+ cursor: params.cursor
1186
+ });
1187
+ }
1188
+ );
1189
+ server.tool(
1190
+ "deploy-hub-repo",
1191
+ "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.",
1192
+ {
1193
+ repo: z4.string().optional().describe(
1194
+ `The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
1195
+ ),
1196
+ hubReleaseId: z4.string().optional().describe(
1197
+ "The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
1198
+ ),
1199
+ name: z4.string().optional().describe(
1200
+ "Name for the new endpoint. Defaults to '<listing title> <release tag>'."
1201
+ ),
1202
+ env: z4.record(z4.string()).optional().describe(
1203
+ "Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
1204
+ ),
1205
+ gpuIds: z4.string().optional().describe(
1206
+ "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."
1207
+ ),
1208
+ gpuCount: z4.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
1209
+ containerDiskInGb: z4.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
1210
+ workersMin: z4.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
1211
+ workersMax: z4.number().int().positive().optional().describe("Maximum workers (default: account default)."),
1212
+ idleTimeout: z4.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
1213
+ scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
1214
+ scalerValue: z4.number().optional().describe("Autoscaler target value (default 4)."),
1215
+ executionTimeoutMs: z4.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
1216
+ flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
1217
+ },
1218
+ { title: "Deploy Hub repo", ...WRITE },
1219
+ async (params) => {
1220
+ if (!params.repo && !params.hubReleaseId) {
1221
+ return jsonReply2({
1222
+ error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
1223
+ });
1224
+ }
1225
+ const catalog = await graphql(listingsQuery(true));
1226
+ let listing;
1227
+ if (params.hubReleaseId) {
1228
+ listing = catalog.listings.find(
1229
+ (l) => l.listedRelease?.id === params.hubReleaseId
1230
+ );
1231
+ } else {
1232
+ const repoKey = params.repo.toLowerCase();
1233
+ listing = catalog.listings.find(
1234
+ (l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
1235
+ );
1236
+ }
1237
+ if (!listing) {
1238
+ return jsonReply2({
1239
+ 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.`
1240
+ });
1241
+ }
1242
+ const release = listing.listedRelease;
1243
+ if (!release?.build?.imageName) {
1244
+ return jsonReply2({
1245
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
1246
+ });
1247
+ }
1248
+ if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
1249
+ return jsonReply2({
1250
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
1251
+ });
1252
+ }
1253
+ const config = parseReleaseConfig(release.config) ?? {};
1254
+ const gpuIds = params.gpuIds ?? config.gpuIds;
1255
+ if (!gpuIds) {
1256
+ return jsonReply2({
1257
+ 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).'
1258
+ });
1259
+ }
1260
+ const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
1261
+ if (missingRequired.length > 0) {
1262
+ return jsonReply2({
1263
+ error: `Missing required environment variables for this release: ${missingRequired.join(
1264
+ ", "
1265
+ )}. Pass them via the env parameter.`
1266
+ });
1267
+ }
1268
+ const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
1269
+ const minCuda = minCudaVersion(config.allowedCudaVersions);
1270
+ const input = {
1271
+ name: endpointName,
1272
+ hubReleaseId: release.id,
1273
+ type: "QB",
1274
+ gpuIds,
1275
+ gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
1276
+ workersMin: params.workersMin ?? 0,
1277
+ workersMax: params.workersMax ?? null,
1278
+ idleTimeout: params.idleTimeout ?? 5,
1279
+ scalerType: params.scalerType ?? "QUEUE_DELAY",
1280
+ scalerValue: params.scalerValue ?? 4,
1281
+ executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
1282
+ flashBootType: params.flashboot ?? "FLASHBOOT",
1283
+ locations: null,
1284
+ networkVolumeIds: null,
1285
+ compliance: [],
1286
+ modelReferences: [],
1287
+ ...minCuda ? {
1288
+ minCudaVersion: minCuda,
1289
+ allowedCudaVersions: config.allowedCudaVersions.join(",")
1290
+ } : {},
1291
+ template: {
1292
+ name: `${endpointName}__template__${randomSuffix()}`,
1293
+ imageName: release.build.imageName,
1294
+ containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
1295
+ containerRegistryAuthId: "",
1296
+ dockerArgs: "",
1297
+ startScript: "",
1298
+ ports: "",
1299
+ env
1300
+ }
1301
+ };
1302
+ const data = await graphqlAuthed(
1303
+ `
1304
+ mutation saveEndpoint($input: EndpointInput!) {
1305
+ saveEndpoint(input: $input) {
1306
+ id
1307
+ name
1308
+ gpuIds
1309
+ gpuCount
1310
+ workersMin
1311
+ workersMax
1312
+ idleTimeout
1313
+ scalerType
1314
+ scalerValue
1315
+ flashBootType
1316
+ templateId
1317
+ }
1318
+ }
1319
+ `,
1320
+ { input }
1321
+ );
1322
+ return jsonReply2({
1323
+ endpoint: data.saveEndpoint,
1324
+ deployed: {
1325
+ repo: `${listing.repoOwner}/${listing.repoName}`,
1326
+ release: release.tagName,
1327
+ hubReleaseId: release.id,
1328
+ imageName: release.build.imageName
1329
+ },
1330
+ note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
1331
+ });
1332
+ }
1333
+ );
1334
+ }
1335
+
1336
+ // src/tools/public-endpoints.ts
1337
+ import { z as z5 } from "zod";
1338
+ function parseMetadata(metadata) {
1339
+ if (!metadata) return {};
1340
+ try {
1341
+ const parsed = JSON.parse(metadata);
1342
+ return parsed && typeof parsed === "object" ? parsed : {};
1343
+ } catch {
1344
+ return {};
1345
+ }
1346
+ }
1347
+ function registerPublicEndpointTools(server, rt) {
1348
+ const { graphql } = rt;
1349
+ server.tool(
1350
+ "list-public-endpoints",
1351
+ "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.",
1352
+ {
1353
+ ...listPaginationParams,
1354
+ searchTerm: z5.string().optional().describe(
1355
+ "Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
1356
+ ),
1357
+ modality: z5.string().optional().describe(
1358
+ "Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
1359
+ ),
1360
+ owner: z5.string().optional().describe(
1361
+ "Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
1362
+ ),
1363
+ includeOffline: z5.boolean().optional().describe(
1364
+ "Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
1365
+ )
1366
+ },
1367
+ { title: "List public endpoints", ...READ_ONLY },
1368
+ async (params) => {
1369
+ const data = await graphql(`
1370
+ query {
1371
+ allAiApiPublicConfigs {
1372
+ id
1373
+ aiApiId
1374
+ modelName
1375
+ displayName
1376
+ description
1377
+ metadata
1378
+ isLive
1379
+ createdAt
1380
+ updatedAt
1381
+ }
1382
+ }
1383
+ `);
1384
+ let configs = data.allAiApiPublicConfigs.map((c) => ({
1385
+ config: c,
1386
+ meta: parseMetadata(c.metadata)
1387
+ }));
1388
+ if (!params.includeOffline) {
1389
+ configs = configs.filter(({ config }) => config.isLive);
1390
+ }
1391
+ if (params.modality) {
1392
+ const term = params.modality.toLowerCase();
1393
+ configs = configs.filter(
1394
+ ({ meta }) => (meta.source ?? "").toLowerCase() === term
1395
+ );
1396
+ }
1397
+ if (params.owner) {
1398
+ const term = params.owner.toLowerCase();
1399
+ configs = configs.filter(
1400
+ ({ meta }) => (meta.owner ?? "").toLowerCase() === term
1401
+ );
1402
+ }
1403
+ if (params.searchTerm) {
1404
+ const term = params.searchTerm.toLowerCase();
1405
+ configs = configs.filter(
1406
+ ({ config, meta }) => [
1407
+ config.displayName,
1408
+ config.modelName,
1409
+ config.aiApiId,
1410
+ config.description,
1411
+ meta.owner,
1412
+ meta.tag,
1413
+ meta.source
1414
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1415
+ );
1416
+ }
1417
+ configs = [...configs].sort(
1418
+ (a, b) => a.config.displayName.localeCompare(b.config.displayName)
1419
+ );
1420
+ const result = configs.map(({ config, meta }) => ({
1421
+ endpointId: config.aiApiId,
1422
+ displayName: config.displayName,
1423
+ modelName: config.modelName,
1424
+ description: config.description,
1425
+ modality: meta.source,
1426
+ tag: meta.tag,
1427
+ owner: meta.owner,
1428
+ pricing: meta.priceString,
1429
+ isLive: config.isLive,
1430
+ baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
1431
+ createdAt: config.createdAt,
1432
+ updatedAt: config.updatedAt
1433
+ }));
1434
+ return capListResult(result, {
1435
+ limit: params.limit,
1436
+ cursor: params.cursor
1437
+ });
1438
+ }
1439
+ );
1440
+ }
1441
+
1442
+ // src/tools/endpoint-gpus.ts
1443
+ import { z as z6 } from "zod";
1444
+ function registerEndpointGpuTools(server, rt) {
1445
+ const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
1446
+ server.tool(
1447
+ "set-endpoint-gpus",
1448
+ "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.",
1449
+ {
1450
+ endpointId: z6.string().describe("ID of the Serverless endpoint to update"),
1451
+ gpuIds: z6.string().optional().describe(
1452
+ "Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
1453
+ ),
1454
+ pools: z6.array(z6.string()).optional().describe(
1455
+ "GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
1456
+ ),
1457
+ excludeGpuTypeIds: z6.array(z6.string()).optional().describe(
1458
+ "GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
1459
+ ),
1460
+ gpuCount: z6.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
1461
+ minCudaVersion: z6.string().optional().describe(
1462
+ "Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
1463
+ ),
1464
+ allowedCudaVersions: z6.string().optional().describe(
1465
+ "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."
1466
+ )
1467
+ },
1468
+ { title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
1469
+ async (params) => {
1470
+ const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
1471
+ ...params.pools,
1472
+ ...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
1473
+ ].join(",") : void 0);
1474
+ if (!gpuIds) {
1475
+ return jsonReply2({
1476
+ error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
1477
+ });
1478
+ }
1479
+ const data = await graphqlAuthed(`
1480
+ query {
1481
+ myself {
1482
+ endpoints {
1483
+ id
1484
+ name
1485
+ gpuIds
1486
+ gpuCount
1487
+ workersMin
1488
+ workersMax
1489
+ idleTimeout
1490
+ scalerType
1491
+ scalerValue
1492
+ executionTimeoutMs
1493
+ flashBootType
1494
+ type
1495
+ locations
1496
+ templateId
1497
+ allowedCudaVersions
1498
+ minCudaVersion
1499
+ compliance
1500
+ modelReferences
1501
+ networkVolumeIds {
1502
+ networkVolumeId
1503
+ dataCenterId
1504
+ }
1505
+ }
1506
+ }
1507
+ }
1508
+ `);
1509
+ const current = data.myself.endpoints.find(
1510
+ (e) => e.id === params.endpointId
1511
+ );
1512
+ if (!current) {
1513
+ return jsonReply2({
1514
+ error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
1515
+ });
1516
+ }
1517
+ const input = {
1518
+ id: current.id,
1519
+ name: current.name,
1520
+ gpuIds,
1521
+ gpuCount: params.gpuCount ?? current.gpuCount,
1522
+ workersMin: current.workersMin,
1523
+ workersMax: current.workersMax,
1524
+ idleTimeout: current.idleTimeout,
1525
+ scalerType: current.scalerType,
1526
+ scalerValue: current.scalerValue,
1527
+ executionTimeoutMs: current.executionTimeoutMs,
1528
+ flashBootType: current.flashBootType,
1529
+ type: current.type,
1530
+ locations: current.locations,
1531
+ networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
1532
+ // Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
1533
+ // ONLY, and the read shape is rejected outright.
1534
+ current.networkVolumeIds.map((v) => ({
1535
+ networkVolumeId: v.networkVolumeId
1536
+ }))
1537
+ ) : null
1538
+ };
1539
+ for (const [key, value] of Object.entries({
1540
+ templateId: current.templateId,
1541
+ allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
1542
+ minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
1543
+ compliance: current.compliance,
1544
+ modelReferences: current.modelReferences
1545
+ })) {
1546
+ if (value !== null && value !== void 0) input[key] = value;
1547
+ }
1548
+ const result = await graphqlAuthed(
1549
+ `
1550
+ mutation saveEndpoint($input: EndpointInput!) {
1551
+ saveEndpoint(input: $input) {
1552
+ id
1553
+ name
1554
+ gpuIds
1555
+ gpuCount
1556
+ workersMin
1557
+ workersMax
1558
+ }
1559
+ }
1560
+ `,
1561
+ { input }
1562
+ );
1563
+ return jsonReply2({
1564
+ endpoint: result.saveEndpoint,
1565
+ previousGpuIds: current.gpuIds
1566
+ });
1567
+ }
1568
+ );
1569
+ }
1570
+
843
1571
  // src/tools/pods.ts
844
- import { z as z3 } from "zod";
1572
+ import { z as z7 } from "zod";
845
1573
  function registerPodTools(server, rt) {
846
1574
  const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
847
1575
  server.tool(
@@ -849,12 +1577,12 @@ function registerPodTools(server, rt) {
849
1577
  "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.",
850
1578
  {
851
1579
  ...listPaginationParams,
852
- computeType: z3.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
853
- gpuTypeId: z3.array(z3.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
854
- dataCenterId: z3.array(z3.string()).optional().describe("Filter to Pods in any of the provided data centers"),
855
- name: z3.string().optional().describe("Filter to Pods with the provided name"),
856
- includeMachine: z3.boolean().optional().describe("Include information about the machine"),
857
- includeNetworkVolume: z3.boolean().optional().describe("Include information about attached network volumes")
1580
+ computeType: z7.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
1581
+ gpuTypeId: z7.array(z7.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
1582
+ dataCenterId: z7.array(z7.string()).optional().describe("Filter to Pods in any of the provided data centers"),
1583
+ name: z7.string().optional().describe("Filter to Pods with the provided name"),
1584
+ includeMachine: z7.boolean().optional().describe("Include information about the machine"),
1585
+ includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
858
1586
  },
859
1587
  { title: "List pods", ...READ_ONLY },
860
1588
  async (params) => {
@@ -898,9 +1626,9 @@ function registerPodTools(server, rt) {
898
1626
  "get-pod",
899
1627
  "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
900
1628
  {
901
- podId: z3.string().describe("ID of the pod to retrieve"),
902
- includeMachine: z3.boolean().optional().describe("Include information about the machine"),
903
- includeNetworkVolume: z3.boolean().optional().describe("Include information about attached network volumes")
1629
+ podId: z7.string().describe("ID of the pod to retrieve"),
1630
+ includeMachine: z7.boolean().optional().describe("Include information about the machine"),
1631
+ includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
904
1632
  },
905
1633
  { title: "Get pod", ...READ_ONLY },
906
1634
  async (params) => {
@@ -928,26 +1656,44 @@ function registerPodTools(server, rt) {
928
1656
  );
929
1657
  server.tool(
930
1658
  "create-pod",
931
- '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). 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.',
1659
+ '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.',
932
1660
  {
933
- name: z3.string().optional().describe("Name for the pod"),
934
- imageName: z3.string().describe("Docker image to use"),
935
- computeType: z3.enum(["GPU", "CPU"]).optional().describe(
1661
+ name: z7.string().optional().describe("Name for the pod"),
1662
+ imageName: z7.string().optional().describe("Docker image to use. Optional when templateId is provided."),
1663
+ templateId: z7.string().optional().describe(
1664
+ "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."
1665
+ ),
1666
+ computeType: z7.enum(["GPU", "CPU"]).optional().describe(
936
1667
  "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
937
1668
  ),
938
- cloudType: z3.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
939
- gpuTypeIds: z3.array(z3.string()).optional().describe("List of acceptable GPU types"),
940
- gpuCount: z3.number().optional().describe("Number of GPUs"),
941
- containerDiskInGb: z3.number().optional().describe("Container disk size in GB"),
942
- volumeInGb: z3.number().optional().describe("Volume size in GB"),
943
- volumeMountPath: z3.string().optional().describe("Path to mount the volume"),
944
- ports: z3.array(z3.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
945
- env: z3.record(z3.string()).optional().describe("Environment variables"),
946
- dataCenterIds: z3.array(z3.string()).optional().describe("List of data centers")
1669
+ cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1670
+ gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
1671
+ gpuCount: z7.number().optional().describe("Number of GPUs"),
1672
+ containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
1673
+ volumeInGb: z7.number().optional().describe("Volume size in GB"),
1674
+ volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
1675
+ ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1676
+ env: z7.record(z7.string()).optional().describe("Environment variables"),
1677
+ containerRegistryAuthId: z7.string().optional().describe(
1678
+ "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)."
1679
+ ),
1680
+ dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
947
1681
  },
948
1682
  { title: "Create pod", ...WRITE },
949
1683
  async (params) => {
950
1684
  const backend = backendFor("pods");
1685
+ if (params.templateId && backend.version !== "v2") {
1686
+ return jsonReply2({
1687
+ error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
1688
+ status: 501
1689
+ });
1690
+ }
1691
+ if (!params.imageName && !params.templateId) {
1692
+ return jsonReply2({
1693
+ error: "Provide imageName, or templateId to deploy from an existing template.",
1694
+ status: 400
1695
+ });
1696
+ }
951
1697
  const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
952
1698
  const hasGpuCount = (params.gpuCount ?? 0) > 0;
953
1699
  const wantsCpu = params.computeType === "CPU";
@@ -972,6 +1718,12 @@ function registerPodTools(server, rt) {
972
1718
  });
973
1719
  }
974
1720
  if (wantsCpu) {
1721
+ if (params.templateId) {
1722
+ return jsonReply2({
1723
+ error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
1724
+ status: 400
1725
+ });
1726
+ }
975
1727
  try {
976
1728
  const result = await callRestUrl(
977
1729
  `${restV1Base(env)}/pods`,
@@ -996,7 +1748,24 @@ function registerPodTools(server, rt) {
996
1748
  }
997
1749
  }
998
1750
  }
999
- const body = backend.mapCreate(params);
1751
+ let body = backend.mapCreate(params);
1752
+ if (params.templateId) {
1753
+ let template;
1754
+ try {
1755
+ template = await callRestUrl(
1756
+ `${restV2Base(env)}/templates/${params.templateId}`
1757
+ );
1758
+ } catch (error) {
1759
+ if (error instanceof HttpError) {
1760
+ return jsonReply2({
1761
+ error: `Failed to load template ${params.templateId}: ${error.message}`,
1762
+ status: error.status
1763
+ });
1764
+ }
1765
+ throw error;
1766
+ }
1767
+ body = { ...podBodyFromTemplate(template), ...body };
1768
+ }
1000
1769
  try {
1001
1770
  const result = await callRestUrl(
1002
1771
  `${backend.base}${backend.list}`,
@@ -1011,11 +1780,8 @@ function registerPodTools(server, rt) {
1011
1780
  }
1012
1781
  return jsonReply2(result);
1013
1782
  } catch (error) {
1014
- if (error instanceof HttpError && error.status === 501) {
1015
- return jsonReply2({
1016
- error: "CPU pods are not yet supported on the v2 REST API. Use a GPU pod, or create the CPU pod via the v1 API (set RUNPOD_REST_VERSION=v1).",
1017
- status: 501
1018
- });
1783
+ if (error instanceof HttpError) {
1784
+ return jsonReply2({ error: error.message, status: error.status });
1019
1785
  }
1020
1786
  throw error;
1021
1787
  }
@@ -1025,14 +1791,14 @@ function registerPodTools(server, rt) {
1025
1791
  "update-pod",
1026
1792
  "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
1027
1793
  {
1028
- podId: z3.string().describe("ID of the pod to update"),
1029
- name: z3.string().optional().describe("New name for the pod"),
1030
- imageName: z3.string().optional().describe("New Docker image"),
1031
- containerDiskInGb: z3.number().optional().describe("New container disk size in GB"),
1032
- volumeInGb: z3.number().optional().describe("New volume size in GB"),
1033
- volumeMountPath: z3.string().optional().describe("New path to mount the volume"),
1034
- ports: z3.array(z3.string()).optional().describe("New ports to expose"),
1035
- env: z3.record(z3.string()).optional().describe("New environment variables")
1794
+ podId: z7.string().describe("ID of the pod to update"),
1795
+ name: z7.string().optional().describe("New name for the pod"),
1796
+ imageName: z7.string().optional().describe("New Docker image"),
1797
+ containerDiskInGb: z7.number().optional().describe("New container disk size in GB"),
1798
+ volumeInGb: z7.number().optional().describe("New volume size in GB"),
1799
+ volumeMountPath: z7.string().optional().describe("New path to mount the volume"),
1800
+ ports: z7.array(z7.string()).optional().describe("New ports to expose"),
1801
+ env: z7.record(z7.string()).optional().describe("New environment variables")
1036
1802
  },
1037
1803
  { title: "Update pod", ...WRITE, idempotentHint: true },
1038
1804
  async (params) => {
@@ -1051,7 +1817,7 @@ function registerPodTools(server, rt) {
1051
1817
  "start-pod",
1052
1818
  "Start a stopped pod (resumes the same pod and its billing).",
1053
1819
  {
1054
- podId: z3.string().describe("ID of the pod to start")
1820
+ podId: z7.string().describe("ID of the pod to start")
1055
1821
  },
1056
1822
  { title: "Start pod", ...WRITE, idempotentHint: true },
1057
1823
  async (params) => {
@@ -1063,7 +1829,7 @@ function registerPodTools(server, rt) {
1063
1829
  "stop-pod",
1064
1830
  "Stop a running pod. The pod and its disk persist; GPU billing stops.",
1065
1831
  {
1066
- podId: z3.string().describe("ID of the pod to stop")
1832
+ podId: z7.string().describe("ID of the pod to stop")
1067
1833
  },
1068
1834
  { title: "Stop pod", ...WRITE, idempotentHint: true },
1069
1835
  async (params) => {
@@ -1075,7 +1841,7 @@ function registerPodTools(server, rt) {
1075
1841
  "restart-pod",
1076
1842
  "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1077
1843
  {
1078
- podId: z3.string().describe("ID of the pod to restart")
1844
+ podId: z7.string().describe("ID of the pod to restart")
1079
1845
  },
1080
1846
  { title: "Restart pod", ...WRITE },
1081
1847
  async (params) => {
@@ -1090,11 +1856,29 @@ function registerPodTools(server, rt) {
1090
1856
  return jsonReply2(result);
1091
1857
  }
1092
1858
  );
1859
+ server.tool(
1860
+ "stream-pod-logs",
1861
+ "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).",
1862
+ {
1863
+ podId: z7.string().describe("ID of the pod whose logs to stream"),
1864
+ ...logStreamParams
1865
+ },
1866
+ { title: "Stream pod logs", ...READ_ONLY },
1867
+ (params) => streamLogsReply(
1868
+ rt,
1869
+ {
1870
+ name: "stream-pod-logs",
1871
+ resource: "pods",
1872
+ logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
1873
+ },
1874
+ params
1875
+ )
1876
+ );
1093
1877
  server.tool(
1094
1878
  "delete-pod",
1095
1879
  "Permanently delete a pod and its data. This cannot be undone.",
1096
1880
  {
1097
- podId: z3.string().describe("ID of the pod to delete")
1881
+ podId: z7.string().describe("ID of the pod to delete")
1098
1882
  },
1099
1883
  { title: "Delete pod", ...DESTRUCTIVE },
1100
1884
  async (params) => {
@@ -1109,7 +1893,7 @@ function registerPodTools(server, rt) {
1109
1893
  }
1110
1894
 
1111
1895
  // src/tools/endpoints.ts
1112
- import { z as z4 } from "zod";
1896
+ import { z as z8 } from "zod";
1113
1897
  function registerEndpointTools(server, rt) {
1114
1898
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1115
1899
  server.tool(
@@ -1117,8 +1901,8 @@ function registerEndpointTools(server, rt) {
1117
1901
  "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
1118
1902
  {
1119
1903
  ...listPaginationParams,
1120
- includeTemplate: z4.boolean().optional().describe("Include template information (v1 only)"),
1121
- includeWorkers: z4.boolean().optional().describe("Include information about workers (v1 only)")
1904
+ includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
1905
+ includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
1122
1906
  },
1123
1907
  { title: "List endpoints", ...READ_ONLY },
1124
1908
  async (params) => {
@@ -1151,9 +1935,9 @@ function registerEndpointTools(server, rt) {
1151
1935
  "get-endpoint",
1152
1936
  "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
1153
1937
  {
1154
- endpointId: z4.string().describe("ID of the endpoint to retrieve"),
1155
- includeTemplate: z4.boolean().optional().describe("Include template information (v1 only)"),
1156
- includeWorkers: z4.boolean().optional().describe("Include information about workers (v1 only)")
1938
+ endpointId: z8.string().describe("ID of the endpoint to retrieve"),
1939
+ includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
1940
+ includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
1157
1941
  },
1158
1942
  { title: "Get endpoint", ...READ_ONLY },
1159
1943
  async (params) => {
@@ -1183,32 +1967,32 @@ function registerEndpointTools(server, rt) {
1183
1967
  "create-endpoint",
1184
1968
  "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).",
1185
1969
  {
1186
- name: z4.string().optional().describe("Name for the endpoint"),
1970
+ name: z8.string().optional().describe("Name for the endpoint"),
1187
1971
  // --- v2 inline-config fields ---
1188
- imageName: z4.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
1189
- gpuPoolIds: z4.array(z4.string()).optional().describe(
1972
+ imageName: z8.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
1973
+ gpuPoolIds: z8.array(z8.string()).optional().describe(
1190
1974
  'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
1191
1975
  ),
1192
- gpuCount: z4.number().optional().describe("GPUs per worker (v2)"),
1193
- args: z4.string().optional().describe("Container start command/args (v2)"),
1194
- containerDiskInGb: z4.number().optional().describe("Container disk size in GB (v2)"),
1195
- ports: z4.array(z4.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
1196
- env: z4.record(z4.string()).optional().describe("Environment variables (v2)"),
1197
- containerRegistryAuthId: z4.string().optional().describe("Container registry auth id for a private image (v2)"),
1198
- networkVolumeIds: z4.array(z4.string()).optional().describe("Network volume ids to attach (v2)"),
1199
- executionTimeoutMs: z4.number().optional().describe("Per-job execution timeout in ms (v2)"),
1200
- flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
1976
+ gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
1977
+ args: z8.string().optional().describe("Container start command/args (v2)"),
1978
+ containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
1979
+ ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
1980
+ env: z8.record(z8.string()).optional().describe("Environment variables (v2)"),
1981
+ containerRegistryAuthId: z8.string().optional().describe("Container registry auth id for a private image (v2)"),
1982
+ networkVolumeIds: z8.array(z8.string()).optional().describe("Network volume ids to attach (v2)"),
1983
+ executionTimeoutMs: z8.number().optional().describe("Per-job execution timeout in ms (v2)"),
1984
+ flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
1201
1985
  // --- v1 template-based fields ---
1202
- templateId: z4.string().optional().describe("Template ID (v1). Required on v1."),
1203
- computeType: z4.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
1204
- gpuTypeIds: z4.array(z4.string()).optional().describe("List of acceptable GPU types (v1)"),
1986
+ templateId: z8.string().optional().describe("Template ID (v1). Required on v1."),
1987
+ computeType: z8.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
1988
+ gpuTypeIds: z8.array(z8.string()).optional().describe("List of acceptable GPU types (v1)"),
1205
1989
  // --- shared autoscaling fields ---
1206
- workersMin: z4.number().optional().describe("Minimum number of workers"),
1207
- workersMax: z4.number().optional().describe("Maximum number of workers"),
1208
- scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
1209
- scalerValue: z4.number().optional().describe("Autoscaler target value"),
1210
- idleTimeout: z4.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
1211
- dataCenterIds: z4.array(z4.string()).optional().describe("List of preferred data centers")
1990
+ workersMin: z8.number().optional().describe("Minimum number of workers"),
1991
+ workersMax: z8.number().optional().describe("Maximum number of workers"),
1992
+ scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
1993
+ scalerValue: z8.number().optional().describe("Autoscaler target value"),
1994
+ idleTimeout: z8.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
1995
+ dataCenterIds: z8.array(z8.string()).optional().describe("List of preferred data centers")
1212
1996
  },
1213
1997
  { title: "Create endpoint", ...WRITE },
1214
1998
  async (params) => {
@@ -1271,25 +2055,25 @@ function registerEndpointTools(server, rt) {
1271
2055
  "update-endpoint",
1272
2056
  "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.",
1273
2057
  {
1274
- endpointId: z4.string().describe("ID of the endpoint to update"),
1275
- name: z4.string().optional().describe("New name for the endpoint"),
1276
- workersMin: z4.number().optional().describe("New minimum number of workers"),
1277
- workersMax: z4.number().optional().describe("New maximum number of workers"),
1278
- idleTimeout: z4.number().optional().describe("New idle timeout in seconds"),
1279
- scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1280
- scalerValue: z4.number().optional().describe("Scaler value"),
2058
+ endpointId: z8.string().describe("ID of the endpoint to update"),
2059
+ name: z8.string().optional().describe("New name for the endpoint"),
2060
+ workersMin: z8.number().optional().describe("New minimum number of workers"),
2061
+ workersMax: z8.number().optional().describe("New maximum number of workers"),
2062
+ idleTimeout: z8.number().optional().describe("New idle timeout in seconds"),
2063
+ scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2064
+ scalerValue: z8.number().optional().describe("Scaler value"),
1281
2065
  // --- v2-only inline-config fields ---
1282
- imageName: z4.string().optional().describe("New Docker image (v2)"),
1283
- gpuPoolIds: z4.array(z4.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
1284
- gpuCount: z4.number().optional().describe("New GPUs per worker (v2)"),
1285
- args: z4.string().optional().describe("New container args (v2)"),
1286
- containerDiskInGb: z4.number().optional().describe("New container disk size in GB (v2)"),
1287
- ports: z4.array(z4.string()).optional().describe("New ports (v2)"),
1288
- env: z4.record(z4.string()).optional().describe("New environment variables (v2)"),
1289
- containerRegistryAuthId: z4.string().optional().describe("New container registry auth id (v2)"),
1290
- networkVolumeIds: z4.array(z4.string()).optional().describe("New network volume ids (v2)"),
1291
- executionTimeoutMs: z4.number().optional().describe("New per-job execution timeout in ms (v2)"),
1292
- flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
2066
+ imageName: z8.string().optional().describe("New Docker image (v2)"),
2067
+ gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
2068
+ gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
2069
+ args: z8.string().optional().describe("New container args (v2)"),
2070
+ containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
2071
+ ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
2072
+ env: z8.record(z8.string()).optional().describe("New environment variables (v2)"),
2073
+ containerRegistryAuthId: z8.string().optional().describe("New container registry auth id (v2)"),
2074
+ networkVolumeIds: z8.array(z8.string()).optional().describe("New network volume ids (v2)"),
2075
+ executionTimeoutMs: z8.number().optional().describe("New per-job execution timeout in ms (v2)"),
2076
+ flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
1293
2077
  },
1294
2078
  { title: "Update endpoint", ...WRITE, idempotentHint: true },
1295
2079
  async (params) => {
@@ -1308,7 +2092,7 @@ function registerEndpointTools(server, rt) {
1308
2092
  "delete-endpoint",
1309
2093
  "Permanently delete a Serverless endpoint. This cannot be undone.",
1310
2094
  {
1311
- endpointId: z4.string().describe("ID of the endpoint to delete")
2095
+ endpointId: z8.string().describe("ID of the endpoint to delete")
1312
2096
  },
1313
2097
  { title: "Delete endpoint", ...DESTRUCTIVE },
1314
2098
  async (params) => {
@@ -1320,12 +2104,39 @@ function registerEndpointTools(server, rt) {
1320
2104
  return jsonReply2(result);
1321
2105
  }
1322
2106
  );
2107
+ server.tool(
2108
+ "list-endpoint-releases",
2109
+ "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.",
2110
+ {
2111
+ ...listPaginationParams,
2112
+ endpointId: z8.string().describe("ID of the Serverless endpoint whose releases to list")
2113
+ },
2114
+ { title: "List endpoint releases", ...READ_ONLY },
2115
+ async (params) => {
2116
+ const backend = backendFor("endpoints");
2117
+ if (backend.version === "v1") {
2118
+ return jsonReply2({
2119
+ error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2120
+ status: 501
2121
+ });
2122
+ }
2123
+ const raw = await callRestUrl(
2124
+ `${backend.base}/serverless/${params.endpointId}/releases`
2125
+ );
2126
+ const releases = Array.isArray(raw?.releases) ? raw.releases : [];
2127
+ return capListResult(
2128
+ releases,
2129
+ { limit: params.limit, cursor: params.cursor },
2130
+ { rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
2131
+ );
2132
+ }
2133
+ );
1323
2134
  server.tool(
1324
2135
  "list-endpoint-workers",
1325
2136
  "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.",
1326
2137
  {
1327
2138
  ...listPaginationParams,
1328
- endpointId: z4.string().describe("ID of the Serverless endpoint whose workers to list")
2139
+ endpointId: z8.string().describe("ID of the Serverless endpoint whose workers to list")
1329
2140
  },
1330
2141
  { title: "List endpoint workers", ...READ_ONLY },
1331
2142
  async (params) => {
@@ -1346,30 +2157,51 @@ function registerEndpointTools(server, rt) {
1346
2157
  );
1347
2158
  }
1348
2159
  );
2160
+ server.tool(
2161
+ "stream-worker-logs",
2162
+ "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).",
2163
+ {
2164
+ endpointId: z8.string().describe("ID of the Serverless endpoint the worker belongs to"),
2165
+ workerId: z8.string().describe(
2166
+ "ID of the worker whose logs to stream (from list-endpoint-workers)"
2167
+ ),
2168
+ ...logStreamParams
2169
+ },
2170
+ { title: "Stream worker logs", ...READ_ONLY },
2171
+ (params) => streamLogsReply(
2172
+ rt,
2173
+ {
2174
+ name: "stream-worker-logs",
2175
+ resource: "workers",
2176
+ logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
2177
+ },
2178
+ params
2179
+ )
2180
+ );
1349
2181
  }
1350
2182
 
1351
2183
  // src/tools/jobs.ts
1352
- import { z as z5 } from "zod";
2184
+ import { z as z9 } from "zod";
1353
2185
  function registerJobTools(server, rt) {
1354
2186
  const { jsonReply: jsonReply2, serverlessRequest } = rt;
1355
- const endpointIdSchema = z5.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1356
- const jobIdSchema = z5.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1357
- const inputSchema = z5.record(z5.unknown()).describe(
2187
+ const endpointIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2188
+ const jobIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2189
+ const inputSchema = z9.record(z9.unknown()).describe(
1358
2190
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
1359
2191
  );
1360
- const webhookSchema = z5.string().url().optional().describe(
2192
+ const webhookSchema = z9.string().url().optional().describe(
1361
2193
  "Webhook URL to receive job completion notifications instead of polling"
1362
2194
  );
1363
- const policySchema = z5.object({
1364
- executionTimeout: z5.number().optional().describe("Maximum execution time in milliseconds"),
1365
- lowPriority: z5.boolean().optional().describe("Submit as a low-priority job"),
1366
- ttl: z5.number().optional().describe("Time-to-live for the job result in milliseconds")
2195
+ const policySchema = z9.object({
2196
+ executionTimeout: z9.number().optional().describe("Maximum execution time in milliseconds"),
2197
+ lowPriority: z9.boolean().optional().describe("Submit as a low-priority job"),
2198
+ ttl: z9.number().optional().describe("Time-to-live for the job result in milliseconds")
1367
2199
  }).optional().describe("Execution policy options");
1368
- const s3ConfigSchema = z5.object({
1369
- accessId: z5.string().describe("S3 access key ID"),
1370
- accessSecret: z5.string().describe("S3 secret access key"),
1371
- bucketName: z5.string().describe("S3 bucket name"),
1372
- endpointUrl: z5.string().describe("S3 endpoint URL")
2200
+ const s3ConfigSchema = z9.object({
2201
+ accessId: z9.string().describe("S3 access key ID"),
2202
+ accessSecret: z9.string().describe("S3 secret access key"),
2203
+ bucketName: z9.string().describe("S3 bucket name"),
2204
+ endpointUrl: z9.string().describe("S3 endpoint URL")
1373
2205
  }).optional().describe("S3-compatible storage config for large outputs");
1374
2206
  server.tool(
1375
2207
  "run-endpoint",
@@ -1403,7 +2235,7 @@ function registerJobTools(server, rt) {
1403
2235
  "ID of the Serverless endpoint to run synchronously"
1404
2236
  ),
1405
2237
  input: inputSchema,
1406
- wait: z5.number().min(1e3).max(3e5).optional().describe(
2238
+ wait: z9.number().min(1e3).max(3e5).optional().describe(
1407
2239
  "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)."
1408
2240
  ),
1409
2241
  webhook: webhookSchema,
@@ -1572,7 +2404,7 @@ function registerJobTools(server, rt) {
1572
2404
  }
1573
2405
 
1574
2406
  // src/tools/templates.ts
1575
- import { z as z6 } from "zod";
2407
+ import { z as z10 } from "zod";
1576
2408
  function registerTemplateTools(server, rt) {
1577
2409
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1578
2410
  server.tool(
@@ -1580,9 +2412,9 @@ function registerTemplateTools(server, rt) {
1580
2412
  `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.`,
1581
2413
  {
1582
2414
  ...listPaginationParams,
1583
- includeRunpodTemplates: z6.boolean().optional().describe("Include official Runpod templates in the response"),
1584
- includePublicTemplates: z6.boolean().optional().describe("Include community-made public templates in the response"),
1585
- includeEndpointBoundTemplates: z6.boolean().optional().describe(
2415
+ includeRunpodTemplates: z10.boolean().optional().describe("Include official Runpod templates in the response"),
2416
+ includePublicTemplates: z10.boolean().optional().describe("Include community-made public templates in the response"),
2417
+ includeEndpointBoundTemplates: z10.boolean().optional().describe(
1586
2418
  "Include templates bound to Serverless endpoints in the response"
1587
2419
  )
1588
2420
  },
@@ -1613,7 +2445,7 @@ function registerTemplateTools(server, rt) {
1613
2445
  "get-template",
1614
2446
  "Get one template by id (image, env, ports, disk/volume config).",
1615
2447
  {
1616
- templateId: z6.string().describe("ID of the template to retrieve")
2448
+ templateId: z10.string().describe("ID of the template to retrieve")
1617
2449
  },
1618
2450
  { title: "Get template", ...READ_ONLY },
1619
2451
  async (params) => {
@@ -1626,23 +2458,27 @@ function registerTemplateTools(server, rt) {
1626
2458
  );
1627
2459
  server.tool(
1628
2460
  "create-template",
1629
- "Create a reusable pod/endpoint template (image, ports, env, disk/volume). On v2 a category is required and defaults to NVIDIA.",
2461
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
1630
2462
  {
1631
- name: z6.string().describe("Name for the template"),
1632
- imageName: z6.string().describe("Docker image to use"),
1633
- isServerless: z6.boolean().optional().describe("Is this a serverless template"),
1634
- ports: z6.array(z6.string()).optional().describe("Ports to expose"),
1635
- dockerEntrypoint: z6.array(z6.string()).optional().describe("Docker entrypoint commands"),
1636
- dockerStartCmd: z6.array(z6.string()).optional().describe("Docker start commands"),
1637
- env: z6.record(z6.string()).optional().describe("Environment variables"),
1638
- containerDiskInGb: z6.number().optional().describe("Container disk size in GB"),
1639
- volumeInGb: z6.number().optional().describe("Volume size in GB"),
1640
- volumeMountPath: z6.string().optional().describe("Path to mount the volume"),
1641
- readme: z6.string().optional().describe("README content in markdown format"),
1642
- category: z6.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
1643
- "Hardware category for the template. On v2 a category is required and defaults to NVIDIA when omitted \u2014 set CPU or AMD for non-NVIDIA templates so they are not mis-categorized."
2463
+ name: z10.string().describe("Name for the template"),
2464
+ imageName: z10.string().describe("Docker image to use"),
2465
+ isServerless: z10.boolean().optional().describe("Is this a serverless template"),
2466
+ ports: z10.array(z10.string()).optional().describe("Ports to expose"),
2467
+ dockerEntrypoint: z10.array(z10.string()).optional().describe(
2468
+ "Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
1644
2469
  ),
1645
- containerRegistryAuthId: z6.string().optional().describe(
2470
+ dockerStartCmd: z10.array(z10.string()).optional().describe(
2471
+ '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 "").'
2472
+ ),
2473
+ env: z10.record(z10.string()).optional().describe("Environment variables"),
2474
+ containerDiskInGb: z10.number().optional().describe("Container disk size in GB"),
2475
+ volumeInGb: z10.number().optional().describe("Volume size in GB"),
2476
+ volumeMountPath: z10.string().optional().describe("Path to mount the volume"),
2477
+ readme: z10.string().optional().describe("README content in markdown format"),
2478
+ category: z10.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2479
+ "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."
2480
+ ),
2481
+ containerRegistryAuthId: z10.string().optional().describe(
1646
2482
  "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."
1647
2483
  )
1648
2484
  },
@@ -1660,15 +2496,18 @@ function registerTemplateTools(server, rt) {
1660
2496
  );
1661
2497
  server.tool(
1662
2498
  "update-template",
1663
- "Update a template's mutable fields (name, image, ports, env, readme). Only provided fields change.",
2499
+ "Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
1664
2500
  {
1665
- templateId: z6.string().describe("ID of the template to update"),
1666
- name: z6.string().optional().describe("New name for the template"),
1667
- imageName: z6.string().optional().describe("New Docker image"),
1668
- ports: z6.array(z6.string()).optional().describe("New ports to expose"),
1669
- env: z6.record(z6.string()).optional().describe("New environment variables"),
1670
- readme: z6.string().optional().describe("New README content in markdown format"),
1671
- containerRegistryAuthId: z6.string().optional().describe(
2501
+ templateId: z10.string().describe("ID of the template to update"),
2502
+ name: z10.string().optional().describe("New name for the template"),
2503
+ imageName: z10.string().optional().describe("New Docker image"),
2504
+ dockerStartCmd: z10.array(z10.string()).optional().describe(
2505
+ "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."
2506
+ ),
2507
+ ports: z10.array(z10.string()).optional().describe("New ports to expose"),
2508
+ env: z10.record(z10.string()).optional().describe("New environment variables"),
2509
+ readme: z10.string().optional().describe("New README content in markdown format"),
2510
+ containerRegistryAuthId: z10.string().optional().describe(
1672
2511
  "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."
1673
2512
  )
1674
2513
  },
@@ -1689,7 +2528,7 @@ function registerTemplateTools(server, rt) {
1689
2528
  "delete-template",
1690
2529
  "Permanently delete a template. This cannot be undone.",
1691
2530
  {
1692
- templateId: z6.string().describe("ID of the template to delete")
2531
+ templateId: z10.string().describe("ID of the template to delete")
1693
2532
  },
1694
2533
  { title: "Delete template", ...DESTRUCTIVE },
1695
2534
  async (params) => {
@@ -1704,7 +2543,7 @@ function registerTemplateTools(server, rt) {
1704
2543
  }
1705
2544
 
1706
2545
  // src/tools/network-volumes.ts
1707
- import { z as z7 } from "zod";
2546
+ import { z as z11 } from "zod";
1708
2547
  function registerNetworkVolumeTools(server, rt) {
1709
2548
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1710
2549
  server.tool(
@@ -1725,7 +2564,7 @@ function registerNetworkVolumeTools(server, rt) {
1725
2564
  "get-network-volume",
1726
2565
  "Get one network volume by id (name, size, data center).",
1727
2566
  {
1728
- networkVolumeId: z7.string().describe("ID of the network volume to retrieve")
2567
+ networkVolumeId: z11.string().describe("ID of the network volume to retrieve")
1729
2568
  },
1730
2569
  { title: "Get network volume", ...READ_ONLY },
1731
2570
  async (params) => {
@@ -1738,11 +2577,17 @@ function registerNetworkVolumeTools(server, rt) {
1738
2577
  );
1739
2578
  server.tool(
1740
2579
  "create-network-volume",
1741
- "Create a persistent network volume in a data center (size 1\u20134000 GB) that pods can mount.",
2580
+ "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.",
1742
2581
  {
1743
- name: z7.string().describe("Name for the network volume"),
1744
- size: z7.number().describe("Size in GB (1-4000)"),
1745
- dataCenterId: z7.string().describe("Data center ID (see list-data-centers)")
2582
+ name: z11.string().describe("Name for the network volume"),
2583
+ // Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
2584
+ // here as well as described, so an out-of-range value fails locally
2585
+ // instead of costing an API roundtrip.
2586
+ size: z11.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
2587
+ dataCenterId: z11.string().describe("Data center ID (see list-data-centers)"),
2588
+ volumeType: z11.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
2589
+ "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."
2590
+ )
1746
2591
  },
1747
2592
  { title: "Create network volume", ...WRITE },
1748
2593
  async (params) => {
@@ -1760,9 +2605,11 @@ function registerNetworkVolumeTools(server, rt) {
1760
2605
  "update-network-volume",
1761
2606
  "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1762
2607
  {
1763
- networkVolumeId: z7.string().describe("ID of the network volume to update"),
1764
- name: z7.string().optional().describe("New name for the network volume"),
1765
- size: z7.number().optional().describe("New size in GB (must be larger than current)")
2608
+ networkVolumeId: z11.string().describe("ID of the network volume to update"),
2609
+ name: z11.string().optional().describe("New name for the network volume"),
2610
+ // Same absolute bounds as create. "Larger than current" stays server-side —
2611
+ // we do not hold the current size here, so only the range is checked.
2612
+ size: z11.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
1766
2613
  },
1767
2614
  { title: "Update network volume", ...WRITE, idempotentHint: true },
1768
2615
  async (params) => {
@@ -1781,7 +2628,7 @@ function registerNetworkVolumeTools(server, rt) {
1781
2628
  "delete-network-volume",
1782
2629
  "Permanently delete a network volume and its data. This cannot be undone.",
1783
2630
  {
1784
- networkVolumeId: z7.string().describe("ID of the network volume to delete")
2631
+ networkVolumeId: z11.string().describe("ID of the network volume to delete")
1785
2632
  },
1786
2633
  { title: "Delete network volume", ...DESTRUCTIVE },
1787
2634
  async (params) => {
@@ -1796,9 +2643,14 @@ function registerNetworkVolumeTools(server, rt) {
1796
2643
  }
1797
2644
 
1798
2645
  // src/tools/registries.ts
1799
- import { z as z8 } from "zod";
2646
+ import { z as z12 } from "zod";
2647
+ var DELEGATIONS = "/delegations";
1800
2648
  function registerRegistryTools(server, rt) {
1801
2649
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2650
+ const delegationsV2OnlyNotice = (tool) => ({
2651
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
2652
+ status: 501
2653
+ });
1802
2654
  server.tool(
1803
2655
  "list-container-registry-auths",
1804
2656
  "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
@@ -1817,7 +2669,7 @@ function registerRegistryTools(server, rt) {
1817
2669
  "get-container-registry-auth",
1818
2670
  "Get one saved container-registry credential by id (the secret/password is not returned).",
1819
2671
  {
1820
- containerRegistryAuthId: z8.string().describe("ID of the container registry auth to retrieve")
2672
+ containerRegistryAuthId: z12.string().describe("ID of the container registry auth to retrieve")
1821
2673
  },
1822
2674
  { title: "Get container registry auth", ...READ_ONLY },
1823
2675
  async (params) => {
@@ -1832,9 +2684,9 @@ function registerRegistryTools(server, rt) {
1832
2684
  "create-container-registry-auth",
1833
2685
  "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1834
2686
  {
1835
- name: z8.string().describe("Name for the container registry auth"),
1836
- username: z8.string().describe("Registry username"),
1837
- password: z8.string().describe("Registry password or access token (stored as a secret)")
2687
+ name: z12.string().describe("Name for the container registry auth"),
2688
+ username: z12.string().describe("Registry username"),
2689
+ password: z12.string().describe("Registry password or access token (stored as a secret)")
1838
2690
  },
1839
2691
  { title: "Create container registry auth", ...WRITE },
1840
2692
  async (params) => {
@@ -1852,7 +2704,7 @@ function registerRegistryTools(server, rt) {
1852
2704
  "delete-container-registry-auth",
1853
2705
  "Permanently delete a saved container-registry credential. This cannot be undone.",
1854
2706
  {
1855
- containerRegistryAuthId: z8.string().describe("ID of the container registry auth to delete")
2707
+ containerRegistryAuthId: z12.string().describe("ID of the container registry auth to delete")
1856
2708
  },
1857
2709
  { title: "Delete container registry auth", ...DESTRUCTIVE },
1858
2710
  async (params) => {
@@ -1864,166 +2716,69 @@ function registerRegistryTools(server, rt) {
1864
2716
  return jsonReply2(result);
1865
2717
  }
1866
2718
  );
1867
- }
1868
-
1869
- // src/tools/tags.ts
1870
- import { z as z9 } from "zod";
1871
- var TAG_RESOURCE_TYPES = [
1872
- "POD",
1873
- "NETWORK_VOLUME",
1874
- "CLUSTER",
1875
- "SERVERLESS_ENDPOINT"
1876
- ];
1877
- var taggedResourceSchema = z9.object({
1878
- id: z9.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
1879
- type: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
1880
- });
1881
- function v2OnlyNotice(tool) {
1882
- return {
1883
- error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
1884
- status: 501
1885
- };
1886
- }
1887
- function registerTagTools(server, rt) {
1888
- const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1889
2719
  server.tool(
1890
- "list-tags",
1891
- "List your tags (key/value labels on pods, volumes, clusters, and endpoints). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
1892
- {
1893
- ...listPaginationParams,
1894
- includeResources: z9.boolean().optional().describe("Include the resources attached to each tag in the response")
1895
- },
1896
- { title: "List tags", ...READ_ONLY },
2720
+ "list-registry-delegations",
2721
+ "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.",
2722
+ listPaginationParams,
2723
+ { title: "List registry delegations", ...READ_ONLY },
1897
2724
  async (params) => {
1898
- const backend = backendFor("tags");
1899
- if (backend.version === "v1") return jsonReply2(v2OnlyNotice("list-tags"));
1900
- const query = params.includeResources ? "?include=resources" : "";
1901
- const raw = await callRestUrl(`${backend.base}${backend.list}${query}`);
1902
- return capListResult(backend.unwrap(raw), {
2725
+ const backend = backendFor("registries");
2726
+ if (backend.version === "v1") {
2727
+ return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
2728
+ }
2729
+ const raw = await callRestUrl(
2730
+ `${backend.base}${backend.list}${DELEGATIONS}`
2731
+ );
2732
+ const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
2733
+ return capListResult(delegations, {
1903
2734
  limit: params.limit,
1904
2735
  cursor: params.cursor
1905
2736
  });
1906
2737
  }
1907
2738
  );
1908
2739
  server.tool(
1909
- "get-tag",
1910
- "Get one tag by id, optionally including its attached resources. v2-only \u2014 returns a 501 notice on the v1 API.",
1911
- {
1912
- tagId: z9.string().describe("ID of the tag to retrieve"),
1913
- includeResources: z9.boolean().optional().describe("Include the resources attached to this tag")
1914
- },
1915
- { title: "Get tag", ...READ_ONLY },
1916
- async (params) => {
1917
- const backend = backendFor("tags");
1918
- if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
1919
- const query = params.includeResources ? "?include=resources" : "";
1920
- const result = await callRestUrl(
1921
- `${backend.base}${backend.get(params.tagId)}${query}`
1922
- );
1923
- return jsonReply2(result);
1924
- }
1925
- );
1926
- server.tool(
1927
- "create-tag",
1928
- "Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
2740
+ "create-registry-delegation",
2741
+ "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.",
1929
2742
  {
1930
- key: z9.string().min(1).describe('Tag key (e.g. "project")'),
1931
- value: z9.string().min(1).describe('Tag value (e.g. "gpt-training")'),
1932
- resources: z9.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
2743
+ resource: z12.string().min(1).describe(
2744
+ 'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
2745
+ ),
2746
+ name: z12.string().optional().describe("Optional human-readable name for the delegation")
1933
2747
  },
1934
- { title: "Create tag", ...WRITE },
2748
+ { title: "Create registry delegation", ...WRITE },
1935
2749
  async (params) => {
1936
- const backend = backendFor("tags");
1937
- if (backend.version === "v1")
1938
- return jsonReply2(v2OnlyNotice("create-tag"));
2750
+ const backend = backendFor("registries");
2751
+ if (backend.version === "v1") {
2752
+ return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
2753
+ }
2754
+ const body = { resource: params.resource };
2755
+ if (params.name !== void 0) body.name = params.name;
1939
2756
  const result = await callRestUrl(
1940
- `${backend.base}${backend.list}`,
2757
+ `${backend.base}${backend.list}${DELEGATIONS}`,
1941
2758
  "POST",
1942
- params
2759
+ body
1943
2760
  );
1944
2761
  return jsonReply2(result);
1945
2762
  }
1946
2763
  );
1947
2764
  server.tool(
1948
- "update-tag",
1949
- "Update a tag (rename its key/value, or replace its resource associations). Provide at least one of key, value, or resources. Sending resources:[] removes all associations; omitting resources leaves them unchanged. Replacing resources is NOT atomic \u2014 a failed update can leave a partially-applied set, so for small association changes prefer attach-tag/detach-tag. v2-only \u2014 returns a 501 notice on the v1 API.",
2765
+ "delete-registry-delegation",
2766
+ "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.",
1950
2767
  {
1951
- tagId: z9.string().describe("ID of the tag to update"),
1952
- key: z9.string().min(1).optional().describe("New tag key"),
1953
- value: z9.string().min(1).optional().describe("New tag value"),
1954
- resources: z9.array(taggedResourceSchema).optional().describe(
1955
- "Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
2768
+ delegationId: z12.string().describe(
2769
+ "ID of the delegation to revoke (from list-registry-delegations)"
1956
2770
  )
1957
2771
  },
1958
- // No idempotentHint: replacing `resources` is non-atomic upstream, so a
1959
- // retried-after-partial-failure update is not guaranteed to be a no-op.
1960
- { title: "Update tag", ...WRITE },
1961
- async (params) => {
1962
- const { tagId, ...updateParams } = params;
1963
- const backend = backendFor("tags");
1964
- if (backend.version === "v1")
1965
- return jsonReply2(v2OnlyNotice("update-tag"));
1966
- const result = await callRestUrl(
1967
- `${backend.base}${backend.get(tagId)}`,
1968
- "PATCH",
1969
- updateParams
1970
- );
1971
- return jsonReply2(result);
1972
- }
1973
- );
1974
- server.tool(
1975
- "delete-tag",
1976
- "Permanently delete a tag (removes it from all resources it is attached to). This cannot be undone. v2-only \u2014 returns a 501 notice on the v1 API.",
1977
- {
1978
- tagId: z9.string().describe("ID of the tag to delete")
1979
- },
1980
- { title: "Delete tag", ...DESTRUCTIVE },
2772
+ { title: "Revoke registry delegation", ...DESTRUCTIVE },
1981
2773
  async (params) => {
1982
- const backend = backendFor("tags");
1983
- if (backend.version === "v1")
1984
- return jsonReply2(v2OnlyNotice("delete-tag"));
1985
- const result = await callRestUrl(
1986
- `${backend.base}${backend.get(params.tagId)}`,
1987
- "DELETE"
1988
- );
1989
- return jsonReply2(result);
1990
- }
1991
- );
1992
- server.tool(
1993
- "attach-tag",
1994
- "Attach a tag to a resource (pod, network volume, cluster, or serverless endpoint). Idempotent \u2014 attaching an already-associated resource is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
1995
- {
1996
- tagId: z9.string().describe("ID of the tag"),
1997
- resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
1998
- resourceId: z9.string().describe("ID of the resource to attach the tag to")
1999
- },
2000
- { title: "Attach tag to resource", ...WRITE, idempotentHint: true },
2001
- async (params) => {
2002
- const backend = backendFor("tags");
2003
- if (backend.version === "v1")
2004
- return jsonReply2(v2OnlyNotice("attach-tag"));
2005
- const result = await callRestUrl(
2006
- `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2007
- "PUT"
2008
- );
2009
- return jsonReply2(result);
2010
- }
2011
- );
2012
- server.tool(
2013
- "detach-tag",
2014
- "Detach a tag from a resource (removes the association only \u2014 the resource and tag are not deleted). Idempotent \u2014 detaching a resource that is not associated is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
2015
- {
2016
- tagId: z9.string().describe("ID of the tag"),
2017
- resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
2018
- resourceId: z9.string().describe("ID of the resource to detach the tag from")
2019
- },
2020
- { title: "Detach tag from resource", ...WRITE, idempotentHint: true },
2021
- async (params) => {
2022
- const backend = backendFor("tags");
2023
- if (backend.version === "v1")
2024
- return jsonReply2(v2OnlyNotice("detach-tag"));
2774
+ const backend = backendFor("registries");
2775
+ if (backend.version === "v1") {
2776
+ return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
2777
+ }
2025
2778
  const result = await callRestUrl(
2026
- `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2779
+ `${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
2780
+ params.delegationId
2781
+ )}`,
2027
2782
  "DELETE"
2028
2783
  );
2029
2784
  return jsonReply2(result);
@@ -2032,7 +2787,7 @@ function registerTagTools(server, rt) {
2032
2787
  }
2033
2788
 
2034
2789
  // src/tools/billing.ts
2035
- import { z as z10 } from "zod";
2790
+ import { z as z13 } from "zod";
2036
2791
  var BILLING_SCOPE_PATH = {
2037
2792
  all: "/billing",
2038
2793
  pods: "/billing/pods",
@@ -2048,7 +2803,7 @@ function registerBillingTools(server, rt) {
2048
2803
  "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.",
2049
2804
  {
2050
2805
  ...listPaginationParams,
2051
- scope: z10.enum([
2806
+ scope: z13.enum([
2052
2807
  "all",
2053
2808
  "pods",
2054
2809
  "serverless",
@@ -2058,14 +2813,14 @@ function registerBillingTools(server, rt) {
2058
2813
  ]).optional().describe(
2059
2814
  'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2060
2815
  ),
2061
- startTime: z10.string().optional().describe(
2816
+ startTime: z13.string().optional().describe(
2062
2817
  "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2063
2818
  ),
2064
- endTime: z10.string().optional().describe(
2819
+ endTime: z13.string().optional().describe(
2065
2820
  "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2066
2821
  ),
2067
- bucketSize: z10.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2068
- lastN: z10.number().int().positive().optional().describe(
2822
+ bucketSize: z13.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2823
+ lastN: z13.number().int().positive().optional().describe(
2069
2824
  "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2070
2825
  )
2071
2826
  },
@@ -2100,17 +2855,24 @@ function registerBillingTools(server, rt) {
2100
2855
  function registerTools(server, ctx, deps = {}) {
2101
2856
  const rt = createToolRuntime(server, ctx, deps);
2102
2857
  registerCatalogTools(server, rt);
2858
+ registerHubTools(server, rt);
2859
+ registerPublicEndpointTools(server, rt);
2103
2860
  registerPodTools(server, rt);
2104
2861
  registerEndpointTools(server, rt);
2862
+ registerEndpointGpuTools(server, rt);
2105
2863
  registerJobTools(server, rt);
2106
2864
  registerTemplateTools(server, rt);
2107
2865
  registerNetworkVolumeTools(server, rt);
2108
2866
  registerRegistryTools(server, rt);
2109
- registerTagTools(server, rt);
2110
2867
  registerBillingTools(server, rt);
2111
2868
  }
2112
2869
 
2113
2870
  export {
2871
+ restV1Base,
2872
+ restV2Base,
2873
+ serverlessBase,
2874
+ authedGraphqlBase,
2875
+ restVersionSummary,
2114
2876
  registerTools
2115
2877
  };
2116
- //# sourceMappingURL=chunk-ZP3TCVXG.mjs.map
2878
+ //# sourceMappingURL=chunk-GTBCNGHE.mjs.map