Skip to content

Instantly share code, notes, and snippets.

@dreamorosi
Created April 6, 2025 17:17
Show Gist options
  • Save dreamorosi/98f0c3809a05935733e79c7ad6d1fead to your computer and use it in GitHub Desktop.
Save dreamorosi/98f0c3809a05935733e79c7ad6d1fead to your computer and use it in GitHub Desktop.
A super-basic MCP Server hosted on AWS Lambda

To deploy, create a Lambda function and enable function URL (no auth - yolo), then use the handler above in your function. That same implementation will also work with API Gateway HTTP (aka v2), if you want to use ALB or API Gateway REST (aka v1) you should swap the schema used for parsing.

Then you can test using a POST request with this body:

{
  "jsonrpc": "2.0",
  "method": "tools/list",
  "id": 2
}

Below the same request, but made with httpie:

http POST <your url here> "Content-Type":application/json jsonrpc="2.0" method="tools/list" id=2
import { Logger } from '@aws-lambda-powertools/logger';
import { LambdaFunctionUrlSchema } from '@aws-lambda-powertools/parser/schemas/lambda';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import {
type JSONRPCMessage,
JSONRPCMessageSchema,
} from '@modelcontextprotocol/sdk/types.js';
import type { Context } from 'aws-lambda';
import { z } from 'zod';
const logger = new Logger({
serviceName: 'mcp-lambda',
logLevel: 'INFO',
});
const server = new McpServer({
name: 'MCP Server on AWS Lambda',
version: '1.0.0',
});
// Add an addition tool
server.tool('add', { a: z.number(), b: z.number() }, async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }],
}));
const isMessageWithId = (
message: JSONRPCMessage
): message is JSONRPCMessage & { id: number | string } =>
'id' in message &&
(typeof message.id === 'number' || typeof message.id === 'string');
class HttpServerTransport implements Transport {
#pendingRequests = new Map<
number | string,
{
resolve: (message: JSONRPCMessage) => void;
reject: (error: Error) => void;
}
>();
public onmessage?: (message: JSONRPCMessage) => void;
public start = async () => {};
public close = async () => {};
public send = async (message: JSONRPCMessage) => {
if (isMessageWithId(message)) {
const pendingRequest = this.#pendingRequests.get(message.id);
if (pendingRequest !== undefined) {
pendingRequest.resolve(message);
this.#pendingRequests.delete(message.id);
}
}
};
#startFreshSession = () => {
this.#pendingRequests.clear();
};
public resolve = async (
jsonRPCMessages: z.infer<typeof JSONRPCMessageSchema>[]
): Promise<JSONRPCMessage[] | JSONRPCMessage | undefined> => {
this.#startFreshSession();
jsonRPCMessages.map((message) => {
this.onmessage?.(message);
});
const messagesWithId = jsonRPCMessages.filter(isMessageWithId);
if (messagesWithId.length > 0) {
return await Promise.all(
messagesWithId.map(
(message) =>
new Promise<JSONRPCMessage>((resolve, reject) => {
this.#pendingRequests.set(message.id, { resolve, reject });
})
)
);
}
return new Promise<JSONRPCMessage>((resolve, reject) => {
this.#pendingRequests.set(messagesWithId[0].id, { resolve, reject });
});
};
}
const mcpEvent = LambdaFunctionUrlSchema.extend({
headers: z.object({
'content-type': z.literal('application/json'),
accept: z.literal('application/json'),
}),
body: z.unknown(),
})
.transform((data, ctx) => {
const { body, isBase64Encoded } = data;
if (typeof body !== 'string') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Body must be a string',
});
return z.NEVER;
}
try {
const decodedBody = isBase64Encoded
? Buffer.from(body, 'base64').toString()
: body;
const parsedJSONBody = JSON.parse(decodedBody);
data.body = Array.isArray(parsedJSONBody)
? parsedJSONBody
: [parsedJSONBody];
data.isBase64Encoded = false;
return data;
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Failed to parse or decode JSON body',
});
return z.NEVER;
}
})
.refine(
(data) => {
const { body } = data;
if (Array.isArray(body)) {
return body.every(
(message) => JSONRPCMessageSchema.safeParse(message).success
);
}
return JSONRPCMessageSchema.safeParse(body).success;
},
{
message: 'Invalid JSON-RPC message format',
}
);
const transport = new HttpServerTransport();
await server.connect(transport);
export const handler = async (event: unknown, context: Context) => {
const parseResult = mcpEvent.safeParse(event);
if (!parseResult.success) {
logger.error('Invalid event format', parseResult.error);
return {
statusCode: 400,
body: JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: Invalid event format',
},
id: null,
}),
};
}
const { body } = parseResult.data;
const responseMessages = await transport.resolve(
body as z.infer<typeof JSONRPCMessageSchema>[]
);
if (responseMessages === undefined) {
return { statusCode: 202, body: '' };
}
return { statusCode: 200, body: JSON.stringify(responseMessages) };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment