mcp-server-kubernetes 2.8.0 → 2.9.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 +251 -2
- package/dist/index.d.ts +106 -0
- package/dist/index.js +6 -0
- package/dist/models/helm-models.d.ts +18 -5
- package/dist/tools/helm-operations.d.ts +74 -14
- package/dist/tools/helm-operations.js +294 -98
- package/dist/tools/node-management.d.ts +100 -0
- package/dist/tools/node-management.js +291 -0
- package/dist/utils/sse.js +21 -0
- package/dist/utils/streamable-http.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: node_management
|
|
3
|
+
* Manage Kubernetes nodes with cordon, drain, and uncordon operations.
|
|
4
|
+
* Provides safety features for node operations and implements proper error handling
|
|
5
|
+
* and confirmation requirements for destructive operations.
|
|
6
|
+
* Note: Use kubectl_get with resourceType="nodes" to list nodes.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
10
|
+
/**
|
|
11
|
+
* Schema for node_management tool.
|
|
12
|
+
* - operation: Node operation to perform (cordon, drain, uncordon)
|
|
13
|
+
* - nodeName: Name of the node to operate on (required for cordon, drain, uncordon)
|
|
14
|
+
* - force: Force the operation even if there are unmanaged pods (for drain)
|
|
15
|
+
* - gracePeriod: Grace period for pod termination (for drain)
|
|
16
|
+
* - deleteLocalData: Delete local data even if emptyDir volumes are used (for drain)
|
|
17
|
+
* - ignoreDaemonsets: Ignore DaemonSet-managed pods (for drain)
|
|
18
|
+
* - timeout: Timeout for drain operation
|
|
19
|
+
* - dryRun: Show what would be done without actually doing it (for drain)
|
|
20
|
+
* - confirmDrain: Explicit confirmation to drain the node (required for drain)
|
|
21
|
+
*/
|
|
22
|
+
export const nodeManagementSchema = {
|
|
23
|
+
name: "node_management",
|
|
24
|
+
description: "Manage Kubernetes nodes with cordon, drain, and uncordon operations",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
operation: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Node operation to perform",
|
|
31
|
+
enum: ["cordon", "drain", "uncordon"],
|
|
32
|
+
},
|
|
33
|
+
nodeName: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Name of the node to operate on (required for cordon, drain, uncordon)",
|
|
36
|
+
},
|
|
37
|
+
force: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
description: "Force the operation even if there are pods not managed by a ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet (for drain operation)",
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
gracePeriod: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description: "Period of time in seconds given to each pod to terminate gracefully (for drain operation). If set to -1, uses the kubectl default grace period.",
|
|
45
|
+
default: -1,
|
|
46
|
+
},
|
|
47
|
+
deleteLocalData: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
description: "Delete local data even if emptyDir volumes are used (for drain operation)",
|
|
50
|
+
default: false,
|
|
51
|
+
},
|
|
52
|
+
ignoreDaemonsets: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
description: "Ignore DaemonSet-managed pods (for drain operation)",
|
|
55
|
+
default: true,
|
|
56
|
+
},
|
|
57
|
+
timeout: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "The length of time to wait before giving up (for drain operation, e.g., '5m', '1h')",
|
|
60
|
+
default: "0",
|
|
61
|
+
},
|
|
62
|
+
dryRun: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Show what would be done without actually doing it (for drain operation)",
|
|
65
|
+
default: false,
|
|
66
|
+
},
|
|
67
|
+
confirmDrain: {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
description: "Explicit confirmation to drain the node (required for drain operation)",
|
|
70
|
+
default: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["operation"],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Execute a command using child_process.execFileSync with proper error handling.
|
|
78
|
+
* @param command - The command to execute
|
|
79
|
+
* @param args - Array of command arguments
|
|
80
|
+
* @returns The command output as a string
|
|
81
|
+
* @throws Error if command execution fails
|
|
82
|
+
*/
|
|
83
|
+
const executeCommand = (command, args) => {
|
|
84
|
+
try {
|
|
85
|
+
return execFileSync(command, args, {
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
timeout: 300000, // 5 minutes timeout for node operations
|
|
88
|
+
maxBuffer: getSpawnMaxBuffer(),
|
|
89
|
+
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new Error(`${command} command failed: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Get the status of a specific node.
|
|
98
|
+
* @param nodeName - Name of the node to get status for
|
|
99
|
+
* @returns Node status as JSON object
|
|
100
|
+
* @throws Error if node status retrieval fails
|
|
101
|
+
*/
|
|
102
|
+
const getNodeStatus = (nodeName) => {
|
|
103
|
+
try {
|
|
104
|
+
const output = executeCommand("kubectl", ["get", "node", nodeName, "-o", "json"]);
|
|
105
|
+
return JSON.parse(output);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new Error(`Failed to get node status: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Main node_management function that handles all node operations.
|
|
113
|
+
* Implements safety features and proper error handling for node management tasks.
|
|
114
|
+
* @param params - Node management parameters
|
|
115
|
+
* @returns Promise with operation results
|
|
116
|
+
*/
|
|
117
|
+
export async function nodeManagement(params) {
|
|
118
|
+
const { operation, nodeName, force = false, gracePeriod = -1, deleteLocalData = false, ignoreDaemonsets = true, timeout = "0", dryRun = false, confirmDrain = false } = params;
|
|
119
|
+
try {
|
|
120
|
+
switch (operation) {
|
|
121
|
+
case "cordon":
|
|
122
|
+
return handleCordonNode(nodeName);
|
|
123
|
+
case "uncordon":
|
|
124
|
+
return handleUncordonNode(nodeName);
|
|
125
|
+
case "drain":
|
|
126
|
+
return handleDrainNode({
|
|
127
|
+
nodeName: nodeName,
|
|
128
|
+
force,
|
|
129
|
+
gracePeriod,
|
|
130
|
+
deleteLocalData,
|
|
131
|
+
ignoreDaemonsets,
|
|
132
|
+
timeout,
|
|
133
|
+
dryRun,
|
|
134
|
+
confirmDrain
|
|
135
|
+
});
|
|
136
|
+
default:
|
|
137
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: `Node management operation failed: ${error.message}`
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Handle the cordon node operation.
|
|
153
|
+
* @param nodeName - Name of the node to cordon
|
|
154
|
+
* @returns Promise with cordon operation results
|
|
155
|
+
*/
|
|
156
|
+
async function handleCordonNode(nodeName) {
|
|
157
|
+
try {
|
|
158
|
+
// Check if node exists and get current status
|
|
159
|
+
const nodeStatus = getNodeStatus(nodeName);
|
|
160
|
+
const isSchedulable = !nodeStatus.spec.unschedulable;
|
|
161
|
+
if (!isSchedulable) {
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `Node '${nodeName}' is already cordoned (unschedulable)`
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Cordon the node
|
|
172
|
+
executeCommand("kubectl", ["cordon", nodeName]);
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: `Successfully cordoned node '${nodeName}'. The node is now unschedulable.`
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw new Error(`Failed to cordon node: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Handle the uncordon node operation.
|
|
188
|
+
* @param nodeName - Name of the node to uncordon
|
|
189
|
+
* @returns Promise with uncordon operation results
|
|
190
|
+
*/
|
|
191
|
+
async function handleUncordonNode(nodeName) {
|
|
192
|
+
try {
|
|
193
|
+
// Check if node exists and get current status
|
|
194
|
+
const nodeStatus = getNodeStatus(nodeName);
|
|
195
|
+
const isSchedulable = !nodeStatus.spec.unschedulable;
|
|
196
|
+
if (isSchedulable) {
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: `Node '${nodeName}' is already uncordoned (schedulable)`
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// Uncordon the node
|
|
207
|
+
executeCommand("kubectl", ["uncordon", nodeName]);
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: `Successfully uncordoned node '${nodeName}'. The node is now schedulable.`
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
throw new Error(`Failed to uncordon node: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Handle the drain node operation with safety checks and confirmation.
|
|
223
|
+
* @param params - Drain operation parameters
|
|
224
|
+
* @returns Promise with drain operation results
|
|
225
|
+
*/
|
|
226
|
+
async function handleDrainNode(params) {
|
|
227
|
+
const { nodeName, force, gracePeriod, deleteLocalData, ignoreDaemonsets, timeout, dryRun, confirmDrain } = params;
|
|
228
|
+
try {
|
|
229
|
+
// Check if node exists and get current status
|
|
230
|
+
const nodeStatus = getNodeStatus(nodeName);
|
|
231
|
+
const isSchedulable = !nodeStatus.spec.unschedulable;
|
|
232
|
+
if (!isSchedulable) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: `Node '${nodeName}' is already cordoned (unschedulable). Drain operation may not be necessary.`
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Check for confirmation if not in dry run mode
|
|
243
|
+
if (!dryRun && !confirmDrain) {
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: `Drain operation requires explicit confirmation. Set confirmDrain=true to proceed with draining node '${nodeName}'.`
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Build drain command arguments
|
|
254
|
+
const drainArgs = ["drain", nodeName];
|
|
255
|
+
if (force)
|
|
256
|
+
drainArgs.push("--force");
|
|
257
|
+
if (gracePeriod >= 0)
|
|
258
|
+
drainArgs.push("--grace-period", gracePeriod.toString());
|
|
259
|
+
if (deleteLocalData)
|
|
260
|
+
drainArgs.push("--delete-local-data");
|
|
261
|
+
if (ignoreDaemonsets)
|
|
262
|
+
drainArgs.push("--ignore-daemonsets");
|
|
263
|
+
if (timeout !== "0")
|
|
264
|
+
drainArgs.push("--timeout", timeout);
|
|
265
|
+
if (dryRun)
|
|
266
|
+
drainArgs.push("--dry-run=client");
|
|
267
|
+
// Execute drain command
|
|
268
|
+
const drainOutput = executeCommand("kubectl", drainArgs);
|
|
269
|
+
if (dryRun) {
|
|
270
|
+
return {
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "text",
|
|
274
|
+
text: `Dry run drain operation for node '${nodeName}':\n\n${drainOutput}\n\nTo perform the actual drain, set dryRun=false and confirmDrain=true.`
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: `Successfully drained node '${nodeName}'.\n\n${drainOutput}`
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
throw new Error(`Failed to drain node: ${error.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
package/dist/utils/sse.js
CHANGED
|
@@ -21,6 +21,27 @@ export function startSSEServer(server) {
|
|
|
21
21
|
.send("Not found. Must pass valid sessionId as query param.");
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
+
app.get("/health", async (req, res) => {
|
|
25
|
+
res.json({ status: "ok" });
|
|
26
|
+
});
|
|
27
|
+
app.get("/ready", async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
// We can add more checks if required
|
|
30
|
+
// For now, we'll consider the server ready if it can respond to this request
|
|
31
|
+
res.json({
|
|
32
|
+
status: "ready",
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Readiness check failed:", error);
|
|
38
|
+
res.status(503).json({
|
|
39
|
+
status: "not ready",
|
|
40
|
+
reason: "Server initialization incomplete",
|
|
41
|
+
timestamp: new Date().toISOString()
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
24
45
|
let port = 3000;
|
|
25
46
|
try {
|
|
26
47
|
port = parseInt(process.env.PORT || "3000", 10);
|
|
@@ -64,6 +64,27 @@ export function startStreamableHTTPServer(server) {
|
|
|
64
64
|
id: null,
|
|
65
65
|
}));
|
|
66
66
|
});
|
|
67
|
+
app.get("/health", async (req, res) => {
|
|
68
|
+
res.json({ status: "ok" });
|
|
69
|
+
});
|
|
70
|
+
app.get("/ready", async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
// We can add more checks if required
|
|
73
|
+
// For now, we'll consider the server ready if it can respond to this request
|
|
74
|
+
res.json({
|
|
75
|
+
status: "ready",
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error("Readiness check failed:", error);
|
|
81
|
+
res.status(503).json({
|
|
82
|
+
status: "not ready",
|
|
83
|
+
reason: "Server initialization incomplete",
|
|
84
|
+
timestamp: new Date().toISOString()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
67
88
|
let port = 3000;
|
|
68
89
|
try {
|
|
69
90
|
port = parseInt(process.env.PORT || "3000", 10);
|