@runpod/mcp-server 1.4.0 → 2.0.0

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