@aws/nx-plugin 1.0.0-rc.17 → 1.0.0-rc.19

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.
Files changed (110) hide show
  1. package/generators.json +9 -1
  2. package/package.json +1 -1
  3. package/src/agentcore-gateway/attach-upstream.d.ts +33 -0
  4. package/src/agentcore-gateway/attach-upstream.js +41 -0
  5. package/src/agentcore-gateway/attach-upstream.js.map +1 -0
  6. package/src/agentcore-gateway/files/project/serve-local.ts.template +3 -2
  7. package/src/agentcore-gateway/gateway-connection/generator.d.ts +21 -0
  8. package/src/agentcore-gateway/gateway-connection/generator.js +102 -0
  9. package/src/agentcore-gateway/gateway-connection/generator.js.map +1 -0
  10. package/src/agentcore-gateway/gateway-connection/schema.d.ts +12 -0
  11. package/src/agentcore-gateway/gateway-connection/schema.json +26 -0
  12. package/src/agentcore-gateway/mcp-connection/generator.js +8 -24
  13. package/src/agentcore-gateway/mcp-connection/generator.js.map +1 -1
  14. package/src/connection/generator.js +66 -63
  15. package/src/connection/generator.js.map +1 -1
  16. package/src/license/generator.js +8 -19
  17. package/src/license/generator.js.map +1 -1
  18. package/src/license/known-exceptions.d.ts +0 -1
  19. package/src/license/known-exceptions.js +1 -8
  20. package/src/license/known-exceptions.js.map +1 -1
  21. package/src/py/agent/__snapshots__/generator.spec.ts.snap +15 -6
  22. package/src/py/agent/a2a-connection/__snapshots__/generator.spec.ts.snap +201 -0
  23. package/src/py/agent/a2a-connection/files/agent-connection/app/{__targetAgentSnakeCase___client.py.template → __targetAgentSnakeCase___client_strands.py.template} +7 -5
  24. package/src/py/agent/a2a-connection/generator.js +6 -5
  25. package/src/py/agent/a2a-connection/generator.js.map +1 -1
  26. package/src/py/agent/files/http/main.py.template +2 -2
  27. package/src/py/agent/gateway-connection/__snapshots__/generator.spec.ts.snap +197 -0
  28. package/src/py/agent/gateway-connection/files/agent-connection/app/{__gatewaySnakeCase___client.py.template → __gatewaySnakeCase___client_strands.py.template} +6 -6
  29. package/src/py/agent/gateway-connection/generator.js +5 -5
  30. package/src/py/agent/gateway-connection/generator.js.map +1 -1
  31. package/src/py/agent/generator.js +3 -5
  32. package/src/py/agent/generator.js.map +1 -1
  33. package/src/py/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +157 -79
  34. package/src/py/agent/mcp-connection/files/agent-connection/app/{__mcpServerSnakeCase___client.py.template → __mcpServerSnakeCase___client_strands.py.template} +7 -5
  35. package/src/py/agent/mcp-connection/generator.js +5 -5
  36. package/src/py/agent/mcp-connection/generator.js.map +1 -1
  37. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +37 -3
  38. package/src/py/fast-api/files/app/__name__/main.py.template +4 -1
  39. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +13 -4
  40. package/src/smithy/project/__snapshots__/generator.spec.ts.snap +1 -0
  41. package/src/smithy/project/files/src/operations/echo.smithy.template +1 -0
  42. package/src/terraform/project/files/application/src/main.tf.template +6 -1
  43. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +35 -4
  44. package/src/trpc/backend/files/src/procedures/echo.ts.template +1 -1
  45. package/src/trpc/backend/files/src/schema/echo.ts.template +2 -2
  46. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +14 -5
  47. package/src/ts/agent/a2a-connection/__snapshots__/generator.spec.ts.snap +256 -0
  48. package/src/ts/agent/a2a-connection/files/agent-connection/app/{__targetAgentKebabCase__-client.ts.template → __targetAgentKebabCase__-client-strands.ts.template} +5 -5
  49. package/src/ts/agent/a2a-connection/generator.js +3 -3
  50. package/src/ts/agent/a2a-connection/generator.js.map +1 -1
  51. package/src/ts/agent/files/http/router.ts.template +1 -1
  52. package/src/ts/agent/gateway-connection/__snapshots__/generator.spec.ts.snap +220 -0
  53. package/src/ts/agent/gateway-connection/files/agent-connection/app/{__gatewayKebabCase__-client.ts.template → __gatewayKebabCase__-client-strands.ts.template} +5 -5
  54. package/src/ts/agent/gateway-connection/generator.js +3 -3
  55. package/src/ts/agent/gateway-connection/generator.js.map +1 -1
  56. package/src/ts/agent/generator.js +3 -0
  57. package/src/ts/agent/generator.js.map +1 -1
  58. package/src/ts/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +183 -110
  59. package/src/ts/agent/mcp-connection/files/agent-connection/app/{__mcpServerKebabCase__-client.ts.template → __mcpServerKebabCase__-client-strands.ts.template} +5 -5
  60. package/src/ts/agent/mcp-connection/generator.js +3 -3
  61. package/src/ts/agent/mcp-connection/generator.js.map +1 -1
  62. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +13 -4
  63. package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +64 -24
  64. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +13 -4
  65. package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +9 -3
  66. package/src/utils/agent-connection/agent-connection.d.ts +50 -24
  67. package/src/utils/agent-connection/agent-connection.js +137 -45
  68. package/src/utils/agent-connection/agent-connection.js.map +1 -1
  69. package/src/utils/agent-connection/files/core-a2a/agentcore-a2a-client-config.ts.template +93 -0
  70. package/src/utils/agent-connection/files/core-auth/agentcore-endpoints.ts.template +37 -0
  71. package/src/utils/agent-connection/files/core-auth/agentcore-fetch.ts.template +54 -0
  72. package/src/utils/agent-connection/files/core-gateway/agentcore-gateway-mcp-transport.ts.template +41 -0
  73. package/src/utils/agent-connection/files/core-mcp/agentcore-mcp-transport.ts.template +70 -0
  74. package/src/utils/agent-connection/files/core-shared/agentcore-transport.ts.template +39 -0
  75. package/src/utils/agent-connection/files/core-strands/a2a/agentcore-a2a-client-strands.ts.template +61 -0
  76. package/src/utils/agent-connection/files/core-strands/gateway/agentcore-gateway-mcp-client-strands.ts.template +26 -0
  77. package/src/utils/agent-connection/files/core-strands/mcp/agentcore-mcp-client-strands.ts.template +27 -0
  78. package/src/utils/agent-connection/files/py-core-a2a/agentcore_a2a_client_config.py.template +43 -0
  79. package/src/utils/agent-connection/files/py-core-auth/agentcore_endpoints.py.template +47 -0
  80. package/src/utils/agent-connection/files/py-core-auth/auth/session.py.template +51 -0
  81. package/src/utils/agent-connection/files/py-core-gateway/agentcore_gateway_mcp_transport.py.template +28 -0
  82. package/src/utils/agent-connection/files/py-core-mcp/agentcore_mcp_transport.py.template +34 -0
  83. package/src/utils/agent-connection/files/py-core-shared/agentcore_transport.py.template +40 -0
  84. package/src/utils/agent-connection/files/py-core-strands/a2a/agentcore_a2a_client_strands.py.template +66 -0
  85. package/src/utils/agent-connection/files/py-core-strands/gateway/agentcore_gateway_mcp_client_strands.py.template +22 -0
  86. package/src/utils/agent-connection/files/py-core-strands/mcp/agentcore_mcp_client_strands.py.template +30 -0
  87. package/src/utils/agent-core-constructs/files/cdk/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.ts.template +3 -0
  88. package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/agentcore-gateway.ts.template +110 -28
  89. package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.tf.template +95 -2
  90. package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +13 -4
  91. package/src/utils/api-constructs/files/cdk/core/api/http/http-api.ts.template +9 -0
  92. package/src/utils/api-constructs/files/cdk/core/api/rest/rest-api.ts.template +23 -1
  93. package/src/utils/files/terraform/src/core/runtime-config/appconfig-deployment/appconfig-deployment.tf.template +9 -3
  94. package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +7 -2
  95. package/src/utils/identity-constructs/files/terraform/core/user-identity/add-callback-url/add-callback-url.tf.template +9 -3
  96. package/src/utils/rdb-constructs/files/terraform/app/dbs/__nameKebabCase__/__nameKebabCase__.tf.template +22 -8
  97. package/src/utils/rdb-constructs/files/terraform/core/rdb/aurora/aurora.tf.template +10 -4
  98. package/src/utils/versions.d.ts +1 -1
  99. package/src/utils/versions.js +1 -1
  100. package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +13 -4
  101. package/src/utils/agent-connection/files/core-a2a/agentcore-a2a-client.ts.template +0 -123
  102. package/src/utils/agent-connection/files/core-gateway/agentcore-gateway-mcp-client.ts.template +0 -78
  103. package/src/utils/agent-connection/files/core-mcp/agentcore-mcp-client.ts.template +0 -128
  104. package/src/utils/agent-connection/files/py-core-a2a/agentcore_a2a_client.py.template +0 -120
  105. package/src/utils/agent-connection/files/py-core-gateway/agentcore_gateway_mcp_client.py.template +0 -78
  106. package/src/utils/agent-connection/files/py-core-mcp/agentcore_mcp_client.py.template +0 -89
  107. /package/src/utils/agent-connection/files/{core-runtime-config/model-errors.ts.template → core-strands/base/model-errors-strands.ts.template} +0 -0
  108. /package/src/utils/agent-connection/files/{core-runtime-config/with-session-id.ts.template → core-strands/base/with-session-id-strands.ts.template} +0 -0
  109. /package/src/utils/agent-connection/files/{py-core-runtime-config/model_errors.py.template → py-core-strands/base/model_errors_strands.py.template} +0 -0
  110. /package/src/utils/agent-connection/files/{py-core-runtime-config/with_session_id.py.template → py-core-strands/base/with_session_id_strands.py.template} +0 -0
