AWS Builder Center
Turn Lambda functions into MCP tools with AgentCore Gateway

Turn Lambda functions into MCP tools with AgentCore Gateway

Two order Lambda functions as agent tools behind Cognito tokens and a Cedar policy, with the tool schema limits, a tested CDK stack and the monthly math.

Principal Cloud & AI Platform Architect | 5x AWS Community Builder (Serverless)
You already have AWS Lambda functions that look up orders and cancel them. Your AI agents need the same actions, and they speak the Model Context Protocol (MCP). Amazon Bedrock AgentCore Gateway can be the MCP server for those functions, so you don't write or host one. We built that for a small order system, with a policy that keeps the cancel tool away from agents that shouldn't have it.
 
This is for developers who run Lambda in production and now need to hand some of it to agents. You get the handler changes AWS asks for, the limits of the tool schema, the auth layers, and the math for a month. We haven't deployed the stack yet. Every result below comes from local tests and CI.
 
Key takeaways
  • The gateway calls your function with the tool arguments as the event and its own metadata in the client context. Your code must strip the target___ prefix from the tool name.
  • The gateway's tool schema knows five keywords: type, description, properties, required and items. Formats, lengths and ranges go in your handler.
  • AgentCore Policy denies by default and hides tools from agents that may not call them. The reader agent's tool list has no cancel_order at all.
  • About $6.30 a month for 100,000 MCP operations. Fetch a new Cognito token per session and the token line alone is $56.25.
  • Test the contract with the official MCP client against a local server built from the same definitions and handlers.
 
At a glance
LevelIntermediate
Time to read9 minutes
CostAbout $6.30 a month for 25,000 agent sessions in us-east-1. No free tier for Gateway, Policy or machine tokens
CodeInfraTales/agentcore-mcp-gateway 
Tested withNode.js 24, AWS Cloud Development Kit (CDK) 2.273.0, MCP TypeScript SDK 1.32.1, DynamoDB Local 3.3.2, 70 automated tests, cdk-nag
DeployedNot yet. The smoke test hasn't run against AWS

Learn the MCP basics first

MCP is an open protocol that lets an AI agent find and use tools it wasn't built with. A server publishes a list of tools. Each one has a name, a description the model reads, and a JSON schema for its inputs. The agent's client asks for that list, and the model decides which tool to call and with what arguments.
 
With a remote server such as a gateway, the client sends JSON remote procedure call (RPC) messages over HTTP. It calls tools/list to learn what exists and tools/call to run one. An MCP client can talk to any MCP server, so you describe a tool once and any agent framework that speaks MCP can call it. Your job as the server owner is to describe each tool well and to decide who may call it.

Give agents three order tools

AgentCore Gateway turns "existing APIs and AWS Lambda functions into agent-compatible tools," in AWS's words. It reached general availability on 13 October 2025, and AgentCore Policy followed on 3 March 2026. Besides Lambda, targets include Amazon API Gateway stages, OpenAPI schemas, Smithy models, other MCP servers and built-in integrations. Agents see all of a gateway's MCP targets as one MCP server.
 
We gave agents two Lambda functions behind three tools. The gateway joins the target and tool names with ___:
Tool an agent seesFunctionWhat it does
orders-read___get_orderreaderOne order by ID
orders-read___list_customer_ordersreaderA customer's recent orders, newest first
orders-admin___cancel_orderadminCancels an open order, safe to retry
 
Two agents use them. Both get tokens from Amazon Cognito, but only the ops agent's token carries the orders/admin scope. A request takes this path:
Agent gets a Cognito token, calls AgentCore Gateway, Policy checks it, a Lambda tool runs
 
Each function has its own narrow DynamoDB role. The reader can only GetItem and Query, and the admin can only UpdateItem.

Write the tool contract once

We give each Lambda target an inline tool schema, and the gateway accepts a small subset of JSON Schema. Keywords such as enum, pattern and maxLength aren't in it. The CDK stack and our local test server import the same file:
1
2
3
4
5
6
7
8
9
10
11
12
13
export const cancelOrder: ToolDefinition = {
name: 'cancel_order',
description:
'Cancel an open order. This cannot be undone. Only open orders can be cancelled; shipped orders are refused.',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID, for example ord-1001' },
reason: { type: 'string', description: 'Why the order is being cancelled, 3 to 200 characters' },
},
required: ['orderId', 'reason'],
},
};
 
