Como construir um MCP server sem estado no AWS Lambda com o TypeScript SDK v2
Com a revisão 2026-07-28 do protocolo MCP e o TypeScript SDK v2, um servidor de ferramentas para agentes de código deixou de precisar de sessão. Isso encaixa o MCP server direto numa function do Lambda, sem tabela de sessão nem sticky session.
Por Danilo Barros19 min de leitura
Com a revisão 2026-07-28 do protocolo MCP e o TypeScript SDK v2, um servidor de ferramentas para agentes de código deixou de precisar de sessão. Isso encaixa o MCP server direto numa function do Lambda, sem tabela de sessão nem sticky session.
Com a revisão 2026-07-28 do protocolo MCP e o TypeScript SDK v2, um servidor de ferramentas para agentes de código deixou de precisar de sessão. Isso encaixa o MCP server direto numa function do Lambda, sem tabela de sessão nem sticky session.
Até a revisão 2025 do Model Context Protocol (MCP), colocar um servidor remoto no Lambda era contorcionismo. O protocolo girava em torno de um handshake com initialize e um Mcp-Session-Id que amarrava as chamadas seguintes à mesma instância. Lambda esquece tudo entre invocações e pode atender a mesma sessão em instâncias diferentes a cada chamada, então rodar sem estado era tratado como caso especial, cheio de limitação.
Num guia publicado na DEV Community, o desenvolvedor identificado como sascha-ms mostra que isso mudou com a revisão 2026-07-28 do MCP: o protocolo base passou a ser descrito como stateless, com cada requisição se bastando e negociando capacidades por conta própria. O TypeScript SDK v2 foi atrás: a função createMcpHandler monta um McpServer novo a cada requisição HTTP, e clientes antigos que ainda falam a revisão 2025 caem automaticamente num modo stateless de compatibilidade. Isso é exatamente o perfil que o Lambda foi feito para atender.
Para quem desenvolve, o ganho é concreto: sem tabela de sessão no DynamoDB, sem sticky session no load balancer, sem caminho especial. O artigo original já tinha mostrado como montar um MCP server local sobre stdio; esse funciona bem para um usuário só. Quando um time inteiro, ou vários agentes, precisa das mesmas ferramentas, entra em cena um servidor remoto com HTTP e autenticação de verdade. É esse servidor que este guia reproduz, com um exemplo de aluguel de bicicletas.
O que você vai construir
O exemplo da fonte é um MCP server fictício de aluguel de bicicletas com três estações. Ele expõe duas tools: list_stations, que lista as estações, e check_availability, que devolve as bikes disponíveis numa estação específica. Os dados ficam hardcoded no código só para simplificar; em um projeto real viriam de uma tabela DynamoDB, o que não mudaria a forma como o servidor é construído.
A arquitetura tem quatro peças: uma HTTP API do API Gateway na frente, uma function Lambda rodando o servidor MCP com Hono, um verificador de token que confere JWTs emitidos por um authorization server externo (o seu identity provider), e um documento de metadata publicado pelo próprio servidor conforme a RFC 9728. O fluxo de uma requisição é curto: o cliente manda um POST para /mcp, o API Gateway repassa para o Lambda, o Hono transforma o evento do Lambda num Request padrão da Web, confere o token e entrega a chamada ao handler do MCP, que constrói um servidor novo, roda a tool e responde em JSON.
Um detalhe de design importa para quem só conhece MCP local: o servidor aqui só desempenha o papel de resource server do OAuth. Ele não emite token nenhum, só verifica o que chega. Quando a requisição não traz um token válido, o servidor responde 401 e aponta, no header WWW-Authenticate, para o documento de protected resource metadata, que diz ao cliente onde buscar um token.
Pré-requisitos
Antes de abrir o terminal, confira a lista:
- Node.js 24 e npm (o Lambda oferece
nodejs24.xcomo runtime) - Conhecimento básico de TypeScript; ajuda ter visto antes como um MCP server é estruturado
- Para o deploy: uma conta AWS, credenciais configuradas no terminal e o Serverless Framework v4
- Um authorization server OAuth que emita access tokens como JWT e consiga setar a audience (
aud) para a URL do seu MCP server
Os passos 1 a 6 não exigem conta AWS nem authorization server real: os testes geram os próprios tokens localmente. Só o deploy final precisa de credenciais.
Nota da redação: as seções sobre permissões de IAM, os cenários de teste, o deploy via Serverless Framework, o
serverless removee a cobrança da AWS descrevem um roteiro proposto pela redação a partir da estrutura do projeto, sem confirmação no trecho de fonte disponível (que termina antes dessas partes). Valide esses comportamentos na documentação oficial da AWS e do Serverless Framework antes de aplicar em produção.
Passo a passo
1. Crie o projeto e instale as dependências. No SDK v2, o antigo pacote @modelcontextprotocol/sdk foi dividido: o servidor vem de @modelcontextprotocol/server, o cliente de teste de @modelcontextprotocol/client. A lib jose cuida da verificação de assinatura do JWT.
mkdir bike-rental-mcp && cd bike-rental-mcp
npm init -y
npm install @modelcontextprotocol/server@2.4.0 @hono/aws-lambda@1.0.1 hono@4.13.14 jose@6.2.13 zod@4.6.7
npm install --save-dev @modelcontextprotocol/client@2.4.0 @types/aws-lambda@8.10.164 @types/node@24.19.2 typescript@7.0.2
mkdir -p src testNo package.json, o detalhe que não pode faltar é "type": "module", porque o SDK é distribuído como ES module. No tsconfig.json, adicione "types": ["node"]: desde o TypeScript 6, os pacotes @types não entram mais automaticamente, e as definições do SDK dependem dos tipos do Node.
2. Defina as tools. Os dados de exemplo são uma lista de estações e uma função de busca:
// src/stations.ts
export interface Station {
id: string;
name: string;
district: string;
bikes: { city: number; ebike: number; cargo: number };
}
export const stations: Station[] = [
{ id: 'hbf', name: 'Central Station', district: 'Downtown', bikes: { city: 12, ebike: 4, cargo: 1 } },
{ id: 'hafen', name: 'Harbour', district: 'North', bikes: { city: 3, ebike: 0, cargo: 2 } },
{ id: 'uni', name: 'University', district: 'West', bikes: { city: 20, ebike: 6, cargo: 0 } },
];
export function findStation(id: string): Station | undefined {
return stations.find((station) => station.id === id);
}O servidor em si gira em torno de buildServer, a factory que o createMcpHandler chama a cada requisição:
// src/server.ts
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
import { findStation, stations } from './stations.js';
export function buildServer(): McpServer {
const server = new McpServer({ name: 'bike-rental', version: '1.0.0' });
server.registerTool('list_stations', {
title: 'List stations',
description: 'Lists all rental stations with ID, name and district.',
inputSchema: z.object({}),
annotations: { readOnlyHint: true },
}, async () => {
const list = stations.map(({ id, name, district }) => ({ id, name, district }));
return { content: [{ type: 'text', text: JSON.stringify(list) }], structuredContent: { stations: list } };
});
server.registerTool('check_availability', {
title: 'Check availability',
description: 'Returns available bikes at a station, split by type.',
inputSchema: z.object({ stationId: z.string().min(1) }),
}, async ({ stationId }, ctx) => {
const station = findStation(stationId);
if (!station) {
return { isError: true, content: [{ type: 'text', text: `Unknown station "${stationId}".` }] };
}
const caller = ctx.http?.authInfo?.clientId ?? 'unknown';
console.log(JSON.stringify({ tool: 'check_availability', stationId, caller }));
return { content: [{ type: 'text', text: JSON.stringify(station.bikes) }], structuredContent: { stationId: station.id, ...station.bikes } };
});
return server;
}Dois pontos valem atenção. Para estação desconhecida, a tool devolve isError: true em vez de lançar exceção, para o modelo conseguir se corrigir lendo a mensagem. E ctx.http?.authInfo?.clientId lê quem está chamando, dado que só existe quando o token já foi verificado; por HTTP ele vem preenchido, por stdio fica undefined.
3. Verifique os tokens. O SDK não valida token sozinho, ele espera um verifier. Aqui a verificação é local, via chave pública do authorization server (JWKS):
// src/auth.ts
import { OAuthError, OAuthErrorCode, type AuthInfo, type OAuthTokenVerifier } from '@modelcontextprotocol/server';
import { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose';
export interface VerifierConfig {
issuer: string;
resource: URL;
keys: JWTVerifyGetKey;
}
export function createJwtVerifier(config: VerifierConfig): OAuthTokenVerifier {
return {
async verifyAccessToken(token: string): Promise<AuthInfo> {
try {
const { payload } = await jwtVerify(token, config.keys, {
issuer: config.issuer,
audience: config.resource.href,
});
const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ').filter(Boolean) : [];
return {
token,
clientId: String(payload.client_id ?? payload.sub ?? 'unknown'),
scopes,
expiresAt: payload.exp,
resource: config.resource,
};
} catch {
throw new OAuthError(OAuthErrorCode.InvalidToken, 'Token invalid or expired');
}
},
};
}
export function remoteKeys(jwksUrl: string): JWTVerifyGetKey {
return createRemoteJWKSet(new URL(jwksUrl));
}Três regras não têm exceção: expiresAt precisa vir preenchido, senão o SDK rejeita todo token com 401; a audience tem que bater exatamente com a URL do seu servidor, senão um token válido para outro serviço funcionaria no seu; e tokens inválidos precisam virar OAuthError do tipo InvalidToken, porque qualquer outra exceção o SDK transforma em 500.
4. Monte a camada HTTP com Hono. O app.ts recebe a configuração de fora, o que permite usar um verifier de teste nos testes e o real no Lambda:
// src/app.ts
import { createMcpHandler, getOAuthProtectedResourceMetadataUrl, requireBearerAuth, type OAuthTokenVerifier } from '@modelcontextprotocol/server';
import { Hono } from 'hono';
import { buildServer } from './server.js';
export interface AppConfig {
resource: URL;
issuer: string;
verifier: OAuthTokenVerifier;
requiredScope: string;
}
export function createApp(config: AppConfig): Hono {
const metadataUrl = getOAuthProtectedResourceMetadataUrl(config.resource);
const mcp = createMcpHandler(buildServer, { responseMode: 'json' });
const gate = requireBearerAuth({
verifier: config.verifier,
requiredScopes: [config.requiredScope],
resourceMetadataUrl: metadataUrl,
expectedResource: config.resource,
});
const app = new Hono();
app.get(new URL(metadataUrl).pathname, (c) =>
c.json({
resource: config.resource.href,
authorization_servers: [config.issuer],
scopes_supported: [config.requiredScope],
bearer_methods_supported: ['header'],
})
);
app.all('/mcp', async (c) => {
const auth = await gate(c.req.raw);
if (auth instanceof Response) return auth;
return mcp.fetch(c.req.raw, { authInfo: auth });
});
return app;
}A opção responseMode: 'json' garante que o handler nunca abra um stream SSE e devolva sempre uma resposta JSON única, o que faz sentido atrás de uma HTTP API do API Gateway, que só entrega a resposta depois de completa de qualquer jeito. O trade-off: notificações de progresso durante uma chamada de tool se perdem, e o SDK avisa isso no log ao subir.
5. Escreva o entry point do Lambda. O handler lê a configuração de variáveis de ambiente e monta o app uma vez por cold start:
// src/handler.ts
import { handle } from '@hono/aws-lambda';
import { createApp } from './app.js';
import { createJwtVerifier, remoteKeys } from './auth.js';
function env(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Environment variable ${name} is missing`);
return value;
}
const resource = new URL(env('MCP_RESOURCE_URL'));
const issuer = env('AUTH_ISSUER');
const app = createApp({
resource,
issuer,
requiredScope: env('MCP_REQUIRED_SCOPE'),
verifier: createJwtVerifier({ issuer, resource, keys: remoteKeys(env('AUTH_JWKS_URL')) }),
});
export const handler = handle(app);O createRemoteJWKSet só busca as chaves do authorization server na chegada do primeiro token e as mantém em memória depois disso. Numa instância quente do Lambda, isso evita bater no authorization server a cada requisição, o que importa tanto para latência quanto para não estourar rate limit do identity provider.
Testando
O ponto que a fonte destaca como mais importante é testar sem precisar de conta AWS nem de authorization server de verdade. O truque é um fetch customizado que o cliente MCP usa para falar direto com app.request do Hono, sem rede nenhuma. O teste gera seu próprio par de chaves RSA com generateKeyPair('RS256') da jose, expõe um JWKS local com createLocalJWKSet e assina tokens de teste com SignJWT, setando issuer, audience e tempo de expiração.
Com essa estrutura, dá para montar um roteiro de testes que cubra, por exemplo, requisição sem token, token com audience errada, chamada bem-sucedida das duas tools com token válido e uma stationId inexistente. Esse roteiro é uma sugestão da redação a partir da estrutura do projeto, e não um conjunto de casos confirmado pela fonte original. Rodar npm run test compila com tsc e executa os arquivos em dist/test/*.test.js com o runner nativo do Node, sem dependência externa de framework de teste.
Limpando os recursos
O deploy com o Serverless Framework v4 e o comando serverless remove para desfazer os recursos criados são o caminho esperado para este tipo de projeto, mas os detalhes específicos (permissões de IAM exigidas, o que exatamente a stack do CloudFormation inclui, o comportamento do serverless remove em relação a log groups do CloudWatch e a secrets, e as regras de cobrança da camada gratuita da AWS) não constam no trecho de fonte disponível para este artigo.
Antes de rodar o deploy ou de assumir que a limpeza foi completa, confira a documentação oficial do Serverless Framework e da AWS para confirmar permissões necessárias, recursos criados e eventuais itens que precisam ser removidos manualmente, como log groups retidos ou secrets configurados à parte.
Próximos passos
O exemplo guarda as estações de bike direto no código só para isolar o que importa: trocar esse array por uma tabela DynamoDB não muda nada na forma como o servidor MCP é construído, só na função findStation. Vale também revisar as descrições das tools com o mesmo cuidado de um README público, porque é só isso que o modelo de linguagem enxerga sobre o que a tool faz, sem acesso ao código por trás.
No lado de autenticação, o guia implementa a verificação mínima exigida pela especificação (assinatura, expiração, issuer e audience), mas um ambiente de produção normalmente soma rate limiting por clientId, rotação de chaves no identity provider e observabilidade sobre tentativas de token inválido, já que qualquer servidor exposto por URL atrai varredura automatizada. E para quem já roda MCP server local sobre stdio, a migração real é de arquitetura, não de protocolo: o buildServer muda de dono (de um processo seu para uma function efêmera), mas a lógica das tools continua a mesma.
Fonte 1: DEV Community — #aws (https://dev.to/sascha-ms/run-an-mcp-server-on-lambda-a-typescript-guide-2597)
Fonte 1: DEV Community — #aws (https://dev.to/sascha-ms/run-an-mcp-server-on-lambda-a-typescript-guide-2597)
Run an MCP Server on Lambda: A TypeScript Guide - DEV Community
Running an MCP server on Lambda used to feel like a hack to me: the protocol leaned on sessions, Lambda forgets everything after each invocation, and workarounds held the two together. The MCP revision 2026-07-28 and TypeScript SDK v2 turned that around. Every request now stands on its own, and the server no longer needs any state. That is exactly the profile Lambda was built for.
In my article Build an MCP server with TypeScript, the server ran locally over stdio. That works fine when you are the only user. As soon as a whole team or several agents need the same tools, though, you need a remote server with HTTP and proper sign-in. In this guide I show you, step by step, how that looks with Lambda, Hono and the Serverless Framework.
In short
With revision 2026-07-28, MCP is stateless, and createMcpHandler from SDK v2 builds a fresh server for every request. That lets an MCP server fit straight into a Lambda function behind an HTTP API. The biggest pitfall is not the code but authentication: without token checks that include the right audience (aud), your server is open to anyone who knows the URL.
What you will learn in this article
Why MCP revision 2026-07-28 and Lambda fit together well
How to wrap an MCP server in a Lambda function with Hono and createMcpHandler
How to verify bearer tokens per the MCP spec, including scope and audience
How to test everything locally, without an AWS account or a real authorization server
How to deploy with the Serverless Framework and connect the server to Claude Code
Why an MCP server on Lambda makes sense now
A quick bit of context first: the Model Context Protocol (MCP) is an open standard that lets AI applications such as Claude Code talk to external tools and data. An MCP server provides those tools, and the client inside the AI tool calls them. Locally this usually runs over stdio, meaning the input and output of a process on your machine. Remotely it runs over HTTP, a transport the protocol calls Streamable HTTP.
In the 2025 revisions, the protocol was designed around a connection with a handshake: the client started with initialize, and many servers issued an Mcp-Session-Id that later requests were tied to. For Lambda that is awkward, because every request can land on a different instance. Stateless operation was possible, but it was treated as a special case with limitations. If you wanted sessions, you needed shared storage or some trickery.
Revision 2026-07-28 explicitly describes the base protocol as stateless: requests are self-contained, and capabilities are negotiated per request. SDK v2 follows through on that. The createMcpHandler function takes a factory and uses it to build a new McpServer instance for each HTTP request. Older clients that still speak the 2025 revision are served by a stateless fallback by default. For you, this means no session table in DynamoDB, no sticky sessions and no special path.
When a remote server pays off
Not every MCP server belongs in the cloud. A server that reads your local files is better off staying local. A remote server pays off when the data already lives in the cloud, several people use the same tools, or you want central control over who may do what. Think of a tool that queries stock levels from a database, or one that creates tickets in an internal system.
The architecture at a glance
As an example, let us take a fictional bike rental with three stations. The MCP server offers two tools: list_stations lists the stations, and check_availability returns the available bikes at a station. To keep things simple, the data lives in the code. In a real project it would come from DynamoDB, which would not change how the server is built.
The setup: an HTTP API in front of a Lambda function, with tokens issued by an external authorization server.
How a request travels through the stack
A request takes a short path: the client sends a POST to /mcp. API Gateway passes it on to the Lambda function. There, Hono translates the Lambda event into a standard Request object, checks the token and hands the request to the MCP handler. The handler builds a fresh server, runs the tool and responds with JSON.
Hono is a small web framework built on the web standards Request and Response. That is exactly the interface SDK v2 expects. Hono ships a ready-made adapter for Lambda, so you do not have to convert API Gateway events by hand.
What the server needs to know about sign-in
For HTTP servers, the MCP specification relies on OAuth. Your MCP server only plays the role of the resource server: it does not issue tokens, it checks them. They are issued by an authorization server, for example your existing identity provider. So that a client knows where to get a token, the MCP server publishes a small JSON document under RFC 9728, the protected resource metadata. When a request arrives without a valid token, the server answers with 401 and points to this document in the WWW-Authenticate header.
Prerequisites
Node.js 24 and npm (Lambda offers nodejs24.x as a runtime)
Basic TypeScript knowledge; ideally you know how an MCP server is structured from the introductory article
For deployment: an AWS account, AWS credentials in your terminal and the Serverless Framework v4
An OAuth authorization server that issues access tokens as JWTs and can set the audience (aud) to the URL of your MCP server
Steps 1 to 6 need neither AWS nor an authorization server, so you build your MCP server on Lambda completely locally first. The tests create their own tokens.
MCP server on Lambda step by step
Step 1: Create the project
Create a new project and install the packages. In SDK v2, the former @modelcontextprotocol/sdk package is split up: you build the server with @modelcontextprotocol/server, and the client for the tests comes from @modelcontextprotocol/client. The jose library will verify the JWT signatures later on.
Next, adjust the package.json. The important part is "type": "module", because the SDK ships as an ES module.
The tsconfig.json needs "types": ["node"]. Since TypeScript 6, @types packages are no longer included automatically, and the SDK's type definitions refer to Node types.
Step 2: Define the tools
First, the sample data. A list and a lookup function are all you need.
Now for the actual MCP server. The buildServer function is the factory that gets called per request later. Pay attention to the tool descriptions: they are the only thing the language model learns about your tool.
Two details deserve a second look. For an unknown station, the tool reports isError: true instead of throwing an exception. That way the error message reaches the model, which can correct itself. In addition, the tool reads who is calling via ctx.http?.authInfo. This data comes from the token check in the next step and is undefined over stdio, hence the question mark.
Step 3: Verify tokens
The SDK does not verify tokens itself; it expects a verifier. The verifier receives the raw token and returns an AuthInfo object. Here I verify the JWT locally: the signature via the authorization server's public keys (JWKS), the expiry, the issuer and the audience.
Three points are mandatory here. The expiresAt field must be set, otherwise the SDK rejects every token with 401. The audience must match your server's URL, so that a token for another service does not work on yours. And you report invalid tokens with an OAuthError of type InvalidToken, because the SDK turns any other exception into a 500.
Step 4: The HTTP layer with Hono
Everything comes together in app.ts. The createApp function receives its configuration from outside. As a result, you can start it with a test verifier in your tests and with the real one in Lambda.
The responseMode: 'json' option makes sure the handler never opens an SSE stream and returns exactly one JSON response instead. Behind API Gateway that is the honest choice, because the HTTP API only delivers responses once they are complete anyway. The trade-off: progress notifications during a tool call get lost. The SDK points this out with a warning in the log at startup.
Meanwhile, the gate from requireBearerAuth returns either the verified token data or a ready-made 401 or 403 response. The second route serves the protected resource metadata. Its path comes from getOAuthProtectedResourceMetadataUrl: https://…/mcp becomes https://…/.well-known/oauth-protected-resource/mcp.
Step 5: The Lambda entry point
The handler reads its configuration from environment variables and builds the app once per cold start. For the rest, you use the handle adapter from the @hono/aws-lambda package. The older hono/aws-lambda import still works, but it is marked as deprecated and goes away with Hono 5.
createRemoteJWKSet loads the JWKS keys only when the first token arrives and keeps them in memory afterwards. A warm Lambda instance therefore does not ask the authorization server again on every request.
Step 6: Test locally without AWS
Now for the part I consider most important for an MCP server on Lambda: tests that run without a network. The trick is a custom fetch for the MCP client that goes straight into Hono's app.request. The test signs its tokens with a key pair it generates itself.
Fonte: DEV Community — #aws
Serviços neste textoMCP
Este artigo foi escrito por Danilo Barros. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.
Editor virtual de Comunidade & Eventos. Cobre o re:Invent, o AWS Summit São Paulo, os Community Days, os user groups brasileiros, os AWS Heroes e Community Builders, e o que sai dos palcos em vídeo. Conta o evento pelo que o leitor leva para o trabalho, não pela pirotecnia.