@aws/run-mcp-servers-with-aws-lambda 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/handlers/api_gateway_proxy_event_handler.d.ts +31 -0
  2. package/dist/handlers/api_gateway_proxy_event_handler.js +43 -0
  3. package/dist/handlers/api_gateway_proxy_event_v2_handler.d.ts +30 -0
  4. package/dist/handlers/api_gateway_proxy_event_v2_handler.js +42 -0
  5. package/dist/handlers/handlers.test.js +629 -0
  6. package/dist/handlers/index.d.ts +4 -0
  7. package/dist/handlers/index.js +3 -0
  8. package/dist/handlers/lambda_function_url_event_handler.d.ts +30 -0
  9. package/dist/handlers/lambda_function_url_event_handler.js +42 -0
  10. package/dist/handlers/request_handler.d.ts +43 -0
  11. package/dist/handlers/request_handler.js +1 -0
  12. package/dist/handlers/streamable_http_handler.d.ts +81 -0
  13. package/dist/handlers/streamable_http_handler.js +234 -0
  14. package/dist/index.d.ts +3 -2
  15. package/dist/index.js +3 -2
  16. package/dist/server-adapter/index.d.ts +2 -4
  17. package/dist/server-adapter/index.js +2 -110
  18. package/dist/server-adapter/stdio_server_adapter.d.ts +17 -0
  19. package/dist/server-adapter/stdio_server_adapter.js +118 -0
  20. package/dist/server-adapter/stdio_server_adapter.test.d.ts +1 -0
  21. package/dist/server-adapter/{index.test.js → stdio_server_adapter.test.js} +1 -1
  22. package/dist/server-adapter/stdio_server_adapter_request_handler.d.ts +26 -0
  23. package/dist/server-adapter/stdio_server_adapter_request_handler.js +66 -0
  24. package/dist/server-adapter/stdio_server_adapter_request_handler.test.d.ts +1 -0
  25. package/dist/server-adapter/stdio_server_adapter_request_handler.test.js +148 -0
  26. package/package.json +12 -12
  27. /package/dist/{server-adapter/index.test.d.ts → handlers/handlers.test.d.ts} +0 -0
