Coverage for gco_mcp / tools / stacks.py: 100.00%

221 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Infrastructure stack management MCP tools.""" 

2 

3from __future__ import annotations 

4 

5import asyncio 

6from typing import Any 

7 

8import cli_runner 

9from audit import audit_logged 

10 

11# FastMCP's Progress / Context dependencies are optional from this 

12# module's perspective — when ``fastmcp[tasks]`` is reachable they 

13# inject real instances per call; otherwise the gated long-running 

14# tools still register but rely on caller-provided fakes (the test path). 

15from fastmcp.server.dependencies import CurrentContext, Progress 

16 

17# TaskConfig opts the gated stack-lifecycle tools into the MCP tasks 

18# extension (SEP-2663, registered in gco_mcp/server.py) with 

19# ``mode="optional"`` — clients that support the task protocol receive a 

20# task ID immediately and poll for progress, while clients without 

21# task-protocol support fall back to inline execution with progress 

22# streamed through FastMCP's Progress dependency. Required-mode would lock 

23# out clients that don't speak the task protocol (e.g. the GCO MCP 

24# orchestrator's ``call_tool`` proxy), and these tools are useful enough 

25# that the inline fallback is worth keeping. 

26from fastmcp.utilities.tasks import TaskConfig 

27from feature_flags import ( 

28 FLAG_CONFIG_MANAGEMENT, 

29 FLAG_INFRASTRUCTURE_DEPLOY, 

30 FLAG_INFRASTRUCTURE_DESTROY, 

31 is_enabled, 

32) 

33from server import mcp 

34 

35from tools._long_task import _run_long_task 

36 

37_TASK_CONFIG_OPTIONAL = TaskConfig(mode="optional") 

38 

39 

40def _expected_stack_count_for_all() -> int | None: 

41 """Return the number of stacks ``deploy-all`` / ``destroy-all`` will touch. 

42 

43 Reads ``cdk.json``'s ``context.deployment_regions`` and counts the 

44 fixed-position stacks (gco-global, gco-api-gateway, gco-monitoring) 

45 plus one per regional region. Returns ``None`` when the config is 

46 unreadable or empty so the caller falls back to indeterminate 

47 progress instead of an inaccurate total. 

48 

49 The count drives ``progress.set_total(...)`` so MCP clients render 

50 a real percentage during a multi-stack deploy or destroy. 

51 """ 

52 try: 

53 from cli.config import _load_cdk_json 

54 except Exception: # noqa: BLE001 — best-effort 

55 return None 

56 try: 

57 cdk_regions = _load_cdk_json() 

58 except Exception: # noqa: BLE001 — best-effort 

59 return None 

60 if not isinstance(cdk_regions, dict): 

61 return None 

62 if "regional" not in cdk_regions: 

63 return None 

64 regional = cdk_regions["regional"] 

65 if not isinstance(regional, list): 

66 return None 

67 # Three fixed stacks (global / api-gateway / monitoring) plus one 

68 # per regional region. Analytics is opt-in and omitted from the 

69 # baseline count — when enabled it adds one more stack but 

70 # under-reporting is preferable to over-reporting (the progress 

71 # bar rolls over rather than stopping at 95 %). 

72 return 3 + len(regional) 

73 

74 

75@mcp.tool(tags={"safe", "stacks"}) 

76@audit_logged 

77def list_stacks() -> str: 

78 """List all GCO CDK stacks.""" 

79 return cli_runner._run_cli("stacks", "list") 

80 

81 

82@mcp.tool(tags={"safe", "stacks"}) 

83@audit_logged 

84def stack_status(stack_name: str, region: str) -> str: 

85 """Get detailed status of a CloudFormation stack. 

86 

87 Args: 

88 stack_name: Stack name (e.g. gco-us-east-1). 

89 region: AWS region. 

90 """ 

91 return cli_runner._run_cli("stacks", "status", stack_name, "-r", region) 

92 

93 

94@mcp.tool(tags={"low-risk", "stacks"}) 

95@audit_logged 

96def setup_cluster_access(cluster: str | None = None, region: str | None = None) -> str: 

97 """Configure kubectl access to a GCO EKS cluster. 

