mcp-server-kubernetes 2.5.1 → 2.6.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/README.md CHANGED
@@ -93,6 +93,7 @@ npx mcp-chat --config "%APPDATA%\Claude\claude_desktop_config.json"
93
93
  - [x] Troubleshooting Prompt (`k8s-diagnose`)
94
94
  - Guides through a systematic Kubernetes troubleshooting flow for pods based on a keyword and optional namespace.
95
95
  - [x] Non-destructive mode for read and create/update-only access to clusters
96
+ - [x] Secrets masking for security (masks sensitive data in `kubectl get secrets` commands, does not affect logs)
96
97
 
97
98
  ## Prompts
98
99
 
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { getSpawnMaxBuffer } from "../config/max-buffer.js";
4
+ import * as yaml from "js-yaml";
4
5
  export const kubectlGetSchema = {
5
6
  name: "kubectl_get",
6
7
  description: "Get or list Kubernetes resources by resource type, name, and optionally namespace",
@@ -120,12 +121,19 @@ export async function kubectlGet(k8sManager, input) {
120
121
  maxBuffer: getSpawnMaxBuffer(),
121
122
  env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
122
123
  });
124
+ // Apply secrets masking if enabled and dealing with secrets
125
+ const shouldMaskSecrets = process.env.MASK_SECRETS !== "false" &&
126
+ (resourceType === "secrets" || resourceType === "secret");
127
+ let processedResult = result;
128
+ if (shouldMaskSecrets) {
129
+ processedResult = maskSecretsData(result, output);
130
+ }
123
131
  // Format the results for better readability
124
132
  const isListOperation = !name;
125
133
  if (isListOperation && output === "json") {
126
134
  try {
127
135
  // Parse JSON and extract key information
128
- const parsed = JSON.parse(result);
136
+ const parsed = JSON.parse(processedResult);
129
137
  if (parsed.kind && parsed.kind.endsWith("List") && parsed.items) {
130
138
  if (resourceType === "events") {
131
139
  const formattedEvents = parsed.items.map((event) => ({
@@ -178,7 +186,7 @@ export async function kubectlGet(k8sManager, input) {
178
186
  content: [
179
187
  {
180
188
  type: "text",
181
- text: result,
189
+ text: processedResult,
182
190
  },
183
191
  ],
184
192
  };
@@ -261,3 +269,86 @@ function isNonNamespacedResource(resourceType) {
261
269
  ];
262
270
  return nonNamespacedResources.includes(resourceType.toLowerCase());
263
271
  }
272
+ /**
273
+ * Recursively traverses an object and masks values in 'data' sections of Kubernetes secrets.
274
+ *
275
+ * @param {any} obj - The object to traverse. Can be an array, object, or primitive value.
276
+ * @returns {any} A new object with masked values in 'data' sections.
277
+ */
278
+ function maskDataValues(obj) {
279
+ if (obj == null) {
280
+ return obj;
281
+ }
282
+ if (Array.isArray(obj)) {
283
+ return obj.map(item => maskDataValues(item));
284
+ }
285
+ if (typeof obj === "object") {
286
+ const result = {};
287
+ for (const key in obj) {
288
+ if (key === "data" && typeof obj[key] === "object" && obj[key] !== null) {
289
+ // This is a data section - mask all leaf values within it
290
+ result[key] = maskAllLeafValues(obj[key]);
291
+ }
292
+ else {
293
+ result[key] = maskDataValues(obj[key]);
294
+ }
295
+ }
296
+ return result;
297
+ }
298
+ return obj;
299
+ }
300
+ /**
301
+ * Recursively masks all leaf values (non-object, non-array values) in an object structure.
302
+ *
303
+ * @param {any} obj - The input object or value to process.
304
+ * @returns {any} A new object or value with all leaf values replaced by a mask.
305
+ */
306
+ function maskAllLeafValues(obj) {
307
+ const maskValue = "***";
308
+ if (obj == null) {
309
+ return obj;
310
+ }
311
+ if (Array.isArray(obj)) {
312
+ return obj.map(item => maskAllLeafValues(item));
313
+ }
314
+ if (typeof obj === "object") {
315
+ const result = {};
316
+ for (const key in obj) {
317
+ result[key] = maskAllLeafValues(obj[key]);
318
+ }
319
+ return result;
320
+ }
321
+ // This is a leaf value (string, number, boolean) - mask it
322
+ return maskValue;
323
+ }
324
+ /**
325
+ * Masks sensitive data in Kubernetes secrets by parsing the raw output and replacing
326
+ * all leaf values in the "data" section with a placeholder value ("***").
327
+ *
328
+ * @param {string} output - The raw output from a `kubectl` command, containing secrets data.
329
+ * @param {string} format - The format of the output, either "json" or "yaml".
330
+ * @returns {string} - The masked output in the same format as the input.
331
+ */
332
+ function maskSecretsData(output, format) {
333
+ try {
334
+ if (format === "json") {
335
+ const parsed = JSON.parse(output);
336
+ const masked = maskDataValues(parsed);
337
+ return JSON.stringify(masked, null, 2);
338
+ }
339
+ else if (format === "yaml") {
340
+ // Parse YAML to JSON, mask, then convert back to YAML
341
+ const parsed = yaml.load(output);
342
+ const masked = maskDataValues(parsed);
343
+ return yaml.dump(masked, {
344
+ indent: 2,
345
+ lineWidth: -1, // Don't wrap lines
346
+ noRefs: true // Don't use references
347
+ });
348
+ }
349
+ }
350
+ catch (error) {
351
+ console.warn("Failed to parse secrets output for masking:", error);
352
+ }
353
+ return output;
354
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-kubernetes",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "MCP server for interacting with Kubernetes clusters via kubectl",
5
5
  "license": "MIT",
6
6
  "type": "module",