mcp-server-kubernetes 4.1.10 → 4.2.1

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.
@@ -1,6 +1,6 @@
1
1
  export declare const serverConfig: {
2
2
  readonly name: "kubernetes";
3
- readonly version: "4.1.10";
3
+ readonly version: "4.2.1";
4
4
  readonly capabilities: {
5
5
  readonly resources: {};
6
6
  readonly tools: {};
@@ -1,6 +1,6 @@
1
1
  export const serverConfig = {
2
2
  name: "kubernetes",
3
- version: "4.1.10",
3
+ version: "4.2.1",
4
4
  capabilities: {
5
5
  resources: {},
6
6
  tools: {},
package/dist/index.d.ts CHANGED
@@ -603,7 +603,6 @@ declare const allTools: ({
603
603
  };
604
604
  readonly output: {
605
605
  readonly type: "string";
606
- readonly enum: readonly ["json", "yaml", "name", "go-template", "template", "jsonpath", "jsonpath-as-json"];
607
606
  readonly description: "Output format. One of: json|yaml|name|go-template|template|jsonpath|jsonpath-as-json. The template formats take their template inline, e.g. 'jsonpath={.metadata.name}'.";
608
607
  readonly default: "yaml";
609
608
  };
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ import { registerPromptHandlers } from "./prompts/index.js";
32
32
  import { ping, pingSchema } from "./tools/ping.js";
33
33
  import { startStreamableHTTPServer } from "./utils/streamable-http.js";
34
34
  import { withTelemetry } from "./middleware/telemetry-middleware.js";
35
+ import { validateToolInput } from "./utils/validate-tool-input.js";
35
36
  // Check environment variables for tool filtering
36
37
  const allowOnlyReadonlyTools = process.env.ALLOW_ONLY_READONLY_TOOLS === "true";
37
38
  const allowedToolsEnv = process.env.ALLOWED_TOOLS;
@@ -153,10 +154,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
153
154
  });
154
155
  server.setRequestHandler(CallToolRequestSchema, withTelemetry(async (request) => {
155
156
  try {
156
- const { name, arguments: input = {} } = request.params;
157
+ const { name, arguments: rawInput } = request.params;
157
158
  if (!isToolAllowed(name)) {
158
159
  throw new McpError(ErrorCode.InvalidRequest, `Tool '${name}' is not allowed under the current server configuration`);
159
160
  }
161
+ // The handlers below only cast their arguments; nothing else checks
162
+ // them against the tool's inputSchema before they reach kubectl/helm.
163
+ const tool = allTools.find((t) => t.name === name);
164
+ const input = tool
165
+ ? validateToolInput(tool, rawInput)
166
+ : rawInput ?? {};
160
167
  // Handle new kubectl-style commands
161
168
  if (name === "kubectl_context") {
162
169
  return await kubectlContext(k8sManager, input);
@@ -12,7 +12,6 @@ export declare const kubectlCreateSchema: {
12
12
  };
13
13
  readonly output: {
14
14
  readonly type: "string";
15
- readonly enum: readonly ["json", "yaml", "name", "go-template", "template", "jsonpath", "jsonpath-as-json"];
16
15
  readonly description: "Output format. One of: json|yaml|name|go-template|template|jsonpath|jsonpath-as-json. The template formats take their template inline, e.g. 'jsonpath={.metadata.name}'.";
17
16
  readonly default: "yaml";
18
17
  };
@@ -30,16 +30,10 @@ export const kubectlCreateSchema = {
30
30
  // General options
31
31
  dryRun: dryRunParameter,
32
32
  output: {
33
+ // No enum: the template formats carry their template after "=",
34
+ // which an enum cannot express. kubectlCreate checks the format
35
+ // against INLINE_OUTPUT_FORMATS instead.
33
36
  type: "string",
34
- enum: [
35
- "json",
36
- "yaml",
37
- "name",
38
- "go-template",
39
- "template",
40
- "jsonpath",
41
- "jsonpath-as-json",
42
- ],
43
37
  description: "Output format. One of: json|yaml|name|go-template|template|jsonpath|jsonpath-as-json. The template formats take their template inline, e.g. 'jsonpath={.metadata.name}'.",
44
38
  default: "yaml",
45
39
  },
@@ -28,7 +28,7 @@ export declare const kubectlPatchSchema: {
28
28
  default: string;
29
29
  };
30
30
  patchData: {
31
- type: string;
31
+ type: string[];
32
32
  description: string;
33
33
  };
34
34
  patchFile: {
@@ -29,8 +29,8 @@ export const kubectlPatchSchema = {
29
29
  default: "strategic",
30
30
  },
31
31
  patchData: {
32
- type: "object",
33
- description: "Patch data as a JSON object",
32
+ type: ["object", "array"],
33
+ description: "Patch data: a JSON object for 'strategic' and 'merge' patches, or an array of JSON Patch operations for patchType 'json'",
34
34
  },
35
35
  patchFile: {
36
36
  type: "string",
@@ -0,0 +1,18 @@
1
+ export type ToolDefinition = {
2
+ name: string;
3
+ inputSchema: unknown;
4
+ };
5
+ /**
6
+ * Validate `input` against `tool.inputSchema` and return the arguments the
7
+ * tool should run with. Throws an McpError (InvalidParams) listing every
8
+ * problem found.
9
+ *
10
+ * Normalisation is limited to unambiguous cases, so a call that would have
11
+ * worked before keeps working:
12
+ * - an enum value that differs only in case is replaced by the listed one
13
+ * ("Pod" -> "pod"); the handlers lower-case several of these anyway
14
+ * - a required argument that is missing but has a schema default gets the
15
+ * default, which is what the handlers fall back to
16
+ * - an optional argument passed as null is treated as omitted
17
+ */
18
+ export declare function validateToolInput(tool: ToolDefinition, input: unknown): Record<string, unknown>;
@@ -0,0 +1,179 @@
1
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
2
+ /**
3
+ * Validate `input` against `tool.inputSchema` and return the arguments the
4
+ * tool should run with. Throws an McpError (InvalidParams) listing every
5
+ * problem found.
6
+ *
7
+ * Normalisation is limited to unambiguous cases, so a call that would have
8
+ * worked before keeps working:
9
+ * - an enum value that differs only in case is replaced by the listed one
10
+ * ("Pod" -> "pod"); the handlers lower-case several of these anyway
11
+ * - a required argument that is missing but has a schema default gets the
12
+ * default, which is what the handlers fall back to
13
+ * - an optional argument passed as null is treated as omitted
14
+ */
15
+ export function validateToolInput(tool, input) {
16
+ const errors = [];
17
+ if (input === undefined || input === null)
18
+ input = {};
19
+ if (!isPlainObject(input)) {
20
+ throw invalid(tool.name, [
21
+ `the arguments must be a JSON object; got ${describe(input)}.`,
22
+ ]);
23
+ }
24
+ const result = validateObject(tool.inputSchema, input, "", errors);
25
+ if (errors.length > 0)
26
+ throw invalid(tool.name, errors);
27
+ return result;
28
+ }
29
+ function validateObject(schema, value, path, errors) {
30
+ const properties = schema.properties ?? {};
31
+ const required = new Set(schema.required ?? []);
32
+ const result = {};
33
+ for (const [key, raw] of Object.entries(value)) {
34
+ const propSchema = properties[key];
35
+ const propPath = join(path, key);
36
+ // null means "not provided": dropped if optional, and reported (or
37
+ // defaulted) by the required check below if not.
38
+ if (raw === null)
39
+ continue;
40
+ if (propSchema) {
41
+ result[key] = validateValue(propSchema, raw, propPath, errors);
42
+ }
43
+ else if (isPlainObject(schema.additionalProperties)) {
44
+ result[key] = validateValue(schema.additionalProperties, raw, propPath, errors);
45
+ }
46
+ else if (schema.additionalProperties === false) {
47
+ errors.push(`"${propPath}" is not a recognised argument. Allowed arguments: ` +
48
+ `${listOf(Object.keys(properties))}.`);
49
+ }
50
+ else {
51
+ // Unknown arguments are ignored by the handlers; pass them through.
52
+ result[key] = raw;
53
+ }
54
+ }
55
+ for (const key of required) {
56
+ if (result[key] !== undefined && result[key] !== null)
57
+ continue;
58
+ const propSchema = properties[key];
59
+ if (propSchema?.default !== undefined) {
60
+ result[key] = propSchema.default;
61
+ continue;
62
+ }
63
+ errors.push(missing(join(path, key), propSchema));
64
+ }
65
+ return result;
66
+ }
67
+ function validateValue(schema, value, path, errors) {
68
+ const types = schema.type === undefined ? [] : [schema.type].flat();
69
+ if (types.length > 0 && !types.some((t) => matchesType(t, value))) {
70
+ errors.push(`"${path}" must be ${types.map(article).join(" or ")}; got ` +
71
+ `${describe(value)}.${typeHint(types, value)}`);
72
+ return value;
73
+ }
74
+ if (schema.enum) {
75
+ if (schema.enum.includes(value))
76
+ return value;
77
+ const folded = typeof value === "string"
78
+ ? schema.enum.find((option) => typeof option === "string" &&
79
+ option.toLowerCase() === value.toLowerCase())
80
+ : undefined;
81
+ if (folded !== undefined)
82
+ return folded;
83
+ errors.push(`"${path}" must be one of ${listOf(schema.enum)}; got ${describe(value)}.`);
84
+ return value;
85
+ }
86
+ if (Array.isArray(value) && schema.items) {
87
+ return value.map((item, i) => validateValue(schema.items, item, `${path}[${i}]`, errors));
88
+ }
89
+ if (isPlainObject(value) && (schema.properties || schema.required)) {
90
+ return validateObject(schema, value, path, errors);
91
+ }
92
+ return value;
93
+ }
94
+ function matchesType(type, value) {
95
+ switch (type) {
96
+ case "string":
97
+ return typeof value === "string";
98
+ case "number":
99
+ return typeof value === "number" && Number.isFinite(value);
100
+ case "integer":
101
+ return Number.isInteger(value);
102
+ case "boolean":
103
+ return typeof value === "boolean";
104
+ case "array":
105
+ return Array.isArray(value);
106
+ case "object":
107
+ return isPlainObject(value);
108
+ case "null":
109
+ return value === null;
110
+ default:
111
+ return true;
112
+ }
113
+ }
114
+ function isPlainObject(value) {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
117
+ // The most common LLM slip is quoting a scalar or JSON-encoding a structure;
118
+ // say what the fix is when that is what happened.
119
+ function typeHint(types, value) {
120
+ if (typeof value !== "string")
121
+ return "";
122
+ const trimmed = value.trim();
123
+ if ((types.includes("number") || types.includes("integer")) &&
124
+ trimmed !== "" &&
125
+ Number.isFinite(Number(trimmed))) {
126
+ return ` Pass the number without quotes: ${trimmed}.`;
127
+ }
128
+ if (types.includes("boolean") && (trimmed === "true" || trimmed === "false")) {
129
+ return ` Pass the boolean without quotes: ${trimmed}.`;
130
+ }
131
+ if ((types.includes("array") && trimmed.startsWith("[")) ||
132
+ (types.includes("object") && trimmed.startsWith("{"))) {
133
+ return " Pass the JSON value itself, not a string containing it.";
134
+ }
135
+ return "";
136
+ }
137
+ function missing(path, schema) {
138
+ const type = schema?.type ? ` (${[schema.type].flat().join(" or ")})` : "";
139
+ const description = schema?.description ? `: ${schema.description}` : ".";
140
+ return `missing required argument "${path}"${type}${description}`;
141
+ }
142
+ function invalid(toolName, errors) {
143
+ return new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool "${toolName}":\n` +
144
+ errors.map((e) => ` - ${e}`).join("\n") +
145
+ `\nCorrect these arguments and call ${toolName} again.`);
146
+ }
147
+ function join(path, key) {
148
+ return path === "" ? key : `${path}.${key}`;
149
+ }
150
+ function article(type) {
151
+ switch (type) {
152
+ case "array":
153
+ return "an array";
154
+ case "object":
155
+ return "an object";
156
+ case "integer":
157
+ return "an integer";
158
+ default:
159
+ return `a ${type}`;
160
+ }
161
+ }
162
+ function listOf(values) {
163
+ return values.map((v) => JSON.stringify(v)).join(", ");
164
+ }
165
+ function describe(value) {
166
+ if (value === null)
167
+ return "null";
168
+ if (value === undefined)
169
+ return "nothing";
170
+ const kind = Array.isArray(value)
171
+ ? "an array"
172
+ : typeof value === "object"
173
+ ? "an object"
174
+ : `a ${typeof value}`;
175
+ let json = JSON.stringify(value) ?? String(value);
176
+ if (json.length > 80)
177
+ json = `${json.slice(0, 77)}...`;
178
+ return `${kind} (${json})`;
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "4.1.10",
3
+ "version": "4.2.1",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",