@@ -0,0 +1,34 @@
1
+ from collections.abc import Callable
2
+
3
+ from .agentcore_endpoints import mcp_url_from_arn, region_from_arn
4
+ from .agentcore_transport import (
5
+ TransportFactory,
6
+ jwt_transport,
7
+ no_auth_transport,
8
+ sigv4_transport,
9
+ )
10
+
11
+
12
+ class AgentCoreMCPTransport:
13
+ """Factory for MCP transport factories that connect to an AgentCore runtime."""
14
+
15
+ @staticmethod
16
+ def with_iam_auth(agent_runtime_arn: str) -> TransportFactory:
17
+ """SigV4-authenticated transport for a Bedrock AgentCore runtime."""
18
+ return sigv4_transport(
19
+ mcp_url_from_arn(agent_runtime_arn), region_from_arn(agent_runtime_arn)
20
+ )
21
+
22
+ @staticmethod
23
+ def with_jwt_auth(
24
+ agent_runtime_arn: str, access_token_provider: Callable[[], str]
25
+ ) -> TransportFactory:
26
+ """Bearer-authenticated transport for a Bedrock AgentCore runtime."""
27
+ return jwt_transport(
28
+ mcp_url_from_arn(agent_runtime_arn), access_token_provider
29
+ )
30
+
31
+ @staticmethod
32
+ def without_auth(url: str) -> TransportFactory:
33
+ """Plain-HTTP transport — for local dev."""
34
+ return no_auth_transport(url)
@@ -0,0 +1,40 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ import boto3
5
+
6
+ from mcp.client.streamable_http import streamablehttp_client
7
+
8
+ from .auth.session import jwt_auth, plain_auth, sigv4_auth
9
+
10
+ # Agent frameworks wrap a transport *factory* — a no-arg callable returning a
11
+ # streamable-HTTP transport context manager. Typed as ``Any`` so this layer
12
+ # stays free of any framework-specific transport type.
13
+ TransportFactory = Callable[[], Any]
14
+
15
+
16
+ def _factory(url: str, auth) -> TransportFactory:
17
+ return lambda: streamablehttp_client(
18
+ url,
19
+ auth=auth,
20
+ timeout=120,
21
+ terminate_on_close=False,
22
+ )
23
+
24
+
25
+ def sigv4_transport(url: str, region: str) -> TransportFactory:
26
+ """SigV4-signed transport factory for a resolved AgentCore endpoint."""
27
+ credentials = boto3.Session(region_name=region).get_credentials()
28
+ return _factory(url, sigv4_auth(credentials, region))
29
+
30
+
31
+ def jwt_transport(
32
+ url: str, access_token_provider: Callable[[], str]
33
+ ) -> TransportFactory:
34
+ """Bearer-token transport factory for a resolved AgentCore endpoint."""
35
+ return _factory(url, jwt_auth(access_token_provider))
36
+
37
+
38
+ def no_auth_transport(url: str) -> TransportFactory:
39
+ """Plain-HTTP transport factory — for local dev."""
40
+ return _factory(url, plain_auth())
@@ -0,0 +1,66 @@
1
+ from collections.abc import Callable
2
+
3
+ from strands.agent.a2a_agent import A2AAgent
4
+
5
+ from .agentcore_a2a_client_config import AgentCoreA2aClientConfig
6
+
7
+
8
+ def _build(
9
+ config: tuple, *, name: str | None, description: str | None
10
+ ) -> A2AAgent:
11
+ url, client_config = config
12
+ kwargs: dict = {"endpoint": url, "client_config": client_config}
13
+ if name:
14
+ kwargs["name"] = name
15
+ if description:
16
+ kwargs["description"] = description
17
+ return A2AAgent(**kwargs)
18
+
19
+
20
+ class AgentCoreA2aClientStrands:
21
+ """Factory for Strands A2A clients that connect to an AgentCore runtime."""
22
+
23
+ @staticmethod
24
+ def with_iam_auth(
25
+ agent_runtime_arn: str,
26
+ *,
27
+ name: str | None = None,
28
+ description: str | None = None,
29
+ ) -> A2AAgent:
30
+ """SigV4-authenticated client for a Bedrock AgentCore runtime."""
31
+ return _build(
32
+ AgentCoreA2aClientConfig.with_iam_auth(agent_runtime_arn),
33
+ name=name,
34
+ description=description,
35
+ )
36
+
37
+ @staticmethod
38
+ def with_jwt_auth(
39
+ agent_runtime_arn: str,
40
+ access_token_provider: Callable[[], str],
41
+ *,
42
+ name: str | None = None,
43
+ description: str | None = None,
44
+ ) -> A2AAgent:
45
+ """Bearer-authenticated client for a Bedrock AgentCore runtime."""
46
+ return _build(
47
+ AgentCoreA2aClientConfig.with_jwt_auth(
48
+ agent_runtime_arn, access_token_provider
49
+ ),
50
+ name=name,
51
+ description=description,
52
+ )
53
+
54
+ @staticmethod
55
+ def without_auth(
56
+ url: str,
57
+ *,
58
+ name: str | None = None,
59
+ description: str | None = None,
60
+ ) -> A2AAgent:
61
+ """Plain-HTTP client — for local dev."""
62
+ return _build(
63
+ AgentCoreA2aClientConfig.without_auth(url),
64
+ name=name,
65
+ description=description,
66
+ )
@@ -0,0 +1,22 @@
1
+ from strands.tools.mcp.mcp_client import MCPClient
2
+
3
+ from .agentcore_gateway_mcp_transport import AgentCoreGatewayMCPTransport
4
+
5
+
6
+ class AgentCoreGatewayMCPClientStrands:
7
+ """Factory for Strands MCP clients that connect to an AgentCore Gateway."""
8
+
9
+ @staticmethod
10
+ def with_iam_auth(
11
+ gateway_url: str,
12
+ region: str | None = None,
13
+ ) -> MCPClient:
14
+ """Create a gateway MCP client authenticated with IAM SigV4."""
15
+ return MCPClient(
16
+ AgentCoreGatewayMCPTransport.with_iam_auth(gateway_url, region)
17
+ )
18
+
19
+ @staticmethod
20
+ def without_auth(gateway_url: str) -> MCPClient:
21
+ """Plain-HTTP gateway client, for the local gateway started by serve-local."""
22
+ return MCPClient(AgentCoreGatewayMCPTransport.without_auth(gateway_url))
@@ -0,0 +1,30 @@
1
+ from collections.abc import Callable
2
+
3
+ from strands.tools.mcp.mcp_client import MCPClient
4
+
5
+ from .agentcore_mcp_transport import AgentCoreMCPTransport
6
+
7
+
8
+ class AgentCoreMCPClientStrands:
9
+ """Factory for Strands MCP clients that connect to an AgentCore runtime."""
10
+
11
+ @staticmethod
12
+ def with_iam_auth(agent_runtime_arn: str) -> MCPClient:
13
+ """SigV4-authenticated client for a Bedrock AgentCore runtime."""
14
+ return MCPClient(AgentCoreMCPTransport.with_iam_auth(agent_runtime_arn))
15
+
16
+ @staticmethod
17
+ def with_jwt_auth(
18
+ agent_runtime_arn: str, access_token_provider: Callable[[], str]
19
+ ) -> MCPClient:
20
+ """Bearer-authenticated client for a Bedrock AgentCore runtime."""
21
+ return MCPClient(
22
+ AgentCoreMCPTransport.with_jwt_auth(
23
+ agent_runtime_arn, access_token_provider
24
+ )
25
+ )
26
+
27
+ @staticmethod
28
+ def without_auth(url: str) -> MCPClient:
29
+ """Plain-HTTP client — for local dev."""
30
+ return MCPClient(AgentCoreMCPTransport.without_auth(url))
@@ -18,6 +18,9 @@ import { findWorkspaceRoot } from '../../../core/workspace.js';
18
18
  <%_ } _%>
