@runpod/mcp-server 4.10.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.cjs CHANGED
@@ -308,6 +308,14 @@ function validateArguments(inputSchema, args) {
308
308
  }
309
309
  return null;
310
310
  }
311
+ function withoutNullOptionals(inputSchema, args) {
312
+ const required = new Set(inputSchema.required ?? []);
313
+ return Object.fromEntries(
314
+ Object.entries(args).filter(
315
+ ([name, value]) => value !== null || required.has(name)
316
+ )
317
+ );
318
+ }
311
319
  async function dispatchGeneratedTool(client, tool, args) {
312
320
  const rejected = validateArguments(tool.inputSchema, args);
313
321
  if (rejected) return rejected;
@@ -325,25 +333,16 @@ async function dispatchGeneratedTool(client, tool, args) {
325
333
  const value = args[param.name];
326
334
  query[param.name] = param.explode === false && Array.isArray(value) ? value.join(",") : value;
327
335
  }
328
- let requestBody = args.body;
329
- if (tool.hasBody && typeof requestBody === "string") {
330
- try {
331
- requestBody = JSON.parse(requestBody);
332
- } catch {
333
- return {
334
- ok: false,
335
- status: 400,
336
- payload: {
337
- error: "The body argument arrived as a string that is not valid JSON. Send body as an object."
338
- }
339
- };
340
- }
341
- }
342
- if (tool.hasBody && requestBody !== void 0) {
343
- const properties = tool.inputSchema.properties ?? {};
344
- const defs = tool.inputSchema.$defs ?? {};
345
- requestBody = coerceScalars(requestBody, properties.body, defs);
346
- }
336
+ const sent = tool.hasBody ? bodyAsSent(tool, args.body) : { body: args.body };
337
+ if (!sent)
338
+ return {
339
+ ok: false,
340
+ status: 400,
341
+ payload: {
342
+ error: "The body argument arrived as a string that is not valid JSON. Send body as an object."
343
+ }
344
+ };
345
+ const requestBody = sent.body;
347
346
  const verbs = client;
348
347
  const { data, error, response } = await verbs[tool.method](tool.path, {
349
348
  params: { path: pathParams, query },
@@ -356,6 +355,32 @@ async function dispatchGeneratedTool(client, tool, args) {
356
355
  payload: data ?? { status: response.status }
357
356
  };
358
357
  }
358
+ function bodyAsSent(tool, body) {
359
+ let parsed = body;
360
+ if (typeof parsed === "string") {
361
+ try {
362
+ parsed = JSON.parse(parsed);
363
+ } catch {
364
+ return null;
365
+ }
366
+ }
367
+ if (parsed === void 0) return { body: parsed };
368
+ const properties = tool.inputSchema.properties ?? {};
369
+ const defs = tool.inputSchema.$defs ?? {};
370
+ return { body: coerceScalars(parsed, properties.body, defs) };
371
+ }
372
+ function argsAsSent(tool, args) {
373
+ if (!tool.hasBody || args.body === void 0) return args;
374
+ const sent = bodyAsSent(tool, args.body);
375
+ return sent ? { ...args, body: sent.body } : null;
376
+ }
377
+ function omittedEmptyQueryArg(tool) {
378
+ const required = new Set(tool.inputSchema.required ?? []);
379
+ const query = new Set(
380
+ tool.params.filter((p) => p.location === "query").map((p) => p.name)
381
+ );
382
+ return (name, value) => query.has(name) && !required.has(name) && isEmptyValue(value);
383
+ }
359
384
  function isEmptyValue(value) {
360
385
  return value === null || value === "" || Array.isArray(value) && !value.length;
361
386
  }
@@ -2792,6 +2817,109 @@ var generatedTools = [
2792
2817
  }
2793
2818
  }
2794
2819
  },