The description carries the rules the schema can't, because the model reads it. The handler enforces them anyway. It checks the ord- format, the reason's length and rejects unknown fields, because anything IAM allows can invoke the function.

Change the handler for the gateway

AWS says an existing function needs code changes before it works as a gateway tool. The arguments arrive as the whole event. The gateway ID, the request IDs and the full tool name arrive in the Lambda client context. Our parser finds the tool and strips the prefix:
1
2
3
4
5
6
function customOf(context: Pick<Context, 'clientContext'>): CustomContext | undefined {
// The Node.js runtime hands over the client context JSON as sent. AWS documents
// the key as `custom`; @types/aws-lambda types it as `Custom`. Accept either.
const cc = context.clientContext as unknown as { custom?: CustomContext; Custom?: CustomContext } | undefined;
return cc?.custom ?? cc?.Custom;
}
 
The two spellings are a real gap. AWS's Python sample reads custom, and the TypeScript types say Custom, so we read both until a deploy shows which one Node.js gets. One more trap: AWS's Python boilerplate sets toolName and then compares tool_name. Copy it as is and every call misses.
 
Each tool returns JSON. Failures the model can act on, such as NOT_FOUND or NOT_CANCELLABLE, come back as { error } with a stable code. The log line records the tool and the IDs but leaves out the arguments, because they can hold customer data.

What we got wrong first: a useless clause

UpdateItem creates an item when the key doesn't exist. We didn't want a typo in an order ID to create a phantom cancelled order. So our first condition was attribute_exists(orderId) AND #s = :open.
 
Our mutation script removes each safeguard and runs the tests that should notice. It dropped attribute_exists, and all 9 engine tests against DynamoDB Local still passed. A missing item has no status, so #s = :open already fails on it, and the extra clause added nothing. We cut it:
1
2
3
4
UpdateExpression: 'SET #s = :cancelled, cancelledAt = :now, cancelReason = :reason',
ConditionExpression: '#s = :open',
ReturnValues: 'ALL_NEW',
ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
 
ALL_OLD hands back the item as it was when the condition failed, so one call tells "already cancelled" from "shipped" from "not found". A second cancel reports success and changes nothing, so retries are safe. Ten concurrent cancels of one order produced one success.

Check the caller with Cognito tokens

The gateway's CUSTOM_JWT authorizer validates JSON Web Tokens (JWTs) against Cognito's discovery URL. Each agent is a Cognito app client using the client-credentials flow, with no human users in the pool.
 
We check allowedClients, not allowedAudience. A Cognito access token has an aud claim only when the client asks for a resource binding, so an audience check would reject our agents. Every token has client_id. Our first docs said Cognito access tokens don't carry aud at all, and pull request 3 corrected that.
 
Outbound, the gateway calls Lambda with its own IAM role (GATEWAY_IAM_ROLE). That role may invoke the two functions and ask the policy engine, nothing else.

Gate the destructive tool with AgentCore Policy

A valid token proves who the agent is. AgentCore Policy decides what it may do. You write policies in Cedar, the engine denies anything no permit matches, and every JWT claim becomes a tag on the principal. Our cancel policy, as the stack renders it:
1
2
3
4
5
6
7
8
9
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"orders-admin___cancel_order",
resource == AgentCore::Gateway::"${GatewayArn}"
)
when {
principal.hasTag("scope") &&
principal.getTag("scope") like "*orders/admin*"
};
 
Check hasTag before getTag, because reading a missing tag is an error, and errors deny. The like match runs on the whole scope string. Don't add a scope like orders/administrator, whose name contains orders/admin.
 
Policy also filters tools/list: the reader agent gets two tools. A model that guesses the cancel tool's name anyway still gets a denial from the engine. Deploy with -c policyMode=LOG_ONLY first to see decisions in the logs before they block anything.

Wire the AgentCore Gateway with CDK

aws-cdk-lib has low-level (L1) constructs for all four resources: CfnGateway, CfnGatewayTarget, CfnPolicyEngine and CfnPolicy. Each target points at a function and carries its slice of the tool definitions:
1
2
3
4
5
6
new agentcore.CfnGatewayTarget(this, id, {
gatewayIdentifier: gateway.attrGatewayIdentifier,
name,
targetConfiguration: { mcp: { lambda: { lambdaArn: fnArn, toolSchema: toolSchema(tools) } } },
credentialProviderConfigurations: [{ credentialProviderType: 'GATEWAY_IAM_ROLE' }],
});
 
