mcp-aws-manager 0.2.0 → 0.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.
@@ -0,0 +1,52 @@
1
+ # Agent Guidance Loop Template (KO)
2
+
3
+ 아래 규칙은 `discover_ec2_with_ssm` 또는 호환 alias 호출 결과를 바탕으로 사용자 개입을 최소화하기 위한 시스템 프롬프트 템플릿입니다.
4
+
5
+ ## 목적
6
+
7
+ - 가능한 범위는 자동으로 끝까지 처리한다.
8
+ - 수동 개입이 필요한 순간에만 사용자를 안내한다.
9
+ - 사용자가 조치를 완료하면 같은 입력으로 자동 재시도한다.
10
+
11
+ ## 입력 가정
12
+
13
+ MCP 응답 JSON에는 아래 필드가 포함된다.
14
+
15
+ - `requiresUserAction: boolean`
16
+ - `requiredActions: [{ code, message, hint }]`
17
+ - `guidance: { assistantMessageTemplate, retryTool, retryArgs, userChecklist, completionTrigger }`
18
+
19
+ ## 실행 규칙
20
+
21
+ 1. 먼저 도구를 실행한다.
22
+ 2. `requiresUserAction=false`이면 결과를 요약하고 종료한다.
23
+ 3. `requiresUserAction=true`이면 `guidance.assistantMessageTemplate`을 사용자에게 그대로 전달한다.
24
+ 4. 사용자의 답변이 `완료` 또는 완료 의사표시이면, `guidance.retryTool` + `guidance.retryArgs`로 동일 요청을 즉시 재실행한다.
25
+ 5. 여전히 `requiresUserAction=true`이면 다음 액션을 다시 안내한다.
26
+ 6. 성공(`requiresUserAction=false`)할 때까지 반복한다.
27
+
28
+ ## 사용자 안내 스타일
29
+
30
+ - 한 번에 하나의 액션만 안내한다.
31
+ - 필요한 명령어는 복붙 가능한 한 줄로 제시한다.
32
+ - 사용자의 AWS 지식 수준을 가정하지 않는다.
33
+ - 매 단계 끝에 반드시 재시도 트리거 문구를 넣는다.
34
+
35
+ 예시 트리거 문구:
36
+
37
+ - `조치가 끝나면 "완료"라고 답해주세요. 제가 바로 같은 요청으로 다시 확인하겠습니다.`
38
+
39
+ ## 금지 사항
40
+
41
+ - 사용자가 요청하지 않은 파괴적 작업을 임의 실행하지 않는다.
42
+ - 여러 개의 복잡한 선택지를 한 번에 던지지 않는다.
43
+ - 내부 오류 로그를 장황하게 그대로 노출하지 않는다.
44
+
45
+ ## 최종 완료 응답
46
+
47
+ 완료 시 아래를 간단히 보고한다.
48
+
49
+ 1. 전체 인스턴스 수
50
+ 2. SSM 관리/온라인 수
51
+ 3. 주요 경고 유무
52
+ 4. 다음 선택 사항(예: 런타임 스냅샷 확장)
@@ -1,4 +1,4 @@
1
- # MCP Client Setup (stdio)
1
+ # MCP Client Setup (stdio)
2
2
 
3
3
  This project provides an MCP stdio wrapper around the SSM-only CLI.
4
4
 
@@ -12,7 +12,38 @@ Exposed MCP tools:
12
12
  - `discover_public_ec2_with_pem` (compatibility alias, same behavior)
13
13
  - `mcp_aws_discover_cli_help`
14
14
 
15
- ## 1) Local Repo (development)
15
+ ## Recommended (Install Once)
16
+
17
+ ```bash
18
+ npm install -g mcp-aws-manager
19
+ mcp-aws-manager
20
+ ```
21
+
22
+ `mcp-aws-manager` (no args) runs bootstrap and registers the MCP server for detected clients (`codex`, `claude`).
23
+
24
+ Verification:
25
+
26
+ ```bash
27
+ mcp-aws-manager doctor
28
+ ```
29
+
30
+ ## Explicit Registration
31
+
32
+ ```bash
33
+ mcp-aws-manager setup
34
+ ```
35
+
36
+ Custom name/command:
37
+
38
+ ```bash
39
+ mcp-aws-manager setup --name mcp-aws-manager --mcp-command mcp-aws-manager-mcp --clients codex,claude
40
+ ```
41
+
42
+ ## Manual Configuration (Fallback)
43
+
44
+ Use only when automatic registration is unavailable in your environment.
45
+
46
+ ### 1) Local Repo (development)
16
47
 
