@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/http.cjs CHANGED
@@ -30,6 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/http.ts
31
31
  var http_exports = {};
32
32
  __export(http_exports, {
33
+ bodyIsCheckable: () => bodyIsCheckable,
34
+ buildToolContext: () => buildToolContext,
35
+ defaultCredentialChecker: () => defaultCredentialChecker,
33
36
  handleMcpRequest: () => handleMcpRequest,
34
37
  registerTools: () => registerTools
35
38
  });
@@ -38,81 +41,6 @@ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamable
38
41
 
39
42
  // src/server.ts
40
43
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
41
- var SERVER_NAME = "Runpod API Server";
42
- var SERVER_VERSION = true ? "2.0.0" : "dev";
43
- function createServer(version = SERVER_VERSION) {
44
- return new import_mcp.McpServer({
45
- name: SERVER_NAME,
46
- version,
47
- capabilities: {
48
- resources: {},
49
- tools: {}
50
- }
51
- });
52
- }
53
-
54
- // src/tools/runtime.ts
55
- var import_node_fetch = __toESM(require("node-fetch"), 1);
56
- var import_node_crypto = require("crypto");
57
-
58
- // src/_shared/http.ts
59
- var HttpError = class extends Error {
60
- constructor(prefix, status, body) {
61
- super(`${prefix}: ${status} - ${body}`);
62
- this.name = "HttpError";
63
- this.status = status;
64
- this.body = body;
65
- }
66
- };
67
- function isJsonContentType(contentType) {
68
- if (!contentType) return false;
69
- const ct = contentType.toLowerCase();
70
- return ct.includes("application/json") || ct.includes("+json");
71
- }
72
- function methodSendsBody(method) {
73
- return method === "POST" || method === "PATCH" || method === "PUT";
74
- }
75
- function buildRequestHeaders(apiKey, tracking) {
76
- return {
77
- Authorization: `Bearer ${apiKey}`,
78
- "Content-Type": "application/json",
79
- ...tracking()
80
- };
81
- }
82
- function createHttpClient(deps) {
83
- return async function request(url, method = "GET", body) {
84
- const init = {
85
- method,
86
- headers: buildRequestHeaders(deps.apiKey, deps.tracking)
87
- };
88
- if (body && methodSendsBody(method)) {
89
- init.body = JSON.stringify(body);
90
- }
91
- const response = await deps.fetch(url, init);
92
- if (!response.ok) {
93
- throw new HttpError(
94
- deps.errorPrefix,
95
- response.status,
96
- await response.text()
97
- );
98
- }
99
- return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
100
- };
101
- }
102
-
103
- // src/_shared/tracking.ts
104
- function sanitizeUaToken(value) {
105
- return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
106
- }
107
- function buildTrackingHeaders(input) {
108
- const name = sanitizeUaToken(input.clientName || "unknown");
109
- const version = sanitizeUaToken(input.clientVersion || "unknown");
110
- const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
111
- return {
112
- "User-Agent": userAgent,
113
- "X-Runpod-Session-Id": input.sessionId
114
- };
115
- }
116
44
 
117
45
  // src/_shared/mappers.ts
