What Is Agent Toolkit for AWS?
is an open-source project developed by AWS that helps AI coding agents work with AWS more reliably. With the recent addition of the newly released MCP server as part of the Toolkit, coding agents using the Toolkit can now access the AWS-specific context, workflows, guardrails, and tools they need to build, deploy, debug, and operate cloud systems without relying solely on general model knowledge, which is often out of date.
Instead of asking a coding agent to improvise from memory, the Toolkit gives it curated, task-specific instructions. These are packaged as skills, plugins, rules, and an MCP server configuration.
Skills are focused instruction packs. They guide the agent through specific AWS tasks, such as creating an S3 Tables lakehouse table, deploying a serverless app, debugging Lambda timeouts, connecting AWS Glue to a database, or adding memory to an AgentCore agent.
Plugins group related skills together. For example, aws-core covers general AWS development, aws-agents covers Bedrock AgentCore workflows, and aws-data-analytics covers S3 Tables, Glue, Athena, data discovery, and vector storage.
Rules files set the agent’s default AWS behaviour. They can tell the agent to prefer infrastructure-as-code, check AWS documentation when unsure, and use AWS MCP tools when available.
AWS MCP Server integration provides agents with access to live AWS documentation, AWS APIs, sandboxed script execution, and auditing via AWS-native controls.
The result should be better systems, with more resilience.
Why It Matters
Modern coding agents can write plausible AWS CLI commands, Terraform, CDK, Lambda handlers, Glue jobs, or IAM policies. Often, these will be correct and usable straight away, but there is a potential problem, and it’s the same issue that ALL coding agents struggle with. Knowledge cut-off.
When agents are trained, they are exposed to the latest information available at that time, but when the models are released, that information is typically many months out of date. For example, OpenAI’s latest model at the time of writing is GPT 5.5. It was released towards the end of April 2026, but its knowledge cut-off data was the 1st December 2025. And during the intervening period, new services are introduced, and existing systems, API calls, documentation, etc., are updated.
Cloud development is full of details that may seem small but can break real systems. For example, when creating an analytics table with Amazon S3 Tables, a generic agent might generate an Athena DDL statement with a LOCATION clause because this pattern is common for external tables. But with S3 Tables, that is wrong: the service manages table storage. The correct pattern is to keep the SQL clean and pass the S3 Tables catalogue through Athena’s query execution context.
The Agent Toolkit for AWS helps avoid that kind of mistake. Its skills guide the agent to:
- Check what already exists before creating new resources
- Use the correct AWS APIs
- Avoid patterns that AWS does not support
- Verify assumptions against current AWS documentation
- Produce tighter IAM policies
- Run checks after making changes
- Follow the right troubleshooting path when something breaks
That matters most in AWS work, where the hard part is not writing code. It is writing code that fits the specific AWS service, permissions model, and operating environment.
Installing Agent Toolkit in your coding agent
The agent toolkit is available for most modern coding agents, such as Claude Code, Cursor, Kiro, and VS Code. Sample installation instructions for each agent are in the AWS Toolkit repo, which I’ll link to at the end of the article.
My favourite coding agent to use right now is Codex, so that’s what I’ll use in my example. Install that first if you want to follow along.
To install the Toolkit for Codex, type the following into a terminal window,
$ codex plugin marketplace add aws/agent-toolkit-for-aws
Next, open the Codex app and type the following.
/plugins
Depending on what you may have previously installed, you should see something like this.
AI Agents on AWS
AWS Core
AWS Data Analytics
Browser
Documents
Presentations
Spreadsheets
That means all the required AWS-related plugins are available for your agent to use. Remember, if you run into any issues, just ask your coding agent to fix them.
Using the Toolkit with your coding agent
This is the easy part, as you literally just tell Codex in English what you want to achieve.
For my example, I wanted to:
- Create an Iceberg orders table using Amazon S3 Tables
- Ingest order data from a JDBC source with AWS Glue
- Validate and query the iceberg table with Athena.
This might seem like a fairly simple request, but when you break it down, it’s a lot more involved than you think. For starters, I don’t have an existing JDBC data source, so I also had to ask Codex to create an RDS database first and populate it with some dummy data. That alone spawns a bunch of other requirements, since my RDS database table is private, I needed a VPC, security groups, IAM permissions, etc.
You get the point, and this will be a well-known issue with anyone reading this who uses AWS in anger. Even a fairly simple AWS system usually requires a complex setup, as you have to account for security, authorisation, and permissions.
But as you will see, the AWS Toolkit does all the heavy lifting for us.
Note: Agent Toolkit for AWS runs inside your coding-agent environment. When it needs to inspect, create or or change AWS resources, it uses the AWS credentials configured in that environment. For local development, that usually means AWS CLI credentials, SSO, or environment variables, so make sure one or other of these methods is set up before you start
To get started, I fired up my Codex application and typed in the following:
Create an Iceberg orders table using Amazon S3 Tables, ingest order data
from a JDBC source with AWS Glue, and validate the table with Athena.
The first thing to do is set up a private JDBC source for the above with all
required VPC, networking and security infrastructure. Populate the JDBC source
with sample data,then everything else
After that, the agent just… kind of gets on with it. Here are some of the outputs it produced for me. I won’t include them all, as it was a pretty long list
Worked for 32m 49s
I'll use the AWS Data Analytics skills for the JDBC source, S3 Tables/Iceberg
table creation, ingestion, and Athena validation. I'm first checking the
workspace shape so the implementation matches what's already here.
The existing workspace already has an aws-data-lake directory, so I'm
inspecting it before deciding whether to extend it or create a separate
deployment package. I also need to verify local AWS tooling and credentials
before any real AWS resources can be created.
...
...
...
Implemented files:
orders-private-jdbc-source.yaml: VPC, private Aurora PostgreSQL source,
NAT/S3 endpoint, security groups, Glue connection/job, IAM roles, seed data
custom resource.
glue_orders_jdbc_to_s3tables.py: Glue 5.1 JDBC-to-S3-Tables ingestion script.
deploy-orders-end-to-end.ps1: deploys stack, uploads Glue script, grants
Lake Formation permissions, runs Glue, validates Athena.
README.md: updated runbook and validation results.
Validation completed:
CloudFormation stack: orders-private-jdbc-source
Glue job: orders-lake-jdbc-to-s3tables-orders
Successful Glue run: jr_a5c6e33544a452a107429f0f7613d5522d8f145062fbb40ad77507529a26263c
Target table: s3tablescatalog/mytablebucket.retail.orders
Athena row count: 6
Athena date range: 2026-04-01 to 2026-05-03
Athena sample rows returned ord-1001 through ord-1005
...
...
The whole process was very comprehensive and took just over 30 minutes from start to finish.
But did it work?
Yes, and here’s the proof.
Cloudformation
This is the service that AWS uses to automate the creation of all resources needed to build a particular system. It’s a single source of truth as to what was actually done. We can use the AWS CLI to check what CloudFormation did.
aws cloudformation describe-stacks --stack-name orders-private-jdbc-source --region us-east-2 --query "Stacks[0].StackStatus" --output text
# Output
UPDATE_COMPLETE
We can also get a full list of all the services and resources that CloudFormation built on our behalf. I’ve shown the command for this below, but please note I have prettified its output to make it more readable.
aws cloudformation list-stack-resources --stack-name orders-private-jdbc-source --region us-east-2 --output table
# Modified Output
+------------------------------------------------------+-------------------------------+-----------------+
| Service Deployed | ResourceType | ResourceStatus |
+------------------------------------------------------+-------------------------------+-----------------+
| S3 bucket (artifact/scripts bucket) | AWS::S3::Bucket | CREATE_COMPLETE |
| Security group rule (ingress) | AWS::EC2::SecurityGroupIngress | CREATE_COMPLETE |
| Secrets Manager secret (DB credentials) | AWS::SecretsManager::Secret | CREATE_COMPLETE |
| Security group (database SG) | AWS::EC2::SecurityGroup | CREATE_COMPLETE |
| RDS DB subnet group (Aurora subnets) | AWS::RDS::DBSubnetGroup | CREATE_COMPLETE |
| IAM role (Glue job execution role) | AWS::IAM::Role | UPDATE_COMPLETE |
| Security group (Glue/Spark SG) | AWS::EC2::SecurityGroup | CREATE_COMPLETE |
| Security group rule (egress) | AWS::EC2::SecurityGroupEgress | CREATE_COMPLETE |
| Security group rule (ingress) | AWS::EC2::SecurityGroupIngress | CREATE_COMPLETE |
| Security group rule (egress) | AWS::EC2::SecurityGroupEgress | CREATE_COMPLETE |
| Internet Gateway (VPC IGW) | AWS::EC2::InternetGateway | CREATE_COMPLETE |
| IAM role (Lake Formation / S3 Tables access role) | AWS::IAM::Role | CREATE_COMPLETE |
| Elastic IP (for NAT Gateway) | AWS::EC2::EIP | CREATE_COMPLETE |
| NAT Gateway | AWS::EC2::NatGateway | CREATE_COMPLETE |
| Aurora DB cluster (PostgreSQL) | AWS::RDS::DBCluster | CREATE_COMPLETE |
| Aurora DB instance (writer/instance) | AWS::RDS::DBInstance | CREATE_COMPLETE |
| Glue job (JDBC -> S3 Tables ingestion) | AWS::Glue::Job | CREATE_COMPLETE |
| Glue JDBC connection (to Aurora/Postgres) | AWS::Glue::Connection | CREATE_COMPLETE |
| Route (private default route, typically to NAT) | AWS::EC2::Route | CREATE_COMPLETE |
| Route table (private) | AWS::EC2::RouteTable | CREATE_COMPLETE |
| Subnet (private subnet 1) | AWS::EC2::Subnet | CREATE_COMPLETE |
| Route table association (private subnet 1) | AWS::EC2::SubnetRouteTableAssoc| CREATE_COMPLETE |
| Subnet (private subnet 2) | AWS::EC2::Subnet | CREATE_COMPLETE |
| Route table association (private subnet 2) | AWS::EC2::SubnetRouteTableAssoc| CREATE_COMPLETE |
| Route (public default route, typically to IGW) | AWS::EC2::Route | CREATE_COMPLETE |
| Route table (public) | AWS::EC2::RouteTable | CREATE_COMPLETE |
| Subnet (public subnet 1) | AWS::EC2::Subnet | CREATE_COMPLETE |
| Route table association (public subnet 1) | AWS::EC2::SubnetRouteTableAssoc| CREATE_COMPLETE |
| VPC endpoint (S3 Gateway Endpoint) | AWS::EC2::VPCEndpoint | CREATE_COMPLETE |
| Custom resource (seed orders data step) | Custom::SeedOrdersData | CREATE_COMPLETE |
| Lambda function (seeds sample orders into DB) | AWS::Lambda::Function | CREATE_COMPLETE |
| IAM role (Lambda execution role for seeding) | AWS::IAM::Role | CREATE_COMPLETE |
| VPC | AWS::EC2::VPC | CREATE_COMPLETE |
| VPC gateway attachment (attach IGW to VPC) | AWS::EC2::VPCGatewayAttachment | CREATE_COMPLETE |
+------------------------------------------------------+-------------------------------+-----------------+
I won’t go over ALL the services that were created, but here is a list of the most important ones with verification.
VPC and networking
A VPC is like having your own mini network within the AWS ecosystem. Scaffolded around that are services like CIDR addresses, routing tables, subnets, and security groups, which control which resources have access to the VPC. Let’s see what was created.
aws ec2 describe-vpcs --region us-east-2 --query "Vpcs[?Tags[?Key=='aws:cloudformation:stack-name' && Value=='orders-private-jdbc-source']].[VpcId,CidrBlock]" --output table
-------------------------------------------
| DescribeVpcs |
+------------------------+----------------+
| vpc-0165f765ce1af50c0 | 10.40.0.0/16 |
+------------------------+----------------+
aws ec2 describe-subnets --region us-east-2 --query "Subnets[?Tags[?Key=='aws:cloudformation:stack-name' && Value=='orders-private-jdbc-source']].[SubnetId,VpcId,CidrBlock,AvailabilityZone,MapPublicIpOnLaunch]" --output table
-----------------------------------------------------------------------------------------------
| DescribeSubnets |
+---------------------------+-------------------------+----------------+-------------+--------+
| subnet-0a9e1bbeeb1e7f53d | vpc-0165f765ce1af50c0 | 10.40.11.0/24 | us-east-2b | False |
| subnet-07dc3d0e99f09cdc4 | vpc-0165f765ce1af50c0 | 10.40.0.0/24 | us-east-2a | True |
| subnet-0c640ae5d30fe00e9 | vpc-0165f765ce1af50c0 | 10.40.10.0/24 | us-east-2a | False |
+---------------------------+-------------------------+----------------+-------------+--------+
aws ec2 describe-security-groups --region us-east-2 --query "SecurityGroups[?Tags[?Key=='aws:cloudformation:stack-name' && Value=='orders-private-jdbc-source']].[GroupId,GroupName,VpcId]" --output table
--------------------------------------------------------------------------------------------------------------------
| DescribeSecurityGroups |
+----------------------+-----------------------------------------------------------------+-------------------------+
| sg-0c56c3639a47dcbdb| orders-private-jdbc-source-DatabaseSecurityGroup-ZS9C0AJXzASB | vpc-0165f765ce1af50c0 |
| sg-0f1c55c20ebbf7acf| orders-private-jdbc-source-GlueSecurityGroup-XvKHWvTsRuap | vpc-0165f765ce1af50c0 |
+----------------------+-----------------------------------------------------------------+-------------------------+
IAM Roles
Identity and Access Management (IAM) is a crucial part of AWS security. It controls who and what has access to which services in AWS.
aws cloudformation list-stack-resources --stack-name orders-private-jdbc-source --region us-east-2 --query "StackResourceSummaries[?ResourceType=='AWS::IAM::Role' || ResourceType=='AWS::IAM::Policy'].[LogicalResourceId,ResourceType,PhysicalResourceId,ResourceStatus]" --output table
# Output
----------------------------------------------------------------------------------------------------------------------------------------
| ListStackResources |
+---------------------------+-----------------+--------------------------------------------------------------------+-------------------+
| GlueJobRole | AWS::IAM::Role | orders-private-jdbc-source-GlueJobRole-7cOVpk9zf1nf | UPDATE_COMPLETE |
| LakeFormationS3TablesRole| AWS::IAM::Role | orders-private-jdbc-sourc-LakeFormationS3TablesRole-4NKeHFJ0VwBh | CREATE_COMPLETE |
| SeedOrdersFunctionRole | AWS::IAM::Role | orders-private-jdbc-source-SeedOrdersFunctionRole-LPniYBvOU4jt | CREATE_COMPLETE |
+---------------------------+-----------------+--------------------------------------------------------------------+-------------------+
We can see that the appropriate roles were created to enable us to create an S3 table, populate our RDS database with data using a Lambda function, and populate our S3 table from the RDS database using a Glue job.
RDS Database
This was created as an initial data source for our iceberg table on S3. After creation, the database table was seeded with dummy data using a Lambda function.
The Lambda function
This was used to “seed” the RDS database with dummy data for forward propagation to the S3 table. I won’t show the code, but the function itself was around 70 lines of Python.
aws cloudformation list-stack-resources --stack-name orders-private-jdbc-source --region us-east-2 --query "StackResourceSummaries[?ResourceType=='AWS::Lambda::Function'].[LogicalResourceId,PhysicalResourceId,ResourceStatus]" --output table
# Output
---------------------------------------------------------------------------------------------------------
| ListStackResources |
+--------------------+--------------------------------------------------------------+-------------------+
| SeedOrdersFunction| orders-private-jdbc-source-SeedOrdersFunction-aDuplePymvWQ | CREATE_COMPLETE |
+--------------------+--------------------------------------------------------------+-------------------+
An Iceberg table on S3
Our ultimate goal was to produce an iceberg table on S3 that we could use to store, manipulate and analyse data. The screenshot below shows the table data, which exactly mirrors the source data in the RDS database.