98 

99 Updates kubeconfig, creates an EKS access entry for your IAM principal, 

100 and associates the cluster admin policy. Handles assumed roles automatically. 

101 

102 Args: 

103 cluster: Cluster name (default: <project_name>-{region}). 

104 region: AWS region (default: first deployment region from cdk.json). 

105 """ 

106 args = ["stacks", "access"] 

107 if cluster: 

108 args.extend(["-c", cluster]) 

109 if region: 

110 args.extend(["-r", region]) 

111 return cli_runner._run_cli(*args) 

112 

113 

114@mcp.tool(tags={"safe", "stacks"}) 

115@audit_logged 

116def fsx_status() -> str: 

117 """Check FSx for Lustre configuration status.""" 

118 return cli_runner._run_cli("stacks", "fsx", "status") 

119 

120 

121# ============================================================================= 

122# Read-only inspection tools (async) 

123# ============================================================================= 

124 

125 

126@mcp.tool(tags={"safe", "stacks"}) 

127@audit_logged 

128async def stack_diff(stack_name: str | None = None) -> str: 

129 """`gco stacks diff` — show CloudFormation diff for a stack. 

130 

131 Args: 

132 stack_name: Stack to diff. If omitted, diffs all stacks. 

133 """ 

134 args = ["stacks", "diff"] 

135 if stack_name: 

136 args.append(stack_name) 

137 return await asyncio.to_thread(cli_runner._run_cli, *args) 

138 

139 

140@mcp.tool(tags={"safe", "stacks"}) 

141@audit_logged 

142async def stack_outputs(stack_name: str, region: str) -> str: 

143 """`gco stacks outputs` — fetch CloudFormation outputs for a stack. 

144 

145 Args: 

146 stack_name: Stack name (e.g. gco-us-east-1). 

147 region: AWS region. 

148 """ 

149 return await asyncio.to_thread( 

150 cli_runner._run_cli, "stacks", "outputs", stack_name, "-r", region 

151 ) 

152 

153 

154@mcp.tool(tags={"safe", "stacks"}) 

155@audit_logged 

156async def stack_synth(stack_name: str | None = None, quiet: bool = True) -> str: 

157 """`gco stacks synth` — synthesize CloudFormation templates from CDK. 

158 

159 Args: 

160 stack_name: Stack to synthesize. If omitted, synthesizes all stacks. 

161 quiet: When True, pass ``--quiet`` to suppress verbose CDK output. 

162 """ 

163 args = ["stacks", "synth"] 

164 if stack_name: 

165 args.append(stack_name) 

166 if quiet: 

167 args.append("--quiet") 

168 return await asyncio.to_thread(cli_runner._run_cli, *args) 

169 

170 

171@mcp.tool(tags={"safe", "stacks"}) 

172@audit_logged 

173async def addons_status(region: str | None = None, all_regions: bool = False) -> str: 

174 """`gco stacks addons status` — show per-chart Helm add-on status from SSM. 

175 

176 Args: 

177 region: Region to inspect. Omit for the first deployment region. 

178 all_regions: Inspect every configured deployment region. 

179 """ 

180 args = ["stacks", "addons", "status"] 

181 if all_regions: 

182 args.append("--all-regions") 

183 elif region: 

184 args += ["-r", region] 

185 return await asyncio.to_thread(cli_runner._run_cli, *args) 

186 

187 

188@mcp.tool(tags={"safe", "stacks"}) 

189@audit_logged 

190async def valkey_status() -> str: 

191 """`gco stacks valkey status` — show Valkey cache stack status.""" 

192 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "valkey", "status") 

193 

194 

195@mcp.tool(tags={"safe", "stacks"}) 

196@audit_logged 

197async def aurora_status() -> str: 

198 """`gco stacks aurora status` — show Aurora database stack status.""" 

199 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "aurora", "status") 

200 

201 

202# ============================================================================= 

203# Mutating cdk.json toggles (low-risk) 

204# ============================================================================= 

205 

206 

207@mcp.tool(tags={"low-risk", "stacks"}) 

208@audit_logged 

209async def enable_fsx() -> str: 

210 """`gco stacks fsx enable` — flip FSx Lustre on in cdk.json. 

