@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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @runpod/mcp-server
2
2
 
3
+ ## 4.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 00dc801: Check every tool call against the tool's full input schema: types, enums, ranges and nested fields, not just top-level required and unknown arguments. By default this runs in shadow mode and only logs a `schema_violation` line (schema keywords and locations, never argument values), so no call that works today is blocked. Set `MCP_SCHEMA_VALIDATION=enforce` to reject an invalid call before any request is sent, with an `invalid_arguments` error naming each field's path, the problem, and the allowed values. With enforcement on, a shape error is no longer hidden behind an authentication failure. A `null` sent for an optional argument at any depth counts as omitted.
8
+ - 7c58f5b: Add `exec-pod`: run one command in a running Pod's container and get stdout, stderr and the exit code back, with no SSH setup (REST v2 `POST /v2/pods/{id}/exec`). The tool description covers background jobs for long downloads, the 25-second limit, and the 404/409 answers while a new Pod starts. Resync the v2 spec from production; the new signup routes are excluded from the tool surface.
9
+
10
+ ### Patch Changes
11
+
12
+ - e9f7fe2: The hosted 404 loop breaker now counts and blocks only `get-pod`, which caused 519 of the 559 blocks in the last month. 404s from other tools, such as `exec-pod` retried while a new Pod is still being created, no longer count toward a block.
13
+
3
14
  ## 4.10.1
4
15
 
5
16
  ### Patch Changes
