Skip to content

Instantly share code, notes, and snippets.

@sholtomaud
Forked from dreamorosi/README.md
Created April 7, 2025 11:55
Show Gist options
  • Save sholtomaud/ec623592cd64a7c766fbc15e7f46c448 to your computer and use it in GitHub Desktop.
Save sholtomaud/ec623592cd64a7c766fbc15e7f46c448 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