@aws/nx-plugin-mcp 1.0.0-rc.20 → 1.0.0-rc.22
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/docs/guides/agentcore-gateway.mdx +39 -7
- package/docs/guides/connection/py-agent-a2a.mdx +39 -10
- package/docs/guides/connection/py-agent-gateway.mdx +33 -7
- package/docs/guides/connection/py-agent-mcp.mdx +34 -8
- package/docs/guides/fastapi.mdx +235 -1
- package/docs/guides/py-agent.mdx +155 -38
- package/docs/guides/py-dynamodb.mdx +36 -9
- package/docs/guides/py-mcp-server.mdx +42 -1
- package/docs/guides/react-website.mdx +53 -1
- package/docs/guides/trpc.mdx +35 -5
- package/docs/guides/ts-agent.mdx +73 -1
- package/docs/guides/ts-dynamodb.mdx +39 -10
- package/docs/guides/ts-mcp-server.mdx +47 -1
- package/docs/guides/ts-rdb.mdx +39 -10
- package/docs/guides/ts-smithy-api.mdx +176 -1
- package/package.json +1 -1
- package/src/py/agent/schema.json +1 -1
|
@@ -4,7 +4,9 @@ description: Create an AgentCore Gateway project
|
|
|
4
4
|
generator: agentcore-gateway
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
import { FileTree } from '@astrojs/starlight/components';
|
|
7
|
+
import { FileTree, CardGrid } from '@astrojs/starlight/components';
|
|
8
|
+
import Astro from '@astrojs/react';
|
|
9
|
+
import ConnectionCard from '@components/connection-card.astro';
|
|
8
10
|
import RunGenerator from '@components/run-generator.astro';
|
|
9
11
|
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
10
12
|
import Infrastructure from '@components/infrastructure.astro';
|
|
@@ -232,9 +234,39 @@ The generator adds a `<name>-serve-local` target to the Gateway project, which r
|
|
|
232
234
|
|
|
233
235
|
See the connection guide for the full local development story.
|
|
234
236
|
|
|
235
|
-
##
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
## Connections
|
|
238
|
+
|
|
239
|
+
Use the <Link path="guides/connection">`connection`</Link> generator to integrate this project with others in your workspace. The following connections involve this project:
|
|
240
|
+
|
|
241
|
+
<CardGrid>
|
|
242
|
+
<ConnectionCard
|
|
243
|
+
title="AgentCore Gateway to MCP Server"
|
|
244
|
+
description="Aggregate an MCP server behind an AgentCore Gateway"
|
|
245
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/agentcore-gateway-mcp`}
|
|
246
|
+
source="agentcore"
|
|
247
|
+
target="mcp"
|
|
248
|
+
/>
|
|
249
|
+
<ConnectionCard
|
|
250
|
+
title="AgentCore Gateway to AgentCore Gateway"
|
|
251
|
+
description="Aggregate an AgentCore Gateway behind another AgentCore Gateway"
|
|
252
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/agentcore-gateway-gateway`}
|
|
253
|
+
source="agentcore"
|
|
254
|
+
target="agentcore"
|
|
255
|
+
/>
|
|
256
|
+
<ConnectionCard
|
|
257
|
+
title="TypeScript Agent to AgentCore Gateway"
|
|
258
|
+
description="Connect a TypeScript Agent to an AgentCore Gateway"
|
|
259
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/ts-agent-gateway`}
|
|
260
|
+
source="strands"
|
|
261
|
+
sourceBadge="typescript"
|
|
262
|
+
target="agentcore"
|
|
263
|
+
/>
|
|
264
|
+
<ConnectionCard
|
|
265
|
+
title="Python Agent to AgentCore Gateway"
|
|
266
|
+
description="Connect a Python Agent to an AgentCore Gateway"
|
|
267
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/py-agent-gateway`}
|
|
268
|
+
source="strands"
|
|
269
|
+
sourceBadge="python"
|
|
270
|
+
target="agentcore"
|
|
271
|
+
/>
|
|
272
|
+
</CardGrid>
|
|
@@ -7,7 +7,7 @@ when:
|
|
|
7
7
|
- ts#agent
|
|
8
8
|
- py#agent
|
|
9
9
|
---
|
|
10
|
-
import { FileTree } from '@astrojs/starlight/components';
|
|
10
|
+
import { FileTree, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
11
11
|
import Link from '@components/link.astro';
|
|
12
12
|
import RunGenerator from '@components/run-generator.astro';
|
|
13
13
|
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
@@ -22,7 +22,7 @@ The generator sets up all the necessary wiring so your agent can discover and in
|
|
|
22
22
|
|
|
23
23
|
Before using this generator, ensure you have:
|
|
24
24
|
|
|
25
|
-
1. A Python project with a <Link path="guides/py-agent">
|
|
25
|
+
1. A Python project with a <Link path="guides/py-agent">Python Agent</Link> component (Strands or LangChain)
|
|
26
26
|
2. A project with an Agent component generated with `--protocol=A2A` and `--auth=IAM` (either <Link path="guides/ts-agent">`ts#agent`</Link> or <Link path="guides/py-agent">`py#agent`</Link>)
|
|
27
27
|
3. Both components created with `infra: agentcore`
|
|
28
28
|
|
|
@@ -50,13 +50,15 @@ The generator creates a shared `agent_connection` Python project at `packages/co
|
|
|
50
50
|
- core
|
|
51
51
|
- agentcore\_endpoints.py Framework-agnostic ARN/URL resolution
|
|
52
52
|
- agentcore\_a2a\_client\_config.py Framework-agnostic A2A client config (signed `ClientConfig`)
|
|
53
|
-
- agentcore\_a2a\_client\
|
|
53
|
+
- agentcore\_a2a\_client\_\<framework>.py A2A client wrapping the config for your agent's framework
|
|
54
54
|
- auth/ Framework-agnostic SigV4 / session-forwarding `httpx.Auth`
|
|
55
55
|
- app
|
|
56
|
-
- \<target\_agent\_name>\_client\
|
|
56
|
+
- \<target\_agent\_name>\_client\_\<framework>.py Per-connection A2A client for each A2A agent
|
|
57
57
|
|
|
58
58
|
</FileTree>
|
|
59
59
|
|
|
60
|
+
The client suffix matches your agent's framework (`_strands` or `_langchain`). Both wrap the same framework-agnostic signed `ClientConfig`: the Strands client wraps a Strands `A2AAgent`, while the LangChain client drives the [a2a SDK](https://pypi.org/project/a2a-sdk/) directly.
|
|
61
|
+
|
|
60
62
|
Additionally, the generator:
|
|
61
63
|
- Transforms your agent's `agent.py` to register the remote A2A agent as a tool using `@tool`
|
|
62
64
|
- Adds the `agent_connection` project as a workspace dependency of your agent project
|
|
@@ -64,17 +66,19 @@ Additionally, the generator:
|
|
|
64
66
|
|
|
65
67
|
## Using the Connected A2A Agent
|
|
66
68
|
|
|
67
|
-
The generator transforms your agent's `agent.py` to wrap the remote A2A agent as a tool:
|
|
69
|
+
The generator transforms your agent's `agent.py` to wrap the remote A2A agent as a tool. The remote agent is registered with a `@tool`-decorated delegate — the decorator's import and the agent constructor differ by framework:
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
<Tabs syncKey="agent-framework">
|
|
72
|
+
<TabItem label="Strands" _filter={{ framework: 'strands' }}>
|
|
73
|
+
```python title="packages/my-project/my_module/agent/agent.py" {4,8-13,15}
|
|
70
74
|
from contextlib import contextmanager
|
|
71
75
|
from strands import Agent, tool
|
|
72
76
|
|
|
73
77
|
from my_scope_agent_connection import RemoteAgentClientStrands
|
|
74
78
|
|
|
75
79
|
@contextmanager
|
|
76
|
-
def get_agent(
|
|
77
|
-
remote_agent = RemoteAgentClientStrands.create(
|
|
80
|
+
def get_agent():
|
|
81
|
+
remote_agent = RemoteAgentClientStrands.create()
|
|
78
82
|
|
|
79
83
|
@tool
|
|
80
84
|
def ask_remote_agent(prompt: str) -> str:
|
|
@@ -86,10 +90,35 @@ def get_agent(session_id: str):
|
|
|
86
90
|
tools=[ask_remote_agent],
|
|
87
91
|
)
|
|
88
92
|
```
|
|
93
|
+
</TabItem>
|
|
94
|
+
<TabItem label="LangChain" _filter={{ framework: 'langchain' }}>
|
|
95
|
+
```python title="packages/my-project/my_module/agent/agent.py" {4,7-12,14}
|
|
96
|
+
from langchain.agents import create_agent
|
|
97
|
+
from langchain_aws import ChatBedrockConverse
|
|
98
|
+
from langchain_core.tools import tool
|
|
99
|
+
|
|
100
|
+
from my_scope_agent_connection import RemoteAgentClientLangChain
|
|
101
|
+
|
|
102
|
+
def get_agent():
|
|
103
|
+
remote_agent = RemoteAgentClientLangChain.create()
|
|
104
|
+
|
|
105
|
+
@tool
|
|
106
|
+
def ask_remote_agent(prompt: str) -> str:
|
|
107
|
+
"""Delegate a question to the remote RemoteAgent A2A agent and return its reply."""
|
|
108
|
+
return str(remote_agent(prompt))
|
|
109
|
+
|
|
110
|
+
return create_agent(
|
|
111
|
+
model=ChatBedrockConverse(model=MODEL_ID, region_name=REGION),
|
|
112
|
+
system_prompt="...",
|
|
113
|
+
tools=[ask_remote_agent],
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
</TabItem>
|
|
117
|
+
</Tabs>
|
|
89
118
|
|
|
90
|
-
|
|
119
|
+
Both clients are directly callable, returning the remote agent's reply, and wrap an `httpx.AsyncClient` that signs requests with SigV4 when deployed to AWS and uses a plain `http://localhost:<port>/` endpoint when `SERVE_LOCAL=true`. The Strands client wraps a Strands `A2AAgent`; the LangChain client drives the a2a SDK directly.
|
|
91
120
|
|
|
92
|
-
|
|
121
|
+
The AgentCore session ID is propagated to the remote agent automatically via the `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` header, regardless of framework: the agent server binds the inbound request's session into an async context, and the connection client's signed `httpx.Auth` stamps it on every outbound call — ensuring consistency for [Bedrock AgentCore Observability](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html).
|
|
93
122
|
|
|
94
123
|
## Infrastructure
|
|
95
124
|
|
|
@@ -5,7 +5,7 @@ when:
|
|
|
5
5
|
sourceType: py#agent
|
|
6
6
|
targetType: agentcore-gateway
|
|
7
7
|
---
|
|
8
|
-
import { FileTree } from '@astrojs/starlight/components';
|
|
8
|
+
import { FileTree, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
9
9
|
import Link from '@components/link.astro';
|
|
10
10
|
import RunGenerator from '@components/run-generator.astro';
|
|
11
11
|
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
@@ -46,17 +46,19 @@ The generator emits shared core-gateway modules into your `agent_connection` Pyt
|
|
|
46
46
|
- core/
|
|
47
47
|
- agentcore\_endpoints.py Framework-agnostic ARN/URL resolution
|
|
48
48
|
- agentcore\_gateway\_mcp\_transport.py Framework-agnostic Gateway MCP transport
|
|
49
|
-
- agentcore\_gateway\_mcp\_client\
|
|
49
|
+
- agentcore\_gateway\_mcp\_client\_\<framework>.py Gateway MCP client for your agent's framework
|
|
50
50
|
- auth/ Framework-agnostic SigV4 / session-forwarding `httpx.Auth`
|
|
51
51
|
- app/
|
|
52
|
-
- \<gateway\_snake>\_client\
|
|
52
|
+
- \<gateway\_snake>\_client\_\<framework>.py Per-Gateway client wrapper
|
|
53
53
|
- \_\_init\_\_.py Re-exports the Gateway client
|
|
54
54
|
|
|
55
55
|
</FileTree>
|
|
56
56
|
|
|
57
|
+
The client suffix matches your agent's framework (`_strands` or `_langchain`).
|
|
58
|
+
|
|
57
59
|
Additionally, the generator:
|
|
58
60
|
|
|
59
|
-
- Modifies your agent's `agent.py` to import the Gateway client
|
|
61
|
+
- Modifies your agent's `agent.py` to import the Gateway client and register its tools in `tools`
|
|
60
62
|
- Adds `agent_connection` as a workspace dependency of the agent
|
|
61
63
|
- Wires the agent's `<agent>-serve-local` target to depend on the Gateway's `<gateway>-serve-local` aggregator
|
|
62
64
|
|
|
@@ -64,6 +66,8 @@ Additionally, the generator:
|
|
|
64
66
|
|
|
65
67
|
The generator transforms your agent's `agent.py` to use the Gateway client:
|
|
66
68
|
|
|
69
|
+
<Tabs syncKey="agent-framework">
|
|
70
|
+
<TabItem label="Strands" _filter={{ framework: 'strands' }}>
|
|
67
71
|
```python title="packages/example/example/my_agent/agent.py" {4,8,9-13}
|
|
68
72
|
from contextlib import contextmanager
|
|
69
73
|
from strands import Agent
|
|
@@ -82,10 +86,32 @@ def get_agent():
|
|
|
82
86
|
)
|
|
83
87
|
```
|
|
84
88
|
|
|
85
|
-
`MyGatewayClientStrands.create()` returns a single context-manageable
|
|
89
|
+
`MyGatewayClientStrands.create()` returns a single context-manageable `MCPClient` whose `list_tools_sync()` yields every tool available through the Gateway.
|
|
90
|
+
</TabItem>
|
|
91
|
+
<TabItem label="LangChain" _filter={{ framework: 'langchain' }}>
|
|
92
|
+
```python title="packages/example/example/my_agent/agent.py" {4,8,12}
|
|
93
|
+
from langchain.agents import create_agent
|
|
94
|
+
from langchain_aws import ChatBedrockConverse
|
|
95
|
+
|
|
96
|
+
from my_scope_agent_connection import MyGatewayClientLangChain
|
|
97
|
+
|
|
98
|
+
def get_agent():
|
|
99
|
+
my_gateway = MyGatewayClientLangChain.create()
|
|
100
|
+
return create_agent(
|
|
101
|
+
model=ChatBedrockConverse(model=MODEL_ID, region_name=REGION),
|
|
102
|
+
system_prompt="...",
|
|
103
|
+
tools=[*my_gateway],
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`MyGatewayClientLangChain.create()` returns a list of tools loaded via [`langchain-mcp-adapters`](https://docs.langchain.com/oss/python/langchain/mcp). Each tool opens a fresh session per call, so no `with` block is needed.
|
|
108
|
+
</TabItem>
|
|
109
|
+
</Tabs>
|
|
110
|
+
|
|
111
|
+
In both cases the client behaves the same way per mode:
|
|
86
112
|
|
|
87
|
-
- **Deployed mode** (`SERVE_LOCAL` unset):
|
|
88
|
-
- **Local mode** (`SERVE_LOCAL=true`):
|
|
113
|
+
- **Deployed mode** (`SERVE_LOCAL` unset): tools pointed at the Gateway's MCP endpoint, SigV4-signed.
|
|
114
|
+
- **Local mode** (`SERVE_LOCAL=true`): plain-HTTP tools pointed at the local gateway started by the Gateway project's `serve-local` target.
|
|
89
115
|
|
|
90
116
|
The session ID is propagated to downstream MCP servers automatically via the `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` header.
|
|
91
117
|
|
|
@@ -7,7 +7,7 @@ when:
|
|
|
7
7
|
- ts#mcp-server
|
|
8
8
|
- py#mcp-server
|
|
9
9
|
---
|
|
10
|
-
import { FileTree } from '@astrojs/starlight/components';
|
|
10
|
+
import { FileTree, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
11
11
|
import Link from '@components/link.astro';
|
|
12
12
|
import RunGenerator from '@components/run-generator.astro';
|
|
13
13
|
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
@@ -22,7 +22,7 @@ The generator sets up all the necessary wiring so your agent can discover and in
|
|
|
22
22
|
|
|
23
23
|
Before using this generator, ensure you have:
|
|
24
24
|
|
|
25
|
-
1. A Python project with a <Link path="guides/py-agent">
|
|
25
|
+
1. A Python project with a <Link path="guides/py-agent">Python Agent</Link> component (Strands or LangChain)
|
|
26
26
|
2. A project with an MCP server component (either <Link path="guides/ts-mcp-server">`ts#mcp-server`</Link> or <Link path="guides/py-mcp-server">`py#mcp-server`</Link>)
|
|
27
27
|
3. Both components created with `infra: agentcore`
|
|
28
28
|
|
|
@@ -50,13 +50,15 @@ The generator creates a shared `agent_connection` Python project at `packages/co
|
|
|
50
50
|
- core
|
|
51
51
|
- agentcore\_endpoints.py Framework-agnostic ARN/URL resolution
|
|
52
52
|
- agentcore\_mcp\_transport.py Framework-agnostic MCP transport
|
|
53
|
-
- agentcore\_mcp\_client\
|
|
53
|
+
- agentcore\_mcp\_client\_\<framework>.py MCP client wrapping the transport for your agent's framework
|
|
54
54
|
- auth/ Framework-agnostic SigV4 / session-forwarding `httpx.Auth`
|
|
55
55
|
- app
|
|
56
|
-
- \<mcp\_server\_name>\_client\
|
|
56
|
+
- \<mcp\_server\_name>\_client\_\<framework>.py Per-connection client for each MCP server
|
|
57
57
|
|
|
58
58
|
</FileTree>
|
|
59
59
|
|
|
60
|
+
The client suffix matches your agent's framework (`_strands` or `_langchain`).
|
|
61
|
+
|
|
60
62
|
Additionally, the generator:
|
|
61
63
|
- Transforms your agent's `agent.py` to import and use the MCP server's tools via a class-based client
|
|
62
64
|
- Adds the `agent_connection` project as a workspace dependency of your agent project
|
|
@@ -66,15 +68,17 @@ Additionally, the generator:
|
|
|
66
68
|
|
|
67
69
|
The generator transforms your agent's `agent.py` to use the MCP server's tools:
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
<Tabs syncKey="agent-framework">
|
|
72
|
+
<TabItem label="Strands" _filter={{ framework: 'strands' }}>
|
|
73
|
+
```python title="packages/my-project/my_module/agent/agent.py" {4,9-13}
|
|
70
74
|
from contextlib import contextmanager
|
|
71
75
|
from strands import Agent
|
|
72
76
|
|
|
73
77
|
from my_scope_agent_connection import MyMcpServerClientStrands
|
|
74
78
|
|
|
75
79
|
@contextmanager
|
|
76
|
-
def get_agent(
|
|
77
|
-
my_mcp_server = MyMcpServerClientStrands.create(
|
|
80
|
+
def get_agent():
|
|
81
|
+
my_mcp_server = MyMcpServerClientStrands.create()
|
|
78
82
|
with (
|
|
79
83
|
my_mcp_server,
|
|
80
84
|
):
|
|
@@ -84,7 +88,29 @@ def get_agent(session_id: str):
|
|
|
84
88
|
)
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
The
|
|
91
|
+
The Strands client is a context manager, entered in a `with` block around the agent.
|
|
92
|
+
</TabItem>
|
|
93
|
+
<TabItem label="LangChain" _filter={{ framework: 'langchain' }}>
|
|
94
|
+
```python title="packages/my-project/my_module/agent/agent.py" {4,8,13}
|
|
95
|
+
from langchain.agents import create_agent
|
|
96
|
+
from langchain_aws import ChatBedrockConverse
|
|
97
|
+
|
|
98
|
+
from my_scope_agent_connection import MyMcpServerClientLangChain
|
|
99
|
+
|
|
100
|
+
def get_agent():
|
|
101
|
+
my_mcp_server = MyMcpServerClientLangChain.create()
|
|
102
|
+
return create_agent(
|
|
103
|
+
model=ChatBedrockConverse(model=MODEL_ID, region_name=REGION),
|
|
104
|
+
system_prompt="...",
|
|
105
|
+
tools=[*my_mcp_server],
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The LangChain client returns a list of tools loaded via [`langchain-mcp-adapters`](https://docs.langchain.com/oss/python/langchain/mcp). Each tool opens a fresh MCP session per call, so the tools stay usable for the agent's lifetime — no `with` block is needed.
|
|
110
|
+
</TabItem>
|
|
111
|
+
</Tabs>
|
|
112
|
+
|
|
113
|
+
The AgentCore session ID is propagated to the MCP server automatically via the `X-Amzn-Bedrock-AgentCore-Runtime-Session-Id` header for both frameworks, ensuring consistency for [Bedrock AgentCore Observability](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html).
|
|
88
114
|
|
|
89
115
|
## Infrastructure
|
|
90
116
|
|
package/docs/guides/fastapi.mdx
CHANGED
|
@@ -6,7 +6,9 @@ when:
|
|
|
6
6
|
framework: [fastapi]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
import { FileTree, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
9
|
+
import { FileTree, Tabs, TabItem, CardGrid } from '@astrojs/starlight/components';
|
|
10
|
+
import Astro from '@astrojs/react';
|
|
11
|
+
import ConnectionCard from '@components/connection-card.astro';
|
|
10
12
|
import Link from '@components/link.astro';
|
|
11
13
|
import RunGenerator from '@components/run-generator.astro';
|
|
12
14
|
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
@@ -185,6 +187,216 @@ Unhandled exceptions are caught by the middleware and:
|
|
|
185
187
|
It's recommended to specify response models for your API operations for better code generation if using the `connection` generator. <Link path="guides/connection/react-fastapi#errors">See here for more details</Link>.
|
|
186
188
|
:::
|
|
187
189
|
|
|
190
|
+
### Accessing the Calling User
|
|
191
|
+
|
|
192
|
+
When your API is protected by authentication, your route handlers often need to know who is calling. The generated FastAPI runs inside AWS Lambda via the [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter), which forwards the API Gateway request context as JSON on the `x-amzn-request-context` header. You can read it from the FastAPI `Request` to extract the caller's identity.
|
|
193
|
+
|
|
194
|
+
As an example, let's add a `/me` endpoint that returns details about the calling user. We'll implement the extraction as a [FastAPI dependency](https://fastapi.tiangolo.com/tutorial/dependencies/) so it can be reused across routes. The shape of the request context — and therefore how you extract the identity — depends on both your selected `auth` method and whether you deployed a REST or HTTP API.
|
|
195
|
+
|
|
196
|
+
<OptionFilter when={{ auth: 'iam' }} description="Identity extraction for IAM-authenticated APIs">
|
|
197
|
+
For `IAM` authentication, we look up the caller in Cognito using the sub extracted from the API Gateway request context. Create `identity.py` alongside `main.py`:
|
|
198
|
+
|
|
199
|
+
<Tabs syncKey="http-rest">
|
|
200
|
+
<TabItem label="REST API" _filter={{ infra: 'rest-lambda' }}>
|
|
201
|
+
```python
|
|
202
|
+
import json
|
|
203
|
+
import os
|
|
204
|
+
from typing import Annotated
|
|
205
|
+
|
|
206
|
+
from boto3 import client
|
|
207
|
+
from fastapi import Depends, HTTPException, Request
|
|
208
|
+
from pydantic import BaseModel
|
|
209
|
+
|
|
210
|
+
cognito = client("cognito-idp")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Identity(BaseModel):
|
|
214
|
+
sub: str
|
|
215
|
+
username: str
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_identity(request: Request) -> Identity:
|
|
219
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
220
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
221
|
+
if not request_context_header:
|
|
222
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
223
|
+
|
|
224
|
+
request_context = json.loads(request_context_header)
|
|
225
|
+
provider = request_context.get("identity", {}).get("cognitoAuthenticationProvider")
|
|
226
|
+
|
|
227
|
+
sub = provider.split(":")[-1] if provider else None
|
|
228
|
+
if not sub:
|
|
229
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
230
|
+
|
|
231
|
+
users = cognito.list_users(
|
|
232
|
+
# Assumes user pool id is configured in lambda environment
|
|
233
|
+
UserPoolId=os.environ["USER_POOL_ID"],
|
|
234
|
+
Limit=1,
|
|
235
|
+
Filter=f'sub="{sub}"',
|
|
236
|
+
).get("Users", [])
|
|
237
|
+
|
|
238
|
+
if len(users) != 1:
|
|
239
|
+
raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
|
|
240
|
+
|
|
241
|
+
return Identity(sub=sub, username=users[0]["Username"])
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
245
|
+
```
|
|
246
|
+
</TabItem>
|
|
247
|
+
<TabItem label="HTTP API" _filter={{ infra: 'http-lambda' }}>
|
|
248
|
+
```python
|
|
249
|
+
import json
|
|
250
|
+
import os
|
|
251
|
+
from typing import Annotated
|
|
252
|
+
|
|
253
|
+
from boto3 import client
|
|
254
|
+
from fastapi import Depends, HTTPException, Request
|
|
255
|
+
from pydantic import BaseModel
|
|
256
|
+
|
|
257
|
+
cognito = client("cognito-idp")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Identity(BaseModel):
|
|
261
|
+
sub: str
|
|
262
|
+
username: str
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_identity(request: Request) -> Identity:
|
|
266
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
267
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
268
|
+
if not request_context_header:
|
|
269
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
270
|
+
|
|
271
|
+
request_context = json.loads(request_context_header)
|
|
272
|
+
amr = (
|
|
273
|
+
request_context.get("authorizer", {})
|
|
274
|
+
.get("iam", {})
|
|
275
|
+
.get("cognitoIdentity", {})
|
|
276
|
+
.get("amr", [])
|
|
277
|
+
)
|
|
278
|
+
sign_in = next((s for s in amr if ":CognitoSignIn:" in s), None)
|
|
279
|
+
sub = sign_in.split(":")[-1] if sign_in else None
|
|
280
|
+
|
|
281
|
+
if not sub:
|
|
282
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
283
|
+
|
|
284
|
+
users = cognito.list_users(
|
|
285
|
+
# Assumes user pool id is configured in lambda environment
|
|
286
|
+
UserPoolId=os.environ["USER_POOL_ID"],
|
|
287
|
+
Limit=1,
|
|
288
|
+
Filter=f'sub="{sub}"',
|
|
289
|
+
).get("Users", [])
|
|
290
|
+
|
|
291
|
+
if len(users) != 1:
|
|
292
|
+
raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
|
|
293
|
+
|
|
294
|
+
return Identity(sub=sub, username=users[0]["Username"])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
298
|
+
```
|
|
299
|
+
</TabItem>
|
|
300
|
+
</Tabs>
|
|
301
|
+
</OptionFilter>
|
|
302
|
+
|
|
303
|
+
<OptionFilter when={{ auth: 'cognito' }} description="Identity extraction for Cognito-authenticated APIs">
|
|
304
|
+
With `auth: 'cognito'`, the API Gateway Cognito User Pools authorizer verifies the JWT that the caller supplies in the `Authorization` header and places the verified claims on the request context.
|
|
305
|
+
|
|
306
|
+
Create `identity.py` alongside `main.py`:
|
|
307
|
+
|
|
308
|
+
<Tabs syncKey="http-rest">
|
|
309
|
+
<TabItem label="REST API" _filter={{ infra: 'rest-lambda' }}>
|
|
310
|
+
```python
|
|
311
|
+
import json
|
|
312
|
+
from typing import Annotated
|
|
313
|
+
|
|
314
|
+
from fastapi import Depends, HTTPException, Request
|
|
315
|
+
from pydantic import BaseModel
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class Identity(BaseModel):
|
|
319
|
+
sub: str
|
|
320
|
+
username: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_identity(request: Request) -> Identity:
|
|
324
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
325
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
326
|
+
if not request_context_header:
|
|
327
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
328
|
+
|
|
329
|
+
request_context = json.loads(request_context_header)
|
|
330
|
+
claims = request_context.get("authorizer", {}).get("claims", {})
|
|
331
|
+
|
|
332
|
+
sub = claims.get("sub")
|
|
333
|
+
username = claims.get("username")
|
|
334
|
+
|
|
335
|
+
if not sub or not username:
|
|
336
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
337
|
+
|
|
338
|
+
return Identity(sub=sub, username=username)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
342
|
+
```
|
|
343
|
+
</TabItem>
|
|
344
|
+
<TabItem label="HTTP API" _filter={{ infra: 'http-lambda' }}>
|
|
345
|
+
HTTP APIs use a JWT authorizer which places the verified claims under `authorizer.jwt.claims`:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
import json
|
|
349
|
+
from typing import Annotated
|
|
350
|
+
|
|
351
|
+
from fastapi import Depends, HTTPException, Request
|
|
352
|
+
from pydantic import BaseModel
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class Identity(BaseModel):
|
|
356
|
+
sub: str
|
|
357
|
+
username: str
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def get_identity(request: Request) -> Identity:
|
|
361
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
362
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
363
|
+
if not request_context_header:
|
|
364
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
365
|
+
|
|
366
|
+
request_context = json.loads(request_context_header)
|
|
367
|
+
claims = request_context.get("authorizer", {}).get("jwt", {}).get("claims", {})
|
|
368
|
+
|
|
369
|
+
sub = claims.get("sub")
|
|
370
|
+
username = claims.get("username")
|
|
371
|
+
|
|
372
|
+
if not sub or not username:
|
|
373
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
374
|
+
|
|
375
|
+
return Identity(sub=sub, username=username)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
379
|
+
```
|
|
380
|
+
</TabItem>
|
|
381
|
+
</Tabs>
|
|
382
|
+
|
|
383
|
+
:::tip[No token verification required]
|
|
384
|
+
You don't need any JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, scopes, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
|
|
385
|
+
:::
|
|
386
|
+
</OptionFilter>
|
|
387
|
+
|
|
388
|
+
You can then inject the `CurrentUser` dependency into any route that needs the caller's identity:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
from .identity import CurrentUser, Identity
|
|
392
|
+
from .init import app, tracer
|
|
393
|
+
|
|
394
|
+
@app.get("/me")
|
|
395
|
+
@tracer.capture_method
|
|
396
|
+
def me(identity: CurrentUser) -> Identity:
|
|
397
|
+
return identity
|
|
398
|
+
```
|
|
399
|
+
|
|
188
400
|
<OptionFilter when={{ infra: 'rest-lambda' }} description="Streaming — REST API only">
|
|
189
401
|
### Streaming
|
|
190
402
|
|
|
@@ -509,3 +721,25 @@ This starts a local FastAPI development server with:
|
|
|
509
721
|
## Invoking your FastAPI
|
|
510
722
|
|
|
511
723
|
To invoke your API from a React website, you can use the <Link path="guides/connection/react-fastapi">`connection` generator</Link>.
|
|
724
|
+
|
|
725
|
+
## Connections
|
|
726
|
+
|
|
727
|
+
Use the <Link path="guides/connection">`connection`</Link> generator to integrate this project with others in your workspace. The following connections involve this project:
|
|
728
|
+
|
|
729
|
+
<CardGrid>
|
|
730
|
+
<ConnectionCard
|
|
731
|
+
title="React to FastAPI"
|
|
732
|
+
description="Call a Python FastAPI from a React website"
|
|
733
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/react-fastapi`}
|
|
734
|
+
source="react"
|
|
735
|
+
target="fastapi"
|
|
736
|
+
/>
|
|
737
|
+
<ConnectionCard
|
|
738
|
+
title="FastAPI to Python DynamoDB"
|
|
739
|
+
description="Connect a FastAPI to a DynamoDB table"
|
|
740
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/py-fast-api-dynamodb`}
|
|
741
|
+
source="fastapi"
|
|
742
|
+
target="dynamodb"
|
|
743
|
+
targetBadge="python"
|
|
744
|
+
/>
|
|
745
|
+
</CardGrid>
|