Ever wondered how Claude can keep context across dozens of calls without re‑sending the whole prompt each time? The Model Context Protocol (MCP) makes that possible, and you can host it in a serverless Lambda in minutes. Let’s demystify the wire‑format and see a real implementation.

What Is MCP and Why It Matters

When you talk to a large language model (LLM) like Claude, you normally send a big text block that contains everything the model needs to remember: the system prompt, user messages, and any prior assistant replies. This works, but it wastes bandwidth and token budget when the conversation grows long.

MCP, short for Model Context Protocol, is a lightweight way to tell the model “here is the next piece of the conversation, and here’s the token that represents everything that came before.” Instead of re‑sending the whole history, you send a small session token that the model expands internally.

In plain English: MCP is like handing a librarian a library card (the token) instead of re‑handing the entire stack of books every time you ask for the next chapter.

Why care about this?

  1. Speed – Less data over the network means lower latency, especially when you are calling the model from a serverless function that may already be warm only for a few milliseconds.
  2. Cost – Tokens you re‑send are tokens you pay for. A session token can be as short as a few characters, saving you money on the model’s per‑token pricing.
  3. Scalability – A single Lambda can handle many concurrent conversations without blowing up its memory budget, because it never stores the full chat history locally.

The trade‑off is that you must respect the MCP wire‑format, a JSON structure that the model expects. If you break it, Claude will think you started a brand‑new session and you’ll lose all the context you worked so hard to build.

The MCP Wire‑Format – A Quick Walkthrough

MCP messages are an array of objects, each object representing a single turn in the conversation. The most important fields are:

Field Meaning Example
role Who is speaking: "user" or "assistant" "user"
content The actual text of the message "What is the weather in Paris?"
session_token Optional short string that refers to the model’s internal memory of earlier turns "eyJzZXNzaW9uIjoiYWJjMTIzIn0="

A complete MCP payload looks like this:

{ "messages": [ { "role": "system", "content": "You are a helpful travel guide." }, { "role": "user", "content": "Tell me about the Eiffel Tower." }, { "role": "assistant", "content": "The Eiffel Tower is ...", "session_token": "abc123" }, { "role": "user", "content": "What are the opening hours?" } ] } 

Notice that the third message (the assistant’s reply) carries a session_token. When the next user turn arrives, you include that same token so Claude knows it belongs to the same conversation.

Key takeaway: The array must be sent as a single JSON object. Splitting it into separate HTTP calls makes Claude think each call is a brand‑new conversation.

A Simple Analogy

Think of a chat as a train. Each car (message) is coupled to the one before it. The session_token is the train’s unique identifier. If you try to add a new car by sending a separate request without the identifier, the railway system treats it as a different train altogether.

Setting Up a Lambda‑Backed MCP Proxy

Now that we understand the format, let’s create a tiny HTTP endpoint that accepts MCP payloads and forwards them to Claude via Bedrock. We’ll use AWS Lambda as the compute engine because it gives us instant scaling and no servers to manage.

1️⃣ Create the Lambda function

In the AWS console, choose Node.js 22.x as the runtime. This is the newest LTS version, but be aware of a known gotcha: the require('esm') hack that many older layers use no longer works out‑of‑the‑box. If you depend on a layer that adds ESM support, you’ll see silent failures at runtime. The safest route is to keep your code fully ESM (use import statements) or avoid the layer altogether.

2️⃣ Add the SDK packages

Add the following dependencies to your package.json:

{ "dependencies": { "@aws-sdk/client-lambda": "^3.600.0", "@aws-sdk/client-bedrockruntime": "^3.600.0" } } 

The first SDK lets us invoke other Lambdas if you ever want to split responsibilities. The second is the actual client that talks to Claude on Bedrock.

3️⃣ Write the handler

Below is a complete TypeScript Lambda handler. It does three things:

  1. Parses the incoming MCP JSON body.
  2. Calls Bedrock’s InvokeModel (via sendMessage) with the same payload.
  3. Streams the model’s response back to the caller, preserving any session_token that Claude returns.
