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.
- package/AGENT_GUIDANCE_LOOP_TEMPLATE_KO.md +52 -0
- package/MCP_CLIENT_SETUP.md +37 -10
- package/README.md +36 -8
- package/bin/mcp-aws-manager-mcp.js +153 -0
- package/bin/mcp-aws-manager.js +366 -7
- package/package.json +3 -2
|
@@ -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. 다음 선택 사항(예: 런타임 스냅샷 확장)
|
package/MCP_CLIENT_SETUP.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
package/bin/mcp-aws-manager.js
CHANGED
|
@@ -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:
|
|
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
|
|
77
|
+
"SSM-only AWS EC2 inventory/runtime collector plus MCP client setup helper.",
|
|
69
78
|
"",
|
|
70
|
-
"
|
|
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
|
|
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
|
|
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.
|
|
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"
|