@runpod/mcp-server 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stdio.cjs CHANGED
@@ -444,11 +444,12 @@ 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");
450
451
  var SERVER_NAME = "Runpod API Server";
451
- var SERVER_VERSION = true ? "1.4.0" : "dev";
452
+ var SERVER_VERSION = true ? "2.0.0" : "dev";
452
453
  function createServer(version = SERVER_VERSION) {
453
454
  return new import_mcp.McpServer({
454
455
  name: SERVER_NAME,
@@ -460,11 +461,598 @@ function createServer(version = SERVER_VERSION) {
460
461
  });
461
462
  }
462
463
 
463
- // src/tools.ts
464
- var import_zod2 = require("zod");
464
+ // src/tools/runtime.ts
465
465
  var import_node_fetch = __toESM(require("node-fetch"), 1);
466
466
  var import_node_crypto = require("crypto");
467
467
 
468
+ // src/_shared/http.ts
469
+ var HttpError = class extends Error {
470
+ constructor(prefix, status, body) {
471
+ super(`${prefix}: ${status} - ${body}`);
472
+ this.name = "HttpError";
473
+ this.status = status;
474
+ this.body = body;
475
+ }
476
+ };
477
+ function isJsonContentType(contentType) {
478
+ if (!contentType) return false;
479
+ const ct = contentType.toLowerCase();
480
+ return ct.includes("application/json") || ct.includes("+json");
481
+ }
482
+ function methodSendsBody(method) {
483
+ return method === "POST" || method === "PATCH" || method === "PUT";
484
+ }
485
+ function buildRequestHeaders(apiKey, tracking) {
486
+ return {
487
+ Authorization: `Bearer ${apiKey}`,
488
+ "Content-Type": "application/json",
489
+ ...tracking()
490
+ };
491
+ }
492
+ function createHttpClient(deps) {
493
+ return async function request(url, method = "GET", body) {
494
+ const init = {
495
+ method,
496
+ headers: buildRequestHeaders(deps.apiKey, deps.tracking)
497
+ };
498
+ if (body && methodSendsBody(method)) {
499
+ init.body = JSON.stringify(body);
500
+ }
501
+ const response = await deps.fetch(url, init);
502
+ if (!response.ok) {
503
+ throw new HttpError(
504
+ deps.errorPrefix,
505
+ response.status,
506
+ await response.text()
507
+ );
508
+ }
509
+ return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
510
+ };
511
+ }
512
+
513
+ // src/_shared/tracking.ts
514
+ function sanitizeUaToken(value) {
515
+ return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
516
+ }
517
+ function buildTrackingHeaders(input) {
518
+ const name = sanitizeUaToken(input.clientName || "unknown");
519
+ const version = sanitizeUaToken(input.clientVersion || "unknown");
520
+ const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
521
+ return {
522
+ "User-Agent": userAgent,
523
+ "X-Runpod-Session-Id": input.sessionId
524
+ };
525
+ }
526
+
527
+ // src/_shared/mappers.ts
528
+ function compact(obj) {
529
+ const out = {};
530
+ for (const [k, v] of Object.entries(obj)) {
531
+ if (v !== void 0) out[k] = v;
532
+ }
533
+ return out;
534
+ }
535
+ function persistentMount(size, path2) {
536
+ if (size === void 0 || path2 === void 0) return void 0;
537
+ return { persistent: { size, path: path2 } };
538
+ }
539
+ function containerConfigToV2(p2) {
540
+ return compact({
541
+ image: p2.imageName,
542
+ disk: p2.containerDiskInGb,
543
+ ports: p2.ports,
544
+ env: p2.env,
545
+ mounts: persistentMount(p2.volumeInGb, p2.volumeMountPath)
546
+ });
547
+ }
548
+ function gpuConfigToV2(gpuTypeIds, gpuCount) {
549
+ const id = gpuTypeIds?.[0];
550
+ if (id === void 0) return void 0;
551
+ return compact({ id, count: gpuCount });
552
+ }
553
+ function mapPodCreateToV2(params) {
554
+ return compact({
555
+ ...containerConfigToV2(params),
556
+ name: params.name,
557
+ cloud: params.cloudType,
558
+ // v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
559
+ // centers; omit/empty = scheduler chooses). Pass the v1 list straight
560
+ // through — the earlier singular `dataCenter` key did not exist on the pod
561
+ // schema, so placement was being silently dropped. GPU, by contrast, IS
562
+ // singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
563
+ // create-pod handler warns when more than one gpuTypeId was supplied.
564
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
565
+ gpu: gpuConfigToV2(params.gpuTypeIds, params.gpuCount)
566
+ });
567
+ }
568
+ function mapPodUpdateToV2(params) {
569
+ return {
570
+ ...containerConfigToV2(params),
571
+ ...compact({ name: params.name })
572
+ };
573
+ }
574
+ function nestedOrUndefined(obj) {
575
+ const c = compact(obj);
576
+ return Object.keys(c).length ? c : void 0;
577
+ }
578
+ function endpointGpuConfig(pools, count) {
579
+ if (!pools?.length) return void 0;
580
+ return compact({ pools, count });
581
+ }
582
+ function mapEndpointToV2(params) {
583
+ return compact({
584
+ name: params.name,
585
+ image: params.imageName,
586
+ args: params.args,
587
+ disk: params.containerDiskInGb,
588
+ ports: params.ports,
589
+ env: params.env,
590
+ registry: params.containerRegistryAuthId,
591
+ gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
592
+ workers: nestedOrUndefined({
593
+ min: params.workersMin,
594
+ max: params.workersMax
595
+ }),
596
+ scaling: nestedOrUndefined({
597
+ type: params.scalerType,
598
+ value: params.scalerValue,
599
+ idleTimeout: params.idleTimeout
600
+ }),
601
+ dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
602
+ networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
603
+ timeout: params.executionTimeoutMs,
604
+ flashboot: params.flashboot
605
+ });
606
+ }
607
+ var mapEndpointCreateToV2 = mapEndpointToV2;
608
+ var mapEndpointUpdateToV2 = mapEndpointToV2;
609
+ function mapNetworkVolumeCreateToV2(params) {
610
+ return compact({
611
+ name: params.name,
612
+ size: params.size,
613
+ dataCenter: params.dataCenterId
614
+ });
615
+ }
616
+ function mapTemplateCreateToV2(params) {
617
+ return {
618
+ ...containerConfigToV2(params),
619
+ ...compact({
620
+ name: params.name,
621
+ serverless: params.isServerless,
622
+ registry: params.containerRegistryAuthId
623
+ }),
624
+ // Required by v2 — always emit, defaulting to NVIDIA.
625
+ category: params.category ?? "NVIDIA"
626
+ };
627
+ }
628
+ function mapTemplateUpdateToV2(params) {
629
+ return {
630
+ ...containerConfigToV2(params),
631
+ ...compact({
632
+ name: params.name,
633
+ registry: params.containerRegistryAuthId
634
+ })
635
+ };
636
+ }
637
+
638
+ // src/_shared/backend.ts
639
+ var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
640
+ var V2_UNWRAP_KEY = {
641
+ pods: "pods",
642
+ templates: "templates",
643
+ networkVolumes: "networkVolumes",
644
+ registries: "registries",
645
+ gpus: "gpus",
646
+ cpus: "cpus",
647
+ dataCenters: "dataCenters",
648
+ endpoints: "endpoints",
649
+ jobs: "jobs",
650
+ tags: "tags",
651
+ workers: "workers",
652
+ billing: "records"
653
+ };
654
+ var RESOURCE_ENV_KEY = {
655
+ pods: "PODS",
656
+ templates: "TEMPLATES",
657
+ networkVolumes: "NETWORK_VOLUMES",
658
+ registries: "REGISTRIES",
659
+ gpus: "GPUS",
660
+ cpus: "CPUS",
661
+ dataCenters: "DATA_CENTERS",
662
+ endpoints: "ENDPOINTS",
663
+ jobs: "JOBS",
664
+ tags: "TAGS",
665
+ workers: "WORKERS",
666
+ billing: "BILLING"
667
+ };
668
+ function restV1Base(env) {
669
+ return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
670
+ }
671
+ function restV2Base(env) {
672
+ return env.RUNPOD_REST_V2_API_URL ?? "https://v2-rest.runpod.io/v2";
673
+ }
674
+ function serverlessBase(env) {
675
+ return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
676
+ }
677
+ function publicGraphqlBase(env) {
678
+ return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
679
+ }
680
+ function unwrapList(resource, version, raw) {
681
+ if (version === "v1") return Array.isArray(raw) ? raw : [];
682
+ if (raw === null || typeof raw !== "object") return [];
683
+ const arr = raw[V2_UNWRAP_KEY[resource]];
684
+ return Array.isArray(arr) ? arr : [];
685
+ }
686
+ function resolveVersion(opts) {
687
+ const { resource, env, ctx, v2Available } = opts;
688
+ if (V1_ONLY.has(resource)) return "v1";
689
+ const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
690
+ const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
691
+ if (setting === "v2") return "v2";
692
+ if (setting === "auto") {
693
+ if (ctx.transport === "http") return "v1";
694
+ return v2Available === true ? "v2" : "v1";
695
+ }
696
+ return "v1";
697
+ }
698
+ function wantsAutoProbe(env) {
699
+ return Object.entries(env).some(
700
+ ([k, v]) => k.startsWith("RUNPOD_REST_VERSION") && (v ?? "").toLowerCase() === "auto"
701
+ );
702
+ }
703
+ var PROBE_TIMEOUT_MS = 4e3;
704
+ function classifyProbeStatus(status) {
705
+ if (status >= 500) return void 0;
706
+ if (status === 401 || status === 403) return void 0;
707
+ return status >= 200 && status < 300;
708
+ }
709
+ async function runV2Probe(deps) {
710
+ try {
711
+ const res = await deps.fetch(`${deps.baseUrl}/catalog/gpus`, {
712
+ method: "GET",
713
+ headers: { Authorization: `Bearer ${deps.apiKey}` },
714
+ signal: AbortSignal.timeout(deps.timeoutMs ?? PROBE_TIMEOUT_MS)
715
+ });
716
+ const verdict = classifyProbeStatus(res.status);
717
+ if (verdict === void 0) {
718
+ deps.warn?.(
719
+ `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.`
720
+ );
721
+ }
722
+ return verdict;
723
+ } catch {
724
+ deps.warn?.(
725
+ `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.`
726
+ );
727
+ return void 0;
728
+ }
729
+ }
730
+ function createV2Prober(deps) {
731
+ let cached;
732
+ let inFlight;
733
+ const probeOnce = async () => {
734
+ const verdict = await runV2Probe(deps);
735
+ if (verdict !== void 0) cached = verdict;
736
+ return verdict ?? false;
737
+ };
738
+ return () => {
739
+ if (cached !== void 0) return Promise.resolve(cached);
740
+ if (!inFlight) {
741
+ inFlight = probeOnce().finally(() => {
742
+ inFlight = void 0;
743
+ });
744
+ }
745
+ return inFlight;
746
+ };
747
+ }
748
+ var identity = (body) => body;
749
+ var V1_REST_PATHS = {
750
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
751
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
752
+ networkVolumes: {
753
+ list: "/networkvolumes",
754
+ get: (id) => `/networkvolumes/${id}`
755
+ },
756
+ registries: {
757
+ list: "/containerregistryauth",
758
+ get: (id) => `/containerregistryauth/${id}`
759
+ },
760
+ endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
761
+ };
762
+ var CATALOG = /* @__PURE__ */ new Set([
763
+ "gpus",
764
+ "cpus",
765
+ "dataCenters"
766
+ ]);
767
+ var V2_ONLY = /* @__PURE__ */ new Set([
768
+ "tags",
769
+ "workers",
770
+ "billing"
771
+ ]);
772
+ var V2_REST_PATHS = {
773
+ pods: { list: "/pods", get: (id) => `/pods/${id}` },
774
+ templates: { list: "/templates", get: (id) => `/templates/${id}` },
775
+ networkVolumes: {
776
+ list: "/network-volumes",
777
+ get: (id) => `/network-volumes/${id}`
778
+ },
779
+ registries: { list: "/registries", get: (id) => `/registries/${id}` },
780
+ gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
781
+ cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
782
+ dataCenters: {
783
+ list: "/catalog/datacenters",
784
+ get: (id) => `/catalog/datacenters/${id}`
785
+ },
786
+ tags: { list: "/tags", get: (id) => `/tags/${id}` },
787
+ // Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
788
+ // response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
789
+ // unwrap key stays correct.
790
+ endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
791
+ // workers/billing build their own sub-paths in the tool from `base`; the
792
+ // list entry just satisfies buildV2Backend (which requires a paths entry).
793
+ workers: { list: "/serverless" },
794
+ billing: { list: "/billing" }
795
+ };
796
+ var V2_MAPPERS = {
797
+ pods: {
798
+ create: (b) => mapPodCreateToV2(b),
799
+ update: (b) => mapPodUpdateToV2(b)
800
+ },
801
+ templates: {
802
+ create: (b) => mapTemplateCreateToV2(b),
803
+ update: (b) => mapTemplateUpdateToV2(b)
804
+ },
805
+ networkVolumes: {
806
+ create: (b) => mapNetworkVolumeCreateToV2(
807
+ b
808
+ ),
809
+ update: identity
810
+ },
811
+ registries: { create: identity, update: identity },
812
+ endpoints: {
813
+ create: (b) => mapEndpointCreateToV2(b),
814
+ update: (b) => mapEndpointUpdateToV2(b)
815
+ }
816
+ };
817
+ function buildV2Backend(resource, env, unwrap) {
818
+ const paths = V2_REST_PATHS[resource];
819
+ if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
820
+ const mappers = V2_MAPPERS[resource];
821
+ return {
822
+ kind: "rest",
823
+ version: "v2",
824
+ base: restV2Base(env),
825
+ list: paths.list,
826
+ get: paths.get,
827
+ unwrap,
828
+ mapCreate: mappers?.create ?? identity,
829
+ mapUpdate: mappers?.update ?? identity
830
+ };
831
+ }
832
+ function buildV1Backend(resource, env, unwrap) {
833
+ if (resource === "jobs") {
834
+ return {
835
+ kind: "rest",
836
+ version: "v1",
837
+ base: serverlessBase(env),
838
+ unwrap,
839
+ mapCreate: identity,
840
+ mapUpdate: identity
841
+ };
842
+ }
843
+ if (V2_ONLY.has(resource)) {
844
+ return {
845
+ kind: "rest",
846
+ version: "v1",
847
+ base: restV1Base(env),
848
+ unwrap,
849
+ mapCreate: identity,
850
+ mapUpdate: identity
851
+ };
852
+ }
853
+ if (CATALOG.has(resource)) {
854
+ return {
855
+ kind: "graphql",
856
+ version: "v1",
857
+ base: publicGraphqlBase(env),
858
+ unwrap,
859
+ mapCreate: identity,
860
+ mapUpdate: identity
861
+ };
862
+ }
863
+ const paths = V1_REST_PATHS[resource];
864
+ if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
865
+ return {
866
+ kind: "rest",
867
+ version: "v1",
868
+ base: restV1Base(env),
869
+ list: paths.list,
870
+ get: paths.get,
871
+ unwrap,
872
+ mapCreate: identity,
873
+ mapUpdate: identity
874
+ };
875
+ }
876
+ function resolveBackend(opts) {
877
+ const { resource, env } = opts;
878
+ const version = resolveVersion(opts);
879
+ const unwrap = (raw) => unwrapList(resource, version, raw);
880
+ return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
881
+ }
882
+
883
+ // src/tools/runtime.ts
884
+ var MCP_SERVER_VERSION = true ? "2.0.0" : "dev";
885
+ var SESSION_ID = (0, import_node_crypto.randomUUID)();
886
+ function trackingHeaders(server, ctx) {
887
+ const info = server.server.getClientVersion();
888
+ return buildTrackingHeaders({
889
+ clientName: info?.name || ctx.clientUserAgent || "unknown",
890
+ clientVersion: info?.version || "unknown",
891
+ transport: ctx.transport,
892
+ // Prefer a server version supplied by the caller (the hosted entrypoint reads
893
+ // it from package.json at runtime, since tsup's build-time define does not
894
+ // run when Vercel compiles api/index.ts). Fall back to the build-time value.
895
+ serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
896
+ sessionId: SESSION_ID
897
+ });
898
+ }
899
+ var defaultFetch = import_node_fetch.default;
900
+ async function withApiErrorLog(label, call) {
901
+ try {
902
+ return await call();
903
+ } catch (error) {
904
+ console.error(label, error);
905
+ throw error;
906
+ }
907
+ }
908
+ function jsonReply(result) {
909
+ return {
910
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
911
+ };
912
+ }
913
+ var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
914
+ var WRITE = { readOnlyHint: false, openWorldHint: true };
915
+ var DESTRUCTIVE = {
916
+ readOnlyHint: false,
917
+ destructiveHint: true,
918
+ idempotentHint: true,
919
+ openWorldHint: true
920
+ };
921
+ async function graphqlRequest(query, tracking, fetchImpl, url) {
922
+ const response = await fetchImpl(url, {
923
+ method: "POST",
924
+ headers: {
925
+ "Content-Type": "application/json",
926
+ ...tracking()
927
+ },
928
+ body: JSON.stringify({ query })
929
+ });
930
+ const result = await response.json();
931
+ if (result.errors && result.errors.length > 0) {
932
+ throw new Error(
933
+ `GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
934
+ );
935
+ }
936
+ if (!result.data) {
937
+ throw new Error("No data returned from GraphQL query");
938
+ }
939
+ return result.data;
940
+ }
941
+ function createToolRuntime(server, ctx, deps = {}) {
942
+ const tracking = () => trackingHeaders(server, ctx);
943
+ const httpFetch = deps.fetch ?? defaultFetch;
944
+ const v1Client = createHttpClient({
945
+ apiKey: ctx.apiKey,
946
+ fetch: httpFetch,
947
+ tracking,
948
+ errorPrefix: "Runpod API Error"
949
+ });
950
+ const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
951
+ "Error calling Runpod API:",
952
+ () => v1Client(`${restV1Base(process.env)}${endpoint}`, method, body)
953
+ );
954
+ const serverlessClient = createHttpClient({
955
+ apiKey: ctx.apiKey,
956
+ fetch: httpFetch,
957
+ tracking,
958
+ errorPrefix: "Runpod Serverless API Error"
959
+ });
960
+ const serverlessRequest = (endpointId, path2, method = "GET", body) => withApiErrorLog(
961
+ "Error calling Runpod Serverless API:",
962
+ () => serverlessClient(
963
+ `${serverlessBase(process.env)}/${endpointId}${path2}`,
964
+ method,
965
+ body
966
+ )
967
+ );
968
+ const restClient = createHttpClient({
969
+ apiKey: ctx.apiKey,
970
+ fetch: httpFetch,
971
+ tracking,
972
+ errorPrefix: "Runpod API Error"
973
+ });
974
+ const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
975
+ "Error calling Runpod API:",
976
+ () => restClient(url, method, body)
977
+ );
978
+ const streamSse = deps.streamSse ?? (async (url, opts) => {
979
+ const controller = new AbortController();
980
+ const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
981
+ try {
982
+ const res = await (0, import_node_fetch.default)(url, {
983
+ method: "GET",
984
+ headers: {
985
+ Authorization: `Bearer ${ctx.apiKey}`,
986
+ Accept: "text/event-stream",
987
+ ...tracking()
988
+ },
989
+ signal: controller.signal
990
+ });
991
+ if (!res.ok) {
992
+ throw new HttpError(
993
+ "Runpod API Error",
994
+ res.status,
995
+ await res.text().catch(() => "")
996
+ );
997
+ }
998
+ const chunks = [];
999
+ let bytes = 0;
1000
+ let truncated = false;
1001
+ try {
1002
+ for await (const chunk of res.body) {
1003
+ chunks.push(chunk);
1004
+ bytes += chunk.length;
1005
+ if (bytes >= opts.maxBytes) {
1006
+ truncated = true;
1007
+ controller.abort();
1008
+ break;
1009
+ }
1010
+ }
1011
+ } catch (err) {
1012
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
1013
+ }
1014
+ return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
1015
+ } finally {
1016
+ clearTimeout(timer);
1017
+ }
1018
+ });
1019
+ const backendFor = (resource) => resolveBackend({
1020
+ resource,
1021
+ env: process.env,
1022
+ ctx: { transport: ctx.transport },
1023
+ v2Available: deps.v2Available
1024
+ });
1025
+ const podAction = async (podId, action) => {
1026
+ const backend = backendFor("pods");
1027
+ const podPath = backend.get(podId);
1028
+ if (backend.version === "v2") {
1029
+ return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
1030
+ action
1031
+ });
1032
+ }
1033
+ return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
1034
+ };
1035
+ return {
1036
+ jsonReply,
1037
+ graphql: (query) => graphqlRequest(
1038
+ query,
1039
+ tracking,
1040
+ httpFetch,
1041
+ publicGraphqlBase(process.env)
1042
+ ),
1043
+ runpodRequest,
1044
+ serverlessRequest,
1045
+ callRestUrl,
1046
+ backendFor,
1047
+ streamSse,
1048
+ podAction,
1049
+ env: process.env
1050
+ };
1051
+ }
1052
+
1053
+ // src/tools/catalog.ts
1054
+ var import_zod2 = require("zod");
1055
+
468
1056
  // src/pagination.ts
469
1057
  var import_zod = require("zod");
470
1058
  var DEFAULT_LIST_LIMIT = 20;
@@ -490,7 +1078,7 @@ function decodeCursorOffset(cursor) {
490
1078
  function encodeCursorOffset(offset) {
491
1079
  return Buffer.from(String(offset), "utf8").toString("base64");
492
1080
  }
493
- function capListResult(result, options) {
1081
+ function capListResult(result, options, extra) {
494
1082
  const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
495
1083
  const offset = decodeCursorOffset(options.cursor);
496
1084
  if (!Array.isArray(result)) {
@@ -504,7 +1092,11 @@ function capListResult(result, options) {
504
1092
  const page = result.slice(offset, offset + limit);
505
1093
  const nextOffset = offset + page.length;
506
1094
  const hasMore = nextOffset < total;
1095
+ const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
1096
+ void _i;
1097
+ void _p;
507
1098
  const envelope = {
1099
+ ...safeExtra,
508
1100
  items: page,
509
1101
  pagination: {
510
1102
  total,
@@ -522,120 +1114,14 @@ function capListResult(result, options) {
522
1114
  };
523
1115
  }
524
1116
 
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);
1117
+ // src/tools/catalog.ts
1118
+ function registerCatalogTools(server, rt) {
1119
+ const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
636
1120
  server.tool(
637
1121
  "list-gpu-types",
1122
+ "List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. On v2 the realtime stock filter is a no-op.",
638
1123
  {
1124
+ ...listPaginationParams,
639
1125
  minMemoryGb: import_zod2.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
640
1126
  secureCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
641
1127
  communityCloudOnly: import_zod2.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
@@ -646,9 +1132,30 @@ function registerTools(server, ctx) {
646
1132
  "Include GPUs that are currently out of stock. Default is false"
647
1133
  )
648
1134
  },
1135
+ { title: "List GPU types", ...READ_ONLY },
649
1136
  async (params) => {
650
- const data = await graphqlRequest(
651
- `
1137
+ const backend = backendFor("gpus");
1138
+ if (backend.version === "v2") {
1139
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
1140
+ let gpus = backend.unwrap(raw);
1141
+ if (params.minMemoryGb !== void 0)
1142
+ gpus = gpus.filter(
1143
+ (g) => Number(g.memory ?? 0) >= params.minMemoryGb
1144
+ );
1145
+ if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
1146
+ if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
1147
+ if (params.searchTerm) {
1148
+ const t = params.searchTerm.toLowerCase();
1149
+ gpus = gpus.filter(
1150
+ (g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
1151
+ );
1152
+ }
1153
+ return capListResult(gpus, {
1154
+ limit: params.limit,
1155
+ cursor: params.cursor
1156
+ });
1157
+ }
1158
+ const data = await graphql(`
652
1159
  query {
653
1160
  gpuTypes {
654
1161
  id
@@ -661,9 +1168,7 @@ function registerTools(server, ctx) {
661
1168
  }
662
1169
  }
663
1170
  }
664
- `,
665
- tracking
666
- );
1171
+ `);
667
1172
  const stockPriority = {
668
1173
  High: 3,
669
1174
  Medium: 2,
@@ -709,26 +1214,39 @@ function registerTools(server, ctx) {
709
1214
  stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
710
1215
  available: isAvailable(gpu)
711
1216
  }));
712
- return {
713
- content: [
714
- {
715
- type: "text",
716
- text: JSON.stringify(result, null, 2)
717
- }
718
- ]
719
- };
1217
+ return capListResult(result, {
1218
+ limit: params.limit,
1219
+ cursor: params.cursor
1220
+ });
720
1221
  }
721
1222
  );
722
1223
  server.tool(
723
1224
  "list-data-centers",
1225
+ "List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
724
1226
  {
1227
+ ...listPaginationParams,
725
1228
  region: import_zod2.z.string().optional().describe(
726
1229
  "Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
727
1230
  )
728
1231
  },
1232
+ { title: "List data centers", ...READ_ONLY },
729
1233
  async (params) => {
730
- const data = await graphqlRequest(
731
- `
1234
+ const backend = backendFor("dataCenters");
1235
+ if (backend.version === "v2") {
1236
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
1237
+ let dcs = backend.unwrap(raw);
1238
+ if (params.region) {
1239
+ const t = params.region.toLowerCase();
1240
+ dcs = dcs.filter(
1241
+ (dc) => String(dc.region ?? "").toLowerCase().includes(t)
1242
+ );
1243
+ }
1244
+ return capListResult(dcs, {
1245
+ limit: params.limit,
1246
+ cursor: params.cursor
1247
+ });
1248
+ }
1249
+ const data = await graphql(`
732
1250
  query {
733
1251
  dataCenters {
734
1252
  id
@@ -736,9 +1254,7 @@ function registerTools(server, ctx) {
736
1254
  location
737
1255
  }
738
1256
  }
739
- `,
740
- tracking
741
- );
1257
+ `);
742
1258
  let dataCenters = data.dataCenters;
743
1259
  if (params.region) {
744
1260
  const term = params.region.toLowerCase();
@@ -746,50 +1262,146 @@ function registerTools(server, ctx) {
746
1262
  (dc) => dc.location.toLowerCase().includes(term)
747
1263
  );
748
1264
  }
749
- return {
750
- content: [
751
- {
752
- type: "text",
753
- text: JSON.stringify(dataCenters, null, 2)
754
- }
755
- ]
756
- };
1265
+ return capListResult(dataCenters, {
1266
+ limit: params.limit,
1267
+ cursor: params.cursor
1268
+ });
1269
+ }
1270
+ );
1271
+ server.tool(
1272
+ "list-cpu-types",
1273
+ "List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
1274
+ { ...listPaginationParams },
1275
+ { title: "List CPU types", ...READ_ONLY },
1276
+ async (params) => {
1277
+ const backend = backendFor("cpus");
1278
+ if (backend.version === "v1") {
1279
+ return jsonReply2({
1280
+ error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1281
+ status: 501
1282
+ });
1283
+ }
1284
+ const raw = await callRestUrl(`${backend.base}${backend.list}`);
1285
+ return capListResult(backend.unwrap(raw), {
1286
+ limit: params.limit,
1287
+ cursor: params.cursor
1288
+ });
757
1289
  }
758
1290
  );
1291
+ server.tool(
1292
+ "get-gpu-type",
1293
+ "Get details for a single GPU type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there).",
1294
+ {
1295
+ gpuTypeId: import_zod2.z.string().describe("ID of the GPU type to retrieve")
1296
+ },
1297
+ { title: "Get GPU type", ...READ_ONLY },
1298
+ async (params) => {
1299
+ const backend = backendFor("gpus");
1300
+ if (backend.version === "v1") {
1301
+ return jsonReply2({
1302
+ error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
1303
+ status: 501
1304
+ });
1305
+ }
1306
+ const result = await callRestUrl(
1307
+ `${backend.base}${backend.get(params.gpuTypeId)}`
1308
+ );
1309
+ return jsonReply2(result);
1310
+ }
1311
+ );
1312
+ server.tool(
1313
+ "get-cpu-type",
1314
+ "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).",
1315
+ {
1316
+ cpuTypeId: import_zod2.z.string().describe("ID of the CPU type to retrieve")
1317
+ },
1318
+ { title: "Get CPU type", ...READ_ONLY },
1319
+ async (params) => {
1320
+ const backend = backendFor("cpus");
1321
+ if (backend.version === "v1") {
1322
+ return jsonReply2({
1323
+ error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1324
+ status: 501
1325
+ });
1326
+ }
1327
+ const result = await callRestUrl(
1328
+ `${backend.base}${backend.get(params.cpuTypeId)}`
1329
+ );
1330
+ return jsonReply2(result);
1331
+ }
1332
+ );
1333
+ server.tool(
1334
+ "get-data-center",
1335
+ "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).",
1336
+ {
1337
+ dataCenterId: import_zod2.z.string().describe("ID of the data center to retrieve")
1338
+ },
1339
+ { title: "Get data center", ...READ_ONLY },
1340
+ async (params) => {
1341
+ const backend = backendFor("dataCenters");
1342
+ if (backend.version === "v1") {
1343
+ return jsonReply2({
1344
+ error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
1345
+ status: 501
1346
+ });
1347
+ }
1348
+ const result = await callRestUrl(
1349
+ `${backend.base}${backend.get(params.dataCenterId)}`
1350
+ );
1351
+ return jsonReply2(result);
1352
+ }
1353
+ );
1354
+ }
1355
+
1356
+ // src/tools/pods.ts
1357
+ var import_zod3 = require("zod");
1358
+ function registerPodTools(server, rt) {
1359
+ const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
759
1360
  server.tool(
760
1361
  "list-pods",
1362
+ "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.",
761
1363
  {
762
1364
  ...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")
1365
+ computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
1366
+ gpuTypeId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
1367
+ dataCenterId: import_zod3.z.array(import_zod3.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
1368
+ name: import_zod3.z.string().optional().describe("Filter to Pods with the provided name"),
1369
+ includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
1370
+ includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
769
1371
  },
1372
+ { title: "List pods", ...READ_ONLY },
770
1373
  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)
777
- );
778
- if (params.dataCenterId)
779
- params.dataCenterId.forEach(
780
- (dc) => queryParams.append("dataCenterId", dc)
781
- );
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()
789
- );
790
- const queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
791
- const result = await runpodRequest(`/pods${queryString}`);
792
- return capListResult(result, {
1374
+ const backend = backendFor("pods");
1375
+ let queryString = "";
1376
+ if (backend.version === "v1") {
1377
+ const queryParams = new URLSearchParams();
1378
+ if (params.computeType)
1379
+ queryParams.append("computeType", params.computeType);
1380
+ if (params.gpuTypeId)
1381
+ params.gpuTypeId.forEach(
1382
+ (type) => queryParams.append("gpuTypeId", type)
1383
+ );
1384
+ if (params.dataCenterId)
1385
+ params.dataCenterId.forEach(
1386
+ (dc) => queryParams.append("dataCenterId", dc)
1387
+ );
1388
+ if (params.name) queryParams.append("name", params.name);
1389
+ if (params.includeMachine)
1390
+ queryParams.append(
1391
+ "includeMachine",
1392
+ params.includeMachine.toString()
1393
+ );
1394
+ if (params.includeNetworkVolume)
1395
+ queryParams.append(
1396
+ "includeNetworkVolume",
1397
+ params.includeNetworkVolume.toString()
1398
+ );
1399
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1400
+ }
1401
+ const result = await callRestUrl(
1402
+ `${backend.base}${backend.list}${queryString}`
1403
+ );
1404
+ return capListResult(backend.unwrap(result), {
793
1405
  limit: params.limit,
794
1406
  cursor: params.cursor
795
1407
  });
@@ -797,159 +1409,252 @@ function registerTools(server, ctx) {
797
1409
  );
798
1410
  server.tool(
799
1411
  "get-pod",
1412
+ "Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
800
1413
  {
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")
1414
+ podId: import_zod3.z.string().describe("ID of the pod to retrieve"),
1415
+ includeMachine: import_zod3.z.boolean().optional().describe("Include information about the machine"),
1416
+ includeNetworkVolume: import_zod3.z.boolean().optional().describe("Include information about attached network volumes")
804
1417
  },
1418
+ { title: "Get pod", ...READ_ONLY },
805
1419
  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
- };
1420
+ const backend = backendFor("pods");
1421
+ let queryString = "";
1422
+ if (backend.version === "v1") {
1423
+ const queryParams = new URLSearchParams();
1424
+ if (params.includeMachine)
1425
+ queryParams.append(
1426
+ "includeMachine",
1427
+ params.includeMachine.toString()
1428
+ );
1429
+ if (params.includeNetworkVolume)
1430
+ queryParams.append(
1431
+ "includeNetworkVolume",
1432
+ params.includeNetworkVolume.toString()
1433
+ );
1434
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1435
+ }
1436
+ const result = await callRestUrl(
1437
+ `${backend.base}${backend.get(params.podId)}${queryString}`
1438
+ );
1439
+ return jsonReply2(result);
824
1440
  }
825
1441
  );
826
1442
  server.tool(
827
1443
  "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.',
1444
+ 'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). If the user does not specify an image, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
829
1445
  {
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")
1446
+ name: import_zod3.z.string().optional().describe("Name for the pod"),
1447
+ imageName: import_zod3.z.string().describe("Docker image to use"),
1448
+ computeType: import_zod3.z.enum(["GPU", "CPU"]).optional().describe(
1449
+ "Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
1450
+ ),
1451
+ cloudType: import_zod3.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
1452
+ gpuTypeIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of acceptable GPU types"),
1453
+ gpuCount: import_zod3.z.number().optional().describe("Number of GPUs"),
1454
+ containerDiskInGb: import_zod3.z.number().optional().describe("Container disk size in GB"),
1455
+ volumeInGb: import_zod3.z.number().optional().describe("Volume size in GB"),
1456
+ volumeMountPath: import_zod3.z.string().optional().describe("Path to mount the volume"),
1457
+ ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
1458
+ env: import_zod3.z.record(import_zod3.z.string()).optional().describe("Environment variables"),
1459
+ dataCenterIds: import_zod3.z.array(import_zod3.z.string()).optional().describe("List of data centers")
841
1460
  },
1461
+ { title: "Create pod", ...WRITE },
842
1462
  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)
1463
+ const backend = backendFor("pods");
1464
+ const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
1465
+ const hasGpuCount = (params.gpuCount ?? 0) > 0;
1466
+ const wantsCpu = params.computeType === "CPU";
1467
+ const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
1468
+ if (backend.version === "v2") {
1469
+ if (wantsGpu && wantsCpu) {
1470
+ return jsonReply2({
1471
+ error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
1472
+ status: 400
1473
+ });
1474
+ }
1475
+ if (!wantsGpu && !wantsCpu) {
1476
+ return jsonReply2({
1477
+ error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
1478
+ status: 400
1479
+ });
1480
+ }
1481
+ if (wantsGpu && !hasGpuTypes) {
1482
+ return jsonReply2({
1483
+ error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
1484
+ status: 400
1485
+ });
1486
+ }
1487
+ if (wantsCpu) {
1488
+ try {
1489
+ const result = await callRestUrl(
1490
+ `${restV1Base(env)}/pods`,
1491
+ "POST",
1492
+ params
1493
+ );
1494
+ return jsonReply2({
1495
+ ...result,
1496
+ _servedBy: "v1",
1497
+ _note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
1498
+ });
1499
+ } catch (error) {
1500
+ if (error instanceof HttpError) {
1501
+ return jsonReply2({
1502
+ error: error.message,
1503
+ status: error.status,
1504
+ _servedBy: "v1",
1505
+ _note: "CPU pod create was routed to the v1 API and failed."
1506
+ });
1507
+ }
1508
+ throw error;
849
1509
  }
850
- ]
851
- };
1510
+ }
1511
+ }
1512
+ const body = backend.mapCreate(params);
1513
+ try {
1514
+ const result = await callRestUrl(
1515
+ `${backend.base}${backend.list}`,
1516
+ "POST",
1517
+ body
1518
+ );
1519
+ if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
1520
+ return jsonReply2({
1521
+ ...result,
1522
+ _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(", ")}.`
1523
+ });
1524
+ }
1525
+ return jsonReply2(result);
1526
+ } catch (error) {
1527
+ if (error instanceof HttpError && error.status === 501) {
1528
+ return jsonReply2({
1529
+ error: "CPU pods are not yet supported on the v2 REST API. Use a GPU pod, or create the CPU pod via the v1 API (set RUNPOD_REST_VERSION=v1).",
1530
+ status: 501
1531
+ });
1532
+ }
1533
+ throw error;
1534
+ }
852
1535
  }
