@aws/run-mcp-servers-with-aws-lambda 0.0.1
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/client/index.d.ts +27 -0
- package/dist/client/index.js +51 -0
- package/dist/client/index.test.d.ts +1 -0
- package/dist/client/index.test.js +191 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/server-adapter/index.d.ts +4 -0
- package/dist/server-adapter/index.js +110 -0
- package/dist/server-adapter/index.test.d.ts +1 -0
- package/dist/server-adapter/index.test.js +189 -0
- package/package.json +61 -0
|
@@ -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 @@
|
|
|
1
|
+
import "aws-sdk-client-mock-jest";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import "aws-sdk-client-mock-jest";
|
|
2
|
+
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
|
|
3
|
+
import { LambdaFunctionClientTransport, } from "./index.js";
|
|
4
|
+
import { Uint8ArrayBlobAdapter } from "@smithy/util-stream";
|
|
5
|
+
import { mockClient } from "aws-sdk-client-mock";
|
|
6
|
+
describe("LambdaFunctionClientTransport", () => {
|
|
7
|
+
// Mock implementation of LambdaClient
|
|
8
|
+
const lambdaMock = mockClient(LambdaClient);
|
|
9
|
+
// Reset mocks before each test
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
lambdaMock.reset();
|
|
13
|
+
});
|
|
14
|
+
describe("constructor", () => {
|
|
15
|
+
test("should initialize client with provided region", async () => {
|
|
16
|
+
const params = {
|
|
17
|
+
functionName: "test-function",
|
|
18
|
+
regionName: "not-a-region",
|
|
19
|
+
};
|
|
20
|
+
const message = {
|
|
21
|
+
jsonrpc: "2.0",
|
|
22
|
+
id: 1,
|
|
23
|
+
method: "test-method",
|
|
24
|
+
};
|
|
25
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
26
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({
|
|
27
|
+
Payload: Uint8ArrayBlobAdapter.fromString(JSON.stringify({ result: "success" })),
|
|
28
|
+
});
|
|
29
|
+
await transport.send(message);
|
|
30
|
+
expect(await lambdaMock.call(0).thisValue.config.region()).toBe("not-a-region");
|
|
31
|
+
});
|
|
32
|
+
test("should use the default region", async () => {
|
|
33
|
+
const originalRegion = process.env.AWS_REGION;
|
|
34
|
+
process.env.AWS_REGION = "default-region";
|
|
35
|
+
try {
|
|
36
|
+
const params = {
|
|
37
|
+
functionName: "test-function",
|
|
38
|
+
};
|
|
39
|
+
const message = {
|
|
40
|
+
jsonrpc: "2.0",
|
|
41
|
+
id: 1,
|
|
42
|
+
method: "test-method",
|
|
43
|
+
};
|
|
44
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
45
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({
|
|
46
|
+
Payload: Uint8ArrayBlobAdapter.fromString(JSON.stringify({ result: "success" })),
|
|
47
|
+
});
|
|
48
|
+
await transport.send(message);
|
|
49
|
+
expect(await lambdaMock.call(0).thisValue.config.region()).toBe("default-region");
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
process.env.AWS_REGION = originalRegion;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("start and close methods", () => {
|
|
57
|
+
test("start method should be a no-op", async () => {
|
|
58
|
+
const params = {
|
|
59
|
+
functionName: "test-function",
|
|
60
|
+
};
|
|
61
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
62
|
+
await expect(transport.start()).resolves.toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
test("close method should be a no-op", async () => {
|
|
65
|
+
const params = {
|
|
66
|
+
functionName: "test-function",
|
|
67
|
+
};
|
|
68
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
69
|
+
await expect(transport.close()).resolves.toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("send method", () => {
|
|
73
|
+
test("should invoke Lambda function with correct parameters", async () => {
|
|
74
|
+
const params = {
|
|
75
|
+
functionName: "test-function",
|
|
76
|
+
};
|
|
77
|
+
const message = {
|
|
78
|
+
jsonrpc: "2.0",
|
|
79
|
+
id: 1,
|
|
80
|
+
method: "test-method",
|
|
81
|
+
};
|
|
82
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
83
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({
|
|
84
|
+
Payload: Uint8ArrayBlobAdapter.fromString(JSON.stringify({ result: "success" })),
|
|
85
|
+
});
|
|
86
|
+
await transport.send(message);
|
|
87
|
+
expect(lambdaMock).toHaveReceivedCommandTimes(InvokeCommand, 1);
|
|
88
|
+
expect(lambdaMock).toHaveReceivedCommandWith(InvokeCommand, {
|
|
89
|
+
FunctionName: "test-function",
|
|
90
|
+
InvocationType: "RequestResponse",
|
|
91
|
+
Payload: JSON.stringify(message),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
test("should call onmessage with parsed response", async () => {
|
|
95
|
+
const params = {
|
|
96
|
+
functionName: "test-function",
|
|
97
|
+
};
|
|
98
|
+
const message = {
|
|
99
|
+
jsonrpc: "2.0",
|
|
100
|
+
id: 1,
|
|
101
|
+
method: "test-method",
|
|
102
|
+
};
|
|
103
|
+
const responsePayload = {
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
id: 1,
|
|
106
|
+
result: { data: "test-data" },
|
|
107
|
+
};
|
|
108
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
109
|
+
const onMessageMock = jest.fn();
|
|
110
|
+
transport.onmessage = onMessageMock;
|
|
111
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({
|
|
112
|
+
Payload: Uint8ArrayBlobAdapter.fromString(JSON.stringify(responsePayload)),
|
|
113
|
+
});
|
|
114
|
+
await transport.send(message);
|
|
115
|
+
expect(onMessageMock).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(onMessageMock).toHaveBeenCalledWith(responsePayload);
|
|
117
|
+
});
|
|
118
|
+
test("should not call onmessage for empty response", async () => {
|
|
119
|
+
const params = {
|
|
120
|
+
functionName: "test-function",
|
|
121
|
+
};
|
|
122
|
+
const message = {
|
|
123
|
+
jsonrpc: "2.0",
|
|
124
|
+
method: "notification",
|
|
125
|
+
};
|
|
126
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
127
|
+
const onMessageMock = jest.fn();
|
|
128
|
+
transport.onmessage = onMessageMock;
|
|
129
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({
|
|
130
|
+
Payload: Uint8ArrayBlobAdapter.fromString("{}"),
|
|
131
|
+
});
|
|
132
|
+
await transport.send(message);
|
|
133
|
+
expect(onMessageMock).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
test("should throw error when Lambda function returns an error", async () => {
|
|
136
|
+
const params = {
|
|
137
|
+
functionName: "test-function",
|
|
138
|
+
};
|
|
139
|
+
const message = {
|
|
140
|
+
jsonrpc: "2.0",
|
|
141
|
+
id: 1,
|
|
142
|
+
method: "test-method",
|
|
143
|
+
};
|
|
144
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
145
|
+
const onErrorMock = jest.fn();
|
|
146
|
+
transport.onerror = onErrorMock;
|
|
147
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({
|
|
148
|
+
FunctionError: "Unhandled",
|
|
149
|
+
Payload: Uint8ArrayBlobAdapter.fromString("Error executing function"),
|
|
150
|
+
});
|
|
151
|
+
await transport.send(message);
|
|
152
|
+
expect(onErrorMock).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(onErrorMock.mock.calls[0][0].message).toBe("Unhandled Error executing function");
|
|
154
|
+
});
|
|
155
|
+
test("should call onerror when Lambda client throws an error", async () => {
|
|
156
|
+
const params = {
|
|
157
|
+
functionName: "test-function",
|
|
158
|
+
};
|
|
159
|
+
const message = {
|
|
160
|
+
jsonrpc: "2.0",
|
|
161
|
+
id: 1,
|
|
162
|
+
method: "test-method",
|
|
163
|
+
};
|
|
164
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
165
|
+
const onErrorMock = jest.fn();
|
|
166
|
+
transport.onerror = onErrorMock;
|
|
167
|
+
const error = new Error("Network error");
|
|
168
|
+
lambdaMock.rejects(error);
|
|
169
|
+
await transport.send(message);
|
|
170
|
+
expect(onErrorMock).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(onErrorMock).toHaveBeenCalledWith(error);
|
|
172
|
+
});
|
|
173
|
+
test("should handle case when no Payload is returned", async () => {
|
|
174
|
+
const params = {
|
|
175
|
+
functionName: "test-function",
|
|
176
|
+
};
|
|
177
|
+
const message = {
|
|
178
|
+
jsonrpc: "2.0",
|
|
179
|
+
id: 1,
|
|
180
|
+
method: "test-method",
|
|
181
|
+
};
|
|
182
|
+
const transport = new LambdaFunctionClientTransport(params);
|
|
183
|
+
const onErrorMock = jest.fn();
|
|
184
|
+
transport.onerror = onErrorMock;
|
|
185
|
+
lambdaMock.on(InvokeCommand).resolvesOnce({});
|
|
186
|
+
await transport.send(message);
|
|
187
|
+
expect(onErrorMock).toHaveBeenCalledTimes(1);
|
|
188
|
+
expect(onErrorMock).toHaveBeenCalledWith(new Error("No payload returned from Lambda function"));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Context } from 'aws-lambda';
|
|
2
|
+
import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
export declare function stdioServerAdapter(serverParams: StdioServerParameters, event: JSONRPCMessage, context: Context): Promise<{}>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport, } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { JSONRPCNotificationSchema, JSONRPCRequestSchema, McpError, ResultSchema, ErrorCode, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { createLogger, format, transports } from 'winston';
|
|
6
|
+
const logger = createLogger({
|
|
7
|
+
level: process.env.LOG_LEVEL?.toLowerCase() || 'info',
|
|
8
|
+
format: format.simple(),
|
|
9
|
+
transports: [new transports.Console()],
|
|
10
|
+
});
|
|
11
|
+
export async function stdioServerAdapter(serverParams, event, context) {
|
|
12
|
+
logger.debug(`Request: ${JSON.stringify(event)}`);
|
|
13
|
+
const response = await handleMessage(serverParams, event, context);
|
|
14
|
+
logger.debug(`Response: ${JSON.stringify(response)}`);
|
|
15
|
+
return response;
|
|
16
|
+
}
|
|
17
|
+
async function handleMessage(serverParams, event, context) {
|
|
18
|
+
// Determine the type of the message
|
|
19
|
+
try {
|
|
20
|
+
const request = JSONRPCRequestSchema.parse(event);
|
|
21
|
+
return await handleRequest(serverParams, request, context);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof z.ZodError) {
|
|
25
|
+
try {
|
|
26
|
+
const notification = JSONRPCNotificationSchema.parse(event);
|
|
27
|
+
return await handleNotification(serverParams, notification, context);
|
|
28
|
+
}
|
|
29
|
+
catch (notificationError) {
|
|
30
|
+
if (notificationError instanceof z.ZodError) {
|
|
31
|
+
return {
|
|
32
|
+
jsonrpc: event.jsonrpc,
|
|
33
|
+
id: 0,
|
|
34
|
+
error: {
|
|
35
|
+
code: ErrorCode.InvalidRequest,
|
|
36
|
+
message: 'Request is neither a valid JSON-RPC request nor a valid JSON-RPC notification',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw notificationError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function handleNotification(_serverParams, _event, _context) {
|
|
51
|
+
// Ignore notifications
|
|
52
|
+
logger.debug('Ignoring notification');
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
async function handleRequest(serverParams, event, _context) {
|
|
56
|
+
logger.debug('Handling request');
|
|
57
|
+
const { jsonrpc, id, ...request } = event;
|
|
58
|
+
const client = new Client({
|
|
59
|
+
name: 'mcp-client',
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
}, {
|
|
62
|
+
capabilities: {
|
|
63
|
+
prompts: {},
|
|
64
|
+
resources: {},
|
|
65
|
+
tools: {},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const transport = new StdioClientTransport(serverParams);
|
|
70
|
+
await client.connect(transport);
|
|
71
|
+
const result = await client.request(request, ResultSchema);
|
|
72
|
+
return {
|
|
73
|
+
jsonrpc: jsonrpc,
|
|
74
|
+
id: id,
|
|
75
|
+
result: result,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof McpError) {
|
|
80
|
+
logger.error(`MCP error: ${error}`);
|
|
81
|
+
return {
|
|
82
|
+
jsonrpc: jsonrpc,
|
|
83
|
+
id: id,
|
|
84
|
+
error: {
|
|
85
|
+
code: error.code,
|
|
86
|
+
message: error.message,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
logger.error(`General exception: ${error}`);
|
|
92
|
+
return {
|
|
93
|
+
jsonrpc: jsonrpc,
|
|
94
|
+
id: id,
|
|
95
|
+
error: {
|
|
96
|
+
code: 500,
|
|
97
|
+
message: 'Internal failure, please check Lambda function logs',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
try {
|
|
104
|
+
await client.close();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
logger.error(`Did not cleanly close client ${error}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { ErrorCode, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { stdioServerAdapter } from './index.js';
|
|
3
|
+
const serverParameters = {
|
|
4
|
+
command: 'npx',
|
|
5
|
+
args: ['tsx', 'test-stdio-server/echo_server.ts'],
|
|
6
|
+
};
|
|
7
|
+
const mockContext = {
|
|
8
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
9
|
+
functionName: 'test-function',
|
|
10
|
+
functionVersion: '1',
|
|
11
|
+
invokedFunctionArn: 'test-arn',
|
|
12
|
+
memoryLimitInMB: '128',
|
|
13
|
+
awsRequestId: 'test-id',
|
|
14
|
+
logGroupName: 'test-group',
|
|
15
|
+
logStreamName: 'test-stream',
|
|
16
|
+
getRemainingTimeInMillis: () => 1000,
|
|
17
|
+
done: () => { },
|
|
18
|
+
fail: () => { },
|
|
19
|
+
succeed: () => { },
|
|
20
|
+
};
|
|
21
|
+
test('should respond to pings', async () => {
|
|
22
|
+
const pingMessage = {
|
|
23
|
+
jsonrpc: '2.0',
|
|
24
|
+
id: 1,
|
|
25
|
+
method: 'ping',
|
|
26
|
+
};
|
|
27
|
+
const expectedResponse = {
|
|
28
|
+
jsonrpc: '2.0',
|
|
29
|
+
id: 1,
|
|
30
|
+
result: {},
|
|
31
|
+
};
|
|
32
|
+
const response = (await stdioServerAdapter(serverParameters, pingMessage, mockContext));
|
|
33
|
+
expect(response).toEqual(expectedResponse);
|
|
34
|
+
});
|
|
35
|
+
test('should respond to initialize', async () => {
|
|
36
|
+
const initializeMessage = {
|
|
37
|
+
jsonrpc: '2.0',
|
|
38
|
+
id: 1,
|
|
39
|
+
method: 'initialize',
|
|
40
|
+
params: {
|
|
41
|
+
protocolVersion: '2024-11-05',
|
|
42
|
+
capabilities: {},
|
|
43
|
+
clientInfo: { name: 'mcp', version: '0.1.0' },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const response = (await stdioServerAdapter(serverParameters, initializeMessage, mockContext));
|
|
47
|
+
expect(response.jsonrpc).toEqual('2.0');
|
|
48
|
+
expect(response.id).toEqual(1);
|
|
49
|
+
expect(response).toHaveProperty('result');
|
|
50
|
+
});
|
|
51
|
+
test('should list tools', async () => {
|
|
52
|
+
const listToolsMessage = {
|
|
53
|
+
jsonrpc: '2.0',
|
|
54
|
+
id: 1,
|
|
55
|
+
method: 'tools/list',
|
|
56
|
+
};
|
|
57
|
+
const expectedResponse = {
|
|
58
|
+
jsonrpc: '2.0',
|
|
59
|
+
id: 1,
|
|
60
|
+
result: {
|
|
61
|
+
tools: [
|
|
62
|
+
{
|
|
63
|
+
name: 'echo',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
message: { type: 'string' },
|
|
69
|
+
},
|
|
70
|
+
required: ['message'],
|
|
71
|
+
type: 'object',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const response = (await stdioServerAdapter(serverParameters, listToolsMessage, mockContext));
|
|
78
|
+
expect(response).toEqual(expectedResponse);
|
|
79
|
+
});
|
|
80
|
+
test('should call a tool', async () => {
|
|
81
|
+
const callToolsMessage = {
|
|
82
|
+
jsonrpc: '2.0',
|
|
83
|
+
id: 1,
|
|
84
|
+
method: 'tools/call',
|
|
85
|
+
params: {
|
|
86
|
+
name: 'echo',
|
|
87
|
+
arguments: {
|
|
88
|
+
message: 'Hello world',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const expectedResponse = {
|
|
93
|
+
jsonrpc: '2.0',
|
|
94
|
+
id: 1,
|
|
95
|
+
result: {
|
|
96
|
+
content: [{ type: 'text', text: 'Echo: Hello world' }],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
const response = (await stdioServerAdapter(serverParameters, callToolsMessage, mockContext));
|
|
100
|
+
expect(response).toEqual(expectedResponse);
|
|
101
|
+
});
|
|
102
|
+
test('accepts a notification', async () => {
|
|
103
|
+
const notification = {
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
method: 'notifications/initialized',
|
|
106
|
+
};
|
|
107
|
+
const response = (await stdioServerAdapter(serverParameters, notification, mockContext));
|
|
108
|
+
expect(response).toEqual({});
|
|
109
|
+
});
|
|
110
|
+
test('return error for invalid request', async () => {
|
|
111
|
+
const invalidRequest = {
|
|
112
|
+
jsonrpc: '2.0',
|
|
113
|
+
id: 1,
|
|
114
|
+
error: {
|
|
115
|
+
code: 400,
|
|
116
|
+
message: 'Invalid request',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const expectedResponse = {
|
|
120
|
+
jsonrpc: '2.0',
|
|
121
|
+
id: 0,
|
|
122
|
+
error: {
|
|
123
|
+
code: ErrorCode.InvalidRequest,
|
|
124
|
+
message: 'Request is neither a valid JSON-RPC request nor a valid JSON-RPC notification',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const response = (await stdioServerAdapter(serverParameters, invalidRequest, mockContext));
|
|
128
|
+
expect(response).toEqual(expectedResponse);
|
|
129
|
+
});
|
|
130
|
+
test('return internal failure for invalid server params', async () => {
|
|
131
|
+
const invalidServerParams = {
|
|
132
|
+
command: 'does-not-exist',
|
|
133
|
+
};
|
|
134
|
+
const pingMessage = {
|
|
135
|
+
jsonrpc: '2.0',
|
|
136
|
+
id: 1,
|
|
137
|
+
method: 'ping',
|
|
138
|
+
};
|
|
139
|
+
const expectedResponse = {
|
|
140
|
+
jsonrpc: '2.0',
|
|
141
|
+
id: 1,
|
|
142
|
+
error: {
|
|
143
|
+
code: 500,
|
|
144
|
+
message: 'Internal failure, please check Lambda function logs',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const response = (await stdioServerAdapter(invalidServerParams, pingMessage, mockContext));
|
|
148
|
+
expect(response).toEqual(expectedResponse);
|
|
149
|
+
});
|
|
150
|
+
test('return error for unknown method', async () => {
|
|
151
|
+
const invalidMessage = {
|
|
152
|
+
jsonrpc: '2.0',
|
|
153
|
+
id: 1,
|
|
154
|
+
method: 'does-not-exist',
|
|
155
|
+
};
|
|
156
|
+
const expectedResponse = {
|
|
157
|
+
jsonrpc: '2.0',
|
|
158
|
+
id: 1,
|
|
159
|
+
error: {
|
|
160
|
+
code: ErrorCode.MethodNotFound,
|
|
161
|
+
message: 'MCP error -32601: Method not found',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const response = (await stdioServerAdapter(serverParameters, invalidMessage, mockContext));
|
|
165
|
+
expect(response).toEqual(expectedResponse);
|
|
166
|
+
});
|
|
167
|
+
test('return error for unknown tool call', async () => {
|
|
168
|
+
const callToolsMessage = {
|
|
169
|
+
jsonrpc: '2.0',
|
|
170
|
+
id: 1,
|
|
171
|
+
method: 'tools/call',
|
|
172
|
+
params: {
|
|
173
|
+
name: 'does-not-exist',
|
|
174
|
+
arguments: {
|
|
175
|
+
message: 'Hello world',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const expectedResponse = {
|
|
180
|
+
jsonrpc: '2.0',
|
|
181
|
+
id: 1,
|
|
182
|
+
error: {
|
|
183
|
+
code: ErrorCode.InvalidParams,
|
|
184
|
+
message: 'MCP error -32602: MCP error -32602: Tool does-not-exist not found',
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const response = (await stdioServerAdapter(serverParameters, callToolsMessage, mockContext));
|
|
188
|
+
expect(response).toEqual(expectedResponse);
|
|
189
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aws/run-mcp-servers-with-aws-lambda",
|
|
3
|
+
"description": "Run Model Context Protocol (MCP) servers with AWS Lambda",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Amazon Web Services",
|
|
9
|
+
"url": "http://aws.amazon.com"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"aws",
|
|
21
|
+
"lambda",
|
|
22
|
+
"mcp",
|
|
23
|
+
"modelcontextprotocol"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"prepack": "tsc",
|
|
28
|
+
"clean": "rm -rf dist/",
|
|
29
|
+
"lint": "eslint src/",
|
|
30
|
+
"test": "jest",
|
|
31
|
+
"test-server": "tsx test-stdio-server/echo_server.ts"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"exports": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@eslint/js": "^9.8.0",
|
|
43
|
+
"@tsconfig/recommended": "^1.0.2",
|
|
44
|
+
"@types/jest": "^29.5.12",
|
|
45
|
+
"@types/node": "^22.14.0",
|
|
46
|
+
"aws-sdk-client-mock": "^4.1.0",
|
|
47
|
+
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
48
|
+
"eslint": "^9.8.0",
|
|
49
|
+
"jest": "^29.7.0",
|
|
50
|
+
"ts-jest": "^29.3.1",
|
|
51
|
+
"tsx": "^4.16.5",
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"typescript-eslint": "^8.29.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@aws-sdk/client-lambda": "^3.777.0",
|
|
57
|
+
"@modelcontextprotocol/sdk": "^1.8.0",
|
|
58
|
+
"@types/aws-lambda": "^8.10.148",
|
|
59
|
+
"winston": "^3.17.0"
|
|
60
|
+
}
|
|
61
|
+
}
|