Skip to content

Commit d3e0459

Browse files
authored
Merge pull request #163 from QuantGeekDev/feat/serverless-lambda-support
feat: add Lambda/serverless support (handleRequest + createLambdaHandler)
2 parents d6353a6 + bec59ab commit d3e0459

15 files changed

+3326
-91
lines changed

src/core/MCPServer.ts

Lines changed: 363 additions & 91 deletions
Large diffs are not rendered by default.

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,20 @@ export type { RequestContextData } from './utils/requestContext.js';
3737
// Transport utilities
3838
export { validateOrigin, getValidatedCorsOrigin } from './transports/utils/origin-validator.js';
3939
export type { OriginValidationConfig } from './transports/utils/origin-validator.js';
40+
41+
// Serverless / Lambda
42+
export type {
43+
APIGatewayV1Event,
44+
APIGatewayV2Event,
45+
LambdaEvent,
46+
LambdaResult,
47+
LambdaContext,
48+
LambdaHandlerConfig,
49+
} from './serverless/types.js';
50+
export {
51+
lambdaEventToRequest,
52+
responseToLambdaResult,
53+
isV2Event,
54+
getSourceIp,
55+
} from './serverless/lambda-adapter.js';
56+
export type { HandleRequestOptions } from './core/MCPServer.js';

src/serverless/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './types.js';
2+
export * from './lambda-adapter.js';
3+
export { authenticateWebRequest, createIncomingMessageShim } from './web-auth-handler.js';