2820
+ {
2821
+ name: "exec-pod",
2822
+ title: "Run a command in a Pod",
2823
+ operationId: "execPod",
2824
+ description: `Run one command inside a running Pod's container and get stdout, stderr and the exit code back. No SSH key, public IP or exposed port is needed. command is an argv array run without a shell: pass ["sh", "-c", "..."] for pipes, redirects, globs or &&. One call waits at most timeoutSeconds (1-25, default 20). timedOut true means the call stopped waiting, NOT that the command stopped: it keeps running in the container. For anything longer (model downloads, pip installs, builds), start it in the background and log to a file, e.g. ["sh", "-c", "nohup wget -c -O /workspace/m.safetensors URL > /tmp/dl.log 2>&1 &"], then check progress with later calls (ls -la, du -h, tail /tmp/dl.log). stdout and stderr are each capped at 1 MiB (truncated true). Right after a create, the Pod can answer 404 "Pod not found" for about 10 seconds even with the right id, then 409 while its container starts: retry in about 10 seconds, a few times at most. A Pod that is not running answers 409. A host not yet updated for exec answers 503. Reaches only Pods owned by the API key's own account. Commands can change or delete data in the Pod: run write commands only for what the user asked.`,
2825
+ directoryDescription: "Run one command inside a running Pod's container and return stdout, stderr and the exit code. The command is an argv array run without a shell. One call waits at most timeoutSeconds (1-25, default 20); a command still running then keeps running in the container, and the result says timedOut. stdout and stderr are each capped at 1 MiB. A Pod that is not running answers 409, and a host not yet updated for exec answers 503. Reaches only Pods owned by the caller's own account. The command can change or delete data in the Pod.",
2826
+ method: "POST",
2827
+ path: "/v2/pods/{id}/exec",
2828
+ params: [
2829
+ {
2830
+ name: "id",
2831
+ location: "path"
2832
+ }
2833
+ ],
2834
+ hasBody: true,
2835
+ annotations: {
2836
+ readOnlyHint: false,
2837
+ destructiveHint: true,
2838
+ idempotentHint: false,
2839
+ openWorldHint: true
2840
+ },
2841
+ inputSchema: {
2842
+ type: "object",
2843
+ properties: {
2844
+ id: {
2845
+ type: "string",
2846
+ description: "Pod identifier"
2847
+ },
2848
+ body: {
2849
+ $ref: "#/$defs/PodExecRequest",
2850
+ type: "object"
2851
+ }
2852
+ },
2853
+ required: ["id", "body"],
2854
+ $defs: {
2855
+ PodExecRequest: {
2856
+ type: "object",
2857
+ additionalProperties: false,
2858
+ required: ["command"],
2859
+ properties: {
2860
+ command: {
2861
+ type: "array",
2862
+ minItems: 1,
2863
+ maxItems: 256,
2864
+ prefixItems: [
2865
+ {
2866
+ type: "string",
2867
+ minLength: 1,
2868
+ maxLength: 4096,
2869
+ pattern: "^[^\\x00]*[^\\t-\\r \x85\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\\x00][^\\x00]*$"
2870
+ }
2871
+ ],
2872
+ items: {
2873
+ type: "string",
2874
+ maxLength: 4096,
2875
+ pattern: "^[^\\x00]*$"
2876
+ },
2877
+ description: 'Command to run, as an argv array of 1-256 arguments of at most 4096 characters each. It is run directly, never through a shell; for pipes, redirects or globbing pass `["sh", "-c", "..."]`.',
2878
+ examples: [
2879
+ ["nvidia-smi", "--query-gpu=name,memory.used", "--format=csv"]
2880
+ ]
2881
+ },
2882
+ timeoutSeconds: {
2883
+ type: "integer",
2884
+ minimum: 1,
2885
+ maximum: 25,
2886
+ default: 20,
2887
+ description: "How long the call waits for the command to finish, in seconds. A command still running at this point returns with `timedOut: true`.",
2888
+ examples: [10]
2889
+ },
2890
+ cwd: {
2891
+ type: "string",
2892
+ pattern: "^/[^\\x00-\\x1f\\x7f]*$",
2893
+ maxLength: 1024,
2894
+ description: "Absolute working directory for the command, at most 1024 characters and without control characters. Defaults to the image's `WORKDIR`.",
2895
+ examples: ["/workspace"]
2896
+ },
2897
+ env: {
2898
+ type: "object",
2899
+ maxProperties: 64,
2900
+ propertyNames: {
2901
+ type: "string",
2902
+ minLength: 1,
2903
+ maxLength: 256,
2904
+ pattern: "^[^=\\x00]+$"
2905
+ },
2906
+ additionalProperties: {
2907
+ type: "string",
2908
+ maxLength: 16384,
2909
+ pattern: "^[^\\x00]*$"
2910
+ },
2911
+ description: "Up to 64 environment variables added to the container's environment for this command only. A name is 1-256 characters and must not contain `=` or a NUL character; a value is at most 16384 characters and must not contain a NUL character.",
2912
+ examples: [
2913
+ {
2914
+ HF_HOME: "/workspace/hf"
2915
+ }
2916
+ ]
2917
+ }
2918
+ }
2919
+ }
2920
+ }
2921
+ }
2922
+ },
2795
2923
  {
2796
2924
  name: "get-cluster",
2797
2925
  title: "Get cluster",
@@ -6854,6 +6982,122 @@ var generatedTools = [
6854
6982
  }
6855
6983
  ];
6856
6984
 
