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