Skip to content

Commit 9994c70

Browse files
committed
e2e_test for typescript
1 parent 5c19ff8 commit 9994c70

File tree

15 files changed

+4846
-38
lines changed

15 files changed

+4846
-38
lines changed

e2e_tests/typescript/package-lock.json

Lines changed: 4112 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e_tests/typescript/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "mcp-e2e-test-typescript",
3+
"version": "0.1.0",
4+
"main": "build/main.js",
5+
"types": "build/main.d.ts",
6+
"scripts": {
7+
"build": "tsc",
8+
"test": "node build/main.js",
9+
"lint": "eslint . --ext .ts"
10+
},
11+
"dependencies": {
12+
"@aws-sdk/client-bedrock-runtime": "^3.750.0",
13+
"@modelcontextprotocol/sdk": "^1.5.0",
14+
"winston": "^3.17.0"
15+
},
16+
"devDependencies": {
17+
"@eslint/js": "^9.8.0",
18+
"@types/node": "^22.13.5",
19+
"eslint": "^9.8.0",
20+
"typescript": "^5.7.3",
21+
"typescript-eslint": "^8.0.0"
22+
}
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"stdioServers": {
3+
"fetch": {
4+
"command": "uvx",
5+
"args": ["mcp-server-fetch"]
6+
}
7+
},
8+
"lambdaFunctionServers": {
9+
"time": {
10+
"functionName": "mcp-server-time",
11+
"region": "us-east-2"
12+
},
13+
"weatherAlerts": {
14+
"functionName": "mcp-server-weather-alerts",
15+
"region": "us-east-2"
16+
}
17+
}
18+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Server } from "./server_clients/server.js";
2+
import { Servers } from "./server_clients/servers.js";
3+
import { LLMClient } from "./llm_client.js";
4+
import logger from "./logger.js";
5+
import {
6+
ContentBlock,
7+
ConverseCommandOutput,
8+
Message,
9+
} from "@aws-sdk/client-bedrock-runtime";
10+
11+
/**
12+
* Orchestrates the interaction between user, LLM, and tools.
13+
*/
14+
export class ChatSession {
15+
private servers: Server[];
16+
private llmClient: LLMClient;
17+
private userUtterances: string[] = [];
18+
19+
constructor(
20+
servers: Server[],
21+
llmClient: LLMClient,
22+
userUtterances: string[]
23+
) {
24+
this.servers = servers;
25+
this.llmClient = llmClient;
26+
this.userUtterances = userUtterances;
27+
}
28+
29+
/**
30+
* Process the LLM response and execute tools if needed.
31+
* @param serversManager The servers manager.
32+
* @param llmResponse The response from the Bedrock Converse API.
33+
* @returns The result of tool execution, if any.
34+
*/
35+
async executeRequestedTools(
36+
serversManager: Servers,
37+
llmResponse: ConverseCommandOutput
38+
): Promise<Message | null> {
39+
const stopReason = llmResponse.stopReason;
40+
41+
if (stopReason === "tool_use") {
42+
try {
43+
const toolResponses: ContentBlock[] = [];
44+
for (const contentItem of llmResponse.output!.message!.content!) {
45+
if ("toolUse" in contentItem) {
46+
logger.info(`Executing tool: ${contentItem.toolUse!.name}`);
47+
logger.info(
48+
`With arguments: ${JSON.stringify(contentItem.toolUse!.input)}`
49+
);
50+
const response = await serversManager.executeTool(
51+
contentItem.toolUse!.name!,
52+
contentItem.toolUse!.toolUseId!,
53+
contentItem.toolUse!.input! as Record<string, any>
54+
);
55+
toolResponses.push(response);
56+
}
57+
}
58+
return { role: "user", content: toolResponses };
59+
} catch (e) {
60+
throw new Error(`Failed to execute tool: ${e}`);
61+
}
62+
} else {
63+
// Assume this catches stop reasons "end_turn", "stop_sequence", and "max_tokens"
64+
return null;
65+
}
66+
}
67+
68+
/**
69+
* Main chat session handler.
70+
*/
71+
async start(): Promise<void> {
72+
const serverManager = new Servers(this.servers);
73+
74+
try {
75+
await serverManager.initialize();
76+
77+
const allTools = await serverManager.listTools();
78+
const toolsDescription = allTools.map((tool) => tool.formatForLLM());
79+
80+
const systemPrompt = "You are a helpful assistant.";
81+
82+
const messages: Message[] = [];
83+
84+
for (const [i, userInput] of this.userUtterances.entries()) {
85+
if (i != 0) {
86+
console.log("\n**Pausing 5 seconds to avoid Bedrock throttling**");
87+
await new Promise((resolve) => setTimeout(resolve, 5000));
88+
}
89+
90+
console.log(`\You: ${userInput}`);
91+
92+
messages.push({ role: "user", content: [{ text: userInput }] });
93+
94+
const llmResponse = await this.llmClient.getResponse(
95+
messages,
96+
systemPrompt,
97+
toolsDescription
98+
);
99+
100+
logger.debug("\nAssistant: " + JSON.stringify(llmResponse, null, 2));
101+
console.log(
102+
`\nAssistant: ${llmResponse.output!.message!.content![0].text}`
103+
);
104+
messages.push(llmResponse.output!.message!);
105+
106+
const toolResults = await this.executeRequestedTools(
107+
serverManager,
108+
llmResponse
109+
);
110+
111+
if (toolResults) {
112+
logger.debug(
113+
"\nTool Results: " + JSON.stringify(toolResults, null, 2)
114+
);
115+
messages.push(toolResults);
116+
const finalResponse = await this.llmClient.getResponse(
117+
messages,
118+
systemPrompt,
119+
toolsDescription
120+
);
121+
logger.debug(
122+
"\nFinal response: " + JSON.stringify(finalResponse, null, 2)
123+
);
124+
console.log(
125+
`\nAssistant: ${finalResponse.output!.message!.content![0].text}`
126+
);
127+
messages.push(finalResponse.output!.message!);
128+
}
129+
}
130+
} finally {
131+
await serverManager.close();
132+
}
133+
}
134+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime';
2+
import * as fs from 'fs';
3+
4+
/**
5+
* Manages configuration for the MCP client and the Bedrock client.
6+
*/
7+
export class Configuration {
8+
modelId: string;
9+
region: string;
10+
11+
/**
12+
* Initialize configuration.
13+
*/
14+
constructor(
15+
modelId: string = 'anthropic.claude-3-5-sonnet-20241022-v2:0',
16+
region: string = 'us-west-2'
17+
) {
18+
this.modelId = modelId;
19+
this.region = region;
20+
}
21+
22+
/**
23+
* Load server configuration from JSON file.
24+
* @param filePath Path to the JSON configuration file.
25+
* @returns Dict containing server configuration.
26+
* @throws Error if configuration file doesn't exist or is invalid JSON.
27+
*/
28+
static loadConfig(filePath: string): Record<string, any> {
29+
try {
30+
const fileContent = fs.readFileSync(filePath, 'utf8');
31+
return JSON.parse(fileContent);
32+
} catch (e) {
33+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
34+
throw new Error(`Configuration file not found: ${filePath}`);
35+
} else {
36+
throw new Error(`Error parsing configuration file: ${e}`);
37+
}
38+
}
39+
}
40+
41+
/**
42+
* Get a Bedrock runtime client.
43+
* @returns The Bedrock client.
44+
*/
45+
get bedrockClient(): BedrockRuntimeClient {
46+
return new BedrockRuntimeClient({ region: this.region });
47+
}
48+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
BedrockRuntimeClient,
3+
ConverseCommand,
4+
ConverseCommandOutput,
5+
Message,
6+
Tool,
7+
} from "@aws-sdk/client-bedrock-runtime";
8+
9+
/**
10+
* Manages communication with Bedrock.
11+
*/
12+
export class LLMClient {
13+
private bedrockClient: BedrockRuntimeClient;
14+
private modelId: string;
15+
16+
constructor(bedrockClient: BedrockRuntimeClient, modelId: string) {
17+
this.bedrockClient = bedrockClient;
18+
this.modelId = modelId;
19+
}
20+
21+
/**
22+
* Get a response from the LLM, using the Bedrock Converse API.
23+
* @param messages A list of message for the model.
24+
* @param systemPrompt The system prompt to use.
25+
* @param tools The list of tools to make available to the model.
26+
* @returns The LLM's full response.
27+
*/
28+
async getResponse(
29+
messages: Message[],
30+
systemPrompt: string,
31+
tools: Tool[]
32+
): Promise<ConverseCommandOutput> {
33+
const command = new ConverseCommand({
34+
modelId: this.modelId,
35+
messages,
36+
system: [{ text: systemPrompt }],
37+
inferenceConfig: {
38+
maxTokens: 4096,
39+
temperature: 0.7,
40+
topP: 1,
41+
},
42+
toolConfig: { tools },
43+
});
44+
45+
return await this.bedrockClient.send(command);
46+
}
47+
}

