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