@runpod/mcp-server 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.mjs CHANGED
@@ -1,5 +1,10 @@
1
1
  // src/http.ts
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import {
4
+ CallToolRequestSchema as CallToolRequestSchema2,
5
+ InitializeRequestSchema,
6
+ JSONRPCMessageSchema
7
+ } from "@modelcontextprotocol/sdk/types.js";
3
8
 
4
9
  // src/specgen/server.ts
5
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -87,11 +92,11 @@ function readParams(params) {
87
92
  }
88
93
  return { values, unreadable };
89
94
  }
90
- function parseExhaustedWindows(header) {
91
- if (!header) return { windows: [], dropped: false };
95
+ function parseExhaustedWindows(header2) {
96
+ if (!header2) return { windows: [], dropped: false };
92
97
  const windows = [];
93
98
  let dropped = false;
94
- for (const item of splitOutsideStrings(header, ",")) {
99
+ for (const item of splitOutsideStrings(header2, ",")) {
95
100
  if (item.trim() === "") continue;
96
101
  const [rawName, ...params] = splitOutsideStrings(item, ";");
97
102
  if (!params.every(isWellFormedParam)) {
@@ -209,10 +214,13 @@ function validateArguments(inputSchema, args) {
209
214
  async function dispatchGeneratedTool(client, tool, args) {
210
215
  const rejected = validateArguments(tool.inputSchema, args);
211
216
  if (rejected) return rejected;
217
+ const required = new Set(tool.inputSchema.required ?? []);
212
218
  const pathParams = {};
213
219
  const query = {};
214
220
  for (const param of tool.params) {
215
221
  if (args[param.name] === void 0) continue;
222
+ if (param.location === "query" && !required.has(param.name) && isEmptyValue(args[param.name]))
223
+ continue;
216
224
  if (param.location === "path") {
217
225
  pathParams[param.name] = args[param.name];
218
226
  continue;
@@ -234,6 +242,11 @@ async function dispatchGeneratedTool(client, tool, args) {
234
242
  };
235
243
  }
236
244
  }
245
+ if (tool.hasBody && requestBody !== void 0) {
246
+ const properties = tool.inputSchema.properties ?? {};
247
+ const defs = tool.inputSchema.$defs ?? {};
248
+ requestBody = coerceScalars(requestBody, properties.body, defs);
249
+ }
237
250
  const verbs = client;
238
251
  const { data, error, response } = await verbs[tool.method](tool.path, {
239
252
  params: { path: pathParams, query },
@@ -246,6 +259,79 @@ async function dispatchGeneratedTool(client, tool, args) {
246
259
  payload: data ?? { status: response.status }
247
260
  };
248
261
  }
262
+ function isEmptyValue(value) {
263
+ return value === null || value === "" || Array.isArray(value) && !value.length;
264
+ }
265
+ function asSchema(value) {
266
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
267
+ }
268
+ function resolveSchema(schema, defs, depth = 0) {
269
+ const node = asSchema(schema);
270
+ if (!node || depth > 16) return node;
271
+ const ref = node.$ref;
272
+ if (typeof ref === "string" && ref.startsWith("#/$defs/"))
273
+ return resolveSchema(defs[ref.slice("#/$defs/".length)], defs, depth + 1);
274
+ return node;
275
+ }
276
+ function branches(schema, defs, depth = 0) {
277
+ const node = resolveSchema(schema, defs);
278
+ if (!node || depth > 16) return [];
279
+ const out = [node];
280
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
281
+ const list = node[key];
282
+ if (Array.isArray(list))
283
+ for (const sub of list) out.push(...branches(sub, defs, depth + 1));
284
+ }
285
+ return out;
286
+ }
287
+ function types(schemas) {
288
+ const out = /* @__PURE__ */ new Set();
289
+ for (const s of schemas) {
290
+ if (typeof s.type === "string") out.add(s.type);
291
+ else if (Array.isArray(s.type)) {
292
+ for (const t of s.type) if (typeof t === "string") out.add(t);
293
+ }
294
+ }
295
+ return out;
296
+ }
297
+ var NUMERIC = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
298
+ function coerceScalars(value, schema, defs, depth = 0) {
299
+ if (depth > 32) return value;
300
+ const applicable = branches(schema, defs);
301
+ if (!applicable.length) return value;
302
+ const allowed = types(applicable);
303
+ if (typeof value === "string") {
304
+ if (allowed.has("string")) return value;
305
+ if ((allowed.has("integer") || allowed.has("number")) && NUMERIC.test(value)) {
306
+ const n = Number(value);
307
+ if (allowed.has("number") || Number.isInteger(n)) return n;
308
+ }
309
+ if (allowed.has("boolean") && (value === "true" || value === "false"))
310
+ return value === "true";
311
+ return value;
312
+ }
313
+ if (Array.isArray(value)) {
314
+ const items = applicable.map((s) => s.items).filter(Boolean);
315
+ if (!items.length) return value;
316
+ return value.map(
317
+ (item) => coerceScalars(item, { anyOf: items }, defs, depth + 1)
318
+ );
319
+ }
320
+ const object = asSchema(value);
321
+ if (!object) return value;
322
+ const out = {};
323
+ for (const [key, child] of Object.entries(object)) {
324
+ const childSchemas = [];
325
+ for (const s of applicable) {
326
+ const props = asSchema(s.properties);
327
+ if (props && key in props) childSchemas.push(props[key]);
328
+ else if (asSchema(s.additionalProperties))
329
+ childSchemas.push(s.additionalProperties);
330
+ }
331
+ out[key] = childSchemas.length ? coerceScalars(child, { anyOf: childSchemas }, defs, depth + 1) : child;
332
+ }
333
+ return out;
334
+ }
249
335
 
250
336
  // src/specgen/generated/tools.gen.ts
251
337
  var generatedTools = [
@@ -263,6 +349,7 @@ var generatedTools = [
263
349
  idempotentHint: false,
264
350
  openWorldHint: true
265
351
  },
352
+ schemaOnDemand: true,
266
353
  inputSchema: {
267
354
  type: "object",
268
355
  properties: {
@@ -274,11 +361,10 @@ var generatedTools = [
274
361
  $defs: {
275
362
  BaseContainerConfig: {
276
363
  type: "object",
277
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
278
364
  properties: {
279
365
  args: {
280
366
  type: "string",
281
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
367
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
282
368
  examples: [
283
369
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
284
370
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -290,7 +376,7 @@ var generatedTools = [
290
376
  items: {
291
377
  type: "string"
292
378
  },
293
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
379
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
294
380
  examples: [
295
381
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
296
382
  ]
@@ -300,7 +386,7 @@ var generatedTools = [
300
386
  items: {
301
387
  type: "string"
302
388
  },
303
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
389
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
304
390
  examples: [["/bin/bash", "-c"]]
305
391
  },
306
392
  disk: {
@@ -336,6 +422,21 @@ var generatedTools = [
336
422
  }
337
423
  }
338
424
  },
425
+ BaseMounts: {
426
+ type: "object",
427
+ properties: {
428
+ persistent: {
429
+ $ref: "#/$defs/PersistentMount"
430
+ },
431
+ network: {
432
+ type: "array",
433
+ maxItems: 1,
434
+ items: {
435
+ $ref: "#/$defs/NetworkMount"
436
+ }
437
+ }
438
+ }
439
+ },
339
440
  ClusterCompute: {
340
441
  type: "object",
341
442
  additionalProperties: false,
@@ -363,6 +464,15 @@ var generatedTools = [
363
464
  }
364
465
  }
365
466
  },
467
+ ClusterMounts: {
468
+ description: "Storage mounts for a cluster create: `persistent` or `network`, not both. Clusters cannot mount global volumes, so `global` is rejected.",
469
+ allOf: [
470
+ {
471
+ $ref: "#/$defs/BaseMounts"
472
+ }
473
+ ],
474
+ unevaluatedProperties: false
475
+ },
366
476
  ClusterType: {
367
477
  type: "string",
368
478
  description: "Cluster type. TRAINING is the generic distributed-training cluster; SLURM provisions a managed Slurm controller/compute topology; RAY provisions a managed Ray head/worker topology; APPLICATION is a general multi-node application cluster.",
@@ -377,7 +487,7 @@ var generatedTools = [
377
487
  {
378
488
  type: "object",
379
489
  required: ["name", "type", "compute"],
380
- description: "Request body for creating a cluster. `compute` defines the\nhomogeneous pod shape; the container configuration (image, env, ports,\n\u2026) applies to every pod. Pass `templateId` to provision every member\npod from a pod template instead of inline container fields \u2014 the\ntemplate also supplies the container registry credential, the only\nprivate-image path for clusters (a bare `registry` property on this\nbody is rejected).\n",
490
+ description: "Request body for creating a cluster. `compute` defines the homogeneous pod shape; the container configuration (image, env, ports, \u2026) applies to every pod.",
381
491
  properties: {
382
492
  compute: {
383
493
  $ref: "#/$defs/ClusterCompute"
@@ -393,7 +503,7 @@ var generatedTools = [
393
503
  templateId: {
394
504
  type: "string",
395
505
  minLength: 1,
396
- description: "ID of a pod template to provision every member pod from. The\ntemplate supplies the container settings (image, args, disk,\nenv, ports) and the container registry credential for private\nimages \u2014 the only private-image path for clusters. Mutually\nexclusive with `image`, `args`, `entrypoint`, `cmd`, `disk`,\n`env`, `ports`, and `mounts` (rejected with 400). The cluster\nretains the link: the `template` response field is set. Must be\na non-serverless pod template accessible to the caller.\n",
506
+ description: "ID of a pod template to provision every member pod from: it supplies the container settings (image, args, disk, env, ports) and the registry credential for private images \u2014 the only private-image path for clusters. Mutually exclusive with the inline container fields (`image`, `args`, `entrypoint`, `cmd`, `disk`, `env`, `ports`, `mounts`). Must be a non-serverless template you can access.",
397
507
  examples: ["30zmvf89kd"]
398
508
  },
399
509
  dataCenterIds: {
@@ -401,11 +511,11 @@ var generatedTools = [
401
511
  items: {
402
512
  type: "string"
403
513
  },
404
- description: "Preferred data centers for placement. Omit or pass an empty\narray to let the scheduler choose. A cluster is always placed\nwithin a single data center.\n",
514
+ description: "Preferred data centers for placement. Omit or pass an empty array to let the scheduler choose. A cluster is always placed within a single data center.",
405
515
  example: ["US-TX-3"]
406
516
  },
407
517
  mounts: {
408
- $ref: "#/$defs/Mounts"
518
+ $ref: "#/$defs/ClusterMounts"
409
519
  },
410
520
  startJupyter: {
411
521
  type: "boolean",
@@ -422,28 +532,11 @@ var generatedTools = [
422
532
  ],
423
533
  unevaluatedProperties: false
424
534
  },
425
- Mounts: {
426
- type: "object",
427
- additionalProperties: false,
428
- description: "Storage mounts attached to a pod. At-most-one of `persistent` or\n`network` may be set today (mutually exclusive, enforced at the\nhandler with 400 if both are present). The `network` field is an\narray for forward compatibility with eventual multi-network-volume\nsupport, but `maxItems` is 1 today.\n\nPATCH semantics:\n- Omitting `mounts` or sending `{}` leaves the existing mount\n unchanged.\n- An explicit `network: []` is rejected with 400 (clearing mounts\n is not supported).\n- Mount kind is fixed at create \u2014 a PATCH that introduces a kind\n not present at create (persistent on a network pod, network on\n a persistent pod, or any mount on a previously-mountless pod)\n is rejected with 400.\n- The `volumeId` of a network mount is immutable; a PATCH that\n names a different `volumeId` is rejected with 400.\n- Partial mounts are not supported \u2014 every mount entry must\n include the full schema (`size` + `path` for persistent,\n `volumeId` + `path` for network). Missing required fields \u2192 422.\n",
429
- properties: {
430
- persistent: {
431
- $ref: "#/$defs/PersistentMount"
432
- },
433
- network: {
434
- type: "array",
435
- maxItems: 1,
436
- items: {
437
- $ref: "#/$defs/NetworkMount"
438
- }
439
- }
440
- }
441
- },
442
535
  NetworkMount: {
443
536
  type: "object",
444
537
  required: ["volumeId", "path"],
445
538
  additionalProperties: false,
446
- description: "Reference to a NetworkVolume. Custom paths are honored at runtime on\nboth GPU and CPU pods. The underlying `volumeId` is immutable\npost-create; the mount `path` may be changed via PATCH.\n",
539
+ description: "Reference to a NetworkVolume. Custom paths are honored at runtime on both GPU and CPU pods. The underlying `volumeId` is immutable post-create; the mount `path` may be changed via PATCH.",
447
540
  properties: {
448
541
  volumeId: {
449
542
  type: "string",
@@ -461,7 +554,7 @@ var generatedTools = [
461
554
  type: "object",
462
555
  required: ["size", "path"],
463
556
  additionalProperties: false,
464
- description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data\ndoes not survive a host failure. Disallowed on CPU pods. Mutually\nexclusive with NetworkMount. Deprecated: prefer NetworkMount for any\ndata you cannot recreate.\n",
557
+ description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data does not survive a host failure. Disallowed on CPU pods. Mutually exclusive with NetworkMount. Deprecated: prefer NetworkMount for any data you cannot recreate.",
465
558
  properties: {
466
559
  size: {
467
560
  type: "integer",
@@ -538,6 +631,7 @@ var generatedTools = [
538
631
  idempotentHint: false,
539
632
  openWorldHint: true
540
633
  },
634
+ schemaOnDemand: true,
541
635
  inputSchema: {
542
636
  type: "object",
543
637
  properties: {
@@ -549,11 +643,10 @@ var generatedTools = [
549
643
  $defs: {
550
644
  BaseContainerConfig: {
551
645
  type: "object",
552
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
553
646
  properties: {
554
647
  args: {
555
648
  type: "string",
556
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
649
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
557
650
  examples: [
558
651
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
559
652
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -565,7 +658,7 @@ var generatedTools = [
565
658
  items: {
566
659
  type: "string"
567
660
  },
568
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
661
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
569
662
  examples: [
570
663
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
571
664
  ]
@@ -575,7 +668,7 @@ var generatedTools = [
575
668
  items: {
576
669
  type: "string"
577
670
  },
578
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
671
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
579
672
  examples: [["/bin/bash", "-c"]]
580
673
  },
581
674
  disk: {
@@ -635,7 +728,7 @@ var generatedTools = [
635
728
  pools: {
636
729
  type: "array",
637
730
  minItems: 1,
638
- description: 'Serverless GPU pool IDs (as returned by `GET /v2/catalog/gpus` in\n`pool`). Workers are placed on whichever listed pool has capacity.\nNarrow a pool down to specific cards with `excludedTypes`.\n\nOn `PATCH`, `pools` and `excludedTypes` are one selection and are\nreplaced together, so sending `pools` by itself **clears the\nexclusions**. Two cases:\n\n- **Changing pools, keeping exclusions** \u2014 send both fields in one\n request: `{"gpu": {"pools": ["ADA_24"], "excludedTypes":\n ["NVIDIA L40"]}}`. `GET` the endpoint first to read the current\n `excludedTypes` and resend the ones that still apply to the new\n pools; an exclusion naming a type outside `pools` is a 400.\n- **Changing only `count` or a CUDA constraint** \u2014 omit `pools`:\n `{"gpu": {"minCudaVersion": "12.4"}}`. The pool list and the\n exclusions are both left exactly as they are.\n\n`excludedTypes` documents the full rule.\n',
731
+ description: "Serverless GPU pool IDs (the `pool` field of `list-gpu-types`). Workers are placed on whichever listed pool has capacity; narrow to specific cards with `excludedTypes`. On update, `pools` and `excludedTypes` are replaced together, so sending `pools` alone clears the exclusions.",
639
732
  items: {
640
733
  type: "string"
641
734
  },
@@ -644,7 +737,7 @@ var generatedTools = [
644
737
  excludedTypes: {
645
738
  type: "array",
646
739
  uniqueItems: true,
647
- description: 'GPU **type** IDs to subtract from the selected pools \u2014 the `id`\nfield of `GET /v2/catalog/gpus`, the same identifiers pods take in\n`gpu.id`. Workers run on every type in `pools` except these. Omit to\nuse the whole pool.\n\nPools stay the unit of selection; types are the unit of\nsubtraction. There is no inclusive allowlist: a card later added to\none of your pools becomes eligible, which is the honest reading of\n"this pool, minus these".\n\nTied to `pools`, because the two together are one selection:\nsupplying `pools` replaces that selection wholesale, so a `PATCH`\nsending `pools` **without `excludedTypes`** **clears** them \u2014\nrestate them to keep them. A `PATCH` that omits `pools` leaves both\nthe pools and the exclusions untouched, so changing only a CUDA\nconstraint cannot widen a pinned endpoint.\n\nRejected with 400 if a value is not a GPU type in one of `pools`;\nupstream accepts unrecognized exclusions silently, so a typo would\notherwise produce a filter that does nothing. Surrounding whitespace\nis trimmed, so `" NVIDIA L40"` and `"NVIDIA L40"` mean the same card.\n',
740
+ description: "GPU type IDs to subtract from the selected pools \u2014 the `id` field of `list-gpu-types`, the same identifiers pods take in `gpu.id`. Workers run on every type in `pools` except these; omit to use the whole pool. A value outside `pools` is rejected. On update, restate them whenever you send `pools`, or they are cleared.",
648
741
  items: {
649
742
  type: "string",
650
743
  pattern: "^\\s*[^-\\s]"
@@ -661,7 +754,6 @@ var generatedTools = [
661
754
  }
662
755
  },
663
756
  ContainerConfig: {
664
- description: "Reusable container configuration shared across templates, pods, and serverless endpoints. Adding a field here automatically propagates to all three resources.\n",
665
757
  allOf: [
666
758
  {
667
759
  $ref: "#/$defs/BaseContainerConfig"
@@ -687,7 +779,7 @@ var generatedTools = [
687
779
  unevaluatedProperties: false
688
780
  },
689
781
  CreateEndpointGpuConfig: {
690
- description: "GPU request for an endpoint create. Carries the CUDA constraints, which\nlive here rather than at the body's top level so they are\nunrepresentable on a CPU endpoint.\n",
782
+ description: "GPU request for an endpoint create. Carries the CUDA constraints, which live here rather than at the body's top level so they are unrepresentable on a CPU endpoint.",
691
783
  allOf: [
692
784
  {
693
785
  $ref: "#/$defs/BaseEndpointGpuConfig"
@@ -702,13 +794,13 @@ var generatedTools = [
702
794
  type: "string",
703
795
  pattern: "^\\d+\\.\\d+$"
704
796
  },
705
- description: "Acceptable CUDA versions for worker placement, as\n`major.minor`. Omit to accept any version (or inherit the\ntemplate's constraint when creating from `templateId`).\nMatching is exact \u2014 discover valid values per GPU type via\n`GET /v2/catalog/gpus?include=AVAILABILITY&product=SERVERLESS`\n(`cudaVersions`).\n\nA non-empty set is mutually exclusive with minCudaVersion (400\nif both are sent). An explicit `[]` states no constraint, so it\nmay accompany a floor.\n",
797
+ description: "Acceptable CUDA versions for worker placement, as `major.minor`. Omit to accept any version (or inherit the template's constraint when creating from `templateId`).",
706
798
  examples: [["12.8", "12.6"]]
707
799
  },
708
800
  minCudaVersion: {
709
801
  type: "string",
710
802
  pattern: "^\\d+\\.\\d+$",
711
- description: "Lowest acceptable CUDA version for worker placement, as\n`major.minor`, compared numerically rather than as a decimal \u2014\nso 12.11 is above 12.2. Use this for an open-ended floor and\nallowedCudaVersions for an exact set.\n\nMutually exclusive with a non-empty allowedCudaVersions (400 if\nboth are sent); an explicit `[]` there states no constraint and\nmay accompany this floor.\n",
803
+ description: "Lowest acceptable CUDA version for worker placement, as `major.minor`, compared numerically rather than as a decimal \u2014 so 12.11 is above 12.2. Use this for an open-ended floor and allowedCudaVersions for an exact set.",
712
804
  examples: ["12.1"]
713
805
  }
714
806
  }
@@ -752,13 +844,13 @@ var generatedTools = [
752
844
  $ref: "#/$defs/EndpointType"
753
845
  }
754
846
  ],
755
- description: "Request-routing model. Required \u2014 it determines the valid scaler\nand request URLs, so it must be chosen explicitly on every create.\n"
847
+ description: "Request-routing model. Required \u2014 it determines the valid scaler and request URLs, so it must be chosen explicitly on every create."
756
848
  },
757
849
  cpu: {
758
850
  type: "array",
759
851
  minItems: 1,
760
852
  uniqueItems: true,
761
- description: "Eligible CPU configurations for each worker. Memory is derived from the\nselected flavor's catalog RAM multiplier. Exact duplicate configurations\nare rejected; the same flavor may be listed at different vCPU counts.\n",
853
+ description: "Eligible CPU configurations for each worker. Memory is derived from the selected flavor's catalog RAM multiplier. Exact duplicate configurations are rejected; the same flavor may be listed at different vCPU counts.",
762
854
  items: {
763
855
  $ref: "#/$defs/CreateCpuConfig"
764
856
  }
@@ -782,12 +874,17 @@ var generatedTools = [
782
874
  type: "array",
783
875
  items: {
784
876
  type: "string"
785
- }
877
+ },
878
+ description: "Network volume IDs.",
879
+ deprecated: true
880
+ },
881
+ mounts: {
882
+ $ref: "#/$defs/EndpointMountsInput"
786
883
  },
787
884
  templateId: {
788
885
  type: "string",
789
886
  minLength: 1,
790
- description: "ID of a serverless template to base this endpoint on. The\ntemplate is resolved at create time into the same container\nsettings you could otherwise spread into this body (image,\nargs, disk, ports, env, registry); explicit body fields\noverride the template's, except `env`, which is merged per\nkey with body values winning. The template's\nallowedCudaVersions seeds `gpu.allowedCudaVersions` when the\nbody omits it \u2014 but only for a GPU create, since a CPU endpoint\nhas no gpu block to seed into, and not when the body sets\n`gpu.minCudaVersion`, since seeding a set beside a floor would\nmanufacture the mutual-exclusion 400 from a valid request. Its\npod-specific startSsh/startJupyter flags are\nignored. Later template edits do not affect the endpoint.\nThe template may be one of your own or a public catalog\ntemplate \u2014 see `GET /v2/catalog/templates` (unknown or\ninaccessible ID \u2192 404) \u2014 and must be a serverless template\n(\u2192 422).\n",
887
+ description: "ID of a serverless template to base this endpoint on (must be serverless). Its container settings (image, args, disk, ports, env, registry) fill in what this body omits; body fields win, and `env` merges per key with body winning. Its allowedCudaVersions seeds `gpu.allowedCudaVersions` when the body omits it; pod-only flags are ignored. Copied once: later edits do not apply.",
791
888
  examples: ["30zmvf89kd"]
792
889
  },
793
890
  timeout: {
@@ -835,8 +932,42 @@ var generatedTools = [
835
932
  }
836
933
  }
837
934
  },
935
+ EndpointMount: {
936
+ type: "object",
937
+ required: ["volumeId"],
938
+ additionalProperties: false,
939
+ description: "A volume mounted on every worker, always at `/runpod-volume`. Custom paths are not supported yet.",
940
+ properties: {
941
+ volumeId: {
942
+ type: "string",
943
+ description: "ID of a NetworkVolume (under `network`) or GlobalVolume (under `global`).",
944
+ examples: ["vol_abc"]
945
+ }
946
+ }
947
+ },
948
+ EndpointMountsInput: {
949
+ type: "object",
950
+ additionalProperties: false,
951
+ description: "Volumes to mount on the endpoint's workers. A `network` or `global` list that is present replaces the endpoint's current list of that kind; an empty list detaches it, and an omitted list is left unchanged.",
952
+ properties: {
953
+ network: {
954
+ type: "array",
955
+ description: "Network volumes, at most one per data center.",
956
+ items: {
957
+ $ref: "#/$defs/EndpointMount"
958
+ }
959
+ },
960
+ global: {
961
+ type: "array",
962
+ maxItems: 1,
963
+ items: {
964
+ $ref: "#/$defs/EndpointMount"
965
+ }
966
+ }
967
+ }
968
+ },
838
969
  EndpointScaling: {
839
- description: "Autoscaling signal \u2014 a discriminated union on `type`: `QUEUE_DELAY`\n(queue-based endpoints only) or `REQUEST_COUNT`. The scaler is chosen\nindependently of the endpoint's routing `type` and can be switched on\nupdate.\n",
970
+ description: "Autoscaling signal \u2014 a discriminated union on `type`: `QUEUE_DELAY` (queue-based endpoints only) or `REQUEST_COUNT`. The scaler is chosen independently of the endpoint's routing `type` and can be switched on update.",
840
971
  oneOf: [
841
972
  {
842
973
  $ref: "#/$defs/QueueDelayScaling"
@@ -855,7 +986,7 @@ var generatedTools = [
855
986
  },
856
987
  EndpointType: {
857
988
  type: "string",
858
- description: "Request-routing semantics for a modern serverless endpoint.\n- `QUEUE` \u2014 submit asynchronous or synchronous jobs through the managed queue.\n- `LOAD_BALANCER` \u2014 send requests directly to worker-defined HTTP paths.\n Configure via `env`: `PORT` (server port, default 80), `PORT_HEALTH`\n (health-check port, default 80), and `HEALTH_CHECK_PATH` (path the\n load balancer polls for worker health, default `/ping`).\n",
989
+ description: "Request-routing semantics for a modern serverless endpoint. `QUEUE` \u2014 submit asynchronous or synchronous jobs through the managed queue. `LOAD_BALANCER` \u2014 send requests directly to worker-defined HTTP paths.",
859
990
  "x-enum-varnames": ["EndpointTypeQueue", "EndpointTypeLoadBalancer"],
860
991
  enum: ["QUEUE", "LOAD_BALANCER"]
861
992
  },
@@ -879,14 +1010,14 @@ var generatedTools = [
879
1010
  type: "integer",
880
1011
  minimum: 1,
881
1012
  maximum: 3600,
882
- description: "Seconds before idle workers scale down. Not applicable to queue-based\nendpoints scaling on `requestCount` \u2014 rejected on create/update and\nomitted from responses for that combination.\n",
1013
+ description: "Seconds before idle workers scale down. Not applicable to queue-based endpoints scaling on `requestCount` \u2014 rejected on create/update and omitted from responses for that combination.",
883
1014
  examples: [5]
884
1015
  }
885
1016
  }
886
1017
  },
887
1018
  FlashBoot: {
888
1019
  type: "string",
889
- description: "FlashBoot cold-start acceleration mode.\n- `OFF` \u2014 disabled\n- `FLASHBOOT` \u2014 enabled\n- `PRIORITY_FLASHBOOT` \u2014 enabled with priority capacity\n",
1020
+ description: "FlashBoot cold-start acceleration mode. `OFF` \u2014 disabled. `FLASHBOOT` \u2014 enabled. `PRIORITY_FLASHBOOT` \u2014 enabled with priority capacity",
890
1021
  enum: ["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]
891
1022
  },
892
1023
  QueueDelayScaling: {
@@ -912,7 +1043,7 @@ var generatedTools = [
912
1043
  RequestCountScaling: {
913
1044
  type: "object",
914
1045
  additionalProperties: false,
915
- description: "Scale on concurrent in-flight requests per worker. Required for\nload-balancing endpoints; also selectable for queue-based.\n",
1046
+ description: "Scale on concurrent in-flight requests per worker. Required for load-balancing endpoints; also selectable for queue-based.",
916
1047
  required: ["type", "requestCount"],
917
1048
  properties: {
918
1049
  type: {
@@ -931,6 +1062,47 @@ var generatedTools = [
931
1062
  }
932
1063
  }
933
1064
  },
1065
+ {
1066
+ name: "create-global-volume",
1067
+ operationId: "createGlobalVolume",
1068
+ description: "Create a global volume. Provisions a new global volume \u2014 storage that mounts into workloads in any data center. Only a `name` is required; storage is provisioned for you. Accounts have a cap on how many global volumes they can hold; creating one past it fails with `400`. Returns `201` with the created global volume, including its assigned `id`.",
1069
+ method: "POST",
1070
+ path: "/v2/global-volumes",
1071
+ params: [],
1072
+ hasBody: true,
1073
+ annotations: {
1074
+ readOnlyHint: false,
1075
+ destructiveHint: false,
1076
+ idempotentHint: false,
1077
+ openWorldHint: true
1078
+ },
1079
+ inputSchema: {
1080
+ type: "object",
1081
+ properties: {
1082
+ body: {
1083
+ $ref: "#/$defs/CreateGlobalVolumeRequest"
1084
+ }
1085
+ },
1086
+ required: ["body"],
1087
+ $defs: {
1088
+ CreateGlobalVolumeRequest: {
1089
+ type: "object",
1090
+ additionalProperties: false,
1091
+ required: ["name"],
1092
+ properties: {
1093
+ name: {
1094
+ type: "string",
1095
+ minLength: 1,
1096
+ maxLength: 191,
1097
+ pattern: "[^\\p{Z}\\t-\\r\x85]",
1098
+ description: "Human-readable name, 1\u2013191 characters with at least one non-whitespace character. Leading and trailing whitespace is trimmed.",
1099
+ examples: ["training-data"]
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ },
934
1106
  {
935
1107
  name: "create-network-volume",
936
1108
  operationId: "createNetworkVolume",
@@ -984,7 +1156,7 @@ var generatedTools = [
984
1156
  $ref: "#/$defs/VolumeType"
985
1157
  }
986
1158
  ],
987
- description: "Storage tier for the volume. Optional. When omitted, the volume is\nprovisioned using the requested data center's default (primary)\nstorage tier. HIGH_PERFORMANCE provisions a high-performance (HPS)\nvolume; STANDARD provisions a standard volume. A volume's tier is\nimmutable after creation.\n"
1159
+ description: "Storage tier for the volume. Optional. When omitted, the volume is provisioned using the requested data center's default (primary) storage tier."
988
1160
  }
989
1161
  }
990
1162
  },
@@ -1010,6 +1182,7 @@ var generatedTools = [
1010
1182
  idempotentHint: false,
1011
1183
  openWorldHint: true
1012
1184
  },
1185
+ schemaOnDemand: true,
1013
1186
  inputSchema: {
1014
1187
  type: "object",
1015
1188
  properties: {
@@ -1021,11 +1194,10 @@ var generatedTools = [
1021
1194
  $defs: {
1022
1195
  BaseContainerConfig: {
1023
1196
  type: "object",
1024
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
1025
1197
  properties: {
1026
1198
  args: {
1027
1199
  type: "string",
1028
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
1200
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
1029
1201
  examples: [
1030
1202
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
1031
1203
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -1037,7 +1209,7 @@ var generatedTools = [
1037
1209
  items: {
1038
1210
  type: "string"
1039
1211
  },
1040
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
1212
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
1041
1213
  examples: [
1042
1214
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
1043
1215
  ]
@@ -1047,7 +1219,7 @@ var generatedTools = [
1047
1219
  items: {
1048
1220
  type: "string"
1049
1221
  },
1050
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
1222
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
1051
1223
  examples: [["/bin/bash", "-c"]]
1052
1224
  },
1053
1225
  disk: {
@@ -1119,13 +1291,27 @@ var generatedTools = [
1119
1291
  }
1120
1292
  }
1121
1293
  },
1294
+ BaseMounts: {
1295
+ type: "object",
1296
+ properties: {
1297
+ persistent: {
1298
+ $ref: "#/$defs/PersistentMount"
1299
+ },
1300
+ network: {
1301
+ type: "array",
1302
+ maxItems: 1,
1303
+ items: {
1304
+ $ref: "#/$defs/NetworkMount"
1305
+ }
1306
+ }
1307
+ }
1308
+ },
1122
1309
  Cloud: {
1123
1310
  type: "string",
1124
- description: "Cloud tier.\n- `SECURE` \u2014 Runpod-owned datacenter hardware\n- `COMMUNITY` \u2014 community-hosted hardware\n",
1311
+ description: "Cloud tier. `SECURE` \u2014 Runpod-owned datacenter hardware. `COMMUNITY` \u2014 community-hosted hardware",
1125
1312
  enum: ["SECURE", "COMMUNITY"]
1126
1313
  },
1127
1314
  ContainerConfig: {
1128
- description: "Reusable container configuration shared across templates, pods, and serverless endpoints. Adding a field here automatically propagates to all three resources.\n",
1129
1315
  allOf: [
1130
1316
  {
1131
1317
  $ref: "#/$defs/BaseContainerConfig"
@@ -1151,7 +1337,7 @@ var generatedTools = [
1151
1337
  unevaluatedProperties: false
1152
1338
  },
1153
1339
  CreateGpuConfig: {
1154
- description: "GPU request for a pod create. Carries the CUDA host constraints, which\nlive here rather than at the body's top level so they are\nunrepresentable on a CPU pod.\n",
1340
+ description: "GPU request for a pod create. Carries the CUDA host constraints, which live here rather than at the body's top level so they are unrepresentable on a CPU pod.",
1155
1341
  allOf: [
1156
1342
  {
1157
1343
  $ref: "#/$defs/BaseGpuConfig"
@@ -1165,13 +1351,13 @@ var generatedTools = [
1165
1351
  type: "string",
1166
1352
  pattern: "^\\d+\\.\\d+$"
1167
1353
  },
1168
- description: "Acceptable CUDA versions for the host machine, as `major.minor`.\nOmit to accept any version. Matching is exact, so a version no\nmachine reports yields a capacity error rather than a fallback \u2014\ndiscover valid values per GPU type via\n`GET /v2/catalog/gpus?include=AVAILABILITY&product=POD`\n(`cudaVersions`).\n\nA non-empty set is mutually exclusive with minCudaVersion (400\nif both are sent). An explicit `[]` states no constraint, so it\nmay accompany a floor.\n",
1354
+ description: "Acceptable CUDA versions for the host machine, as `major.minor`. Omit to accept any version.",
1169
1355
  examples: [["12.8", "12.6"]]
1170
1356
  },
1171
1357
  minCudaVersion: {
1172
1358
  type: "string",
1173
1359
  pattern: "^\\d+\\.\\d+$",
1174
- description: "Lowest acceptable CUDA version for the host machine, as\n`major.minor`, compared numerically rather than as a decimal \u2014\nso 12.11 is above 12.2. Use this for an open-ended floor and\nallowedCudaVersions for an exact set.\n\nMutually exclusive with a non-empty allowedCudaVersions (400 if\nboth are sent); an explicit `[]` there states no constraint and\nmay accompany this floor.\n",
1360
+ description: "Lowest acceptable CUDA version for the host machine, as `major.minor`, compared numerically rather than as a decimal \u2014 so 12.11 is above 12.2. Use this for an open-ended floor and allowedCudaVersions for an exact set.",
1175
1361
  examples: ["12.1"]
1176
1362
  },
1177
1363
  minRamPerGpu: {
@@ -1199,7 +1385,7 @@ var generatedTools = [
1199
1385
  {
1200
1386
  type: "object",
1201
1387
  required: ["name"],
1202
- description: "Request body for creating a pod. Exactly one of `gpu` or `cpu`\nmust be set \u2014 enforced at the handler layer. For CPU pods, memory\nis derived by the API from the selected flavor's RAM multiplier;\nclients provide only CPU flavor and vCPU count. CPU pods support\ncontainer disk and network volumes only; `mounts.persistent` is\ninvalid when `cpu` is set.\n\n`image` is required unless `templateId` is set.\n",
1388
+ description: "Request body for creating a pod. Exactly one of `gpu` or `cpu` must be set \u2014 enforced at the handler layer.",
1203
1389
  properties: {
1204
1390
  name: {
1205
1391
  type: "string",
@@ -1223,13 +1409,13 @@ var generatedTools = [
1223
1409
  items: {
1224
1410
  type: "string"
1225
1411
  },
1226
- description: "Preferred data centers for placement. Omit or pass an empty\narray to let the scheduler choose.\n",
1412
+ description: "Preferred data centers for placement. Omit or pass an empty array to let the scheduler choose.",
1227
1413
  examples: [["US-TX-3"]]
1228
1414
  },
1229
1415
  globalNetworking: {
1230
1416
  type: "boolean",
1231
1417
  default: false,
1232
- description: "Enable global networking, giving the pod a private IP reachable across data centers. Requires an NVIDIA GPU and a global-networking-enabled data center (both enforced upstream). See `GET /v2/catalog/datacenters` (`globalNetwork`) for eligible data centers.",
1418
+ description: "Enable global networking, giving the pod a private IP reachable across data centers. Requires an NVIDIA GPU and a global-networking-enabled data center (both enforced upstream).",
1233
1419
  examples: [false]
1234
1420
  },
1235
1421
  gpu: {
@@ -1241,19 +1427,19 @@ var generatedTools = [
1241
1427
  startJupyter: {
1242
1428
  type: "boolean",
1243
1429
  default: false,
1244
- description: "Create-time flag telling the provisioner to start JupyterLab:\ninjects a generated `JUPYTER_PASSWORD` environment variable,\nunless the request already sets one. Only images that honor\nthe convention start Jupyter from it (Runpod official images\ndo); expose `8888/http` in `ports` to reach it.\n\nNot part of the pod's readable config \u2014 never returned by\nGET and not changeable by PATCH.\n",
1430
+ description: "Create-time flag: starts JupyterLab by injecting a generated `JUPYTER_PASSWORD` environment variable, unless the request sets one. Expose `8888/http` in `ports` to reach it.",
1245
1431
  examples: [true]
1246
1432
  },
1247
1433
  startSsh: {
1248
1434
  type: "boolean",
1249
1435
  default: false,
1250
- description: "Create-time flag telling the provisioner to set up SSH\naccess: injects a `PUBLIC_KEY` environment variable carrying\nyour account's registered SSH public keys, unless the request\nalready sets one. **Requires registered keys** (`PUT\n/v2/account/ssh-keys`) \u2014 with none registered the flag does\nnothing and the pod has no SSH access. Only images that honor\nthe convention start sshd from it (all Runpod official images\ndo). Connect using the pod's `ssh` block; the `ssh.direct`\nvariant additionally needs a `22/tcp` entry in `ports`.\n\nNot part of the pod's readable config \u2014 never returned by\nGET and not changeable by PATCH.\n",
1436
+ description: "Create-time flag: sets up SSH access by injecting a `PUBLIC_KEY` environment variable with your account's registered SSH keys (`update-ssh-keys`), unless the request sets one. With no registered keys it does nothing and the pod has no SSH access. Direct SSH also needs a `22/tcp` entry in `ports`.",
1251
1437
  examples: [true]
1252
1438
  },
1253
1439
  templateId: {
1254
1440
  type: "string",
1255
1441
  minLength: 1,
1256
- description: "ID of a pod template to base this pod on. The template is\nresolved at create time into the same container settings you\ncould otherwise spread into this body (image, args, disk,\nports, env, registry, persistent mount, startSsh,\nstartJupyter, allowedCudaVersions); explicit body fields\noverride the template's, except `env`, which is merged per\nkey with body values winning. Sending either CUDA field\n(`gpu.allowedCudaVersions` or `gpu.minCudaVersion`) replaces\nthe template's CUDA constraint entirely, and CPU pods ignore\nit (like the persistent mount). The template is a one-time\nsource of settings: later template edits do not affect the\npod, and the created pod does not retain a link to the\ntemplate (`template` stays null). The template may be one\nof your own or a public catalog template \u2014 see\n`GET /v2/catalog/templates` (unknown or inaccessible ID \u2192\n404) \u2014 and must not be a serverless template (\u2192 422). CPU\npods do not inherit a template's persistent mount.\n",
1442
+ description: "ID of a pod template to base this pod on (not a serverless one). Its container settings (image, args, disk, ports, env, registry, persistent mount) fill in what this body omits; body fields win, and `env` merges per key with body winning. A CUDA field in `gpu` replaces the template's CUDA constraint; CPU pods ignore its mount. Copied once: later template edits do not affect the pod.",
1257
1443
  examples: ["30zmvf89kd"]
1258
1444
  }
1259
1445
  }
@@ -1269,28 +1455,51 @@ var generatedTools = [
1269
1455
  required: ["image"]
1270
1456
  }
1271
1457
  },
1272
- Mounts: {
1458
+ GlobalMount: {
1273
1459
  type: "object",
1460
+ required: ["volumeId"],
1274
1461
  additionalProperties: false,
1275
- description: "Storage mounts attached to a pod. At-most-one of `persistent` or\n`network` may be set today (mutually exclusive, enforced at the\nhandler with 400 if both are present). The `network` field is an\narray for forward compatibility with eventual multi-network-volume\nsupport, but `maxItems` is 1 today.\n\nPATCH semantics:\n- Omitting `mounts` or sending `{}` leaves the existing mount\n unchanged.\n- An explicit `network: []` is rejected with 400 (clearing mounts\n is not supported).\n- Mount kind is fixed at create \u2014 a PATCH that introduces a kind\n not present at create (persistent on a network pod, network on\n a persistent pod, or any mount on a previously-mountless pod)\n is rejected with 400.\n- The `volumeId` of a network mount is immutable; a PATCH that\n names a different `volumeId` is rejected with 400.\n- Partial mounts are not supported \u2014 every mount entry must\n include the full schema (`size` + `path` for persistent,\n `volumeId` + `path` for network). Missing required fields \u2192 422.\n",
1462
+ description: "Reference to a GlobalVolume. Usage-billed, so it has no size. The underlying `volumeId` is immutable post-create; the mount `path` may be changed via PATCH.",
1276
1463
  properties: {
1277
- persistent: {
1278
- $ref: "#/$defs/PersistentMount"
1464
+ volumeId: {
1465
+ type: "string",
1466
+ description: "ID of an existing GlobalVolume.",
1467
+ examples: ["gsb_abc"]
1279
1468
  },
1280
- network: {
1281
- type: "array",
1282
- maxItems: 1,
1283
- items: {
1284
- $ref: "#/$defs/NetworkMount"
1285
- }
1469
+ path: {
1470
+ type: "string",
1471
+ description: "Mount path inside the container. Optional on create, where a default is chosen for you; a PATCH that omits it leaves the path unchanged.",
1472
+ examples: ["/data"]
1286
1473
  }
1287
1474
  }
1288
1475
  },
1476
+ Mounts: {
1477
+ description: "Storage mounts for the pod: `persistent` (size + path) excludes both `network` and `global`; `network` (volumeId + path) and `global` (volumeId, optional path) take one entry each and can be combined. Every entry needs its full shape. On update, omit the field to leave the mounts as they are; the mount kind and a volume's volumeId cannot change, and mounts cannot be cleared.",
1478
+ allOf: [
1479
+ {
1480
+ $ref: "#/$defs/BaseMounts"
1481
+ },
1482
+ {
1483
+ type: "object",
1484
+ properties: {
1485
+ global: {
1486
+ type: "array",
1487
+ maxItems: 1,
1488
+ description: "At most one global volume, on GPU and CPU pods, with no data center constraint. Combinable with `network` but not with `persistent` (rejected): upstream drops a persistent disk whenever a volume is attached.",
1489
+ items: {
1490
+ $ref: "#/$defs/GlobalMount"
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+ ],
1496
+ unevaluatedProperties: false
1497
+ },
1289
1498
  NetworkMount: {
1290
1499
  type: "object",
1291
1500
  required: ["volumeId", "path"],
1292
1501
  additionalProperties: false,
1293
- description: "Reference to a NetworkVolume. Custom paths are honored at runtime on\nboth GPU and CPU pods. The underlying `volumeId` is immutable\npost-create; the mount `path` may be changed via PATCH.\n",
1502
+ description: "Reference to a NetworkVolume. Custom paths are honored at runtime on both GPU and CPU pods. The underlying `volumeId` is immutable post-create; the mount `path` may be changed via PATCH.",
1294
1503
  properties: {
1295
1504
  volumeId: {
1296
1505
  type: "string",
@@ -1308,7 +1517,7 @@ var generatedTools = [
1308
1517
  type: "object",
1309
1518
  required: ["size", "path"],
1310
1519
  additionalProperties: false,
1311
- description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data\ndoes not survive a host failure. Disallowed on CPU pods. Mutually\nexclusive with NetworkMount. Deprecated: prefer NetworkMount for any\ndata you cannot recreate.\n",
1520
+ description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data does not survive a host failure. Disallowed on CPU pods. Mutually exclusive with NetworkMount. Deprecated: prefer NetworkMount for any data you cannot recreate.",
1312
1521
  properties: {
1313
1522
  size: {
1314
1523
  type: "integer",
@@ -1391,6 +1600,7 @@ var generatedTools = [
1391
1600
  idempotentHint: false,
1392
1601
  openWorldHint: true
1393
1602
  },
1603
+ schemaOnDemand: true,
1394
1604
  inputSchema: {
1395
1605
  type: "object",
1396
1606
  properties: {
@@ -1410,7 +1620,7 @@ var generatedTools = [
1410
1620
  minLength: 1,
1411
1621
  maxLength: 191,
1412
1622
  pattern: "^[a-zA-Z_][a-zA-Z0-9_.\\-/]*$",
1413
- description: "Unique name for the secret \u2014 referenced from environment variables as `{{ RUNPOD_SECRET_<name> }}`; immutable after creation. Maximum 191 characters, must start with a letter or underscore, and may contain letters, digits, and `_.-/`. Names beginning with the reserved prefix `RUNPOD` are rejected (case-insensitive).",
1623
+ description: "Unique name for the secret \u2014 referenced from environment variables as `{{ RUNPOD_SECRET_<name> }}`; immutable after creation. Maximum 191 characters, must start with a letter or underscore, and may contain letters, digits, and `_.-/`.",
1414
1624
  examples: ["hf-token"]
1415
1625
  },
1416
1626
  value: {
@@ -1444,6 +1654,7 @@ var generatedTools = [
1444
1654
  idempotentHint: false,
1445
1655
  openWorldHint: true
1446
1656
  },
1657
+ schemaOnDemand: true,
1447
1658
  inputSchema: {
1448
1659
  type: "object",
1449
1660
  properties: {
@@ -1455,11 +1666,10 @@ var generatedTools = [
1455
1666
  $defs: {
1456
1667
  BaseContainerConfig: {
1457
1668
  type: "object",
1458
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
1459
1669
  properties: {
1460
1670
  args: {
1461
1671
  type: "string",
1462
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
1672
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
1463
1673
  examples: [
1464
1674
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
1465
1675
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -1471,7 +1681,7 @@ var generatedTools = [
1471
1681
  items: {
1472
1682
  type: "string"
1473
1683
  },
1474
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
1684
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
1475
1685
  examples: [
1476
1686
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
1477
1687
  ]
@@ -1481,7 +1691,7 @@ var generatedTools = [
1481
1691
  items: {
1482
1692
  type: "string"
1483
1693
  },
1484
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
1694
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
1485
1695
  examples: [["/bin/bash", "-c"]]
1486
1696
  },
1487
1697
  disk: {
@@ -1518,7 +1728,6 @@ var generatedTools = [
1518
1728
  }
1519
1729
  },
1520
1730
  ContainerConfig: {
1521
- description: "Reusable container configuration shared across templates, pods, and serverless endpoints. Adding a field here automatically propagates to all three resources.\n",
1522
1731
  allOf: [
1523
1732
  {
1524
1733
  $ref: "#/$defs/BaseContainerConfig"
@@ -1555,7 +1764,7 @@ var generatedTools = [
1555
1764
  type: "string",
1556
1765
  pattern: "^\\d+\\.\\d+$"
1557
1766
  },
1558
- description: "Acceptable CUDA versions for containers created from this\ntemplate, as `major.minor`. Omit to accept any version \u2014 see\nthe same field on `createPod` for matching semantics.\nExpanded into GPU pod and serverless endpoint creates; CPU\npods ignore it.\n",
1767
+ description: "Acceptable CUDA versions for containers created from this template, as `major.minor`. Omit to accept any version \u2014 see the same field on `createPod` for matching semantics.",
1559
1768
  examples: [["12.8", "12.6"]]
1560
1769
  },
1561
1770
  category: {
@@ -1581,13 +1790,13 @@ var generatedTools = [
1581
1790
  startJupyter: {
1582
1791
  type: "boolean",
1583
1792
  default: true,
1584
- description: "Start JupyterLab in containers created from this template:\ninjects a generated `JUPYTER_PASSWORD` environment variable,\nunless `env` already sets one. Only images that honor the\nconvention start Jupyter from it (Runpod official images do);\nexpose `8888/http` in `ports` to reach it. Defaults to `true`\nwhen omitted, matching console-created templates.\n",
1793
+ description: "Start JupyterLab in containers created from this template: injects a generated `JUPYTER_PASSWORD` environment variable, unless `env` already sets one.",
1585
1794
  examples: [false]
1586
1795
  },
1587
1796
  startSsh: {
1588
1797
  type: "boolean",
1589
1798
  default: true,
1590
- description: "Provision SSH access in containers created from this template:\ninjects a `PUBLIC_KEY` environment variable carrying the\ndeployer's registered SSH public keys (`PUT\n/v2/account/ssh-keys` \u2014 with none registered the flag does\nnothing), unless `env` already sets one. Only images that\nhonor the convention start sshd from it (all Runpod official\nimages do); direct SSH also needs a `22/tcp` entry in\n`ports`. Defaults to `true` when omitted, matching\nconsole-created templates.\n",
1799
+ description: "Provision SSH access in containers created from this template: injects a `PUBLIC_KEY` environment variable carrying the deployer's registered SSH public keys (`update-ssh-keys` \u2014 with none registered the flag does nothing), unless `env` already sets one.",
1591
1800
  examples: [true]
1592
1801
  }
1593
1802
  }
@@ -1599,7 +1808,7 @@ var generatedTools = [
1599
1808
  type: "object",
1600
1809
  required: ["size", "path"],
1601
1810
  additionalProperties: false,
1602
- description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data\ndoes not survive a host failure. Disallowed on CPU pods. Mutually\nexclusive with NetworkMount. Deprecated: prefer NetworkMount for any\ndata you cannot recreate.\n",
1811
+ description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data does not survive a host failure. Disallowed on CPU pods. Mutually exclusive with NetworkMount. Deprecated: prefer NetworkMount for any data you cannot recreate.",
1603
1812
  properties: {
1604
1813
  size: {
1605
1814
  type: "integer",
@@ -1616,13 +1825,13 @@ var generatedTools = [
1616
1825
  },
1617
1826
  TemplateCategory: {
1618
1827
  type: "string",
1619
- description: "Controls how the template is grouped and filtered in the Runpod console.\nIt does not affect hardware selection, scheduling, or billing.\n- `CPU` \u2014 CPU-only workloads\n- `NVIDIA` \u2014 NVIDIA GPU workloads\n- `AMD` \u2014 AMD GPU workloads\n",
1828
+ description: "Controls how the template is grouped and filtered in the Runpod console. It does not affect hardware selection, scheduling, or billing. `CPU` \u2014 CPU-only workloads. `NVIDIA` \u2014 NVIDIA GPU workloads. `AMD` \u2014 AMD GPU workloads",
1620
1829
  enum: ["CPU", "NVIDIA", "AMD"]
1621
1830
  },
1622
1831
  TemplateMounts: {
1623
1832
  type: "object",
1624
1833
  additionalProperties: false,
1625
- description: "Storage mounts attached to a template. Templates support only a\nsingle persistent mount today; any `network` property is rejected\nwith 422 by the schema validator.\n\nPATCH semantics: omitting `mounts` or sending `{}` leaves the\nexisting mount unchanged.\n",
1834
+ description: "Storage mounts for containers from this template: a single `persistent` mount only (`network` is rejected). On update, omitting `mounts` or sending `{}` leaves the existing mount unchanged.",
1626
1835
  properties: {
1627
1836
  persistent: {
1628
1837
  $ref: "#/$defs/PersistentMount"
@@ -1692,6 +1901,36 @@ var generatedTools = [
1692
1901
  required: ["id"]
1693
1902
  }
1694
1903
  },
1904
+ {
1905
+ name: "delete-global-volume",
1906
+ operationId: "deleteGlobalVolume",
1907
+ description: "Delete a global volume. Permanently deletes a global volume and its stored data. Fails with `409` while the volume is still mounted: terminate every pod that mounts it (stopped pods count) and detach it from every serverless endpoint first. Nothing is detached or terminated on your behalf.",
1908
+ method: "DELETE",
1909
+ path: "/v2/global-volumes/{id}",
1910
+ params: [
1911
+ {
1912
+ name: "id",
1913
+ location: "path"
1914
+ }
1915
+ ],
1916
+ hasBody: false,
1917
+ annotations: {
1918
+ readOnlyHint: false,
1919
+ destructiveHint: true,
1920
+ idempotentHint: true,
1921
+ openWorldHint: true
1922
+ },
1923
+ inputSchema: {
1924
+ type: "object",
1925
+ properties: {
1926
+ id: {
1927
+ type: "string",
1928
+ description: "Global volume identifier"
1929
+ }
1930
+ },
1931
+ required: ["id"]
1932
+ }
1933
+ },
1695
1934
  {
1696
1935
  name: "delete-network-volume",
1697
1936
  operationId: "deleteNetworkVolume",
@@ -2057,12 +2296,42 @@ var generatedTools = [
2057
2296
  },
2058
2297
  buildId: {
2059
2298
  type: "string",
2060
- description: "GitHub build identifier (from GET /v2/serverless/{id}/builds or a release's `buildId`)"
2299
+ description: "GitHub build identifier (from `list-endpoint-builds` or a release's `buildId`)"
2061
2300
  }
2062
2301
  },
2063
2302
  required: ["id", "buildId"]
2064
2303
  }
2065
2304
  },
2305
+ {
2306
+ name: "get-global-volume",
2307
+ operationId: "getGlobalVolume",
2308
+ description: "Get a global volume. Returns a single global volume by ID.",
2309
+ method: "GET",
2310
+ path: "/v2/global-volumes/{id}",
2311
+ params: [
2312
+ {
2313
+ name: "id",
2314
+ location: "path"
2315
+ }
2316
+ ],
2317
+ hasBody: false,
2318
+ annotations: {
2319
+ readOnlyHint: true,
2320
+ destructiveHint: false,
2321
+ idempotentHint: true,
2322
+ openWorldHint: true
2323
+ },
2324
+ inputSchema: {
2325
+ type: "object",
2326
+ properties: {
2327
+ id: {
2328
+ type: "string",
2329
+ description: "Global volume identifier"
2330
+ }
2331
+ },
2332
+ required: ["id"]
2333
+ }
2334
+ },
2066
2335
  {
2067
2336
  name: "get-gpu-type",
2068
2337
  operationId: "getGpuType",
@@ -2133,7 +2402,7 @@ var generatedTools = [
2133
2402
  items: {
2134
2403
  $ref: "#/$defs/Product"
2135
2404
  },
2136
- description: "Comma-separated availability product contexts. Supported values: POD, CLUSTER, SERVERLESS. Required with include=AVAILABILITY, and valid only with it (400 either way). There is no default: the same GPU type can be scarce for pods and plentiful for serverless, so the context has to be stated rather than assumed."
2405
+ description: "Comma-separated availability product contexts. Supported values: POD, CLUSTER, SERVERLESS. Required with include=AVAILABILITY, and valid only with it (400 either way)."
2137
2406
  },
2138
2407
  count: {
2139
2408
  type: "integer",
@@ -2151,7 +2420,7 @@ var generatedTools = [
2151
2420
  type: "string",
2152
2421
  pattern: "^[A-Z]{2}$"
2153
2422
  },
2154
- description: "Comma-separated ISO 3166-1 alpha-2 country codes, uppercase, to constrain availability to \u2014 e.g. FR or FR,DE. Values within this filter use OR semantics. Valid only with include=AVAILABILITY (400 otherwise); a malformed entry is a 422. Scopes availability, lowest-price calculations and the dataCenters array to those countries, so a listed data center outside them is omitted rather than returned with availability NONE. On the list endpoint a GPU type with no data center in those countries drops out entirely; the single-GPU endpoint still returns the requested type, with availability NONE and dataCenters omitted, so a 404 keeps meaning the GPU type does not exist. Read the NONE on availability rather than the absence of dataCenters, which is also absent when availability was not requested."
2423
+ description: "Comma-separated ISO 3166-1 alpha-2 country codes, uppercase, to constrain availability to \u2014 e.g. FR or FR,DE. Values within this filter use OR semantics. Valid only with include=AVAILABILITY; a malformed entry is a 422."
2155
2424
  },
2156
2425
  cudaVersions: {
2157
2426
  type: "array",
@@ -2159,12 +2428,12 @@ var generatedTools = [
2159
2428
  type: "string",
2160
2429
  pattern: "^\\d+\\.\\d+$"
2161
2430
  },
2162
- description: "Comma-separated CUDA versions to scope availability and lowest-price calculations to, matched exactly. Format: major.minor, e.g. 12.8 \u2014 a bare major is rejected here because it identifies no version. Valid only with include=AVAILABILITY (400 otherwise) and mutually exclusive with minCudaVersion (400 if both are sent); a malformed entry is a 422. Also narrows the returned cudaVersions array; omit it to enumerate every version offered."
2431
+ description: "Comma-separated CUDA versions to scope availability and lowest-price calculations to, matched exactly. Format: major.minor, e.g. 12.8 \u2014 a bare major is rejected here because it identifies no version."
2163
2432
  },
2164
2433
  minCudaVersion: {
2165
2434
  type: "string",
2166
2435
  pattern: "^\\d+(\\.\\d+)?$",
2167
- description: "Lowest acceptable CUDA version to scope availability and lowest-price calculations to, compared numerically. Format: integer major or major.minor, e.g. 12 or 12.1 \u2014 unlike the `gpu.minCudaVersion` body field on pod and endpoint create, a bare major is accepted here and means any release of that major, because this filter only widens a read. Valid only with include=AVAILABILITY (400 otherwise) and mutually exclusive with cudaVersions (400 if both are sent); a malformed value is a 422. Use this for an open-ended floor and cudaVersions for an exact set."
2436
+ description: "Lowest acceptable CUDA version to scope availability and lowest-price calculations to, compared numerically."
2168
2437
  }
2169
2438
  },
2170
2439
  required: ["id"],
@@ -2217,6 +2486,25 @@ var generatedTools = [
2217
2486
  required: ["id"]
2218
2487
  }
2219
2488
  },
2489
+ {
2490
+ name: "get-organization",
2491
+ operationId: "getOrganization",
2492
+ description: "Get your current organization. Returns the authenticated caller's current organization. Organization context comes from the caller's membership; no organization selector is required. Returns 404 when the caller does not belong to an organization, or 403 when organizations are unavailable to the caller.",
2493
+ method: "GET",
2494
+ path: "/v2/organization",
2495
+ params: [],
2496
+ hasBody: false,
2497
+ annotations: {
2498
+ readOnlyHint: true,
2499
+ destructiveHint: false,
2500
+ idempotentHint: true,
2501
+ openWorldHint: true
2502
+ },
2503
+ inputSchema: {
2504
+ type: "object",
2505
+ properties: {}
2506
+ }
2507
+ },
2220
2508
  {
2221
2509
  name: "get-pod",
2222
2510
  operationId: "getPod",
@@ -2391,12 +2679,12 @@ var generatedTools = [
2391
2679
  startTime: {
2392
2680
  type: "string",
2393
2681
  format: "date-time",
2394
- description: "Start of the billing period (RFC 3339). Defaults to 30 days ago. Snapped down to the start of its bucketSize bucket so the window aligns with the returned records; provide a boundary-aligned value (e.g. midnight for bucketSize=day) to avoid widening.\n"
2682
+ description: "Start of the billing period (RFC 3339). Defaults to 30 days ago."
2395
2683
  },
2396
2684
  endTime: {
2397
2685
  type: "string",
2398
2686
  format: "date-time",
2399
- description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records.\n"
2687
+ description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records."
2400
2688
  },
2401
2689
  bucketSize: {
2402
2690
  $ref: "#/$defs/BillingBucketSize",
@@ -2405,7 +2693,7 @@ var generatedTools = [
2405
2693
  lastN: {
2406
2694
  type: "integer",
2407
2695
  minimum: 1,
2408
- description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days". The resolved window is aligned to bucket boundaries: startTime is the start of the earliest bucket (e.g. midnight of the earliest day) and endTime is the end of the current bucket. Mutually exclusive with startTime/endTime; provide one or the other, not both.\n'
2696
+ description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days".'
2409
2697
  }
2410
2698
  },
2411
2699
  $defs: {
@@ -2467,12 +2755,12 @@ var generatedTools = [
2467
2755
  startTime: {
2468
2756
  type: "string",
2469
2757
  format: "date-time",
2470
- description: "Start of the billing period (RFC 3339). Defaults to 30 days ago. Snapped down to the start of its bucketSize bucket so the window aligns with the returned records; provide a boundary-aligned value (e.g. midnight for bucketSize=day) to avoid widening.\n"
2758
+ description: "Start of the billing period (RFC 3339). Defaults to 30 days ago."
2471
2759
  },
2472
2760
  endTime: {
2473
2761
  type: "string",
2474
2762
  format: "date-time",
2475
- description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records.\n"
2763
+ description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records."
2476
2764
  },
2477
2765
  bucketSize: {
2478
2766
  $ref: "#/$defs/BillingBucketSize",
@@ -2481,7 +2769,7 @@ var generatedTools = [
2481
2769
  lastN: {
2482
2770
  type: "integer",
2483
2771
  minimum: 1,
2484
- description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days". The resolved window is aligned to bucket boundaries: startTime is the start of the earliest bucket (e.g. midnight of the earliest day) and endTime is the end of the current bucket. Mutually exclusive with startTime/endTime; provide one or the other, not both.\n'
2772
+ description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days".'
2485
2773
  },
2486
2774
  clusterId: {
2487
2775
  type: "string",
@@ -2539,10 +2827,19 @@ var generatedTools = [
2539
2827
  {
2540
2828
  name: "list-clusters",
2541
2829
  operationId: "listClusters",
2542
- description: "List clusters. Returns all clusters owned by the authenticated user.",
2830
+ description: "List clusters. Returns clusters owned by the authenticated user, cursor-paginated newest-first; an omitted `limit` defaults to 1000. Follow `pagination.nextCursor` until `hasNextPage` is false.",
2543
2831
  method: "GET",
2544
2832
  path: "/v2/clusters",
2545
- params: [],
2833
+ params: [
2834
+ {
2835
+ name: "cursor",
2836
+ location: "query"
2837
+ },
2838
+ {
2839
+ name: "limit",
2840
+ location: "query"
2841
+ }
2842
+ ],
2546
2843
  hasBody: false,
2547
2844
  annotations: {
2548
2845
  readOnlyHint: true,
@@ -2552,7 +2849,19 @@ var generatedTools = [
2552
2849
  },
2553
2850
  inputSchema: {
2554
2851
  type: "object",
2555
- properties: {}
2852
+ properties: {
2853
+ cursor: {
2854
+ type: "string",
2855
+ minLength: 1,
2856
+ description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected."
2857
+ },
2858
+ limit: {
2859
+ type: "integer",
2860
+ minimum: 1,
2861
+ maximum: 1e3,
2862
+ description: "Page size, 1\u20131000. Defaults to 1000 when omitted."
2863
+ }
2864
+ }
2556
2865
  }
2557
2866
  },
2558
2867
  {
@@ -2806,12 +3115,12 @@ var generatedTools = [
2806
3115
  startTime: {
2807
3116
  type: "string",
2808
3117
  format: "date-time",
2809
- description: "Start of the billing period (RFC 3339). Defaults to 30 days ago. Snapped down to the start of its bucketSize bucket so the window aligns with the returned records; provide a boundary-aligned value (e.g. midnight for bucketSize=day) to avoid widening.\n"
3118
+ description: "Start of the billing period (RFC 3339). Defaults to 30 days ago."
2810
3119
  },
2811
3120
  endTime: {
2812
3121
  type: "string",
2813
3122
  format: "date-time",
2814
- description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records.\n"
3123
+ description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records."
2815
3124
  },
2816
3125
  bucketSize: {
2817
3126
  $ref: "#/$defs/BillingBucketSize",
@@ -2820,7 +3129,7 @@ var generatedTools = [
2820
3129
  lastN: {
2821
3130
  type: "integer",
2822
3131
  minimum: 1,
2823
- description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days". The resolved window is aligned to bucket boundaries: startTime is the start of the earliest bucket (e.g. midnight of the earliest day) and endTime is the end of the current bucket. Mutually exclusive with startTime/endTime; provide one or the other, not both.\n'
3132
+ description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days".'
2824
3133
  }
2825
3134
  },
2826
3135
  $defs: {
@@ -2878,7 +3187,7 @@ var generatedTools = [
2878
3187
  cursor: {
2879
3188
  type: "string",
2880
3189
  minLength: 1,
2881
- description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected with 422.\n"
3190
+ description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected."
2882
3191
  },
2883
3192
  limit: {
2884
3193
  type: "integer",
@@ -2927,7 +3236,7 @@ var generatedTools = [
2927
3236
  cursor: {
2928
3237
  type: "string",
2929
3238
  minLength: 1,
2930
- description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected with 422.\n"
3239
+ description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected."
2931
3240
  },
2932
3241
  limit: {
2933
3242
  type: "integer",
@@ -2969,6 +3278,46 @@ var generatedTools = [
2969
3278
  required: ["id"]
2970
3279
  }
2971
3280
  },
3281
+ {
3282
+ name: "list-global-volumes",
3283
+ operationId: "listGlobalVolumes",
3284
+ description: "List global volumes. Returns global volumes owned by the authenticated user, cursor-paginated newest-first; an omitted `limit` defaults to 1000. Follow `pagination.nextCursor` until `hasNextPage` is false.",
3285
+ method: "GET",
3286
+ path: "/v2/global-volumes",
3287
+ params: [
3288
+ {
3289
+ name: "cursor",
3290
+ location: "query"
3291
+ },
3292
+ {
3293
+ name: "limit",
3294
+ location: "query"
3295
+ }
3296
+ ],
3297
+ hasBody: false,
3298
+ annotations: {
3299
+ readOnlyHint: true,
3300
+ destructiveHint: false,
3301
+ idempotentHint: true,
3302
+ openWorldHint: true
3303
+ },
3304
+ inputSchema: {
3305
+ type: "object",
3306
+ properties: {
3307
+ cursor: {
3308
+ type: "string",
3309
+ minLength: 1,
3310
+ description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected."
3311
+ },
3312
+ limit: {
3313
+ type: "integer",
3314
+ minimum: 1,
3315
+ maximum: 1e3,
3316
+ description: "Page size, 1\u20131000. Defaults to 1000 when omitted."
3317
+ }
3318
+ }
3319
+ }
3320
+ },
2972
3321
  {
2973
3322
  name: "list-gpu-types",
2974
3323
  operationId: "listGpuTypes",
@@ -3032,7 +3381,7 @@ var generatedTools = [
3032
3381
  items: {
3033
3382
  $ref: "#/$defs/Product"
3034
3383
  },
3035
- description: "Comma-separated availability product contexts. Supported values: POD, CLUSTER, SERVERLESS. Required with include=AVAILABILITY, and valid only with it (400 either way). There is no default: the same GPU type can be scarce for pods and plentiful for serverless, so the context has to be stated rather than assumed."
3384
+ description: "Comma-separated availability product contexts. Supported values: POD, CLUSTER, SERVERLESS. Required with include=AVAILABILITY, and valid only with it (400 either way)."
3036
3385
  },
3037
3386
  count: {
3038
3387
  type: "integer",
@@ -3050,7 +3399,7 @@ var generatedTools = [
3050
3399
  type: "string",
3051
3400
  pattern: "^[A-Z]{2}$"
3052
3401
  },
3053
- description: "Comma-separated ISO 3166-1 alpha-2 country codes, uppercase, to constrain availability to \u2014 e.g. FR or FR,DE. Values within this filter use OR semantics. Valid only with include=AVAILABILITY (400 otherwise); a malformed entry is a 422. Scopes availability, lowest-price calculations and the dataCenters array to those countries, so a listed data center outside them is omitted rather than returned with availability NONE. On the list endpoint a GPU type with no data center in those countries drops out entirely; the single-GPU endpoint still returns the requested type, with availability NONE and dataCenters omitted, so a 404 keeps meaning the GPU type does not exist. Read the NONE on availability rather than the absence of dataCenters, which is also absent when availability was not requested."
3402
+ description: "Comma-separated ISO 3166-1 alpha-2 country codes, uppercase, to constrain availability to \u2014 e.g. FR or FR,DE. Values within this filter use OR semantics. Valid only with include=AVAILABILITY; a malformed entry is a 422."
3054
3403
  },
3055
3404
  cudaVersions: {
3056
3405
  type: "array",
@@ -3058,12 +3407,12 @@ var generatedTools = [
3058
3407
  type: "string",
3059
3408
  pattern: "^\\d+\\.\\d+$"
3060
3409
  },
3061
- description: "Comma-separated CUDA versions to scope availability and lowest-price calculations to, matched exactly. Format: major.minor, e.g. 12.8 \u2014 a bare major is rejected here because it identifies no version. Valid only with include=AVAILABILITY (400 otherwise) and mutually exclusive with minCudaVersion (400 if both are sent); a malformed entry is a 422. Also narrows the returned cudaVersions array; omit it to enumerate every version offered."
3410
+ description: "Comma-separated CUDA versions to scope availability and lowest-price calculations to, matched exactly. Format: major.minor, e.g. 12.8 \u2014 a bare major is rejected here because it identifies no version."
3062
3411
  },
3063
3412
  minCudaVersion: {
3064
3413
  type: "string",
3065
3414
  pattern: "^\\d+(\\.\\d+)?$",
3066
- description: "Lowest acceptable CUDA version to scope availability and lowest-price calculations to, compared numerically. Format: integer major or major.minor, e.g. 12 or 12.1 \u2014 unlike the `gpu.minCudaVersion` body field on pod and endpoint create, a bare major is accepted here and means any release of that major, because this filter only widens a read. Valid only with include=AVAILABILITY (400 otherwise) and mutually exclusive with cudaVersions (400 if both are sent); a malformed value is a 422. Use this for an open-ended floor and cudaVersions for an exact set."
3415
+ description: "Lowest acceptable CUDA version to scope availability and lowest-price calculations to, compared numerically."
3067
3416
  }
3068
3417
  },
3069
3418
  $defs: {
@@ -3126,12 +3475,12 @@ var generatedTools = [
3126
3475
  startTime: {
3127
3476
  type: "string",
3128
3477
  format: "date-time",
3129
- description: "Start of the billing period (RFC 3339). Defaults to 30 days ago. Snapped down to the start of its bucketSize bucket so the window aligns with the returned records; provide a boundary-aligned value (e.g. midnight for bucketSize=day) to avoid widening.\n"
3478
+ description: "Start of the billing period (RFC 3339). Defaults to 30 days ago."
3130
3479
  },
3131
3480
  endTime: {
3132
3481
  type: "string",
3133
3482
  format: "date-time",
3134
- description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records.\n"
3483
+ description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records."
3135
3484
  },
3136
3485
  bucketSize: {
3137
3486
  $ref: "#/$defs/BillingBucketSize",
@@ -3140,7 +3489,7 @@ var generatedTools = [
3140
3489
  lastN: {
3141
3490
  type: "integer",
3142
3491
  minimum: 1,
3143
- description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days". The resolved window is aligned to bucket boundaries: startTime is the start of the earliest bucket (e.g. midnight of the earliest day) and endTime is the end of the current bucket. Mutually exclusive with startTime/endTime; provide one or the other, not both.\n'
3492
+ description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days".'
3144
3493
  },
3145
3494
  networkVolumeId: {
3146
3495
  type: "string",
@@ -3168,10 +3517,19 @@ var generatedTools = [
3168
3517
  {
3169
3518
  name: "list-network-volumes",
3170
3519
  operationId: "listNetworkVolumes",
3171
- description: "List network volumes. Returns all network volumes owned by the authenticated user.",
3520
+ description: "List network volumes. Returns network volumes owned by the authenticated user, cursor-paginated newest-first; an omitted `limit` defaults to 1000. Follow `pagination.nextCursor` until `hasNextPage` is false.",
3172
3521
  method: "GET",
3173
3522
  path: "/v2/network-volumes",
3174
- params: [],
3523
+ params: [
3524
+ {
3525
+ name: "cursor",
3526
+ location: "query"
3527
+ },
3528
+ {
3529
+ name: "limit",
3530
+ location: "query"
3531
+ }
3532
+ ],
3175
3533
  hasBody: false,
3176
3534
  annotations: {
3177
3535
  readOnlyHint: true,
@@ -3181,7 +3539,19 @@ var generatedTools = [
3181
3539
  },
3182
3540
  inputSchema: {
3183
3541
  type: "object",
3184
- properties: {}
3542
+ properties: {
3543
+ cursor: {
3544
+ type: "string",
3545
+ minLength: 1,
3546
+ description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected."
3547
+ },
3548
+ limit: {
3549
+ type: "integer",
3550
+ minimum: 1,
3551
+ maximum: 1e3,
3552
+ description: "Page size, 1\u20131000. Defaults to 1000 when omitted."
3553
+ }
3554
+ }
3185
3555
  }
3186
3556
  },
3187
3557
  {
@@ -3225,12 +3595,12 @@ var generatedTools = [
3225
3595
  startTime: {
3226
3596
  type: "string",
3227
3597
  format: "date-time",
3228
- description: "Start of the billing period (RFC 3339). Defaults to 30 days ago. Snapped down to the start of its bucketSize bucket so the window aligns with the returned records; provide a boundary-aligned value (e.g. midnight for bucketSize=day) to avoid widening.\n"
3598
+ description: "Start of the billing period (RFC 3339). Defaults to 30 days ago."
3229
3599
  },
3230
3600
  endTime: {
3231
3601
  type: "string",
3232
3602
  format: "date-time",
3233
- description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records.\n"
3603
+ description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records."
3234
3604
  },
3235
3605
  bucketSize: {
3236
3606
  $ref: "#/$defs/BillingBucketSize",
@@ -3239,7 +3609,7 @@ var generatedTools = [
3239
3609
  lastN: {
3240
3610
  type: "integer",
3241
3611
  minimum: 1,
3242
- description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days". The resolved window is aligned to bucket boundaries: startTime is the start of the earliest bucket (e.g. midnight of the earliest day) and endTime is the end of the current bucket. Mutually exclusive with startTime/endTime; provide one or the other, not both.\n'
3612
+ description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days".'
3243
3613
  },
3244
3614
  podId: {
3245
3615
  type: "string",
@@ -3267,7 +3637,7 @@ var generatedTools = [
3267
3637
  {
3268
3638
  name: "list-pods",
3269
3639
  operationId: "listPods",
3270
- description: "List pods. Returns pods owned by the authenticated user. Cluster member pods are excluded by default; set `includeClusterPods=true` to include them (each carries a non-null `cluster` membership block). Results are cursor-paginated newest-first; an omitted `limit` defaults to 1000. When cluster member pods are excluded, the exclusion applies to each page after it is cut, so a page may hold fewer than `limit` pods \u2014 follow `pagination.nextCursor` until `hasNextPage` is false rather than counting items.",
3640
+ description: "List pods. Returns pods owned by the authenticated user. Cluster member pods are excluded by default; set `includeClusterPods=true` to include them (each carries a non-null `cluster` membership block). Results are cursor-paginated newest-first; an omitted `limit` defaults to 1000. The cluster-member exclusion is applied before the page is cut, so a page holds `limit` pods whenever that many remain. Follow `pagination.nextCursor` until `hasNextPage` is false.",
3271
3641
  method: "GET",
3272
3642
  path: "/v2/pods",
3273
3643
  params: [
@@ -3302,7 +3672,7 @@ var generatedTools = [
3302
3672
  cursor: {
3303
3673
  type: "string",
3304
3674
  minLength: 1,
3305
- description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected with 422.\n"
3675
+ description: "Opaque resume cursor \u2014 pass the previous response's `pagination.nextCursor` through verbatim; omit for the first page. A cursor is only valid for the operation and parameters that issued it; a malformed or foreign cursor is rejected."
3306
3676
  },
3307
3677
  limit: {
3308
3678
  type: "integer",
@@ -3339,7 +3709,7 @@ var generatedTools = [
3339
3709
  type: "string",
3340
3710
  enum: ["official", "verified", "community"],
3341
3711
  default: "official",
3342
- description: "Which slice of the catalog to return: `official` for\nRunpod-curated templates (default), `verified` for\nRunpod-verified community templates, or `community` for all other\npublicly shared templates.\n"
3712
+ description: "Which slice of the catalog to return: `official` for Runpod-curated templates (default), `verified` for Runpod-verified community templates, or `community` for all other publicly shared templates."
3343
3713
  }
3344
3714
  }
3345
3715
  }
@@ -3433,12 +3803,12 @@ var generatedTools = [
3433
3803
  startTime: {
3434
3804
  type: "string",
3435
3805
  format: "date-time",
3436
- description: "Start of the billing period (RFC 3339). Defaults to 30 days ago. Snapped down to the start of its bucketSize bucket so the window aligns with the returned records; provide a boundary-aligned value (e.g. midnight for bucketSize=day) to avoid widening.\n"
3806
+ description: "Start of the billing period (RFC 3339). Defaults to 30 days ago."
3437
3807
  },
3438
3808
  endTime: {
3439
3809
  type: "string",
3440
3810
  format: "date-time",
3441
- description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records.\n"
3811
+ description: "End of the billing period (RFC 3339), exclusive. Defaults to now. Snapped up to the end of the bucketSize bucket it lands in (unless already on a boundary) so the window aligns with the returned records."
3442
3812
  },
3443
3813
  bucketSize: {
3444
3814
  $ref: "#/$defs/BillingBucketSize",
@@ -3447,7 +3817,7 @@ var generatedTools = [
3447
3817
  lastN: {
3448
3818
  type: "integer",
3449
3819
  minimum: 1,
3450
- description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days". The resolved window is aligned to bucket boundaries: startTime is the start of the earliest bucket (e.g. midnight of the earliest day) and endTime is the end of the current bucket. Mutually exclusive with startTime/endTime; provide one or the other, not both.\n'
3820
+ description: 'Return the last N buckets of bucketSize, ending with the current (in-progress) bucket \u2014 e.g. lastN=100 with bucketSize=day is "last 100 days".'
3451
3821
  },
3452
3822
  serverlessId: {
3453
3823
  type: "string",
@@ -3586,7 +3956,7 @@ var generatedTools = [
3586
3956
  type: "object",
3587
3957
  additionalProperties: false,
3588
3958
  minProperties: 1,
3589
- description: "Request body for updating a cluster. Only the cluster name can be\nchanged \u2014 this endpoint is a rename. Compute shape, type, and container\nconfiguration are fixed at creation.\n",
3959
+ description: "Request body for updating a cluster. Only the cluster name can be changed \u2014 this endpoint is a rename. Compute shape, type, and container configuration are fixed at creation.",
3590
3960
  properties: {
3591
3961
  name: {
3592
3962
  type: "string",
@@ -3617,6 +3987,7 @@ var generatedTools = [
3617
3987
  idempotentHint: false,
3618
3988
  openWorldHint: true
3619
3989
  },
3990
+ schemaOnDemand: true,
3620
3991
  inputSchema: {
3621
3992
  type: "object",
3622
3993
  properties: {
@@ -3632,11 +4003,10 @@ var generatedTools = [
3632
4003
  $defs: {
3633
4004
  BaseContainerConfig: {
3634
4005
  type: "object",
3635
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
3636
4006
  properties: {
3637
4007
  args: {
3638
4008
  type: "string",
3639
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
4009
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
3640
4010
  examples: [
3641
4011
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
3642
4012
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -3648,7 +4018,7 @@ var generatedTools = [
3648
4018
  items: {
3649
4019
  type: "string"
3650
4020
  },
3651
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
4021
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
3652
4022
  examples: [
3653
4023
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
3654
4024
  ]
@@ -3658,7 +4028,7 @@ var generatedTools = [
3658
4028
  items: {
3659
4029
  type: "string"
3660
4030
  },
3661
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
4031
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
3662
4032
  examples: [["/bin/bash", "-c"]]
3663
4033
  },
3664
4034
  disk: {
@@ -3718,7 +4088,7 @@ var generatedTools = [
3718
4088
  pools: {
3719
4089
  type: "array",
3720
4090
  minItems: 1,
3721
- description: 'Serverless GPU pool IDs (as returned by `GET /v2/catalog/gpus` in\n`pool`). Workers are placed on whichever listed pool has capacity.\nNarrow a pool down to specific cards with `excludedTypes`.\n\nOn `PATCH`, `pools` and `excludedTypes` are one selection and are\nreplaced together, so sending `pools` by itself **clears the\nexclusions**. Two cases:\n\n- **Changing pools, keeping exclusions** \u2014 send both fields in one\n request: `{"gpu": {"pools": ["ADA_24"], "excludedTypes":\n ["NVIDIA L40"]}}`. `GET` the endpoint first to read the current\n `excludedTypes` and resend the ones that still apply to the new\n pools; an exclusion naming a type outside `pools` is a 400.\n- **Changing only `count` or a CUDA constraint** \u2014 omit `pools`:\n `{"gpu": {"minCudaVersion": "12.4"}}`. The pool list and the\n exclusions are both left exactly as they are.\n\n`excludedTypes` documents the full rule.\n',
4091
+ description: "Serverless GPU pool IDs (the `pool` field of `list-gpu-types`). Workers are placed on whichever listed pool has capacity; narrow to specific cards with `excludedTypes`. On update, `pools` and `excludedTypes` are replaced together, so sending `pools` alone clears the exclusions.",
3722
4092
  items: {
3723
4093
  type: "string"
3724
4094
  },
@@ -3727,7 +4097,7 @@ var generatedTools = [
3727
4097
  excludedTypes: {
3728
4098
  type: "array",
3729
4099
  uniqueItems: true,
3730
- description: 'GPU **type** IDs to subtract from the selected pools \u2014 the `id`\nfield of `GET /v2/catalog/gpus`, the same identifiers pods take in\n`gpu.id`. Workers run on every type in `pools` except these. Omit to\nuse the whole pool.\n\nPools stay the unit of selection; types are the unit of\nsubtraction. There is no inclusive allowlist: a card later added to\none of your pools becomes eligible, which is the honest reading of\n"this pool, minus these".\n\nTied to `pools`, because the two together are one selection:\nsupplying `pools` replaces that selection wholesale, so a `PATCH`\nsending `pools` **without `excludedTypes`** **clears** them \u2014\nrestate them to keep them. A `PATCH` that omits `pools` leaves both\nthe pools and the exclusions untouched, so changing only a CUDA\nconstraint cannot widen a pinned endpoint.\n\nRejected with 400 if a value is not a GPU type in one of `pools`;\nupstream accepts unrecognized exclusions silently, so a typo would\notherwise produce a filter that does nothing. Surrounding whitespace\nis trimmed, so `" NVIDIA L40"` and `"NVIDIA L40"` mean the same card.\n',
4100
+ description: "GPU type IDs to subtract from the selected pools \u2014 the `id` field of `list-gpu-types`, the same identifiers pods take in `gpu.id`. Workers run on every type in `pools` except these; omit to use the whole pool. A value outside `pools` is rejected. On update, restate them whenever you send `pools`, or they are cleared.",
3731
4101
  items: {
3732
4102
  type: "string",
3733
4103
  pattern: "^\\s*[^-\\s]"
@@ -3744,7 +4114,6 @@ var generatedTools = [
3744
4114
  }
3745
4115
  },
3746
4116
  ContainerConfig: {
3747
- description: "Reusable container configuration shared across templates, pods, and serverless endpoints. Adding a field here automatically propagates to all three resources.\n",
3748
4117
  allOf: [
3749
4118
  {
3750
4119
  $ref: "#/$defs/BaseContainerConfig"
@@ -3769,8 +4138,42 @@ var generatedTools = [
3769
4138
  ],
3770
4139
  unevaluatedProperties: false
3771
4140
  },
4141
+ EndpointMount: {
4142
+ type: "object",
4143
+ required: ["volumeId"],
4144
+ additionalProperties: false,
4145
+ description: "A volume mounted on every worker, always at `/runpod-volume`. Custom paths are not supported yet.",
4146
+ properties: {
4147
+ volumeId: {
4148
+ type: "string",
4149
+ description: "ID of a NetworkVolume (under `network`) or GlobalVolume (under `global`).",
4150
+ examples: ["vol_abc"]
4151
+ }
4152
+ }
4153
+ },
4154
+ EndpointMountsInput: {
4155
+ type: "object",
4156
+ additionalProperties: false,
4157
+ description: "Volumes to mount on the endpoint's workers. A `network` or `global` list that is present replaces the endpoint's current list of that kind; an empty list detaches it, and an omitted list is left unchanged.",
4158
+ properties: {
4159
+ network: {
4160
+ type: "array",
4161
+ description: "Network volumes, at most one per data center.",
4162
+ items: {
4163
+ $ref: "#/$defs/EndpointMount"
4164
+ }
4165
+ },
4166
+ global: {
4167
+ type: "array",
4168
+ maxItems: 1,
4169
+ items: {
4170
+ $ref: "#/$defs/EndpointMount"
4171
+ }
4172
+ }
4173
+ }
4174
+ },
3772
4175
  EndpointScaling: {
3773
- description: "Autoscaling signal \u2014 a discriminated union on `type`: `QUEUE_DELAY`\n(queue-based endpoints only) or `REQUEST_COUNT`. The scaler is chosen\nindependently of the endpoint's routing `type` and can be switched on\nupdate.\n",
4176
+ description: "Autoscaling signal \u2014 a discriminated union on `type`: `QUEUE_DELAY` (queue-based endpoints only) or `REQUEST_COUNT`. The scaler is chosen independently of the endpoint's routing `type` and can be switched on update.",
3774
4177
  oneOf: [
3775
4178
  {
3776
4179
  $ref: "#/$defs/QueueDelayScaling"
@@ -3807,14 +4210,14 @@ var generatedTools = [
3807
4210
  type: "integer",
3808
4211
  minimum: 1,
3809
4212
  maximum: 3600,
3810
- description: "Seconds before idle workers scale down. Not applicable to queue-based\nendpoints scaling on `requestCount` \u2014 rejected on create/update and\nomitted from responses for that combination.\n",
4213
+ description: "Seconds before idle workers scale down. Not applicable to queue-based endpoints scaling on `requestCount` \u2014 rejected on create/update and omitted from responses for that combination.",
3811
4214
  examples: [5]
3812
4215
  }
3813
4216
  }
3814
4217
  },
3815
4218
  FlashBoot: {
3816
4219
  type: "string",
3817
- description: "FlashBoot cold-start acceleration mode.\n- `OFF` \u2014 disabled\n- `FLASHBOOT` \u2014 enabled\n- `PRIORITY_FLASHBOOT` \u2014 enabled with priority capacity\n",
4220
+ description: "FlashBoot cold-start acceleration mode. `OFF` \u2014 disabled. `FLASHBOOT` \u2014 enabled. `PRIORITY_FLASHBOOT` \u2014 enabled with priority capacity",
3818
4221
  enum: ["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]
3819
4222
  },
3820
4223
  QueueDelayScaling: {
@@ -3840,7 +4243,7 @@ var generatedTools = [
3840
4243
  RequestCountScaling: {
3841
4244
  type: "object",
3842
4245
  additionalProperties: false,
3843
- description: "Scale on concurrent in-flight requests per worker. Required for\nload-balancing endpoints; also selectable for queue-based.\n",
4246
+ description: "Scale on concurrent in-flight requests per worker. Required for load-balancing endpoints; also selectable for queue-based.",
3844
4247
  required: ["type", "requestCount"],
3845
4248
  properties: {
3846
4249
  type: {
@@ -3857,7 +4260,7 @@ var generatedTools = [
3857
4260
  }
3858
4261
  },
3859
4262
  UpdateEndpointGpuConfig: {
3860
- description: "Partial GPU update \u2014 every field is optional and an omitted one is left\nunchanged. Unlike create, `pools` is optional, so changing only a CUDA\nconstraint does not require resending the pool list.\n\n`excludedTypes` requires `pools`, because the two are one selection and\nonly a supplied `pools` replaces it \u2014 an exclusion on its own would\notherwise be silently dropped.\n",
4263
+ description: "Partial GPU update \u2014 every field is optional and an omitted one is left unchanged. `pools` is optional here, so a CUDA-only change does not resend the pool list; `excludedTypes` needs `pools` in the same request, and `pools` sent alone clears the exclusions.",
3861
4264
  allOf: [
3862
4265
  {
3863
4266
  $ref: "#/$defs/BaseEndpointGpuConfig"
@@ -3874,12 +4277,12 @@ var generatedTools = [
3874
4277
  type: "string",
3875
4278
  pattern: "^\\d+\\.\\d+$"
3876
4279
  },
3877
- description: "Acceptable CUDA versions for worker placement, as\n`major.minor`. An explicit `[]` clears the constraint;\nomitting the field leaves it unchanged. Takes effect as\nworkers are replaced.\n\nA non-empty set is mutually exclusive with minCudaVersion (400\nif both are sent). An explicit `[]` states no constraint, so it\nmay accompany a floor.\nSetting one does not clear the other \u2014 clear it explicitly in\nthe same patch if the endpoint already carries it.\n"
4280
+ description: "Acceptable CUDA versions for worker placement, as `major.minor`. An explicit `[]` clears the constraint; omitting the field leaves it unchanged. A non-empty set cannot be sent with minCudaVersion, and setting one does not clear the other: clear the other explicitly in the same patch. Takes effect as workers are replaced."
3878
4281
  },
3879
4282
  minCudaVersion: {
3880
4283
  type: "string",
3881
4284
  pattern: "^$|^\\d+\\.\\d+$",
3882
- description: 'Lowest acceptable CUDA version for worker placement, as\n`major.minor`. An explicit `""` clears the floor; omitting the\nfield leaves it unchanged. Takes effect as workers are\nreplaced.\n\nMutually exclusive with a non-empty allowedCudaVersions (400 if\nboth are sent); an explicit `[]` there states no constraint and\nmay accompany this floor.\n',
4285
+ description: 'Lowest acceptable CUDA version for worker placement, as `major.minor`. An explicit `""` clears the floor; omitting the field leaves it unchanged. Cannot be sent with a non-empty allowedCudaVersions (an explicit `[]` there is fine); clear that explicitly in the same patch if the endpoint carries one. Takes effect as workers are replaced.',
3883
4286
  examples: ["12.1"]
3884
4287
  }
3885
4288
  }
@@ -3900,7 +4303,7 @@ var generatedTools = [
3900
4303
  type: "array",
3901
4304
  minItems: 1,
3902
4305
  uniqueItems: true,
3903
- description: "Complete replacement CPU selection. Valid only for an existing CPU\nendpoint; endpoint compute family cannot be changed.\n",
4306
+ description: "Complete replacement CPU selection. Valid only for an existing CPU endpoint; endpoint compute family cannot be changed.",
3904
4307
  items: {
3905
4308
  $ref: "#/$defs/CreateCpuConfig"
3906
4309
  }
@@ -3926,14 +4329,19 @@ var generatedTools = [
3926
4329
  type: "array",
3927
4330
  items: {
3928
4331
  type: "string"
3929
- }
4332
+ },
4333
+ description: "Network volume IDs. Pass `[]` to detach.",
4334
+ deprecated: true
4335
+ },
4336
+ mounts: {
4337
+ $ref: "#/$defs/EndpointMountsInput"
3930
4338
  },
3931
4339
  scaling: {
3932
4340
  $ref: "#/$defs/EndpointScaling"
3933
4341
  },
3934
4342
  templateId: {
3935
4343
  type: "string",
3936
- description: "ID of a serverless template whose container settings are\napplied as if they were provided in this PATCH body (image,\nargs, disk, ports, env, registry). Explicit body fields\noverride the template's; `env` merges template and body per\nkey (body wins) and, per PATCH semantics, replaces the\nendpoint's env. One-time application \u2014 no link to the\ntemplate is retained. Must be one of your templates or a\npublic template (unknown or inaccessible ID \u2192 404); must be\na serverless template (\u2192 422).\n",
4344
+ description: "ID of a serverless template whose container settings (image, args, disk, ports, env, registry) are applied as if sent in this PATCH body; explicit body fields win. `env` merges template and body per key (body wins) and replaces the endpoint's existing env. Applied once; must be a serverless template you can access.",
3937
4345
  examples: ["30zmvf89kd"]
3938
4346
  },
3939
4347
  timeout: {
@@ -3944,8 +4352,59 @@ var generatedTools = [
3944
4352
  }
3945
4353
  }
3946
4354
  }
3947
- ],
3948
- unevaluatedProperties: false
4355
+ ],
4356
+ unevaluatedProperties: false
4357
+ }
4358
+ }
4359
+ }
4360
+ },
4361
+ {
4362
+ name: "update-global-volume",
4363
+ operationId: "updateGlobalVolume",
4364
+ description: "Update a global volume. Updates mutable fields on a global volume. Only provided fields are changed.",
4365
+ method: "PATCH",
4366
+ path: "/v2/global-volumes/{id}",
4367
+ params: [
4368
+ {
4369
+ name: "id",
4370
+ location: "path"
4371
+ }
4372
+ ],
4373
+ hasBody: true,
4374
+ annotations: {
4375
+ readOnlyHint: false,
4376
+ destructiveHint: false,
4377
+ idempotentHint: false,
4378
+ openWorldHint: true
4379
+ },
4380
+ inputSchema: {
4381
+ type: "object",
4382
+ properties: {
4383
+ id: {
4384
+ type: "string",
4385
+ description: "Global volume identifier"
4386
+ },
4387
+ body: {
4388
+ $ref: "#/$defs/UpdateGlobalVolumeRequest"
4389
+ }
4390
+ },
4391
+ required: ["id", "body"],
4392
+ $defs: {
4393
+ UpdateGlobalVolumeRequest: {
4394
+ type: "object",
4395
+ additionalProperties: false,
4396
+ minProperties: 1,
4397
+ description: "Only the provided fields are updated. At least one field must be present; an empty body is rejected.",
4398
+ properties: {
4399
+ name: {
4400
+ type: "string",
4401
+ minLength: 1,
4402
+ maxLength: 191,
4403
+ pattern: "[^\\p{Z}\\t-\\r\x85]",
4404
+ description: "New human-readable name, 1\u2013191 characters with at least one non-whitespace character. Leading and trailing whitespace is trimmed.",
4405
+ examples: ["renamed-volume"]
4406
+ }
4407
+ }
3949
4408
  }
3950
4409
  }
3951
4410
  }
@@ -3986,7 +4445,7 @@ var generatedTools = [
3986
4445
  type: "object",
3987
4446
  additionalProperties: false,
3988
4447
  minProperties: 1,
3989
- description: "Only the provided fields are updated. At least one field must be\npresent; an empty body is rejected.\n",
4448
+ description: "Only the provided fields are updated. At least one field must be present; an empty body is rejected.",
3990
4449
  properties: {
3991
4450
  name: {
3992
4451
  type: "string",
@@ -3998,7 +4457,7 @@ var generatedTools = [
3998
4457
  type: "integer",
3999
4458
  minimum: 10,
4000
4459
  maximum: 4096,
4001
- description: "New size in GB. Must be greater than or equal to the current size \u2014\nnetwork volume storage cannot be reduced.\n",
4460
+ description: "New size in GB. Must be greater than or equal to the current size \u2014 network volume storage cannot be reduced.",
4002
4461
  examples: [100]
4003
4462
  }
4004
4463
  }
@@ -4025,6 +4484,7 @@ var generatedTools = [
4025
4484
  idempotentHint: false,
4026
4485
  openWorldHint: true
4027
4486
  },
4487
+ schemaOnDemand: true,
4028
4488
  inputSchema: {
4029
4489
  type: "object",
4030
4490
  properties: {
@@ -4040,11 +4500,10 @@ var generatedTools = [
4040
4500
  $defs: {
4041
4501
  BaseContainerConfig: {
4042
4502
  type: "object",
4043
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
4044
4503
  properties: {
4045
4504
  args: {
4046
4505
  type: "string",
4047
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
4506
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
4048
4507
  examples: [
4049
4508
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
4050
4509
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -4056,7 +4515,7 @@ var generatedTools = [
4056
4515
  items: {
4057
4516
  type: "string"
4058
4517
  },
4059
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
4518
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
4060
4519
  examples: [
4061
4520
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
4062
4521
  ]
@@ -4066,7 +4525,7 @@ var generatedTools = [
4066
4525
  items: {
4067
4526
  type: "string"
4068
4527
  },
4069
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
4528
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
4070
4529
  examples: [["/bin/bash", "-c"]]
4071
4530
  },
4072
4531
  disk: {
@@ -4102,8 +4561,22 @@ var generatedTools = [
4102
4561
  }
4103
4562
  }
4104
4563
  },
4564
+ BaseMounts: {
4565
+ type: "object",
4566
+ properties: {
4567
+ persistent: {
4568
+ $ref: "#/$defs/PersistentMount"
4569
+ },
4570
+ network: {
4571
+ type: "array",
4572
+ maxItems: 1,
4573
+ items: {
4574
+ $ref: "#/$defs/NetworkMount"
4575
+ }
4576
+ }
4577
+ }
4578
+ },
4105
4579
  ContainerConfig: {
4106
- description: "Reusable container configuration shared across templates, pods, and serverless endpoints. Adding a field here automatically propagates to all three resources.\n",
4107
4580
  allOf: [
4108
4581
  {
4109
4582
  $ref: "#/$defs/BaseContainerConfig"
@@ -4120,28 +4593,51 @@ var generatedTools = [
4120
4593
  }
4121
4594
  ]
4122
4595
  },
4123
- Mounts: {
4596
+ GlobalMount: {
4124
4597
  type: "object",
4598
+ required: ["volumeId"],
4125
4599
  additionalProperties: false,
4126
- description: "Storage mounts attached to a pod. At-most-one of `persistent` or\n`network` may be set today (mutually exclusive, enforced at the\nhandler with 400 if both are present). The `network` field is an\narray for forward compatibility with eventual multi-network-volume\nsupport, but `maxItems` is 1 today.\n\nPATCH semantics:\n- Omitting `mounts` or sending `{}` leaves the existing mount\n unchanged.\n- An explicit `network: []` is rejected with 400 (clearing mounts\n is not supported).\n- Mount kind is fixed at create \u2014 a PATCH that introduces a kind\n not present at create (persistent on a network pod, network on\n a persistent pod, or any mount on a previously-mountless pod)\n is rejected with 400.\n- The `volumeId` of a network mount is immutable; a PATCH that\n names a different `volumeId` is rejected with 400.\n- Partial mounts are not supported \u2014 every mount entry must\n include the full schema (`size` + `path` for persistent,\n `volumeId` + `path` for network). Missing required fields \u2192 422.\n",
4600
+ description: "Reference to a GlobalVolume. Usage-billed, so it has no size. The underlying `volumeId` is immutable post-create; the mount `path` may be changed via PATCH.",
4127
4601
  properties: {
4128
- persistent: {
4129
- $ref: "#/$defs/PersistentMount"
4602
+ volumeId: {
4603
+ type: "string",
4604
+ description: "ID of an existing GlobalVolume.",
4605
+ examples: ["gsb_abc"]
4130
4606
  },
4131
- network: {
4132
- type: "array",
4133
- maxItems: 1,
4134
- items: {
4135
- $ref: "#/$defs/NetworkMount"
4136
- }
4607
+ path: {
4608
+ type: "string",
4609
+ description: "Mount path inside the container. Optional on create, where a default is chosen for you; a PATCH that omits it leaves the path unchanged.",
4610
+ examples: ["/data"]
4137
4611
  }
4138
4612
  }
4139
4613
  },
4614
+ Mounts: {
4615
+ description: "Storage mounts for the pod: `persistent` (size + path) excludes both `network` and `global`; `network` (volumeId + path) and `global` (volumeId, optional path) take one entry each and can be combined. Every entry needs its full shape. On update, omit the field to leave the mounts as they are; the mount kind and a volume's volumeId cannot change, and mounts cannot be cleared.",
4616
+ allOf: [
4617
+ {
4618
+ $ref: "#/$defs/BaseMounts"
4619
+ },
4620
+ {
4621
+ type: "object",
4622
+ properties: {
4623
+ global: {
4624
+ type: "array",
4625
+ maxItems: 1,
4626
+ description: "At most one global volume, on GPU and CPU pods, with no data center constraint. Combinable with `network` but not with `persistent` (rejected): upstream drops a persistent disk whenever a volume is attached.",
4627
+ items: {
4628
+ $ref: "#/$defs/GlobalMount"
4629
+ }
4630
+ }
4631
+ }
4632
+ }
4633
+ ],
4634
+ unevaluatedProperties: false
4635
+ },
4140
4636
  NetworkMount: {
4141
4637
  type: "object",
4142
4638
  required: ["volumeId", "path"],
4143
4639
  additionalProperties: false,
4144
- description: "Reference to a NetworkVolume. Custom paths are honored at runtime on\nboth GPU and CPU pods. The underlying `volumeId` is immutable\npost-create; the mount `path` may be changed via PATCH.\n",
4640
+ description: "Reference to a NetworkVolume. Custom paths are honored at runtime on both GPU and CPU pods. The underlying `volumeId` is immutable post-create; the mount `path` may be changed via PATCH.",
4145
4641
  properties: {
4146
4642
  volumeId: {
4147
4643
  type: "string",
@@ -4159,7 +4655,7 @@ var generatedTools = [
4159
4655
  type: "object",
4160
4656
  required: ["size", "path"],
4161
4657
  additionalProperties: false,
4162
- description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data\ndoes not survive a host failure. Disallowed on CPU pods. Mutually\nexclusive with NetworkMount. Deprecated: prefer NetworkMount for any\ndata you cannot recreate.\n",
4658
+ description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data does not survive a host failure. Disallowed on CPU pods. Mutually exclusive with NetworkMount. Deprecated: prefer NetworkMount for any data you cannot recreate.",
4163
4659
  properties: {
4164
4660
  size: {
4165
4661
  type: "integer",
@@ -4184,7 +4680,7 @@ var generatedTools = [
4184
4680
  properties: {
4185
4681
  globalNetworking: {
4186
4682
  type: "boolean",
4187
- description: "Enable (true) or disable (false) global networking. Takes effect on the next pod start/restart, not live. Requires an NVIDIA GPU and a global-networking-enabled data center (both enforced upstream). See `GET /v2/catalog/datacenters` (`globalNetwork`) for eligible data centers."
4683
+ description: "Enable (true) or disable (false) global networking. Takes effect on the next pod start/restart, not live. Requires an NVIDIA GPU and a global-networking-enabled data center (both enforced upstream)."
4188
4684
  },
4189
4685
  locked: {
4190
4686
  type: "boolean",
@@ -4199,7 +4695,7 @@ var generatedTools = [
4199
4695
  },
4200
4696
  templateId: {
4201
4697
  type: "string",
4202
- description: "ID of a pod template whose container settings are applied as\nif they were provided in this PATCH body (image, args, disk,\nports, env, registry \u2014 mounts are not applied on update).\nExplicit body fields override the template's; `env` merges\ntemplate and body per key (body wins) and, per PATCH\nsemantics, replaces the pod's env. One-time application \u2014\nno link to the template is retained. Must be one of your\ntemplates or a public template (unknown or inaccessible ID\n\u2192 404); must not be a serverless template (\u2192 422).\n",
4698
+ description: "ID of a pod template whose container settings (image, args, disk, ports, env, registry; not mounts) are applied as if sent in this PATCH body; explicit body fields win. `env` merges template and body per key (body wins) and replaces the pod's existing env. Applied once; must be a non-serverless template you can access.",
4203
4699
  examples: ["30zmvf89kd"]
4204
4700
  }
4205
4701
  }
@@ -4229,6 +4725,7 @@ var generatedTools = [
4229
4725
  idempotentHint: false,
4230
4726
  openWorldHint: true
4231
4727
  },
4728
+ schemaOnDemand: true,
4232
4729
  inputSchema: {
4233
4730
  type: "object",
4234
4731
  properties: {
@@ -4246,7 +4743,7 @@ var generatedTools = [
4246
4743
  type: "object",
4247
4744
  additionalProperties: false,
4248
4745
  minProperties: 1,
4249
- description: "Only the provided fields are updated; at least one field is required.\nThe secret's `name` is immutable.\n",
4746
+ description: "Only the provided fields are updated; at least one field is required. The secret's `name` is immutable.",
4250
4747
  properties: {
4251
4748
  value: {
4252
4749
  type: "string",
@@ -4298,7 +4795,7 @@ var generatedTools = [
4298
4795
  type: "string",
4299
4796
  pattern: "^(ssh|ecdsa|sk)-[^\\s]+ [^\\s]+([ \\t][^\\n\\r]*)?$"
4300
4797
  },
4301
- description: "The full set of SSH public keys to register \u2014 this is a complete\nreplacement, not a merge. Each entry is an authorized_keys-style\nline: `<type> <base64-key> [comment]`, e.g. from\n`~/.ssh/id_ed25519.pub`. Send `[]` to remove all keys. These keys\nare provisioned into pods created with `startSsh` and\nauthenticate both SSH paths reported in the pod's `ssh` block.\n",
4798
+ description: "The full set of SSH public keys to register \u2014 this is a complete replacement, not a merge. Each entry is an authorized_keys-style line: `<type> <base64-key> [comment]`, e.g. from `~/.ssh/id_ed25519.pub`. Send `[]` to remove all keys.",
4302
4799
  examples: [
4303
4800
  [
4304
4801
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILXGDN/SclOozk1xsDztpmhGiKkkrfQB9SKoO8dSIQQZ me@example.com"
@@ -4329,6 +4826,7 @@ var generatedTools = [
4329
4826
  idempotentHint: false,
4330
4827
  openWorldHint: true
4331
4828
  },
4829
+ schemaOnDemand: true,
4332
4830
  inputSchema: {
4333
4831
  type: "object",
4334
4832
  properties: {
@@ -4343,11 +4841,10 @@ var generatedTools = [
4343
4841
  $defs: {
4344
4842
  BaseContainerConfig: {
4345
4843
  type: "object",
4346
- description: "Container configuration universal to every containerized resource. Compose ContainerConfig instead unless the resource cannot support a direct registry credential (clusters \u2014 there the registry credential arrives via a pod template, see CreateClusterRequest.templateId).\n",
4347
4844
  properties: {
4348
4845
  args: {
4349
4846
  type: "string",
4350
- description: 'The container\'s command, as a single raw string. This is the field `entrypoint` and `cmd` encode into, exposed in its stored form.\n\nTwo shapes are accepted. A bare shell string is treated as CMD and split into arguments, which is what the console\'s "Container start command" field writes. A JSON object of the form `{"entrypoint":[...],"cmd":[...]}` sets either or both explicitly.\n\nResponses always return both representations: `args` exactly as stored, plus the deconstructed `entrypoint` and `cmd`. Supplying `args` together with `entrypoint` or `cmd` is allowed only when they describe the same command, so a read-modify-write client can send back everything it received. Send `""` to clear, omit to leave unchanged.\n',
4847
+ description: 'The container\'s command as one raw string: a bare shell string is treated as CMD and split into arguments; a JSON object `{"entrypoint":[...],"cmd":[...]}` sets either or both. Send `""` to clear; omit to leave unchanged.',
4351
4848
  examples: [
4352
4849
  "--model meta-llama/Llama-3-8B --max-model-len 8192",
4353
4850
  '{"entrypoint":["/bin/bash","-c"],"cmd":["python -u main.py"]}',
@@ -4359,7 +4856,7 @@ var generatedTools = [
4359
4856
  items: {
4360
4857
  type: "string"
4361
4858
  },
4362
- description: "Container CMD in exec form. When the image defines an ENTRYPOINT, this is the argument list passed to it. Encoded into the `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
4859
+ description: "Container CMD in exec form: when the image defines an ENTRYPOINT, the argument list passed to it. Encoded into `args`; supply both only when they describe the same command. Send `[]` to clear; omit to leave unchanged.",
4363
4860
  examples: [
4364
4861
  ["--model", "meta-llama/Llama-3-8B", "--max-model-len", "8192"]
4365
4862
  ]
@@ -4369,7 +4866,7 @@ var generatedTools = [
4369
4866
  items: {
4370
4867
  type: "string"
4371
4868
  },
4372
- description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.\n",
4869
+ description: "Container ENTRYPOINT in exec form, overriding the image's own. Encoded into `args` field; supplying both is allowed only when they describe the same command. Send `[]` to clear, omit to leave unchanged.",
4373
4870
  examples: [["/bin/bash", "-c"]]
4374
4871
  },
4375
4872
  disk: {
@@ -4406,7 +4903,6 @@ var generatedTools = [
4406
4903
  }
4407
4904
  },
4408
4905
  ContainerConfig: {
4409
- description: "Reusable container configuration shared across templates, pods, and serverless endpoints. Adding a field here automatically propagates to all three resources.\n",
4410
4906
  allOf: [
4411
4907
  {
4412
4908
  $ref: "#/$defs/BaseContainerConfig"
@@ -4427,7 +4923,7 @@ var generatedTools = [
4427
4923
  type: "object",
4428
4924
  required: ["size", "path"],
4429
4925
  additionalProperties: false,
4430
- description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data\ndoes not survive a host failure. Disallowed on CPU pods. Mutually\nexclusive with NetworkMount. Deprecated: prefer NetworkMount for any\ndata you cannot recreate.\n",
4926
+ description: "Host-local persistent storage. Pinned to the pod's host machine \u2014 data does not survive a host failure. Disallowed on CPU pods. Mutually exclusive with NetworkMount. Deprecated: prefer NetworkMount for any data you cannot recreate.",
4431
4927
  properties: {
4432
4928
  size: {
4433
4929
  type: "integer",
@@ -4444,13 +4940,13 @@ var generatedTools = [
4444
4940
  },
4445
4941
  TemplateCategory: {
4446
4942
  type: "string",
4447
- description: "Controls how the template is grouped and filtered in the Runpod console.\nIt does not affect hardware selection, scheduling, or billing.\n- `CPU` \u2014 CPU-only workloads\n- `NVIDIA` \u2014 NVIDIA GPU workloads\n- `AMD` \u2014 AMD GPU workloads\n",
4943
+ description: "Controls how the template is grouped and filtered in the Runpod console. It does not affect hardware selection, scheduling, or billing. `CPU` \u2014 CPU-only workloads. `NVIDIA` \u2014 NVIDIA GPU workloads. `AMD` \u2014 AMD GPU workloads",
4448
4944
  enum: ["CPU", "NVIDIA", "AMD"]
4449
4945
  },
4450
4946
  TemplateMounts: {
4451
4947
  type: "object",
4452
4948
  additionalProperties: false,
4453
- description: "Storage mounts attached to a template. Templates support only a\nsingle persistent mount today; any `network` property is rejected\nwith 422 by the schema validator.\n\nPATCH semantics: omitting `mounts` or sending `{}` leaves the\nexisting mount unchanged.\n",
4949
+ description: "Storage mounts for containers from this template: a single `persistent` mount only (`network` is rejected). On update, omitting `mounts` or sending `{}` leaves the existing mount unchanged.",
4454
4950
  properties: {
4455
4951
  persistent: {
4456
4952
  $ref: "#/$defs/PersistentMount"
@@ -5199,6 +5695,8 @@ var listHubRepos = {
5199
5695
  };
5200
5696
  var deployHubRepo = {
5201
5697
  name: "deploy-hub-repo",
5698
+ // Two sequential upstream calls: together they can outlast a client's request timeout.
5699
+ holdsResponse: true,
5202
5700
  annotations: write,
5203
5701
  description: "Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet).",
5204
5702
  inputSchema: {
@@ -5687,6 +6185,7 @@ var runEndpoint = {
5687
6185
  };
5688
6186
  var runsyncEndpoint = {
5689
6187
  name: "runsync-endpoint",
6188
+ holdsResponse: true,
5690
6189
  annotations: write,
5691
6190
  description: "Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. The wait parameter extends the server-side wait" + (HOSTED ? ` \u2014 this server runs behind a 60-second gateway deadline, so waits clamp to ${HTTP_LONG_POLL_BUDGET_MS} ms, which is also the effective default. For longer jobs use run-endpoint + get-job-status.` : " to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."),
5692
6191
  inputSchema: {
@@ -5731,6 +6230,7 @@ var runsyncEndpoint = {
5731
6230
  };
5732
6231
  var getJobStatus = {
5733
6232
  name: "get-job-status",
6233
+ holdsResponse: true,
5734
6234
  annotations: readOnly,
5735
6235
  description: "Check the status of a Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT. Pass `wait` (milliseconds, up to " + STATUS_WAIT_MAX_MS + ") to BLOCK server-side, polling until the job reaches a terminal status or the budget expires \u2014 use it to ride out a cold start (a first job on a fresh worker pulls the image and loads the model, commonly 1\u20135+ minutes) in a single call instead of returning on the first IN_QUEUE and tight-looping the tool. Without `wait` it returns the current status immediately (a single check). If the budget expires before a terminal status, it returns the latest non-terminal status with pollingTimedOut:true \u2014 call again (with `wait`) to keep blocking. IMPORTANT: a job stuck IN_QUEUE does not necessarily mean a capacity shortage \u2014 a worker may have spun up and crash-looped. When the job is IN_QUEUE this tool attaches a workerHealth summary and a hint. To dig deeper call list-endpoint-workers, then read stream-worker-logs regardless of the status a worker reports: a container failing to start loops while its worker still shows RUNNING or THROTTLED and unhealthy stays 0, so UNHEALTHY is one symptom of a crash loop and not a test for it. The logs are the authority.",
5736
6236
  inputSchema: {
@@ -5779,6 +6279,7 @@ var getJobStatus = {
5779
6279
  };
5780
6280
  var streamJob = {
5781
6281
  name: "stream-job",
6282
+ holdsResponse: true,
5782
6283
  annotations: readOnly,
5783
6284
  description: `Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${formatBudget(STREAM_BUDGET_MS)}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.`,
5784
6285
  inputSchema: {
@@ -6011,8 +6512,11 @@ function publicGraphqlBase(env) {
6011
6512
  function authedGraphqlBase(env) {
6012
6513
  return pick(env.RUNPOD_AUTHED_GRAPHQL_URL, "https://api.runpod.io/graphql");
6013
6514
  }
6515
+ function knowledgeSearchBase(env) {
6516
+ return pick(env.KNOWLEDGE_SEARCH_URL, "") || void 0;
6517
+ }
6014
6518
 
6015
- // node_modules/.pnpm/@runpod+typescript-api-sdk@0.2.0/node_modules/@runpod/typescript-api-sdk/dist/index.js
6519
+ // node_modules/.pnpm/@runpod+typescript-api-sdk@0.3.0/node_modules/@runpod/typescript-api-sdk/dist/index.js
6016
6520
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
6017
6521
  var supportsRequestInitExt = () => {
6018
6522
  return typeof process === "object" && Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 && process.versions.undici;
@@ -6909,9 +7413,9 @@ function isDigit(char) {
6909
7413
  return false;
6910
7414
  return isDigitRegex.test(char);
6911
7415
  }
6912
- function parseRetryAfter(header, now = Date.now()) {
6913
- if (header === null) return void 0;
6914
- const value = header.trim();
7416
+ function parseRetryAfter(header2, now = Date.now()) {
7417
+ if (header2 === null) return void 0;
7418
+ const value = header2.trim();
6915
7419
  if (/^\d+$/.test(value)) {
6916
7420
  const ms = Number(value) * 1e3;
6917
7421
  return Number.isSafeInteger(ms) ? ms : void 0;
@@ -7416,6 +7920,7 @@ function logParams(args) {
7416
7920
  }
7417
7921
  var streamPodLogs = {
7418
7922
  name: "stream-pod-logs",
7923
+ holdsResponse: true,
7419
7924
  annotations: readOnly,
7420
7925
  description: "Read a bounded snapshot of a Pod's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the byte cap cut the output. Use `since` to resume from a timestamp.",
7421
7926
  inputSchema: {
@@ -7438,6 +7943,7 @@ var streamPodLogs = {
7438
7943
  };
7439
7944
  var streamWorkerLogs = {
7440
7945
  name: "stream-worker-logs",
7946
+ holdsResponse: true,
7441
7947
  annotations: readOnly,
7442
7948
  description: "Read a bounded snapshot of a Serverless worker's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the byte cap cut the output. Get worker IDs from list-endpoint-workers. Use this whenever a job stays IN_QUEUE while workers exist, whatever status they report \u2014 a crash-looping container often still shows RUNNING or THROTTLED with unhealthy 0, so these logs, not the worker status, are what identify a crash loop.",
7443
7949
  inputSchema: {
@@ -7702,6 +8208,7 @@ var source = {
7702
8208
  var listGuides = {
7703
8209
  name: "list-guides",
7704
8210
  annotations: bundledRead,
8211
+ recordedArguments: ["kind", "query", "concept"],
7705
8212
  description: "List the official Runpod guides: skills (how to work with each tool), reference docs (how Runpod works), and golden paths (live-verified end-to-end walkthroughs such as a dev pod, a Whisper endpoint or a fine-tune-to-serverless pipeline). Read one with read-guide. For a golden path, mcp says whether an agent with only these MCP tools can finish it: full (follow it with tool calls), partial (use it for the plan and facts; name the step the user must run in a terminal), none (reference only). Skills and reference docs have mcp null. concepts lists the concept ids it works with (see lookup-concept). Filter with kind, query or concept.",
7706
8213
  inputSchema: {
7707
8214
  type: "object",
@@ -7737,6 +8244,8 @@ var listGuides = {
7737
8244
  var readGuide = {
7738
8245
  name: "read-guide",
7739
8246
  annotations: bundledRead,
8247
+ recordedArguments: ["id"],
8248
+ recordedGuideKind: (args) => knowledge.guides.find((guide) => guide.id === String(args.id).trim())?.kind,
7740
8249
  description: 'Read one official Runpod guide in full, by the id list-guides returns (for example "runpod-usage/storage" or "golden-path/06-dev-pod").',
7741
8250
  inputSchema: {
7742
8251
  type: "object",
@@ -7774,6 +8283,7 @@ var readGuide = {
7774
8283
  var lookupConcept = {
7775
8284
  name: "lookup-concept",
7776
8285
  annotations: bundledRead,
8286
+ recordedArguments: ["concept"],
7777
8287
  description: 'Look up one Runpod concept (pod, network volume, machine, worker, template, \u2026) by id, name or alias; case, plurals and small typos are tolerated, and `matched_as` shows the key used when the match was not exact. Returns its summary, fields, states, relations and rules, each rule with the public source it comes from, the concepts that point at it, and the linked guides: examples (golden paths that use it, with their mcp level) and docs (skills and reference docs that explain it). Use it for exact facts, for example "is a volume disk tied to one machine" or "what happens to a stopped pod\'s GPU".',
7778
8288
  inputSchema: {
7779
8289
  type: "object",
@@ -7826,6 +8336,7 @@ var lookupConcept = {
7826
8336
  var searchConcepts = {
7827
8337
  name: "search-concepts",
7828
8338
  annotations: bundledRead,
8339
+ recordedArguments: ["query", "limit"],
7829
8340
  description: 'Search the rules of the Runpod concept graph by words, for example "restart stopped pod no gpu" or "network volume data center". Returns the best-matching rules with their concept and public evidence. Follow up with lookup-concept for the full concept.',
7830
8341
  inputSchema: {
7831
8342
  type: "object",
@@ -7890,9 +8401,143 @@ var guideResources = knowledge.guides.map((guide) => ({
7890
8401
  text: guide.body
7891
8402
  }));
7892
8403
 
8404
+ // src/specgen/prose.ts
8405
+ var ABBREVIATIONS = ["e.g.", "i.e.", "etc.", "vs."];
8406
+ var DOT = "\0";
8407
+ function splitSentences(text) {
8408
+ let guarded = text;
8409
+ for (const abbreviation of ABBREVIATIONS) {
8410
+ guarded = guarded.replaceAll(
8411
+ abbreviation,
8412
+ abbreviation.replaceAll(".", DOT)
8413
+ );
8414
+ }
8415
+ return guarded.split(/(?<=[.!?])\s+(?=[A-Z`"'([])/).map((sentence) => sentence.replaceAll(DOT, ".").trim()).filter(Boolean);
8416
+ }
8417
+ function leadSentence(text) {
8418
+ return splitSentences(text)[0] ?? text;
8419
+ }
8420
+
8421
+ // src/specgen/on-demand.ts
8422
+ var DESCRIBE_TOOL_NAME = "describe-tool";
8423
+ var ON_DEMAND_POINTER = ` Call ${DESCRIBE_TOOL_NAME} for the full argument schema before the first use.`;
8424
+ function listedAnnotations(annotations) {
8425
+ const { idempotentHint, ...listed } = annotations;
8426
+ return idempotentHint ? annotations : listed;
8427
+ }
8428
+ function listTool(tool) {
8429
+ const { name, description, inputSchema } = tool;
8430
+ const annotations = listedAnnotations(tool.annotations);
8431
+ if (!tool.schemaOnDemand)
8432
+ return { name, description, inputSchema, annotations };
8433
+ return {
8434
+ name,
8435
+ description: description + ON_DEMAND_POINTER,
8436
+ inputSchema: callShape(inputSchema),
8437
+ annotations
8438
+ };
8439
+ }
8440
+ function callShape(schema) {
8441
+ const defs = schema.$defs ?? {};
8442
+ const resolve = (node) => {
8443
+ if (typeof node.$ref !== "string") return node;
8444
+ const name = node.$ref.split("/").pop() ?? "";
8445
+ return defs[name] ?? {};
8446
+ };
8447
+ const properties = Object.fromEntries(
8448
+ Object.entries(schema.properties ?? {}).map(
8449
+ ([key, value]) => [key, shallowProperty(resolve(value), resolve)]
8450
+ )
8451
+ );
8452
+ return {
8453
+ type: "object",
8454
+ properties,
8455
+ ...schema.required ? { required: schema.required } : {}
8456
+ };
8457
+ }
8458
+ function shallowProperty(node, resolve) {
8459
+ const out = {};
8460
+ const type = schemaType(node, resolve);
8461
+ if (type !== void 0) out.type = type;
8462
+ if (type === "array") {
8463
+ const items = resolve(node.items ?? {});
8464
+ const itemType = schemaType(items, resolve);
8465
+ if (itemType !== void 0) out.items = { type: itemType };
8466
+ }
8467
+ if (Array.isArray(node.enum)) out.enum = node.enum;
8468
+ if (typeof node.description === "string")
8469
+ out.description = leadSentence(node.description);
8470
+ return out;
8471
+ }
8472
+ function schemaType(node, resolve) {
8473
+ if (node.type !== void 0) return node.type;
8474
+ if (node.properties || node.allOf) return "object";
8475
+ const branches2 = node.anyOf ?? node.oneOf;
8476
+ const allObjects = branches2?.every(
8477
+ (branch) => schemaType(resolve(branch), resolve) === "object"
8478
+ ) ?? false;
8479
+ return allObjects ? "object" : void 0;
8480
+ }
8481
+
8482
+ // src/specgen/tools/describe.ts
8483
+ function createDescribeTool(served) {
8484
+ return {
8485
+ name: DESCRIBE_TOOL_NAME,
8486
+ description: "Return one tool's full definition: its description, complete argument schema (every field, nested objects, constraints) and annotations. Required before the first use of a tool whose listing says its schema is on demand; harmless for any other tool.",
8487
+ inputSchema: {
8488
+ type: "object",
8489
+ properties: {
8490
+ name: {
8491
+ type: "string",
8492
+ description: "A tool name exactly as this server's tool list spells it."
8493
+ }
8494
+ },
8495
+ required: ["name"]
8496
+ },
8497
+ annotations: {
8498
+ readOnlyHint: true,
8499
+ destructiveHint: false,
8500
+ idempotentHint: true,
8501
+ openWorldHint: false
8502
+ },
8503
+ handler: async (_ctx, args) => {
8504
+ const name = String(args.name ?? "");
8505
+ const tools = served();
8506
+ const tool = tools.find((candidate) => candidate.name === name);
8507
+ if (!tool) {
8508
+ return {
8509
+ ok: false,
8510
+ status: 404,
8511
+ payload: {
8512
+ error: `No tool named ${name}.`,
8513
+ suggestions: similarNames(
8514
+ name,
8515
+ tools.map((candidate) => candidate.name)
8516
+ ),
8517
+ hint: "Tool names are exactly as this server lists them; the suggestions share a word with the name you sent."
8518
+ }
8519
+ };
8520
+ }
8521
+ const { description, inputSchema, annotations } = tool;
8522
+ return ok({ name: tool.name, description, inputSchema, annotations });
8523
+ }
8524
+ };
8525
+ }
8526
+ function similarNames(query, names) {
8527
+ const wanted = new Set(words2(query));
8528
+ return names.map((name) => {
8529
+ const own = new Set(words2(name));
8530
+ const shared = [...own].filter((word) => wanted.has(word)).length;
8531
+ return { name, score: shared / (/* @__PURE__ */ new Set([...own, ...wanted])).size };
8532
+ }).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, 8).map((entry) => entry.name);
8533
+ }
8534
+ function words2(name) {
8535
+ return name.toLowerCase().split(/[-_\s]+/).filter(Boolean).map((word) => word.replace(/s$/, ""));
8536
+ }
8537
+
7893
8538
  // src/server.ts
7894
8539
  var SERVER_NAME = "Runpod API Server";
7895
- var SERVER_VERSION = true ? "4.2.0" : "dev";
8540
+ var SERVER_VERSION = true ? "4.4.0" : "dev";
7896
8541
 
7897
8542
  // src/specgen/analytics.ts
7898
8543
  import { createHmac } from "crypto";
@@ -7908,7 +8553,22 @@ function analyticsCallerId(apiKey, accountId) {
7908
8553
  const prefix = accountId ? "a:" : "k:";
7909
8554
  return prefix + createHmac("sha256", salt).update(identity).digest("hex").slice(0, 16);
7910
8555
  }
7911
- function captureToolCall(apiKey, event) {
8556
+ var MAX_RECORDED_VALUE_LENGTH = 200;
8557
+ function bounded(value) {
8558
+ return value.slice(0, MAX_RECORDED_VALUE_LENGTH);
8559
+ }
8560
+ function recordedArgumentValues(names, args) {
8561
+ if (!names?.length) return void 0;
8562
+ const values = {};
8563
+ for (const name of names) {
8564
+ const value = args[name];
8565
+ if (typeof value === "string" || typeof value === "number") {
8566
+ values[name] = bounded(String(value).trim());
8567
+ }
8568
+ }
8569
+ return Object.keys(values).length ? values : void 0;
8570
+ }
8571
+ function capture(event, distinctId, properties) {
7912
8572
  const posthogKey = process.env.POSTHOG_API_KEY;
7913
8573
  if (!posthogKey) return;
7914
8574
  void fetch(`${posthogHost()}/capture/`, {
@@ -7916,18 +8576,12 @@ function captureToolCall(apiKey, event) {
7916
8576
  headers: { "Content-Type": "application/json" },
7917
8577
  body: JSON.stringify({
7918
8578
  api_key: posthogKey,
7919
- event: "runpod_mcp_tool_call",
7920
- distinct_id: analyticsCallerId(apiKey, event.accountId),
8579
+ event,
8580
+ distinct_id: distinctId,
7921
8581
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7922
8582
  properties: {
7923
- tool: event.tool,
7924
- ok: event.ok,
7925
- status: event.status,
7926
- duration_ms: event.durationMs,
7927
- transport: event.transport,
7928
- server_version: event.serverVersion,
8583
+ ...properties,
7929
8584
  surface_version: SURFACE_VERSION,
7930
- ...event.clientName ? { client_name: event.clientName } : {},
7931
8585
  $process_person_profile: false
7932
8586
  }
7933
8587
  }),
@@ -7935,6 +8589,63 @@ function captureToolCall(apiKey, event) {
7935
8589
  }).catch(() => {
7936
8590
  });
7937
8591
  }
8592
+ function captureToolCall(apiKey, event) {
8593
+ const args = Object.fromEntries(
8594
+ Object.entries(event.args ?? {}).map(([name, value]) => [
8595
+ `arg_${name}`,
8596
+ value
8597
+ ])
8598
+ );
8599
+ capture("runpod_mcp_tool_call", analyticsCallerId(apiKey, event.accountId), {
8600
+ tool: event.tool,
8601
+ ok: event.ok,
8602
+ status: event.status,
8603
+ duration_ms: event.durationMs,
8604
+ transport: event.transport,
8605
+ server_version: event.serverVersion,
8606
+ ...event.clientName ? { client_name: event.clientName } : {},
8607
+ ...event.guideKind ? { guide_kind: event.guideKind } : {},
8608
+ ...event.blockedBy ? { blocked_by: event.blockedBy } : {},
8609
+ ...args
8610
+ });
8611
+ }
8612
+ function captureLoopBlocked(apiKey, event) {
8613
+ capture(
8614
+ "runpod_mcp_loop_blocked",
8615
+ analyticsCallerId(apiKey, event.accountId),
8616
+ {
8617
+ tool: event.tool,
8618
+ blocked_for_s: event.blockedForS,
8619
+ transport: event.transport,
8620
+ server_version: event.serverVersion,
8621
+ ...event.clientName ? { client_name: event.clientName } : {}
8622
+ }
8623
+ );
8624
+ }
8625
+ function captureResourceCall(apiKey, event) {
8626
+ capture(
8627
+ "runpod_mcp_resource_call",
8628
+ analyticsCallerId(apiKey, event.accountId),
8629
+ {
8630
+ method: event.method,
8631
+ uri: bounded(event.uri),
8632
+ ok: event.ok,
8633
+ transport: event.transport,
8634
+ server_version: event.serverVersion,
8635
+ ...event.clientName ? { client_name: event.clientName } : {}
8636
+ }
8637
+ );
8638
+ }
8639
+
8640
+ // src/alp/ingest.ts
8641
+ function isSinkUrl(value) {
8642
+ try {
8643
+ const url = new URL(value);
8644
+ return url.protocol === "https:" && url.hostname.endsWith(".convex.site") && url.pathname === "/alp/submit";
8645
+ } catch {
8646
+ return false;
8647
+ }
8648
+ }
7938
8649
 
7939
8650
  // src/alp/read.ts
7940
8651
  var DEFAULT_JOURNAL_LIMIT = 20;
@@ -8156,6 +8867,177 @@ function createAlpTools(opts) {
8156
8867
  return [reportFeedback, saveToJournal, askQuestion, readJournalTool];
8157
8868
  }
8158
8869
 
8870
+ // src/search/knowledge.ts
8871
+ var SEARCH_NAMESPACES = ["docs", "knowledge", "blog", "site"];
8872
+ var MAX_QUERY_CHARS = 2e3;
8873
+ var MAX_SEARCH_LIMIT = 20;
8874
+ var ATTEMPTS = 7;
8875
+ var BASE_DELAY_MS = 250;
8876
+ var ATTEMPT_TIMEOUT_MS = 1e4;
8877
+ var RETRY_DEADLINE_MS = 3e4;
8878
+ var PLATFORM_LIMIT = /TooManyConcurrentRequests|Too many concurrent requests/;
8879
+ var AT_CAPACITY = {
8880
+ status: 503,
8881
+ body: {
8882
+ error: "Runpod knowledge search is at capacity.",
8883
+ hint: "Retry the same call in a few seconds."
8884
+ }
8885
+ };
8886
+ var UNREACHABLE = {
8887
+ status: 504,
8888
+ body: {
8889
+ error: "Runpod knowledge search did not respond.",
8890
+ hint: "Retry the same call once."
8891
+ }
8892
+ };
8893
+ function searchConfigured(env = process.env) {
8894
+ return Boolean(knowledgeSearchBase(env) && env.RUNPOD_ASSISTANT_API_KEY);
8895
+ }
8896
+ function parseSearchRequest(raw) {
8897
+ if (typeof raw.query !== "string" || !raw.query.trim()) {
8898
+ return { error: "query must be a non-empty string." };
8899
+ }
8900
+ if (raw.query.length > MAX_QUERY_CHARS) {
8901
+ return { error: `query exceeds ${MAX_QUERY_CHARS} characters.` };
8902
+ }
8903
+ const request = { query: raw.query };
8904
+ if (raw.namespaces !== void 0) {
8905
+ if (!Array.isArray(raw.namespaces) || raw.namespaces.length === 0 || !raw.namespaces.every(
8906
+ (ns) => SEARCH_NAMESPACES.includes(ns)
8907
+ )) {
8908
+ return {
8909
+ error: `namespaces must be a non-empty list of: ${SEARCH_NAMESPACES.join(", ")}.`
8910
+ };
8911
+ }
8912
+ request.namespaces = raw.namespaces;
8913
+ }
8914
+ if (raw.limit !== void 0) {
8915
+ if (typeof raw.limit !== "number" || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > MAX_SEARCH_LIMIT) {
8916
+ return {
8917
+ error: `limit must be an integer from 1 to ${MAX_SEARCH_LIMIT}.`
8918
+ };
8919
+ }
8920
+ request.limit = raw.limit;
8921
+ }
8922
+ return { request };
8923
+ }
8924
+ async function searchKnowledge(request, opts = {}) {
8925
+ const env = opts.env ?? process.env;
8926
+ const doFetch = opts.fetch ?? fetch;
8927
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
8928
+ const now = opts.now ?? Date.now;
8929
+ if (!searchConfigured(env)) {
8930
+ return {
8931
+ status: 503,
8932
+ body: { error: "Knowledge search is not configured on this server." }
8933
+ };
8934
+ }
8935
+ const url = `${knowledgeSearchBase(env)}/v1/search`;
8936
+ const init = {
8937
+ method: "POST",
8938
+ headers: {
8939
+ Authorization: `Bearer ${env.RUNPOD_ASSISTANT_API_KEY}`,
8940
+ "Content-Type": "application/json"
8941
+ },
8942
+ body: JSON.stringify({ ...request, source: "runpod-mcp" })
8943
+ };
8944
+ const startedAt = now();
8945
+ for (let attempt = 1; ; attempt++) {
8946
+ try {
8947
+ const res = await doFetch(url, {
8948
+ ...init,
8949
+ signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS)
8950
+ });
8951
+ if (res.ok) {
8952
+ const data = await res.json().catch(() => null);
8953
+ if (!data) {
8954
+ console.warn("knowledge_search_failed status=200 invalid_json");
8955
+ return {
8956
+ status: 502,
8957
+ body: { error: "Runpod knowledge search failed." }
8958
+ };
8959
+ }
8960
+ return {
8961
+ status: 200,
8962
+ body: {
8963
+ text: String(data.text ?? ""),
8964
+ totalResults: Number(data.totalResults ?? 0),
8965
+ failedNamespaces: Array.isArray(data.failedNamespaces) ? data.failedNamespaces : []
8966
+ }
8967
+ };
8968
+ }
8969
+ const text = await res.text();
8970
+ const retryable = res.status === 503 || res.status === 429 || PLATFORM_LIMIT.test(text);
8971
+ if (!retryable) {
8972
+ console.warn(`knowledge_search_failed status=${res.status}`);
8973
+ return res.status === 400 ? { status: 400, body: { error: errorField(text) } } : { status: 502, body: { error: "Runpod knowledge search failed." } };
8974
+ }
8975
+ } catch (error) {
8976
+ console.warn(
8977
+ `knowledge_search_unreachable ${error instanceof Error ? error.name : "error"}`
8978
+ );
8979
+ return UNREACHABLE;
8980
+ }
8981
+ const delay = Math.random() * BASE_DELAY_MS * 2 ** (attempt - 1);
8982
+ if (attempt >= ATTEMPTS || now() - startedAt + delay > RETRY_DEADLINE_MS) {
8983
+ return AT_CAPACITY;
8984
+ }
8985
+ await sleep2(delay);
8986
+ }
8987
+ }
8988
+ function errorField(text) {
8989
+ try {
8990
+ const parsed = JSON.parse(text);
8991
+ if (typeof parsed.error === "string") return parsed.error;
8992
+ } catch {
8993
+ }
8994
+ return "Invalid search request.";
8995
+ }
8996
+
8997
+ // src/specgen/tools/search.ts
8998
+ function createSearchDocsTool(search) {
8999
+ return {
9000
+ name: "search-docs",
9001
+ annotations: readOnly,
9002
+ recordedArguments: ["limit"],
9003
+ description: 'Search Runpod documentation, curated support knowledge, the Runpod blog and runpod.io for passages that answer a question, for example "how do I attach a network volume to a Serverless endpoint". Returns ranked passages with their source URLs. Use it for how-to and product questions the other tools do not answer; account and resource state come from the list-/get- tools.',
9004
+ inputSchema: {
9005
+ type: "object",
9006
+ properties: {
9007
+ query: {
9008
+ type: "string",
9009
+ maxLength: MAX_QUERY_CHARS,
9010
+ description: "The question, in natural language."
9011
+ },
9012
+ namespaces: {
9013
+ type: "array",
9014
+ items: { type: "string", enum: [...SEARCH_NAMESPACES] },
9015
+ minItems: 1,
9016
+ description: "Sources to search: docs (docs.runpod.io), knowledge (curated support answers), blog, site (runpod.io pages). Default: docs, knowledge, blog."
9017
+ },
9018
+ limit: {
9019
+ type: "integer",
9020
+ minimum: 1,
9021
+ maximum: MAX_SEARCH_LIMIT,
9022
+ description: "Most passages to return. Default 8."
9023
+ }
9024
+ },
9025
+ required: ["query"],
9026
+ additionalProperties: false
9027
+ },
9028
+ handler: async (ctx, args) => {
9029
+ const parsed = parseSearchRequest(args);
9030
+ if ("error" in parsed) return badRequest(parsed.error);
9031
+ const outcome = await search(parsed.request, ctx.apiKey);
9032
+ return {
9033
+ ok: outcome.status === 200,
9034
+ status: outcome.status,
9035
+ payload: outcome.body
9036
+ };
9037
+ }
9038
+ };
9039
+ }
9040
+
8159
9041
  // src/specgen/ops.ts
8160
9042
  import { createHash, randomBytes } from "crypto";
8161
9043
  var SALT = randomBytes(16).toString("hex");
@@ -8180,6 +9062,9 @@ var curatedTools = [
8180
9062
  listPublicEndpoints,
8181
9063
  ...knowledgeTools
8182
9064
  ];
9065
+ var responseHoldingTools = new Set(
9066
+ curatedTools.filter((tool) => tool.holdsResponse).map((tool) => tool.name)
9067
+ );
8183
9068
  var SERVER_INSTRUCTIONS = `These tools cover the Runpod v2 REST surface: compute catalog (GPUs, CPUs, data centers, capacity), pods, serverless endpoints and jobs, templates, network volumes, container registries, billing, public endpoints, and the Hub.
8184
9069
 
8185
9070
  Answer from live reads, as facts. Account and availability questions can only be answered from tool data: call the relevant list-/get- tool and quote the figures, names, and ids it returns verbatim. State stock and status definitively from the read you just made \u2014 never "probably" or "check later". When the user names a resource loosely ("my comfyui pod"), resolve it with a list- tool; ask only on genuine ambiguity. If nothing exists (no pods, zero spend), say exactly that \u2014 an honest empty answer is complete.
@@ -8196,14 +9081,22 @@ PLAYBOOKS \u2014 READ BEFORE ACTING. Each Runpod task has a journey playbook car
8196
9081
 
8197
9082
  THE OFFICIAL RUNPOD PLUGIN. Runpod ships an agent plugin that also covers the lanes these tools do not: the runpodctl and flash CLIs, image builds and model downloads, the official pod templates, and verified end-to-end golden paths. Check once per session whether skills named runpod, runpod-usage, or runpodctl are available to you \u2014 read what you already have, never shell out to look. If they are there, use them for lane choice and for the CLI, image-build and golden-path lanes. For work done with these tools, the journey playbook is the procedure, wherever it is served from: the plugin's runpod-mcp skill and the journey skills it routes to when you have them, otherwise the same playbooks through read-guide here. If you do not see them, recommend the plugin to the user, and recommend updating to the latest if what they have is missing something you need. Point them at https://github.com/runpod/runpod-plugins-official \u2014 its README carries the install, update, and clone commands for each client, so read it yourself when you need the exact one rather than guessing a command. This is a recommendation, never a prerequisite: make it once, briefly, and get on with the task. Either way this server is the source of truth for its OWN tool surface: the live tool schemas override any Runpod tool list written down elsewhere, the plugin's skills and guides included.
8198
9083
 
8199
- The tool schemas are generated from the Runpod v2 OpenAPI contract, served as a machine-readable document at https://api.runpod.io/v2/openapi.json \u2014 consult it for fields beyond the tool surface. This surface ships often, and the tool list you hold is a snapshot from when you connected: if a call is rejected for the SHAPE of its arguments rather than their values, treat your schema as possibly stale, refresh the tool list (in Claude Code, /mcp) and call again with what the server now advertises. Retrying variants of a rejected shape cannot succeed. These instructions are a snapshot from the same moment. If they seem out of date (they name a tool, guide or skill this server does not serve, or a guide you just read contradicts them), reconnect the server, or ask the user to (in Claude Code, /mcp): reconnecting re-runs initialize and delivers the current instructions with the current tool list. Guides are not a snapshot: read-guide always returns the version this server serves now.`;
9084
+ The tool schemas are generated from the Runpod v2 OpenAPI contract, served as a machine-readable document at https://api.runpod.io/v2/openapi.json \u2014 consult it for fields beyond the tool surface. Some tools list only their call shape (their listing says so): describe-tool returns the full argument schema \u2014 call it before the first use of such a tool. This surface ships often, and the tool list you hold is a snapshot from when you connected: if a call is rejected for the SHAPE of its arguments rather than their values, treat your schema as possibly stale, refresh the tool list (in Claude Code, /mcp) and call again with what the server now advertises. Retrying variants of a rejected shape cannot succeed. These instructions are a snapshot from the same moment. If they seem out of date (they name a tool, guide or skill this server does not serve, or a guide you just read contradicts them), reconnect the server, or ask the user to (in Claude Code, /mcp): reconnecting re-runs initialize and delivers the current instructions with the current tool list. Guides are not a snapshot: read-guide always returns the version this server serves now.`;
8200
9085
  var ALP_INSTRUCTIONS = `
8201
9086
 
8202
9087
  CONTRIBUTING BACK IS OPTIONAL. Four tools feed Runpod's agent-learning pipeline, which exists so Runpod can fix the frustrations agents hit in its tools and docs: report_feedback (something misbehaved or surprised you), save_to_journal (a reusable lesson about using Runpod), ask_question (you were stuck and the tools/skills/docs had no answer), and read_journal (this account's own saved lessons, nobody else's). Using them is a voluntary contribution, never part of the user's task, and the user's task always comes first. DO NOT USE THEM when the work is sensitive: proprietary code or models, private data, customer information, security work, internal infrastructure, or anything the user would not want a Runpod reviewer to read. Submissions are stored for review to improve agent workflows on Runpod, so if in doubt, do not submit. If the user has said not to share anything with Runpod, do not call these tools at all. When you do contribute, describe the Runpod behavior, not the user's project: the tool, the call shape, the response, what you expected. Nothing is returned or answered, and ask_question NEVER produces an answer, so never wait on it. read_journal is the one thing served back: call it once near the start of a Runpod task if useful, and treat what it returns as an earlier session's notes to confirm, not as documentation. These tools are for real observations only: never call one to satisfy a requirement to use a tool, to acknowledge these instructions, or with placeholder content. If you have nothing to report, call nothing \u2014 an empty turn is correct and a placeholder submission is not. Never include API keys or secrets in submissions.`;
8203
9088
  function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8204
9089
  const rateLimiter = opts.rateLimiter ?? noopRateLimiter;
8205
9090
  const caller = callerId(ctx.apiKey);
8206
- const servedCuratedTools = opts.alp ? [...curatedTools, ...createAlpTools(opts.alp)] : curatedTools;
9091
+ const servedCuratedTools = [
9092
+ ...curatedTools,
9093
+ ...opts.search ? [createSearchDocsTool(opts.search)] : [],
9094
+ ...opts.alp ? createAlpTools(opts.alp) : [],
9095
+ createDescribeTool(() => [...servedCuratedTools, ...generatedTools])
9096
+ ];
9097
+ const callShapeOnlyTools = new Set(
9098
+ [...servedCuratedTools, ...generatedTools].filter((tool) => tool.schemaOnDemand).map((tool) => tool.name)
9099
+ );
8207
9100
  const server = new Server(
8208
9101
  { name: SERVER_NAME, version: `${version} [specgen]` },
8209
9102
  {
@@ -8222,24 +9115,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8222
9115
  }
8223
9116
  };
8224
9117
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
8225
- tools: [
8226
- ...servedCuratedTools.map(
8227
- ({ name, description, inputSchema, annotations }) => ({
8228
- name,
8229
- description,
8230
- inputSchema,
8231
- annotations
8232
- })
8233
- ),
8234
- ...generatedTools.map(
8235
- ({ name, description, inputSchema, annotations }) => ({
8236
- name,
8237
- description,
8238
- inputSchema,
8239
- annotations
8240
- })
8241
- )
8242
- ]
9118
+ tools: [...servedCuratedTools, ...generatedTools].map(listTool)
8243
9119
  }));
8244
9120
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
8245
9121
  resources: guideResources.map(({ text: _text, ...resource }) => resource)
@@ -8251,6 +9127,14 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8251
9127
  const guide = guideResources.find(
8252
9128
  (candidate) => candidate.uri === request.params.uri
8253
9129
  );
9130
+ if (opts.analytics) {
9131
+ captureResourceCall(ctx.apiKey, {
9132
+ method: "resources/read",
9133
+ uri: guide ? guide.uri : "unknown",
9134
+ ok: Boolean(guide),
9135
+ ...opts.analytics
9136
+ });
9137
+ }
8254
9138
  if (!guide) {
8255
9139
  throw new Error(
8256
9140
  `Unknown resource ${request.params.uri}. Resources are the plugin guides under ${GUIDE_URI_PREFIX}; call list-guides for the guide ids.`
@@ -8273,6 +9157,16 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8273
9157
  status: 429,
8274
9158
  durationMs: 0
8275
9159
  });
9160
+ if (opts.analytics) {
9161
+ captureToolCall(ctx.apiKey, {
9162
+ tool: request.params.name,
9163
+ ok: false,
9164
+ status: 429,
9165
+ durationMs: 0,
9166
+ ...verdict.reason ? { blockedBy: verdict.reason } : {},
9167
+ ...opts.analytics
9168
+ });
9169
+ }
8276
9170
  return {
8277
9171
  content: [
8278
9172
  {
@@ -8310,11 +9204,14 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8310
9204
  ok: result.ok,
8311
9205
  status: result.status,
8312
9206
  durationMs: Date.now() - startedAt,
9207
+ args: recordedArgumentValues(curated?.recordedArguments, args),
9208
+ guideKind: curated?.recordedGuideKind?.(args),
8313
9209
  ...opts.analytics
8314
9210
  });
8315
9211
  }
9212
+ opts.onToolResult?.(request.params.name, result.status);
8316
9213
  if (!result.ok && result.status === 401) opts.onUnauthorized?.();
8317
- const payload = result.ok ? result.payload : withRecoveryHint(result);
9214
+ const payload = result.ok ? result.payload : withRecoveryHint(result, callShapeOnlyTools.has(request.params.name));
8318
9215
  return {
8319
9216
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
8320
9217
  isError: !result.ok
@@ -8332,9 +9229,29 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
8332
9229
  }
8333
9230
  return server;
8334
9231
  }
8335
- function errorHint(status) {
8336
- if (status === 400 || status === 422)
8337
- return "Input shape mismatch: check this tool's schema for required fields. For job payloads, the worker's release config defines the expected input object \u2014 fix the payload and retry; don't abandon the task.";
9232
+ var SHAPE_HINT = "Input shape mismatch: check this tool's schema for required fields. For job payloads, the worker's release config defines the expected input object \u2014 fix the payload and retry; don't abandon the task.";
9233
+ var DETAIL_HINTS = [
9234
+ [
9235
+ /no longer any instances available|not enough free GPUs/i,
9236
+ "Capacity, not input: the request is valid but no machine can take it right now. Don't change the payload shape. Retry with another GPU type, data center, cloud, CUDA version or count, or try again later. For a stopped Pod, its host's GPUs are taken; that Pod can only start once they free up."
9237
+ ],
9238
+ [
9239
+ /at least \$\d|insufficient (?:funds|balance)|\bbalance\b/i,
9240
+ "Account balance, not input: this operation needs funds on the account. Ask the user to add credit in the Runpod console; list-billing shows recent spend."
9241
+ ],
9242
+ [
9243
+ /mutually exclusive|cannot be combined with/i,
9244
+ "Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else."
9245
+ ]
9246
+ ];
9247
+ function errorHint(status, detail) {
9248
+ if (status === 400 && detail) {
9249
+ for (const [pattern, hint] of DETAIL_HINTS)
9250
+ if (pattern.test(detail)) return hint;
9251
+ if (/^unauthorized$/i.test(detail.trim()))
9252
+ return "Upstream refused this write. With a key that can read, this usually means the account has no balance: ask the user to check their balance in the Runpod console before retrying.";
9253
+ }
9254
+ if (status === 400 || status === 422) return SHAPE_HINT;
8338
9255
  if (status === 401) return "The Runpod API key is missing or invalid.";
8339
9256
  if (status === 402)
8340
9257
  return "Check your account balance in the Runpod console before retrying billable operations; list-billing shows the recent spend behind it.";
@@ -8348,13 +9265,23 @@ function errorHint(status) {
8348
9265
  return "Upstream Runpod error: retry once; if it persists, report it as an API-side failure.";
8349
9266
  return void 0;
8350
9267
  }
8351
- function withRecoveryHint(result) {
9268
+ var CALL_SHAPE_ONLY_HINT = `This tool lists its call shape only; ${DESCRIBE_TOOL_NAME} serves the full argument schema.`;
9269
+ function withRecoveryHint(result, listsCallShapeOnly = false) {
8352
9270
  const payload = asRecord(result.payload) ?? { error: result.payload };
9271
+ const hint = payload.hint ?? asRecord(payload.detail)?.hint ?? errorHint(result.status, detailText(payload));
9272
+ const shapeRejected = result.status === 400 || result.status === 422;
8353
9273
  return {
8354
9274
  ...payload,
8355
- hint: payload.hint ?? asRecord(payload.detail)?.hint ?? errorHint(result.status)
9275
+ hint: listsCallShapeOnly && shapeRejected ? [hint, CALL_SHAPE_ONLY_HINT].filter(Boolean).join(" ") : hint
8356
9276
  };
8357
9277
  }
9278
+ function detailText(payload) {
9279
+ if (Array.isArray(payload.errors) && payload.errors.length) return void 0;
9280
+ const detail = payload.detail ?? payload.error;
9281
+ if (typeof detail === "string") return detail;
9282
+ const nested = asRecord(detail);
9283
+ return typeof nested?.detail === "string" ? nested.detail : void 0;
9284
+ }
8358
9285
  function asRecord(value) {
8359
9286
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
8360
9287
  }
@@ -8520,6 +9447,58 @@ function createToolContext(options = {}) {
8520
9447
  };
8521
9448
  }
8522
9449
 
9450
+ // src/specgen/not-found-limiter.ts
9451
+ var SINK_TIMEOUT_MS = 3e3;
9452
+ var NOT_FOUND_LOOP = "not_found_loop";
9453
+ var blocked = /* @__PURE__ */ new Map();
9454
+ function notFoundSinkUrl(submitUrl) {
9455
+ if (!isSinkUrl(submitUrl)) return null;
9456
+ const url = new URL(submitUrl);
9457
+ url.pathname = "/ratelimit/not-found";
9458
+ return url.toString();
9459
+ }
9460
+ function createNotFoundLimiter(opts) {
9461
+ const fetchImpl = opts.fetchImpl ?? fetch;
9462
+ const now = opts.now ?? Date.now;
9463
+ return {
9464
+ rateLimiter: async (_caller, tool) => {
9465
+ const pair = `${opts.key}|${tool}`;
9466
+ const until = blocked.get(pair);
9467
+ if (until === void 0) return { allowed: true };
9468
+ if (until <= now()) {
9469
+ blocked.delete(pair);
9470
+ return { allowed: true };
9471
+ }
9472
+ return {
9473
+ allowed: false,
9474
+ retryAfterS: Math.ceil((until - now()) / 1e3),
9475
+ reason: NOT_FOUND_LOOP
9476
+ };
9477
+ },
9478
+ onToolResult: (tool, status) => {
9479
+ if (status !== 404) return;
9480
+ const pair = `${opts.key}|${tool}`;
9481
+ void fetchImpl(opts.sinkUrl, {
9482
+ method: "POST",
9483
+ headers: {
9484
+ "Content-Type": "application/json",
9485
+ "X-ALP-Secret": opts.secret
9486
+ },
9487
+ body: JSON.stringify({ key: pair }),
9488
+ signal: AbortSignal.timeout(SINK_TIMEOUT_MS)
9489
+ }).then((response) => response.ok ? response.json() : null).then((body) => {
9490
+ const reply = body;
9491
+ const until = reply?.blockedUntil;
9492
+ if (typeof until === "number" && until > now()) {
9493
+ blocked.set(pair, until);
9494
+ if (reply?.started === true) opts.onBlockStarted?.(tool, until);
9495
+ }
9496
+ }).catch(() => {
9497
+ });
9498
+ }
9499
+ };
9500
+ }
9501
+
8523
9502
  // src/_shared/credential-check.ts
8524
9503
  import { createHash as createHash2 } from "crypto";
8525
9504
  var VALID_TTL_MS = 6e4;
@@ -8631,8 +9610,7 @@ function extractBearerToken(req) {
8631
9610
  return parts[1];
8632
9611
  }
8633
9612
  function analyticsOptedOut(req) {
8634
- const raw = req.headers["x-runpod-analytics"];
8635
- const value = (Array.isArray(raw) ? raw[0] : raw)?.trim().toLowerCase();
9613
+ const value = header(req, "x-runpod-analytics")?.trim().toLowerCase();
8636
9614
  return value === "off" || value === "0" || value === "false";
8637
9615
  }
8638
9616
  function getBaseUrl(req) {
@@ -8691,14 +9669,16 @@ function credentialCheckMayBeWrongEnvironment(env = process.env) {
8691
9669
  );
8692
9670
  return restMoved !== graphqlMoved;
8693
9671
  }
8694
- function looksLikeMcpMessage(value) {
8695
- if (typeof value !== "object" || value === null) return false;
8696
- const msg = value;
8697
- return msg.jsonrpc === "2.0" && typeof msg.method === "string";
9672
+ function parseMcpMessages(body) {
9673
+ const elements = Array.isArray(body) ? body : [body];
9674
+ return elements.flatMap((element) => {
9675
+ const parsed = JSONRPCMessageSchema.safeParse(element);
9676
+ return parsed.success ? [parsed.data] : [];
9677
+ });
8698
9678
  }
9679
+ var invokesMethod = (message) => "method" in message;
8699
9680
  function bodyIsCheckable(body) {
8700
- const messages = Array.isArray(body) ? body : [body];
8701
- return messages.some(looksLikeMcpMessage);
9681
+ return parseMcpMessages(body).some(invokesMethod);
8702
9682
  }
8703
9683
  function isJsonContentType(req) {
8704
9684
  const ct = req.headers["content-type"];
@@ -8736,17 +9716,79 @@ function earlyCredentialCheckSkip(req) {
8736
9716
  if (credentialCheckMayBeWrongEnvironment()) return "skip_env_mismatch";
8737
9717
  return null;
8738
9718
  }
9719
+ var callsResponseHoldingTool = (message) => {
9720
+ const call = CallToolRequestSchema2.safeParse(message);
9721
+ return call.success && responseHoldingTools.has(call.data.params.name);
9722
+ };
9723
+ function prefersJsonResponse(messages) {
9724
+ return messages.length > 0 && !messages.some(callsResponseHoldingTool);
9725
+ }
9726
+ var DEFAULT_REQUEST_LOG_SAMPLE_RATE = 0.1;
9727
+ function requestLogSampleRate(env = process.env) {
9728
+ if (env.MCP_VERBOSE_LOGS === "true") return 1;
9729
+ const raw = env.MCP_REQUEST_LOG_SAMPLE_RATE;
9730
+ if (raw === void 0 || raw === "") return DEFAULT_REQUEST_LOG_SAMPLE_RATE;
9731
+ const parsed = Number(raw);
9732
+ if (!Number.isFinite(parsed)) return DEFAULT_REQUEST_LOG_SAMPLE_RATE;
9733
+ return Math.min(1, Math.max(0, parsed));
9734
+ }
9735
+ function summarizeRpc(messages) {
9736
+ const rpc = messages.map((message) => {
9737
+ const call = CallToolRequestSchema2.safeParse(message);
9738
+ if (call.success) return `tools/call:${call.data.params.name}`;
9739
+ return "method" in message ? message.method : "response";
9740
+ });
9741
+ const init = messages.map((message) => InitializeRequestSchema.safeParse(message)).find((result) => result.success);
9742
+ const client = init?.success ? `${init.data.params.clientInfo.name}/${init.data.params.clientInfo.version}` : void 0;
9743
+ return { rpc: rpc.length > 0 ? rpc.join(",") : void 0, client };
9744
+ }
9745
+ function header(req, name) {
9746
+ const value = req.headers[name];
9747
+ return Array.isArray(value) ? value[0] : value;
9748
+ }
9749
+ function clientAddress(req) {
9750
+ return header(req, "x-real-ip") || header(req, "x-forwarded-for")?.split(",")[0].trim() || req.socket?.remoteAddress || void 0;
9751
+ }
9752
+ function startRequestLog(req, res, bearerToken) {
9753
+ const sampleRate = requestLogSampleRate();
9754
+ if (Math.random() >= sampleRate) return { note() {
9755
+ } };
9756
+ const startedAt = Date.now();
9757
+ let summary = {};
9758
+ const address = clientAddress(req);
9759
+ const userAgent = header(req, "user-agent");
9760
+ let emitted = false;
9761
+ const emit = () => {
9762
+ if (emitted) return;
9763
+ emitted = true;
9764
+ console.log("mcp_request", {
9765
+ method: req.method,
9766
+ url: req.url,
9767
+ status: res.writableFinished ? res.statusCode : void 0,
9768
+ durationMs: Date.now() - startedAt,
9769
+ rpc: summary.rpc,
9770
+ client: summary.client,
9771
+ ua: userAgent?.slice(0, 160),
9772
+ caller: callerId(bearerToken ?? void 0),
9773
+ ip: address ? callerId(address) : void 0,
9774
+ sampleRate
9775
+ });
9776
+ };
9777
+ res.on("finish", emit);
9778
+ res.on("close", emit);
9779
+ return {
9780
+ note(next) {
9781
+ summary = next;
9782
+ }
9783
+ };
9784
+ }
8739
9785
  function logCredentialCheck(outcome) {
8740
9786
  if (outcome === "pass" && process.env.MCP_VERBOSE_LOGS !== "true") return;
8741
9787
  console.log("credential_check", { outcome });
8742
9788
  }
8743
9789
  async function handleMcpRequest(req, res, opts = {}) {
8744
9790
  const bearerToken = extractBearerToken(req);
8745
- console.log("mcp_request", {
8746
- method: req.method,
8747
- url: req.url,
8748
- hasBearerToken: !!bearerToken
8749
- });
9791
+ const requestLog = startRequestLog(req, res, bearerToken);
8750
9792
  if (req.method === "GET") {
8751
9793
  res.writeHead(405, {
8752
9794
  Allow: "POST, DELETE",
@@ -8775,9 +9817,7 @@ async function handleMcpRequest(req, res, opts = {}) {
8775
9817
  let body = req.body;
8776
9818
  let accountId;
8777
9819
  const early = earlyCredentialCheckSkip(req);
8778
- if (early) {
8779
- logCredentialCheck(early);
8780
- } else {
9820
+ if (!early) {
8781
9821
  const read = await readJsonBody(req);
8782
9822
  if (read === BODY_TOO_LARGE) {
8783
9823
  res.writeHead(413, { "Content-Type": "application/json" });
@@ -8785,26 +9825,47 @@ async function handleMcpRequest(req, res, opts = {}) {
8785
9825
  return;
8786
9826
  }
8787
9827
  body = read;
8788
- if (!bodyIsCheckable(body)) {
8789
- logCredentialCheck("skip_not_mcp_body");
8790
- } else {
8791
- const verify = opts.verifyCredential ?? defaultCredentialChecker.verify;
8792
- const verdict = await verify(bearerToken);
8793
- if (verdict.status === "valid") accountId = verdict.accountId;
8794
- logCredentialCheck(
8795
- verdict.status === "invalid" ? "reject" : verdict.status === "valid" ? "pass" : "fail_open"
9828
+ }
9829
+ const messages = parseMcpMessages(body);
9830
+ requestLog.note(summarizeRpc(messages));
9831
+ if (early) {
9832
+ logCredentialCheck(early);
9833
+ } else if (!messages.some(invokesMethod)) {
9834
+ logCredentialCheck("skip_not_mcp_body");
9835
+ } else {
9836
+ const verify = opts.verifyCredential ?? defaultCredentialChecker.verify;
9837
+ const verdict = await verify(bearerToken);
9838
+ if (verdict.status === "valid") accountId = verdict.accountId;
9839
+ logCredentialCheck(
9840
+ verdict.status === "invalid" ? "reject" : verdict.status === "valid" ? "pass" : "fail_open"
9841
+ );
9842
+ if (verdict.status === "invalid") {
9843
+ writeUnauthorized(
9844
+ req,
9845
+ res,
9846
+ `${verdict.reason} Re-authenticate to continue.`,
9847
+ true
8796
9848
  );
8797
- if (verdict.status === "invalid") {
8798
- writeUnauthorized(
8799
- req,
8800
- res,
8801
- `${verdict.reason} Re-authenticate to continue.`,
8802
- true
8803
- );
8804
- return;
8805
- }
9849
+ return;
8806
9850
  }
8807
9851
  }
9852
+ const notFoundUrl = process.env.ALP_SINK_URL ? notFoundSinkUrl(process.env.ALP_SINK_URL) : null;
9853
+ const notFoundLimiter = bearerToken && notFoundUrl && process.env.ALP_SINK_SECRET ? createNotFoundLimiter({
9854
+ key: analyticsCallerId(bearerToken, accountId),
9855
+ sinkUrl: notFoundUrl,
9856
+ secret: process.env.ALP_SINK_SECRET,
9857
+ // One usage event per block, unless this client opted out.
9858
+ onBlockStarted: analyticsOptedOut(req) ? void 0 : (tool, blockedUntil) => captureLoopBlocked(bearerToken, {
9859
+ tool,
9860
+ blockedForS: Math.round((blockedUntil - Date.now()) / 1e3),
9861
+ transport: "http",
9862
+ serverVersion: opts.serverVersion ?? SERVER_VERSION,
9863
+ clientName: sanitizeUaToken(
9864
+ String(req.headers["user-agent"] ?? "unknown")
9865
+ ),
9866
+ accountId
9867
+ })
9868
+ }) : null;
8808
9869
  const server = createSpecgenServer(
8809
9870
  createToolContext({
8810
9871
  apiKey: bearerToken,
@@ -8845,6 +9906,13 @@ async function handleMcpRequest(req, res, opts = {}) {
8845
9906
  harnessSource: "user_agent"
8846
9907
  }
8847
9908
  } : {},
9909
+ // search-docs: listed only where the deployment holds the assistant's
9910
+ // service key; the search runs in-process with it.
9911
+ ...searchConfigured() ? { search: (request) => searchKnowledge(request) } : {},
9912
+ ...notFoundLimiter ? {
9913
+ rateLimiter: notFoundLimiter.rateLimiter,
9914
+ onToolResult: notFoundLimiter.onToolResult
9915
+ } : {},
8848
9916
  // A tool call that 401s proves a cached "valid" verdict wrong before
8849
9917
  // its TTL — drop it so the next request re-checks and re-auths.
8850
9918
  onUnauthorized: () => (opts.invalidateCredential ?? defaultCredentialChecker.invalidate)(
@@ -8853,8 +9921,9 @@ async function handleMcpRequest(req, res, opts = {}) {
8853
9921
  }
8854
9922
  );
8855
9923
  const transport = new StreamableHTTPServerTransport({
8856
- sessionIdGenerator: void 0
9924
+ sessionIdGenerator: void 0,
8857
9925
  // stateless — no session persistence
9926
+ enableJsonResponse: prefersJsonResponse(messages)
8858
9927
  });
8859
9928
  res.on("close", () => {
8860
9929
  transport.close().catch(() => {
@@ -8868,7 +9937,12 @@ async function handleMcpRequest(req, res, opts = {}) {
8868
9937
  export {
8869
9938
  bodyIsCheckable,
8870
9939
  defaultCredentialChecker,
9940
+ extractBearerToken,
8871
9941
  getBaseUrl,
8872
- handleMcpRequest
9942
+ handleMcpRequest,
9943
+ parseMcpMessages,
9944
+ prefersJsonResponse,
9945
+ requestLogSampleRate,
9946
+ summarizeRpc
8873
9947
  };
8874
9948
  //# sourceMappingURL=http.mjs.map