6985
+ // src/specgen/validate.ts
6986
+ var import__ = require("ajv/dist/2020.js");
6987
+ function schemaValidationMode(value = process.env.MCP_SCHEMA_VALIDATION) {
6988
+ const mode = value?.trim().toLowerCase();
6989
+ return mode === "off" || mode === "enforce" ? mode : "shadow";
6990
+ }
6991
+ var ajv = new import__.Ajv2020({
6992
+ strict: false,
6993
+ allErrors: true,
6994
+ validateFormats: false
6995
+ });
6996
+ var compiled = /* @__PURE__ */ new WeakMap();
6997
+ function validatorFor(schema) {
6998
+ let validate = compiled.get(schema);
6999
+ if (!validate) {
7000
+ validate = ajv.compile(structuredClone(schema));
7001
+ compiled.set(schema, validate);
7002
+ }
7003
+ return validate;
7004
+ }
7005
+ var MAX_NULL_PASSES = 8;
7006
+ function isNullTypeError(error, data) {
7007
+ return error.keyword === "type" && error.instancePath !== "" && valueAt(data, error.instancePath) === null;
7008
+ }
7009
+ function pointerSegments(pointer) {
7010
+ return pointer.split("/").slice(1).map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
7011
+ }
7012
+ function valueAt(data, pointer) {
7013
+ let node = data;
7014
+ for (const key of pointerSegments(pointer)) {
7015
+ if (node === null || typeof node !== "object") return void 0;
7016
+ node = node[key];
7017
+ }
7018
+ return node;
7019
+ }
7020
+ function removeAt(data, pointer) {
7021
+ const segments = pointerSegments(pointer);
7022
+ const last = segments.pop();
7023
+ const parent = valueAt(data, segments.length ? `/${segments.join("/")}` : "");
7024
+ if (last === void 0 || parent === null || typeof parent !== "object")
7025
+ return false;
7026
+ if (Array.isArray(parent)) return false;
7027
+ delete parent[last];
7028
+ return true;
7029
+ }
7030
+ var WRAPPER_KEYWORDS = /* @__PURE__ */ new Set(["if"]);
7031
+ function toViolation(error) {
7032
+ const base = pointerSegments(error.instancePath);
7033
+ let path = base.join(".");
7034
+ let message = error.message ?? "is invalid";
7035
+ const params = error.params;
7036
+ if (error.keyword === "required" && typeof params.missingProperty === "string") {
7037
+ path = [...base, params.missingProperty].join(".");
7038
+ message = "is required";
7039
+ } else if (error.keyword === "additionalProperties" && typeof params.additionalProperty === "string") {
7040
+ path = [...base, params.additionalProperty].join(".");
7041
+ message = "is not an accepted argument here";
7042
+ } else if (error.keyword === "pattern") {
7043
+ message = "is not in an accepted format";
7044
+ }
7045
+ const allowed = error.keyword === "enum" ? params.allowedValues : error.keyword === "const" ? [params.allowedValue] : void 0;
7046
+ return {
7047
+ path,
7048
+ keyword: error.keyword,
7049
+ message,
7050
+ ...allowed ? { allowed } : {},
7051
+ schemaPath: error.schemaPath
7052
+ };
7053
+ }
7054
+ function schemaViolations(schema, args, omitted = () => false) {
7055
+ const validate = validatorFor(schema);
7056
+ const data = structuredClone(
7057
+ Object.fromEntries(
7058
+ Object.entries(args).filter(([name, value]) => !omitted(name, value))
7059
+ )
7060
+ );
7061
+ for (let pass = 0; ; pass++) {
7062
+ if (validate(data)) return [];
7063
+ const errors = validate.errors ?? [];
7064
+ let removed = false;
7065
+ if (pass < MAX_NULL_PASSES) {
7066
+ for (const error of errors.filter((e) => isNullTypeError(e, data)))
7067
+ removed = removeAt(data, error.instancePath) || removed;
7068
+ }
7069
+ if (!removed) {
7070
+ return errors.filter((e) => !WRAPPER_KEYWORDS.has(e.keyword)).map(toViolation);
7071
+ }
7072
+ }
7073
+ }
7074
+ var INVALID_ARGUMENTS_HINT = "Fix the listed fields and call again.";
7075
+ function invalidArgumentsPayload(violations) {
7076
+ return {
7077
+ error: "Invalid arguments",
7078
+ code: "invalid_arguments",
7079
+ fields: violations.map(({ path, message, allowed }) => ({
7080
+ path,
7081
+ problem: message,
7082
+ ...allowed ? { allowed } : {}
7083
+ })),
7084
+ hint: INVALID_ARGUMENTS_HINT
7085
+ };
7086
+ }
7087
+ function logSchemaViolations(tool, mode, violations) {
7088
+ console.error(
7089
+ "schema_violation",
7090
+ JSON.stringify({
7091
+ tool,
7092
+ mode,
7093
+ violations: violations.map(({ keyword, schemaPath }) => ({
7094
+ keyword,
7095
+ schemaPath
7096
+ }))
7097
+ })
7098
+ );
7099
+ }
7100
+
6857
7101
  // src/specgen/tools/annotations.ts