Order matters. Creating a policy checks each action against the gateway's tools, so both policies depend on both targets. The identity that creates a policy also needs bedrock-agentcore:InvokeGateway, or the policy lands in CREATE_FAILED. A test fails if anyone reorders those resources.
 
List MCP version 2025-03-26 in the gateway's supportedVersions. A request without an MCP-Protocol-Version header counts as that version, and the gateway rejects it if the version isn't listed. The official TypeScript client sends its first initialize call with no header, and our first stack didn't list the version, which would have locked that client out.

Test the tool contract without AWS

The gateway's JWT check and Policy only exist in AWS, but the tool contract doesn't. Our local MCP server lists the same definitions under the same target___tool names. It builds the same client context and calls the real handlers. The official MCP TypeScript client talks to it over Streamable HTTP on /mcp, and one test fails if the local tool list differs from the schema CDK deploys.
 
npm test runs 61 tests: unit, handler flow, MCP contract, CDK template, cdk-nag and bundle loading. npm run test:engine runs 9 more against DynamoDB Local, including 5 MCP clients racing to cancel one order. The mutation check breaks 14 safeguards one at a time on every CI run, and some test catches each.

Work out the monthly cost

AgentCore Gateway, AgentCore Policy and Cognito machine-to-machine tokens have no free tier. Take 25,000 agent sessions a month in us-east-1. Each lists tools once and makes three calls, for 100,000 MCP operations:
LineMathMonthly
Gateway operations100,000 × $0.000005$0.50
Policy checksUp to 100,000 × $0.000025$2.50
Cognito tokens, cached for their 60-minute life2 agents × 24 × 30 = 1,440 × $0.00225$3.24
Lambda75,000 requests, 50 ms each at 256 MB$0.03
DynamoDB60,000 reads, 30,000 write units$0.03
TotalAbout $6.30
 
The token line is the trap. If every session fetches its own token, the month needs 25,000 tokens: 25,000 × $0.00225 = $56.25. That's about 18 times the other lines combined, and the month goes from $6.30 to about $59. Cache the token and refresh it a few minutes before it expires.
 
Two numbers are assumptions. We count tool lists as policy checks, so $2.50 is an upper bound. And 50 ms per Lambda call is our guess, but the Lambda free tier covers that line either way.

State what we couldn't confirm

Two behaviours need a real deploy to settle:
  • What the gateway returns when Policy denies a tools/call: a JSON-RPC error or a result with isError. The smoke test accepts either.
  • Whether Node.js receives the client context as custom or Custom. The handler reads both.
 
AWS documents a 401 for a request with no token, and the smoke test checks for it, but we haven't seen it from a deployed gateway. CloudFormation hasn't created these AgentCore resources from our template yet. Policy validation (FAIL_ON_ANY_FINDINGS) hasn't run against a real gateway either.

When this isn't the right fit

  • Tools that already sit behind a described API: an OpenAPI schema, Smithy model or API Gateway target reuses that description.
  • Agents that call AWS with IAM, not tokens: use the AWS_IAM authorizer and skip Cognito.
  • Rules beyond JSON types: the gateway won't enforce them, so the handler code stays yours.
  • Cost-sensitive traffic at high volume: Policy checks cost five times as much as gateway operations, so price that line first.

Try it yourself

You need Node.js 24, Docker for DynamoDB Local, and for a deploy a CDK-bootstrapped account in a Region with Gateway and Policy. Run these commands:
1
2
3
4
npm ci
npm run check # type check, lint, tests, synth with cdk-nag
npm run deploy # add -c domainPrefix=<unique> if the Cognito domain is taken
npm run seed && npm run smoke
 
The smoke test checks four things only AWS can show. A request with no token gets 401, and the reader agent doesn't see cancel_order. The reader can't cancel, and the ops agent can. We haven't run it, so please tell us what breaks.

Clean up

Run npm run destroy. It deletes the gateway, targets, policy engine, functions, user pool, table and log groups, because they hold sample data only. Change the removal policies before you put real orders in the table.
 
If you gave an agent one of your Lambda functions this month, which one would go first? And what should the agent prove before it calls a write tool?
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article