src/serverless/lambda-adapter.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Lambda adapter utilities.
3+
*
4+
* Pure functions to convert between AWS API Gateway events and
5+
* Web Standard Request/Response objects. No framework dependencies.
6+
*/
7+
8+
import type {
9+
LambdaEvent,
10+
LambdaResult,
11+
APIGatewayV1Event,
12+
APIGatewayV2Event,
13+
APIGatewayV1Result,
14+
APIGatewayV2Result,
15+
} from './types.js';
16+
17+
/** Type guard: true for API Gateway v2 (HTTP API / Function URL) events. */
18+
export function isV2Event(event: LambdaEvent): event is APIGatewayV2Event {
19+
return 'version' in event && (event as any).version === '2.0';
20+
}
21+
22+
/** Extract source IP from a Lambda event (v1 or v2). */
23+
export function getSourceIp(event: LambdaEvent): string | undefined {
24+
if (isV2Event(event)) {
25+
return event.requestContext.http.sourceIp;
26+
}
27+
return (event as APIGatewayV1Event).requestContext?.identity?.sourceIp;
28+
}
29+
30+
/** Convert a Lambda event (v1 or v2) to a Web Standard Request. */
31+
export function lambdaEventToRequest(event: LambdaEvent, basePath?: string): Request {
32+
let method: string;
33+
let path: string;
34+
let queryString: string;
35+
const headers = new Headers();
36+
37+
if (isV2Event(event)) {
38+
method = event.requestContext.http.method;
39+
path = event.rawPath ?? event.requestContext.http.path;
40+
queryString = event.rawQueryString ?? '';
41+
42+
// Copy headers
43+
if (event.headers) {
44+
for (const [key, value] of Object.entries(event.headers)) {
45+
if (value !== undefined) {
46+
headers.set(key, value);
47+
}
48+
}
49+
}
50+
} else {
51+
const v1 = event as APIGatewayV1Event;
52+
method = v1.httpMethod;
53+
path = v1.path;
54+
55+
// Reconstruct query string from multiValueQueryStringParameters (preserves duplicates)
56+
// Fall back to queryStringParameters if multi-value not present
57+
if (v1.multiValueQueryStringParameters) {
58+
const parts: string[] = [];
59+
for (const [key, values] of Object.entries(v1.multiValueQueryStringParameters)) {
60+
if (values) {
61+
for (const val of values) {
62+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`);
63+
}
64+
}
65+
}
66+
queryString = parts.join('&');
67+
} else if (v1.queryStringParameters) {
68+
const parts: string[] = [];
69+
for (const [key, value] of Object.entries(v1.queryStringParameters)) {
70+
if (value !== undefined) {
71+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
72+
}
73+
}
74+
queryString = parts.join('&');
75+
} else {
76+
queryString = '';
77+
}
78+
79+
// Copy headers — use multiValueHeaders to preserve duplicates
80+
if (v1.multiValueHeaders) {
81+
for (const [key, values] of Object.entries(v1.multiValueHeaders)) {
82+
if (values) {
83+
for (const val of values) {
84+
headers.append(key, val);
85+
}
86+
}
87+
}
88+
} else if (v1.headers) {
89+
for (const [key, value] of Object.entries(v1.headers)) {
90+
if (value !== undefined) {
91+
headers.set(key, value);
92+
}
93+
}
94+
}
95+
}
96+
97+
// Strip basePath prefix
98+
if (basePath && path.startsWith(basePath)) {
99+
path = path.slice(basePath.length) || '/';
100+
}
101+
102+
const url = `https://lambda.local${path}${queryString ? '?' + queryString : ''}`;
103+
104+
// Decode body
105+
let body: string | undefined;
106+
if (event.body != null && event.body !== '') {
107+
body = event.isBase64Encoded
108+
? Buffer.from(event.body, 'base64').toString('utf-8')
109+
: event.body;
110+
}
111+
112+
// Only set body on methods that support it
113+
const hasBody = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase());
114+
115+
return new Request(url, {
116+
method,
117+
headers,
118+
body: hasBody ? (body ?? '') : undefined,
119+
});
120+
}
121+
122+
/** Convert a Web Standard Response to a Lambda result (v1 or v2 format). */
123+
export async function responseToLambdaResult(
124+
response: Response,
125+
event: LambdaEvent,
126+
): Promise<LambdaResult> {
127+
const body = await response.text();
128+
const headers: Record<string, string> = {};
129+
130+
response.headers.forEach((value, key) => {
131+
headers[key] = value;
132+
});
133+
134+
if (isV2Event(event)) {
135+
const result: APIGatewayV2Result = {
136+
statusCode: response.status,
137+
headers,
138+
body,
139+
isBase64Encoded: false,
140+
};
141+
return result;
142+
}
143+
144+
// v1: also include multiValueHeaders for Set-Cookie etc.
145+
const multiValueHeaders: Record<string, string[]> = {};
146+
response.headers.forEach((value, key) => {
147+
if (!multiValueHeaders[key]) {
148+
multiValueHeaders[key] = [];
149+
}
150+
multiValueHeaders[key].push(value);
151+
});
152+
153+
const result: APIGatewayV1Result = {
154+
statusCode: response.status,
155+
headers,
156+
multiValueHeaders,
157+
body,
158+
isBase64Encoded: false,
159+
};
160+
return result;
161+
}

src/serverless/types.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Lambda / serverless type definitions.
3+
*
4+
* Lightweight types for API Gateway v1 (REST API) and v2 (HTTP API / Function URL).
5+
* No dependency on @types/aws-lambda — keeps the framework lightweight.
6+
*/
7+
8+
/** API Gateway v1 (REST API) event */
9+
export interface APIGatewayV1Event {
10+
httpMethod: string;
11+
path: string;
12+
headers: Record<string, string | undefined>;
13+
multiValueHeaders?: Record<string, string[] | undefined>;
14+
queryStringParameters?: Record<string, string | undefined> | null;
15+
multiValueQueryStringParameters?: Record<string, string[] | undefined> | null;
16+
body?: string | null;
17+
isBase64Encoded?: boolean;
18+
requestContext?: {
19+
identity?: {
20+
sourceIp?: string;
21+
};
22+
[key: string]: unknown;
23+
};
24+
[key: string]: unknown;
25+
}
26+
27+
/** API Gateway v2 (HTTP API / Function URL) event */
28+
export interface APIGatewayV2Event {
29+
version: '2.0';
30+
requestContext: {
31+
http: {
32+
method: string;
33+
path: string;
34+
sourceIp?: string;
35+
};
36+
domainName?: string;
37+
[key: string]: unknown;
38+
};
39+
headers: Record<string, string | undefined>;
40+
queryStringParameters?: Record<string, string | undefined> | null;
41+
body?: string | null;
42+
isBase64Encoded?: boolean;
43+
rawPath?: string;
44+
rawQueryString?: string;
45+
[key: string]: unknown;
46+
}
47+
48+
export type LambdaEvent = APIGatewayV1Event | APIGatewayV2Event;
49+
50+
/** API Gateway v1 result */
51+
export interface APIGatewayV1Result {
52+
statusCode: number;
53+
headers: Record<string, string>;
54+
multiValueHeaders?: Record<string, string[]>;
55+
body: string;
56+
isBase64Encoded: boolean;
57+
}
58+
59+
/** API Gateway v2 result */
60+
export interface APIGatewayV2Result {
61+
statusCode: number;
62+
headers: Record<string, string>;
63+
body: string;
64+
isBase64Encoded: boolean;
65+
}
66+
67+
export type LambdaResult = APIGatewayV1Result | APIGatewayV2Result;
68+
69+
export type LambdaContext = Record<string, unknown>;
70+
71+
/** Configuration for the Lambda handler */
72+
export interface LambdaHandlerConfig {
73+
/**
74+
* CORS configuration for Lambda responses.
75+
* Set to false to disable CORS headers entirely.
76+
* @default { allowOrigin: '*' }
77+
*/
78+
cors?: {
79+
allowOrigin?: string;
80+
allowMethods?: string;
81+
allowHeaders?: string;
82+
exposeHeaders?: string;
83+
maxAge?: string;
84+
} | false;
85+
86+
/**
87+
* Base path prefix to strip from the incoming request path.
88+
* Useful when API Gateway has a stage prefix (e.g., '/prod').
89+
*/
90+
basePath?: string;
91+
}

0 commit comments

Comments
 (0)