19
19
  */
20
20
  export class <%- nameClassName %> extends AgentCoreGateway {
21
+ /** Default Gateway target name when added to another Gateway. */
22
+ public readonly gatewayName = '<%- nameKebabCase %>';
23
+
21
24
  constructor(scope: Construct, id: string) {
22
25
  super(scope, id, {
23
26
  <%_ if (cedarPolicy) { _%>
@@ -187,34 +187,7 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
187
187
  cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', arn))),
188
188
  );
189
189
  const endpoint = `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${encodedArn}/invocations?qualifier=DEFAULT`;
190
- // The construct id is derived from the target name so the two change
191
- // together — target names are unique per gateway, and replacing a target
192
- // under an unchanged name fails with AlreadyExists.
193
- const target = new agentcore.CfnGatewayTarget(
194
- this,
195
- `Target-${props.gatewayTargetName}`,
196
- {
197
- gatewayIdentifier: this.gateway.gatewayId,
198
- name: props.gatewayTargetName,
199
- targetConfiguration: {
200
- mcp: {
201
- mcpServer: {
202
- endpoint,
203
- },
204
- },
205
- },
206
- credentialProviderConfigurations: [
207
- {
208
- credentialProviderType: 'GATEWAY_IAM_ROLE',
209
- credentialProvider: {
210
- iamCredentialProvider: {
211
- service: 'bedrock-agentcore',
212
- },
213
- },
214
- },
215
- ],
216
- },
217
- );
190
+ const target = this.addTarget(props.gatewayTargetName, endpoint);
218
191
 
219
192
  // Grant the gateway role invoke access to the target runtime before the
220
193
  // target is created — AgentCore probes the target during creation, which
@@ -253,6 +226,115 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
253
226
  }
254
227
  target.node.addDependency(probe.trigger);
255
228
 
229
+ return target;
230
+ }
231
+
232
+ /**
233
+ * Add another Gateway as a target of this Gateway, exposing its tools
234
+ * under `<targetName>___<innerTargetName>___<toolName>` names.
235
+ *
236
+ * The target name defaults to the gateway construct's `gatewayName`
237
+ * (its project's class name in kebab-case, e.g. `MyGateway` ->
238
+ * `my-gateway`). It prefixes Cedar action names and the gateway's tool
239
+ * names, so renaming it later is a breaking change for consumers.
240
+ *
241
+ * Both gateways evaluate their own Cedar policies: this gateway
242
+ * authorizes the prefixed action, then the target gateway authorizes the
243
+ * inner action for this gateway's role.
244
+ */
245
+ public addGateway(
246
+ gateway: Construct & {
247
+ readonly gateway: agentcore.Gateway;
248
+ readonly gatewayName: string;
249
+ },
250
+ props?: {
251
+ gatewayTargetName?: string;
252
+ },
253
+ ): agentcore.CfnGatewayTarget {
254
+ const target = this.addGatewayTarget({
255
+ gatewayTargetName: props?.gatewayTargetName ?? gateway.gatewayName,
256
+ // The Gateway L2 only populates gatewayUrl when created from its own
257
+ // props; fall back to the CloudFormation attribute otherwise.
258
+ gatewayUrl:
259
+ gateway.gateway.gatewayUrl ??
260
+ (gateway.gateway.node.defaultChild as agentcore.CfnGateway)
261
+ .attrGatewayUrl,
262
+ gatewayArn: gateway.gateway.gatewayArn,
263
+ });
264
+ // AgentCore fetches the target gateway's tools during target creation,
265
+ // so the target gateway and all of its own targets must exist first.
266
+ // Construct dependencies resolve at synth time, covering targets added
267
+ // to the target gateway after this call.
268
+ target.node.addDependency(gateway);
269
+ return target;
270
+ }
271
+
272
+ /**
273
+ * Register another Gateway's MCP endpoint as a target of this Gateway.
274
+ * Prefer {@link addGateway} when the construct is in scope — it also
275
+ * orders this gateway's target after the target gateway's own targets,
276
+ * so its tools are registered before they are fetched.
277
+ */
278
+ public addGatewayTarget(props: {
279
+ gatewayTargetName: string;
280
+ gatewayUrl: string;
281
+ gatewayArn: string;
282
+ }): agentcore.CfnGatewayTarget {
283
+ const target = this.addTarget(props.gatewayTargetName, props.gatewayUrl);
284
+
285
+ // Grant the gateway role invoke access to the target gateway before the
286
+ // target is created — AgentCore fetches the target's tools during
287
+ // creation, which fails if the permission is not yet in place.
288
+ const grant = this.gateway.role.addToPrincipalPolicy(
289
+ new iam.PolicyStatement({
290
+ actions: ['bedrock-agentcore:InvokeGateway'],
291
+ resources: [props.gatewayArn],
292
+ }),
293
+ );
294
+ if (grant.policyDependable) {
295
+ target.node.addDependency(grant.policyDependable);
296
+ }
297
+
298
+ return target;
299
+ }
300
+
301
+ /**
302
+ * Create a Gateway target for an MCP endpoint, with outbound calls signed
303
+ * using this gateway's own execution role.
304
+ */
305
+ private addTarget(
306
+ gatewayTargetName: string,
307
+ endpoint: string,
308
+ ): agentcore.CfnGatewayTarget {
309
+ // The construct id is derived from the target name so the two change
310
+ // together — target names are unique per gateway, and replacing a target
311
+ // under an unchanged name fails with AlreadyExists.
312
+ const target = new agentcore.CfnGatewayTarget(
313
+ this,
314
+ `Target-${gatewayTargetName}`,
315
+ {
316
+ gatewayIdentifier: this.gateway.gatewayId,
317
+ name: gatewayTargetName,
318
+ targetConfiguration: {
319
+ mcp: {
320
+ mcpServer: {
321
+ endpoint,
322
+ },
323
+ },
324
+ },
325
+ credentialProviderConfigurations: [
326
+ {
327
+ credentialProviderType: 'GATEWAY_IAM_ROLE',
328
+ credentialProvider: {
329
+ iamCredentialProvider: {
330
+ service: 'bedrock-agentcore',
331
+ },
332
+ },
333
+ },
334
+ ],
335
+ },
336
+ );
337
+
256
338
  // Policies referencing this target's actions (e.g.
257
339
  // AgentCore::Action::"<targetName>___<tool>") only validate once the
258
340
  // target has registered them, so create policies after targets.
@@ -28,6 +28,12 @@ variable "additional_iam_policy_statements" {
28
28
  }))
29
29
  default = []
30
30
  }
