@runpod/mcp-server 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.cjs CHANGED
@@ -33,31 +33,416 @@ __export(tools_exports, {
33
33
  registerTools: () => registerTools
34
34
  });
35
35
  module.exports = __toCommonJS(tools_exports);
36
- var import_zod = require("zod");
36
+
37
+ // src/tools/runtime.ts
37
38
  var import_node_fetch = __toESM(require("node-fetch"), 1);
38
39
  var import_node_crypto = require("crypto");
39
- var API_BASE_URL = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
40
- var SERVERLESS_API_BASE_URL = process.env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
41
- var GRAPHQL_URL = process.env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
42
- var MCP_SERVER_VERSION = true ? "1.3.0" : "dev";
43
- var SESSION_ID = (0, import_node_crypto.randomUUID)();
40
+
41
+ // src/_shared/http.ts
42
+ var HttpError = class extends Error {
43
+ constructor(prefix, status, body) {
44
+ super(`${prefix}: ${status} - ${body}`);
45
+ this.name = "HttpError";
46
+ this.status = status;
47
+ this.body = body;
48
+ }
49
+ };
50
+ function isJsonContentType(contentType) {
51
+ if (!contentType) return false;
52
+ const ct = contentType.toLowerCase();
53
+ return ct.includes("application/json") || ct.includes("+json");
54
+ }
55
+ function methodSendsBody(method) {
56
+ return method === "POST" || method === "PATCH" || method === "PUT";
57
+ }
58
+ function buildRequestHeaders(apiKey, tracking) {
59
+ return {
60
+ Authorization: `Bearer ${apiKey}`,
61
+ "Content-Type": "application/json",
62
+ ...tracking()
63
+ };
64
+ }
65
+ function createHttpClient(deps) {
66
+ return async function request(url, method = "GET", body) {
67
+ const init = {
68
+ method,
69
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
70
+ };
71
+ if (body && methodSendsBody(method)) {
72
+ init.body = JSON.stringify(body);
73
+ }
74
+ const response = await deps.fetch(url, init);
75
+ if (!response.ok) {
76
+ throw new HttpError(
77
+ deps.errorPrefix,
78
+ response.status,
79
+ await response.text()
80
+ );
81
+ }
82
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
83
+ };
84
+ }
85
+
86
+ // src/_shared/tracking.ts
44
87
  function sanitizeUaToken(value) {
45
88
  return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
46
89
  }
90
+ function buildTrackingHeaders(input) {
91
+ const name = sanitizeUaToken(input.clientName || "unknown");
92
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
93
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
94
+ return {
95
+ "User-Agent": userAgent,
96
+ "X-Runpod-Session-Id": input.sessionId
97
+ };
98
+ }
99
+
100
+ // src/_shared/mappers.ts
101
+ function compact(obj) {
102
+ const out = {};
103
+ for (const [k, v] of Object.entries(obj)) {
104
+ if (v !== void 0) out[k] = v;
105
+ }
106
+ return out;
107
+ }
108
+ function persistentMount(size, path) {
109
+ if (size === void 0 || path === void 0) return void 0;
110
+ return { persistent: { size, path } };
111
+ }
112
+ function containerConfigToV2(p) {
113
+ return compact({
114
+ image: p.imageName,
115
+ disk: p.containerDiskInGb,
116
+ ports: p.ports,
117
+ env: p.env,
118
+ mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
119
+ });
120
+ }
121
+ function gpuConfigToV2(gpuTypeIds, gpuCount) {
122
+ const id = gpuTypeIds?.[0];
123
+ if (id === void 0) return void 0;
124
+ return compact({ id, count: gpuCount });
125
+ }
126
+ function mapPodCreateToV2(params) {
127
+ return compact({
128
+ ...containerConfigToV2(params),
129
+ name: params.name,
130
+ cloud: params.cloudType,
131
+ // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
132
+ // centers; omit/empty = scheduler chooses). Pass the v1 list straight
133
+ // through — the earlier singular `dataCenter` key did not exist on the pod
134
+ // schema, so placement was being silently dropped. GPU, by contrast, IS
135
+ // singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
136
+ // create-pod handler warns when more than one gpuTypeId was supplied.
137
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
138
+ gpu: gpuConfigToV2(params.gpuTypeIds, params.gpuCount)
139
+ });
140
+ }
141
+ function mapPodUpdateToV2(params) {
142
+ return {
143
+ ...containerConfigToV2(params),
144
+ ...compact({ name: params.name })
145
+ };
146
+ }
147
+ function nestedOrUndefined(obj) {
148
+ const c = compact(obj);
149
+ return Object.keys(c).length ? c : void 0;
150
+ }
151
+ function endpointGpuConfig(pools, count) {
152
+ if (!pools?.length) return void 0;
153
+ return compact({ pools, count });
154
+ }
155
+ function mapEndpointToV2(params) {
156
+ return compact({
157
+ name: params.name,
158
+ image: params.imageName,
159
+ args: params.args,
160
+ disk: params.containerDiskInGb,
161
+ ports: params.ports,
162
+ env: params.env,
163
+ registry: params.containerRegistryAuthId,
164
+ gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
165
+ workers: nestedOrUndefined({
166
+ min: params.workersMin,
167
+ max: params.workersMax
168
+ }),
169
+ scaling: nestedOrUndefined({
170
+ type: params.scalerType,
171
+ value: params.scalerValue,
172
+ idleTimeout: params.idleTimeout
173
+ }),
174
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
175
+ networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
176
+ timeout: params.executionTimeoutMs,
177
+ flashboot: params.flashboot
178
+ });
179
+ }
180
+ var mapEndpointCreateToV2 = mapEndpointToV2;
181
+ var mapEndpointUpdateToV2 = mapEndpointToV2;
182
+ function mapNetworkVolumeCreateToV2(params) {
183
+ return compact({
184
+ name: params.name,
185
+ size: params.size,
186
+ dataCenter: params.dataCenterId
187
+ });
188
+ }
189
+ function mapTemplateCreateToV2(params) {
190
+ return {
191
+ ...containerConfigToV2(params),
192
+ ...compact({
193
+ name: params.name,
194
+ serverless: params.isServerless,
195
+ registry: params.containerRegistryAuthId
196
+ }),
197
+ // Required by v2 — always emit, defaulting to NVIDIA.
198
+ category: params.category ?? "NVIDIA"
199
+ };
200
+ }
201
+ function mapTemplateUpdateToV2(params) {
202
+ return {
203
+ ...containerConfigToV2(params),
204
+ ...compact({
205
+ name: params.name,
206
+ registry: params.containerRegistryAuthId
207
+ })
208
+ };
209
+ }
210
+
211
+ // src/_shared/backend.ts
212
+ var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
213
+ var V2_UNWRAP_KEY = {
214
+ pods: "pods",
215
+ templates: "templates",
216
+ networkVolumes: "networkVolumes",
217
+ registries: "registries",
218
+ gpus: "gpus",
219
+ cpus: "cpus",
220
+ dataCenters: "dataCenters",
221
+ endpoints: "endpoints",
222
+ jobs: "jobs",
223
+ tags: "tags",
224
+ workers: "workers",
225
+ billing: "records"
226
+ };
227
+ var RESOURCE_ENV_KEY = {
228
+ pods: "PODS",
229
+ templates: "TEMPLATES",
230
+ networkVolumes: "NETWORK_VOLUMES",
231
+ registries: "REGISTRIES",
232
+ gpus: "GPUS",
233
+ cpus: "CPUS",
234
+ dataCenters: "DATA_CENTERS",
235
+ endpoints: "ENDPOINTS",
236
+ jobs: "JOBS",
237
+ tags: "TAGS",
238
+ workers: "WORKERS",
239
+ billing: "BILLING"
240
+ };
241
+ function restV1Base(env) {
242
+ return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
243
+ }
244
+ function restV2Base(env) {
245
+ return env.RUNPOD_REST_V2_API_URL ?? "https://v2-rest.runpod.io/v2";
246
+ }
247
+ function serverlessBase(env) {
248
+ return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
249
+ }
250
+ function publicGraphqlBase(env) {
251
+ return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
252
+ }
253
+ function unwrapList(resource, version, raw) {
254
+ if (version === "v1") return Array.isArray(raw) ? raw : [];
255
+ if (raw === null || typeof raw !== "object") return [];
256
+ const arr = raw[V2_UNWRAP_KEY[resource]];
257
+ return Array.isArray(arr) ? arr : [];
258
+ }
259
+ function resolveVersion(opts) {
260
+ const { resource, env, ctx, v2Available } = opts;
261
+ if (V1_ONLY.has(resource)) return "v1";
262
+ const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
263
+ const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
264
+ if (setting === "v2") return "v2";
265
+ if (setting === "auto") {
266
+ if (ctx.transport === "http") return "v1";
267
+ return v2Available === true ? "v2" : "v1";
268
+ }
269
+ return "v1";
270
+ }
271
+ var identity = (body) => body;
272
+ var V1_REST_PATHS = {
273
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
274
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
275
+ networkVolumes: {
276
+ list: "/networkvolumes",
277
+ get: (id) => `/networkvolumes/${id}`
278
+ },
279
+ registries: {
280
+ list: "/containerregistryauth",
281
+ get: (id) => `/containerregistryauth/${id}`
282
+ },
283
+ endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
284
+ };
285
+ var CATALOG = /* @__PURE__ */ new Set([
286
+ "gpus",
287
+ "cpus",
288
+ "dataCenters"
289
+ ]);
290
+ var V2_ONLY = /* @__PURE__ */ new Set([
291
+ "tags",
292
+ "workers",
293
+ "billing"
294
+ ]);
295
+ var V2_REST_PATHS = {
296
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
297
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
298
+ networkVolumes: {
299
+ list: "/network-volumes",
300
+ get: (id) => `/network-volumes/${id}`
301
+ },
302
+ registries: { list: "/registries", get: (id) => `/registries/${id}` },
303
+ gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
304
+ cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
305
+ dataCenters: {
306
+ list: "/catalog/datacenters",
307
+ get: (id) => `/catalog/datacenters/${id}`
308
+ },
309
+ tags: { list: "/tags", get: (id) => `/tags/${id}` },
310
+ // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
311
+ // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
312
+ // unwrap key stays correct.
313
+ endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
314
+ // workers/billing build their own sub-paths in the tool from `base`; the
315
+ // list entry just satisfies buildV2Backend (which requires a paths entry).
316
+ workers: { list: "/serverless" },
317
+ billing: { list: "/billing" }
318
+ };
319
+ var V2_MAPPERS = {
320
+ pods: {
321
+ create: (b) => mapPodCreateToV2(b),
322
+ update: (b) => mapPodUpdateToV2(b)
323
+ },
324
+ templates: {
325
+ create: (b) => mapTemplateCreateToV2(b),
326
+ update: (b) => mapTemplateUpdateToV2(b)
327
+ },
328
+ networkVolumes: {
329
+ create: (b) => mapNetworkVolumeCreateToV2(
330
+ b
331
+ ),
332
+ update: identity
333
+ },
334
+ registries: { create: identity, update: identity },
335
+ endpoints: {
336
+ create: (b) => mapEndpointCreateToV2(b),
337
+ update: (b) => mapEndpointUpdateToV2(b)
338
+ }
339
+ };
340
+ function buildV2Backend(resource, env, unwrap) {
341
+ const paths = V2_REST_PATHS[resource];
342
+ if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
343
+ const mappers = V2_MAPPERS[resource];
344
+ return {
345
+ kind: "rest",
346
+ version: "v2",
347
+ base: restV2Base(env),
348
+ list: paths.list,
349
+ get: paths.get,
350
+ unwrap,
351
+ mapCreate: mappers?.create ?? identity,
352
+ mapUpdate: mappers?.update ?? identity
353
+ };
354
+ }
355
+ function buildV1Backend(resource, env, unwrap) {
356
+ if (resource === "jobs") {
357
+ return {
358
+ kind: "rest",
359
+ version: "v1",
360
+ base: serverlessBase(env),
361
+ unwrap,
362
+ mapCreate: identity,
363
+ mapUpdate: identity
364
+ };
365
+ }
366
+ if (V2_ONLY.has(resource)) {
367
+ return {
368
+ kind: "rest",
369
+ version: "v1",
370
+ base: restV1Base(env),
371
+ unwrap,
372
+ mapCreate: identity,
373
+ mapUpdate: identity
374
+ };
375
+ }
376
+ if (CATALOG.has(resource)) {
377
+ return {
378
+ kind: "graphql",
379
+ version: "v1",
380
+ base: publicGraphqlBase(env),
381
+ unwrap,
382
+ mapCreate: identity,
383
+ mapUpdate: identity
384
+ };
385
+ }
386
+ const paths = V1_REST_PATHS[resource];
387
+ if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
388
+ return {
389
+ kind: "rest",
390
+ version: "v1",
391
+ base: restV1Base(env),
392
+ list: paths.list,
393
+ get: paths.get,
394
+ unwrap,
395
+ mapCreate: identity,
396
+ mapUpdate: identity
397
+ };
398
+ }
399
+ function resolveBackend(opts) {
400
+ const { resource, env } = opts;
401
+ const version = resolveVersion(opts);
402
+ const unwrap = (raw) => unwrapList(resource, version, raw);
403
+ return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
404
+ }
405
+
406
+ // src/tools/runtime.ts
407
+ var MCP_SERVER_VERSION = true ? "2.0.0" : "dev";
408
+ var SESSION_ID = (0, import_node_crypto.randomUUID)();
47
409
  function trackingHeaders(server, ctx) {
48
410
  const info = server.server.getClientVersion();
49
- const fallbackName = ctx.clientUserAgent || "unknown";
50
- const name = sanitizeUaToken(info?.name || fallbackName);
51
- const version = sanitizeUaToken(info?.version || "unknown");
52
- const serverVersion = ctx.serverVersion || MCP_SERVER_VERSION;
53
- const userAgent = `runpod-mcp-server/${serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${ctx.transport})`;
411
+ return buildTrackingHeaders({
412
+ clientName: info?.name || ctx.clientUserAgent || "unknown",
413
+ clientVersion: info?.version || "unknown",
414
+ transport: ctx.transport,
415
+ // Prefer a server version supplied by the caller (the hosted entrypoint reads
416
+ // it from package.json at runtime, since tsup's build-time define does not
417
+ // run when Vercel compiles api/index.ts). Fall back to the build-time value.
418
+ serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
419
+ sessionId: SESSION_ID
420
+ });
421
+ }
422
+ var defaultFetch = import_node_fetch.default;
423
+ async function withApiErrorLog(label, call) {
424
+ try {
425
+ return await call();
426
+ } catch (error) {
427
+ console.error(label, error);
428
+ throw error;
429
+ }
430
+ }
431
+ function jsonReply(result) {
54
432
  return {
55
- "User-Agent": userAgent,
56
- "X-Runpod-Session-Id": SESSION_ID
433
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
57
434
  };
58
435
  }