17
48
  ```json
18
49
  {
@@ -28,11 +59,7 @@ Exposed MCP tools:
28
59
  }
29
60
  ```
30
61
 
31
- ## 2) Global npm Install
32
-
33
- ```bash
34
- npm install -g mcp-aws-manager
35
- ```
62
+ ### 2) Global npm Install
36
63
 
37
64
  ```json
38
65
  {
@@ -44,7 +71,7 @@ npm install -g mcp-aws-manager
44
71
  }
45
72
  ```
46
73
 
47
- ## 3) npx (no global install)
74
+ ### 3) npx (no global install)
48
75
 
49
76
  ```json
50
77
  {
@@ -66,5 +93,5 @@ npm install -g mcp-aws-manager
66
93
 
67
94
  - Discovery is SSM-only; PEM path arguments are no longer required.
68
95
  - Keep AWS credentials/profiles available on the host running MCP.
69
- - When `requiresUserAction=true` is returned, surface `requiredActions` to the user and retry after intervention.
70
- - For auto remediation, pass `autoRemediateSsm` and an instance profile name/arn.
96
+ - When `requiresUserAction=true` is returned, use `guidance.assistantMessageTemplate` to prompt the user, then retry with `guidance.retryTool` + `guidance.retryArgs` after user confirmation.
97
+ - For auto remediation, pass `autoRemediateSsm` and an instance profile name/arn.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # mcp-aws-manager
1
+ # mcp-aws-manager
2
2
 
3
3
  AWS operations CLI and MCP server package (SSM-only mode).
4
4
 
@@ -15,6 +15,7 @@ Current implementation focuses on:
15
15
  - Optional SSM auto-remediation (instance profile association)
16
16
  - Human-in-the-loop guidance via `ACTION_REQUIRED` messages
17
17
  - JSON/CSV output (CLI)
18
+ - Codex/Claude MCP registration bootstrap helpers
18
19
 
19
20
  ## Install
20
21
 
@@ -22,6 +23,16 @@ Current implementation focuses on:
22
23
  npm install -g mcp-aws-manager
23
24
  ```
24
25
 
26
+ ## One-Time Bootstrap (Recommended)
27
+
28
+ After install, run once:
29
+
30
+ ```bash
31
+ mcp-aws-manager
32
+ ```
33
+
34
+ This ensures `mcp-aws-manager` is registered in detected clients (`codex`, `claude`).
35
+
25
36
  ## Prerequisites
26
37
 
27
38
  - Node.js `>=18`
@@ -31,28 +42,36 @@ npm install -g mcp-aws-manager
31
42
 
32
43
  ## Quick Start
33
44
 
45
+ Bootstrap / setup / doctor:
46
+
47
+ ```bash
48
+ mcp-aws-manager # bootstrap (default command)
49
+ mcp-aws-manager setup # register/re-register MCP server
50
+ mcp-aws-manager doctor # verify install + registration
51
+ ```
52
+
34
53
  Basic discovery:
35
54
 
36
55
  ```bash
37
- mcp-aws-manager --profiles default
56
+ mcp-aws-manager discover --profiles default
38
57
  ```
39
58
 
40
59
  Only public IP instances:
41
60
 
42
61
  ```bash
43
- mcp-aws-manager --profiles default --public-only
62
+ mcp-aws-manager discover --profiles default --public-only
44
63
  ```
45
64
 
46
65
  Collect runtime snapshots:
47
66
 
48
67
  ```bash
49
- mcp-aws-manager --profiles default --runtime-snapshot
68
+ mcp-aws-manager discover --profiles default --runtime-snapshot
50
69
  ```
51
70
 
52
71
  Try automatic remediation for unmanaged instances:
53
72
 
54
73
  ```bash
55
- mcp-aws-manager \
74
+ mcp-aws-manager discover \
56
75
  --profiles default \
57
76
  --auto-remediate-ssm \
58
77
  --ssm-instance-profile-name MySsmInstanceProfile
@@ -61,9 +80,14 @@ mcp-aws-manager \
61
80
  Output CSV file:
62
81
 
63
82
  ```bash