211 

212 Note: this only edits the cdk.json toggle. The change does not take effect 

213 until ``gco stacks deploy-all`` runs to provision the FSx file system. 

214 """ 

215 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "fsx", "enable", "-y") 

216 

217 

218@mcp.tool(tags={"low-risk", "stacks"}) 

219@audit_logged 

220async def disable_fsx() -> str: 

221 """`gco stacks fsx disable` — flip FSx Lustre off in cdk.json. 

222 

223 Note: this only edits the cdk.json toggle. The change does not take effect 

224 until ``gco stacks deploy-all`` runs to remove the FSx file system. 

225 """ 

226 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "fsx", "disable", "-y") 

227 

228 

229@mcp.tool(tags={"low-risk", "stacks"}) 

230@audit_logged 

231async def enable_valkey() -> str: 

232 """`gco stacks valkey enable` — flip Valkey Serverless on in cdk.json. 

233 

234 Note: this only edits the cdk.json toggle. The change does not take effect 

235 until ``gco stacks deploy-all`` runs to provision the Valkey cache. 

236 """ 

237 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "valkey", "enable", "-y") 

238 

239 

240@mcp.tool(tags={"low-risk", "stacks"}) 

241@audit_logged 

242async def disable_valkey() -> str: 

243 """`gco stacks valkey disable` — flip Valkey Serverless off in cdk.json. 

244 

245 Note: this only edits the cdk.json toggle. The change does not take effect 

246 until ``gco stacks deploy-all`` runs to remove the Valkey cache. 

247 """ 

248 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "valkey", "disable", "-y") 

249 

250 

251@mcp.tool(tags={"low-risk", "stacks"}) 

252@audit_logged 

253async def enable_aurora() -> str: 

254 """`gco stacks aurora enable` — flip Aurora pgvector on in cdk.json. 

255 

256 Note: this only edits the cdk.json toggle. The change does not take effect 

257 until ``gco stacks deploy-all`` runs to provision the Aurora cluster. 

258 """ 

259 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "aurora", "enable", "-y") 

260 

261 

262@mcp.tool(tags={"low-risk", "stacks"}) 

263@audit_logged 

264async def disable_aurora() -> str: 

265 """`gco stacks aurora disable` — flip Aurora pgvector off in cdk.json. 

266 

267 Note: this only edits the cdk.json toggle. The change does not take effect 

268 until ``gco stacks deploy-all`` runs to remove the Aurora cluster. 

269 """ 

270 return await asyncio.to_thread(cli_runner._run_cli, "stacks", "aurora", "disable", "-y") 

271 

272 

273# ============================================================================= 

274# Long-running stack lifecycle tools — gated by GCO_ENABLE_INFRASTRUCTURE_DEPLOY 

275# ============================================================================= 

276# 

277# deploy_stack / deploy_all / bootstrap_cdk drive CDK lifecycle operations 

278# that exceed the short-running ``cli_runner._run_cli`` 120-second timeout. 

279# They run via ``_run_long_task`` so progress streams back through the 

280# FastMCP Progress dependency and clients can poll task status through 

281# the standard MCP task protocol. 

282 

283if is_enabled(FLAG_INFRASTRUCTURE_DEPLOY): 

284 

285 @mcp.tool(tags={"infrastructure", "stacks"}) 

286 @audit_logged 

287 async def addons_install(region: str | None = None, all_regions: bool = False) -> str: 

288 """[gated by GCO_ENABLE_INFRASTRUCTURE_DEPLOY] infrastructure mutation. 

289 

290 `gco stacks addons install` — start an idempotent Helm add-on 

291 re-convergence from the deployment input persisted in SSM. The command 

292 starts each region's installer state machine and returns immediately; 

293 inspect progress with ``addons_status``. 

294 

295 Args: 