@@ -33,6 +33,8 @@ var DIRECTORY_TOOLS = {
33
33
  "deploy-hub-repo": IN,
34
34
  "detach-cost-center-resource": IN,
35
35
  "endpoint-health": IN,
36
+ // env can carry tokens (HF_TOKEN for a model download), so the directory omits it.
37
+ "exec-pod": { include: true, omitInputs: ["body.env"] },
36
38
  "get-capacity": IN,
37
39
  "get-cluster": IN,
38
40
  "get-cost-center": IN,
@@ -152,6 +154,21 @@ function directoryOmittedInputs(name) {
152
154
  const decision = DIRECTORY_TOOLS[name];
153
155
  return decision?.include ? decision.omitInputs ?? [] : [];
154
156
  }
157
+ function omittedInputsIn(omit, args) {
158
+ return omit.filter((key) => {
159
+ const [parent, child] = key.split(".");
160
+ if (child === void 0) return parent in args;
161
+ let value = args[parent];
162
+ if (typeof value === "string") {
163
+ try {
164
+ value = JSON.parse(value);
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+ return !!value && typeof value === "object" && child in value;
170
+ });
171
+ }
155
172
  function profileForPath(pathname) {
156
173
  return pathname.replace(/\/+$/, "") === DIRECTORY_PATH ? "directory" : "default";
157
174
  }
@@ -161,6 +178,7 @@ export {
161
178
  DIRECTORY_TOOLS,
162
179
  directoryIncludes,
163
180
  directoryOmittedInputs,
181
+ omittedInputsIn,
164
182
  profileForPath
165
183
  };
166
- //# sourceMappingURL=chunk-FQJFKWUV.mjs.map
184
+ //# sourceMappingURL=chunk-24UOECYY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/specgen/profile.ts"],"sourcesContent":["// Which tool surface a connection gets. `default` is the full surface every\n// client has always had. `directory` is what app directories (Anthropic's\n// connector directory, OpenAI's plugin directory) list: their policies forbid\n// tool descriptions that instruct the model, steer it toward other tools or\n// external instructions, or collect conversation data beyond the tool's job.\n// The directory profile is served only at DIRECTORY_PATH; nothing about the\n// default surface changes.\nexport type ServerProfile = 'default' | 'directory';\n\nexport const DIRECTORY_PATH = '/directory';\n\n/** Whether a tool is listed on the directory profile. Every tool the server\n * can serve must have an entry, so a new tool (a spec regeneration, a new\n * curated tool) fails tests/specgen-directory.test.ts until someone decides.\n * The rules for deciding are in specgen/DIRECTORY_POLICY.md.\n * `omitInputs` hides arguments the directory must not ask for (a credential,\n * for example); a call that passes one is refused. A key is a top-level\n * argument (`s3Config`) or one level inside an object argument (`body.env`). */\nexport type DirectoryDecision =\n | { include: true; omitInputs?: readonly string[] }\n | { include: false; reason: string };\n\nconst IN = { include: true } as const;\nconst CREDENTIAL_MANAGEMENT =\n 'Manages stored credentials; OpenAI restricts plugins that handle access credentials, so the directory leaves the whole credential family out.';\nconst CREDENTIAL_INPUT =\n 'Takes a secret or password as input; directories forbid collecting credentials.';\nconst ALP =\n 'Collects conversation-derived data beyond the task; the directory profile never serves it.';\n\nexport const DIRECTORY_TOOLS: Readonly<Record<string, DirectoryDecision>> = {\n 'add-organization-group-member': IN,\n 'attach-cost-center-resource': IN,\n 'cancel-job': IN,\n 'create-cluster': IN,\n 'create-cost-center': IN,\n 'create-delegation': IN,\n 'create-endpoint': IN,\n 'create-global-volume': IN,\n 'create-network-volume': IN,\n 'create-organization-group': IN,\n 'create-organization-invite': IN,\n 'create-organization-role': IN,\n 'create-pod': IN,\n 'create-template': IN,\n 'delete-cluster': IN,\n 'delete-cost-center': IN,\n 'delete-endpoint': IN,\n 'delete-global-volume': IN,\n 'delete-network-volume': IN,\n 'delete-organization-group': IN,\n 'delete-organization-invite': IN,\n 'delete-organization-role': IN,\n 'delete-organization-user': IN,\n 'delete-pod': IN,\n 'delete-template': IN,\n 'deploy-hub-repo': IN,\n 'detach-cost-center-resource': IN,\n 'endpoint-health': IN,\n // env can carry tokens (HF_TOKEN for a model download), so the directory omits it.\n 'exec-pod': { include: true, omitInputs: ['body.env'] },\n 'get-capacity': IN,\n 'get-cluster': IN,\n 'get-cost-center': IN,\n 'get-cpu-type': IN,\n 'get-data-center': IN,\n 'get-endpoint': IN,\n 'get-endpoint-build': IN,\n 'get-endpoint-cold-start-metrics': IN,\n 'get-endpoint-request-metrics': IN,\n 'get-endpoint-worker-metrics': IN,\n 'get-global-volume': IN,\n 'get-gpu-type': IN,\n 'get-job-status': IN,\n 'get-network-volume': IN,\n 'get-organization': IN,\n 'get-organization-group': IN,\n 'get-organization-invite': IN,\n 'get-organization-role': IN,\n 'get-organization-role-capabilities': IN,\n 'get-organization-user': IN,\n 'get-pod': IN,\n 'get-ssh-keys': IN,\n 'get-template': IN,\n 'list-audit-logs': IN,\n 'list-billing': IN,\n 'list-cluster-billing': IN,\n 'list-cluster-pods': IN,\n 'list-clusters': IN,\n 'list-cost-center-resources': IN,\n 'list-cost-centers': IN,\n 'list-cpu-types': IN,\n 'list-data-centers': IN,\n 'list-delegations': IN,\n 'list-endpoint-billing': IN,\n 'list-endpoint-builds': IN,\n 'list-endpoint-releases': IN,\n 'list-endpoint-workers': IN,\n 'list-endpoints': IN,\n 'list-global-volumes': IN,\n 'list-gpu-types': IN,\n 'list-hub-repos': IN,\n 'list-network-volume-billing': IN,\n 'list-network-volumes': IN,\n 'list-organization-group-members': IN,\n 'list-organization-groups': IN,\n 'list-organization-invites': IN,\n 'list-organization-roles': IN,\n 'list-organization-users': IN,\n 'list-pod-billing': IN,\n 'list-pods': IN,\n 'list-public-endpoints': IN,\n 'list-public-templates': IN,\n 'list-serverless-billing': IN,\n 'list-templates': IN,\n 'lookup-concept': IN,\n 'purge-endpoint-queue': IN,\n 'remove-organization-group-member': IN,\n 'retry-job': IN,\n 'revoke-delegation': IN,\n 'search-concepts': IN,\n 'search-docs': IN,\n 'set-endpoint-gpus': IN,\n 'stream-job': IN,\n 'stream-pod-logs': IN,\n 'stream-worker-logs': IN,\n 'update-cluster': IN,\n 'update-cost-center': IN,\n 'update-endpoint': IN,\n 'update-global-volume': IN,\n 'update-network-volume': IN,\n 'update-organization-group': IN,\n 'update-organization-role': IN,\n 'update-organization-user': IN,\n 'update-pod': IN,\n 'update-ssh-keys': IN,\n 'update-template': IN,\n 'run-endpoint': { include: true, omitInputs: ['s3Config'] },\n 'runsync-endpoint': { include: true, omitInputs: ['s3Config'] },\n 'pod-action': {\n include: false,\n reason:\n 'Takes the transition as an argument; OpenAI asks for one tool per operation, so the directory serves start-pod, stop-pod and restart-pod (terminate is delete-pod).',\n },\n 'start-pod': IN,\n 'stop-pod': IN,\n 'restart-pod': IN,\n 'list-guides': {\n include: false,\n reason: 'Serves playbooks that instruct the model how to behave.',\n },\n 'read-guide': {\n include: false,\n reason: 'Serves playbooks that instruct the model how to behave.',\n },\n 'describe-tool': {\n include: false,\n reason:\n 'Fetches schemas the directory already lists in full; directories reject schema fetching.',\n },\n 'create-secret': { include: false, reason: CREDENTIAL_INPUT },\n 'get-secret': { include: false, reason: CREDENTIAL_MANAGEMENT },\n 'list-secrets': { include: false, reason: CREDENTIAL_MANAGEMENT },\n 'delete-secret': { include: false, reason: CREDENTIAL_MANAGEMENT },\n 'get-registry': { include: false, reason: CREDENTIAL_MANAGEMENT },\n 'list-registries': { include: false, reason: CREDENTIAL_MANAGEMENT },\n 'delete-registry': { include: false, reason: CREDENTIAL_MANAGEMENT },\n 'update-secret': { include: false, reason: CREDENTIAL_INPUT },\n 'create-registry': { include: false, reason: CREDENTIAL_INPUT },\n report_feedback: { include: false, reason: ALP },\n report_how_user_found_runpod: { include: false, reason: ALP },\n save_to_journal: { include: false, reason: ALP },\n read_journal: { include: false, reason: ALP },\n ask_question: { include: false, reason: ALP },\n};\n\n/** Served on the directory profile only with an explicit include. */\nexport function directoryIncludes(name: string): boolean {\n return DIRECTORY_TOOLS[name]?.include === true;\n}\n\nexport function directoryOmittedInputs(name: string): readonly string[] {\n const decision = DIRECTORY_TOOLS[name];\n return decision?.include ? (decision.omitInputs ?? []) : [];\n}\n\n/** The omitted inputs (see DirectoryDecision) that a call's arguments set. */\nexport function omittedInputsIn(\n omit: readonly string[],\n args: Record<string, unknown>\n): string[] {\n return omit.filter((key) => {\n const [parent, child] = key.split('.');\n if (child === undefined) return parent in args;\n let value = args[parent];\n // Dispatch parses a body sent as a JSON string, so check its parsed form;\n // a string that is not JSON fails in dispatch instead.\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value);\n } catch {\n return false;\n }\n }\n return !!value && typeof value === 'object' && child in value;\n });\n}\n\nexport function profileForPath(pathname: string): ServerProfile {\n return pathname.replace(/\\/+$/, '') === DIRECTORY_PATH\n ? 'directory'\n : 'default';\n}\n"],"mappings":"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","names":[]}
@@ -8,8 +8,9 @@ import {
8
8
  DIRECTORY_PATH,
9
9
  directoryIncludes,
10
10
  directoryOmittedInputs,
11
+ omittedInputsIn,
11
12
  profileForPath
12
- } from "./chunk-FQJFKWUV.mjs";
13
+ } from "./chunk-24UOECYY.mjs";
13
14
 
14
15
  // src/http.ts
15
16
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -291,25 +292,16 @@ async function dispatchGeneratedTool(client, tool, args) {
291
292
  const value = args[param.name];
292
293
  query[param.name] = param.explode === false && Array.isArray(value) ? value.join(",") : value;
293
294
  }
294
- let requestBody = args.body;
295
- if (tool.hasBody && typeof requestBody === "string") {
296
- try {
297
- requestBody = JSON.parse(requestBody);
298
- } catch {
299
- return {
300
- ok: false,
301
- status: 400,
302
- payload: {
303
- error: "The body argument arrived as a string that is not valid JSON. Send body as an object."
304
- }
305
- };
306
- }
307
- }
308
- if (tool.hasBody && requestBody !== void 0) {
309
- const properties = tool.inputSchema.properties ?? {};
310
- const defs = tool.inputSchema.$defs ?? {};
311
- requestBody = coerceScalars(requestBody, properties.body, defs);
312
- }
295
+ const sent = tool.hasBody ? bodyAsSent(tool, args.body) : { body: args.body };
296
+ if (!sent)
297
+ return {
298
+ ok: false,
299
+ status: 400,
300
+ payload: {
301
+ error: "The body argument arrived as a string that is not valid JSON. Send body as an object."
302
+ }
303
+ };
304
+ const requestBody = sent.body;
313
305
  const verbs = client;
314
306
  const { data, error, response } = await verbs[tool.method](tool.path, {
315
307
  params: { path: pathParams, query },
@@ -322,6 +314,32 @@ async function dispatchGeneratedTool(client, tool, args) {
322
314
  payload: data ?? { status: response.status }
323
315
  };
324
316
  }
317
+ function bodyAsSent(tool, body) {
318
+ let parsed = body;
319
+ if (typeof parsed === "string") {
320
+ try {
321
+ parsed = JSON.parse(parsed);
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
326
+ if (parsed === void 0) return { body: parsed };
327
+ const properties = tool.inputSchema.properties ?? {};
328
+ const defs = tool.inputSchema.$defs ?? {};
329
+ return { body: coerceScalars(parsed, properties.body, defs) };
330
+ }
331
+ function argsAsSent(tool, args) {
332
+ if (!tool.hasBody || args.body === void 0) return args;
333
+ const sent = bodyAsSent(tool, args.body);
334
+ return sent ? { ...args, body: sent.body } : null;
335
+ }
336
+ function omittedEmptyQueryArg(tool) {
337
+ const required = new Set(tool.inputSchema.required ?? []);
338
+ const query = new Set(
339
+ tool.params.filter((p) => p.location === "query").map((p) => p.name)
340
+ );
341
+ return (name, value) => query.has(name) && !required.has(name) && isEmptyValue(value);
342
+ }
325
343
  function isEmptyValue(value) {
326
344
  return value === null || value === "" || Array.isArray(value) && !value.length;
327
345
  }
@@ -2758,6 +2776,109 @@ var generatedTools = [
2758
2776
  }
2759
2777
  }
2760
2778
  },
2779
+ {
2780
+ name: "exec-pod",
2781
+ title: "Run a command in a Pod",
2782
+ operationId: "execPod",
2783
+ 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.`,
2784
+ 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.",
2785
+ method: "POST",
2786
+ path: "/v2/pods/{id}/exec",
2787
+ params: [
2788
+ {
2789
+ name: "id",
2790
+ location: "path"
2791
+ }
2792
+ ],
2793
+ hasBody: true,
2794
+ annotations: {
2795
+ readOnlyHint: false,
2796
+ destructiveHint: true,
2797
+ idempotentHint: false,
2798
+ openWorldHint: true
2799
+ },
2800
+ inputSchema: {
2801
+ type: "object",
2802
+ properties: {
2803
+ id: {
2804
+ type: "string",
2805
+ description: "Pod identifier"
2806
+ },
2807
+ body: {
2808
+ $ref: "#/$defs/PodExecRequest",
2809
+ type: "object"
2810
+ }
2811
+ },
2812
+ required: ["id", "body"],
2813
+ $defs: {
2814
+ PodExecRequest: {
2815
+ type: "object",
2816
+ additionalProperties: false,
2817
+ required: ["command"],
2818
+ properties: {
2819
+ command: {
2820
+ type: "array",
2821
+ minItems: 1,
2822
+ maxItems: 256,
2823
+ prefixItems: [
2824
+ {
2825
+ type: "string",
2826
+ minLength: 1,
2827
+ maxLength: 4096,
2828
+ pattern: "^[^\\x00]*[^\\t-\\r \x85\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\\x00][^\\x00]*$"
2829
+ }
2830
+ ],
2831
+ items: {
2832
+ type: "string",
2833
+ maxLength: 4096,
2834
+ pattern: "^[^\\x00]*$"
2835
+ },
2836
+ 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", "..."]`.',
2837
+ examples: [
2838
+ ["nvidia-smi", "--query-gpu=name,memory.used", "--format=csv"]
2839
+ ]
2840
+ },
2841
+ timeoutSeconds: {
2842
+ type: "integer",
2843
+ minimum: 1,
2844
+ maximum: 25,
2845
+ default: 20,
2846
+ description: "How long the call waits for the command to finish, in seconds. A command still running at this point returns with `timedOut: true`.",
2847
+ examples: [10]
2848
+ },
2849
+ cwd: {
2850
+ type: "string",
2851
+ pattern: "^/[^\\x00-\\x1f\\x7f]*$",
2852
+ maxLength: 1024,
2853
+ description: "Absolute working directory for the command, at most 1024 characters and without control characters. Defaults to the image's `WORKDIR`.",
2854
+ examples: ["/workspace"]
2855
+ },
2856
+ env: {
2857
+ type: "object",
2858
+ maxProperties: 64,
2859
+ propertyNames: {
2860
+ type: "string",
2861
+ minLength: 1,
2862
+ maxLength: 256,
2863
+ pattern: "^[^=\\x00]+$"
2864
+ },
2865
+ additionalProperties: {
2866
+ type: "string",
2867
+ maxLength: 16384,
2868
+ pattern: "^[^\\x00]*$"
2869
+ },
2870
+ 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.",
2871
+ examples: [
2872
+ {
2873
+ HF_HOME: "/workspace/hf"
2874
+ }
2875
+ ]
2876
+ }
2877
+ }
2878
+ }
2879
+ }
2880
+ }
2881
+ },
2761
2882
  {
2762
2883
  name: "get-cluster",
2763
2884
  title: "Get cluster",
@@ -6820,6 +6941,122 @@ var generatedTools = [
6820
6941
  }
6821
6942
  ];