64
- mcp-aws-manager --profiles default --format csv --out ./inventory.csv
83
+ mcp-aws-manager discover --profiles default --format csv --out ./inventory.csv
65
84
  ```
66
85
 
86
+ Compatibility note:
87
+
88
+ - Legacy invocation without subcommand still works for discovery when options are passed.
89
+ - Example: `mcp-aws-manager --profiles default --public-only`
90
+
67
91
  ## MCP (LLM Tool) Usage
68
92
 
69
93
  Run as an MCP stdio server:
@@ -98,7 +122,11 @@ When fully automatic execution is not possible, the CLI/MCP returns actionable g
98
122
  - `ACTION_REQUIRED: [SSM_ROLE_OR_AGENT_REQUIRED] ...`
99
123
  - `ACTION_REQUIRED: [IAM_PROFILE_ASSOCIATION_FAILED] ...`
100
124
 
101
- The MCP wrapper surfaces these in a structured `requiredActions` list.
125
+ The MCP wrapper surfaces these in a structured `requiredActions` list and a `guidance` object (`assistantMessageTemplate`, `retryTool`, `retryArgs`).
126
+
127
+ For agent orchestration, see:
128
+
129
+ - `AGENT_GUIDANCE_LOOP_TEMPLATE_KO.md`
102
130
 
103
131
  ## Security Notes
104
132
 
@@ -111,4 +139,4 @@ The MCP wrapper surfaces these in a structured `requiredActions` list.
111
139
  These legacy commands are still available:
112
140
 
113
141
  - `mcp-aws-discover`
114
- - `mcp-aws-discover-mcp`
142
+ - `mcp-aws-discover-mcp`
@@ -248,6 +248,157 @@ function summarizeRecords(records) {
248
248
  return summary;
249
249
  }
250
250
 
251
+ function firstProfileArg(args) {
252
+ if (args && Array.isArray(args.profiles) && args.profiles.length > 0) {
253
+ return String(args.profiles[0]);
254
+ }
255
+ return "default";
256
+ }
257
+
258
+ function inferSsoLoginCommand(action, args) {
259
+ const hint = action && action.hint ? String(action.hint) : "";
260
+ const matched = /aws sso login --profile\s+[^\s'"]+/i.exec(hint);
261
+ if (matched) {
262
+ return matched[0];
263
+ }
264
+ return `aws sso login --profile ${firstProfileArg(args)}`;
265
+ }
266
+
267
+ function guidanceForAction(action, args) {
268
+ const code = action && action.code ? String(action.code) : "UNKNOWN";
269
+ const defaultItem = {
270
+ code,
271
+ title: "Manual action required",
272
+ steps: [
273
+ action && action.message ? action.message : "A manual action is required.",
274
+ action && action.hint ? action.hint : "After completing the action, reply '완료' to continue."
275
+ ],
276
+ confirmText: "조치가 완료되면 '완료'라고 답해주세요. 같은 요청으로 자동 재시도하겠습니다."
277
+ };
278
+
279
+ switch (code) {
280
+ case "SSO_LOGIN_NEEDED":
281
+ case "SSO_REAUTH_REQUIRED": {
282
+ const cmd = inferSsoLoginCommand(action, args);
283
+ return {
284
+ code,
285
+ title: "AWS SSO login required",
286
+ steps: [
287
+ `터미널에서 다음 명령을 실행하세요: ${cmd}`,
288
+ "브라우저 인증/MFA를 완료하세요.",
289
+ "완료 후 '완료'라고 답해주세요."
290
+ ],
291
+ confirmText: "SSO 로그인이 끝났다면 '완료'라고 답해주세요."
292
+ };
293
+ }
294
+ case "AWS_CREDENTIALS_REQUIRED":
295
+ return {
296
+ code,
297
+ title: "AWS credentials required",
298
+ steps: [
299
+ "사용할 프로필의 자격증명을 설정하세요 (SSO 또는 access key).",
300
+ "SSO라면 'aws configure sso --profile <profile>' 후 로그인하세요.",
301
+ "완료 후 '완료'라고 답해주세요."
302
+ ],
303
+ confirmText: "자격증명 설정/로그인이 끝났다면 '완료'라고 답해주세요."
304
+ };
305
+ case "SET_SSM_INSTANCE_PROFILE":
306
+ return {
307
+ code,
308
+ title: "SSM remediation target missing",
309
+ steps: [
310
+ "자동 복구를 사용하려면 instance profile 이름 또는 ARN을 지정해야 합니다.",
311
+ "다음 옵션 중 하나를 함께 전달하세요: --ssm-instance-profile-name 또는 --ssm-instance-profile-arn",
312
+ "완료 후 '완료'라고 답해주세요."
313
+ ],
314
+ confirmText: "프로파일 대상을 지정했다면 '완료'라고 답해주세요."
315
+ };
316
+ case "SSM_ROLE_OR_AGENT_REQUIRED":
317
+ return {
318
+ code,
319
+ title: "Instance is not SSM managed",
320
+ steps: [
321
+ "인스턴스 역할에 AmazonSSMManagedInstanceCore를 포함하세요.",
322
+ "SSM Agent와 네트워크(SSM endpoint/인터넷 경로)가 정상인지 확인하세요.",
323
+ "완료 후 '완료'라고 답해주세요."
324
+ ],
325
+ confirmText: "SSM 관리 상태를 조치했다면 '완료'라고 답해주세요."
326
+ };
327
+ case "INSTANCE_HAS_PROFILE":
328
+ return {
329
+ code,
330
+ title: "Existing instance profile detected",
331
+ steps: [
332
+ "기존 인스턴스 프로파일이 있습니다.",
333
+ "선택 1: 기존 역할 정책에 SSM 권한을 추가합니다.",
334
+ "선택 2: 자동 교체를 원하면 allowReplaceProfile=true 로 재시도합니다."
335
+ ],
336
+ confirmText: "적용할 방법을 정했다면 '완료'라고 답해주세요."
337
+ };
338
+ case "IAM_PROFILE_ASSOCIATION_FAILED":
339
+ case "IAM_PROFILE_REPLACE_FAILED":
340
+ return {
341
+ code,
342
+ title: "Missing IAM permission for remediation",
343
+ steps: [
344
+ "실행 주체에 EC2 인스턴스 프로파일 연결/교체 권한을 부여하세요.",
345
+ "필요 권한: ec2:AssociateIamInstanceProfile, ec2:ReplaceIamInstanceProfileAssociation(교체 시), iam:PassRole",
346
+ "완료 후 '완료'라고 답해주세요."
347
+ ],
348
+ confirmText: "IAM 권한 반영이 끝났다면 '완료'라고 답해주세요."
349
+ };
350
+ case "SSM_RUNCOMMAND_PERMISSION_REQUIRED":
351
+ return {
352
+ code,
353
+ title: "Missing SSM RunCommand permission",
354
+ steps: [
355
+ "실행 주체에 SSM 명령 권한을 부여하세요.",
356
+ "필요 권한: ssm:SendCommand, ssm:GetCommandInvocation",
357
+ "완료 후 '완료'라고 답해주세요."
358
+ ],
359
+ confirmText: "SSM 권한 반영이 끝났다면 '완료'라고 답해주세요."
360
+ };
361
+ default:
362
+ return defaultItem;
363
+ }
364
+ }
365
+
366
+ function buildAgentGuidance(requiredActions, toolName, args) {
367
+ const items = Array.isArray(requiredActions)
368
+ ? requiredActions.map((action) => guidanceForAction(action, args))
369
+ : [];
370
+
371
+ if (!items.length) {
372
+ return {
373
+ mode: "none",
374
+ autoRetryRecommended: false,
375
+ retryTool: toolName,
376
+ retryArgs: args,
377
+ userChecklist: [],
378
+ assistantMessageTemplate: "상태 조회가 완료되었습니다."
379
+ };
380
+ }
381
+
382
+ const firstItem = items[0];
383
+ const lines = [];
384
+ lines.push("AWS 상태 조회를 계속하려면 아래 조치가 필요합니다.");
385
+ lines.push(`1. [${firstItem.code}] ${firstItem.title}`);
386
+ for (let i = 0; i < firstItem.steps.length; i += 1) {
387
+ lines.push(`${i + 1}. ${firstItem.steps[i]}`);
388
+ }
389
+ lines.push(firstItem.confirmText);
390
+
391
+ return {
392
+ mode: "human_in_the_loop",
393
+ autoRetryRecommended: true,
394
+ retryTool: toolName,
395
+ retryArgs: args,
396
+ completionTrigger: "사용자가 '완료' 또는 조치 완료를 확인하면 같은 입력으로 도구를 재실행",
397
+ userChecklist: items,
398
+ assistantMessageTemplate: lines.join("\n")
399
+ };
400
+ }
401
+
251
402
  function buildToolTextResponse(payload) {
252
403
  return truncateText(JSON.stringify(payload, null, 2), DEFAULT_JSON_TEXT_LIMIT);
253
404
  }
@@ -353,6 +504,7 @@ function registerDiscoverTool(server, name, title, description) {
353
504
 
354
505
  const acceptedExitCodes = new Set([0, 2, 3]);
355
506
  const requiresUserAction = logInfo.requiredActions.length > 0 || cliResult.exitCode === 3;
507
+ const guidance = buildAgentGuidance(logInfo.requiredActions, name, args);
356
508
 
357
509
  const response = {
358
510
  ok: acceptedExitCodes.has(cliResult.exitCode),
@@ -362,6 +514,7 @@ function registerDiscoverTool(server, name, title, description) {
362
514
  signal: cliResult.signal,
363
515
  requiresUserAction,
364
516
  requiredActions: logInfo.requiredActions,
517
+ guidance,
365
518
  summary: {
366
519
  ...summarizeRecords(allRecords),
367
520
  returnedRecords: records.length,
@@ -4,11 +4,14 @@
4
4
  const fs = require("node:fs");
5
5
  const os = require("node:os");
6
6
  const path = require("node:path");
7
- const { spawn } = require("node:child_process");
7
+ const { spawn, spawnSync } = require("node:child_process");
8
8
 
9
9
  const TOTAL_STEPS = 9;
10
10
  const DEFAULT_SNAPSHOT_CONCURRENCY = 3;
11
11
  const MAX_SSM_FILTER_IDS = 50;
12
+ const DEFAULT_SERVER_NAME = "mcp-aws-manager";
13
+ const DEFAULT_MCP_COMMAND = "mcp-aws-manager-mcp";
14
+ const SUPPORTED_CLIENTS = new Set(["codex", "claude"]);
12
15
 
13
16
  function eprint(msg) {
14
17
  process.stderr.write(String(msg) + "\n");
@@ -63,11 +66,30 @@ function expandHome(input) {
63
66
 
64
67
  function usageText() {
65
68
  return [
66
- "Usage: mcp-aws-manager [options]",
69
+ "Usage:",
70
+ " mcp-aws-manager",
71
+ " mcp-aws-manager bootstrap [options]",
72
+ " mcp-aws-manager setup [options]",
73
+ " mcp-aws-manager doctor [options]",
74
+ " mcp-aws-manager discover [discover-options]",
75
+ " mcp-aws-manager [discover-options]",
67
76
  "",
68
- "SSM-only AWS EC2 inventory and runtime snapshot collector.",
77
+ "SSM-only AWS EC2 inventory/runtime collector plus MCP client setup helper.",
69
78
  "",
70
- "Options:",
79
+ "Commands:",
80
+ " bootstrap Ensure mcp-aws-manager MCP server is registered (default command)",
81
+ " setup Register/re-register MCP server for Codex/Claude",
82
+ " doctor Check install and registration health",
83
+ " discover Run EC2+SSM inventory workflow",
84
+ "",
85
+ "Setup/Bootstrap/Doctor options:",
86
+ " --name <server-name> (default: mcp-aws-manager)",
87
+ " --mcp-command <command> (default: mcp-aws-manager-mcp)",
88
+ " --clients <codex,claude> (default: codex,claude)",
89
+ " --force (setup/bootstrap only; always remove then add)",
90
+ " -h, --help",
91
+ "",
92
+ "Discover options:",
71
93
  " --profiles <a,b,c>",
72
94
  " --regions <a,b,c>",
73
95
  " --instance-ids <id1,id2>",
@@ -100,7 +122,131 @@ function usageText() {
100
122
  ].join("\n");
101
123
  }
102
124
 
103
- function parseArgs(argv) {
125
+ function parseClients(raw) {
126
+ const values = parseCsv(raw) || [];
127
+ if (!values.length) {
128
+ throw new Error("--clients must include at least one of: codex, claude");
129
+ }
130
+ const out = [];
131
+ const seen = new Set();
132
+ for (const value of values) {
133
+ const name = String(value).trim().toLowerCase();
134
+ if (!SUPPORTED_CLIENTS.has(name)) {
135
+ throw new Error(`Unsupported client '${value}'. Supported: codex, claude`);
136
+ }
137
+ if (!seen.has(name)) {
138
+ seen.add(name);
139
+ out.push(name);
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+
145
+ function parseCommand(argv) {
146
+ const args = Array.from(argv || []);
147
+ if (!args.length) {
148
+ return { command: "bootstrap", args: [] };
149
+ }
150
+
151
+ const first = String(args[0] || "");
152
+ if (first === "-h" || first === "--help") {
153
+ return { command: "help", args: [] };
154
+ }
155
+
156
+ if (first === "bootstrap" || first === "init") {
157
+ return { command: "bootstrap", args: args.slice(1) };
158
+ }
159
+ if (first === "setup" || first === "doctor" || first === "discover") {
160
+ return { command: first, args: args.slice(1) };
161
+ }
162
+ if (first.startsWith("-")) {
163
+ return { command: "discover", args };
164
+ }
165
+
166
+ throw new Error(`Unknown command '${first}'. Run --help for usage.`);
167
+ }
168
+
169
+ function parseRegistrationArgs(argv, opts = {}) {
170
+ const allowForce = opts.allowForce === true;
171
+ const options = {
172
+ serverName: null,
173
+ mcpCommand: null,
174
+ clients: null,
175
+ force: false,
176
+ help: false
177
+ };
178
+
179
+ function setKV(key, value) {
180
+ switch (key) {
181
+ case "name":
182
+ options.serverName = String(value || "").trim();
183
+ break;
184
+ case "mcp-command":
185
+ options.mcpCommand = String(value || "").trim();
186
+ break;
187
+ case "clients":
188
+ options.clients = String(value || "").trim();
189
+ break;
190
+ default:
191
+ throw new Error(`Unknown option --${key}`);
192
+ }
193
+ }
194
+
195
+ const args = Array.from(argv || []);
196
+ for (let i = 0; i < args.length; i += 1) {
197
+ const arg = String(args[i] || "");
198
+ if (arg === "-h" || arg === "--help") {
199
+ options.help = true;
200
+ continue;
201
+ }
202
+ if (arg === "--force") {
203
+ if (!allowForce) {
204
+ throw new Error("--force is only supported by setup/bootstrap");
205
+ }
206
+ options.force = true;
207
+ continue;
208
+ }
209
+ if (!arg.startsWith("--")) {
210
+ throw new Error(`Unexpected argument: ${arg}`);
211
+ }
212
+
213
+ const eq = arg.indexOf("=");
214
+ if (eq >= 0) {
215
+ const key = arg.slice(2, eq);
216
+ const value = arg.slice(eq + 1);
217
+ if (!value) throw new Error(`Missing value for --${key}`);
218
+ setKV(key, value);
219
+ continue;
220
+ }
221
+
222
+ const key = arg.slice(2);
223
+ const next = args[i + 1];
224
+ if (!next || String(next).startsWith("--")) {
225
+ throw new Error(`Missing value for --${key}`);
226
+ }
227
+ setKV(key, next);
228
+ i += 1;
229
+ }
230
+
231
+ if (options.help) {
232
+ return { help: true };
233
+ }
234
+
235
+ const serverName = options.serverName || DEFAULT_SERVER_NAME;
236
+ const mcpCommand = options.mcpCommand || DEFAULT_MCP_COMMAND;
237
+ if (!serverName) throw new Error("--name cannot be empty");
238
+ if (!mcpCommand) throw new Error("--mcp-command cannot be empty");
239
+
240
+ return {
241
+ help: false,
242
+ serverName,
243
+ mcpCommand,
244
+ clients: options.clients ? parseClients(options.clients) : ["codex", "claude"],
245
+ force: options.force
246
+ };
247
+ }
248
+
249
+ function parseDiscoverArgs(argv) {
104
250
  const options = {
105
251
  profiles: null,
106
252
  regions: null,
@@ -136,7 +282,7 @@ function parseArgs(argv) {
136
282
  case "snapshot-max-kb": options.snapshotMaxKb = value; break;
137
283
  case "format": options.format = value; break;
138
284
  case "out": options.outPath = value; break;
139
- default: throw new Error(`Unknown option --${key}`);
285
+ default: throw new Error(`Unknown discover option --${key}`);
140
286
  }
141
287
  }
142
288
 
@@ -254,6 +400,179 @@ function listLocalAwsProfiles() {
254
400
  return Array.from(found).filter(Boolean).sort();
255
401
  }
256
402
 
403
+ function runCLICommand(cliBin, args, options = {}) {
404
+ const execOptions = {
405
+ cwd: process.cwd(),
406
+ env: process.env,
407
+ stdio: options.stdio || "pipe",
408
+ encoding: "utf8",
409
+ shell: false
410
+ };
411
+
412
+ const direct = spawnSync(cliBin, args, execOptions);
413
+ if (process.platform !== "win32") {
414
+ return direct;
415
+ }
416
+
417
+ const errCode = String(direct && direct.error && direct.error.code ? direct.error.code : "");
418
+ if (!direct.error || (errCode !== "ENOENT" && errCode !== "EINVAL")) {
419
+ return direct;
420
+ }
421
+
422
+ // On Windows, global npm CLIs are often .cmd wrappers and can fail direct lookup.
423
+ return spawnSync("cmd.exe", ["/d", "/s", "/c", cliBin, ...args], execOptions);
424
+ }
425
+
426
+ function runStatusLabel(run) {
427
+ if (run && typeof run.status === "number") {
428
+ return `exit ${run.status}`;
429
+ }
430
+ if (run && run.error) {
431
+ return run.error.message || String(run.error);
432
+ }
433
+ return "unknown";
434
+ }
435
+
436
+ function commandExists(bin, checkArgs) {
437
+ const run = runCLICommand(bin, Array.isArray(checkArgs) ? checkArgs : ["--help"], { stdio: "ignore" });
438
+ return run && run.status === 0;
439
+ }
440
+
441
+ function removeRegistration(cliBin, serverName) {
442
+ if (cliBin === "claude") {
443
+ runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "user"], { stdio: "ignore" });
444
+ runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "local"], { stdio: "ignore" });
445
+ return;
446
+ }
447
+ runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
448
+ }
449
+
450
+ function isRegistered(cliBin, serverName) {
451
+ const args = cliBin === "claude"
452
+ ? ["mcp", "get", serverName]
453
+ : ["mcp", "get", serverName, "--json"];
454
+ const run = runCLICommand(cliBin, args, { stdio: "ignore" });
455
+ return run && run.status === 0;
456
+ }
457
+
458
+ function registrationAttempts(cliBin, serverName, mcpCommand) {
459
+ if (cliBin === "claude") {
460
+ return [
461
+ ["mcp", "add", "--scope", "user", serverName, "--", mcpCommand],
462
+ ["mcp", "add", "--scope", "user", serverName, mcpCommand]
463
+ ];
464
+ }
465
+ return [
466
+ ["mcp", "add", serverName, "--", mcpCommand],
467
+ ["mcp", "add", serverName, mcpCommand]
468
+ ];
469
+ }
470
+
471
+ function tryRegister(cliBin, serverName, mcpCommand) {
472
+ const attempts = registrationAttempts(cliBin, serverName, mcpCommand);
473
+ let lastRun = null;
474
+ let lastArgs = null;
475
+ for (const args of attempts) {
476
+ const run = runCLICommand(cliBin, args, { stdio: "pipe" });
477
+ if (run && run.status === 0) {
478
+ return { ok: true, args, run };
479
+ }
480
+ lastRun = run;
481
+ lastArgs = args;
482
+ }
483
+ return { ok: false, args: lastArgs, run: lastRun };
484
+ }
485
+
486
+ function runSetupInternal(config, options = {}) {
487
+ const ensureOnly = options.ensureOnly === true;
488
+ const clients = config.clients.filter((cli) => commandExists(cli, ["mcp", "--help"]));
489
+ const results = [];
490
+
491
+ process.stdout.write(ensureOnly ? "Bootstrap start.\n" : "Setup start.\n");
492
+ process.stdout.write(`Server: ${config.serverName}\n`);
493
+ process.stdout.write(`Command: ${config.mcpCommand}\n`);
494
+ process.stdout.write(`Clients: ${config.clients.join(",")}\n`);
495
+
496
+ if (!clients.length) {
497
+ process.stdout.write("No codex/claude CLI found. Registration skipped.\n");
498
+ return 2;
499
+ }
500
+
501
+ for (const cliBin of clients) {
502
+ const alreadyRegistered = isRegistered(cliBin, config.serverName);
503
+ if (config.force || !ensureOnly || alreadyRegistered) {
504
+ removeRegistration(cliBin, config.serverName);
505
+ }
506
+
507
+ const registered = tryRegister(cliBin, config.serverName, config.mcpCommand);
508
+ if (registered.ok) {
509
+ const action = ensureOnly
510
+ ? (alreadyRegistered ? "updated" : "registered")
511
+ : (alreadyRegistered ? "re-registered" : "registered");
512
+ results.push({ cliBin, ok: true, action, detail: "" });
513
+ continue;
514
+ }
515
+
516
+ const stderr = String(registered.run && registered.run.stderr ? registered.run.stderr : "").trim();
517
+ const stdout = String(registered.run && registered.run.stdout ? registered.run.stdout : "").trim();
518
+ const detail = stderr || stdout || runStatusLabel(registered.run);
519
+ results.push({ cliBin, ok: false, action: "registration failed", detail });
520
+ }
521
+
522
+ for (const row of results) {
523
+ process.stdout.write(`${row.cliBin}: ${row.action}\n`);
524
+ if (!row.ok && row.detail) {
525
+ process.stdout.write(` detail: ${row.detail}\n`);
526
+ }
527
+ }
528
+
529
+ const failed = results.filter((r) => !r.ok).length;
530
+ process.stdout.write(failed ? "Setup finished with failures.\n" : "Setup finished successfully.\n");
531
+ return failed ? 2 : 0;
532
+ }
533
+
534
+ function runDoctor(config) {
535
+ process.stdout.write("Doctor start.\n");
536
+ let hasIssue = false;
537
+ let foundClient = false;
538
+
539
+ const mcpCommandRun = runCLICommand(config.mcpCommand, ["--help"], { stdio: "pipe" });
540
+ if (mcpCommandRun && mcpCommandRun.status === 0) {
541
+ process.stdout.write(`mcp-command: ok (${config.mcpCommand})\n`);
542
+ } else {
543
+ hasIssue = true;
544
+ const detail = String(mcpCommandRun && (mcpCommandRun.stderr || mcpCommandRun.stdout) ? (mcpCommandRun.stderr || mcpCommandRun.stdout) : "").trim();
545
+ process.stdout.write(`mcp-command: fail (${config.mcpCommand})\n`);
546
+ if (detail) process.stdout.write(` detail: ${detail}\n`);
547
+ }
548
+
549
+ for (const cliBin of config.clients) {
550
+ const exists = commandExists(cliBin, ["mcp", "--help"]);
551
+ if (!exists) {
552
+ hasIssue = true;
553
+ process.stdout.write(`${cliBin}: not installed or not available in PATH\n`);
554
+ continue;
555
+ }
556
+
557
+ foundClient = true;
558
+ const registered = isRegistered(cliBin, config.serverName);
559
+ if (registered) {
560
+ process.stdout.write(`${cliBin}: registered (${config.serverName})\n`);
561
+ } else {
562
+ hasIssue = true;
563
+ process.stdout.write(`${cliBin}: missing registration (${config.serverName})\n`);
564
+ process.stdout.write(` action: mcp-aws-manager setup --name ${config.serverName} --mcp-command ${config.mcpCommand} --clients ${cliBin}\n`);
565
+ }
566
+ }
567
+
568
+ if (!foundClient) {
569
+ process.stdout.write("No requested clients detected. Install Codex or Claude Code first.\n");
570
+ }
571
+
572
+ process.stdout.write(hasIssue ? "Doctor result: issues found.\n" : "Doctor result: healthy.\n");
573
+ return hasIssue ? 2 : 0;
574
+ }
575
+
257
576
  let awsModulesCache = null;
258
577
  function loadAwsModules() {
259
578
  if (awsModulesCache) return awsModulesCache;
@@ -1058,7 +1377,47 @@ async function runWorkflow(config) {
1058
1377
 
1059
1378
  async function main() {
1060
1379
  try {
1061
- const config = parseArgs(process.argv.slice(2));
1380
+ const parsed = parseCommand(process.argv.slice(2));
1381
+ if (parsed.command === "help") {
1382
+ process.stdout.write(usageText());
1383
+ process.exitCode = 0;
1384
+ return;
1385
+ }
1386
+
1387
+ if (parsed.command === "setup") {
1388
+ const config = parseRegistrationArgs(parsed.args, { allowForce: true });
1389
+ if (config.help) {
1390
+ process.stdout.write(usageText());
1391
+ process.exitCode = 0;
1392
+ return;
1393
+ }
1394
+ process.exitCode = runSetupInternal(config, { ensureOnly: false });
1395
+ return;
1396
+ }
1397
+
1398
+ if (parsed.command === "bootstrap") {
1399
+ const config = parseRegistrationArgs(parsed.args, { allowForce: true });
1400
+ if (config.help) {
1401
+ process.stdout.write(usageText());
1402
+ process.exitCode = 0;
1403
+ return;
1404
+ }
1405
+ process.exitCode = runSetupInternal(config, { ensureOnly: true });
1406
+ return;
1407
+ }
1408
+
1409
+ if (parsed.command === "doctor") {
1410
+ const config = parseRegistrationArgs(parsed.args, { allowForce: false });
1411
+ if (config.help) {
1412
+ process.stdout.write(usageText());
1413
+ process.exitCode = 0;
1414
+ return;
1415
+ }
1416
+ process.exitCode = runDoctor(config);
1417
+ return;
1418
+ }
1419
+
1420
+ const config = parseDiscoverArgs(parsed.args);
1062
1421
  if (config.help) {
1063
1422
  process.stdout.write(usageText());
1064
1423
  process.exitCode = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-aws-manager",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "AWS operations CLI and MCP server (SSM-only) for EC2 inventory, remediation, and runtime snapshots",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -23,7 +23,8 @@
23
23
  },
24
24
  "files": [
25
25
  "bin",
26
- "MCP_CLIENT_SETUP.md"
26
+ "MCP_CLIENT_SETUP.md",
27
+ "AGENT_GUIDANCE_LOOP_TEMPLATE_KO.md"
27
28
  ],
28
29
  "engines": {
29
30
  "node": ">=18"