6858
7102
  var readOnly = {
6859
7103
  readOnlyHint: true,
@@ -7068,6 +7312,8 @@ var DIRECTORY_TOOLS = {
7068
7312
  "deploy-hub-repo": IN,
7069
7313
  "detach-cost-center-resource": IN,
7070
7314
  "endpoint-health": IN,
7315
+ // env can carry tokens (HF_TOKEN for a model download), so the directory omits it.
7316
+ "exec-pod": { include: true, omitInputs: ["body.env"] },
7071
7317
  "get-capacity": IN,
7072
7318
  "get-cluster": IN,
7073
7319
  "get-cost-center": IN,
@@ -7187,6 +7433,21 @@ function directoryOmittedInputs(name) {
7187
7433
  const decision = DIRECTORY_TOOLS[name];
7188
7434
  return decision?.include ? decision.omitInputs ?? [] : [];
7189
7435
  }
7436
+ function omittedInputsIn(omit, args) {
7437
+ return omit.filter((key) => {
7438
+ const [parent, child] = key.split(".");
7439
+ if (child === void 0) return parent in args;
7440
+ let value = args[parent];
7441
+ if (typeof value === "string") {
7442
+ try {
7443
+ value = JSON.parse(value);
7444
+ } catch {
7445
+ return false;
7446
+ }
7447
+ }
7448
+ return !!value && typeof value === "object" && child in value;
7449
+ });
7450
+ }
7190
7451
  function profileForPath(pathname) {
7191
7452
  return pathname.replace(/\/+$/, "") === DIRECTORY_PATH ? "directory" : "default";
7192
7453
  }
@@ -9961,23 +10222,40 @@ function createRunpodClient(options = {}) {
9961
10222
  // src/specgen/clients/sse.ts
9962
10223
  var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
9963
10224
  var LOG_STREAM_MAX_BYTES = 256 * 1024;
10225
+ var LOG_STREAM_CONNECT_TIMEOUT_MS = 1e4;
10226
+ var LOG_SNAPSHOT_MAX_CHARS = 32e3;
9964
10227
  function createSseReader(options = {}) {
9965
10228
  const apiKey = options.apiKey ?? process.env.RUNPOD_API_KEY;
9966
10229
  const fetchImpl = options.fetchImpl ?? fetch;
10230
+ const connectTimeoutMs = options.connectTimeoutMs ?? LOG_STREAM_CONNECT_TIMEOUT_MS;
9967
10231
  return async (url, opts) => {
9968
10232
  if (!apiKey) throw missingKeyError();
9969
10233
  const chunks = [];
9970
10234
  let bytes = 0;
9971
10235
  let truncated = false;
9972
10236
  let streamEstablished = false;
10237
+ const connect = new AbortController();
10238
+ const connectTimer = setTimeout(
10239
+ () => connect.abort(
10240
+ new DOMException(
10241
+ `Runpod log stream sent no headers within ${connectTimeoutMs}ms`,
10242
+ "TimeoutError"
10243
+ )
10244
+ ),
10245
+ connectTimeoutMs
10246
+ );
9973
10247
  try {
9974
- const response = await createDeadlineFetch(fetchImpl, opts.maxWaitMs)(url, {
10248
+ const response = await createDeadlineFetch(
10249
+ fetchImpl,
10250
+ connectTimeoutMs + opts.maxWaitMs
10251
+ )(url, {
9975
10252
  method: "GET",
9976
10253
  headers: {
9977
10254
  Authorization: `Bearer ${apiKey}`,
9978
10255
  Accept: "text/event-stream"
9979
- }
9980
- });
10256
+ },
10257
+ signal: connect.signal
10258
+ }).finally(() => clearTimeout(connectTimer));
9981
10259
  if (!response.ok) {
9982
10260
  const body = await response.text().catch(() => "");
9983
10261
  throw new HttpError(
@@ -9988,14 +10266,31 @@ function createSseReader(options = {}) {
9988
10266
  }
9989
10267
  streamEstablished = true;
9990
10268
  if (response.body) {
9991
- for await (const chunk of response.body) {
9992
- const boundedChunk = chunk.subarray(0, opts.maxBytes - bytes);
9993
- chunks.push(boundedChunk);
9994
- bytes += boundedChunk.length;
9995
- if (bytes >= opts.maxBytes) {
9996
- truncated = true;
9997
- break;
10269
+ const reader = response.body.getReader();
10270
+ let timer;
10271
+ const windowClosed = new Promise((resolve) => {
10272
+ timer = setTimeout(() => resolve(null), opts.maxWaitMs);
10273
+ });
10274
+ try {
10275
+ for (; ; ) {
10276
+ const read = reader.read();
10277
+ read.catch(() => {
10278
+ });
10279
+ const next = await Promise.race([read, windowClosed]);
10280
+ if (next === null || next.done) break;
10281
+ const boundedChunk = next.value.subarray(0, opts.maxBytes - bytes);
10282
+ chunks.push(boundedChunk);
10283
+ bytes += boundedChunk.length;
10284
+ if (bytes >= opts.maxBytes) {
10285
+ truncated = true;
10286
+ break;
10287
+ }
9998
10288
  }
10289
+ } finally {
10290
+ clearTimeout(timer);
10291
+ void reader.cancel().catch(() => {
10292
+ });
10293
+ reader.releaseLock();
9999
10294
  }
10000
10295
  }
10001
10296
  } catch (err) {
@@ -10021,7 +10316,80 @@ async function collectLogSnapshot(reader, logsUrl, params) {
10021
10316
  for await (const event of iterateLogEvents(new Response(raw).body)) {
10022
10317
  items.push(event.data);
10023
10318
  }
10024
- return { items, count: items.length, truncated };
10319
+ return fitSnapshot(items, truncated);
10320
+ }
10321
+ var serializedLength = (snapshot) => JSON.stringify(snapshot, null, 2).length;
10322
+ var LOG_ENTRY_MAX_CHARS = 16e3;
10323
+ var CLIPPABLE_KEYS = ["line", "raw"];
10324
+ function clipText(text, keep) {
10325
+ const code = text.charCodeAt(keep - 1);
10326
+ if (keep > 0 && code >= 55296 && code <= 56319) keep--;
10327
+ return `${text.slice(0, keep)}\u2026 [clipped ${text.length - keep} chars]`;
10328
+ }
10329
+ function clipEntry(entry) {
10330
+ let clipped = false;
10331
+ const out = { ...entry };
10332
+ for (const key of CLIPPABLE_KEYS) {
10333
+ const text = out[key];
10334
+ if (typeof text === "string" && text.length > LOG_ENTRY_MAX_CHARS) {
10335
+ out[key] = clipText(text, LOG_ENTRY_MAX_CHARS);
10336
+ clipped = true;
10337
+ }
10338
+ }
10339
+ return { entry: out, clipped };
10340
+ }
10341
+ function fitEntry(entry, maxChars) {
10342
+ const size = (e) => serializedLength({ items: [e], count: 1, truncated: false });
10343
+ const out = { ...entry };
10344
+ const keys = Object.keys(out).filter((key) => key !== "ts" && typeof out[key] === "string").sort((a, b) => out[b].length - out[a].length);
10345
+ for (const key of keys) {
10346
+ if (size(out) <= maxChars) break;
10347
+ const text = entry[key];
10348
+ let lo = 0;
10349
+ let hi = Math.min(text.length, LOG_ENTRY_MAX_CHARS);
10350
+ while (lo < hi) {
10351
+ const mid = Math.ceil((lo + hi) / 2);
10352
+ if (size({ ...out, [key]: clipText(text, mid) }) <= maxChars)
10353
+ lo = mid;
10354
+ else hi = mid - 1;
10355
+ }
10356
+ out[key] = clipText(text, lo);
10357
+ }
10358
+ return out;
10359
+ }
10360
+ var entryTs = (entry) => entry?.ts;
10361
+ function fitSnapshot(items, truncated, maxChars = LOG_SNAPSHOT_MAX_CHARS) {
10362
+ let cut = truncated;
10363
+ const entries = items.map((item) => {
10364
+ const { entry, clipped } = clipEntry(item);
10365
+ if (clipped) cut = true;
10366
+ return entry;
10367
+ });
10368
+ const snapshot = (n, isCut) => ({
10369
+ items: entries.slice(0, n),
10370
+ count: n,
10371
+ truncated: isCut
10372
+ });
10373
+ const whole = snapshot(entries.length, cut);
10374
+ if (entries.length === 0 || serializedLength(whole) <= maxChars) return whole;
10375
+ if (serializedLength(snapshot(1, true)) > maxChars) {
10376
+ entries[0] = fitEntry(items[0], maxChars);
10377
+ if (entries.length === 1) return snapshot(1, true);
10378
+ }
10379
+ let lo = 1;
10380
+ let hi = entries.length - 1;
10381
+ while (lo < hi) {
10382
+ const mid = Math.ceil((lo + hi) / 2);
10383
+ if (serializedLength(snapshot(mid, true)) <= maxChars) lo = mid;
10384
+ else hi = mid - 1;
10385
+ }
10386
+ let keep = lo;
10387
+ const omittedTs = entryTs(entries[keep]);
10388
+ if (omittedTs !== void 0) {
10389
+ while (keep > 0 && entryTs(entries[keep - 1]) === omittedTs) keep--;
10390
+ if (keep === 0) keep = lo;
10391
+ }
10392
+ return snapshot(keep, true);
10025
10393
  }
10026
10394
 
10027
10395
  // src/specgen/tools/logs.ts
@@ -10039,13 +10407,13 @@ var logStreamProperties = {
10039
10407
  },
10040
10408
  since: {
10041
10409
  type: "string",
10042
- description: "RFC3339 timestamp to resume from; when set, `tail` is ignored."
10410
+ description: "RFC3339 timestamp to resume after (exclusive). Replays container lines only; system lines stream live from the call. When set, `tail` is ignored."
10043
10411
  },
10044
10412
  maxWaitMs: {
10045
10413
  type: "integer",
10046
10414
  minimum: 500,
10047
10415
  maximum: 3e4,
10048
- description: "How long to read the stream, in ms (default 5000, max 30000)"
10416
+ description: "How long to read the stream once it opens, in ms (default 5000, max 30000)"
10049
10417
  }
10050
10418
  };
10051
10419
  function baseUrl() {
@@ -10071,7 +10439,7 @@ var streamPodLogs = {
10071
10439
  title: "Stream Pod logs",
10072
10440
  holdsResponse: true,
10073
10441
  annotations: readOnly,
10074
- 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.",
10442
+ 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 size cap cut the output. For later container lines, call again with `since` set to the last container line's `ts`; `since` does not replay system lines, so for those request a smaller `tail`.",
10075
10443
  inputSchema: {
10076
10444
  type: "object",
10077
10445
  properties: {
@@ -10093,10 +10461,10 @@ var streamPodLogs = {
10093
10461
  var streamWorkerLogs = {
10094
10462
  name: "stream-worker-logs",
10095
10463
  title: "Stream worker logs",
10096
- directoryDescription: "Read a bounded snapshot of a Serverless worker's logs (container, system, or both). Holds the live stream open for maxWaitMs and returns the parsed log lines; truncated: true means the byte cap cut the output. A worker whose container keeps failing to start can report RUNNING or THROTTLED while its logs show the failure.",
10464
+ directoryDescription: "Read a bounded snapshot of a Serverless worker's logs (container, system, or both). Holds the live stream open for maxWaitMs and returns the parsed log lines; truncated: true means the size cap cut the output. A worker whose container keeps failing to start can report RUNNING or THROTTLED while its logs show the failure.",
10097
10465
  holdsResponse: true,
10098
10466
  annotations: readOnly,
10099
- 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.",
10467
+ 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 size cap cut the output. For later container lines, call again with `since` set to the last container line's `ts`; `since` does not replay system lines, so for those request a smaller `tail`. 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.",
10100
10468
  inputSchema: {
10101
10469
  type: "object",
10102
10470
  properties: {
@@ -10240,6 +10608,7 @@ var listPublicEndpoints = {
10240
10608
  // src/specgen/tools/knowledge.ts
10241
10609
  var import_plugin_knowledge = require("@runpod/plugin-knowledge");
10242
10610
  var knowledge = (0, import_plugin_knowledge.loadKnowledge)();
10611
+ var resolveGuideId = (id) => knowledge.aliases?.[id] ?? id;
10243
10612
  var hiddenRules = new Set(
10244
10613
  (process.env.KNOWLEDGE_HIDDEN_RULES ?? "").split(",").map((id) => id.trim()).filter(Boolean)
10245
10614
  );
@@ -10416,7 +10785,9 @@ var readGuide = {
10416
10785
  title: "Read guide",
10417
10786
  annotations: bundledRead,
10418
10787
  recordedArguments: ["id"],
10419
- recordedGuideKind: (args) => knowledge.guides.find((guide) => guide.id === String(args.id).trim())?.kind,
10788
+ recordedGuideKind: (args) => knowledge.guides.find(
10789
+ (guide) => guide.id === resolveGuideId(String(args.id).trim())
10790
+ )?.kind,
10420
10791
  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").',
10421
10792
  inputSchema: {
10422
10793
  type: "object",
@@ -10428,7 +10799,9 @@ var readGuide = {
10428
10799
  },
10429
10800
  handler: async (_ctx, args) => {
10430
10801
  const id = String(args.id).trim();
10431
- const guide = knowledge.guides.find((candidate) => candidate.id === id);
10802
+ const guide = knowledge.guides.find(
10803
+ (candidate) => candidate.id === resolveGuideId(id)
10804
+ );
10432
10805
  if (!guide) {
10433
10806
  const close = knowledge.guides.filter(
10434
10807
  (candidate) => candidate.id.includes(id) || id.includes(candidate.id)
@@ -10583,6 +10956,12 @@ var guideResources = knowledge.guides.map((guide) => ({
10583
10956
  mimeType: "text/markdown",
10584
10957
  text: guide.body
10585
10958
  }));
10959
+ var findGuideResource = (resources, uri) => {
10960
+ const id = uri.startsWith(GUIDE_URI_PREFIX) ? resolveGuideId(uri.slice(GUIDE_URI_PREFIX.length)) : void 0;
10961
+ return resources.find(
10962
+ (resource) => resource.uri === uri || resource.uri === GUIDE_URI_PREFIX + id
10963
+ );
10964
+ };
10586
10965
 
10587
10966
  // src/specgen/prose.ts
10588
10967
  var ABBREVIATIONS = ["e.g.", "i.e.", "etc.", "vs."];
@@ -10658,13 +11037,40 @@ function withoutCredentialExamples(schema) {
10658
11037
  };
10659
11038
  return strip(schema);
10660
11039
  }
10661
- function withoutInputs(schema, omit) {
10662
- if (!omit.length) return schema;
11040
+ function withoutKeys(schema, omit) {
10663
11041
  const properties = { ...schema.properties ?? {} };
10664
11042
  for (const key of omit) delete properties[key];
10665
11043
  const required = Array.isArray(schema.required) ? schema.required.filter((key) => !omit.includes(key)) : schema.required;
10666
11044
  return { ...schema, properties, ...required ? { required } : {} };
10667
11045
  }
11046
+ function withoutInputs(schema, omit) {
11047
+ if (!omit.length) return schema;
11048
+ let out = withoutKeys(
11049
+ schema,
11050
+ omit.filter((key) => !key.includes("."))
11051
+ );
11052
+ for (const key of omit.filter((k) => k.includes("."))) {
11053
+ const [parent, child] = key.split(".");
11054
+ const properties = out.properties ?? {};
11055
+ const node = properties[parent];
11056
+ if (!node) continue;
11057
+ const ref = typeof node.$ref === "string" ? node.$ref : void 0;
11058
+ const defName = ref?.startsWith("#/$defs/") ? ref.slice(8) : void 0;
11059
+ const defs = out.$defs ?? {};
11060
+ if (defName && defs[defName]) {
11061
+ out = {
11062
+ ...out,
11063
+ $defs: { ...defs, [defName]: withoutKeys(defs[defName], [child]) }
11064
+ };
11065
+ } else {
11066
+ out = {
11067
+ ...out,
11068
+ properties: { ...properties, [parent]: withoutKeys(node, [child]) }
11069
+ };
11070
+ }
11071
+ }
11072
+ return out;
11073
+ }
10668
11074
  function callShape(schema) {
10669
11075
  const defs = schema.$defs ?? {};
10670
11076
  const resolve = (node) => {
@@ -10766,7 +11172,7 @@ function words2(name) {
10766
11172
 
10767
11173
  // src/server.ts
10768
11174
  var SERVER_NAME = "Runpod API Server";
10769
- var SERVER_VERSION = true ? "4.10.0" : "dev";
11175
+ var SERVER_VERSION = true ? "4.11.0" : "dev";
10770
11176
 
10771
11177
  // src/specgen/analytics.ts
10772
11178
  var import_node_crypto = require("crypto");
@@ -10834,6 +11240,7 @@ function captureToolCall(apiKey, event) {
10834
11240
  server_version: event.serverVersion,
10835
11241
  ...event.clientName ? { client_name: event.clientName } : {},
10836
11242
  ...event.guideKind ? { guide_kind: event.guideKind } : {},
11243
+ ...event.errorClass ? { error_class: event.errorClass } : {},
10837
11244
  ...event.blockedBy ? { blocked_by: event.blockedBy } : {},
10838
11245
  ...event.profile ? { profile: event.profile } : {},
10839
11246
  ...args
@@ -11367,7 +11774,7 @@ These tools manage infrastructure only. They do not do SSH sessions, file transf
11367
11774
 
11368
11775
  OFFICIAL GUIDES AND FACTS. list-guides and read-guide serve the official Runpod plugin's skills, reference docs and live-verified golden paths, so you have them even without the plugin installed. For a multi-step or billable task, look for a matching golden path first. lookup-concept and search-concepts answer exact questions about how Runpod works (what a stopped pod keeps, where a volume lives) with the public source for each fact.
11369
11776
 
11370
- PLAYBOOKS \u2014 READ BEFORE ACTING. Each Runpod task has a journey playbook carrying its procedure, pitfalls and report format. Before the FIRST Runpod tool call of a session, load the plugin's runpod-mcp skill if you have it, otherwise read-guide runpod-mcp (also the runpod://guides/runpod-mcp resource): it routes the request to a journey guide \u2014 deploying an endpoint reads serverless-deploy, diagnosing a broken pod reads pod-doctor, cost questions read cost-audit, and so on \u2014 and states the answer contract. A reply produced without the routed playbook loaded is out of contract.
11777
+ PLAYBOOKS \u2014 READ BEFORE ACTING. Each Runpod task has a journey playbook carrying its procedure, pitfalls and report format. Before the FIRST Runpod tool call of a session, load the plugin's runpod-mcp skill if you have it, otherwise read-guide runpod-mcp (also the runpod://guides/runpod-mcp resource): it routes the request to a journey guide \u2014 deploying an endpoint reads runpod-serverless-deploy, diagnosing a broken pod reads runpod-pod-doctor, cost questions read runpod-cost-audit, and so on \u2014 and states the answer contract. A reply produced without the routed playbook loaded is out of contract.
11371
11778
 
11372
11779
  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.
11373
11780
 
@@ -11386,6 +11793,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11386
11793
  const caller = callerId(ctx.apiKey);
11387
11794
  const profile = opts.profile ?? "default";
11388
11795
  const directory = profile === "directory";
11796
+ const schemaMode = schemaValidationMode();
11389
11797
  const alp = directory ? void 0 : opts.alp;
11390
11798
  const servedGeneratedTools = directory ? generatedTools.filter((tool) => directoryIncludes(tool.name)) : generatedTools;
11391
11799
  const servedCuratedTools = [
@@ -11428,9 +11836,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11428
11836
  resourceTemplates: []
11429
11837
  }));
11430
11838
  server.setRequestHandler(import_types.ReadResourceRequestSchema, async (request) => {
11431
- const guide = servedGuides.find(
11432
- (candidate) => candidate.uri === request.params.uri
11433
- );
11839
+ const guide = findGuideResource(servedGuides, request.params.uri);
11434
11840
  if (opts.analytics) {
11435
11841
  captureResourceCall(ctx.apiKey, {
11436
11842
  method: "resources/read",
@@ -11467,6 +11873,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11467
11873
  ok: false,
11468
11874
  status: 429,
11469
11875
  durationMs: 0,
11876
+ errorClass: "rate_limited",
11470
11877
  ...verdict.reason ? { blockedBy: verdict.reason } : {},
11471
11878
  profile,
11472
11879
  ...opts.analytics
@@ -11492,7 +11899,8 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11492
11899
  const generated = servedGeneratedTools.find(
11493
11900
  (tool) => tool.name === request.params.name
11494
11901
  );
11495
- const omitted = directory ? directoryOmittedInputs(request.params.name).filter((key) => key in args) : [];
11902
+ const curatedArgs = curated ? withoutNullOptionals(curated.inputSchema, args) : args;
11903
+ const omitted = directory ? omittedInputsIn(directoryOmittedInputs(request.params.name), args) : [];
11496
11904
  const rejected = omitted.length ? {
11497
11905
  ok: false,
11498
11906
  status: 400,
@@ -11501,10 +11909,28 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11501
11909
  }
11502
11910
  } : curated && !curated.lenientArguments ? validateArguments(
11503
11911
  curated.inputSchema,
11504
- args
11505
- ) : null;
11506
- const result = rejected ?? await runTool(
11507
- () => curated ? curated.handler(ctx, args) : dispatchTool(generated, request.params.name, args)
11912
+ curatedArgs
11913
+ ) : generated ? validateArguments(generated.inputSchema, args) : null;
11914
+ let schemaRejected = null;
11915
+ if (!rejected && schemaMode !== "off") {
11916
+ const sent = generated ? argsAsSent(generated, args) : null;
11917
+ const violations = curated ? curated.lenientArguments ? [] : schemaViolations(curated.inputSchema, curatedArgs) : generated && sent ? schemaViolations(
11918
+ generated.inputSchema,
11919
+ sent,
11920
+ omittedEmptyQueryArg(generated)
11921
+ ) : [];
11922
+ if (violations.length) {
11923
+ logSchemaViolations(request.params.name, schemaMode, violations);
11924
+ if (schemaMode === "enforce")
11925
+ schemaRejected = {
11926
+ ok: false,
11927
+ status: 400,
11928
+ payload: invalidArgumentsPayload(violations)
11929
+ };
11930
+ }
11931
+ }
11932
+ const result = rejected ?? schemaRejected ?? await runTool(
11933
+ () => curated ? curated.handler(ctx, curatedArgs) : dispatchTool(generated, request.params.name, args)
11508
11934
  );
11509
11935
  logToolCall({
11510
11936
  tool: request.params.name,
@@ -11519,6 +11945,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11519
11945
  ok: result.ok,
11520
11946
  status: result.status,
11521
11947
  durationMs: Date.now() - startedAt,
11948
+ ...result.ok ? {} : { errorClass: errorClass(result) },
11522
11949
  // The directory profile records which tool ran, never what was asked.
11523
11950
  ...directory ? {} : {
11524
11951
  args: recordedArgumentValues(curated?.recordedArguments, args),
@@ -11558,24 +11985,50 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11558
11985
  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.";
11559
11986
  var DETAIL_HINTS = [
11560
11987
  [
11561
- /no longer any instances available|not enough free GPUs/i,
11562
- "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."
11988
+ /no longer any instances available|no instances currently available|not enough free GPUs/i,
11989
+ "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.",
11990
+ "capacity"
11563
11991
  ],
11564
11992
  [
11565
11993
  /at least \$\d|insufficient (?:funds|balance)|\bbalance\b/i,
11566
- "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."
11994
+ "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.",
11995
+ "balance"
11567
11996
  ],
11568
11997
  [
11569
11998
  /mutually exclusive|cannot be combined with/i,
11570
- "Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else."
11999
+ "Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else.",
12000
+ "conflicting_args"
12001
+ ],
12002
+ // A 400 with a bare "Unauthorized" detail from a write the same key can
12003
+ // otherwise make is upstream's answer for an unfunded account.
12004
+ [
12005
+ /^\s*unauthorized\s*$/i,
12006
+ "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.",
12007
+ "balance"
11571
12008
  ]
11572
12009
  ];
12010
+ function errorClass(result) {
12011
+ const { status } = result;
12012
+ const payload = asRecord(result.payload) ?? { error: result.payload };
12013
+ const detail = detailText(payload);
12014
+ if (status === 400 && detail) {
12015
+ for (const [pattern, , cls] of DETAIL_HINTS)
12016
+ if (pattern.test(detail)) return cls;
12017
+ }
12018
+ if (status === 400 || status === 422) return "validation";
12019
+ if (status === 401) return "auth";
12020
+ if (status === 402) return "balance";
12021
+ if (status === 403) return "permission";
12022
+ if (status === 404) return "not_found";
12023
+ if (status === 429) return "rate_limited";
12024
+ if (status === 504) return "timeout";
12025
+ if (status >= 500) return "server";
12026
+ return "other";
12027
+ }
11573
12028
  function errorHint(status, detail) {
11574
12029
  if (status === 400 && detail) {
11575
12030
  for (const [pattern, hint] of DETAIL_HINTS)
11576
12031
  if (pattern.test(detail)) return hint;
11577
- if (/^unauthorized$/i.test(detail.trim()))
11578
- 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.";
11579
12032
  }
11580
12033
  if (status === 400 || status === 422) return SHAPE_HINT;
11581
12034
  if (status === 401) return "The Runpod API key is missing or invalid.";
@@ -11815,6 +12268,7 @@ function createToolContext(options = {}) {
11815
12268
 
11816
12269
  // src/specgen/not-found-limiter.ts
11817
12270
  var SINK_TIMEOUT_MS = 3e3;
12271
+ var LOOP_LIMITED_TOOLS = /* @__PURE__ */ new Set(["get-pod"]);
11818
12272
  var NOT_FOUND_LOOP = "not_found_loop";
11819
12273
  var blocked = /* @__PURE__ */ new Map();
11820
12274
  function notFoundSinkUrl(submitUrl) {
@@ -11839,7 +12293,7 @@ function createNotFoundLimiter(opts) {
11839
12293
  };
11840
12294
  },
11841
12295
  onToolResult: (tool, status) => {
11842
- if (status !== 404) return;
12296
+ if (status !== 404 || !LOOP_LIMITED_TOOLS.has(tool)) return;
11843
12297
  const pair = `${opts.key}|${tool}`;
11844
12298
  void fetchImpl(opts.sinkUrl, {
11845
12299
  method: "POST",