@aws/run-mcp-servers-with-aws-lambda 0.2.3 → 0.2.4

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.
@@ -1,27 +1,2 @@
1
- import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
- import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
3
- export type LambdaFunctionParameters = {
4
- /**
5
- * The name or ARN of the Lambda function, version, or alias.
6
- */
7
- functionName: string;
8
- /**
9
- * The AWS region of the Lambda function.
10
- */
11
- regionName?: string;
12
- };
13
- /**
14
- * Client transport for Lambda functions:
15
- * this will connect to a server by calling the Lambda Invoke API.
16
- */
17
- export declare class LambdaFunctionClientTransport implements Transport {
18
- private _serverParams;
19
- private _lambdaClient;
20
- onclose?: () => void;
21
- onerror?: (error: Error) => void;
22
- onmessage?: (message: JSONRPCMessage) => void;
23
- constructor(server: LambdaFunctionParameters);
24
- start(): Promise<void>;
25
- close(): Promise<void>;
26
- send(message: JSONRPCMessage): Promise<void>;
27
- }
1
+ export * from "./lambdaFunction.js";
2
+ export * from "./streamableHttpWithSigV4.js";
@@ -1,51 +1,4 @@
1
- import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
2
- /**
3
- * Client transport for Lambda functions:
4
- * this will connect to a server by calling the Lambda Invoke API.
5
- */
6
- export class LambdaFunctionClientTransport {
7
- _serverParams;
8
- _lambdaClient;
9
- onclose;
10
- onerror;
11
- onmessage;
12
- constructor(server) {
13
- this._serverParams = server;
14
- this._lambdaClient = new LambdaClient({
15
- region: this._serverParams.regionName,
16
- });
17
- }
18
- async start() {
19
- // no-op
20
- }
21
- async close() {
22
- // no-op
23
- }
24
- async send(message) {
25
- try {
26
- const invokeCommand = new InvokeCommand({
27
- FunctionName: this._serverParams.functionName,
28
- InvocationType: "RequestResponse",
29
- Payload: JSON.stringify(message),
30
- });
31
- const invokeCommandOutput = await this._lambdaClient.send(invokeCommand);
32
- if (invokeCommandOutput.Payload) {
33
- const responseMessage = Buffer.from(invokeCommandOutput.Payload).toString("utf-8");
34
- if (invokeCommandOutput.FunctionError) {
35
- throw new Error(`${invokeCommandOutput.FunctionError} ${responseMessage}`);
36
- }
37
- if (responseMessage === "{}") {
38
- // Assume we sent a notification and do not expect a response
39
- return;
40
- }
41
- this.onmessage?.(JSON.parse(responseMessage));
42
- }
43
- else {
44
- throw new Error("No payload returned from Lambda function");
45
- }
46
- }
47
- catch (error) {
48
- this.onerror?.(error);
49
- }
50
- }
51
- }
1
+ // Export Lambda function client transport
2
+ export * from "./lambdaFunction.js";
3
+ // Export SigV4 HTTP client transport
4
+ export * from "./streamableHttpWithSigV4.js";
@@ -0,0 +1,27 @@
1
+ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
3
+ export type LambdaFunctionParameters = {
4
+ /**
5
+ * The name or ARN of the Lambda function, version, or alias.
6
+ */
7
+ functionName: string;
8
+ /**
9
+ * The AWS region of the Lambda function.
10
+ */
11
+ regionName?: string;
12
+ };
13
+ /**
14
+ * Client transport for Lambda functions:
15
+ * this will connect to a server by calling the Lambda Invoke API.
16
+ */
17
+ export declare class LambdaFunctionClientTransport implements Transport {
18
+ private _serverParams;
19
+ private _lambdaClient;
20
+ onclose?: () => void;
21
+ onerror?: (error: Error) => void;
22
+ onmessage?: (message: JSONRPCMessage) => void;
23
+ constructor(server: LambdaFunctionParameters);
24
+ start(): Promise<void>;
25
+ close(): Promise<void>;
26
+ send(message: JSONRPCMessage): Promise<void>;
27
+ }
@@ -0,0 +1,51 @@
1
+ import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
2
+ /**
3
+ * Client transport for Lambda functions:
4
+ * this will connect to a server by calling the Lambda Invoke API.
5
+ */
6
+ export class LambdaFunctionClientTransport {
7
+ _serverParams;
8
+ _lambdaClient;
9
+ onclose;
10
+ onerror;
11
+ onmessage;
12
+ constructor(server) {
13
+ this._serverParams = server;
14
+ this._lambdaClient = new LambdaClient({
15
+ region: this._serverParams.regionName,
16
+ });
17
+ }
18
+ async start() {
19
+ // no-op
20
+ }
21
+ async close() {
22
+ // no-op
23
+ }
24
+ async send(message) {
25
+ try {
26
+ const invokeCommand = new InvokeCommand({
27
+ FunctionName: this._serverParams.functionName,
28
+ InvocationType: "RequestResponse",
29
+ Payload: JSON.stringify(message),
30
+ });
31
+ const invokeCommandOutput = await this._lambdaClient.send(invokeCommand);
32
+ if (invokeCommandOutput.Payload) {
33
+ const responseMessage = Buffer.from(invokeCommandOutput.Payload).toString("utf-8");
34
+ if (invokeCommandOutput.FunctionError) {
35
+ throw new Error(`${invokeCommandOutput.FunctionError} ${responseMessage}`);
36
+ }
37
+ if (responseMessage === "{}") {
38
+ // Assume we sent a notification and do not expect a response
39
+ return;
40
+ }
41
+ this.onmessage?.(JSON.parse(responseMessage));
42
+ }
43
+ else {
44
+ throw new Error("No payload returned from Lambda function");
45
+ }
46
+ }
47
+ catch (error) {
48
+ this.onerror?.(error);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,88 @@
1
+ import { StreamableHTTPClientTransport, StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2
+ import { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
3
+ import { AwsCredentialIdentity, Provider } from "@aws-sdk/types";
4
+ /**
5
+ * Configuration options for SigV4FetchLike
6
+ */
7
+ export interface SigV4FetchLikeOptions {
8
+ /**
9
+ * AWS credentials (provider), if not provided, default provider will be used
10
+ */
11
+ credentials?: AwsCredentialIdentity | Provider<AwsCredentialIdentity>;
12
+ /**
13
+ * AWS region for signing requests
14
+ */
15
+ region: string;
16
+ /**
17
+ * AWS service name for signing requests (e.g., 'lambda', 'execute-api')
18
+ */
19
+ service: string;
20
+ /**
21
+ * Base fetch implementation to use for actual HTTP requests
22
+ * If not provided, the global fetch will be used
23
+ */
24
+ baseFetch?: FetchLike;
25
+ }
26
+ /**
27
+ * A FetchLike implementation that signs HTTP requests using AWS Signature Version 4.
28
+ *
29
+ * This class can be passed to any transport that accepts a FetchLike parameter.
30
+ * It wraps a base fetch implementation and automatically signs all requests with AWS SigV4.
31
+ */
32
+ export declare class SigV4FetchLike {
33
+ private _signer;
34
+ private _baseFetch;
35
+ constructor(options: SigV4FetchLikeOptions);
36
+ /**
37
+ * Fetch implementation that signs requests with AWS SigV4
38
+ */
39
+ fetch(url: string | URL, init?: RequestInit): Promise<Response>;
40
+ }
41
+ /**
42
+ * Creates a FetchLike function that signs HTTP requests using AWS Signature Version 4.
43
+ *
44
+ * This function can be passed to any transport that accepts a FetchLike parameter,
45
+ * such as StreamableHTTPClientTransport.
46
+ *
47
+ * @param options Configuration options for AWS SigV4 signing
48
+ * @returns A FetchLike function that automatically signs requests
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const sigV4Fetch = createSigV4Fetch({
53
+ * region: 'us-east-1',
54
+ * service: 'lambda'
55
+ * });
56
+ *
57
+ * const transport = new StreamableHTTPClientTransport(url, {
58
+ * fetch: sigV4Fetch
59
+ * });
60
+ * ```
61
+ */
62
+ export declare function createSigV4Fetch(options: SigV4FetchLikeOptions): FetchLike;
63
+ /**
64
+ * Extended options for StreamableHTTPClientWithSigV4Transport
65
+ */
66
+ export interface StreamableHTTPClientWithSigV4TransportOptions extends StreamableHTTPClientTransportOptions {
67
+ /**
68
+ * AWS credentials (provider), if not provided, default provider will be used
69
+ */
70
+ credentials?: AwsCredentialIdentity | Provider<AwsCredentialIdentity>;
71
+ /**
72
+ * AWS region for signing requests
73
+ */
74
+ region: string;
75
+ /**
76
+ * AWS service name for signing requests (e.g., 'lambda', 'execute-api')
77
+ */
78
+ service: string;
79
+ }
80
+ /**
81
+ * Streamable HTTP client transport with AWS SigV4 signing support.
82
+ *
83
+ * This transport enables communication with MCP servers that authenticate using AWS IAM,
84
+ * such as servers behind a Lambda function URL or API Gateway.
85
+ */
86
+ export declare class StreamableHTTPClientWithSigV4Transport extends StreamableHTTPClientTransport {
87
+ constructor(url: URL, options: StreamableHTTPClientWithSigV4TransportOptions);
88
+ }
@@ -0,0 +1,116 @@
1
+ import { StreamableHTTPClientTransport, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2
+ import { SignatureV4 } from "@smithy/signature-v4";
3
+ import { defaultProvider } from "@aws-sdk/credential-provider-node";
4
+ import { Sha256 } from "@aws-crypto/sha256-js";
5
+ import { HttpRequest } from "@aws-sdk/protocol-http";
6
+ /**
7
+ * A FetchLike implementation that signs HTTP requests using AWS Signature Version 4.
8
+ *
9
+ * This class can be passed to any transport that accepts a FetchLike parameter.
10
+ * It wraps a base fetch implementation and automatically signs all requests with AWS SigV4.
11
+ */
12
+ export class SigV4FetchLike {
13
+ _signer;
14
+ _baseFetch;
15
+ constructor(options) {
16
+ this._baseFetch = options.baseFetch ?? fetch;
17
+ this._signer = new SignatureV4({
18
+ service: options.service,
19
+ region: options.region,
20
+ credentials: options.credentials ?? defaultProvider(),
21
+ sha256: Sha256,
22
+ });
23
+ }
24
+ /**
25
+ * Fetch implementation that signs requests with AWS SigV4
26
+ */
27
+ async fetch(url, init) {
28
+ const urlObj = typeof url === "string" ? new URL(url) : url;
29
+ const requestInit = init || {};
30
+ // Convert fetch RequestInit to HttpRequest for signing
31
+ const headers = {
32
+ host: urlObj.hostname,
33
+ };
34
+ if (requestInit.headers) {
35
+ if (requestInit.headers instanceof Headers) {
36
+ requestInit.headers.forEach((value, key) => {
37
+ headers[key.toLowerCase()] = value;
38
+ });
39
+ }
40
+ else if (Array.isArray(requestInit.headers)) {
41
+ requestInit.headers.forEach(([key, value]) => {
42
+ headers[key.toLowerCase()] = value;
43
+ });
44
+ }
45
+ else {
46
+ Object.entries(requestInit.headers).forEach(([key, value]) => {
47
+ headers[key.toLowerCase()] = value;
48
+ });
49
+ }
50
+ }
51
+ const unsignedRequest = new HttpRequest({
52
+ method: requestInit.method || "GET",
53
+ protocol: urlObj.protocol,
54
+ hostname: urlObj.hostname,
55
+ port: urlObj.port ? parseInt(urlObj.port) : undefined,
56
+ path: urlObj.pathname + urlObj.search,
57
+ headers,
58
+ body: requestInit.body,
59
+ });
60
+ // Sign the request
61
+ const signedRequest = await this._signer.sign(unsignedRequest);
62
+ // Convert signed request back to fetch format
63
+ const signedInit = {
64
+ ...requestInit,
65
+ method: signedRequest.method,
66
+ headers: signedRequest.headers,
67
+ body: signedRequest.body,
68
+ };
69
+ return this._baseFetch(urlObj, signedInit);
70
+ }
71
+ }
72
+ /**
73
+ * Creates a FetchLike function that signs HTTP requests using AWS Signature Version 4.
74
+ *
75
+ * This function can be passed to any transport that accepts a FetchLike parameter,
76
+ * such as StreamableHTTPClientTransport.
77
+ *
78
+ * @param options Configuration options for AWS SigV4 signing
79
+ * @returns A FetchLike function that automatically signs requests
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const sigV4Fetch = createSigV4Fetch({
84
+ * region: 'us-east-1',
85
+ * service: 'lambda'
86
+ * });
87
+ *
88
+ * const transport = new StreamableHTTPClientTransport(url, {
89
+ * fetch: sigV4Fetch
90
+ * });
91
+ * ```
92
+ */
93
+ export function createSigV4Fetch(options) {
94
+ const sigV4Fetch = new SigV4FetchLike(options);
95
+ return sigV4Fetch.fetch.bind(sigV4Fetch);
96
+ }
97
+ /**
98
+ * Streamable HTTP client transport with AWS SigV4 signing support.
99
+ *
100
+ * This transport enables communication with MCP servers that authenticate using AWS IAM,
101
+ * such as servers behind a Lambda function URL or API Gateway.
102
+ */
103
+ export class StreamableHTTPClientWithSigV4Transport extends StreamableHTTPClientTransport {
104
+ constructor(url, options) {
105
+ const sigV4Fetch = createSigV4Fetch({
106
+ credentials: options.credentials,
107
+ region: options.region,
108
+ service: options.service,
109
+ baseFetch: options.fetch,
110
+ });
111
+ super(url, {
112
+ ...options,
113
+ fetch: sigV4Fetch,
114
+ });
115
+ }
116
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,442 @@
1
+ import { SignatureV4 } from "@smithy/signature-v4";
2
+ import { defaultProvider } from "@aws-sdk/credential-provider-node";
3
+ import { HttpRequest } from "@aws-sdk/protocol-http";
4
+ import { StreamableHTTPClientWithSigV4Transport, SigV4FetchLike, createSigV4Fetch, } from "./streamableHttpWithSigV4.js";
5
+ // Mock dependencies
6
+ jest.mock("@smithy/signature-v4");
7
+ jest.mock("@aws-sdk/credential-provider-node");
8
+ jest.mock("@aws-sdk/protocol-http");
9
+ jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js");
10
+ describe("StreamableHTTPClientWithSigV4Transport", () => {
11
+ const mockSign = jest.fn();
12
+ const mockDefaultProvider = jest.fn();
13
+ const mockCredentials = {
14
+ accessKeyId: "test-access-key",
15
+ secretAccessKey: "test-secret-key",
16
+ };
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ // Mock SignatureV4
20
+ SignatureV4.mockImplementation(() => ({
21
+ sign: mockSign,
22
+ }));
23
+ // Mock defaultProvider
24
+ defaultProvider.mockReturnValue(mockDefaultProvider);
25
+ mockDefaultProvider.mockResolvedValue(mockCredentials);
26
+ });
27
+ describe("constructor", () => {
28
+ test("should initialize with required options", () => {
29
+ const url = new URL("https://example.com/mcp");
30
+ const options = {
31
+ region: "us-east-1",
32
+ service: "lambda",
33
+ };
34
+ new StreamableHTTPClientWithSigV4Transport(url, options);
35
+ expect(SignatureV4).toHaveBeenCalledWith({
36
+ service: "lambda",
37
+ region: "us-east-1",
38
+ credentials: mockDefaultProvider,
39
+ sha256: expect.any(Function),
40
+ });
41
+ });
42
+ test("should use provided credentials instead of default provider", () => {
43
+ const url = new URL("https://example.com/mcp");
44
+ const customCredentials = {
45
+ accessKeyId: "custom-access-key",
46
+ secretAccessKey: "custom-secret-key",
47
+ };
48
+ const options = {
49
+ region: "us-west-2",
50
+ service: "execute-api",
51
+ credentials: customCredentials,
52
+ };
53
+ new StreamableHTTPClientWithSigV4Transport(url, options);
54
+ expect(SignatureV4).toHaveBeenCalledWith({
55
+ service: "execute-api",
56
+ region: "us-west-2",
57
+ credentials: customCredentials,
58
+ sha256: expect.any(Function),
59
+ });
60
+ expect(defaultProvider).not.toHaveBeenCalled();
61
+ });
62
+ test("should use provided fetch function", () => {
63
+ const url = new URL("https://example.com/mcp");
64
+ const customFetch = jest.fn();
65
+ const options = {
66
+ region: "us-east-1",
67
+ service: "lambda",
68
+ fetch: customFetch,
69
+ };
70
+ new StreamableHTTPClientWithSigV4Transport(url, options);
71
+ // The custom fetch should be passed to the SigV4FetchLike as baseFetch
72
+ expect(customFetch).not.toHaveBeenCalled();
73
+ });
74
+ });
75
+ describe("SigV4FetchLike", () => {
76
+ let mockFetch;
77
+ beforeEach(() => {
78
+ mockFetch = jest.fn();
79
+ });
80
+ it("should sign and execute GET request", async () => {
81
+ const testUrl = "https://example.com/test";
82
+ const signedHeaders = {
83
+ authorization: "AWS4-HMAC-SHA256 ...",
84
+ "x-amz-date": "20231201T120000Z",
85
+ };
86
+ mockSign.mockResolvedValue({
87
+ method: "GET",
88
+ headers: signedHeaders,
89
+ body: undefined,
90
+ });
91
+ const mockResponse = new Response("test response");
92
+ mockFetch.mockResolvedValue(mockResponse);
93
+ const options = {
94
+ region: "us-east-1",
95
+ service: "lambda",
96
+ baseFetch: mockFetch,
97
+ };
98
+ const sigV4Fetch = new SigV4FetchLike(options);
99
+ const result = await sigV4Fetch.fetch(testUrl, undefined);
100
+ expect(HttpRequest).toHaveBeenCalledWith({
101
+ method: "GET",
102
+ protocol: "https:",
103
+ hostname: "example.com",
104
+ path: "/test",
105
+ headers: {
106
+ host: "example.com",
107
+ },
108
+ body: undefined,
109
+ });
110
+ expect(mockSign).toHaveBeenCalledWith(expect.any(HttpRequest));
111
+ expect(mockFetch).toHaveBeenCalledWith(new URL(testUrl), {
112
+ method: "GET",
113
+ headers: signedHeaders,
114
+ body: undefined,
115
+ });
116
+ expect(result).toBe(mockResponse);
117
+ });
118
+ it("should sign and execute POST request with body", async () => {
119
+ const testUrl = "https://example.com/api";
120
+ const requestInit = {
121
+ method: "POST",
122
+ headers: {
123
+ "content-type": "application/json",
124
+ },
125
+ body: JSON.stringify({ test: "data" }),
126
+ };
127
+ const signedHeaders = {
128
+ authorization: "AWS4-HMAC-SHA256 ...",
129
+ "x-amz-date": "20231201T120000Z",
130
+ "content-type": "application/json",
131
+ };
132
+ mockSign.mockResolvedValue({
133
+ method: "POST",
134
+ headers: signedHeaders,
135
+ body: requestInit.body,
136
+ });
137
+ const mockResponse = new Response("success");
138
+ mockFetch.mockResolvedValue(mockResponse);
139
+ const options = {
140
+ region: "us-east-1",
141
+ service: "lambda",
142
+ baseFetch: mockFetch,
143
+ };
144
+ const sigV4Fetch = new SigV4FetchLike(options);
145
+ const result = await sigV4Fetch.fetch(testUrl, requestInit);
146
+ expect(HttpRequest).toHaveBeenCalledWith({
147
+ method: "POST",
148
+ protocol: "https:",
149
+ hostname: "example.com",
150
+ path: "/api",
151
+ headers: {
152
+ host: "example.com",
153
+ "content-type": "application/json",
154
+ },
155
+ body: requestInit.body,
156
+ });
157
+ expect(mockSign).toHaveBeenCalledWith(expect.any(HttpRequest));
158
+ expect(mockFetch).toHaveBeenCalledWith(new URL(testUrl), {
159
+ method: "POST",
160
+ headers: signedHeaders,
161
+ body: requestInit.body,
162
+ });
163
+ expect(result).toBe(mockResponse);
164
+ });
165
+ it("should handle URL with port and query parameters", async () => {
166
+ const testUrl = "https://example.com:8443/path?param=value";
167
+ mockSign.mockResolvedValue({
168
+ method: "GET",
169
+ headers: { authorization: "AWS4-HMAC-SHA256 ..." },
170
+ body: undefined,
171
+ });
172
+ mockFetch.mockResolvedValue(new Response("test"));
173
+ const options = {
174
+ region: "us-east-1",
175
+ service: "lambda",
176
+ baseFetch: mockFetch,
177
+ };
178
+ const sigV4Fetch = new SigV4FetchLike(options);
179
+ await sigV4Fetch.fetch(testUrl, undefined);
180
+ expect(HttpRequest).toHaveBeenCalledWith({
181
+ method: "GET",
182
+ protocol: "https:",
183
+ hostname: "example.com",
184
+ port: 8443,
185
+ path: "/path?param=value",
186
+ headers: {
187
+ host: "example.com",
188
+ },
189
+ body: undefined,
190
+ });
191
+ });
192
+ it("should handle Headers object in request init", async () => {
193
+ const testUrl = "https://example.com/test";
194
+ const headers = new Headers();
195
+ headers.set("Content-Type", "application/json");
196
+ headers.set("X-Custom-Header", "custom-value");
197
+ const requestInit = {
198
+ method: "POST",
199
+ headers,
200
+ body: "test body",
201
+ };
202
+ mockSign.mockResolvedValue({
203
+ method: "POST",
204
+ headers: { authorization: "AWS4-HMAC-SHA256 ..." },
205
+ body: "test body",
206
+ });
207
+ mockFetch.mockResolvedValue(new Response("test"));
208
+ const options = {
209
+ region: "us-east-1",
210
+ service: "lambda",
211
+ baseFetch: mockFetch,
212
+ };
213
+ const sigV4Fetch = new SigV4FetchLike(options);
214
+ await sigV4Fetch.fetch(testUrl, requestInit);
215
+ expect(HttpRequest).toHaveBeenCalledWith({
216
+ method: "POST",
217
+ protocol: "https:",
218
+ hostname: "example.com",
219
+ path: "/test",
220
+ headers: {
221
+ host: "example.com",
222
+ "content-type": "application/json",
223
+ "x-custom-header": "custom-value",
224
+ },
225
+ body: "test body",
226
+ });
227
+ });
228
+ it("should handle array headers in request init", async () => {
229
+ const testUrl = "https://example.com/test";
230
+ const requestInit = {
231
+ method: "POST",
232
+ headers: [
233
+ ["Content-Type", "application/json"],
234
+ ["X-Custom-Header", "custom-value"],
235
+ ],
236
+ body: "test body",
237
+ };
238
+ mockSign.mockResolvedValue({
239
+ method: "POST",
240
+ headers: { authorization: "AWS4-HMAC-SHA256 ..." },
241
+ body: "test body",
242
+ });
243
+ mockFetch.mockResolvedValue(new Response("test"));
244
+ const options = {
245
+ region: "us-east-1",
246
+ service: "lambda",
247
+ baseFetch: mockFetch,
248
+ };
249
+ const sigV4Fetch = new SigV4FetchLike(options);
250
+ await sigV4Fetch.fetch(testUrl, requestInit);
251
+ expect(HttpRequest).toHaveBeenCalledWith({
252
+ method: "POST",
253
+ protocol: "https:",
254
+ hostname: "example.com",
255
+ path: "/test",
256
+ headers: {
257
+ host: "example.com",
258
+ "content-type": "application/json",
259
+ "x-custom-header": "custom-value",
260
+ },
261
+ body: "test body",
262
+ });
263
+ });
264
+ it("should handle URL object as input", async () => {
265
+ const testUrl = new URL("https://example.com/test");
266
+ mockSign.mockResolvedValue({
267
+ method: "GET",
268
+ headers: { authorization: "AWS4-HMAC-SHA256 ..." },
269
+ body: undefined,
270
+ });
271
+ mockFetch.mockResolvedValue(new Response("test"));
272
+ const options = {
273
+ region: "us-east-1",
274
+ service: "lambda",
275
+ baseFetch: mockFetch,
276
+ };
277
+ const sigV4Fetch = new SigV4FetchLike(options);
278
+ await sigV4Fetch.fetch(testUrl, undefined);
279
+ expect(HttpRequest).toHaveBeenCalledWith({
280
+ method: "GET",
281
+ protocol: "https:",
282
+ hostname: "example.com",
283
+ path: "/test",
284
+ headers: {
285
+ host: "example.com",
286
+ },
287
+ body: undefined,
288
+ });
289
+ });
290
+ it("should propagate signing errors", async () => {
291
+ const testUrl = "https://example.com/test";
292
+ mockSign.mockRejectedValue(new Error("Signing failed"));
293
+ const options = {
294
+ region: "us-east-1",
295
+ service: "lambda",
296
+ baseFetch: mockFetch,
297
+ };
298
+ const sigV4Fetch = new SigV4FetchLike(options);
299
+ await expect(sigV4Fetch.fetch(testUrl, undefined)).rejects.toThrow("Signing failed");
300
+ expect(mockFetch).not.toHaveBeenCalled();
301
+ });
302
+ it("should propagate fetch errors", async () => {
303
+ const testUrl = "https://example.com/test";
304
+ mockSign.mockResolvedValue({
305
+ method: "GET",
306
+ headers: { authorization: "AWS4-HMAC-SHA256 ..." },
307
+ body: undefined,
308
+ });
309
+ mockFetch.mockRejectedValue(new Error("Network error"));
310
+ const options = {
311
+ region: "us-east-1",
312
+ service: "lambda",
313
+ baseFetch: mockFetch,
314
+ };
315
+ const sigV4Fetch = new SigV4FetchLike(options);
316
+ await expect(sigV4Fetch.fetch(testUrl, undefined)).rejects.toThrow("Network error");
317
+ });
318
+ });
319
+ describe("createSigV4Fetch", () => {
320
+ let mockFetch;
321
+ beforeEach(() => {
322
+ mockFetch = jest.fn();
323
+ });
324
+ it("should create a FetchLike function that signs requests", async () => {
325
+ const testUrl = "https://example.com/test";
326
+ const signedHeaders = {
327
+ authorization: "AWS4-HMAC-SHA256 ...",
328
+ "x-amz-date": "20231201T120000Z",
329
+ };
330
+ mockSign.mockResolvedValue({
331
+ method: "GET",
332
+ headers: signedHeaders,
333
+ body: undefined,
334
+ });
335
+ const mockResponse = new Response("test response");
336
+ mockFetch.mockResolvedValue(mockResponse);
337
+ const options = {
338
+ region: "us-east-1",
339
+ service: "lambda",
340
+ baseFetch: mockFetch,
341
+ };
342
+ const sigV4Fetch = createSigV4Fetch(options);
343
+ const result = await sigV4Fetch(testUrl, undefined);
344
+ expect(HttpRequest).toHaveBeenCalledWith({
345
+ method: "GET",
346
+ protocol: "https:",
347
+ hostname: "example.com",
348
+ path: "/test",
349
+ headers: {
350
+ host: "example.com",
351
+ },
352
+ body: undefined,
353
+ });
354
+ expect(mockSign).toHaveBeenCalledWith(expect.any(HttpRequest));
355
+ expect(mockFetch).toHaveBeenCalledWith(new URL(testUrl), {
356
+ method: "GET",
357
+ headers: signedHeaders,
358
+ body: undefined,
359
+ });
360
+ expect(result).toBe(mockResponse);
361
+ });
362
+ it("should work with StreamableHTTPClientTransport", async () => {
363
+ const testUrl = "https://example.com/test";
364
+ const signedHeaders = {
365
+ authorization: "AWS4-HMAC-SHA256 ...",
366
+ "x-amz-date": "20231201T120000Z",
367
+ };
368
+ mockSign.mockResolvedValue({
369
+ method: "GET",
370
+ headers: signedHeaders,
371
+ body: undefined,
372
+ });
373
+ const mockResponse = new Response("test response");
374
+ mockFetch.mockResolvedValue(mockResponse);
375
+ const options = {
376
+ region: "us-east-1",
377
+ service: "lambda",
378
+ baseFetch: mockFetch,
379
+ };
380
+ const sigV4Fetch = createSigV4Fetch(options);
381
+ // This demonstrates how users can now use the SigV4 fetch with any transport
382
+ const result = await sigV4Fetch(testUrl, { method: "GET" });
383
+ expect(result).toBe(mockResponse);
384
+ expect(mockSign).toHaveBeenCalled();
385
+ expect(mockFetch).toHaveBeenCalled();
386
+ });
387
+ });
388
+ describe("credential provider integration", () => {
389
+ test("should use default provider when no credentials provided", () => {
390
+ const url = new URL("https://example.com/mcp");
391
+ const options = {
392
+ region: "us-east-1",
393
+ service: "lambda",
394
+ };
395
+ new StreamableHTTPClientWithSigV4Transport(url, options);
396
+ expect(defaultProvider).toHaveBeenCalled();
397
+ expect(SignatureV4).toHaveBeenCalledWith({
398
+ service: "lambda",
399
+ region: "us-east-1",
400
+ credentials: mockDefaultProvider,
401
+ sha256: expect.any(Function),
402
+ });
403
+ });
404
+ test("should use provided credential provider function", () => {
405
+ const url = new URL("https://example.com/mcp");
406
+ const customProvider = jest.fn().mockResolvedValue(mockCredentials);
407
+ const options = {
408
+ region: "us-east-1",
409
+ service: "lambda",
410
+ credentials: customProvider,
411
+ };
412
+ new StreamableHTTPClientWithSigV4Transport(url, options);
413
+ expect(SignatureV4).toHaveBeenCalledWith({
414
+ service: "lambda",
415
+ region: "us-east-1",
416
+ credentials: customProvider,
417
+ sha256: expect.any(Function),
418
+ });
419
+ expect(defaultProvider).not.toHaveBeenCalled();
420
+ });
421
+ test("should use provided static credentials", () => {
422
+ const url = new URL("https://example.com/mcp");
423
+ const staticCredentials = {
424
+ accessKeyId: "static-key",
425
+ secretAccessKey: "static-secret",
426
+ };
427
+ const options = {
428
+ region: "us-east-1",
429
+ service: "lambda",
430
+ credentials: staticCredentials,
431
+ };
432
+ new StreamableHTTPClientWithSigV4Transport(url, options);
433
+ expect(SignatureV4).toHaveBeenCalledWith({
434
+ service: "lambda",
435
+ region: "us-east-1",
436
+ credentials: staticCredentials,
437
+ sha256: expect.any(Function),
438
+ });
439
+ expect(defaultProvider).not.toHaveBeenCalled();
440
+ });
441
+ });
442
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aws/run-mcp-servers-with-aws-lambda",
3
3
  "description": "Run Model Context Protocol (MCP) servers with AWS Lambda",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "author": {
@@ -53,8 +53,13 @@
53
53
  "typescript-eslint": "^8.35.1"
54
54
  },
55
55
  "dependencies": {
56
+ "@aws-crypto/sha256-js": "^5.2.0",
56
57
  "@aws-sdk/client-lambda": "^3.840.0",
58
+ "@aws-sdk/credential-provider-node": "^3.840.0",
59
+ "@aws-sdk/protocol-http": "^3.370.0",
60
+ "@aws-sdk/types": "^3.840.0",
57
61
  "@modelcontextprotocol/sdk": "^1.15.0",
62
+ "@smithy/signature-v4": "^5.1.2",
58
63
  "@types/aws-lambda": "^8.10.149",
59
64
  "winston": "^3.17.0"
60
65
  }