296 region: Region to re-converge. Omit for the first deployment region. 

297 all_regions: Re-converge every configured deployment region. 

298 """ 

299 args = ["stacks", "addons", "install"] 

300 if all_regions: 

301 args.append("--all-regions") 

302 elif region: 

303 args += ["-r", region] 

304 return await asyncio.to_thread(cli_runner._run_cli, *args) 

305 

306 @mcp.tool(tags={"infrastructure", "stacks"}, task=_TASK_CONFIG_OPTIONAL) 

307 @audit_logged 

308 async def deploy_stack( 

309 stack_name: str, 

310 yes: bool = True, 

311 outputs_file: str | None = None, 

312 tags: list[str] | None = None, 

313 *, 

314 ctx: Any = CurrentContext(), 

315 progress: Any = Progress(), 

316 ) -> str: 

317 """[gated by GCO_ENABLE_INFRASTRUCTURE_DEPLOY] long-running. 

318 

319 `gco stacks deploy` — deploy a single CDK stack to AWS. 

320 

321 Typical wall-clock: 15-30 minutes per regional stack. Clients that 

322 speak FastMCP's task protocol can receive a task ID immediately 

323 and poll `tasks://gco/{task_id}` for progress; clients that don't 

324 run the tool inline with progress streamed through the FastMCP 

325 Progress dependency. Cancellation sends SIGTERM to the running 

326 CDK process and partial CloudFormation state may remain — inspect 

327 via stack_status or the AWS console. 

328 

329 Args: 

330 stack_name: Stack to deploy (e.g. ``gco-us-east-1``). 

331 yes: Skip approval prompts (passes ``-y``). Defaults to True. 

332 outputs_file: Optional path to write stack outputs JSON. 

333 tags: Optional list of ``key=value`` tag strings applied to the stack. 

334 """ 

335 argv = [ 

336 "gco", 

337 "stacks", 

338 "deploy", 

339 stack_name, 

340 ] 

341 if yes: 

342 argv.append("-y") 

343 if outputs_file: 

344 argv += ["--outputs-file", outputs_file] 

345 for tag in tags or []: 

346 argv += ["--tag", tag] 

347 return await _run_long_task( 

348 argv, 

349 ctx=ctx, 

350 progress=progress, 

351 is_stack_op=True, 

352 total_units=1, 

353 ) 

354 

355 @mcp.tool(tags={"infrastructure", "stacks"}, task=_TASK_CONFIG_OPTIONAL) 

356 @audit_logged 

357 async def deploy_all( 

358 yes: bool = True, 

359 outputs_file: str | None = None, 

360 tags: list[str] | None = None, 

361 parallel: bool = False, 

362 max_workers: int | None = None, 

363 *, 

364 ctx: Any = CurrentContext(), 

365 progress: Any = Progress(), 

366 ) -> str: 

367 """[gated by GCO_ENABLE_INFRASTRUCTURE_DEPLOY] long-running. 

368 

369 `gco stacks deploy-all` — deploy every CDK stack in dependency order. 

370 

371 Typical wall-clock: 30-60 minutes for a fresh multi-region deploy. 

372 Clients that speak FastMCP's task protocol can receive a task ID 

373 immediately and poll `tasks://gco/{task_id}` for progress; clients 

374 that don't run the tool inline with progress streamed through the 

375 FastMCP Progress dependency. Cancellation sends SIGTERM to the 

376 running CDK process and partial CloudFormation state may remain — 

377 inspect via stack_status or the AWS console. 

378 

379 Args: 

380 yes: Skip approval prompts (passes ``-y``). Defaults to True. 

381 outputs_file: Optional path to write stack outputs JSON. 

382 tags: Optional list of ``key=value`` tag strings applied to every stack. 

383 parallel: Deploy regional stacks concurrently when True. 

384 max_workers: Cap on parallel deployments when ``parallel=True``. 