59
- async function graphqlRequest(query, tracking) {
60
- const response = await (0, import_node_fetch.default)(GRAPHQL_URL, {
436
+ var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
437
+ var WRITE = { readOnlyHint: false, openWorldHint: true };
438
+ var DESTRUCTIVE = {
439
+ readOnlyHint: false,
440
+ destructiveHint: true,
441
+ idempotentHint: true,
442
+ openWorldHint: true
443
+ };
444
+ async function graphqlRequest(query, tracking, fetchImpl, url) {
445
+ const response = await fetchImpl(url, {
61
446
  method: "POST",
62
447
  headers: {
63
448
  "Content-Type": "application/json",
@@ -76,92 +461,224 @@ async function graphqlRequest(query, tracking) {
76
461
  }
77
462
  return result.data;
78
463
  }
79
- function createRunpodRequest(apiKey, tracking) {
80
- return async function runpodRequest(endpoint, method = "GET", body) {
81
- const url = `${API_BASE_URL}${endpoint}`;
82
- const headers = {
83
- Authorization: `Bearer ${apiKey}`,
84
- "Content-Type": "application/json",
85
- ...tracking()
86
- };
87
- const options = {
464
+ function createToolRuntime(server, ctx, deps = {}) {
465
+ const tracking = () => trackingHeaders(server, ctx);
466
+ const httpFetch = deps.fetch ?? defaultFetch;
467
+ const v1Client = createHttpClient({
468
+ apiKey: ctx.apiKey,
469
+ fetch: httpFetch,
470
+ tracking,
471
+ errorPrefix: "Runpod API Error"
472
+ });
473
+ const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
474
+ "Error calling Runpod API:",
475
+ () => v1Client(`${restV1Base(process.env)}${endpoint}`, method, body)
476
+ );
477
+ const serverlessClient = createHttpClient({
478
+ apiKey: ctx.apiKey,
479
+ fetch: httpFetch,
480
+ tracking,
481
+ errorPrefix: "Runpod Serverless API Error"
482
+ });
483
+ const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
484
+ "Error calling Runpod Serverless API:",
485
+ () => serverlessClient(
486
+ `${serverlessBase(process.env)}/${endpointId}${path}`,
88
487
  method,
89
- headers
90
- };
91
- if (body && (method === "POST" || method === "PATCH")) {
92
- options.body = JSON.stringify(body);
93
- }
488
+ body
489
+ )
490
+ );
491
+ const restClient = createHttpClient({
492
+ apiKey: ctx.apiKey,
493
+ fetch: httpFetch,
494
+ tracking,
495
+ errorPrefix: "Runpod API Error"
496
+ });
497
+ const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
498
+ "Error calling Runpod API:",
499
+ () => restClient(url, method, body)
500
+ );
501
+ const streamSse = deps.streamSse ?? (async (url, opts) => {
502
+ const controller = new AbortController();
503
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
94
504
  try {
95
- const response = await (0, import_node_fetch.default)(url, options);
96
- if (!response.ok) {
97
- const errorText = await response.text();
98
- throw new Error(`Runpod API Error: ${response.status} - ${errorText}`);
505
+ const res = await (0, import_node_fetch.default)(url, {
506
+ method: "GET",
507
+ headers: {
508
+ Authorization: `Bearer ${ctx.apiKey}`,
509
+ Accept: "text/event-stream",
510
+ ...tracking()
511
+ },
512
+ signal: controller.signal
513
+ });
514
+ if (!res.ok) {
515
+ throw new HttpError(
516
+ "Runpod API Error",
517
+ res.status,
518
+ await res.text().catch(() => "")
519
+ );
99
520
  }
100
- const contentType = response.headers.get("content-type");
101
- if (contentType && contentType.includes("application/json")) {
102
- return await response.json();
521
+ const chunks = [];
522
+ let bytes = 0;
523
+ let truncated = false;
524
+ try {
525
+ for await (const chunk of res.body) {
526
+ chunks.push(chunk);
527
+ bytes += chunk.length;
528
+ if (bytes >= opts.maxBytes) {
529
+ truncated = true;
530
+ controller.abort();
531
+ break;
532
+ }
533
+ }
534
+ } catch (err) {
535
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
103
536
  }
104
- return { success: true, status: response.status };
105
- } catch (error) {
106
- console.error("Error calling Runpod API:", error);
107
- throw error;
537
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
538
+ } finally {
539
+ clearTimeout(timer);
108
540
  }
541
+ });
542
+ const backendFor = (resource) => resolveBackend({
543
+ resource,
544
+ env: process.env,
545
+ ctx: { transport: ctx.transport },
546
+ v2Available: deps.v2Available
547
+ });
548
+ const podAction = async (podId, action) => {
549
+ const backend = backendFor("pods");
550
+ const podPath = backend.get(podId);
551
+ if (backend.version === "v2") {
552
+ return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
553
+ action
554
+ });
555
+ }
556
+ return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
557
+ };
558
+ return {
559
+ jsonReply,
560
+ graphql: (query) => graphqlRequest(
561
+ query,
562
+ tracking,
563
+ httpFetch,
564
+ publicGraphqlBase(process.env)
565
+ ),
566
+ runpodRequest,
567
+ serverlessRequest,
568
+ callRestUrl,
569
+ backendFor,
570
+ streamSse,
571
+ podAction,
572
+ env: process.env
109
573
  };
110
574
  }
111
- function createServerlessRequest(apiKey, tracking) {
112
- return async function serverlessRequest(endpointId, path, method = "GET", body) {
113
- const url = `${SERVERLESS_API_BASE_URL}/${endpointId}${path}`;
114
- const headers = {
115
- Authorization: `Bearer ${apiKey}`,
116
- "Content-Type": "application/json",
117
- ...tracking()
118
- };
119
- const options = {
120
- method,
121
- headers
575
+
576
+ // src/tools/catalog.ts
577
+ var import_zod2 = require("zod");
578
+
579
+ // src/pagination.ts
580
+ var import_zod = require("zod");
581
+ var DEFAULT_LIST_LIMIT = 20;
582
+ var MAX_LIST_LIMIT = 100;
583
+ var listPaginationParams = {
584
+ limit: import_zod.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
585
+ `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
586
+ ),
587
+ cursor: import_zod.z.string().optional().describe(
588
+ "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
589
+ )
590
+ };
591
+ function decodeCursorOffset(cursor) {
592
+ if (!cursor) return 0;
593
+ try {
594
+ const decoded = Buffer.from(cursor, "base64").toString("utf8");
595
+ const offset = Number.parseInt(decoded, 10);
596
+ return Number.isFinite(offset) && offset >= 0 ? offset : 0;
597
+ } catch {
598
+ return 0;
599
+ }
600
+ }
601
+ function encodeCursorOffset(offset) {
602
+ return Buffer.from(String(offset), "utf8").toString("base64");
603
+ }
604
+ function capListResult(result, options, extra) {
605
+ const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
606
+ const offset = decodeCursorOffset(options.cursor);
607
+ if (!Array.isArray(result)) {
608
+ return {
609
+ content: [
610
+ { type: "text", text: JSON.stringify(result, null, 2) }
611
+ ]
122
612
  };
123
- if (body && (method === "POST" || method === "PATCH")) {
124
- options.body = JSON.stringify(body);
125
- }
126
- try {
127
- const response = await (0, import_node_fetch.default)(url, options);
128
- if (!response.ok) {
129
- const errorText = await response.text();
130
- throw new Error(
131
- `Runpod Serverless API Error: ${response.status} - ${errorText}`
132
- );
133
- }
134
- const contentType = response.headers.get("content-type");
135
- if (contentType && contentType.includes("application/json")) {
136
- return await response.json();
137
- }
138
- return { success: true, status: response.status };
139
- } catch (error) {
140
- console.error("Error calling Runpod Serverless API:", error);
141
- throw error;
613
+ }
614
+ const total = result.length;
615
+ const page = result.slice(offset, offset + limit);
616
+ const nextOffset = offset + page.length;
617
+ const hasMore = nextOffset < total;
618
+ const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
619
+ void _i;
620
+ void _p;
621
+ const envelope = {
622
+ ...safeExtra,
623
+ items: page,
624
+ pagination: {
625
+ total,
626
+ returned: page.length,
627
+ offset,
628
+ truncated: hasMore,
629
+ nextCursor: hasMore ? encodeCursorOffset(nextOffset) : null,
630
+ note: hasMore ? `Showing ${page.length} of ${total}. Pass cursor=nextCursor to fetch more, or narrow the query with filters.` : void 0
142
631
  }
143
632
  };
633
+ return {
634
+ content: [
635
+ { type: "text", text: JSON.stringify(envelope, null, 2) }
636
+ ]
637
+ };
144
638
  }
145
- function registerTools(server, ctx) {
146
- const tracking = () => trackingHeaders(server, ctx);
147
- const runpodRequest = createRunpodRequest(ctx.apiKey, tracking);
148
- const serverlessRequest = createServerlessRequest(ctx.apiKey, tracking);
639
+
640
+ // src/tools/catalog.ts
641
+ function registerCatalogTools(server, rt) {
642
+ const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
149
643
  server.tool(
150
644
  "list-gpu-types",
645
+ "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.",
151
646
  {
152
- minMemoryGb: import_zod.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
153
- secureCloudOnly: import_zod.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
154
- communityCloudOnly: import_zod.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
155
- searchTerm: import_zod.z.string().optional().describe(
647
+ ...listPaginationParams,
648
+ minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
649
+ secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
650
+ communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
651
+ searchTerm: import_zod2.z.string().optional().describe(
156
652
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
157
653
  ),
158
- includeUnavailable: import_zod.z.boolean().optional().describe(
654
+ includeUnavailable: import_zod2.z.boolean().optional().describe(
159
655
  "Include GPUs that are currently out of stock. Default is false"
160
656
  )
161
657
  },
658
+ { title: "List GPU types", ...READ_ONLY },
162
659
  async (params) => {
163
- const data = await graphqlRequest(
164
- `
660
+ const backend = backendFor("gpus");
661
+ if (backend.version === "v2") {
662
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
663
+ let gpus = backend.unwrap(raw);
664
+ if (params.minMemoryGb !== void 0)
665
+ gpus = gpus.filter(
666
+ (g) => Number(g.memory ?? 0) >= params.minMemoryGb
667
+ );
668
+ if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
669
+ if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
670
+ if (params.searchTerm) {
671
+ const t = params.searchTerm.toLowerCase();
672
+ gpus = gpus.filter(
673
+ (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
674
+ );
675
+ }
676
+ return capListResult(gpus, {
677
+ limit: params.limit,
678
+ cursor: params.cursor
679
+ });
680
+ }
681
+ const data = await graphql(`
165
682
  query {
166
683
  gpuTypes {
167
684
  id
@@ -174,9 +691,7 @@ function registerTools(server, ctx) {
174
691
  }
175
692
  }
176
693
  }
177
- `,
178
- tracking
179
- );
694
+ `);
180
695
  const stockPriority = {
181
696
  High: 3,
182
697
  Medium: 2,
@@ -222,26 +737,39 @@ function registerTools(server, ctx) {
222
737
  stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
223
738
  available: isAvailable(gpu)
224
739
  }));
225
- return {
226
- content: [
227
- {
228
- type: "text",
229
- text: JSON.stringify(result, null, 2)
230
- }
231
- ]
232
- };
740
+ return capListResult(result, {
741
+ limit: params.limit,
742
+ cursor: params.cursor
743
+ });
233
744
  }
234
745
  );
235
746
  server.tool(
236
747
  "list-data-centers",
748
+ "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
237
749
  {
238
- region: import_zod.z.string().optional().describe(
750
+ ...listPaginationParams,
751
+ region: import_zod2.z.string().optional().describe(
239
752
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
240
753
  )
241
754
  },
755
+ { title: "List data centers", ...READ_ONLY },
242
756
  async (params) => {
243
- const data = await graphqlRequest(
244
- `
757
+ const backend = backendFor("dataCenters");
758
+ if (backend.version === "v2") {
759
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
760
+ let dcs = backend.unwrap(raw);
761
+ if (params.region) {
762
+ const t = params.region.toLowerCase();
763
+ dcs = dcs.filter(
764
+ (dc) => String(dc.region ?? "").toLowerCase().includes(t)
765
+ );
766
+ }
767
+ return capListResult(dcs, {
768
+ limit: params.limit,
769
+ cursor: params.cursor
770
+ });
771
+ }
772
+ const data = await graphql(`
245
773
  query {
246
774
  dataCenters {
247
775
  id
@@ -249,9 +777,7 @@ function registerTools(server, ctx) {
249
777
  location
250
778
  }
251
779
  }
252
- `,
253
- tracking
254
- );
780
+ `);
255
781
  let dataCenters = data.dataCenters;
256
782
  if (params.region) {
257
783
  const term = params.region.toLowerCase();
@@ -259,341 +785,627 @@ function registerTools(server, ctx) {
259
785
  (dc) => dc.location.toLowerCase().includes(term)
260
786
  );
261
787
  }
262
- return {
263
- content: [
264
- {
265
- type: "text",
266
- text: JSON.stringify(dataCenters, null, 2)
267
- }
268
- ]
269
- };
788
+ return capListResult(dataCenters, {
789
+ limit: params.limit,
790
+ cursor: params.cursor
791
+ });
792
+ }
793
+ );
794
+ server.tool(
795
+ "list-cpu-types",
796
+ "List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
797
+ { ...listPaginationParams },
798
+ { title: "List CPU types", ...READ_ONLY },
799
+ async (params) => {
800
+ const backend = backendFor("cpus");
801
+ if (backend.version === "v1") {
802
+ return jsonReply2({
803
+ error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
804
+ status: 501
805
+ });
806
+ }
807
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
808
+ return capListResult(backend.unwrap(raw), {
809
+ limit: params.limit,
810
+ cursor: params.cursor
811
+ });
812
+ }
813
+ );
814
+ server.tool(
815
+ "get-gpu-type",
816
+ "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).",
817
+ {
818
+ gpuTypeId: import_zod2.z.string().describe("ID of the GPU type to retrieve")
819
+ },
820
+ { title: "Get GPU type", ...READ_ONLY },
821
+ async (params) => {
822
+ const backend = backendFor("gpus");
823
+ if (backend.version === "v1") {
824
+ return jsonReply2({
825
+ error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
826
+ status: 501
827
+ });
828
+ }
829
+ const result = await callRestUrl(
830
+ `${backend.base}${backend.get(params.gpuTypeId)}`
831
+ );
832
+ return jsonReply2(result);
833
+ }
834
+ );
835
+ server.tool(
836
+ "get-cpu-type",
837
+ "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).",
838
+ {
839
+ cpuTypeId: import_zod2.z.string().describe("ID of the CPU type to retrieve")
840
+ },
841
+ { title: "Get CPU type", ...READ_ONLY },
842
+ async (params) => {
843
+ const backend = backendFor("cpus");
844
+ if (backend.version === "v1") {
845
+ return jsonReply2({
846
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
847
+ status: 501
848
+ });
849
+ }
850
+ const result = await callRestUrl(
851
+ `${backend.base}${backend.get(params.cpuTypeId)}`
852
+ );
853
+ return jsonReply2(result);
854
+ }
855
+ );
856
+ server.tool(
857
+ "get-data-center",
858
+ "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).",
859
+ {
860
+ dataCenterId: import_zod2.z.string().describe("ID of the data center to retrieve")
861
+ },
862
+ { title: "Get data center", ...READ_ONLY },
863
+ async (params) => {
864
+ const backend = backendFor("dataCenters");
865
+ if (backend.version === "v1") {
866
+ return jsonReply2({
867
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
868
+ status: 501
869
+ });
870
+ }
871
+ const result = await callRestUrl(
872
+ `${backend.base}${backend.get(params.dataCenterId)}`
873
+ );
874
+ return jsonReply2(result);
270
875
  }
271
876
  );
877
+ }
878
+
879
+ // src/tools/pods.ts
880
+ var import_zod3 = require("zod");
881
+ function registerPodTools(server, rt) {
882
+ const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
272
883
  server.tool(
273
884
  "list-pods",
885
+ "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.",
274
886
  {
275
- computeType: import_zod.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
276
- gpuTypeId: import_zod.z.array(import_zod.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
277
- dataCenterId: import_zod.z.array(import_zod.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
278
- name: import_zod.z.string().optional().describe("Filter to Pods with the provided name"),
279
- includeMachine: import_zod.z.boolean().optional().describe("Include information about the machine"),
280
- includeNetworkVolume: import_zod.z.boolean().optional().describe("Include information about attached network volumes")
887
+ ...listPaginationParams,
888
+ computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
889
+ gpuTypeId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
890
+ dataCenterId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
891
+ name: import_zod3.z.string().optional().describe("Filter to Pods with the provided name"),
892
+ includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
893
+ includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
281
894
  },
895
+ { title: "List pods", ...READ_ONLY },
282
896
  async (params) => {
283
- const queryParams = new URLSearchParams();
284
- if (params.computeType)
285
- queryParams.append("computeType", params.computeType);
286
- if (params.gpuTypeId)
287
- params.gpuTypeId.forEach(
288
- (type) => queryParams.append("gpuTypeId", type)
289
- );
290
- if (params.dataCenterId)
291
- params.dataCenterId.forEach(
292
- (dc) => queryParams.append("dataCenterId", dc)
293
- );
294
- if (params.name) queryParams.append("name", params.name);
295
- if (params.includeMachine)
296
- queryParams.append("includeMachine", params.includeMachine.toString());
297
- if (params.includeNetworkVolume)
298
- queryParams.append(
299
- "includeNetworkVolume",
300
- params.includeNetworkVolume.toString()
301
- );
302
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
303
- const result = await runpodRequest(`/pods${queryString}`);
304
- return {
305
- content: [
306
- {
307
- type: "text",
308
- text: JSON.stringify(result, null, 2)
309
- }
310
- ]
311
- };
897
+ const backend = backendFor("pods");
898
+ let queryString = "";
899
+ if (backend.version === "v1") {
900
+ const queryParams = new URLSearchParams();
901
+ if (params.computeType)
902
+ queryParams.append("computeType", params.computeType);
903
+ if (params.gpuTypeId)
904
+ params.gpuTypeId.forEach(
905
+ (type) => queryParams.append("gpuTypeId", type)
906
+ );
907
+ if (params.dataCenterId)
908
+ params.dataCenterId.forEach(
909
+ (dc) => queryParams.append("dataCenterId", dc)
910
+ );
911
+ if (params.name) queryParams.append("name", params.name);
912
+ if (params.includeMachine)
913
+ queryParams.append(
914
+ "includeMachine",
915
+ params.includeMachine.toString()
916
+ );
917
+ if (params.includeNetworkVolume)
918
+ queryParams.append(
919
+ "includeNetworkVolume",
920
+ params.includeNetworkVolume.toString()
921
+ );
922
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
923
+ }
924
+ const result = await callRestUrl(
925
+ `${backend.base}${backend.list}${queryString}`
926
+ );
927
+ return capListResult(backend.unwrap(result), {
928
+ limit: params.limit,
929
+ cursor: params.cursor
930
+ });
312
931
  }
313
932
  );
314
933
  server.tool(
315
934
  "get-pod",
935
+ "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
316
936
  {
317
- podId: import_zod.z.string().describe("ID of the pod to retrieve"),
318
- includeMachine: import_zod.z.boolean().optional().describe("Include information about the machine"),
319
- includeNetworkVolume: import_zod.z.boolean().optional().describe("Include information about attached network volumes")
937
+ podId: import_zod3.z.string().describe("ID of the pod to retrieve"),
938
+ includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
939
+ includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
320
940
  },
941
+ { title: "Get pod", ...READ_ONLY },
321
942
  async (params) => {
322
- const queryParams = new URLSearchParams();
323
- if (params.includeMachine)
324
- queryParams.append("includeMachine", params.includeMachine.toString());
325
- if (params.includeNetworkVolume)
326
- queryParams.append(
327
- "includeNetworkVolume",
328
- params.includeNetworkVolume.toString()
329
- );
330
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
331
- const result = await runpodRequest(`/pods/${params.podId}${queryString}`);
332
- return {
333
- content: [
334
- {
335
- type: "text",
336
- text: JSON.stringify(result, null, 2)
337
- }
338
- ]
339
- };
943
+ const backend = backendFor("pods");
944
+ let queryString = "";
945
+ if (backend.version === "v1") {
946
+ const queryParams = new URLSearchParams();
947
+ if (params.includeMachine)
948
+ queryParams.append(
949
+ "includeMachine",
950
+ params.includeMachine.toString()
951
+ );
952
+ if (params.includeNetworkVolume)
953
+ queryParams.append(
954
+ "includeNetworkVolume",
955
+ params.includeNetworkVolume.toString()
956
+ );
957
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
958
+ }
959
+ const result = await callRestUrl(
960
+ `${backend.base}${backend.get(params.podId)}${queryString}`
961
+ );
962
+ return jsonReply2(result);
340
963
  }
341
964
  );
342
965
  server.tool(
343
966
  "create-pod",
344
- 'Create a new GPU/CPU pod on Runpod. If the user does not specify an image, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
967
+ '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.',
345
968
  {
346
- name: import_zod.z.string().optional().describe("Name for the pod"),
347
- imageName: import_zod.z.string().describe("Docker image to use"),
348
- cloudType: import_zod.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
349
- gpuTypeIds: import_zod.z.array(import_zod.z.string()).optional().describe("List of acceptable GPU types"),
350
- gpuCount: import_zod.z.number().optional().describe("Number of GPUs"),
351
- containerDiskInGb: import_zod.z.number().optional().describe("Container disk size in GB"),
352
- volumeInGb: import_zod.z.number().optional().describe("Volume size in GB"),
353
- volumeMountPath: import_zod.z.string().optional().describe("Path to mount the volume"),
354
- ports: import_zod.z.array(import_zod.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
355
- env: import_zod.z.record(import_zod.z.string()).optional().describe("Environment variables"),
356
- dataCenterIds: import_zod.z.array(import_zod.z.string()).optional().describe("List of data centers")
969
+ name: import_zod3.z.string().optional().describe("Name for the pod"),
970
+ imageName: import_zod3.z.string().describe("Docker image to use"),
971
+ computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe(
972
+ "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
973
+ ),
974
+ cloudType: import_zod3.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
975
+ gpuTypeIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of acceptable GPU types"),
976
+ gpuCount: import_zod3.z.number().optional().describe("Number of GPUs"),
977
+ containerDiskInGb: import_zod3.z.number().optional().describe("Container disk size in GB"),
978
+ volumeInGb: import_zod3.z.number().optional().describe("Volume size in GB"),
979
+ volumeMountPath: import_zod3.z.string().optional().describe("Path to mount the volume"),
980
+ ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
981
+ env: import_zod3.z.record(import_zod3.z.string()).optional().describe("Environment variables"),
982
+ dataCenterIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of data centers")
357
983
  },
984
+ { title: "Create pod", ...WRITE },
358
985
  async (params) => {
359
- const result = await runpodRequest("/pods", "POST", params);
360
- return {
361
- content: [
362
- {
363
- type: "text",
364
- text: JSON.stringify(result, null, 2)
986
+ const backend = backendFor("pods");
987
+ const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
988
+ const hasGpuCount = (params.gpuCount ?? 0) > 0;
989
+ const wantsCpu = params.computeType === "CPU";
990
+ const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
991
+ if (backend.version === "v2") {
992
+ if (wantsGpu && wantsCpu) {
993
+ return jsonReply2({
994
+ error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
995
+ status: 400
996
+ });
997
+ }
998
+ if (!wantsGpu && !wantsCpu) {
999
+ return jsonReply2({
1000
+ error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
1001
+ status: 400
1002
+ });
1003
+ }
1004
+ if (wantsGpu && !hasGpuTypes) {
1005
+ return jsonReply2({
1006
+ error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
1007
+ status: 400
1008
+ });
1009
+ }
1010
+ if (wantsCpu) {
1011
+ try {
1012
+ const result = await callRestUrl(
1013
+ `${restV1Base(env)}/pods`,
1014
+ "POST",
1015
+ params
1016
+ );
1017
+ return jsonReply2({
1018
+ ...result,
1019
+ _servedBy: "v1",
1020
+ _note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
1021
+ });
1022
+ } catch (error) {
1023
+ if (error instanceof HttpError) {
1024
+ return jsonReply2({
1025
+ error: error.message,
1026
+ status: error.status,
1027
+ _servedBy: "v1",
1028
+ _note: "CPU pod create was routed to the v1 API and failed."
1029
+ });
1030
+ }
1031
+ throw error;
365
1032
  }
366
- ]
367
- };
1033
+ }
1034
+ }
1035
+ const body = backend.mapCreate(params);
1036
+ try {
1037
+ const result = await callRestUrl(
1038
+ `${backend.base}${backend.list}`,
1039
+ "POST",
1040
+ body
1041
+ );
1042
+ if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
1043
+ return jsonReply2({
1044
+ ...result,
1045
+ _warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
1046
+ });
1047
+ }
1048
+ return jsonReply2(result);
1049
+ } catch (error) {
1050
+ if (error instanceof HttpError && error.status === 501) {
1051
+ return jsonReply2({
1052
+ 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).",
1053
+ status: 501
1054
+ });
1055
+ }
1056
+ throw error;
1057
+ }
368
1058
  }
369
1059
  );
370
1060
  server.tool(
371
1061
  "update-pod",
1062
+ "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
372
1063
  {
373
- podId: import_zod.z.string().describe("ID of the pod to update"),
374
- name: import_zod.z.string().optional().describe("New name for the pod"),
375
- imageName: import_zod.z.string().optional().describe("New Docker image"),
376
- containerDiskInGb: import_zod.z.number().optional().describe("New container disk size in GB"),
377
- volumeInGb: import_zod.z.number().optional().describe("New volume size in GB"),
378
- volumeMountPath: import_zod.z.string().optional().describe("New path to mount the volume"),
379
- ports: import_zod.z.array(import_zod.z.string()).optional().describe("New ports to expose"),
380
- env: import_zod.z.record(import_zod.z.string()).optional().describe("New environment variables")
1064
+ podId: import_zod3.z.string().describe("ID of the pod to update"),
1065
+ name: import_zod3.z.string().optional().describe("New name for the pod"),
1066
+ imageName: import_zod3.z.string().optional().describe("New Docker image"),
1067
+ containerDiskInGb: import_zod3.z.number().optional().describe("New container disk size in GB"),
1068
+ volumeInGb: import_zod3.z.number().optional().describe("New volume size in GB"),
1069
+ volumeMountPath: import_zod3.z.string().optional().describe("New path to mount the volume"),
1070
+ ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("New ports to expose"),
1071
+ env: import_zod3.z.record(import_zod3.z.string()).optional().describe("New environment variables")
381
1072
  },
1073
+ { title: "Update pod", ...WRITE, idempotentHint: true },
382
1074
  async (params) => {
383
1075
  const { podId, ...updateParams } = params;
384
- const result = await runpodRequest(
385
- `/pods/${podId}`,
1076
+ const backend = backendFor("pods");
1077
+ const body = backend.mapUpdate(updateParams);
1078
+ const result = await callRestUrl(
1079
+ `${backend.base}${backend.get(podId)}`,
386
1080
  "PATCH",
387
- updateParams
1081
+ body
388
1082
  );
389
- return {
390
- content: [
391
- {
392
- type: "text",
393
- text: JSON.stringify(result, null, 2)
394
- }
395
- ]
396
- };
1083
+ return jsonReply2(result);
397
1084
  }
398
1085
  );
399
1086
  server.tool(
400
1087
  "start-pod",
1088
+ "Start a stopped pod (resumes the same pod and its billing).",
401
1089
  {
402
- podId: import_zod.z.string().describe("ID of the pod to start")
1090
+ podId: import_zod3.z.string().describe("ID of the pod to start")
403
1091
  },
1092
+ { title: "Start pod", ...WRITE, idempotentHint: true },
404
1093
  async (params) => {
405
- const result = await runpodRequest(`/pods/${params.podId}/start`, "POST");
406
- return {
407
- content: [
408
- {
409
- type: "text",
410
- text: JSON.stringify(result, null, 2)
411
- }
412
- ]
413
- };
1094
+ const result = await podAction(params.podId, "start");
1095
+ return jsonReply2(result);
414
1096
  }
415
1097
  );
416
1098
  server.tool(
417
1099
  "stop-pod",
1100
+ "Stop a running pod. The pod and its disk persist; GPU billing stops.",
418
1101
  {
419
- podId: import_zod.z.string().describe("ID of the pod to stop")
1102
+ podId: import_zod3.z.string().describe("ID of the pod to stop")
420
1103
  },
1104
+ { title: "Stop pod", ...WRITE, idempotentHint: true },
421
1105
  async (params) => {
422
- const result = await runpodRequest(`/pods/${params.podId}/stop`, "POST");
423
- return {
424
- content: [
425
- {
426
- type: "text",
427
- text: JSON.stringify(result, null, 2)
428
- }
429
- ]
430
- };
1106
+ const result = await podAction(params.podId, "stop");
1107
+ return jsonReply2(result);
1108
+ }
1109
+ );
1110
+ server.tool(
1111
+ "restart-pod",
1112
+ "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1113
+ {
1114
+ podId: import_zod3.z.string().describe("ID of the pod to restart")
1115
+ },
1116
+ { title: "Restart pod", ...WRITE },
1117
+ async (params) => {
1118
+ const backend = backendFor("pods");
1119
+ if (backend.version === "v1") {
1120
+ return jsonReply2({
1121
+ error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
1122
+ status: 501
1123
+ });
1124
+ }
1125
+ const result = await podAction(params.podId, "restart");
1126
+ return jsonReply2(result);
431
1127
  }
432
1128
  );
433
1129
  server.tool(
434
1130
  "delete-pod",
1131
+ "Permanently delete a pod and its data. This cannot be undone.",
435
1132
  {
436
- podId: import_zod.z.string().describe("ID of the pod to delete")
1133
+ podId: import_zod3.z.string().describe("ID of the pod to delete")
437
1134
  },
1135
+ { title: "Delete pod", ...DESTRUCTIVE },
438
1136
  async (params) => {
439
- const result = await runpodRequest(`/pods/${params.podId}`, "DELETE");
440
- return {
441
- content: [
442
- {
443
- type: "text",
444
- text: JSON.stringify(result, null, 2)
445
- }
446
- ]
447
- };
1137
+ const backend = backendFor("pods");
1138
+ const result = await callRestUrl(
1139
+ `${backend.base}${backend.get(params.podId)}`,
1140
+ "DELETE"
1141
+ );
1142
+ return jsonReply2(result);
448
1143
  }
449
1144
  );
1145
+ }
1146
+
1147
+ // src/tools/endpoints.ts
1148
+ var import_zod4 = require("zod");
1149
+ function registerEndpointTools(server, rt) {
1150
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
450
1151
  server.tool(
451
1152
  "list-endpoints",
1153
+ "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
452
1154
  {
453
- includeTemplate: import_zod.z.boolean().optional().describe("Include template information"),
454
- includeWorkers: import_zod.z.boolean().optional().describe("Include information about workers")
1155
+ ...listPaginationParams,
1156
+ includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
1157
+ includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
455
1158
  },
1159
+ { title: "List endpoints", ...READ_ONLY },
456
1160
  async (params) => {
457
- const queryParams = new URLSearchParams();
458
- if (params.includeTemplate)
459
- queryParams.append(
460
- "includeTemplate",
461
- params.includeTemplate.toString()
462
- );
463
- if (params.includeWorkers)
464
- queryParams.append("includeWorkers", params.includeWorkers.toString());
465
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
466
- const result = await runpodRequest(`/endpoints${queryString}`);
467
- return {
468
- content: [
469
- {
470
- type: "text",
471
- text: JSON.stringify(result, null, 2)
472
- }
473
- ]
474
- };
1161
+ const backend = backendFor("endpoints");
1162
+ let queryString = "";
1163
+ if (backend.version === "v1") {
1164
+ const queryParams = new URLSearchParams();
1165
+ if (params.includeTemplate)
1166
+ queryParams.append(
1167
+ "includeTemplate",
1168
+ params.includeTemplate.toString()
1169
+ );
1170
+ if (params.includeWorkers)
1171
+ queryParams.append(
1172
+ "includeWorkers",
1173
+ params.includeWorkers.toString()
1174
+ );
1175
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1176
+ }
1177
+ const result = await callRestUrl(
1178
+ `${backend.base}${backend.list}${queryString}`
1179
+ );
1180
+ return capListResult(backend.unwrap(result), {
1181
+ limit: params.limit,
1182
+ cursor: params.cursor
1183
+ });
475
1184
  }
476
1185
  );
477
1186
  server.tool(
478
1187
  "get-endpoint",
1188
+ "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
479
1189
  {
480
- endpointId: import_zod.z.string().describe("ID of the endpoint to retrieve"),
481
- includeTemplate: import_zod.z.boolean().optional().describe("Include template information"),
482
- includeWorkers: import_zod.z.boolean().optional().describe("Include information about workers")
1190
+ endpointId: import_zod4.z.string().describe("ID of the endpoint to retrieve"),
1191
+ includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
1192
+ includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
483
1193
  },
1194
+ { title: "Get endpoint", ...READ_ONLY },
484
1195
  async (params) => {
485
- const queryParams = new URLSearchParams();
486
- if (params.includeTemplate)
487
- queryParams.append(
488
- "includeTemplate",
489
- params.includeTemplate.toString()
490
- );
491
- if (params.includeWorkers)
492
- queryParams.append("includeWorkers", params.includeWorkers.toString());
493
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
494
- const result = await runpodRequest(
495
- `/endpoints/${params.endpointId}${queryString}`
1196
+ const backend = backendFor("endpoints");
1197
+ let queryString = "";
1198
+ if (backend.version === "v1") {
1199
+ const queryParams = new URLSearchParams();
1200
+ if (params.includeTemplate)
1201
+ queryParams.append(
1202
+ "includeTemplate",
1203
+ params.includeTemplate.toString()
1204
+ );
1205
+ if (params.includeWorkers)
1206
+ queryParams.append(
1207
+ "includeWorkers",
1208
+ params.includeWorkers.toString()
1209
+ );
1210
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1211
+ }
1212
+ const result = await callRestUrl(
1213
+ `${backend.base}${backend.get(params.endpointId)}${queryString}`
496
1214
  );
497
- return {
498
- content: [
499
- {
500
- type: "text",
501
- text: JSON.stringify(result, null, 2)
502
- }
503
- ]
504
- };
1215
+ return jsonReply2(result);
505
1216
  }
506
1217
  );
507
1218
  server.tool(
508
1219
  "create-endpoint",
1220
+ "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).",
509
1221
  {
510
- name: import_zod.z.string().optional().describe("Name for the endpoint"),
511
- templateId: import_zod.z.string().describe("Template ID to use"),
512
- computeType: import_zod.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint"),
513
- gpuTypeIds: import_zod.z.array(import_zod.z.string()).optional().describe("List of acceptable GPU types"),
514
- gpuCount: import_zod.z.number().optional().describe("Number of GPUs per worker"),
515
- workersMin: import_zod.z.number().optional().describe("Minimum number of workers"),
516
- workersMax: import_zod.z.number().optional().describe("Maximum number of workers"),
517
- dataCenterIds: import_zod.z.array(import_zod.z.string()).optional().describe("List of data centers")
1222
+ name: import_zod4.z.string().optional().describe("Name for the endpoint"),
1223
+ // --- v2 inline-config fields ---
1224
+ imageName: import_zod4.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
1225
+ gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe(
1226
+ 'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
1227
+ ),
1228
+ gpuCount: import_zod4.z.number().optional().describe("GPUs per worker (v2)"),
1229
+ args: import_zod4.z.string().optional().describe("Container start command/args (v2)"),
1230
+ containerDiskInGb: import_zod4.z.number().optional().describe("Container disk size in GB (v2)"),
1231
+ ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
1232
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe("Environment variables (v2)"),
1233
+ containerRegistryAuthId: import_zod4.z.string().optional().describe("Container registry auth id for a private image (v2)"),
1234
+ networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("Network volume ids to attach (v2)"),
1235
+ executionTimeoutMs: import_zod4.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
1236
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
1237
+ // --- v1 template-based fields ---
1238
+ templateId: import_zod4.z.string().optional().describe("Template ID (v1). Required on v1."),
1239
+ computeType: import_zod4.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
1240
+ gpuTypeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of acceptable GPU types (v1)"),
1241
+ // --- shared autoscaling fields ---
1242
+ workersMin: import_zod4.z.number().optional().describe("Minimum number of workers"),
1243
+ workersMax: import_zod4.z.number().optional().describe("Maximum number of workers"),
1244
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
1245
+ scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value"),
1246
+ idleTimeout: import_zod4.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
1247
+ dataCenterIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of preferred data centers")
518
1248
  },
1249
+ { title: "Create endpoint", ...WRITE },
519
1250
  async (params) => {
520
- const result = await runpodRequest("/endpoints", "POST", params);
521
- return {
522
- content: [
523
- {
524
- type: "text",
525
- text: JSON.stringify(result, null, 2)
526
- }
527
- ]
528
- };
1251
+ const backend = backendFor("endpoints");
1252
+ if (backend.version === "v2") {
1253
+ if (!params.name) {
1254
+ return jsonReply2({
1255
+ error: "On the v2 REST API, create-endpoint requires a name.",
1256
+ status: 400
1257
+ });
1258
+ }
1259
+ if (!params.imageName) {
1260
+ return jsonReply2({
1261
+ error: "On the v2 REST API, create-endpoint needs an imageName (v2 endpoints are image-based, not template-based). To use a templateId, set RUNPOD_REST_VERSION=v1.",
1262
+ status: 400
1263
+ });
1264
+ }
1265
+ if (!params.gpuPoolIds?.length) {
1266
+ return jsonReply2({
1267
+ error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
1268
+ status: 400
1269
+ });
1270
+ }
1271
+ const body = backend.mapCreate(params);
1272
+ const result2 = await callRestUrl(
1273
+ `${backend.base}${backend.list}`,
1274
+ "POST",
1275
+ body
1276
+ );
1277
+ return jsonReply2(result2);
1278
+ }
1279
+ if (!params.templateId) {
1280
+ return jsonReply2({
1281
+ error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
1282
+ status: 400
1283
+ });
1284
+ }
1285
+ const v1Body = {};
1286
+ for (const k of [
1287
+ "templateId",
1288
+ "name",
1289
+ "computeType",
1290
+ "gpuTypeIds",
1291
+ "gpuCount",
1292
+ "workersMin",
1293
+ "workersMax",
1294
+ "dataCenterIds"
1295
+ ]) {
1296
+ if (params[k] !== void 0) v1Body[k] = params[k];
1297
+ }
1298
+ const result = await callRestUrl(
1299
+ `${backend.base}${backend.list}`,
1300
+ "POST",
1301
+ v1Body
1302
+ );
1303
+ return jsonReply2(result);
529
1304
  }
530
1305
  );
531
1306
  server.tool(
532
1307
  "update-endpoint",
1308
+ "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.",
533
1309
  {
534
- endpointId: import_zod.z.string().describe("ID of the endpoint to update"),
535
- name: import_zod.z.string().optional().describe("New name for the endpoint"),
536
- workersMin: import_zod.z.number().optional().describe("New minimum number of workers"),
537
- workersMax: import_zod.z.number().optional().describe("New maximum number of workers"),
538
- idleTimeout: import_zod.z.number().optional().describe("New idle timeout in seconds"),
539
- scalerType: import_zod.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
540
- scalerValue: import_zod.z.number().optional().describe("Scaler value")
1310
+ endpointId: import_zod4.z.string().describe("ID of the endpoint to update"),
1311
+ name: import_zod4.z.string().optional().describe("New name for the endpoint"),
1312
+ workersMin: import_zod4.z.number().optional().describe("New minimum number of workers"),
1313
+ workersMax: import_zod4.z.number().optional().describe("New maximum number of workers"),
1314
+ idleTimeout: import_zod4.z.number().optional().describe("New idle timeout in seconds"),
1315
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1316
+ scalerValue: import_zod4.z.number().optional().describe("Scaler value"),
1317
+ // --- v2-only inline-config fields ---
1318
+ imageName: import_zod4.z.string().optional().describe("New Docker image (v2)"),
1319
+ gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
1320
+ gpuCount: import_zod4.z.number().optional().describe("New GPUs per worker (v2)"),
1321
+ args: import_zod4.z.string().optional().describe("New container args (v2)"),
1322
+ containerDiskInGb: import_zod4.z.number().optional().describe("New container disk size in GB (v2)"),
1323
+ ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("New ports (v2)"),
1324
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe("New environment variables (v2)"),
1325
+ containerRegistryAuthId: import_zod4.z.string().optional().describe("New container registry auth id (v2)"),
1326
+ networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("New network volume ids (v2)"),
1327
+ executionTimeoutMs: import_zod4.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
1328
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
541
1329
  },
1330
+ { title: "Update endpoint", ...WRITE, idempotentHint: true },
542
1331
  async (params) => {
543
1332
  const { endpointId, ...updateParams } = params;
544
- const result = await runpodRequest(
545
- `/endpoints/${endpointId}`,
1333
+ const backend = backendFor("endpoints");
1334
+ const body = backend.mapUpdate(updateParams);
1335
+ const result = await callRestUrl(
1336
+ `${backend.base}${backend.get(endpointId)}`,
546
1337
  "PATCH",
547
- updateParams
1338
+ body
548
1339
  );
549
- return {
550
- content: [
551
- {
552
- type: "text",
553
- text: JSON.stringify(result, null, 2)
554
- }
555
- ]
556
- };
1340
+ return jsonReply2(result);
557
1341
  }
558
1342
  );
559
1343
  server.tool(
560
1344
  "delete-endpoint",
1345
+ "Permanently delete a Serverless endpoint. This cannot be undone.",
561
1346
  {
562
- endpointId: import_zod.z.string().describe("ID of the endpoint to delete")
1347
+ endpointId: import_zod4.z.string().describe("ID of the endpoint to delete")
563
1348
  },
1349
+ { title: "Delete endpoint", ...DESTRUCTIVE },
564
1350
  async (params) => {
565
- const result = await runpodRequest(
566
- `/endpoints/${params.endpointId}`,
1351
+ const backend = backendFor("endpoints");
1352
+ const result = await callRestUrl(
1353
+ `${backend.base}${backend.get(params.endpointId)}`,
567
1354
  "DELETE"
568
1355
  );
569
- return {
570
- content: [
571
- {
572
- type: "text",
573
- text: JSON.stringify(result, null, 2)
574
- }
575
- ]
576
- };
1356
+ return jsonReply2(result);
577
1357
  }
578
1358
  );
579
- const endpointIdSchema = import_zod.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
580
- const jobIdSchema = import_zod.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
581
- const inputSchema = import_zod.z.record(import_zod.z.unknown()).describe(
1359
+ server.tool(
1360
+ "list-endpoint-workers",
1361
+ "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.",
1362
+ {
1363
+ ...listPaginationParams,
1364
+ endpointId: import_zod4.z.string().describe("ID of the Serverless endpoint whose workers to list")
1365
+ },
1366
+ { title: "List endpoint workers", ...READ_ONLY },
1367
+ async (params) => {
1368
+ const backend = backendFor("workers");
1369
+ if (backend.version === "v1") {
1370
+ return jsonReply2({
1371
+ error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
1372
+ status: 501
1373
+ });
1374
+ }
1375
+ const raw = await callRestUrl(
1376
+ `${backend.base}/serverless/${params.endpointId}/workers`
1377
+ );
1378
+ return capListResult(
1379
+ backend.unwrap(raw),
1380
+ { limit: params.limit, cursor: params.cursor },
1381
+ { summary: raw?.summary, endpointVersion: raw?.endpointVersion }
1382
+ );
1383
+ }
1384
+ );
1385
+ }
1386
+
1387
+ // src/tools/jobs.ts
1388
+ var import_zod5 = require("zod");
1389
+ function registerJobTools(server, rt) {
1390
+ const { jsonReply: jsonReply2, serverlessRequest } = rt;
1391
+ const endpointIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1392
+ const jobIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1393
+ const inputSchema = import_zod5.z.record(import_zod5.z.unknown()).describe(
582
1394
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
583
1395
  );
584
- const webhookSchema = import_zod.z.string().url().optional().describe(
1396
+ const webhookSchema = import_zod5.z.string().url().optional().describe(
585
1397
  "Webhook URL to receive job completion notifications instead of polling"
586
1398
  );
587
- const policySchema = import_zod.z.object({
588
- executionTimeout: import_zod.z.number().optional().describe("Maximum execution time in milliseconds"),
589
- lowPriority: import_zod.z.boolean().optional().describe("Submit as a low-priority job"),
590
- ttl: import_zod.z.number().optional().describe("Time-to-live for the job result in milliseconds")
1399
+ const policySchema = import_zod5.z.object({
1400
+ executionTimeout: import_zod5.z.number().optional().describe("Maximum execution time in milliseconds"),
1401
+ lowPriority: import_zod5.z.boolean().optional().describe("Submit as a low-priority job"),
1402
+ ttl: import_zod5.z.number().optional().describe("Time-to-live for the job result in milliseconds")
591
1403
  }).optional().describe("Execution policy options");
592
- const s3ConfigSchema = import_zod.z.object({
593
- accessId: import_zod.z.string().describe("S3 access key ID"),
594
- accessSecret: import_zod.z.string().describe("S3 secret access key"),
595
- bucketName: import_zod.z.string().describe("S3 bucket name"),
596
- endpointUrl: import_zod.z.string().describe("S3 endpoint URL")
1404
+ const s3ConfigSchema = import_zod5.z.object({
1405
+ accessId: import_zod5.z.string().describe("S3 access key ID"),
1406
+ accessSecret: import_zod5.z.string().describe("S3 secret access key"),
1407
+ bucketName: import_zod5.z.string().describe("S3 bucket name"),
1408
+ endpointUrl: import_zod5.z.string().describe("S3 endpoint URL")
597
1409
  }).optional().describe("S3-compatible storage config for large outputs");
598
1410
  server.tool(
599
1411
  "run-endpoint",
@@ -607,6 +1419,7 @@ function registerTools(server, ctx) {
607
1419
  policy: policySchema,
608
1420
  s3Config: s3ConfigSchema
609
1421
  },
1422
+ { title: "Run endpoint (async)", ...WRITE },
610
1423
  async (params) => {
611
1424
  const { endpointId, ...body } = params;
612
1425
  const result = await serverlessRequest(
@@ -615,14 +1428,7 @@ function registerTools(server, ctx) {
615
1428
  "POST",
616
1429
  body
617
1430
  );
618
- return {
619
- content: [
620
- {
621
- type: "text",
622
- text: JSON.stringify(result, null, 2)
623
- }
624
- ]
625
- };
1431
+ return jsonReply2(result);
626
1432
  }
627
1433
  );
628
1434
  server.tool(
@@ -633,13 +1439,14 @@ function registerTools(server, ctx) {
633
1439
  "ID of the Serverless endpoint to run synchronously"
634
1440
  ),
635
1441
  input: inputSchema,
636
- wait: import_zod.z.number().min(1e3).max(3e5).optional().describe(
1442
+ wait: import_zod5.z.number().min(1e3).max(3e5).optional().describe(
637
1443
  "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)."
638
1444
  ),
639
1445
  webhook: webhookSchema,
640
1446
  policy: policySchema,
641
1447
  s3Config: s3ConfigSchema
642
1448
  },
1449
+ { title: "Run endpoint (sync)", ...WRITE },
643
1450
  async (params) => {
644
1451
  const { endpointId, wait, ...body } = params;
645
1452
  const path = wait ? `/runsync?wait=${wait}` : "/runsync";
@@ -649,14 +1456,7 @@ function registerTools(server, ctx) {
649
1456
  "POST",
650
1457
  body
651
1458
  );
652
- return {
653
- content: [
654
- {
655
- type: "text",
656
- text: JSON.stringify(result, null, 2)
657
- }
658
- ]
659
- };
1459
+ return jsonReply2(result);
660
1460
  }
661
1461
  );
662
1462
  server.tool(
@@ -668,19 +1468,13 @@ function registerTools(server, ctx) {
668
1468
  ),
669
1469
  jobId: jobIdSchema.describe("ID of the job to check")
670
1470
  },
1471
+ { title: "Get job status", ...READ_ONLY },
671
1472
  async (params) => {
672
1473
  const result = await serverlessRequest(
673
1474
  params.endpointId,
674
1475
  `/status/${params.jobId}`
675
1476
  );
676
- return {
677
- content: [
678
- {
679
- type: "text",
680
- text: JSON.stringify(result, null, 2)
681
- }
682
- ]
683
- };
1477
+ return jsonReply2(result);
684
1478
  }
685
1479
  );
686
1480
  server.tool(
@@ -692,6 +1486,7 @@ function registerTools(server, ctx) {
692
1486
  ),
693
1487
  jobId: jobIdSchema.describe("ID of the job to stream results from")
694
1488
  },
1489
+ { title: "Stream job", ...READ_ONLY },
695
1490
  async (params) => {
696
1491
  const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
697
1492
  "COMPLETED",
@@ -737,18 +1532,7 @@ function registerTools(server, ctx) {
737
1532
  }
738
1533
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
739
1534
  }
740
- return {
741
- content: [
742
- {
743
- type: "text",
744
- text: JSON.stringify(
745
- { ...finalResult, stream: allChunks },
746
- null,
747
- 2
748
- )
749
- }
750
- ]
751
- };
1535
+ return jsonReply2({ ...finalResult, stream: allChunks });
752
1536
  }
753
1537
  );
754
1538
  server.tool(
@@ -760,20 +1544,14 @@ function registerTools(server, ctx) {
760
1544
  ),
761
1545
  jobId: jobIdSchema.describe("ID of the job to cancel")
762
1546
  },
1547
+ { title: "Cancel job", ...WRITE, idempotentHint: true },
763
1548
  async (params) => {
764
1549
  const result = await serverlessRequest(
765
1550
  params.endpointId,
766
1551
  `/cancel/${params.jobId}`,
767
1552
  "POST"
768
1553
  );
769
- return {
770
- content: [
771
- {
772
- type: "text",
773
- text: JSON.stringify(result, null, 2)
774
- }
775
- ]
776
- };
1554
+ return jsonReply2(result);
777
1555
  }
778
1556
  );
779
1557
  server.tool(
@@ -785,20 +1563,14 @@ function registerTools(server, ctx) {
785
1563
  ),
786
1564
  jobId: jobIdSchema.describe("ID of the job to retry")
787
1565
  },
1566
+ { title: "Retry job", ...WRITE },
788
1567
  async (params) => {
789
1568
  const result = await serverlessRequest(
790
1569
  params.endpointId,
791
1570
  `/retry/${params.jobId}`,
792
1571
  "POST"
793
1572
  );
794
- return {
795
- content: [
796
- {
797
- type: "text",
798
- text: JSON.stringify(result, null, 2)
799
- }
800
- ]
801
- };
1573
+ return jsonReply2(result);
802
1574
  }
803
1575
  );
804
1576
  server.tool(
@@ -809,16 +1581,10 @@ function registerTools(server, ctx) {
809
1581
  "ID of the Serverless endpoint to check health for"
810
1582
  )
811
1583
  },
1584
+ { title: "Endpoint health", ...READ_ONLY },
812
1585
  async (params) => {
813
1586
  const result = await serverlessRequest(params.endpointId, "/health");
814
- return {
815
- content: [
816
- {
817
- type: "text",
818
- text: JSON.stringify(result, null, 2)
819
- }
820
- ]
821
- };
1587
+ return jsonReply2(result);
822
1588
  }
823
1589
  );
824
1590
  server.tool(
@@ -829,311 +1595,555 @@ function registerTools(server, ctx) {
829
1595
  "ID of the Serverless endpoint to purge the queue for"
830
1596
  )
831
1597
  },
1598
+ { title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
832
1599
  async (params) => {
833
1600
  const result = await serverlessRequest(
834
1601
  params.endpointId,
835
1602
  "/purge-queue",
836
1603
  "POST"
837
1604
  );
838
- return {
839
- content: [
840
- {
841
- type: "text",
842
- text: JSON.stringify(result, null, 2)
843
- }
844
- ]
845
- };
1605
+ return jsonReply2(result);
846
1606
  }
847
1607
  );
1608
+ }
1609
+
1610
+ // src/tools/templates.ts
1611
+ var import_zod6 = require("zod");
1612
+ function registerTemplateTools(server, rt) {
1613
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
848
1614
  server.tool(
849
1615
  "list-templates",
850
1616
  `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.`,
851
1617
  {
852
- includeRunpodTemplates: import_zod.z.boolean().optional().describe("Include official Runpod templates in the response"),
853
- includePublicTemplates: import_zod.z.boolean().optional().describe("Include community-made public templates in the response"),
854
- includeEndpointBoundTemplates: import_zod.z.boolean().optional().describe(
1618
+ ...listPaginationParams,
1619
+ includeRunpodTemplates: import_zod6.z.boolean().optional().describe("Include official Runpod templates in the response"),
1620
+ includePublicTemplates: import_zod6.z.boolean().optional().describe("Include community-made public templates in the response"),
1621
+ includeEndpointBoundTemplates: import_zod6.z.boolean().optional().describe(
855
1622
  "Include templates bound to Serverless endpoints in the response"
856
1623
  )
857
1624
  },
1625
+ { title: "List templates", ...READ_ONLY },
858
1626
  async (params) => {
859
- const queryParams = new URLSearchParams();
860
- if (params.includeRunpodTemplates)
861
- queryParams.set("includeRunpodTemplates", "true");
862
- if (params.includePublicTemplates)
863
- queryParams.set("includePublicTemplates", "true");
864
- if (params.includeEndpointBoundTemplates)
865
- queryParams.set("includeEndpointBoundTemplates", "true");
866
- const query = queryParams.toString();
867
- const result = await runpodRequest(
868
- `/templates${query ? `?${query}` : ""}`
1627
+ const backend = backendFor("templates");
1628
+ let query = "";
1629
+ if (backend.version === "v1") {
1630
+ const queryParams = new URLSearchParams();
1631
+ if (params.includeRunpodTemplates)
1632
+ queryParams.set("includeRunpodTemplates", "true");
1633
+ if (params.includePublicTemplates)
1634
+ queryParams.set("includePublicTemplates", "true");
1635
+ if (params.includeEndpointBoundTemplates)
1636
+ queryParams.set("includeEndpointBoundTemplates", "true");
1637
+ query = queryParams.toString() ? `?${queryParams.toString()}` : "";
1638
+ }
1639
+ const result = await callRestUrl(
1640
+ `${backend.base}${backend.list}${query}`
869
1641
  );
870
- return {
871
- content: [
872
- {
873
- type: "text",
874
- text: JSON.stringify(result, null, 2)
875
- }
876
- ]
877
- };
1642
+ return capListResult(backend.unwrap(result), {
1643
+ limit: params.limit,
1644
+ cursor: params.cursor
1645
+ });
878
1646
  }
879
1647
  );
880
1648
  server.tool(
881
1649
  "get-template",
1650
+ "Get one template by id (image, env, ports, disk/volume config).",
882
1651
  {
883
- templateId: import_zod.z.string().describe("ID of the template to retrieve")
1652
+ templateId: import_zod6.z.string().describe("ID of the template to retrieve")
884
1653
  },
1654
+ { title: "Get template", ...READ_ONLY },
885
1655
  async (params) => {
886
- const result = await runpodRequest(`/templates/${params.templateId}`);
887
- return {
888
- content: [
889
- {
890
- type: "text",
891
- text: JSON.stringify(result, null, 2)
892
- }
893
- ]
894
- };
1656
+ const backend = backendFor("templates");
1657
+ const result = await callRestUrl(
1658
+ `${backend.base}${backend.get(params.templateId)}`
1659
+ );
1660
+ return jsonReply2(result);
895
1661
  }
896
1662
  );
897
1663
  server.tool(
898
1664
  "create-template",
1665
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). On v2 a category is required and defaults to NVIDIA.",
899
1666
  {
900
- name: import_zod.z.string().describe("Name for the template"),
901
- imageName: import_zod.z.string().describe("Docker image to use"),
902
- isServerless: import_zod.z.boolean().optional().describe("Is this a serverless template"),
903
- ports: import_zod.z.array(import_zod.z.string()).optional().describe("Ports to expose"),
904
- dockerEntrypoint: import_zod.z.array(import_zod.z.string()).optional().describe("Docker entrypoint commands"),
905
- dockerStartCmd: import_zod.z.array(import_zod.z.string()).optional().describe("Docker start commands"),
906
- env: import_zod.z.record(import_zod.z.string()).optional().describe("Environment variables"),
907
- containerDiskInGb: import_zod.z.number().optional().describe("Container disk size in GB"),
908
- volumeInGb: import_zod.z.number().optional().describe("Volume size in GB"),
909
- volumeMountPath: import_zod.z.string().optional().describe("Path to mount the volume"),
910
- readme: import_zod.z.string().optional().describe("README content in markdown format")
1667
+ name: import_zod6.z.string().describe("Name for the template"),
1668
+ imageName: import_zod6.z.string().describe("Docker image to use"),
1669
+ isServerless: import_zod6.z.boolean().optional().describe("Is this a serverless template"),
1670
+ ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("Ports to expose"),
1671
+ dockerEntrypoint: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker entrypoint commands"),
1672
+ dockerStartCmd: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker start commands"),
1673
+ env: import_zod6.z.record(import_zod6.z.string()).optional().describe("Environment variables"),
1674
+ containerDiskInGb: import_zod6.z.number().optional().describe("Container disk size in GB"),
1675
+ volumeInGb: import_zod6.z.number().optional().describe("Volume size in GB"),
1676
+ volumeMountPath: import_zod6.z.string().optional().describe("Path to mount the volume"),
1677
+ readme: import_zod6.z.string().optional().describe("README content in markdown format"),
1678
+ category: import_zod6.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
1679
+ "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."
1680
+ ),
1681
+ containerRegistryAuthId: import_zod6.z.string().optional().describe(
1682
+ "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."
1683
+ )
911
1684
  },
1685
+ { title: "Create template", ...WRITE },
912
1686
  async (params) => {
913
- const result = await runpodRequest("/templates", "POST", params);
914
- return {
915
- content: [
916
- {
917
- type: "text",
918
- text: JSON.stringify(result, null, 2)
919
- }
920
- ]
921
- };
1687
+ const backend = backendFor("templates");
1688
+ const body = backend.mapCreate(params);
1689
+ const result = await callRestUrl(
1690
+ `${backend.base}${backend.list}`,
1691
+ "POST",
1692
+ body
1693
+ );
1694
+ return jsonReply2(result);
922
1695
  }
923
1696
  );
924
1697
  server.tool(
925
1698
  "update-template",
1699
+ "Update a template's mutable fields (name, image, ports, env, readme). Only provided fields change.",
926
1700
  {
927
- templateId: import_zod.z.string().describe("ID of the template to update"),
928
- name: import_zod.z.string().optional().describe("New name for the template"),
929
- imageName: import_zod.z.string().optional().describe("New Docker image"),
930
- ports: import_zod.z.array(import_zod.z.string()).optional().describe("New ports to expose"),
931
- env: import_zod.z.record(import_zod.z.string()).optional().describe("New environment variables"),
932
- readme: import_zod.z.string().optional().describe("New README content in markdown format")
1701
+ templateId: import_zod6.z.string().describe("ID of the template to update"),
1702
+ name: import_zod6.z.string().optional().describe("New name for the template"),
1703
+ imageName: import_zod6.z.string().optional().describe("New Docker image"),
1704
+ ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("New ports to expose"),
1705
+ env: import_zod6.z.record(import_zod6.z.string()).optional().describe("New environment variables"),
1706
+ readme: import_zod6.z.string().optional().describe("New README content in markdown format"),
1707
+ containerRegistryAuthId: import_zod6.z.string().optional().describe(
1708
+ "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."
1709
+ )
933
1710
  },
1711
+ { title: "Update template", ...WRITE, idempotentHint: true },
934
1712
  async (params) => {
935
1713
  const { templateId, ...updateParams } = params;
936
- const result = await runpodRequest(
937
- `/templates/${templateId}`,
1714
+ const backend = backendFor("templates");
1715
+ const body = backend.mapUpdate(updateParams);
1716
+ const result = await callRestUrl(
1717
+ `${backend.base}${backend.get(templateId)}`,
938
1718
  "PATCH",
939
- updateParams
1719
+ body
940
1720
  );
941
- return {
942
- content: [
943
- {
944
- type: "text",
945
- text: JSON.stringify(result, null, 2)
946
- }
947
- ]
948
- };
1721
+ return jsonReply2(result);
949
1722
  }
950
1723
  );
951
1724
  server.tool(
952
1725
  "delete-template",
1726
+ "Permanently delete a template. This cannot be undone.",
953
1727
  {
954
- templateId: import_zod.z.string().describe("ID of the template to delete")
1728
+ templateId: import_zod6.z.string().describe("ID of the template to delete")
955
1729
  },
1730
+ { title: "Delete template", ...DESTRUCTIVE },
956
1731
  async (params) => {
957
- const result = await runpodRequest(
958
- `/templates/${params.templateId}`,
1732
+ const backend = backendFor("templates");
1733
+ const result = await callRestUrl(
1734
+ `${backend.base}${backend.get(params.templateId)}`,
959
1735
  "DELETE"
960
1736
  );
961
- return {
962
- content: [
963
- {
964
- type: "text",
965
- text: JSON.stringify(result, null, 2)
966
- }
967
- ]
968
- };
1737
+ return jsonReply2(result);
1738
+ }
1739
+ );
1740
+ }
1741
+
1742
+ // src/tools/network-volumes.ts
1743
+ var import_zod7 = require("zod");
1744
+ function registerNetworkVolumeTools(server, rt) {
1745
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1746
+ server.tool(
1747
+ "list-network-volumes",
1748
+ "List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
1749
+ listPaginationParams,
1750
+ { title: "List network volumes", ...READ_ONLY },
1751
+ async (params) => {
1752
+ const backend = backendFor("networkVolumes");
1753
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
1754
+ return capListResult(backend.unwrap(result), {
1755
+ limit: params.limit,
1756
+ cursor: params.cursor
1757
+ });
969
1758
  }
970
1759
  );
971
- server.tool("list-network-volumes", {}, async () => {
972
- const result = await runpodRequest("/networkvolumes");
973
- return {
974
- content: [
975
- {
976
- type: "text",
977
- text: JSON.stringify(result, null, 2)
978
- }
979
- ]
980
- };
981
- });
982
1760
  server.tool(
983
1761
  "get-network-volume",
1762
+ "Get one network volume by id (name, size, data center).",
984
1763
  {
985
- networkVolumeId: import_zod.z.string().describe("ID of the network volume to retrieve")
1764
+ networkVolumeId: import_zod7.z.string().describe("ID of the network volume to retrieve")
986
1765
  },
1766
+ { title: "Get network volume", ...READ_ONLY },
987
1767
  async (params) => {
988
- const result = await runpodRequest(
989
- `/networkvolumes/${params.networkVolumeId}`
1768
+ const backend = backendFor("networkVolumes");
1769
+ const result = await callRestUrl(
1770
+ `${backend.base}${backend.get(params.networkVolumeId)}`
990
1771
  );
991
- return {
992
- content: [
993
- {
994
- type: "text",
995
- text: JSON.stringify(result, null, 2)
996
- }
997
- ]
998
- };
1772
+ return jsonReply2(result);
999
1773
  }
1000
1774
  );
1001
1775
  server.tool(
1002
1776
  "create-network-volume",
1777
+ "Create a persistent network volume in a data center (size 1\u20134000 GB) that pods can mount.",
1003
1778
  {
1004
- name: import_zod.z.string().describe("Name for the network volume"),
1005
- size: import_zod.z.number().describe("Size in GB (1-4000)"),
1006
- dataCenterId: import_zod.z.string().describe("Data center ID")
1779
+ name: import_zod7.z.string().describe("Name for the network volume"),
1780
+ size: import_zod7.z.number().describe("Size in GB (1-4000)"),
1781
+ dataCenterId: import_zod7.z.string().describe("Data center ID (see list-data-centers)")
1007
1782
  },
1783
+ { title: "Create network volume", ...WRITE },
1008
1784
  async (params) => {
1009
- const result = await runpodRequest("/networkvolumes", "POST", params);
1010
- return {
1011
- content: [
1012
- {
1013
- type: "text",
1014
- text: JSON.stringify(result, null, 2)
1015
- }
1016
- ]
1017
- };
1785
+ const backend = backendFor("networkVolumes");
1786
+ const body = backend.mapCreate(params);
1787
+ const result = await callRestUrl(
1788
+ `${backend.base}${backend.list}`,
1789
+ "POST",
1790
+ body
1791
+ );
1792
+ return jsonReply2(result);
1018
1793
  }
1019
1794
  );
1020
1795
  server.tool(
1021
1796
  "update-network-volume",
1797
+ "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1022
1798
  {
1023
- networkVolumeId: import_zod.z.string().describe("ID of the network volume to update"),
1024
- name: import_zod.z.string().optional().describe("New name for the network volume"),
1025
- size: import_zod.z.number().optional().describe("New size in GB (must be larger than current)")
1799
+ networkVolumeId: import_zod7.z.string().describe("ID of the network volume to update"),
1800
+ name: import_zod7.z.string().optional().describe("New name for the network volume"),
1801
+ size: import_zod7.z.number().optional().describe("New size in GB (must be larger than current)")
1026
1802
  },
1803
+ { title: "Update network volume", ...WRITE, idempotentHint: true },
1027
1804
  async (params) => {
1028
1805
  const { networkVolumeId, ...updateParams } = params;
1029
- const result = await runpodRequest(
1030
- `/networkvolumes/${networkVolumeId}`,
1806
+ const backend = backendFor("networkVolumes");
1807
+ const body = backend.mapUpdate(updateParams);
1808
+ const result = await callRestUrl(
1809
+ `${backend.base}${backend.get(networkVolumeId)}`,
1031
1810
  "PATCH",
1032
- updateParams
1811
+ body
1033
1812
  );
1034
- return {
1035
- content: [
1036
- {
1037
- type: "text",
1038
- text: JSON.stringify(result, null, 2)
1039
- }
1040
- ]
1041
- };
1813
+ return jsonReply2(result);
1042
1814
  }
1043
1815
  );
1044
1816
  server.tool(
1045
1817
  "delete-network-volume",
1818
+ "Permanently delete a network volume and its data. This cannot be undone.",
1046
1819
  {
1047
- networkVolumeId: import_zod.z.string().describe("ID of the network volume to delete")
1820
+ networkVolumeId: import_zod7.z.string().describe("ID of the network volume to delete")
1048
1821
  },
1822
+ { title: "Delete network volume", ...DESTRUCTIVE },
1049
1823
  async (params) => {
1050
- const result = await runpodRequest(
1051
- `/networkvolumes/${params.networkVolumeId}`,
1824
+ const backend = backendFor("networkVolumes");
1825
+ const result = await callRestUrl(
1826
+ `${backend.base}${backend.get(params.networkVolumeId)}`,
1052
1827
  "DELETE"
1053
1828
  );
1054
- return {
1055
- content: [
1056
- {
1057
- type: "text",
1058
- text: JSON.stringify(result, null, 2)
1059
- }
1060
- ]
1061
- };
1829
+ return jsonReply2(result);
1830
+ }
1831
+ );
1832
+ }
1833
+
1834
+ // src/tools/registries.ts
1835
+ var import_zod8 = require("zod");
1836
+ function registerRegistryTools(server, rt) {
1837
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1838
+ server.tool(
1839
+ "list-container-registry-auths",
1840
+ "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
1841
+ listPaginationParams,
1842
+ { title: "List container registry auths", ...READ_ONLY },
1843
+ async (params) => {
1844
+ const backend = backendFor("registries");
1845
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
1846
+ return capListResult(backend.unwrap(result), {
1847
+ limit: params.limit,
1848
+ cursor: params.cursor
1849
+ });
1062
1850
  }
1063
1851
  );
1064
- server.tool("list-container-registry-auths", {}, async () => {
1065
- const result = await runpodRequest("/containerregistryauth");
1066
- return {
1067
- content: [
1068
- {
1069
- type: "text",
1070
- text: JSON.stringify(result, null, 2)
1071
- }
1072
- ]
1073
- };
1074
- });
1075
1852
  server.tool(
1076
1853
  "get-container-registry-auth",
1854
+ "Get one saved container-registry credential by id (the secret/password is not returned).",
1077
1855
  {
1078
- containerRegistryAuthId: import_zod.z.string().describe("ID of the container registry auth to retrieve")
1856
+ containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to retrieve")
1079
1857
  },
1858
+ { title: "Get container registry auth", ...READ_ONLY },
1080
1859
  async (params) => {
1081
- const result = await runpodRequest(
1082
- `/containerregistryauth/${params.containerRegistryAuthId}`
1860
+ const backend = backendFor("registries");
1861
+ const result = await callRestUrl(
1862
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`
1083
1863
  );
1084
- return {
1085
- content: [
1086
- {
1087
- type: "text",
1088
- text: JSON.stringify(result, null, 2)
1089
- }
1090
- ]
1091
- };
1864
+ return jsonReply2(result);
1092
1865
  }
1093
1866
  );
1094
1867
  server.tool(
1095
1868
  "create-container-registry-auth",
1869
+ "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1096
1870
  {
1097
- name: import_zod.z.string().describe("Name for the container registry auth"),
1098
- username: import_zod.z.string().describe("Registry username"),
1099
- password: import_zod.z.string().describe("Registry password")
1871
+ name: import_zod8.z.string().describe("Name for the container registry auth"),
1872
+ username: import_zod8.z.string().describe("Registry username"),
1873
+ password: import_zod8.z.string().describe("Registry password or access token (stored as a secret)")
1100
1874
  },
1875
+ { title: "Create container registry auth", ...WRITE },
1101
1876
  async (params) => {
1102
- const result = await runpodRequest(
1103
- "/containerregistryauth",
1877
+ const backend = backendFor("registries");
1878
+ const body = backend.mapCreate(params);
1879
+ const result = await callRestUrl(
1880
+ `${backend.base}${backend.list}`,
1104
1881
  "POST",
1105
- params
1882
+ body
1106
1883
  );
1107
- return {
1108
- content: [
1109
- {
1110
- type: "text",
1111
- text: JSON.stringify(result, null, 2)
1112
- }
1113
- ]
1114
- };
1884
+ return jsonReply2(result);
1115
1885
  }
1116
1886
  );
1117
1887
  server.tool(
1118
1888
  "delete-container-registry-auth",
1889
+ "Permanently delete a saved container-registry credential. This cannot be undone.",
1119
1890
  {
1120
- containerRegistryAuthId: import_zod.z.string().describe("ID of the container registry auth to delete")
1891
+ containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to delete")
1121
1892
  },
1893
+ { title: "Delete container registry auth", ...DESTRUCTIVE },
1122
1894
  async (params) => {
1123
- const result = await runpodRequest(
1124
- `/containerregistryauth/${params.containerRegistryAuthId}`,
1895
+ const backend = backendFor("registries");
1896
+ const result = await callRestUrl(
1897
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`,
1125
1898
  "DELETE"
1126
1899
  );
1127
- return {
1128
- content: [
1129
- {
1130
- type: "text",
1131
- text: JSON.stringify(result, null, 2)
1132
- }
1133
- ]
1134
- };
1900
+ return jsonReply2(result);
1901
+ }
1902
+ );
1903
+ }
1904
+
1905
+ // src/tools/tags.ts
1906
+ var import_zod9 = require("zod");
1907
+ var TAG_RESOURCE_TYPES = [
1908
+ "POD",
1909
+ "NETWORK_VOLUME",
1910
+ "CLUSTER",
1911
+ "SERVERLESS_ENDPOINT"
1912
+ ];
1913
+ var taggedResourceSchema = import_zod9.z.object({
1914
+ id: import_zod9.z.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
1915
+ type: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
1916
+ });
1917
+ function v2OnlyNotice(tool) {
1918
+ return {
1919
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
1920
+ status: 501
1921
+ };
1922
+ }
1923
+ function registerTagTools(server, rt) {
1924
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1925
+ server.tool(
1926
+ "list-tags",
1927
+ "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.",
1928
+ {
1929
+ ...listPaginationParams,
1930
+ includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to each tag in the response")
1931
+ },
1932
+ { title: "List tags", ...READ_ONLY },
1933
+ async (params) => {
1934
+ const backend = backendFor("tags");
1935
+ if (backend.version === "v1") return jsonReply2(v2OnlyNotice("list-tags"));
1936
+ const query = params.includeResources ? "?include=resources" : "";
1937
+ const raw = await callRestUrl(`${backend.base}${backend.list}${query}`);
1938
+ return capListResult(backend.unwrap(raw), {
1939
+ limit: params.limit,
1940
+ cursor: params.cursor
1941
+ });
1942
+ }
1943
+ );
1944
+ server.tool(
1945
+ "get-tag",
1946
+ "Get one tag by id, optionally including its attached resources. v2-only \u2014 returns a 501 notice on the v1 API.",
1947
+ {
1948
+ tagId: import_zod9.z.string().describe("ID of the tag to retrieve"),
1949
+ includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to this tag")
1950
+ },
1951
+ { title: "Get tag", ...READ_ONLY },
1952
+ async (params) => {
1953
+ const backend = backendFor("tags");
1954
+ if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
1955
+ const query = params.includeResources ? "?include=resources" : "";
1956
+ const result = await callRestUrl(
1957
+ `${backend.base}${backend.get(params.tagId)}${query}`
1958
+ );
1959
+ return jsonReply2(result);
1960
+ }
1961
+ );
1962
+ server.tool(
1963
+ "create-tag",
1964
+ "Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
1965
+ {
1966
+ key: import_zod9.z.string().min(1).describe('Tag key (e.g. "project")'),
1967
+ value: import_zod9.z.string().min(1).describe('Tag value (e.g. "gpt-training")'),
1968
+ resources: import_zod9.z.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
1969
+ },
1970
+ { title: "Create tag", ...WRITE },
1971
+ async (params) => {
1972
+ const backend = backendFor("tags");
1973
+ if (backend.version === "v1")
1974
+ return jsonReply2(v2OnlyNotice("create-tag"));
1975
+ const result = await callRestUrl(
1976
+ `${backend.base}${backend.list}`,
1977
+ "POST",
1978
+ params
1979
+ );
1980
+ return jsonReply2(result);
1981
+ }
1982
+ );
1983
+ server.tool(
1984
+ "update-tag",
1985
+ "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.",
1986
+ {
1987
+ tagId: import_zod9.z.string().describe("ID of the tag to update"),
1988
+ key: import_zod9.z.string().min(1).optional().describe("New tag key"),
1989
+ value: import_zod9.z.string().min(1).optional().describe("New tag value"),
1990
+ resources: import_zod9.z.array(taggedResourceSchema).optional().describe(
1991
+ "Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
1992
+ )
1993
+ },
1994
+ // No idempotentHint: replacing `resources` is non-atomic upstream, so a
1995
+ // retried-after-partial-failure update is not guaranteed to be a no-op.
1996
+ { title: "Update tag", ...WRITE },
1997
+ async (params) => {
1998
+ const { tagId, ...updateParams } = params;
1999
+ const backend = backendFor("tags");
2000
+ if (backend.version === "v1")
2001
+ return jsonReply2(v2OnlyNotice("update-tag"));
2002
+ const result = await callRestUrl(
2003
+ `${backend.base}${backend.get(tagId)}`,
2004
+ "PATCH",
2005
+ updateParams
2006
+ );
2007
+ return jsonReply2(result);
1135
2008
  }
1136
2009
  );
2010
+ server.tool(
2011
+ "delete-tag",
2012
+ "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.",
2013
+ {
2014
+ tagId: import_zod9.z.string().describe("ID of the tag to delete")
2015
+ },
2016
+ { title: "Delete tag", ...DESTRUCTIVE },
2017
+ async (params) => {
2018
+ const backend = backendFor("tags");
2019
+ if (backend.version === "v1")
2020
+ return jsonReply2(v2OnlyNotice("delete-tag"));
2021
+ const result = await callRestUrl(
2022
+ `${backend.base}${backend.get(params.tagId)}`,
2023
+ "DELETE"
2024
+ );
2025
+ return jsonReply2(result);
2026
+ }
2027
+ );
2028
+ server.tool(
2029
+ "attach-tag",
2030
+ "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.",
2031
+ {
2032
+ tagId: import_zod9.z.string().describe("ID of the tag"),
2033
+ resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
2034
+ resourceId: import_zod9.z.string().describe("ID of the resource to attach the tag to")
2035
+ },
2036
+ { title: "Attach tag to resource", ...WRITE, idempotentHint: true },
2037
+ async (params) => {
2038
+ const backend = backendFor("tags");
2039
+ if (backend.version === "v1")
2040
+ return jsonReply2(v2OnlyNotice("attach-tag"));
2041
+ const result = await callRestUrl(
2042
+ `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2043
+ "PUT"
2044
+ );
2045
+ return jsonReply2(result);
2046
+ }
2047
+ );
2048
+ server.tool(
2049
+ "detach-tag",
2050
+ "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.",
2051
+ {
2052
+ tagId: import_zod9.z.string().describe("ID of the tag"),
2053
+ resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
2054
+ resourceId: import_zod9.z.string().describe("ID of the resource to detach the tag from")
2055
+ },
2056
+ { title: "Detach tag from resource", ...WRITE, idempotentHint: true },
2057
+ async (params) => {
2058
+ const backend = backendFor("tags");
2059
+ if (backend.version === "v1")
2060
+ return jsonReply2(v2OnlyNotice("detach-tag"));
2061
+ const result = await callRestUrl(
2062
+ `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2063
+ "DELETE"
2064
+ );
2065
+ return jsonReply2(result);
2066
+ }
2067
+ );
2068
+ }
2069
+
2070
+ // src/tools/billing.ts
2071
+ var import_zod10 = require("zod");
2072
+ var BILLING_SCOPE_PATH = {
2073
+ all: "/billing",
2074
+ pods: "/billing/pods",
2075
+ serverless: "/billing/serverless",
2076
+ endpoints: "/billing/endpoints",
2077
+ networkvolumes: "/billing/networkvolumes",
2078
+ clusters: "/billing/clusters"
2079
+ };
2080
+ function registerBillingTools(server, rt) {
2081
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2082
+ server.tool(
2083
+ "get-billing",
2084
+ "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.",
2085
+ {
2086
+ ...listPaginationParams,
2087
+ scope: import_zod10.z.enum([
2088
+ "all",
2089
+ "pods",
2090
+ "serverless",
2091
+ "endpoints",
2092
+ "networkvolumes",
2093
+ "clusters"
2094
+ ]).optional().describe(
2095
+ 'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2096
+ ),
2097
+ startTime: import_zod10.z.string().optional().describe(
2098
+ "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2099
+ ),
2100
+ endTime: import_zod10.z.string().optional().describe(
2101
+ "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2102
+ ),
2103
+ bucketSize: import_zod10.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2104
+ lastN: import_zod10.z.number().int().positive().optional().describe(
2105
+ "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2106
+ )
2107
+ },
2108
+ { title: "Get billing history", ...READ_ONLY },
2109
+ async (params) => {
2110
+ const backend = backendFor("billing");
2111
+ if (backend.version === "v1") {
2112
+ return jsonReply2({
2113
+ error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2114
+ status: 501
2115
+ });
2116
+ }
2117
+ const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
2118
+ const query = new URLSearchParams();
2119
+ if (params.startTime) query.set("startTime", params.startTime);
2120
+ if (params.endTime) query.set("endTime", params.endTime);
2121
+ if (params.bucketSize) query.set("bucketSize", params.bucketSize);
2122
+ if (params.lastN !== void 0 && !params.startTime && !params.endTime)
2123
+ query.set("lastN", String(params.lastN));
2124
+ const qs = query.toString() ? `?${query.toString()}` : "";
2125
+ const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
2126
+ return capListResult(
2127
+ backend.unwrap(raw),
2128
+ { limit: params.limit, cursor: params.cursor },
2129
+ { metadata: raw?.metadata }
2130
+ );
2131
+ }
2132
+ );
2133
+ }
2134
+
2135
+ // src/tools.ts
2136
+ function registerTools(server, ctx, deps = {}) {
2137
+ const rt = createToolRuntime(server, ctx, deps);
2138
+ registerCatalogTools(server, rt);
2139
+ registerPodTools(server, rt);
2140
+ registerEndpointTools(server, rt);
2141
+ registerJobTools(server, rt);
2142
+ registerTemplateTools(server, rt);
2143
+ registerNetworkVolumeTools(server, rt);
2144
+ registerRegistryTools(server, rt);
2145
+ registerTagTools(server, rt);
2146
+ registerBillingTools(server, rt);
1137
2147
  }
1138
2148
  // Annotate the CommonJS export names for ESM import in node:
1139
2149
  0 && (module.exports = {