// src/handler.ts import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { BedrockRuntimeClient, InvokeModelCommand, InvokeModelCommandInput, } from '@aws-sdk/client-bedrockruntime'; // Create a Bedrock client once per container (reused across invocations) const bedrockClient = new BedrockRuntimeClient({ region: 'us-east-1' }); /** * Lambda entry point. * Expects a POST request with a JSON body that follows the MCP format. */ export const handler = async ( event: APIGatewayProxyEvent ): Promise<APIGatewayProxyResult> => { // Guard against non‑JSON payloads if (!event.body) { return { statusCode: 400, body: JSON.stringify({ error: 'Missing request body' }), }; } // Parse the incoming MCP payload let mcpPayload: any; try { mcpPayload = JSON.parse(event.body); } catch { return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON' }), }; } // Build the Bedrock request – the model name can be changed to any Claude variant const bedrockInput: InvokeModelCommandInput = { modelId: 'anthropic.claude-3-5-sonnet-20240620-v1:0', // Bedrock expects a raw string payload, not a wrapped object body: JSON.stringify(mcpPayload), contentType: 'application/json', accept: 'application/json', }; // Send the request to Bedrock let bedrockResponse; try { bedrockResponse = await bedrockClient.send(new InvokeModelCommand(bedrockInput)); } catch (err) { console.error('Bedrock call failed', err); return { statusCode: 502, body: JSON.stringify({ error: 'Failed to contact Claude' }), }; } // Bedrock streams the answer back as a Uint8Array; we need to turn it into text const responseText = new TextDecoder('utf-8').decode(bedrockResponse.body); // The response will include a new session_token if Claude wants to keep state // Forward it unchanged so the caller can store it for the next turn return { statusCode: 200, // Important: for streaming responses you must set these headers, otherwise API Gateway buffers everything. headers: { 'Content-Type': 'application/json', // The header below tells API Gateway not to compress the payload (which can break incremental streaming) 'X-Accel-Buffering': 'no', }, body: responseText, }; }; 

Why each line matters

  • import { APIGatewayProxyEvent, APIGatewayProxyResult } – Types that describe the shape of the incoming HTTP request and outgoing response.
  • new BedrockRuntimeClient({ region }) – Creates a client that knows how to talk to the Bedrock service in a specific AWS region.
  • The if (!event.body) guard prevents the function from crashing when a caller forgets to send data.
  • JSON.parse(event.body) turns the raw string into a JavaScript object we can inspect or modify later.
  • InvokeModelCommand – The low‑level API that sends the MCP payload to Claude.
  • new TextDecoder('utf-8').decode(bedrockResponse.body) – Bedrock returns a binary buffer; we need readable text for the API consumer.
  • The response headers include X-Accel-Buffering: no. Without this, API Gateway will buffer the whole answer before sending it back, which defeats the purpose of incremental streaming.

Tip: If you enable SnapStart on this Lambda, remember that SnapStart does not help when the function is attached to a VPC. The cold start time is dominated by the VPC ENI attachment, not the JVM warm‑up.

4️⃣ Deploy

Package the code with npm run build (or use the AWS SAM CLI). Deploy using the console or a CI pipeline. Remember to give the Lambda permission to call bedrock:InvokeModel by attaching the AmazonBedrockFullAccess policy or a more restrictive custom policy.

Calling Claude via Bedrock from Lambda

Now that the proxy exists, let’s look at the exact Bedrock call we made and why it respects MCP.

1️⃣ The sendMessage command (conceptually)

The AWS SDK for Bedrock provides a higher‑level method called sendMessage in some language bindings. In the low‑level InvokeModelCommand we used, the payload is exactly what Claude expects: an object with a messages array that follows the MCP spec.

// This is the shape the model expects interface MCPMessage { role: 'system' | 'user' | 'assistant'; content: string; session_token?: string; // optional, only when you have an existing token } interface MCPPayload { messages: MCPMessage[]; } 

When you include a session_token on the last assistant message, Claude internally retrieves the hidden context that the token represents. The model then appends its new reply to that hidden context and returns a new token for the next round.

In plain English: Think of the token as a secret key that unlocks the conversation history stored inside Claude’s brain.

2️⃣ Handling incremental responses

Claude can return its answer in chunks, which is useful for UI that wants to display text as it arrives. To keep the Lambda simple, we collected the whole stream into a string. If you need true streaming, you must enable Lambda response streaming and set the Content-Type: text/event-stream header. A common gotcha is forgetting this header, which makes API Gateway buffer the entire response and defeats the streaming effect.