385 """ 

386 argv = [ 

387 "gco", 

388 "stacks", 

389 "deploy-all", 

390 ] 

391 if yes: 

392 argv.append("-y") 

393 if outputs_file: 

394 argv += ["--outputs-file", outputs_file] 

395 for tag in tags or []: 

396 argv += ["--tag", tag] 

397 if parallel: 

398 argv.append("--parallel") 

399 if max_workers is not None: 

400 argv += ["--max-workers", str(max_workers)] 

401 return await _run_long_task( 

402 argv, 

403 ctx=ctx, 

404 progress=progress, 

405 is_stack_op=True, 

406 total_units=_expected_stack_count_for_all(), 

407 ) 

408 

409 @mcp.tool(tags={"infrastructure", "stacks"}, task=_TASK_CONFIG_OPTIONAL) 

410 @audit_logged 

411 async def bootstrap_cdk( 

412 region: str, 

413 account: str | None = None, 

414 *, 

415 ctx: Any = CurrentContext(), 

416 progress: Any = Progress(), 

417 ) -> str: 

418 """[gated by GCO_ENABLE_INFRASTRUCTURE_DEPLOY] long-running. 

419 

420 `gco stacks bootstrap` — bootstrap CDK in an AWS account/region. 

421 

422 Typical wall-clock: 2-5 minutes. Required before any stack can be 

423 deployed to a new account/region. Clients that speak FastMCP's 

424 task protocol can receive a task ID immediately and poll 

425 `tasks://gco/{task_id}` for progress; clients that don't run the 

426 tool inline with progress streamed through the FastMCP Progress 

427 dependency. Cancellation sends SIGTERM to the running CDK process 

428 and partial CloudFormation state may remain — inspect via 

429 stack_status or the AWS console. 

430 

431 Args: 

432 region: Target AWS region. 

433 account: Optional AWS account ID. Defaults to the caller's account. 

434 """ 

435 argv = ["gco", "stacks", "bootstrap", "--region", region] 

436 if account: 

437 argv += ["--account", account] 

438 return await _run_long_task( 

439 argv, 

440 ctx=ctx, 

441 progress=progress, 

442 is_stack_op=True, 

443 total_units=1, 

444 ) 

445 

446 

447# ============================================================================= 

448# Long-running stack lifecycle tools — gated by GCO_ENABLE_INFRASTRUCTURE_DESTROY 

449# ============================================================================= 

450 

451if is_enabled(FLAG_INFRASTRUCTURE_DESTROY): 

452 

453 @mcp.tool(tags={"infrastructure", "stacks"}, task=_TASK_CONFIG_OPTIONAL) 

454 @audit_logged 

455 async def destroy_stack( 

456 stack_name: str, 

457 yes: bool = True, 

458 retain_volumes: bool = False, 

459 *, 

460 ctx: Any = CurrentContext(), 

461 progress: Any = Progress(), 

462 ) -> str: 

463 """[gated by GCO_ENABLE_INFRASTRUCTURE_DESTROY] long-running. 

464 

465 `gco stacks destroy` — destroy a single CDK stack. 

466 

467 Typical wall-clock: 5-20 minutes per stack. Clients that speak 

468 FastMCP's task protocol can receive a task ID immediately and 

469 poll `tasks://gco/{task_id}` for progress; clients that don't 

470 run the tool inline with progress streamed through the FastMCP 

471 Progress dependency. Cancellation sends SIGTERM to the running 

472 CDK process and partial CloudFormation state may remain — 

473 inspect via stack_status or the AWS console before retrying. 

474 

475 Args: 

476 stack_name: Stack to destroy (e.g. ``gco-us-east-1``). 

477 yes: Skip the confirmation prompt (passes ``-y``). Defaults to True. 

478 retain_volumes: Report the cluster's orphaned EBS volumes instead 

479 of deleting them (passes ``--retain-volumes``). Defaults to 

480 False, matching the CLI: deleting an EKS cluster does not 

481 delete the volumes its CSI driver provisioned, so they bill 

482 indefinitely with nothing able to reattach them. 

