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