mcp-server-kubernetes 2.2.1 → 2.3.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.
- package/dist/config/container-templates.d.ts +2 -2
- package/dist/index.js +6 -6
- package/dist/resources/handlers.js +11 -11
- package/dist/tools/helm-operations.js +2 -1
- package/dist/tools/kubectl-apply.js +1 -1
- package/dist/tools/kubectl-context.js +6 -6
- package/dist/tools/kubectl-create.js +1 -1
- package/dist/tools/kubectl-delete.js +1 -1
- package/dist/tools/kubectl-describe.js +1 -1
- package/dist/tools/kubectl-generic.js +1 -1
- package/dist/tools/kubectl-get.js +32 -18
- package/dist/tools/kubectl-list.js +1 -1
- package/dist/tools/kubectl-logs.js +5 -5
- package/dist/tools/kubectl-operations.js +1 -1
- package/dist/tools/kubectl-patch.js +1 -1
- package/dist/tools/kubectl-rollout.js +3 -2
- package/dist/tools/kubectl-scale.js +1 -1
- package/dist/utils/kubernetes-manager.d.ts +6 -0
- package/dist/utils/kubernetes-manager.js +103 -30
- package/package.json +2 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import
|
|
2
|
+
import type { V1Container } from "@kubernetes/client-node";
|
|
3
3
|
export declare const ContainerTemplate: z.ZodEnum<["ubuntu", "nginx", "busybox", "alpine", "custom"]>;
|
|
4
4
|
export type ContainerTemplateName = z.infer<typeof ContainerTemplate>;
|
|
5
5
|
export declare const CustomContainerConfig: z.ZodObject<{
|
|
@@ -103,4 +103,4 @@ export declare const CustomContainerConfig: z.ZodObject<{
|
|
|
103
103
|
}[] | undefined;
|
|
104
104
|
}>;
|
|
105
105
|
export type CustomContainerConfigType = z.infer<typeof CustomContainerConfig>;
|
|
106
|
-
export declare const containerTemplates: Record<string,
|
|
106
|
+
export declare const containerTemplates: Record<string, V1Container>;
|
package/dist/index.js
CHANGED
|
@@ -11,22 +11,22 @@ import { cleanupSchema } from "./config/cleanup-config.js";
|
|
|
11
11
|
import { startSSEServer } from "./utils/sse.js";
|
|
12
12
|
import { startPortForward, PortForwardSchema, stopPortForward, StopPortForwardSchema, } from "./tools/port_forward.js";
|
|
13
13
|
import { kubectlScale, kubectlScaleSchema } from "./tools/kubectl-scale.js";
|
|
14
|
-
import { kubectlContext, kubectlContextSchema } from "./tools/kubectl-context.js";
|
|
14
|
+
import { kubectlContext, kubectlContextSchema, } from "./tools/kubectl-context.js";
|
|
15
15
|
import { kubectlGet, kubectlGetSchema } from "./tools/kubectl-get.js";
|
|
16
|
-
import { kubectlDescribe, kubectlDescribeSchema } from "./tools/kubectl-describe.js";
|
|
16
|
+
import { kubectlDescribe, kubectlDescribeSchema, } from "./tools/kubectl-describe.js";
|
|
17
17
|
import { kubectlList, kubectlListSchema } from "./tools/kubectl-list.js";
|
|
18
18
|
import { kubectlApply, kubectlApplySchema } from "./tools/kubectl-apply.js";
|
|
19
19
|
import { kubectlDelete, kubectlDeleteSchema } from "./tools/kubectl-delete.js";
|
|
20
20
|
import { kubectlCreate, kubectlCreateSchema } from "./tools/kubectl-create.js";
|
|
21
21
|
import { kubectlLogs, kubectlLogsSchema } from "./tools/kubectl-logs.js";
|
|
22
|
-
import { kubectlGeneric, kubectlGenericSchema } from "./tools/kubectl-generic.js";
|
|
22
|
+
import { kubectlGeneric, kubectlGenericSchema, } from "./tools/kubectl-generic.js";
|
|
23
23
|
import { kubectlPatch, kubectlPatchSchema } from "./tools/kubectl-patch.js";
|
|
24
|
-
import { kubectlRollout, kubectlRolloutSchema } from "./tools/kubectl-rollout.js";
|
|
24
|
+
import { kubectlRollout, kubectlRolloutSchema, } from "./tools/kubectl-rollout.js";
|
|
25
25
|
// Check if non-destructive tools only mode is enabled
|
|
26
26
|
const nonDestructiveTools = process.env.ALLOW_ONLY_NON_DESTRUCTIVE_TOOLS === "true";
|
|
27
27
|
// Define destructive tools (delete and uninstall operations)
|
|
28
28
|
const destructiveTools = [
|
|
29
|
-
kubectlDeleteSchema, // This replaces all individual delete operations
|
|
29
|
+
kubectlDeleteSchema, // This replaces all individual delete operations
|
|
30
30
|
uninstallHelmChartSchema,
|
|
31
31
|
cleanupSchema, // Cleanup is also destructive as it deletes resources
|
|
32
32
|
kubectlGenericSchema, // Generic kubectl command can perform destructive operations
|
|
@@ -123,7 +123,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
123
123
|
fieldSelector: input.fieldSelector,
|
|
124
124
|
labelSelector: input.labelSelector,
|
|
125
125
|
sortBy: input.sortBy,
|
|
126
|
-
output: input.output
|
|
126
|
+
output: input.output,
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
// Handle specific non-kubectl operations
|
|
@@ -44,13 +44,13 @@ export const getResourceHandlers = (k8sManager) => ({
|
|
|
44
44
|
const isNodes = parts[0] === "nodes";
|
|
45
45
|
if ((isNamespaces || isNodes) && parts.length === 1) {
|
|
46
46
|
const fn = isNodes ? "listNode" : "listNamespace";
|
|
47
|
-
const {
|
|
47
|
+
const { items } = await k8sManager.getCoreApi()[fn]();
|
|
48
48
|
return {
|
|
49
49
|
contents: [
|
|
50
50
|
{
|
|
51
51
|
uri: request.params.uri,
|
|
52
52
|
mimeType: "application/json",
|
|
53
|
-
text: JSON.stringify(
|
|
53
|
+
text: JSON.stringify(items, null, 2),
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
};
|
|
@@ -58,43 +58,43 @@ export const getResourceHandlers = (k8sManager) => ({
|
|
|
58
58
|
const [namespace, resourceType] = parts;
|
|
59
59
|
switch (resourceType) {
|
|
60
60
|
case "pods": {
|
|
61
|
-
const {
|
|
61
|
+
const { items } = await k8sManager
|
|
62
62
|
.getCoreApi()
|
|
63
|
-
.listNamespacedPod(namespace);
|
|
63
|
+
.listNamespacedPod({ namespace });
|
|
64
64
|
return {
|
|
65
65
|
contents: [
|
|
66
66
|
{
|
|
67
67
|
uri: request.params.uri,
|
|
68
68
|
mimeType: "application/json",
|
|
69
|
-
text: JSON.stringify(
|
|
69
|
+
text: JSON.stringify(items, null, 2),
|
|
70
70
|
},
|
|
71
71
|
],
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
case "deployments": {
|
|
75
|
-
const {
|
|
75
|
+
const { items } = await k8sManager
|
|
76
76
|
.getAppsApi()
|
|
77
|
-
.listNamespacedDeployment(namespace);
|
|
77
|
+
.listNamespacedDeployment({ namespace });
|
|
78
78
|
return {
|
|
79
79
|
contents: [
|
|
80
80
|
{
|
|
81
81
|
uri: request.params.uri,
|
|
82
82
|
mimeType: "application/json",
|
|
83
|
-
text: JSON.stringify(
|
|
83
|
+
text: JSON.stringify(items, null, 2),
|
|
84
84
|
},
|
|
85
85
|
],
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
case "services": {
|
|
89
|
-
const {
|
|
89
|
+
const { items } = await k8sManager
|
|
90
90
|
.getCoreApi()
|
|
91
|
-
.listNamespacedService(namespace);
|
|
91
|
+
.listNamespacedService({ namespace });
|
|
92
92
|
return {
|
|
93
93
|
contents: [
|
|
94
94
|
{
|
|
95
95
|
uri: request.params.uri,
|
|
96
96
|
mimeType: "application/json",
|
|
97
|
-
text: JSON.stringify(
|
|
97
|
+
text: JSON.stringify(items, null, 2),
|
|
98
98
|
},
|
|
99
99
|
],
|
|
100
100
|
};
|
|
@@ -88,7 +88,8 @@ const executeHelmCommand = (command) => {
|
|
|
88
88
|
// Add a generous timeout of 60 seconds for Helm operations
|
|
89
89
|
return execSync(command, {
|
|
90
90
|
encoding: "utf8",
|
|
91
|
-
timeout: 60000 // 60 seconds timeout
|
|
91
|
+
timeout: 60000, // 60 seconds timeout
|
|
92
|
+
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG }
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
catch (error) {
|
|
@@ -69,7 +69,7 @@ export async function kubectlApply(k8sManager, input) {
|
|
|
69
69
|
}
|
|
70
70
|
// Execute the command
|
|
71
71
|
try {
|
|
72
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
72
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
73
73
|
// Clean up temp file if created
|
|
74
74
|
if (tempFile) {
|
|
75
75
|
try {
|
|
@@ -52,7 +52,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
52
52
|
}
|
|
53
53
|
else if (output === "custom" || output === "json") {
|
|
54
54
|
// For custom or JSON output, we'll format it ourselves
|
|
55
|
-
const rawResult = execSync(command, { encoding: "utf8" });
|
|
55
|
+
const rawResult = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
56
56
|
// Parse the tabular output from kubectl
|
|
57
57
|
const lines = rawResult.trim().split("\n");
|
|
58
58
|
const headers = lines[0].trim().split(/\s+/);
|
|
@@ -83,17 +83,17 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
// Execute the command for non-json outputs
|
|
86
|
-
result = execSync(command, { encoding: "utf8" });
|
|
86
|
+
result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
87
87
|
break;
|
|
88
88
|
case "get":
|
|
89
89
|
// Build command to get current context
|
|
90
90
|
command = "kubectl config current-context";
|
|
91
91
|
// Execute the command
|
|
92
92
|
try {
|
|
93
|
-
const currentContext = execSync(command, { encoding: "utf8" }).trim();
|
|
93
|
+
const currentContext = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim();
|
|
94
94
|
if (detailed) {
|
|
95
95
|
// For detailed context info, we need to use get-contexts and filter
|
|
96
|
-
const allContextsOutput = execSync("kubectl config get-contexts", { encoding: "utf8" });
|
|
96
|
+
const allContextsOutput = execSync("kubectl config get-contexts", { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
97
97
|
// Parse the tabular output from kubectl
|
|
98
98
|
const lines = allContextsOutput.trim().split("\n");
|
|
99
99
|
const headers = lines[0].trim().split(/\s+/);
|
|
@@ -169,7 +169,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
169
169
|
}
|
|
170
170
|
// First check if the context exists
|
|
171
171
|
try {
|
|
172
|
-
const allContextsOutput = execSync("kubectl config get-contexts -o name", { encoding: "utf8" });
|
|
172
|
+
const allContextsOutput = execSync("kubectl config get-contexts -o name", { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
173
173
|
const availableContexts = allContextsOutput.trim().split("\n");
|
|
174
174
|
// Extract the short name from the ARN if needed
|
|
175
175
|
let contextName = name;
|
|
@@ -186,7 +186,7 @@ export async function kubectlContext(k8sManager, input) {
|
|
|
186
186
|
// Build command to set context
|
|
187
187
|
command = `kubectl config use-context "${contextName}"`;
|
|
188
188
|
// Execute the command
|
|
189
|
-
result = execSync(command, { encoding: "utf8" });
|
|
189
|
+
result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
190
190
|
// For tests to pass, we need to return the original name format that was passed in
|
|
191
191
|
return {
|
|
192
192
|
content: [
|
|
@@ -280,7 +280,7 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
280
280
|
command += ` -o ${output}`;
|
|
281
281
|
// Execute the command
|
|
282
282
|
try {
|
|
283
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
283
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
284
284
|
// Clean up temp file if created
|
|
285
285
|
if (tempFile) {
|
|
286
286
|
try {
|
|
@@ -109,7 +109,7 @@ export async function kubectlDelete(k8sManager, input) {
|
|
|
109
109
|
}
|
|
110
110
|
// Execute the command
|
|
111
111
|
try {
|
|
112
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
112
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
113
113
|
// Clean up temp file if created
|
|
114
114
|
if (tempFile) {
|
|
115
115
|
try {
|
|
@@ -49,7 +49,7 @@ export async function kubectlDescribe(k8sManager, input) {
|
|
|
49
49
|
}
|
|
50
50
|
// Execute the command
|
|
51
51
|
try {
|
|
52
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
52
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
53
53
|
return {
|
|
54
54
|
content: [
|
|
55
55
|
{
|
|
@@ -98,7 +98,7 @@ export async function kubectlGeneric(k8sManager, input) {
|
|
|
98
98
|
const command = cmdArgs.slice(1).join(' ');
|
|
99
99
|
try {
|
|
100
100
|
console.error(`Executing: kubectl ${command}`);
|
|
101
|
-
const result = execSync(`kubectl ${command}`, { encoding: "utf8" });
|
|
101
|
+
const result = execSync(`kubectl ${command}`, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
102
102
|
return {
|
|
103
103
|
content: [
|
|
104
104
|
{
|
|
@@ -8,43 +8,43 @@ export const kubectlGetSchema = {
|
|
|
8
8
|
properties: {
|
|
9
9
|
resourceType: {
|
|
10
10
|
type: "string",
|
|
11
|
-
description: "Type of resource to get (e.g., pods, deployments, services, configmaps, events, etc.)"
|
|
11
|
+
description: "Type of resource to get (e.g., pods, deployments, services, configmaps, events, etc.)",
|
|
12
12
|
},
|
|
13
13
|
name: {
|
|
14
14
|
type: "string",
|
|
15
|
-
description: "Name of the resource (optional - if not provided, lists all resources of the specified type)"
|
|
15
|
+
description: "Name of the resource (optional - if not provided, lists all resources of the specified type)",
|
|
16
16
|
},
|
|
17
17
|
namespace: {
|
|
18
18
|
type: "string",
|
|
19
19
|
description: "Namespace of the resource (optional - defaults to 'default' for namespaced resources)",
|
|
20
|
-
default: "default"
|
|
20
|
+
default: "default",
|
|
21
21
|
},
|
|
22
22
|
output: {
|
|
23
23
|
type: "string",
|
|
24
24
|
enum: ["json", "yaml", "wide", "name", "custom"],
|
|
25
25
|
description: "Output format",
|
|
26
|
-
default: "json"
|
|
26
|
+
default: "json",
|
|
27
27
|
},
|
|
28
28
|
allNamespaces: {
|
|
29
29
|
type: "boolean",
|
|
30
30
|
description: "If true, list resources across all namespaces",
|
|
31
|
-
default: false
|
|
31
|
+
default: false,
|
|
32
32
|
},
|
|
33
33
|
labelSelector: {
|
|
34
34
|
type: "string",
|
|
35
35
|
description: "Filter resources by label selector (e.g. 'app=nginx')",
|
|
36
|
-
optional: true
|
|
36
|
+
optional: true,
|
|
37
37
|
},
|
|
38
38
|
fieldSelector: {
|
|
39
39
|
type: "string",
|
|
40
40
|
description: "Filter resources by field selector (e.g. 'metadata.name=my-pod')",
|
|
41
|
-
optional: true
|
|
41
|
+
optional: true,
|
|
42
42
|
},
|
|
43
43
|
sortBy: {
|
|
44
44
|
type: "string",
|
|
45
45
|
description: "Sort events by a field (default: lastTimestamp). Only applicable for events.",
|
|
46
|
-
optional: true
|
|
47
|
-
}
|
|
46
|
+
optional: true,
|
|
47
|
+
},
|
|
48
48
|
},
|
|
49
49
|
required: ["resourceType"],
|
|
50
50
|
},
|
|
@@ -68,8 +68,11 @@ export async function kubectlGet(k8sManager, input) {
|
|
|
68
68
|
command += ` ${name}`;
|
|
69
69
|
}
|
|
70
70
|
// For events, default to all namespaces unless explicitly specified
|
|
71
|
-
const shouldShowAllNamespaces = resourceType === "events"
|
|
72
|
-
|
|
71
|
+
const shouldShowAllNamespaces = resourceType === "events"
|
|
72
|
+
? input.namespace
|
|
73
|
+
? false
|
|
74
|
+
: true
|
|
75
|
+
: allNamespaces;
|
|
73
76
|
// Add namespace flag unless all namespaces is specified
|
|
74
77
|
if (shouldShowAllNamespaces) {
|
|
75
78
|
command += " --all-namespaces";
|
|
@@ -115,7 +118,10 @@ export async function kubectlGet(k8sManager, input) {
|
|
|
115
118
|
}
|
|
116
119
|
// Execute the command
|
|
117
120
|
try {
|
|
118
|
-
const result = execSync(command, {
|
|
121
|
+
const result = execSync(command, {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
124
|
+
});
|
|
119
125
|
// Format the results for better readability
|
|
120
126
|
const isListOperation = !name;
|
|
121
127
|
if (isListOperation && output === "json") {
|
|
@@ -152,7 +158,7 @@ export async function kubectlGet(k8sManager, input) {
|
|
|
152
158
|
namespace: item.metadata?.namespace || "",
|
|
153
159
|
kind: item.kind || resourceType,
|
|
154
160
|
status: getResourceStatus(item),
|
|
155
|
-
createdAt: item.metadata?.creationTimestamp
|
|
161
|
+
createdAt: item.metadata?.creationTimestamp,
|
|
156
162
|
}));
|
|
157
163
|
return {
|
|
158
164
|
content: [
|
|
@@ -239,13 +245,21 @@ function getResourceStatus(resource) {
|
|
|
239
245
|
// Helper function to determine if a resource is non-namespaced
|
|
240
246
|
function isNonNamespacedResource(resourceType) {
|
|
241
247
|
const nonNamespacedResources = [
|
|
242
|
-
"nodes",
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
248
|
+
"nodes",
|
|
249
|
+
"node",
|
|
250
|
+
"no",
|
|
251
|
+
"namespaces",
|
|
252
|
+
"namespace",
|
|
253
|
+
"ns",
|
|
254
|
+
"persistentvolumes",
|
|
255
|
+
"pv",
|
|
256
|
+
"storageclasses",
|
|
257
|
+
"sc",
|
|
246
258
|
"clusterroles",
|
|
247
259
|
"clusterrolebindings",
|
|
248
|
-
"customresourcedefinitions",
|
|
260
|
+
"customresourcedefinitions",
|
|
261
|
+
"crd",
|
|
262
|
+
"crds",
|
|
249
263
|
];
|
|
250
264
|
return nonNamespacedResources.includes(resourceType.toLowerCase());
|
|
251
265
|
}
|
|
@@ -142,7 +142,7 @@ export async function kubectlList(k8sManager, input) {
|
|
|
142
142
|
command += ` -o custom-columns="${customColumns}"`;
|
|
143
143
|
// Execute the command
|
|
144
144
|
try {
|
|
145
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
145
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
146
146
|
return {
|
|
147
147
|
content: [
|
|
148
148
|
{
|
|
@@ -83,7 +83,7 @@ export async function kubectlLogs(k8sManager, input) {
|
|
|
83
83
|
baseCommand = addLogOptions(baseCommand, input);
|
|
84
84
|
// Execute the command
|
|
85
85
|
try {
|
|
86
|
-
const result = execSync(baseCommand, { encoding: "utf8" });
|
|
86
|
+
const result = execSync(baseCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
87
87
|
return formatLogOutput(name, result);
|
|
88
88
|
}
|
|
89
89
|
catch (error) {
|
|
@@ -104,7 +104,7 @@ export async function kubectlLogs(k8sManager, input) {
|
|
|
104
104
|
// For cronjobs, it's more complex - need to find the job first
|
|
105
105
|
const jobsCommand = `kubectl -n ${namespace} get jobs --selector=job-name=${name} -o jsonpath='{.items[*].metadata.name}'`;
|
|
106
106
|
try {
|
|
107
|
-
const jobs = execSync(jobsCommand, { encoding: "utf8" }).trim().split(' ');
|
|
107
|
+
const jobs = execSync(jobsCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim().split(' ');
|
|
108
108
|
if (jobs.length === 0 || (jobs.length === 1 && jobs[0] === '')) {
|
|
109
109
|
return {
|
|
110
110
|
content: [
|
|
@@ -148,7 +148,7 @@ export async function kubectlLogs(k8sManager, input) {
|
|
|
148
148
|
if (!selectorCommand) {
|
|
149
149
|
throw new Error("Selector command is undefined");
|
|
150
150
|
}
|
|
151
|
-
const selectorJson = execSync(selectorCommand, { encoding: "utf8" }).trim();
|
|
151
|
+
const selectorJson = execSync(selectorCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim();
|
|
152
152
|
const selector = JSON.parse(selectorJson.replace(/'/g, '"'));
|
|
153
153
|
// Convert to label selector format
|
|
154
154
|
const labelSelector = Object.entries(selector)
|
|
@@ -216,7 +216,7 @@ async function getLabelSelectorLogs(labelSelector, namespace, input) {
|
|
|
216
216
|
try {
|
|
217
217
|
// First, find all pods matching the label selector
|
|
218
218
|
const podsCommand = `kubectl -n ${namespace} get pods --selector=${labelSelector} -o jsonpath='{.items[*].metadata.name}'`;
|
|
219
|
-
const pods = execSync(podsCommand, { encoding: "utf8" }).trim().split(' ');
|
|
219
|
+
const pods = execSync(podsCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } }).trim().split(' ');
|
|
220
220
|
if (pods.length === 0 || (pods.length === 1 && pods[0] === '')) {
|
|
221
221
|
return {
|
|
222
222
|
content: [
|
|
@@ -243,7 +243,7 @@ async function getLabelSelectorLogs(labelSelector, namespace, input) {
|
|
|
243
243
|
// Add other options
|
|
244
244
|
podCommand = addLogOptions(podCommand, input);
|
|
245
245
|
try {
|
|
246
|
-
const logs = execSync(podCommand, { encoding: "utf8" });
|
|
246
|
+
const logs = execSync(podCommand, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
247
247
|
logsMap[pod] = logs;
|
|
248
248
|
}
|
|
249
249
|
catch (error) {
|
|
@@ -60,7 +60,7 @@ export const listApiResourcesSchema = {
|
|
|
60
60
|
};
|
|
61
61
|
const executeKubectlCommand = (command) => {
|
|
62
62
|
try {
|
|
63
|
-
return execSync(command, { encoding: "utf8" });
|
|
63
|
+
return execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
64
64
|
}
|
|
65
65
|
catch (error) {
|
|
66
66
|
throw new Error(`Kubectl command failed: ${error.message}`);
|
|
@@ -87,7 +87,7 @@ export async function kubectlPatch(k8sManager, input) {
|
|
|
87
87
|
}
|
|
88
88
|
// Execute the command
|
|
89
89
|
try {
|
|
90
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
90
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
91
91
|
// Clean up temp file if created
|
|
92
92
|
if (tempFile) {
|
|
93
93
|
try {
|
|
@@ -79,7 +79,8 @@ export async function kubectlRollout(k8sManager, input) {
|
|
|
79
79
|
// and capture the output until that point
|
|
80
80
|
const result = execSync(command, {
|
|
81
81
|
encoding: "utf8",
|
|
82
|
-
timeout: 15000 // Reduced from 30 seconds to 15 seconds
|
|
82
|
+
timeout: 15000, // Reduced from 30 seconds to 15 seconds
|
|
83
|
+
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG }
|
|
83
84
|
});
|
|
84
85
|
return {
|
|
85
86
|
content: [
|
|
@@ -91,7 +92,7 @@ export async function kubectlRollout(k8sManager, input) {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
else {
|
|
94
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
95
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
95
96
|
return {
|
|
96
97
|
content: [
|
|
97
98
|
{
|
|
@@ -36,7 +36,7 @@ export async function kubectlScale(k8sManager, input) {
|
|
|
36
36
|
let command = `kubectl scale ${resourceType} ${input.name} --replicas=${input.replicas} --namespace=${namespace}`;
|
|
37
37
|
// Execute the command
|
|
38
38
|
try {
|
|
39
|
-
const result = execSync(command, { encoding: "utf8" });
|
|
39
|
+
const result = execSync(command, { encoding: "utf8", env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG } });
|
|
40
40
|
return {
|
|
41
41
|
content: [
|
|
42
42
|
{
|
|
@@ -45,6 +45,7 @@ export declare class KubernetesManager {
|
|
|
45
45
|
* Check if KUBECONFIG_PATH environment variable is available
|
|
46
46
|
*/
|
|
47
47
|
private hasEnvKubeconfigPath;
|
|
48
|
+
private hasEnvKubeconfig;
|
|
48
49
|
/**
|
|
49
50
|
* Set the current context to the desired context name.
|
|
50
51
|
*
|
|
@@ -67,4 +68,9 @@ export declare class KubernetesManager {
|
|
|
67
68
|
* Uses K8S_NAMESPACE environment variable if set, otherwise defaults to "default"
|
|
68
69
|
*/
|
|
69
70
|
getDefaultNamespace(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Create temporary kubeconfig file from YAML content for kubectl commands
|
|
73
|
+
* @param kubeconfigYaml YAML content of the kubeconfig
|
|
74
|
+
*/
|
|
75
|
+
private createTempKubeconfigFromYaml;
|
|
70
76
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import * as k8s from "@kubernetes/client-node";
|
|
2
1
|
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as k8s from "@kubernetes/client-node";
|
|
3
5
|
export class KubernetesManager {
|
|
4
6
|
resources = [];
|
|
5
7
|
portForwards = [];
|
|
@@ -10,48 +12,61 @@ export class KubernetesManager {
|
|
|
10
12
|
k8sBatchApi;
|
|
11
13
|
constructor() {
|
|
12
14
|
this.kc = new k8s.KubeConfig();
|
|
13
|
-
if (this.
|
|
14
|
-
// Priority 1:
|
|
15
|
-
this.kc.loadFromCluster();
|
|
16
|
-
}
|
|
17
|
-
else if (this.hasEnvKubeconfigYaml()) {
|
|
18
|
-
// Priority 2: Full kubeconfig as YAML string
|
|
15
|
+
if (this.hasEnvKubeconfigYaml()) {
|
|
16
|
+
// Priority 1: Full kubeconfig as YAML string
|
|
19
17
|
try {
|
|
20
18
|
this.loadEnvKubeconfigYaml();
|
|
19
|
+
this.createTempKubeconfigFromYaml(process.env.KUBECONFIG_YAML);
|
|
21
20
|
}
|
|
22
21
|
catch (error) {
|
|
23
|
-
throw new Error(`Failed to parse KUBECONFIG_YAML: ${error instanceof Error ? error.message :
|
|
22
|
+
throw new Error(`Failed to parse KUBECONFIG_YAML: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
24
23
|
}
|
|
25
24
|
}
|
|
25
|
+
else if (this.isRunningInCluster()) {
|
|
26
|
+
// Priority 2: Check if running in cluster
|
|
27
|
+
this.kc.loadFromCluster();
|
|
28
|
+
}
|
|
26
29
|
else if (this.hasEnvKubeconfigJson()) {
|
|
27
30
|
// Priority 3: Full kubeconfig as JSON string
|
|
28
31
|
try {
|
|
29
32
|
this.loadEnvKubeconfigJson();
|
|
33
|
+
// Create temp kubeconfig file for kubectl commands from JSON
|
|
34
|
+
const yamlConfig = this.kc.exportConfig();
|
|
35
|
+
this.createTempKubeconfigFromYaml(yamlConfig);
|
|
30
36
|
}
|
|
31
37
|
catch (error) {
|
|
32
|
-
throw new Error(`Failed to parse KUBECONFIG_JSON: ${error instanceof Error ? error.message :
|
|
38
|
+
throw new Error(`Failed to parse KUBECONFIG_JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
else if (this.hasEnvMinimalKubeconfig()) {
|
|
36
42
|
// Priority 4: Minimal config with individual environment variables
|
|
37
43
|
try {
|
|
38
44
|
this.loadEnvMinimalKubeconfig();
|
|
45
|
+
// Create temp kubeconfig file for kubectl commands from minimal config
|
|
46
|
+
const yamlConfig = this.kc.exportConfig();
|
|
47
|
+
this.createTempKubeconfigFromYaml(yamlConfig);
|
|
39
48
|
}
|
|
40
49
|
catch (error) {
|
|
41
|
-
throw new Error(`Failed to create kubeconfig from K8S_SERVER and K8S_TOKEN: ${error instanceof Error ? error.message :
|
|
50
|
+
throw new Error(`Failed to create kubeconfig from K8S_SERVER and K8S_TOKEN: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
else if (this.hasEnvKubeconfigPath()) {
|
|
45
|
-
// Priority 5: Custom kubeconfig file path
|
|
54
|
+
// Priority 5: Custom kubeconfig file path using KUBECONFIG_PATH
|
|
46
55
|
try {
|
|
47
56
|
this.loadEnvKubeconfigPath();
|
|
57
|
+
// Set KUBECONFIG environment variable to the custom path for kubectl commands
|
|
58
|
+
process.env.KUBECONFIG = process.env.KUBECONFIG_PATH;
|
|
48
59
|
}
|
|
49
60
|
catch (error) {
|
|
50
|
-
throw new Error(`Failed to load kubeconfig from KUBECONFIG_PATH: ${error instanceof Error ? error.message :
|
|
61
|
+
throw new Error(`Failed to load kubeconfig from KUBECONFIG_PATH: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
64
|
+
else if (this.hasEnvKubeconfig()) {
|
|
65
|
+
// Load from KUBECONFIG
|
|
66
|
+
this.kc.loadFromFile(process.env.KUBECONFIG);
|
|
67
|
+
}
|
|
53
68
|
else {
|
|
54
|
-
// Priority
|
|
69
|
+
// Priority 7: Default file-based configuration (existing fallback)
|
|
55
70
|
this.kc.loadFromDefault();
|
|
56
71
|
}
|
|
57
72
|
// Apply context override if specified
|
|
@@ -60,7 +75,7 @@ export class KubernetesManager {
|
|
|
60
75
|
this.setCurrentContext(process.env.K8S_CONTEXT);
|
|
61
76
|
}
|
|
62
77
|
catch (error) {
|
|
63
|
-
console.warn(`Warning: Could not set context to ${process.env.K8S_CONTEXT}: ${error instanceof Error ? error.message :
|
|
78
|
+
console.warn(`Warning: Could not set context to ${process.env.K8S_CONTEXT}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
64
79
|
}
|
|
65
80
|
}
|
|
66
81
|
// Initialize API clients
|
|
@@ -111,6 +126,10 @@ export class KubernetesManager {
|
|
|
111
126
|
* Load kubeconfig from KUBECONFIG_YAML environment variable (YAML format)
|
|
112
127
|
*/
|
|
113
128
|
loadEnvKubeconfigYaml() {
|
|
129
|
+
if (!process.env.KUBECONFIG_YAML) {
|
|
130
|
+
throw new Error("KUBECONFIG_YAML environment variable is not set");
|
|
131
|
+
}
|
|
132
|
+
// Load the config into the JavaScript client
|
|
114
133
|
this.kc.loadFromString(process.env.KUBECONFIG_YAML);
|
|
115
134
|
}
|
|
116
135
|
/**
|
|
@@ -125,28 +144,29 @@ export class KubernetesManager {
|
|
|
125
144
|
*/
|
|
126
145
|
loadEnvMinimalKubeconfig() {
|
|
127
146
|
if (!process.env.K8S_SERVER || !process.env.K8S_TOKEN) {
|
|
128
|
-
throw new Error(
|
|
147
|
+
throw new Error("K8S_SERVER and K8S_TOKEN environment variables are required");
|
|
129
148
|
}
|
|
130
149
|
const cluster = {
|
|
131
|
-
name:
|
|
150
|
+
name: "env-cluster",
|
|
132
151
|
server: process.env.K8S_SERVER,
|
|
133
|
-
skipTLSVerify: process.env.K8S_SKIP_TLS_VERIFY ===
|
|
152
|
+
skipTLSVerify: process.env.K8S_SKIP_TLS_VERIFY === "true",
|
|
134
153
|
};
|
|
135
154
|
const user = {
|
|
136
|
-
name:
|
|
137
|
-
token: process.env.K8S_TOKEN
|
|
155
|
+
name: "env-user",
|
|
156
|
+
token: process.env.K8S_TOKEN,
|
|
138
157
|
};
|
|
139
158
|
const context = {
|
|
140
|
-
name:
|
|
159
|
+
name: "env-context",
|
|
141
160
|
user: user.name,
|
|
142
|
-
cluster: cluster.name
|
|
161
|
+
cluster: cluster.name,
|
|
143
162
|
};
|
|
144
|
-
|
|
163
|
+
const kubeconfigContent = {
|
|
145
164
|
clusters: [cluster],
|
|
146
165
|
users: [user],
|
|
147
166
|
contexts: [context],
|
|
148
|
-
currentContext: context.name
|
|
149
|
-
}
|
|
167
|
+
currentContext: context.name,
|
|
168
|
+
};
|
|
169
|
+
this.kc.loadFromOptions(kubeconfigContent);
|
|
150
170
|
}
|
|
151
171
|
/**
|
|
152
172
|
* Check if KUBECONFIG_PATH environment variable is available
|
|
@@ -154,6 +174,9 @@ export class KubernetesManager {
|
|
|
154
174
|
hasEnvKubeconfigPath() {
|
|
155
175
|
return !!(process.env.KUBECONFIG_PATH && process.env.KUBECONFIG_PATH.trim());
|
|
156
176
|
}
|
|
177
|
+
hasEnvKubeconfig() {
|
|
178
|
+
return !!(process.env.KUBECONFIG && process.env.KUBECONFIG.trim());
|
|
179
|
+
}
|
|
157
180
|
/**
|
|
158
181
|
* Set the current context to the desired context name.
|
|
159
182
|
*
|
|
@@ -184,7 +207,7 @@ export class KubernetesManager {
|
|
|
184
207
|
await this.deleteResource(resource.kind, resource.name, resource.namespace);
|
|
185
208
|
}
|
|
186
209
|
catch (error) {
|
|
187
|
-
|
|
210
|
+
process.stderr.write(`Failed to delete ${resource.kind} ${resource.name}: ${error}\n`);
|
|
188
211
|
}
|
|
189
212
|
}
|
|
190
213
|
}
|
|
@@ -194,16 +217,16 @@ export class KubernetesManager {
|
|
|
194
217
|
async deleteResource(kind, name, namespace) {
|
|
195
218
|
switch (kind.toLowerCase()) {
|
|
196
219
|
case "pod":
|
|
197
|
-
await this.k8sApi.deleteNamespacedPod(name, namespace);
|
|
220
|
+
await this.k8sApi.deleteNamespacedPod({ name, namespace });
|
|
198
221
|
break;
|
|
199
222
|
case "deployment":
|
|
200
|
-
await this.k8sAppsApi.deleteNamespacedDeployment(name, namespace);
|
|
223
|
+
await this.k8sAppsApi.deleteNamespacedDeployment({ name, namespace });
|
|
201
224
|
break;
|
|
202
225
|
case "service":
|
|
203
|
-
await this.k8sApi.deleteNamespacedService(name, namespace);
|
|
226
|
+
await this.k8sApi.deleteNamespacedService({ name, namespace });
|
|
204
227
|
break;
|
|
205
228
|
case "cronjob":
|
|
206
|
-
await this.k8sBatchApi.deleteNamespacedCronJob(name, namespace);
|
|
229
|
+
await this.k8sBatchApi.deleteNamespacedCronJob({ name, namespace });
|
|
207
230
|
break;
|
|
208
231
|
}
|
|
209
232
|
this.resources = this.resources.filter((r) => !(r.kind === kind && r.name === name && r.namespace === namespace));
|
|
@@ -237,6 +260,56 @@ export class KubernetesManager {
|
|
|
237
260
|
* Uses K8S_NAMESPACE environment variable if set, otherwise defaults to "default"
|
|
238
261
|
*/
|
|
239
262
|
getDefaultNamespace() {
|
|
240
|
-
return process.env.K8S_NAMESPACE ||
|
|
263
|
+
return process.env.K8S_NAMESPACE || "default";
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Create temporary kubeconfig file from YAML content for kubectl commands
|
|
267
|
+
* @param kubeconfigYaml YAML content of the kubeconfig
|
|
268
|
+
*/
|
|
269
|
+
createTempKubeconfigFromYaml(kubeconfigYaml) {
|
|
270
|
+
try {
|
|
271
|
+
if (!kubeconfigYaml || typeof kubeconfigYaml !== "string") {
|
|
272
|
+
throw new Error(`Invalid kubeconfigYaml: ${typeof kubeconfigYaml}`);
|
|
273
|
+
}
|
|
274
|
+
const tempDir = os.tmpdir();
|
|
275
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
276
|
+
const randomString = Math.random().toString(36).substring(2);
|
|
277
|
+
const tempKubeconfigPath = path.join(tempDir, `kubeconfig-${timestamp}-${randomString}`);
|
|
278
|
+
// Write temporary kubeconfig file
|
|
279
|
+
fs.writeFileSync(tempKubeconfigPath, kubeconfigYaml, {
|
|
280
|
+
mode: 0o600,
|
|
281
|
+
encoding: "utf8",
|
|
282
|
+
});
|
|
283
|
+
// Set KUBECONFIG environment variable for kubectl commands
|
|
284
|
+
process.env.KUBECONFIG = tempKubeconfigPath;
|
|
285
|
+
// Function to clean up the temporary file
|
|
286
|
+
const cleanupTempFile = () => {
|
|
287
|
+
try {
|
|
288
|
+
if (fs.existsSync(tempKubeconfigPath)) {
|
|
289
|
+
fs.unlinkSync(tempKubeconfigPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (cleanupError) {
|
|
293
|
+
// Ignore cleanup errors
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
// Schedule cleanup of temporary file when process exits
|
|
297
|
+
process.on("exit", cleanupTempFile);
|
|
298
|
+
// Also clean up on SIGINT and SIGTERM (common in Docker containers)
|
|
299
|
+
["SIGINT", "SIGTERM"].forEach((signal) => {
|
|
300
|
+
process.on(signal, () => {
|
|
301
|
+
cleanupTempFile();
|
|
302
|
+
process.exit(0);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
// Additional cleanup for Docker container lifecycle
|
|
306
|
+
["SIGUSR1", "SIGUSR2"].forEach((signal) => {
|
|
307
|
+
process.on(signal, cleanupTempFile);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
// Continue without temporary file - kubectl commands may fail but JavaScript client will work
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
241
314
|
}
|
|
242
315
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-kubernetes",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "MCP server for interacting with Kubernetes clusters via kubectl",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@kubernetes/client-node": "
|
|
38
|
+
"@kubernetes/client-node": "1.3.0",
|
|
39
39
|
"@modelcontextprotocol/sdk": "1.7.0",
|
|
40
40
|
"express": "4.21.2",
|
|
41
41
|
"js-yaml": "4.1.0",
|
|
@@ -49,10 +49,5 @@
|
|
|
49
49
|
"shx": "0.3.4",
|
|
50
50
|
"typescript": "5.6.2",
|
|
51
51
|
"vitest": "2.1.9"
|
|
52
|
-
},
|
|
53
|
-
"overrides": {
|
|
54
|
-
"@kubernetes/client-node": {
|
|
55
|
-
"jsonpath-plus": "^10.3.0"
|
|
56
|
-
}
|
|
57
52
|
}
|
|
58
53
|
}
|