@runpod/mcp-server 1.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stdio.mjs CHANGED
@@ -2,15 +2,413 @@
2
2
 
3
3
  // src/stdio.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import fetch2 from "node-fetch";
5
6
 
6
7
  // src/server.ts
7
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+
10
+ // src/_shared/mappers.ts
11
+ function compact(obj) {
12
+ const out = {};
13
+ for (const [k, v] of Object.entries(obj)) {
14
+ if (v !== void 0) out[k] = v;
15
+ }
16
+ return out;
17
+ }
18
+ function persistentMount(size, path) {
19
+ if (size === void 0 || path === void 0) return void 0;
20
+ return { persistent: { size, path } };
21
+ }
22
+ function joinStartCmd(cmd) {
23
+ return cmd && cmd.length ? cmd.join(" ") : void 0;
24
+ }
25
+ function containerConfigToV2(p) {
26
+ return compact({
27
+ image: p.imageName,
28
+ disk: p.containerDiskInGb,
29
+ ports: p.ports,
30
+ env: p.env,
31
+ mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
32
+ });
33
+ }
34
+ function gpuConfigToV2(gpuTypeIds, gpuCount) {
35
+ const id = gpuTypeIds?.[0];
36
+ if (id === void 0) return void 0;
37
+ return compact({ id, count: gpuCount });
38
+ }
39
+ function mapPodCreateToV2(params) {
40
+ return compact({
41
+ ...containerConfigToV2(params),
42
+ name: params.name,
43
+ // Private-image registry credential (v2 ContainerConfig `registry`). When
44
+ // deploying from a template, this spreads over the template's registry, so
45
+ // passing it overrides the inherited credential. An empty string is an
46
+ // explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
47
+ // template's inherited credential. Undefined is dropped by compact, so an
48
+ // omitted param leaves the template's registry in place.
49
+ registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
50
+ cloud: params.cloudType,
51
+ // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
52
+ // centers; omit/empty = scheduler chooses). Pass the v1 list straight
53
+ // through — the earlier singular `dataCenter` key did not exist on the pod
54
+ // schema, so placement was being silently dropped. GPU, by contrast, IS
55
+ // singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
56
+ // create-pod handler warns when more than one gpuTypeId was supplied.
57
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
58
+ gpu: gpuConfigToV2(params.gpuTypeIds, params.gpuCount)
59
+ });
60
+ }
61
+ function mapPodUpdateToV2(params) {
62
+ return {
63
+ ...containerConfigToV2(params),
64
+ ...compact({ name: params.name })
65
+ };
66
+ }
67
+ function nestedOrUndefined(obj) {
68
+ const c = compact(obj);
69
+ return Object.keys(c).length ? c : void 0;
70
+ }
71
+ function endpointGpuConfig(pools, count) {
72
+ if (!pools?.length) return void 0;
73
+ return compact({ pools, count });
74
+ }
75
+ function mapEndpointToV2(params) {
76
+ return compact({
77
+ name: params.name,
78
+ image: params.imageName,
79
+ args: params.args,
80
+ disk: params.containerDiskInGb,
81
+ ports: params.ports,
82
+ env: params.env,
83
+ registry: params.containerRegistryAuthId,
84
+ gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
85
+ workers: nestedOrUndefined({
86
+ min: params.workersMin,
87
+ max: params.workersMax
88
+ }),
89
+ scaling: nestedOrUndefined({
90
+ type: params.scalerType,
91
+ value: params.scalerValue,
92
+ idleTimeout: params.idleTimeout
93
+ }),
94
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
95
+ networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
96
+ timeout: params.executionTimeoutMs,
97
+ flashboot: params.flashboot
98
+ });
99
+ }
100
+ var mapEndpointCreateToV2 = mapEndpointToV2;
101
+ var mapEndpointUpdateToV2 = mapEndpointToV2;
102
+ function mapNetworkVolumeCreateToV2(params) {
103
+ return compact({
104
+ name: params.name,
105
+ size: params.size,
106
+ dataCenter: params.dataCenterId,
107
+ type: params.volumeType
108
+ });
109
+ }
110
+ function mapTemplateCreateToV2(params) {
111
+ return {
112
+ ...containerConfigToV2(params),
113
+ ...compact({
114
+ name: params.name,
115
+ serverless: params.isServerless,
116
+ registry: params.containerRegistryAuthId,
117
+ args: joinStartCmd(params.dockerStartCmd),
118
+ // No longer required by v2 (it defaults to NVIDIA server-side), so an
119
+ // unset category is dropped by compact instead of being forced here.
120
+ category: params.category
121
+ })
122
+ };
123
+ }
124
+ function podBodyFromTemplate(template) {
125
+ const out = {};
126
+ const keys = [
127
+ "name",
128
+ "image",
129
+ "args",
130
+ "disk",
131
+ "ports",
132
+ "env",
133
+ "mounts",
134
+ "registry"
135
+ ];
136
+ for (const key of keys) {
137
+ const value = template[key];
138
+ if (value !== void 0 && value !== null) out[key] = value;
139
+ }
140
+ return out;
141
+ }
142
+ function mapTemplateUpdateToV2(params) {
143
+ return {
144
+ ...containerConfigToV2(params),
145
+ ...compact({
146
+ name: params.name,
147
+ registry: params.containerRegistryAuthId,
148
+ args: joinStartCmd(params.dockerStartCmd)
149
+ })
150
+ };
151
+ }
152
+
153
+ // src/_shared/backend.ts
154
+ var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
155
+ var V2_UNWRAP_KEY = {
156
+ pods: "pods",
157
+ templates: "templates",
158
+ networkVolumes: "networkVolumes",
159
+ registries: "registries",
160
+ gpus: "gpus",
161
+ cpus: "cpus",
162
+ dataCenters: "dataCenters",
163
+ endpoints: "endpoints",
164
+ jobs: "jobs",
165
+ workers: "workers",
166
+ billing: "records"
167
+ };
168
+ var RESOURCE_ENV_KEY = {
169
+ pods: "PODS",
170
+ templates: "TEMPLATES",
171
+ networkVolumes: "NETWORK_VOLUMES",
172
+ registries: "REGISTRIES",
173
+ gpus: "GPUS",
174
+ cpus: "CPUS",
175
+ dataCenters: "DATA_CENTERS",
176
+ endpoints: "ENDPOINTS",
177
+ jobs: "JOBS",
178
+ workers: "WORKERS",
179
+ billing: "BILLING"
180
+ };
181
+ function restV1Base(env) {
182
+ return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
183
+ }
184
+ function restV2Base(env) {
185
+ return env.RUNPOD_REST_V2_API_URL ?? "https://v2-rest.runpod.io/v2";
186
+ }
187
+ function serverlessBase(env) {
188
+ return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
189
+ }
190
+ function publicGraphqlBase(env) {
191
+ return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
192
+ }
193
+ function authedGraphqlBase(env) {
194
+ return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
195
+ }
196
+ function unwrapList(resource, version, raw) {
197
+ if (version === "v1") return Array.isArray(raw) ? raw : [];
198
+ if (raw === null || typeof raw !== "object") return [];
199
+ const arr = raw[V2_UNWRAP_KEY[resource]];
200
+ return Array.isArray(arr) ? arr : [];
201
+ }
202
+ function resolveVersion(opts) {
203
+ const { resource, env, ctx, v2Available } = opts;
204
+ if (V1_ONLY.has(resource)) return "v1";
205
+ const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
206
+ const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
207
+ if (setting === "v2") return "v2";
208
+ if (setting === "auto") {
209
+ if (ctx.transport === "http") return "v1";
210
+ return v2Available === true ? "v2" : "v1";
211
+ }
212
+ return "v1";
213
+ }
214
+ function wantsAutoProbe(env) {
215
+ return Object.entries(env).some(
216
+ ([k, v]) => k.startsWith("RUNPOD_REST_VERSION") && (v ?? "").toLowerCase() === "auto"
217
+ );
218
+ }
219
+ function restVersionSummary(env) {
220
+ const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
221
+ const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
222
+ const overrides = Object.keys(env).filter(
223
+ (k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
224
+ ).length;
225
+ return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
226
+ }
227
+ var PROBE_TIMEOUT_MS = 4e3;
228
+ function classifyProbeStatus(status) {
229
+ if (status >= 500) return void 0;
230
+ if (status === 401 || status === 403) return void 0;
231
+ return status >= 200 && status < 300;
232
+ }
233
+ async function runV2Probe(deps) {
234
+ try {
235
+ const res = await deps.fetch(`${deps.baseUrl}/catalog/gpus`, {
236
+ method: "GET",
237
+ headers: { Authorization: `Bearer ${deps.apiKey}` },
238
+ signal: AbortSignal.timeout(deps.timeoutMs ?? PROBE_TIMEOUT_MS)
239
+ });
240
+ const verdict = classifyProbeStatus(res.status);
241
+ if (verdict === void 0) {
242
+ deps.warn?.(
243
+ `v2 auto-probe: ${deps.baseUrl} returned ${res.status} (transient) \u2014 using v1 for now; will re-probe. Pin RUNPOD_REST_VERSION=v2 to force v2.`
244
+ );
245
+ }
246
+ return verdict;
247
+ } catch {
248
+ deps.warn?.(
249
+ `v2 auto-probe: ${deps.baseUrl} unreachable (timed out after ${deps.timeoutMs ?? PROBE_TIMEOUT_MS}ms or network error) \u2014 using v1 for now; will re-probe. Pin RUNPOD_REST_VERSION=v2 to force v2.`
250
+ );
251
+ return void 0;
252
+ }
253
+ }
254
+ function createV2Prober(deps) {
255
+ let cached;
256
+ let inFlight;
257
+ const probeOnce = async () => {
258
+ const verdict = await runV2Probe(deps);
259
+ if (verdict !== void 0) cached = verdict;
260
+ return verdict ?? false;
261
+ };
262
+ return () => {
263
+ if (cached !== void 0) return Promise.resolve(cached);
264
+ if (!inFlight) {
265
+ inFlight = probeOnce().finally(() => {
266
+ inFlight = void 0;
267
+ });
268
+ }
269
+ return inFlight;
270
+ };
271
+ }
272
+ var identity = (body) => body;
273
+ var V1_REST_PATHS = {
274
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
275
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
276
+ networkVolumes: {
277
+ list: "/networkvolumes",
278
+ get: (id) => `/networkvolumes/${id}`
279
+ },
280
+ registries: {
281
+ list: "/containerregistryauth",
282
+ get: (id) => `/containerregistryauth/${id}`
283
+ },
284
+ endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
285
+ };
286
+ var CATALOG = /* @__PURE__ */ new Set([
287
+ "gpus",
288
+ "cpus",
289
+ "dataCenters"
290
+ ]);
291
+ var V2_ONLY = /* @__PURE__ */ new Set([
292
+ "workers",
293
+ "billing"
294
+ ]);
295
+ var V2_REST_PATHS = {
296
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
297
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
298
+ networkVolumes: {
299
+ list: "/network-volumes",
300
+ get: (id) => `/network-volumes/${id}`
301
+ },
302
+ registries: { list: "/registries", get: (id) => `/registries/${id}` },
303
+ gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
304
+ cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
305
+ dataCenters: {
306
+ list: "/catalog/datacenters",
307
+ get: (id) => `/catalog/datacenters/${id}`
308
+ },
309
+ // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
310
+ // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
311
+ // unwrap key stays correct.
312
+ endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
313
+ // workers/billing build their own sub-paths in the tool from `base`; the
314
+ // list entry just satisfies buildV2Backend (which requires a paths entry).
315
+ workers: { list: "/serverless" },
316
+ billing: { list: "/billing" }
317
+ };
318
+ var V2_MAPPERS = {
319
+ pods: {
320
+ create: (b) => mapPodCreateToV2(b),
321
+ update: (b) => mapPodUpdateToV2(b)
322
+ },
323
+ templates: {
324
+ create: (b) => mapTemplateCreateToV2(b),
325
+ update: (b) => mapTemplateUpdateToV2(b)
326
+ },
327
+ networkVolumes: {
328
+ create: (b) => mapNetworkVolumeCreateToV2(
329
+ b
330
+ ),
331
+ update: identity
332
+ },
333
+ registries: { create: identity, update: identity },
334
+ endpoints: {
335
+ create: (b) => mapEndpointCreateToV2(b),
336
+ update: (b) => mapEndpointUpdateToV2(b)
337
+ }
338
+ };
339
+ function buildV2Backend(resource, env, unwrap) {
340
+ const paths = V2_REST_PATHS[resource];
341
+ if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
342
+ const mappers = V2_MAPPERS[resource];
343
+ return {
344
+ kind: "rest",
345
+ version: "v2",
346
+ base: restV2Base(env),
347
+ list: paths.list,
348
+ get: paths.get,
349
+ unwrap,
350
+ mapCreate: mappers?.create ?? identity,
351
+ mapUpdate: mappers?.update ?? identity
352
+ };
353
+ }
354
+ function buildV1Backend(resource, env, unwrap) {
355
+ if (resource === "jobs") {
356
+ return {
357
+ kind: "rest",
358
+ version: "v1",
359
+ base: serverlessBase(env),
360
+ unwrap,
361
+ mapCreate: identity,
362
+ mapUpdate: identity
363
+ };
364
+ }
365
+ if (V2_ONLY.has(resource)) {
366
+ return {
367
+ kind: "rest",
368
+ version: "v1",
369
+ base: restV1Base(env),
370
+ unwrap,
371
+ mapCreate: identity,
372
+ mapUpdate: identity
373
+ };
374
+ }
375
+ if (CATALOG.has(resource)) {
376
+ return {
377
+ kind: "graphql",
378
+ version: "v1",
379
+ base: publicGraphqlBase(env),
380
+ unwrap,
381
+ mapCreate: identity,
382
+ mapUpdate: identity
383
+ };
384
+ }
385
+ const paths = V1_REST_PATHS[resource];
386
+ if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
387
+ return {
388
+ kind: "rest",
389
+ version: "v1",
390
+ base: restV1Base(env),
391
+ list: paths.list,
392
+ get: paths.get,
393
+ unwrap,
394
+ mapCreate: identity,
395
+ mapUpdate: identity
396
+ };
397
+ }
398
+ function resolveBackend(opts) {
399
+ const { resource, env } = opts;
400
+ const version = resolveVersion(opts);
401
+ const unwrap = (raw) => unwrapList(resource, version, raw);
402
+ return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
403
+ }
404
+
405
+ // src/server.ts
8
406
  var SERVER_NAME = "Runpod API Server";
9
- var SERVER_VERSION = true ? "1.4.0" : "dev";
407
+ var SERVER_VERSION = true ? "3.0.0" : "dev";
10
408
  function createServer(version = SERVER_VERSION) {
11
409
  return new McpServer({
12
410
  name: SERVER_NAME,
13
- version,
411
+ version: `${version} [${restVersionSummary(process.env)}]`,
14
412
  capabilities: {
15
413
  resources: {},
16
414
  tools: {}
@@ -18,20 +416,347 @@ function createServer(version = SERVER_VERSION) {
18
416
  });
19
417
  }
20
418
 
21
- // src/tools.ts
22
- import { z as z2 } from "zod";
419
+ // src/tools/runtime.ts
23
420
  import fetch from "node-fetch";
24
421
  import { randomUUID } from "crypto";
25
422
 
26
- // src/pagination.ts
423
+ // src/_shared/http.ts
424
+ var HttpError = class extends Error {
425
+ constructor(prefix, status, body) {
426
+ const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
427
+ super(`${prefix}: ${status} - ${body}${hint}`);
428
+ this.name = "HttpError";
429
+ this.status = status;
430
+ this.body = body;
431
+ }
432
+ };
433
+ function isJsonContentType(contentType) {
434
+ if (!contentType) return false;
435
+ const ct = contentType.toLowerCase();
436
+ return ct.includes("application/json") || ct.includes("+json");
437
+ }
438
+ function methodSendsBody(method) {
439
+ return method === "POST" || method === "PATCH" || method === "PUT";
440
+ }
441
+ function buildRequestHeaders(apiKey, tracking) {
442
+ return {
443
+ Authorization: `Bearer ${apiKey}`,
444
+ "Content-Type": "application/json",
445
+ ...tracking()
446
+ };
447
+ }
448
+ function createHttpClient(deps) {
449
+ return async function request(url, method = "GET", body) {
450
+ const init = {
451
+ method,
452
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
453
+ };
454
+ if (body && methodSendsBody(method)) {
455
+ init.body = JSON.stringify(body);
456
+ }
457
+ const response = await deps.fetch(url, init);
458
+ if (!response.ok) {
459
+ throw new HttpError(
460
+ deps.errorPrefix,
461
+ response.status,
462
+ await response.text()
463
+ );
464
+ }
465
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
466
+ };
467
+ }
468
+
469
+ // src/_shared/tracking.ts
470
+ function sanitizeUaToken(value) {
471
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
472
+ }
473
+ function buildTrackingHeaders(input) {
474
+ const name = sanitizeUaToken(input.clientName || "unknown");
475
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
476
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
477
+ return {
478
+ "User-Agent": userAgent,
479
+ "X-Runpod-Session-Id": input.sessionId
480
+ };
481
+ }
482
+
483
+ // src/tools/logs.ts
27
484
  import { z } from "zod";
485
+ function parseLogSse(raw) {
486
+ const items = [];
487
+ for (const block of raw.split(/\r?\n\r?\n/)) {
488
+ const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
489
+ if (!dataLines.length) continue;
490
+ const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
491
+ if (!payload.trim()) continue;
492
+ try {
493
+ const parsed = JSON.parse(payload);
494
+ items.push(
495
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
496
+ );
497
+ } catch {
498
+ items.push({ raw: payload });
499
+ }
500
+ }
501
+ return items;
502
+ }
503
+ var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
504
+ var LOG_STREAM_MAX_BYTES = 256 * 1024;
505
+ async function readSseSnapshot(fetchImpl, url, headers, opts) {
506
+ const controller = new AbortController();
507
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
508
+ const chunks = [];
509
+ let bytes = 0;
510
+ let truncated = false;
511
+ try {
512
+ const res = await fetchImpl(url, {
513
+ method: "GET",
514
+ headers,
515
+ signal: controller.signal
516
+ });
517
+ if (!res.ok) {
518
+ throw new HttpError(
519
+ "Runpod API Error",
520
+ res.status,
521
+ await res.text().catch(() => "")
522
+ );
523
+ }
524
+ for await (const chunk of res.body ?? []) {
525
+ chunks.push(chunk);
526
+ bytes += chunk.length;
527
+ if (bytes >= opts.maxBytes) {
528
+ truncated = true;
529
+ controller.abort();
530
+ break;
531
+ }
532
+ }
533
+ } catch (err) {
534
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
535
+ } finally {
536
+ clearTimeout(timer);
537
+ }
538
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
539
+ }
540
+ var logStreamParams = {
541
+ source: z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
542
+ tail: z.number().int().min(0).max(5e3).optional().describe(
543
+ "Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
544
+ ),
545
+ since: z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
546
+ maxWaitMs: z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
547
+ };
548
+ async function collectLogSnapshot(streamSse, logsUrl, params) {
549
+ const qs = new URLSearchParams();
550
+ if (params.source && params.source !== "both")
551
+ qs.append("source", params.source);
552
+ if (params.tail !== void 0) qs.append("tail", String(params.tail));
553
+ if (params.since) qs.append("since", params.since);
554
+ const query = qs.toString() ? `?${qs}` : "";
555
+ const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
556
+ maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
557
+ maxBytes: LOG_STREAM_MAX_BYTES
558
+ });
559
+ const items = parseLogSse(raw);
560
+ if (truncated) items.pop();
561
+ return { items, count: items.length, truncated };
562
+ }
563
+ async function streamLogsReply(rt, tool, params) {
564
+ const backend = rt.backendFor(tool.resource);
565
+ if (backend.version === "v1") {
566
+ return rt.jsonReply({
567
+ error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
568
+ status: 501
569
+ });
570
+ }
571
+ try {
572
+ return rt.jsonReply(
573
+ await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
574
+ );
575
+ } catch (error) {
576
+ if (error instanceof HttpError) {
577
+ return rt.jsonReply({ error: error.message, status: error.status });
578
+ }
579
+ throw error;
580
+ }
581
+ }
582
+
583
+ // src/tools/runtime.ts
584
+ var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
585
+ var SESSION_ID = randomUUID();
586
+ function trackingHeaders(server, ctx) {
587
+ const info = server.server.getClientVersion();
588
+ return buildTrackingHeaders({
589
+ clientName: info?.name || ctx.clientUserAgent || "unknown",
590
+ clientVersion: info?.version || "unknown",
591
+ transport: ctx.transport,
592
+ // Prefer a server version supplied by the caller (the hosted entrypoint reads
593
+ // it from package.json at runtime, since tsup's build-time define does not
594
+ // run when Vercel compiles api/index.ts). Fall back to the build-time value.
595
+ serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
596
+ sessionId: SESSION_ID
597
+ });
598
+ }
599
+ var defaultFetch = fetch;
600
+ async function withApiErrorLog(label, call) {
601
+ try {
602
+ return await call();
603
+ } catch (error) {
604
+ console.error(label, error);
605
+ throw error;
606
+ }
607
+ }
608
+ function jsonReply(result) {
609
+ return {
610
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
611
+ };
612
+ }
613
+ var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
614
+ var WRITE = { readOnlyHint: false, openWorldHint: true };
615
+ var DESTRUCTIVE = {
616
+ readOnlyHint: false,
617
+ destructiveHint: true,
618
+ idempotentHint: true,
619
+ openWorldHint: true
620
+ };
621
+ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
622
+ const response = await fetchImpl(url, {
623
+ method: "POST",
624
+ headers: {
625
+ "Content-Type": "application/json",
626
+ ...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
627
+ ...tracking()
628
+ },
629
+ body: JSON.stringify({
630
+ query,
631
+ ...options?.variables ? { variables: options.variables } : {}
632
+ })
633
+ });
634
+ const result = await response.json();
635
+ if (result.errors && result.errors.length > 0) {
636
+ throw new Error(
637
+ `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
638
+ );
639
+ }
640
+ if (!result.data) {
641
+ throw new Error("No data returned from GraphQL query");
642
+ }
643
+ return result.data;
644
+ }
645
+ function createToolRuntime(server, ctx, deps = {}) {
646
+ const tracking = () => trackingHeaders(server, ctx);
647
+ function observeUnauthorized(fn) {
648
+ const notify = ctx.onUnauthorized;
649
+ if (!notify) return fn;
650
+ return async (...args) => {
651
+ const response = await fn(...args);
652
+ if (response.status === 401) notify();
653
+ return response;
654
+ };
655
+ }
656
+ const rawFetch = deps.fetch ?? defaultFetch;
657
+ const httpFetch = observeUnauthorized(rawFetch);
658
+ const v1Client = createHttpClient({
659
+ apiKey: ctx.apiKey,
660
+ fetch: httpFetch,
661
+ tracking,
662
+ errorPrefix: "Runpod API Error"
663
+ });
664
+ const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
665
+ "Error calling Runpod API:",
666
+ () => v1Client(`${restV1Base(process.env)}${endpoint}`, method, body)
667
+ );
668
+ const serverlessClient = createHttpClient({
669
+ apiKey: ctx.apiKey,
670
+ fetch: httpFetch,
671
+ tracking,
672
+ errorPrefix: "Runpod Serverless API Error"
673
+ });
674
+ const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
675
+ "Error calling Runpod Serverless API:",
676
+ () => serverlessClient(
677
+ `${serverlessBase(process.env)}/${endpointId}${path}`,
678
+ method,
679
+ body
680
+ )
681
+ );
682
+ const restClient = createHttpClient({
683
+ apiKey: ctx.apiKey,
684
+ fetch: httpFetch,
685
+ tracking,
686
+ errorPrefix: "Runpod API Error"
687
+ });
688
+ const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
689
+ "Error calling Runpod API:",
690
+ () => restClient(url, method, body)
691
+ );
692
+ const sseFetch = observeUnauthorized(
693
+ deps.sseFetch ?? fetch
694
+ );
695
+ const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
696
+ sseFetch,
697
+ url,
698
+ {
699
+ Authorization: `Bearer ${ctx.apiKey}`,
700
+ Accept: "text/event-stream",
701
+ ...tracking()
702
+ },
703
+ opts
704
+ ));
705
+ const backendFor = (resource) => resolveBackend({
706
+ resource,
707
+ env: process.env,
708
+ ctx: { transport: ctx.transport },
709
+ v2Available: deps.v2Available
710
+ });
711
+ const podAction = async (podId, action) => {
712
+ const backend = backendFor("pods");
713
+ const podPath = backend.get(podId);
714
+ if (backend.version === "v2") {
715
+ return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
716
+ action
717
+ });
718
+ }
719
+ return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
720
+ };
721
+ return {
722
+ jsonReply,
723
+ graphql: (query) => graphqlRequest(
724
+ query,
725
+ tracking,
726
+ // rawFetch, not httpFetch: this call carries no credential (see above).
727
+ rawFetch,
728
+ publicGraphqlBase(process.env)
729
+ ),
730
+ graphqlAuthed: (query, variables) => graphqlRequest(
731
+ query,
732
+ tracking,
733
+ httpFetch,
734
+ // NOT publicGraphqlBase: this call carries the caller's API key.
735
+ authedGraphqlBase(process.env),
736
+ { variables, apiKey: ctx.apiKey }
737
+ ),
738
+ runpodRequest,
739
+ serverlessRequest,
740
+ callRestUrl,
741
+ backendFor,
742
+ streamSse,
743
+ podAction,
744
+ env: process.env
745
+ };
746
+ }
747
+
748
+ // src/tools/catalog.ts
749
+ import { z as z3 } from "zod";
750
+
751
+ // src/pagination.ts
752
+ import { z as z2 } from "zod";
28
753
  var DEFAULT_LIST_LIMIT = 20;
29
754
  var MAX_LIST_LIMIT = 100;
30
755
  var listPaginationParams = {
31
- limit: z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
756
+ limit: z2.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
32
757
  `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
33
758
  ),
34
- cursor: z.string().optional().describe(
759
+ cursor: z2.string().optional().describe(
35
760
  "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
36
761
  )
37
762
  };
@@ -48,7 +773,7 @@ function decodeCursorOffset(cursor) {
48
773
  function encodeCursorOffset(offset) {
49
774
  return Buffer.from(String(offset), "utf8").toString("base64");
50
775
  }
51
- function capListResult(result, options) {
776
+ function capListResult(result, options, extra) {
52
777
  const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
53
778
  const offset = decodeCursorOffset(options.cursor);
54
779
  if (!Array.isArray(result)) {
@@ -62,7 +787,11 @@ function capListResult(result, options) {
62
787
  const page = result.slice(offset, offset + limit);
63
788
  const nextOffset = offset + page.length;
64
789
  const hasMore = nextOffset < total;
790
+ const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
791
+ void _i;
792
+ void _p;
65
793
  const envelope = {
794
+ ...safeExtra,
66
795
  items: page,
67
796
  pagination: {
68
797
  total,
@@ -80,133 +809,67 @@ function capListResult(result, options) {
80
809
  };
81
810
  }
82
811
 
83
- // src/tools.ts
84
- var API_BASE_URL = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
85
- var SERVERLESS_API_BASE_URL = process.env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
86
- var GRAPHQL_URL = process.env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
87
- var MCP_SERVER_VERSION = true ? "1.4.0" : "dev";
88
- var SESSION_ID = randomUUID();
89
- function sanitizeUaToken(value) {
90
- return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
91
- }
92
- function trackingHeaders(server, ctx) {
93
- const info = server.server.getClientVersion();
94
- const fallbackName = ctx.clientUserAgent || "unknown";
95
- const name = sanitizeUaToken(info?.name || fallbackName);
96
- const version = sanitizeUaToken(info?.version || "unknown");
97
- const serverVersion = ctx.serverVersion || MCP_SERVER_VERSION;
98
- const userAgent = `runpod-mcp-server/${serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${ctx.transport})`;
99
- return {
100
- "User-Agent": userAgent,
101
- "X-Runpod-Session-Id": SESSION_ID
102
- };
103
- }
104
- async function graphqlRequest(query, tracking) {
105
- const response = await fetch(GRAPHQL_URL, {
106
- method: "POST",
107
- headers: {
108
- "Content-Type": "application/json",
109
- ...tracking()
110
- },
111
- body: JSON.stringify({ query })
112
- });
113
- const result = await response.json();
114
- if (result.errors && result.errors.length > 0) {
115
- throw new Error(
116
- `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
117
- );
118
- }
119
- if (!result.data) {
120
- throw new Error("No data returned from GraphQL query");
121
- }
122
- return result.data;
123
- }
124
- function createRunpodRequest(apiKey, tracking) {
125
- return async function runpodRequest(endpoint, method = "GET", body) {
126
- const url = `${API_BASE_URL}${endpoint}`;
127
- const headers = {
128
- Authorization: `Bearer ${apiKey}`,
129
- "Content-Type": "application/json",
130
- ...tracking()
131
- };
132
- const options = {
133
- method,
134
- headers
135
- };
136
- if (body && (method === "POST" || method === "PATCH")) {
137
- options.body = JSON.stringify(body);
138
- }
139
- try {
140
- const response = await fetch(url, options);
141
- if (!response.ok) {
142
- const errorText = await response.text();
143
- throw new Error(`Runpod API Error: ${response.status} - ${errorText}`);
144
- }
145
- const contentType = response.headers.get("content-type");
146
- if (contentType && contentType.includes("application/json")) {
147
- return await response.json();
148
- }
149
- return { success: true, status: response.status };
150
- } catch (error) {
151
- console.error("Error calling Runpod API:", error);
152
- throw error;
153
- }
154
- };
155
- }
156
- function createServerlessRequest(apiKey, tracking) {
157
- return async function serverlessRequest(endpointId, path, method = "GET", body) {
158
- const url = `${SERVERLESS_API_BASE_URL}/${endpointId}${path}`;
159
- const headers = {
160
- Authorization: `Bearer ${apiKey}`,
161
- "Content-Type": "application/json",
162
- ...tracking()
163
- };
164
- const options = {
165
- method,
166
- headers
167
- };
168
- if (body && (method === "POST" || method === "PATCH")) {
169
- options.body = JSON.stringify(body);
170
- }
171
- try {
172
- const response = await fetch(url, options);
173
- if (!response.ok) {
174
- const errorText = await response.text();
175
- throw new Error(
176
- `Runpod Serverless API Error: ${response.status} - ${errorText}`
177
- );
178
- }
179
- const contentType = response.headers.get("content-type");
180
- if (contentType && contentType.includes("application/json")) {
181
- return await response.json();
182
- }
183
- return { success: true, status: response.status };
184
- } catch (error) {
185
- console.error("Error calling Runpod Serverless API:", error);
186
- throw error;
187
- }
188
- };
189
- }
190
- function registerTools(server, ctx) {
191
- const tracking = () => trackingHeaders(server, ctx);
192
- const runpodRequest = createRunpodRequest(ctx.apiKey, tracking);
193
- const serverlessRequest = createServerlessRequest(ctx.apiKey, tracking);
812
+ // src/tools/catalog.ts
813
+ function registerCatalogTools(server, rt) {
814
+ const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
194
815
  server.tool(
195
816
  "list-gpu-types",
817
+ "List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. By default the full catalog is returned: each result includes an `availability` summary (HIGH/MEDIUM/LOW/NONE) and results are sorted with the most-available GPUs first, but nothing is hidden. Set includeUnavailable:false to drop out-of-stock GPUs and list only deployable ones (set includeAvailability:false to skip the stock lookup entirely). For per-datacenter availability (to pick a dataCenterIds), use get-gpu-type.",
196
818
  {
197
- minMemoryGb: z2.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
198
- secureCloudOnly: z2.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
199
- communityCloudOnly: z2.boolean().optional().describe("Filter to only GPUs available in community cloud"),
200
- searchTerm: z2.string().optional().describe(
819
+ ...listPaginationParams,
820
+ minMemoryGb: z3.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
821
+ secureCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
822
+ communityCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in community cloud"),
823
+ searchTerm: z3.string().optional().describe(
201
824
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
202
825
  ),
203
- includeUnavailable: z2.boolean().optional().describe(
204
- "Include GPUs that are currently out of stock. Default is false"
826
+ includeUnavailable: z3.boolean().optional().describe(
827
+ "Out-of-stock GPUs are included by default (annotated availability:NONE and sorted last). Set false to hide them and list only currently-deployable GPUs."
828
+ ),
829
+ includeAvailability: z3.boolean().optional().describe(
830
+ "Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
205
831
  )
206
832
  },
833
+ { title: "List GPU types", ...READ_ONLY },
207
834
  async (params) => {
208
- const data = await graphqlRequest(
209
- `
835
+ const backend = backendFor("gpus");
836
+ if (backend.version === "v2") {
837
+ const wantAvailability = params.includeAvailability !== false;
838
+ const raw = await callRestUrl(
839
+ `${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
840
+ );
841
+ let gpus = backend.unwrap(raw);
842
+ gpus = gpus.filter((g) => g.id !== "unknown");
843
+ if (params.minMemoryGb !== void 0)
844
+ gpus = gpus.filter(
845
+ (g) => Number(g.memory ?? 0) >= params.minMemoryGb
846
+ );
847
+ if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
848
+ if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
849
+ if (params.searchTerm) {
850
+ const t = params.searchTerm.toLowerCase();
851
+ gpus = gpus.filter(
852
+ (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
853
+ );
854
+ }
855
+ if (params.includeUnavailable === false)
856
+ gpus = gpus.filter((g) => g.availability !== "NONE");
857
+ const rank = {
858
+ HIGH: 3,
859
+ MEDIUM: 2,
860
+ LOW: 1,
861
+ NONE: 0
862
+ };
863
+ gpus.sort(
864
+ (a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
865
+ );
866
+ gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
867
+ return capListResult(gpus, {
868
+ limit: params.limit,
869
+ cursor: params.cursor
870
+ });
871
+ }
872
+ const data = await graphql(`
210
873
  query {
211
874
  gpuTypes {
212
875
  id
@@ -219,9 +882,7 @@ function registerTools(server, ctx) {
219
882
  }
220
883
  }
221
884
  }
222
- `,
223
- tracking
224
- );
885
+ `);
225
886
  const stockPriority = {
226
887
  High: 3,
227
888
  Medium: 2,
@@ -232,7 +893,7 @@ function registerTools(server, ctx) {
232
893
  return !!status && status !== "Out";
233
894
  };
234
895
  let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
235
- if (!params.includeUnavailable) {
896
+ if (params.includeUnavailable === false) {
236
897
  gpuTypes = gpuTypes.filter(isAvailable);
237
898
  }
238
899
  if (params.minMemoryGb) {
@@ -267,26 +928,39 @@ function registerTools(server, ctx) {
267
928
  stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
268
929
  available: isAvailable(gpu)
269
930
  }));
270
- return {
271
- content: [
272
- {
273
- type: "text",
274
- text: JSON.stringify(result, null, 2)
275
- }
276
- ]
277
- };
931
+ return capListResult(result, {
932
+ limit: params.limit,
933
+ cursor: params.cursor
934
+ });
278
935
  }
279
936
  );
280
937
  server.tool(
281
938
  "list-data-centers",
939
+ "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
282
940
  {
283
- region: z2.string().optional().describe(
941
+ ...listPaginationParams,
942
+ region: z3.string().optional().describe(
284
943
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
285
944
  )
286
945
  },
946
+ { title: "List data centers", ...READ_ONLY },
287
947
  async (params) => {
288
- const data = await graphqlRequest(
289
- `
948
+ const backend = backendFor("dataCenters");
949
+ if (backend.version === "v2") {
950
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
951
+ let dcs = backend.unwrap(raw);
952
+ if (params.region) {
953
+ const t = params.region.toLowerCase();
954
+ dcs = dcs.filter(
955
+ (dc) => String(dc.region ?? "").toLowerCase().includes(t)
956
+ );
957
+ }
958
+ return capListResult(dcs, {
959
+ limit: params.limit,
960
+ cursor: params.cursor
961
+ });
962
+ }
963
+ const data = await graphql(`
290
964
  query {
291
965
  dataCenters {
292
966
  id
@@ -294,9 +968,7 @@ function registerTools(server, ctx) {
294
968
  location
295
969
  }
296
970
  }
297
- `,
298
- tracking
299
- );
971
+ `);
300
972
  let dataCenters = data.dataCenters;
301
973
  if (params.region) {
302
974
  const term = params.region.toLowerCase();
@@ -304,50 +976,720 @@ function registerTools(server, ctx) {
304
976
  (dc) => dc.location.toLowerCase().includes(term)
305
977
  );
306
978
  }
307
- return {
308
- content: [
309
- {
310
- type: "text",
311
- text: JSON.stringify(dataCenters, null, 2)
979
+ return capListResult(dataCenters, {
980
+ limit: params.limit,
981
+ cursor: params.cursor
982
+ });
983
+ }
984
+ );
985
+ server.tool(
986
+ "list-cpu-types",
987
+ "List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
988
+ { ...listPaginationParams },
989
+ { title: "List CPU types", ...READ_ONLY },
990
+ async (params) => {
991
+ const backend = backendFor("cpus");
992
+ if (backend.version === "v1") {
993
+ return jsonReply2({
994
+ error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
995
+ status: 501
996
+ });
997
+ }
998
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
999
+ return capListResult(backend.unwrap(raw), {
1000
+ limit: params.limit,
1001
+ cursor: params.cursor
1002
+ });
1003
+ }
1004
+ );
1005
+ server.tool(
1006
+ "get-gpu-type",
1007
+ "Get details for a single GPU type by id, including per-datacenter availability. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there). Use the returned dataCenters[].availability to pick a dataCenterIds with stock before creating a pod.",
1008
+ {
1009
+ gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
1010
+ includeAvailability: z3.boolean().optional().describe(
1011
+ "Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
1012
+ )
1013
+ },
1014
+ { title: "Get GPU type", ...READ_ONLY },
1015
+ async (params) => {
1016
+ const backend = backendFor("gpus");
1017
+ if (backend.version === "v1") {
1018
+ return jsonReply2({
1019
+ error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
1020
+ status: 501
1021
+ });
1022
+ }
1023
+ const path = backend.get(encodeURIComponent(params.gpuTypeId));
1024
+ const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
1025
+ const result = await callRestUrl(`${backend.base}${path}${query}`);
1026
+ return jsonReply2(result);
1027
+ }
1028
+ );
1029
+ server.tool(
1030
+ "get-cpu-type",
1031
+ "Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
1032
+ {
1033
+ cpuTypeId: z3.string().describe("ID of the CPU type to retrieve")
1034
+ },
1035
+ { title: "Get CPU type", ...READ_ONLY },
1036
+ async (params) => {
1037
+ const backend = backendFor("cpus");
1038
+ if (backend.version === "v1") {
1039
+ return jsonReply2({
1040
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1041
+ status: 501
1042
+ });
1043
+ }
1044
+ const result = await callRestUrl(
1045
+ `${backend.base}${backend.get(params.cpuTypeId)}`
1046
+ );
1047
+ return jsonReply2(result);
1048
+ }
1049
+ );
1050
+ server.tool(
1051
+ "get-data-center",
1052
+ "Get details for a single data center by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-data-centers there).",
1053
+ {
1054
+ dataCenterId: z3.string().describe("ID of the data center to retrieve")
1055
+ },
1056
+ { title: "Get data center", ...READ_ONLY },
1057
+ async (params) => {
1058
+ const backend = backendFor("dataCenters");
1059
+ if (backend.version === "v1") {
1060
+ return jsonReply2({
1061
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1062
+ status: 501
1063
+ });
1064
+ }
1065
+ const result = await callRestUrl(
1066
+ `${backend.base}${backend.get(params.dataCenterId)}`
1067
+ );
1068
+ return jsonReply2(result);
1069
+ }
1070
+ );
1071
+ }
1072
+
1073
+ // src/tools/hub.ts
1074
+ import { z as z4 } from "zod";
1075
+ function parseReleaseConfig(config) {
1076
+ if (!config) return void 0;
1077
+ try {
1078
+ return JSON.parse(config);
1079
+ } catch {
1080
+ return config;
1081
+ }
1082
+ }
1083
+ function listingsQuery(withConfig) {
1084
+ return `
1085
+ query {
1086
+ listings(input: {}) {
1087
+ id
1088
+ repoId
1089
+ title
1090
+ description
1091
+ repoName
1092
+ repoOwner
1093
+ createdAt
1094
+ updatedAt
1095
+ views
1096
+ stars
1097
+ deploys
1098
+ language
1099
+ category
1100
+ tags
1101
+ type
1102
+ listedRelease {
1103
+ id
1104
+ name
1105
+ tagName
1106
+ createdAt${withConfig ? "\n config" : ""}
1107
+ build {
1108
+ id
1109
+ imageName
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ `;
1115
+ }
1116
+ function serializeBoolean(value, input) {
1117
+ if (value === "true") return input.trueValue ?? "true";
1118
+ if (value === "false") return input.falseValue ?? "false";
1119
+ return value;
1120
+ }
1121
+ function buildHubEnv(config, overrides) {
1122
+ const env = [];
1123
+ const missingRequired = [];
1124
+ const remaining = { ...overrides };
1125
+ for (const entry of config.env ?? []) {
1126
+ const input = entry.input ?? {};
1127
+ const override = entry.key in remaining ? remaining[entry.key] : void 0;
1128
+ delete remaining[entry.key];
1129
+ if (override !== void 0 && override !== "") {
1130
+ env.push({
1131
+ key: entry.key,
1132
+ value: input.type === "boolean" ? serializeBoolean(override, input) : override
1133
+ });
1134
+ continue;
1135
+ }
1136
+ if (input.default !== void 0 && input.default !== null) {
1137
+ env.push({
1138
+ key: entry.key,
1139
+ value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
1140
+ });
1141
+ continue;
1142
+ }
1143
+ if (input.required) {
1144
+ missingRequired.push(entry.key);
1145
+ env.push({ key: entry.key, value: "" });
1146
+ continue;
1147
+ }
1148
+ }
1149
+ for (const [key, value] of Object.entries(remaining)) {
1150
+ env.push({ key, value });
1151
+ }
1152
+ return { env, missingRequired };
1153
+ }
1154
+ function minCudaVersion(versions) {
1155
+ if (!versions || versions.length === 0) return null;
1156
+ const compare = (a, b) => {
1157
+ const pa = a.split(".").map(Number);
1158
+ const pb = b.split(".").map(Number);
1159
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1160
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1161
+ if (d !== 0) return d;
1162
+ }
1163
+ return 0;
1164
+ };
1165
+ return [...versions].sort(compare)[0];
1166
+ }
1167
+ function randomSuffix() {
1168
+ return Math.random().toString(36).slice(2, 8);
1169
+ }
1170
+ function registerHubTools(server, rt) {
1171
+ const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
1172
+ server.tool(
1173
+ "list-hub-repos",
1174
+ "List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
1175
+ {
1176
+ ...listPaginationParams,
1177
+ searchTerm: z4.string().optional().describe(
1178
+ "Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
1179
+ ),
1180
+ category: z4.string().optional().describe(
1181
+ "Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
1182
+ ),
1183
+ type: z4.enum(["SERVERLESS", "POD"]).optional().describe(
1184
+ "Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
1185
+ ),
1186
+ repoOwner: z4.string().optional().describe(
1187
+ "Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
1188
+ ),
1189
+ includeConfig: z4.boolean().optional().describe(
1190
+ "Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
1191
+ )
1192
+ },
1193
+ { title: "List Hub repos", ...READ_ONLY },
1194
+ async (params) => {
1195
+ const data = await graphql(
1196
+ listingsQuery(params.includeConfig === true)
1197
+ );
1198
+ let listings = data.listings;
1199
+ if (params.type) {
1200
+ listings = listings.filter(
1201
+ (l) => (l.type ?? "").toUpperCase() === params.type
1202
+ );
1203
+ }
1204
+ if (params.category) {
1205
+ const term = params.category.toLowerCase();
1206
+ listings = listings.filter(
1207
+ (l) => (l.category ?? "").toLowerCase() === term
1208
+ );
1209
+ }
1210
+ if (params.repoOwner) {
1211
+ const term = params.repoOwner.toLowerCase();
1212
+ listings = listings.filter(
1213
+ (l) => (l.repoOwner ?? "").toLowerCase() === term
1214
+ );
1215
+ }
1216
+ if (params.searchTerm) {
1217
+ const term = params.searchTerm.toLowerCase();
1218
+ listings = listings.filter(
1219
+ (l) => [
1220
+ l.title,
1221
+ l.description,
1222
+ l.repoName,
1223
+ l.repoOwner,
1224
+ ...l.tags ?? []
1225
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1226
+ );
1227
+ }
1228
+ listings = [...listings].sort(
1229
+ (a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
1230
+ );
1231
+ const result = listings.map((l) => ({
1232
+ id: l.id,
1233
+ repo: `${l.repoOwner}/${l.repoName}`,
1234
+ title: l.title,
1235
+ description: l.description,
1236
+ type: l.type,
1237
+ category: l.category,
1238
+ tags: l.tags,
1239
+ language: l.language,
1240
+ stars: l.stars,
1241
+ deploys: l.deploys,
1242
+ views: l.views,
1243
+ githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
1244
+ hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
1245
+ createdAt: l.createdAt,
1246
+ updatedAt: l.updatedAt,
1247
+ listedRelease: l.listedRelease ? {
1248
+ hubReleaseId: l.listedRelease.id,
1249
+ name: l.listedRelease.name,
1250
+ tagName: l.listedRelease.tagName,
1251
+ createdAt: l.listedRelease.createdAt,
1252
+ imageName: l.listedRelease.build?.imageName,
1253
+ ...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
1254
+ } : null
1255
+ }));
1256
+ return capListResult(result, {
1257
+ limit: params.limit,
1258
+ cursor: params.cursor
1259
+ });
1260
+ }
1261
+ );
1262
+ server.tool(
1263
+ "deploy-hub-repo",
1264
+ "Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet), so it works the same on v1 and v2.",
1265
+ {
1266
+ repo: z4.string().optional().describe(
1267
+ `The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
1268
+ ),
1269
+ hubReleaseId: z4.string().optional().describe(
1270
+ "The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
1271
+ ),
1272
+ name: z4.string().optional().describe(
1273
+ "Name for the new endpoint. Defaults to '<listing title> <release tag>'."
1274
+ ),
1275
+ env: z4.record(z4.string()).optional().describe(
1276
+ "Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
1277
+ ),
1278
+ gpuIds: z4.string().optional().describe(
1279
+ "Comma-separated GPU pool names for workers (e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). To pin specific GPU SKUs within a pool, append exclusions prefixed with '-' using GPU type ids from list-gpu-types (e.g. 'AMPERE_16,-NVIDIA RTX 2000 Ada Generation,-NVIDIA RTX A4500' allows only the pool's remaining SKUs). Defaults to the release config's gpuIds; required when the config does not specify one."
1280
+ ),
1281
+ gpuCount: z4.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
1282
+ containerDiskInGb: z4.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
1283
+ workersMin: z4.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
1284
+ workersMax: z4.number().int().positive().optional().describe("Maximum workers (default: account default)."),
1285
+ idleTimeout: z4.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
1286
+ scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
1287
+ scalerValue: z4.number().optional().describe("Autoscaler target value (default 4)."),
1288
+ executionTimeoutMs: z4.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
1289
+ flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
1290
+ },
1291
+ { title: "Deploy Hub repo", ...WRITE },
1292
+ async (params) => {
1293
+ if (!params.repo && !params.hubReleaseId) {
1294
+ return jsonReply2({
1295
+ error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
1296
+ });
1297
+ }
1298
+ const catalog = await graphql(listingsQuery(true));
1299
+ let listing;
1300
+ if (params.hubReleaseId) {
1301
+ listing = catalog.listings.find(
1302
+ (l) => l.listedRelease?.id === params.hubReleaseId
1303
+ );
1304
+ } else {
1305
+ const repoKey = params.repo.toLowerCase();
1306
+ listing = catalog.listings.find(
1307
+ (l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
1308
+ );
1309
+ }
1310
+ if (!listing) {
1311
+ return jsonReply2({
1312
+ error: `No Hub listing found for ${params.hubReleaseId ? `hubReleaseId "${params.hubReleaseId}" (only each repo's currently listed release is deployable)` : `repo "${params.repo}"`}. Use list-hub-repos to see the catalog.`
1313
+ });
1314
+ }
1315
+ const release = listing.listedRelease;
1316
+ if (!release?.build?.imageName) {
1317
+ return jsonReply2({
1318
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
1319
+ });
1320
+ }
1321
+ if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
1322
+ return jsonReply2({
1323
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
1324
+ });
1325
+ }
1326
+ const config = parseReleaseConfig(release.config) ?? {};
1327
+ const gpuIds = params.gpuIds ?? config.gpuIds;
1328
+ if (!gpuIds) {
1329
+ return jsonReply2({
1330
+ error: 'This release does not specify a GPU pool \u2014 pass gpuIds (comma-separated pool names, e.g. "ADA_80_PRO,AMPERE_80"; see the pool field on list-gpu-types).'
1331
+ });
1332
+ }
1333
+ const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
1334
+ if (missingRequired.length > 0) {
1335
+ return jsonReply2({
1336
+ error: `Missing required environment variables for this release: ${missingRequired.join(
1337
+ ", "
1338
+ )}. Pass them via the env parameter.`
1339
+ });
1340
+ }
1341
+ const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
1342
+ const minCuda = minCudaVersion(config.allowedCudaVersions);
1343
+ const input = {
1344
+ name: endpointName,
1345
+ hubReleaseId: release.id,
1346
+ type: "QB",
1347
+ gpuIds,
1348
+ gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
1349
+ workersMin: params.workersMin ?? 0,
1350
+ workersMax: params.workersMax ?? null,
1351
+ idleTimeout: params.idleTimeout ?? 5,
1352
+ scalerType: params.scalerType ?? "QUEUE_DELAY",
1353
+ scalerValue: params.scalerValue ?? 4,
1354
+ executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
1355
+ flashBootType: params.flashboot ?? "FLASHBOOT",
1356
+ locations: null,
1357
+ networkVolumeIds: null,
1358
+ compliance: [],
1359
+ modelReferences: [],
1360
+ ...minCuda ? {
1361
+ minCudaVersion: minCuda,
1362
+ allowedCudaVersions: config.allowedCudaVersions.join(",")
1363
+ } : {},
1364
+ template: {
1365
+ name: `${endpointName}__template__${randomSuffix()}`,
1366
+ imageName: release.build.imageName,
1367
+ containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
1368
+ containerRegistryAuthId: "",
1369
+ dockerArgs: "",
1370
+ startScript: "",
1371
+ ports: "",
1372
+ env
1373
+ }
1374
+ };
1375
+ const data = await graphqlAuthed(
1376
+ `
1377
+ mutation saveEndpoint($input: EndpointInput!) {
1378
+ saveEndpoint(input: $input) {
1379
+ id
1380
+ name
1381
+ gpuIds
1382
+ gpuCount
1383
+ workersMin
1384
+ workersMax
1385
+ idleTimeout
1386
+ scalerType
1387
+ scalerValue
1388
+ flashBootType
1389
+ templateId
1390
+ }
312
1391
  }
313
- ]
1392
+ `,
1393
+ { input }
1394
+ );
1395
+ return jsonReply2({
1396
+ endpoint: data.saveEndpoint,
1397
+ deployed: {
1398
+ repo: `${listing.repoOwner}/${listing.repoName}`,
1399
+ release: release.tagName,
1400
+ hubReleaseId: release.id,
1401
+ imageName: release.build.imageName
1402
+ },
1403
+ note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
1404
+ });
1405
+ }
1406
+ );
1407
+ }
1408
+
1409
+ // src/tools/public-endpoints.ts
1410
+ import { z as z5 } from "zod";
1411
+ function parseMetadata(metadata) {
1412
+ if (!metadata) return {};
1413
+ try {
1414
+ const parsed = JSON.parse(metadata);
1415
+ return parsed && typeof parsed === "object" ? parsed : {};
1416
+ } catch {
1417
+ return {};
1418
+ }
1419
+ }
1420
+ function registerPublicEndpointTools(server, rt) {
1421
+ const { graphql } = rt;
1422
+ server.tool(
1423
+ "list-public-endpoints",
1424
+ "List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
1425
+ {
1426
+ ...listPaginationParams,
1427
+ searchTerm: z5.string().optional().describe(
1428
+ "Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
1429
+ ),
1430
+ modality: z5.string().optional().describe(
1431
+ "Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
1432
+ ),
1433
+ owner: z5.string().optional().describe(
1434
+ "Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
1435
+ ),
1436
+ includeOffline: z5.boolean().optional().describe(
1437
+ "Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
1438
+ )
1439
+ },
1440
+ { title: "List public endpoints", ...READ_ONLY },
1441
+ async (params) => {
1442
+ const data = await graphql(`
1443
+ query {
1444
+ allAiApiPublicConfigs {
1445
+ id
1446
+ aiApiId
1447
+ modelName
1448
+ displayName
1449
+ description
1450
+ metadata
1451
+ isLive
1452
+ createdAt
1453
+ updatedAt
1454
+ }
1455
+ }
1456
+ `);
1457
+ let configs = data.allAiApiPublicConfigs.map((c) => ({
1458
+ config: c,
1459
+ meta: parseMetadata(c.metadata)
1460
+ }));
1461
+ if (!params.includeOffline) {
1462
+ configs = configs.filter(({ config }) => config.isLive);
1463
+ }
1464
+ if (params.modality) {
1465
+ const term = params.modality.toLowerCase();
1466
+ configs = configs.filter(
1467
+ ({ meta }) => (meta.source ?? "").toLowerCase() === term
1468
+ );
1469
+ }
1470
+ if (params.owner) {
1471
+ const term = params.owner.toLowerCase();
1472
+ configs = configs.filter(
1473
+ ({ meta }) => (meta.owner ?? "").toLowerCase() === term
1474
+ );
1475
+ }
1476
+ if (params.searchTerm) {
1477
+ const term = params.searchTerm.toLowerCase();
1478
+ configs = configs.filter(
1479
+ ({ config, meta }) => [
1480
+ config.displayName,
1481
+ config.modelName,
1482
+ config.aiApiId,
1483
+ config.description,
1484
+ meta.owner,
1485
+ meta.tag,
1486
+ meta.source
1487
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1488
+ );
1489
+ }
1490
+ configs = [...configs].sort(
1491
+ (a, b) => a.config.displayName.localeCompare(b.config.displayName)
1492
+ );
1493
+ const result = configs.map(({ config, meta }) => ({
1494
+ endpointId: config.aiApiId,
1495
+ displayName: config.displayName,
1496
+ modelName: config.modelName,
1497
+ description: config.description,
1498
+ modality: meta.source,
1499
+ tag: meta.tag,
1500
+ owner: meta.owner,
1501
+ pricing: meta.priceString,
1502
+ isLive: config.isLive,
1503
+ baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
1504
+ createdAt: config.createdAt,
1505
+ updatedAt: config.updatedAt
1506
+ }));
1507
+ return capListResult(result, {
1508
+ limit: params.limit,
1509
+ cursor: params.cursor
1510
+ });
1511
+ }
1512
+ );
1513
+ }
1514
+
1515
+ // src/tools/endpoint-gpus.ts
1516
+ import { z as z6 } from "zod";
1517
+ function registerEndpointGpuTools(server, rt) {
1518
+ const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
1519
+ server.tool(
1520
+ "set-endpoint-gpus",
1521
+ "Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs, which create-endpoint/update-endpoint cannot express. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly (e.g. pools:['AMPERE_16'], excludeGpuTypeIds:['NVIDIA RTX 2000 Ada Generation','NVIDIA RTX 4000 Ada Generation','NVIDIA RTX A4500'] pins RTX A4000). All other endpoint settings (workers, scaling, timeouts, template) are read first and preserved. Works on any Serverless endpoint via the authenticated GraphQL API.",
1522
+ {
1523
+ endpointId: z6.string().describe("ID of the Serverless endpoint to update"),
1524
+ gpuIds: z6.string().optional().describe(
1525
+ "Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
1526
+ ),
1527
+ pools: z6.array(z6.string()).optional().describe(
1528
+ "GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
1529
+ ),
1530
+ excludeGpuTypeIds: z6.array(z6.string()).optional().describe(
1531
+ "GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
1532
+ ),
1533
+ gpuCount: z6.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
1534
+ minCudaVersion: z6.string().optional().describe(
1535
+ "Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
1536
+ ),
1537
+ allowedCudaVersions: z6.string().optional().describe(
1538
+ "Comma-separated allowed host CUDA versions (e.g. '12.8,12.7,12.6'). Omit to keep the current value. CUDA compatibility is part of GPU selection \u2014 a narrow list can leave an endpoint unable to schedule workers."
1539
+ )
1540
+ },
1541
+ { title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
1542
+ async (params) => {
1543
+ const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
1544
+ ...params.pools,
1545
+ ...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
1546
+ ].join(",") : void 0);
1547
+ if (!gpuIds) {
1548
+ return jsonReply2({
1549
+ error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
1550
+ });
1551
+ }
1552
+ const data = await graphqlAuthed(`
1553
+ query {
1554
+ myself {
1555
+ endpoints {
1556
+ id
1557
+ name
1558
+ gpuIds
1559
+ gpuCount
1560
+ workersMin
1561
+ workersMax
1562
+ idleTimeout
1563
+ scalerType
1564
+ scalerValue
1565
+ executionTimeoutMs
1566
+ flashBootType
1567
+ type
1568
+ locations
1569
+ templateId
1570
+ allowedCudaVersions
1571
+ minCudaVersion
1572
+ compliance
1573
+ modelReferences
1574
+ networkVolumeIds {
1575
+ networkVolumeId
1576
+ dataCenterId
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ `);
1582
+ const current = data.myself.endpoints.find(
1583
+ (e) => e.id === params.endpointId
1584
+ );
1585
+ if (!current) {
1586
+ return jsonReply2({
1587
+ error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
1588
+ });
1589
+ }
1590
+ const input = {
1591
+ id: current.id,
1592
+ name: current.name,
1593
+ gpuIds,
1594
+ gpuCount: params.gpuCount ?? current.gpuCount,
1595
+ workersMin: current.workersMin,
1596
+ workersMax: current.workersMax,
1597
+ idleTimeout: current.idleTimeout,
1598
+ scalerType: current.scalerType,
1599
+ scalerValue: current.scalerValue,
1600
+ executionTimeoutMs: current.executionTimeoutMs,
1601
+ flashBootType: current.flashBootType,
1602
+ type: current.type,
1603
+ locations: current.locations,
1604
+ networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
1605
+ // Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
1606
+ // ONLY, and the read shape is rejected outright.
1607
+ current.networkVolumeIds.map((v) => ({
1608
+ networkVolumeId: v.networkVolumeId
1609
+ }))
1610
+ ) : null
314
1611
  };
1612
+ for (const [key, value] of Object.entries({
1613
+ templateId: current.templateId,
1614
+ allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
1615
+ minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
1616
+ compliance: current.compliance,
1617
+ modelReferences: current.modelReferences
1618
+ })) {
1619
+ if (value !== null && value !== void 0) input[key] = value;
1620
+ }
1621
+ const result = await graphqlAuthed(
1622
+ `
1623
+ mutation saveEndpoint($input: EndpointInput!) {
1624
+ saveEndpoint(input: $input) {
1625
+ id
1626
+ name
1627
+ gpuIds
1628
+ gpuCount
1629
+ workersMin
1630
+ workersMax
1631
+ }
1632
+ }
1633
+ `,
1634
+ { input }
1635
+ );
1636
+ return jsonReply2({
1637
+ endpoint: result.saveEndpoint,
1638
+ previousGpuIds: current.gpuIds
1639
+ });
315
1640
  }
316
1641
  );
1642
+ }
1643
+
1644
+ // src/tools/pods.ts
1645
+ import { z as z7 } from "zod";
1646
+ function registerPodTools(server, rt) {
1647
+ const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
317
1648
  server.tool(
318
1649
  "list-pods",
1650
+ "List your pods (running and stopped) with optional filters \u2014 compute type, GPU type, data center, name \u2014 plus machine/network-volume expansion. Paginated via limit/cursor.",
319
1651
  {
320
1652
  ...listPaginationParams,
321
- computeType: z2.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
322
- gpuTypeId: z2.array(z2.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
323
- dataCenterId: z2.array(z2.string()).optional().describe("Filter to Pods in any of the provided data centers"),
324
- name: z2.string().optional().describe("Filter to Pods with the provided name"),
325
- includeMachine: z2.boolean().optional().describe("Include information about the machine"),
326
- includeNetworkVolume: z2.boolean().optional().describe("Include information about attached network volumes")
1653
+ computeType: z7.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
1654
+ gpuTypeId: z7.array(z7.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
1655
+ dataCenterId: z7.array(z7.string()).optional().describe("Filter to Pods in any of the provided data centers"),
1656
+ name: z7.string().optional().describe("Filter to Pods with the provided name"),
1657
+ includeMachine: z7.boolean().optional().describe("Include information about the machine"),
1658
+ includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
327
1659
  },
1660
+ { title: "List pods", ...READ_ONLY },
328
1661
  async (params) => {
329
- const queryParams = new URLSearchParams();
330
- if (params.computeType)
331
- queryParams.append("computeType", params.computeType);
332
- if (params.gpuTypeId)
333
- params.gpuTypeId.forEach(
334
- (type) => queryParams.append("gpuTypeId", type)
335
- );
336
- if (params.dataCenterId)
337
- params.dataCenterId.forEach(
338
- (dc) => queryParams.append("dataCenterId", dc)
339
- );
340
- if (params.name) queryParams.append("name", params.name);
341
- if (params.includeMachine)
342
- queryParams.append("includeMachine", params.includeMachine.toString());
343
- if (params.includeNetworkVolume)
344
- queryParams.append(
345
- "includeNetworkVolume",
346
- params.includeNetworkVolume.toString()
347
- );
348
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
349
- const result = await runpodRequest(`/pods${queryString}`);
350
- return capListResult(result, {
1662
+ const backend = backendFor("pods");
1663
+ let queryString = "";
1664
+ if (backend.version === "v1") {
1665
+ const queryParams = new URLSearchParams();
1666
+ if (params.computeType)
1667
+ queryParams.append("computeType", params.computeType);
1668
+ if (params.gpuTypeId)
1669
+ params.gpuTypeId.forEach(
1670
+ (type) => queryParams.append("gpuTypeId", type)
1671
+ );
1672
+ if (params.dataCenterId)
1673
+ params.dataCenterId.forEach(
1674
+ (dc) => queryParams.append("dataCenterId", dc)
1675
+ );
1676
+ if (params.name) queryParams.append("name", params.name);
1677
+ if (params.includeMachine)
1678
+ queryParams.append(
1679
+ "includeMachine",
1680
+ params.includeMachine.toString()
1681
+ );
1682
+ if (params.includeNetworkVolume)
1683
+ queryParams.append(
1684
+ "includeNetworkVolume",
1685
+ params.includeNetworkVolume.toString()
1686
+ );
1687
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1688
+ }
1689
+ const result = await callRestUrl(
1690
+ `${backend.base}${backend.list}${queryString}`
1691
+ );
1692
+ return capListResult(backend.unwrap(result), {
351
1693
  limit: params.limit,
352
1694
  cursor: params.cursor
353
1695
  });
@@ -355,159 +1697,308 @@ function registerTools(server, ctx) {
355
1697
  );
356
1698
  server.tool(
357
1699
  "get-pod",
1700
+ "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
358
1701
  {
359
- podId: z2.string().describe("ID of the pod to retrieve"),
360
- includeMachine: z2.boolean().optional().describe("Include information about the machine"),
361
- includeNetworkVolume: z2.boolean().optional().describe("Include information about attached network volumes")
1702
+ podId: z7.string().describe("ID of the pod to retrieve"),
1703
+ includeMachine: z7.boolean().optional().describe("Include information about the machine"),
1704
+ includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
362
1705
  },
1706
+ { title: "Get pod", ...READ_ONLY },
363
1707
  async (params) => {
364
- const queryParams = new URLSearchParams();
365
- if (params.includeMachine)
366
- queryParams.append("includeMachine", params.includeMachine.toString());
367
- if (params.includeNetworkVolume)
368
- queryParams.append(
369
- "includeNetworkVolume",
370
- params.includeNetworkVolume.toString()
371
- );
372
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
373
- const result = await runpodRequest(`/pods/${params.podId}${queryString}`);
374
- return {
375
- content: [
376
- {
377
- type: "text",
378
- text: JSON.stringify(result, null, 2)
379
- }
380
- ]
381
- };
1708
+ const backend = backendFor("pods");
1709
+ let queryString = "";
1710
+ if (backend.version === "v1") {
1711
+ const queryParams = new URLSearchParams();
1712
+ if (params.includeMachine)
1713
+ queryParams.append(
1714
+ "includeMachine",
1715
+ params.includeMachine.toString()
1716
+ );
1717
+ if (params.includeNetworkVolume)
1718
+ queryParams.append(
1719
+ "includeNetworkVolume",
1720
+ params.includeNetworkVolume.toString()
1721
+ );
1722
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1723
+ }
1724
+ const result = await callRestUrl(
1725
+ `${backend.base}${backend.get(params.podId)}${queryString}`
1726
+ );
1727
+ return jsonReply2(result);
382
1728
  }
383
1729
  );
384
1730
  server.tool(
385
1731
  "create-pod",
386
- '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.',
1732
+ 'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
387
1733
  {
388
- name: z2.string().optional().describe("Name for the pod"),
389
- imageName: z2.string().describe("Docker image to use"),
390
- cloudType: z2.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
391
- gpuTypeIds: z2.array(z2.string()).optional().describe("List of acceptable GPU types"),
392
- gpuCount: z2.number().optional().describe("Number of GPUs"),
393
- containerDiskInGb: z2.number().optional().describe("Container disk size in GB"),
394
- volumeInGb: z2.number().optional().describe("Volume size in GB"),
395
- volumeMountPath: z2.string().optional().describe("Path to mount the volume"),
396
- ports: z2.array(z2.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
397
- env: z2.record(z2.string()).optional().describe("Environment variables"),
398
- dataCenterIds: z2.array(z2.string()).optional().describe("List of data centers")
1734
+ name: z7.string().optional().describe("Name for the pod"),
1735
+ imageName: z7.string().optional().describe("Docker image to use. Optional when templateId is provided."),
1736
+ templateId: z7.string().optional().describe(
1737
+ "Deploy from an existing template (see list-templates / get-template). The template supplies name, image, start command, ports, env, disk, volume, and registry credential as defaults; any field you also pass replaces the template value for that field (whole-field, not a merge). Override the registry credential with containerRegistryAuthId. Requires the v2 REST API. Still pass gpuTypeIds or computeType to choose the compute \u2014 a template does not pin the pod compute."
1738
+ ),
1739
+ computeType: z7.enum(["GPU", "CPU"]).optional().describe(
1740
+ "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
1741
+ ),
1742
+ cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1743
+ gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
1744
+ gpuCount: z7.number().optional().describe("Number of GPUs"),
1745
+ containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
1746
+ volumeInGb: z7.number().optional().describe("Volume size in GB"),
1747
+ volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
1748
+ ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1749
+ env: z7.record(z7.string()).optional().describe("Environment variables"),
1750
+ containerRegistryAuthId: z7.string().optional().describe(
1751
+ "Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
1752
+ ),
1753
+ dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
399
1754
  },
1755
+ { title: "Create pod", ...WRITE },
400
1756
  async (params) => {
401
- const result = await runpodRequest("/pods", "POST", params);
402
- return {
403
- content: [
404
- {
405
- type: "text",
406
- text: JSON.stringify(result, null, 2)
1757
+ const backend = backendFor("pods");
1758
+ if (params.templateId && backend.version !== "v2") {
1759
+ return jsonReply2({
1760
+ error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
1761
+ status: 501
1762
+ });
1763
+ }
1764
+ if (!params.imageName && !params.templateId) {
1765
+ return jsonReply2({
1766
+ error: "Provide imageName, or templateId to deploy from an existing template.",
1767
+ status: 400
1768
+ });
1769
+ }
1770
+ const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
1771
+ const hasGpuCount = (params.gpuCount ?? 0) > 0;
1772
+ const wantsCpu = params.computeType === "CPU";
1773
+ const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
1774
+ if (backend.version === "v2") {
1775
+ if (wantsGpu && wantsCpu) {
1776
+ return jsonReply2({
1777
+ error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
1778
+ status: 400
1779
+ });
1780
+ }
1781
+ if (!wantsGpu && !wantsCpu) {
1782
+ return jsonReply2({
1783
+ error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
1784
+ status: 400
1785
+ });
1786
+ }
1787
+ if (wantsGpu && !hasGpuTypes) {
1788
+ return jsonReply2({
1789
+ error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
1790
+ status: 400
1791
+ });
1792
+ }
1793
+ if (wantsCpu) {
1794
+ if (params.templateId) {
1795
+ return jsonReply2({
1796
+ error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
1797
+ status: 400
1798
+ });
407
1799
  }
408
- ]
409
- };
1800
+ try {
1801
+ const result = await callRestUrl(
1802
+ `${restV1Base(env)}/pods`,
1803
+ "POST",
1804
+ params
1805
+ );
1806
+ return jsonReply2({
1807
+ ...result,
1808
+ _servedBy: "v1",
1809
+ _note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
1810
+ });
1811
+ } catch (error) {
1812
+ if (error instanceof HttpError) {
1813
+ return jsonReply2({
1814
+ error: error.message,
1815
+ status: error.status,
1816
+ _servedBy: "v1",
1817
+ _note: "CPU pod create was routed to the v1 API and failed."
1818
+ });
1819
+ }
1820
+ throw error;
1821
+ }
1822
+ }
1823
+ }
1824
+ let body = backend.mapCreate(params);
1825
+ if (params.templateId) {
1826
+ let template;
1827
+ try {
1828
+ template = await callRestUrl(
1829
+ `${restV2Base(env)}/templates/${params.templateId}`
1830
+ );
1831
+ } catch (error) {
1832
+ if (error instanceof HttpError) {
1833
+ return jsonReply2({
1834
+ error: `Failed to load template ${params.templateId}: ${error.message}`,
1835
+ status: error.status
1836
+ });
1837
+ }
1838
+ throw error;
1839
+ }
1840
+ body = { ...podBodyFromTemplate(template), ...body };
1841
+ }
1842
+ try {
1843
+ const result = await callRestUrl(
1844
+ `${backend.base}${backend.list}`,
1845
+ "POST",
1846
+ body
1847
+ );
1848
+ if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
1849
+ return jsonReply2({
1850
+ ...result,
1851
+ _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(", ")}.`
1852
+ });
1853
+ }
1854
+ return jsonReply2(result);
1855
+ } catch (error) {
1856
+ if (error instanceof HttpError) {
1857
+ return jsonReply2({ error: error.message, status: error.status });
1858
+ }
1859
+ throw error;
1860
+ }
410
1861
  }
411
1862
  );
412
1863
  server.tool(
413
1864
  "update-pod",
1865
+ "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
414
1866
  {
415
- podId: z2.string().describe("ID of the pod to update"),
416
- name: z2.string().optional().describe("New name for the pod"),
417
- imageName: z2.string().optional().describe("New Docker image"),
418
- containerDiskInGb: z2.number().optional().describe("New container disk size in GB"),
419
- volumeInGb: z2.number().optional().describe("New volume size in GB"),
420
- volumeMountPath: z2.string().optional().describe("New path to mount the volume"),
421
- ports: z2.array(z2.string()).optional().describe("New ports to expose"),
422
- env: z2.record(z2.string()).optional().describe("New environment variables")
1867
+ podId: z7.string().describe("ID of the pod to update"),
1868
+ name: z7.string().optional().describe("New name for the pod"),
1869
+ imageName: z7.string().optional().describe("New Docker image"),
1870
+ containerDiskInGb: z7.number().optional().describe("New container disk size in GB"),
1871
+ volumeInGb: z7.number().optional().describe("New volume size in GB"),
1872
+ volumeMountPath: z7.string().optional().describe("New path to mount the volume"),
1873
+ ports: z7.array(z7.string()).optional().describe("New ports to expose"),
1874
+ env: z7.record(z7.string()).optional().describe("New environment variables")
423
1875
  },
1876
+ { title: "Update pod", ...WRITE, idempotentHint: true },
424
1877
  async (params) => {
425
1878
  const { podId, ...updateParams } = params;
426
- const result = await runpodRequest(
427
- `/pods/${podId}`,
1879
+ const backend = backendFor("pods");
1880
+ const body = backend.mapUpdate(updateParams);
1881
+ const result = await callRestUrl(
1882
+ `${backend.base}${backend.get(podId)}`,
428
1883
  "PATCH",
429
- updateParams
1884
+ body
430
1885
  );
431
- return {
432
- content: [
433
- {
434
- type: "text",
435
- text: JSON.stringify(result, null, 2)
436
- }
437
- ]
438
- };
1886
+ return jsonReply2(result);
439
1887
  }
440
1888
  );
441
1889
  server.tool(
442
1890
  "start-pod",
1891
+ "Start a stopped pod (resumes the same pod and its billing).",
443
1892
  {
444
- podId: z2.string().describe("ID of the pod to start")
1893
+ podId: z7.string().describe("ID of the pod to start")
445
1894
  },
1895
+ { title: "Start pod", ...WRITE, idempotentHint: true },
446
1896
  async (params) => {
447
- const result = await runpodRequest(`/pods/${params.podId}/start`, "POST");
448
- return {
449
- content: [
450
- {
451
- type: "text",
452
- text: JSON.stringify(result, null, 2)
453
- }
454
- ]
455
- };
1897
+ const result = await podAction(params.podId, "start");
1898
+ return jsonReply2(result);
456
1899
  }
457
1900
  );
458
1901
  server.tool(
459
1902
  "stop-pod",
1903
+ "Stop a running pod. The pod and its disk persist; GPU billing stops.",
460
1904
  {
461
- podId: z2.string().describe("ID of the pod to stop")
1905
+ podId: z7.string().describe("ID of the pod to stop")
462
1906
  },
1907
+ { title: "Stop pod", ...WRITE, idempotentHint: true },
463
1908
  async (params) => {
464
- const result = await runpodRequest(`/pods/${params.podId}/stop`, "POST");
465
- return {
466
- content: [
467
- {
468
- type: "text",
469
- text: JSON.stringify(result, null, 2)
470
- }
471
- ]
472
- };
1909
+ const result = await podAction(params.podId, "stop");
1910
+ return jsonReply2(result);
1911
+ }
1912
+ );
1913
+ server.tool(
1914
+ "restart-pod",
1915
+ "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1916
+ {
1917
+ podId: z7.string().describe("ID of the pod to restart")
1918
+ },
1919
+ { title: "Restart pod", ...WRITE },
1920
+ async (params) => {
1921
+ const backend = backendFor("pods");
1922
+ if (backend.version === "v1") {
1923
+ return jsonReply2({
1924
+ error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
1925
+ status: 501
1926
+ });
1927
+ }
1928
+ const result = await podAction(params.podId, "restart");
1929
+ return jsonReply2(result);
473
1930
  }
474
1931
  );
1932
+ server.tool(
1933
+ "stream-pod-logs",
1934
+ "Fetch a bounded snapshot of a pod's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
1935
+ {
1936
+ podId: z7.string().describe("ID of the pod whose logs to stream"),
1937
+ ...logStreamParams
1938
+ },
1939
+ { title: "Stream pod logs", ...READ_ONLY },
1940
+ (params) => streamLogsReply(
1941
+ rt,
1942
+ {
1943
+ name: "stream-pod-logs",
1944
+ resource: "pods",
1945
+ logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
1946
+ },
1947
+ params
1948
+ )
1949
+ );
475
1950
  server.tool(
476
1951
  "delete-pod",
1952
+ "Permanently delete a pod and its data. This cannot be undone.",
477
1953
  {
478
- podId: z2.string().describe("ID of the pod to delete")
1954
+ podId: z7.string().describe("ID of the pod to delete")
479
1955
  },
1956
+ { title: "Delete pod", ...DESTRUCTIVE },
480
1957
  async (params) => {
481
- const result = await runpodRequest(`/pods/${params.podId}`, "DELETE");
482
- return {
483
- content: [
484
- {
485
- type: "text",
486
- text: JSON.stringify(result, null, 2)
487
- }
488
- ]
489
- };
1958
+ const backend = backendFor("pods");
1959
+ const result = await callRestUrl(
1960
+ `${backend.base}${backend.get(params.podId)}`,
1961
+ "DELETE"
1962
+ );
1963
+ return jsonReply2(result);
490
1964
  }
491
1965
  );
1966
+ }
1967
+
1968
+ // src/tools/endpoints.ts
1969
+ import { z as z8 } from "zod";
1970
+ function registerEndpointTools(server, rt) {
1971
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
492
1972
  server.tool(
493
1973
  "list-endpoints",
1974
+ "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
494
1975
  {
495
1976
  ...listPaginationParams,
496
- includeTemplate: z2.boolean().optional().describe("Include template information"),
497
- includeWorkers: z2.boolean().optional().describe("Include information about workers")
1977
+ includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
1978
+ includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
498
1979
  },
1980
+ { title: "List endpoints", ...READ_ONLY },
499
1981
  async (params) => {
500
- const queryParams = new URLSearchParams();
501
- if (params.includeTemplate)
502
- queryParams.append(
503
- "includeTemplate",
504
- params.includeTemplate.toString()
505
- );
506
- if (params.includeWorkers)
507
- queryParams.append("includeWorkers", params.includeWorkers.toString());
508
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
509
- const result = await runpodRequest(`/endpoints${queryString}`);
510
- return capListResult(result, {
1982
+ const backend = backendFor("endpoints");
1983
+ let queryString = "";
1984
+ if (backend.version === "v1") {
1985
+ const queryParams = new URLSearchParams();
1986
+ if (params.includeTemplate)
1987
+ queryParams.append(
1988
+ "includeTemplate",
1989
+ params.includeTemplate.toString()
1990
+ );
1991
+ if (params.includeWorkers)
1992
+ queryParams.append(
1993
+ "includeWorkers",
1994
+ params.includeWorkers.toString()
1995
+ );
1996
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1997
+ }
1998
+ const result = await callRestUrl(
1999
+ `${backend.base}${backend.list}${queryString}`
2000
+ );
2001
+ return capListResult(backend.unwrap(result), {
511
2002
  limit: params.limit,
512
2003
  cursor: params.cursor
513
2004
  });
@@ -515,124 +2006,275 @@ function registerTools(server, ctx) {
515
2006
  );
516
2007
  server.tool(
517
2008
  "get-endpoint",
2009
+ "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
518
2010
  {
519
- endpointId: z2.string().describe("ID of the endpoint to retrieve"),
520
- includeTemplate: z2.boolean().optional().describe("Include template information"),
521
- includeWorkers: z2.boolean().optional().describe("Include information about workers")
2011
+ endpointId: z8.string().describe("ID of the endpoint to retrieve"),
2012
+ includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
2013
+ includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
522
2014
  },
2015
+ { title: "Get endpoint", ...READ_ONLY },
523
2016
  async (params) => {
524
- const queryParams = new URLSearchParams();
525
- if (params.includeTemplate)
526
- queryParams.append(
527
- "includeTemplate",
528
- params.includeTemplate.toString()
529
- );
530
- if (params.includeWorkers)
531
- queryParams.append("includeWorkers", params.includeWorkers.toString());
532
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
533
- const result = await runpodRequest(
534
- `/endpoints/${params.endpointId}${queryString}`
2017
+ const backend = backendFor("endpoints");
2018
+ let queryString = "";
2019
+ if (backend.version === "v1") {
2020
+ const queryParams = new URLSearchParams();
2021
+ if (params.includeTemplate)
2022
+ queryParams.append(
2023
+ "includeTemplate",
2024
+ params.includeTemplate.toString()
2025
+ );
2026
+ if (params.includeWorkers)
2027
+ queryParams.append(
2028
+ "includeWorkers",
2029
+ params.includeWorkers.toString()
2030
+ );
2031
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
2032
+ }
2033
+ const result = await callRestUrl(
2034
+ `${backend.base}${backend.get(params.endpointId)}${queryString}`
535
2035
  );
536
- return {
537
- content: [
538
- {
539
- type: "text",
540
- text: JSON.stringify(result, null, 2)
541
- }
542
- ]
543
- };
2036
+ return jsonReply2(result);
544
2037
  }
545
2038
  );
546
2039
  server.tool(
547
2040
  "create-endpoint",
2041
+ "Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero).",
548
2042
  {
549
- name: z2.string().optional().describe("Name for the endpoint"),
550
- templateId: z2.string().describe("Template ID to use"),
551
- computeType: z2.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint"),
552
- gpuTypeIds: z2.array(z2.string()).optional().describe("List of acceptable GPU types"),
553
- gpuCount: z2.number().optional().describe("Number of GPUs per worker"),
554
- workersMin: z2.number().optional().describe("Minimum number of workers"),
555
- workersMax: z2.number().optional().describe("Maximum number of workers"),
556
- dataCenterIds: z2.array(z2.string()).optional().describe("List of data centers")
2043
+ name: z8.string().optional().describe("Name for the endpoint"),
2044
+ // --- v2 inline-config fields ---
2045
+ imageName: z8.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
2046
+ gpuPoolIds: z8.array(z8.string()).optional().describe(
2047
+ 'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
2048
+ ),
2049
+ gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
2050
+ args: z8.string().optional().describe("Container start command/args (v2)"),
2051
+ containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
2052
+ ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
2053
+ env: z8.record(z8.string()).optional().describe("Environment variables (v2)"),
2054
+ containerRegistryAuthId: z8.string().optional().describe("Container registry auth id for a private image (v2)"),
2055
+ networkVolumeIds: z8.array(z8.string()).optional().describe("Network volume ids to attach (v2)"),
2056
+ executionTimeoutMs: z8.number().optional().describe("Per-job execution timeout in ms (v2)"),
2057
+ flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
2058
+ // --- v1 template-based fields ---
2059
+ templateId: z8.string().optional().describe("Template ID (v1). Required on v1."),
2060
+ computeType: z8.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
2061
+ gpuTypeIds: z8.array(z8.string()).optional().describe("List of acceptable GPU types (v1)"),
2062
+ // --- shared autoscaling fields ---
2063
+ workersMin: z8.number().optional().describe("Minimum number of workers"),
2064
+ workersMax: z8.number().optional().describe("Maximum number of workers"),
2065
+ scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
2066
+ scalerValue: z8.number().optional().describe("Autoscaler target value"),
2067
+ idleTimeout: z8.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
2068
+ dataCenterIds: z8.array(z8.string()).optional().describe("List of preferred data centers")
557
2069
  },
2070
+ { title: "Create endpoint", ...WRITE },
558
2071
  async (params) => {
559
- const result = await runpodRequest("/endpoints", "POST", params);
560
- return {
561
- content: [
562
- {
563
- type: "text",
564
- text: JSON.stringify(result, null, 2)
565
- }
566
- ]
567
- };
2072
+ const backend = backendFor("endpoints");
2073
+ if (backend.version === "v2") {
2074
+ if (!params.name) {
2075
+ return jsonReply2({
2076
+ error: "On the v2 REST API, create-endpoint requires a name.",
2077
+ status: 400
2078
+ });
2079
+ }
2080
+ if (!params.imageName) {
2081
+ return jsonReply2({
2082
+ 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.",
2083
+ status: 400
2084
+ });
2085
+ }
2086
+ if (!params.gpuPoolIds?.length) {
2087
+ return jsonReply2({
2088
+ error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
2089
+ status: 400
2090
+ });
2091
+ }
2092
+ const body = backend.mapCreate(params);
2093
+ const result2 = await callRestUrl(
2094
+ `${backend.base}${backend.list}`,
2095
+ "POST",
2096
+ body
2097
+ );
2098
+ return jsonReply2(result2);
2099
+ }
2100
+ if (!params.templateId) {
2101
+ return jsonReply2({
2102
+ error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
2103
+ status: 400
2104
+ });
2105
+ }
2106
+ const v1Body = {};
2107
+ for (const k of [
2108
+ "templateId",
2109
+ "name",
2110
+ "computeType",
2111
+ "gpuTypeIds",
2112
+ "gpuCount",
2113
+ "workersMin",
2114
+ "workersMax",
2115
+ "dataCenterIds"
2116
+ ]) {
2117
+ if (params[k] !== void 0) v1Body[k] = params[k];
2118
+ }
2119
+ const result = await callRestUrl(
2120
+ `${backend.base}${backend.list}`,
2121
+ "POST",
2122
+ v1Body
2123
+ );
2124
+ return jsonReply2(result);
568
2125
  }
569
2126
  );
570
2127
  server.tool(
571
2128
  "update-endpoint",
2129
+ "Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change.",
572
2130
  {
573
- endpointId: z2.string().describe("ID of the endpoint to update"),
574
- name: z2.string().optional().describe("New name for the endpoint"),
575
- workersMin: z2.number().optional().describe("New minimum number of workers"),
576
- workersMax: z2.number().optional().describe("New maximum number of workers"),
577
- idleTimeout: z2.number().optional().describe("New idle timeout in seconds"),
578
- scalerType: z2.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
579
- scalerValue: z2.number().optional().describe("Scaler value")
2131
+ endpointId: z8.string().describe("ID of the endpoint to update"),
2132
+ name: z8.string().optional().describe("New name for the endpoint"),
2133
+ workersMin: z8.number().optional().describe("New minimum number of workers"),
2134
+ workersMax: z8.number().optional().describe("New maximum number of workers"),
2135
+ idleTimeout: z8.number().optional().describe("New idle timeout in seconds"),
2136
+ scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2137
+ scalerValue: z8.number().optional().describe("Scaler value"),
2138
+ // --- v2-only inline-config fields ---
2139
+ imageName: z8.string().optional().describe("New Docker image (v2)"),
2140
+ gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
2141
+ gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
2142
+ args: z8.string().optional().describe("New container args (v2)"),
2143
+ containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
2144
+ ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
2145
+ env: z8.record(z8.string()).optional().describe("New environment variables (v2)"),
2146
+ containerRegistryAuthId: z8.string().optional().describe("New container registry auth id (v2)"),
2147
+ networkVolumeIds: z8.array(z8.string()).optional().describe("New network volume ids (v2)"),
2148
+ executionTimeoutMs: z8.number().optional().describe("New per-job execution timeout in ms (v2)"),
2149
+ flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
580
2150
  },
2151
+ { title: "Update endpoint", ...WRITE, idempotentHint: true },
581
2152
  async (params) => {
582
2153
  const { endpointId, ...updateParams } = params;
583
- const result = await runpodRequest(
584
- `/endpoints/${endpointId}`,
2154
+ const backend = backendFor("endpoints");
2155
+ const body = backend.mapUpdate(updateParams);
2156
+ const result = await callRestUrl(
2157
+ `${backend.base}${backend.get(endpointId)}`,
585
2158
  "PATCH",
586
- updateParams
2159
+ body
587
2160
  );
588
- return {
589
- content: [
590
- {
591
- type: "text",
592
- text: JSON.stringify(result, null, 2)
593
- }
594
- ]
595
- };
2161
+ return jsonReply2(result);
596
2162
  }
597
2163
  );
598
2164
  server.tool(
599
2165
  "delete-endpoint",
2166
+ "Permanently delete a Serverless endpoint. This cannot be undone.",
600
2167
  {
601
- endpointId: z2.string().describe("ID of the endpoint to delete")
2168
+ endpointId: z8.string().describe("ID of the endpoint to delete")
602
2169
  },
2170
+ { title: "Delete endpoint", ...DESTRUCTIVE },
603
2171
  async (params) => {
604
- const result = await runpodRequest(
605
- `/endpoints/${params.endpointId}`,
2172
+ const backend = backendFor("endpoints");
2173
+ const result = await callRestUrl(
2174
+ `${backend.base}${backend.get(params.endpointId)}`,
606
2175
  "DELETE"
607
2176
  );
608
- return {
609
- content: [
610
- {
611
- type: "text",
612
- text: JSON.stringify(result, null, 2)
613
- }
614
- ]
615
- };
2177
+ return jsonReply2(result);
2178
+ }
2179
+ );
2180
+ server.tool(
2181
+ "list-endpoint-releases",
2182
+ "List a Serverless endpoint's release history and current rollout status (workers on the latest version). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2183
+ {
2184
+ ...listPaginationParams,
2185
+ endpointId: z8.string().describe("ID of the Serverless endpoint whose releases to list")
2186
+ },
2187
+ { title: "List endpoint releases", ...READ_ONLY },
2188
+ async (params) => {
2189
+ const backend = backendFor("endpoints");
2190
+ if (backend.version === "v1") {
2191
+ return jsonReply2({
2192
+ error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2193
+ status: 501
2194
+ });
2195
+ }
2196
+ const raw = await callRestUrl(
2197
+ `${backend.base}/serverless/${params.endpointId}/releases`
2198
+ );
2199
+ const releases = Array.isArray(raw?.releases) ? raw.releases : [];
2200
+ return capListResult(
2201
+ releases,
2202
+ { limit: params.limit, cursor: params.cursor },
2203
+ { rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
2204
+ );
2205
+ }
2206
+ );
2207
+ server.tool(
2208
+ "list-endpoint-workers",
2209
+ "List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2210
+ {
2211
+ ...listPaginationParams,
2212
+ endpointId: z8.string().describe("ID of the Serverless endpoint whose workers to list")
2213
+ },
2214
+ { title: "List endpoint workers", ...READ_ONLY },
2215
+ async (params) => {
2216
+ const backend = backendFor("workers");
2217
+ if (backend.version === "v1") {
2218
+ return jsonReply2({
2219
+ error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
2220
+ status: 501
2221
+ });
2222
+ }
2223
+ const raw = await callRestUrl(
2224
+ `${backend.base}/serverless/${params.endpointId}/workers`
2225
+ );
2226
+ return capListResult(
2227
+ backend.unwrap(raw),
2228
+ { limit: params.limit, cursor: params.cursor },
2229
+ { summary: raw?.summary, endpointVersion: raw?.endpointVersion }
2230
+ );
616
2231
  }
617
2232
  );
618
- const endpointIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
619
- const jobIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
620
- const inputSchema = z2.record(z2.unknown()).describe(
2233
+ server.tool(
2234
+ "stream-worker-logs",
2235
+ "Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
2236
+ {
2237
+ endpointId: z8.string().describe("ID of the Serverless endpoint the worker belongs to"),
2238
+ workerId: z8.string().describe(
2239
+ "ID of the worker whose logs to stream (from list-endpoint-workers)"
2240
+ ),
2241
+ ...logStreamParams
2242
+ },
2243
+ { title: "Stream worker logs", ...READ_ONLY },
2244
+ (params) => streamLogsReply(
2245
+ rt,
2246
+ {
2247
+ name: "stream-worker-logs",
2248
+ resource: "workers",
2249
+ logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
2250
+ },
2251
+ params
2252
+ )
2253
+ );
2254
+ }
2255
+
2256
+ // src/tools/jobs.ts
2257
+ import { z as z9 } from "zod";
2258
+ function registerJobTools(server, rt) {
2259
+ const { jsonReply: jsonReply2, serverlessRequest } = rt;
2260
+ const endpointIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2261
+ const jobIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2262
+ const inputSchema = z9.record(z9.unknown()).describe(
621
2263
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
622
2264
  );
623
- const webhookSchema = z2.string().url().optional().describe(
2265
+ const webhookSchema = z9.string().url().optional().describe(
624
2266
  "Webhook URL to receive job completion notifications instead of polling"
625
2267
  );
626
- const policySchema = z2.object({
627
- executionTimeout: z2.number().optional().describe("Maximum execution time in milliseconds"),
628
- lowPriority: z2.boolean().optional().describe("Submit as a low-priority job"),
629
- ttl: z2.number().optional().describe("Time-to-live for the job result in milliseconds")
2268
+ const policySchema = z9.object({
2269
+ executionTimeout: z9.number().optional().describe("Maximum execution time in milliseconds"),
2270
+ lowPriority: z9.boolean().optional().describe("Submit as a low-priority job"),
2271
+ ttl: z9.number().optional().describe("Time-to-live for the job result in milliseconds")
630
2272
  }).optional().describe("Execution policy options");
631
- const s3ConfigSchema = z2.object({
632
- accessId: z2.string().describe("S3 access key ID"),
633
- accessSecret: z2.string().describe("S3 secret access key"),
634
- bucketName: z2.string().describe("S3 bucket name"),
635
- endpointUrl: z2.string().describe("S3 endpoint URL")
2273
+ const s3ConfigSchema = z9.object({
2274
+ accessId: z9.string().describe("S3 access key ID"),
2275
+ accessSecret: z9.string().describe("S3 secret access key"),
2276
+ bucketName: z9.string().describe("S3 bucket name"),
2277
+ endpointUrl: z9.string().describe("S3 endpoint URL")
636
2278
  }).optional().describe("S3-compatible storage config for large outputs");
637
2279
  server.tool(
638
2280
  "run-endpoint",
@@ -646,6 +2288,7 @@ function registerTools(server, ctx) {
646
2288
  policy: policySchema,
647
2289
  s3Config: s3ConfigSchema
648
2290
  },
2291
+ { title: "Run endpoint (async)", ...WRITE },
649
2292
  async (params) => {
650
2293
  const { endpointId, ...body } = params;
651
2294
  const result = await serverlessRequest(
@@ -654,14 +2297,7 @@ function registerTools(server, ctx) {
654
2297
  "POST",
655
2298
  body
656
2299
  );
657
- return {
658
- content: [
659
- {
660
- type: "text",
661
- text: JSON.stringify(result, null, 2)
662
- }
663
- ]
664
- };
2300
+ return jsonReply2(result);
665
2301
  }
666
2302
  );
667
2303
  server.tool(
@@ -672,13 +2308,14 @@ function registerTools(server, ctx) {
672
2308
  "ID of the Serverless endpoint to run synchronously"
673
2309
  ),
674
2310
  input: inputSchema,
675
- wait: z2.number().min(1e3).max(3e5).optional().describe(
2311
+ wait: z9.number().min(1e3).max(3e5).optional().describe(
676
2312
  "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
677
2313
  ),
678
2314
  webhook: webhookSchema,
679
2315
  policy: policySchema,
680
2316
  s3Config: s3ConfigSchema
681
2317
  },
2318
+ { title: "Run endpoint (sync)", ...WRITE },
682
2319
  async (params) => {
683
2320
  const { endpointId, wait, ...body } = params;
684
2321
  const path = wait ? `/runsync?wait=${wait}` : "/runsync";
@@ -688,14 +2325,7 @@ function registerTools(server, ctx) {
688
2325
  "POST",
689
2326
  body
690
2327
  );
691
- return {
692
- content: [
693
- {
694
- type: "text",
695
- text: JSON.stringify(result, null, 2)
696
- }
697
- ]
698
- };
2328
+ return jsonReply2(result);
699
2329
  }
700
2330
  );
701
2331
  server.tool(
@@ -707,19 +2337,13 @@ function registerTools(server, ctx) {
707
2337
  ),
708
2338
  jobId: jobIdSchema.describe("ID of the job to check")
709
2339
  },
2340
+ { title: "Get job status", ...READ_ONLY },
710
2341
  async (params) => {
711
2342
  const result = await serverlessRequest(
712
2343
  params.endpointId,
713
2344
  `/status/${params.jobId}`
714
2345
  );
715
- return {
716
- content: [
717
- {
718
- type: "text",
719
- text: JSON.stringify(result, null, 2)
720
- }
721
- ]
722
- };
2346
+ return jsonReply2(result);
723
2347
  }
724
2348
  );
725
2349
  server.tool(
@@ -731,6 +2355,7 @@ function registerTools(server, ctx) {
731
2355
  ),
732
2356
  jobId: jobIdSchema.describe("ID of the job to stream results from")
733
2357
  },
2358
+ { title: "Stream job", ...READ_ONLY },
734
2359
  async (params) => {
735
2360
  const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
736
2361
  "COMPLETED",
@@ -776,18 +2401,7 @@ function registerTools(server, ctx) {
776
2401
  }
777
2402
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
778
2403
  }
779
- return {
780
- content: [
781
- {
782
- type: "text",
783
- text: JSON.stringify(
784
- { ...finalResult, stream: allChunks },
785
- null,
786
- 2
787
- )
788
- }
789
- ]
790
- };
2404
+ return jsonReply2({ ...finalResult, stream: allChunks });
791
2405
  }
792
2406
  );
793
2407
  server.tool(
@@ -799,20 +2413,14 @@ function registerTools(server, ctx) {
799
2413
  ),
800
2414
  jobId: jobIdSchema.describe("ID of the job to cancel")
801
2415
  },
2416
+ { title: "Cancel job", ...WRITE, idempotentHint: true },
802
2417
  async (params) => {
803
2418
  const result = await serverlessRequest(
804
2419
  params.endpointId,
805
2420
  `/cancel/${params.jobId}`,
806
2421
  "POST"
807
2422
  );
808
- return {
809
- content: [
810
- {
811
- type: "text",
812
- text: JSON.stringify(result, null, 2)
813
- }
814
- ]
815
- };
2423
+ return jsonReply2(result);
816
2424
  }
817
2425
  );
818
2426
  server.tool(
@@ -824,20 +2432,14 @@ function registerTools(server, ctx) {
824
2432
  ),
825
2433
  jobId: jobIdSchema.describe("ID of the job to retry")
826
2434
  },
2435
+ { title: "Retry job", ...WRITE },
827
2436
  async (params) => {
828
2437
  const result = await serverlessRequest(
829
2438
  params.endpointId,
830
2439
  `/retry/${params.jobId}`,
831
2440
  "POST"
832
2441
  );
833
- return {
834
- content: [
835
- {
836
- type: "text",
837
- text: JSON.stringify(result, null, 2)
838
- }
839
- ]
840
- };
2442
+ return jsonReply2(result);
841
2443
  }
842
2444
  );
843
2445
  server.tool(
@@ -848,16 +2450,10 @@ function registerTools(server, ctx) {
848
2450
  "ID of the Serverless endpoint to check health for"
849
2451
  )
850
2452
  },
2453
+ { title: "Endpoint health", ...READ_ONLY },
851
2454
  async (params) => {
852
2455
  const result = await serverlessRequest(params.endpointId, "/health");
853
- return {
854
- content: [
855
- {
856
- type: "text",
857
- text: JSON.stringify(result, null, 2)
858
- }
859
- ]
860
- };
2456
+ return jsonReply2(result);
861
2457
  }
862
2458
  );
863
2459
  server.tool(
@@ -868,46 +2464,51 @@ function registerTools(server, ctx) {
868
2464
  "ID of the Serverless endpoint to purge the queue for"
869
2465
  )
870
2466
  },
2467
+ { title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
871
2468
  async (params) => {
872
2469
  const result = await serverlessRequest(
873
2470
  params.endpointId,
874
2471
  "/purge-queue",
875
2472
  "POST"
876
2473
  );
877
- return {
878
- content: [
879
- {
880
- type: "text",
881
- text: JSON.stringify(result, null, 2)
882
- }
883
- ]
884
- };
2474
+ return jsonReply2(result);
885
2475
  }
886
2476
  );
2477
+ }
2478
+
2479
+ // src/tools/templates.ts
2480
+ import { z as z10 } from "zod";
2481
+ function registerTemplateTools(server, rt) {
2482
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
887
2483
  server.tool(
888
2484
  "list-templates",
889
2485
  `List available templates. By default returns only the user's own templates. Use includeRunpodTemplates to also include official Runpod templates. The recommended default template for new pods is "Runpod Pytorch 2.8.0" (ID: runpod-torch-v280) \u2014 it has the latest CUDA and PyTorch versions.`,
890
2486
  {
891
2487
  ...listPaginationParams,
892
- includeRunpodTemplates: z2.boolean().optional().describe("Include official Runpod templates in the response"),
893
- includePublicTemplates: z2.boolean().optional().describe("Include community-made public templates in the response"),
894
- includeEndpointBoundTemplates: z2.boolean().optional().describe(
2488
+ includeRunpodTemplates: z10.boolean().optional().describe("Include official Runpod templates in the response"),
2489
+ includePublicTemplates: z10.boolean().optional().describe("Include community-made public templates in the response"),
2490
+ includeEndpointBoundTemplates: z10.boolean().optional().describe(
895
2491
  "Include templates bound to Serverless endpoints in the response"
896
2492
  )
897
2493
  },
2494
+ { title: "List templates", ...READ_ONLY },
898
2495
  async (params) => {
899
- const queryParams = new URLSearchParams();
900
- if (params.includeRunpodTemplates)
901
- queryParams.set("includeRunpodTemplates", "true");
902
- if (params.includePublicTemplates)
903
- queryParams.set("includePublicTemplates", "true");
904
- if (params.includeEndpointBoundTemplates)
905
- queryParams.set("includeEndpointBoundTemplates", "true");
906
- const query = queryParams.toString();
907
- const result = await runpodRequest(
908
- `/templates${query ? `?${query}` : ""}`
2496
+ const backend = backendFor("templates");
2497
+ let query = "";
2498
+ if (backend.version === "v1") {
2499
+ const queryParams = new URLSearchParams();
2500
+ if (params.includeRunpodTemplates)
2501
+ queryParams.set("includeRunpodTemplates", "true");
2502
+ if (params.includePublicTemplates)
2503
+ queryParams.set("includePublicTemplates", "true");
2504
+ if (params.includeEndpointBoundTemplates)
2505
+ queryParams.set("includeEndpointBoundTemplates", "true");
2506
+ query = queryParams.toString() ? `?${queryParams.toString()}` : "";
2507
+ }
2508
+ const result = await callRestUrl(
2509
+ `${backend.base}${backend.list}${query}`
909
2510
  );
910
- return capListResult(result, {
2511
+ return capListResult(backend.unwrap(result), {
911
2512
  limit: params.limit,
912
2513
  cursor: params.cursor
913
2514
  });
@@ -915,196 +2516,223 @@ function registerTools(server, ctx) {
915
2516
  );
916
2517
  server.tool(
917
2518
  "get-template",
2519
+ "Get one template by id (image, env, ports, disk/volume config).",
918
2520
  {
919
- templateId: z2.string().describe("ID of the template to retrieve")
2521
+ templateId: z10.string().describe("ID of the template to retrieve")
920
2522
  },
2523
+ { title: "Get template", ...READ_ONLY },
921
2524
  async (params) => {
922
- const result = await runpodRequest(`/templates/${params.templateId}`);
923
- return {
924
- content: [
925
- {
926
- type: "text",
927
- text: JSON.stringify(result, null, 2)
928
- }
929
- ]
930
- };
2525
+ const backend = backendFor("templates");
2526
+ const result = await callRestUrl(
2527
+ `${backend.base}${backend.get(params.templateId)}`
2528
+ );
2529
+ return jsonReply2(result);
931
2530
  }
932
2531
  );
933
2532
  server.tool(
934
2533
  "create-template",
2534
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
935
2535
  {
936
- name: z2.string().describe("Name for the template"),
937
- imageName: z2.string().describe("Docker image to use"),
938
- isServerless: z2.boolean().optional().describe("Is this a serverless template"),
939
- ports: z2.array(z2.string()).optional().describe("Ports to expose"),
940
- dockerEntrypoint: z2.array(z2.string()).optional().describe("Docker entrypoint commands"),
941
- dockerStartCmd: z2.array(z2.string()).optional().describe("Docker start commands"),
942
- env: z2.record(z2.string()).optional().describe("Environment variables"),
943
- containerDiskInGb: z2.number().optional().describe("Container disk size in GB"),
944
- volumeInGb: z2.number().optional().describe("Volume size in GB"),
945
- volumeMountPath: z2.string().optional().describe("Path to mount the volume"),
946
- readme: z2.string().optional().describe("README content in markdown format"),
947
- containerRegistryAuthId: z2.string().optional().describe(
2536
+ name: z10.string().describe("Name for the template"),
2537
+ imageName: z10.string().describe("Docker image to use"),
2538
+ isServerless: z10.boolean().optional().describe("Is this a serverless template"),
2539
+ ports: z10.array(z10.string()).optional().describe("Ports to expose"),
2540
+ dockerEntrypoint: z10.array(z10.string()).optional().describe(
2541
+ "Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
2542
+ ),
2543
+ dockerStartCmd: z10.array(z10.string()).optional().describe(
2544
+ 'Container start command. On v2 this is stored as the template\'s `args` string; a multi-element array is joined with spaces (e.g. ["python","-u","app.py"] \u2192 "python -u app.py"). The join is lossy: an element that itself contains spaces is NOT preserved as one argument \u2014 e.g. ["sh","-c","echo hi"] becomes "sh -c echo hi", so pass a shell command as a single pre-quoted element if that distinction matters. Omit or pass an empty array to leave the command unset (never overwrites with "").'
2545
+ ),
2546
+ env: z10.record(z10.string()).optional().describe("Environment variables"),
2547
+ containerDiskInGb: z10.number().optional().describe("Container disk size in GB"),
2548
+ volumeInGb: z10.number().optional().describe("Volume size in GB"),
2549
+ volumeMountPath: z10.string().optional().describe("Path to mount the volume"),
2550
+ readme: z10.string().optional().describe("README content in markdown format"),
2551
+ category: z10.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2552
+ "How the template is grouped and filtered in the Runpod console \u2014 it does NOT affect hardware selection, scheduling, or billing. Optional; defaults to NVIDIA. Set CPU or AMD only to file the template correctly in the console."
2553
+ ),
2554
+ containerRegistryAuthId: z10.string().optional().describe(
948
2555
  "ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Required when imageName points at a private registry; without it serverless workers fail the image pull."
949
2556
  )
950
2557
  },
2558
+ { title: "Create template", ...WRITE },
951
2559
  async (params) => {
952
- const result = await runpodRequest("/templates", "POST", params);
953
- return {
954
- content: [
955
- {
956
- type: "text",
957
- text: JSON.stringify(result, null, 2)
958
- }
959
- ]
960
- };
2560
+ const backend = backendFor("templates");
2561
+ const body = backend.mapCreate(params);
2562
+ const result = await callRestUrl(
2563
+ `${backend.base}${backend.list}`,
2564
+ "POST",
2565
+ body
2566
+ );
2567
+ return jsonReply2(result);
961
2568
  }
962
2569
  );
963
2570
  server.tool(
964
2571
  "update-template",
2572
+ "Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
965
2573
  {
966
- templateId: z2.string().describe("ID of the template to update"),
967
- name: z2.string().optional().describe("New name for the template"),
968
- imageName: z2.string().optional().describe("New Docker image"),
969
- ports: z2.array(z2.string()).optional().describe("New ports to expose"),
970
- env: z2.record(z2.string()).optional().describe("New environment variables"),
971
- readme: z2.string().optional().describe("New README content in markdown format"),
972
- containerRegistryAuthId: z2.string().optional().describe(
2574
+ templateId: z10.string().describe("ID of the template to update"),
2575
+ name: z10.string().optional().describe("New name for the template"),
2576
+ imageName: z10.string().optional().describe("New Docker image"),
2577
+ dockerStartCmd: z10.array(z10.string()).optional().describe(
2578
+ "New container start command. On v2 stored as the template's `args` string; a multi-element array is joined with spaces (lossy \u2014 an element containing spaces is split into separate tokens). Note: this can only set a command, not clear one \u2014 an empty or absent array is ignored so the existing command is preserved."
2579
+ ),
2580
+ ports: z10.array(z10.string()).optional().describe("New ports to expose"),
2581
+ env: z10.record(z10.string()).optional().describe("New environment variables"),
2582
+ readme: z10.string().optional().describe("New README content in markdown format"),
2583
+ containerRegistryAuthId: z10.string().optional().describe(
973
2584
  "ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Set this to attach a credential to a template referencing a private registry."
974
2585
  )
975
2586
  },
2587
+ { title: "Update template", ...WRITE, idempotentHint: true },
976
2588
  async (params) => {
977
2589
  const { templateId, ...updateParams } = params;
978
- const result = await runpodRequest(
979
- `/templates/${templateId}`,
2590
+ const backend = backendFor("templates");
2591
+ const body = backend.mapUpdate(updateParams);
2592
+ const result = await callRestUrl(
2593
+ `${backend.base}${backend.get(templateId)}`,
980
2594
  "PATCH",
981
- updateParams
2595
+ body
982
2596
  );
983
- return {
984
- content: [
985
- {
986
- type: "text",
987
- text: JSON.stringify(result, null, 2)
988
- }
989
- ]
990
- };
2597
+ return jsonReply2(result);
991
2598
  }
992
2599
  );
993
2600
  server.tool(
994
2601
  "delete-template",
2602
+ "Permanently delete a template. This cannot be undone.",
995
2603
  {
996
- templateId: z2.string().describe("ID of the template to delete")
2604
+ templateId: z10.string().describe("ID of the template to delete")
997
2605
  },
2606
+ { title: "Delete template", ...DESTRUCTIVE },
998
2607
  async (params) => {
999
- const result = await runpodRequest(
1000
- `/templates/${params.templateId}`,
2608
+ const backend = backendFor("templates");
2609
+ const result = await callRestUrl(
2610
+ `${backend.base}${backend.get(params.templateId)}`,
1001
2611
  "DELETE"
1002
2612
  );
1003
- return {
1004
- content: [
1005
- {
1006
- type: "text",
1007
- text: JSON.stringify(result, null, 2)
1008
- }
1009
- ]
1010
- };
2613
+ return jsonReply2(result);
2614
+ }
2615
+ );
2616
+ }
2617
+
2618
+ // src/tools/network-volumes.ts
2619
+ import { z as z11 } from "zod";
2620
+ function registerNetworkVolumeTools(server, rt) {
2621
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2622
+ server.tool(
2623
+ "list-network-volumes",
2624
+ "List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
2625
+ listPaginationParams,
2626
+ { title: "List network volumes", ...READ_ONLY },
2627
+ async (params) => {
2628
+ const backend = backendFor("networkVolumes");
2629
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
2630
+ return capListResult(backend.unwrap(result), {
2631
+ limit: params.limit,
2632
+ cursor: params.cursor
2633
+ });
1011
2634
  }
1012
2635
  );
1013
- server.tool("list-network-volumes", listPaginationParams, async (params) => {
1014
- const result = await runpodRequest("/networkvolumes");
1015
- return capListResult(result, {
1016
- limit: params.limit,
1017
- cursor: params.cursor
1018
- });
1019
- });
1020
2636
  server.tool(
1021
2637
  "get-network-volume",
2638
+ "Get one network volume by id (name, size, data center).",
1022
2639
  {
1023
- networkVolumeId: z2.string().describe("ID of the network volume to retrieve")
2640
+ networkVolumeId: z11.string().describe("ID of the network volume to retrieve")
1024
2641
  },
2642
+ { title: "Get network volume", ...READ_ONLY },
1025
2643
  async (params) => {
1026
- const result = await runpodRequest(
1027
- `/networkvolumes/${params.networkVolumeId}`
2644
+ const backend = backendFor("networkVolumes");
2645
+ const result = await callRestUrl(
2646
+ `${backend.base}${backend.get(params.networkVolumeId)}`
1028
2647
  );
1029
- return {
1030
- content: [
1031
- {
1032
- type: "text",
1033
- text: JSON.stringify(result, null, 2)
1034
- }
1035
- ]
1036
- };
2648
+ return jsonReply2(result);
1037
2649
  }
1038
2650
  );
1039
2651
  server.tool(
1040
2652
  "create-network-volume",
2653
+ "Create a persistent network volume in a data center (size 10\u20134096 GB) that pods can mount. Pass volumeType to pick a storage tier; omit it to get the data center default.",
1041
2654
  {
1042
- name: z2.string().describe("Name for the network volume"),
1043
- size: z2.number().describe("Size in GB (1-4000)"),
1044
- dataCenterId: z2.string().describe("Data center ID")
2655
+ name: z11.string().describe("Name for the network volume"),
2656
+ // Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
2657
+ // here as well as described, so an out-of-range value fails locally
2658
+ // instead of costing an API roundtrip.
2659
+ size: z11.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
2660
+ dataCenterId: z11.string().describe("Data center ID (see list-data-centers)"),
2661
+ volumeType: z11.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
2662
+ "Storage tier. Omit for the data center's default (primary) tier. HIGH_PERFORMANCE provisions an HPS volume. Immutable after creation \u2014 update-network-volume cannot change it."
2663
+ )
1045
2664
  },
2665
+ { title: "Create network volume", ...WRITE },
1046
2666
  async (params) => {
1047
- const result = await runpodRequest("/networkvolumes", "POST", params);
1048
- return {
1049
- content: [
1050
- {
1051
- type: "text",
1052
- text: JSON.stringify(result, null, 2)
1053
- }
1054
- ]
1055
- };
2667
+ const backend = backendFor("networkVolumes");
2668
+ const body = backend.mapCreate(params);
2669
+ const result = await callRestUrl(
2670
+ `${backend.base}${backend.list}`,
2671
+ "POST",
2672
+ body
2673
+ );
2674
+ return jsonReply2(result);
1056
2675
  }
1057
2676
  );
1058
2677
  server.tool(
1059
2678
  "update-network-volume",
2679
+ "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1060
2680
  {
1061
- networkVolumeId: z2.string().describe("ID of the network volume to update"),
1062
- name: z2.string().optional().describe("New name for the network volume"),
1063
- size: z2.number().optional().describe("New size in GB (must be larger than current)")
2681
+ networkVolumeId: z11.string().describe("ID of the network volume to update"),
2682
+ name: z11.string().optional().describe("New name for the network volume"),
2683
+ // Same absolute bounds as create. "Larger than current" stays server-side —
2684
+ // we do not hold the current size here, so only the range is checked.
2685
+ size: z11.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
1064
2686
  },
2687
+ { title: "Update network volume", ...WRITE, idempotentHint: true },
1065
2688
  async (params) => {
1066
2689
  const { networkVolumeId, ...updateParams } = params;
1067
- const result = await runpodRequest(
1068
- `/networkvolumes/${networkVolumeId}`,
2690
+ const backend = backendFor("networkVolumes");
2691
+ const body = backend.mapUpdate(updateParams);
2692
+ const result = await callRestUrl(
2693
+ `${backend.base}${backend.get(networkVolumeId)}`,
1069
2694
  "PATCH",
1070
- updateParams
2695
+ body
1071
2696
  );
1072
- return {
1073
- content: [
1074
- {
1075
- type: "text",
1076
- text: JSON.stringify(result, null, 2)
1077
- }
1078
- ]
1079
- };
2697
+ return jsonReply2(result);
1080
2698
  }
1081
2699
  );
1082
2700
  server.tool(
1083
2701
  "delete-network-volume",
2702
+ "Permanently delete a network volume and its data. This cannot be undone.",
1084
2703
  {
1085
- networkVolumeId: z2.string().describe("ID of the network volume to delete")
2704
+ networkVolumeId: z11.string().describe("ID of the network volume to delete")
1086
2705
  },
2706
+ { title: "Delete network volume", ...DESTRUCTIVE },
1087
2707
  async (params) => {
1088
- const result = await runpodRequest(
1089
- `/networkvolumes/${params.networkVolumeId}`,
2708
+ const backend = backendFor("networkVolumes");
2709
+ const result = await callRestUrl(
2710
+ `${backend.base}${backend.get(params.networkVolumeId)}`,
1090
2711
  "DELETE"
1091
2712
  );
1092
- return {
1093
- content: [
1094
- {
1095
- type: "text",
1096
- text: JSON.stringify(result, null, 2)
1097
- }
1098
- ]
1099
- };
2713
+ return jsonReply2(result);
1100
2714
  }
1101
2715
  );
2716
+ }
2717
+
2718
+ // src/tools/registries.ts
2719
+ import { z as z12 } from "zod";
2720
+ var DELEGATIONS = "/delegations";
2721
+ function registerRegistryTools(server, rt) {
2722
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2723
+ const delegationsV2OnlyNotice = (tool) => ({
2724
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
2725
+ status: 501
2726
+ });
1102
2727
  server.tool(
1103
2728
  "list-container-registry-auths",
2729
+ "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
1104
2730
  listPaginationParams,
2731
+ { title: "List container registry auths", ...READ_ONLY },
1105
2732
  async (params) => {
1106
- const result = await runpodRequest("/containerregistryauth");
1107
- return capListResult(result, {
2733
+ const backend = backendFor("registries");
2734
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
2735
+ return capListResult(backend.unwrap(result), {
1108
2736
  limit: params.limit,
1109
2737
  cursor: params.cursor
1110
2738
  });
@@ -1112,72 +2740,229 @@ function registerTools(server, ctx) {
1112
2740
  );
1113
2741
  server.tool(
1114
2742
  "get-container-registry-auth",
2743
+ "Get one saved container-registry credential by id (the secret/password is not returned).",
1115
2744
  {
1116
- containerRegistryAuthId: z2.string().describe("ID of the container registry auth to retrieve")
2745
+ containerRegistryAuthId: z12.string().describe("ID of the container registry auth to retrieve")
1117
2746
  },
2747
+ { title: "Get container registry auth", ...READ_ONLY },
1118
2748
  async (params) => {
1119
- const result = await runpodRequest(
1120
- `/containerregistryauth/${params.containerRegistryAuthId}`
2749
+ const backend = backendFor("registries");
2750
+ const result = await callRestUrl(
2751
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`
1121
2752
  );
1122
- return {
1123
- content: [
1124
- {
1125
- type: "text",
1126
- text: JSON.stringify(result, null, 2)
1127
- }
1128
- ]
1129
- };
2753
+ return jsonReply2(result);
1130
2754
  }
1131
2755
  );
1132
2756
  server.tool(
1133
2757
  "create-container-registry-auth",
2758
+ "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1134
2759
  {
1135
- name: z2.string().describe("Name for the container registry auth"),
1136
- username: z2.string().describe("Registry username"),
1137
- password: z2.string().describe("Registry password")
2760
+ name: z12.string().describe("Name for the container registry auth"),
2761
+ username: z12.string().describe("Registry username"),
2762
+ password: z12.string().describe("Registry password or access token (stored as a secret)")
1138
2763
  },
2764
+ { title: "Create container registry auth", ...WRITE },
1139
2765
  async (params) => {
1140
- const result = await runpodRequest(
1141
- "/containerregistryauth",
2766
+ const backend = backendFor("registries");
2767
+ const body = backend.mapCreate(params);
2768
+ const result = await callRestUrl(
2769
+ `${backend.base}${backend.list}`,
1142
2770
  "POST",
1143
- params
2771
+ body
1144
2772
  );
1145
- return {
1146
- content: [
1147
- {
1148
- type: "text",
1149
- text: JSON.stringify(result, null, 2)
1150
- }
1151
- ]
1152
- };
2773
+ return jsonReply2(result);
1153
2774
  }
1154
2775
  );
1155
2776
  server.tool(
1156
2777
  "delete-container-registry-auth",
2778
+ "Permanently delete a saved container-registry credential. This cannot be undone.",
1157
2779
  {
1158
- containerRegistryAuthId: z2.string().describe("ID of the container registry auth to delete")
2780
+ containerRegistryAuthId: z12.string().describe("ID of the container registry auth to delete")
1159
2781
  },
2782
+ { title: "Delete container registry auth", ...DESTRUCTIVE },
1160
2783
  async (params) => {
1161
- const result = await runpodRequest(
1162
- `/containerregistryauth/${params.containerRegistryAuthId}`,
2784
+ const backend = backendFor("registries");
2785
+ const result = await callRestUrl(
2786
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`,
1163
2787
  "DELETE"
1164
2788
  );
1165
- return {
1166
- content: [
1167
- {
1168
- type: "text",
1169
- text: JSON.stringify(result, null, 2)
1170
- }
1171
- ]
1172
- };
2789
+ return jsonReply2(result);
2790
+ }
2791
+ );
2792
+ server.tool(
2793
+ "list-registry-delegations",
2794
+ "List your AWS ECR delegations \u2014 ECR repositories Runpod has been granted scoped pull access to, so private ECR images can be pulled without storing AWS credentials. Each entry includes the `dockerRegistryUri` to pull from. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2795
+ listPaginationParams,
2796
+ { title: "List registry delegations", ...READ_ONLY },
2797
+ async (params) => {
2798
+ const backend = backendFor("registries");
2799
+ if (backend.version === "v1") {
2800
+ return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
2801
+ }
2802
+ const raw = await callRestUrl(
2803
+ `${backend.base}${backend.list}${DELEGATIONS}`
2804
+ );
2805
+ const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
2806
+ return capListResult(delegations, {
2807
+ limit: params.limit,
2808
+ cursor: params.cursor
2809
+ });
2810
+ }
2811
+ );
2812
+ server.tool(
2813
+ "create-registry-delegation",
2814
+ "Register an AWS ECR repository so Runpod can pull private images from it without stored credentials. Pass the repository ARN; the reply carries the `dockerRegistryUri`, resolved repository/tag, and AWS region. v2-only \u2014 returns a 501 notice on the v1 API. For a non-ECR private registry use create-container-registry-auth instead.",
2815
+ {
2816
+ resource: z12.string().min(1).describe(
2817
+ 'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
2818
+ ),
2819
+ name: z12.string().optional().describe("Optional human-readable name for the delegation")
2820
+ },
2821
+ { title: "Create registry delegation", ...WRITE },
2822
+ async (params) => {
2823
+ const backend = backendFor("registries");
2824
+ if (backend.version === "v1") {
2825
+ return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
2826
+ }
2827
+ const body = { resource: params.resource };
2828
+ if (params.name !== void 0) body.name = params.name;
2829
+ const result = await callRestUrl(
2830
+ `${backend.base}${backend.list}${DELEGATIONS}`,
2831
+ "POST",
2832
+ body
2833
+ );
2834
+ return jsonReply2(result);
2835
+ }
2836
+ );
2837
+ server.tool(
2838
+ "delete-registry-delegation",
2839
+ "Revoke an AWS ECR delegation, withdrawing Runpod's pull access to that repository. Existing workers keep running; future pulls fail. v2-only \u2014 returns a 501 notice on the v1 API. This cannot be undone.",
2840
+ {
2841
+ delegationId: z12.string().describe(
2842
+ "ID of the delegation to revoke (from list-registry-delegations)"
2843
+ )
2844
+ },
2845
+ { title: "Revoke registry delegation", ...DESTRUCTIVE },
2846
+ async (params) => {
2847
+ const backend = backendFor("registries");
2848
+ if (backend.version === "v1") {
2849
+ return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
2850
+ }
2851
+ const result = await callRestUrl(
2852
+ `${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
2853
+ params.delegationId
2854
+ )}`,
2855
+ "DELETE"
2856
+ );
2857
+ return jsonReply2(result);
2858
+ }
2859
+ );
2860
+ }
2861
+
2862
+ // src/tools/billing.ts
2863
+ import { z as z13 } from "zod";
2864
+ var BILLING_SCOPE_PATH = {
2865
+ all: "/billing",
2866
+ pods: "/billing/pods",
2867
+ serverless: "/billing/serverless",
2868
+ endpoints: "/billing/endpoints",
2869
+ networkvolumes: "/billing/networkvolumes",
2870
+ clusters: "/billing/clusters"
2871
+ };
2872
+ function registerBillingTools(server, rt) {
2873
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2874
+ server.tool(
2875
+ "get-billing",
2876
+ "Get time-bucketed Runpod spend history, either aggregated across all resources or broken down by resource type (pods, serverless, endpoints, network volumes, clusters). v2-only \u2014 returns a 501 notice on the v1 API. Records are capped via limit/cursor; narrow the window with startTime/endTime or lastN.",
2877
+ {
2878
+ ...listPaginationParams,
2879
+ scope: z13.enum([
2880
+ "all",
2881
+ "pods",
2882
+ "serverless",
2883
+ "endpoints",
2884
+ "networkvolumes",
2885
+ "clusters"
2886
+ ]).optional().describe(
2887
+ 'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2888
+ ),
2889
+ startTime: z13.string().optional().describe(
2890
+ "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2891
+ ),
2892
+ endTime: z13.string().optional().describe(
2893
+ "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2894
+ ),
2895
+ bucketSize: z13.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2896
+ lastN: z13.number().int().positive().optional().describe(
2897
+ "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2898
+ )
2899
+ },
2900
+ { title: "Get billing history", ...READ_ONLY },
2901
+ async (params) => {
2902
+ const backend = backendFor("billing");
2903
+ if (backend.version === "v1") {
2904
+ return jsonReply2({
2905
+ error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2906
+ status: 501
2907
+ });
2908
+ }
2909
+ const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
2910
+ const query = new URLSearchParams();
2911
+ if (params.startTime) query.set("startTime", params.startTime);
2912
+ if (params.endTime) query.set("endTime", params.endTime);
2913
+ if (params.bucketSize) query.set("bucketSize", params.bucketSize);
2914
+ if (params.lastN !== void 0 && !params.startTime && !params.endTime)
2915
+ query.set("lastN", String(params.lastN));
2916
+ const qs = query.toString() ? `?${query.toString()}` : "";
2917
+ const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
2918
+ return capListResult(
2919
+ backend.unwrap(raw),
2920
+ { limit: params.limit, cursor: params.cursor },
2921
+ { metadata: raw?.metadata }
2922
+ );
1173
2923
  }
1174
2924
  );
1175
2925
  }
1176
2926
 
2927
+ // src/tools.ts
2928
+ function registerTools(server, ctx, deps = {}) {
2929
+ const rt = createToolRuntime(server, ctx, deps);
2930
+ registerCatalogTools(server, rt);
2931
+ registerHubTools(server, rt);
2932
+ registerPublicEndpointTools(server, rt);
2933
+ registerPodTools(server, rt);
2934
+ registerEndpointTools(server, rt);
2935
+ registerEndpointGpuTools(server, rt);
2936
+ registerJobTools(server, rt);
2937
+ registerTemplateTools(server, rt);
2938
+ registerNetworkVolumeTools(server, rt);
2939
+ registerRegistryTools(server, rt);
2940
+ registerBillingTools(server, rt);
2941
+ }
2942
+
1177
2943
  // src/stdio.ts
1178
2944
  var WIZARD_SUBCOMMANDS = ["add", "remove"];
1179
2945
  var subcommand = process.argv[2];
1180
2946
  var isWizardMode = WIZARD_SUBCOMMANDS.includes(subcommand);
2947
+ async function main(apiKey) {
2948
+ let v2Available;
2949
+ if (wantsAutoProbe(process.env)) {
2950
+ const probe = createV2Prober({
2951
+ // node-fetch's signature is wider than the `{ status }` the prober needs.
2952
+ fetch: fetch2,
2953
+ baseUrl: restV2Base(process.env),
2954
+ apiKey,
2955
+ // Surface a transient/timeout fallback to v1 — otherwise `auto` silently
2956
+ // pins the session to v1 with no signal (stderr is the MCP log channel).
2957
+ warn: (message) => console.error(`[runpod-mcp] ${message}`)
2958
+ });
2959
+ v2Available = await probe();
2960
+ }
2961
+ const server = createServer();
2962
+ registerTools(server, { apiKey, transport: "stdio" }, { v2Available });
2963
+ const transport = new StdioServerTransport();
2964
+ await server.connect(transport);
2965
+ }
1181
2966
  if (isWizardMode) {
1182
2967
  import("./wizard-37ZJSVB5.mjs").then(({ runWizard }) => runWizard(subcommand)).catch((error) => {
1183
2968
  console.error(error instanceof Error ? error.message : String(error));
@@ -1189,9 +2974,9 @@ if (isWizardMode) {
1189
2974
  console.error("RUNPOD_API_KEY environment variable is required");
1190
2975
  process.exit(1);
1191
2976
  }
1192
- const server = createServer();
1193
- registerTools(server, { apiKey: API_KEY, transport: "stdio" });
1194
- const transport = new StdioServerTransport();
1195
- server.connect(transport);
2977
+ main(API_KEY).catch((error) => {
2978
+ console.error("Failed to start Runpod MCP server:", error);
2979
+ process.exit(1);
2980
+ });
1196
2981
  }
1197
2982
  //# sourceMappingURL=stdio.mjs.map