@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.
package/dist/stdio.mjs CHANGED
@@ -6,81 +6,6 @@ import fetch2 from "node-fetch";
6
6
 
7
7
  // src/server.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
- var SERVER_NAME = "Runpod API Server";
10
- var SERVER_VERSION = true ? "2.0.0" : "dev";
11
- function createServer(version = SERVER_VERSION) {
12
- return new McpServer({
13
- name: SERVER_NAME,
14
- version,
15
- capabilities: {
16
- resources: {},
17
- tools: {}
18
- }
19
- });
20
- }
21
-
22
- // src/tools/runtime.ts
23
- import fetch from "node-fetch";
24
- import { randomUUID } from "crypto";
25
-
26
- // src/_shared/http.ts
27
- var HttpError = class extends Error {
28
- constructor(prefix, status, body) {
29
- super(`${prefix}: ${status} - ${body}`);
30
- this.name = "HttpError";
31
- this.status = status;
32
- this.body = body;
33
- }
34
- };
35
- function isJsonContentType(contentType) {
36
- if (!contentType) return false;
37
- const ct = contentType.toLowerCase();
38
- return ct.includes("application/json") || ct.includes("+json");
39
- }
40
- function methodSendsBody(method) {
41
- return method === "POST" || method === "PATCH" || method === "PUT";
42
- }
43
- function buildRequestHeaders(apiKey, tracking) {
44
- return {
45
- Authorization: `Bearer ${apiKey}`,
46
- "Content-Type": "application/json",
47
- ...tracking()
48
- };
49
- }
50
- function createHttpClient(deps) {
51
- return async function request(url, method = "GET", body) {
52
- const init = {
53
- method,
54
- headers: buildRequestHeaders(deps.apiKey, deps.tracking)
55
- };
56
- if (body && methodSendsBody(method)) {
57
- init.body = JSON.stringify(body);
58
- }
59
- const response = await deps.fetch(url, init);
60
- if (!response.ok) {
61
- throw new HttpError(
62
- deps.errorPrefix,
63
- response.status,
64
- await response.text()
65
- );
66
- }
67
- return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
68
- };
69
- }
70
-
71
- // src/_shared/tracking.ts
72
- function sanitizeUaToken(value) {
73
- return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
74
- }
75
- function buildTrackingHeaders(input) {
76
- const name = sanitizeUaToken(input.clientName || "unknown");
77
- const version = sanitizeUaToken(input.clientVersion || "unknown");
78
- const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
79
- return {
80
- "User-Agent": userAgent,
81
- "X-Runpod-Session-Id": input.sessionId
82
- };
83
- }
84
9
 
85
10
  // src/_shared/mappers.ts
