mcp-server-kubernetes 4.2.0 → 4.2.2
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/config/server-config.d.ts +1 -1
- package/dist/config/server-config.js +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +8 -1
- package/dist/models/helm-models.d.ts +3 -0
- package/dist/tools/helm-operations.js +22 -2
- package/dist/tools/kubectl-create.d.ts +0 -1
- package/dist/tools/kubectl-create.js +3 -9
- package/dist/tools/kubectl-patch.d.ts +1 -1
- package/dist/tools/kubectl-patch.js +2 -2
- package/dist/utils/validate-tool-input.d.ts +18 -0
- package/dist/utils/validate-tool-input.js +179 -0
- package/package.json +1 -1
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:
|
|
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);
|
|
@@ -25,6 +25,7 @@ export declare const HelmValuesSchema: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
|
25
25
|
export interface HelmUninstallOperation {
|
|
26
26
|
name: string;
|
|
27
27
|
namespace: string;
|
|
28
|
+
context?: string;
|
|
28
29
|
}
|
|
29
30
|
export interface HelmInstallOperation {
|
|
30
31
|
name: string;
|
|
@@ -35,6 +36,7 @@ export interface HelmInstallOperation {
|
|
|
35
36
|
valuesFile?: string;
|
|
36
37
|
createNamespace?: boolean;
|
|
37
38
|
useTemplate?: boolean;
|
|
39
|
+
context?: string;
|
|
38
40
|
}
|
|
39
41
|
export interface HelmUpgradeOperation {
|
|
40
42
|
name: string;
|
|
@@ -43,6 +45,7 @@ export interface HelmUpgradeOperation {
|
|
|
43
45
|
repo?: string;
|
|
44
46
|
values?: Record<string, any>;
|
|
45
47
|
valuesFile?: string;
|
|
48
|
+
context?: string;
|
|
46
49
|
}
|
|
47
50
|
export type HelmResponse = {
|
|
48
51
|
status: "installed" | "upgraded" | "uninstalled" | "failed";
|
|
@@ -178,7 +178,13 @@ const assertOperandsNotFlagLike = (params) => {
|
|
|
178
178
|
assertNotFlagLike(params.chart, "chart");
|
|
179
179
|
assertNotFlagLike(params.namespace, "namespace");
|
|
180
180
|
assertNotFlagLike(params.repo, "repo");
|
|
181
|
+
assertNotFlagLike(params.context, "context");
|
|
181
182
|
};
|
|
183
|
+
// helm selects the kubeconfig context with --kube-context, kubectl with
|
|
184
|
+
// --context. Without them the command runs against the current context, not
|
|
185
|
+
// the one the caller asked for.
|
|
186
|
+
const helmContextArgs = (context) => context ? ["--kube-context", context] : [];
|
|
187
|
+
const kubectlContextArgs = (context) => context ? ["--context", context] : [];
|
|
182
188
|
/**
|
|
183
189
|
* Install a Helm chart using template mode (helm template + kubectl apply).
|
|
184
190
|
* This mode bypasses authentication issues and kubeconfig API version mismatches.
|
|
@@ -197,7 +203,12 @@ async function installHelmChartTemplate(params) {
|
|
|
197
203
|
// Step 2: Create namespace
|
|
198
204
|
steps.push(`Creating namespace: ${params.namespace}`);
|
|
199
205
|
try {
|
|
200
|
-
executeCommand("kubectl", [
|
|
206
|
+
executeCommand("kubectl", [
|
|
207
|
+
"create",
|
|
208
|
+
"namespace",
|
|
209
|
+
params.namespace,
|
|
210
|
+
...kubectlContextArgs(params.context),
|
|
211
|
+
]);
|
|
201
212
|
}
|
|
202
213
|
catch (error) {
|
|
203
214
|
if (!error.message.includes("already exists")) {
|
|
@@ -213,6 +224,7 @@ async function installHelmChartTemplate(params) {
|
|
|
213
224
|
params.chart,
|
|
214
225
|
"--namespace",
|
|
215
226
|
params.namespace,
|
|
227
|
+
...helmContextArgs(params.context),
|
|
216
228
|
];
|
|
217
229
|
if (params.repo) {
|
|
218
230
|
templateArgs.push("--repo", params.repo);
|
|
@@ -243,7 +255,12 @@ async function installHelmChartTemplate(params) {
|
|
|
243
255
|
steps.push("Applying YAML using kubectl");
|
|
244
256
|
const tempYamlFile = writeTempFile("template.yaml", yamlOutput);
|
|
245
257
|
try {
|
|
246
|
-
executeCommand("kubectl", [
|
|
258
|
+
executeCommand("kubectl", [
|
|
259
|
+
"apply",
|
|
260
|
+
"-f",
|
|
261
|
+
tempYamlFile,
|
|
262
|
+
...kubectlContextArgs(params.context),
|
|
263
|
+
]);
|
|
247
264
|
steps.push("Helm chart installed successfully using template mode");
|
|
248
265
|
}
|
|
249
266
|
finally {
|
|
@@ -303,6 +320,7 @@ export async function installHelmChart(params) {
|
|
|
303
320
|
params.chart,
|
|
304
321
|
"--namespace",
|
|
305
322
|
params.namespace,
|
|
323
|
+
...helmContextArgs(params.context),
|
|
306
324
|
];
|
|
307
325
|
// Add create namespace flag if requested
|
|
308
326
|
if (params.createNamespace !== false) {
|
|
@@ -374,6 +392,7 @@ export async function upgradeHelmChart(params) {
|
|
|
374
392
|
params.chart,
|
|
375
393
|
"--namespace",
|
|
376
394
|
params.namespace,
|
|
395
|
+
...helmContextArgs(params.context),
|
|
377
396
|
];
|
|
378
397
|
// Add values file if provided
|
|
379
398
|
if (params.valuesFile) {
|
|
@@ -433,6 +452,7 @@ export async function uninstallHelmChart(params) {
|
|
|
433
452
|
params.name,
|
|
434
453
|
"--namespace",
|
|
435
454
|
params.namespace,
|
|
455
|
+
...helmContextArgs(params.context),
|
|
436
456
|
]);
|
|
437
457
|
return {
|
|
438
458
|
content: [
|
|
@@ -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
|
},
|
|
@@ -29,8 +29,8 @@ export const kubectlPatchSchema = {
|
|
|
29
29
|
default: "strategic",
|
|
30
30
|
},
|
|
31
31
|
patchData: {
|
|
32
|
-
type: "object",
|
|
33
|
-
description: "Patch data
|
|
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
|
+
}
|