mcp-aws-manager 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  {
@@ -67,4 +94,4 @@ npm install -g mcp-aws-manager
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
96
  - 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.
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:
@@ -111,4 +135,4 @@ The MCP wrapper surfaces these in a structured `requiredActions` list.
111
135
  These legacy commands are still available:
112
136
 
113
137
  - `mcp-aws-discover`
114
- - `mcp-aws-discover-mcp`
138
+ - `mcp-aws-discover-mcp`
@@ -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.0",
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": {