thinkwork-cli 0.4.1 → 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 +65 -29
- 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
|
@@ -1486,12 +1486,13 @@ function getApiEndpoint(stage, region) {
|
|
|
1486
1486
|
return null;
|
|
1487
1487
|
}
|
|
1488
1488
|
}
|
|
1489
|
-
async function apiFetch(apiUrl, authSecret, path2, options = {}) {
|
|
1489
|
+
async function apiFetch(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
|
|
1490
1490
|
const res = await fetch(`${apiUrl}${path2}`, {
|
|
1491
1491
|
...options,
|
|
1492
1492
|
headers: {
|
|
1493
1493
|
"Content-Type": "application/json",
|
|
1494
1494
|
Authorization: `Bearer ${authSecret}`,
|
|
1495
|
+
...extraHeaders,
|
|
1495
1496
|
...options.headers
|
|
1496
1497
|
}
|
|
1497
1498
|
});
|
|
@@ -1517,8 +1518,8 @@ function resolveApiConfig(stage) {
|
|
|
1517
1518
|
return { apiUrl, authSecret };
|
|
1518
1519
|
}
|
|
1519
1520
|
function registerMcpCommand(program2) {
|
|
1520
|
-
const mcp = program2.command("mcp").description("Manage MCP servers for
|
|
1521
|
-
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) => {
|
|
1522
1523
|
const check = validateStage(opts.stage);
|
|
1523
1524
|
if (!check.valid) {
|
|
1524
1525
|
printError(check.error);
|
|
@@ -1528,20 +1529,21 @@ function registerMcpCommand(program2) {
|
|
|
1528
1529
|
if (!api) process.exit(1);
|
|
1529
1530
|
printHeader("mcp list", opts.stage);
|
|
1530
1531
|
try {
|
|
1531
|
-
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 });
|
|
1532
1533
|
if (!servers || servers.length === 0) {
|
|
1533
|
-
console.log(chalk7.dim(" No MCP servers registered
|
|
1534
|
+
console.log(chalk7.dim(" No MCP servers registered."));
|
|
1534
1535
|
return;
|
|
1535
1536
|
}
|
|
1536
1537
|
console.log("");
|
|
1537
1538
|
for (const s of servers) {
|
|
1538
1539
|
const status = s.enabled ? chalk7.green("enabled") : chalk7.dim("disabled");
|
|
1539
|
-
|
|
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}`);
|
|
1540
1542
|
console.log(` URL: ${s.url}`);
|
|
1541
1543
|
console.log(` Transport: ${s.transport}`);
|
|
1542
|
-
console.log(` Auth: ${
|
|
1544
|
+
console.log(` Auth: ${authLabel}`);
|
|
1543
1545
|
if (s.tools?.length) {
|
|
1544
|
-
console.log(` Tools: ${s.tools.
|
|
1546
|
+
console.log(` Tools: ${s.tools.length} cached`);
|
|
1545
1547
|
}
|
|
1546
1548
|
console.log("");
|
|
1547
1549
|
}
|
|
@@ -1550,7 +1552,7 @@ function registerMcpCommand(program2) {
|
|
|
1550
1552
|
process.exit(1);
|
|
1551
1553
|
}
|
|
1552
1554
|
});
|
|
1553
|
-
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) => {
|
|
1554
1556
|
const check = validateStage(opts.stage);
|
|
1555
1557
|
if (!check.valid) {
|
|
1556
1558
|
printError(check.error);
|
|
@@ -1558,29 +1560,26 @@ function registerMcpCommand(program2) {
|
|
|
1558
1560
|
}
|
|
1559
1561
|
const api = resolveApiConfig(opts.stage);
|
|
1560
1562
|
if (!api) process.exit(1);
|
|
1561
|
-
printHeader("mcp add", opts.stage);
|
|
1562
1563
|
const body = {
|
|
1563
1564
|
name,
|
|
1564
1565
|
url: opts.url,
|
|
1565
|
-
transport: opts.transport
|
|
1566
|
-
authType: opts.authType !== "none" ? opts.authType : void 0
|
|
1566
|
+
transport: opts.transport
|
|
1567
1567
|
};
|
|
1568
|
-
if (opts.
|
|
1569
|
-
if (opts.
|
|
1570
|
-
if (opts.
|
|
1571
|
-
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;
|
|
1572
1571
|
try {
|
|
1573
|
-
const result = await apiFetch(api.apiUrl, api.authSecret,
|
|
1572
|
+
const result = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {
|
|
1574
1573
|
method: "POST",
|
|
1575
1574
|
body: JSON.stringify(body)
|
|
1576
|
-
});
|
|
1577
|
-
printSuccess(`MCP server "${name}" ${result.created ? "
|
|
1575
|
+
}, { "x-tenant-slug": opts.tenant });
|
|
1576
|
+
printSuccess(`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`);
|
|
1578
1577
|
} catch (err) {
|
|
1579
1578
|
printError(err.message);
|
|
1580
1579
|
process.exit(1);
|
|
1581
1580
|
}
|
|
1582
1581
|
});
|
|
1583
|
-
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) => {
|
|
1584
1583
|
const check = validateStage(opts.stage);
|
|
1585
1584
|
if (!check.valid) {
|
|
1586
1585
|
printError(check.error);
|
|
@@ -1589,16 +1588,16 @@ function registerMcpCommand(program2) {
|
|
|
1589
1588
|
const api = resolveApiConfig(opts.stage);
|
|
1590
1589
|
if (!api) process.exit(1);
|
|
1591
1590
|
try {
|
|
1592
|
-
await apiFetch(api.apiUrl, api.authSecret, `/api/skills/
|
|
1591
|
+
await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}`, {
|
|
1593
1592
|
method: "DELETE"
|
|
1594
|
-
});
|
|
1595
|
-
printSuccess(`MCP server
|
|
1593
|
+
}, { "x-tenant-slug": opts.tenant });
|
|
1594
|
+
printSuccess(`MCP server removed.`);
|
|
1596
1595
|
} catch (err) {
|
|
1597
1596
|
printError(err.message);
|
|
1598
1597
|
process.exit(1);
|
|
1599
1598
|
}
|
|
1600
1599
|
});
|
|
1601
|
-
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) => {
|
|
1602
1601
|
const check = validateStage(opts.stage);
|
|
1603
1602
|
if (!check.valid) {
|
|
1604
1603
|
printError(check.error);
|
|
@@ -1608,17 +1607,17 @@ function registerMcpCommand(program2) {
|
|
|
1608
1607
|
if (!api) process.exit(1);
|
|
1609
1608
|
printHeader("mcp test", opts.stage);
|
|
1610
1609
|
try {
|
|
1611
|
-
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`, {
|
|
1612
1611
|
method: "POST"
|
|
1613
|
-
});
|
|
1612
|
+
}, { "x-tenant-slug": opts.tenant });
|
|
1614
1613
|
if (result.ok) {
|
|
1615
|
-
printSuccess(
|
|
1614
|
+
printSuccess("Connection successful.");
|
|
1616
1615
|
if (result.tools?.length) {
|
|
1617
1616
|
console.log(chalk7.bold(`
|
|
1618
|
-
|
|
1617
|
+
Discovered tools (${result.tools.length}):
|
|
1619
1618
|
`));
|
|
1620
1619
|
for (const t of result.tools) {
|
|
1621
|
-
console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(`
|
|
1620
|
+
console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(` - ${t.description}`) : ""}`);
|
|
1622
1621
|
}
|
|
1623
1622
|
console.log("");
|
|
1624
1623
|
} else {
|
|
@@ -1633,6 +1632,43 @@ function registerMcpCommand(program2) {
|
|
|
1633
1632
|
process.exit(1);
|
|
1634
1633
|
}
|
|
1635
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
|
+
});
|
|
1636
1672
|
}
|
|
1637
1673
|
|
|
1638
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
|