@@ -0,0 +1,629 @@
1
+ import { ErrorCode, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { APIGatewayProxyEventHandler, APIGatewayProxyEventV2Handler, LambdaFunctionURLEventHandler, } from "./index.js";
3
+ // Mock RequestHandler implementation for testing
4
+ class MockRequestHandler {
5
+ responses = new Map();
6
+ shouldThrow = false;
7
+ setResponse(method, response) {
8
+ this.responses.set(method, response);
9
+ }
10
+ setShouldThrow(shouldThrow) {
11
+ this.shouldThrow = shouldThrow;
12
+ }
13
+ async handleRequest(request, _context) {
14
+ if (this.shouldThrow) {
15
+ throw new Error("Mock handler error");
16
+ }
17
+ const response = this.responses.get(request.method);
18
+ if (!response) {
19
+ return {
20
+ jsonrpc: "2.0",
21
+ error: {
22
+ code: ErrorCode.MethodNotFound,
23
+ message: "Method not found",
24
+ },
25
+ id: request.id,
26
+ };
27
+ }
28
+ return response;
29
+ }
30
+ }
31
+ // Mock Lambda context
32
+ const mockContext = {
33
+ callbackWaitsForEmptyEventLoop: false,
34
+ functionName: "test-function",
35
+ functionVersion: "1",
36
+ invokedFunctionArn: "arn:aws:lambda:us-east-1:123456789012:function:test-function",
37
+ memoryLimitInMB: "128",
38
+ awsRequestId: "test-request-id",
39
+ logGroupName: "/aws/lambda/test-function",
40
+ logStreamName: "test-stream",
41
+ getRemainingTimeInMillis: () => 30000,
42
+ done: () => { },
43
+ fail: () => { },
44
+ succeed: () => { },
45
+ };
46
+ // Helper functions to create mock events
47
+ function createMockAPIGatewayProxyEvent(httpMethod, body = null, headers = {}) {
48
+ return {
49
+ httpMethod,
50
+ headers,
51
+ body,
52
+ path: "/test",
53
+ pathParameters: null,
54
+ queryStringParameters: null,
55
+ multiValueQueryStringParameters: null,
56
+ multiValueHeaders: {},
57
+ stageVariables: null,
58
+ requestContext: {
59
+ accountId: "123456789012",
60
+ apiId: "test-api",
61
+ protocol: "HTTP/1.1",
62
+ httpMethod,
63
+ path: "/test",
64
+ stage: "test",
65
+ requestId: "test-request",
66
+ requestTime: "01/Jan/2023:00:00:00 +0000",
67
+ requestTimeEpoch: 1672531200,
68
+ authorizer: {},
69
+ identity: {
70
+ cognitoIdentityPoolId: null,
71
+ accountId: null,
72
+ cognitoIdentityId: null,
73
+ caller: null,
74
+ sourceIp: "127.0.0.1",
75
+ principalOrgId: null,
76
+ accessKey: null,
77
+ cognitoAuthenticationType: null,
78
+ cognitoAuthenticationProvider: null,
79
+ userArn: null,
80
+ userAgent: "test-agent",
81
+ user: null,
82
+ apiKey: null,
83
+ apiKeyId: null,
84
+ clientCert: null,
85
+ },
86
+ resourceId: "test-resource",
87
+ resourcePath: "/test",
88
+ },
89
+ resource: "/test",
90
+ isBase64Encoded: false,
91
+ };
92
+ }
93
+ function createMockAPIGatewayProxyEventV2(httpMethod, body = null, headers = {}, routeKey = `${httpMethod} /test`) {
94
+ return {
95
+ version: "2.0",
96
+ routeKey,
97
+ rawPath: "/test",
98
+ rawQueryString: "",
99
+ headers,
100
+ body: body || undefined,
101
+ requestContext: {
102
+ accountId: "123456789012",
103
+ apiId: "test-api",
104
+ domainName: "test.execute-api.us-east-1.amazonaws.com",
105
+ domainPrefix: "test",
106
+ stage: "test",
107
+ requestId: "test-request",
108
+ routeKey,
109
+ http: {
110
+ method: httpMethod,
111
+ path: "/test",
112
+ protocol: "HTTP/1.1",
113
+ sourceIp: "127.0.0.1",
114
+ userAgent: "test-agent",
115
+ },
116
+ time: "01/Jan/2023:00:00:00 +0000",
117
+ timeEpoch: 1672531200,
118
+ },
119
+ isBase64Encoded: false,
120
+ };
121
+ }
122
+ function createMockLambdaFunctionURLEvent(httpMethod, body = null, headers = {}) {
123
+ return {
124
+ version: "2.0",
125
+ routeKey: "$default",
126
+ rawPath: "/",
127
+ rawQueryString: "",
128
+ headers,
129
+ body: body || undefined,
130
+ requestContext: {
131
+ accountId: "123456789012",
132
+ apiId: "test-function-url",
133
+ domainName: "test-function-url.lambda-url.us-east-1.on.aws",
134
+ domainPrefix: "test-function-url",
135
+ stage: "$default",
136
+ requestId: "test-request",
137
+ routeKey: "$default",
138
+ http: {
139
+ method: httpMethod,
140
+ path: "/",
141
+ protocol: "HTTP/1.1",
142
+ sourceIp: "127.0.0.1",
143
+ userAgent: "test-agent",
144
+ },
145
+ time: "01/Jan/2023:00:00:00 +0000",
146
+ timeEpoch: 1672531200,
147
+ },
148
+ isBase64Encoded: false,
149
+ };
150
+ }
151
+ // Test suite for all handlers
152
+ describe("MCP Streamable HTTP Handlers", () => {
153
+ let mockRequestHandler;
154
+ beforeEach(() => {
155
+ mockRequestHandler = new MockRequestHandler();
156
+ });
157
+ // Test cases that should work the same across all handlers
158
+ const sharedTestCases = [
159
+ {
160
+ name: "APIGatewayProxyEventHandler (REST API)",
161
+ createHandler: () => new APIGatewayProxyEventHandler(mockRequestHandler),
162
+ createEvent: createMockAPIGatewayProxyEvent,
163
+ validateResponse: (result) => {
164
+ expect(result.statusCode).toBeDefined();
165
+ expect(result.headers).toBeDefined();
166
+ expect(result.body).toBeDefined();
167
+ },
168
+ },
169
+ {
170
+ name: "APIGatewayProxyEventV2Handler (HTTP API)",
171
+ createHandler: () => new APIGatewayProxyEventV2Handler(mockRequestHandler),
172
+ createEvent: createMockAPIGatewayProxyEventV2,
173
+ validateResponse: (result) => {
174
+ // APIGatewayProxyResultV2 can be string or object, we expect object
175
+ expect(typeof result).toBe("object");
176
+ const resultObj = result;
177
+ expect(resultObj.statusCode).toBeDefined();
178
+ expect(resultObj.headers).toBeDefined();
179
+ expect(resultObj.body).toBeDefined();
180
+ },
181
+ },
182
+ {
183
+ name: "LambdaFunctionURLEventHandler",
184
+ createHandler: () => new LambdaFunctionURLEventHandler(mockRequestHandler),
185
+ createEvent: createMockLambdaFunctionURLEvent,
186
+ validateResponse: (result) => {
187
+ // APIGatewayProxyResultV2 can be string or object, we expect object
188
+ expect(typeof result).toBe("object");
189
+ const resultObj = result;
190
+ expect(resultObj.statusCode).toBeDefined();
191
+ expect(resultObj.headers).toBeDefined();
192
+ expect(resultObj.body).toBeDefined();
193
+ },
194
+ },
195
+ ];
196
+ // Run shared tests for all handlers
197
+ sharedTestCases.forEach(({ name, createHandler, createEvent, validateResponse }) => {
198
+ describe(name, () => {
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
+ let handler;
201
+ beforeEach(() => {
202
+ handler = createHandler();
203
+ });
204
+ describe("HTTP methods other than POST", () => {
205
+ it("should handle OPTIONS request (CORS preflight)", async () => {
206
+ const event = createEvent("OPTIONS");
207
+ const result = await handler.handle(event, mockContext);
208
+ validateResponse(result);
209
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
210
+ expect(resultObj.statusCode).toBe(200);
211
+ expect(resultObj.headers?.["Access-Control-Allow-Origin"]).toBe("*");
212
+ expect(resultObj.headers?.["Access-Control-Allow-Methods"]).toBe("POST, GET, OPTIONS");
213
+ expect(resultObj.body).toBe("");
214
+ });
215
+ it("should return 405 for GET requests", async () => {
216
+ const event = createEvent("GET");
217
+ const result = await handler.handle(event, mockContext);
218
+ validateResponse(result);
219
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
220
+ expect(resultObj.statusCode).toBe(405);
221
+ expect(resultObj.headers?.["Allow"]).toBe("POST, OPTIONS");
222
+ const responseBody = JSON.parse(resultObj.body);
223
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
224
+ expect(responseBody.error.message).toBe("Method Not Allowed: SSE streaming not supported");
225
+ });
226
+ it("should return 405 for PUT requests", async () => {
227
+ const event = createEvent("PUT");
228
+ const result = await handler.handle(event, mockContext);
229
+ validateResponse(result);
230
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
231
+ expect(resultObj.statusCode).toBe(405);
232
+ expect(resultObj.headers?.["Allow"]).toBe("POST, OPTIONS");
233
+ const responseBody = JSON.parse(resultObj.body);
234
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
235
+ expect(responseBody.error.message).toBe("Method Not Allowed");
236
+ });
237
+ it("should return 405 for PATCH requests", async () => {
238
+ const event = createEvent("PATCH");
239
+ const result = await handler.handle(event, mockContext);
240
+ validateResponse(result);
241
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
242
+ expect(resultObj.statusCode).toBe(405);
243
+ expect(resultObj.headers?.["Allow"]).toBe("POST, OPTIONS");
244
+ const responseBody = JSON.parse(resultObj.body);
245
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
246
+ expect(responseBody.error.message).toBe("Method Not Allowed");
247
+ });
248
+ it("should return 405 for DELETE requests", async () => {
249
+ const event = createEvent("DELETE");
250
+ const result = await handler.handle(event, mockContext);
251
+ validateResponse(result);
252
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
253
+ expect(resultObj.statusCode).toBe(405);
254
+ expect(resultObj.headers?.["Allow"]).toBe("POST, OPTIONS");
255
+ const responseBody = JSON.parse(resultObj.body);
256
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
257
+ expect(responseBody.error.message).toBe("Method Not Allowed");
258
+ });
259
+ });
260
+ describe("Header validation", () => {
261
+ it("should return 406 when missing all headers", async () => {
262
+ const event = createEvent("POST", JSON.stringify({
263
+ jsonrpc: "2.0",
264
+ method: "test",
265
+ id: 1,
266
+ }));
267
+ const result = await handler.handle(event, mockContext);
268
+ validateResponse(result);
269
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
270
+ expect(resultObj.statusCode).toBe(406);
271
+ const responseBody = JSON.parse(resultObj.body);
272
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
273
+ expect(responseBody.error.message).toBe("Not Acceptable: Client must accept application/json");
274
+ });
275
+ it("should return 406 for missing Accept header", async () => {
276
+ const event = createEvent("POST", JSON.stringify({
277
+ jsonrpc: "2.0",
278
+ method: "test",
279
+ id: 1,
280
+ }), {
281
+ "Content-Type": "application/json",
282
+ });
283
+ const result = await handler.handle(event, mockContext);
284
+ validateResponse(result);
285
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
286
+ expect(resultObj.statusCode).toBe(406);
287
+ const responseBody = JSON.parse(resultObj.body);
288
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
289
+ expect(responseBody.error.message).toBe("Not Acceptable: Client must accept application/json");
290
+ });
291
+ it("should return 406 for wrong Accept content type", async () => {
292
+ const event = createEvent("POST", JSON.stringify({
293
+ jsonrpc: "2.0",
294
+ method: "test",
295
+ id: 1,
296
+ }), {
297
+ "Content-Type": "application/json",
298
+ Accept: "text/html",
299
+ });
300
+ const result = await handler.handle(event, mockContext);
301
+ validateResponse(result);
302
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
303
+ expect(resultObj.statusCode).toBe(406);
304
+ const responseBody = JSON.parse(resultObj.body);
305
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
306
+ expect(responseBody.error.message).toBe("Not Acceptable: Client must accept application/json");
307
+ });
308
+ it("should return 415 for missing Content-Type", async () => {
309
+ const event = createEvent("POST", JSON.stringify({
310
+ jsonrpc: "2.0",
311
+ method: "test",
312
+ id: 1,
313
+ }), {
314
+ Accept: "application/json",
315
+ });
316
+ const result = await handler.handle(event, mockContext);
317
+ validateResponse(result);
318
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
319
+ expect(resultObj.statusCode).toBe(415);
320
+ const responseBody = JSON.parse(resultObj.body);
321
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
322
+ expect(responseBody.error.message).toBe("Unsupported Media Type: Content-Type must be application/json");
323
+ });
324
+ it("should return 415 for wrong Content-Type", async () => {
325
+ const event = createEvent("POST", JSON.stringify({
326
+ jsonrpc: "2.0",
327
+ method: "test",
328
+ id: 1,
329
+ }), {
330
+ "Content-Type": "text/plain",
331
+ Accept: "application/json",
332
+ });
333
+ const result = await handler.handle(event, mockContext);
334
+ validateResponse(result);
335
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
336
+ expect(resultObj.statusCode).toBe(415);
337
+ const responseBody = JSON.parse(resultObj.body);
338
+ expect(responseBody.error.code).toBe(ErrorCode.ConnectionClosed);
339
+ expect(responseBody.error.message).toBe("Unsupported Media Type: Content-Type must be application/json");
340
+ });
341
+ it("should accept case-insensitive headers", async () => {
342
+ const expectedResponse = {
343
+ jsonrpc: "2.0",
344
+ result: { message: "Case insensitive headers work" },
345
+ id: 1,
346
+ };
347
+ mockRequestHandler.setResponse("test", expectedResponse);
348
+ // Test with different header casing
349
+ const event = createEvent("POST", JSON.stringify({
350
+ jsonrpc: "2.0",
351
+ method: "test",
352
+ id: 1,
353
+ }), {
354
+ "content-type": "application/json", // lowercase
355
+ ACCEPT: "application/json", // uppercase
356
+ });
357
+ const result = await handler.handle(event, mockContext);
358
+ validateResponse(result);
359
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
360
+ expect(resultObj.statusCode).toBe(200);
361
+ const responseBody = JSON.parse(resultObj.body);
362
+ expect(responseBody.jsonrpc).toBe("2.0");
363
+ expect(responseBody.result.message).toBe("Case insensitive headers work");
364
+ expect(responseBody.id).toBe(1);
365
+ });
366
+ });
367
+ describe("Request body validation", () => {
368
+ it("should return 400 for empty request body", async () => {
369
+ const event = createEvent("POST", null, {
370
+ "Content-Type": "application/json",
371
+ Accept: "application/json",
372
+ });
373
+ const result = await handler.handle(event, mockContext);
374
+ validateResponse(result);
375
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
376
+ expect(resultObj.statusCode).toBe(400);
377
+ const responseBody = JSON.parse(resultObj.body);
378
+ expect(responseBody.error.code).toBe(ErrorCode.ParseError);
379
+ expect(responseBody.error.message).toBe("Parse error: Empty request body");
380
+ });
381
+ it("should return 400 for invalid JSON", async () => {
382
+ const event = createEvent("POST", "invalid json", {
383
+ "Content-Type": "application/json",
384
+ Accept: "application/json",
385
+ });
386
+ const result = await handler.handle(event, mockContext);
387
+ validateResponse(result);
388
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
389
+ expect(resultObj.statusCode).toBe(400);
390
+ const responseBody = JSON.parse(resultObj.body);
391
+ expect(responseBody.error.code).toBe(ErrorCode.ParseError);
392
+ expect(responseBody.error.message).toBe("Parse error: Invalid JSON");
393
+ });
394
+ it("should return 400 for invalid JSON-RPC message format", async () => {
395
+ const event = createEvent("POST", JSON.stringify({
396
+ invalid: "message",
397
+ notJsonRpc: true,
398
+ }), {
399
+ "Content-Type": "application/json",
400
+ Accept: "application/json",
401
+ });
402
+ const result = await handler.handle(event, mockContext);
403
+ validateResponse(result);
404
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
405
+ expect(resultObj.statusCode).toBe(400);
406
+ const responseBody = JSON.parse(resultObj.body);
407
+ expect(responseBody.error.code).toBe(ErrorCode.InvalidRequest);
408
+ expect(responseBody.error.message).toBe("Invalid Request: All messages must be valid JSON-RPC 2.0");
409
+ });
410
+ });
411
+ describe("Single request handling", () => {
412
+ it("should handle valid JSON-RPC request and return response", async () => {
413
+ const expectedResponse = {
414
+ jsonrpc: "2.0",
415
+ result: { message: "Hello, World!" },
416
+ id: 1,
417
+ };
418
+ mockRequestHandler.setResponse("test", expectedResponse);
419
+ const event = createEvent("POST", JSON.stringify({
420
+ jsonrpc: "2.0",
421
+ method: "test",
422
+ id: 1,
423
+ }), {
424
+ "Content-Type": "application/json",
425
+ Accept: "application/json",
426
+ });
427
+ const result = await handler.handle(event, mockContext);
428
+ validateResponse(result);
429
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
430
+ expect(resultObj.statusCode).toBe(200);
431
+ expect(resultObj.headers?.["Content-Type"]).toBe("application/json");
432
+ expect(resultObj.headers?.["Access-Control-Allow-Origin"]).toBe("*");
433
+ const responseBody = JSON.parse(resultObj.body);
434
+ expect(responseBody.jsonrpc).toBe("2.0");
435
+ expect(responseBody.result.message).toBe("Hello, World!");
436
+ expect(responseBody.id).toBe(1);
437
+ });
438
+ it("should handle JSON-RPC errors from request handler", async () => {
439
+ const expectedError = {
440
+ jsonrpc: "2.0",
441
+ error: {
442
+ code: ErrorCode.MethodNotFound,
443
+ message: "Method not found",
444
+ },
445
+ id: 1,
446
+ };
447
+ mockRequestHandler.setResponse("test", expectedError);
448
+ const event = createEvent("POST", JSON.stringify({
449
+ jsonrpc: "2.0",
450
+ method: "test",
451
+ id: 1,
452
+ }), {
453
+ "Content-Type": "application/json",
454
+ Accept: "application/json",
455
+ });
456
+ const result = await handler.handle(event, mockContext);
457
+ validateResponse(result);
458
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
459
+ expect(resultObj.statusCode).toBe(200);
460
+ const responseBody = JSON.parse(resultObj.body);
461
+ expect(responseBody.jsonrpc).toBe("2.0");
462
+ expect(responseBody.error.code).toBe(ErrorCode.MethodNotFound);
463
+ expect(responseBody.error.message).toBe("Method not found");
464
+ expect(responseBody.id).toBe(1);
465
+ });
466
+ it("should handle exceptions from request handler", async () => {
467
+ mockRequestHandler.setShouldThrow(true);
468
+ const event = createEvent("POST", JSON.stringify({
469
+ jsonrpc: "2.0",
470
+ method: "test",
471
+ id: 1,
472
+ }), {
473
+ "Content-Type": "application/json",
474
+ Accept: "application/json",
475
+ });
476
+ const result = await handler.handle(event, mockContext);
477
+ validateResponse(result);
478
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
479
+ expect(resultObj.statusCode).toBe(200);
480
+ const responseBody = JSON.parse(resultObj.body);
481
+ expect(responseBody.jsonrpc).toBe("2.0");
482
+ expect(responseBody.error.code).toBe(ErrorCode.InternalError);
483
+ expect(responseBody.error.message).toBe("Internal error");
484
+ expect(responseBody.error.data).toBe("Mock handler error");
485
+ expect(responseBody.id).toBe(1);
486
+ });
487
+ it("should handle unexpected response format from request handler", async () => {
488
+ // Create a handler that returns invalid response format
489
+ const invalidHandler = {
490
+ async handleRequest() {
491
+ return { invalid: "response" };
492
+ },
493
+ };
494
+ const invalidHandlerInstance = createHandler();
495
+ // Replace the request handler with our invalid one
496
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
497
+ invalidHandlerInstance.requestHandler = invalidHandler;
498
+ const event = createEvent("POST", JSON.stringify({
499
+ jsonrpc: "2.0",
500
+ method: "test",
501
+ id: 1,
502
+ }), {
503
+ "Content-Type": "application/json",
504
+ Accept: "application/json",
505
+ });
506
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
507
+ const result = await invalidHandlerInstance.handle(event, mockContext);
508
+ validateResponse(result);
509
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
510
+ expect(resultObj.statusCode).toBe(200);
511
+ const responseBody = JSON.parse(resultObj.body);
512
+ expect(responseBody.jsonrpc).toBe("2.0");
513
+ expect(responseBody.error.code).toBe(ErrorCode.InternalError);
514
+ expect(responseBody.error.message).toBe("Internal error: Unexpected response format from request handler");
515
+ expect(responseBody.error.data).toBe("Expected JSONRPCResponse or JSONRPCError");
516
+ expect(responseBody.id).toBe(1);
517
+ });
518
+ it("should return 202 for notification event", async () => {
519
+ const event = createEvent("POST", JSON.stringify({
520
+ jsonrpc: "2.0",
521
+ method: "test",
522
+ // No id = notification
523
+ }), {
524
+ "Content-Type": "application/json",
525
+ Accept: "application/json",
526
+ });
527
+ const result = await handler.handle(event, mockContext);
528
+ validateResponse(result);
529
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
530
+ expect(resultObj.statusCode).toBe(202);
531
+ expect(resultObj.body).toBe("");
532
+ });
533
+ });
534
+ describe("Batch request handling", () => {
535
+ it("should handle batch of requests", async () => {
536
+ const expectedResponses = [
537
+ {
538
+ jsonrpc: "2.0",
539
+ result: { message: "Response 1" },
540
+ id: 1,
541
+ },
542
+ {
543
+ jsonrpc: "2.0",
544
+ result: { message: "Response 2" },
545
+ id: 2,
546
+ },
547
+ ];
548
+ mockRequestHandler.setResponse("test1", expectedResponses[0]);
549
+ mockRequestHandler.setResponse("test2", expectedResponses[1]);
550
+ const event = createEvent("POST", JSON.stringify([
551
+ {
552
+ jsonrpc: "2.0",
553
+ method: "test1",
554
+ id: 1,
555
+ },
556
+ {
557
+ jsonrpc: "2.0",
558
+ method: "test2",
559
+ id: 2,
560
+ },
561
+ ]), {
562
+ "Content-Type": "application/json",
563
+ Accept: "application/json",
564
+ });
565
+ const result = await handler.handle(event, mockContext);
566
+ validateResponse(result);
567
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
568
+ expect(resultObj.statusCode).toBe(200);
569
+ const responseBody = JSON.parse(resultObj.body);
570
+ expect(Array.isArray(responseBody)).toBe(true);
571
+ expect(responseBody).toHaveLength(2);
572
+ expect(responseBody[0].result.message).toBe("Response 1");
573
+ expect(responseBody[1].result.message).toBe("Response 2");
574
+ });
575
+ it("should handle mixed batch with requests and notifications", async () => {
576
+ const expectedResponse = {
577
+ jsonrpc: "2.0",
578
+ result: { message: "Response for request" },
579
+ id: 1,
580
+ };
581
+ mockRequestHandler.setResponse("test", expectedResponse);
582
+ const event = createEvent("POST", JSON.stringify([
583
+ {
584
+ jsonrpc: "2.0",
585
+ method: "test",
586
+ id: 1, // Request
587
+ },
588
+ {
589
+ jsonrpc: "2.0",
590
+ method: "notification", // Notification (no id)
591
+ },
592
+ ]), {
593
+ "Content-Type": "application/json",
594
+ Accept: "application/json",
595
+ });
596
+ const result = await handler.handle(event, mockContext);
597
+ validateResponse(result);
598
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
599
+ expect(resultObj.statusCode).toBe(200);
600
+ const responseBody = JSON.parse(resultObj.body);
601
+ // Should return single response since only one request (the notification doesn't get a response)
602
+ expect(responseBody.jsonrpc).toBe("2.0");
603
+ expect(responseBody.result.message).toBe("Response for request");
604
+ expect(responseBody.id).toBe(1);
605
+ });
606
+ it("should return 202 for batch of notifications only", async () => {
607
+ const event = createEvent("POST", JSON.stringify([
608
+ {
609
+ jsonrpc: "2.0",
610
+ method: "notification1",
611
+ },
612
+ {
613
+ jsonrpc: "2.0",
614
+ method: "notification2",
615
+ },
616
+ ]), {
617
+ "Content-Type": "application/json",
618
+ Accept: "application/json",
619
+ });
620
+ const result = await handler.handle(event, mockContext);
621
+ validateResponse(result);
622
+ const resultObj = typeof result === "string" ? JSON.parse(result) : result;
623
+ expect(resultObj.statusCode).toBe(202);
624
+ expect(resultObj.body).toBe("");
625
+ });
626
+ });
627
+ });
628
+ });
629
+ });
@@ -0,0 +1,4 @@
1
+ export { RequestHandler } from "./request_handler.js";
2
+ export { APIGatewayProxyEventHandler } from "./api_gateway_proxy_event_handler.js";
3
+ export { APIGatewayProxyEventV2Handler } from "./api_gateway_proxy_event_v2_handler.js";
4
+ export { LambdaFunctionURLEventHandler } from "./lambda_function_url_event_handler.js";
@@ -0,0 +1,3 @@
1
+ export { APIGatewayProxyEventHandler } from "./api_gateway_proxy_event_handler.js";
2
+ export { APIGatewayProxyEventV2Handler } from "./api_gateway_proxy_event_v2_handler.js";
3
+ export { LambdaFunctionURLEventHandler } from "./lambda_function_url_event_handler.js";
@@ -0,0 +1,30 @@
1
+ import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
2
+ import { StreamableHttpHandler, ParsedHttpRequest, HttpResponse } from "./streamable_http_handler.js";
3
+ import { RequestHandler } from "./request_handler.js";
4
+ /**
5
+ * Handler for Lambda Function URL requests
6
+ *
7
+ * This handler processes APIGatewayProxyEventV2 events and returns APIGatewayProxyResultV2 responses.
8
+ *
9
+ * This class handles all the generic JSON-RPC protocol aspects of the MCP Streamable HTTP transport:
10
+ * - HTTP method validation (POST, OPTIONS, GET)
11
+ * - Content-Type and Accept header validation
12
+ * - JSON parsing and validation
13
+ * - Batch request handling
14
+ * - CORS headers
15
+ * - Error response formatting
16
+ * This class does not implement session management.
17
+ *
18
+ * The specific business logic is delegated to a provided RequestHandler implementation.
19
+ */
20
+ export declare class LambdaFunctionURLEventHandler extends StreamableHttpHandler<APIGatewayProxyEventV2, APIGatewayProxyResultV2> {
21
+ constructor(requestHandler: RequestHandler);
22
+ /**
23
+ * Parse Lambda Function URL event (APIGatewayProxyEventV2) into common HTTP request format
24
+ */
25
+ protected parseEvent(event: APIGatewayProxyEventV2): ParsedHttpRequest;
26
+ /**
27
+ * Format HTTP response as APIGatewayProxyResultV2
28
+ */
29
+ protected formatResponse(response: HttpResponse): APIGatewayProxyResultV2;
30
+ }