@runpod/mcp-server 1.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stdio.cjs CHANGED
@@ -444,15 +444,413 @@ var init_wizard = __esm({
444
444
 
445
445
  // src/stdio.ts
446
446
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
447
+ var import_node_fetch2 = __toESM(require("node-fetch"), 1);
447
448
 
448
449
  // src/server.ts
449
450
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
451
+
452
+ // src/_shared/mappers.ts
453
+ function compact(obj) {
454
+ const out = {};
455
+ for (const [k, v] of Object.entries(obj)) {
456
+ if (v !== void 0) out[k] = v;
457
+ }
458
+ return out;
459
+ }
460
+ function persistentMount(size, path2) {
461
+ if (size === void 0 || path2 === void 0) return void 0;
462
+ return { persistent: { size, path: path2 } };
463
+ }
464
+ function joinStartCmd(cmd) {
465
+ return cmd && cmd.length ? cmd.join(" ") : void 0;
466
+ }
467
+ function containerConfigToV2(p2) {
468
+ return compact({
469
+ image: p2.imageName,
470
+ disk: p2.containerDiskInGb,
471
+ ports: p2.ports,
472
+ env: p2.env,
473
+ mounts: persistentMount(p2.volumeInGb, p2.volumeMountPath)
474
+ });
475
+ }
476
+ function gpuConfigToV2(gpuTypeIds, gpuCount) {
477
+ const id = gpuTypeIds?.[0];
478
+ if (id === void 0) return void 0;
479
+ return compact({ id, count: gpuCount });
480
+ }
481
+ function mapPodCreateToV2(params) {
482
+ return compact({
483
+ ...containerConfigToV2(params),
484
+ name: params.name,
485
+ // Private-image registry credential (v2 ContainerConfig `registry`). When
486
+ // deploying from a template, this spreads over the template's registry, so
487
+ // passing it overrides the inherited credential. An empty string is an
488
+ // explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
489
+ // template's inherited credential. Undefined is dropped by compact, so an
490
+ // omitted param leaves the template's registry in place.
491
+ registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
492
+ cloud: params.cloudType,
493
+ // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
494
+ // centers; omit/empty = scheduler chooses). Pass the v1 list straight
495
+ // through — the earlier singular `dataCenter` key did not exist on the pod
496
+ // schema, so placement was being silently dropped. GPU, by contrast, IS
497
+ // singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
498
+ // create-pod handler warns when more than one gpuTypeId was supplied.
499
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
500
+ gpu: gpuConfigToV2(params.gpuTypeIds, params.gpuCount)
501
+ });
502
+ }
503
+ function mapPodUpdateToV2(params) {
504
+ return {
505
+ ...containerConfigToV2(params),
506
+ ...compact({ name: params.name })
507
+ };
508
+ }
509
+ function nestedOrUndefined(obj) {
510
+ const c = compact(obj);
511
+ return Object.keys(c).length ? c : void 0;
512
+ }
513
+ function endpointGpuConfig(pools, count) {
514
+ if (!pools?.length) return void 0;
515
+ return compact({ pools, count });
516
+ }
517
+ function mapEndpointToV2(params) {
518
+ return compact({
519
+ name: params.name,
520
+ image: params.imageName,
521
+ args: params.args,
522
+ disk: params.containerDiskInGb,
523
+ ports: params.ports,
524
+ env: params.env,
525
+ registry: params.containerRegistryAuthId,
526
+ gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
527
+ workers: nestedOrUndefined({
528
+ min: params.workersMin,
529
+ max: params.workersMax
530
+ }),
531
+ scaling: nestedOrUndefined({
532
+ type: params.scalerType,
533
+ value: params.scalerValue,
534
+ idleTimeout: params.idleTimeout
535
+ }),
536
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
537
+ networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
538
+ timeout: params.executionTimeoutMs,
539
+ flashboot: params.flashboot
540
+ });
541
+ }
542
+ var mapEndpointCreateToV2 = mapEndpointToV2;
543
+ var mapEndpointUpdateToV2 = mapEndpointToV2;
544
+ function mapNetworkVolumeCreateToV2(params) {
545
+ return compact({
546
+ name: params.name,
547
+ size: params.size,
548
+ dataCenter: params.dataCenterId,
549
+ type: params.volumeType
550
+ });
551
+ }
552
+ function mapTemplateCreateToV2(params) {
553
+ return {
554
+ ...containerConfigToV2(params),
555
+ ...compact({
556
+ name: params.name,
557
+ serverless: params.isServerless,
558
+ registry: params.containerRegistryAuthId,
559
+ args: joinStartCmd(params.dockerStartCmd),
560
+ // No longer required by v2 (it defaults to NVIDIA server-side), so an
561
+ // unset category is dropped by compact instead of being forced here.
562
+ category: params.category
563
+ })
564
+ };
565
+ }
566
+ function podBodyFromTemplate(template) {
567
+ const out = {};
568
+ const keys = [
569
+ "name",
570
+ "image",
571
+ "args",
572
+ "disk",
573
+ "ports",
574
+ "env",
575
+ "mounts",
576
+ "registry"
577
+ ];
578
+ for (const key of keys) {
579
+ const value = template[key];
580
+ if (value !== void 0 && value !== null) out[key] = value;
581
+ }
582
+ return out;
583
+ }
584
+ function mapTemplateUpdateToV2(params) {
585
+ return {
586
+ ...containerConfigToV2(params),
587
+ ...compact({
588
+ name: params.name,
589
+ registry: params.containerRegistryAuthId,
590
+ args: joinStartCmd(params.dockerStartCmd)
591
+ })
592
+ };
593
+ }
594
+
595
+ // src/_shared/backend.ts
596
+ var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
597
+ var V2_UNWRAP_KEY = {
598
+ pods: "pods",
599
+ templates: "templates",
600
+ networkVolumes: "networkVolumes",
601
+ registries: "registries",
602
+ gpus: "gpus",
603
+ cpus: "cpus",
604
+ dataCenters: "dataCenters",
605
+ endpoints: "endpoints",
606
+ jobs: "jobs",
607
+ workers: "workers",
608
+ billing: "records"
609
+ };
610
+ var RESOURCE_ENV_KEY = {
611
+ pods: "PODS",
612
+ templates: "TEMPLATES",
613
+ networkVolumes: "NETWORK_VOLUMES",
614
+ registries: "REGISTRIES",
615
+ gpus: "GPUS",
616
+ cpus: "CPUS",
617
+ dataCenters: "DATA_CENTERS",
618
+ endpoints: "ENDPOINTS",
619
+ jobs: "JOBS",
620
+ workers: "WORKERS",
621
+ billing: "BILLING"
622
+ };
623
+ function restV1Base(env) {
624
+ return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
625
+ }
626
+ function restV2Base(env) {
627
+ return env.RUNPOD_REST_V2_API_URL ?? "https://v2-rest.runpod.io/v2";
628
+ }
629
+ function serverlessBase(env) {
630
+ return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
631
+ }
632
+ function publicGraphqlBase(env) {
633
+ return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
634
+ }
635
+ function authedGraphqlBase(env) {
636
+ return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
637
+ }
638
+ function unwrapList(resource, version, raw) {
639
+ if (version === "v1") return Array.isArray(raw) ? raw : [];
640
+ if (raw === null || typeof raw !== "object") return [];
641
+ const arr = raw[V2_UNWRAP_KEY[resource]];
642
+ return Array.isArray(arr) ? arr : [];
643
+ }
644
+ function resolveVersion(opts) {
645
+ const { resource, env, ctx, v2Available } = opts;
646
+ if (V1_ONLY.has(resource)) return "v1";
647
+ const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
648
+ const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
649
+ if (setting === "v2") return "v2";
650
+ if (setting === "auto") {
651
+ if (ctx.transport === "http") return "v1";
652
+ return v2Available === true ? "v2" : "v1";
653
+ }
654
+ return "v1";
655
+ }
656
+ function wantsAutoProbe(env) {
657
+ return Object.entries(env).some(
658
+ ([k, v]) => k.startsWith("RUNPOD_REST_VERSION") && (v ?? "").toLowerCase() === "auto"
659
+ );
660
+ }
661
+ function restVersionSummary(env) {
662
+ const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
663
+ const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
664
+ const overrides = Object.keys(env).filter(
665
+ (k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
666
+ ).length;
667
+ return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
668
+ }
669
+ var PROBE_TIMEOUT_MS = 4e3;
670
+ function classifyProbeStatus(status) {
671
+ if (status >= 500) return void 0;
672
+ if (status === 401 || status === 403) return void 0;
673
+ return status >= 200 && status < 300;
674
+ }
675
+ async function runV2Probe(deps) {
676
+ try {
677
+ const res = await deps.fetch(`${deps.baseUrl}/catalog/gpus`, {
678
+ method: "GET",
679
+ headers: { Authorization: `Bearer ${deps.apiKey}` },
680
+ signal: AbortSignal.timeout(deps.timeoutMs ?? PROBE_TIMEOUT_MS)
681
+ });
682
+ const verdict = classifyProbeStatus(res.status);
683
+ if (verdict === void 0) {
684
+ deps.warn?.(
685
+ `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.`
686
+ );
687
+ }
688
+ return verdict;
689
+ } catch {
690
+ deps.warn?.(
691
+ `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.`
692
+ );
693
+ return void 0;
694
+ }
695
+ }
696
+ function createV2Prober(deps) {
697
+ let cached;
698
+ let inFlight;
699
+ const probeOnce = async () => {
700
+ const verdict = await runV2Probe(deps);
701
+ if (verdict !== void 0) cached = verdict;
702
+ return verdict ?? false;
703
+ };
704
+ return () => {
705
+ if (cached !== void 0) return Promise.resolve(cached);
706
+ if (!inFlight) {
707
+ inFlight = probeOnce().finally(() => {
708
+ inFlight = void 0;
709
+ });
710
+ }
711
+ return inFlight;
712
+ };
713
+ }
714
+ var identity = (body) => body;
715
+ var V1_REST_PATHS = {
716
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
717
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
718
+ networkVolumes: {
719
+ list: "/networkvolumes",
720
+ get: (id) => `/networkvolumes/${id}`
721
+ },
722
+ registries: {
723
+ list: "/containerregistryauth",
724
+ get: (id) => `/containerregistryauth/${id}`
725
+ },
726
+ endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
727
+ };
728
+ var CATALOG = /* @__PURE__ */ new Set([
729
+ "gpus",
730
+ "cpus",
731
+ "dataCenters"
732
+ ]);
733
+ var V2_ONLY = /* @__PURE__ */ new Set([
734
+ "workers",
735
+ "billing"
736
+ ]);
737
+ var V2_REST_PATHS = {
738
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
739
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
740
+ networkVolumes: {
741
+ list: "/network-volumes",
742
+ get: (id) => `/network-volumes/${id}`
743
+ },
744
+ registries: { list: "/registries", get: (id) => `/registries/${id}` },
745
+ gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
746
+ cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
747
+ dataCenters: {
748
+ list: "/catalog/datacenters",
749
+ get: (id) => `/catalog/datacenters/${id}`
750
+ },
751
+ // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
752
+ // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
753
+ // unwrap key stays correct.
754
+ endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
755
+ // workers/billing build their own sub-paths in the tool from `base`; the
756
+ // list entry just satisfies buildV2Backend (which requires a paths entry).
757
+ workers: { list: "/serverless" },
758
+ billing: { list: "/billing" }
759
+ };
760
+ var V2_MAPPERS = {
761
+ pods: {
762
+ create: (b) => mapPodCreateToV2(b),
763
+ update: (b) => mapPodUpdateToV2(b)
764
+ },
765
+ templates: {
766
+ create: (b) => mapTemplateCreateToV2(b),
767
+ update: (b) => mapTemplateUpdateToV2(b)
768
+ },
769
+ networkVolumes: {
770
+ create: (b) => mapNetworkVolumeCreateToV2(
771
+ b
772
+ ),
773
+ update: identity
774
+ },
775
+ registries: { create: identity, update: identity },
776
+ endpoints: {
777
+ create: (b) => mapEndpointCreateToV2(b),
778
+ update: (b) => mapEndpointUpdateToV2(b)
779
+ }
780
+ };
781
+ function buildV2Backend(resource, env, unwrap) {
782
+ const paths = V2_REST_PATHS[resource];
783
+ if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
784
+ const mappers = V2_MAPPERS[resource];
785
+ return {
786
+ kind: "rest",
787
+ version: "v2",
788
+ base: restV2Base(env),
789
+ list: paths.list,
790
+ get: paths.get,
791
+ unwrap,
792
+ mapCreate: mappers?.create ?? identity,
793
+ mapUpdate: mappers?.update ?? identity
794
+ };
795
+ }
796
+ function buildV1Backend(resource, env, unwrap) {
797
+ if (resource === "jobs") {
798
+ return {
799
+ kind: "rest",
800
+ version: "v1",
801
+ base: serverlessBase(env),
802
+ unwrap,
803
+ mapCreate: identity,
804
+ mapUpdate: identity
805
+ };
806
+ }
807
+ if (V2_ONLY.has(resource)) {
808
+ return {
809
+ kind: "rest",
810
+ version: "v1",
811
+ base: restV1Base(env),
812
+ unwrap,
813
+ mapCreate: identity,
814
+ mapUpdate: identity
815
+ };
816
+ }
817
+ if (CATALOG.has(resource)) {
818
+ return {
819
+ kind: "graphql",
820
+ version: "v1",
821
+ base: publicGraphqlBase(env),
822
+ unwrap,
823
+ mapCreate: identity,
824
+ mapUpdate: identity
825
+ };
826
+ }
827
+ const paths = V1_REST_PATHS[resource];
828
+ if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
829
+ return {
830
+ kind: "rest",
831
+ version: "v1",
832
+ base: restV1Base(env),
833
+ list: paths.list,
834
+ get: paths.get,
835
+ unwrap,
836
+ mapCreate: identity,
837
+ mapUpdate: identity
838
+ };
839
+ }
840
+ function resolveBackend(opts) {
841
+ const { resource, env } = opts;
842
+ const version = resolveVersion(opts);
843
+ const unwrap = (raw) => unwrapList(resource, version, raw);
844
+ return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
845
+ }
846
+
847
+ // src/server.ts
450
848
  var SERVER_NAME = "Runpod API Server";
451
- var SERVER_VERSION = true ? "1.4.0" : "dev";
849
+ var SERVER_VERSION = true ? "3.0.0" : "dev";
452
850
  function createServer(version = SERVER_VERSION) {
453
851
  return new import_mcp.McpServer({
454
852
  name: SERVER_NAME,
455
- version,
853
+ version: `${version} [${restVersionSummary(process.env)}]`,
456
854
  capabilities: {
457
855
  resources: {},
458
856
  tools: {}
@@ -460,20 +858,347 @@ function createServer(version = SERVER_VERSION) {
460
858
  });
461
859
  }
462
860
 
463
- // src/tools.ts
464
- var import_zod2 = require("zod");
861
+ // src/tools/runtime.ts
465
862
  var import_node_fetch = __toESM(require("node-fetch"), 1);
466
863
  var import_node_crypto = require("crypto");
467
864
 
468
- // src/pagination.ts
865
+ // src/_shared/http.ts
866
+ var HttpError = class extends Error {
867
+ constructor(prefix, status, body) {
868
+ const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
869
+ super(`${prefix}: ${status} - ${body}${hint}`);
870
+ this.name = "HttpError";
871
+ this.status = status;
872
+ this.body = body;
873
+ }
874
+ };
875
+ function isJsonContentType(contentType) {
876
+ if (!contentType) return false;
877
+ const ct = contentType.toLowerCase();
878
+ return ct.includes("application/json") || ct.includes("+json");
879
+ }
880
+ function methodSendsBody(method) {
881
+ return method === "POST" || method === "PATCH" || method === "PUT";
882
+ }
883
+ function buildRequestHeaders(apiKey, tracking) {
884
+ return {
885
+ Authorization: `Bearer ${apiKey}`,
886
+ "Content-Type": "application/json",
887
+ ...tracking()
888
+ };
889
+ }
890
+ function createHttpClient(deps) {
891
+ return async function request(url, method = "GET", body) {
892
+ const init = {
893
+ method,
894
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
895
+ };
896
+ if (body && methodSendsBody(method)) {
897
+ init.body = JSON.stringify(body);
898
+ }
899
+ const response = await deps.fetch(url, init);
900
+ if (!response.ok) {
901
+ throw new HttpError(
902
+ deps.errorPrefix,
903
+ response.status,
904
+ await response.text()
905
+ );
906
+ }
907
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
908
+ };
909
+ }
910
+
911
+ // src/_shared/tracking.ts
912
+ function sanitizeUaToken(value) {
913
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
914
+ }
915
+ function buildTrackingHeaders(input) {
916
+ const name = sanitizeUaToken(input.clientName || "unknown");
917
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
918
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
919
+ return {
920
+ "User-Agent": userAgent,
921
+ "X-Runpod-Session-Id": input.sessionId
922
+ };
923
+ }
924
+
925
+ // src/tools/logs.ts
469
926
  var import_zod = require("zod");
927
+ function parseLogSse(raw) {
928
+ const items = [];
929
+ for (const block of raw.split(/\r?\n\r?\n/)) {
930
+ const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
931
+ if (!dataLines.length) continue;
932
+ const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
933
+ if (!payload.trim()) continue;
934
+ try {
935
+ const parsed = JSON.parse(payload);
936
+ items.push(
937
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
938
+ );
939
+ } catch {
940
+ items.push({ raw: payload });
941
+ }
942
+ }
943
+ return items;
944
+ }
945
+ var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
946
+ var LOG_STREAM_MAX_BYTES = 256 * 1024;
947
+ async function readSseSnapshot(fetchImpl, url, headers, opts) {
948
+ const controller = new AbortController();
949
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
950
+ const chunks = [];
951
+ let bytes = 0;
952
+ let truncated = false;
953
+ try {
954
+ const res = await fetchImpl(url, {
955
+ method: "GET",
956
+ headers,
957
+ signal: controller.signal
958
+ });
959
+ if (!res.ok) {
960
+ throw new HttpError(
961
+ "Runpod API Error",
962
+ res.status,
963
+ await res.text().catch(() => "")
964
+ );
965
+ }
966
+ for await (const chunk of res.body ?? []) {
967
+ chunks.push(chunk);
968
+ bytes += chunk.length;
969
+ if (bytes >= opts.maxBytes) {
970
+ truncated = true;
971
+ controller.abort();
972
+ break;
973
+ }
974
+ }
975
+ } catch (err) {
976
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
977
+ } finally {
978
+ clearTimeout(timer);
979
+ }
980
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
981
+ }
982
+ var logStreamParams = {
983
+ source: import_zod.z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
984
+ tail: import_zod.z.number().int().min(0).max(5e3).optional().describe(
985
+ "Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
986
+ ),
987
+ since: import_zod.z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
988
+ maxWaitMs: import_zod.z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
989
+ };
990
+ async function collectLogSnapshot(streamSse, logsUrl, params) {
991
+ const qs = new URLSearchParams();
992
+ if (params.source && params.source !== "both")
993
+ qs.append("source", params.source);
994
+ if (params.tail !== void 0) qs.append("tail", String(params.tail));
995
+ if (params.since) qs.append("since", params.since);
996
+ const query = qs.toString() ? `?${qs}` : "";
997
+ const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
998
+ maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
999
+ maxBytes: LOG_STREAM_MAX_BYTES
1000
+ });
1001
+ const items = parseLogSse(raw);
1002
+ if (truncated) items.pop();
1003
+ return { items, count: items.length, truncated };
1004
+ }
1005
+ async function streamLogsReply(rt, tool, params) {
1006
+ const backend = rt.backendFor(tool.resource);
1007
+ if (backend.version === "v1") {
1008
+ return rt.jsonReply({
1009
+ error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
1010
+ status: 501
1011
+ });
1012
+ }
1013
+ try {
1014
+ return rt.jsonReply(
1015
+ await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
1016
+ );
1017
+ } catch (error) {
1018
+ if (error instanceof HttpError) {
1019
+ return rt.jsonReply({ error: error.message, status: error.status });
1020
+ }
1021
+ throw error;
1022
+ }
1023
+ }
1024
+
1025
+ // src/tools/runtime.ts
1026
+ var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
1027
+ var SESSION_ID = (0, import_node_crypto.randomUUID)();
1028
+ function trackingHeaders(server, ctx) {
1029
+ const info = server.server.getClientVersion();
1030
+ return buildTrackingHeaders({
1031
+ clientName: info?.name || ctx.clientUserAgent || "unknown",
1032
+ clientVersion: info?.version || "unknown",
1033
+ transport: ctx.transport,
1034
+ // Prefer a server version supplied by the caller (the hosted entrypoint reads
1035
+ // it from package.json at runtime, since tsup's build-time define does not
1036
+ // run when Vercel compiles api/index.ts). Fall back to the build-time value.
1037
+ serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
1038
+ sessionId: SESSION_ID
1039
+ });
1040
+ }
1041
+ var defaultFetch = import_node_fetch.default;
1042
+ async function withApiErrorLog(label, call) {
1043
+ try {
1044
+ return await call();
1045
+ } catch (error) {
1046
+ console.error(label, error);
1047
+ throw error;
1048
+ }
1049
+ }
1050
+ function jsonReply(result) {
1051
+ return {
1052
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1053
+ };
1054
+ }
1055
+ var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
1056
+ var WRITE = { readOnlyHint: false, openWorldHint: true };
1057
+ var DESTRUCTIVE = {
1058
+ readOnlyHint: false,
1059
+ destructiveHint: true,
1060
+ idempotentHint: true,
1061
+ openWorldHint: true
1062
+ };
1063
+ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
1064
+ const response = await fetchImpl(url, {
1065
+ method: "POST",
1066
+ headers: {
1067
+ "Content-Type": "application/json",
1068
+ ...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
1069
+ ...tracking()
1070
+ },
1071
+ body: JSON.stringify({
1072
+ query,
1073
+ ...options?.variables ? { variables: options.variables } : {}
1074
+ })
1075
+ });
1076
+ const result = await response.json();
1077
+ if (result.errors && result.errors.length > 0) {
1078
+ throw new Error(
1079
+ `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
1080
+ );
1081
+ }
1082
+ if (!result.data) {
1083
+ throw new Error("No data returned from GraphQL query");
1084
+ }
1085
+ return result.data;
1086
+ }
1087
+ function createToolRuntime(server, ctx, deps = {}) {
1088
+ const tracking = () => trackingHeaders(server, ctx);
1089
+ function observeUnauthorized(fn) {
1090
+ const notify = ctx.onUnauthorized;
1091
+ if (!notify) return fn;
1092
+ return async (...args) => {
1093
+ const response = await fn(...args);
1094
+ if (response.status === 401) notify();
1095
+ return response;
1096
+ };
1097
+ }
1098
+ const rawFetch = deps.fetch ?? defaultFetch;
1099
+ const httpFetch = observeUnauthorized(rawFetch);
1100
+ const v1Client = createHttpClient({
1101
+ apiKey: ctx.apiKey,
1102
+ fetch: httpFetch,
1103
+ tracking,
1104
+ errorPrefix: "Runpod API Error"
1105
+ });
1106
+ const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
1107
+ "Error calling Runpod API:",
1108
+ () => v1Client(`${restV1Base(process.env)}${endpoint}`, method, body)
1109
+ );
1110
+ const serverlessClient = createHttpClient({
1111
+ apiKey: ctx.apiKey,
1112
+ fetch: httpFetch,
1113
+ tracking,
1114
+ errorPrefix: "Runpod Serverless API Error"
1115
+ });
1116
+ const serverlessRequest = (endpointId, path2, method = "GET", body) => withApiErrorLog(
1117
+ "Error calling Runpod Serverless API:",
1118
+ () => serverlessClient(
1119
+ `${serverlessBase(process.env)}/${endpointId}${path2}`,
1120
+ method,
1121
+ body
1122
+ )
1123
+ );
1124
+ const restClient = createHttpClient({
1125
+ apiKey: ctx.apiKey,
1126
+ fetch: httpFetch,
1127
+ tracking,
1128
+ errorPrefix: "Runpod API Error"
1129
+ });
1130
+ const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
1131
+ "Error calling Runpod API:",
1132
+ () => restClient(url, method, body)
1133
+ );
1134
+ const sseFetch = observeUnauthorized(
1135
+ deps.sseFetch ?? import_node_fetch.default
1136
+ );
1137
+ const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
1138
+ sseFetch,
1139
+ url,
1140
+ {
1141
+ Authorization: `Bearer ${ctx.apiKey}`,
1142
+ Accept: "text/event-stream",
1143
+ ...tracking()
1144
+ },
1145
+ opts
1146
+ ));
1147
+ const backendFor = (resource) => resolveBackend({
1148
+ resource,
1149
+ env: process.env,
1150
+ ctx: { transport: ctx.transport },
1151
+ v2Available: deps.v2Available
1152
+ });
1153
+ const podAction = async (podId, action) => {
1154
+ const backend = backendFor("pods");
1155
+ const podPath = backend.get(podId);
1156
+ if (backend.version === "v2") {
1157
+ return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
1158
+ action
1159
+ });
1160
+ }
1161
+ return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
1162
+ };
1163
+ return {
1164
+ jsonReply,
1165
+ graphql: (query) => graphqlRequest(
1166
+ query,
1167
+ tracking,
1168
+ // rawFetch, not httpFetch: this call carries no credential (see above).
1169
+ rawFetch,
1170
+ publicGraphqlBase(process.env)
1171
+ ),
1172
+ graphqlAuthed: (query, variables) => graphqlRequest(
1173
+ query,
1174
+ tracking,
1175
+ httpFetch,
1176
+ // NOT publicGraphqlBase: this call carries the caller's API key.
1177
+ authedGraphqlBase(process.env),
1178
+ { variables, apiKey: ctx.apiKey }
1179
+ ),
1180
+ runpodRequest,
1181
+ serverlessRequest,
1182
+ callRestUrl,
1183
+ backendFor,
1184
+ streamSse,
1185
+ podAction,
1186
+ env: process.env
1187
+ };
1188
+ }
1189
+
1190
+ // src/tools/catalog.ts
1191
+ var import_zod3 = require("zod");
1192
+
1193
+ // src/pagination.ts
1194
+ var import_zod2 = require("zod");
470
1195
  var DEFAULT_LIST_LIMIT = 20;
471
1196
  var MAX_LIST_LIMIT = 100;
472
1197
  var listPaginationParams = {
473
- limit: import_zod.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
1198
+ limit: import_zod2.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
474
1199
  `Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
475
1200
  ),
476
- cursor: import_zod.z.string().optional().describe(
1201
+ cursor: import_zod2.z.string().optional().describe(
477
1202
  "Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
478
1203
  )
479
1204
  };
@@ -490,7 +1215,7 @@ function decodeCursorOffset(cursor) {
490
1215
  function encodeCursorOffset(offset) {
491
1216
  return Buffer.from(String(offset), "utf8").toString("base64");
492
1217
  }
493
- function capListResult(result, options) {
1218
+ function capListResult(result, options, extra) {
494
1219
  const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
495
1220
  const offset = decodeCursorOffset(options.cursor);
496
1221
  if (!Array.isArray(result)) {
@@ -504,7 +1229,11 @@ function capListResult(result, options) {
504
1229
  const page = result.slice(offset, offset + limit);
505
1230
  const nextOffset = offset + page.length;
506
1231
  const hasMore = nextOffset < total;
1232
+ const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
1233
+ void _i;
1234
+ void _p;
507
1235
  const envelope = {
1236
+ ...safeExtra,
508
1237
  items: page,
509
1238
  pagination: {
510
1239
  total,
@@ -522,133 +1251,67 @@ function capListResult(result, options) {
522
1251
  };
523
1252
  }
524
1253
 
525
- // src/tools.ts
526
- var API_BASE_URL = process.env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
527
- var SERVERLESS_API_BASE_URL = process.env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
528
- var GRAPHQL_URL = process.env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
529
- var MCP_SERVER_VERSION = true ? "1.4.0" : "dev";
530
- var SESSION_ID = (0, import_node_crypto.randomUUID)();
531
- function sanitizeUaToken(value) {
532
- return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
533
- }
534
- function trackingHeaders(server, ctx) {
535
- const info = server.server.getClientVersion();
536
- const fallbackName = ctx.clientUserAgent || "unknown";
537
- const name = sanitizeUaToken(info?.name || fallbackName);
538
- const version = sanitizeUaToken(info?.version || "unknown");
539
- const serverVersion = ctx.serverVersion || MCP_SERVER_VERSION;
540
- const userAgent = `runpod-mcp-server/${serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${ctx.transport})`;
541
- return {
542
- "User-Agent": userAgent,
543
- "X-Runpod-Session-Id": SESSION_ID
544
- };
545
- }
546
- async function graphqlRequest(query, tracking) {
547
- const response = await (0, import_node_fetch.default)(GRAPHQL_URL, {
548
- method: "POST",
549
- headers: {
550
- "Content-Type": "application/json",
551
- ...tracking()
552
- },
553
- body: JSON.stringify({ query })
554
- });
555
- const result = await response.json();
556
- if (result.errors && result.errors.length > 0) {
557
- throw new Error(
558
- `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
559
- );
560
- }
561
- if (!result.data) {
562
- throw new Error("No data returned from GraphQL query");
563
- }
564
- return result.data;
565
- }
566
- function createRunpodRequest(apiKey, tracking) {
567
- return async function runpodRequest(endpoint, method = "GET", body) {
568
- const url = `${API_BASE_URL}${endpoint}`;
569
- const headers = {
570
- Authorization: `Bearer ${apiKey}`,
571
- "Content-Type": "application/json",
572
- ...tracking()
573
- };
574
- const options = {
575
- method,
576
- headers
577
- };
578
- if (body && (method === "POST" || method === "PATCH")) {
579
- options.body = JSON.stringify(body);
580
- }
581
- try {
582
- const response = await (0, import_node_fetch.default)(url, options);
583
- if (!response.ok) {
584
- const errorText = await response.text();
585
- throw new Error(`Runpod API Error: ${response.status} - ${errorText}`);
586
- }
587
- const contentType = response.headers.get("content-type");
588
- if (contentType && contentType.includes("application/json")) {
589
- return await response.json();
590
- }
591
- return { success: true, status: response.status };
592
- } catch (error) {
593
- console.error("Error calling Runpod API:", error);
594
- throw error;
595
- }
596
- };
597
- }
598
- function createServerlessRequest(apiKey, tracking) {
599
- return async function serverlessRequest(endpointId, path2, method = "GET", body) {
600
- const url = `${SERVERLESS_API_BASE_URL}/${endpointId}${path2}`;
601
- const headers = {
602
- Authorization: `Bearer ${apiKey}`,
603
- "Content-Type": "application/json",
604
- ...tracking()
605
- };
606
- const options = {
607
- method,
608
- headers
609
- };
610
- if (body && (method === "POST" || method === "PATCH")) {
611
- options.body = JSON.stringify(body);
612
- }
613
- try {
614
- const response = await (0, import_node_fetch.default)(url, options);
615
- if (!response.ok) {
616
- const errorText = await response.text();
617
- throw new Error(
618
- `Runpod Serverless API Error: ${response.status} - ${errorText}`
619
- );
620
- }
621
- const contentType = response.headers.get("content-type");
622
- if (contentType && contentType.includes("application/json")) {
623
- return await response.json();
624
- }
625
- return { success: true, status: response.status };
626
- } catch (error) {
627
- console.error("Error calling Runpod Serverless API:", error);
628
- throw error;
629
- }
630
- };
631
- }
632
- function registerTools(server, ctx) {
633
- const tracking = () => trackingHeaders(server, ctx);
634
- const runpodRequest = createRunpodRequest(ctx.apiKey, tracking);
635
- const serverlessRequest = createServerlessRequest(ctx.apiKey, tracking);
1254
+ // src/tools/catalog.ts
1255
+ function registerCatalogTools(server, rt) {
1256
+ const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
636
1257
  server.tool(
637
1258
  "list-gpu-types",
1259
+ "List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. By default the full catalog is returned: each result includes an `availability` summary (HIGH/MEDIUM/LOW/NONE) and results are sorted with the most-available GPUs first, but nothing is hidden. Set includeUnavailable:false to drop out-of-stock GPUs and list only deployable ones (set includeAvailability:false to skip the stock lookup entirely). For per-datacenter availability (to pick a dataCenterIds), use get-gpu-type.",
638
1260
  {
639
- minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
640
- secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
641
- communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
642
- searchTerm: import_zod2.z.string().optional().describe(
1261
+ ...listPaginationParams,
1262
+ minMemoryGb: import_zod3.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
1263
+ secureCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
1264
+ communityCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
1265
+ searchTerm: import_zod3.z.string().optional().describe(
643
1266
  "Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
644
1267
  ),
645
- includeUnavailable: import_zod2.z.boolean().optional().describe(
646
- "Include GPUs that are currently out of stock. Default is false"
1268
+ includeUnavailable: import_zod3.z.boolean().optional().describe(
1269
+ "Out-of-stock GPUs are included by default (annotated availability:NONE and sorted last). Set false to hide them and list only currently-deployable GPUs."
1270
+ ),
1271
+ includeAvailability: import_zod3.z.boolean().optional().describe(
1272
+ "Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
647
1273
  )
648
1274
  },
1275
+ { title: "List GPU types", ...READ_ONLY },
649
1276
  async (params) => {
650
- const data = await graphqlRequest(
651
- `
1277
+ const backend = backendFor("gpus");
1278
+ if (backend.version === "v2") {
1279
+ const wantAvailability = params.includeAvailability !== false;
1280
+ const raw = await callRestUrl(
1281
+ `${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
1282
+ );
1283
+ let gpus = backend.unwrap(raw);
1284
+ gpus = gpus.filter((g) => g.id !== "unknown");
1285
+ if (params.minMemoryGb !== void 0)
1286
+ gpus = gpus.filter(
1287
+ (g) => Number(g.memory ?? 0) >= params.minMemoryGb
1288
+ );
1289
+ if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
1290
+ if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
1291
+ if (params.searchTerm) {
1292
+ const t = params.searchTerm.toLowerCase();
1293
+ gpus = gpus.filter(
1294
+ (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
1295
+ );
1296
+ }
1297
+ if (params.includeUnavailable === false)
1298
+ gpus = gpus.filter((g) => g.availability !== "NONE");
1299
+ const rank = {
1300
+ HIGH: 3,
1301
+ MEDIUM: 2,
1302
+ LOW: 1,
1303
+ NONE: 0
1304
+ };
1305
+ gpus.sort(
1306
+ (a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
1307
+ );
1308
+ gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
1309
+ return capListResult(gpus, {
1310
+ limit: params.limit,
1311
+ cursor: params.cursor
1312
+ });
1313
+ }
1314
+ const data = await graphql(`
652
1315
  query {
653
1316
  gpuTypes {
654
1317
  id
@@ -661,9 +1324,7 @@ function registerTools(server, ctx) {
661
1324
  }
662
1325
  }
663
1326
  }
664
- `,
665
- tracking
666
- );
1327
+ `);
667
1328
  const stockPriority = {
668
1329
  High: 3,
669
1330
  Medium: 2,
@@ -674,7 +1335,7 @@ function registerTools(server, ctx) {
674
1335
  return !!status && status !== "Out";
675
1336
  };
676
1337
  let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
677
- if (!params.includeUnavailable) {
1338
+ if (params.includeUnavailable === false) {
678
1339
  gpuTypes = gpuTypes.filter(isAvailable);
679
1340
  }
680
1341
  if (params.minMemoryGb) {
@@ -709,26 +1370,39 @@ function registerTools(server, ctx) {
709
1370
  stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
710
1371
  available: isAvailable(gpu)
711
1372
  }));
712
- return {
713
- content: [
714
- {
715
- type: "text",
716
- text: JSON.stringify(result, null, 2)
717
- }
718
- ]
719
- };
1373
+ return capListResult(result, {
1374
+ limit: params.limit,
1375
+ cursor: params.cursor
1376
+ });
720
1377
  }
721
1378
  );
722
1379
  server.tool(
723
1380
  "list-data-centers",
1381
+ "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
724
1382
  {
725
- region: import_zod2.z.string().optional().describe(
1383
+ ...listPaginationParams,
1384
+ region: import_zod3.z.string().optional().describe(
726
1385
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
727
1386
  )
728
1387
  },
1388
+ { title: "List data centers", ...READ_ONLY },
729
1389
  async (params) => {
730
- const data = await graphqlRequest(
731
- `
1390
+ const backend = backendFor("dataCenters");
1391
+ if (backend.version === "v2") {
1392
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
1393
+ let dcs = backend.unwrap(raw);
1394
+ if (params.region) {
1395
+ const t = params.region.toLowerCase();
1396
+ dcs = dcs.filter(
1397
+ (dc) => String(dc.region ?? "").toLowerCase().includes(t)
1398
+ );
1399
+ }
1400
+ return capListResult(dcs, {
1401
+ limit: params.limit,
1402
+ cursor: params.cursor
1403
+ });
1404
+ }
1405
+ const data = await graphql(`
732
1406
  query {
733
1407
  dataCenters {
734
1408
  id
@@ -736,9 +1410,7 @@ function registerTools(server, ctx) {
736
1410
  location
737
1411
  }
738
1412
  }
739
- `,
740
- tracking
741
- );
1413
+ `);
742
1414
  let dataCenters = data.dataCenters;
743
1415
  if (params.region) {
744
1416
  const term = params.region.toLowerCase();
@@ -746,210 +1418,1029 @@ function registerTools(server, ctx) {
746
1418
  (dc) => dc.location.toLowerCase().includes(term)
747
1419
  );
748
1420
  }
749
- return {
750
- content: [
751
- {
752
- type: "text",
753
- text: JSON.stringify(dataCenters, null, 2)
1421
+ return capListResult(dataCenters, {
1422
+ limit: params.limit,
1423
+ cursor: params.cursor
1424
+ });
1425
+ }
1426
+ );
1427
+ server.tool(
1428
+ "list-cpu-types",
1429
+ "List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
1430
+ { ...listPaginationParams },
1431
+ { title: "List CPU types", ...READ_ONLY },
1432
+ async (params) => {
1433
+ const backend = backendFor("cpus");
1434
+ if (backend.version === "v1") {
1435
+ return jsonReply2({
1436
+ error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1437
+ status: 501
1438
+ });
1439
+ }
1440
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
1441
+ return capListResult(backend.unwrap(raw), {
1442
+ limit: params.limit,
1443
+ cursor: params.cursor
1444
+ });
1445
+ }
1446
+ );
1447
+ server.tool(
1448
+ "get-gpu-type",
1449
+ "Get details for a single GPU type by id, including per-datacenter availability. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there). Use the returned dataCenters[].availability to pick a dataCenterIds with stock before creating a pod.",
1450
+ {
1451
+ gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
1452
+ includeAvailability: import_zod3.z.boolean().optional().describe(
1453
+ "Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
1454
+ )
1455
+ },
1456
+ { title: "Get GPU type", ...READ_ONLY },
1457
+ async (params) => {
1458
+ const backend = backendFor("gpus");
1459
+ if (backend.version === "v1") {
1460
+ return jsonReply2({
1461
+ error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
1462
+ status: 501
1463
+ });
1464
+ }
1465
+ const path2 = backend.get(encodeURIComponent(params.gpuTypeId));
1466
+ const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
1467
+ const result = await callRestUrl(`${backend.base}${path2}${query}`);
1468
+ return jsonReply2(result);
1469
+ }
1470
+ );
1471
+ server.tool(
1472
+ "get-cpu-type",
1473
+ "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).",
1474
+ {
1475
+ cpuTypeId: import_zod3.z.string().describe("ID of the CPU type to retrieve")
1476
+ },
1477
+ { title: "Get CPU type", ...READ_ONLY },
1478
+ async (params) => {
1479
+ const backend = backendFor("cpus");
1480
+ if (backend.version === "v1") {
1481
+ return jsonReply2({
1482
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1483
+ status: 501
1484
+ });
1485
+ }
1486
+ const result = await callRestUrl(
1487
+ `${backend.base}${backend.get(params.cpuTypeId)}`
1488
+ );
1489
+ return jsonReply2(result);
1490
+ }
1491
+ );
1492
+ server.tool(
1493
+ "get-data-center",
1494
+ "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).",
1495
+ {
1496
+ dataCenterId: import_zod3.z.string().describe("ID of the data center to retrieve")
1497
+ },
1498
+ { title: "Get data center", ...READ_ONLY },
1499
+ async (params) => {
1500
+ const backend = backendFor("dataCenters");
1501
+ if (backend.version === "v1") {
1502
+ return jsonReply2({
1503
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1504
+ status: 501
1505
+ });
1506
+ }
1507
+ const result = await callRestUrl(
1508
+ `${backend.base}${backend.get(params.dataCenterId)}`
1509
+ );
1510
+ return jsonReply2(result);
1511
+ }
1512
+ );
1513
+ }
1514
+
1515
+ // src/tools/hub.ts
1516
+ var import_zod4 = require("zod");
1517
+ function parseReleaseConfig(config) {
1518
+ if (!config) return void 0;
1519
+ try {
1520
+ return JSON.parse(config);
1521
+ } catch {
1522
+ return config;
1523
+ }
1524
+ }
1525
+ function listingsQuery(withConfig) {
1526
+ return `
1527
+ query {
1528
+ listings(input: {}) {
1529
+ id
1530
+ repoId
1531
+ title
1532
+ description
1533
+ repoName
1534
+ repoOwner
1535
+ createdAt
1536
+ updatedAt
1537
+ views
1538
+ stars
1539
+ deploys
1540
+ language
1541
+ category
1542
+ tags
1543
+ type
1544
+ listedRelease {
1545
+ id
1546
+ name
1547
+ tagName
1548
+ createdAt${withConfig ? "\n config" : ""}
1549
+ build {
1550
+ id
1551
+ imageName
754
1552
  }
755
- ]
1553
+ }
1554
+ }
1555
+ }
1556
+ `;
1557
+ }
1558
+ function serializeBoolean(value, input) {
1559
+ if (value === "true") return input.trueValue ?? "true";
1560
+ if (value === "false") return input.falseValue ?? "false";
1561
+ return value;
1562
+ }
1563
+ function buildHubEnv(config, overrides) {
1564
+ const env = [];
1565
+ const missingRequired = [];
1566
+ const remaining = { ...overrides };
1567
+ for (const entry of config.env ?? []) {
1568
+ const input = entry.input ?? {};
1569
+ const override = entry.key in remaining ? remaining[entry.key] : void 0;
1570
+ delete remaining[entry.key];
1571
+ if (override !== void 0 && override !== "") {
1572
+ env.push({
1573
+ key: entry.key,
1574
+ value: input.type === "boolean" ? serializeBoolean(override, input) : override
1575
+ });
1576
+ continue;
1577
+ }
1578
+ if (input.default !== void 0 && input.default !== null) {
1579
+ env.push({
1580
+ key: entry.key,
1581
+ value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
1582
+ });
1583
+ continue;
1584
+ }
1585
+ if (input.required) {
1586
+ missingRequired.push(entry.key);
1587
+ env.push({ key: entry.key, value: "" });
1588
+ continue;
1589
+ }
1590
+ }
1591
+ for (const [key, value] of Object.entries(remaining)) {
1592
+ env.push({ key, value });
1593
+ }
1594
+ return { env, missingRequired };
1595
+ }
1596
+ function minCudaVersion(versions) {
1597
+ if (!versions || versions.length === 0) return null;
1598
+ const compare = (a, b) => {
1599
+ const pa = a.split(".").map(Number);
1600
+ const pb = b.split(".").map(Number);
1601
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1602
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1603
+ if (d !== 0) return d;
1604
+ }
1605
+ return 0;
1606
+ };
1607
+ return [...versions].sort(compare)[0];
1608
+ }
1609
+ function randomSuffix() {
1610
+ return Math.random().toString(36).slice(2, 8);
1611
+ }
1612
+ function registerHubTools(server, rt) {
1613
+ const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
1614
+ server.tool(
1615
+ "list-hub-repos",
1616
+ "List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
1617
+ {
1618
+ ...listPaginationParams,
1619
+ searchTerm: import_zod4.z.string().optional().describe(
1620
+ "Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
1621
+ ),
1622
+ category: import_zod4.z.string().optional().describe(
1623
+ "Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
1624
+ ),
1625
+ type: import_zod4.z.enum(["SERVERLESS", "POD"]).optional().describe(
1626
+ "Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
1627
+ ),
1628
+ repoOwner: import_zod4.z.string().optional().describe(
1629
+ "Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
1630
+ ),
1631
+ includeConfig: import_zod4.z.boolean().optional().describe(
1632
+ "Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
1633
+ )
1634
+ },
1635
+ { title: "List Hub repos", ...READ_ONLY },
1636
+ async (params) => {
1637
+ const data = await graphql(
1638
+ listingsQuery(params.includeConfig === true)
1639
+ );
1640
+ let listings = data.listings;
1641
+ if (params.type) {
1642
+ listings = listings.filter(
1643
+ (l) => (l.type ?? "").toUpperCase() === params.type
1644
+ );
1645
+ }
1646
+ if (params.category) {
1647
+ const term = params.category.toLowerCase();
1648
+ listings = listings.filter(
1649
+ (l) => (l.category ?? "").toLowerCase() === term
1650
+ );
1651
+ }
1652
+ if (params.repoOwner) {
1653
+ const term = params.repoOwner.toLowerCase();
1654
+ listings = listings.filter(
1655
+ (l) => (l.repoOwner ?? "").toLowerCase() === term
1656
+ );
1657
+ }
1658
+ if (params.searchTerm) {
1659
+ const term = params.searchTerm.toLowerCase();
1660
+ listings = listings.filter(
1661
+ (l) => [
1662
+ l.title,
1663
+ l.description,
1664
+ l.repoName,
1665
+ l.repoOwner,
1666
+ ...l.tags ?? []
1667
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
1668
+ );
1669
+ }
1670
+ listings = [...listings].sort(
1671
+ (a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
1672
+ );
1673
+ const result = listings.map((l) => ({
1674
+ id: l.id,
1675
+ repo: `${l.repoOwner}/${l.repoName}`,
1676
+ title: l.title,
1677
+ description: l.description,
1678
+ type: l.type,
1679
+ category: l.category,
1680
+ tags: l.tags,
1681
+ language: l.language,
1682
+ stars: l.stars,
1683
+ deploys: l.deploys,
1684
+ views: l.views,
1685
+ githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
1686
+ hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
1687
+ createdAt: l.createdAt,
1688
+ updatedAt: l.updatedAt,
1689
+ listedRelease: l.listedRelease ? {
1690
+ hubReleaseId: l.listedRelease.id,
1691
+ name: l.listedRelease.name,
1692
+ tagName: l.listedRelease.tagName,
1693
+ createdAt: l.listedRelease.createdAt,
1694
+ imageName: l.listedRelease.build?.imageName,
1695
+ ...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
1696
+ } : null
1697
+ }));
1698
+ return capListResult(result, {
1699
+ limit: params.limit,
1700
+ cursor: params.cursor
1701
+ });
1702
+ }
1703
+ );
1704
+ server.tool(
1705
+ "deploy-hub-repo",
1706
+ "Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet), so it works the same on v1 and v2.",
1707
+ {
1708
+ repo: import_zod4.z.string().optional().describe(
1709
+ `The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
1710
+ ),
1711
+ hubReleaseId: import_zod4.z.string().optional().describe(
1712
+ "The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
1713
+ ),
1714
+ name: import_zod4.z.string().optional().describe(
1715
+ "Name for the new endpoint. Defaults to '<listing title> <release tag>'."
1716
+ ),
1717
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe(
1718
+ "Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
1719
+ ),
1720
+ gpuIds: import_zod4.z.string().optional().describe(
1721
+ "Comma-separated GPU pool names for workers (e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). To pin specific GPU SKUs within a pool, append exclusions prefixed with '-' using GPU type ids from list-gpu-types (e.g. 'AMPERE_16,-NVIDIA RTX 2000 Ada Generation,-NVIDIA RTX A4500' allows only the pool's remaining SKUs). Defaults to the release config's gpuIds; required when the config does not specify one."
1722
+ ),
1723
+ gpuCount: import_zod4.z.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
1724
+ containerDiskInGb: import_zod4.z.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
1725
+ workersMin: import_zod4.z.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
1726
+ workersMax: import_zod4.z.number().int().positive().optional().describe("Maximum workers (default: account default)."),
1727
+ idleTimeout: import_zod4.z.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
1728
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
1729
+ scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value (default 4)."),
1730
+ executionTimeoutMs: import_zod4.z.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
1731
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
1732
+ },
1733
+ { title: "Deploy Hub repo", ...WRITE },
1734
+ async (params) => {
1735
+ if (!params.repo && !params.hubReleaseId) {
1736
+ return jsonReply2({
1737
+ error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
1738
+ });
1739
+ }
1740
+ const catalog = await graphql(listingsQuery(true));
1741
+ let listing;
1742
+ if (params.hubReleaseId) {
1743
+ listing = catalog.listings.find(
1744
+ (l) => l.listedRelease?.id === params.hubReleaseId
1745
+ );
1746
+ } else {
1747
+ const repoKey = params.repo.toLowerCase();
1748
+ listing = catalog.listings.find(
1749
+ (l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
1750
+ );
1751
+ }
1752
+ if (!listing) {
1753
+ return jsonReply2({
1754
+ error: `No Hub listing found for ${params.hubReleaseId ? `hubReleaseId "${params.hubReleaseId}" (only each repo's currently listed release is deployable)` : `repo "${params.repo}"`}. Use list-hub-repos to see the catalog.`
1755
+ });
1756
+ }
1757
+ const release = listing.listedRelease;
1758
+ if (!release?.build?.imageName) {
1759
+ return jsonReply2({
1760
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
1761
+ });
1762
+ }
1763
+ if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
1764
+ return jsonReply2({
1765
+ error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
1766
+ });
1767
+ }
1768
+ const config = parseReleaseConfig(release.config) ?? {};
1769
+ const gpuIds = params.gpuIds ?? config.gpuIds;
1770
+ if (!gpuIds) {
1771
+ return jsonReply2({
1772
+ error: 'This release does not specify a GPU pool \u2014 pass gpuIds (comma-separated pool names, e.g. "ADA_80_PRO,AMPERE_80"; see the pool field on list-gpu-types).'
1773
+ });
1774
+ }
1775
+ const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
1776
+ if (missingRequired.length > 0) {
1777
+ return jsonReply2({
1778
+ error: `Missing required environment variables for this release: ${missingRequired.join(
1779
+ ", "
1780
+ )}. Pass them via the env parameter.`
1781
+ });
1782
+ }
1783
+ const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
1784
+ const minCuda = minCudaVersion(config.allowedCudaVersions);
1785
+ const input = {
1786
+ name: endpointName,
1787
+ hubReleaseId: release.id,
1788
+ type: "QB",
1789
+ gpuIds,
1790
+ gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
1791
+ workersMin: params.workersMin ?? 0,
1792
+ workersMax: params.workersMax ?? null,
1793
+ idleTimeout: params.idleTimeout ?? 5,
1794
+ scalerType: params.scalerType ?? "QUEUE_DELAY",
1795
+ scalerValue: params.scalerValue ?? 4,
1796
+ executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
1797
+ flashBootType: params.flashboot ?? "FLASHBOOT",
1798
+ locations: null,
1799
+ networkVolumeIds: null,
1800
+ compliance: [],
1801
+ modelReferences: [],
1802
+ ...minCuda ? {
1803
+ minCudaVersion: minCuda,
1804
+ allowedCudaVersions: config.allowedCudaVersions.join(",")
1805
+ } : {},
1806
+ template: {
1807
+ name: `${endpointName}__template__${randomSuffix()}`,
1808
+ imageName: release.build.imageName,
1809
+ containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
1810
+ containerRegistryAuthId: "",
1811
+ dockerArgs: "",
1812
+ startScript: "",
1813
+ ports: "",
1814
+ env
1815
+ }
756
1816
  };
1817
+ const data = await graphqlAuthed(
1818
+ `
1819
+ mutation saveEndpoint($input: EndpointInput!) {
1820
+ saveEndpoint(input: $input) {
1821
+ id
1822
+ name
1823
+ gpuIds
1824
+ gpuCount
1825
+ workersMin
1826
+ workersMax
1827
+ idleTimeout
1828
+ scalerType
1829
+ scalerValue
1830
+ flashBootType
1831
+ templateId
1832
+ }
1833
+ }
1834
+ `,
1835
+ { input }
1836
+ );
1837
+ return jsonReply2({
1838
+ endpoint: data.saveEndpoint,
1839
+ deployed: {
1840
+ repo: `${listing.repoOwner}/${listing.repoName}`,
1841
+ release: release.tagName,
1842
+ hubReleaseId: release.id,
1843
+ imageName: release.build.imageName
1844
+ },
1845
+ note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
1846
+ });
757
1847
  }
758
1848
  );
1849
+ }
1850
+
1851
+ // src/tools/public-endpoints.ts
1852
+ var import_zod5 = require("zod");
1853
+ function parseMetadata(metadata) {
1854
+ if (!metadata) return {};
1855
+ try {
1856
+ const parsed = JSON.parse(metadata);
1857
+ return parsed && typeof parsed === "object" ? parsed : {};
1858
+ } catch {
1859
+ return {};
1860
+ }
1861
+ }
1862
+ function registerPublicEndpointTools(server, rt) {
1863
+ const { graphql } = rt;
759
1864
  server.tool(
760
- "list-pods",
1865
+ "list-public-endpoints",
1866
+ "List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
761
1867
  {
762
1868
  ...listPaginationParams,
763
- computeType: import_zod2.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
764
- gpuTypeId: import_zod2.z.array(import_zod2.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
765
- dataCenterId: import_zod2.z.array(import_zod2.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
766
- name: import_zod2.z.string().optional().describe("Filter to Pods with the provided name"),
767
- includeMachine: import_zod2.z.boolean().optional().describe("Include information about the machine"),
768
- includeNetworkVolume: import_zod2.z.boolean().optional().describe("Include information about attached network volumes")
1869
+ searchTerm: import_zod5.z.string().optional().describe(
1870
+ "Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
1871
+ ),
1872
+ modality: import_zod5.z.string().optional().describe(
1873
+ "Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
1874
+ ),
1875
+ owner: import_zod5.z.string().optional().describe(
1876
+ "Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
1877
+ ),
1878
+ includeOffline: import_zod5.z.boolean().optional().describe(
1879
+ "Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
1880
+ )
769
1881
  },
1882
+ { title: "List public endpoints", ...READ_ONLY },
770
1883
  async (params) => {
771
- const queryParams = new URLSearchParams();
772
- if (params.computeType)
773
- queryParams.append("computeType", params.computeType);
774
- if (params.gpuTypeId)
775
- params.gpuTypeId.forEach(
776
- (type) => queryParams.append("gpuTypeId", type)
1884
+ const data = await graphql(`
1885
+ query {
1886
+ allAiApiPublicConfigs {
1887
+ id
1888
+ aiApiId
1889
+ modelName
1890
+ displayName
1891
+ description
1892
+ metadata
1893
+ isLive
1894
+ createdAt
1895
+ updatedAt
1896
+ }
1897
+ }
1898
+ `);
1899
+ let configs = data.allAiApiPublicConfigs.map((c) => ({
1900
+ config: c,
1901
+ meta: parseMetadata(c.metadata)
1902
+ }));
1903
+ if (!params.includeOffline) {
1904
+ configs = configs.filter(({ config }) => config.isLive);
1905
+ }
1906
+ if (params.modality) {
1907
+ const term = params.modality.toLowerCase();
1908
+ configs = configs.filter(
1909
+ ({ meta }) => (meta.source ?? "").toLowerCase() === term
777
1910
  );
778
- if (params.dataCenterId)
779
- params.dataCenterId.forEach(
780
- (dc) => queryParams.append("dataCenterId", dc)
1911
+ }
1912
+ if (params.owner) {
1913
+ const term = params.owner.toLowerCase();
1914
+ configs = configs.filter(
1915
+ ({ meta }) => (meta.owner ?? "").toLowerCase() === term
781
1916
  );
782
- if (params.name) queryParams.append("name", params.name);
783
- if (params.includeMachine)
784
- queryParams.append("includeMachine", params.includeMachine.toString());
785
- if (params.includeNetworkVolume)
786
- queryParams.append(
787
- "includeNetworkVolume",
788
- params.includeNetworkVolume.toString()
1917
+ }
1918
+ if (params.searchTerm) {
1919
+ const term = params.searchTerm.toLowerCase();
1920
+ configs = configs.filter(
1921
+ ({ config, meta }) => [
1922
+ config.displayName,
1923
+ config.modelName,
1924
+ config.aiApiId,
1925
+ config.description,
1926
+ meta.owner,
1927
+ meta.tag,
1928
+ meta.source
1929
+ ].some((field) => (field ?? "").toLowerCase().includes(term))
789
1930
  );
790
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
791
- const result = await runpodRequest(`/pods${queryString}`);
1931
+ }
1932
+ configs = [...configs].sort(
1933
+ (a, b) => a.config.displayName.localeCompare(b.config.displayName)
1934
+ );
1935
+ const result = configs.map(({ config, meta }) => ({
1936
+ endpointId: config.aiApiId,
1937
+ displayName: config.displayName,
1938
+ modelName: config.modelName,
1939
+ description: config.description,
1940
+ modality: meta.source,
1941
+ tag: meta.tag,
1942
+ owner: meta.owner,
1943
+ pricing: meta.priceString,
1944
+ isLive: config.isLive,
1945
+ baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
1946
+ createdAt: config.createdAt,
1947
+ updatedAt: config.updatedAt
1948
+ }));
792
1949
  return capListResult(result, {
793
1950
  limit: params.limit,
794
1951
  cursor: params.cursor
795
1952
  });
796
1953
  }
797
1954
  );
1955
+ }
1956
+
1957
+ // src/tools/endpoint-gpus.ts
1958
+ var import_zod6 = require("zod");
1959
+ function registerEndpointGpuTools(server, rt) {
1960
+ const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
1961
+ server.tool(
1962
+ "set-endpoint-gpus",
1963
+ "Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs, which create-endpoint/update-endpoint cannot express. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly (e.g. pools:['AMPERE_16'], excludeGpuTypeIds:['NVIDIA RTX 2000 Ada Generation','NVIDIA RTX 4000 Ada Generation','NVIDIA RTX A4500'] pins RTX A4000). All other endpoint settings (workers, scaling, timeouts, template) are read first and preserved. Works on any Serverless endpoint via the authenticated GraphQL API.",
1964
+ {
1965
+ endpointId: import_zod6.z.string().describe("ID of the Serverless endpoint to update"),
1966
+ gpuIds: import_zod6.z.string().optional().describe(
1967
+ "Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
1968
+ ),
1969
+ pools: import_zod6.z.array(import_zod6.z.string()).optional().describe(
1970
+ "GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
1971
+ ),
1972
+ excludeGpuTypeIds: import_zod6.z.array(import_zod6.z.string()).optional().describe(
1973
+ "GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
1974
+ ),
1975
+ gpuCount: import_zod6.z.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
1976
+ minCudaVersion: import_zod6.z.string().optional().describe(
1977
+ "Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
1978
+ ),
1979
+ allowedCudaVersions: import_zod6.z.string().optional().describe(
1980
+ "Comma-separated allowed host CUDA versions (e.g. '12.8,12.7,12.6'). Omit to keep the current value. CUDA compatibility is part of GPU selection \u2014 a narrow list can leave an endpoint unable to schedule workers."
1981
+ )
1982
+ },
1983
+ { title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
1984
+ async (params) => {
1985
+ const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
1986
+ ...params.pools,
1987
+ ...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
1988
+ ].join(",") : void 0);
1989
+ if (!gpuIds) {
1990
+ return jsonReply2({
1991
+ error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
1992
+ });
1993
+ }
1994
+ const data = await graphqlAuthed(`
1995
+ query {
1996
+ myself {
1997
+ endpoints {
1998
+ id
1999
+ name
2000
+ gpuIds
2001
+ gpuCount
2002
+ workersMin
2003
+ workersMax
2004
+ idleTimeout
2005
+ scalerType
2006
+ scalerValue
2007
+ executionTimeoutMs
2008
+ flashBootType
2009
+ type
2010
+ locations
2011
+ templateId
2012
+ allowedCudaVersions
2013
+ minCudaVersion
2014
+ compliance
2015
+ modelReferences
2016
+ networkVolumeIds {
2017
+ networkVolumeId
2018
+ dataCenterId
2019
+ }
2020
+ }
2021
+ }
2022
+ }
2023
+ `);
2024
+ const current = data.myself.endpoints.find(
2025
+ (e) => e.id === params.endpointId
2026
+ );
2027
+ if (!current) {
2028
+ return jsonReply2({
2029
+ error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
2030
+ });
2031
+ }
2032
+ const input = {
2033
+ id: current.id,
2034
+ name: current.name,
2035
+ gpuIds,
2036
+ gpuCount: params.gpuCount ?? current.gpuCount,
2037
+ workersMin: current.workersMin,
2038
+ workersMax: current.workersMax,
2039
+ idleTimeout: current.idleTimeout,
2040
+ scalerType: current.scalerType,
2041
+ scalerValue: current.scalerValue,
2042
+ executionTimeoutMs: current.executionTimeoutMs,
2043
+ flashBootType: current.flashBootType,
2044
+ type: current.type,
2045
+ locations: current.locations,
2046
+ networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
2047
+ // Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
2048
+ // ONLY, and the read shape is rejected outright.
2049
+ current.networkVolumeIds.map((v) => ({
2050
+ networkVolumeId: v.networkVolumeId
2051
+ }))
2052
+ ) : null
2053
+ };
2054
+ for (const [key, value] of Object.entries({
2055
+ templateId: current.templateId,
2056
+ allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
2057
+ minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
2058
+ compliance: current.compliance,
2059
+ modelReferences: current.modelReferences
2060
+ })) {
2061
+ if (value !== null && value !== void 0) input[key] = value;
2062
+ }
2063
+ const result = await graphqlAuthed(
2064
+ `
2065
+ mutation saveEndpoint($input: EndpointInput!) {
2066
+ saveEndpoint(input: $input) {
2067
+ id
2068
+ name
2069
+ gpuIds
2070
+ gpuCount
2071
+ workersMin
2072
+ workersMax
2073
+ }
2074
+ }
2075
+ `,
2076
+ { input }
2077
+ );
2078
+ return jsonReply2({
2079
+ endpoint: result.saveEndpoint,
2080
+ previousGpuIds: current.gpuIds
2081
+ });
2082
+ }
2083
+ );
2084
+ }
2085
+
2086
+ // src/tools/pods.ts
2087
+ var import_zod7 = require("zod");
2088
+ function registerPodTools(server, rt) {
2089
+ const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
2090
+ server.tool(
2091
+ "list-pods",
2092
+ "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.",
2093
+ {
2094
+ ...listPaginationParams,
2095
+ computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
2096
+ gpuTypeId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
2097
+ dataCenterId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
2098
+ name: import_zod7.z.string().optional().describe("Filter to Pods with the provided name"),
2099
+ includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
2100
+ includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
2101
+ },
2102
+ { title: "List pods", ...READ_ONLY },
2103
+ async (params) => {
2104
+ const backend = backendFor("pods");
2105
+ let queryString = "";
2106
+ if (backend.version === "v1") {
2107
+ const queryParams = new URLSearchParams();
2108
+ if (params.computeType)
2109
+ queryParams.append("computeType", params.computeType);
2110
+ if (params.gpuTypeId)
2111
+ params.gpuTypeId.forEach(
2112
+ (type) => queryParams.append("gpuTypeId", type)
2113
+ );
2114
+ if (params.dataCenterId)
2115
+ params.dataCenterId.forEach(
2116
+ (dc) => queryParams.append("dataCenterId", dc)
2117
+ );
2118
+ if (params.name) queryParams.append("name", params.name);
2119
+ if (params.includeMachine)
2120
+ queryParams.append(
2121
+ "includeMachine",
2122
+ params.includeMachine.toString()
2123
+ );
2124
+ if (params.includeNetworkVolume)
2125
+ queryParams.append(
2126
+ "includeNetworkVolume",
2127
+ params.includeNetworkVolume.toString()
2128
+ );
2129
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
2130
+ }
2131
+ const result = await callRestUrl(
2132
+ `${backend.base}${backend.list}${queryString}`
2133
+ );
2134
+ return capListResult(backend.unwrap(result), {
2135
+ limit: params.limit,
2136
+ cursor: params.cursor
2137
+ });
2138
+ }
2139
+ );
798
2140
  server.tool(
799
2141
  "get-pod",
2142
+ "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
800
2143
  {
801
- podId: import_zod2.z.string().describe("ID of the pod to retrieve"),
802
- includeMachine: import_zod2.z.boolean().optional().describe("Include information about the machine"),
803
- includeNetworkVolume: import_zod2.z.boolean().optional().describe("Include information about attached network volumes")
2144
+ podId: import_zod7.z.string().describe("ID of the pod to retrieve"),
2145
+ includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
2146
+ includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
804
2147
  },
2148
+ { title: "Get pod", ...READ_ONLY },
805
2149
  async (params) => {
806
- const queryParams = new URLSearchParams();
807
- if (params.includeMachine)
808
- queryParams.append("includeMachine", params.includeMachine.toString());
809
- if (params.includeNetworkVolume)
810
- queryParams.append(
811
- "includeNetworkVolume",
812
- params.includeNetworkVolume.toString()
813
- );
814
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
815
- const result = await runpodRequest(`/pods/${params.podId}${queryString}`);
816
- return {
817
- content: [
818
- {
819
- type: "text",
820
- text: JSON.stringify(result, null, 2)
821
- }
822
- ]
823
- };
2150
+ const backend = backendFor("pods");
2151
+ let queryString = "";
2152
+ if (backend.version === "v1") {
2153
+ const queryParams = new URLSearchParams();
2154
+ if (params.includeMachine)
2155
+ queryParams.append(
2156
+ "includeMachine",
2157
+ params.includeMachine.toString()
2158
+ );
2159
+ if (params.includeNetworkVolume)
2160
+ queryParams.append(
2161
+ "includeNetworkVolume",
2162
+ params.includeNetworkVolume.toString()
2163
+ );
2164
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
2165
+ }
2166
+ const result = await callRestUrl(
2167
+ `${backend.base}${backend.get(params.podId)}${queryString}`
2168
+ );
2169
+ return jsonReply2(result);
824
2170
  }
825
2171
  );
826
2172
  server.tool(
827
2173
  "create-pod",
828
- '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.',
2174
+ 'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
829
2175
  {
830
- name: import_zod2.z.string().optional().describe("Name for the pod"),
831
- imageName: import_zod2.z.string().describe("Docker image to use"),
832
- cloudType: import_zod2.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
833
- gpuTypeIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of acceptable GPU types"),
834
- gpuCount: import_zod2.z.number().optional().describe("Number of GPUs"),
835
- containerDiskInGb: import_zod2.z.number().optional().describe("Container disk size in GB"),
836
- volumeInGb: import_zod2.z.number().optional().describe("Volume size in GB"),
837
- volumeMountPath: import_zod2.z.string().optional().describe("Path to mount the volume"),
838
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
839
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("Environment variables"),
840
- dataCenterIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of data centers")
2176
+ name: import_zod7.z.string().optional().describe("Name for the pod"),
2177
+ imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
2178
+ templateId: import_zod7.z.string().optional().describe(
2179
+ "Deploy from an existing template (see list-templates / get-template). The template supplies name, image, start command, ports, env, disk, volume, and registry credential as defaults; any field you also pass replaces the template value for that field (whole-field, not a merge). Override the registry credential with containerRegistryAuthId. Requires the v2 REST API. Still pass gpuTypeIds or computeType to choose the compute \u2014 a template does not pin the pod compute."
2180
+ ),
2181
+ computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe(
2182
+ "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
2183
+ ),
2184
+ cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
2185
+ gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
2186
+ gpuCount: import_zod7.z.number().optional().describe("Number of GPUs"),
2187
+ containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
2188
+ volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
2189
+ volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
2190
+ ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
2191
+ env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
2192
+ containerRegistryAuthId: import_zod7.z.string().optional().describe(
2193
+ "Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
2194
+ ),
2195
+ dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
841
2196
  },
2197
+ { title: "Create pod", ...WRITE },
842
2198
  async (params) => {
843
- const result = await runpodRequest("/pods", "POST", params);
844
- return {
845
- content: [
846
- {
847
- type: "text",
848
- text: JSON.stringify(result, null, 2)
2199
+ const backend = backendFor("pods");
2200
+ if (params.templateId && backend.version !== "v2") {
2201
+ return jsonReply2({
2202
+ error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
2203
+ status: 501
2204
+ });
2205
+ }
2206
+ if (!params.imageName && !params.templateId) {
2207
+ return jsonReply2({
2208
+ error: "Provide imageName, or templateId to deploy from an existing template.",
2209
+ status: 400
2210
+ });
2211
+ }
2212
+ const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
2213
+ const hasGpuCount = (params.gpuCount ?? 0) > 0;
2214
+ const wantsCpu = params.computeType === "CPU";
2215
+ const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
2216
+ if (backend.version === "v2") {
2217
+ if (wantsGpu && wantsCpu) {
2218
+ return jsonReply2({
2219
+ error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
2220
+ status: 400
2221
+ });
2222
+ }
2223
+ if (!wantsGpu && !wantsCpu) {
2224
+ return jsonReply2({
2225
+ error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
2226
+ status: 400
2227
+ });
2228
+ }
2229
+ if (wantsGpu && !hasGpuTypes) {
2230
+ return jsonReply2({
2231
+ error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
2232
+ status: 400
2233
+ });
2234
+ }
2235
+ if (wantsCpu) {
2236
+ if (params.templateId) {
2237
+ return jsonReply2({
2238
+ error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
2239
+ status: 400
2240
+ });
849
2241
  }
850
- ]
851
- };
2242
+ try {
2243
+ const result = await callRestUrl(
2244
+ `${restV1Base(env)}/pods`,
2245
+ "POST",
2246
+ params
2247
+ );
2248
+ return jsonReply2({
2249
+ ...result,
2250
+ _servedBy: "v1",
2251
+ _note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
2252
+ });
2253
+ } catch (error) {
2254
+ if (error instanceof HttpError) {
2255
+ return jsonReply2({
2256
+ error: error.message,
2257
+ status: error.status,
2258
+ _servedBy: "v1",
2259
+ _note: "CPU pod create was routed to the v1 API and failed."
2260
+ });
2261
+ }
2262
+ throw error;
2263
+ }
2264
+ }
2265
+ }
2266
+ let body = backend.mapCreate(params);
2267
+ if (params.templateId) {
2268
+ let template;
2269
+ try {
2270
+ template = await callRestUrl(
2271
+ `${restV2Base(env)}/templates/${params.templateId}`
2272
+ );
2273
+ } catch (error) {
2274
+ if (error instanceof HttpError) {
2275
+ return jsonReply2({
2276
+ error: `Failed to load template ${params.templateId}: ${error.message}`,
2277
+ status: error.status
2278
+ });
2279
+ }
2280
+ throw error;
2281
+ }
2282
+ body = { ...podBodyFromTemplate(template), ...body };
2283
+ }
2284
+ try {
2285
+ const result = await callRestUrl(
2286
+ `${backend.base}${backend.list}`,
2287
+ "POST",
2288
+ body
2289
+ );
2290
+ if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
2291
+ return jsonReply2({
2292
+ ...result,
2293
+ _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(", ")}.`
2294
+ });
2295
+ }
2296
+ return jsonReply2(result);
2297
+ } catch (error) {
2298
+ if (error instanceof HttpError) {
2299
+ return jsonReply2({ error: error.message, status: error.status });
2300
+ }
2301
+ throw error;
2302
+ }
852
2303
  }
853
2304
  );
854
2305
  server.tool(
855
2306
  "update-pod",
2307
+ "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
856
2308
  {
857
- podId: import_zod2.z.string().describe("ID of the pod to update"),
858
- name: import_zod2.z.string().optional().describe("New name for the pod"),
859
- imageName: import_zod2.z.string().optional().describe("New Docker image"),
860
- containerDiskInGb: import_zod2.z.number().optional().describe("New container disk size in GB"),
861
- volumeInGb: import_zod2.z.number().optional().describe("New volume size in GB"),
862
- volumeMountPath: import_zod2.z.string().optional().describe("New path to mount the volume"),
863
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("New ports to expose"),
864
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("New environment variables")
2309
+ podId: import_zod7.z.string().describe("ID of the pod to update"),
2310
+ name: import_zod7.z.string().optional().describe("New name for the pod"),
2311
+ imageName: import_zod7.z.string().optional().describe("New Docker image"),
2312
+ containerDiskInGb: import_zod7.z.number().optional().describe("New container disk size in GB"),
2313
+ volumeInGb: import_zod7.z.number().optional().describe("New volume size in GB"),
2314
+ volumeMountPath: import_zod7.z.string().optional().describe("New path to mount the volume"),
2315
+ ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("New ports to expose"),
2316
+ env: import_zod7.z.record(import_zod7.z.string()).optional().describe("New environment variables")
865
2317
  },
2318
+ { title: "Update pod", ...WRITE, idempotentHint: true },
866
2319
  async (params) => {
867
2320
  const { podId, ...updateParams } = params;
868
- const result = await runpodRequest(
869
- `/pods/${podId}`,
2321
+ const backend = backendFor("pods");
2322
+ const body = backend.mapUpdate(updateParams);
2323
+ const result = await callRestUrl(
2324
+ `${backend.base}${backend.get(podId)}`,
870
2325
  "PATCH",
871
- updateParams
2326
+ body
872
2327
  );
873
- return {
874
- content: [
875
- {
876
- type: "text",
877
- text: JSON.stringify(result, null, 2)
878
- }
879
- ]
880
- };
2328
+ return jsonReply2(result);
881
2329
  }
882
2330
  );
883
2331
  server.tool(
884
2332
  "start-pod",
2333
+ "Start a stopped pod (resumes the same pod and its billing).",
885
2334
  {
886
- podId: import_zod2.z.string().describe("ID of the pod to start")
2335
+ podId: import_zod7.z.string().describe("ID of the pod to start")
887
2336
  },
2337
+ { title: "Start pod", ...WRITE, idempotentHint: true },
888
2338
  async (params) => {
889
- const result = await runpodRequest(`/pods/${params.podId}/start`, "POST");
890
- return {
891
- content: [
892
- {
893
- type: "text",
894
- text: JSON.stringify(result, null, 2)
895
- }
896
- ]
897
- };
2339
+ const result = await podAction(params.podId, "start");
2340
+ return jsonReply2(result);
898
2341
  }
899
2342
  );
900
2343
  server.tool(
901
2344
  "stop-pod",
2345
+ "Stop a running pod. The pod and its disk persist; GPU billing stops.",
902
2346
  {
903
- podId: import_zod2.z.string().describe("ID of the pod to stop")
2347
+ podId: import_zod7.z.string().describe("ID of the pod to stop")
904
2348
  },
2349
+ { title: "Stop pod", ...WRITE, idempotentHint: true },
905
2350
  async (params) => {
906
- const result = await runpodRequest(`/pods/${params.podId}/stop`, "POST");
907
- return {
908
- content: [
909
- {
910
- type: "text",
911
- text: JSON.stringify(result, null, 2)
912
- }
913
- ]
914
- };
2351
+ const result = await podAction(params.podId, "stop");
2352
+ return jsonReply2(result);
2353
+ }
2354
+ );
2355
+ server.tool(
2356
+ "restart-pod",
2357
+ "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
2358
+ {
2359
+ podId: import_zod7.z.string().describe("ID of the pod to restart")
2360
+ },
2361
+ { title: "Restart pod", ...WRITE },
2362
+ async (params) => {
2363
+ const backend = backendFor("pods");
2364
+ if (backend.version === "v1") {
2365
+ return jsonReply2({
2366
+ error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
2367
+ status: 501
2368
+ });
2369
+ }
2370
+ const result = await podAction(params.podId, "restart");
2371
+ return jsonReply2(result);
915
2372
  }
916
2373
  );
2374
+ server.tool(
2375
+ "stream-pod-logs",
2376
+ "Fetch a bounded snapshot of a pod's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
2377
+ {
2378
+ podId: import_zod7.z.string().describe("ID of the pod whose logs to stream"),
2379
+ ...logStreamParams
2380
+ },
2381
+ { title: "Stream pod logs", ...READ_ONLY },
2382
+ (params) => streamLogsReply(
2383
+ rt,
2384
+ {
2385
+ name: "stream-pod-logs",
2386
+ resource: "pods",
2387
+ logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
2388
+ },
2389
+ params
2390
+ )
2391
+ );
917
2392
  server.tool(
918
2393
  "delete-pod",
2394
+ "Permanently delete a pod and its data. This cannot be undone.",
919
2395
  {
920
- podId: import_zod2.z.string().describe("ID of the pod to delete")
2396
+ podId: import_zod7.z.string().describe("ID of the pod to delete")
921
2397
  },
2398
+ { title: "Delete pod", ...DESTRUCTIVE },
922
2399
  async (params) => {
923
- const result = await runpodRequest(`/pods/${params.podId}`, "DELETE");
924
- return {
925
- content: [
926
- {
927
- type: "text",
928
- text: JSON.stringify(result, null, 2)
929
- }
930
- ]
931
- };
2400
+ const backend = backendFor("pods");
2401
+ const result = await callRestUrl(
2402
+ `${backend.base}${backend.get(params.podId)}`,
2403
+ "DELETE"
2404
+ );
2405
+ return jsonReply2(result);
932
2406
  }
933
2407
  );
2408
+ }
2409
+
2410
+ // src/tools/endpoints.ts
2411
+ var import_zod8 = require("zod");
2412
+ function registerEndpointTools(server, rt) {
2413
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
934
2414
  server.tool(
935
2415
  "list-endpoints",
2416
+ "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
936
2417
  {
937
2418
  ...listPaginationParams,
938
- includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
939
- includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
2419
+ includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
2420
+ includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
940
2421
  },
2422
+ { title: "List endpoints", ...READ_ONLY },
941
2423
  async (params) => {
942
- const queryParams = new URLSearchParams();
943
- if (params.includeTemplate)
944
- queryParams.append(
945
- "includeTemplate",
946
- params.includeTemplate.toString()
947
- );
948
- if (params.includeWorkers)
949
- queryParams.append("includeWorkers", params.includeWorkers.toString());
950
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
951
- const result = await runpodRequest(`/endpoints${queryString}`);
952
- return capListResult(result, {
2424
+ const backend = backendFor("endpoints");
2425
+ let queryString = "";
2426
+ if (backend.version === "v1") {
2427
+ const queryParams = new URLSearchParams();
2428
+ if (params.includeTemplate)
2429
+ queryParams.append(
2430
+ "includeTemplate",
2431
+ params.includeTemplate.toString()
2432
+ );
2433
+ if (params.includeWorkers)
2434
+ queryParams.append(
2435
+ "includeWorkers",
2436
+ params.includeWorkers.toString()
2437
+ );
2438
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
2439
+ }
2440
+ const result = await callRestUrl(
2441
+ `${backend.base}${backend.list}${queryString}`
2442
+ );
2443
+ return capListResult(backend.unwrap(result), {
953
2444
  limit: params.limit,
954
2445
  cursor: params.cursor
955
2446
  });
@@ -957,124 +2448,275 @@ function registerTools(server, ctx) {
957
2448
  );
958
2449
  server.tool(
959
2450
  "get-endpoint",
2451
+ "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
960
2452
  {
961
- endpointId: import_zod2.z.string().describe("ID of the endpoint to retrieve"),
962
- includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
963
- includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
2453
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
2454
+ includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
2455
+ includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
964
2456
  },
2457
+ { title: "Get endpoint", ...READ_ONLY },
965
2458
  async (params) => {
966
- const queryParams = new URLSearchParams();
967
- if (params.includeTemplate)
968
- queryParams.append(
969
- "includeTemplate",
970
- params.includeTemplate.toString()
971
- );
972
- if (params.includeWorkers)
973
- queryParams.append("includeWorkers", params.includeWorkers.toString());
974
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
975
- const result = await runpodRequest(
976
- `/endpoints/${params.endpointId}${queryString}`
2459
+ const backend = backendFor("endpoints");
2460
+ let queryString = "";
2461
+ if (backend.version === "v1") {
2462
+ const queryParams = new URLSearchParams();
2463
+ if (params.includeTemplate)
2464
+ queryParams.append(
2465
+ "includeTemplate",
2466
+ params.includeTemplate.toString()
2467
+ );
2468
+ if (params.includeWorkers)
2469
+ queryParams.append(
2470
+ "includeWorkers",
2471
+ params.includeWorkers.toString()
2472
+ );
2473
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
2474
+ }
2475
+ const result = await callRestUrl(
2476
+ `${backend.base}${backend.get(params.endpointId)}${queryString}`
977
2477
  );
978
- return {
979
- content: [
980
- {
981
- type: "text",
982
- text: JSON.stringify(result, null, 2)
983
- }
984
- ]
985
- };
2478
+ return jsonReply2(result);
986
2479
  }
987
2480
  );
988
2481
  server.tool(
989
2482
  "create-endpoint",
2483
+ "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).",
990
2484
  {
991
- name: import_zod2.z.string().optional().describe("Name for the endpoint"),
992
- templateId: import_zod2.z.string().describe("Template ID to use"),
993
- computeType: import_zod2.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint"),
994
- gpuTypeIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of acceptable GPU types"),
995
- gpuCount: import_zod2.z.number().optional().describe("Number of GPUs per worker"),
996
- workersMin: import_zod2.z.number().optional().describe("Minimum number of workers"),
997
- workersMax: import_zod2.z.number().optional().describe("Maximum number of workers"),
998
- dataCenterIds: import_zod2.z.array(import_zod2.z.string()).optional().describe("List of data centers")
2485
+ name: import_zod8.z.string().optional().describe("Name for the endpoint"),
2486
+ // --- v2 inline-config fields ---
2487
+ imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
2488
+ gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
2489
+ 'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
2490
+ ),
2491
+ gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
2492
+ args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
2493
+ containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
2494
+ ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
2495
+ env: import_zod8.z.record(import_zod8.z.string()).optional().describe("Environment variables (v2)"),
2496
+ containerRegistryAuthId: import_zod8.z.string().optional().describe("Container registry auth id for a private image (v2)"),
2497
+ networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("Network volume ids to attach (v2)"),
2498
+ executionTimeoutMs: import_zod8.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
2499
+ flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
2500
+ // --- v1 template-based fields ---
2501
+ templateId: import_zod8.z.string().optional().describe("Template ID (v1). Required on v1."),
2502
+ computeType: import_zod8.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
2503
+ gpuTypeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of acceptable GPU types (v1)"),
2504
+ // --- shared autoscaling fields ---
2505
+ workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
2506
+ workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
2507
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
2508
+ scalerValue: import_zod8.z.number().optional().describe("Autoscaler target value"),
2509
+ idleTimeout: import_zod8.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
2510
+ dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
999
2511
  },
2512
+ { title: "Create endpoint", ...WRITE },
1000
2513
  async (params) => {
1001
- const result = await runpodRequest("/endpoints", "POST", params);
1002
- return {
1003
- content: [
1004
- {
1005
- type: "text",
1006
- text: JSON.stringify(result, null, 2)
1007
- }
1008
- ]
1009
- };
2514
+ const backend = backendFor("endpoints");
2515
+ if (backend.version === "v2") {
2516
+ if (!params.name) {
2517
+ return jsonReply2({
2518
+ error: "On the v2 REST API, create-endpoint requires a name.",
2519
+ status: 400
2520
+ });
2521
+ }
2522
+ if (!params.imageName) {
2523
+ return jsonReply2({
2524
+ 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.",
2525
+ status: 400
2526
+ });
2527
+ }
2528
+ if (!params.gpuPoolIds?.length) {
2529
+ return jsonReply2({
2530
+ error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
2531
+ status: 400
2532
+ });
2533
+ }
2534
+ const body = backend.mapCreate(params);
2535
+ const result2 = await callRestUrl(
2536
+ `${backend.base}${backend.list}`,
2537
+ "POST",
2538
+ body
2539
+ );
2540
+ return jsonReply2(result2);
2541
+ }
2542
+ if (!params.templateId) {
2543
+ return jsonReply2({
2544
+ error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
2545
+ status: 400
2546
+ });
2547
+ }
2548
+ const v1Body = {};
2549
+ for (const k of [
2550
+ "templateId",
2551
+ "name",
2552
+ "computeType",
2553
+ "gpuTypeIds",
2554
+ "gpuCount",
2555
+ "workersMin",
2556
+ "workersMax",
2557
+ "dataCenterIds"
2558
+ ]) {
2559
+ if (params[k] !== void 0) v1Body[k] = params[k];
2560
+ }
2561
+ const result = await callRestUrl(
2562
+ `${backend.base}${backend.list}`,
2563
+ "POST",
2564
+ v1Body
2565
+ );
2566
+ return jsonReply2(result);
1010
2567
  }
1011
2568
  );
1012
2569
  server.tool(
1013
2570
  "update-endpoint",
2571
+ "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.",
1014
2572
  {
1015
- endpointId: import_zod2.z.string().describe("ID of the endpoint to update"),
1016
- name: import_zod2.z.string().optional().describe("New name for the endpoint"),
1017
- workersMin: import_zod2.z.number().optional().describe("New minimum number of workers"),
1018
- workersMax: import_zod2.z.number().optional().describe("New maximum number of workers"),
1019
- idleTimeout: import_zod2.z.number().optional().describe("New idle timeout in seconds"),
1020
- scalerType: import_zod2.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1021
- scalerValue: import_zod2.z.number().optional().describe("Scaler value")
2573
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
2574
+ name: import_zod8.z.string().optional().describe("New name for the endpoint"),
2575
+ workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
2576
+ workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
2577
+ idleTimeout: import_zod8.z.number().optional().describe("New idle timeout in seconds"),
2578
+ scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
2579
+ scalerValue: import_zod8.z.number().optional().describe("Scaler value"),
2580
+ // --- v2-only inline-config fields ---
2581
+ imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
2582
+ gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
2583
+ gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
2584
+ args: import_zod8.z.string().optional().describe("New container args (v2)"),
2585
+ containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
2586
+ ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
2587
+ env: import_zod8.z.record(import_zod8.z.string()).optional().describe("New environment variables (v2)"),
2588
+ containerRegistryAuthId: import_zod8.z.string().optional().describe("New container registry auth id (v2)"),
2589
+ networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("New network volume ids (v2)"),
2590
+ executionTimeoutMs: import_zod8.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
2591
+ flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
1022
2592
  },
2593
+ { title: "Update endpoint", ...WRITE, idempotentHint: true },
1023
2594
  async (params) => {
1024
2595
  const { endpointId, ...updateParams } = params;
1025
- const result = await runpodRequest(
1026
- `/endpoints/${endpointId}`,
2596
+ const backend = backendFor("endpoints");
2597
+ const body = backend.mapUpdate(updateParams);
2598
+ const result = await callRestUrl(
2599
+ `${backend.base}${backend.get(endpointId)}`,
1027
2600
  "PATCH",
1028
- updateParams
2601
+ body
1029
2602
  );
1030
- return {
1031
- content: [
1032
- {
1033
- type: "text",
1034
- text: JSON.stringify(result, null, 2)
1035
- }
1036
- ]
1037
- };
2603
+ return jsonReply2(result);
1038
2604
  }
1039
2605
  );
1040
2606
  server.tool(
1041
2607
  "delete-endpoint",
2608
+ "Permanently delete a Serverless endpoint. This cannot be undone.",
1042
2609
  {
1043
- endpointId: import_zod2.z.string().describe("ID of the endpoint to delete")
2610
+ endpointId: import_zod8.z.string().describe("ID of the endpoint to delete")
1044
2611
  },
2612
+ { title: "Delete endpoint", ...DESTRUCTIVE },
1045
2613
  async (params) => {
1046
- const result = await runpodRequest(
1047
- `/endpoints/${params.endpointId}`,
2614
+ const backend = backendFor("endpoints");
2615
+ const result = await callRestUrl(
2616
+ `${backend.base}${backend.get(params.endpointId)}`,
1048
2617
  "DELETE"
1049
2618
  );
1050
- return {
1051
- content: [
1052
- {
1053
- type: "text",
1054
- text: JSON.stringify(result, null, 2)
1055
- }
1056
- ]
1057
- };
2619
+ return jsonReply2(result);
2620
+ }
2621
+ );
2622
+ server.tool(
2623
+ "list-endpoint-releases",
2624
+ "List a Serverless endpoint's release history and current rollout status (workers on the latest version). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2625
+ {
2626
+ ...listPaginationParams,
2627
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose releases to list")
2628
+ },
2629
+ { title: "List endpoint releases", ...READ_ONLY },
2630
+ async (params) => {
2631
+ const backend = backendFor("endpoints");
2632
+ if (backend.version === "v1") {
2633
+ return jsonReply2({
2634
+ error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2635
+ status: 501
2636
+ });
2637
+ }
2638
+ const raw = await callRestUrl(
2639
+ `${backend.base}/serverless/${params.endpointId}/releases`
2640
+ );
2641
+ const releases = Array.isArray(raw?.releases) ? raw.releases : [];
2642
+ return capListResult(
2643
+ releases,
2644
+ { limit: params.limit, cursor: params.cursor },
2645
+ { rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
2646
+ );
2647
+ }
2648
+ );
2649
+ server.tool(
2650
+ "list-endpoint-workers",
2651
+ "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.",
2652
+ {
2653
+ ...listPaginationParams,
2654
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
2655
+ },
2656
+ { title: "List endpoint workers", ...READ_ONLY },
2657
+ async (params) => {
2658
+ const backend = backendFor("workers");
2659
+ if (backend.version === "v1") {
2660
+ return jsonReply2({
2661
+ error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
2662
+ status: 501
2663
+ });
2664
+ }
2665
+ const raw = await callRestUrl(
2666
+ `${backend.base}/serverless/${params.endpointId}/workers`
2667
+ );
2668
+ return capListResult(
2669
+ backend.unwrap(raw),
2670
+ { limit: params.limit, cursor: params.cursor },
2671
+ { summary: raw?.summary, endpointVersion: raw?.endpointVersion }
2672
+ );
1058
2673
  }
1059
2674
  );
1060
- const endpointIdSchema = import_zod2.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1061
- const jobIdSchema = import_zod2.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1062
- const inputSchema = import_zod2.z.record(import_zod2.z.unknown()).describe(
2675
+ server.tool(
2676
+ "stream-worker-logs",
2677
+ "Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
2678
+ {
2679
+ endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
2680
+ workerId: import_zod8.z.string().describe(
2681
+ "ID of the worker whose logs to stream (from list-endpoint-workers)"
2682
+ ),
2683
+ ...logStreamParams
2684
+ },
2685
+ { title: "Stream worker logs", ...READ_ONLY },
2686
+ (params) => streamLogsReply(
2687
+ rt,
2688
+ {
2689
+ name: "stream-worker-logs",
2690
+ resource: "workers",
2691
+ logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
2692
+ },
2693
+ params
2694
+ )
2695
+ );
2696
+ }
2697
+
2698
+ // src/tools/jobs.ts
2699
+ var import_zod9 = require("zod");
2700
+ function registerJobTools(server, rt) {
2701
+ const { jsonReply: jsonReply2, serverlessRequest } = rt;
2702
+ const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
2703
+ const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
2704
+ const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
1063
2705
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
1064
2706
  );
1065
- const webhookSchema = import_zod2.z.string().url().optional().describe(
2707
+ const webhookSchema = import_zod9.z.string().url().optional().describe(
1066
2708
  "Webhook URL to receive job completion notifications instead of polling"
1067
2709
  );
1068
- const policySchema = import_zod2.z.object({
1069
- executionTimeout: import_zod2.z.number().optional().describe("Maximum execution time in milliseconds"),
1070
- lowPriority: import_zod2.z.boolean().optional().describe("Submit as a low-priority job"),
1071
- ttl: import_zod2.z.number().optional().describe("Time-to-live for the job result in milliseconds")
2710
+ const policySchema = import_zod9.z.object({
2711
+ executionTimeout: import_zod9.z.number().optional().describe("Maximum execution time in milliseconds"),
2712
+ lowPriority: import_zod9.z.boolean().optional().describe("Submit as a low-priority job"),
2713
+ ttl: import_zod9.z.number().optional().describe("Time-to-live for the job result in milliseconds")
1072
2714
  }).optional().describe("Execution policy options");
1073
- const s3ConfigSchema = import_zod2.z.object({
1074
- accessId: import_zod2.z.string().describe("S3 access key ID"),
1075
- accessSecret: import_zod2.z.string().describe("S3 secret access key"),
1076
- bucketName: import_zod2.z.string().describe("S3 bucket name"),
1077
- endpointUrl: import_zod2.z.string().describe("S3 endpoint URL")
2715
+ const s3ConfigSchema = import_zod9.z.object({
2716
+ accessId: import_zod9.z.string().describe("S3 access key ID"),
2717
+ accessSecret: import_zod9.z.string().describe("S3 secret access key"),
2718
+ bucketName: import_zod9.z.string().describe("S3 bucket name"),
2719
+ endpointUrl: import_zod9.z.string().describe("S3 endpoint URL")
1078
2720
  }).optional().describe("S3-compatible storage config for large outputs");
1079
2721
  server.tool(
1080
2722
  "run-endpoint",
@@ -1088,6 +2730,7 @@ function registerTools(server, ctx) {
1088
2730
  policy: policySchema,
1089
2731
  s3Config: s3ConfigSchema
1090
2732
  },
2733
+ { title: "Run endpoint (async)", ...WRITE },
1091
2734
  async (params) => {
1092
2735
  const { endpointId, ...body } = params;
1093
2736
  const result = await serverlessRequest(
@@ -1096,14 +2739,7 @@ function registerTools(server, ctx) {
1096
2739
  "POST",
1097
2740
  body
1098
2741
  );
1099
- return {
1100
- content: [
1101
- {
1102
- type: "text",
1103
- text: JSON.stringify(result, null, 2)
1104
- }
1105
- ]
1106
- };
2742
+ return jsonReply2(result);
1107
2743
  }
1108
2744
  );
1109
2745
  server.tool(
@@ -1114,13 +2750,14 @@ function registerTools(server, ctx) {
1114
2750
  "ID of the Serverless endpoint to run synchronously"
1115
2751
  ),
1116
2752
  input: inputSchema,
1117
- wait: import_zod2.z.number().min(1e3).max(3e5).optional().describe(
2753
+ wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
1118
2754
  "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)."
1119
2755
  ),
1120
2756
  webhook: webhookSchema,
1121
2757
  policy: policySchema,
1122
2758
  s3Config: s3ConfigSchema
1123
2759
  },
2760
+ { title: "Run endpoint (sync)", ...WRITE },
1124
2761
  async (params) => {
1125
2762
  const { endpointId, wait, ...body } = params;
1126
2763
  const path2 = wait ? `/runsync?wait=${wait}` : "/runsync";
@@ -1130,14 +2767,7 @@ function registerTools(server, ctx) {
1130
2767
  "POST",
1131
2768
  body
1132
2769
  );
1133
- return {
1134
- content: [
1135
- {
1136
- type: "text",
1137
- text: JSON.stringify(result, null, 2)
1138
- }
1139
- ]
1140
- };
2770
+ return jsonReply2(result);
1141
2771
  }
1142
2772
  );
1143
2773
  server.tool(
@@ -1149,19 +2779,13 @@ function registerTools(server, ctx) {
1149
2779
  ),
1150
2780
  jobId: jobIdSchema.describe("ID of the job to check")
1151
2781
  },
2782
+ { title: "Get job status", ...READ_ONLY },
1152
2783
  async (params) => {
1153
2784
  const result = await serverlessRequest(
1154
2785
  params.endpointId,
1155
2786
  `/status/${params.jobId}`
1156
2787
  );
1157
- return {
1158
- content: [
1159
- {
1160
- type: "text",
1161
- text: JSON.stringify(result, null, 2)
1162
- }
1163
- ]
1164
- };
2788
+ return jsonReply2(result);
1165
2789
  }
1166
2790
  );
1167
2791
  server.tool(
@@ -1173,6 +2797,7 @@ function registerTools(server, ctx) {
1173
2797
  ),
1174
2798
  jobId: jobIdSchema.describe("ID of the job to stream results from")
1175
2799
  },
2800
+ { title: "Stream job", ...READ_ONLY },
1176
2801
  async (params) => {
1177
2802
  const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1178
2803
  "COMPLETED",
@@ -1218,18 +2843,7 @@ function registerTools(server, ctx) {
1218
2843
  }
1219
2844
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1220
2845
  }
1221
- return {
1222
- content: [
1223
- {
1224
- type: "text",
1225
- text: JSON.stringify(
1226
- { ...finalResult, stream: allChunks },
1227
- null,
1228
- 2
1229
- )
1230
- }
1231
- ]
1232
- };
2846
+ return jsonReply2({ ...finalResult, stream: allChunks });
1233
2847
  }
1234
2848
  );
1235
2849
  server.tool(
@@ -1241,20 +2855,14 @@ function registerTools(server, ctx) {
1241
2855
  ),
1242
2856
  jobId: jobIdSchema.describe("ID of the job to cancel")
1243
2857
  },
2858
+ { title: "Cancel job", ...WRITE, idempotentHint: true },
1244
2859
  async (params) => {
1245
2860
  const result = await serverlessRequest(
1246
2861
  params.endpointId,
1247
2862
  `/cancel/${params.jobId}`,
1248
2863
  "POST"
1249
2864
  );
1250
- return {
1251
- content: [
1252
- {
1253
- type: "text",
1254
- text: JSON.stringify(result, null, 2)
1255
- }
1256
- ]
1257
- };
2865
+ return jsonReply2(result);
1258
2866
  }
1259
2867
  );
1260
2868
  server.tool(
@@ -1266,20 +2874,14 @@ function registerTools(server, ctx) {
1266
2874
  ),
1267
2875
  jobId: jobIdSchema.describe("ID of the job to retry")
1268
2876
  },
2877
+ { title: "Retry job", ...WRITE },
1269
2878
  async (params) => {
1270
2879
  const result = await serverlessRequest(
1271
2880
  params.endpointId,
1272
2881
  `/retry/${params.jobId}`,
1273
2882
  "POST"
1274
2883
  );
1275
- return {
1276
- content: [
1277
- {
1278
- type: "text",
1279
- text: JSON.stringify(result, null, 2)
1280
- }
1281
- ]
1282
- };
2884
+ return jsonReply2(result);
1283
2885
  }
1284
2886
  );
1285
2887
  server.tool(
@@ -1290,16 +2892,10 @@ function registerTools(server, ctx) {
1290
2892
  "ID of the Serverless endpoint to check health for"
1291
2893
  )
1292
2894
  },
2895
+ { title: "Endpoint health", ...READ_ONLY },
1293
2896
  async (params) => {
1294
2897
  const result = await serverlessRequest(params.endpointId, "/health");
1295
- return {
1296
- content: [
1297
- {
1298
- type: "text",
1299
- text: JSON.stringify(result, null, 2)
1300
- }
1301
- ]
1302
- };
2898
+ return jsonReply2(result);
1303
2899
  }
1304
2900
  );
1305
2901
  server.tool(
@@ -1310,46 +2906,51 @@ function registerTools(server, ctx) {
1310
2906
  "ID of the Serverless endpoint to purge the queue for"
1311
2907
  )
1312
2908
  },
2909
+ { title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
1313
2910
  async (params) => {
1314
2911
  const result = await serverlessRequest(
1315
2912
  params.endpointId,
1316
2913
  "/purge-queue",
1317
2914
  "POST"
1318
2915
  );
1319
- return {
1320
- content: [
1321
- {
1322
- type: "text",
1323
- text: JSON.stringify(result, null, 2)
1324
- }
1325
- ]
1326
- };
2916
+ return jsonReply2(result);
1327
2917
  }
1328
2918
  );
2919
+ }
2920
+
2921
+ // src/tools/templates.ts
2922
+ var import_zod10 = require("zod");
2923
+ function registerTemplateTools(server, rt) {
2924
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1329
2925
  server.tool(
1330
2926
  "list-templates",
1331
2927
  `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.`,
1332
2928
  {
1333
2929
  ...listPaginationParams,
1334
- includeRunpodTemplates: import_zod2.z.boolean().optional().describe("Include official Runpod templates in the response"),
1335
- includePublicTemplates: import_zod2.z.boolean().optional().describe("Include community-made public templates in the response"),
1336
- includeEndpointBoundTemplates: import_zod2.z.boolean().optional().describe(
2930
+ includeRunpodTemplates: import_zod10.z.boolean().optional().describe("Include official Runpod templates in the response"),
2931
+ includePublicTemplates: import_zod10.z.boolean().optional().describe("Include community-made public templates in the response"),
2932
+ includeEndpointBoundTemplates: import_zod10.z.boolean().optional().describe(
1337
2933
  "Include templates bound to Serverless endpoints in the response"
1338
2934
  )
1339
2935
  },
2936
+ { title: "List templates", ...READ_ONLY },
1340
2937
  async (params) => {
1341
- const queryParams = new URLSearchParams();
1342
- if (params.includeRunpodTemplates)
1343
- queryParams.set("includeRunpodTemplates", "true");
1344
- if (params.includePublicTemplates)
1345
- queryParams.set("includePublicTemplates", "true");
1346
- if (params.includeEndpointBoundTemplates)
1347
- queryParams.set("includeEndpointBoundTemplates", "true");
1348
- const query = queryParams.toString();
1349
- const result = await runpodRequest(
1350
- `/templates${query ? `?${query}` : ""}`
2938
+ const backend = backendFor("templates");
2939
+ let query = "";
2940
+ if (backend.version === "v1") {
2941
+ const queryParams = new URLSearchParams();
2942
+ if (params.includeRunpodTemplates)
2943
+ queryParams.set("includeRunpodTemplates", "true");
2944
+ if (params.includePublicTemplates)
2945
+ queryParams.set("includePublicTemplates", "true");
2946
+ if (params.includeEndpointBoundTemplates)
2947
+ queryParams.set("includeEndpointBoundTemplates", "true");
2948
+ query = queryParams.toString() ? `?${queryParams.toString()}` : "";
2949
+ }
2950
+ const result = await callRestUrl(
2951
+ `${backend.base}${backend.list}${query}`
1351
2952
  );
1352
- return capListResult(result, {
2953
+ return capListResult(backend.unwrap(result), {
1353
2954
  limit: params.limit,
1354
2955
  cursor: params.cursor
1355
2956
  });
@@ -1357,196 +2958,223 @@ function registerTools(server, ctx) {
1357
2958
  );
1358
2959
  server.tool(
1359
2960
  "get-template",
2961
+ "Get one template by id (image, env, ports, disk/volume config).",
1360
2962
  {
1361
- templateId: import_zod2.z.string().describe("ID of the template to retrieve")
2963
+ templateId: import_zod10.z.string().describe("ID of the template to retrieve")
1362
2964
  },
2965
+ { title: "Get template", ...READ_ONLY },
1363
2966
  async (params) => {
1364
- const result = await runpodRequest(`/templates/${params.templateId}`);
1365
- return {
1366
- content: [
1367
- {
1368
- type: "text",
1369
- text: JSON.stringify(result, null, 2)
1370
- }
1371
- ]
1372
- };
2967
+ const backend = backendFor("templates");
2968
+ const result = await callRestUrl(
2969
+ `${backend.base}${backend.get(params.templateId)}`
2970
+ );
2971
+ return jsonReply2(result);
1373
2972
  }
1374
2973
  );
1375
2974
  server.tool(
1376
2975
  "create-template",
2976
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
1377
2977
  {
1378
- name: import_zod2.z.string().describe("Name for the template"),
1379
- imageName: import_zod2.z.string().describe("Docker image to use"),
1380
- isServerless: import_zod2.z.boolean().optional().describe("Is this a serverless template"),
1381
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("Ports to expose"),
1382
- dockerEntrypoint: import_zod2.z.array(import_zod2.z.string()).optional().describe("Docker entrypoint commands"),
1383
- dockerStartCmd: import_zod2.z.array(import_zod2.z.string()).optional().describe("Docker start commands"),
1384
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("Environment variables"),
1385
- containerDiskInGb: import_zod2.z.number().optional().describe("Container disk size in GB"),
1386
- volumeInGb: import_zod2.z.number().optional().describe("Volume size in GB"),
1387
- volumeMountPath: import_zod2.z.string().optional().describe("Path to mount the volume"),
1388
- readme: import_zod2.z.string().optional().describe("README content in markdown format"),
1389
- containerRegistryAuthId: import_zod2.z.string().optional().describe(
2978
+ name: import_zod10.z.string().describe("Name for the template"),
2979
+ imageName: import_zod10.z.string().describe("Docker image to use"),
2980
+ isServerless: import_zod10.z.boolean().optional().describe("Is this a serverless template"),
2981
+ ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("Ports to expose"),
2982
+ dockerEntrypoint: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2983
+ "Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
2984
+ ),
2985
+ dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
2986
+ 'Container start command. On v2 this is stored as the template\'s `args` string; a multi-element array is joined with spaces (e.g. ["python","-u","app.py"] \u2192 "python -u app.py"). The join is lossy: an element that itself contains spaces is NOT preserved as one argument \u2014 e.g. ["sh","-c","echo hi"] becomes "sh -c echo hi", so pass a shell command as a single pre-quoted element if that distinction matters. Omit or pass an empty array to leave the command unset (never overwrites with "").'
2987
+ ),
2988
+ env: import_zod10.z.record(import_zod10.z.string()).optional().describe("Environment variables"),
2989
+ containerDiskInGb: import_zod10.z.number().optional().describe("Container disk size in GB"),
2990
+ volumeInGb: import_zod10.z.number().optional().describe("Volume size in GB"),
2991
+ volumeMountPath: import_zod10.z.string().optional().describe("Path to mount the volume"),
2992
+ readme: import_zod10.z.string().optional().describe("README content in markdown format"),
2993
+ category: import_zod10.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2994
+ "How the template is grouped and filtered in the Runpod console \u2014 it does NOT affect hardware selection, scheduling, or billing. Optional; defaults to NVIDIA. Set CPU or AMD only to file the template correctly in the console."
2995
+ ),
2996
+ containerRegistryAuthId: import_zod10.z.string().optional().describe(
1390
2997
  "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."
1391
2998
  )
1392
2999
  },
3000
+ { title: "Create template", ...WRITE },
1393
3001
  async (params) => {
1394
- const result = await runpodRequest("/templates", "POST", params);
1395
- return {
1396
- content: [
1397
- {
1398
- type: "text",
1399
- text: JSON.stringify(result, null, 2)
1400
- }
1401
- ]
1402
- };
3002
+ const backend = backendFor("templates");
3003
+ const body = backend.mapCreate(params);
3004
+ const result = await callRestUrl(
3005
+ `${backend.base}${backend.list}`,
3006
+ "POST",
3007
+ body
3008
+ );
3009
+ return jsonReply2(result);
1403
3010
  }
1404
3011
  );
1405
3012
  server.tool(
1406
3013
  "update-template",
3014
+ "Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
1407
3015
  {
1408
- templateId: import_zod2.z.string().describe("ID of the template to update"),
1409
- name: import_zod2.z.string().optional().describe("New name for the template"),
1410
- imageName: import_zod2.z.string().optional().describe("New Docker image"),
1411
- ports: import_zod2.z.array(import_zod2.z.string()).optional().describe("New ports to expose"),
1412
- env: import_zod2.z.record(import_zod2.z.string()).optional().describe("New environment variables"),
1413
- readme: import_zod2.z.string().optional().describe("New README content in markdown format"),
1414
- containerRegistryAuthId: import_zod2.z.string().optional().describe(
3016
+ templateId: import_zod10.z.string().describe("ID of the template to update"),
3017
+ name: import_zod10.z.string().optional().describe("New name for the template"),
3018
+ imageName: import_zod10.z.string().optional().describe("New Docker image"),
3019
+ dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
3020
+ "New container start command. On v2 stored as the template's `args` string; a multi-element array is joined with spaces (lossy \u2014 an element containing spaces is split into separate tokens). Note: this can only set a command, not clear one \u2014 an empty or absent array is ignored so the existing command is preserved."
3021
+ ),
3022
+ ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("New ports to expose"),
3023
+ env: import_zod10.z.record(import_zod10.z.string()).optional().describe("New environment variables"),
3024
+ readme: import_zod10.z.string().optional().describe("New README content in markdown format"),
3025
+ containerRegistryAuthId: import_zod10.z.string().optional().describe(
1415
3026
  "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."
1416
3027
  )
1417
3028
  },
3029
+ { title: "Update template", ...WRITE, idempotentHint: true },
1418
3030
  async (params) => {
1419
3031
  const { templateId, ...updateParams } = params;
1420
- const result = await runpodRequest(
1421
- `/templates/${templateId}`,
3032
+ const backend = backendFor("templates");
3033
+ const body = backend.mapUpdate(updateParams);
3034
+ const result = await callRestUrl(
3035
+ `${backend.base}${backend.get(templateId)}`,
1422
3036
  "PATCH",
1423
- updateParams
3037
+ body
1424
3038
  );
1425
- return {
1426
- content: [
1427
- {
1428
- type: "text",
1429
- text: JSON.stringify(result, null, 2)
1430
- }
1431
- ]
1432
- };
3039
+ return jsonReply2(result);
1433
3040
  }
1434
3041
  );
1435
3042
  server.tool(
1436
3043
  "delete-template",
3044
+ "Permanently delete a template. This cannot be undone.",
1437
3045
  {
1438
- templateId: import_zod2.z.string().describe("ID of the template to delete")
3046
+ templateId: import_zod10.z.string().describe("ID of the template to delete")
1439
3047
  },
3048
+ { title: "Delete template", ...DESTRUCTIVE },
1440
3049
  async (params) => {
1441
- const result = await runpodRequest(
1442
- `/templates/${params.templateId}`,
3050
+ const backend = backendFor("templates");
3051
+ const result = await callRestUrl(
3052
+ `${backend.base}${backend.get(params.templateId)}`,
1443
3053
  "DELETE"
1444
3054
  );
1445
- return {
1446
- content: [
1447
- {
1448
- type: "text",
1449
- text: JSON.stringify(result, null, 2)
1450
- }
1451
- ]
1452
- };
3055
+ return jsonReply2(result);
3056
+ }
3057
+ );
3058
+ }
3059
+
3060
+ // src/tools/network-volumes.ts
3061
+ var import_zod11 = require("zod");
3062
+ function registerNetworkVolumeTools(server, rt) {
3063
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
3064
+ server.tool(
3065
+ "list-network-volumes",
3066
+ "List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
3067
+ listPaginationParams,
3068
+ { title: "List network volumes", ...READ_ONLY },
3069
+ async (params) => {
3070
+ const backend = backendFor("networkVolumes");
3071
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
3072
+ return capListResult(backend.unwrap(result), {
3073
+ limit: params.limit,
3074
+ cursor: params.cursor
3075
+ });
1453
3076
  }
1454
3077
  );
1455
- server.tool("list-network-volumes", listPaginationParams, async (params) => {
1456
- const result = await runpodRequest("/networkvolumes");
1457
- return capListResult(result, {
1458
- limit: params.limit,
1459
- cursor: params.cursor
1460
- });
1461
- });
1462
3078
  server.tool(
1463
3079
  "get-network-volume",
3080
+ "Get one network volume by id (name, size, data center).",
1464
3081
  {
1465
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to retrieve")
3082
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to retrieve")
1466
3083
  },
3084
+ { title: "Get network volume", ...READ_ONLY },
1467
3085
  async (params) => {
1468
- const result = await runpodRequest(
1469
- `/networkvolumes/${params.networkVolumeId}`
3086
+ const backend = backendFor("networkVolumes");
3087
+ const result = await callRestUrl(
3088
+ `${backend.base}${backend.get(params.networkVolumeId)}`
1470
3089
  );
1471
- return {
1472
- content: [
1473
- {
1474
- type: "text",
1475
- text: JSON.stringify(result, null, 2)
1476
- }
1477
- ]
1478
- };
3090
+ return jsonReply2(result);
1479
3091
  }
1480
3092
  );
1481
3093
  server.tool(
1482
3094
  "create-network-volume",
3095
+ "Create a persistent network volume in a data center (size 10\u20134096 GB) that pods can mount. Pass volumeType to pick a storage tier; omit it to get the data center default.",
1483
3096
  {
1484
- name: import_zod2.z.string().describe("Name for the network volume"),
1485
- size: import_zod2.z.number().describe("Size in GB (1-4000)"),
1486
- dataCenterId: import_zod2.z.string().describe("Data center ID")
3097
+ name: import_zod11.z.string().describe("Name for the network volume"),
3098
+ // Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
3099
+ // here as well as described, so an out-of-range value fails locally
3100
+ // instead of costing an API roundtrip.
3101
+ size: import_zod11.z.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
3102
+ dataCenterId: import_zod11.z.string().describe("Data center ID (see list-data-centers)"),
3103
+ volumeType: import_zod11.z.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
3104
+ "Storage tier. Omit for the data center's default (primary) tier. HIGH_PERFORMANCE provisions an HPS volume. Immutable after creation \u2014 update-network-volume cannot change it."
3105
+ )
1487
3106
  },
3107
+ { title: "Create network volume", ...WRITE },
1488
3108
  async (params) => {
1489
- const result = await runpodRequest("/networkvolumes", "POST", params);
1490
- return {
1491
- content: [
1492
- {
1493
- type: "text",
1494
- text: JSON.stringify(result, null, 2)
1495
- }
1496
- ]
1497
- };
3109
+ const backend = backendFor("networkVolumes");
3110
+ const body = backend.mapCreate(params);
3111
+ const result = await callRestUrl(
3112
+ `${backend.base}${backend.list}`,
3113
+ "POST",
3114
+ body
3115
+ );
3116
+ return jsonReply2(result);
1498
3117
  }
1499
3118
  );
1500
3119
  server.tool(
1501
3120
  "update-network-volume",
3121
+ "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1502
3122
  {
1503
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to update"),
1504
- name: import_zod2.z.string().optional().describe("New name for the network volume"),
1505
- size: import_zod2.z.number().optional().describe("New size in GB (must be larger than current)")
3123
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to update"),
3124
+ name: import_zod11.z.string().optional().describe("New name for the network volume"),
3125
+ // Same absolute bounds as create. "Larger than current" stays server-side —
3126
+ // we do not hold the current size here, so only the range is checked.
3127
+ size: import_zod11.z.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
1506
3128
  },
3129
+ { title: "Update network volume", ...WRITE, idempotentHint: true },
1507
3130
  async (params) => {
1508
3131
  const { networkVolumeId, ...updateParams } = params;
1509
- const result = await runpodRequest(
1510
- `/networkvolumes/${networkVolumeId}`,
3132
+ const backend = backendFor("networkVolumes");
3133
+ const body = backend.mapUpdate(updateParams);
3134
+ const result = await callRestUrl(
3135
+ `${backend.base}${backend.get(networkVolumeId)}`,
1511
3136
  "PATCH",
1512
- updateParams
3137
+ body
1513
3138
  );
1514
- return {
1515
- content: [
1516
- {
1517
- type: "text",
1518
- text: JSON.stringify(result, null, 2)
1519
- }
1520
- ]
1521
- };
3139
+ return jsonReply2(result);
1522
3140
  }
1523
3141
  );
1524
3142
  server.tool(
1525
3143
  "delete-network-volume",
3144
+ "Permanently delete a network volume and its data. This cannot be undone.",
1526
3145
  {
1527
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to delete")
3146
+ networkVolumeId: import_zod11.z.string().describe("ID of the network volume to delete")
1528
3147
  },
3148
+ { title: "Delete network volume", ...DESTRUCTIVE },
1529
3149
  async (params) => {
1530
- const result = await runpodRequest(
1531
- `/networkvolumes/${params.networkVolumeId}`,
3150
+ const backend = backendFor("networkVolumes");
3151
+ const result = await callRestUrl(
3152
+ `${backend.base}${backend.get(params.networkVolumeId)}`,
1532
3153
  "DELETE"
1533
3154
  );
1534
- return {
1535
- content: [
1536
- {
1537
- type: "text",
1538
- text: JSON.stringify(result, null, 2)
1539
- }
1540
- ]
1541
- };
3155
+ return jsonReply2(result);
1542
3156
  }
1543
3157
  );
3158
+ }
3159
+
3160
+ // src/tools/registries.ts
3161
+ var import_zod12 = require("zod");
3162
+ var DELEGATIONS = "/delegations";
3163
+ function registerRegistryTools(server, rt) {
3164
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
3165
+ const delegationsV2OnlyNotice = (tool) => ({
3166
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
3167
+ status: 501
3168
+ });
1544
3169
  server.tool(
1545
3170
  "list-container-registry-auths",
3171
+ "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
1546
3172
  listPaginationParams,
3173
+ { title: "List container registry auths", ...READ_ONLY },
1547
3174
  async (params) => {
1548
- const result = await runpodRequest("/containerregistryauth");
1549
- return capListResult(result, {
3175
+ const backend = backendFor("registries");
3176
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
3177
+ return capListResult(backend.unwrap(result), {
1550
3178
  limit: params.limit,
1551
3179
  cursor: params.cursor
1552
3180
  });
@@ -1554,72 +3182,229 @@ function registerTools(server, ctx) {
1554
3182
  );
1555
3183
  server.tool(
1556
3184
  "get-container-registry-auth",
3185
+ "Get one saved container-registry credential by id (the secret/password is not returned).",
1557
3186
  {
1558
- containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to retrieve")
3187
+ containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to retrieve")
1559
3188
  },
3189
+ { title: "Get container registry auth", ...READ_ONLY },
1560
3190
  async (params) => {
1561
- const result = await runpodRequest(
1562
- `/containerregistryauth/${params.containerRegistryAuthId}`
3191
+ const backend = backendFor("registries");
3192
+ const result = await callRestUrl(
3193
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`
1563
3194
  );
1564
- return {
1565
- content: [
1566
- {
1567
- type: "text",
1568
- text: JSON.stringify(result, null, 2)
1569
- }
1570
- ]
1571
- };
3195
+ return jsonReply2(result);
1572
3196
  }
1573
3197
  );
1574
3198
  server.tool(
1575
3199
  "create-container-registry-auth",
3200
+ "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1576
3201
  {
1577
- name: import_zod2.z.string().describe("Name for the container registry auth"),
1578
- username: import_zod2.z.string().describe("Registry username"),
1579
- password: import_zod2.z.string().describe("Registry password")
3202
+ name: import_zod12.z.string().describe("Name for the container registry auth"),
3203
+ username: import_zod12.z.string().describe("Registry username"),
3204
+ password: import_zod12.z.string().describe("Registry password or access token (stored as a secret)")
1580
3205
  },
3206
+ { title: "Create container registry auth", ...WRITE },
1581
3207
  async (params) => {
1582
- const result = await runpodRequest(
1583
- "/containerregistryauth",
3208
+ const backend = backendFor("registries");
3209
+ const body = backend.mapCreate(params);
3210
+ const result = await callRestUrl(
3211
+ `${backend.base}${backend.list}`,
1584
3212
  "POST",
1585
- params
3213
+ body
1586
3214
  );
1587
- return {
1588
- content: [
1589
- {
1590
- type: "text",
1591
- text: JSON.stringify(result, null, 2)
1592
- }
1593
- ]
1594
- };
3215
+ return jsonReply2(result);
1595
3216
  }
1596
3217
  );
1597
3218
  server.tool(
1598
3219
  "delete-container-registry-auth",
3220
+ "Permanently delete a saved container-registry credential. This cannot be undone.",
1599
3221
  {
1600
- containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to delete")
3222
+ containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to delete")
1601
3223
  },
3224
+ { title: "Delete container registry auth", ...DESTRUCTIVE },
1602
3225
  async (params) => {
1603
- const result = await runpodRequest(
1604
- `/containerregistryauth/${params.containerRegistryAuthId}`,
3226
+ const backend = backendFor("registries");
3227
+ const result = await callRestUrl(
3228
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`,
1605
3229
  "DELETE"
1606
3230
  );
1607
- return {
1608
- content: [
1609
- {
1610
- type: "text",
1611
- text: JSON.stringify(result, null, 2)
1612
- }
1613
- ]
1614
- };
3231
+ return jsonReply2(result);
3232
+ }
3233
+ );
3234
+ server.tool(
3235
+ "list-registry-delegations",
3236
+ "List your AWS ECR delegations \u2014 ECR repositories Runpod has been granted scoped pull access to, so private ECR images can be pulled without storing AWS credentials. Each entry includes the `dockerRegistryUri` to pull from. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
3237
+ listPaginationParams,
3238
+ { title: "List registry delegations", ...READ_ONLY },
3239
+ async (params) => {
3240
+ const backend = backendFor("registries");
3241
+ if (backend.version === "v1") {
3242
+ return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
3243
+ }
3244
+ const raw = await callRestUrl(
3245
+ `${backend.base}${backend.list}${DELEGATIONS}`
3246
+ );
3247
+ const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
3248
+ return capListResult(delegations, {
3249
+ limit: params.limit,
3250
+ cursor: params.cursor
3251
+ });
3252
+ }
3253
+ );
3254
+ server.tool(
3255
+ "create-registry-delegation",
3256
+ "Register an AWS ECR repository so Runpod can pull private images from it without stored credentials. Pass the repository ARN; the reply carries the `dockerRegistryUri`, resolved repository/tag, and AWS region. v2-only \u2014 returns a 501 notice on the v1 API. For a non-ECR private registry use create-container-registry-auth instead.",
3257
+ {
3258
+ resource: import_zod12.z.string().min(1).describe(
3259
+ 'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
3260
+ ),
3261
+ name: import_zod12.z.string().optional().describe("Optional human-readable name for the delegation")
3262
+ },
3263
+ { title: "Create registry delegation", ...WRITE },
3264
+ async (params) => {
3265
+ const backend = backendFor("registries");
3266
+ if (backend.version === "v1") {
3267
+ return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
3268
+ }
3269
+ const body = { resource: params.resource };
3270
+ if (params.name !== void 0) body.name = params.name;
3271
+ const result = await callRestUrl(
3272
+ `${backend.base}${backend.list}${DELEGATIONS}`,
3273
+ "POST",
3274
+ body
3275
+ );
3276
+ return jsonReply2(result);
3277
+ }
3278
+ );
3279
+ server.tool(
3280
+ "delete-registry-delegation",
3281
+ "Revoke an AWS ECR delegation, withdrawing Runpod's pull access to that repository. Existing workers keep running; future pulls fail. v2-only \u2014 returns a 501 notice on the v1 API. This cannot be undone.",
3282
+ {
3283
+ delegationId: import_zod12.z.string().describe(
3284
+ "ID of the delegation to revoke (from list-registry-delegations)"
3285
+ )
3286
+ },
3287
+ { title: "Revoke registry delegation", ...DESTRUCTIVE },
3288
+ async (params) => {
3289
+ const backend = backendFor("registries");
3290
+ if (backend.version === "v1") {
3291
+ return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
3292
+ }
3293
+ const result = await callRestUrl(
3294
+ `${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
3295
+ params.delegationId
3296
+ )}`,
3297
+ "DELETE"
3298
+ );
3299
+ return jsonReply2(result);
3300
+ }
3301
+ );
3302
+ }
3303
+
3304
+ // src/tools/billing.ts
3305
+ var import_zod13 = require("zod");
3306
+ var BILLING_SCOPE_PATH = {
3307
+ all: "/billing",
3308
+ pods: "/billing/pods",
3309
+ serverless: "/billing/serverless",
3310
+ endpoints: "/billing/endpoints",
3311
+ networkvolumes: "/billing/networkvolumes",
3312
+ clusters: "/billing/clusters"
3313
+ };
3314
+ function registerBillingTools(server, rt) {
3315
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
3316
+ server.tool(
3317
+ "get-billing",
3318
+ "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.",
3319
+ {
3320
+ ...listPaginationParams,
3321
+ scope: import_zod13.z.enum([
3322
+ "all",
3323
+ "pods",
3324
+ "serverless",
3325
+ "endpoints",
3326
+ "networkvolumes",
3327
+ "clusters"
3328
+ ]).optional().describe(
3329
+ 'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
3330
+ ),
3331
+ startTime: import_zod13.z.string().optional().describe(
3332
+ "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
3333
+ ),
3334
+ endTime: import_zod13.z.string().optional().describe(
3335
+ "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
3336
+ ),
3337
+ bucketSize: import_zod13.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
3338
+ lastN: import_zod13.z.number().int().positive().optional().describe(
3339
+ "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
3340
+ )
3341
+ },
3342
+ { title: "Get billing history", ...READ_ONLY },
3343
+ async (params) => {
3344
+ const backend = backendFor("billing");
3345
+ if (backend.version === "v1") {
3346
+ return jsonReply2({
3347
+ error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
3348
+ status: 501
3349
+ });
3350
+ }
3351
+ const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
3352
+ const query = new URLSearchParams();
3353
+ if (params.startTime) query.set("startTime", params.startTime);
3354
+ if (params.endTime) query.set("endTime", params.endTime);
3355
+ if (params.bucketSize) query.set("bucketSize", params.bucketSize);
3356
+ if (params.lastN !== void 0 && !params.startTime && !params.endTime)
3357
+ query.set("lastN", String(params.lastN));
3358
+ const qs = query.toString() ? `?${query.toString()}` : "";
3359
+ const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
3360
+ return capListResult(
3361
+ backend.unwrap(raw),
3362
+ { limit: params.limit, cursor: params.cursor },
3363
+ { metadata: raw?.metadata }
3364
+ );
1615
3365
  }
1616
3366
  );
1617
3367
  }
1618
3368
 
3369
+ // src/tools.ts
3370
+ function registerTools(server, ctx, deps = {}) {
3371
+ const rt = createToolRuntime(server, ctx, deps);
3372
+ registerCatalogTools(server, rt);
3373
+ registerHubTools(server, rt);
3374
+ registerPublicEndpointTools(server, rt);
3375
+ registerPodTools(server, rt);
3376
+ registerEndpointTools(server, rt);
3377
+ registerEndpointGpuTools(server, rt);
3378
+ registerJobTools(server, rt);
3379
+ registerTemplateTools(server, rt);
3380
+ registerNetworkVolumeTools(server, rt);
3381
+ registerRegistryTools(server, rt);
3382
+ registerBillingTools(server, rt);
3383
+ }
3384
+
1619
3385
  // src/stdio.ts
1620
3386
  var WIZARD_SUBCOMMANDS = ["add", "remove"];
1621
3387
  var subcommand = process.argv[2];
1622
3388
  var isWizardMode = WIZARD_SUBCOMMANDS.includes(subcommand);
3389
+ async function main(apiKey) {
3390
+ let v2Available;
3391
+ if (wantsAutoProbe(process.env)) {
3392
+ const probe = createV2Prober({
3393
+ // node-fetch's signature is wider than the `{ status }` the prober needs.
3394
+ fetch: import_node_fetch2.default,
3395
+ baseUrl: restV2Base(process.env),
3396
+ apiKey,
3397
+ // Surface a transient/timeout fallback to v1 — otherwise `auto` silently
3398
+ // pins the session to v1 with no signal (stderr is the MCP log channel).
3399
+ warn: (message) => console.error(`[runpod-mcp] ${message}`)
3400
+ });
3401
+ v2Available = await probe();
3402
+ }
3403
+ const server = createServer();
3404
+ registerTools(server, { apiKey, transport: "stdio" }, { v2Available });
3405
+ const transport = new import_stdio.StdioServerTransport();
3406
+ await server.connect(transport);
3407
+ }
1623
3408
  if (isWizardMode) {
1624
3409
  Promise.resolve().then(() => (init_wizard(), wizard_exports)).then(({ runWizard: runWizard2 }) => runWizard2(subcommand)).catch((error) => {
1625
3410
  console.error(error instanceof Error ? error.message : String(error));
@@ -1631,9 +3416,9 @@ if (isWizardMode) {
1631
3416
  console.error("RUNPOD_API_KEY environment variable is required");
1632
3417
  process.exit(1);
1633
3418
  }
1634
- const server = createServer();
1635
- registerTools(server, { apiKey: API_KEY, transport: "stdio" });
1636
- const transport = new import_stdio.StdioServerTransport();
1637
- server.connect(transport);
3419
+ main(API_KEY).catch((error) => {
3420
+ console.error("Failed to start Runpod MCP server:", error);
3421
+ process.exit(1);
3422
+ });
1638
3423
  }
1639
3424
  //# sourceMappingURL=stdio.cjs.map