118
46
  function compact(obj) {
@@ -126,6 +54,9 @@ function persistentMount(size, path) {
126
54
  if (size === void 0 || path === void 0) return void 0;
127
55
  return { persistent: { size, path } };
128
56
  }
57
+ function joinStartCmd(cmd) {
58
+ return cmd && cmd.length ? cmd.join(" ") : void 0;
59
+ }
129
60
  function containerConfigToV2(p) {
130
61
  return compact({
131
62
  image: p.imageName,
@@ -144,6 +75,13 @@ function mapPodCreateToV2(params) {
144
75
  return compact({
145
76
  ...containerConfigToV2(params),
146
77
  name: params.name,
78
+ // Private-image registry credential (v2 ContainerConfig `registry`). When
79
+ // deploying from a template, this spreads over the template's registry, so
80
+ // passing it overrides the inherited credential. An empty string is an
81
+ // explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
82
+ // template's inherited credential. Undefined is dropped by compact, so an
83
+ // omitted param leaves the template's registry in place.
84
+ registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
147
85
  cloud: params.cloudType,
148
86
  // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
149
87
  // centers; omit/empty = scheduler chooses). Pass the v1 list straight
@@ -200,7 +138,8 @@ function mapNetworkVolumeCreateToV2(params) {
200
138
  return compact({
201
139
  name: params.name,
202
140
  size: params.size,
203
- dataCenter: params.dataCenterId
141
+ dataCenter: params.dataCenterId,
142
+ type: params.volumeType
204
143
  });
205
144
  }
206
145
  function mapTemplateCreateToV2(params) {
@@ -209,18 +148,39 @@ function mapTemplateCreateToV2(params) {
209
148
  ...compact({
210
149
  name: params.name,
211
150
  serverless: params.isServerless,
212
- registry: params.containerRegistryAuthId
213
- }),
214
- // Required by v2 — always emit, defaulting to NVIDIA.
215
- category: params.category ?? "NVIDIA"
151
+ registry: params.containerRegistryAuthId,
152
+ args: joinStartCmd(params.dockerStartCmd),
153
+ // No longer required by v2 (it defaults to NVIDIA server-side), so an
154
+ // unset category is dropped by compact instead of being forced here.
155
+ category: params.category
156
+ })
216
157
  };
217
158
  }
159
+ function podBodyFromTemplate(template) {
160
+ const out = {};
161
+ const keys = [
162
+ "name",
163
+ "image",
164
+ "args",
165
+ "disk",
166
+ "ports",
167
+ "env",
168
+ "mounts",
169
+ "registry"
170
+ ];
171
+ for (const key of keys) {
172
+ const value = template[key];
173
+ if (value !== void 0 && value !== null) out[key] = value;
174
+ }
175
+ return out;
176
+ }
218
177
  function mapTemplateUpdateToV2(params) {
219
178
  return {
220
179
  ...containerConfigToV2(params),
221
180
  ...compact({
222
181
  name: params.name,
223
- registry: params.containerRegistryAuthId
182
+ registry: params.containerRegistryAuthId,
183
+ args: joinStartCmd(params.dockerStartCmd)
224
184
  })
225
185
  };
226
186
  }
@@ -237,7 +197,6 @@ var V2_UNWRAP_KEY = {
237
197
  dataCenters: "dataCenters",
238
198
  endpoints: "endpoints",
239
199
  jobs: "jobs",
240
- tags: "tags",
241
200
  workers: "workers",
242
201
  billing: "records"
243
202
  };
@@ -251,7 +210,6 @@ var RESOURCE_ENV_KEY = {
251
210
  dataCenters: "DATA_CENTERS",
252
211
  endpoints: "ENDPOINTS",
253
212
  jobs: "JOBS",
254
- tags: "TAGS",
255
213
  workers: "WORKERS",
256
214
  billing: "BILLING"
257
215
  };
@@ -267,6 +225,9 @@ function serverlessBase(env) {
267
225
  function publicGraphqlBase(env) {
268
226
  return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
269
227
  }
228
+ function authedGraphqlBase(env) {
229
+ return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
230
+ }
270
231
  function unwrapList(resource, version, raw) {
271
232
  if (version === "v1") return Array.isArray(raw) ? raw : [];
272
233
  if (raw === null || typeof raw !== "object") return [];
@@ -285,6 +246,14 @@ function resolveVersion(opts) {
285
246
  }
286
247
  return "v1";
287
248
  }
249
+ function restVersionSummary(env) {
250
+ const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
251
+ const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
252
+ const overrides = Object.keys(env).filter(
253
+ (k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
254
+ ).length;
255
+ return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
256
+ }
288
257
  var identity = (body) => body;
289
258
  var V1_REST_PATHS = {
290
259
  pods: { list: "/pods", get: (id) => `/pods/${id}` },
@@ -305,7 +274,6 @@ var CATALOG = /* @__PURE__ */ new Set([
305
274
  "dataCenters"
306
275
  ]);
307
276
  var V2_ONLY = /* @__PURE__ */ new Set([
308
- "tags",
309
277
  "workers",
310
278
  "billing"
311
279
  ]);
@@ -323,7 +291,6 @@ var V2_REST_PATHS = {
323
291
  list: "/catalog/datacenters",
324
292
  get: (id) => `/catalog/datacenters/${id}`
325
293
  },
326
- tags: { list: "/tags", get: (id) => `/tags/${id}` },
327
294
  // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
328
295
  // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
329
296
  // unwrap key stays correct.
@@ -420,8 +387,186 @@ function resolveBackend(opts) {
420
387
  return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
421
388
  }
422
389
 
390
+ // src/server.ts
391
+ var SERVER_NAME = "Runpod API Server";
392
+ var SERVER_VERSION = true ? "3.0.0" : "dev";
393
+ function createServer(version = SERVER_VERSION) {
394
+ return new import_mcp.McpServer({
395
+ name: SERVER_NAME,
396
+ version: `${version} [${restVersionSummary(process.env)}]`,
397
+ capabilities: {
398
+ resources: {},
399
+ tools: {}
400
+ }
401
+ });
402
+ }
403
+
404
+ // src/tools/runtime.ts
405
+ var import_node_fetch = __toESM(require("node-fetch"), 1);
406
+ var import_node_crypto = require("crypto");
407
+
408
+ // src/_shared/http.ts
409
+ var HttpError = class extends Error {
410
+ constructor(prefix, status, body) {
411
+ 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)" : "";
412
+ super(`${prefix}: ${status} - ${body}${hint}`);
413
+ this.name = "HttpError";
414
+ this.status = status;
415
+ this.body = body;
416
+ }
417
+ };
418
+ function isJsonContentType(contentType) {
419
+ if (!contentType) return false;
420
+ const ct = contentType.toLowerCase();
421
+ return ct.includes("application/json") || ct.includes("+json");
422
+ }
423
+ function methodSendsBody(method) {
424
+ return method === "POST" || method === "PATCH" || method === "PUT";
425
+ }
426
+ function buildRequestHeaders(apiKey, tracking) {
427
+ return {
428
+ Authorization: `Bearer ${apiKey}`,
429
+ "Content-Type": "application/json",
430
+ ...tracking()
431
+ };
432
+ }
433
+ function createHttpClient(deps) {
434
+ return async function request(url, method = "GET", body) {
435
+ const init = {
436
+ method,
437
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
438
+ };
439
+ if (body && methodSendsBody(method)) {
440
+ init.body = JSON.stringify(body);
441
+ }
442
+ const response = await deps.fetch(url, init);
443
+ if (!response.ok) {
444
+ throw new HttpError(
445
+ deps.errorPrefix,
446
+ response.status,
447
+ await response.text()
448
+ );
449
+ }
450
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
451
+ };
452
+ }
453
+
454
+ // src/_shared/tracking.ts
455
+ function sanitizeUaToken(value) {
456
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
457
+ }
458
+ function buildTrackingHeaders(input) {
459
+ const name = sanitizeUaToken(input.clientName || "unknown");
460
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
461
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
462
+ return {
463
+ "User-Agent": userAgent,
464
+ "X-Runpod-Session-Id": input.sessionId
465
+ };
466
+ }
467
+
468
+ // src/tools/logs.ts
469
+ var import_zod = require("zod");
470
+ function parseLogSse(raw) {
471
+ const items = [];
472
+ for (const block of raw.split(/\r?\n\r?\n/)) {
473
+ const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
474
+ if (!dataLines.length) continue;
475
+ const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
476
+ if (!payload.trim()) continue;
477
+ try {
478
+ const parsed = JSON.parse(payload);
479
+ items.push(
480
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
481
+ );
482
+ } catch {
483
+ items.push({ raw: payload });
484
+ }
485
+ }
486
+ return items;
487
+ }
488
+ var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
489
+ var LOG_STREAM_MAX_BYTES = 256 * 1024;
490
+ async function readSseSnapshot(fetchImpl, url, headers, opts) {
491
+ const controller = new AbortController();
492
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
493
+ const chunks = [];
494
+ let bytes = 0;
495
+ let truncated = false;
496
+ try {
497
+ const res = await fetchImpl(url, {
498
+ method: "GET",
499
+ headers,
500
+ signal: controller.signal
501
+ });
502
+ if (!res.ok) {
503
+ throw new HttpError(
504
+ "Runpod API Error",
505
+ res.status,
506
+ await res.text().catch(() => "")
507
+ );
508
+ }
509
+ for await (const chunk of res.body ?? []) {
510
+ chunks.push(chunk);
511
+ bytes += chunk.length;
512
+ if (bytes >= opts.maxBytes) {
513
+ truncated = true;
514
+ controller.abort();
515
+ break;
516
+ }
517
+ }
518
+ } catch (err) {
519
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
520
+ } finally {
521
+ clearTimeout(timer);
522
+ }
523
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
524
+ }
525
+ var logStreamParams = {
526
+ source: import_zod.z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
527
+ tail: import_zod.z.number().int().min(0).max(5e3).optional().describe(
528
+ "Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
529
+ ),
530
+ since: import_zod.z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
531
+ maxWaitMs: import_zod.z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
532
+ };
533
+ async function collectLogSnapshot(streamSse, logsUrl, params) {
534
+ const qs = new URLSearchParams();
535
+ if (params.source && params.source !== "both")
536
+ qs.append("source", params.source);
537
+ if (params.tail !== void 0) qs.append("tail", String(params.tail));
538
+ if (params.since) qs.append("since", params.since);
539
+ const query = qs.toString() ? `?${qs}` : "";
540
+ const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
541
+ maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
542
+ maxBytes: LOG_STREAM_MAX_BYTES
543
+ });
544
+ const items = parseLogSse(raw);
545
+ if (truncated) items.pop();
546
+ return { items, count: items.length, truncated };
547
+ }
548
+ async function streamLogsReply(rt, tool, params) {
549
+ const backend = rt.backendFor(tool.resource);
550
+ if (backend.version === "v1") {
551
+ return rt.jsonReply({
552
+ error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
553
+ status: 501
554
+ });
555
+ }
556
+ try {
557
+ return rt.jsonReply(
558
+ await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
559
+ );
560
+ } catch (error) {
561
+ if (error instanceof HttpError) {
562
+ return rt.jsonReply({ error: error.message, status: error.status });
563
+ }
564
+ throw error;
565
+ }
566
+ }
567
+
423
568
  // src/tools/runtime.ts
424
- var MCP_SERVER_VERSION = true ? "2.0.0" : "dev";
569
+ var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
425
570
  var SESSION_ID = (0, import_node_crypto.randomUUID)();
426
571
  function trackingHeaders(server, ctx) {
427
572
  const info = server.server.getClientVersion();
@@ -458,14 +603,18 @@ var DESTRUCTIVE = {
458
603
  idempotentHint: true,
459
604
  openWorldHint: true
460
605
  };
461
- async function graphqlRequest(query, tracking, fetchImpl, url) {
606
+ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
462
607
  const response = await fetchImpl(url, {
463
608
  method: "POST",
464
609
  headers: {
465
610
  "Content-Type": "application/json",
611
+ ...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
466
612
  ...tracking()
467
613
  },
468
- body: JSON.stringify({ query })
614
+ body: JSON.stringify({
615
+ query,
616
+ ...options?.variables ? { variables: options.variables } : {}
617
+ })
469
618
  });
470
619
  const result = await response.json();
471
620
  if (result.errors && result.errors.length > 0) {
@@ -480,7 +629,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
480
629
  }
481
630
  function createToolRuntime(server, ctx, deps = {}) {
482
631
  const tracking = () => trackingHeaders(server, ctx);
483
- const httpFetch = deps.fetch ?? defaultFetch;
632
+ function observeUnauthorized(fn) {
633
+ const notify = ctx.onUnauthorized;
634
+ if (!notify) return fn;
635
+ return async (...args) => {
636
+ const response = await fn(...args);
637
+ if (response.status === 401) notify();
638
+ return response;
639
+ };
640
+ }
641
+ const rawFetch = deps.fetch ?? defaultFetch;
642
+ const httpFetch = observeUnauthorized(rawFetch);
484
643
  const v1Client = createHttpClient({
485
644
  apiKey: ctx.apiKey,
486
645
  fetch: httpFetch,
@@ -515,47 +674,19 @@ function createToolRuntime(server, ctx, deps = {}) {
515
674
  "Error calling Runpod API:",
516
675
  () => restClient(url, method, body)
517
676
  );
518
- const streamSse = deps.streamSse ?? (async (url, opts) => {
519
- const controller = new AbortController();
520
- const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
521
- try {
522
- const res = await (0, import_node_fetch.default)(url, {
523
- method: "GET",
524
- headers: {
525
- Authorization: `Bearer ${ctx.apiKey}`,
526
- Accept: "text/event-stream",
527
- ...tracking()
528
- },
529
- signal: controller.signal
530
- });
531
- if (!res.ok) {
532
- throw new HttpError(
533
- "Runpod API Error",
534
- res.status,
535
- await res.text().catch(() => "")
536
- );
537
- }
538
- const chunks = [];
539
- let bytes = 0;
540
- let truncated = false;
541
- try {
542
- for await (const chunk of res.body) {
543
- chunks.push(chunk);
544
- bytes += chunk.length;
545
- if (bytes >= opts.maxBytes) {
546
- truncated = true;
547
- controller.abort();
548
- break;
549
- }
550
- }
551
- } catch (err) {
552
- if (!(err instanceof Error && err.name === "AbortError")) throw err;
553
- }
554
- return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
555
- } finally {
556
- clearTimeout(timer);
557
- }
558
- });
677
+ const sseFetch = observeUnauthorized(
678
+ deps.sseFetch ?? import_node_fetch.default
679
+ );
680
+ const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
681
+ sseFetch,
682
+ url,
683
+ {
684
+ Authorization: `Bearer ${ctx.apiKey}`,
685
+ Accept: "text/event-stream",
686
+ ...tracking()
687
+ },
688
+ opts
689
+ ));
559
690
  const backendFor = (resource) => resolveBackend({
560
691
  resource,
561
692
  env: process.env,
@@ -577,9 +708,18 @@ function createToolRuntime(server, ctx, deps = {}) {
577
708
  graphql: (query) => graphqlRequest(
578
709
  query,
579
710
  tracking,
580
- httpFetch,
711
+ // rawFetch, not httpFetch: this call carries no credential (see above).
712
+ rawFetch,
581
713
  publicGraphqlBase(process.env)
582
714
  ),
715
+ graphqlAuthed: (query, variables) => graphqlRequest(
716
+ query,
717
+ tracking,
718
+ httpFetch,
719
+ // NOT publicGraphqlBase: this call carries the caller's API key.
720
+ authedGraphqlBase(process.env),
721
+ { variables, apiKey: ctx.apiKey }
722
+ ),
583
723
  runpodRequest,
584
724
  serverlessRequest,
585
725
  callRestUrl,
@@ -591,17 +731,17 @@ function createToolRuntime(server, ctx, deps = {}) {
591
731
  }
592
732
 
593
733
  // src/tools/catalog.ts
594
- var import_zod2 = require("zod");
734
+ var import_zod3 = require("zod");
595
735
 
596
736
  // src/pagination.ts
597
- var import_zod = require("zod");
737
+ var import_zod2 = require("zod");
598
738
  var DEFAULT_LIST_LIMIT = 20;
599
739
  var MAX_LIST_LIMIT = 100;
600
740
  var listPaginationParams = {
601
- limit: import_zod.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
741
+ limit: import_zod2.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
602
742
  `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
603
743
  ),
604
- cursor: import_zod.z.string().optional().describe(
744
+ cursor: import_zod2.z.string().optional().describe(
605
745
  "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
606
746
  )
607
747
  };
@@ -659,25 +799,32 @@ function registerCatalogTools(server, rt) {
659
799
  const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
660
800
  server.tool(
661
801
  "list-gpu-types",
662
- "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.",
802
+ "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.",
663
803
  {
664
804
  ...listPaginationParams,
665
- minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
666
- secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
667
- communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
668
- searchTerm: import_zod2.z.string().optional().describe(
805
+ minMemoryGb: import_zod3.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
806
+ secureCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
807
+ communityCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
808
+ searchTerm: import_zod3.z.string().optional().describe(
669
809
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
670
810
  ),
671
- includeUnavailable: import_zod2.z.boolean().optional().describe(
672
- "Include GPUs that are currently out of stock. Default is false"
811
+ includeUnavailable: import_zod3.z.boolean().optional().describe(
812
+ "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."
813
+ ),
814
+ includeAvailability: import_zod3.z.boolean().optional().describe(
815
+ "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."
673
816
  )
674
817
  },
675
818
  { title: "List GPU types", ...READ_ONLY },
676
819
  async (params) => {
677
820
  const backend = backendFor("gpus");
678
821
  if (backend.version === "v2") {
679
- const raw = await callRestUrl(`${backend.base}${backend.list}`);
822
+ const wantAvailability = params.includeAvailability !== false;
823
+ const raw = await callRestUrl(
824
+ `${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
825
+ );
680
826
  let gpus = backend.unwrap(raw);
827
+ gpus = gpus.filter((g) => g.id !== "unknown");
681
828
  if (params.minMemoryGb !== void 0)
682
829
  gpus = gpus.filter(
683
830
  (g) => Number(g.memory ?? 0) >= params.minMemoryGb
@@ -690,6 +837,18 @@ function registerCatalogTools(server, rt) {
690
837
  (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
691
838
  );
692
839
  }
840
+ if (params.includeUnavailable === false)
841
+ gpus = gpus.filter((g) => g.availability !== "NONE");
842
+ const rank = {
843
+ HIGH: 3,
844
+ MEDIUM: 2,
845
+ LOW: 1,
846
+ NONE: 0
847
+ };
848
+ gpus.sort(
849
+ (a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
850
+ );
851
+ gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
693
852
  return capListResult(gpus, {
694
853
  limit: params.limit,
695
854
  cursor: params.cursor
@@ -719,7 +878,7 @@ function registerCatalogTools(server, rt) {
719
878
  return !!status && status !== "Out";
720
879
  };
721
880
  let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
722
- if (!params.includeUnavailable) {
881
+ if (params.includeUnavailable === false) {
723
882
  gpuTypes = gpuTypes.filter(isAvailable);
724
883
  }
725
884
  if (params.minMemoryGb) {
@@ -765,7 +924,7 @@ function registerCatalogTools(server, rt) {
765
924
  "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
766
925
  {
767
926
  ...listPaginationParams,
768
- region: import_zod2.z.string().optional().describe(
927
+ region: import_zod3.z.string().optional().describe(
769
928
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
770
929
  )
771
930
  },
@@ -830,9 +989,12 @@ function registerCatalogTools(server, rt) {
830
989
  );
831
990
  server.tool(
832
991
  "get-gpu-type",
833
- "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).",
992
+ "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.",
834
993
  {
835
- gpuTypeId: import_zod2.z.string().describe("ID of the GPU type to retrieve")
994
+ gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
995
+ includeAvailability: import_zod3.z.boolean().optional().describe(
996
+ "Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
997
+ )
836
998
  },
837
999
  { title: "Get GPU type", ...READ_ONLY },
838
1000
  async (params) => {
@@ -843,58 +1005,629 @@ function registerCatalogTools(server, rt) {
843
1005
  status: 501
844
1006
  });
845
1007
  }
846
- const result = await callRestUrl(
847
- `${backend.base}${backend.get(params.gpuTypeId)}`
1008
+ const path = backend.get(encodeURIComponent(params.gpuTypeId));
1009
+ const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
1010
+ const result = await callRestUrl(`${backend.base}${path}${query}`);
1011
+ return jsonReply2(result);
1012
+ }
1013
+ );
1014
+ server.tool(
1015
+ "get-cpu-type",
1016
+ "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).",
1017
+ {
1018
+ cpuTypeId: import_zod3.z.string().describe("ID of the CPU type to retrieve")
1019
+ },
1020
+ { title: "Get CPU type", ...READ_ONLY },
1021
+ async (params) => {
1022
+ const backend = backendFor("cpus");
1023
+ if (backend.version === "v1") {
1024
+ return jsonReply2({
1025
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1026
+ status: 501
1027
+ });
1028
+ }
1029
+ const result = await callRestUrl(
1030
+ `${backend.base}${backend.get(params.cpuTypeId)}`
1031
+ );
1032
+ return jsonReply2(result);
1033
+ }
1034
+ );
1035
+ server.tool(
1036
+ "get-data-center",
1037
+ "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).",
1038
+ {
1039
+ dataCenterId: import_zod3.z.string().describe("ID of the data center to retrieve")
1040
+ },
1041
+ { title: "Get data center", ...READ_ONLY },
1042
+ async (params) => {
1043
+ const backend = backendFor("dataCenters");
1044
+ if (backend.version === "v1") {
1045
+ return jsonReply2({
1046
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1047
+ status: 501
1048
+ });
1049
+ }
1050
+ const result = await callRestUrl(
1051
+ `${backend.base}${backend.get(params.dataCenterId)}`
1052
+ );
1053
+ return jsonReply2(result);
1054
+ }
1055
+ );
1056
+ }
1057
+
1058
+ // src/tools/hub.ts
1059
+ var import_zod4 = require("zod");
1060
+ function parseReleaseConfig(config) {
1061
+ if (!config) return void 0;
1062
+ try {
1063
+ return JSON.parse(config);
1064
+ } catch {
1065
+ return config;
1066
+ }
1067
+ }
1068
+ function listingsQuery(withConfig) {
1069
+ return `
1070
+ query {
1071
+ listings(input: {}) {
1072
+ id
1073
+ repoId
1074
+ title
1075
+ description
1076
+ repoName
1077
+ repoOwner
1078
+ createdAt
1079
+ updatedAt
1080
+ views
1081
+ stars
1082
+ deploys
1083
+ language
1084
+ category
1085
+ tags
1086
+ type
1087
+ listedRelease {
1088
+ id
1089
+ name
1090
+ tagName
1091
+ createdAt${withConfig ? "\n config" : ""}
1092
+ build {
1093
+ id
1094
+ imageName
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+ `;
1100
+ }
1101
+ function serializeBoolean(value, input) {
1102
+ if (value === "true") return input.trueValue ?? "true";
1103
+ if (value === "false") return input.falseValue ?? "false";
1104
+ return value;
1105
+ }
1106
+ function buildHubEnv(config, overrides) {
1107
+ const env = [];
1108
+ const missingRequired = [];
1109
+ const remaining = { ...overrides };
1110
+ for (const entry of config.env ?? []) {
1111
+ const input = entry.input ?? {};
1112
+ const override = entry.key in remaining ? remaining[entry.key] : void 0;
1113
+ delete remaining[entry.key];
1114
+ if (override !== void 0 && override !== "") {
1115
+ env.push({
1116
+ key: entry.key,
1117
+ value: input.type === "boolean" ? serializeBoolean(override, input) : override
1118
+ });
1119
+ continue;
1120
+ }
1121
+ if (input.default !== void 0 && input.default !== null) {
1122
+ env.push({
1123
+ key: entry.key,
1124
+ value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
1125
+ });
1126
+ continue;
1127
+ }
1128
+ if (input.required) {
1129
+ missingRequired.push(entry.key);
1130
+ env.push({ key: entry.key, value: "" });
1131
+ continue;
1132
+ }
1133
+ }
1134
+ for (const [key, value] of Object.entries(remaining)) {
1135
+ env.push({ key, value });
1136
+ }
1137
+ return { env, missingRequired };
1138
+ }
1139
+ function minCudaVersion(versions) {
1140
+ if (!versions || versions.length === 0) return null;
1141
+ const compare = (a, b) => {
1142
+ const pa = a.split(".").map(Number);
1143
+ const pb = b.split(".").map(Number);
1144
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1145
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1146
+ if (d !== 0) return d;
1147
+ }
1148
+ return 0;
1149
+ };
1150
+ return [...versions].sort(compare)[0];
1151
+ }
1152
+ function randomSuffix() {
1153
+ return Math.random().toString(36).slice(2, 8);
1154
+ }
1155
+ function registerHubTools(server, rt) {
1156
+ const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
1157
+ server.tool(
1158
+ "list-hub-repos",
1159
+ "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.",
1160
+ {
1161
+ ...listPaginationParams,
1162
+ searchTerm: import_zod4.z.string().optional().describe(
1163
+ "Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
1164
+ ),
1165
+ category: import_zod4.z.string().optional().describe(
1166
+ "Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
1167
+ ),
1168
+ type: import_zod4.z.enum(["SERVERLESS", "POD"]).optional().describe(
1169
+ "Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
1170
+ ),
1171
+ repoOwner: import_zod4.z.string().optional().describe(
1172
+ "Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
1173
+ ),
1174
+ includeConfig: import_zod4.z.boolean().optional().describe(
1175
+ "Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
1176
+ )
1177
+ },
1178
+ { title: "List Hub repos", ...READ_ONLY },
1179
+ async (params) => {
1180
+ const data = await graphql(
1181
+ listingsQuery(params.includeConfig === true)
1182
+ );
1183
+ let listings = data.listings;
1184
+ if (params.type) {
1185
+ listings = listings.filter(
1186
+ (l) => (l.type ?? "").toUpperCase() === params.type
1187
+ );
1188
+ }
1189
+ if (params.category) {
1190
+ const term = params.category.toLowerCase();
1191
+ listings = listings.filter(
1192
+ (l) => (l.category ?? "").toLowerCase() === term
1193
+ );
1194
+ }
1195
+ if (params.repoOwner) {
1196
+ const term = params.repoOwner.toLowerCase();
1197
+ listings = listings.filter(
1198
+ (l) => (l.repoOwner ?? "").toLowerCase() === term
1199
+ );
1200
+ }
1201
+ if (params.searchTerm) {
1202
+ const term = params.searchTerm.toLowerCase();
1203
+ listings = listings.filter(
1204
+ (l) => [
1205
+ l.title,
1206
+ l.description,
1207
+ l.repoName,
1208
+ l.repoOwner,
1209
+ ...l.tags ?? []
1210
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1211
+ );
1212
+ }
1213
+ listings = [...listings].sort(
1214
+ (a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
1215
+ );
1216
+ const result = listings.map((l) => ({
1217
+ id: l.id,
1218
+ repo: `${l.repoOwner}/${l.repoName}`,
1219
+ title: l.title,
1220
+ description: l.description,
1221
+ type: l.type,
1222
+ category: l.category,
1223
+ tags: l.tags,
1224
+ language: l.language,
1225
+ stars: l.stars,
1226
+ deploys: l.deploys,
1227
+ views: l.views,
1228
+ githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
1229
+ hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
1230
+ createdAt: l.createdAt,
1231
+ updatedAt: l.updatedAt,
1232
+ listedRelease: l.listedRelease ? {
1233
+ hubReleaseId: l.listedRelease.id,
1234
+ name: l.listedRelease.name,
1235
+ tagName: l.listedRelease.tagName,
1236
+ createdAt: l.listedRelease.createdAt,
1237
+ imageName: l.listedRelease.build?.imageName,
1238
+ ...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
1239
+ } : null
1240
+ }));
1241
+ return capListResult(result, {
1242
+ limit: params.limit,
1243
+ cursor: params.cursor
1244
+ });
1245
+ }
1246
+ );
1247
+ server.tool(
1248
+ "deploy-hub-repo",
1249
+ "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.",
1250
+ {
1251
+ repo: import_zod4.z.string().optional().describe(
1252
+ `The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
1253
+ ),
1254
+ hubReleaseId: import_zod4.z.string().optional().describe(
1255
+ "The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
1256
+ ),
1257
+ name: import_zod4.z.string().optional().describe(
1258
+ "Name for the new endpoint. Defaults to '<listing title> <release tag>'."
1259
+ ),
1260
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe(
1261
+ "Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
1262
+ ),
1263
+ gpuIds: import_zod4.z.string().optional().describe(
1264
+ "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."
1265
+ ),
1266
+ gpuCount: import_zod4.z.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
1267
+ containerDiskInGb: import_zod4.z.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
1268
+ workersMin: import_zod4.z.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
1269
+ workersMax: import_zod4.z.number().int().positive().optional().describe("Maximum workers (default: account default)."),
1270
+ idleTimeout: import_zod4.z.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
1271
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
1272
+ scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value (default 4)."),
1273
+ executionTimeoutMs: import_zod4.z.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
1274
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
1275
+ },
1276
+ { title: "Deploy Hub repo", ...WRITE },
1277
+ async (params) => {
1278
+ if (!params.repo && !params.hubReleaseId) {
1279
+ return jsonReply2({
1280
+ error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
1281
+ });
1282
+ }
1283
+ const catalog = await graphql(listingsQuery(true));
1284
+ let listing;
1285
+ if (params.hubReleaseId) {
1286
+ listing = catalog.listings.find(
1287
+ (l) => l.listedRelease?.id === params.hubReleaseId
1288
+ );
1289
+ } else {
1290
+ const repoKey = params.repo.toLowerCase();
1291
+ listing = catalog.listings.find(
1292
+ (l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
1293
+ );
1294
+ }
1295
+ if (!listing) {
1296
+ return jsonReply2({
1297
+ 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.`
1298
+ });
1299
+ }
1300
+ const release = listing.listedRelease;
1301
+ if (!release?.build?.imageName) {
1302
+ return jsonReply2({
1303
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
1304
+ });
1305
+ }
1306
+ if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
1307
+ return jsonReply2({
1308
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
1309
+ });
1310
+ }
1311
+ const config = parseReleaseConfig(release.config) ?? {};
1312
+ const gpuIds = params.gpuIds ?? config.gpuIds;
1313
+ if (!gpuIds) {
1314
+ return jsonReply2({
1315
+ 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).'
1316
+ });
1317
+ }
1318
+ const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
1319
+ if (missingRequired.length > 0) {
1320
+ return jsonReply2({
1321
+ error: `Missing required environment variables for this release: ${missingRequired.join(
1322
+ ", "
1323
+ )}. Pass them via the env parameter.`
1324
+ });
1325
+ }
1326
+ const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
1327
+ const minCuda = minCudaVersion(config.allowedCudaVersions);
1328
+ const input = {
1329
+ name: endpointName,
1330
+ hubReleaseId: release.id,
1331
+ type: "QB",
1332
+ gpuIds,
1333
+ gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
1334
+ workersMin: params.workersMin ?? 0,
1335
+ workersMax: params.workersMax ?? null,
1336
+ idleTimeout: params.idleTimeout ?? 5,
1337
+ scalerType: params.scalerType ?? "QUEUE_DELAY",
1338
+ scalerValue: params.scalerValue ?? 4,
1339
+ executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
1340
+ flashBootType: params.flashboot ?? "FLASHBOOT",
1341
+ locations: null,
1342
+ networkVolumeIds: null,
1343
+ compliance: [],
1344
+ modelReferences: [],
1345
+ ...minCuda ? {
1346
+ minCudaVersion: minCuda,
1347
+ allowedCudaVersions: config.allowedCudaVersions.join(",")
1348
+ } : {},
1349
+ template: {
1350
+ name: `${endpointName}__template__${randomSuffix()}`,
1351
+ imageName: release.build.imageName,
1352
+ containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
1353
+ containerRegistryAuthId: "",
1354
+ dockerArgs: "",
1355
+ startScript: "",
1356
+ ports: "",
1357
+ env
1358
+ }
1359
+ };
1360
+ const data = await graphqlAuthed(
1361
+ `
1362
+ mutation saveEndpoint($input: EndpointInput!) {
1363
+ saveEndpoint(input: $input) {
1364
+ id
1365
+ name
1366
+ gpuIds
1367
+ gpuCount
1368
+ workersMin
1369
+ workersMax
1370
+ idleTimeout
1371
+ scalerType
1372
+ scalerValue
1373
+ flashBootType
1374
+ templateId
1375
+ }
1376
+ }
1377
+ `,
1378
+ { input }
1379
+ );
1380
+ return jsonReply2({
1381
+ endpoint: data.saveEndpoint,
1382
+ deployed: {
1383
+ repo: `${listing.repoOwner}/${listing.repoName}`,
1384
+ release: release.tagName,
1385
+ hubReleaseId: release.id,
1386
+ imageName: release.build.imageName
1387
+ },
1388
+ note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
1389
+ });
1390
+ }
1391
+ );
1392
+ }
1393
+
1394
+ // src/tools/public-endpoints.ts
1395
+ var import_zod5 = require("zod");
1396
+ function parseMetadata(metadata) {
1397
+ if (!metadata) return {};
1398
+ try {
1399
+ const parsed = JSON.parse(metadata);
1400
+ return parsed && typeof parsed === "object" ? parsed : {};
1401
+ } catch {
1402
+ return {};
1403
+ }
1404
+ }
1405
+ function registerPublicEndpointTools(server, rt) {
1406
+ const { graphql } = rt;
1407
+ server.tool(
1408
+ "list-public-endpoints",
1409
+ "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.",
1410
+ {
1411
+ ...listPaginationParams,
1412
+ searchTerm: import_zod5.z.string().optional().describe(
1413
+ "Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
1414
+ ),
1415
+ modality: import_zod5.z.string().optional().describe(
1416
+ "Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
1417
+ ),
1418
+ owner: import_zod5.z.string().optional().describe(
1419
+ "Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
1420
+ ),
1421
+ includeOffline: import_zod5.z.boolean().optional().describe(
1422
+ "Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
1423
+ )
1424
+ },
1425
+ { title: "List public endpoints", ...READ_ONLY },
1426
+ async (params) => {
1427
+ const data = await graphql(`
1428
+ query {
1429
+ allAiApiPublicConfigs {
1430
+ id
1431
+ aiApiId
1432
+ modelName
1433
+ displayName
1434
+ description
1435
+ metadata
1436
+ isLive
1437
+ createdAt
1438
+ updatedAt
1439
+ }
1440
+ }
1441
+ `);
1442
+ let configs = data.allAiApiPublicConfigs.map((c) => ({
1443
+ config: c,
1444
+ meta: parseMetadata(c.metadata)
1445
+ }));
1446
+ if (!params.includeOffline) {
1447
+ configs = configs.filter(({ config }) => config.isLive);
1448
+ }
1449
+ if (params.modality) {
1450
+ const term = params.modality.toLowerCase();
1451
+ configs = configs.filter(
1452
+ ({ meta }) => (meta.source ?? "").toLowerCase() === term
1453
+ );
1454
+ }
1455
+ if (params.owner) {
1456
+ const term = params.owner.toLowerCase();
1457
+ configs = configs.filter(
1458
+ ({ meta }) => (meta.owner ?? "").toLowerCase() === term
1459
+ );
1460
+ }
1461
+ if (params.searchTerm) {
1462
+ const term = params.searchTerm.toLowerCase();
1463
+ configs = configs.filter(
1464
+ ({ config, meta }) => [
1465
+ config.displayName,
1466
+ config.modelName,
1467
+ config.aiApiId,
1468
+ config.description,
1469
+ meta.owner,
1470
+ meta.tag,
1471
+ meta.source
1472
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1473
+ );
1474
+ }
1475
+ configs = [...configs].sort(
1476
+ (a, b) => a.config.displayName.localeCompare(b.config.displayName)
848
1477
  );
849
- return jsonReply2(result);
1478
+ const result = configs.map(({ config, meta }) => ({
1479
+ endpointId: config.aiApiId,
1480
+ displayName: config.displayName,
1481
+ modelName: config.modelName,
1482
+ description: config.description,
1483
+ modality: meta.source,
1484
+ tag: meta.tag,
1485
+ owner: meta.owner,
1486
+ pricing: meta.priceString,
1487
+ isLive: config.isLive,
1488
+ baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
1489
+ createdAt: config.createdAt,
1490
+ updatedAt: config.updatedAt
1491
+ }));
1492
+ return capListResult(result, {
1493
+ limit: params.limit,
1494
+ cursor: params.cursor
1495
+ });
850
1496
  }
851
1497
  );
1498
+ }
1499
+
1500
+ // src/tools/endpoint-gpus.ts
1501
+ var import_zod6 = require("zod");
1502
+ function registerEndpointGpuTools(server, rt) {
1503
+ const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
852
1504
  server.tool(
853
- "get-cpu-type",
854
- "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).",
1505
+ "set-endpoint-gpus",
1506
+ "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.",
855
1507
  {
856
- cpuTypeId: import_zod2.z.string().describe("ID of the CPU type to retrieve")
1508
+ endpointId: import_zod6.z.string().describe("ID of the Serverless endpoint to update"),
1509
+ gpuIds: import_zod6.z.string().optional().describe(
1510
+ "Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
1511
+ ),
1512
+ pools: import_zod6.z.array(import_zod6.z.string()).optional().describe(
1513
+ "GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
1514
+ ),
1515
+ excludeGpuTypeIds: import_zod6.z.array(import_zod6.z.string()).optional().describe(
1516
+ "GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
1517
+ ),
1518
+ gpuCount: import_zod6.z.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
1519
+ minCudaVersion: import_zod6.z.string().optional().describe(
1520
+ "Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
1521
+ ),
1522
+ allowedCudaVersions: import_zod6.z.string().optional().describe(
1523
+ "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."
1524
+ )
857
1525
  },
858
- { title: "Get CPU type", ...READ_ONLY },
1526
+ { title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
859
1527
  async (params) => {
860
- const backend = backendFor("cpus");
861
- if (backend.version === "v1") {
1528
+ const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
1529
+ ...params.pools,
1530
+ ...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
1531
+ ].join(",") : void 0);
1532
+ if (!gpuIds) {
862
1533
  return jsonReply2({
863
- error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
864
- status: 501
1534
+ error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
865
1535
  });
866
1536
  }
867
- const result = await callRestUrl(
868
- `${backend.base}${backend.get(params.cpuTypeId)}`
1537
+ const data = await graphqlAuthed(`
1538
+ query {
1539
+ myself {
1540
+ endpoints {
1541
+ id
1542
+ name
1543
+ gpuIds
1544
+ gpuCount
1545
+ workersMin
1546
+ workersMax
1547
+ idleTimeout
1548
+ scalerType
1549
+ scalerValue
1550
+ executionTimeoutMs
1551
+ flashBootType
1552
+ type
1553
+ locations
1554
+ templateId
1555
+ allowedCudaVersions
1556
+ minCudaVersion
1557
+ compliance
1558
+ modelReferences
1559
+ networkVolumeIds {
1560
+ networkVolumeId
1561
+ dataCenterId
1562
+ }
1563
+ }
1564
+ }
1565
+ }
1566
+ `);
1567
+ const current = data.myself.endpoints.find(
1568
+ (e) => e.id === params.endpointId
869
1569
  );
870
- return jsonReply2(result);
871
- }
872
- );
873
- server.tool(
874
- "get-data-center",
875
- "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).",
876
- {
877
- dataCenterId: import_zod2.z.string().describe("ID of the data center to retrieve")
878
- },
879
- { title: "Get data center", ...READ_ONLY },
880
- async (params) => {
881
- const backend = backendFor("dataCenters");
882
- if (backend.version === "v1") {
1570
+ if (!current) {
883
1571
  return jsonReply2({
884
- error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
885
- status: 501
1572
+ error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
886
1573
  });
887
1574
  }
888
- const result = await callRestUrl(
889
- `${backend.base}${backend.get(params.dataCenterId)}`
1575
+ const input = {
1576
+ id: current.id,
1577
+ name: current.name,
1578
+ gpuIds,
1579
+ gpuCount: params.gpuCount ?? current.gpuCount,
1580
+ workersMin: current.workersMin,
1581
+ workersMax: current.workersMax,
1582
+ idleTimeout: current.idleTimeout,
1583
+ scalerType: current.scalerType,
1584
+ scalerValue: current.scalerValue,
1585
+ executionTimeoutMs: current.executionTimeoutMs,
1586
+ flashBootType: current.flashBootType,
1587
+ type: current.type,
1588
+ locations: current.locations,
1589
+ networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
1590
+ // Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
1591
+ // ONLY, and the read shape is rejected outright.
1592
+ current.networkVolumeIds.map((v) => ({
1593
+ networkVolumeId: v.networkVolumeId
1594
+ }))
1595
+ ) : null
1596
+ };
1597
+ for (const [key, value] of Object.entries({
1598
+ templateId: current.templateId,
1599
+ allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
1600
+ minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
1601
+ compliance: current.compliance,
1602
+ modelReferences: current.modelReferences
1603
+ })) {
1604
+ if (value !== null && value !== void 0) input[key] = value;
1605
+ }
1606
+ const result = await graphqlAuthed(
1607
+ `
1608
+ mutation saveEndpoint($input: EndpointInput!) {
1609
+ saveEndpoint(input: $input) {
1610
+ id
1611
+ name
1612
+ gpuIds
1613
+ gpuCount
1614
+ workersMin
1615
+ workersMax
1616
+ }
1617
+ }
1618
+ `,
1619
+ { input }
890
1620
  );
891
- return jsonReply2(result);
1621
+ return jsonReply2({
1622
+ endpoint: result.saveEndpoint,
1623
+ previousGpuIds: current.gpuIds
1624
+ });
892
1625
  }
893
1626
  );
894
1627
  }
895
1628
 
896
1629
  // src/tools/pods.ts
897
- var import_zod3 = require("zod");
1630
+ var import_zod7 = require("zod");
898
1631
  function registerPodTools(server, rt) {
899
1632
  const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
900
1633
  server.tool(
@@ -902,12 +1635,12 @@ function registerPodTools(server, rt) {
902
1635
  "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.",
903
1636
  {
904
1637
  ...listPaginationParams,
905
- computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
906
- gpuTypeId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
907
- dataCenterId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
908
- name: import_zod3.z.string().optional().describe("Filter to Pods with the provided name"),
909
- includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
910
- includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
1638
+ computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
1639
+ gpuTypeId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
1640
+ dataCenterId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
1641
+ name: import_zod7.z.string().optional().describe("Filter to Pods with the provided name"),
1642
+ includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
1643
+ includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
911
1644
  },
912
1645
  { title: "List pods", ...READ_ONLY },
913
1646
  async (params) => {
@@ -951,9 +1684,9 @@ function registerPodTools(server, rt) {
951
1684
  "get-pod",
952
1685
  "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
953
1686
  {
954
- podId: import_zod3.z.string().describe("ID of the pod to retrieve"),
955
- includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
956
- includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
1687
+ podId: import_zod7.z.string().describe("ID of the pod to retrieve"),
1688
+ includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
1689
+ includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
957
1690
  },
958
1691
  { title: "Get pod", ...READ_ONLY },
959
1692
  async (params) => {
@@ -981,26 +1714,44 @@ function registerPodTools(server, rt) {
981
1714
  );
982
1715
  server.tool(
983
1716
  "create-pod",
984
- '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.',
1717
+ '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.',
985
1718
  {
986
- name: import_zod3.z.string().optional().describe("Name for the pod"),
987
- imageName: import_zod3.z.string().describe("Docker image to use"),
988
- computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe(
1719
+ name: import_zod7.z.string().optional().describe("Name for the pod"),
1720
+ imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
1721
+ templateId: import_zod7.z.string().optional().describe(
1722
+ "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."
1723
+ ),
1724
+ computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe(
989
1725
  "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
990
1726
  ),
991
- cloudType: import_zod3.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
992
- gpuTypeIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of acceptable GPU types"),
993
- gpuCount: import_zod3.z.number().optional().describe("Number of GPUs"),
994
- containerDiskInGb: import_zod3.z.number().optional().describe("Container disk size in GB"),
995
- volumeInGb: import_zod3.z.number().optional().describe("Volume size in GB"),
996
- volumeMountPath: import_zod3.z.string().optional().describe("Path to mount the volume"),
997
- ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
998
- env: import_zod3.z.record(import_zod3.z.string()).optional().describe("Environment variables"),
999
- dataCenterIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of data centers")
1727
+ cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1728
+ gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
1729
+ gpuCount: import_zod7.z.number().optional().describe("Number of GPUs"),
1730
+ containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
1731
+ volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
1732
+ volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
1733
+ ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1734
+ env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
1735
+ containerRegistryAuthId: import_zod7.z.string().optional().describe(
1736
+ "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)."
1737
+ ),
1738
+ dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
1000
1739
  },
1001
1740
  { title: "Create pod", ...WRITE },
1002
1741
  async (params) => {
1003
1742
  const backend = backendFor("pods");
1743
+ if (params.templateId && backend.version !== "v2") {
1744
+ return jsonReply2({
1745
+ error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
1746
+ status: 501
1747
+ });
1748
+ }
1749
+ if (!params.imageName && !params.templateId) {
1750
+ return jsonReply2({
1751
+ error: "Provide imageName, or templateId to deploy from an existing template.",
1752
+ status: 400
1753
+ });
1754
+ }
1004
1755
  const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
1005
1756
  const hasGpuCount = (params.gpuCount ?? 0) > 0;
1006
1757
  const wantsCpu = params.computeType === "CPU";
@@ -1025,6 +1776,12 @@ function registerPodTools(server, rt) {
1025
1776
  });
1026
1777
  }
1027
1778
  if (wantsCpu) {
1779
+ if (params.templateId) {
1780
+ return jsonReply2({
1781
+ error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
1782
+ status: 400
1783
+ });
1784
+ }
1028
1785
  try {
1029
1786
  const result = await callRestUrl(
1030
1787
  `${restV1Base(env)}/pods`,
@@ -1049,7 +1806,24 @@ function registerPodTools(server, rt) {
1049
1806
  }
1050
1807
  }
1051
1808
  }
1052
- const body = backend.mapCreate(params);
1809
+ let body = backend.mapCreate(params);
1810
+ if (params.templateId) {
1811
+ let template;
1812
+ try {
1813
+ template = await callRestUrl(
1814
+ `${restV2Base(env)}/templates/${params.templateId}`
1815
+ );
1816
+ } catch (error) {
1817
+ if (error instanceof HttpError) {
1818
+ return jsonReply2({
1819
+ error: `Failed to load template ${params.templateId}: ${error.message}`,
1820
+ status: error.status
1821
+ });
1822
+ }
1823
+ throw error;
1824
+ }
1825
+ body = { ...podBodyFromTemplate(template), ...body };
1826
+ }
1053
1827
  try {
1054
1828
  const result = await callRestUrl(
1055
1829
  `${backend.base}${backend.list}`,
@@ -1064,11 +1838,8 @@ function registerPodTools(server, rt) {
1064
1838
  }
1065
1839
  return jsonReply2(result);
1066
1840
  } catch (error) {
1067
- if (error instanceof HttpError && error.status === 501) {
1068
- return jsonReply2({
1069
- 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).",
1070
- status: 501
1071
- });
1841
+ if (error instanceof HttpError) {
1842
+ return jsonReply2({ error: error.message, status: error.status });
1072
1843
  }
1073
1844
  throw error;
1074
1845
  }
@@ -1078,14 +1849,14 @@ function registerPodTools(server, rt) {
1078
1849
  "update-pod",
1079
1850
  "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
1080
1851
  {
1081
- podId: import_zod3.z.string().describe("ID of the pod to update"),
1082
- name: import_zod3.z.string().optional().describe("New name for the pod"),
1083
- imageName: import_zod3.z.string().optional().describe("New Docker image"),
1084
- containerDiskInGb: import_zod3.z.number().optional().describe("New container disk size in GB"),
1085
- volumeInGb: import_zod3.z.number().optional().describe("New volume size in GB"),
1086
- volumeMountPath: import_zod3.z.string().optional().describe("New path to mount the volume"),
1087
- ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("New ports to expose"),
1088
- env: import_zod3.z.record(import_zod3.z.string()).optional().describe("New environment variables")
1852
+ podId: import_zod7.z.string().describe("ID of the pod to update"),
1853
+ name: import_zod7.z.string().optional().describe("New name for the pod"),
1854
+ imageName: import_zod7.z.string().optional().describe("New Docker image"),
1855
+ containerDiskInGb: import_zod7.z.number().optional().describe("New container disk size in GB"),
1856
+ volumeInGb: import_zod7.z.number().optional().describe("New volume size in GB"),
1857
+ volumeMountPath: import_zod7.z.string().optional().describe("New path to mount the volume"),
1858
+ ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("New ports to expose"),
1859
+ env: import_zod7.z.record(import_zod7.z.string()).optional().describe("New environment variables")
1089
1860
  },
1090
1861
  { title: "Update pod", ...WRITE, idempotentHint: true },
1091
1862
  async (params) => {
@@ -1104,7 +1875,7 @@ function registerPodTools(server, rt) {
1104
1875
  "start-pod",
1105
1876
  "Start a stopped pod (resumes the same pod and its billing).",
1106
1877
  {
1107
- podId: import_zod3.z.string().describe("ID of the pod to start")
1878
+ podId: import_zod7.z.string().describe("ID of the pod to start")
1108
1879
  },
1109
1880
  { title: "Start pod", ...WRITE, idempotentHint: true },
1110
1881
  async (params) => {
@@ -1116,7 +1887,7 @@ function registerPodTools(server, rt) {
1116
1887
  "stop-pod",
1117
1888
  "Stop a running pod. The pod and its disk persist; GPU billing stops.",
1118
1889
  {
1119
- podId: import_zod3.z.string().describe("ID of the pod to stop")
1890
+ podId: import_zod7.z.string().describe("ID of the pod to stop")
1120
1891
  },
1121
1892
  { title: "Stop pod", ...WRITE, idempotentHint: true },
1122
1893
  async (params) => {
@@ -1128,7 +1899,7 @@ function registerPodTools(server, rt) {
1128
1899
  "restart-pod",
1129
1900
  "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1130
1901
  {
1131
- podId: import_zod3.z.string().describe("ID of the pod to restart")
1902
+ podId: import_zod7.z.string().describe("ID of the pod to restart")
1132
1903
  },
1133
1904
  { title: "Restart pod", ...WRITE },
1134
1905
  async (params) => {
@@ -1143,11 +1914,29 @@ function registerPodTools(server, rt) {
1143
1914
  return jsonReply2(result);
1144
1915
  }
1145
1916
  );
1917
+ server.tool(
1918
+ "stream-pod-logs",
1919
+ "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).",
1920
+ {
1921
+ podId: import_zod7.z.string().describe("ID of the pod whose logs to stream"),
1922
+ ...logStreamParams
1923
+ },
1924
+ { title: "Stream pod logs", ...READ_ONLY },
1925
+ (params) => streamLogsReply(
1926
+ rt,
1927
+ {
1928
+ name: "stream-pod-logs",
1929
+ resource: "pods",
1930
+ logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
1931
+ },
1932
+ params
1933
+ )
1934
+ );
1146
1935
  server.tool(
1147
1936
  "delete-pod",
1148
1937
  "Permanently delete a pod and its data. This cannot be undone.",
1149
1938
  {
1150
- podId: import_zod3.z.string().describe("ID of the pod to delete")
1939
+ podId: import_zod7.z.string().describe("ID of the pod to delete")
1151
1940
  },
1152
1941
  { title: "Delete pod", ...DESTRUCTIVE },
1153
1942
  async (params) => {
@@ -1162,7 +1951,7 @@ function registerPodTools(server, rt) {
1162
1951
  }
1163
1952
 
1164
1953
  // src/tools/endpoints.ts
1165
- var import_zod4 = require("zod");
1954
+ var import_zod8 = require("zod");
1166
1955
  function registerEndpointTools(server, rt) {
1167
1956
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1168
1957
  server.tool(
@@ -1170,8 +1959,8 @@ function registerEndpointTools(server, rt) {
1170
1959
  "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
1171
1960
  {
1172
1961
  ...listPaginationParams,
1173
- includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
1174
- includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
1962
+ includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
1963
+ includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
1175
1964
  },
1176
1965
  { title: "List endpoints", ...READ_ONLY },
1177
1966
  async (params) => {
@@ -1204,9 +1993,9 @@ function registerEndpointTools(server, rt) {
1204
1993
  "get-endpoint",
1205
1994
  "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
1206
1995
  {
1207
- endpointId: import_zod4.z.string().describe("ID of the endpoint to retrieve"),
1208
- includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
1209
- includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
1996
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
1997
+ includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
1998
+ includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
1210
1999
  },
1211
2000
  { title: "Get endpoint", ...READ_ONLY },
1212
2001
  async (params) => {
@@ -1236,32 +2025,32 @@ function registerEndpointTools(server, rt) {
1236
2025
  "create-endpoint",
1237
2026
  "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).",
1238
2027
  {
1239
- name: import_zod4.z.string().optional().describe("Name for the endpoint"),
2028
+ name: import_zod8.z.string().optional().describe("Name for the endpoint"),
1240
2029
  // --- v2 inline-config fields ---
1241
- imageName: import_zod4.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
1242
- gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe(
2030
+ imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
2031
+ gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
1243
2032
  'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
1244
2033
  ),
1245
- gpuCount: import_zod4.z.number().optional().describe("GPUs per worker (v2)"),
1246
- args: import_zod4.z.string().optional().describe("Container start command/args (v2)"),
1247
- containerDiskInGb: import_zod4.z.number().optional().describe("Container disk size in GB (v2)"),
1248
- ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
1249
- env: import_zod4.z.record(import_zod4.z.string()).optional().describe("Environment variables (v2)"),
1250
- containerRegistryAuthId: import_zod4.z.string().optional().describe("Container registry auth id for a private image (v2)"),
1251
- networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("Network volume ids to attach (v2)"),
1252
- executionTimeoutMs: import_zod4.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
1253
- flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
2034
+ gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
2035
+ args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
2036
+ containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
2037
+ ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
2038
+ env: import_zod8.z.record(import_zod8.z.string()).optional().describe("Environment variables (v2)"),
2039
+ containerRegistryAuthId: import_zod8.z.string().optional().describe("Container registry auth id for a private image (v2)"),
2040
+ networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("Network volume ids to attach (v2)"),
2041
+ executionTimeoutMs: import_zod8.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
2042
+ flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
1254
2043
  // --- v1 template-based fields ---
1255
- templateId: import_zod4.z.string().optional().describe("Template ID (v1). Required on v1."),
1256
- computeType: import_zod4.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
1257
- gpuTypeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of acceptable GPU types (v1)"),
2044
+ templateId: import_zod8.z.string().optional().describe("Template ID (v1). Required on v1."),
2045
+ computeType: import_zod8.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
2046
+ gpuTypeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of acceptable GPU types (v1)"),
1258
2047
  // --- shared autoscaling fields ---
1259
- workersMin: import_zod4.z.number().optional().describe("Minimum number of workers"),
1260
- workersMax: import_zod4.z.number().optional().describe("Maximum number of workers"),
1261
- scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
1262
- scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value"),
1263
- idleTimeout: import_zod4.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
1264
- dataCenterIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of preferred data centers")
2048
+ workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
2049
+ workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
2050
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
2051
+ scalerValue: import_zod8.z.number().optional().describe("Autoscaler target value"),
2052
+ idleTimeout: import_zod8.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
2053
+ dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
1265
2054
  },
1266
2055
  { title: "Create endpoint", ...WRITE },
1267
2056
  async (params) => {
@@ -1324,25 +2113,25 @@ function registerEndpointTools(server, rt) {
1324
2113
  "update-endpoint",
1325
2114
  "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.",
1326
2115
  {
1327
- endpointId: import_zod4.z.string().describe("ID of the endpoint to update"),
1328
- name: import_zod4.z.string().optional().describe("New name for the endpoint"),
1329
- workersMin: import_zod4.z.number().optional().describe("New minimum number of workers"),
1330
- workersMax: import_zod4.z.number().optional().describe("New maximum number of workers"),
1331
- idleTimeout: import_zod4.z.number().optional().describe("New idle timeout in seconds"),
1332
- scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1333
- scalerValue: import_zod4.z.number().optional().describe("Scaler value"),
2116
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
2117
+ name: import_zod8.z.string().optional().describe("New name for the endpoint"),
2118
+ workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
2119
+ workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
2120
+ idleTimeout: import_zod8.z.number().optional().describe("New idle timeout in seconds"),
2121
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2122
+ scalerValue: import_zod8.z.number().optional().describe("Scaler value"),
1334
2123
  // --- v2-only inline-config fields ---
1335
- imageName: import_zod4.z.string().optional().describe("New Docker image (v2)"),
1336
- gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
1337
- gpuCount: import_zod4.z.number().optional().describe("New GPUs per worker (v2)"),
1338
- args: import_zod4.z.string().optional().describe("New container args (v2)"),
1339
- containerDiskInGb: import_zod4.z.number().optional().describe("New container disk size in GB (v2)"),
1340
- ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("New ports (v2)"),
1341
- env: import_zod4.z.record(import_zod4.z.string()).optional().describe("New environment variables (v2)"),
1342
- containerRegistryAuthId: import_zod4.z.string().optional().describe("New container registry auth id (v2)"),
1343
- networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("New network volume ids (v2)"),
1344
- executionTimeoutMs: import_zod4.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
1345
- flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
2124
+ imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
2125
+ gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
2126
+ gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
2127
+ args: import_zod8.z.string().optional().describe("New container args (v2)"),
2128
+ containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
2129
+ ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
2130
+ env: import_zod8.z.record(import_zod8.z.string()).optional().describe("New environment variables (v2)"),
2131
+ containerRegistryAuthId: import_zod8.z.string().optional().describe("New container registry auth id (v2)"),
2132
+ networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("New network volume ids (v2)"),
2133
+ executionTimeoutMs: import_zod8.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
2134
+ flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
1346
2135
  },
1347
2136
  { title: "Update endpoint", ...WRITE, idempotentHint: true },
1348
2137
  async (params) => {
@@ -1361,7 +2150,7 @@ function registerEndpointTools(server, rt) {
1361
2150
  "delete-endpoint",
1362
2151
  "Permanently delete a Serverless endpoint. This cannot be undone.",
1363
2152
  {
1364
- endpointId: import_zod4.z.string().describe("ID of the endpoint to delete")
2153
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to delete")
1365
2154
  },
1366
2155
  { title: "Delete endpoint", ...DESTRUCTIVE },
1367
2156
  async (params) => {
@@ -1373,12 +2162,39 @@ function registerEndpointTools(server, rt) {
1373
2162
  return jsonReply2(result);
1374
2163
  }
1375
2164
  );
2165
+ server.tool(
2166
+ "list-endpoint-releases",
2167
+ "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.",
2168
+ {
2169
+ ...listPaginationParams,
2170
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose releases to list")
2171
+ },
2172
+ { title: "List endpoint releases", ...READ_ONLY },
2173
+ async (params) => {
2174
+ const backend = backendFor("endpoints");
2175
+ if (backend.version === "v1") {
2176
+ return jsonReply2({
2177
+ error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2178
+ status: 501
2179
+ });
2180
+ }
2181
+ const raw = await callRestUrl(
2182
+ `${backend.base}/serverless/${params.endpointId}/releases`
2183
+ );
2184
+ const releases = Array.isArray(raw?.releases) ? raw.releases : [];
2185
+ return capListResult(
2186
+ releases,
2187
+ { limit: params.limit, cursor: params.cursor },
2188
+ { rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
2189
+ );
2190
+ }
2191
+ );
1376
2192
  server.tool(
1377
2193
  "list-endpoint-workers",
1378
2194
  "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.",
1379
2195
  {
1380
2196
  ...listPaginationParams,
1381
- endpointId: import_zod4.z.string().describe("ID of the Serverless endpoint whose workers to list")
2197
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
1382
2198
  },
1383
2199
  { title: "List endpoint workers", ...READ_ONLY },
1384
2200
  async (params) => {
@@ -1399,30 +2215,51 @@ function registerEndpointTools(server, rt) {
1399
2215
  );
1400
2216
  }
1401
2217
  );
2218
+ server.tool(
2219
+ "stream-worker-logs",
2220
+ "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).",
2221
+ {
2222
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
2223
+ workerId: import_zod8.z.string().describe(
2224
+ "ID of the worker whose logs to stream (from list-endpoint-workers)"
2225
+ ),
2226
+ ...logStreamParams
2227
+ },
2228
+ { title: "Stream worker logs", ...READ_ONLY },
2229
+ (params) => streamLogsReply(
2230
+ rt,
2231
+ {
2232
+ name: "stream-worker-logs",
2233
+ resource: "workers",
2234
+ logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
2235
+ },
2236
+ params
2237
+ )
2238
+ );
1402
2239
  }
1403
2240
 
1404
2241
  // src/tools/jobs.ts
1405
- var import_zod5 = require("zod");
2242
+ var import_zod9 = require("zod");
1406
2243
  function registerJobTools(server, rt) {
1407
2244
  const { jsonReply: jsonReply2, serverlessRequest } = rt;
1408
- const endpointIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1409
- const jobIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1410
- const inputSchema = import_zod5.z.record(import_zod5.z.unknown()).describe(
2245
+ const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2246
+ const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2247
+ const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
1411
2248
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
1412
2249
  );
1413
- const webhookSchema = import_zod5.z.string().url().optional().describe(
2250
+ const webhookSchema = import_zod9.z.string().url().optional().describe(
1414
2251
  "Webhook URL to receive job completion notifications instead of polling"
1415
2252
  );
1416
- const policySchema = import_zod5.z.object({
1417
- executionTimeout: import_zod5.z.number().optional().describe("Maximum execution time in milliseconds"),
1418
- lowPriority: import_zod5.z.boolean().optional().describe("Submit as a low-priority job"),
1419
- ttl: import_zod5.z.number().optional().describe("Time-to-live for the job result in milliseconds")
2253
+ const policySchema = import_zod9.z.object({
2254
+ executionTimeout: import_zod9.z.number().optional().describe("Maximum execution time in milliseconds"),
2255
+ lowPriority: import_zod9.z.boolean().optional().describe("Submit as a low-priority job"),
2256
+ ttl: import_zod9.z.number().optional().describe("Time-to-live for the job result in milliseconds")
1420
2257
  }).optional().describe("Execution policy options");
1421
- const s3ConfigSchema = import_zod5.z.object({
1422
- accessId: import_zod5.z.string().describe("S3 access key ID"),
1423
- accessSecret: import_zod5.z.string().describe("S3 secret access key"),
1424
- bucketName: import_zod5.z.string().describe("S3 bucket name"),
1425
- endpointUrl: import_zod5.z.string().describe("S3 endpoint URL")
2258
+ const s3ConfigSchema = import_zod9.z.object({
2259
+ accessId: import_zod9.z.string().describe("S3 access key ID"),
2260
+ accessSecret: import_zod9.z.string().describe("S3 secret access key"),
2261
+ bucketName: import_zod9.z.string().describe("S3 bucket name"),
2262
+ endpointUrl: import_zod9.z.string().describe("S3 endpoint URL")
1426
2263
  }).optional().describe("S3-compatible storage config for large outputs");
1427
2264
  server.tool(
1428
2265
  "run-endpoint",
@@ -1456,7 +2293,7 @@ function registerJobTools(server, rt) {
1456
2293
  "ID of the Serverless endpoint to run synchronously"
1457
2294
  ),
1458
2295
  input: inputSchema,
1459
- wait: import_zod5.z.number().min(1e3).max(3e5).optional().describe(
2296
+ wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
1460
2297
  "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)."
1461
2298
  ),
1462
2299
  webhook: webhookSchema,
@@ -1625,7 +2462,7 @@ function registerJobTools(server, rt) {
1625
2462
  }
1626
2463
 
1627
2464
  // src/tools/templates.ts
1628
- var import_zod6 = require("zod");
2465
+ var import_zod10 = require("zod");
1629
2466
  function registerTemplateTools(server, rt) {
1630
2467
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1631
2468
  server.tool(
@@ -1633,9 +2470,9 @@ function registerTemplateTools(server, rt) {
1633
2470
  `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.`,
1634
2471
  {
1635
2472
  ...listPaginationParams,
1636
- includeRunpodTemplates: import_zod6.z.boolean().optional().describe("Include official Runpod templates in the response"),
1637
- includePublicTemplates: import_zod6.z.boolean().optional().describe("Include community-made public templates in the response"),
1638
- includeEndpointBoundTemplates: import_zod6.z.boolean().optional().describe(
2473
+ includeRunpodTemplates: import_zod10.z.boolean().optional().describe("Include official Runpod templates in the response"),
2474
+ includePublicTemplates: import_zod10.z.boolean().optional().describe("Include community-made public templates in the response"),
2475
+ includeEndpointBoundTemplates: import_zod10.z.boolean().optional().describe(
1639
2476
  "Include templates bound to Serverless endpoints in the response"
1640
2477
  )
1641
2478
  },
@@ -1666,7 +2503,7 @@ function registerTemplateTools(server, rt) {
1666
2503
  "get-template",
1667
2504
  "Get one template by id (image, env, ports, disk/volume config).",
1668
2505
  {
1669
- templateId: import_zod6.z.string().describe("ID of the template to retrieve")
2506
+ templateId: import_zod10.z.string().describe("ID of the template to retrieve")
1670
2507
  },
1671
2508
  { title: "Get template", ...READ_ONLY },
1672
2509
  async (params) => {
@@ -1679,23 +2516,27 @@ function registerTemplateTools(server, rt) {
1679
2516
  );
1680
2517
  server.tool(
1681
2518
  "create-template",
1682
- "Create a reusable pod/endpoint template (image, ports, env, disk/volume). On v2 a category is required and defaults to NVIDIA.",
2519
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
1683
2520
  {
1684
- name: import_zod6.z.string().describe("Name for the template"),
1685
- imageName: import_zod6.z.string().describe("Docker image to use"),
1686
- isServerless: import_zod6.z.boolean().optional().describe("Is this a serverless template"),
1687
- ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("Ports to expose"),
1688
- dockerEntrypoint: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker entrypoint commands"),
1689
- dockerStartCmd: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker start commands"),
1690
- env: import_zod6.z.record(import_zod6.z.string()).optional().describe("Environment variables"),
1691
- containerDiskInGb: import_zod6.z.number().optional().describe("Container disk size in GB"),
1692
- volumeInGb: import_zod6.z.number().optional().describe("Volume size in GB"),
1693
- volumeMountPath: import_zod6.z.string().optional().describe("Path to mount the volume"),
1694
- readme: import_zod6.z.string().optional().describe("README content in markdown format"),
1695
- category: import_zod6.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
1696
- "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."
2521
+ name: import_zod10.z.string().describe("Name for the template"),
2522
+ imageName: import_zod10.z.string().describe("Docker image to use"),
2523
+ isServerless: import_zod10.z.boolean().optional().describe("Is this a serverless template"),
2524
+ ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("Ports to expose"),
2525
+ dockerEntrypoint: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2526
+ "Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
2527
+ ),
2528
+ dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2529
+ '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 "").'
2530
+ ),
2531
+ env: import_zod10.z.record(import_zod10.z.string()).optional().describe("Environment variables"),
2532
+ containerDiskInGb: import_zod10.z.number().optional().describe("Container disk size in GB"),
2533
+ volumeInGb: import_zod10.z.number().optional().describe("Volume size in GB"),
2534
+ volumeMountPath: import_zod10.z.string().optional().describe("Path to mount the volume"),
2535
+ readme: import_zod10.z.string().optional().describe("README content in markdown format"),
2536
+ category: import_zod10.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2537
+ "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."
1697
2538
  ),
1698
- containerRegistryAuthId: import_zod6.z.string().optional().describe(
2539
+ containerRegistryAuthId: import_zod10.z.string().optional().describe(
1699
2540
  "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."
1700
2541
  )
1701
2542
  },
@@ -1713,15 +2554,18 @@ function registerTemplateTools(server, rt) {
1713
2554
  );
1714
2555
  server.tool(
1715
2556
  "update-template",
1716
- "Update a template's mutable fields (name, image, ports, env, readme). Only provided fields change.",
2557
+ "Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
1717
2558
  {
1718
- templateId: import_zod6.z.string().describe("ID of the template to update"),
1719
- name: import_zod6.z.string().optional().describe("New name for the template"),
1720
- imageName: import_zod6.z.string().optional().describe("New Docker image"),
1721
- ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("New ports to expose"),
1722
- env: import_zod6.z.record(import_zod6.z.string()).optional().describe("New environment variables"),
1723
- readme: import_zod6.z.string().optional().describe("New README content in markdown format"),
1724
- containerRegistryAuthId: import_zod6.z.string().optional().describe(
2559
+ templateId: import_zod10.z.string().describe("ID of the template to update"),
2560
+ name: import_zod10.z.string().optional().describe("New name for the template"),
2561
+ imageName: import_zod10.z.string().optional().describe("New Docker image"),
2562
+ dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2563
+ "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."
2564
+ ),
2565
+ ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("New ports to expose"),
2566
+ env: import_zod10.z.record(import_zod10.z.string()).optional().describe("New environment variables"),
2567
+ readme: import_zod10.z.string().optional().describe("New README content in markdown format"),
2568
+ containerRegistryAuthId: import_zod10.z.string().optional().describe(
1725
2569
  "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."
1726
2570
  )
1727
2571
  },
@@ -1742,7 +2586,7 @@ function registerTemplateTools(server, rt) {
1742
2586
  "delete-template",
1743
2587
  "Permanently delete a template. This cannot be undone.",
1744
2588
  {
1745
- templateId: import_zod6.z.string().describe("ID of the template to delete")
2589
+ templateId: import_zod10.z.string().describe("ID of the template to delete")
1746
2590
  },
1747
2591
  { title: "Delete template", ...DESTRUCTIVE },
1748
2592
  async (params) => {
@@ -1757,7 +2601,7 @@ function registerTemplateTools(server, rt) {
1757
2601
  }
1758
2602
 
1759
2603
  // src/tools/network-volumes.ts
1760
- var import_zod7 = require("zod");
2604
+ var import_zod11 = require("zod");
1761
2605
  function registerNetworkVolumeTools(server, rt) {
1762
2606
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1763
2607
  server.tool(
@@ -1778,7 +2622,7 @@ function registerNetworkVolumeTools(server, rt) {
1778
2622
  "get-network-volume",
1779
2623
  "Get one network volume by id (name, size, data center).",
1780
2624
  {
1781
- networkVolumeId: import_zod7.z.string().describe("ID of the network volume to retrieve")
2625
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to retrieve")
1782
2626
  },
1783
2627
  { title: "Get network volume", ...READ_ONLY },
1784
2628
  async (params) => {
@@ -1791,11 +2635,17 @@ function registerNetworkVolumeTools(server, rt) {
1791
2635
  );
1792
2636
  server.tool(
1793
2637
  "create-network-volume",
1794
- "Create a persistent network volume in a data center (size 1\u20134000 GB) that pods can mount.",
2638
+ "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.",
1795
2639
  {
1796
- name: import_zod7.z.string().describe("Name for the network volume"),
1797
- size: import_zod7.z.number().describe("Size in GB (1-4000)"),
1798
- dataCenterId: import_zod7.z.string().describe("Data center ID (see list-data-centers)")
2640
+ name: import_zod11.z.string().describe("Name for the network volume"),
2641
+ // Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
2642
+ // here as well as described, so an out-of-range value fails locally
2643
+ // instead of costing an API roundtrip.
2644
+ size: import_zod11.z.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
2645
+ dataCenterId: import_zod11.z.string().describe("Data center ID (see list-data-centers)"),
2646
+ volumeType: import_zod11.z.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
2647
+ "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."
2648
+ )
1799
2649
  },
1800
2650
  { title: "Create network volume", ...WRITE },
1801
2651
  async (params) => {
@@ -1813,9 +2663,11 @@ function registerNetworkVolumeTools(server, rt) {
1813
2663
  "update-network-volume",
1814
2664
  "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1815
2665
  {
1816
- networkVolumeId: import_zod7.z.string().describe("ID of the network volume to update"),
1817
- name: import_zod7.z.string().optional().describe("New name for the network volume"),
1818
- size: import_zod7.z.number().optional().describe("New size in GB (must be larger than current)")
2666
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to update"),
2667
+ name: import_zod11.z.string().optional().describe("New name for the network volume"),
2668
+ // Same absolute bounds as create. "Larger than current" stays server-side —
2669
+ // we do not hold the current size here, so only the range is checked.
2670
+ size: import_zod11.z.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
1819
2671
  },
1820
2672
  { title: "Update network volume", ...WRITE, idempotentHint: true },
1821
2673
  async (params) => {
@@ -1834,7 +2686,7 @@ function registerNetworkVolumeTools(server, rt) {
1834
2686
  "delete-network-volume",
1835
2687
  "Permanently delete a network volume and its data. This cannot be undone.",
1836
2688
  {
1837
- networkVolumeId: import_zod7.z.string().describe("ID of the network volume to delete")
2689
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to delete")
1838
2690
  },
1839
2691
  { title: "Delete network volume", ...DESTRUCTIVE },
1840
2692
  async (params) => {
@@ -1849,9 +2701,14 @@ function registerNetworkVolumeTools(server, rt) {
1849
2701
  }
1850
2702
 
1851
2703
  // src/tools/registries.ts
1852
- var import_zod8 = require("zod");
2704
+ var import_zod12 = require("zod");
2705
+ var DELEGATIONS = "/delegations";
1853
2706
  function registerRegistryTools(server, rt) {
1854
2707
  const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2708
+ const delegationsV2OnlyNotice = (tool) => ({
2709
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
2710
+ status: 501
2711
+ });
1855
2712
  server.tool(
1856
2713
  "list-container-registry-auths",
1857
2714
  "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
@@ -1870,7 +2727,7 @@ function registerRegistryTools(server, rt) {
1870
2727
  "get-container-registry-auth",
1871
2728
  "Get one saved container-registry credential by id (the secret/password is not returned).",
1872
2729
  {
1873
- containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to retrieve")
2730
+ containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to retrieve")
1874
2731
  },
1875
2732
  { title: "Get container registry auth", ...READ_ONLY },
1876
2733
  async (params) => {
@@ -1885,9 +2742,9 @@ function registerRegistryTools(server, rt) {
1885
2742
  "create-container-registry-auth",
1886
2743
  "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1887
2744
  {
1888
- name: import_zod8.z.string().describe("Name for the container registry auth"),
1889
- username: import_zod8.z.string().describe("Registry username"),
1890
- password: import_zod8.z.string().describe("Registry password or access token (stored as a secret)")
2745
+ name: import_zod12.z.string().describe("Name for the container registry auth"),
2746
+ username: import_zod12.z.string().describe("Registry username"),
2747
+ password: import_zod12.z.string().describe("Registry password or access token (stored as a secret)")
1891
2748
  },
1892
2749
  { title: "Create container registry auth", ...WRITE },
1893
2750
  async (params) => {
@@ -1905,7 +2762,7 @@ function registerRegistryTools(server, rt) {
1905
2762
  "delete-container-registry-auth",
1906
2763
  "Permanently delete a saved container-registry credential. This cannot be undone.",
1907
2764
  {
1908
- containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to delete")
2765
+ containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to delete")
1909
2766
  },
1910
2767
  { title: "Delete container registry auth", ...DESTRUCTIVE },
1911
2768
  async (params) => {
@@ -1917,166 +2774,69 @@ function registerRegistryTools(server, rt) {
1917
2774
  return jsonReply2(result);
1918
2775
  }
1919
2776
  );
1920
- }
1921
-
1922
- // src/tools/tags.ts
1923
- var import_zod9 = require("zod");
1924
- var TAG_RESOURCE_TYPES = [
1925
- "POD",
1926
- "NETWORK_VOLUME",
1927
- "CLUSTER",
1928
- "SERVERLESS_ENDPOINT"
1929
- ];
1930
- var taggedResourceSchema = import_zod9.z.object({
1931
- id: import_zod9.z.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
1932
- type: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
1933
- });
1934
- function v2OnlyNotice(tool) {
1935
- return {
1936
- error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
1937
- status: 501
1938
- };
1939
- }
1940
- function registerTagTools(server, rt) {
1941
- const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1942
2777
  server.tool(
1943
- "list-tags",
1944
- "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.",
1945
- {
1946
- ...listPaginationParams,
1947
- includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to each tag in the response")
1948
- },
1949
- { title: "List tags", ...READ_ONLY },
2778
+ "list-registry-delegations",
2779
+ "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.",
2780
+ listPaginationParams,
2781
+ { title: "List registry delegations", ...READ_ONLY },
1950
2782
  async (params) => {
1951
- const backend = backendFor("tags");
1952
- if (backend.version === "v1") return jsonReply2(v2OnlyNotice("list-tags"));
1953
- const query = params.includeResources ? "?include=resources" : "";
1954
- const raw = await callRestUrl(`${backend.base}${backend.list}${query}`);
1955
- return capListResult(backend.unwrap(raw), {
2783
+ const backend = backendFor("registries");
2784
+ if (backend.version === "v1") {
2785
+ return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
2786
+ }
2787
+ const raw = await callRestUrl(
2788
+ `${backend.base}${backend.list}${DELEGATIONS}`
2789
+ );
2790
+ const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
2791
+ return capListResult(delegations, {
1956
2792
  limit: params.limit,
1957
2793
  cursor: params.cursor
1958
2794
  });
1959
2795
  }
1960
2796
  );
1961
2797
  server.tool(
1962
- "get-tag",
1963
- "Get one tag by id, optionally including its attached resources. v2-only \u2014 returns a 501 notice on the v1 API.",
1964
- {
1965
- tagId: import_zod9.z.string().describe("ID of the tag to retrieve"),
1966
- includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to this tag")
1967
- },
1968
- { title: "Get tag", ...READ_ONLY },
1969
- async (params) => {
1970
- const backend = backendFor("tags");
1971
- if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
1972
- const query = params.includeResources ? "?include=resources" : "";
1973
- const result = await callRestUrl(
1974
- `${backend.base}${backend.get(params.tagId)}${query}`
1975
- );
1976
- return jsonReply2(result);
1977
- }
1978
- );
1979
- server.tool(
1980
- "create-tag",
1981
- "Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
2798
+ "create-registry-delegation",
2799
+ "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
2800
  {
1983
- key: import_zod9.z.string().min(1).describe('Tag key (e.g. "project")'),
1984
- value: import_zod9.z.string().min(1).describe('Tag value (e.g. "gpt-training")'),
1985
- resources: import_zod9.z.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
2801
+ resource: import_zod12.z.string().min(1).describe(
2802
+ 'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
2803
+ ),
2804
+ name: import_zod12.z.string().optional().describe("Optional human-readable name for the delegation")
1986
2805
  },
1987
- { title: "Create tag", ...WRITE },
2806
+ { title: "Create registry delegation", ...WRITE },
1988
2807
  async (params) => {
1989
- const backend = backendFor("tags");
1990
- if (backend.version === "v1")
1991
- return jsonReply2(v2OnlyNotice("create-tag"));
2808
+ const backend = backendFor("registries");
2809
+ if (backend.version === "v1") {
2810
+ return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
2811
+ }
2812
+ const body = { resource: params.resource };
2813
+ if (params.name !== void 0) body.name = params.name;
1992
2814
  const result = await callRestUrl(
1993
- `${backend.base}${backend.list}`,
2815
+ `${backend.base}${backend.list}${DELEGATIONS}`,
1994
2816
  "POST",
1995
- params
2817
+ body
1996
2818
  );
1997
2819
  return jsonReply2(result);
1998
2820
  }
1999
2821
  );
2000
2822
  server.tool(
2001
- "update-tag",
2002
- "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.",
2823
+ "delete-registry-delegation",
2824
+ "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.",
2003
2825
  {
2004
- tagId: import_zod9.z.string().describe("ID of the tag to update"),
2005
- key: import_zod9.z.string().min(1).optional().describe("New tag key"),
2006
- value: import_zod9.z.string().min(1).optional().describe("New tag value"),
2007
- resources: import_zod9.z.array(taggedResourceSchema).optional().describe(
2008
- "Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
2826
+ delegationId: import_zod12.z.string().describe(
2827
+ "ID of the delegation to revoke (from list-registry-delegations)"
2009
2828
  )
2010
2829
  },
2011
- // No idempotentHint: replacing `resources` is non-atomic upstream, so a
2012
- // retried-after-partial-failure update is not guaranteed to be a no-op.
2013
- { title: "Update tag", ...WRITE },
2014
- async (params) => {
2015
- const { tagId, ...updateParams } = params;
2016
- const backend = backendFor("tags");
2017
- if (backend.version === "v1")
2018
- return jsonReply2(v2OnlyNotice("update-tag"));
2019
- const result = await callRestUrl(
2020
- `${backend.base}${backend.get(tagId)}`,
2021
- "PATCH",
2022
- updateParams
2023
- );
2024
- return jsonReply2(result);
2025
- }
2026
- );
2027
- server.tool(
2028
- "delete-tag",
2029
- "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.",
2030
- {
2031
- tagId: import_zod9.z.string().describe("ID of the tag to delete")
2032
- },
2033
- { title: "Delete tag", ...DESTRUCTIVE },
2034
- async (params) => {
2035
- const backend = backendFor("tags");
2036
- if (backend.version === "v1")
2037
- return jsonReply2(v2OnlyNotice("delete-tag"));
2038
- const result = await callRestUrl(
2039
- `${backend.base}${backend.get(params.tagId)}`,
2040
- "DELETE"
2041
- );
2042
- return jsonReply2(result);
2043
- }
2044
- );
2045
- server.tool(
2046
- "attach-tag",
2047
- "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.",
2048
- {
2049
- tagId: import_zod9.z.string().describe("ID of the tag"),
2050
- resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
2051
- resourceId: import_zod9.z.string().describe("ID of the resource to attach the tag to")
2052
- },
2053
- { title: "Attach tag to resource", ...WRITE, idempotentHint: true },
2054
- async (params) => {
2055
- const backend = backendFor("tags");
2056
- if (backend.version === "v1")
2057
- return jsonReply2(v2OnlyNotice("attach-tag"));
2058
- const result = await callRestUrl(
2059
- `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2060
- "PUT"
2061
- );
2062
- return jsonReply2(result);
2063
- }
2064
- );
2065
- server.tool(
2066
- "detach-tag",
2067
- "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.",
2068
- {
2069
- tagId: import_zod9.z.string().describe("ID of the tag"),
2070
- resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
2071
- resourceId: import_zod9.z.string().describe("ID of the resource to detach the tag from")
2072
- },
2073
- { title: "Detach tag from resource", ...WRITE, idempotentHint: true },
2830
+ { title: "Revoke registry delegation", ...DESTRUCTIVE },
2074
2831
  async (params) => {
2075
- const backend = backendFor("tags");
2076
- if (backend.version === "v1")
2077
- return jsonReply2(v2OnlyNotice("detach-tag"));
2832
+ const backend = backendFor("registries");
2833
+ if (backend.version === "v1") {
2834
+ return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
2835
+ }
2078
2836
  const result = await callRestUrl(
2079
- `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2837
+ `${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
2838
+ params.delegationId
2839
+ )}`,
2080
2840
  "DELETE"
2081
2841
  );
2082
2842
  return jsonReply2(result);
@@ -2085,7 +2845,7 @@ function registerTagTools(server, rt) {
2085
2845
  }
2086
2846
 
2087
2847
  // src/tools/billing.ts
2088
- var import_zod10 = require("zod");
2848
+ var import_zod13 = require("zod");
2089
2849
  var BILLING_SCOPE_PATH = {
2090
2850
  all: "/billing",
2091
2851
  pods: "/billing/pods",
@@ -2101,7 +2861,7 @@ function registerBillingTools(server, rt) {
2101
2861
  "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.",
2102
2862
  {
2103
2863
  ...listPaginationParams,
2104
- scope: import_zod10.z.enum([
2864
+ scope: import_zod13.z.enum([
2105
2865
  "all",
2106
2866
  "pods",
2107
2867
  "serverless",
@@ -2111,14 +2871,14 @@ function registerBillingTools(server, rt) {
2111
2871
  ]).optional().describe(
2112
2872
  'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2113
2873
  ),
2114
- startTime: import_zod10.z.string().optional().describe(
2874
+ startTime: import_zod13.z.string().optional().describe(
2115
2875
  "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2116
2876
  ),
2117
- endTime: import_zod10.z.string().optional().describe(
2877
+ endTime: import_zod13.z.string().optional().describe(
2118
2878
  "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2119
2879
  ),
2120
- bucketSize: import_zod10.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2121
- lastN: import_zod10.z.number().int().positive().optional().describe(
2880
+ bucketSize: import_zod13.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2881
+ lastN: import_zod13.z.number().int().positive().optional().describe(
2122
2882
  "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2123
2883
  )
2124
2884
  },
@@ -2153,16 +2913,119 @@ function registerBillingTools(server, rt) {
2153
2913
  function registerTools(server, ctx, deps = {}) {
2154
2914
  const rt = createToolRuntime(server, ctx, deps);
2155
2915
  registerCatalogTools(server, rt);
2916
+ registerHubTools(server, rt);
2917
+ registerPublicEndpointTools(server, rt);
2156
2918
  registerPodTools(server, rt);
2157
2919
  registerEndpointTools(server, rt);
2920
+ registerEndpointGpuTools(server, rt);
2158
2921
  registerJobTools(server, rt);
2159
2922
  registerTemplateTools(server, rt);
2160
2923
  registerNetworkVolumeTools(server, rt);
2161
2924
  registerRegistryTools(server, rt);
2162
- registerTagTools(server, rt);
2163
2925
  registerBillingTools(server, rt);
2164
2926
  }
2165
2927
 
2928
+ // src/_shared/credential-check.ts
2929
+ var import_node_crypto2 = require("crypto");
2930
+ var VALID_TTL_MS = 6e4;
2931
+ var INVALID_TTL_MS = 3e4;
2932
+ var REQUEST_TIMEOUT_MS = 4e3;
2933
+ var MAX_CACHE_ENTRIES = 1e3;
2934
+ function hashToken(token) {
2935
+ return (0, import_node_crypto2.createHash)("sha256").update(token).digest("hex");
2936
+ }
2937
+ function createCredentialChecker(deps) {
2938
+ const now = deps.now ?? (() => performance.now());
2939
+ const validTtl = deps.validTtlMs ?? VALID_TTL_MS;
2940
+ const invalidTtl = deps.invalidTtlMs ?? INVALID_TTL_MS;
2941
+ const timeoutMs = deps.timeoutMs ?? REQUEST_TIMEOUT_MS;
2942
+ const maxEntries = Math.max(1, deps.maxEntries ?? MAX_CACHE_ENTRIES);
2943
+ const entries = /* @__PURE__ */ new Map();
2944
+ async function fetchVerdict(token) {
2945
+ try {
2946
+ const response = await deps.fetch(deps.url(), {
2947
+ method: "POST",
2948
+ headers: {
2949
+ "Content-Type": "application/json",
2950
+ Authorization: `Bearer ${token}`
2951
+ },
2952
+ body: JSON.stringify({ query: "query { myself { id } }" }),
2953
+ signal: AbortSignal.timeout(timeoutMs)
2954
+ });
2955
+ if (response.status === 401) {
2956
+ return {
2957
+ status: "invalid",
2958
+ reason: "The Runpod API rejected the credential (it may have been revoked)."
2959
+ };
2960
+ }
2961
+ if (!response.ok) return { status: "unknown" };
2962
+ const result = await response.json();
2963
+ if (result?.data?.myself?.id) return { status: "valid" };
2964
+ if (Array.isArray(result?.errors) && result.errors.length > 0) {
2965
+ return { status: "unknown" };
2966
+ }
2967
+ if (result?.data && result.data.myself === null) {
2968
+ return {
2969
+ status: "invalid",
2970
+ reason: "The credential does not resolve to a Runpod account."
2971
+ };
2972
+ }
2973
+ return { status: "unknown" };
2974
+ } catch {
2975
+ return { status: "unknown" };
2976
+ }
2977
+ }
2978
+ function evictIfFull() {
2979
+ while (entries.size >= maxEntries) {
2980
+ let victim;
2981
+ for (const [k, e] of entries) {
2982
+ if (!e.promise) {
2983
+ victim = k;
2984
+ break;
2985
+ }
2986
+ }
2987
+ if (victim === void 0) return;
2988
+ entries.delete(victim);
2989
+ }
2990
+ }
2991
+ async function verify(token) {
2992
+ const key = hashToken(token);
2993
+ const existing = entries.get(key);
2994
+ if (existing?.verdict) {
2995
+ if (existing.expiresAt > now()) {
2996
+ entries.delete(key);
2997
+ entries.set(key, existing);
2998
+ return existing.verdict;
2999
+ }
3000
+ entries.delete(key);
3001
+ } else if (existing?.promise) {
3002
+ return existing.promise;
3003
+ }
3004
+ const entry = { expiresAt: 0 };
3005
+ entry.promise = fetchVerdict(token).then((verdict) => {
3006
+ if (entries.get(key) === entry) {
3007
+ if (verdict.status !== "unknown") {
3008
+ entry.verdict = verdict;
3009
+ entry.expiresAt = now() + (verdict.status === "valid" ? validTtl : invalidTtl);
3010
+ entry.promise = void 0;
3011
+ } else {
3012
+ entries.delete(key);
3013
+ }
3014
+ }
3015
+ return verdict;
3016
+ });
3017
+ evictIfFull();
3018
+ entries.set(key, entry);
3019
+ return entry.promise;
3020
+ }
3021
+ return {
3022
+ verify,
3023
+ invalidate: (token) => {
3024
+ entries.delete(hashToken(token));
3025
+ }
3026
+ };
3027
+ }
3028
+
2166
3029
  // src/http.ts
2167
3030
  function extractBearerToken(req) {
2168
3031
  const authHeader = req.headers.authorization;
@@ -2179,16 +3042,111 @@ function getBaseUrl(req) {
2179
3042
  }
2180
3043
  return `${proto}://${host}`;
2181
3044
  }
2182
- function writeUnauthorized(req, res, error) {
2183
- const headers = {
2184
- "Content-Type": "application/json",
2185
- "WWW-Authenticate": `Bearer realm="mcp", resource_metadata="${getBaseUrl(
3045
+ function writeUnauthorized(req, res, error, invalidToken = false) {
3046
+ const challenge = [
3047
+ 'Bearer realm="mcp"',
3048
+ ...invalidToken ? [
3049
+ 'error="invalid_token"',
3050
+ 'error_description="The credential was rejected; re-authenticate."'
3051
+ ] : [],
3052
+ `resource_metadata="${getBaseUrl(
2186
3053
  req
2187
3054
  )}/.well-known/oauth-protected-resource"`
2188
- };
2189
- res.writeHead(401, headers);
3055
+ ].join(", ");
3056
+ res.writeHead(401, {
3057
+ "Content-Type": "application/json",
3058
+ "WWW-Authenticate": challenge
3059
+ });
2190
3060
  res.end(JSON.stringify({ error }));
2191
3061
  }
3062
+ function authGraphqlUrl(env) {
3063
+ return authedGraphqlBase(env);
3064
+ }
3065
+ var defaultCredentialChecker = createCredentialChecker({
3066
+ fetch: (url, init) => fetch(url, init),
3067
+ url: () => authGraphqlUrl(process.env)
3068
+ });
3069
+ var sameHost = (a, b) => normalizeUrl(a) === normalizeUrl(b);
3070
+ function normalizeUrl(raw) {
3071
+ try {
3072
+ const u = new URL(raw);
3073
+ return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
3074
+ } catch {
3075
+ return raw.trim().toLowerCase().replace(/\/+$/, "");
3076
+ }
3077
+ }
3078
+ function credentialCheckMayBeWrongEnvironment(env = process.env) {
3079
+ const envWithoutEmpties = Object.fromEntries(
3080
+ Object.entries(env).filter(([, v]) => v !== "")
3081
+ );
3082
+ const restMoved = !sameHost(restV1Base(envWithoutEmpties), restV1Base({})) || !sameHost(restV2Base(envWithoutEmpties), restV2Base({})) || !sameHost(serverlessBase(envWithoutEmpties), serverlessBase({}));
3083
+ const graphqlMoved = !sameHost(authGraphqlUrl(env), authGraphqlUrl({}));
3084
+ return restMoved !== graphqlMoved;
3085
+ }
3086
+ function looksLikeMcpMessage(value) {
3087
+ if (typeof value !== "object" || value === null) return false;
3088
+ const msg = value;
3089
+ return msg.jsonrpc === "2.0" && typeof msg.method === "string";
3090
+ }
3091
+ function bodyIsCheckable(body) {
3092
+ const messages = Array.isArray(body) ? body : [body];
3093
+ return messages.some(looksLikeMcpMessage);
3094
+ }
3095
+ function isJsonContentType2(req) {
3096
+ const ct = req.headers["content-type"];
3097
+ if (!ct) return false;
3098
+ return ct.toLowerCase().includes("application/json");
3099
+ }
3100
+ var MAX_BODY_BYTES = 4 * 1024 * 1024;
3101
+ var BODY_TOO_LARGE = Symbol("body_too_large");
3102
+ async function readJsonBody(req) {
3103
+ const existing = req.body;
3104
+ if (existing !== void 0) return existing;
3105
+ const declared = Number(req.headers["content-length"]);
3106
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
3107
+ return BODY_TOO_LARGE;
3108
+ }
3109
+ try {
3110
+ const chunks = [];
3111
+ let total = 0;
3112
+ for await (const chunk of req) {
3113
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3114
+ total += buf.length;
3115
+ if (total > MAX_BODY_BYTES) return BODY_TOO_LARGE;
3116
+ chunks.push(buf);
3117
+ }
3118
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
3119
+ return raw ? JSON.parse(raw) : void 0;
3120
+ } catch {
3121
+ return void 0;
3122
+ }
3123
+ }
3124
+ function earlyCredentialCheckSkip(req) {
3125
+ if (process.env.MCP_SKIP_CREDENTIAL_CHECK === "true") return "skip_env_var";
3126
+ if (req.method !== "POST") return "skip_not_post";
3127
+ if (!isJsonContentType2(req)) return "skip_not_json";
3128
+ if (credentialCheckMayBeWrongEnvironment()) return "skip_env_mismatch";
3129
+ return null;
3130
+ }
3131
+ function buildToolContext(req, bearerToken, opts = {}) {
3132
+ const drop = opts.invalidateCredential ?? defaultCredentialChecker.invalidate;
3133
+ return {
3134
+ apiKey: bearerToken,
3135
+ transport: "http",
3136
+ // A tools/call is a separate request from `initialize` in stateless HTTP, so
3137
+ // the per-request server never sees the MCP `clientInfo`; fall back to the
3138
+ // inbound User-Agent so caller-tracking still attributes the client.
3139
+ clientUserAgent: req.headers["user-agent"],
3140
+ serverVersion: opts.serverVersion,
3141
+ // A tool call that 401s proves a cached "valid" wrong before its TTL. Drop it
3142
+ // so the next request re-checks and can emit the 401 that triggers re-auth.
3143
+ onUnauthorized: () => drop(bearerToken)
3144
+ };
3145
+ }
3146
+ function logCredentialCheck(outcome) {
3147
+ if (outcome === "pass" && process.env.MCP_VERBOSE_LOGS !== "true") return;
3148
+ console.log("credential_check", { outcome });
3149
+ }
2192
3150
  async function handleMcpRequest(req, res, opts = {}) {
2193
3151
  const bearerToken = extractBearerToken(req);
2194
3152
  console.log("mcp_request", {
@@ -2204,13 +3162,39 @@ async function handleMcpRequest(req, res, opts = {}) {
2204
3162
  );
2205
3163
  return;
2206
3164
  }
3165
+ let body = req.body;
3166
+ const early = earlyCredentialCheckSkip(req);
3167
+ if (early) {
3168
+ logCredentialCheck(early);
3169
+ } else {
3170
+ const read = await readJsonBody(req);
3171
+ if (read === BODY_TOO_LARGE) {
3172
+ res.writeHead(413, { "Content-Type": "application/json" });
3173
+ res.end(JSON.stringify({ error: "Request body too large." }));
3174
+ return;
3175
+ }
3176
+ body = read;
3177
+ if (!bodyIsCheckable(body)) {
3178
+ logCredentialCheck("skip_not_mcp_body");
3179
+ } else {
3180
+ const verify = opts.verifyCredential ?? defaultCredentialChecker.verify;
3181
+ const verdict = await verify(bearerToken);
3182
+ logCredentialCheck(
3183
+ verdict.status === "invalid" ? "reject" : verdict.status === "valid" ? "pass" : "fail_open"
3184
+ );
3185
+ if (verdict.status === "invalid") {
3186
+ writeUnauthorized(
3187
+ req,
3188
+ res,
3189
+ `${verdict.reason} Re-authenticate to continue.`,
3190
+ true
3191
+ );
3192
+ return;
3193
+ }
3194
+ }
3195
+ }
2207
3196
  const server = createServer(opts.serverVersion);
2208
- registerTools(server, {
2209
- apiKey: bearerToken,
2210
- transport: "http",
2211
- clientUserAgent: req.headers["user-agent"],
2212
- serverVersion: opts.serverVersion
2213
- });
3197
+ registerTools(server, buildToolContext(req, bearerToken, opts));
2214
3198
  const transport = new import_streamableHttp.StreamableHTTPServerTransport({
2215
3199
  sessionIdGenerator: void 0
2216
3200
  // stateless — no session persistence
@@ -2222,10 +3206,13 @@ async function handleMcpRequest(req, res, opts = {}) {
2222
3206
  });
2223
3207
  });
2224
3208
  await server.connect(transport);
2225
- await transport.handleRequest(req, res, req.body);
3209
+ await transport.handleRequest(req, res, body);
2226
3210
  }
2227
3211
  // Annotate the CommonJS export names for ESM import in node:
2228
3212
  0 && (module.exports = {
3213
+ bodyIsCheckable,
3214
+ buildToolContext,
3215
+ defaultCredentialChecker,
2229
3216
  handleMcpRequest,
2230
3217
  registerTools
2231
3218
  });