mcp-aws-manager 0.3.4 → 0.3.6
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/AWS_SSO_SETUP_GUIDE.md +133 -0
- package/AWS_SSO_SETUP_GUIDE_KO.md +70 -0
- package/IMPLEMENTATION_INTEGRATIONS.md +38 -4
- package/MCP_CLIENT_SETUP.md +3 -1
- package/MCP_CLIENT_SETUP_KO.md +107 -0
- package/README.md +248 -30
- package/README_KO.md +115 -0
- package/bin/mcp-aws-manager-mcp.js +1097 -649
- package/bin/mcp-aws-manager.js +970 -27
- package/package.json +6 -2
- package/AGENT_GUIDANCE_LOOP_TEMPLATE_KO.md +0 -68
|
@@ -1,736 +1,1184 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const { spawn } = require("node:child_process");
|
|
6
|
-
|
|
7
|
-
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
-
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
-
const z4 = require("zod/v4");
|
|
10
|
-
|
|
11
|
-
const z = z4.z || z4;
|
|
12
|
-
const pkg = require("../package.json");
|
|
13
|
-
|
|
14
|
-
const CLI_SCRIPT_PATH = path.join(__dirname, "mcp-aws-manager.js");
|
|
15
|
-
const DEFAULT_TIMEOUT_SEC = 300;
|
|
16
|
-
const DEFAULT_STDIO_TEXT_LIMIT = 16000;
|
|
17
|
-
const DEFAULT_JSON_TEXT_LIMIT = 120000;
|
|
18
|
-
|
|
19
|
-
function usageText() {
|
|
20
|
-
return [
|
|
21
|
-
"mcp-aws-manager-mcp",
|
|
22
|
-
"",
|
|
23
|
-
"MCP stdio wrapper for the mcp-aws-manager CLI.",
|
|
24
|
-
"",
|
|
25
|
-
"Usage:",
|
|
26
|
-
" mcp-aws-manager-mcp",
|
|
27
|
-
" mcp-aws-manager-mcp --help",
|
|
28
|
-
"",
|
|
29
|
-
"Notes:",
|
|
30
|
-
" - This process is an MCP stdio server.",
|
|
31
|
-
" - Exposes multi-service AWS inventory and optional runtime tools.",
|
|
32
|
-
""
|
|
33
|
-
].join("\n");
|
|
34
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const z4 = require("zod/v4");
|
|
10
|
+
|
|
11
|
+
const z = z4.z || z4;
|
|
12
|
+
const pkg = require("../package.json");
|
|
13
|
+
|
|
14
|
+
const CLI_SCRIPT_PATH = path.join(__dirname, "mcp-aws-manager.js");
|
|
15
|
+
const DEFAULT_TIMEOUT_SEC = 300;
|
|
16
|
+
const DEFAULT_STDIO_TEXT_LIMIT = 16000;
|
|
17
|
+
const DEFAULT_JSON_TEXT_LIMIT = 120000;
|
|
18
|
+
|
|
19
|
+
function usageText() {
|
|
20
|
+
return [
|
|
21
|
+
"mcp-aws-manager-mcp",
|
|
22
|
+
"",
|
|
23
|
+
"MCP stdio wrapper for the mcp-aws-manager CLI.",
|
|
24
|
+
"",
|
|
25
|
+
"Usage:",
|
|
26
|
+
" mcp-aws-manager-mcp",
|
|
27
|
+
" mcp-aws-manager-mcp --help",
|
|
28
|
+
"",
|
|
29
|
+
"Notes:",
|
|
30
|
+
" - This process is an MCP stdio server.",
|
|
31
|
+
" - Exposes multi-service AWS inventory and optional runtime tools.",
|
|
32
|
+
""
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldShowHelp(argv) {
|
|
37
|
+
return argv.includes("-h") || argv.includes("--help");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toCsvArg(values) {
|
|
41
|
+
if (!Array.isArray(values) || !values.length) return null;
|
|
42
|
+
return values.map((v) => String(v).trim()).filter(Boolean).join(",");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function truncateText(value, maxLength = DEFAULT_STDIO_TEXT_LIMIT) {
|
|
46
|
+
const text = String(value || "");
|
|
47
|
+
if (text.length <= maxLength) return text;
|
|
48
|
+
const suffix = `\n... [truncated ${text.length - maxLength} chars]`;
|
|
49
|
+
return text.slice(0, maxLength) + suffix;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseStderr(stderrText) {
|
|
53
|
+
const lines = String(stderrText || "")
|
|
54
|
+
.split(/\r?\n/)
|
|
55
|
+
.map((line) => line.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
const warnings = [];
|
|
59
|
+
const requiredActions = [];
|
|
60
|
+
let summaryLine = null;
|
|
61
|
+
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (line.startsWith("WARNING: ")) {
|
|
64
|
+
warnings.push(line.slice("WARNING: ".length));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.startsWith("Summary: ")) {
|
|
68
|
+
summaryLine = line;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (line.startsWith("ACTION_REQUIRED: ")) {
|
|
72
|
+
const payload = line.slice("ACTION_REQUIRED: ".length).trim();
|
|
73
|
+
const m = /^\[(.+?)\]\s*(.*)$/.exec(payload);
|
|
74
|
+
requiredActions.push({
|
|
75
|
+
code: m ? m[1] : "UNKNOWN",
|
|
76
|
+
message: m ? m[2] : payload,
|
|
77
|
+
hint: null
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (line.startsWith("ACTION_HINT: ")) {
|
|
82
|
+
if (requiredActions.length) {
|
|
83
|
+
requiredActions[requiredActions.length - 1].hint = line.slice("ACTION_HINT: ".length);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { lines, warnings, requiredActions, summaryLine };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildCliArgs(input) {
|
|
92
|
+
const args = [CLI_SCRIPT_PATH, "--format", "json"];
|
|
93
|
+
|
|
94
|
+
const profiles = toCsvArg(input.profiles);
|
|
95
|
+
if (profiles) args.push("--profiles", profiles);
|
|
96
|
+
|
|
97
|
+
const regions = toCsvArg(input.regions);
|
|
98
|
+
if (regions) args.push("--regions", regions);
|
|
99
|
+
|
|
100
|
+
const instanceIds = toCsvArg(input.instanceIds);
|
|
101
|
+
if (instanceIds) args.push("--instance-ids", instanceIds);
|
|
102
|
+
|
|
103
|
+
if (input.includeLambda === true) args.push("--include-lambda");
|
|
104
|
+
if (input.includeLambda === false) args.push("--no-include-lambda");
|
|
105
|
+
if (input.includeEc2 === true) args.push("--include-ec2");
|
|
106
|
+
if (input.includeEc2 === false) args.push("--no-ec2");
|
|
107
|
+
if (input.includeAlb === true) args.push("--include-alb");
|
|
108
|
+
if (input.includeAlb === false) args.push("--no-include-alb");
|
|
109
|
+
if (input.includeAsg === true) args.push("--include-asg");
|
|
110
|
+
if (input.includeAsg === false) args.push("--no-include-asg");
|
|
111
|
+
if (input.includeRds === true) args.push("--include-rds");
|
|
112
|
+
if (input.includeRds === false) args.push("--no-include-rds");
|
|
113
|
+
if (input.includeElastiCache === true) args.push("--include-elasticache");
|
|
114
|
+
if (input.includeElastiCache === false) args.push("--no-include-elasticache");
|
|
115
|
+
if (input.includeRoute53 === true) args.push("--include-route53");
|
|
116
|
+
if (input.includeRoute53 === false) args.push("--no-include-route53");
|
|
117
|
+
|
|
118
|
+
if (input.publicOnly) args.push("--public-only");
|
|
119
|
+
if (input.managedOnly) args.push("--managed-only");
|
|
120
|
+
|
|
121
|
+
if (input.autoRemediateSsm) args.push("--auto-remediate-ssm");
|
|
122
|
+
if (input.ssmInstanceProfileName) args.push("--ssm-instance-profile-name", input.ssmInstanceProfileName);
|
|
123
|
+
if (input.ssmInstanceProfileArn) args.push("--ssm-instance-profile-arn", input.ssmInstanceProfileArn);
|
|
124
|
+
if (input.allowReplaceProfile) args.push("--allow-replace-profile");
|
|
125
|
+
if (input.remediationWaitSec != null) args.push("--remediation-wait", String(input.remediationWaitSec));
|
|
126
|
+
|
|
127
|
+
if (input.runtimeSnapshot === false) args.push("--no-runtime-snapshot");
|
|
128
|
+
if (input.runtimeSnapshot === true) args.push("--runtime-snapshot");
|
|
129
|
+
if (input.snapshotTimeoutSec != null) args.push("--snapshot-timeout", String(input.snapshotTimeoutSec));
|
|
130
|
+
if (input.snapshotConcurrency != null) args.push("--snapshot-concurrency", String(input.snapshotConcurrency));
|
|
131
|
+
if (input.snapshotMaxKb != null) args.push("--snapshot-max-kb", String(input.snapshotMaxKb));
|
|
132
|
+
|
|
133
|
+
if (input.manualServerListPath) args.push("--manual-server-list", input.manualServerListPath);
|
|
134
|
+
const pemPaths = toCsvArg(input.pemPaths);
|
|
135
|
+
if (pemPaths) args.push("--pem-paths", pemPaths);
|
|
136
|
+
if (input.sshUser) args.push("--ssh-user", input.sshUser);
|
|
137
|
+
if (input.sshPort != null) args.push("--ssh-port", String(input.sshPort));
|
|
138
|
+
if (input.sshConnectTimeoutSec != null) args.push("--ssh-connect-timeout", String(input.sshConnectTimeoutSec));
|
|
139
|
+
if (input.htmlOutPath) args.push("--html-out", input.htmlOutPath);
|
|
140
|
+
if (input.openHtml) args.push("--open-html");
|
|
35
141
|
|
|
36
|
-
|
|
37
|
-
|
|
142
|
+
if (input.autoSsoLogin === false) args.push("--no-auto-sso-login");
|
|
143
|
+
if (input.autoSsoLogin === true) args.push("--auto-sso-login");
|
|
144
|
+
|
|
145
|
+
if (input.noProgress !== false) args.push("--no-progress");
|
|
146
|
+
|
|
147
|
+
return args;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function runDiscoverCli(input) {
|
|
151
|
+
const timeoutSec =
|
|
152
|
+
typeof input.timeoutSec === "number" && Number.isFinite(input.timeoutSec)
|
|
153
|
+
? input.timeoutSec
|
|
154
|
+
: DEFAULT_TIMEOUT_SEC;
|
|
155
|
+
const timeoutMs = Math.max(1, Math.round(timeoutSec * 1000));
|
|
156
|
+
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
const child = spawn(process.execPath, buildCliArgs(input), {
|
|
159
|
+
cwd: input.workingDirectory || process.cwd(),
|
|
160
|
+
env: process.env,
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let stdout = "";
|
|
165
|
+
let stderr = "";
|
|
166
|
+
let timedOut = false;
|
|
167
|
+
let exited = false;
|
|
168
|
+
|
|
169
|
+
if (child.stdout) {
|
|
170
|
+
child.stdout.setEncoding("utf8");
|
|
171
|
+
child.stdout.on("data", (chunk) => {
|
|
172
|
+
stdout += chunk;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (child.stderr) {
|
|
176
|
+
child.stderr.setEncoding("utf8");
|
|
177
|
+
child.stderr.on("data", (chunk) => {
|
|
178
|
+
stderr += chunk;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
if (exited) return;
|
|
184
|
+
timedOut = true;
|
|
185
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
if (!exited) {
|
|
188
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
189
|
+
}
|
|
190
|
+
}, 3000).unref();
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
timer.unref();
|
|
193
|
+
|
|
194
|
+
child.on("error", (error) => {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
exited = true;
|
|
197
|
+
resolve({
|
|
198
|
+
ok: false,
|
|
199
|
+
exitCode: null,
|
|
200
|
+
signal: null,
|
|
201
|
+
stdout,
|
|
202
|
+
stderr,
|
|
203
|
+
timedOut,
|
|
204
|
+
spawnError: error && error.message ? error.message : String(error)
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
child.on("close", (code, signal) => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
exited = true;
|
|
211
|
+
resolve({
|
|
212
|
+
ok: true,
|
|
213
|
+
exitCode: typeof code === "number" ? code : null,
|
|
214
|
+
signal: signal || null,
|
|
215
|
+
stdout,
|
|
216
|
+
stderr,
|
|
217
|
+
timedOut,
|
|
218
|
+
spawnError: null
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
38
222
|
}
|
|
39
223
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
224
|
+
let mutationAwsModules = null;
|
|
225
|
+
function loadMutationAwsModules() {
|
|
226
|
+
if (mutationAwsModules) return mutationAwsModules;
|
|
227
|
+
try {
|
|
228
|
+
const ec2 = require("@aws-sdk/client-ec2");
|
|
229
|
+
const creds = require("@aws-sdk/credential-providers");
|
|
230
|
+
mutationAwsModules = { ec2, creds };
|
|
231
|
+
return mutationAwsModules;
|
|
232
|
+
} catch {
|
|
233
|
+
throw new Error("Missing AWS SDK dependencies. Run: npm install");
|
|
234
|
+
}
|
|
43
235
|
}
|
|
44
236
|
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
237
|
+
function mutationAwsConfig(profile, region) {
|
|
238
|
+
const { creds } = loadMutationAwsModules();
|
|
239
|
+
const config = { region };
|
|
240
|
+
if (profile) {
|
|
241
|
+
config.credentials = creds.fromIni({ profile });
|
|
242
|
+
}
|
|
243
|
+
return config;
|
|
50
244
|
}
|
|
51
245
|
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
summaryLine = line;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (line.startsWith("ACTION_REQUIRED: ")) {
|
|
72
|
-
const payload = line.slice("ACTION_REQUIRED: ".length).trim();
|
|
73
|
-
const m = /^\[(.+?)\]\s*(.*)$/.exec(payload);
|
|
74
|
-
requiredActions.push({
|
|
75
|
-
code: m ? m[1] : "UNKNOWN",
|
|
76
|
-
message: m ? m[2] : payload,
|
|
77
|
-
hint: null
|
|
78
|
-
});
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (line.startsWith("ACTION_HINT: ")) {
|
|
82
|
-
if (requiredActions.length) {
|
|
83
|
-
requiredActions[requiredActions.length - 1].hint = line.slice("ACTION_HINT: ".length);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
246
|
+
function mutationErrorMeta(error, profile, region) {
|
|
247
|
+
const detail = `${error && error.name ? error.name : "Error"}: ${error && error.message ? error.message : String(error)}`;
|
|
248
|
+
const text = detail.toLowerCase();
|
|
249
|
+
if (
|
|
250
|
+
text.includes("credential") ||
|
|
251
|
+
text.includes("sso") ||
|
|
252
|
+
text.includes("token") ||
|
|
253
|
+
text.includes("unable to locate credentials")
|
|
254
|
+
) {
|
|
255
|
+
return {
|
|
256
|
+
code: "AWS_CREDENTIALS_REQUIRED",
|
|
257
|
+
message: `AWS authentication failed for profile=${profile || "default"} region=${region}.`,
|
|
258
|
+
hint: profile
|
|
259
|
+
? `Run 'aws sso login --profile ${profile}' or configure access key; then retry.`
|
|
260
|
+
: "Configure AWS credentials (SSO or access key) and retry."
|
|
261
|
+
};
|
|
86
262
|
}
|
|
87
|
-
|
|
88
|
-
|
|
263
|
+
if (text.includes("accessdenied") || text.includes("not authorized")) {
|
|
264
|
+
return {
|
|
265
|
+
code: "IAM_PERMISSION_REQUIRED",
|
|
266
|
+
message: `Missing AWS IAM permission for profile=${profile || "default"} region=${region}.`,
|
|
267
|
+
hint: detail
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
code: "AWS_OPERATION_FAILED",
|
|
272
|
+
message: `AWS operation failed for profile=${profile || "default"} region=${region}.`,
|
|
273
|
+
hint: detail
|
|
274
|
+
};
|
|
89
275
|
}
|
|
90
276
|
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const profiles = toCsvArg(input.profiles);
|
|
95
|
-
if (profiles) args.push("--profiles", profiles);
|
|
96
|
-
|
|
97
|
-
const regions = toCsvArg(input.regions);
|
|
98
|
-
if (regions) args.push("--regions", regions);
|
|
99
|
-
|
|
100
|
-
const instanceIds = toCsvArg(input.instanceIds);
|
|
101
|
-
if (instanceIds) args.push("--instance-ids", instanceIds);
|
|
102
|
-
|
|
103
|
-
if (input.includeLambda === true) args.push("--include-lambda");
|
|
104
|
-
if (input.includeLambda === false) args.push("--no-include-lambda");
|
|
105
|
-
if (input.includeEc2 === true) args.push("--include-ec2");
|
|
106
|
-
if (input.includeEc2 === false) args.push("--no-ec2");
|
|
107
|
-
if (input.includeAlb === true) args.push("--include-alb");
|
|
108
|
-
if (input.includeAlb === false) args.push("--no-include-alb");
|
|
109
|
-
if (input.includeAsg === true) args.push("--include-asg");
|
|
110
|
-
if (input.includeAsg === false) args.push("--no-include-asg");
|
|
111
|
-
if (input.includeRds === true) args.push("--include-rds");
|
|
112
|
-
if (input.includeRds === false) args.push("--no-include-rds");
|
|
113
|
-
if (input.includeElastiCache === true) args.push("--include-elasticache");
|
|
114
|
-
if (input.includeElastiCache === false) args.push("--no-include-elasticache");
|
|
115
|
-
if (input.includeRoute53 === true) args.push("--include-route53");
|
|
116
|
-
if (input.includeRoute53 === false) args.push("--no-include-route53");
|
|
117
|
-
|
|
118
|
-
if (input.publicOnly) args.push("--public-only");
|
|
119
|
-
if (input.managedOnly) args.push("--managed-only");
|
|
120
|
-
|
|
121
|
-
if (input.autoRemediateSsm) args.push("--auto-remediate-ssm");
|
|
122
|
-
if (input.ssmInstanceProfileName) args.push("--ssm-instance-profile-name", input.ssmInstanceProfileName);
|
|
123
|
-
if (input.ssmInstanceProfileArn) args.push("--ssm-instance-profile-arn", input.ssmInstanceProfileArn);
|
|
124
|
-
if (input.allowReplaceProfile) args.push("--allow-replace-profile");
|
|
125
|
-
if (input.remediationWaitSec != null) args.push("--remediation-wait", String(input.remediationWaitSec));
|
|
126
|
-
|
|
127
|
-
if (input.runtimeSnapshot === false) args.push("--no-runtime-snapshot");
|
|
128
|
-
if (input.runtimeSnapshot === true) args.push("--runtime-snapshot");
|
|
129
|
-
if (input.snapshotTimeoutSec != null) args.push("--snapshot-timeout", String(input.snapshotTimeoutSec));
|
|
130
|
-
if (input.snapshotConcurrency != null) args.push("--snapshot-concurrency", String(input.snapshotConcurrency));
|
|
131
|
-
if (input.snapshotMaxKb != null) args.push("--snapshot-max-kb", String(input.snapshotMaxKb));
|
|
132
|
-
|
|
133
|
-
if (input.autoSsoLogin === false) args.push("--no-auto-sso-login");
|
|
134
|
-
if (input.autoSsoLogin === true) args.push("--auto-sso-login");
|
|
135
|
-
|
|
136
|
-
if (input.noProgress !== false) args.push("--no-progress");
|
|
137
|
-
|
|
138
|
-
return args;
|
|
277
|
+
function normalizeProfile(profile) {
|
|
278
|
+
const value = String(profile == null ? "" : profile).trim();
|
|
279
|
+
return value || "default";
|
|
139
280
|
}
|
|
140
281
|
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
let stdout = "";
|
|
156
|
-
let stderr = "";
|
|
157
|
-
let timedOut = false;
|
|
158
|
-
let exited = false;
|
|
282
|
+
async function runEc2InstanceAction(input, action) {
|
|
283
|
+
const { ec2 } = loadMutationAwsModules();
|
|
284
|
+
const profile = normalizeProfile(input.profile);
|
|
285
|
+
const region = String(input.region || "").trim();
|
|
286
|
+
const instanceIds = Array.isArray(input.instanceIds) ? input.instanceIds.map((v) => String(v).trim()).filter(Boolean) : [];
|
|
287
|
+
if (!region) {
|
|
288
|
+
return { ok: false, error: "region is required", requiredAction: null };
|
|
289
|
+
}
|
|
290
|
+
if (!instanceIds.length) {
|
|
291
|
+
return { ok: false, error: "instanceIds is required", requiredAction: null };
|
|
292
|
+
}
|
|
159
293
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
294
|
+
const client = new ec2.EC2Client(mutationAwsConfig(profile, region));
|
|
295
|
+
try {
|
|
296
|
+
if (action === "start") {
|
|
297
|
+
const output = await client.send(new ec2.StartInstancesCommand({
|
|
298
|
+
InstanceIds: instanceIds,
|
|
299
|
+
DryRun: input.dryRun === true
|
|
300
|
+
}));
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
action,
|
|
304
|
+
profile,
|
|
305
|
+
region,
|
|
306
|
+
instanceIds,
|
|
307
|
+
response: {
|
|
308
|
+
startingInstances: output && output.StartingInstances ? output.StartingInstances : []
|
|
309
|
+
}
|
|
310
|
+
};
|
|
171
311
|
}
|
|
172
312
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
313
|
+
if (action === "stop") {
|
|
314
|
+
const output = await client.send(new ec2.StopInstancesCommand({
|
|
315
|
+
InstanceIds: instanceIds,
|
|
316
|
+
Force: input.force === true,
|
|
317
|
+
DryRun: input.dryRun === true
|
|
318
|
+
}));
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
action,
|
|
322
|
+
profile,
|
|
323
|
+
region,
|
|
324
|
+
instanceIds,
|
|
325
|
+
response: {
|
|
326
|
+
stoppingInstances: output && output.StoppingInstances ? output.StoppingInstances : []
|
|
180
327
|
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
timer.unref();
|
|
184
|
-
|
|
185
|
-
child.on("error", (error) => {
|
|
186
|
-
clearTimeout(timer);
|
|
187
|
-
exited = true;
|
|
188
|
-
resolve({
|
|
189
|
-
ok: false,
|
|
190
|
-
exitCode: null,
|
|
191
|
-
signal: null,
|
|
192
|
-
stdout,
|
|
193
|
-
stderr,
|
|
194
|
-
timedOut,
|
|
195
|
-
spawnError: error && error.message ? error.message : String(error)
|
|
196
|
-
});
|
|
197
|
-
});
|
|
328
|
+
};
|
|
329
|
+
}
|
|
198
330
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
331
|
+
if (action === "reboot") {
|
|
332
|
+
await client.send(new ec2.RebootInstancesCommand({
|
|
333
|
+
InstanceIds: instanceIds,
|
|
334
|
+
DryRun: input.dryRun === true
|
|
335
|
+
}));
|
|
336
|
+
return {
|
|
203
337
|
ok: true,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function tryParseJsonArray(text) {
|
|
216
|
-
const trimmed = String(text || "").trim();
|
|
217
|
-
if (!trimmed) {
|
|
218
|
-
return { ok: false, error: "Empty stdout" };
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const parsed = JSON.parse(trimmed);
|
|
222
|
-
if (!Array.isArray(parsed)) {
|
|
223
|
-
return { ok: false, error: "CLI JSON output was not an array" };
|
|
338
|
+
action,
|
|
339
|
+
profile,
|
|
340
|
+
region,
|
|
341
|
+
instanceIds,
|
|
342
|
+
response: { rebootRequested: true }
|
|
343
|
+
};
|
|
224
344
|
}
|
|
225
|
-
|
|
345
|
+
|
|
346
|
+
return { ok: false, error: `unsupported action: ${action}`, requiredAction: null };
|
|
226
347
|
} catch (error) {
|
|
227
|
-
return {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
error: error && error.message ? error.message : String(error),
|
|
351
|
+
requiredAction: mutationErrorMeta(error, profile, region)
|
|
352
|
+
};
|
|
353
|
+
} finally {
|
|
354
|
+
client.destroy();
|
|
228
355
|
}
|
|
229
356
|
}
|
|
230
357
|
|
|
231
|
-
function
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
asgRecords: 0,
|
|
239
|
-
rdsRecords: 0,
|
|
240
|
-
elasticacheRecords: 0,
|
|
241
|
-
route53ZoneRecords: 0,
|
|
242
|
-
publicIpRecords: 0,
|
|
243
|
-
ssmManagedCount: 0,
|
|
244
|
-
ssmOnlineCount: 0,
|
|
245
|
-
runtimeSnapshotCount: 0,
|
|
246
|
-
runtimeSnapshotSuccessCount: 0,
|
|
247
|
-
profiles: [],
|
|
248
|
-
regions: []
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const profileSet = new Set();
|
|
252
|
-
const regionSet = new Set();
|
|
253
|
-
|
|
254
|
-
for (const item of Array.isArray(records) ? records : []) {
|
|
255
|
-
summary.totalRecords += 1;
|
|
256
|
-
const resourceType = item && item.resourceType ? String(item.resourceType).toLowerCase() : null;
|
|
257
|
-
if (resourceType === "ec2") summary.ec2Records += 1;
|
|
258
|
-
if (resourceType === "lambda") summary.lambdaRecords += 1;
|
|
259
|
-
if (resourceType === "alb") summary.albRecords += 1;
|
|
260
|
-
if (resourceType === "target_group") summary.targetGroupRecords += 1;
|
|
261
|
-
if (resourceType === "asg") summary.asgRecords += 1;
|
|
262
|
-
if (resourceType === "rds") summary.rdsRecords += 1;
|
|
263
|
-
if (resourceType === "elasticache") summary.elasticacheRecords += 1;
|
|
264
|
-
if (resourceType === "route53_zone") summary.route53ZoneRecords += 1;
|
|
265
|
-
if (item && item.publicIp) summary.publicIpRecords += 1;
|
|
266
|
-
if (item && item.ssmManaged === true) summary.ssmManagedCount += 1;
|
|
267
|
-
if (item && item.ssmOnline === true) summary.ssmOnlineCount += 1;
|
|
268
|
-
if (item && item.runtimeSnapshot) {
|
|
269
|
-
summary.runtimeSnapshotCount += 1;
|
|
270
|
-
if (item.runtimeSnapshot.status === "Success") {
|
|
271
|
-
summary.runtimeSnapshotSuccessCount += 1;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (item && item.profile) profileSet.add(String(item.profile));
|
|
275
|
-
if (item && item.region) regionSet.add(String(item.region));
|
|
358
|
+
async function applyInstanceProfile(input) {
|
|
359
|
+
const { ec2 } = loadMutationAwsModules();
|
|
360
|
+
const profile = normalizeProfile(input.profile);
|
|
361
|
+
const region = String(input.region || "").trim();
|
|
362
|
+
const instanceId = String(input.instanceId || "").trim();
|
|
363
|
+
if (!region) {
|
|
364
|
+
return { ok: false, error: "region is required", requiredAction: null };
|
|
276
365
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
summary.regions = Array.from(regionSet).sort();
|
|
280
|
-
return summary;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function firstProfileArg(args) {
|
|
284
|
-
if (args && Array.isArray(args.profiles) && args.profiles.length > 0) {
|
|
285
|
-
return String(args.profiles[0]);
|
|
366
|
+
if (!instanceId) {
|
|
367
|
+
return { ok: false, error: "instanceId is required", requiredAction: null };
|
|
286
368
|
}
|
|
287
|
-
return "default";
|
|
288
|
-
}
|
|
289
369
|
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return matched[0];
|
|
370
|
+
const name = input.instanceProfileName ? String(input.instanceProfileName).trim() : "";
|
|
371
|
+
const arn = input.instanceProfileArn ? String(input.instanceProfileArn).trim() : "";
|
|
372
|
+
if (!name && !arn) {
|
|
373
|
+
return { ok: false, error: "instanceProfileName or instanceProfileArn is required", requiredAction: null };
|
|
295
374
|
}
|
|
296
|
-
|
|
297
|
-
}
|
|
375
|
+
const target = arn ? { Arn: arn } : { Name: name };
|
|
298
376
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
377
|
+
const client = new ec2.EC2Client(mutationAwsConfig(profile, region));
|
|
378
|
+
try {
|
|
379
|
+
const associations = await client.send(new ec2.DescribeIamInstanceProfileAssociationsCommand({
|
|
380
|
+
Filters: [{ Name: "instance-id", Values: [instanceId] }]
|
|
381
|
+
}));
|
|
382
|
+
const list = Array.isArray(associations && associations.IamInstanceProfileAssociations)
|
|
383
|
+
? associations.IamInstanceProfileAssociations
|
|
384
|
+
: [];
|
|
385
|
+
const active = list.find((item) => {
|
|
386
|
+
const state = item && item.State ? String(item.State).toLowerCase() : "";
|
|
387
|
+
return state === "associated" || state === "associating";
|
|
388
|
+
});
|
|
310
389
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
390
|
+
if (!active) {
|
|
391
|
+
const out = await client.send(new ec2.AssociateIamInstanceProfileCommand({
|
|
392
|
+
InstanceId: instanceId,
|
|
393
|
+
IamInstanceProfile: target
|
|
394
|
+
}));
|
|
315
395
|
return {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
396
|
+
ok: true,
|
|
397
|
+
action: "associate",
|
|
398
|
+
profile,
|
|
399
|
+
region,
|
|
400
|
+
instanceId,
|
|
401
|
+
response: {
|
|
402
|
+
associationId: out && out.IamInstanceProfileAssociation ? out.IamInstanceProfileAssociation.AssociationId : null
|
|
403
|
+
}
|
|
324
404
|
};
|
|
325
405
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
code,
|
|
329
|
-
title: "AWS credentials required",
|
|
330
|
-
steps: [
|
|
331
|
-
"??????熬곣뫁夷?熬곣뫗踰????遊꾤춯?밸퉾筌?????깆젧??琉얠돪??(SSO ???裕?access key).",
|
|
332
|
-
"SSO??寃밸듆 'aws configure sso --profile <profile>' ???β돦裕??筌뤿굝由?筌뤾쑴??",
|
|
333
|
-
"?熬곣뫁????'?熬곣뫁?????┑?????면썒??닔???"
|
|
334
|
-
],
|
|
335
|
-
confirmText: "???遊꾤춯?밸퉾筌????깆젧/?β돦裕??筌뤾쑴逾???硫명뀬???좊듆 '?熬곣뫁?????┑?????면썒??닔???"
|
|
336
|
-
};
|
|
337
|
-
case "SET_SSM_INSTANCE_PROFILE":
|
|
338
|
-
return {
|
|
339
|
-
code,
|
|
340
|
-
title: "SSM remediation target missing",
|
|
341
|
-
steps: [
|
|
342
|
-
"???吏??곌랜踰?袁ㅻご??????濡?졎嶺?instance profile ???藥????裕?ARN??嶺뚯솘??筌먐삵돵????紐껊퉵??",
|
|
343
|
-
"???깅쾳 ?????繞???濡る룎????節띾쐾 ?熬곣뫀堉??琉얠돪?? --ssm-instance-profile-name ???裕?--ssm-instance-profile-arn",
|
|
344
|
-
"?熬곣뫁????'?熬곣뫁?????┑?????면썒??닔???"
|
|
345
|
-
],
|
|
346
|
-
confirmText: "?熬곣뫁夷???逾?????⑤챷諭?嶺뚯솘??筌먐삳빳???좊듆 '?熬곣뫁?????┑?????면썒??닔???"
|
|
347
|
-
};
|
|
348
|
-
case "SSM_ROLE_OR_AGENT_REQUIRED":
|
|
349
|
-
return {
|
|
350
|
-
code,
|
|
351
|
-
title: "Instance is not SSM managed",
|
|
352
|
-
steps: [
|
|
353
|
-
"?筌뤾쑬裕??怨룸츩 ?????AmazonSSMManagedInstanceCore???????琉얠돪??",
|
|
354
|
-
"SSM Agent?? ???덈콦??怨뚯씩(SSM endpoint/?筌뤿굛????롪퍔?δ빳??띠럾? ?筌먦끆留?筌? ?筌먦끉逾??琉얠돪??",
|
|
355
|
-
"?熬곣뫁????'?熬곣뫁?????┑?????면썒??닔???"
|
|
356
|
-
],
|
|
357
|
-
confirmText: "SSM ??㉱????⑤객臾???브퀗?????덈펲嶺?'?熬곣뫁?????┑?????면썒??닔???"
|
|
358
|
-
};
|
|
359
|
-
case "INSTANCE_HAS_PROFILE":
|
|
360
|
-
return {
|
|
361
|
-
code,
|
|
362
|
-
title: "Existing instance profile detected",
|
|
363
|
-
steps: [
|
|
364
|
-
"?リ옇????筌뤾쑬裕??怨룸츩 ?熬곣뫁夷???逾?????곕????덈펲.",
|
|
365
|
-
"??ルㅎ臾?1: ?リ옇????????筌먦끉???SSM 雅?굝??뇡???怨뺣뼺???紐껊퉵??",
|
|
366
|
-
"??ルㅎ臾?2: ???吏???흮?우뮁紐???믨퀡由?춯?allowReplaceProfile=true ??????熬곥굥????덈펲."
|
|
367
|
-
],
|
|
368
|
-
confirmText: "??⑤챷????꾩렮維뽬떋???筌먐삳빳???좊듆 '?熬곣뫁?????┑?????면썒??닔???"
|
|
369
|
-
};
|
|
370
|
-
case "IAM_PROFILE_ASSOCIATION_FAILED":
|
|
371
|
-
case "IAM_PROFILE_REPLACE_FAILED":
|
|
372
|
-
return {
|
|
373
|
-
code,
|
|
374
|
-
title: "Missing IAM permission for remediation",
|
|
375
|
-
steps: [
|
|
376
|
-
"???덈뺄 ?낅슣?섊뙼??EC2 ?筌뤾쑬裕??怨룸츩 ?熬곣뫁夷???逾???⑤슡????흮??雅?굝??뇡???遊붋????筌뤾쑴??",
|
|
377
|
-
"?熬곣뫗??雅?굝??뇡? ec2:AssociateIamInstanceProfile, ec2:ReplaceIamInstanceProfileAssociation(??흮????, iam:PassRole",
|
|
378
|
-
"?熬곣뫁????'?熬곣뫁?????┑?????면썒??닔???"
|
|
379
|
-
],
|
|
380
|
-
confirmText: "IAM 雅?굝??뇡??꾩룇瑗?????硫명뀬???좊듆 '?熬곣뫁?????┑?????면썒??닔???"
|
|
381
|
-
};
|
|
382
|
-
case "SSM_RUNCOMMAND_PERMISSION_REQUIRED":
|
|
383
|
-
return {
|
|
384
|
-
code,
|
|
385
|
-
title: "Missing SSM RunCommand permission",
|
|
386
|
-
steps: [
|
|
387
|
-
"???덈뺄 ?낅슣?섊뙼??SSM 嶺뚮ㅏ援앲??雅?굝??뇡???遊붋????筌뤾쑴??",
|
|
388
|
-
"?熬곣뫗??雅?굝??뇡? ssm:SendCommand, ssm:GetCommandInvocation",
|
|
389
|
-
"?熬곣뫁????'?熬곣뫁?????┑?????면썒??닔???"
|
|
390
|
-
],
|
|
391
|
-
confirmText: "SSM 雅?굝??뇡??꾩룇瑗?????硫명뀬???좊듆 '?熬곣뫁?????┑?????면썒??닔???"
|
|
392
|
-
};
|
|
393
|
-
case "LAMBDA_LIST_PERMISSION_REQUIRED":
|
|
394
|
-
return {
|
|
395
|
-
code,
|
|
396
|
-
title: "Missing Lambda list permission",
|
|
397
|
-
steps: [
|
|
398
|
-
"??쎈뻬 雅뚯눘猿??Lambda 鈺곌퀬??亦낅슦釉???봔鈺곌퉲鍮??덈뼄.",
|
|
399
|
-
"?袁⑹뒄 亦낅슦釉? lambda:ListFunctions",
|
|
400
|
-
"亦낅슦釉?獄쏆꼷????'??袁⑥┷'??⑦????젻雅뚯눘苑??"
|
|
401
|
-
],
|
|
402
|
-
confirmText: "Lambda 亦낅슦釉?獄쏆꼷?????멸돌筌?'??袁⑥┷'??⑦????젻雅뚯눘苑??"
|
|
403
|
-
};
|
|
404
|
-
case "ELBV2_LIST_PERMISSION_REQUIRED":
|
|
405
|
-
return {
|
|
406
|
-
code,
|
|
407
|
-
title: "Missing ELBv2 list permission",
|
|
408
|
-
steps: [
|
|
409
|
-
"Grant permissions to list load balancers and target groups.",
|
|
410
|
-
"Required: elasticloadbalancing:DescribeLoadBalancers and elasticloadbalancing:DescribeTargetGroups.",
|
|
411
|
-
"Retry after permission update."
|
|
412
|
-
],
|
|
413
|
-
confirmText: "After ELBv2 permission update, reply 'completed' and retry."
|
|
414
|
-
};
|
|
415
|
-
case "ASG_LIST_PERMISSION_REQUIRED":
|
|
416
|
-
return {
|
|
417
|
-
code,
|
|
418
|
-
title: "Missing Auto Scaling list permission",
|
|
419
|
-
steps: [
|
|
420
|
-
"Grant permission to read Auto Scaling Groups.",
|
|
421
|
-
"Required: autoscaling:DescribeAutoScalingGroups.",
|
|
422
|
-
"Retry after permission update."
|
|
423
|
-
],
|
|
424
|
-
confirmText: "After Auto Scaling permission update, reply 'completed' and retry."
|
|
425
|
-
};
|
|
426
|
-
case "RDS_LIST_PERMISSION_REQUIRED":
|
|
427
|
-
return {
|
|
428
|
-
code,
|
|
429
|
-
title: "Missing RDS list permission",
|
|
430
|
-
steps: [
|
|
431
|
-
"Grant permission to list RDS DB instances.",
|
|
432
|
-
"Required: rds:DescribeDBInstances.",
|
|
433
|
-
"Retry after permission update."
|
|
434
|
-
],
|
|
435
|
-
confirmText: "After RDS permission update, reply 'completed' and retry."
|
|
436
|
-
};
|
|
437
|
-
case "ELASTICACHE_LIST_PERMISSION_REQUIRED":
|
|
438
|
-
return {
|
|
439
|
-
code,
|
|
440
|
-
title: "Missing ElastiCache list permission",
|
|
441
|
-
steps: [
|
|
442
|
-
"Grant permission to list ElastiCache clusters.",
|
|
443
|
-
"Required: elasticache:DescribeCacheClusters.",
|
|
444
|
-
"Retry after permission update."
|
|
445
|
-
],
|
|
446
|
-
confirmText: "After ElastiCache permission update, reply 'completed' and retry."
|
|
447
|
-
};
|
|
448
|
-
case "ROUTE53_LIST_PERMISSION_REQUIRED":
|
|
406
|
+
|
|
407
|
+
if (!input.allowReplaceProfile) {
|
|
449
408
|
return {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"
|
|
456
|
-
|
|
457
|
-
|
|
409
|
+
ok: true,
|
|
410
|
+
requiresUserAction: true,
|
|
411
|
+
requiredAction: {
|
|
412
|
+
code: "INSTANCE_HAS_PROFILE",
|
|
413
|
+
message: `Instance ${instanceId} already has an instance profile.`,
|
|
414
|
+
hint: "Set allowReplaceProfile=true to replace existing association."
|
|
415
|
+
},
|
|
416
|
+
action: "skipped-existing-profile",
|
|
417
|
+
profile,
|
|
418
|
+
region,
|
|
419
|
+
instanceId
|
|
458
420
|
};
|
|
459
|
-
|
|
460
|
-
return defaultItem;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function buildAgentGuidance(requiredActions, toolName, args) {
|
|
465
|
-
const items = Array.isArray(requiredActions)
|
|
466
|
-
? requiredActions.map((action) => guidanceForAction(action, args))
|
|
467
|
-
: [];
|
|
421
|
+
}
|
|
468
422
|
|
|
469
|
-
|
|
423
|
+
const out = await client.send(new ec2.ReplaceIamInstanceProfileAssociationCommand({
|
|
424
|
+
AssociationId: active.AssociationId,
|
|
425
|
+
IamInstanceProfile: target
|
|
426
|
+
}));
|
|
470
427
|
return {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
428
|
+
ok: true,
|
|
429
|
+
action: "replace",
|
|
430
|
+
profile,
|
|
431
|
+
region,
|
|
432
|
+
instanceId,
|
|
433
|
+
response: {
|
|
434
|
+
associationId: out && out.IamInstanceProfileAssociation ? out.IamInstanceProfileAssociation.AssociationId : active.AssociationId
|
|
435
|
+
}
|
|
477
436
|
};
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
error: error && error.message ? error.message : String(error),
|
|
441
|
+
requiredAction: mutationErrorMeta(error, profile, region)
|
|
442
|
+
};
|
|
443
|
+
} finally {
|
|
444
|
+
client.destroy();
|
|
478
445
|
}
|
|
479
|
-
|
|
480
|
-
const firstItem = items[0];
|
|
481
|
-
const lines = [];
|
|
482
|
-
lines.push("AWS 상태 조회를 계속하려면 아래 조치가 필요합니다.");
|
|
483
|
-
lines.push(`1. [${firstItem.code}] ${firstItem.title}`);
|
|
484
|
-
for (let i = 0; i < firstItem.steps.length; i += 1) {
|
|
485
|
-
lines.push(`${i + 1}. ${firstItem.steps[i]}`);
|
|
486
|
-
}
|
|
487
|
-
lines.push(firstItem.confirmText);
|
|
488
|
-
|
|
489
|
-
return {
|
|
490
|
-
mode: "human_in_the_loop",
|
|
491
|
-
autoRetryRecommended: true,
|
|
492
|
-
retryTool: toolName,
|
|
493
|
-
retryArgs: args,
|
|
494
|
-
completionTrigger: "사용자가 '완료' 또는 조치 완료 의사를 전달하면 같은 입력으로 재시도",
|
|
495
|
-
userChecklist: items,
|
|
496
|
-
assistantMessageTemplate: lines.join("\n")
|
|
497
|
-
};
|
|
498
446
|
}
|
|
499
447
|
|
|
500
|
-
function
|
|
501
|
-
|
|
448
|
+
function tryParseJsonArray(text) {
|
|
449
|
+
const trimmed = String(text || "").trim();
|
|
450
|
+
if (!trimmed) {
|
|
451
|
+
return { ok: false, error: "Empty stdout" };
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const parsed = JSON.parse(trimmed);
|
|
455
|
+
if (!Array.isArray(parsed)) {
|
|
456
|
+
return { ok: false, error: "CLI JSON output was not an array" };
|
|
457
|
+
}
|
|
458
|
+
return { ok: true, value: parsed };
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return { ok: false, error: error && error.message ? error.message : String(error) };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function summarizeRecords(records) {
|
|
465
|
+
const summary = {
|
|
466
|
+
totalRecords: 0,
|
|
467
|
+
ec2Records: 0,
|
|
468
|
+
lambdaRecords: 0,
|
|
469
|
+
albRecords: 0,
|
|
470
|
+
targetGroupRecords: 0,
|
|
471
|
+
asgRecords: 0,
|
|
472
|
+
rdsRecords: 0,
|
|
473
|
+
elasticacheRecords: 0,
|
|
474
|
+
route53ZoneRecords: 0,
|
|
475
|
+
publicIpRecords: 0,
|
|
476
|
+
ssmManagedCount: 0,
|
|
477
|
+
ssmOnlineCount: 0,
|
|
478
|
+
runtimeSnapshotCount: 0,
|
|
479
|
+
runtimeSnapshotSuccessCount: 0,
|
|
480
|
+
profiles: [],
|
|
481
|
+
regions: []
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const profileSet = new Set();
|
|
485
|
+
const regionSet = new Set();
|
|
486
|
+
|
|
487
|
+
for (const item of Array.isArray(records) ? records : []) {
|
|
488
|
+
summary.totalRecords += 1;
|
|
489
|
+
const resourceType = item && item.resourceType ? String(item.resourceType).toLowerCase() : null;
|
|
490
|
+
if (resourceType === "ec2") summary.ec2Records += 1;
|
|
491
|
+
if (resourceType === "lambda") summary.lambdaRecords += 1;
|
|
492
|
+
if (resourceType === "alb") summary.albRecords += 1;
|
|
493
|
+
if (resourceType === "target_group") summary.targetGroupRecords += 1;
|
|
494
|
+
if (resourceType === "asg") summary.asgRecords += 1;
|
|
495
|
+
if (resourceType === "rds") summary.rdsRecords += 1;
|
|
496
|
+
if (resourceType === "elasticache") summary.elasticacheRecords += 1;
|
|
497
|
+
if (resourceType === "route53_zone") summary.route53ZoneRecords += 1;
|
|
498
|
+
if (item && item.publicIp) summary.publicIpRecords += 1;
|
|
499
|
+
if (item && item.ssmManaged === true) summary.ssmManagedCount += 1;
|
|
500
|
+
if (item && item.ssmOnline === true) summary.ssmOnlineCount += 1;
|
|
501
|
+
if (item && item.runtimeSnapshot) {
|
|
502
|
+
summary.runtimeSnapshotCount += 1;
|
|
503
|
+
if (item.runtimeSnapshot.status === "Success") {
|
|
504
|
+
summary.runtimeSnapshotSuccessCount += 1;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (item && item.profile) profileSet.add(String(item.profile));
|
|
508
|
+
if (item && item.region) regionSet.add(String(item.region));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
summary.profiles = Array.from(profileSet).sort();
|
|
512
|
+
summary.regions = Array.from(regionSet).sort();
|
|
513
|
+
return summary;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function firstProfileArg(args) {
|
|
517
|
+
if (args && Array.isArray(args.profiles) && args.profiles.length > 0) {
|
|
518
|
+
return String(args.profiles[0]);
|
|
519
|
+
}
|
|
520
|
+
return "default";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function inferSsoLoginCommand(action, args) {
|
|
524
|
+
const hint = action && action.hint ? String(action.hint) : "";
|
|
525
|
+
const matched = /aws sso login --profile\s+[^\s'"]+/i.exec(hint);
|
|
526
|
+
if (matched) {
|
|
527
|
+
return matched[0];
|
|
528
|
+
}
|
|
529
|
+
return `aws sso login --profile ${firstProfileArg(args)}`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function guidanceForAction(action, args) {
|
|
533
|
+
const code = action && action.code ? String(action.code) : "UNKNOWN";
|
|
534
|
+
const defaultItem = {
|
|
535
|
+
code,
|
|
536
|
+
title: "Manual action required",
|
|
537
|
+
steps: [
|
|
538
|
+
action && action.message ? action.message : "A manual action is required.",
|
|
539
|
+
action && action.hint ? action.hint : "After completing the action, reply '????썹땟?? to continue."
|
|
540
|
+
],
|
|
541
|
+
confirmText: "??됰슦????잙?猷뉓떋? ????썹땟???????'????썹땟??????維◈?????筌롫똻??????? ??醫딆┻?? ???됰Ŋ??????Β?????嶺????????꾣뤃管逾??????泥??????딅젩."
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
switch (code) {
|
|
545
|
+
case "SSO_LOGIN_NEEDED":
|
|
546
|
+
case "SSO_REAUTH_REQUIRED": {
|
|
547
|
+
const cmd = inferSsoLoginCommand(action, args);
|
|
548
|
+
return {
|
|
549
|
+
code,
|
|
550
|
+
title: "AWS SSO login required",
|
|
551
|
+
steps: [
|
|
552
|
+
`Run: ${cmd}`,
|
|
553
|
+
"Complete browser SSO login and MFA.",
|
|
554
|
+
"Alternative: if SSO/access key cannot be used, switch to manual fallback with manualServerListPath (JSON/CSV server list + PEM)."
|
|
555
|
+
],
|
|
556
|
+
confirmText: "After completing one path (SSO/access key or manual fallback), reply 'completed' and retry."
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
case "AWS_CREDENTIALS_REQUIRED":
|
|
560
|
+
return {
|
|
561
|
+
code,
|
|
562
|
+
title: "AWS credentials required",
|
|
563
|
+
steps: [
|
|
564
|
+
"Configure AWS auth first (SSO recommended, or access key).",
|
|
565
|
+
"SSO example: aws configure sso --profile <profile> then aws sso login --profile <profile>.",
|
|
566
|
+
"Alternative: if auth cannot be issued in this environment, switch to manual fallback with manualServerListPath (JSON/CSV server list + PEM)."
|
|
567
|
+
],
|
|
568
|
+
confirmText: "After completing one path (auth setup or manual fallback), reply 'completed' and retry."
|
|
569
|
+
};
|
|
570
|
+
case "SET_SSM_INSTANCE_PROFILE":
|
|
571
|
+
return {
|
|
572
|
+
code,
|
|
573
|
+
title: "SSM remediation target missing",
|
|
574
|
+
steps: [
|
|
575
|
+
"???嶺???⑤슢?뽫뵓嫄붋??ш낄??????????鈺곕〕??instance profile ??????????ARN???꿔꺂????癲ル슢怡??щ쨫????嶺뚮ㅎ????",
|
|
576
|
+
"???繹먮굞???????嚥????β뼯援η뙴????壤굿??좊㎧ ????썹땟????嶺뚮슣堉??? --ssm-instance-profile-name ?????--ssm-instance-profile-arn",
|
|
577
|
+
"????썹땟????'????썹땟??????維◈?????筌롫똻???????"
|
|
578
|
+
],
|
|
579
|
+
confirmText: "????썹땟怨⒲뀋???????????쇨덫???꿔꺂????癲ル슢怡??몃춣????ル봾諭?'????썹땟??????維◈?????筌롫똻???????"
|
|
580
|
+
};
|
|
581
|
+
case "SSM_ROLE_OR_AGENT_REQUIRED":
|
|
582
|
+
return {
|
|
583
|
+
code,
|
|
584
|
+
title: "Instance is not SSM managed",
|
|
585
|
+
steps: [
|
|
586
|
+
"?癲ル슢???吏????ㅿ폍筌??????AmazonSSMManagedInstanceCore???????嶺뚮슣堉???",
|
|
587
|
+
"SSM Agent?? ?????됱뎽????ㅼ뒩??SSM endpoint/?癲ル슢?뤸뤃????嚥▲굧???뚪뜮???醫딆쓧? ?癲ル슢캉??낆춹?癲? ?癲ル슢캉?????嶺뚮슣堉???",
|
|
588
|
+
"????썹땟????'????썹땟??????維◈?????筌롫똻???????"
|
|
589
|
+
],
|
|
590
|
+
confirmText: "SSM ???援온??????븐뻤?????됰슦????????딅젩??'????썹땟??????維◈?????筌롫똻???????"
|
|
591
|
+
};
|
|
592
|
+
case "INSTANCE_HAS_PROFILE":
|
|
593
|
+
return {
|
|
594
|
+
code,
|
|
595
|
+
title: "Existing instance profile detected",
|
|
596
|
+
steps: [
|
|
597
|
+
"???뚯?????癲ル슢???吏????ㅿ폍筌?????썹땟怨⒲뀋????????????????딅젩.",
|
|
598
|
+
"????k??1: ???뚯?????????癲ル슢캉????SSM ???????????ㅻ쿋????嶺뚮ㅎ????",
|
|
599
|
+
"????k??2: ???嶺???????怨뺤툋嶺???沃섃뫂??먮뎨???allowReplaceProfile=true ?????????꾣뤃??????딅젩."
|
|
600
|
+
],
|
|
601
|
+
confirmText: "????쇨덫????熬곣뫖?삥납??????癲ル슢怡??몃춣????ル봾諭?'????썹땟??????維◈?????筌롫똻???????"
|
|
602
|
+
};
|
|
603
|
+
case "IAM_PROFILE_ASSOCIATION_FAILED":
|
|
604
|
+
case "IAM_PROFILE_REPLACE_FAILED":
|
|
605
|
+
return {
|
|
606
|
+
code,
|
|
607
|
+
title: "Missing IAM permission for remediation",
|
|
608
|
+
steps: [
|
|
609
|
+
"?????덊떀 ???녿뮝???낆뇢??EC2 ?癲ル슢???吏????ㅿ폍筌?????썹땟怨⒲뀋?????????쇰뮚???????????????????낇뀘?????癲ル슢????",
|
|
610
|
+
"????썹땟????????? ec2:AssociateIamInstanceProfile, ec2:ReplaceIamInstanceProfileAssociation(????????, iam:PassRole",
|
|
611
|
+
"????썹땟????'????썹땟??????維◈?????筌롫똻???????"
|
|
612
|
+
],
|
|
613
|
+
confirmText: "IAM ????????熬곣뫖利??????嶺뚮∥梨?????ル봾諭?'????썹땟??????維◈?????筌롫똻???????"
|
|
614
|
+
};
|
|
615
|
+
case "SSM_RUNCOMMAND_PERMISSION_REQUIRED":
|
|
616
|
+
return {
|
|
617
|
+
code,
|
|
618
|
+
title: "Missing SSM RunCommand permission",
|
|
619
|
+
steps: [
|
|
620
|
+
"?????덊떀 ???녿뮝???낆뇢??SSM ?꿔꺂??琉몃쨨??????????????낇뀘?????癲ル슢????",
|
|
621
|
+
"????썹땟????????? ssm:SendCommand, ssm:GetCommandInvocation",
|
|
622
|
+
"????썹땟????'????썹땟??????維◈?????筌롫똻???????"
|
|
623
|
+
],
|
|
624
|
+
confirmText: "SSM ????????熬곣뫖利??????嶺뚮∥梨?????ル봾諭?'????썹땟??????維◈?????筌롫똻???????"
|
|
625
|
+
};
|
|
626
|
+
case "LAMBDA_LIST_PERMISSION_REQUIRED":
|
|
627
|
+
return {
|
|
628
|
+
code,
|
|
629
|
+
title: "Missing Lambda list permission",
|
|
630
|
+
steps: [
|
|
631
|
+
"????덈틖 ??낆뒩??딅쇊??Lambda ?釉뚰?????援???????딅텑??釉뚰?轅대쑏?????덊렡.",
|
|
632
|
+
"??ш끽維????援???? lambda:ListFunctions",
|
|
633
|
+
"??援?????袁⑸즵?????'???ш끽維??????뫢???????뜏??????"
|
|
634
|
+
],
|
|
635
|
+
confirmText: "Lambda ??援?????袁⑸즵??????筌롫챶猷롳┼?'???ш끽維??????뫢???????뜏??????"
|
|
636
|
+
};
|
|
637
|
+
case "ELBV2_LIST_PERMISSION_REQUIRED":
|
|
638
|
+
return {
|
|
639
|
+
code,
|
|
640
|
+
title: "Missing ELBv2 list permission",
|
|
641
|
+
steps: [
|
|
642
|
+
"Grant permissions to list load balancers and target groups.",
|
|
643
|
+
"Required: elasticloadbalancing:DescribeLoadBalancers and elasticloadbalancing:DescribeTargetGroups.",
|
|
644
|
+
"Retry after permission update."
|
|
645
|
+
],
|
|
646
|
+
confirmText: "After ELBv2 permission update, reply 'completed' and retry."
|
|
647
|
+
};
|
|
648
|
+
case "ASG_LIST_PERMISSION_REQUIRED":
|
|
649
|
+
return {
|
|
650
|
+
code,
|
|
651
|
+
title: "Missing Auto Scaling list permission",
|
|
652
|
+
steps: [
|
|
653
|
+
"Grant permission to read Auto Scaling Groups.",
|
|
654
|
+
"Required: autoscaling:DescribeAutoScalingGroups.",
|
|
655
|
+
"Retry after permission update."
|
|
656
|
+
],
|
|
657
|
+
confirmText: "After Auto Scaling permission update, reply 'completed' and retry."
|
|
658
|
+
};
|
|
659
|
+
case "RDS_LIST_PERMISSION_REQUIRED":
|
|
660
|
+
return {
|
|
661
|
+
code,
|
|
662
|
+
title: "Missing RDS list permission",
|
|
663
|
+
steps: [
|
|
664
|
+
"Grant permission to list RDS DB instances.",
|
|
665
|
+
"Required: rds:DescribeDBInstances.",
|
|
666
|
+
"Retry after permission update."
|
|
667
|
+
],
|
|
668
|
+
confirmText: "After RDS permission update, reply 'completed' and retry."
|
|
669
|
+
};
|
|
670
|
+
case "ELASTICACHE_LIST_PERMISSION_REQUIRED":
|
|
671
|
+
return {
|
|
672
|
+
code,
|
|
673
|
+
title: "Missing ElastiCache list permission",
|
|
674
|
+
steps: [
|
|
675
|
+
"Grant permission to list ElastiCache clusters.",
|
|
676
|
+
"Required: elasticache:DescribeCacheClusters.",
|
|
677
|
+
"Retry after permission update."
|
|
678
|
+
],
|
|
679
|
+
confirmText: "After ElastiCache permission update, reply 'completed' and retry."
|
|
680
|
+
};
|
|
681
|
+
case "ROUTE53_LIST_PERMISSION_REQUIRED":
|
|
682
|
+
return {
|
|
683
|
+
code,
|
|
684
|
+
title: "Missing Route53 list permission",
|
|
685
|
+
steps: [
|
|
686
|
+
"Grant permission to list Route53 hosted zones.",
|
|
687
|
+
"Required: route53:ListHostedZones (and route53:ListResourceRecordSets for record counts).",
|
|
688
|
+
"Retry after permission update."
|
|
689
|
+
],
|
|
690
|
+
confirmText: "After Route53 permission update, reply 'completed' and retry."
|
|
691
|
+
};
|
|
692
|
+
case "MANUAL_SERVER_LIST_EMPTY":
|
|
693
|
+
return {
|
|
694
|
+
code,
|
|
695
|
+
title: "Manual server list is empty",
|
|
696
|
+
steps: [
|
|
697
|
+
"Provide at least one server row in JSON or CSV format.",
|
|
698
|
+
"Minimum fields: host/publicIp/privateIp, optional pemPath/keyName.",
|
|
699
|
+
"Retry after updating the file."
|
|
700
|
+
],
|
|
701
|
+
confirmText: "After updating the server list file, reply 'completed' and retry."
|
|
702
|
+
};
|
|
703
|
+
case "MANUAL_SERVER_HOST_REQUIRED":
|
|
704
|
+
return {
|
|
705
|
+
code,
|
|
706
|
+
title: "Server host is required",
|
|
707
|
+
steps: [
|
|
708
|
+
"At least one of host/publicIp/privateIp/publicDns is required per server row.",
|
|
709
|
+
"Update the manual server list row(s) that are missing host info.",
|
|
710
|
+
"Retry with the same manual list path."
|
|
711
|
+
],
|
|
712
|
+
confirmText: "After fixing host fields, reply 'completed' and retry."
|
|
713
|
+
};
|
|
714
|
+
case "PEM_KEY_NOT_FOUND":
|
|
715
|
+
return {
|
|
716
|
+
code,
|
|
717
|
+
title: "PEM key file not found",
|
|
718
|
+
steps: [
|
|
719
|
+
"Check pemPath in server rows or --pem-paths input.",
|
|
720
|
+
"Use absolute paths or valid relative paths from the working directory.",
|
|
721
|
+
"Retry after fixing key paths."
|
|
722
|
+
],
|
|
723
|
+
confirmText: "After fixing PEM paths, reply 'completed' and retry."
|
|
724
|
+
};
|
|
725
|
+
case "PEM_MAPPING_REQUIRED":
|
|
726
|
+
return {
|
|
727
|
+
code,
|
|
728
|
+
title: "PEM mapping required",
|
|
729
|
+
steps: [
|
|
730
|
+
"Set pemPath per server row, or pass --pem-paths.",
|
|
731
|
+
"If multiple PEM files are used, include keyName per server for deterministic mapping.",
|
|
732
|
+
"Retry after mapping PEM keys."
|
|
733
|
+
],
|
|
734
|
+
confirmText: "After PEM mapping is configured, reply 'completed' and retry."
|
|
735
|
+
};
|
|
736
|
+
case "SSH_CLIENT_NOT_FOUND":
|
|
737
|
+
return {
|
|
738
|
+
code,
|
|
739
|
+
title: "SSH client missing",
|
|
740
|
+
steps: [
|
|
741
|
+
"Install OpenSSH client on this machine.",
|
|
742
|
+
"Verify with: ssh -V",
|
|
743
|
+
"Retry after SSH is available."
|
|
744
|
+
],
|
|
745
|
+
confirmText: "After OpenSSH is installed, reply 'completed' and retry."
|
|
746
|
+
};
|
|
747
|
+
case "SSH_AUTH_OR_CONNECT_FAILED":
|
|
748
|
+
return {
|
|
749
|
+
code,
|
|
750
|
+
title: "SSH authentication or connectivity failed",
|
|
751
|
+
steps: [
|
|
752
|
+
"Verify security group/NACL, host reachability, and target SSH port.",
|
|
753
|
+
"Check SSH user and PEM key permissions.",
|
|
754
|
+
"Retry after connectivity/auth fix."
|
|
755
|
+
],
|
|
756
|
+
confirmText: "After fixing SSH access, reply 'completed' and retry."
|
|
757
|
+
};
|
|
758
|
+
default:
|
|
759
|
+
return defaultItem;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function buildAgentGuidance(requiredActions, toolName, args) {
|
|
764
|
+
const items = Array.isArray(requiredActions)
|
|
765
|
+
? requiredActions.map((action) => guidanceForAction(action, args))
|
|
766
|
+
: [];
|
|
767
|
+
|
|
768
|
+
if (!items.length) {
|
|
769
|
+
return {
|
|
770
|
+
mode: "none",
|
|
771
|
+
autoRetryRecommended: false,
|
|
772
|
+
retryTool: toolName,
|
|
773
|
+
retryArgs: args,
|
|
774
|
+
userChecklist: [],
|
|
775
|
+
assistantMessageTemplate: "?怨밴묶 鈺곌퀬?뜹첎? ?袁⑥┷??뤿???щ빍??"
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const firstItem = items[0];
|
|
780
|
+
const lines = [];
|
|
781
|
+
lines.push("AWS ?怨밴묶 鈺곌퀬?띄몴??④쑴???롮젻筌??袁⑥삋 鈺곌퀣?귛첎? ?袁⑹뒄??몃빍??");
|
|
782
|
+
lines.push(`1. [${firstItem.code}] ${firstItem.title}`);
|
|
783
|
+
for (let i = 0; i < firstItem.steps.length; i += 1) {
|
|
784
|
+
lines.push(`${i + 1}. ${firstItem.steps[i]}`);
|
|
785
|
+
}
|
|
786
|
+
lines.push(firstItem.confirmText);
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
mode: "human_in_the_loop",
|
|
790
|
+
autoRetryRecommended: true,
|
|
791
|
+
retryTool: toolName,
|
|
792
|
+
retryArgs: args,
|
|
793
|
+
completionTrigger: "User replies that the required action is completed.",
|
|
794
|
+
userChecklist: items,
|
|
795
|
+
assistantMessageTemplate: lines.join("\n")
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function buildToolTextResponse(payload) {
|
|
800
|
+
return truncateText(JSON.stringify(payload, null, 2), DEFAULT_JSON_TEXT_LIMIT);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function toolSchema() {
|
|
804
|
+
return {
|
|
805
|
+
profiles: z.array(z.string().min(1)).optional().describe("Optional AWS profiles."),
|
|
806
|
+
regions: z.array(z.string().min(1)).optional().describe("Optional AWS regions."),
|
|
807
|
+
instanceIds: z.array(z.string().min(1)).optional().describe("Optional EC2 instance ids."),
|
|
808
|
+
includeLambda: z.boolean().optional().describe("If true, include Lambda inventory."),
|
|
809
|
+
includeEc2: z.boolean().optional().describe("If false, skip EC2 inventory."),
|
|
810
|
+
includeAlb: z.boolean().optional().describe("If true, include ALB/NLB and target group inventory."),
|
|
811
|
+
includeAsg: z.boolean().optional().describe("If true, include Auto Scaling Group inventory."),
|
|
812
|
+
includeRds: z.boolean().optional().describe("If true, include RDS DB instance inventory."),
|
|
813
|
+
includeElastiCache: z.boolean().optional().describe("If true, include ElastiCache cluster inventory."),
|
|
814
|
+
includeRoute53: z.boolean().optional().describe("If true, include Route53 hosted zone inventory."),
|
|
815
|
+
publicOnly: z.boolean().optional().describe("If true, include only public IPv4 instances."),
|
|
816
|
+
managedOnly: z.boolean().optional().describe("If true, include only SSM-managed instances."),
|
|
817
|
+
autoRemediateSsm: z.boolean().optional().describe("If true, try attaching/replacing instance profile for unmanaged instances."),
|
|
818
|
+
ssmInstanceProfileName: z.string().min(1).optional().describe("Instance profile name used for remediation."),
|
|
819
|
+
ssmInstanceProfileArn: z.string().min(1).optional().describe("Instance profile ARN used for remediation."),
|
|
820
|
+
allowReplaceProfile: z.boolean().optional().describe("Allow replacement when instance already has a profile."),
|
|
821
|
+
remediationWaitSec: z.number().positive().optional().describe("Seconds to wait before post-remediation SSM recheck."),
|
|
822
|
+
runtimeSnapshot: z.boolean().optional().describe("Enable/disable runtime snapshot collection."),
|
|
823
|
+
snapshotTimeoutSec: z.number().positive().optional().describe("SSM command timeout in seconds."),
|
|
824
|
+
snapshotConcurrency: z.number().int().positive().optional().describe("Parallel workers for runtime snapshots."),
|
|
825
|
+
snapshotMaxKb: z.number().int().positive().optional().describe("Max runtime snapshot output size per instance in KB."),
|
|
826
|
+
manualServerListPath: z.string().min(1).optional().describe("JSON/CSV manual server list path (fallback when AWS auth is unavailable)."),
|
|
827
|
+
pemPaths: z.array(z.string().min(1)).optional().describe("Optional PEM key paths for SSH runtime snapshot in manual mode."),
|
|
828
|
+
sshUser: z.string().min(1).optional().describe("Default SSH username for manual mode (default: ec2-user)."),
|
|
829
|
+
sshPort: z.number().int().positive().optional().describe("Default SSH port for manual mode (default: 22)."),
|
|
830
|
+
sshConnectTimeoutSec: z.number().positive().optional().describe("SSH connect timeout seconds (default: 8)."),
|
|
831
|
+
htmlOutPath: z.string().min(1).optional().describe("Optional HTML GUI report output path."),
|
|
832
|
+
openHtml: z.boolean().optional().describe("If true, attempt to open the generated HTML report."),
|
|
833
|
+
autoSsoLogin: z.boolean().optional().describe("Enable/disable automatic aws sso login retry."),
|
|
834
|
+
noProgress: z.boolean().optional().describe("Suppress CLI progress logs."),
|
|
835
|
+
timeoutSec: z.number().positive().max(3600).optional().describe("Wrapper timeout for CLI process."),
|
|
836
|
+
workingDirectory: z.string().min(1).optional().describe("Optional working directory for subprocess."),
|
|
837
|
+
maxRecords: z.number().int().positive().max(10000).optional().describe("Optional max records in MCP response."),
|
|
838
|
+
includeStderr: z.boolean().optional().describe("Include stderr logs in response.")
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function registerDiscoverTool(server, name, title, description) {
|
|
843
|
+
server.registerTool(
|
|
844
|
+
name,
|
|
845
|
+
{
|
|
846
|
+
title,
|
|
847
|
+
description,
|
|
848
|
+
annotations: {
|
|
849
|
+
readOnlyHint: true,
|
|
850
|
+
destructiveHint: false,
|
|
851
|
+
idempotentHint: true,
|
|
852
|
+
openWorldHint: true
|
|
853
|
+
},
|
|
854
|
+
inputSchema: toolSchema()
|
|
855
|
+
},
|
|
856
|
+
async (args) => {
|
|
857
|
+
const startedAt = new Date().toISOString();
|
|
858
|
+
const cliResult = await runDiscoverCli(args);
|
|
859
|
+
const logInfo = parseStderr(cliResult.stderr);
|
|
860
|
+
|
|
861
|
+
if (cliResult.spawnError) {
|
|
862
|
+
return {
|
|
863
|
+
content: [{ type: "text", text: `Failed to start CLI subprocess: ${cliResult.spawnError}` }],
|
|
864
|
+
isError: true
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (cliResult.timedOut) {
|
|
869
|
+
return {
|
|
870
|
+
content: [
|
|
871
|
+
{
|
|
872
|
+
type: "text",
|
|
873
|
+
text: buildToolTextResponse({
|
|
874
|
+
ok: false,
|
|
875
|
+
error: "CLI process timed out",
|
|
876
|
+
startedAt,
|
|
877
|
+
timeoutSec: typeof args.timeoutSec === "number" ? args.timeoutSec : DEFAULT_TIMEOUT_SEC,
|
|
878
|
+
exitCode: cliResult.exitCode,
|
|
879
|
+
signal: cliResult.signal,
|
|
880
|
+
stderr: truncateText(cliResult.stderr)
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
],
|
|
884
|
+
isError: true
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const parsed = tryParseJsonArray(cliResult.stdout);
|
|
889
|
+
if (!parsed.ok) {
|
|
890
|
+
return {
|
|
891
|
+
content: [
|
|
892
|
+
{
|
|
893
|
+
type: "text",
|
|
894
|
+
text: buildToolTextResponse({
|
|
895
|
+
ok: false,
|
|
896
|
+
error: "Failed to parse CLI JSON output",
|
|
897
|
+
parseError: parsed.error,
|
|
898
|
+
exitCode: cliResult.exitCode,
|
|
899
|
+
signal: cliResult.signal,
|
|
900
|
+
stderr: truncateText(cliResult.stderr),
|
|
901
|
+
stdout: truncateText(cliResult.stdout)
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
],
|
|
905
|
+
isError: true
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const allRecords = parsed.value;
|
|
910
|
+
const maxRecords =
|
|
911
|
+
typeof args.maxRecords === "number" && Number.isFinite(args.maxRecords)
|
|
912
|
+
? args.maxRecords
|
|
913
|
+
: null;
|
|
914
|
+
const records = maxRecords && allRecords.length > maxRecords ? allRecords.slice(0, maxRecords) : allRecords;
|
|
915
|
+
|
|
916
|
+
const acceptedExitCodes = new Set([0, 2, 3]);
|
|
917
|
+
const requiresUserAction = logInfo.requiredActions.length > 0 || cliResult.exitCode === 3;
|
|
918
|
+
const guidance = buildAgentGuidance(logInfo.requiredActions, name, args);
|
|
919
|
+
|
|
920
|
+
const response = {
|
|
921
|
+
ok: acceptedExitCodes.has(cliResult.exitCode),
|
|
922
|
+
startedAt,
|
|
923
|
+
finishedAt: new Date().toISOString(),
|
|
924
|
+
exitCode: cliResult.exitCode,
|
|
925
|
+
signal: cliResult.signal,
|
|
926
|
+
requiresUserAction,
|
|
927
|
+
requiredActions: logInfo.requiredActions,
|
|
928
|
+
guidance,
|
|
929
|
+
summary: {
|
|
930
|
+
...summarizeRecords(allRecords),
|
|
931
|
+
returnedRecords: records.length,
|
|
932
|
+
truncated: records.length !== allRecords.length,
|
|
933
|
+
warningCount: logInfo.warnings.length,
|
|
934
|
+
summaryLine: logInfo.summaryLine
|
|
935
|
+
},
|
|
936
|
+
records
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
if (args.includeStderr) {
|
|
940
|
+
response.stderr = truncateText(cliResult.stderr);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!acceptedExitCodes.has(cliResult.exitCode)) {
|
|
944
|
+
return {
|
|
945
|
+
content: [{ type: "text", text: buildToolTextResponse({ ...response, ok: false, stderr: truncateText(cliResult.stderr) }) }],
|
|
946
|
+
isError: true
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
content: [{ type: "text", text: buildToolTextResponse(response) }],
|
|
952
|
+
isError: false
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
);
|
|
502
956
|
}
|
|
503
957
|
|
|
504
|
-
function
|
|
958
|
+
function ec2ActionSchema(extra = {}) {
|
|
505
959
|
return {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
instanceIds: z.array(z.string().min(1)).
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
includeAlb: z.boolean().optional().describe("If true, include ALB/NLB and target group inventory."),
|
|
512
|
-
includeAsg: z.boolean().optional().describe("If true, include Auto Scaling Group inventory."),
|
|
513
|
-
includeRds: z.boolean().optional().describe("If true, include RDS DB instance inventory."),
|
|
514
|
-
includeElastiCache: z.boolean().optional().describe("If true, include ElastiCache cluster inventory."),
|
|
515
|
-
includeRoute53: z.boolean().optional().describe("If true, include Route53 hosted zone inventory."),
|
|
516
|
-
publicOnly: z.boolean().optional().describe("If true, include only public IPv4 instances."),
|
|
517
|
-
managedOnly: z.boolean().optional().describe("If true, include only SSM-managed instances."),
|
|
518
|
-
autoRemediateSsm: z.boolean().optional().describe("If true, try attaching/replacing instance profile for unmanaged instances."),
|
|
519
|
-
ssmInstanceProfileName: z.string().min(1).optional().describe("Instance profile name used for remediation."),
|
|
520
|
-
ssmInstanceProfileArn: z.string().min(1).optional().describe("Instance profile ARN used for remediation."),
|
|
521
|
-
allowReplaceProfile: z.boolean().optional().describe("Allow replacement when instance already has a profile."),
|
|
522
|
-
remediationWaitSec: z.number().positive().optional().describe("Seconds to wait before post-remediation SSM recheck."),
|
|
523
|
-
runtimeSnapshot: z.boolean().optional().describe("Enable/disable runtime snapshot collection."),
|
|
524
|
-
snapshotTimeoutSec: z.number().positive().optional().describe("SSM command timeout in seconds."),
|
|
525
|
-
snapshotConcurrency: z.number().int().positive().optional().describe("Parallel workers for runtime snapshots."),
|
|
526
|
-
snapshotMaxKb: z.number().int().positive().optional().describe("Max runtime snapshot output size per instance in KB."),
|
|
527
|
-
autoSsoLogin: z.boolean().optional().describe("Enable/disable automatic aws sso login retry."),
|
|
528
|
-
noProgress: z.boolean().optional().describe("Suppress CLI progress logs."),
|
|
529
|
-
timeoutSec: z.number().positive().max(3600).optional().describe("Wrapper timeout for CLI process."),
|
|
530
|
-
workingDirectory: z.string().min(1).optional().describe("Optional working directory for subprocess."),
|
|
531
|
-
maxRecords: z.number().int().positive().max(10000).optional().describe("Optional max records in MCP response."),
|
|
532
|
-
includeStderr: z.boolean().optional().describe("Include stderr logs in response.")
|
|
960
|
+
profile: z.string().min(1).optional().describe("AWS profile name (default: default)."),
|
|
961
|
+
region: z.string().min(1).describe("AWS region, e.g. ap-southeast-1."),
|
|
962
|
+
instanceIds: z.array(z.string().min(1)).min(1).describe("Target EC2 instance ids."),
|
|
963
|
+
dryRun: z.boolean().optional().describe("If true, AWS DryRun is used where supported."),
|
|
964
|
+
...extra
|
|
533
965
|
};
|
|
534
966
|
}
|
|
535
967
|
|
|
536
|
-
function
|
|
968
|
+
function registerEc2MutationTool(server, name, title, description, action, schema = ec2ActionSchema()) {
|
|
537
969
|
server.registerTool(
|
|
538
970
|
name,
|
|
539
971
|
{
|
|
540
972
|
title,
|
|
541
973
|
description,
|
|
542
974
|
annotations: {
|
|
543
|
-
readOnlyHint:
|
|
544
|
-
destructiveHint:
|
|
545
|
-
idempotentHint:
|
|
975
|
+
readOnlyHint: false,
|
|
976
|
+
destructiveHint: true,
|
|
977
|
+
idempotentHint: false,
|
|
546
978
|
openWorldHint: true
|
|
547
979
|
},
|
|
548
|
-
inputSchema:
|
|
980
|
+
inputSchema: schema
|
|
549
981
|
},
|
|
550
982
|
async (args) => {
|
|
551
983
|
const startedAt = new Date().toISOString();
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
984
|
+
const result = await runEc2InstanceAction(args, action);
|
|
985
|
+
if (!result.ok) {
|
|
986
|
+
const payload = {
|
|
987
|
+
ok: false,
|
|
988
|
+
action,
|
|
989
|
+
startedAt,
|
|
990
|
+
finishedAt: new Date().toISOString(),
|
|
991
|
+
error: result.error,
|
|
992
|
+
requiredActions: result.requiredAction ? [result.requiredAction] : []
|
|
559
993
|
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (cliResult.timedOut) {
|
|
563
|
-
return {
|
|
564
|
-
content: [
|
|
565
|
-
{
|
|
566
|
-
type: "text",
|
|
567
|
-
text: buildToolTextResponse({
|
|
568
|
-
ok: false,
|
|
569
|
-
error: "CLI process timed out",
|
|
570
|
-
startedAt,
|
|
571
|
-
timeoutSec: typeof args.timeoutSec === "number" ? args.timeoutSec : DEFAULT_TIMEOUT_SEC,
|
|
572
|
-
exitCode: cliResult.exitCode,
|
|
573
|
-
signal: cliResult.signal,
|
|
574
|
-
stderr: truncateText(cliResult.stderr)
|
|
575
|
-
})
|
|
576
|
-
}
|
|
577
|
-
],
|
|
578
|
-
isError: true
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const parsed = tryParseJsonArray(cliResult.stdout);
|
|
583
|
-
if (!parsed.ok) {
|
|
584
994
|
return {
|
|
585
|
-
content: [
|
|
586
|
-
{
|
|
587
|
-
type: "text",
|
|
588
|
-
text: buildToolTextResponse({
|
|
589
|
-
ok: false,
|
|
590
|
-
error: "Failed to parse CLI JSON output",
|
|
591
|
-
parseError: parsed.error,
|
|
592
|
-
exitCode: cliResult.exitCode,
|
|
593
|
-
signal: cliResult.signal,
|
|
594
|
-
stderr: truncateText(cliResult.stderr),
|
|
595
|
-
stdout: truncateText(cliResult.stdout)
|
|
596
|
-
})
|
|
597
|
-
}
|
|
598
|
-
],
|
|
995
|
+
content: [{ type: "text", text: buildToolTextResponse(payload) }],
|
|
599
996
|
isError: true
|
|
600
997
|
};
|
|
601
998
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const response = {
|
|
615
|
-
ok: acceptedExitCodes.has(cliResult.exitCode),
|
|
616
|
-
startedAt,
|
|
617
|
-
finishedAt: new Date().toISOString(),
|
|
618
|
-
exitCode: cliResult.exitCode,
|
|
619
|
-
signal: cliResult.signal,
|
|
620
|
-
requiresUserAction,
|
|
621
|
-
requiredActions: logInfo.requiredActions,
|
|
622
|
-
guidance,
|
|
623
|
-
summary: {
|
|
624
|
-
...summarizeRecords(allRecords),
|
|
625
|
-
returnedRecords: records.length,
|
|
626
|
-
truncated: records.length !== allRecords.length,
|
|
627
|
-
warningCount: logInfo.warnings.length,
|
|
628
|
-
summaryLine: logInfo.summaryLine
|
|
629
|
-
},
|
|
630
|
-
records
|
|
999
|
+
return {
|
|
1000
|
+
content: [{
|
|
1001
|
+
type: "text",
|
|
1002
|
+
text: buildToolTextResponse({
|
|
1003
|
+
ok: true,
|
|
1004
|
+
action,
|
|
1005
|
+
startedAt,
|
|
1006
|
+
finishedAt: new Date().toISOString(),
|
|
1007
|
+
...result
|
|
1008
|
+
})
|
|
1009
|
+
}],
|
|
1010
|
+
isError: false
|
|
631
1011
|
};
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
632
1015
|
|
|
633
|
-
|
|
634
|
-
|
|
1016
|
+
function registerEc2ProfileTool(server) {
|
|
1017
|
+
server.registerTool(
|
|
1018
|
+
"ec2_apply_instance_profile",
|
|
1019
|
+
{
|
|
1020
|
+
title: "Apply EC2 IAM Instance Profile",
|
|
1021
|
+
description: "Associates or replaces IAM instance profile on one EC2 instance.",
|
|
1022
|
+
annotations: {
|
|
1023
|
+
readOnlyHint: false,
|
|
1024
|
+
destructiveHint: true,
|
|
1025
|
+
idempotentHint: false,
|
|
1026
|
+
openWorldHint: true
|
|
1027
|
+
},
|
|
1028
|
+
inputSchema: {
|
|
1029
|
+
profile: z.string().min(1).optional().describe("AWS profile name (default: default)."),
|
|
1030
|
+
region: z.string().min(1).describe("AWS region, e.g. ap-southeast-1."),
|
|
1031
|
+
instanceId: z.string().min(1).describe("Target EC2 instance id."),
|
|
1032
|
+
instanceProfileName: z.string().min(1).optional().describe("Target instance profile name."),
|
|
1033
|
+
instanceProfileArn: z.string().min(1).optional().describe("Target instance profile ARN."),
|
|
1034
|
+
allowReplaceProfile: z.boolean().optional().describe("If true, existing association can be replaced.")
|
|
635
1035
|
}
|
|
636
|
-
|
|
637
|
-
|
|
1036
|
+
},
|
|
1037
|
+
async (args) => {
|
|
1038
|
+
const startedAt = new Date().toISOString();
|
|
1039
|
+
const result = await applyInstanceProfile(args);
|
|
1040
|
+
if (!result.ok) {
|
|
1041
|
+
const payload = {
|
|
1042
|
+
ok: false,
|
|
1043
|
+
action: "ec2_apply_instance_profile",
|
|
1044
|
+
startedAt,
|
|
1045
|
+
finishedAt: new Date().toISOString(),
|
|
1046
|
+
error: result.error,
|
|
1047
|
+
requiredActions: result.requiredAction ? [result.requiredAction] : []
|
|
1048
|
+
};
|
|
638
1049
|
return {
|
|
639
|
-
content: [{ type: "text", text: buildToolTextResponse(
|
|
1050
|
+
content: [{ type: "text", text: buildToolTextResponse(payload) }],
|
|
640
1051
|
isError: true
|
|
641
1052
|
};
|
|
642
1053
|
}
|
|
643
|
-
|
|
644
1054
|
return {
|
|
645
|
-
content: [{
|
|
1055
|
+
content: [{
|
|
1056
|
+
type: "text",
|
|
1057
|
+
text: buildToolTextResponse({
|
|
1058
|
+
ok: true,
|
|
1059
|
+
action: "ec2_apply_instance_profile",
|
|
1060
|
+
startedAt,
|
|
1061
|
+
finishedAt: new Date().toISOString(),
|
|
1062
|
+
...result
|
|
1063
|
+
})
|
|
1064
|
+
}],
|
|
646
1065
|
isError: false
|
|
647
1066
|
};
|
|
648
1067
|
}
|
|
649
1068
|
);
|
|
650
|
-
}
|
|
1069
|
+
}
|
|
651
1070
|
|
|
652
1071
|
async function registerTools(server) {
|
|
653
1072
|
registerDiscoverTool(
|
|
654
1073
|
server,
|
|
655
1074
|
"discover_ec2_with_ssm",
|
|
656
|
-
"Discover AWS Inventory (multi-service + SSM runtime)",
|
|
657
|
-
"Runs mcp-aws-manager and returns inventory across EC2/Lambda/ALB/ASG/RDS/ElastiCache/Route53 with optional SSM
|
|
1075
|
+
"Discover AWS Inventory (multi-service + SSM runtime)",
|
|
1076
|
+
"Runs mcp-aws-manager and returns inventory across EC2/Lambda/ALB/ASG/RDS/ElastiCache/Route53 with optional runtime snapshots (SSM or manual-server-list + PEM SSH)."
|
|
658
1077
|
);
|
|
659
1078
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
readOnlyHint: true,
|
|
667
|
-
destructiveHint: false,
|
|
668
|
-
idempotentHint: true,
|
|
669
|
-
openWorldHint: false
|
|
670
|
-
}
|
|
671
|
-
},
|
|
672
|
-
async () => {
|
|
673
|
-
const child = await new Promise((resolve) => {
|
|
674
|
-
const proc = spawn(process.execPath, [CLI_SCRIPT_PATH, "--help"], {
|
|
675
|
-
cwd: process.cwd(),
|
|
676
|
-
env: process.env,
|
|
677
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
let stdout = "";
|
|
681
|
-
let stderr = "";
|
|
682
|
-
if (proc.stdout) {
|
|
683
|
-
proc.stdout.setEncoding("utf8");
|
|
684
|
-
proc.stdout.on("data", (c) => { stdout += c; });
|
|
685
|
-
}
|
|
686
|
-
if (proc.stderr) {
|
|
687
|
-
proc.stderr.setEncoding("utf8");
|
|
688
|
-
proc.stderr.on("data", (c) => { stderr += c; });
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
proc.on("close", (code, signal) => {
|
|
692
|
-
resolve({ code: code == null ? null : code, signal, stdout, stderr });
|
|
693
|
-
});
|
|
694
|
-
proc.on("error", (error) => {
|
|
695
|
-
resolve({ code: null, signal: null, stdout, stderr: String(error && error.message ? error.message : error) });
|
|
696
|
-
});
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
const payload = {
|
|
700
|
-
ok: child.code === 0,
|
|
701
|
-
exitCode: child.code,
|
|
702
|
-
signal: child.signal || null,
|
|
703
|
-
stdout: truncateText(child.stdout, DEFAULT_JSON_TEXT_LIMIT),
|
|
704
|
-
stderr: truncateText(child.stderr)
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
return {
|
|
708
|
-
content: [{ type: "text", text: buildToolTextResponse(payload) }],
|
|
709
|
-
isError: child.code !== 0
|
|
710
|
-
};
|
|
711
|
-
}
|
|
1079
|
+
registerEc2MutationTool(
|
|
1080
|
+
server,
|
|
1081
|
+
"ec2_start_instances",
|
|
1082
|
+
"Start EC2 Instances",
|
|
1083
|
+
"Starts EC2 instances in a region.",
|
|
1084
|
+
"start"
|
|
712
1085
|
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
async function main() {
|
|
716
|
-
if (shouldShowHelp(process.argv.slice(2))) {
|
|
717
|
-
process.stdout.write(usageText());
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
1086
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1087
|
+
registerEc2MutationTool(
|
|
1088
|
+
server,
|
|
1089
|
+
"ec2_stop_instances",
|
|
1090
|
+
"Stop EC2 Instances",
|
|
1091
|
+
"Stops EC2 instances in a region (supports force).",
|
|
1092
|
+
"stop",
|
|
1093
|
+
ec2ActionSchema({
|
|
1094
|
+
force: z.boolean().optional().describe("If true, force stop is requested.")
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
725
1097
|
|
|
726
|
-
|
|
1098
|
+
registerEc2MutationTool(
|
|
1099
|
+
server,
|
|
1100
|
+
"ec2_reboot_instances",
|
|
1101
|
+
"Reboot EC2 Instances",
|
|
1102
|
+
"Reboots EC2 instances in a region.",
|
|
1103
|
+
"reboot"
|
|
1104
|
+
);
|
|
727
1105
|
|
|
728
|
-
|
|
729
|
-
await server.connect(transport);
|
|
730
|
-
}
|
|
1106
|
+
registerEc2ProfileTool(server);
|
|
731
1107
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1108
|
+
server.registerTool(
|
|
1109
|
+
"mcp_aws_discover_cli_help",
|
|
1110
|
+
{
|
|
1111
|
+
title: "Show CLI Help",
|
|
1112
|
+
description: "Returns the local mcp-aws-manager CLI usage text.",
|
|
1113
|
+
annotations: {
|
|
1114
|
+
readOnlyHint: true,
|
|
1115
|
+
destructiveHint: false,
|
|
1116
|
+
idempotentHint: true,
|
|
1117
|
+
openWorldHint: false
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
async () => {
|
|
1121
|
+
const child = await new Promise((resolve) => {
|
|
1122
|
+
const proc = spawn(process.execPath, [CLI_SCRIPT_PATH, "--help"], {
|
|
1123
|
+
cwd: process.cwd(),
|
|
1124
|
+
env: process.env,
|
|
1125
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
let stdout = "";
|
|
1129
|
+
let stderr = "";
|
|
1130
|
+
if (proc.stdout) {
|
|
1131
|
+
proc.stdout.setEncoding("utf8");
|
|
1132
|
+
proc.stdout.on("data", (c) => { stdout += c; });
|
|
1133
|
+
}
|
|
1134
|
+
if (proc.stderr) {
|
|
1135
|
+
proc.stderr.setEncoding("utf8");
|
|
1136
|
+
proc.stderr.on("data", (c) => { stderr += c; });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
proc.on("close", (code, signal) => {
|
|
1140
|
+
resolve({ code: code == null ? null : code, signal, stdout, stderr });
|
|
1141
|
+
});
|
|
1142
|
+
proc.on("error", (error) => {
|
|
1143
|
+
resolve({ code: null, signal: null, stdout, stderr: String(error && error.message ? error.message : error) });
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
const payload = {
|
|
1148
|
+
ok: child.code === 0,
|
|
1149
|
+
exitCode: child.code,
|
|
1150
|
+
signal: child.signal || null,
|
|
1151
|
+
stdout: truncateText(child.stdout, DEFAULT_JSON_TEXT_LIMIT),
|
|
1152
|
+
stderr: truncateText(child.stderr)
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
return {
|
|
1156
|
+
content: [{ type: "text", text: buildToolTextResponse(payload) }],
|
|
1157
|
+
isError: child.code !== 0
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async function main() {
|
|
1164
|
+
if (shouldShowHelp(process.argv.slice(2))) {
|
|
1165
|
+
process.stdout.write(usageText());
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const server = new McpServer({
|
|
1170
|
+
name: "mcp-aws-manager",
|
|
1171
|
+
version: pkg.version
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
await registerTools(server);
|
|
1175
|
+
|
|
1176
|
+
const transport = new StdioServerTransport();
|
|
1177
|
+
await server.connect(transport);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
main().catch((error) => {
|
|
1181
|
+
const message = error && error.stack ? error.stack : String(error);
|
|
1182
|
+
process.stderr.write(`mcp-aws-manager-mcp startup error: ${message}\n`);
|
|
1183
|
+
process.exitCode = 1;
|
|
736
1184
|
});
|