853
1536
  );
854
1537
  server.tool(
855
1538
  "update-pod",
1539
+ "Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
856
1540
  {
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")
1541
+ podId: import_zod3.z.string().describe("ID of the pod to update"),
1542
+ name: import_zod3.z.string().optional().describe("New name for the pod"),
1543
+ imageName: import_zod3.z.string().optional().describe("New Docker image"),
1544
+ containerDiskInGb: import_zod3.z.number().optional().describe("New container disk size in GB"),
1545
+ volumeInGb: import_zod3.z.number().optional().describe("New volume size in GB"),
1546
+ volumeMountPath: import_zod3.z.string().optional().describe("New path to mount the volume"),
1547
+ ports: import_zod3.z.array(import_zod3.z.string()).optional().describe("New ports to expose"),
1548
+ env: import_zod3.z.record(import_zod3.z.string()).optional().describe("New environment variables")
865
1549
  },
1550
+ { title: "Update pod", ...WRITE, idempotentHint: true },
866
1551
  async (params) => {
867
1552
  const { podId, ...updateParams } = params;
868
- const result = await runpodRequest(
869
- `/pods/${podId}`,
1553
+ const backend = backendFor("pods");
1554
+ const body = backend.mapUpdate(updateParams);
1555
+ const result = await callRestUrl(
1556
+ `${backend.base}${backend.get(podId)}`,
870
1557
  "PATCH",
871
- updateParams
1558
+ body
872
1559
  );
873
- return {
874
- content: [
875
- {
876
- type: "text",
877
- text: JSON.stringify(result, null, 2)
878
- }
879
- ]
880
- };
1560
+ return jsonReply2(result);
881
1561
  }
882
1562
  );
883
1563
  server.tool(
884
1564
  "start-pod",
1565
+ "Start a stopped pod (resumes the same pod and its billing).",
885
1566
  {
886
- podId: import_zod2.z.string().describe("ID of the pod to start")
1567
+ podId: import_zod3.z.string().describe("ID of the pod to start")
887
1568
  },
1569
+ { title: "Start pod", ...WRITE, idempotentHint: true },
888
1570
  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
- };
1571
+ const result = await podAction(params.podId, "start");
1572
+ return jsonReply2(result);
898
1573
  }
