@runpod/mcp-server 4.10.1 → 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/hosted.mjs CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  handleMcpRequest,
11
11
  parseSearchRequest,
12
12
  searchKnowledge
13
- } from "./chunk-AKX5ZMCL.mjs";
13
+ } from "./chunk-4M7QDHK4.mjs";
14
14
  import {
15
15
  ALP_REFERRAL_SOURCES,
16
16
  ALP_REFERRAL_USE_CASES,
@@ -20,7 +20,7 @@ import {
20
20
  } from "./chunk-577IYCJT.mjs";
21
21
  import {
22
22
  DIRECTORY_PATH
23
- } from "./chunk-FQJFKWUV.mjs";
23
+ } from "./chunk-24UOECYY.mjs";
24
24
 
25
25
  // src/alp/ingest.ts
26
26
  var MAX_CONTENT_CHARS = 2e4;
package/dist/http.cjs CHANGED
@@ -333,25 +333,16 @@ async function dispatchGeneratedTool(client, tool, args) {
333
333
  const value = args[param.name];
334
334
  query[param.name] = param.explode === false && Array.isArray(value) ? value.join(",") : value;
335
335
  }
336
- let requestBody = args.body;
337
- if (tool.hasBody && typeof requestBody === "string") {
338
- try {
339
- requestBody = JSON.parse(requestBody);
340
- } catch {
341
- return {
342
- ok: false,
343
- status: 400,
344
- payload: {
345
- error: "The body argument arrived as a string that is not valid JSON. Send body as an object."
346
- }
347
- };
348
- }
349
- }
350
- if (tool.hasBody && requestBody !== void 0) {
351
- const properties = tool.inputSchema.properties ?? {};
352
- const defs = tool.inputSchema.$defs ?? {};
353
- requestBody = coerceScalars(requestBody, properties.body, defs);
354
- }
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;
355
346
  const verbs = client;
356
347
  const { data, error, response } = await verbs[tool.method](tool.path, {
357
348
  params: { path: pathParams, query },
@@ -364,6 +355,32 @@ async function dispatchGeneratedTool(client, tool, args) {
364
355
  payload: data ?? { status: response.status }
365
356
  };
366
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
+ }
367
384
  function isEmptyValue(value) {
368
385
  return value === null || value === "" || Array.isArray(value) && !value.length;
369
386
  }
@@ -2800,6 +2817,109 @@ var generatedTools = [
2800
2817
  }
2801
2818
  }
2802
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
+ },
2803
2923
  {
2804
2924
  name: "get-cluster",
2805
2925
  title: "Get cluster",
@@ -6862,6 +6982,122 @@ var generatedTools = [
6862
6982
  }
6863
6983
  ];
6864
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
+
6865
7101
  // src/specgen/tools/annotations.ts
6866
7102
  var readOnly = {
6867
7103
  readOnlyHint: true,
@@ -7076,6 +7312,8 @@ var DIRECTORY_TOOLS = {
7076
7312
  "deploy-hub-repo": IN,
7077
7313
  "detach-cost-center-resource": IN,
7078
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"] },
7079
7317
  "get-capacity": IN,
7080
7318
  "get-cluster": IN,
7081
7319
  "get-cost-center": IN,
@@ -7195,6 +7433,21 @@ function directoryOmittedInputs(name) {
7195
7433
  const decision = DIRECTORY_TOOLS[name];
7196
7434
  return decision?.include ? decision.omitInputs ?? [] : [];
7197
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
+ }
7198
7451
  function profileForPath(pathname) {
7199
7452
  return pathname.replace(/\/+$/, "") === DIRECTORY_PATH ? "directory" : "default";
7200
7453
  }
@@ -10784,13 +11037,40 @@ function withoutCredentialExamples(schema) {
10784
11037
  };
10785
11038
  return strip(schema);
10786
11039
  }
10787
- function withoutInputs(schema, omit) {
10788
- if (!omit.length) return schema;
11040
+ function withoutKeys(schema, omit) {
10789
11041
  const properties = { ...schema.properties ?? {} };
10790
11042
  for (const key of omit) delete properties[key];
10791
11043
  const required = Array.isArray(schema.required) ? schema.required.filter((key) => !omit.includes(key)) : schema.required;
10792
11044
  return { ...schema, properties, ...required ? { required } : {} };
10793
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
+ }
10794
11074
  function callShape(schema) {
10795
11075
  const defs = schema.$defs ?? {};
10796
11076
  const resolve = (node) => {
@@ -10892,7 +11172,7 @@ function words2(name) {
10892
11172
 
10893
11173
  // src/server.ts
10894
11174
  var SERVER_NAME = "Runpod API Server";
10895
- var SERVER_VERSION = true ? "4.10.1" : "dev";
11175
+ var SERVER_VERSION = true ? "4.11.0" : "dev";
10896
11176
 
10897
11177
  // src/specgen/analytics.ts
10898
11178
  var import_node_crypto = require("crypto");
@@ -11513,6 +11793,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11513
11793
  const caller = callerId(ctx.apiKey);
11514
11794
  const profile = opts.profile ?? "default";
11515
11795
  const directory = profile === "directory";
11796
+ const schemaMode = schemaValidationMode();
11516
11797
  const alp = directory ? void 0 : opts.alp;
11517
11798
  const servedGeneratedTools = directory ? generatedTools.filter((tool) => directoryIncludes(tool.name)) : generatedTools;
11518
11799
  const servedCuratedTools = [
@@ -11619,7 +11900,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11619
11900
  (tool) => tool.name === request.params.name
11620
11901
  );
11621
11902
  const curatedArgs = curated ? withoutNullOptionals(curated.inputSchema, args) : args;
11622
- const omitted = directory ? directoryOmittedInputs(request.params.name).filter((key) => key in args) : [];
11903
+ const omitted = directory ? omittedInputsIn(directoryOmittedInputs(request.params.name), args) : [];
11623
11904
  const rejected = omitted.length ? {
11624
11905
  ok: false,
11625
11906
  status: 400,
@@ -11629,8 +11910,26 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11629
11910
  } : curated && !curated.lenientArguments ? validateArguments(
11630
11911
  curated.inputSchema,
11631
11912
  curatedArgs
11632
- ) : null;
11633
- const result = rejected ?? await runTool(
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(
11634
11933
  () => curated ? curated.handler(ctx, curatedArgs) : dispatchTool(generated, request.params.name, args)
11635
11934
  );
11636
11935
  logToolCall({
@@ -11969,6 +12268,7 @@ function createToolContext(options = {}) {
11969
12268
 
11970
12269
  // src/specgen/not-found-limiter.ts
11971
12270
  var SINK_TIMEOUT_MS = 3e3;
12271
+ var LOOP_LIMITED_TOOLS = /* @__PURE__ */ new Set(["get-pod"]);
11972
12272
  var NOT_FOUND_LOOP = "not_found_loop";
11973
12273
  var blocked = /* @__PURE__ */ new Map();
11974
12274
  function notFoundSinkUrl(submitUrl) {
@@ -11993,7 +12293,7 @@ function createNotFoundLimiter(opts) {
11993
12293
  };
11994
12294
  },
11995
12295
  onToolResult: (tool, status) => {
11996
- if (status !== 404) return;
12296
+ if (status !== 404 || !LOOP_LIMITED_TOOLS.has(tool)) return;
11997
12297
  const pair = `${opts.key}|${tool}`;
11998
12298
  void fetchImpl(opts.sinkUrl, {
11999
12299
  method: "POST",