Athena
It’s no good having an S3 table with data unless we can query, update or delete that data. That’s exactly what AWS’s Athena service allows you to do. Unfortunately, I did come across an issue when trying to use Athena initially. When I checked the Athena console, I couldn’t see the new S3 table in the list of databases and tables, so I couldn’t query it. However, after a little bit of back and forth with Codex, it eventually found this:
The key mismatch is now clear: database/table permissions are present, but
catalog-level Lake Formation permissions are empty. AWS's Athena S3 Tables
docs say the Athena principal also needs permissions on the S3 Tables catalog
itself. I'm going to grant ALL on xxxxxxxxxxxx:s3tablescatalog/mytablebucket
to the root user, then verify it.
After that, access was fine.
aws athena list-data-catalogs --region us-east-2 --output table
# Output
---------------------------------------------------------
| ListDataCatalogs |
+-------------------------------------------------------+
|| DataCatalogsSummary ||
|+-------------------------+-------------------+-------+|
|| CatalogName | Status | Type ||
|+-------------------------+-------------------+-------+|
|| AwsDataCatalog | CREATE_COMPLETE | GLUE ||
|| mytablebucket_s3tables | CREATE_COMPLETE | GLUE ||
|+-------------------------+-------------------+-------+|
And I was able to query the table in the Athena console.