899
1574
  );
900
1575
  server.tool(
901
1576
  "stop-pod",
1577
+ "Stop a running pod. The pod and its disk persist; GPU billing stops.",
902
1578
  {
903
- podId: import_zod2.z.string().describe("ID of the pod to stop")
1579
+ podId: import_zod3.z.string().describe("ID of the pod to stop")
904
1580
  },
1581
+ { title: "Stop pod", ...WRITE, idempotentHint: true },
905
1582
  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
- };
1583
+ const result = await podAction(params.podId, "stop");
1584
+ return jsonReply2(result);
1585
+ }
1586
+ );
1587
+ server.tool(
1588
+ "restart-pod",
1589
+ "Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
1590
+ {
1591
+ podId: import_zod3.z.string().describe("ID of the pod to restart")
1592
+ },
1593
+ { title: "Restart pod", ...WRITE },
1594
+ async (params) => {
1595
+ const backend = backendFor("pods");
1596
+ if (backend.version === "v1") {
1597
+ return jsonReply2({
1598
+ error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
1599
+ status: 501
1600
+ });
1601
+ }
1602
+ const result = await podAction(params.podId, "restart");
1603
+ return jsonReply2(result);
915
1604
  }
916
1605
  );
917
1606
  server.tool(
918
1607
  "delete-pod",
1608
+ "Permanently delete a pod and its data. This cannot be undone.",
919
1609
  {
920
- podId: import_zod2.z.string().describe("ID of the pod to delete")
1610
+ podId: import_zod3.z.string().describe("ID of the pod to delete")
921
1611
  },
1612
+ { title: "Delete pod", ...DESTRUCTIVE },
922
1613
  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
- };
1614
+ const backend = backendFor("pods");
1615
+ const result = await callRestUrl(
1616
+ `${backend.base}${backend.get(params.podId)}`,
1617
+ "DELETE"
1618
+ );
1619
+ return jsonReply2(result);
932
1620
  }