// Example of enabling streaming (requires Lambda >= Node 22 and API Gateway v2) return { statusCode: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', // Required for API Gateway to forward chunks immediately 'Transfer-Encoding': 'chunked', }, body: responseText, // each chunk would be written separately in a real streaming scenario }; 

3️⃣ Cost awareness

Provisioned Concurrency guarantees that a certain number of Lambda instances are always warm, which removes cold start latency. However, you still pay for those instances even when they are idle. Many teams have been surprised by the monthly bill when they enable it on a function that only sees occasional traffic.

Tip: Start with on‑demand Lambda and monitor latency. Switch to Provisioned Concurrency only if you consistently need sub‑100 ms response times.

Testing the End‑to‑End Flow

A good tutorial includes a way to verify that everything works as expected. Below is a minimal test using curl and a simple Node script.

1️⃣ Deploy a test endpoint

Assume your Lambda is fronted by an API Gateway HTTP API at https://abcde12345.execute-api.us-east-1.amazonaws.com/mcp.

2️⃣ First request – start a new session

curl -X POST https://abcde12345.execute-api.us-east-1.amazonaws.com/mcp \ -H "Content-Type: application/json" \ -d '{ "messages": [ {"role": "system", "content": "You are a friendly travel assistant."}, {"role": "user", "content": "Hi! What can you tell me about Rome?"} ] }' -o response1.json 

Inspect the result:

cat response1.json 

You should see something like:

{ "messages": [ { "role": "assistant", "content": "Rome is the capital of Italy...", "session_token": "eyJzZXNzaW9uIjoiZDEyMzQ1In0=" } ] } 

Save the token for the next call.

3️⃣ Follow‑up request – keep context

TOKEN=$(jq -r '.messages[0].session_token' response1.json) curl -X POST https://abcde12345.execute-api.us-east-1.amazonaws.com/mcp \ -H "Content-Type: application/json" \ -d "{ \"messages\": [ {\"role\": \"assistant\", \"content\": \"Rome is the capital of Italy...\", \"session_token\": \"$TOKEN\"}, {\"role\": \"user\", \"content\": \"What’s the best time to visit?\"} ] }" -o response2.json 

Now the model’s answer will be tailored to the earlier statement about Rome because the same token was supplied.

Key takeaway: Always include the most recent session_token on the assistant message you send back. Forgetting it resets the conversation.

4️⃣ Automated test script (optional)

// test-mcp.js import fetch from 'node-fetch'; const endpoint = 'https://abcde12345.execute-api.us-east-1.amazonaws.com/mcp'; async function callMCP(messages) { const resp = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages }), }); return resp.json(); } (async () => { // Step 1 – start conversation const first = await callMCP([ { role: 'system', content: 'You are a friendly travel assistant.' }, { role: 'user', content: 'Tell me about Kyoto.' }, ]); console.log('First reply:', first); // Extract token const token = first.messages[0].session_token; // Step 2 – continue with token const second = await callMCP([ { role: 'assistant', content: first.messages[0].content, session_token: token }, { role: 'user', content: 'When is cherry blossom season?' }, ]); console.log('Second reply:', second); })(); 

Run with node test-mcp.js. If you see the second answer referencing Kyoto, you’ve successfully kept the context.

The Takeaway

What you should remember after reading this guide

  • MCP lets you keep a conversation alive with a tiny token instead of re‑sending the whole chat history.
  • The wire‑format must be a single JSON object that contains an array of messages; breaking it into multiple HTTP calls resets the session.
  • A TypeScript Lambda can act as a thin proxy: parse the incoming MCP payload, forward it to Bedrock’s Claude via InvokeModelCommand, and stream the response back.
  • Pay attention to Lambda‑specific quirks: Node 22 ESM layers, SnapStart + VPC, response‑streaming headers, and provisioned concurrency costs.
  • Test end‑to‑end with curl or a small script, and always carry forward the latest session_token on the assistant message you send.

With these pieces in place, you now have a low‑latency, cost‑efficient Claude‑powered AI agent that can run forever in a serverless environment. Happy coding!

Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-11-06 · Primary focus: MCP

All code blocks are intended to be correct and runnable, but please verify them
against the Model Context Protocol spec before using in production.

Find an error? Drop a comment — corrections are always welcome.