Glue & Spark
Glue is AWS’s ETL tool and has two main purposes. It can catalogue data sources, making them available to other AWS services such as Athena. Glue can also use Spark or Pandas to read data sources (such as RDS databases) and use any data it finds to create and populate data stores on other services, such as S3 tables and objects.
aws glue get-connection --name orders-lake-orders-aurora-postgres --region us-east-2 --output json
# Output
{
"Connection": {
"Name": "orders-lake-orders-aurora-postgres",
"Description": "Private Aurora PostgreSQL orders source for S3 Tables ingestion.",
"ConnectionType": "JDBC",
"ConnectionProperties": {
"JDBC_ENFORCE_SSL": "false",
"JDBC_CONNECTION_URL": "jdbc:postgresql://orders-private-jdbc-source-ordersdbcluster-wxxm5ygu3dig.cluster-chfygkamm03d.us-east-2.rds.amazonaws.com:5432/ordersdb",
"SECRET_ID": "arn:aws:secretsmanager:us-east-2:XXXXXXXXXXXX:secret:DatabaseSecret-AYWd1SzbgdsG-3K7a0X"
},
"PhysicalConnectionRequirements": {
"SubnetId": "subnet-0c640ae5d30fe00e9",
"SecurityGroupIdList": [
"sg-0f1c55c20ebbf7acf"
],
"AvailabilityZone": "us-east-2a"
},
"CreationTime": "2026-05-08T21:27:26.593000+01:00",
"LastUpdatedTime": "2026-05-08T21:27:26.593000+01:00",
"LastUpdatedBy": "user/administrator",
"ConnectionSchemaVersion": 1
}
}
Codex also generated the Spark code in Glue to load data from the RDS database into Iceberg. I won’t show the whole code, as it’s almost 100 lines, but here’s a snippet.
import sys
from datetime import datetime, timezone
import boto3
from awsglue.context import GlueContext
from awsglue.job import Job
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from pyspark.sql.functions import col, lit, to_date
args = getResolvedOptions(
sys.argv,
[
"JOB_NAME",
"connection_name",
"source_table",
"target_table",
"watermark_bucket",
"watermark_key",
],
)
sc = SparkContext()
...
...
...
row_count = changed_df.count()
print(f"Found {row_count} changed rows")
if row_count > 0:
orders_df = changed_df.select(
col("order_id").cast("string").alias("order_id"),
col("customer_id").cast("string").alias("customer_id"),
to_date(col("order_date")).alias("order_date"),
col("status").cast("string").alias("status"),
col("amount").cast("double").alias("amount"),
col("updated_at").cast("timestamp").alias("updated_at"),
lit(datetime.now(timezone.utc)).cast("timestamp").alias("load_timestamp"),
)
orders_df.writeTo(args["target_table"]).append()
new_watermark = changed_df.agg({"updated_at": "max"}).collect()[0][0]
s3.put_object(
Bucket=args["watermark_bucket"],
Key=args["watermark_key"],
Body=str(new_watermark),
)
print(f"Updated watermark to {new_watermark}")
else:
print("No new rows to ingest")
job.commit()
Other considerations when using the AWS toolkit
1/ Limiting the agent’s access to certain AWS services.
The Toolkit’s AWS MCP server uses your default IAM permissions to create and access AWS services. If you want to restrict access to certain AWS services, you have a few choices.
a) Two global condition context keys are automatically added to all requests made through the AWS MCP Server:
- aws:ViaAWSMCPService – Set to true for any request that passes through an AWS-managed MCP server.
- aws:CalledViaAWSMCP – Contains the service principal of the specific AWS-managed MCP server (for example, aws-mcp.amazonaws.com).
You can use these context keys in your IAM policies to allow or deny actions initiated through any AWS-managed MCP server. For example, let’s say you wanted to deny the MCP server the ability to delete S3 buckets or objects. You could use this policy,
{
"Effect": "Deny",
"Action": ["s3:DeleteBucket", "s3:DeleteObject"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:CalledViaAWSMCP": "aws-mcp.amazonaws.com"
}
}
}
b) Another option is to create a dedicated role for the AWS toolkit. Attach whatever restricted policies you want to that role, then create a named AWS CLI profile for it using the aws configure command.
Then, before starting your coding agent (e.g., Codex), set the AWS_PROFILE environment variable to your new Codex-only profile name.
2/ Observability
Monitoring the AWS Agent Toolkit is primarily done through the AWS MCP Server, as it’s the managed component that receives tool calls and performs AWS actions. As such, the two main AWS services used for monitoring are the same ones used for most other AWS services – CloudWatch and CloudTrail.
The AWS MCP Server automatically publishes metrics to CloudWatch in the AWS-MCP namespace. You can see:
- Invocation: how many times a tool was called
- Success: successful tool calls
- UserError: client-side errors, often IAM denied actions or bad parameters
- SystemError: server-side failures
- Throttle: throttled requests
CloudTrail records the actual AWS API calls made in your account. This is where you can check:
- Who made the call
- What API was called
- When it happened
- The source IP
- The assumed role or IAM principal
- Whether the action succeeded or failed
Conclusion
If you’re a data engineer, data architect or a DevOps specialist, using the AWS Toolkit is a real boon. Through the plug-ins and tools it provides, you can access all AWS services and over 15,000 API calls.
In short, when used with a coding agent, the AWS toolkit can,
- Create AWS resources, write code, and deploy apps. The toolkit helps it choose the right services and follow AWS best practices.
- Get access to up-to-date AWS docs, APIs, and service details.
- For complex tasks such as IAM policies, data pipelines, or serverless apps, the agent follows tested, documented AWS workflows rather than guessing.
- Your agent can help investigate failed deployments, errors, or cost spikes by using AWS logs, metrics, stack status, and troubleshooting guidance.
- You can monitor agent activity, control access with IAM, and set guardrails such as read-only access or blocking specific AWS actions.
- Work with many different coding agents that are MCP-compatible, including Claude Code, Cursor, Codex, Kiro, Windsurf, etc.
However, as the issue I faced with my Athena set-up shows, the Toolkit, whilst a great time-saver, isn’t infallible, so, as with all agentic outputs, check your work before putting anything into production.
For more information on the Agent Toolkit for AWS, check out the official GitHub repo.