@aws/run-mcp-servers-with-aws-lambda 0.2.1 → 0.2.3
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.
- package/dist/handlers/api_gateway_proxy_event_handler.d.ts +31 -0
- package/dist/handlers/api_gateway_proxy_event_handler.js +43 -0
- package/dist/handlers/api_gateway_proxy_event_v2_handler.d.ts +30 -0
- package/dist/handlers/api_gateway_proxy_event_v2_handler.js +42 -0
- package/dist/handlers/handlers.test.js +629 -0
- package/dist/handlers/index.d.ts +4 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/lambda_function_url_event_handler.d.ts +30 -0
- package/dist/handlers/lambda_function_url_event_handler.js +42 -0
- package/dist/handlers/request_handler.d.ts +43 -0
- package/dist/handlers/request_handler.js +1 -0
- package/dist/handlers/streamable_http_handler.d.ts +81 -0
- package/dist/handlers/streamable_http_handler.js +234 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/server-adapter/index.d.ts +2 -4
- package/dist/server-adapter/index.js +2 -110
- package/dist/server-adapter/stdio_server_adapter.d.ts +17 -0
- package/dist/server-adapter/stdio_server_adapter.js +118 -0
- package/dist/server-adapter/stdio_server_adapter.test.d.ts +1 -0
- package/dist/server-adapter/{index.test.js → stdio_server_adapter.test.js} +1 -1
- package/dist/server-adapter/stdio_server_adapter_request_handler.d.ts +26 -0
- package/dist/server-adapter/stdio_server_adapter_request_handler.js +66 -0
- package/dist/server-adapter/stdio_server_adapter_request_handler.test.d.ts +1 -0
- package/dist/server-adapter/stdio_server_adapter_request_handler.test.js +148 -0
- package/package.json +12 -12
- /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,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
|
+
}
|