thinkwork-cli 0.4.0 → 0.5.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.
- package/dist/cli.js +97 -94
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +10 -4
- package/dist/terraform/modules/app/lambda-api/main.tf +20 -9
- package/dist/terraform/modules/app/lambda-api/variables.tf +6 -0
- package/dist/terraform/modules/app/static-site/main.tf +35 -4
- package/dist/terraform/modules/data/s3-buckets/main.tf +9 -3
- package/package.json +1 -1
- package/dist/terraform/modules/app/lambda-api/.build/placeholder.zip +0 -0
package/dist/cli.js
CHANGED
|
@@ -1372,6 +1372,36 @@ function discoverAwsStages(region) {
|
|
|
1372
1372
|
}
|
|
1373
1373
|
return stages;
|
|
1374
1374
|
}
|
|
1375
|
+
function printStageDetail(info) {
|
|
1376
|
+
console.log(chalk6.bold.cyan(` \u2B21 ${info.stage}`));
|
|
1377
|
+
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1378
|
+
console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
|
|
1379
|
+
console.log(` ${chalk6.bold("Region:")} ${info.region}`);
|
|
1380
|
+
console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
|
|
1381
|
+
console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
|
|
1382
|
+
console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
|
|
1383
|
+
console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
|
|
1384
|
+
if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
|
|
1385
|
+
if (info.bucketName) console.log(` ${chalk6.bold("S3 bucket:")} ${info.bucketName}`);
|
|
1386
|
+
if (info.dbEndpoint) console.log(` ${chalk6.bold("Database:")} ${info.dbEndpoint}`);
|
|
1387
|
+
if (info.ecrUrl) console.log(` ${chalk6.bold("ECR:")} ${info.ecrUrl}`);
|
|
1388
|
+
console.log("");
|
|
1389
|
+
console.log(chalk6.bold(" URLs:"));
|
|
1390
|
+
if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
|
|
1391
|
+
if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
|
|
1392
|
+
if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
|
|
1393
|
+
if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
|
|
1394
|
+
if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
|
|
1395
|
+
if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
|
|
1396
|
+
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1397
|
+
const local = loadEnvironment(info.stage);
|
|
1398
|
+
if (local) {
|
|
1399
|
+
console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
|
|
1400
|
+
} else {
|
|
1401
|
+
console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
|
|
1402
|
+
}
|
|
1403
|
+
console.log("");
|
|
1404
|
+
}
|
|
1375
1405
|
function registerStatusCommand(program2) {
|
|
1376
1406
|
program2.command("status").description("Show all Thinkwork environments (AWS + local)").option("-s, --stage <name>", "Show details for a specific stage").option("--region <region>", "AWS region to scan", "us-east-1").action(async (opts) => {
|
|
1377
1407
|
const identity = getAwsIdentity();
|
|
@@ -1410,34 +1440,7 @@ function registerStatusCommand(program2) {
|
|
|
1410
1440
|
printError(`No environment "${opts.stage}" found in AWS or local config.`);
|
|
1411
1441
|
process.exit(1);
|
|
1412
1442
|
}
|
|
1413
|
-
|
|
1414
|
-
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1415
|
-
console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
|
|
1416
|
-
console.log(` ${chalk6.bold("Region:")} ${info.region}`);
|
|
1417
|
-
console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
|
|
1418
|
-
console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
|
|
1419
|
-
console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
|
|
1420
|
-
console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
|
|
1421
|
-
if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
|
|
1422
|
-
if (info.bucketName) console.log(` ${chalk6.bold("S3 bucket:")} ${info.bucketName}`);
|
|
1423
|
-
if (info.dbEndpoint) console.log(` ${chalk6.bold("Database:")} ${info.dbEndpoint}`);
|
|
1424
|
-
if (info.ecrUrl) console.log(` ${chalk6.bold("ECR:")} ${info.ecrUrl}`);
|
|
1425
|
-
console.log("");
|
|
1426
|
-
console.log(chalk6.bold(" URLs:"));
|
|
1427
|
-
if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
|
|
1428
|
-
if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
|
|
1429
|
-
if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
|
|
1430
|
-
if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
|
|
1431
|
-
if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
|
|
1432
|
-
if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
|
|
1433
|
-
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1434
|
-
const local = loadEnvironment(opts.stage);
|
|
1435
|
-
if (local) {
|
|
1436
|
-
console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
|
|
1437
|
-
} else {
|
|
1438
|
-
console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${opts.stage}`));
|
|
1439
|
-
}
|
|
1440
|
-
console.log("");
|
|
1443
|
+
printStageDetail(info);
|
|
1441
1444
|
return;
|
|
1442
1445
|
}
|
|
1443
1446
|
if (merged.size === 0) {
|
|
@@ -1446,45 +1449,9 @@ function registerStatusCommand(program2) {
|
|
|
1446
1449
|
console.log("");
|
|
1447
1450
|
return;
|
|
1448
1451
|
}
|
|
1449
|
-
const COL1 = 16;
|
|
1450
|
-
const COL2 = 10;
|
|
1451
|
-
const COL3 = 10;
|
|
1452
|
-
const COL4 = 14;
|
|
1453
|
-
const COL5 = 14;
|
|
1454
|
-
const pad = " ".repeat(2 + 2 + COL1 + COL2 + COL3 + COL4 + COL5);
|
|
1455
|
-
console.log(chalk6.bold(" Environments"));
|
|
1456
|
-
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1457
|
-
console.log(
|
|
1458
|
-
chalk6.dim(" ") + "Stage".padEnd(COL1) + "Source".padEnd(COL2) + "Lambdas".padEnd(COL3) + "AgentCore".padEnd(COL4) + "Memory".padEnd(COL5) + "URLs"
|
|
1459
|
-
);
|
|
1460
|
-
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1461
1452
|
for (const [, info] of [...merged].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
1462
|
-
|
|
1463
|
-
const acStatus = info.agentcoreStatus === "Active" ? chalk6.green("active") : info.agentcoreStatus === "not deployed" ? chalk6.dim("\u2014") : chalk6.yellow(info.agentcoreStatus || "\u2014");
|
|
1464
|
-
const memBadge = info.memoryEngine === "hindsight" ? info.hindsightHealth === "healthy" ? chalk6.magenta("hindsight \u2713") : chalk6.yellow("hindsight ?") : chalk6.dim(info.memoryEngine || "\u2014");
|
|
1465
|
-
const prefix = ` ${sourceBadge} ` + chalk6.bold(info.stage.padEnd(COL1 - 1)) + " " + (info.source === "both" ? "aws+cli" : info.source).padEnd(COL2) + String(info.lambdaCount || "\u2014").padEnd(COL3);
|
|
1466
|
-
const urls = [];
|
|
1467
|
-
if (info.adminUrl) urls.push(`Admin: ${link(info.adminUrl, info.adminUrl)}`);
|
|
1468
|
-
if (info.docsUrl) urls.push(`Docs: ${link(info.docsUrl, info.docsUrl)}`);
|
|
1469
|
-
if (info.apiEndpoint) urls.push(`API: ${link(info.apiEndpoint, info.apiEndpoint)}`);
|
|
1470
|
-
if (info.appsyncUrl) urls.push(`WS: ${link(info.appsyncUrl, info.appsyncUrl.replace("wss://", "").split(".")[0] + "...")}`);
|
|
1471
|
-
if (info.hindsightEndpoint) urls.push(`Mem: ${link(info.hindsightEndpoint, info.hindsightEndpoint)}`);
|
|
1472
|
-
if (urls.length === 0) {
|
|
1473
|
-
console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim("\u2014"));
|
|
1474
|
-
} else {
|
|
1475
|
-
console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim(urls[0]));
|
|
1476
|
-
for (let i = 1; i < urls.length; i++) {
|
|
1477
|
-
console.log(pad + chalk6.dim(urls[i]));
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1453
|
+
printStageDetail(info);
|
|
1480
1454
|
}
|
|
1481
|
-
console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1482
|
-
console.log(
|
|
1483
|
-
chalk6.dim(` ${merged.size} environment(s) `) + chalk6.green("\u25CF") + chalk6.dim(" aws+cli ") + chalk6.yellow("\u25CF") + chalk6.dim(" aws only ") + chalk6.dim("\u25CB local only")
|
|
1484
|
-
);
|
|
1485
|
-
console.log("");
|
|
1486
|
-
console.log(` Details: ${chalk6.cyan("thinkwork status -s <stage>")}`);
|
|
1487
|
-
console.log("");
|
|
1488
1455
|
});
|
|
1489
1456
|
}
|
|
1490
1457
|
|
|
@@ -1519,12 +1486,13 @@ function getApiEndpoint(stage, region) {
|
|
|
1519
1486
|
return null;
|
|
1520
1487
|
}
|
|
1521
1488
|
}
|
|
1522
|
-
async function apiFetch(apiUrl, authSecret, path2, options = {}) {
|
|
1489
|
+
async function apiFetch(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
|
|
1523
1490
|
const res = await fetch(`${apiUrl}${path2}`, {
|
|
1524
1491
|
...options,
|
|
1525
1492
|
headers: {
|
|
1526
1493
|
"Content-Type": "application/json",
|
|
1527
1494
|
Authorization: `Bearer ${authSecret}`,
|
|
1495
|
+
...extraHeaders,
|
|
1528
1496
|
...options.headers
|
|
1529
1497
|
}
|
|
1530
1498
|
});
|
|
@@ -1550,8 +1518,8 @@ function resolveApiConfig(stage) {
|
|
|
1550
1518
|
return { apiUrl, authSecret };
|
|
1551
1519
|
}
|
|
1552
1520
|
function registerMcpCommand(program2) {
|
|
1553
|
-
const mcp = program2.command("mcp").description("Manage MCP servers for
|
|
1554
|
-
mcp.command("list").description("List MCP servers
|
|
1521
|
+
const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
|
|
1522
|
+
mcp.command("list").description("List registered MCP servers").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
|
|
1555
1523
|
const check = validateStage(opts.stage);
|
|
1556
1524
|
if (!check.valid) {
|
|
1557
1525
|
printError(check.error);
|
|
@@ -1561,20 +1529,21 @@ function registerMcpCommand(program2) {
|
|
|
1561
1529
|
if (!api) process.exit(1);
|
|
1562
1530
|
printHeader("mcp list", opts.stage);
|
|
1563
1531
|
try {
|
|
1564
|
-
const { servers } = await apiFetch(api.apiUrl, api.authSecret,
|
|
1532
|
+
const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
|
|
1565
1533
|
if (!servers || servers.length === 0) {
|
|
1566
|
-
console.log(chalk7.dim(" No MCP servers registered
|
|
1534
|
+
console.log(chalk7.dim(" No MCP servers registered."));
|
|
1567
1535
|
return;
|
|
1568
1536
|
}
|
|
1569
1537
|
console.log("");
|
|
1570
1538
|
for (const s of servers) {
|
|
1571
1539
|
const status = s.enabled ? chalk7.green("enabled") : chalk7.dim("disabled");
|
|
1572
|
-
|
|
1540
|
+
const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
|
|
1541
|
+
console.log(` ${chalk7.bold(s.name)} ${chalk7.dim(s.slug)} ${status}`);
|
|
1573
1542
|
console.log(` URL: ${s.url}`);
|
|
1574
1543
|
console.log(` Transport: ${s.transport}`);
|
|
1575
|
-
console.log(` Auth: ${
|
|
1544
|
+
console.log(` Auth: ${authLabel}`);
|
|
1576
1545
|
if (s.tools?.length) {
|
|
1577
|
-
console.log(` Tools: ${s.tools.
|
|
1546
|
+
console.log(` Tools: ${s.tools.length} cached`);
|
|
1578
1547
|
}
|
|
1579
1548
|
console.log("");
|
|
1580
1549
|
}
|
|
@@ -1583,7 +1552,7 @@ function registerMcpCommand(program2) {
|
|
|
1583
1552
|
process.exit(1);
|
|
1584
1553
|
}
|
|
1585
1554
|
});
|
|
1586
|
-
mcp.command("add <name>").description("Register an MCP server
|
|
1555
|
+
mcp.command("add <name>").description("Register an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").requiredOption("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|tenant_api_key|per_user_oauth)", "none").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").action(async (name, opts) => {
|
|
1587
1556
|
const check = validateStage(opts.stage);
|
|
1588
1557
|
if (!check.valid) {
|
|
1589
1558
|
printError(check.error);
|
|
@@ -1591,29 +1560,26 @@ function registerMcpCommand(program2) {
|
|
|
1591
1560
|
}
|
|
1592
1561
|
const api = resolveApiConfig(opts.stage);
|
|
1593
1562
|
if (!api) process.exit(1);
|
|
1594
|
-
printHeader("mcp add", opts.stage);
|
|
1595
1563
|
const body = {
|
|
1596
1564
|
name,
|
|
1597
1565
|
url: opts.url,
|
|
1598
|
-
transport: opts.transport
|
|
1599
|
-
authType: opts.authType !== "none" ? opts.authType : void 0
|
|
1566
|
+
transport: opts.transport
|
|
1600
1567
|
};
|
|
1601
|
-
if (opts.
|
|
1602
|
-
if (opts.
|
|
1603
|
-
if (opts.
|
|
1604
|
-
if (opts.tools) body.tools = opts.tools.split(",").map((t) => t.trim());
|
|
1568
|
+
if (opts.authType !== "none") body.authType = opts.authType;
|
|
1569
|
+
if (opts.apiKey) body.apiKey = opts.apiKey;
|
|
1570
|
+
if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
|
|
1605
1571
|
try {
|
|
1606
|
-
const result = await apiFetch(api.apiUrl, api.authSecret,
|
|
1572
|
+
const result = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {
|
|
1607
1573
|
method: "POST",
|
|
1608
1574
|
body: JSON.stringify(body)
|
|
1609
|
-
});
|
|
1610
|
-
printSuccess(`MCP server "${name}" ${result.created ? "
|
|
1575
|
+
}, { "x-tenant-slug": opts.tenant });
|
|
1576
|
+
printSuccess(`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`);
|
|
1611
1577
|
} catch (err) {
|
|
1612
1578
|
printError(err.message);
|
|
1613
1579
|
process.exit(1);
|
|
1614
1580
|
}
|
|
1615
1581
|
});
|
|
1616
|
-
mcp.command("remove <
|
|
1582
|
+
mcp.command("remove <id>").description("Remove an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
|
|
1617
1583
|
const check = validateStage(opts.stage);
|
|
1618
1584
|
if (!check.valid) {
|
|
1619
1585
|
printError(check.error);
|
|
@@ -1622,16 +1588,16 @@ function registerMcpCommand(program2) {
|
|
|
1622
1588
|
const api = resolveApiConfig(opts.stage);
|
|
1623
1589
|
if (!api) process.exit(1);
|
|
1624
1590
|
try {
|
|
1625
|
-
await apiFetch(api.apiUrl, api.authSecret, `/api/skills/
|
|
1591
|
+
await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}`, {
|
|
1626
1592
|
method: "DELETE"
|
|
1627
|
-
});
|
|
1628
|
-
printSuccess(`MCP server
|
|
1593
|
+
}, { "x-tenant-slug": opts.tenant });
|
|
1594
|
+
printSuccess(`MCP server removed.`);
|
|
1629
1595
|
} catch (err) {
|
|
1630
1596
|
printError(err.message);
|
|
1631
1597
|
process.exit(1);
|
|
1632
1598
|
}
|
|
1633
1599
|
});
|
|
1634
|
-
mcp.command("test <
|
|
1600
|
+
mcp.command("test <id>").description("Test connection and discover tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
|
|
1635
1601
|
const check = validateStage(opts.stage);
|
|
1636
1602
|
if (!check.valid) {
|
|
1637
1603
|
printError(check.error);
|
|
@@ -1641,17 +1607,17 @@ function registerMcpCommand(program2) {
|
|
|
1641
1607
|
if (!api) process.exit(1);
|
|
1642
1608
|
printHeader("mcp test", opts.stage);
|
|
1643
1609
|
try {
|
|
1644
|
-
const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/
|
|
1610
|
+
const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}/test`, {
|
|
1645
1611
|
method: "POST"
|
|
1646
|
-
});
|
|
1612
|
+
}, { "x-tenant-slug": opts.tenant });
|
|
1647
1613
|
if (result.ok) {
|
|
1648
|
-
printSuccess(
|
|
1614
|
+
printSuccess("Connection successful.");
|
|
1649
1615
|
if (result.tools?.length) {
|
|
1650
1616
|
console.log(chalk7.bold(`
|
|
1651
|
-
|
|
1617
|
+
Discovered tools (${result.tools.length}):
|
|
1652
1618
|
`));
|
|
1653
1619
|
for (const t of result.tools) {
|
|
1654
|
-
console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(`
|
|
1620
|
+
console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(` - ${t.description}`) : ""}`);
|
|
1655
1621
|
}
|
|
1656
1622
|
console.log("");
|
|
1657
1623
|
} else {
|
|
@@ -1666,6 +1632,43 @@ function registerMcpCommand(program2) {
|
|
|
1666
1632
|
process.exit(1);
|
|
1667
1633
|
}
|
|
1668
1634
|
});
|
|
1635
|
+
mcp.command("assign <mcpServerId>").description("Assign an MCP server to an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (mcpServerId, opts) => {
|
|
1636
|
+
const check = validateStage(opts.stage);
|
|
1637
|
+
if (!check.valid) {
|
|
1638
|
+
printError(check.error);
|
|
1639
|
+
process.exit(1);
|
|
1640
|
+
}
|
|
1641
|
+
const api = resolveApiConfig(opts.stage);
|
|
1642
|
+
if (!api) process.exit(1);
|
|
1643
|
+
try {
|
|
1644
|
+
const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers`, {
|
|
1645
|
+
method: "POST",
|
|
1646
|
+
body: JSON.stringify({ mcpServerId })
|
|
1647
|
+
});
|
|
1648
|
+
printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
|
|
1649
|
+
} catch (err) {
|
|
1650
|
+
printError(err.message);
|
|
1651
|
+
process.exit(1);
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
mcp.command("unassign <mcpServerId>").description("Remove an MCP server from an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (mcpServerId, opts) => {
|
|
1655
|
+
const check = validateStage(opts.stage);
|
|
1656
|
+
if (!check.valid) {
|
|
1657
|
+
printError(check.error);
|
|
1658
|
+
process.exit(1);
|
|
1659
|
+
}
|
|
1660
|
+
const api = resolveApiConfig(opts.stage);
|
|
1661
|
+
if (!api) process.exit(1);
|
|
1662
|
+
try {
|
|
1663
|
+
await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers/${mcpServerId}`, {
|
|
1664
|
+
method: "DELETE"
|
|
1665
|
+
});
|
|
1666
|
+
printSuccess("MCP server unassigned from agent.");
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
printError(err.message);
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1669
1672
|
}
|
|
1670
1673
|
|
|
1671
1674
|
// src/cli.ts
|
|
@@ -92,7 +92,7 @@ resource "aws_iam_role" "agentcore" {
|
|
|
92
92
|
Version = "2012-10-17"
|
|
93
93
|
Statement = [{
|
|
94
94
|
Effect = "Allow"
|
|
95
|
-
Principal = { Service = ["ecs-tasks.amazonaws.com", "lambda.amazonaws.com"] }
|
|
95
|
+
Principal = { Service = ["ecs-tasks.amazonaws.com", "lambda.amazonaws.com", "bedrock-agentcore.amazonaws.com"] }
|
|
96
96
|
Action = "sts:AssumeRole"
|
|
97
97
|
}]
|
|
98
98
|
})
|
|
@@ -118,18 +118,24 @@ resource "aws_iam_role_policy" "agentcore" {
|
|
|
118
118
|
Sid = "BedrockInvoke"
|
|
119
119
|
Effect = "Allow"
|
|
120
120
|
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:InvokeAgent"]
|
|
121
|
-
Resource = "
|
|
121
|
+
Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
Sid = "CloudWatchLogs"
|
|
125
125
|
Effect = "Allow"
|
|
126
126
|
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
|
127
|
-
Resource = "arn:aws:logs:${var.region}:${var.account_id}
|
|
127
|
+
Resource = "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/lambda/thinkwork-${var.stage}-*"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
Sid = "ECRPull"
|
|
131
131
|
Effect = "Allow"
|
|
132
|
-
Action = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"
|
|
132
|
+
Action = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"]
|
|
133
|
+
Resource = "arn:aws:ecr:${var.region}:${var.account_id}:repository/thinkwork-${var.stage}-*"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
Sid = "ECRAuth"
|
|
137
|
+
Effect = "Allow"
|
|
138
|
+
Action = ["ecr:GetAuthorizationToken"]
|
|
133
139
|
Resource = "*"
|
|
134
140
|
},
|
|
135
141
|
{
|
|
@@ -27,9 +27,9 @@ resource "aws_apigatewayv2_api" "main" {
|
|
|
27
27
|
protocol_type = "HTTP"
|
|
28
28
|
|
|
29
29
|
cors_configuration {
|
|
30
|
-
allow_headers = ["
|
|
31
|
-
allow_methods = ["
|
|
32
|
-
allow_origins =
|
|
30
|
+
allow_headers = ["Content-Type", "Authorization", "x-api-key", "x-tenant-id", "x-tenant-slug", "x-principal-id"]
|
|
31
|
+
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
32
|
+
allow_origins = var.cors_allowed_origins
|
|
33
33
|
max_age = 3600
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -118,11 +118,22 @@ resource "aws_iam_role_policy" "lambda_secrets" {
|
|
|
118
118
|
|
|
119
119
|
policy = jsonencode({
|
|
120
120
|
Version = "2012-10-17"
|
|
121
|
-
Statement = [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
Statement = [
|
|
122
|
+
{
|
|
123
|
+
Effect = "Allow"
|
|
124
|
+
Action = ["secretsmanager:GetSecretValue"]
|
|
125
|
+
Resource = var.graphql_db_secret_arn
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
Effect = "Allow"
|
|
129
|
+
Action = [
|
|
130
|
+
"secretsmanager:CreateSecret",
|
|
131
|
+
"secretsmanager:UpdateSecret",
|
|
132
|
+
"secretsmanager:GetSecretValue"
|
|
133
|
+
]
|
|
134
|
+
Resource = "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:thinkwork/*"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
126
137
|
})
|
|
127
138
|
}
|
|
128
139
|
|
|
@@ -175,7 +186,7 @@ resource "aws_iam_role_policy" "lambda_bedrock" {
|
|
|
175
186
|
Statement = [{
|
|
176
187
|
Effect = "Allow"
|
|
177
188
|
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
|
|
178
|
-
Resource = "
|
|
189
|
+
Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
|
|
179
190
|
}]
|
|
180
191
|
})
|
|
181
192
|
}
|
|
@@ -151,3 +151,9 @@ variable "agentcore_function_name" {
|
|
|
151
151
|
type = string
|
|
152
152
|
default = ""
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
variable "cors_allowed_origins" {
|
|
156
|
+
description = "Allowed CORS origins for the API Gateway. Use [\"*\"] for development."
|
|
157
|
+
type = list(string)
|
|
158
|
+
default = ["*"]
|
|
159
|
+
}
|
|
@@ -69,6 +69,31 @@ resource "aws_cloudfront_origin_access_control" "site" {
|
|
|
69
69
|
signing_protocol = "sigv4"
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
################################################################################
|
|
73
|
+
# CloudFront Function — rewrite directory URIs to index.html
|
|
74
|
+
#
|
|
75
|
+
# S3 with OAC doesn't auto-serve index.html for subdirectory requests.
|
|
76
|
+
# /getting-started/ → /getting-started/index.html
|
|
77
|
+
################################################################################
|
|
78
|
+
|
|
79
|
+
resource "aws_cloudfront_function" "rewrite" {
|
|
80
|
+
name = "thinkwork-${var.stage}-${var.site_name}-rewrite"
|
|
81
|
+
runtime = "cloudfront-js-2.0"
|
|
82
|
+
publish = true
|
|
83
|
+
code = <<-EOF
|
|
84
|
+
function handler(event) {
|
|
85
|
+
var request = event.request;
|
|
86
|
+
var uri = request.uri;
|
|
87
|
+
if (uri.endsWith('/')) {
|
|
88
|
+
request.uri += 'index.html';
|
|
89
|
+
} else if (!uri.includes('.')) {
|
|
90
|
+
request.uri += '/index.html';
|
|
91
|
+
}
|
|
92
|
+
return request;
|
|
93
|
+
}
|
|
94
|
+
EOF
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
################################################################################
|
|
73
98
|
# CloudFront Distribution
|
|
74
99
|
################################################################################
|
|
@@ -92,6 +117,11 @@ resource "aws_cloudfront_distribution" "site" {
|
|
|
92
117
|
cached_methods = ["GET", "HEAD"]
|
|
93
118
|
compress = true
|
|
94
119
|
|
|
120
|
+
function_association {
|
|
121
|
+
event_type = "viewer-request"
|
|
122
|
+
function_arn = aws_cloudfront_function.rewrite.arn
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
forwarded_values {
|
|
96
126
|
query_string = false
|
|
97
127
|
cookies {
|
|
@@ -100,16 +130,17 @@ resource "aws_cloudfront_distribution" "site" {
|
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
# Fallback for true 404s (e.g. deleted pages) — serve the 404 page
|
|
103
134
|
custom_error_response {
|
|
104
135
|
error_code = 404
|
|
105
|
-
response_code =
|
|
106
|
-
response_page_path = "/
|
|
136
|
+
response_code = 404
|
|
137
|
+
response_page_path = "/404.html"
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
custom_error_response {
|
|
110
141
|
error_code = 403
|
|
111
|
-
response_code =
|
|
112
|
-
response_page_path = "/
|
|
142
|
+
response_code = 404
|
|
143
|
+
response_page_path = "/404.html"
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
restrictions {
|
|
@@ -20,6 +20,12 @@ variable "bucket_name" {
|
|
|
20
20
|
type = string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
variable "cors_allowed_origins" {
|
|
24
|
+
description = "Allowed CORS origins. Use [\"*\"] for development."
|
|
25
|
+
type = list(string)
|
|
26
|
+
default = ["*"]
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
resource "aws_s3_bucket" "main" {
|
|
24
30
|
bucket = var.bucket_name
|
|
25
31
|
|
|
@@ -33,9 +39,9 @@ resource "aws_s3_bucket_cors_configuration" "main" {
|
|
|
33
39
|
bucket = aws_s3_bucket.main.id
|
|
34
40
|
|
|
35
41
|
cors_rule {
|
|
36
|
-
allowed_headers = ["
|
|
37
|
-
allowed_methods = ["GET", "PUT", "
|
|
38
|
-
allowed_origins =
|
|
42
|
+
allowed_headers = ["Content-Type", "Authorization", "x-amz-*"]
|
|
43
|
+
allowed_methods = ["GET", "PUT", "HEAD"]
|
|
44
|
+
allowed_origins = var.cors_allowed_origins
|
|
39
45
|
expose_headers = ["ETag"]
|
|
40
46
|
max_age_seconds = 3000
|
|
41
47
|
}
|
package/package.json
CHANGED
|
Binary file
|