933
1621
  );
1622
+ }
1623
+
1624
+ // src/tools/endpoints.ts
1625
+ var import_zod4 = require("zod");
1626
+ function registerEndpointTools(server, rt) {
1627
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
934
1628
  server.tool(
935
1629
  "list-endpoints",
1630
+ "List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
936
1631
  {
937
1632
  ...listPaginationParams,
938
- includeTemplate: import_zod2.z.boolean().optional().describe("Include template information"),
939
- includeWorkers: import_zod2.z.boolean().optional().describe("Include information about workers")
1633
+ includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
1634
+ includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
940
1635
  },
1636
+ { title: "List endpoints", ...READ_ONLY },
941
1637
  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, {
1638
+ const backend = backendFor("endpoints");
1639
+ let queryString = "";
1640
+ if (backend.version === "v1") {
1641
+ const queryParams = new URLSearchParams();
1642
+ if (params.includeTemplate)
1643
+ queryParams.append(
1644
+ "includeTemplate",
1645
+ params.includeTemplate.toString()
1646
+ );
1647
+ if (params.includeWorkers)
1648
+ queryParams.append(
1649
+ "includeWorkers",
1650
+ params.includeWorkers.toString()
1651
+ );
1652
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1653
+ }
1654
+ const result = await callRestUrl(
1655
+ `${backend.base}${backend.list}${queryString}`
1656
+ );
1657
+ return capListResult(backend.unwrap(result), {
953
1658
  limit: params.limit,
954
1659
  cursor: params.cursor
955
1660
  });
@@ -957,124 +1662,227 @@ function registerTools(server, ctx) {
957
1662
  );
958
1663
  server.tool(
959
1664
  "get-endpoint",
1665
+ "Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
960
1666
  {
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")
1667
+ endpointId: import_zod4.z.string().describe("ID of the endpoint to retrieve"),
1668
+ includeTemplate: import_zod4.z.boolean().optional().describe("Include template information (v1 only)"),
1669
+ includeWorkers: import_zod4.z.boolean().optional().describe("Include information about workers (v1 only)")
964
1670
  },
1671
+ { title: "Get endpoint", ...READ_ONLY },
965
1672
  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}`
1673
+ const backend = backendFor("endpoints");
1674
+ let queryString = "";
1675
+ if (backend.version === "v1") {
1676
+ const queryParams = new URLSearchParams();
1677
+ if (params.includeTemplate)
1678
+ queryParams.append(
1679
+ "includeTemplate",
1680
+ params.includeTemplate.toString()
1681
+ );
1682
+ if (params.includeWorkers)
1683
+ queryParams.append(
1684
+ "includeWorkers",
1685
+ params.includeWorkers.toString()
1686
+ );
1687
+ queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
1688
+ }
1689
+ const result = await callRestUrl(
1690
+ `${backend.base}${backend.get(params.endpointId)}${queryString}`
977
1691
  );
978
- return {
979
- content: [
980
- {
981
- type: "text",
982
- text: JSON.stringify(result, null, 2)
983
- }
984
- ]
985
- };
1692
+ return jsonReply2(result);
986
1693
  }
987
1694
  );
988
1695
  server.tool(
989
1696
  "create-endpoint",
1697
+ "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
1698
  {
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")
1699
+ name: import_zod4.z.string().optional().describe("Name for the endpoint"),
1700
+ // --- v2 inline-config fields ---
1701
+ imageName: import_zod4.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
1702
+ gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe(
1703
+ 'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
1704
+ ),
1705
+ gpuCount: import_zod4.z.number().optional().describe("GPUs per worker (v2)"),
1706
+ args: import_zod4.z.string().optional().describe("Container start command/args (v2)"),
1707
+ containerDiskInGb: import_zod4.z.number().optional().describe("Container disk size in GB (v2)"),
1708
+ ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
1709
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe("Environment variables (v2)"),
1710
+ containerRegistryAuthId: import_zod4.z.string().optional().describe("Container registry auth id for a private image (v2)"),
1711
+ networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("Network volume ids to attach (v2)"),
1712
+ executionTimeoutMs: import_zod4.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
1713
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
1714
+ // --- v1 template-based fields ---
1715
+ templateId: import_zod4.z.string().optional().describe("Template ID (v1). Required on v1."),
1716
+ computeType: import_zod4.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
1717
+ gpuTypeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of acceptable GPU types (v1)"),
1718
+ // --- shared autoscaling fields ---
1719
+ workersMin: import_zod4.z.number().optional().describe("Minimum number of workers"),
1720
+ workersMax: import_zod4.z.number().optional().describe("Maximum number of workers"),
1721
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
1722
+ scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value"),
1723
+ idleTimeout: import_zod4.z.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
1724
+ dataCenterIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("List of preferred data centers")
999
1725
  },
1726
+ { title: "Create endpoint", ...WRITE },
1000
1727
  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
- };
1728
+ const backend = backendFor("endpoints");
1729
+ if (backend.version === "v2") {
1730
+ if (!params.name) {
1731
+ return jsonReply2({
1732
+ error: "On the v2 REST API, create-endpoint requires a name.",
1733
+ status: 400
1734
+ });
1735
+ }
1736
+ if (!params.imageName) {
1737
+ return jsonReply2({
1738
+ 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.",
1739
+ status: 400
1740
+ });
1741
+ }
1742
+ if (!params.gpuPoolIds?.length) {
1743
+ return jsonReply2({
1744
+ error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
1745
+ status: 400
1746
+ });
1747
+ }
1748
+ const body = backend.mapCreate(params);
1749
+ const result2 = await callRestUrl(
1750
+ `${backend.base}${backend.list}`,
1751
+ "POST",
1752
+ body
1753
+ );
1754
+ return jsonReply2(result2);
1755
+ }
1756
+ if (!params.templateId) {
1757
+ return jsonReply2({
1758
+ error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
1759
+ status: 400
1760
+ });
1761
+ }
1762
+ const v1Body = {};
1763
+ for (const k of [
1764
+ "templateId",
1765
+ "name",
1766
+ "computeType",
1767
+ "gpuTypeIds",
1768
+ "gpuCount",
1769
+ "workersMin",
1770
+ "workersMax",
1771
+ "dataCenterIds"
1772
+ ]) {
1773
+ if (params[k] !== void 0) v1Body[k] = params[k];
1774
+ }
1775
+ const result = await callRestUrl(
1776
+ `${backend.base}${backend.list}`,
1777
+ "POST",
1778
+ v1Body
1779
+ );
1780
+ return jsonReply2(result);
1010
1781
  }
1011
1782
  );
1012
1783
  server.tool(
1013
1784
  "update-endpoint",
1785
+ "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
1786
  {
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")
1787
+ endpointId: import_zod4.z.string().describe("ID of the endpoint to update"),
1788
+ name: import_zod4.z.string().optional().describe("New name for the endpoint"),
1789
+ workersMin: import_zod4.z.number().optional().describe("New minimum number of workers"),
1790
+ workersMax: import_zod4.z.number().optional().describe("New maximum number of workers"),
1791
+ idleTimeout: import_zod4.z.number().optional().describe("New idle timeout in seconds"),
1792
+ scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
1793
+ scalerValue: import_zod4.z.number().optional().describe("Scaler value"),
1794
+ // --- v2-only inline-config fields ---
1795
+ imageName: import_zod4.z.string().optional().describe("New Docker image (v2)"),
1796
+ gpuPoolIds: import_zod4.z.array(import_zod4.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
1797
+ gpuCount: import_zod4.z.number().optional().describe("New GPUs per worker (v2)"),
1798
+ args: import_zod4.z.string().optional().describe("New container args (v2)"),
1799
+ containerDiskInGb: import_zod4.z.number().optional().describe("New container disk size in GB (v2)"),
1800
+ ports: import_zod4.z.array(import_zod4.z.string()).optional().describe("New ports (v2)"),
1801
+ env: import_zod4.z.record(import_zod4.z.string()).optional().describe("New environment variables (v2)"),
1802
+ containerRegistryAuthId: import_zod4.z.string().optional().describe("New container registry auth id (v2)"),
1803
+ networkVolumeIds: import_zod4.z.array(import_zod4.z.string()).optional().describe("New network volume ids (v2)"),
1804
+ executionTimeoutMs: import_zod4.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
1805
+ flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
1022
1806
  },
1807
+ { title: "Update endpoint", ...WRITE, idempotentHint: true },
1023
1808
  async (params) => {
1024
1809
  const { endpointId, ...updateParams } = params;
1025
- const result = await runpodRequest(
1026
- `/endpoints/${endpointId}`,
1810
+ const backend = backendFor("endpoints");
1811
+ const body = backend.mapUpdate(updateParams);
1812
+ const result = await callRestUrl(
1813
+ `${backend.base}${backend.get(endpointId)}`,
1027
1814
  "PATCH",
1028
- updateParams
1815
+ body
1029
1816
  );
1030
- return {
1031
- content: [
1032
- {
1033
- type: "text",
1034
- text: JSON.stringify(result, null, 2)
1035
- }
1036
- ]
1037
- };
1817
+ return jsonReply2(result);
1038
1818
  }
1039
1819
  );
1040
1820
  server.tool(
1041
1821
  "delete-endpoint",
1822
+ "Permanently delete a Serverless endpoint. This cannot be undone.",
1042
1823
  {
1043
- endpointId: import_zod2.z.string().describe("ID of the endpoint to delete")
1824
+ endpointId: import_zod4.z.string().describe("ID of the endpoint to delete")
1044
1825
  },
1826
+ { title: "Delete endpoint", ...DESTRUCTIVE },
1045
1827
  async (params) => {
1046
- const result = await runpodRequest(
1047
- `/endpoints/${params.endpointId}`,
1828
+ const backend = backendFor("endpoints");
1829
+ const result = await callRestUrl(
1830
+ `${backend.base}${backend.get(params.endpointId)}`,
1048
1831
  "DELETE"
1049
1832
  );
1050
- return {
1051
- content: [
1052
- {
1053
- type: "text",
1054
- text: JSON.stringify(result, null, 2)
1055
- }
1056
- ]
1057
- };
1833
+ return jsonReply2(result);
1058
1834
  }
1059
1835
  );
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(
1836
+ server.tool(
1837
+ "list-endpoint-workers",
1838
+ "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.",
1839
+ {
1840
+ ...listPaginationParams,
1841
+ endpointId: import_zod4.z.string().describe("ID of the Serverless endpoint whose workers to list")
1842
+ },
1843
+ { title: "List endpoint workers", ...READ_ONLY },
1844
+ async (params) => {
1845
+ const backend = backendFor("workers");
1846
+ if (backend.version === "v1") {
1847
+ return jsonReply2({
1848
+ error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
1849
+ status: 501
1850
+ });
1851
+ }
1852
+ const raw = await callRestUrl(
1853
+ `${backend.base}/serverless/${params.endpointId}/workers`
1854
+ );
1855
+ return capListResult(
1856
+ backend.unwrap(raw),
1857
+ { limit: params.limit, cursor: params.cursor },
1858
+ { summary: raw?.summary, endpointVersion: raw?.endpointVersion }
1859
+ );
1860
+ }
1861
+ );
1862
+ }
1863
+
1864
+ // src/tools/jobs.ts
1865
+ var import_zod5 = require("zod");
1866
+ function registerJobTools(server, rt) {
1867
+ const { jsonReply: jsonReply2, serverlessRequest } = rt;
1868
+ const endpointIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
1869
+ const jobIdSchema = import_zod5.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
1870
+ const inputSchema = import_zod5.z.record(import_zod5.z.unknown()).describe(
1063
1871
  "Input payload for the worker handler. The expected fields depend on the deployed model or worker."
1064
1872
  );
1065
- const webhookSchema = import_zod2.z.string().url().optional().describe(
1873
+ const webhookSchema = import_zod5.z.string().url().optional().describe(
1066
1874
  "Webhook URL to receive job completion notifications instead of polling"
1067
1875
  );
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")
1876
+ const policySchema = import_zod5.z.object({
1877
+ executionTimeout: import_zod5.z.number().optional().describe("Maximum execution time in milliseconds"),
1878
+ lowPriority: import_zod5.z.boolean().optional().describe("Submit as a low-priority job"),
1879
+ ttl: import_zod5.z.number().optional().describe("Time-to-live for the job result in milliseconds")
1072
1880
  }).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")
1881
+ const s3ConfigSchema = import_zod5.z.object({
1882
+ accessId: import_zod5.z.string().describe("S3 access key ID"),
1883
+ accessSecret: import_zod5.z.string().describe("S3 secret access key"),
1884
+ bucketName: import_zod5.z.string().describe("S3 bucket name"),
1885
+ endpointUrl: import_zod5.z.string().describe("S3 endpoint URL")
1078
1886
  }).optional().describe("S3-compatible storage config for large outputs");
1079
1887
  server.tool(
1080
1888
  "run-endpoint",
@@ -1088,6 +1896,7 @@ function registerTools(server, ctx) {
1088
1896
  policy: policySchema,
1089
1897
  s3Config: s3ConfigSchema
1090
1898
  },
1899
+ { title: "Run endpoint (async)", ...WRITE },
1091
1900
  async (params) => {
1092
1901
  const { endpointId, ...body } = params;
1093
1902
  const result = await serverlessRequest(
@@ -1096,14 +1905,7 @@ function registerTools(server, ctx) {
1096
1905
  "POST",
1097
1906
  body
1098
1907
  );
1099
- return {
1100
- content: [
1101
- {
1102
- type: "text",
1103
- text: JSON.stringify(result, null, 2)
1104
- }
1105
- ]
1106
- };
1908
+ return jsonReply2(result);
1107
1909
  }
1108
1910
  );
1109
1911
  server.tool(
@@ -1114,13 +1916,14 @@ function registerTools(server, ctx) {
1114
1916
  "ID of the Serverless endpoint to run synchronously"
1115
1917
  ),
1116
1918
  input: inputSchema,
1117
- wait: import_zod2.z.number().min(1e3).max(3e5).optional().describe(
1919
+ wait: import_zod5.z.number().min(1e3).max(3e5).optional().describe(
1118
1920
  "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
1921
  ),
1120
1922
  webhook: webhookSchema,
1121
1923
  policy: policySchema,
1122
1924
  s3Config: s3ConfigSchema
1123
1925
  },
1926
+ { title: "Run endpoint (sync)", ...WRITE },
1124
1927
  async (params) => {
1125
1928
  const { endpointId, wait, ...body } = params;
1126
1929
  const path2 = wait ? `/runsync?wait=${wait}` : "/runsync";
@@ -1130,14 +1933,7 @@ function registerTools(server, ctx) {
1130
1933
  "POST",
1131
1934
  body
1132
1935
  );
1133
- return {
1134
- content: [
1135
- {
1136
- type: "text",
1137
- text: JSON.stringify(result, null, 2)
1138
- }
1139
- ]
1140
- };
1936
+ return jsonReply2(result);
1141
1937
  }
1142
1938
  );
1143
1939
  server.tool(
@@ -1149,19 +1945,13 @@ function registerTools(server, ctx) {
1149
1945
  ),
1150
1946
  jobId: jobIdSchema.describe("ID of the job to check")
1151
1947
  },
1948
+ { title: "Get job status", ...READ_ONLY },
1152
1949
  async (params) => {
1153
1950
  const result = await serverlessRequest(
1154
1951
  params.endpointId,
1155
1952
  `/status/${params.jobId}`
1156
1953
  );
1157
- return {
1158
- content: [
1159
- {
1160
- type: "text",
1161
- text: JSON.stringify(result, null, 2)
1162
- }
1163
- ]
1164
- };
1954
+ return jsonReply2(result);
1165
1955
  }
1166
1956
  );
1167
1957
  server.tool(
@@ -1173,6 +1963,7 @@ function registerTools(server, ctx) {
1173
1963
  ),
1174
1964
  jobId: jobIdSchema.describe("ID of the job to stream results from")
1175
1965
  },
1966
+ { title: "Stream job", ...READ_ONLY },
1176
1967
  async (params) => {
1177
1968
  const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1178
1969
  "COMPLETED",
@@ -1218,18 +2009,7 @@ function registerTools(server, ctx) {
1218
2009
  }
1219
2010
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1220
2011
  }
1221
- return {
1222
- content: [
1223
- {
1224
- type: "text",
1225
- text: JSON.stringify(
1226
- { ...finalResult, stream: allChunks },
1227
- null,
1228
- 2
1229
- )
1230
- }
1231
- ]
1232
- };
2012
+ return jsonReply2({ ...finalResult, stream: allChunks });
1233
2013
  }
1234
2014
  );
1235
2015
  server.tool(
@@ -1241,20 +2021,14 @@ function registerTools(server, ctx) {
1241
2021
  ),
1242
2022
  jobId: jobIdSchema.describe("ID of the job to cancel")
1243
2023
  },
2024
+ { title: "Cancel job", ...WRITE, idempotentHint: true },
1244
2025
  async (params) => {
1245
2026
  const result = await serverlessRequest(
1246
2027
  params.endpointId,
1247
2028
  `/cancel/${params.jobId}`,
1248
2029
  "POST"
1249
2030
  );
1250
- return {
1251
- content: [
1252
- {
1253
- type: "text",
1254
- text: JSON.stringify(result, null, 2)
1255
- }
1256
- ]
1257
- };
2031
+ return jsonReply2(result);
1258
2032
  }
1259
2033
  );
1260
2034
  server.tool(
@@ -1266,20 +2040,14 @@ function registerTools(server, ctx) {
1266
2040
  ),
1267
2041
  jobId: jobIdSchema.describe("ID of the job to retry")
1268
2042
  },
2043
+ { title: "Retry job", ...WRITE },
1269
2044
  async (params) => {
1270
2045
  const result = await serverlessRequest(
1271
2046
  params.endpointId,
1272
2047
  `/retry/${params.jobId}`,
1273
2048
  "POST"
1274
2049
  );
1275
- return {
1276
- content: [
1277
- {
1278
- type: "text",
1279
- text: JSON.stringify(result, null, 2)
1280
- }
1281
- ]
1282
- };
2050
+ return jsonReply2(result);
1283
2051
  }
1284
2052
  );
1285
2053
  server.tool(
@@ -1290,16 +2058,10 @@ function registerTools(server, ctx) {
1290
2058
  "ID of the Serverless endpoint to check health for"
1291
2059
  )
1292
2060
  },
2061
+ { title: "Endpoint health", ...READ_ONLY },
1293
2062
  async (params) => {
1294
2063
  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
- };
2064
+ return jsonReply2(result);
1303
2065
  }
1304
2066
  );
1305
2067
  server.tool(
@@ -1310,46 +2072,51 @@ function registerTools(server, ctx) {
1310
2072
  "ID of the Serverless endpoint to purge the queue for"
1311
2073
  )
1312
2074
  },
2075
+ { title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
1313
2076
  async (params) => {
1314
2077
  const result = await serverlessRequest(
1315
2078
  params.endpointId,
1316
2079
  "/purge-queue",
1317
2080
  "POST"
1318
2081
  );
1319
- return {
1320
- content: [
1321
- {
1322
- type: "text",
1323
- text: JSON.stringify(result, null, 2)
1324
- }
1325
- ]
1326
- };
2082
+ return jsonReply2(result);
1327
2083
  }
1328
2084
  );
2085
+ }
2086
+
2087
+ // src/tools/templates.ts
2088
+ var import_zod6 = require("zod");
2089
+ function registerTemplateTools(server, rt) {
2090
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1329
2091
  server.tool(
1330
2092
  "list-templates",
1331
2093
  `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
2094
  {
1333
2095
  ...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(
2096
+ includeRunpodTemplates: import_zod6.z.boolean().optional().describe("Include official Runpod templates in the response"),
2097
+ includePublicTemplates: import_zod6.z.boolean().optional().describe("Include community-made public templates in the response"),
2098
+ includeEndpointBoundTemplates: import_zod6.z.boolean().optional().describe(
1337
2099
  "Include templates bound to Serverless endpoints in the response"
1338
2100
  )
1339
2101
  },
2102
+ { title: "List templates", ...READ_ONLY },
1340
2103
  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}` : ""}`
2104
+ const backend = backendFor("templates");
2105
+ let query = "";
2106
+ if (backend.version === "v1") {
2107
+ const queryParams = new URLSearchParams();
2108
+ if (params.includeRunpodTemplates)
2109
+ queryParams.set("includeRunpodTemplates", "true");
2110
+ if (params.includePublicTemplates)
2111
+ queryParams.set("includePublicTemplates", "true");
2112
+ if (params.includeEndpointBoundTemplates)
2113
+ queryParams.set("includeEndpointBoundTemplates", "true");
2114
+ query = queryParams.toString() ? `?${queryParams.toString()}` : "";
2115
+ }
2116
+ const result = await callRestUrl(
2117
+ `${backend.base}${backend.list}${query}`
1351
2118
  );
1352
- return capListResult(result, {
2119
+ return capListResult(backend.unwrap(result), {
1353
2120
  limit: params.limit,
1354
2121
  cursor: params.cursor
1355
2122
  });
@@ -1357,196 +2124,203 @@ function registerTools(server, ctx) {
1357
2124
  );
1358
2125
  server.tool(
1359
2126
  "get-template",
2127
+ "Get one template by id (image, env, ports, disk/volume config).",
1360
2128
  {
1361
- templateId: import_zod2.z.string().describe("ID of the template to retrieve")
2129
+ templateId: import_zod6.z.string().describe("ID of the template to retrieve")
1362
2130
  },
2131
+ { title: "Get template", ...READ_ONLY },
1363
2132
  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
- };
2133
+ const backend = backendFor("templates");
2134
+ const result = await callRestUrl(
2135
+ `${backend.base}${backend.get(params.templateId)}`
2136
+ );
2137
+ return jsonReply2(result);
1373
2138
  }
1374
2139
  );
1375
2140
  server.tool(
1376
2141
  "create-template",
2142
+ "Create a reusable pod/endpoint template (image, ports, env, disk/volume). On v2 a category is required and defaults to NVIDIA.",
1377
2143
  {
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(
2144
+ name: import_zod6.z.string().describe("Name for the template"),
2145
+ imageName: import_zod6.z.string().describe("Docker image to use"),
2146
+ isServerless: import_zod6.z.boolean().optional().describe("Is this a serverless template"),
2147
+ ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("Ports to expose"),
2148
+ dockerEntrypoint: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker entrypoint commands"),
2149
+ dockerStartCmd: import_zod6.z.array(import_zod6.z.string()).optional().describe("Docker start commands"),
2150
+ env: import_zod6.z.record(import_zod6.z.string()).optional().describe("Environment variables"),
2151
+ containerDiskInGb: import_zod6.z.number().optional().describe("Container disk size in GB"),
2152
+ volumeInGb: import_zod6.z.number().optional().describe("Volume size in GB"),
2153
+ volumeMountPath: import_zod6.z.string().optional().describe("Path to mount the volume"),
2154
+ readme: import_zod6.z.string().optional().describe("README content in markdown format"),
2155
+ category: import_zod6.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
2156
+ "Hardware category for the template. On v2 a category is required and defaults to NVIDIA when omitted \u2014 set CPU or AMD for non-NVIDIA templates so they are not mis-categorized."
2157
+ ),
2158
+ containerRegistryAuthId: import_zod6.z.string().optional().describe(
1390
2159
  "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
2160
  )
1392
2161
  },
2162
+ { title: "Create template", ...WRITE },
1393
2163
  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
- };
2164
+ const backend = backendFor("templates");
2165
+ const body = backend.mapCreate(params);
2166
+ const result = await callRestUrl(
2167
+ `${backend.base}${backend.list}`,
2168
+ "POST",
2169
+ body
2170
+ );
2171
+ return jsonReply2(result);
1403
2172
  }
1404
2173
  );
1405
2174
  server.tool(
1406
2175
  "update-template",
2176
+ "Update a template's mutable fields (name, image, ports, env, readme). Only provided fields change.",
1407
2177
  {
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(
2178
+ templateId: import_zod6.z.string().describe("ID of the template to update"),
2179
+ name: import_zod6.z.string().optional().describe("New name for the template"),
2180
+ imageName: import_zod6.z.string().optional().describe("New Docker image"),
2181
+ ports: import_zod6.z.array(import_zod6.z.string()).optional().describe("New ports to expose"),
2182
+ env: import_zod6.z.record(import_zod6.z.string()).optional().describe("New environment variables"),
2183
+ readme: import_zod6.z.string().optional().describe("New README content in markdown format"),
2184
+ containerRegistryAuthId: import_zod6.z.string().optional().describe(
1415
2185
  "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
2186
  )
1417
2187
  },
2188
+ { title: "Update template", ...WRITE, idempotentHint: true },
1418
2189
  async (params) => {
1419
2190
  const { templateId, ...updateParams } = params;
1420
- const result = await runpodRequest(
1421
- `/templates/${templateId}`,
2191
+ const backend = backendFor("templates");
2192
+ const body = backend.mapUpdate(updateParams);
2193
+ const result = await callRestUrl(
2194
+ `${backend.base}${backend.get(templateId)}`,
1422
2195
  "PATCH",
1423
- updateParams
2196
+ body
1424
2197
  );
1425
- return {
1426
- content: [
1427
- {
1428
- type: "text",
1429
- text: JSON.stringify(result, null, 2)
1430
- }
1431
- ]
1432
- };
2198
+ return jsonReply2(result);
1433
2199
  }
1434
2200
  );
1435
2201
  server.tool(
1436
2202
  "delete-template",
2203
+ "Permanently delete a template. This cannot be undone.",
1437
2204
  {
1438
- templateId: import_zod2.z.string().describe("ID of the template to delete")
2205
+ templateId: import_zod6.z.string().describe("ID of the template to delete")
1439
2206
  },
2207
+ { title: "Delete template", ...DESTRUCTIVE },
1440
2208
  async (params) => {
1441
- const result = await runpodRequest(
1442
- `/templates/${params.templateId}`,
2209
+ const backend = backendFor("templates");
2210
+ const result = await callRestUrl(
2211
+ `${backend.base}${backend.get(params.templateId)}`,
1443
2212
  "DELETE"
1444
2213
  );
1445
- return {
1446
- content: [
1447
- {
1448
- type: "text",
1449
- text: JSON.stringify(result, null, 2)
1450
- }
1451
- ]
1452
- };
2214
+ return jsonReply2(result);
2215
+ }
2216
+ );
2217
+ }
2218
+
2219
+ // src/tools/network-volumes.ts
2220
+ var import_zod7 = require("zod");
2221
+ function registerNetworkVolumeTools(server, rt) {
2222
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2223
+ server.tool(
2224
+ "list-network-volumes",
2225
+ "List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
2226
+ listPaginationParams,
2227
+ { title: "List network volumes", ...READ_ONLY },
2228
+ async (params) => {
2229
+ const backend = backendFor("networkVolumes");
2230
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
2231
+ return capListResult(backend.unwrap(result), {
2232
+ limit: params.limit,
2233
+ cursor: params.cursor
2234
+ });
1453
2235
  }
1454
2236
  );
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
2237
  server.tool(
1463
2238
  "get-network-volume",
2239
+ "Get one network volume by id (name, size, data center).",
1464
2240
  {
1465
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to retrieve")
2241
+ networkVolumeId: import_zod7.z.string().describe("ID of the network volume to retrieve")
1466
2242
  },
2243
+ { title: "Get network volume", ...READ_ONLY },
1467
2244
  async (params) => {
1468
- const result = await runpodRequest(
1469
- `/networkvolumes/${params.networkVolumeId}`
2245
+ const backend = backendFor("networkVolumes");
2246
+ const result = await callRestUrl(
2247
+ `${backend.base}${backend.get(params.networkVolumeId)}`
1470
2248
  );
1471
- return {
1472
- content: [
1473
- {
1474
- type: "text",
1475
- text: JSON.stringify(result, null, 2)
1476
- }
1477
- ]
1478
- };
2249
+ return jsonReply2(result);
1479
2250
  }
1480
2251
  );
1481
2252
  server.tool(
1482
2253
  "create-network-volume",
2254
+ "Create a persistent network volume in a data center (size 1\u20134000 GB) that pods can mount.",
1483
2255
  {
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")
2256
+ name: import_zod7.z.string().describe("Name for the network volume"),
2257
+ size: import_zod7.z.number().describe("Size in GB (1-4000)"),
2258
+ dataCenterId: import_zod7.z.string().describe("Data center ID (see list-data-centers)")
1487
2259
  },
2260
+ { title: "Create network volume", ...WRITE },
1488
2261
  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
- };
2262
+ const backend = backendFor("networkVolumes");
2263
+ const body = backend.mapCreate(params);
2264
+ const result = await callRestUrl(
2265
+ `${backend.base}${backend.list}`,
2266
+ "POST",
2267
+ body
2268
+ );
2269
+ return jsonReply2(result);
1498
2270
  }
1499
2271
  );
1500
2272
  server.tool(
1501
2273
  "update-network-volume",
2274
+ "Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
1502
2275
  {
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)")
2276
+ networkVolumeId: import_zod7.z.string().describe("ID of the network volume to update"),
2277
+ name: import_zod7.z.string().optional().describe("New name for the network volume"),
2278
+ size: import_zod7.z.number().optional().describe("New size in GB (must be larger than current)")
1506
2279
  },
2280
+ { title: "Update network volume", ...WRITE, idempotentHint: true },
1507
2281
  async (params) => {
1508
2282
  const { networkVolumeId, ...updateParams } = params;
1509
- const result = await runpodRequest(
1510
- `/networkvolumes/${networkVolumeId}`,
2283
+ const backend = backendFor("networkVolumes");
2284
+ const body = backend.mapUpdate(updateParams);
2285
+ const result = await callRestUrl(
2286
+ `${backend.base}${backend.get(networkVolumeId)}`,
1511
2287
  "PATCH",
1512
- updateParams
2288
+ body
1513
2289
  );
1514
- return {
1515
- content: [
1516
- {
1517
- type: "text",
1518
- text: JSON.stringify(result, null, 2)
1519
- }
1520
- ]
1521
- };
2290
+ return jsonReply2(result);
1522
2291
  }
1523
2292
  );
1524
2293
  server.tool(
1525
2294
  "delete-network-volume",
2295
+ "Permanently delete a network volume and its data. This cannot be undone.",
1526
2296
  {
1527
- networkVolumeId: import_zod2.z.string().describe("ID of the network volume to delete")
2297
+ networkVolumeId: import_zod7.z.string().describe("ID of the network volume to delete")
1528
2298
  },
2299
+ { title: "Delete network volume", ...DESTRUCTIVE },
1529
2300
  async (params) => {
1530
- const result = await runpodRequest(
1531
- `/networkvolumes/${params.networkVolumeId}`,
2301
+ const backend = backendFor("networkVolumes");
2302
+ const result = await callRestUrl(
2303
+ `${backend.base}${backend.get(params.networkVolumeId)}`,
1532
2304
  "DELETE"
1533
2305
  );
1534
- return {
1535
- content: [
1536
- {
1537
- type: "text",
1538
- text: JSON.stringify(result, null, 2)
1539
- }
1540
- ]
1541
- };
2306
+ return jsonReply2(result);
1542
2307
  }
1543
2308
  );
2309
+ }
2310
+
2311
+ // src/tools/registries.ts
2312
+ var import_zod8 = require("zod");
2313
+ function registerRegistryTools(server, rt) {
2314
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
1544
2315
  server.tool(
1545
2316
  "list-container-registry-auths",
2317
+ "List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
1546
2318
  listPaginationParams,
2319
+ { title: "List container registry auths", ...READ_ONLY },
1547
2320
  async (params) => {
1548
- const result = await runpodRequest("/containerregistryauth");
1549
- return capListResult(result, {
2321
+ const backend = backendFor("registries");
2322
+ const result = await callRestUrl(`${backend.base}${backend.list}`);
2323
+ return capListResult(backend.unwrap(result), {
1550
2324
  limit: params.limit,
1551
2325
  cursor: params.cursor
1552
2326
  });
@@ -1554,72 +2328,324 @@ function registerTools(server, ctx) {
1554
2328
  );
1555
2329
  server.tool(
1556
2330
  "get-container-registry-auth",
2331
+ "Get one saved container-registry credential by id (the secret/password is not returned).",
1557
2332
  {
1558
- containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to retrieve")
2333
+ containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to retrieve")
1559
2334
  },
2335
+ { title: "Get container registry auth", ...READ_ONLY },
1560
2336
  async (params) => {
1561
- const result = await runpodRequest(
1562
- `/containerregistryauth/${params.containerRegistryAuthId}`
2337
+ const backend = backendFor("registries");
2338
+ const result = await callRestUrl(
2339
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`
1563
2340
  );
1564
- return {
1565
- content: [
1566
- {
1567
- type: "text",
1568
- text: JSON.stringify(result, null, 2)
1569
- }
1570
- ]
1571
- };
2341
+ return jsonReply2(result);
1572
2342
  }
1573
2343
  );
1574
2344
  server.tool(
1575
2345
  "create-container-registry-auth",
2346
+ "Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
1576
2347
  {
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")
2348
+ name: import_zod8.z.string().describe("Name for the container registry auth"),
2349
+ username: import_zod8.z.string().describe("Registry username"),
2350
+ password: import_zod8.z.string().describe("Registry password or access token (stored as a secret)")
1580
2351
  },
2352
+ { title: "Create container registry auth", ...WRITE },
1581
2353
  async (params) => {
1582
- const result = await runpodRequest(
1583
- "/containerregistryauth",
2354
+ const backend = backendFor("registries");
2355
+ const body = backend.mapCreate(params);
2356
+ const result = await callRestUrl(
2357
+ `${backend.base}${backend.list}`,
1584
2358
  "POST",
1585
- params
2359
+ body
1586
2360
  );
1587
- return {
1588
- content: [
1589
- {
1590
- type: "text",
1591
- text: JSON.stringify(result, null, 2)
1592
- }
1593
- ]
1594
- };
2361
+ return jsonReply2(result);
1595
2362
  }
1596
2363
  );
1597
2364
  server.tool(
1598
2365
  "delete-container-registry-auth",
2366
+ "Permanently delete a saved container-registry credential. This cannot be undone.",
1599
2367
  {
1600
- containerRegistryAuthId: import_zod2.z.string().describe("ID of the container registry auth to delete")
2368
+ containerRegistryAuthId: import_zod8.z.string().describe("ID of the container registry auth to delete")
1601
2369
  },
2370
+ { title: "Delete container registry auth", ...DESTRUCTIVE },
1602
2371
  async (params) => {
1603
- const result = await runpodRequest(
1604
- `/containerregistryauth/${params.containerRegistryAuthId}`,
2372
+ const backend = backendFor("registries");
2373
+ const result = await callRestUrl(
2374
+ `${backend.base}${backend.get(params.containerRegistryAuthId)}`,
1605
2375
  "DELETE"
1606
2376
  );
1607
- return {
1608
- content: [
1609
- {
1610
- type: "text",
1611
- text: JSON.stringify(result, null, 2)
1612
- }
1613
- ]
1614
- };
2377
+ return jsonReply2(result);
2378
+ }
2379
+ );
2380
+ }
2381
+
2382
+ // src/tools/tags.ts
2383
+ var import_zod9 = require("zod");
2384
+ var TAG_RESOURCE_TYPES = [
2385
+ "POD",
2386
+ "NETWORK_VOLUME",
2387
+ "CLUSTER",
2388
+ "SERVERLESS_ENDPOINT"
2389
+ ];
2390
+ var taggedResourceSchema = import_zod9.z.object({
2391
+ id: import_zod9.z.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
2392
+ type: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
2393
+ });
2394
+ function v2OnlyNotice(tool) {
2395
+ return {
2396
+ error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
2397
+ status: 501
2398
+ };
2399
+ }
2400
+ function registerTagTools(server, rt) {
2401
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2402
+ server.tool(
2403
+ "list-tags",
2404
+ "List your tags (key/value labels on pods, volumes, clusters, and endpoints). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
2405
+ {
2406
+ ...listPaginationParams,
2407
+ includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to each tag in the response")
2408
+ },
2409
+ { title: "List tags", ...READ_ONLY },
2410
+ async (params) => {
2411
+ const backend = backendFor("tags");
2412
+ if (backend.version === "v1") return jsonReply2(v2OnlyNotice("list-tags"));
2413
+ const query = params.includeResources ? "?include=resources" : "";
2414
+ const raw = await callRestUrl(`${backend.base}${backend.list}${query}`);
2415
+ return capListResult(backend.unwrap(raw), {
2416
+ limit: params.limit,
2417
+ cursor: params.cursor
2418
+ });
2419
+ }
2420
+ );
2421
+ server.tool(
2422
+ "get-tag",
2423
+ "Get one tag by id, optionally including its attached resources. v2-only \u2014 returns a 501 notice on the v1 API.",
2424
+ {
2425
+ tagId: import_zod9.z.string().describe("ID of the tag to retrieve"),
2426
+ includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to this tag")
2427
+ },
2428
+ { title: "Get tag", ...READ_ONLY },
2429
+ async (params) => {
2430
+ const backend = backendFor("tags");
2431
+ if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
2432
+ const query = params.includeResources ? "?include=resources" : "";
2433
+ const result = await callRestUrl(
2434
+ `${backend.base}${backend.get(params.tagId)}${query}`
2435
+ );
2436
+ return jsonReply2(result);
2437
+ }
2438
+ );
2439
+ server.tool(
2440
+ "create-tag",
2441
+ "Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
2442
+ {
2443
+ key: import_zod9.z.string().min(1).describe('Tag key (e.g. "project")'),
2444
+ value: import_zod9.z.string().min(1).describe('Tag value (e.g. "gpt-training")'),
2445
+ resources: import_zod9.z.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
2446
+ },
2447
+ { title: "Create tag", ...WRITE },
2448
+ async (params) => {
2449
+ const backend = backendFor("tags");
2450
+ if (backend.version === "v1")
2451
+ return jsonReply2(v2OnlyNotice("create-tag"));
2452
+ const result = await callRestUrl(
2453
+ `${backend.base}${backend.list}`,
2454
+ "POST",
2455
+ params
2456
+ );
2457
+ return jsonReply2(result);
2458
+ }
2459
+ );
2460
+ server.tool(
2461
+ "update-tag",
2462
+ "Update a tag (rename its key/value, or replace its resource associations). Provide at least one of key, value, or resources. Sending resources:[] removes all associations; omitting resources leaves them unchanged. Replacing resources is NOT atomic \u2014 a failed update can leave a partially-applied set, so for small association changes prefer attach-tag/detach-tag. v2-only \u2014 returns a 501 notice on the v1 API.",
2463
+ {
2464
+ tagId: import_zod9.z.string().describe("ID of the tag to update"),
2465
+ key: import_zod9.z.string().min(1).optional().describe("New tag key"),
2466
+ value: import_zod9.z.string().min(1).optional().describe("New tag value"),
2467
+ resources: import_zod9.z.array(taggedResourceSchema).optional().describe(
2468
+ "Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
2469
+ )
2470
+ },
2471
+ // No idempotentHint: replacing `resources` is non-atomic upstream, so a
2472
+ // retried-after-partial-failure update is not guaranteed to be a no-op.
2473
+ { title: "Update tag", ...WRITE },
2474
+ async (params) => {
2475
+ const { tagId, ...updateParams } = params;
2476
+ const backend = backendFor("tags");
2477
+ if (backend.version === "v1")
2478
+ return jsonReply2(v2OnlyNotice("update-tag"));
2479
+ const result = await callRestUrl(
2480
+ `${backend.base}${backend.get(tagId)}`,
2481
+ "PATCH",
2482
+ updateParams
2483
+ );
2484
+ return jsonReply2(result);
2485
+ }
2486
+ );
2487
+ server.tool(
2488
+ "delete-tag",
2489
+ "Permanently delete a tag (removes it from all resources it is attached to). This cannot be undone. v2-only \u2014 returns a 501 notice on the v1 API.",
2490
+ {
2491
+ tagId: import_zod9.z.string().describe("ID of the tag to delete")
2492
+ },
2493
+ { title: "Delete tag", ...DESTRUCTIVE },
2494
+ async (params) => {
2495
+ const backend = backendFor("tags");
2496
+ if (backend.version === "v1")
2497
+ return jsonReply2(v2OnlyNotice("delete-tag"));
2498
+ const result = await callRestUrl(
2499
+ `${backend.base}${backend.get(params.tagId)}`,
2500
+ "DELETE"
2501
+ );
2502
+ return jsonReply2(result);
2503
+ }
2504
+ );
2505
+ server.tool(
2506
+ "attach-tag",
2507
+ "Attach a tag to a resource (pod, network volume, cluster, or serverless endpoint). Idempotent \u2014 attaching an already-associated resource is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
2508
+ {
2509
+ tagId: import_zod9.z.string().describe("ID of the tag"),
2510
+ resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
2511
+ resourceId: import_zod9.z.string().describe("ID of the resource to attach the tag to")
2512
+ },
2513
+ { title: "Attach tag to resource", ...WRITE, idempotentHint: true },
2514
+ async (params) => {
2515
+ const backend = backendFor("tags");
2516
+ if (backend.version === "v1")
2517
+ return jsonReply2(v2OnlyNotice("attach-tag"));
2518
+ const result = await callRestUrl(
2519
+ `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2520
+ "PUT"
2521
+ );
2522
+ return jsonReply2(result);
2523
+ }
2524
+ );
2525
+ server.tool(
2526
+ "detach-tag",
2527
+ "Detach a tag from a resource (removes the association only \u2014 the resource and tag are not deleted). Idempotent \u2014 detaching a resource that is not associated is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
2528
+ {
2529
+ tagId: import_zod9.z.string().describe("ID of the tag"),
2530
+ resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
2531
+ resourceId: import_zod9.z.string().describe("ID of the resource to detach the tag from")
2532
+ },
2533
+ { title: "Detach tag from resource", ...WRITE, idempotentHint: true },
2534
+ async (params) => {
2535
+ const backend = backendFor("tags");
2536
+ if (backend.version === "v1")
2537
+ return jsonReply2(v2OnlyNotice("detach-tag"));
2538
+ const result = await callRestUrl(
2539
+ `${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
2540
+ "DELETE"
2541
+ );
2542
+ return jsonReply2(result);
2543
+ }
2544
+ );
2545
+ }
2546
+
2547
+ // src/tools/billing.ts
2548
+ var import_zod10 = require("zod");
2549
+ var BILLING_SCOPE_PATH = {
2550
+ all: "/billing",
2551
+ pods: "/billing/pods",
2552
+ serverless: "/billing/serverless",
2553
+ endpoints: "/billing/endpoints",
2554
+ networkvolumes: "/billing/networkvolumes",
2555
+ clusters: "/billing/clusters"
2556
+ };
2557
+ function registerBillingTools(server, rt) {
2558
+ const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
2559
+ server.tool(
2560
+ "get-billing",
2561
+ "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.",
2562
+ {
2563
+ ...listPaginationParams,
2564
+ scope: import_zod10.z.enum([
2565
+ "all",
2566
+ "pods",
2567
+ "serverless",
2568
+ "endpoints",
2569
+ "networkvolumes",
2570
+ "clusters"
2571
+ ]).optional().describe(
2572
+ 'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
2573
+ ),
2574
+ startTime: import_zod10.z.string().optional().describe(
2575
+ "Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
2576
+ ),
2577
+ endTime: import_zod10.z.string().optional().describe(
2578
+ "End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
2579
+ ),
2580
+ bucketSize: import_zod10.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
2581
+ lastN: import_zod10.z.number().int().positive().optional().describe(
2582
+ "Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
2583
+ )
2584
+ },
2585
+ { title: "Get billing history", ...READ_ONLY },
2586
+ async (params) => {
2587
+ const backend = backendFor("billing");
2588
+ if (backend.version === "v1") {
2589
+ return jsonReply2({
2590
+ error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
2591
+ status: 501
2592
+ });
2593
+ }
2594
+ const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
2595
+ const query = new URLSearchParams();
2596
+ if (params.startTime) query.set("startTime", params.startTime);
2597
+ if (params.endTime) query.set("endTime", params.endTime);
2598
+ if (params.bucketSize) query.set("bucketSize", params.bucketSize);
2599
+ if (params.lastN !== void 0 && !params.startTime && !params.endTime)
2600
+ query.set("lastN", String(params.lastN));
2601
+ const qs = query.toString() ? `?${query.toString()}` : "";
2602
+ const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
2603
+ return capListResult(
2604
+ backend.unwrap(raw),
2605
+ { limit: params.limit, cursor: params.cursor },
2606
+ { metadata: raw?.metadata }
2607
+ );
1615
2608
  }
1616
2609
  );
1617
2610
  }
1618
2611
 
2612
+ // src/tools.ts
2613
+ function registerTools(server, ctx, deps = {}) {
2614
+ const rt = createToolRuntime(server, ctx, deps);
2615
+ registerCatalogTools(server, rt);
2616
+ registerPodTools(server, rt);
2617
+ registerEndpointTools(server, rt);
2618
+ registerJobTools(server, rt);
2619
+ registerTemplateTools(server, rt);
2620
+ registerNetworkVolumeTools(server, rt);
2621
+ registerRegistryTools(server, rt);
2622
+ registerTagTools(server, rt);
2623
+ registerBillingTools(server, rt);
2624
+ }
2625
+
1619
2626
  // src/stdio.ts
1620
2627
  var WIZARD_SUBCOMMANDS = ["add", "remove"];
1621
2628
  var subcommand = process.argv[2];
1622
2629
  var isWizardMode = WIZARD_SUBCOMMANDS.includes(subcommand);
2630
+ async function main(apiKey) {
2631
+ let v2Available;
2632
+ if (wantsAutoProbe(process.env)) {
2633
+ const probe = createV2Prober({
2634
+ // node-fetch's signature is wider than the `{ status }` the prober needs.
2635
+ fetch: import_node_fetch2.default,
2636
+ baseUrl: restV2Base(process.env),
2637
+ apiKey,
2638
+ // Surface a transient/timeout fallback to v1 — otherwise `auto` silently
2639
+ // pins the session to v1 with no signal (stderr is the MCP log channel).
2640
+ warn: (message) => console.error(`[runpod-mcp] ${message}`)
2641
+ });
2642
+ v2Available = await probe();
2643
+ }
2644
+ const server = createServer();
2645
+ registerTools(server, { apiKey, transport: "stdio" }, { v2Available });
2646
+ const transport = new import_stdio.StdioServerTransport();
2647
+ await server.connect(transport);
2648
+ }
1623
2649
  if (isWizardMode) {
1624
2650
  Promise.resolve().then(() => (init_wizard(), wizard_exports)).then(({ runWizard: runWizard2 }) => runWizard2(subcommand)).catch((error) => {
1625
2651
  console.error(error instanceof Error ? error.message : String(error));
@@ -1631,9 +2657,9 @@ if (isWizardMode) {
1631
2657
  console.error("RUNPOD_API_KEY environment variable is required");
1632
2658
  process.exit(1);
1633
2659
  }
1634
- const server = createServer();
1635
- registerTools(server, { apiKey: API_KEY, transport: "stdio" });
1636
- const transport = new import_stdio.StdioServerTransport();
1637
- server.connect(transport);
2660
+ main(API_KEY).catch((error) => {
2661
+ console.error("Failed to start Runpod MCP server:", error);
2662
+ process.exit(1);
2663
+ });
1638
2664
  }
1639
2665
  //# sourceMappingURL=stdio.cjs.map