483 """ 

484 argv = ["gco", "stacks", "destroy", stack_name] 

485 if yes: 

486 argv.append("-y") 

487 if retain_volumes: 

488 argv.append("--retain-volumes") 

489 return await _run_long_task( 

490 argv, 

491 ctx=ctx, 

492 progress=progress, 

493 is_stack_op=True, 

494 total_units=1, 

495 ) 

496 

497 @mcp.tool(tags={"infrastructure", "stacks"}, task=_TASK_CONFIG_OPTIONAL) 

498 @audit_logged 

499 async def destroy_all( 

500 yes: bool = True, 

501 parallel: bool = False, 

502 max_workers: int | None = None, 

503 retain_volumes: bool = False, 

504 *, 

505 ctx: Any = CurrentContext(), 

506 progress: Any = Progress(), 

507 ) -> str: 

508 """[gated by GCO_ENABLE_INFRASTRUCTURE_DESTROY] long-running. 

509 

510 `gco stacks destroy-all` — destroy every CDK stack in reverse dependency order. 

511 

512 Typical wall-clock: 20-40 minutes for a multi-region teardown. 

513 Clients that speak FastMCP's task protocol can receive a task 

514 ID immediately and poll `tasks://gco/{task_id}` for progress; 

515 clients that don't run the tool inline with progress streamed 

516 through the FastMCP Progress dependency. Cancellation sends 

517 SIGTERM to the running CDK process and partial CloudFormation 

518 state may remain — inspect via stack_status or the AWS console 

519 before retrying. 

520 

521 Args: 

522 yes: Skip the confirmation prompt (passes ``-y``). Defaults to True. 

523 parallel: Destroy regional stacks concurrently when True. 

524 max_workers: Cap on parallel destructions when ``parallel=True``. 

525 retain_volumes: Report each cluster's orphaned EBS volumes instead 

526 of deleting them (passes ``--retain-volumes``). Defaults to 

527 False, matching the CLI. 

528 """ 

529 argv = ["gco", "stacks", "destroy-all"] 

530 if yes: 

531 argv.append("-y") 

532 if parallel: 

533 argv.append("--parallel") 

534 if max_workers is not None: 

535 argv += ["--max-workers", str(max_workers)] 

536 if retain_volumes: 

537 argv.append("--retain-volumes") 

538 return await _run_long_task( 

539 argv, 

540 ctx=ctx, 

541 progress=progress, 

542 is_stack_op=True, 

543 total_units=_expected_stack_count_for_all(), 

544 ) 

545 

546 

547# ============================================================================= 

548# Managed deployment configuration — disabled by default. 

549# Set GCO_ENABLE_CONFIG_MANAGEMENT=true to enable. 

550# ============================================================================= 

551# These tools edit cdk.json on the MCP host through the managed-config 

552# engine (cli/managed_config.py): validated against the same rules CDK 

553# synth enforces, atomic, idempotent, and audited. They never deploy — 

554# an explicit deploy_stack / deploy_all_stacks call (separately gated by 

555# GCO_ENABLE_INFRASTRUCTURE_DEPLOY) is still required for a topology 

556# change to reach AWS. Installed (uvx/pip) servers resolve a read-only 

557# packaged cdk.json; the engine refuses those with guidance rather than 

558# half-working, so these tools are useful from a GCO checkout. 

559 

560if is_enabled(FLAG_CONFIG_MANAGEMENT): 

561 

562 @mcp.tool(tags={"safe", "stacks"}) 

563 @audit_logged 

564 def list_deployment_regions() -> str: 

565 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

566 

567 Show the deployment-region topology configured in cdk.json. 

568 

569 Reports the global/api_gateway/monitoring Regions, the workload 

570 (regional) Region list, the resolved AWS partition, and the cdk.json 

571 path backing the answer. Works on a broken configuration too — the 

572 partition_error field explains what CDK synth would reject. 

573 """ 

574 return cli_runner._run_cli("stacks", "regions", "list") 

575 

576 @mcp.tool(tags={"low-risk", "stacks"}) 

577 @audit_logged 

578 def add_deployment_region(region: str) -> str: 

579 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

580 

581 Add a workload Region to cdk.json deployment_regions.regional. 

582 