6822
6943
 
6944
+ // src/specgen/validate.ts
6945
+ import { Ajv2020 } from "ajv/dist/2020.js";
6946
+ function schemaValidationMode(value = process.env.MCP_SCHEMA_VALIDATION) {
6947
+ const mode = value?.trim().toLowerCase();
6948
+ return mode === "off" || mode === "enforce" ? mode : "shadow";
6949
+ }
6950
+ var ajv = new Ajv2020({
6951
+ strict: false,
6952
+ allErrors: true,
6953
+ validateFormats: false
6954
+ });
6955
+ var compiled = /* @__PURE__ */ new WeakMap();
6956
+ function validatorFor(schema) {
6957
+ let validate = compiled.get(schema);
6958
+ if (!validate) {
6959
+ validate = ajv.compile(structuredClone(schema));
6960
+ compiled.set(schema, validate);
6961
+ }
6962
+ return validate;
6963
+ }
6964
+ var MAX_NULL_PASSES = 8;
6965
+ function isNullTypeError(error, data) {
6966
+ return error.keyword === "type" && error.instancePath !== "" && valueAt(data, error.instancePath) === null;
6967
+ }
6968
+ function pointerSegments(pointer) {
6969
+ return pointer.split("/").slice(1).map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
6970
+ }
6971
+ function valueAt(data, pointer) {
6972
+ let node = data;
6973
+ for (const key of pointerSegments(pointer)) {
6974
+ if (node === null || typeof node !== "object") return void 0;
6975
+ node = node[key];
6976
+ }
6977
+ return node;
6978
+ }
6979
+ function removeAt(data, pointer) {
6980
+ const segments = pointerSegments(pointer);
6981
+ const last = segments.pop();
6982
+ const parent = valueAt(data, segments.length ? `/${segments.join("/")}` : "");
6983
+ if (last === void 0 || parent === null || typeof parent !== "object")
6984
+ return false;
6985
+ if (Array.isArray(parent)) return false;
6986
+ delete parent[last];
6987
+ return true;
6988
+ }
6989
+ var WRAPPER_KEYWORDS = /* @__PURE__ */ new Set(["if"]);
6990
+ function toViolation(error) {
6991
+ const base = pointerSegments(error.instancePath);
6992
+ let path = base.join(".");
6993
+ let message = error.message ?? "is invalid";
6994
+ const params = error.params;
6995
+ if (error.keyword === "required" && typeof params.missingProperty === "string") {
6996
+ path = [...base, params.missingProperty].join(".");
6997
+ message = "is required";
6998
+ } else if (error.keyword === "additionalProperties" && typeof params.additionalProperty === "string") {
6999
+ path = [...base, params.additionalProperty].join(".");
7000
+ message = "is not an accepted argument here";
7001
+ } else if (error.keyword === "pattern") {
7002
+ message = "is not in an accepted format";
7003
+ }
7004
+ const allowed = error.keyword === "enum" ? params.allowedValues : error.keyword === "const" ? [params.allowedValue] : void 0;
7005
+ return {
7006
+ path,
7007
+ keyword: error.keyword,
7008
+ message,
7009
+ ...allowed ? { allowed } : {},
7010
+ schemaPath: error.schemaPath
7011
+ };
7012
+ }
7013
+ function schemaViolations(schema, args, omitted = () => false) {
7014
+ const validate = validatorFor(schema);
7015
+ const data = structuredClone(
7016
+ Object.fromEntries(
7017
+ Object.entries(args).filter(([name, value]) => !omitted(name, value))
7018
+ )
7019
+ );
7020
+ for (let pass = 0; ; pass++) {
7021
+ if (validate(data)) return [];
7022
+ const errors = validate.errors ?? [];
7023
+ let removed = false;
7024
+ if (pass < MAX_NULL_PASSES) {
7025
+ for (const error of errors.filter((e) => isNullTypeError(e, data)))
7026
+ removed = removeAt(data, error.instancePath) || removed;
7027
+ }
7028
+ if (!removed) {
7029
+ return errors.filter((e) => !WRAPPER_KEYWORDS.has(e.keyword)).map(toViolation);
7030
+ }
7031
+ }
7032
+ }
7033
+ var INVALID_ARGUMENTS_HINT = "Fix the listed fields and call again.";
7034
+ function invalidArgumentsPayload(violations) {
7035
+ return {
7036
+ error: "Invalid arguments",
7037
+ code: "invalid_arguments",
7038
+ fields: violations.map(({ path, message, allowed }) => ({
7039
+ path,
7040
+ problem: message,
7041
+ ...allowed ? { allowed } : {}
7042
+ })),
7043
+ hint: INVALID_ARGUMENTS_HINT
7044
+ };
7045
+ }
7046
+ function logSchemaViolations(tool, mode, violations) {
7047
+ console.error(
7048
+ "schema_violation",
7049
+ JSON.stringify({
7050
+ tool,
7051
+ mode,
7052
+ violations: violations.map(({ keyword, schemaPath }) => ({
7053
+ keyword,
7054
+ schemaPath
7055
+ }))
7056
+ })
7057
+ );
7058
+ }
7059
+
6823
7060
  // src/specgen/tools/annotations.ts