31
+
32
+ variable "tool_dependencies" {
33
+ description = "Target ids whose tools must be queryable before `gateway_url` is consumable. Set this to the gateway's own target ids (e.g. from agentcore-gateway#mcp-connection) when another gateway aggregates this one: the readiness probe waits until this gateway serves those tools, since a parent gateway fetches them at target creation."
34
+ type = list(string)
35
+ default = []
36
+ }
31
37
  <%_ if (cedarPolicy) { _%>
32
38
 
33
39
  variable "policy_dependencies" {
@@ -210,6 +216,91 @@ resource "aws_bedrockagentcore_policy" "policies" {
210
216
  }
211
217
  <%_ } _%>
212
218
 
219
+ # A parent gateway fetches this gateway's tools when it registers it as a
220
+ # target, so this gateway must already serve them — its own targets register
221
+ # their tools asynchronously after creation. Probe `tools/list` over SigV4
222
+ # until the configured `tool_dependencies` are served, and route `gateway_url`
223
+ # through this resource so a parent gateway target waits for readiness. Only
224
+ # runs when this gateway is aggregated by another (empty `tool_dependencies`).
225
+ resource "null_resource" "gateway_ready" {
226
+ count = length(var.tool_dependencies) > 0 ? 1 : 0
227
+
228
+ triggers = {
229
+ gateway_url = aws_bedrockagentcore_gateway.this.gateway_url
230
+ region = local.aws_region
231
+ tool_dependencies = join(",", var.tool_dependencies)
232
+ }
233
+
234
+ provisioner "local-exec" {
235
+ command = <<-EOT
236
+ uv run --with boto3 --with httpx --with mcp python -c "
237
+ import asyncio
238
+ import os
239
+ import time
240
+
241
+ import boto3
242
+ import httpx
243
+ from botocore.auth import SigV4Auth
244
+ from botocore.awsrequest import AWSRequest
245
+ from mcp import ClientSession
246
+ from mcp.client.streamable_http import streamablehttp_client
247
+
248
+ gateway_url = os.environ['GATEWAY_URL']
249
+ region = os.environ['REGION']
250
+ credentials = boto3.Session(region_name=region).get_credentials()
251
+ signer = SigV4Auth(credentials, 'bedrock-agentcore', region)
252
+
253
+
254
+ class SigV4Auth_(httpx.Auth):
255
+ requires_request_body = True
256
+
257
+ def auth_flow(self, request):
258
+ aws_request = AWSRequest(
259
+ method=request.method,
260
+ url=str(request.url),
261
+ data=request.content,
262
+ headers=dict(request.headers),
263
+ )
264
+ signer.add_auth(aws_request)
265
+ request.headers.update(dict(aws_request.headers))
266
+ yield request
267
+
268
+
269
+ async def list_tools():
270
+ async with streamablehttp_client(
271
+ gateway_url, auth=SigV4Auth_(), timeout=120
272
+ ) as (read, write, _):
273
+ async with ClientSession(read, write) as session:
274
+ await session.initialize()
275
+ result = await session.list_tools()
276
+ return [t.name for t in result.tools]
277
+
278
+
279
+ # The built-in semantic-search tool is always present, so require at least one
280
+ # more tool before declaring the gateway ready to serve its aggregated targets.
281
+ deadline = time.time() + 5 * 60
282
+ while True:
283
+ try:
284
+ tools = asyncio.run(list_tools())
285
+ if len([t for t in tools if t != 'x_amz_bedrock_agentcore_search']) > 0:
286
+ break
287
+ last = f'gateway serves no aggregated tools yet (got {tools})'
288
+ except Exception as e:
289
+ last = str(e)
290
+ if time.time() >= deadline:
291
+ raise SystemExit(f'Gateway {gateway_url} not ready: {last}')
292
+ time.sleep(5)
293
+
294
+ print(f'Gateway {gateway_url} is serving tools')
295
+ "
296
+ EOT
297
+ environment = {
298
+ GATEWAY_URL = self.triggers.gateway_url
299
+ REGION = self.triggers.region
300
+ }
301
+ }
302
+ }
303
+
213
304
  # Add gateway URL to runtime config
214
305
  module "add_gateway_url_to_runtime_config" {
215
306
  source = "../../../core/runtime-config/entry"
@@ -232,8 +323,10 @@ output "gateway_arn" {
232
323
  }
233
324
 
234
325
  output "gateway_url" {
235
- description = "MCP endpoint URL of the Bedrock AgentCore Gateway"
236
- value = aws_bedrockagentcore_gateway.this.gateway_url
326
+ description = "MCP endpoint URL of the Bedrock AgentCore Gateway. When `tool_dependencies` is set, this value flows through the readiness probe so consumers (e.g. a parent gateway target) wait until the gateway serves its aggregated tools."
327
+ value = length(var.tool_dependencies) > 0 ? (
328
+ null_resource.gateway_ready[0].triggers.gateway_url
329
+ ) : aws_bedrockagentcore_gateway.this.gateway_url
237
330
  }
238
331
 
239
332
  output "gateway_role_arn" {
@@ -281,14 +281,19 @@ resource "null_resource" "docker_publish" {
281
281
  provisioner "local-exec" {
282
282
  command = <<-EOT
283
283
  # Get ECR login token
284
- aws ecr get-login-password --region ${local.aws_region} | <%= containers %> login --username AWS --password-stdin ${self.triggers.repository_url}
284
+ aws ecr get-login-password --region "$AWS_REGION" | <%= containers %> login --username AWS --password-stdin "$REPOSITORY_URL"
285
285
 
286
286
  # Tag the image
287
- <%= containers %> tag ${self.triggers.docker_image_tag} ${self.triggers.repository_url}:latest
287
+ <%= containers %> tag "$DOCKER_IMAGE_TAG" "$REPOSITORY_URL:latest"
288
288
 
289
289
  # Push the image
290
- <%= containers %> push ${self.triggers.repository_url}:latest
290
+ <%= containers %> push "$REPOSITORY_URL:latest"
291
291
  EOT
292
+ environment = {
293
+ AWS_REGION = local.aws_region
294
+ REPOSITORY_URL = self.triggers.repository_url
295
+ DOCKER_IMAGE_TAG = self.triggers.docker_image_tag
296
+ }
292
297
  }
293
298
 
294
299
  depends_on = [aws_ecr_repository_policy.agent_core_ecr_policy]
@@ -350,10 +355,11 @@ resource "null_resource" "runtime_ready" {
350
355
  uv run --with boto3 python -c "
351
356
  import boto3
352
357
  import json
358
+ import os
353
359
  import time
354
360
  import uuid
355
361
 
356
- runtime_arn = '${self.triggers.runtime_arn}'
362
+ runtime_arn = os.environ['RUNTIME_ARN']
357
363
  client = boto3.client('bedrock-agentcore')
358
364
  initialize_request = json.dumps({
359
365
  'jsonrpc': '2.0',
@@ -385,6 +391,9 @@ while True:
385
391
  print(f'Runtime {runtime_arn} is serving MCP')
386
392
  "
387
393
  EOT
394
+ environment = {
395
+ RUNTIME_ARN = self.triggers.runtime_arn
396
+ }
388
397
  }
389
398
  }
390
399
 
@@ -12,6 +12,7 @@ import {
12
12
  HttpMethod,
13
13
  HttpStage,
14
14
  LogGroupLogDestination,
15
+ ThrottleSettings,
15
16
  } from 'aws-cdk-lib/aws-apigatewayv2';
16
17
  import { LogGroup } from 'aws-cdk-lib/aws-logs';
17
18
  import { suppressRules } from '../checkov.js';
@@ -38,6 +39,12 @@ export interface HttpApiProps<
38
39
  * Map of operation names to their API Gateway integrations
39
40
  */
40
41
  readonly integrations: TIntegrations;
42
+ /**
43
+ * Default throttling limits applied to the API's default stage.
44
+ *
45
+ * @default { rateLimit: 10000, burstLimit: 5000 }
46
+ */
47
+ readonly throttle?: ThrottleSettings;
41
48
  }
42
49
 
43
50
  /**
@@ -71,6 +78,7 @@ export class HttpApi<
71
78
  apiName,
72
79
  operations,
73
80
  integrations,
81
+ throttle = { rateLimit: 10000, burstLimit: 5000 },
74
82
  ...props
75
83
  }: HttpApiProps<TIntegrations, TOperation>,
76
84
  ) {
@@ -98,6 +106,7 @@ export class HttpApi<
98
106
  this.defaultStage = new HttpStage(this, 'DefaultStage', {
99
107
  httpApi: this.api,
100
108
  autoDeploy: true,
109
+ throttle,
101
110
  accessLogSettings: {
102
111
  destination: new LogGroupLogDestination(accessLogGroup),
103
112
  },
@@ -6,6 +6,7 @@ import {
6
6
  RestApiProps as _RestApiProps,
7
7
  IResource,
8
8
  Stage,
9
+ ThrottleSettings,
9
10
  } from 'aws-cdk-lib/aws-apigateway';
10
11
  import { IAspect, RemovalPolicy } from 'aws-cdk-lib';
11
12
  import {
@@ -52,6 +53,12 @@ export interface RestApiProps<
52
53
  * @default true
53
54
  */
54
55
  readonly enableWaf?: boolean;
56
+ /**
57
+ * Default throttling limits applied to all methods on the API's default stage.
58
+ *
59
+ * @default { rateLimit: 10000, burstLimit: 5000 }
60
+ */
61
+ readonly throttle?: ThrottleSettings;
55
62
  }
56
63
 
57
64
  /**
@@ -86,6 +93,7 @@ export class RestApi<
86
93
  operations,
87
94
  integrations,
88
95
  enableWaf = true,
96
+ throttle = { rateLimit: 10000, burstLimit: 5000 },
89
97
  ...props
90
98
  }: RestApiProps<TIntegrations, TOperation>,
91
99
  ) {
@@ -93,7 +101,21 @@ export class RestApi<
93
101
  this.integrations = integrations;
94
102
 
95
103
  // Create the API Gateway REST API
96
- this.api = new _RestApi(this, 'Api', props);
104
+ this.api = new _RestApi(this, 'Api', {
105
+ ...props,
106
+ deployOptions: {
107
+ ...props.deployOptions,
108
+ methodOptions: {
109
+ ...props.deployOptions?.methodOptions,
110
+ // Default throttling applied to all resource paths and methods
111
+ '/*/*': {
112
+ throttlingRateLimit: throttle.rateLimit,
113
+ throttlingBurstLimit: throttle.burstLimit,
114
+ ...props.deployOptions?.methodOptions?.['/*/*'],
115
+ },
116
+ },
117
+ },
118
+ });
97
119
 