583 Config-only and idempotent: the Region must be SDK-known and share 

584 the AWS partition of the already-configured Regions; re-adding a 

585 present Region is a reported no-op. No stack is deployed — follow 

586 up with deploy_stack / deploy_all_stacks to apply the topology. 

587 

588 Args: 

589 region: AWS Region name to add (e.g. us-west-2). 

590 """ 

591 return cli_runner._run_cli("stacks", "regions", "add", region, "-y") 

592 

593 @mcp.tool(tags={"low-risk", "stacks"}) 

594 @audit_logged 

595 def remove_deployment_region(region: str) -> str: 

596 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

597 

598 Remove a workload Region from cdk.json deployment_regions.regional. 

599 

600 Config-only and idempotent: the resulting list must stay valid (at 

601 least one Region); removing an absent Region is a reported no-op. 

602 A deployed stack for the removed Region is NOT destroyed — that 

603 requires an explicit destroy_stack call (separately gated). 

604 

605 Args: 

606 region: AWS Region name to remove (e.g. us-west-2). 

607 """ 

608 return cli_runner._run_cli("stacks", "regions", "remove", region, "-y") 

609 

610 @mcp.tool(tags={"low-risk", "stacks"}) 

611 @audit_logged 

612 def set_deployment_region(role: str, region: str) -> str: 

613 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

614 

615 Set a control-plane Region scalar in cdk.json deployment_regions. 

616 

617 Config-only and idempotent: the Region must be SDK-known and keep 

618 the whole topology (all three scalars plus the workload list) in one 

619 AWS partition. Already-deployed stacks are not moved or destroyed — 

620 the next deploy creates the stack in the new Region. 

621 

622 Args: 

623 role: Which scalar to set: "global", "api_gateway", or "monitoring". 

624 region: AWS Region name (e.g. us-east-2). 

625 """ 

626 return cli_runner._run_cli("stacks", "regions", "set", role, region, "-y") 

627 

628 @mcp.tool(tags={"low-risk", "stacks"}) 

629 @audit_logged 

630 def set_eks_endpoint_access(mode: str, cidrs: list[str] | None = None) -> str: 

631 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

632 

633 Set cdk.json eks_cluster.endpoint_access (the EKS API endpoint mode). 

634 

635 Config-only and synth-time: no stack is deployed, and `gco stacks 

636 status` reports the configured-vs-live endpoint as drift until a 

637 deploy converges it. PUBLIC_AND_PRIVATE requires an explicit CIDR 

638 allowlist — the CLI refuses to widen control-plane access without 

639 one, and an internet-open endpoint must be spelled out as 0.0.0.0/0. 

640 PRIVATE needs no CIDRs (use cluster_tunnel_command / `gco cluster 

641 tunnel` for laptop access, plus an access entry via `gco stacks 

642 access`). 

643 

644 Args: 

645 mode: "PRIVATE" or "PUBLIC_AND_PRIVATE". 

646 cidrs: CIDR allowlist entries for the public endpoint 

647 (e.g. ["203.0.113.7/32"]). Required for PUBLIC_AND_PRIVATE. 

648 """ 

649 args = ["stacks", "eks", "endpoint", "set", mode] 

650 for cidr in cidrs or []: 

651 args.extend(["--cidr", cidr]) 

652 args.append("-y") 

653 return cli_runner._run_cli(*args) 

654 

655 @mcp.tool(tags={"low-risk", "stacks"}) 

656 @audit_logged 

657 def set_mission_default_model(model_id: str) -> str: 

658 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

659 

660 Set cdk.json bedrock.mission_default_model_id (Mission sampling). 

661 

662 Config-only and idempotent. The capacity advisor and gco autopilot 

663 have their own keys (set_capacity_advisor_default_model and 

664 set_claude_code_default_model). Model and inference-profile IDs are 

665 free-form (custom profiles, marketplace models); validation mirrors 

666 the runtime reader (non-empty, no surrounding whitespace). Sibling 

667 settings (bedrock.generation_reasoning, the other model keys) are preserved; 