e2e_tests/typescript/src/logger.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import winston from 'winston';
2+
3+
// Configure the logger
4+
const logger = winston.createLogger({
5+
level: process.env.LOG_LEVEL || 'info',
6+
format: winston.format.combine(
7+
winston.format.timestamp(),
8+
winston.format.printf(({ level, message, timestamp }) => {
9+
return `${timestamp} - ${level.toUpperCase()}: ${message}`;
10+
})
11+
),
12+
transports: [
13+
new winston.transports.Console({
14+
format: winston.format.combine(
15+
winston.format.colorize(),
16+
winston.format.timestamp(),
17+
winston.format.printf(({ level, message, timestamp }) => {
18+
return `${timestamp} - ${level.toUpperCase()}: ${message}`;
19+
})
20+
),
21+
}),
22+
],
23+
});
24+
25+
export default logger;

e2e_tests/typescript/src/main.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Configuration } from "./configuration.js";
2+
import { ChatSession } from "./chat_session.js";
3+
import { LLMClient } from "./llm_client.js";
4+
import { StdioServer } from "./server_clients/stdio_server.js";
5+
import { LambdaFunctionClient } from "./server_clients/lambda_function.js";
6+
import { Server } from "./server_clients/server.js";
7+
import logger from "./logger.js";
8+
9+
/**
10+
* Initialize and run the chat session.
11+
*/
12+
async function main(): Promise<void> {
13+
const config = new Configuration();
14+
const serverConfig = Configuration.loadConfig("./servers_config.json");
15+
16+
const servers: Server[] = [];
17+
18+
// Initialize stdio servers
19+
for (const [name, srvConfig] of Object.entries(serverConfig.stdioServers)) {
20+
servers.push(new StdioServer(name, srvConfig as Record<string, any>));
21+
}
22+
23+
// Initialize Lambda function servers
24+
for (const [name, srvConfig] of Object.entries(
25+
serverConfig.lambdaFunctionServers
26+
)) {
27+
servers.push(
28+
new LambdaFunctionClient(name, srvConfig as Record<string, any>)
29+
);
30+
}
31+
32+
const userUtterances = [
33+
"Hello!",
34+
"What is the current time in Seattle?",
35+
"Are there any weather alerts right now?",
36+
"Who is Tom Cruise?",
37+
];
38+
39+
const llmClient = new LLMClient(config.bedrockClient, config.modelId);
40+
const chatSession = new ChatSession(servers, llmClient, userUtterances);
41+
42+
await chatSession.start();
43+
}
44+
45+
// Handle errors
46+
process.on("unhandledRejection", (reason, promise) => {
47+
logger.error("Unhandled Rejection at:", promise, "reason:", reason);
48+
process.exit(1);
49+
});
50+
51+
// Run the main function
52+
main().catch((error) => {
53+
logger.error("Error in main:", error);
54+
process.exit(1);
55+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
LambdaFunctionParameters,
3+
LambdaFunctionClientTransport,
4+
} from "mcp-lambda";
5+
import { Server } from "./server.js";
6+
import logger from "../logger.js";
7+
8+
/**
9+
* Manages MCP server connections and tool execution for servers running in Lambda functions.
10+
*/
11+
export class LambdaFunctionClient extends Server {
12+
/**
13+
* Initialize the server connection.
14+
* @throws ValueError if initialization parameters are invalid
15+
* @throws RuntimeError if server fails to initialize
16+
*/
17+
async initialize(): Promise<void> {
18+
if (!this.config.functionName) {
19+
throw new Error(
20+
"The functionName must be a valid string and cannot be None."
21+
);
22+
}
23+
24+
const serverParams: LambdaFunctionParameters = {
25+
functionName: this.config.functionName,
26+
regionName: this.config.region,
27+
};
28+
29+
const transport = new LambdaFunctionClientTransport(serverParams);
30+
31+
try {
32+
await this.client.connect(transport);
33+
} catch (e) {
34+
logger.error(`Error initializing server ${this.name}: ${e}`);
35+
throw e;
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)