86
11
  function compact(obj) {
@@ -94,6 +19,9 @@ function persistentMount(size, path) {
94
19
  if (size === void 0 || path === void 0) return void 0;
95
20
  return { persistent: { size, path } };
96
21
  }
22
+ function joinStartCmd(cmd) {
23
+ return cmd && cmd.length ? cmd.join(" ") : void 0;
24
+ }
97
25
  function containerConfigToV2(p) {
98
26
  return compact({
99
27
  image: p.imageName,
@@ -112,6 +40,13 @@ function mapPodCreateToV2(params) {
112
40
  return compact({
113
41
  ...containerConfigToV2(params),
114
42
  name: params.name,
43
+ // Private-image registry credential (v2 ContainerConfig `registry`). When
44
+ // deploying from a template, this spreads over the template's registry, so
45
+ // passing it overrides the inherited credential. An empty string is an
46
+ // explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
47
+ // template's inherited credential. Undefined is dropped by compact, so an
48
+ // omitted param leaves the template's registry in place.
49
+ registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
115
50
  cloud: params.cloudType,
116
51
  // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
117
52
  // centers; omit/empty = scheduler chooses). Pass the v1 list straight
@@ -168,7 +103,8 @@ function mapNetworkVolumeCreateToV2(params) {
168
103
  return compact({
169
104
  name: params.name,
170
105
  size: params.size,
171
- dataCenter: params.dataCenterId
106
+ dataCenter: params.dataCenterId,
107
+ type: params.volumeType
172
108
  });
173
109
  }
174
110
  function mapTemplateCreateToV2(params) {
@@ -177,18 +113,39 @@ function mapTemplateCreateToV2(params) {
177
113
  ...compact({
178
114
  name: params.name,
179
115
  serverless: params.isServerless,
180
- registry: params.containerRegistryAuthId
181
- }),
182
- // Required by v2 — always emit, defaulting to NVIDIA.
183
- category: params.category ?? "NVIDIA"
116
+ registry: params.containerRegistryAuthId,
117
+ args: joinStartCmd(params.dockerStartCmd),
118
+ // No longer required by v2 (it defaults to NVIDIA server-side), so an
119
+ // unset category is dropped by compact instead of being forced here.
120
+ category: params.category
121
+ })
184
122
  };
185
123
  }
124
+ function podBodyFromTemplate(template) {
125
+ const out = {};
126
+ const keys = [
127
+ "name",
128
+ "image",
129
+ "args",
130
+ "disk",
131
+ "ports",
132
+ "env",
133
+ "mounts",
134
+ "registry"
135
+ ];
136
+ for (const key of keys) {
137
+ const value = template[key];
138
+ if (value !== void 0 && value !== null) out[key] = value;
139
+ }
140
+ return out;
141
+ }
186
142
  function mapTemplateUpdateToV2(params) {
187
143
  return {
188
144
  ...containerConfigToV2(params),
189
145
  ...compact({
190
146
  name: params.name,
191
- registry: params.containerRegistryAuthId
147
+ registry: params.containerRegistryAuthId,
148
+ args: joinStartCmd(params.dockerStartCmd)
192
149
  })
193
150
  };
194
151
  }
@@ -205,7 +162,6 @@ var V2_UNWRAP_KEY = {
205
162
  dataCenters: "dataCenters",
206
163
  endpoints: "endpoints",
207
164
  jobs: "jobs",
208
- tags: "tags",
209
165
  workers: "workers",
210
166
  billing: "records"
211
167
  };
@@ -219,7 +175,6 @@ var RESOURCE_ENV_KEY = {
219
175
  dataCenters: "DATA_CENTERS",
220
176
  endpoints: "ENDPOINTS",
221
177
  jobs: "JOBS",
222
- tags: "TAGS",
223
178
  workers: "WORKERS",
224
179
  billing: "BILLING"
225
180
  };
@@ -235,6 +190,9 @@ function serverlessBase(env) {
235
190
  function publicGraphqlBase(env) {
236
191
  return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
237
192
  }
193
+ function authedGraphqlBase(env) {
194
+ return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
195
+ }
238
196
  function unwrapList(resource, version, raw) {
239
197
  if (version === "v1") return Array.isArray(raw) ? raw : [];
240
198
  if (raw === null || typeof raw !== "object") return [];
@@ -258,6 +216,14 @@ function wantsAutoProbe(env) {
258
216
  ([k, v]) => k.startsWith("RUNPOD_REST_VERSION") && (v ?? "").toLowerCase() === "auto"
259
217
  );
260
218
  }
219
+ function restVersionSummary(env) {
220
+ const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
221
+ const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
222
+ const overrides = Object.keys(env).filter(
223
+ (k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
224
+ ).length;
225
+ return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
226
+ }
261
227
  var PROBE_TIMEOUT_MS = 4e3;
262
228
  function classifyProbeStatus(status) {
263
229
  if (status >= 500) return void 0;
@@ -323,7 +289,6 @@ var CATALOG = /* @__PURE__ */ new Set([
323
289
  "dataCenters"
324
290
  ]);
325
291
  var V2_ONLY = /* @__PURE__ */ new Set([
326
- "tags",
327
292
  "workers",
328
293
  "billing"
329
294
  ]);
@@ -341,7 +306,6 @@ var V2_REST_PATHS = {
341
306
  list: "/catalog/datacenters",
342
307
  get: (id) => `/catalog/datacenters/${id}`
343
308
  },
344
- tags: { list: "/tags", get: (id) => `/tags/${id}` },
345
309
  // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
346
310
  // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
347
311
  // unwrap key stays correct.
@@ -438,8 +402,186 @@ function resolveBackend(opts) {
438
402
  return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
439
403
  }
440
404
 
405
+ // src/server.ts
406
+ var SERVER_NAME = "Runpod API Server";
407
+ var SERVER_VERSION = true ? "3.0.0" : "dev";
408
+ function createServer(version = SERVER_VERSION) {
409
+ return new McpServer({
410
+ name: SERVER_NAME,
411
+ version: `${version} [${restVersionSummary(process.env)}]`,
412
+ capabilities: {
413
+ resources: {},
414
+ tools: {}
415
+ }
416
+ });
417
+ }
418
+
419
+ // src/tools/runtime.ts
420
+ import fetch from "node-fetch";
421
+ import { randomUUID } from "crypto";
422
+
423
+ // src/_shared/http.ts
424
+ var HttpError = class extends Error {
425
+ constructor(prefix, status, body) {
426
+ 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)" : "";
427
+ super(`${prefix}: ${status} - ${body}${hint}`);
428
+ this.name = "HttpError";
429
+ this.status = status;
430
+ this.body = body;
431
+ }
432
+ };
433
+ function isJsonContentType(contentType) {
434
+ if (!contentType) return false;
435
+ const ct = contentType.toLowerCase();
436
+ return ct.includes("application/json") || ct.includes("+json");
437
+ }
438
+ function methodSendsBody(method) {
439
+ return method === "POST" || method === "PATCH" || method === "PUT";
440
+ }
441
+ function buildRequestHeaders(apiKey, tracking) {
442
+ return {
443
+ Authorization: `Bearer ${apiKey}`,
444
+ "Content-Type": "application/json",
445
+ ...tracking()
446
+ };
447
+ }
448
+ function createHttpClient(deps) {
449
+ return async function request(url, method = "GET", body) {
450
+ const init = {
451
+ method,
452
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
453
+ };
454
+ if (body && methodSendsBody(method)) {
455
+ init.body = JSON.stringify(body);
456
+ }
457
+ const response = await deps.fetch(url, init);
458
+ if (!response.ok) {
459
+ throw new HttpError(
460
+ deps.errorPrefix,
461
+ response.status,
462
+ await response.text()
463
+ );
464
+ }
465
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
466
+ };
467
+ }
468
+
469
+ // src/_shared/tracking.ts
470
+ function sanitizeUaToken(value) {
471
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
472
+ }
473
+ function buildTrackingHeaders(input) {
474
+ const name = sanitizeUaToken(input.clientName || "unknown");
475
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
476
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
477
+ return {
478
+ "User-Agent": userAgent,
479
+ "X-Runpod-Session-Id": input.sessionId
480
+ };
481
+ }
482
+
483
+ // src/tools/logs.ts
484
+ import { z } from "zod";
485
+ function parseLogSse(raw) {
486
+ const items = [];
487
+ for (const block of raw.split(/\r?\n\r?\n/)) {
488
+ const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
489
+ if (!dataLines.length) continue;
490
+ const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
491
+ if (!payload.trim()) continue;
492
+ try {
493
+ const parsed = JSON.parse(payload);
494
+ items.push(
495
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
496
+ );
497
+ } catch {
498
+ items.push({ raw: payload });
499
+ }
500
+ }
501
+ return items;
502
+ }
503
+ var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
504
+ var LOG_STREAM_MAX_BYTES = 256 * 1024;
505
+ async function readSseSnapshot(fetchImpl, url, headers, opts) {
506
+ const controller = new AbortController();
507
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
508
+ const chunks = [];
509
+ let bytes = 0;
510
+ let truncated = false;
511
+ try {
512
+ const res = await fetchImpl(url, {
513
+ method: "GET",
514
+ headers,
515
+ signal: controller.signal
516
+ });
517
+ if (!res.ok) {
518
+ throw new HttpError(
519
+ "Runpod API Error",
520
+ res.status,
521
+ await res.text().catch(() => "")
522
+ );
523
+ }
524
+ for await (const chunk of res.body ?? []) {
525
+ chunks.push(chunk);
526
+ bytes += chunk.length;
527
+ if (bytes >= opts.maxBytes) {
528
+ truncated = true;
529
+ controller.abort();
530
+ break;
531
+ }
532
+ }
533
+ } catch (err) {
534
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
535
+ } finally {
536
+ clearTimeout(timer);
537
+ }
538
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
539
+ }
540
+ var logStreamParams = {
541
+ source: z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
542
+ tail: z.number().int().min(0).max(5e3).optional().describe(
543
+ "Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
544
+ ),
545
+ since: z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
546
+ maxWaitMs: z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
547
+ };
548
+ async function collectLogSnapshot(streamSse, logsUrl, params) {
549
+ const qs = new URLSearchParams();
550
+ if (params.source && params.source !== "both")
551
+ qs.append("source", params.source);
552
+ if (params.tail !== void 0) qs.append("tail", String(params.tail));
553
+ if (params.since) qs.append("since", params.since);
554
+ const query = qs.toString() ? `?${qs}` : "";
555
+ const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
556
+ maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
557
+ maxBytes: LOG_STREAM_MAX_BYTES
558
+ });
559
+ const items = parseLogSse(raw);
560
+ if (truncated) items.pop();
561
+ return { items, count: items.length, truncated };
562
+ }
563
+ async function streamLogsReply(rt, tool, params) {
564
+ const backend = rt.backendFor(tool.resource);
565
+ if (backend.version === "v1") {
566
+ return rt.jsonReply({
567
+ error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
568
+ status: 501
569
+ });
570
+ }
571
+ try {
572
+ return rt.jsonReply(
573
+ await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
574
+ );
575
+ } catch (error) {
576
+ if (error instanceof HttpError) {
577
+ return rt.jsonReply({ error: error.message, status: error.status });
578
+ }
579
+ throw error;
580
+ }
581
+ }
582
+
441
583
  // src/tools/runtime.ts
442
- var MCP_SERVER_VERSION = true ? "2.0.0" : "dev";
584
+ var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
443
585
  var SESSION_ID = randomUUID();
444
586
  function trackingHeaders(server, ctx) {
445
587
  const info = server.server.getClientVersion();
@@ -476,14 +618,18 @@ var DESTRUCTIVE = {
476
618
  idempotentHint: true,
477
619
  openWorldHint: true
478
620
  };
479
- async function graphqlRequest(query, tracking, fetchImpl, url) {
621
+ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
480
622
  const response = await fetchImpl(url, {
481
623
  method: "POST",
482
624
  headers: {
483
625
  "Content-Type": "application/json",
626
+ ...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
484
627
  ...tracking()
485
628
  },
486
- body: JSON.stringify({ query })
629
+ body: JSON.stringify({
630
+ query,
631
+ ...options?.variables ? { variables: options.variables } : {}
632
+ })
487
633
  });
488
634
  const result = await response.json();
489
635
  if (result.errors && result.errors.length > 0) {
@@ -498,7 +644,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
498
644
  }
499
645
  function createToolRuntime(server, ctx, deps = {}) {
500
646
  const tracking = () => trackingHeaders(server, ctx);
501
- const httpFetch = deps.fetch ?? defaultFetch;
647
+ function observeUnauthorized(fn) {
648
+ const notify = ctx.onUnauthorized;
649
+ if (!notify) return fn;
650
+ return async (...args) => {
651
+ const response = await fn(...args);
652
+ if (response.status === 401) notify();
653
+ return response;
654
+ };
655
+ }
656
+ const rawFetch = deps.fetch ?? defaultFetch;
657
+ const httpFetch = observeUnauthorized(rawFetch);
502
658
  const v1Client = createHttpClient({
503
659
  apiKey: ctx.apiKey,
504
660
  fetch: httpFetch,
@@ -533,47 +689,19 @@ function createToolRuntime(server, ctx, deps = {}) {
533
689
  "Error calling Runpod API:",
534
690
  () => restClient(url, method, body)
535
691
  );
536
- const streamSse = deps.streamSse ?? (async (url, opts) => {
537
- const controller = new AbortController();
538
- const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
539
- try {
540
- const res = await fetch(url, {
541
- method: "GET",
542
- headers: {
543
- Authorization: `Bearer ${ctx.apiKey}`,
544
- Accept: "text/event-stream",
545
- ...tracking()
546
- },
547
- signal: controller.signal
548
- });
549
- if (!res.ok) {
550
- throw new HttpError(
551
- "Runpod API Error",
552
- res.status,
553
- await res.text().catch(() => "")
554
- );
555
- }
556
- const chunks = [];
557
- let bytes = 0;
558
- let truncated = false;
559
- try {
560
- for await (const chunk of res.body) {
561
- chunks.push(chunk);
562
- bytes += chunk.length;
563
- if (bytes >= opts.maxBytes) {
564
- truncated = true;
565
- controller.abort();
566
- break;
567
- }
568
- }
569
- } catch (err) {
570
- if (!(err instanceof Error && err.name === "AbortError")) throw err;
571
- }
572
- return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
573
- } finally {
574
- clearTimeout(timer);
575
- }
576
- });
692
+ const sseFetch = observeUnauthorized(
693
+ deps.sseFetch ?? fetch
694
+ );
695
+ const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
696
+ sseFetch,
697
+ url,
698
+ {
699
+ Authorization: `Bearer ${ctx.apiKey}`,
700
+ Accept: "text/event-stream",
701
+ ...tracking()
702
+ },
703
+ opts
704
+ ));
577
705
  const backendFor = (resource) => resolveBackend({
578
706
  resource,
579
707
  env: process.env,
@@ -595,9 +723,18 @@ function createToolRuntime(server, ctx, deps = {}) {
595
723
  graphql: (query) => graphqlRequest(
596
724
  query,
597
725
  tracking,
598
- httpFetch,
726
+ // rawFetch, not httpFetch: this call carries no credential (see above).
727
+ rawFetch,
599
728
  publicGraphqlBase(process.env)
600
729
  ),
730
+ graphqlAuthed: (query, variables) => graphqlRequest(
731
+ query,
732
+ tracking,
733
+ httpFetch,
734
+ // NOT publicGraphqlBase: this call carries the caller's API key.
735
+ authedGraphqlBase(process.env),
736
+ { variables, apiKey: ctx.apiKey }
737
+ ),
601
738
  runpodRequest,
602
739
  serverlessRequest,
603
740
  callRestUrl,
@@ -609,17 +746,17 @@ function createToolRuntime(server, ctx, deps = {}) {
609
746
  }
610
747
 
611
748
  // src/tools/catalog.ts
612
- import { z as z2 } from "zod";
749
+ import { z as z3 } from "zod";
613
750
 
614
751
  // src/pagination.ts
615
- import { z } from "zod";
752
+ import { z as z2 } from "zod";
616
753
  var DEFAULT_LIST_LIMIT = 20;
617
754
  var MAX_LIST_LIMIT = 100;
618
755
  var listPaginationParams = {
619
- limit: z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
756
+ limit: z2.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
620
757
  `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
621
758
  ),
622
- cursor: z.string().optional().describe(
759
+ cursor: z2.string().optional().describe(
623
760
  "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
624
761
  )
625
762
  };
@@ -677,25 +814,32 @@ function registerCatalogTools(server, rt) {
677
814
  const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
678
815
  server.tool(
679
816
  "list-gpu-types",
680
- "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.",
817
+ "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.",
681
818
  {
682
819
  ...listPaginationParams,
683
- minMemoryGb: z2.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
684
- secureCloudOnly: z2.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
685
- communityCloudOnly: z2.boolean().optional().describe("Filter to only GPUs available in community cloud"),
686
- searchTerm: z2.string().optional().describe(
820
+ minMemoryGb: z3.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
821
+ secureCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
822
+ communityCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in community cloud"),
823
+ searchTerm: z3.string().optional().describe(
687
824
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
688
825
  ),
689
- includeUnavailable: z2.boolean().optional().describe(
690
- "Include GPUs that are currently out of stock. Default is false"
826
+ includeUnavailable: z3.boolean().optional().describe(
827
+ "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."
828
+ ),
829
+ includeAvailability: z3.boolean().optional().describe(
830
+ "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."
691
831
  )
692
832
  },
693
833
  { title: "List GPU types", ...READ_ONLY },
694
834
  async (params) => {
695
835
  const backend = backendFor("gpus");
696
836
  if (backend.version === "v2") {
697
- const raw = await callRestUrl(`${backend.base}${backend.list}`);
837
+ const wantAvailability = params.includeAvailability !== false;
838
+ const raw = await callRestUrl(
839
+ `${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
840
+ );
698
841
  let gpus = backend.unwrap(raw);
842
+ gpus = gpus.filter((g) => g.id !== "unknown");
699
843
  if (params.minMemoryGb !== void 0)
700
844
  gpus = gpus.filter(
701
845
  (g) => Number(g.memory ?? 0) >= params.minMemoryGb
@@ -708,6 +852,18 @@ function registerCatalogTools(server, rt) {
708
852
  (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
709
853
  );
710
854
  }
855
+ if (params.includeUnavailable === false)
856
+ gpus = gpus.filter((g) => g.availability !== "NONE");
857
+ const rank = {
858
+ HIGH: 3,
859
+ MEDIUM: 2,
860
+ LOW: 1,
861
+ NONE: 0
862
+ };
863
+ gpus.sort(
864
+ (a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
865
+ );
866
+ gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
711
867
  return capListResult(gpus, {
712
868
  limit: params.limit,
713
869
  cursor: params.cursor
@@ -737,7 +893,7 @@ function registerCatalogTools(server, rt) {
737
893
  return !!status && status !== "Out";
738
894
  };
739
895
  let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
740
- if (!params.includeUnavailable) {
896
+ if (params.includeUnavailable === false) {
741
897
  gpuTypes = gpuTypes.filter(isAvailable);
742
898
  }
743
899
  if (params.minMemoryGb) {
@@ -783,7 +939,7 @@ function registerCatalogTools(server, rt) {
783
939
  "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
784
940
  {
785
941
  ...listPaginationParams,
786
- region: z2.string().optional().describe(
942
+ region: z3.string().optional().describe(
787
943
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
788
944
  )
789
945
  },
@@ -839,80 +995,654 @@ function registerCatalogTools(server, rt) {
839
995
  status: 501
840
996
  });
841
997
  }
842
- const raw = await callRestUrl(`${backend.base}${backend.list}`);
843
- return capListResult(backend.unwrap(raw), {
998
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
999
+ return capListResult(backend.unwrap(raw), {
1000
+ limit: params.limit,
1001
+ cursor: params.cursor
1002
+ });
1003
+ }
1004
+ );
1005
+ server.tool(
1006
+ "get-gpu-type",
1007
+ "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.",
1008
+ {
1009
+ gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
1010
+ includeAvailability: z3.boolean().optional().describe(
1011
+ "Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
1012
+ )
1013
+ },
1014
+ { title: "Get GPU type", ...READ_ONLY },
1015
+ async (params) => {
1016
+ const backend = backendFor("gpus");
1017
+ if (backend.version === "v1") {
1018
+ return jsonReply2({
1019
+ error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
1020
+ status: 501
1021
+ });
1022
+ }
1023
+ const path = backend.get(encodeURIComponent(params.gpuTypeId));
1024
+ const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
1025
+ const result = await callRestUrl(`${backend.base}${path}${query}`);
1026
+ return jsonReply2(result);
1027
+ }
1028
+ );
1029
+ server.tool(
1030
+ "get-cpu-type",
1031
+ "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).",
1032
+ {
1033
+ cpuTypeId: z3.string().describe("ID of the CPU type to retrieve")
1034
+ },
1035
+ { title: "Get CPU type", ...READ_ONLY },
1036
+ async (params) => {
1037
+ const backend = backendFor("cpus");
1038
+ if (backend.version === "v1") {
1039
+ return jsonReply2({
1040
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1041
+ status: 501
1042
+ });
1043
+ }
1044
+ const result = await callRestUrl(
1045
+ `${backend.base}${backend.get(params.cpuTypeId)}`
1046
+ );
1047
+ return jsonReply2(result);
1048
+ }
1049
+ );
1050
+ server.tool(
1051
+ "get-data-center",
1052
+ "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).",
1053
+ {
1054
+ dataCenterId: z3.string().describe("ID of the data center to retrieve")
1055
+ },
1056
+ { title: "Get data center", ...READ_ONLY },
1057
+ async (params) => {
1058
+ const backend = backendFor("dataCenters");
1059
+ if (backend.version === "v1") {
1060
+ return jsonReply2({
1061
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1062
+ status: 501
1063
+ });
1064
+ }
1065
+ const result = await callRestUrl(
1066
+ `${backend.base}${backend.get(params.dataCenterId)}`
1067
+ );
1068
+ return jsonReply2(result);
1069
+ }
1070
+ );
1071
+ }
1072
+
1073
+ // src/tools/hub.ts
1074
+ import { z as z4 } from "zod";
1075
+ function parseReleaseConfig(config) {
1076
+ if (!config) return void 0;
1077
+ try {
1078
+ return JSON.parse(config);
1079
+ } catch {
1080
+ return config;
1081
+ }
1082
+ }
1083
+ function listingsQuery(withConfig) {
1084
+ return `
1085
+ query {
1086
+ listings(input: {}) {
1087
+ id
1088
+ repoId
1089
+ title
1090
+ description
1091
+ repoName
1092
+ repoOwner
1093
+ createdAt
1094
+ updatedAt
1095
+ views
1096
+ stars
1097
+ deploys
1098
+ language
1099
+ category
1100
+ tags
1101
+ type
1102
+ listedRelease {
1103
+ id
1104
+ name
1105
+ tagName
1106
+ createdAt${withConfig ? "\n config" : ""}
1107
+ build {
1108
+ id
1109
+ imageName
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ `;
1115
+ }
1116
+ function serializeBoolean(value, input) {
1117
+ if (value === "true") return input.trueValue ?? "true";
1118
+ if (value === "false") return input.falseValue ?? "false";
1119
+ return value;
1120
+ }
1121
+ function buildHubEnv(config, overrides) {
1122
+ const env = [];
1123
+ const missingRequired = [];
1124
+ const remaining = { ...overrides };
1125
+ for (const entry of config.env ?? []) {
1126
+ const input = entry.input ?? {};
1127
+ const override = entry.key in remaining ? remaining[entry.key] : void 0;
1128
+ delete remaining[entry.key];
1129
+ if (override !== void 0 && override !== "") {
1130
+ env.push({
1131
+ key: entry.key,
1132
+ value: input.type === "boolean" ? serializeBoolean(override, input) : override
1133
+ });
1134
+ continue;
1135
+ }
1136
+ if (input.default !== void 0 && input.default !== null) {
1137
+ env.push({
1138
+ key: entry.key,
1139
+ value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
1140
+ });
1141
+ continue;
1142
+ }
1143
+ if (input.required) {
1144
+ missingRequired.push(entry.key);
1145
+ env.push({ key: entry.key, value: "" });
1146
+ continue;
1147
+ }
1148
+ }
1149
+ for (const [key, value] of Object.entries(remaining)) {
1150
+ env.push({ key, value });
1151
+ }
1152
+ return { env, missingRequired };
1153
+ }
1154
+ function minCudaVersion(versions) {
1155
+ if (!versions || versions.length === 0) return null;
1156
+ const compare = (a, b) => {
1157
+ const pa = a.split(".").map(Number);
1158
+ const pb = b.split(".").map(Number);
1159
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1160
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1161
+ if (d !== 0) return d;
1162
+ }
1163
+ return 0;
1164
+ };
1165
+ return [...versions].sort(compare)[0];
1166
+ }
1167
+ function randomSuffix() {
1168
+ return Math.random().toString(36).slice(2, 8);
1169
+ }
1170
+ function registerHubTools(server, rt) {
1171
+ const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
1172
+ server.tool(
1173
+ "list-hub-repos",
1174
+ "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.",
1175
+ {
1176
+ ...listPaginationParams,
1177
+ searchTerm: z4.string().optional().describe(
1178
+ "Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
1179
+ ),
1180
+ category: z4.string().optional().describe(
1181
+ "Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
1182
+ ),
1183
+ type: z4.enum(["SERVERLESS", "POD"]).optional().describe(
1184
+ "Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
1185
+ ),
1186
+ repoOwner: z4.string().optional().describe(
1187
+ "Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
1188
+ ),
1189
+ includeConfig: z4.boolean().optional().describe(
1190
+ "Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
1191
+ )
1192
+ },
1193
+ { title: "List Hub repos", ...READ_ONLY },
1194
+ async (params) => {
1195
+ const data = await graphql(
1196
+ listingsQuery(params.includeConfig === true)
1197
+ );
1198
+ let listings = data.listings;
1199
+ if (params.type) {
1200
+ listings = listings.filter(
1201
+ (l) => (l.type ?? "").toUpperCase() === params.type
1202
+ );
1203
+ }
1204
+ if (params.category) {
1205
+ const term = params.category.toLowerCase();
1206
+ listings = listings.filter(
1207
+ (l) => (l.category ?? "").toLowerCase() === term
1208
+ );
1209
+ }
1210
+ if (params.repoOwner) {
1211
+ const term = params.repoOwner.toLowerCase();
1212
+ listings = listings.filter(
1213
+ (l) => (l.repoOwner ?? "").toLowerCase() === term
1214
+ );
1215
+ }
1216
+ if (params.searchTerm) {
1217
+ const term = params.searchTerm.toLowerCase();
1218
+ listings = listings.filter(
1219
+ (l) => [
1220
+ l.title,
1221
+ l.description,
1222
+ l.repoName,
1223
+ l.repoOwner,
1224
+ ...l.tags ?? []
1225
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1226
+ );
1227
+ }
1228
+ listings = [...listings].sort(
1229
+ (a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
1230
+ );
1231
+ const result = listings.map((l) => ({
1232
+ id: l.id,
1233
+ repo: `${l.repoOwner}/${l.repoName}`,
1234
+ title: l.title,
1235
+ description: l.description,
1236
+ type: l.type,
1237
+ category: l.category,
1238
+ tags: l.tags,
1239
+ language: l.language,
1240
+ stars: l.stars,
1241
+ deploys: l.deploys,
1242
+ views: l.views,
1243
+ githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
1244
+ hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
1245
+ createdAt: l.createdAt,
1246
+ updatedAt: l.updatedAt,
1247
+ listedRelease: l.listedRelease ? {
1248
+ hubReleaseId: l.listedRelease.id,
1249
+ name: l.listedRelease.name,
1250
+ tagName: l.listedRelease.tagName,
1251
+ createdAt: l.listedRelease.createdAt,
1252
+ imageName: l.listedRelease.build?.imageName,
1253
+ ...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
1254
+ } : null
1255
+ }));
1256
+ return capListResult(result, {
844
1257
  limit: params.limit,
845
1258
  cursor: params.cursor
846
1259
  });
847
1260
  }
848
1261
  );
849
1262
  server.tool(
850
- "get-gpu-type",
851
- "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).",
1263
+ "deploy-hub-repo",
1264
+ "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.",
852
1265
  {
853
- gpuTypeId: z2.string().describe("ID of the GPU type to retrieve")
1266
+ repo: z4.string().optional().describe(
1267
+ `The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
1268
+ ),
1269
+ hubReleaseId: z4.string().optional().describe(
1270
+ "The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
1271
+ ),
1272
+ name: z4.string().optional().describe(
1273
+ "Name for the new endpoint. Defaults to '<listing title> <release tag>'."
1274
+ ),
1275
+ env: z4.record(z4.string()).optional().describe(
1276
+ "Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
1277
+ ),
1278
+ gpuIds: z4.string().optional().describe(
1279
+ "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."
1280
+ ),
1281
+ gpuCount: z4.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
1282
+ containerDiskInGb: z4.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
1283
+ workersMin: z4.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
1284
+ workersMax: z4.number().int().positive().optional().describe("Maximum workers (default: account default)."),
1285
+ idleTimeout: z4.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
1286
+ scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
1287
+ scalerValue: z4.number().optional().describe("Autoscaler target value (default 4)."),
1288
+ executionTimeoutMs: z4.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
1289
+ flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
854
1290
  },
855
- { title: "Get GPU type", ...READ_ONLY },
1291
+ { title: "Deploy Hub repo", ...WRITE },
856
1292
  async (params) => {
857
- const backend = backendFor("gpus");
858
- if (backend.version === "v1") {
1293
+ if (!params.repo && !params.hubReleaseId) {
859
1294
  return jsonReply2({
860
- error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
861
- status: 501
1295
+ error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
862
1296
  });
863
1297
  }
864
- const result = await callRestUrl(
865
- `${backend.base}${backend.get(params.gpuTypeId)}`
1298
+ const catalog = await graphql(listingsQuery(true));
1299
+ let listing;
1300
+ if (params.hubReleaseId) {
1301
+ listing = catalog.listings.find(
1302
+ (l) => l.listedRelease?.id === params.hubReleaseId
1303
+ );
1304
+ } else {
1305
+ const repoKey = params.repo.toLowerCase();
1306
+ listing = catalog.listings.find(
1307
+ (l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
1308
+ );
1309
+ }
1310
+ if (!listing) {
1311
+ return jsonReply2({
1312
+ 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.`
1313
+ });
1314
+ }
1315
+ const release = listing.listedRelease;
1316
+ if (!release?.build?.imageName) {
1317
+ return jsonReply2({
1318
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
1319
+ });
1320
+ }
1321
+ if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
1322
+ return jsonReply2({
1323
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
1324
+ });
1325
+ }
1326
+ const config = parseReleaseConfig(release.config) ?? {};
1327
+ const gpuIds = params.gpuIds ?? config.gpuIds;
1328
+ if (!gpuIds) {
1329
+ return jsonReply2({
1330
+ 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).'
1331
+ });
1332
+ }
1333
+ const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
1334
+ if (missingRequired.length > 0) {
1335
+ return jsonReply2({
1336
+ error: `Missing required environment variables for this release: ${missingRequired.join(
1337
+ ", "
1338
+ )}. Pass them via the env parameter.`
1339
+ });
1340
+ }
1341
+ const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
1342
+ const minCuda = minCudaVersion(config.allowedCudaVersions);
1343
+ const input = {
1344
+ name: endpointName,
1345
+ hubReleaseId: release.id,
1346
+ type: "QB",
1347
+ gpuIds,
1348
+ gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
1349
+ workersMin: params.workersMin ?? 0,
1350
+ workersMax: params.workersMax ?? null,
1351
+ idleTimeout: params.idleTimeout ?? 5,
1352
+ scalerType: params.scalerType ?? "QUEUE_DELAY",
1353
+ scalerValue: params.scalerValue ?? 4,
1354
+ executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
1355
+ flashBootType: params.flashboot ?? "FLASHBOOT",
1356
+ locations: null,
1357
+ networkVolumeIds: null,
1358
+ compliance: [],
1359
+ modelReferences: [],
1360
+ ...minCuda ? {
1361
+ minCudaVersion: minCuda,
1362
+ allowedCudaVersions: config.allowedCudaVersions.join(",")
1363
+ } : {},
1364
+ template: {
1365
+ name: `${endpointName}__template__${randomSuffix()}`,
1366
+ imageName: release.build.imageName,
1367
+ containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
1368
+ containerRegistryAuthId: "",
1369
+ dockerArgs: "",
1370
+ startScript: "",
1371
+ ports: "",
1372
+ env
1373
+ }
1374
+ };
1375
+ const data = await graphqlAuthed(
1376
+ `
1377
+ mutation saveEndpoint($input: EndpointInput!) {
1378
+ saveEndpoint(input: $input) {
1379
+ id
1380
+ name
1381
+ gpuIds
1382
+ gpuCount
1383
+ workersMin
1384
+ workersMax
1385
+ idleTimeout
1386
+ scalerType
1387
+ scalerValue
1388
+ flashBootType
1389
+ templateId
1390
+ }
1391
+ }
1392
+ `,
1393
+ { input }
866
1394
  );
867
- return jsonReply2(result);
1395
+ return jsonReply2({
1396
+ endpoint: data.saveEndpoint,
1397
+ deployed: {
1398
+ repo: `${listing.repoOwner}/${listing.repoName}`,
1399
+ release: release.tagName,
1400
+ hubReleaseId: release.id,
1401
+ imageName: release.build.imageName
1402
+ },
1403
+ note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
1404
+ });
868
1405
  }
869
1406
  );
1407
+ }
1408
+
1409
+ // src/tools/public-endpoints.ts
1410
+ import { z as z5 } from "zod";
1411
+ function parseMetadata(metadata) {
1412
+ if (!metadata) return {};
1413
+ try {
1414
+ const parsed = JSON.parse(metadata);
1415
+ return parsed && typeof parsed === "object" ? parsed : {};
1416
+ } catch {
1417
+ return {};
1418
+ }
1419
+ }
1420
+ function registerPublicEndpointTools(server, rt) {
1421
+ const { graphql } = rt;
870
1422
  server.tool(
871
- "get-cpu-type",
872
- "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).",
1423
+ "list-public-endpoints",
1424
+ "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.",
873
1425
  {
874
- cpuTypeId: z2.string().describe("ID of the CPU type to retrieve")
1426
+ ...listPaginationParams,
1427
+ searchTerm: z5.string().optional().describe(
1428
+ "Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
1429
+ ),
1430
+ modality: z5.string().optional().describe(
1431
+ "Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
1432
+ ),
1433
+ owner: z5.string().optional().describe(
1434
+ "Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
1435
+ ),
1436
+ includeOffline: z5.boolean().optional().describe(
1437
+ "Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
1438
+ )
875
1439
  },
876
- { title: "Get CPU type", ...READ_ONLY },
1440
+ { title: "List public endpoints", ...READ_ONLY },
877
1441
  async (params) => {
878
- const backend = backendFor("cpus");
879
- if (backend.version === "v1") {
880
- return jsonReply2({
881
- error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
882
- status: 501
883
- });
1442
+ const data = await graphql(`
1443
+ query {
1444
+ allAiApiPublicConfigs {
1445
+ id
1446
+ aiApiId
1447
+ modelName
1448
+ displayName
1449
+ description
1450
+ metadata
1451
+ isLive
1452
+ createdAt
1453
+ updatedAt
1454
+ }
1455
+ }
1456
+ `);
1457
+ let configs = data.allAiApiPublicConfigs.map((c) => ({
1458
+ config: c,
1459
+ meta: parseMetadata(c.metadata)
1460
+ }));
1461
+ if (!params.includeOffline) {
1462
+ configs = configs.filter(({ config }) => config.isLive);
884
1463
  }
885
- const result = await callRestUrl(
886
- `${backend.base}${backend.get(params.cpuTypeId)}`
1464
+ if (params.modality) {
1465
+ const term = params.modality.toLowerCase();
1466
+ configs = configs.filter(
1467
+ ({ meta }) => (meta.source ?? "").toLowerCase() === term
1468
+ );
1469
+ }
1470
+ if (params.owner) {
1471
+ const term = params.owner.toLowerCase();
1472
+ configs = configs.filter(
1473
+ ({ meta }) => (meta.owner ?? "").toLowerCase() === term
1474
+ );
1475
+ }
1476
+ if (params.searchTerm) {
1477
+ const term = params.searchTerm.toLowerCase();
1478
+ configs = configs.filter(
1479
+ ({ config, meta }) => [
1480
+ config.displayName,
1481
+ config.modelName,
1482
+ config.aiApiId,
1483
+ config.description,
1484
+ meta.owner,
1485
+ meta.tag,
1486
+ meta.source
1487
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1488
+ );
1489
+ }
1490
+ configs = [...configs].sort(
1491
+ (a, b) => a.config.displayName.localeCompare(b.config.displayName)
887
1492
  );
888
- return jsonReply2(result);
1493
+ const result = configs.map(({ config, meta }) => ({
1494
+ endpointId: config.aiApiId,
1495
+ displayName: config.displayName,
1496
+ modelName: config.modelName,
1497
+ description: config.description,
1498
+ modality: meta.source,
1499
+ tag: meta.tag,
1500
+ owner: meta.owner,
1501
+ pricing: meta.priceString,
1502
+ isLive: config.isLive,
1503
+ baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
1504
+ createdAt: config.createdAt,
1505
+ updatedAt: config.updatedAt
1506
+ }));
1507
+ return capListResult(result, {
1508
+ limit: params.limit,
1509
+ cursor: params.cursor
1510
+ });
889
1511
  }
890
1512
  );
1513
+ }
1514
+
1515
+ // src/tools/endpoint-gpus.ts
1516
+ import { z as z6 } from "zod";
1517
+ function registerEndpointGpuTools(server, rt) {
1518
+ const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
891
1519
  server.tool(
892
- "get-data-center",
893
- "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).",
1520
+ "set-endpoint-gpus",
1521
+ "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.",
894
1522
  {
895
- dataCenterId: z2.string().describe("ID of the data center to retrieve")
1523
+ endpointId: z6.string().describe("ID of the Serverless endpoint to update"),
1524
+ gpuIds: z6.string().optional().describe(
1525
+ "Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
1526
+ ),
1527
+ pools: z6.array(z6.string()).optional().describe(
1528
+ "GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
1529
+ ),
1530
+ excludeGpuTypeIds: z6.array(z6.string()).optional().describe(
1531
+ "GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
1532
+ ),
1533
+ gpuCount: z6.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
1534
+ minCudaVersion: z6.string().optional().describe(
1535
+ "Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
1536
+ ),
1537
+ allowedCudaVersions: z6.string().optional().describe(
1538
+ "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."
1539
+ )
896
1540
  },
897
- { title: "Get data center", ...READ_ONLY },
1541
+ { title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
898
1542
  async (params) => {
899
- const backend = backendFor("dataCenters");
900
- if (backend.version === "v1") {
1543
+ const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
1544
+ ...params.pools,
1545
+ ...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
1546
+ ].join(",") : void 0);
1547
+ if (!gpuIds) {
901
1548
  return jsonReply2({
902
- error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
903
- status: 501
1549
+ error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
904
1550
  });
905
1551
  }
906
- const result = await callRestUrl(
907
- `${backend.base}${backend.get(params.dataCenterId)}`
1552
+ const data = await graphqlAuthed(`
1553
+ query {
1554
+ myself {
1555
+ endpoints {
1556
+ id
1557
+ name
1558
+ gpuIds
1559
+ gpuCount
1560
+ workersMin
1561
+ workersMax
1562
+ idleTimeout
1563
+ scalerType
1564
+ scalerValue
1565
+ executionTimeoutMs
1566
+ flashBootType
1567
+ type
1568
+ locations
1569
+ templateId
1570
+ allowedCudaVersions
1571
+ minCudaVersion
1572
+ compliance
1573
+ modelReferences
1574
+ networkVolumeIds {
1575
+ networkVolumeId
1576
+ dataCenterId
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ `);
1582
+ const current = data.myself.endpoints.find(
1583
+ (e) => e.id === params.endpointId
908
1584
  );
909
- return jsonReply2(result);
1585
+ if (!current) {
1586
+ return jsonReply2({
1587
+ error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
1588
+ });
1589
+ }
1590
+ const input = {
1591
+ id: current.id,
1592
+ name: current.name,
1593
+ gpuIds,
1594
+ gpuCount: params.gpuCount ?? current.gpuCount,
1595
+ workersMin: current.workersMin,
1596
+ workersMax: current.workersMax,
1597
+ idleTimeout: current.idleTimeout,
1598
+ scalerType: current.scalerType,
1599
+ scalerValue: current.scalerValue,
1600
+ executionTimeoutMs: current.executionTimeoutMs,
1601
+ flashBootType: current.flashBootType,
1602
+ type: current.type,
1603
+ locations: current.locations,
1604
+ networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
1605
+ // Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
1606
+ // ONLY, and the read shape is rejected outright.
1607
+ current.networkVolumeIds.map((v) => ({
1608
+ networkVolumeId: v.networkVolumeId
1609
+ }))
1610
+ ) : null
1611
+ };
1612
+ for (const [key, value] of Object.entries({
1613
+ templateId: current.templateId,
1614
+ allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
1615
+ minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
1616
+ compliance: current.compliance,
1617
+ modelReferences: current.modelReferences
1618
+ })) {
1619
+ if (value !== null && value !== void 0) input[key] = value;
1620
+ }
1621
+ const result = await graphqlAuthed(
1622
+ `
1623
+ mutation saveEndpoint($input: EndpointInput!) {
1624
+ saveEndpoint(input: $input) {
1625
+ id
1626
+ name
1627
+ gpuIds
1628
+ gpuCount
1629
+ workersMin
1630
+ workersMax
1631
+ }
1632
+ }
1633
+ `,
1634
+ { input }
1635
+ );
1636
+ return jsonReply2({
1637
+ endpoint: result.saveEndpoint,
1638
+ previousGpuIds: current.gpuIds
1639
+ });
910
1640
  }
911
1641
  );
912
1642
  }
913
1643
 
914
1644
  // src/tools/pods.ts
915
- import { z as z3 } from "zod";
1645
+ import { z as z7 } from "zod";
916
1646
  function registerPodTools(server, rt) {
917
1647
  const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
918
1648
  server.tool(
@@ -920,12 +1650,12 @@ function registerPodTools(server, rt) {
920
1650
  "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.",
921
1651
  {
922
1652
  ...listPaginationParams,
923
- computeType: z3.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
924
- gpuTypeId: z3.array(z3.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
925
- dataCenterId: z3.array(z3.string()).optional().describe("Filter to Pods in any of the provided data centers"),
926
- name: z3.string().optional().describe("Filter to Pods with the provided name"),
927
- includeMachine: z3.boolean().optional().describe("Include information about the machine"),
928
- includeNetworkVolume: z3.boolean().optional().describe("Include information about attached network volumes")
1653
+ computeType: z7.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
1654
+ gpuTypeId: z7.array(z7.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
1655
+ dataCenterId: z7.array(z7.string()).optional().describe("Filter to Pods in any of the provided data centers"),
1656
+ name: z7.string().optional().describe("Filter to Pods with the provided name"),
1657
+ includeMachine: z7.boolean().optional().describe("Include information about the machine"),
1658
+ includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
929
1659
  },
930
1660
  { title: "List pods", ...READ_ONLY },
931
1661
  async (params) => {
@@ -969,9 +1699,9 @@ function registerPodTools(server, rt) {
969
1699
  "get-pod",
970
1700
  "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
971
1701
  {
972
- podId: z3.string().describe("ID of the pod to retrieve"),
973
- includeMachine: z3.boolean().optional().describe("Include information about the machine"),
974
- includeNetworkVolume: z3.boolean().optional().describe("Include information about attached network volumes")
1702
+ podId: z7.string().describe("ID of the pod to retrieve"),
1703
+ includeMachine: z7.boolean().optional().describe("Include information about the machine"),
1704
+ includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
975
1705
  },
976
1706
  { title: "Get pod", ...READ_ONLY },
977
1707
  async (params) => {
@@ -999,26 +1729,44 @@ function registerPodTools(server, rt) {
999
1729
  );
1000
1730
  server.tool(
1001
1731
  "create-pod",
1002
- '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.',
1732
+ '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.',
1003
1733
  {
1004
- name: z3.string().optional().describe("Name for the pod"),
1005
- imageName: z3.string().describe("Docker image to use"),
1006
- computeType: z3.enum(["GPU", "CPU"]).optional().describe(
1734
+ name: z7.string().optional().describe("Name for the pod"),
1735
+ imageName: z7.string().optional().describe("Docker image to use. Optional when templateId is provided."),
1736
+ templateId: z7.string().optional().describe(
1737
+ "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."
1738
+ ),
1739
+ computeType: z7.enum(["GPU", "CPU"]).optional().describe(
1007
1740
  "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
1008
1741
  ),
1009
- cloudType: z3.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1010
- gpuTypeIds: z3.array(z3.string()).optional().describe("List of acceptable GPU types"),
1011
- gpuCount: z3.number().optional().describe("Number of GPUs"),
1012
- containerDiskInGb: z3.number().optional().describe("Container disk size in GB"),
1013
- volumeInGb: z3.number().optional().describe("Volume size in GB"),
1014
- volumeMountPath: z3.string().optional().describe("Path to mount the volume"),
1015
- ports: z3.array(z3.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1016
- env: z3.record(z3.string()).optional().describe("Environment variables"),
1017
- dataCenterIds: z3.array(z3.string()).optional().describe("List of data centers")
1742
+ cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1743
+ gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
1744
+ gpuCount: z7.number().optional().describe("Number of GPUs"),
1745
+ containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
1746
+ volumeInGb: z7.number().optional().describe("Volume size in GB"),
1747
+ volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
1748
+ ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1749
+ env: z7.record(z7.string()).optional().describe("Environment variables"),
1750
+ containerRegistryAuthId: z7.string().optional().describe(
1751
+ "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)."
1752
+ ),
1753
+ dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
1018
1754
  },
1019
1755
  { title: "Create pod", ...WRITE },
1020
1756
  async (params) => {
1021
1757
  const backend = backendFor("pods");
1758
+ if (params.templateId && backend.version !== "v2") {
1759
+ return jsonReply2({
1760
+ error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
1761
+ status: 501
1762
+ });
1763
+ }
1764
+ if (!params.imageName && !params.templateId) {
1765
+ return jsonReply2({
1766
+ error: "Provide imageName, or templateId to deploy from an existing template.",
1767
+ status: 400
1768
+ });
1769
+ }
1022
1770
  const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
1023
1771
  const hasGpuCount = (params.gpuCount ?? 0) > 0;
1024
1772
  const wantsCpu = params.computeType === "CPU";
@@ -1043,6 +1791,12 @@ function registerPodTools(server, rt) {
1043
1791
  });
1044
1792
  }
1045
1793
  if (wantsCpu) {
1794
+ if (params.templateId) {
1795
+ return jsonReply2({
1796
+ error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
1797
+ status: 400
1798
+ });
1799
+ }
1046
1800
  try {
1047
1801
  const result = await callRestUrl(
1048
1802
  `${restV1Base(env)}/pods`,
@@ -1067,7 +1821,24 @@ function registerPodTools(server, rt) {
1067
1821
  }
1068
1822
  }
1069
1823
  }
1070
- const body = backend.mapCreate(params);
1824
+ let body = backend.mapCreate(params);
1825
+ if (params.templateId) {
1826
+ let template;
1827
+ try {
1828
+ template = await callRestUrl(
1829
+ `${restV2Base(env)}/templates/${params.templateId}`
1830
+ );
1831
+ } catch (error) {
1832
+ if (error instanceof HttpError) {
1833
+ return jsonReply2({
1834
+ error: `Failed to load template ${params.templateId}: ${error.message}`,
1835
+ status: error.status
1836
+ });
1837
+ }
1838
+ throw error;
1839
+ }
1840
+ body = { ...podBodyFromTemplate(template), ...body };
1841
+ }
1071
1842
  try {
1072
1843
  const result = await callRestUrl(
1073
1844
  `${backend.base}${backend.list}`,
@@ -1082,11 +1853,8 @@ function registerPodTools(server, rt) {
1082
1853
  }
1083
1854
  return jsonReply2(result);
1084
1855
  } catch (error) {
1085
- if (error instanceof HttpError && error.status === 501) {
1086
- return jsonReply2({
1087
- 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).",
1088
- status: 501
1089
- });
1856
+ if (error instanceof HttpError) {
1857
+ return jsonReply2({ error: error.message, status: error.status });
1090
1858
  }
1091
1859
  throw error;
1092
1860
  }
@@ -1096,14 +1864,14 @@ function registerPodTools(server, rt) {
1096
1864
  "update-pod",
1097
1865
  "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
1098
1866
  {
1099
- podId: z3.string().describe("ID of the pod to update"),
1100
- name: z3.string().optional().describe("New name for the pod"),
1101
- imageName: z3.string().optional().describe("New Docker image"),
1102
- containerDiskInGb: z3.number().optional().describe("New container disk size in GB"),
1103
- volumeInGb: z3.number().optional().describe("New volume size in GB"),
1104
- volumeMountPath: z3.string().optional().describe("New path to mount the volume"),
1105
- ports: z3.array(z3.string()).optional().describe("New ports to expose"),
1106
- env: z3.record(z3.string()).optional().describe("New environment variables")
1867
+ podId: z7.string().describe("ID of the pod to update"),
1868
+ name: z7.string().optional().describe("New name for the pod"),
1869
+ imageName: z7.string().optional().describe("New Docker image"),
1870
+ containerDiskInGb: z7.number().optional().describe("New container disk size in GB"),
1871
+ volumeInGb: z7.number().optional().describe("New volume size in GB"),
1872
+ volumeMountPath: z7.string().optional().describe("New path to mount the volume"),
1873
+ ports: z7.array(z7.string()).optional().describe("New ports to expose"),
1874
+ env: z7.record(z7.string()).optional().describe("New environment variables")
1107
1875
  },
1108
1876
  { title: "Update pod", ...WRITE, idempotentHint: true },
1109
1877
  async (params) => {
@@ -1122,7 +1890,7 @@ function registerPodTools(server, rt) {
1122
1890
  "start-pod",
1123
1891
  "Start a stopped pod (resumes the same pod and its billing).",
1124
1892
  {
1125
- podId: z3.string().describe("ID of the pod to start")
1893
+ podId: z7.string().describe("ID of the pod to start")
1126
1894
  },
1127
1895
  { title: "Start pod", ...WRITE, idempotentHint: true },
1128
1896
  async (params) => {
@@ -1134,7 +1902,7 @@ function registerPodTools(server, rt) {
1134
1902
  "stop-pod",
1135
1903
  "Stop a running pod. The pod and its disk persist; GPU billing stops.",
1136
1904
  {
1137
- podId: z3.string().describe("ID of the pod to stop")
1905
+ podId: z7.string().describe("ID of the pod to stop")
1138
1906
  },
1139
1907
  { title: "Stop pod", ...WRITE, idempotentHint: true },
1140
1908
  async (params) => {
@@ -1146,7 +1914,7 @@ function registerPodTools(server, rt) {
1146
1914
  "restart-pod",
1147
1915
  "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1148
1916
  {
1149
- podId: z3.string().describe("ID of the pod to restart")
1917
+ podId: z7.string().describe("ID of the pod to restart")
1150
1918
  },
1151
1919
  { title: "Restart pod", ...WRITE },
1152
1920
  async (params) => {
@@ -1161,11 +1929,29 @@ function registerPodTools(server, rt) {
1161
1929
  return jsonReply2(result);
1162
1930
  }
1163
1931
  );
1932
+ server.tool(
1933
+ "stream-pod-logs",
1934
+ "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).",
1935
+ {
1936
+ podId: z7.string().describe("ID of the pod whose logs to stream"),
1937
+ ...logStreamParams
1938
+ },
1939
+ { title: "Stream pod logs", ...READ_ONLY },
1940
+ (params) => streamLogsReply(
1941
+ rt,
1942
+ {
1943
+ name: "stream-pod-logs",
1944
+ resource: "pods",
1945
+ logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
1946
+ },
1947
+ params
1948
+ )
1949
+ );
1164
1950
  server.tool(
1165
1951
  "delete-pod",
1166
1952
  "Permanently delete a pod and its data. This cannot be undone.",
1167
1953
  {
1168
- podId: z3.string().describe("ID of the pod to delete")
1954
+ podId: z7.string().describe("ID of the pod to delete")
1169
1955
  },
1170
1956
  { title: "Delete pod", ...DESTRUCTIVE },
1171
1957
  async (params) => {
@@ -1180,7 +1966,7 @@ function registerPodTools(server, rt) {
1180
1966
  }
1181
1967
 
1182
1968
  // src/tools/endpoints.ts
1183
- import { z as z4 } from "zod";
1969
+ import { z as z8 } from "zod";
1184
1970
  function registerEndpointTools(server, rt) {
1185
1971
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1186
1972
  server.tool(
@@ -1188,8 +1974,8 @@ function registerEndpointTools(server, rt) {
1188
1974
  "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
1189
1975
  {
1190
1976
  ...listPaginationParams,
1191
- includeTemplate: z4.boolean().optional().describe("Include template information (v1 only)"),
1192
- includeWorkers: z4.boolean().optional().describe("Include information about workers (v1 only)")
1977
+ includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
1978
+ includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
1193
1979
  },
1194
1980
  { title: "List endpoints", ...READ_ONLY },
1195
1981
  async (params) => {
@@ -1222,9 +2008,9 @@ function registerEndpointTools(server, rt) {
1222
2008
  "get-endpoint",
1223
2009
  "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
1224
2010
  {
1225
- endpointId: z4.string().describe("ID of the endpoint to retrieve"),
1226
- includeTemplate: z4.boolean().optional().describe("Include template information (v1 only)"),
1227
- includeWorkers: z4.boolean().optional().describe("Include information about workers (v1 only)")
2011
+ endpointId: z8.string().describe("ID of the endpoint to retrieve"),
2012
+ includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
2013
+ includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
1228
2014
  },
1229
2015
  { title: "Get endpoint", ...READ_ONLY },
1230
2016
  async (params) => {
@@ -1254,32 +2040,32 @@ function registerEndpointTools(server, rt) {
1254
2040
  "create-endpoint",
1255
2041
  "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).",
1256
2042
  {
1257
- name: z4.string().optional().describe("Name for the endpoint"),
2043
+ name: z8.string().optional().describe("Name for the endpoint"),
1258
2044
  // --- v2 inline-config fields ---
1259
- imageName: z4.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
1260
- gpuPoolIds: z4.array(z4.string()).optional().describe(
2045
+ imageName: z8.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
2046
+ gpuPoolIds: z8.array(z8.string()).optional().describe(
1261
2047
  'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
1262
2048
  ),
1263
- gpuCount: z4.number().optional().describe("GPUs per worker (v2)"),
1264
- args: z4.string().optional().describe("Container start command/args (v2)"),
1265
- containerDiskInGb: z4.number().optional().describe("Container disk size in GB (v2)"),
1266
- ports: z4.array(z4.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
1267
- env: z4.record(z4.string()).optional().describe("Environment variables (v2)"),
1268
- containerRegistryAuthId: z4.string().optional().describe("Container registry auth id for a private image (v2)"),
1269
- networkVolumeIds: z4.array(z4.string()).optional().describe("Network volume ids to attach (v2)"),
1270
- executionTimeoutMs: z4.number().optional().describe("Per-job execution timeout in ms (v2)"),
1271
- flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
2049
+ gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
2050
+ args: z8.string().optional().describe("Container start command/args (v2)"),
2051
+ containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
2052
+ ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
2053
+ env: z8.record(z8.string()).optional().describe("Environment variables (v2)"),
2054
+ containerRegistryAuthId: z8.string().optional().describe("Container registry auth id for a private image (v2)"),
2055
+ networkVolumeIds: z8.array(z8.string()).optional().describe("Network volume ids to attach (v2)"),
2056
+ executionTimeoutMs: z8.number().optional().describe("Per-job execution timeout in ms (v2)"),
2057
+ flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
1272
2058
  // --- v1 template-based fields ---
1273
- templateId: z4.string().optional().describe("Template ID (v1). Required on v1."),
1274
- computeType: z4.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
1275
- gpuTypeIds: z4.array(z4.string()).optional().describe("List of acceptable GPU types (v1)"),
2059
+ templateId: z8.string().optional().describe("Template ID (v1). Required on v1."),
2060
+ computeType: z8.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
2061
+ gpuTypeIds: z8.array(z8.string()).optional().describe("List of acceptable GPU types (v1)"),
1276
2062
  // --- shared autoscaling fields ---
1277
- workersMin: z4.number().optional().describe("Minimum number of workers"),
1278
- workersMax: z4.number().optional().describe("Maximum number of workers"),
1279
- scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
1280
- scalerValue: z4.number().optional().describe("Autoscaler target value"),
1281
- idleTimeout: z4.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
1282
- dataCenterIds: z4.array(z4.string()).optional().describe("List of preferred data centers")
2063
+ workersMin: z8.number().optional().describe("Minimum number of workers"),
2064
+ workersMax: z8.number().optional().describe("Maximum number of workers"),
2065
+ scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
2066
+ scalerValue: z8.number().optional().describe("Autoscaler target value"),
2067
+ idleTimeout: z8.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
2068
+ dataCenterIds: z8.array(z8.string()).optional().describe("List of preferred data centers")
1283
2069
  },
1284
2070
  { title: "Create endpoint", ...WRITE },
1285
2071
  async (params) => {
@@ -1342,25 +2128,25 @@ function registerEndpointTools(server, rt) {
1342
2128
  "update-endpoint",
1343
2129
  "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.",
1344
2130
  {
1345
- endpointId: z4.string().describe("ID of the endpoint to update"),
1346
- name: z4.string().optional().describe("New name for the endpoint"),
1347
- workersMin: z4.number().optional().describe("New minimum number of workers"),
1348
- workersMax: z4.number().optional().describe("New maximum number of workers"),
1349
- idleTimeout: z4.number().optional().describe("New idle timeout in seconds"),
1350
- scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1351
- scalerValue: z4.number().optional().describe("Scaler value"),
2131
+ endpointId: z8.string().describe("ID of the endpoint to update"),
2132
+ name: z8.string().optional().describe("New name for the endpoint"),
2133
+ workersMin: z8.number().optional().describe("New minimum number of workers"),
2134
+ workersMax: z8.number().optional().describe("New maximum number of workers"),
2135
+ idleTimeout: z8.number().optional().describe("New idle timeout in seconds"),
2136
+ scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2137
+ scalerValue: z8.number().optional().describe("Scaler value"),
1352
2138
  // --- v2-only inline-config fields ---
1353
- imageName: z4.string().optional().describe("New Docker image (v2)"),
1354
- gpuPoolIds: z4.array(z4.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
1355
- gpuCount: z4.number().optional().describe("New GPUs per worker (v2)"),
1356
- args: z4.string().optional().describe("New container args (v2)"),
1357
- containerDiskInGb: z4.number().optional().describe("New container disk size in GB (v2)"),
1358
- ports: z4.array(z4.string()).optional().describe("New ports (v2)"),
1359
- env: z4.record(z4.string()).optional().describe("New environment variables (v2)"),
1360
- containerRegistryAuthId: z4.string().optional().describe("New container registry auth id (v2)"),
1361
- networkVolumeIds: z4.array(z4.string()).optional().describe("New network volume ids (v2)"),
1362
- executionTimeoutMs: z4.number().optional().describe("New per-job execution timeout in ms (v2)"),
1363
- flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
2139
+ imageName: z8.string().optional().describe("New Docker image (v2)"),
2140
+ gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
2141
+ gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
2142
+ args: z8.string().optional().describe("New container args (v2)"),
2143
+ containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
2144
+ ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
2145
+ env: z8.record(z8.string()).optional().describe("New environment variables (v2)"),
2146
+ containerRegistryAuthId: z8.string().optional().describe("New container registry auth id (v2)"),
2147
+ networkVolumeIds: z8.array(z8.string()).optional().describe("New network volume ids (v2)"),
2148
+ executionTimeoutMs: z8.number().optional().describe("New per-job execution timeout in ms (v2)"),
2149
+ flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
1364
2150
  },
1365
2151
  { title: "Update endpoint", ...WRITE, idempotentHint: true },
1366
2152
  async (params) => {
@@ -1379,7 +2165,7 @@ function registerEndpointTools(server, rt) {
1379
2165
  "delete-endpoint",
1380
2166
  "Permanently delete a Serverless endpoint. This cannot be undone.",
1381
2167
  {
1382
- endpointId: z4.string().describe("ID of the endpoint to delete")
2168
+ endpointId: z8.string().describe("ID of the endpoint to delete")
1383
2169
  },
1384
2170
  { title: "Delete endpoint", ...DESTRUCTIVE },
1385
2171
  async (params) => {
@@ -1391,12 +2177,39 @@ function registerEndpointTools(server, rt) {
1391
2177
  return jsonReply2(result);
1392
2178
  }
1393
2179
  );
2180
+ server.tool(
2181
+ "list-endpoint-releases",
2182
+ "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.",
2183
+ {
2184
+ ...listPaginationParams,
2185
+ endpointId: z8.string().describe("ID of the Serverless endpoint whose releases to list")
2186
+ },
2187
+ { title: "List endpoint releases", ...READ_ONLY },
2188
+ async (params) => {
2189
+ const backend = backendFor("endpoints");
2190
+ if (backend.version === "v1") {
2191
+ return jsonReply2({
2192
+ error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2193
+ status: 501
2194
+ });
2195
+ }
2196
+ const raw = await callRestUrl(
2197
+ `${backend.base}/serverless/${params.endpointId}/releases`
2198
+ );
2199
+ const releases = Array.isArray(raw?.releases) ? raw.releases : [];
2200
+ return capListResult(
2201
+ releases,
2202
+ { limit: params.limit, cursor: params.cursor },
2203
+ { rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
2204
+ );
2205
+ }
2206
+ );
1394
2207
  server.tool(
1395
2208
  "list-endpoint-workers",
1396
2209
  "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.",
1397
2210
  {
1398
2211
  ...listPaginationParams,
1399
- endpointId: z4.string().describe("ID of the Serverless endpoint whose workers to list")
2212
+ endpointId: z8.string().describe("ID of the Serverless endpoint whose workers to list")
1400
2213
  },
1401
2214
  { title: "List endpoint workers", ...READ_ONLY },
1402
2215
  async (params) => {
@@ -1417,30 +2230,51 @@ function registerEndpointTools(server, rt) {
1417
2230
  );
1418
2231
  }
1419
2232
  );
2233
+ server.tool(
2234
+ "stream-worker-logs",
2235
+ "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).",
2236
+ {
2237
+ endpointId: z8.string().describe("ID of the Serverless endpoint the worker belongs to"),
2238
+ workerId: z8.string().describe(
2239
+ "ID of the worker whose logs to stream (from list-endpoint-workers)"
2240
+ ),
2241
+ ...logStreamParams
2242
+ },
2243
+ { title: "Stream worker logs", ...READ_ONLY },
2244
+ (params) => streamLogsReply(
2245
+ rt,
2246
+ {
2247
+ name: "stream-worker-logs",
2248
+ resource: "workers",
2249
+ logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
2250
+ },
2251
+ params
2252
+ )
2253
+ );
1420
2254
  }
1421
2255
 
1422
2256
  // src/tools/jobs.ts
1423
- import { z as z5 } from "zod";
2257
+ import { z as z9 } from "zod";
1424
2258
  function registerJobTools(server, rt) {
1425
2259
  const { jsonReply: jsonReply2, serverlessRequest } = rt;
1426
- const endpointIdSchema = z5.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1427
- const jobIdSchema = z5.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1428
- const inputSchema = z5.record(z5.unknown()).describe(
2260
+ const endpointIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2261
+ const jobIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2262
+ const inputSchema = z9.record(z9.unknown()).describe(
1429
2263
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
1430
2264
  );
1431
- const webhookSchema = z5.string().url().optional().describe(
2265
+ const webhookSchema = z9.string().url().optional().describe(
1432
2266
  "Webhook URL to receive job completion notifications instead of polling"
1433
2267
  );
1434
- const policySchema = z5.object({
1435
- executionTimeout: z5.number().optional().describe("Maximum execution time in milliseconds"),
1436
- lowPriority: z5.boolean().optional().describe("Submit as a low-priority job"),
1437
- ttl: z5.number().optional().describe("Time-to-live for the job result in milliseconds")
2268
+ const policySchema = z9.object({
2269
+ executionTimeout: z9.number().optional().describe("Maximum execution time in milliseconds"),
2270
+ lowPriority: z9.boolean().optional().describe("Submit as a low-priority job"),
2271
+ ttl: z9.number().optional().describe("Time-to-live for the job result in milliseconds")
1438
2272
  }).optional().describe("Execution policy options");
1439
- const s3ConfigSchema = z5.object({
1440
- accessId: z5.string().describe("S3 access key ID"),
1441
- accessSecret: z5.string().describe("S3 secret access key"),
1442
- bucketName: z5.string().describe("S3 bucket name"),
1443
- endpointUrl: z5.string().describe("S3 endpoint URL")
2273
+ const s3ConfigSchema = z9.object({
2274
+ accessId: z9.string().describe("S3 access key ID"),
2275
+ accessSecret: z9.string().describe("S3 secret access key"),
2276
+ bucketName: z9.string().describe("S3 bucket name"),
2277
+ endpointUrl: z9.string().describe("S3 endpoint URL")
1444
2278
  }).optional().describe("S3-compatible storage config for large outputs");
1445
2279
  server.tool(
1446
2280
  "run-endpoint",
@@ -1474,7 +2308,7 @@ function registerJobTools(server, rt) {
1474
2308
  "ID of the Serverless endpoint to run synchronously"
1475
2309
  ),
1476
2310
  input: inputSchema,
1477
- wait: z5.number().min(1e3).max(3e5).optional().describe(
2311
+ wait: z9.number().min(1e3).max(3e5).optional().describe(
1478
2312
  "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)."
1479
2313
  ),
1480
2314
  webhook: webhookSchema,
@@ -1643,7 +2477,7 @@ function registerJobTools(server, rt) {
1643
2477
  }
1644
2478
 
1645
2479
  // src/tools/templates.ts
1646
- import { z as z6 } from "zod";
2480
+ import { z as z10 } from "zod";
1647
2481
  function registerTemplateTools(server, rt) {
1648
2482
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1649
2483
  server.tool(
@@ -1651,9 +2485,9 @@ function registerTemplateTools(server, rt) {
1651
2485
  `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.`,
1652
2486
  {
1653
2487
  ...listPaginationParams,
1654
- includeRunpodTemplates: z6.boolean().optional().describe("Include official Runpod templates in the response"),
1655
- includePublicTemplates: z6.boolean().optional().describe("Include community-made public templates in the response"),
1656
- includeEndpointBoundTemplates: z6.boolean().optional().describe(
2488
+ includeRunpodTemplates: z10.boolean().optional().describe("Include official Runpod templates in the response"),
2489
+ includePublicTemplates: z10.boolean().optional().describe("Include community-made public templates in the response"),
2490
+ includeEndpointBoundTemplates: z10.boolean().optional().describe(
1657
2491
  "Include templates bound to Serverless endpoints in the response"
1658
2492
  )
1659
2493
  },
@@ -1684,7 +2518,7 @@ function registerTemplateTools(server, rt) {
1684
2518
  "get-template",
1685
2519
  "Get one template by id (image, env, ports, disk/volume config).",
1686
2520
  {
1687
- templateId: z6.string().describe("ID of the template to retrieve")
2521
+ templateId: z10.string().describe("ID of the template to retrieve")
1688
2522
  },
1689
2523
  { title: "Get template", ...READ_ONLY },
1690
2524
  async (params) => {
@@ -1697,23 +2531,27 @@ function registerTemplateTools(server, rt) {
1697
2531
  );
1698
2532
  server.tool(
1699
2533
  "create-template",
1700
- "Create a reusable pod/endpoint template (image, ports, env, disk/volume). On v2 a category is required and defaults to NVIDIA.",
2534
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
1701
2535
  {
1702
- name: z6.string().describe("Name for the template"),
1703
- imageName: z6.string().describe("Docker image to use"),
1704
- isServerless: z6.boolean().optional().describe("Is this a serverless template"),
1705
- ports: z6.array(z6.string()).optional().describe("Ports to expose"),
1706
- dockerEntrypoint: z6.array(z6.string()).optional().describe("Docker entrypoint commands"),
1707
- dockerStartCmd: z6.array(z6.string()).optional().describe("Docker start commands"),
1708
- env: z6.record(z6.string()).optional().describe("Environment variables"),
1709
- containerDiskInGb: z6.number().optional().describe("Container disk size in GB"),
1710
- volumeInGb: z6.number().optional().describe("Volume size in GB"),
1711
- volumeMountPath: z6.string().optional().describe("Path to mount the volume"),
1712
- readme: z6.string().optional().describe("README content in markdown format"),
1713
- category: z6.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
1714
- "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."
2536
+ name: z10.string().describe("Name for the template"),
2537
+ imageName: z10.string().describe("Docker image to use"),
2538
+ isServerless: z10.boolean().optional().describe("Is this a serverless template"),
2539
+ ports: z10.array(z10.string()).optional().describe("Ports to expose"),
2540
+ dockerEntrypoint: z10.array(z10.string()).optional().describe(
2541
+ "Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
1715
2542
  ),
1716
- containerRegistryAuthId: z6.string().optional().describe(
2543
+ dockerStartCmd: z10.array(z10.string()).optional().describe(
2544
+ '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 "").'
2545
+ ),
2546
+ env: z10.record(z10.string()).optional().describe("Environment variables"),
2547
+ containerDiskInGb: z10.number().optional().describe("Container disk size in GB"),
2548
+ volumeInGb: z10.number().optional().describe("Volume size in GB"),
2549
+ volumeMountPath: z10.string().optional().describe("Path to mount the volume"),
2550
+ readme: z10.string().optional().describe("README content in markdown format"),
2551
+ category: z10.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2552
+ "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."
2553
+ ),
2554
+ containerRegistryAuthId: z10.string().optional().describe(
1717
2555
  "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."
1718
2556
  )
1719
2557
  },
@@ -1731,15 +2569,18 @@ function registerTemplateTools(server, rt) {
1731
2569
  );
1732
2570
  server.tool(
1733
2571
  "update-template",
1734
- "Update a template's mutable fields (name, image, ports, env, readme). Only provided fields change.",
2572
+ "Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
1735
2573
  {
1736
- templateId: z6.string().describe("ID of the template to update"),
1737
- name: z6.string().optional().describe("New name for the template"),
1738
- imageName: z6.string().optional().describe("New Docker image"),
1739
- ports: z6.array(z6.string()).optional().describe("New ports to expose"),
1740
- env: z6.record(z6.string()).optional().describe("New environment variables"),
1741
- readme: z6.string().optional().describe("New README content in markdown format"),
1742
- containerRegistryAuthId: z6.string().optional().describe(
2574
+ templateId: z10.string().describe("ID of the template to update"),
2575
+ name: z10.string().optional().describe("New name for the template"),
2576
+ imageName: z10.string().optional().describe("New Docker image"),
2577
+ dockerStartCmd: z10.array(z10.string()).optional().describe(
2578
+ "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."
2579
+ ),
2580
+ ports: z10.array(z10.string()).optional().describe("New ports to expose"),
2581
+ env: z10.record(z10.string()).optional().describe("New environment variables"),
2582
+ readme: z10.string().optional().describe("New README content in markdown format"),
2583
+ containerRegistryAuthId: z10.string().optional().describe(
1743
2584
  "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."
1744
2585
  )
1745
2586
  },
@@ -1760,7 +2601,7 @@ function registerTemplateTools(server, rt) {
1760
2601
  "delete-template",
1761
2602
  "Permanently delete a template. This cannot be undone.",
1762
2603
  {
1763
- templateId: z6.string().describe("ID of the template to delete")
2604
+ templateId: z10.string().describe("ID of the template to delete")
1764
2605
  },
1765
2606
  { title: "Delete template", ...DESTRUCTIVE },
1766
2607
  async (params) => {
@@ -1775,7 +2616,7 @@ function registerTemplateTools(server, rt) {
1775
2616
  }
1776
2617
 
1777
2618
  // src/tools/network-volumes.ts
1778
- import { z as z7 } from "zod";
2619
+ import { z as z11 } from "zod";
1779
2620
  function registerNetworkVolumeTools(server, rt) {
1780
2621
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1781
2622
  server.tool(
@@ -1796,7 +2637,7 @@ function registerNetworkVolumeTools(server, rt) {
1796
2637
  "get-network-volume",
1797
2638
  "Get one network volume by id (name, size, data center).",
1798
2639
  {
1799
- networkVolumeId: z7.string().describe("ID of the network volume to retrieve")
2640
+ networkVolumeId: z11.string().describe("ID of the network volume to retrieve")
1800
2641
  },
1801
2642
  { title: "Get network volume", ...READ_ONLY },
1802
2643
  async (params) => {
@@ -1809,11 +2650,17 @@ function registerNetworkVolumeTools(server, rt) {
1809
2650
  );
1810
2651
  server.tool(
1811
2652
  "create-network-volume",
1812
- "Create a persistent network volume in a data center (size 1\u20134000 GB) that pods can mount.",
2653
+ "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.",
1813
2654
  {
1814
- name: z7.string().describe("Name for the network volume"),
1815
- size: z7.number().describe("Size in GB (1-4000)"),
1816
- dataCenterId: z7.string().describe("Data center ID (see list-data-centers)")
2655
+ name: z11.string().describe("Name for the network volume"),
2656
+ // Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
2657
+ // here as well as described, so an out-of-range value fails locally
2658
+ // instead of costing an API roundtrip.
2659
+ size: z11.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
2660
+ dataCenterId: z11.string().describe("Data center ID (see list-data-centers)"),
2661
+ volumeType: z11.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
2662
+ "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."
2663
+ )
1817
2664
  },
1818
2665
  { title: "Create network volume", ...WRITE },
1819
2666
  async (params) => {
@@ -1831,9 +2678,11 @@ function registerNetworkVolumeTools(server, rt) {
1831
2678
  "update-network-volume",
1832
2679
  "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1833
2680
  {
1834
- networkVolumeId: z7.string().describe("ID of the network volume to update"),
1835
- name: z7.string().optional().describe("New name for the network volume"),
1836
- size: z7.number().optional().describe("New size in GB (must be larger than current)")
2681
+ networkVolumeId: z11.string().describe("ID of the network volume to update"),
2682
+ name: z11.string().optional().describe("New name for the network volume"),
2683
+ // Same absolute bounds as create. "Larger than current" stays server-side —
2684
+ // we do not hold the current size here, so only the range is checked.
2685
+ size: z11.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
1837
2686
  },
1838
2687
  { title: "Update network volume", ...WRITE, idempotentHint: true },
1839
2688
  async (params) => {
@@ -1852,7 +2701,7 @@ function registerNetworkVolumeTools(server, rt) {
1852
2701
  "delete-network-volume",
1853
2702
  "Permanently delete a network volume and its data. This cannot be undone.",
1854
2703
  {
1855
- networkVolumeId: z7.string().describe("ID of the network volume to delete")
2704
+ networkVolumeId: z11.string().describe("ID of the network volume to delete")
1856
2705
  },
1857
2706
  { title: "Delete network volume", ...DESTRUCTIVE },
1858
2707
  async (params) => {
@@ -1867,9 +2716,14 @@ function registerNetworkVolumeTools(server, rt) {
1867
2716
  }
1868
2717
 
1869
2718
  // src/tools/registries.ts
1870
- import { z as z8 } from "zod";
2719
+ import { z as z12 } from "zod";
2720
+ var DELEGATIONS = "/delegations";
1871
2721
  function registerRegistryTools(server, rt) {
1872
2722
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2723
+ const delegationsV2OnlyNotice = (tool) => ({
2724
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
2725
+ status: 501
2726
+ });
1873
2727
  server.tool(
1874
2728
  "list-container-registry-auths",
1875
2729
  "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
@@ -1888,7 +2742,7 @@ function registerRegistryTools(server, rt) {
1888
2742
  "get-container-registry-auth",
1889
2743
  "Get one saved container-registry credential by id (the secret/password is not returned).",
1890
2744
  {
1891
- containerRegistryAuthId: z8.string().describe("ID of the container registry auth to retrieve")
2745
+ containerRegistryAuthId: z12.string().describe("ID of the container registry auth to retrieve")
1892
2746
  },
1893
2747
  { title: "Get container registry auth", ...READ_ONLY },
1894
2748
  async (params) => {
@@ -1903,9 +2757,9 @@ function registerRegistryTools(server, rt) {
1903
2757
  "create-container-registry-auth",
1904
2758
  "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1905
2759
  {
1906
- name: z8.string().describe("Name for the container registry auth"),
1907
- username: z8.string().describe("Registry username"),
1908
- password: z8.string().describe("Registry password or access token (stored as a secret)")
2760
+ name: z12.string().describe("Name for the container registry auth"),
2761
+ username: z12.string().describe("Registry username"),
2762
+ password: z12.string().describe("Registry password or access token (stored as a secret)")
1909
2763
  },
1910
2764
  { title: "Create container registry auth", ...WRITE },
1911
2765
  async (params) => {
@@ -1923,7 +2777,7 @@ function registerRegistryTools(server, rt) {
1923
2777
  "delete-container-registry-auth",
1924
2778
  "Permanently delete a saved container-registry credential. This cannot be undone.",
1925
2779
  {
1926
- containerRegistryAuthId: z8.string().describe("ID of the container registry auth to delete")
2780
+ containerRegistryAuthId: z12.string().describe("ID of the container registry auth to delete")
1927
2781
  },
1928
2782
  { title: "Delete container registry auth", ...DESTRUCTIVE },
1929
2783
  async (params) => {
@@ -1935,166 +2789,69 @@ function registerRegistryTools(server, rt) {
1935
2789
  return jsonReply2(result);
1936
2790
  }
1937
2791
  );
1938
- }
1939
-
1940
- // src/tools/tags.ts
1941
- import { z as z9 } from "zod";
1942
- var TAG_RESOURCE_TYPES = [
1943
- "POD",
1944
- "NETWORK_VOLUME",
1945
- "CLUSTER",
1946
- "SERVERLESS_ENDPOINT"
1947
- ];
1948
- var taggedResourceSchema = z9.object({
1949
- id: z9.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
1950
- type: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
1951
- });
1952
- function v2OnlyNotice(tool) {
1953
- return {
1954
- error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
1955
- status: 501
1956
- };
1957
- }
1958
- function registerTagTools(server, rt) {
1959
- const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1960
2792
  server.tool(
1961
- "list-tags",
1962
- "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.",
1963
- {
1964
- ...listPaginationParams,
1965
- includeResources: z9.boolean().optional().describe("Include the resources attached to each tag in the response")
1966
- },
1967
- { title: "List tags", ...READ_ONLY },
2793
+ "list-registry-delegations",
2794
+ "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.",
2795
+ listPaginationParams,
2796
+ { title: "List registry delegations", ...READ_ONLY },
1968
2797
  async (params) => {
1969
- const backend = backendFor("tags");
1970
- if (backend.version === "v1") return jsonReply2(v2OnlyNotice("list-tags"));
1971
- const query = params.includeResources ? "?include=resources" : "";
1972
- const raw = await callRestUrl(`${backend.base}${backend.list}${query}`);
1973
- return capListResult(backend.unwrap(raw), {
2798
+ const backend = backendFor("registries");
2799
+ if (backend.version === "v1") {
2800
+ return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
2801
+ }
2802
+ const raw = await callRestUrl(
2803
+ `${backend.base}${backend.list}${DELEGATIONS}`
2804
+ );
2805
+ const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
2806
+ return capListResult(delegations, {
1974
2807
  limit: params.limit,
1975
2808
  cursor: params.cursor
1976
2809
  });
1977
2810
  }
1978
2811
  );
1979
2812
  server.tool(
1980
- "get-tag",
1981
- "Get one tag by id, optionally including its attached resources. v2-only \u2014 returns a 501 notice on the v1 API.",
2813
+ "create-registry-delegation",
2814
+ "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.",
1982
2815
  {
1983
- tagId: z9.string().describe("ID of the tag to retrieve"),
1984
- includeResources: z9.boolean().optional().describe("Include the resources attached to this tag")
1985
- },
1986
- { title: "Get tag", ...READ_ONLY },
1987
- async (params) => {
1988
- const backend = backendFor("tags");
1989
- if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
1990
- const query = params.includeResources ? "?include=resources" : "";
1991
- const result = await callRestUrl(
1992
- `${backend.base}${backend.get(params.tagId)}${query}`
1993
- );
1994
- return jsonReply2(result);
1995
- }
1996
- );
1997
- server.tool(
1998
- "create-tag",
1999
- "Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
2000
- {
2001
- key: z9.string().min(1).describe('Tag key (e.g. "project")'),
2002
- value: z9.string().min(1).describe('Tag value (e.g. "gpt-training")'),
2003
- resources: z9.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
2816
+ resource: z12.string().min(1).describe(
2817
+ 'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
2818
+ ),
2819
+ name: z12.string().optional().describe("Optional human-readable name for the delegation")
2004
2820
  },
2005
- { title: "Create tag", ...WRITE },
2821
+ { title: "Create registry delegation", ...WRITE },
2006
2822
  async (params) => {
2007
- const backend = backendFor("tags");
2008
- if (backend.version === "v1")
2009
- return jsonReply2(v2OnlyNotice("create-tag"));
2823
+ const backend = backendFor("registries");
2824
+ if (backend.version === "v1") {
2825
+ return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
2826
+ }
2827
+ const body = { resource: params.resource };
2828
+ if (params.name !== void 0) body.name = params.name;
2010
2829
  const result = await callRestUrl(
2011
- `${backend.base}${backend.list}`,
2830
+ `${backend.base}${backend.list}${DELEGATIONS}`,
2012
2831
  "POST",
2013
- params
2832
+ body
2014
2833
  );
2015
2834
  return jsonReply2(result);
2016
2835
  }
2017
2836
  );
2018
2837
  server.tool(
2019
- "update-tag",
2020
- "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.",
2838
+ "delete-registry-delegation",
2839
+ "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.",
2021
2840
  {
2022
- tagId: z9.string().describe("ID of the tag to update"),
2023
- key: z9.string().min(1).optional().describe("New tag key"),
2024
- value: z9.string().min(1).optional().describe("New tag value"),
2025
- resources: z9.array(taggedResourceSchema).optional().describe(
2026
- "Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
2841
+ delegationId: z12.string().describe(
2842
+ "ID of the delegation to revoke (from list-registry-delegations)"
2027
2843
  )
2028
2844
  },
2029
- // No idempotentHint: replacing `resources` is non-atomic upstream, so a
2030
- // retried-after-partial-failure update is not guaranteed to be a no-op.
2031
- { title: "Update tag", ...WRITE },
2032
- async (params) => {
2033
- const { tagId, ...updateParams } = params;
2034
- const backend = backendFor("tags");
2035
- if (backend.version === "v1")
2036
- return jsonReply2(v2OnlyNotice("update-tag"));
2037
- const result = await callRestUrl(
2038
- `${backend.base}${backend.get(tagId)}`,
2039
- "PATCH",
2040
- updateParams
2041
- );
2042
- return jsonReply2(result);
2043
- }
2044
- );
2045
- server.tool(
2046
- "delete-tag",
2047
- "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.",
2048
- {
2049
- tagId: z9.string().describe("ID of the tag to delete")
2050
- },
2051
- { title: "Delete tag", ...DESTRUCTIVE },
2052
- async (params) => {
2053
- const backend = backendFor("tags");
2054
- if (backend.version === "v1")
2055
- return jsonReply2(v2OnlyNotice("delete-tag"));
2056
- const result = await callRestUrl(
2057
- `${backend.base}${backend.get(params.tagId)}`,
2058
- "DELETE"
2059
- );
2060
- return jsonReply2(result);
2061
- }
2062
- );
2063
- server.tool(
2064
- "attach-tag",
2065
- "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.",
2066
- {
2067
- tagId: z9.string().describe("ID of the tag"),
2068
- resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
2069
- resourceId: z9.string().describe("ID of the resource to attach the tag to")
2070
- },
2071
- { title: "Attach tag to resource", ...WRITE, idempotentHint: true },
2072
- async (params) => {
2073
- const backend = backendFor("tags");
2074
- if (backend.version === "v1")
2075
- return jsonReply2(v2OnlyNotice("attach-tag"));
2076
- const result = await callRestUrl(
2077
- `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2078
- "PUT"
2079
- );
2080
- return jsonReply2(result);
2081
- }
2082
- );
2083
- server.tool(
2084
- "detach-tag",
2085
- "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.",
2086
- {
2087
- tagId: z9.string().describe("ID of the tag"),
2088
- resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
2089
- resourceId: z9.string().describe("ID of the resource to detach the tag from")
2090
- },
2091
- { title: "Detach tag from resource", ...WRITE, idempotentHint: true },
2845
+ { title: "Revoke registry delegation", ...DESTRUCTIVE },
2092
2846
  async (params) => {
2093
- const backend = backendFor("tags");
2094
- if (backend.version === "v1")
2095
- return jsonReply2(v2OnlyNotice("detach-tag"));
2847
+ const backend = backendFor("registries");
2848
+ if (backend.version === "v1") {
2849
+ return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
2850
+ }
2096
2851
  const result = await callRestUrl(
2097
- `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2852
+ `${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
2853
+ params.delegationId
2854
+ )}`,
2098
2855
  "DELETE"
2099
2856
  );
2100
2857
  return jsonReply2(result);
@@ -2103,7 +2860,7 @@ function registerTagTools(server, rt) {
2103
2860
  }
2104
2861
 
2105
2862
  // src/tools/billing.ts
2106
- import { z as z10 } from "zod";
2863
+ import { z as z13 } from "zod";
2107
2864
  var BILLING_SCOPE_PATH = {
2108
2865
  all: "/billing",
2109
2866
  pods: "/billing/pods",
@@ -2119,7 +2876,7 @@ function registerBillingTools(server, rt) {
2119
2876
  "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.",
2120
2877
  {
2121
2878
  ...listPaginationParams,
2122
- scope: z10.enum([
2879
+ scope: z13.enum([
2123
2880
  "all",
2124
2881
  "pods",
2125
2882
  "serverless",
@@ -2129,14 +2886,14 @@ function registerBillingTools(server, rt) {
2129
2886
  ]).optional().describe(
2130
2887
  'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2131
2888
  ),
2132
- startTime: z10.string().optional().describe(
2889
+ startTime: z13.string().optional().describe(
2133
2890
  "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2134
2891
  ),
2135
- endTime: z10.string().optional().describe(
2892
+ endTime: z13.string().optional().describe(
2136
2893
  "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2137
2894
  ),
2138
- bucketSize: z10.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2139
- lastN: z10.number().int().positive().optional().describe(
2895
+ bucketSize: z13.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2896
+ lastN: z13.number().int().positive().optional().describe(
2140
2897
  "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2141
2898
  )
2142
2899
  },
@@ -2171,13 +2928,15 @@ function registerBillingTools(server, rt) {
2171
2928
  function registerTools(server, ctx, deps = {}) {
2172
2929
  const rt = createToolRuntime(server, ctx, deps);
2173
2930
  registerCatalogTools(server, rt);
2931
+ registerHubTools(server, rt);
2932
+ registerPublicEndpointTools(server, rt);
2174
2933
  registerPodTools(server, rt);
2175
2934
  registerEndpointTools(server, rt);
2935
+ registerEndpointGpuTools(server, rt);
2176
2936
  registerJobTools(server, rt);
2177
2937
  registerTemplateTools(server, rt);
2178
2938
  registerNetworkVolumeTools(server, rt);
2179
2939
  registerRegistryTools(server, rt);
2180
- registerTagTools(server, rt);
2181
2940
  registerBillingTools(server, rt);
2182
2941
  }
2183
2942