668 explicit --bedrock-model-id / env overrides still take precedence 

669 at run time. 

670 

671 Args: 

672 model_id: Bedrock model or inference-profile ID 

673 (e.g. us.amazon.nova-2-lite-v1:0). 

674 """ 

675 return cli_runner._run_cli("stacks", "bedrock", "set-mission-model", model_id, "-y") 

676 

677 @mcp.tool(tags={"low-risk", "stacks"}) 

678 @audit_logged 

679 def set_capacity_advisor_default_model(model_id: str) -> str: 

680 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

681 

682 Set cdk.json bedrock.capacity_advisor_default_model_id. 

683 

684 The default model for gco capacity advise and its historical 

685 variant. Config-only and idempotent; Mission sampling and gco 

686 autopilot have their own keys (set_mission_default_model and 

687 set_claude_code_default_model). Validation mirrors the runtime 

688 reader (non-empty, no surrounding whitespace). Sibling settings 

689 (bedrock.generation_reasoning, the other model keys) are preserved; explicit 

690 --model overrides still take precedence at run time. 

691 

692 Args: 

693 model_id: Bedrock model or inference-profile ID 

694 (e.g. us.amazon.nova-2-lite-v1:0). 

695 """ 

696 return cli_runner._run_cli( 

697 "stacks", "bedrock", "set-capacity-advisor-model", model_id, "-y" 

698 ) 

699 

700 @mcp.tool(tags={"low-risk", "stacks"}) 

701 @audit_logged 

702 def set_claude_code_default_model(model_id: str) -> str: 

703 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

704 

705 Set cdk.json bedrock.claude_code_default_model_id (autopilot model). 

706 

707 The session model gco autopilot hands to Claude Code, independent of 

708 the mission_default_model_id and capacity_advisor_default_model_id 

709 knobs consumed by Mission sampling and the capacity advisor. 

710 Config-only and idempotent; validation mirrors the runtime reader 

711 (non-empty, no surrounding whitespace). Sibling settings are 

712 preserved; explicit --model / GCO_AUTOPILOT_MODEL overrides still 

713 take precedence at launch time. 

714 

715 Args: 

716 model_id: Bedrock model or inference-profile ID 

717 (e.g. us.anthropic.claude-sonnet-4-6). 

718 """ 

719 return cli_runner._run_cli("stacks", "bedrock", "set-claude-code-model", model_id, "-y") 

720 

721 @mcp.tool(tags={"low-risk", "stacks"}) 

722 @audit_logged 

723 def set_codex_default_model(model_id: str) -> str: 

724 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

725 

726 Set cdk.json bedrock.codex_default_model_id. 

727 

728 Config-only and idempotent. The reviewed 

729 bedrock.codex.reasoning_effort sibling is preserved; review the pair 

730 together when changing model families. Explicit --model, 

731 GCO_AUTOPILOT_CODEX_MODEL, and GCO_AUTOPILOT_MODEL overrides still 

732 take precedence at launch. 

733 

734 Args: 

735 model_id: Bedrock model or inference-profile ID 

736 (e.g. global.openai.<model-id>). 

737 """ 

738 return cli_runner._run_cli("stacks", "bedrock", "set-codex-model", model_id, "-y") 

739 

740 @mcp.tool(tags={"low-risk", "stacks"}) 

741 @audit_logged 

742 def set_codex_reasoning_effort(reasoning_effort: str) -> str: 

743 """[gated by GCO_ENABLE_CONFIG_MANAGEMENT] 

744 

745 Set cdk.json bedrock.codex.reasoning_effort. 

746 

747 Allowed values are minimal, low, medium, high, and xhigh. The setting 

748 applies only to the canonical Codex model; explicit model overrides 

749 intentionally omit canonical reasoning. 

750 

751 Args: 

752 reasoning_effort: Reviewed canonical Codex reasoning effort. 

753 """ 

754 return cli_runner._run_cli( 

755 "stacks", 

756 "bedrock", 

757 "set-codex-reasoning-effort", 

758 reasoning_effort, 

759 "-y", 

760 )