Skip to content

Commit 1a616a1

Browse files
committed
Q Dev CLI created a ts chatbot!
1 parent a2931cc commit 1a616a1

File tree

14 files changed

+746
-0
lines changed

14 files changed

+746
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# TypeScript MCP Chatbot Example
2+
3+
This example demonstrates how to build a simple chatbot that uses the Model Context Protocol (MCP) to interact with various tools. The chatbot uses AWS Bedrock for the LLM and connects to both local MCP servers and Lambda-based MCP servers.
4+
5+
## Features
6+
7+
- Connects to local MCP servers using stdio
8+
- Connects to remote MCP servers running in AWS Lambda
9+
- Uses AWS Bedrock Claude model for chat interactions
10+
- Supports tool use through the MCP protocol
11+
- Uses Winston for structured logging
12+
13+
## Setup
14+
15+
1. Install dependencies:
16+
17+
```bash
18+
npm install
19+
```
20+
21+
2. Build the TypeScript code:
22+
23+
```bash
24+
npm run build
25+
```
26+
27+
3. Make sure you have AWS credentials configured with access to AWS Bedrock and the Lambda functions.
28+
29+
## Running the Chatbot
30+
31+
Start the chatbot:
32+
33+
```bash
34+
npm start
35+
```
36+
37+
You can interact with the chatbot by typing messages. The chatbot will respond and may use tools when appropriate.
38+
39+
## Available Tools
40+
41+
The chatbot connects to three servers:
42+
43+
1. The Lambda function-based 'time' MCP server
44+
- Ask: "What is the current time?"
45+
46+
2. The Lambda function-based 'weather-alerts' MCP server
47+
- Ask: "Are there any weather alerts right now?"
48+
49+
3. A local 'fetch' MCP server
50+
- Ask: "Who is Tom Cruise?"
51+
52+
Type 'quit' or 'exit' to end the session.
53+
54+
## Configuration
55+
56+
The servers are configured in `servers_config.json`. You can modify this file to add or remove servers.
57+
58+
## Logging
59+
60+
The application uses Winston for logging. You can control the log level using environment variables:
61+
62+
```bash
63+
# For normal operation
64+
npm start
65+
66+
# For debug logging
67+
LOG_LEVEL=debug npm start
68+
69+
# Alternative debug mode
70+
DEBUG=true npm start
71+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "mcp-chatbot-typescript",
3+
"version": "1.0.0",
4+
"description": "TypeScript chatbot using Model Context Protocol with AWS Lambda",
5+
"main": "dist/main.js",
6+
"scripts": {
7+
"build": "tsc",
8+
"start": "node dist/main.js",
9+
"dev": "ts-node src/main.ts",
10+
"lint": "eslint . --ext .ts"
11+
},
12+
"dependencies": {
13+
"@aws-sdk/client-bedrock-runtime": "^3.529.1",
14+
"@modelcontextprotocol/sdk": "^0.1.0",
15+
"mcp-lambda": "file:../../src/typescript",
16+
"readline-sync": "^1.4.10",
17+
"winston": "^3.11.0"
18+
},
19+
"devDependencies": {
20+
"@types/node": "^20.11.24",
21+
"@types/readline-sync": "^1.4.8",
22+
"@typescript-eslint/eslint-plugin": "^7.0.2",
23+
"@typescript-eslint/parser": "^7.0.2",
24+
"eslint": "^8.56.0",
25+
"ts-node": "^10.9.2",
26+
"typescript": "^5.3.3"
27+
}
28+
}
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": "npx",
5+
"args": ["--offline", "@modelcontextprotocol/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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as readline from 'readline-sync';
2+
import { Server } from './server_clients/server.js';
3+
import { Servers } from './server_clients/servers.js';
4+
import { LLMClient } from './llm_client.js';
5+
import logger from './logger.js';
6+
7+
/**
8+
* Orchestrates the interaction between user, LLM, and tools.
9+
*/
10+
export class ChatSession {
11+
private servers: Server[];
12+
private llmClient: LLMClient;
13+
14+
constructor(servers: Server[], llmClient: LLMClient) {
15+
this.servers = servers;
16+
this.llmClient = llmClient;
17+
}
18+
19+
/**
20+
* Process the LLM response and execute tools if needed.
21+
* @param serversManager The servers manager.
22+
* @param llmResponse The response from the Bedrock Converse API.
23+
* @returns The result of tool execution, if any.
24+
*/
25+
async executeRequestedTools(
26+
serversManager: Servers,
27+
llmResponse: Record<string, any>
28+
): Promise<Record<string, any> | null> {
29+
const stopReason = llmResponse.stopReason;
30+
31+
if (stopReason === 'tool_use') {
32+
try {
33+
const toolResponses = [];
34+
for (const contentItem of llmResponse.output.message.content) {
35+
if ('toolUse' in contentItem) {
36+
logger.info(`Executing tool: ${contentItem.toolUse.name}`);
37+
logger.info(`With arguments: ${JSON.stringify(contentItem.toolUse.input)}`);
38+
const response = await serversManager.executeTool(
39+
contentItem.toolUse.name,
40+
contentItem.toolUse.toolUseId,
41+
contentItem.toolUse.input
42+
);
43+
toolResponses.push(response);
44+
}
45+
}
46+
return { role: 'user', content: toolResponses };
47+
} catch (e) {
48+
throw new Error(`Failed to execute tool: ${e}`);
49+
}
50+
} else {
51+
// Assume this catches stop reasons "end_turn", "stop_sequence", and "max_tokens"
52+
return null;
53+
}
54+
}
55+
56+
/**
57+
* Main chat session handler.
58+
*/
59+
async start(): Promise<void> {
60+
const serverManager = new Servers(this.servers);
61+
62+
try {
63+
await serverManager.initialize();
64+
65+
const allTools = await serverManager.listTools();
66+
const toolsDescription = allTools.map(tool => tool.formatForLLM());
67+
68+
const systemPrompt = 'You are a helpful assistant.';
69+
70+
const messages: Record<string, any>[] = [];
71+
72+
while (true) {
73+
try {
74+
const userInput = readline.question('\nYou: ').trim().toLowerCase();
75+
if (userInput === 'quit' || userInput === 'exit') {
76+
logger.info('\nExiting...');
77+
break;
78+
}
79+
80+
messages.push({ role: 'user', content: [{ text: userInput }] });
81+
82+
const llmResponse = await this.llmClient.getResponse(
83+
messages,
84+
systemPrompt,
85+
toolsDescription
86+
);
87+
88+
logger.debug('\nAssistant: ' + JSON.stringify(llmResponse, null, 2));
89+
console.log(`\nAssistant: ${llmResponse.output.message.content[0].text}`);
90+
messages.push(llmResponse.output.message);
91+
92+
const toolResults = await this.executeRequestedTools(serverManager, llmResponse);
93+
94+
if (toolResults) {
95+
logger.debug('\nTool Results: ' + JSON.stringify(toolResults, null, 2));
96+
messages.push(toolResults);
97+
const finalResponse = await this.llmClient.getResponse(
98+
messages,
99+
systemPrompt,
100+
toolsDescription
101+
);
102+
logger.debug('\nFinal response: ' + JSON.stringify(finalResponse, null, 2));
103+
console.log(`\nAssistant: ${finalResponse.output.message.content[0].text}`);
104+
messages.push(finalResponse.output.message);
105+
}
106+
} catch (e) {
107+
logger.error(`Error: ${e}`);
108+
}
109+
}
110+
} finally {
111+
await serverManager.close();
112+
}
113+
}
114+
}
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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';
2+
3+
/**
4+
* Manages communication with Bedrock.
5+
*/
6+
export class LLMClient {
7+
private bedrockClient: BedrockRuntimeClient;
8+
private modelId: string;
9+
10+
constructor(bedrockClient: BedrockRuntimeClient, modelId: string) {
11+
this.bedrockClient = bedrockClient;
12+
this.modelId = modelId;
13+
}
14+
15+
/**
16+
* Get a response from the LLM, using the Bedrock Converse API.
17+
* @param messages A list of message dictionaries.
18+
* @param systemPrompt The system prompt to use.
19+
* @param tools The list of tools to make available to the model.
20+
* @returns The LLM's full response.
21+
*/
22+
async getResponse(
23+
messages: Record<string, any>[],
24+
systemPrompt: string,
25+
tools: Record<string, any>[]
26+
): Promise<Record<string, any>> {
27+
const command = new ConverseCommand({
28+
modelId: this.modelId,
29+
messages,
30+
system: [{ text: systemPrompt }],
31+
inferenceConfig: {
32+
maxTokens: 4096,
33+
temperature: 0.7,
34+
topP: 1,
35+
},
36+
toolConfig: { tools },
37+
});
38+
39+
const response = await this.bedrockClient.send(command);
40+
return response as Record<string, any>;
41+
}
42+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
// Silence AWS SDK logs unless explicitly enabled
26+
if (process.env.LOG_LEVEL !== 'debug') {
27+
logger.silent = false;
28+
}
29+
30+
export default logger;

0 commit comments

Comments
 (0)