98
120
  if (enableWaf) {
99
121
  this.webAcl = new CfnWebACL(this, 'WebAcl', {
@@ -65,12 +65,13 @@ resource "null_resource" "aggregate_namespace" {
65
65
  command = <<-EOT
66
66
  uv run python -c "
67
67
  import json
68
+ import os
68
69
  import pathlib
69
70
  import sys
70
71
 
71
- namespace = '${each.key}'
72
- entries_dir = pathlib.Path('${local.entries_dir}') / namespace
73
- output_file = pathlib.Path('${local.config_dir}') / f'{namespace}.json'
72
+ namespace = os.environ['NAMESPACE']
73
+ entries_dir = pathlib.Path(os.environ['ENTRIES_DIR']) / namespace
74
+ output_file = pathlib.Path(os.environ['CONFIG_DIR']) / f'{namespace}.json'
74
75
 
75
76
  def deep_merge(target: dict, source: dict) -> dict:
76
77
  for k, v in source.items():
@@ -103,6 +104,11 @@ output_file.write_text(json.dumps(merged, indent=2))
103
104
  print(f'Aggregated {namespace}.json with {len(merged)} section(s)')
104
105
  "
105
106
  EOT
107
+ environment = {
108
+ NAMESPACE = each.key
109
+ ENTRIES_DIR = local.entries_dir
110
+ CONFIG_DIR = local.config_dir
111
+ }
106
112
  }
107
113
  }
108
114
 
@@ -38,11 +38,12 @@ resource "null_resource" "aggregate_namespace" {
38
38
  command = <<-EOT
39
39
  uv run python -c "
40
40
  import json
41
+ import os
41
42
  import pathlib
42
43
  import sys
43
44
 
44
- entries_dir = pathlib.Path('${local.entries_dir}')
45
- output_file = pathlib.Path('${local.namespace_path}')
45
+ entries_dir = pathlib.Path(os.environ['ENTRIES_DIR'])
46
+ output_file = pathlib.Path(os.environ['NAMESPACE_PATH'])
46
47
 
47
48
  def deep_merge(target: dict, source: dict) -> dict:
48
49
  for k, v in source.items():
@@ -71,6 +72,10 @@ output_file.parent.mkdir(parents=True, exist_ok=True)
71
72
  output_file.write_text(json.dumps(merged, indent=2))
72
73
  "
73
74
  EOT
75
+ environment = {
76
+ ENTRIES_DIR = local.entries_dir
77
+ NAMESPACE_PATH = local.namespace_path
78
+ }
74
79
  }
75
80
  }
76
81