6824
7061
  var readOnly = {
6825
7062
  readOnlyHint: true,
@@ -10586,13 +10823,40 @@ function withoutCredentialExamples(schema) {
10586
10823
  };
10587
10824
  return strip(schema);
10588
10825
  }
10589
- function withoutInputs(schema, omit) {
10590
- if (!omit.length) return schema;
10826
+ function withoutKeys(schema, omit) {
10591
10827
  const properties = { ...schema.properties ?? {} };
10592
10828
  for (const key of omit) delete properties[key];
10593
10829
  const required = Array.isArray(schema.required) ? schema.required.filter((key) => !omit.includes(key)) : schema.required;
10594
10830
  return { ...schema, properties, ...required ? { required } : {} };
10595
10831
  }
10832
+ function withoutInputs(schema, omit) {
10833
+ if (!omit.length) return schema;
10834
+ let out = withoutKeys(
10835
+ schema,
10836
+ omit.filter((key) => !key.includes("."))
10837
+ );
10838
+ for (const key of omit.filter((k) => k.includes("."))) {
10839
+ const [parent, child] = key.split(".");
10840
+ const properties = out.properties ?? {};
10841
+ const node = properties[parent];
10842
+ if (!node) continue;
10843
+ const ref = typeof node.$ref === "string" ? node.$ref : void 0;
10844
+ const defName = ref?.startsWith("#/$defs/") ? ref.slice(8) : void 0;
10845
+ const defs = out.$defs ?? {};
10846
+ if (defName && defs[defName]) {
10847
+ out = {
10848
+ ...out,
10849
+ $defs: { ...defs, [defName]: withoutKeys(defs[defName], [child]) }
10850
+ };
10851
+ } else {
10852
+ out = {
10853
+ ...out,
10854
+ properties: { ...properties, [parent]: withoutKeys(node, [child]) }
10855
+ };
10856
+ }
10857
+ }
10858
+ return out;
10859
+ }
10596
10860
  function callShape(schema) {
10597
10861
  const defs = schema.$defs ?? {};
10598
10862
  const resolve = (node) => {
@@ -10694,7 +10958,7 @@ function words2(name) {
10694
10958
 
10695
10959
  // src/server.ts
10696
10960
  var SERVER_NAME = "Runpod API Server";
10697
- var SERVER_VERSION = true ? "4.10.1" : "dev";
10961
+ var SERVER_VERSION = true ? "4.11.0" : "dev";
10698
10962
 
10699
10963
  // src/specgen/analytics.ts
10700
10964
  import { createHmac } from "crypto";
@@ -11405,6 +11669,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11405
11669
  const caller = callerId(ctx.apiKey);
11406
11670
  const profile = opts.profile ?? "default";
11407
11671
  const directory = profile === "directory";
11672
+ const schemaMode = schemaValidationMode();
11408
11673
  const alp = directory ? void 0 : opts.alp;
11409
11674
  const servedGeneratedTools = directory ? generatedTools.filter((tool) => directoryIncludes(tool.name)) : generatedTools;
11410
11675
  const servedCuratedTools = [
@@ -11511,7 +11776,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11511
11776
  (tool) => tool.name === request.params.name
11512
11777
  );
11513
11778
  const curatedArgs = curated ? withoutNullOptionals(curated.inputSchema, args) : args;
11514
- const omitted = directory ? directoryOmittedInputs(request.params.name).filter((key) => key in args) : [];
11779
+ const omitted = directory ? omittedInputsIn(directoryOmittedInputs(request.params.name), args) : [];
11515
11780
  const rejected = omitted.length ? {
11516
11781
  ok: false,
11517
11782
  status: 400,
@@ -11521,8 +11786,26 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
11521
11786
  } : curated && !curated.lenientArguments ? validateArguments(
11522
11787
  curated.inputSchema,
11523
11788
  curatedArgs
11524
- ) : null;
11525
- const result = rejected ?? await runTool(
11789
+ ) : generated ? validateArguments(generated.inputSchema, args) : null;
11790
+ let schemaRejected = null;
11791
+ if (!rejected && schemaMode !== "off") {
11792
+ const sent = generated ? argsAsSent(generated, args) : null;
11793
+ const violations = curated ? curated.lenientArguments ? [] : schemaViolations(curated.inputSchema, curatedArgs) : generated && sent ? schemaViolations(
11794
+ generated.inputSchema,
11795
+ sent,
11796
+ omittedEmptyQueryArg(generated)
11797
+ ) : [];
11798
+ if (violations.length) {
11799
+ logSchemaViolations(request.params.name, schemaMode, violations);
11800
+ if (schemaMode === "enforce")
11801
+ schemaRejected = {
11802
+ ok: false,
11803
+ status: 400,
11804
+ payload: invalidArgumentsPayload(violations)
11805
+ };
11806
+ }
11807
+ }
11808
+ const result = rejected ?? schemaRejected ?? await runTool(
11526
11809
  () => curated ? curated.handler(ctx, curatedArgs) : dispatchTool(generated, request.params.name, args)
11527
11810
  );
11528
11811
  logToolCall({
@@ -11861,6 +12144,7 @@ function createToolContext(options = {}) {
11861
12144
 
11862
12145
  // src/specgen/not-found-limiter.ts
11863
12146
  var SINK_TIMEOUT_MS2 = 3e3;
12147
+ var LOOP_LIMITED_TOOLS = /* @__PURE__ */ new Set(["get-pod"]);
11864
12148
  var NOT_FOUND_LOOP = "not_found_loop";
11865
12149
  var blocked = /* @__PURE__ */ new Map();
11866
12150
  function notFoundSinkUrl(submitUrl) {
@@ -11885,7 +12169,7 @@ function createNotFoundLimiter(opts) {
11885
12169
  };
11886
12170
  },
11887
12171
  onToolResult: (tool, status) => {
11888
- if (status !== 404) return;
12172
+ if (status !== 404 || !LOOP_LIMITED_TOOLS.has(tool)) return;
11889
12173
  const pair = `${opts.key}|${tool}`;
11890
12174
  void fetchImpl(opts.sinkUrl, {
11891
12175
  method: "POST",
@@ -12378,4 +12662,4 @@ export {
12378
12662
  summarizeRpc,
12379
12663
  handleMcpRequest
12380
12664
  };
12381
- //# sourceMappingURL=chunk-AKX5ZMCL.mjs.map
12665
+ //# sourceMappingURL=chunk-4M7QDHK4.mjs.map