GitHub Stars
2
User Rating
Not Rated
Forks
2
Issues
0
Views
1
Favorites
0
Bedrock MCP Connector
A TypeScript client for interacting with AWS Bedrock and MCP (Model Context Protocol) servers.
Features
- Seamless integration with AWS Bedrock's Converse API
- Support for Claude and other Bedrock models
- Connect to MCP servers to discover and use available tools
- Register custom tool handlers
- Event-based architecture for streaming responses
- TypeScript support with full type definitions
- Command-line interface for interactive usage
- Modular design for easy integration into other projects
Installation
npm install @juspay/bedrock-mcp-connector
Usage
Basic Usage
import { BedrockMCPClient, LogLevel } from "@juspay/bedrock-mcp-connector";
// Create a client
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
systemPrompt: "You are a helpful assistant.",
mcpServerUrl: "http://localhost:5713/sse", // Optional
});
// Set the log level (optional)
// LogLevel.INFO - Default, shows important information
// LogLevel.DEBUG - Shows detailed debugging information
// LogLevel.WARN - Shows only warnings and errors
// LogLevel.ERROR - Shows only errors
// LogLevel.NONE - Suppresses all logs
client.setLogLevel(LogLevel.INFO);
// Connect to MCP server (if URL is provided)
if (client.mcpServerUrl) {
await client.connect();
}
// Send a prompt
const response = await client.sendPrompt("What is the capital of France?");
console.log("Response:", response);
// Disconnect when done
if (client.isConnectedToMCP()) {
await client.disconnect();
}
With Event Listeners
import { BedrockMCPClient, LogLevel } from "@juspay/bedrock-mcp-connector";
// Create a client
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
});
// Set the log level (optional)
client.setLogLevel(LogLevel.INFO);
// Set up event listeners
const emitter = client.getEmitter();
emitter.on("message", (message) => {
console.log(`Message: ${message}`);
});
emitter.on("error", (error) => {
console.error(`Error: ${error.message}`);
});
emitter.on("tool:start", (toolName, input) => {
console.log(`Tool started: ${toolName}`);
});
emitter.on("tool:end", (toolName, result) => {
console.log(`Tool completed: ${toolName}`);
});
emitter.on("response:start", () => {
console.log("Response started");
});
emitter.on("response:chunk", (chunk) => {
console.log(`Response chunk: ${chunk.substring(0, 50)}...`);
});
emitter.on("response:end", (fullResponse) => {
console.log("Response completed");
});
// Send a prompt
const response = await client.sendPrompt("What is the capital of France?");
Creating and Registering Tools
Tools are a powerful feature that allow the LLM to perform actions and access external data. This section provides detailed guidance on how to create and register tools effectively.
Tool Registration Basics
The registerTool
method takes four parameters:
client.registerTool(
name, // String: Unique identifier for the tool
handler, // Function: Async function that implements the tool
description, // String: Human-readable description of what the tool does
inputSchema // Object: JSON Schema defining the tool's parameters
);
Best Practices for Tool Design
- Single Responsibility: Each tool should do one thing well
- Clear Naming: Use descriptive, action-oriented names (e.g.,
getCurrentTime
,searchDatabase
) - Comprehensive Descriptions: Provide detailed descriptions that help the LLM understand when to use the tool
- Thorough Input Validation: Always validate inputs to prevent errors
- Informative Error Messages: Return clear error messages when something goes wrong
- Consistent Return Format: Always return results in the same format
- Appropriate Logging: Include logging to help with debugging
Detailed Tool Registration Example
import { BedrockMCPClient, LogLevel } from "@juspay/bedrock-mcp-connector";
// Create a client
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
});
// Set the log level (optional)
client.setLogLevel(LogLevel.INFO);
// Register a custom tool
client.registerTool(
// Name: Use a clear, descriptive name
"getCurrentTime",
// Handler: Implement the tool's functionality
async (name, input) => {
// Input validation with default values
const timezone = input.timezone || "UTC";
try {
// Core functionality
const date = new Date().toLocaleString("en-US", { timeZone: timezone });
// Return successful result
return {
content: [{ text: `The current time is ${date} in ${timezone}` }],
};
} catch (error) {
// Error handling
return {
content: [{ text: `Error getting time: ${error.message}` }],
isError: true,
};
}
},
// Description: Clearly explain what the tool does
"Get the current time in the specified timezone. This tool returns the current date and time formatted according to US locale conventions.",
// Input Schema: Define the parameters using JSON Schema
{
type: "object",
properties: {
timezone: {
type: "string",
description:
"The timezone to get the time for (e.g., UTC, America/New_York, Europe/London)",
examples: ["UTC", "America/New_York", "Europe/Paris", "Asia/Tokyo"],
},
},
required: [], // Empty array means no parameters are required
}
);
// Send a prompt that might use the tool
const response = await client.sendPrompt("What time is it now in Tokyo?");
Input Schema Design
The input schema uses JSON Schema format to define the parameters your tool accepts:
{
type: "object",
properties: {
// Define each parameter
paramName: {
type: "string" | "number" | "boolean" | "array" | "object",
description: "Clear description of the parameter",
examples: ["example1", "example2"], // Optional but helpful
enum: ["option1", "option2"], // For parameters with fixed options
minimum: 1, // For number validation
maximum: 100, // For number validation
pattern: "^[a-z]+$", // For string validation with regex
// Additional JSON Schema properties as needed
},
// More parameters...
},
required: ["paramName1", "paramName2"], // List required parameters
additionalProperties: false // Prevent extra parameters (optional)
}
Error Handling in Tools
Proper error handling is crucial for tools:
client.registerTool(
"divideNumbers",
async (name, input) => {
// Parameter validation
if (typeof input.dividend !== "number") {
return {
content: [{ text: "Error: dividend must be a number" }],
isError: true,
};
}
if (typeof input.divisor !== "number") {
return {
content: [{ text: "Error: divisor must be a number" }],
isError: true,
};
}
// Business logic validation
if (input.divisor === 0) {
return {
content: [{ text: "Error: Cannot divide by zero" }],
isError: true,
};
}
try {
// Perform the operation
const result = input.dividend / input.divisor;
return {
content: [
{
text: `${input.dividend} divided by ${input.divisor} equals ${result}`,
},
],
};
} catch (error) {
return {
content: [{ text: `Calculation error: ${error.message}` }],
isError: true,
};
}
},
"Divide two numbers",
{
type: "object",
properties: {
dividend: {
type: "number",
description: "The number to be divided",
},
divisor: {
type: "number",
description: "The number to divide by (cannot be zero)",
},
},
required: ["dividend", "divisor"],
}
);
Examples of Different Tool Types
1. Data Retrieval Tool
client.registerTool(
"getWeatherForecast",
async (name, input) => {
const { city, days = 3 } = input;
if (!city) {
return {
content: [{ text: "Error: city parameter is required" }],
isError: true,
};
}
try {
// In a real implementation, this would call a weather API
const forecast = await weatherService.getForecast(city, days);
return {
content: [
{
text: `Weather forecast for ${city} for the next ${days} days:\n\n${forecast}`,
},
],
};
} catch (error) {
return {
content: [{ text: `Error getting weather forecast: ${error.message}` }],
isError: true,
};
}
},
"Get weather forecast for a city",
{
type: "object",
properties: {
city: {
type: "string",
description: "The city to get the weather forecast for",
},
days: {
type: "number",
description: "Number of days to forecast (default: 3)",
minimum: 1,
maximum: 10,
},
},
required: ["city"],
}
);
2. Computational Tool
client.registerTool(
"calculateStatistics",
async (name, input) => {
const { numbers } = input;
if (!Array.isArray(numbers) || numbers.length === 0) {
return {
content: [
{ text: "Error: numbers must be a non-empty array of numbers" },
],
isError: true,
};
}
if (!numbers.every((n) => typeof n === "number")) {
return {
content: [{ text: "Error: all elements in numbers must be numbers" }],
isError: true,
};
}
try {
const sum = numbers.reduce((a, b) => a + b, 0);
const mean = sum / numbers.length;
const sortedNumbers = [...numbers].sort((a, b) => a - b);
const median =
sortedNumbers.length % 2 === 0
? (sortedNumbers[sortedNumbers.length / 2 - 1] +
sortedNumbers[sortedNumbers.length / 2]) /
2
: sortedNumbers[Math.floor(sortedNumbers.length / 2)];
return {
content: [
{
text: `Statistics for [${numbers.join(
", "
)}]:\n- Sum: ${sum}\n- Mean: ${mean}\n- Median: ${median}\n- Min: ${Math.min(
...numbers
)}\n- Max: ${Math.max(...numbers)}`,
},
],
};
} catch (error) {
return {
content: [{ text: `Error calculating statistics: ${error.message}` }],
isError: true,
};
}
},
"Calculate basic statistics for an array of numbers",
{
type: "object",
properties: {
numbers: {
type: "array",
items: {
type: "number",
},
description: "Array of numbers to calculate statistics for",
},
},
required: ["numbers"],
}
);
3. External API Tool
client.registerTool(
"searchWikipedia",
async (name, input) => {
const { query, limit = 3 } = input;
if (!query || typeof query !== "string") {
return {
content: [
{ text: "Error: query parameter is required and must be a string" },
],
isError: true,
};
}
try {
// In a real implementation, this would call the Wikipedia API
const searchUrl = `https://en.wikipedia.org/w/api.php?action=opensearch&search=${encodeURIComponent(
query
)}&limit=${limit}&namespace=0&format=json`;
const response = await fetch(searchUrl);
const [searchTerm, titles, descriptions, urls] = await response.json();
let resultText = `Wikipedia search results for "${query}":\n\n`;
for (let i = 0; i < titles.length; i++) {
resultText += `${i + 1}. ${titles[i]}\n`;
resultText += ` ${descriptions[i]}\n`;
resultText += ` ${urls[i]}\n\n`;
}
return {
content: [{ text: resultText }],
};
} catch (error) {
return {
content: [{ text: `Error searching Wikipedia: ${error.message}` }],
isError: true,
};
}
},
"Search Wikipedia for information on a topic",
{
type: "object",
properties: {
query: {
type: "string",
description: "The search query",
},
limit: {
type: "number",
description: "Maximum number of results to return (default: 3)",
minimum: 1,
maximum: 10,
},
},
required: ["query"],
}
);
Tips for Effective Tool Usage
- Provide Clear Instructions in System Prompt: Guide the LLM on when and how to use tools
- Use Descriptive Tool Names: Names should clearly indicate the tool's purpose
- Include Detailed Parameter Descriptions: Help the LLM understand what each parameter does
- Add Examples in Parameter Descriptions: Show valid values for parameters
- Implement Thorough Validation: Check all inputs before processing
- Return Structured Data: Format tool results consistently
- Handle Errors Gracefully: Provide clear error messages
- Test Tools Thoroughly: Ensure they work with various inputs
Advanced Tool Registration
For more complex tools, you can structure your code to make it more maintainable:
// Define tool handlers in separate files
import { searchDatabase } from "./tools/database.js";
import { processImage } from "./tools/image.js";
import { translateText } from "./tools/translation.js";
// Import tool schemas
import {
searchDatabaseSchema,
processImageSchema,
translateTextSchema,
} from "./schemas/toolSchemas.js";
// Register tools
client.registerTool(
"searchDatabase",
searchDatabase,
"Search the database for records matching the query",
searchDatabaseSchema
);
client.registerTool(
"processImage",
processImage,
"Process and analyze an image",
processImageSchema
);
client.registerTool(
"translateText",
translateText,
"Translate text between languages",
translateTextSchema
);
By following these best practices, you can create tools that are easy for the LLM to understand and use effectively, resulting in better responses for your users.
Command Line Interface
The package includes a command-line interface for interactive usage:
npx @juspay/bedrock-mcp-connector
Or install globally:
npm install -g @juspay/bedrock-mcp-connector
@juspay/bedrock-mcp-connector
CLI Options
Options:
-m, --model <id> AWS Bedrock model ID (default: anthropic.claude-3-sonnet-20240229-v1:0)
-r, --region <region> AWS region (default: us-east-1)
-s, --system-prompt <text> System prompt for the model
-u, --mcp-url <url> MCP server URL
-n, --name <name> Client name
-v, --version <version> Client version
-h, --help Show this help message
API Reference
BedrockMCPClient
Constructor
new BedrockMCPClient(config: {
modelId: string;
region?: string;
systemPrompt?: string;
mcpServerUrl?: string;
clientName?: string;
clientVersion?: string;
maxTokens?: number;
temperature?: number;
responseOutputTags?: [string, string];
})
Methods
connect(): Promise<void>
- Connect to the MCP serverdisconnect(): Promise<void>
- Disconnect from the MCP serverisConnectedToMCP(): boolean
- Check if the client is connected to the MCP serversendPrompt(prompt: string): Promise<string>
- Send a prompt to the agentregisterTool(name: string, handler: ToolHandler, description?: string, inputSchema?: Record<string, any>): void
- Register a custom toolgetTools(): Array<{ name: string; description?: string }>
- Get all registered toolsgetEmitter(): BedrockMCPClientEmitter
- Get the event emittergetAgent(): ConverseAgent
- Get the agentgetConversationHistory()
- Get the conversation historyclearConversationHistory(): void
- Clear the conversation historysetLogLevel(level: LogLevel): void
- Set the log level for the client and its components
Events
message
- Emitted when a message is loggederror
- Emitted when an error occurstool:start
- Emitted when a tool starts executingtool:end
- Emitted when a tool finishes executingresponse:start
- Emitted when a response startsresponse:chunk
- Emitted when a response chunk is receivedresponse:end
- Emitted when a response endsconnected
- Emitted when connected to the MCP serverdisconnected
- Emitted when disconnected from the MCP server
Logging System
The package includes a comprehensive logging system that allows you to control the verbosity of logs and debug information.
Log Levels
The following log levels are available (from most to least verbose):
LogLevel.DEBUG
(0) - Detailed debugging informationLogLevel.INFO
(1) - General information messages (default)LogLevel.WARN
(2) - Warning messagesLogLevel.ERROR
(3) - Error messagesLogLevel.NONE
(4) - No logging
Setting the Log Level
You can set the log level for the client and all its components:
import { BedrockMCPClient, LogLevel } from "@juspay/bedrock-mcp-connector";
// Create a client
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
});
// Set the log level
client.setLogLevel(LogLevel.INFO); // Default level
// or
client.setLogLevel(LogLevel.DEBUG); // For detailed debugging
What Gets Logged at Each Level
- DEBUG: All messages, including detailed API responses, tool execution details, and internal state changes
- INFO: General operational information, tool requests, connection status, and important events
- WARN: Potential issues that don't prevent operation but might need attention
- ERROR: Errors that affect operation but don't necessarily crash the application
- NONE: No logs will be output (except for unhandled exceptions)
Use Cases
Development and Debugging (LogLevel.DEBUG)
Use the DEBUG level when:
- Developing new features or tools
- Troubleshooting issues with API responses
- Debugging tool execution problems
- Understanding the flow of data through the system
// Enable detailed debugging
client.setLogLevel(LogLevel.DEBUG);
// Now you'll see detailed information about:
// - Full API responses from Bedrock
// - Tool input and output details
// - Message content and processing
Production Usage (LogLevel.INFO or LogLevel.WARN)
For production environments:
- Use INFO to keep track of normal operations
- Use WARN to only see potential issues
// For normal operation with important information
client.setLogLevel(LogLevel.INFO);
// Or for minimal logging (only warnings and errors)
client.setLogLevel(LogLevel.WARN);
Audit and Monitoring (LogLevel.INFO)
When you need to track tool usage and interactions:
client.setLogLevel(LogLevel.INFO);
// This will log:
// - When tools are requested and executed
// - Connection events
// - Response start/end events
Silent Operation (LogLevel.NONE)
When you want to suppress all logs:
client.setLogLevel(LogLevel.NONE);
Conversation Management
The package maintains conversation history automatically, allowing for natural back-and-forth interactions with the model. This enables the model to remember context from previous messages and provide coherent responses throughout a conversation.
How Conversation History Works
- Each message sent to and received from the model is stored in memory
- The full conversation history is included with each new request to the model
- This allows the model to reference previous messages and maintain context
Managing Conversation History
You can access and manage the conversation history using these methods:
// Get the current conversation history
const history = client.getConversationHistory();
console.log("Current conversation:", history);
// Clear the conversation history to start a new conversation
client.clearConversationHistory();
console.log("Started a new conversation");
Example: Multi-turn Conversation
import { BedrockMCPClient } from "@juspay/bedrock-mcp-connector";
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
});
// First message
let response = await client.sendPrompt("What are the three primary colors?");
console.log("Response 1:", response);
// Follow-up question (model remembers the context)
response = await client.sendPrompt(
"And what colors do you get when you mix them?"
);
console.log("Response 2:", response);
// Start a new conversation
client.clearConversationHistory();
// This is now a completely new conversation with no memory of the previous exchange
response = await client.sendPrompt("What's the tallest mountain in the world?");
console.log("New conversation response:", response);
Tool Results Across Multiple API Calls
The package maintains tool results across multiple API calls, ensuring that all tool results are included in the final response. This is particularly useful when the model needs to use multiple tools to answer a complex question.
Handling Missing Required Parameters
When using tools that require specific parameters, you can implement an interactive flow where the model asks for missing information. There are two key approaches to handling missing parameters:
1. Using a Specialized System Prompt
You can guide the model's behavior with a specialized system prompt that instructs it on how to handle missing parameters:
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
systemPrompt: `You are a helpful assistant with access to external tools.
- Use available tools **only when necessary** to provide accurate or up-to-date information.
- If a question can be answered based on your knowledge, respond directly **without using tools**.
- If a tool is required:
1. **Check if all necessary parameters are available.** If they are, use the tool directly.
2. **If any parameters are missing, do not proceed.** Instead, ask the user for the required information, explaining why it is needed.
3. **Wait for the user's response before using the tool.**
- If the user asks multiple questions, **handle them one by one**.
- If some questions require tools and others don't, **answer what you can immediately**, then use tools as needed.
- After using a tool, continue answering any remaining questions.
`,
});
This system prompt instructs the model to:
- Not attempt to use tools with incomplete parameters
- Clearly explain which parameters are missing and why they're needed
- Ask the user for the missing information
- Only use the tool once all required parameters are available
2. Implementing Parameter Validation in Tools
Implement thorough parameter validation in your tool handlers to ensure they handle missing parameters gracefully:
client.registerTool(
"calculator",
async (name, input) => {
const { operation, a, b } = input;
// Validate required parameters
if (!operation) {
return {
content: [
{ text: `Error: Operation parameter is required for calculator` },
],
isError: true,
};
}
if (a === undefined || a === null) {
return {
content: [
{ text: `Error: First operand (a) is required for calculator` },
],
isError: true,
};
}
if (b === undefined || b === null) {
return {
content: [
{ text: `Error: Second operand (b) is required for calculator` },
],
isError: true,
};
}
// Tool implementation...
let result;
switch (operation) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
if (b === 0) throw new Error("Division by zero");
result = a / b;
break;
default:
throw new Error(`Unknown operation: ${operation}`);
}
return {
content: [{ text: `The result of ${a} ${operation} ${b} is ${result}` }],
};
},
"Perform basic arithmetic operations",
{
type: "object",
properties: {
operation: {
type: "string",
description:
"The operation to perform (add, subtract, multiply, divide)",
enum: ["add", "subtract", "multiply", "divide"],
},
a: { type: "number", description: "The first operand" },
b: { type: "number", description: "The second operand" },
},
required: ["operation", "a", "b"],
}
);
3. Interactive Conversation Flow
Implement an interactive conversation flow that handles the back-and-forth between the user and the model:
import { BedrockMCPClient } from "@juspay/bedrock-mcp-connector";
import * as readline from "readline";
// Create a readline interface for user input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Function to get user input
const getUserInput = (question) =>
new Promise((resolve) => {
rl.question(question, (answer) => resolve(answer));
});
async function runInteractiveSession() {
const client = new BedrockMCPClient({
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
region: "us-east-1",
systemPrompt: `You are a helpful assistant with access to external tools.
- Use available tools **only when necessary** to provide accurate or up-to-date information.
- If a question can be answered based on your knowledge, respond directly **without using tools**.
- If a tool is required:
1. **Check if all necessary parameters are available.** If they are, use the tool directly.
2. **If any parameters are missing, do not proceed.** Instead, ask the user for the required information, explaining why it is needed.
3. **Wait for the user's response before using the tool.**
- If the user asks multiple questions, **handle them one by one**.
- If some questions require tools and others don't, **answer what you can immediately**, then use tools as needed.
- After using a tool, continue answering any remaining questions.
`,
});
// Register tools (calculator, weather forecast, etc.)
// ...
// Start the conversation
console.log("Ask me anything! (Type 'exit' to quit)");
while (true) {
// Get user input
const userInput = await getUserInput("> ");
if (userInput.toLowerCase() === "exit") break;
// Send to the model
const response = await client.sendPrompt(userInput);
console.log("\nAssistant:", response);
}
rl.close();
}
runInteractiveSession().catch(console.error);
4. Managing Tool Results Between Conversations
When implementing multi-turn conversations, it's important to clear accumulated tool results between separate conversations to prevent results from previous conversations affecting new ones:
// After completing a conversation or when starting a new one
client.clearConversationHistory();
client.getAgent().clearAccumulatedToolResults();
This ensures that tool results from previous conversations don't appear in new conversations.
Example: Complete Conversation Flow
Here's an example of a complete conversation flow where the model asks for missing parameters:
User asks a question with incomplete parameters:
User: "Can you calculate something for me? I want to multiply 42 by something."
Model responds, asking for the missing parameter:
Assistant: "I'd be happy to help you with that calculation. To use the calculator tool, I need two operands (numbers) and the operation. You've provided one number (42) and mentioned you want to multiply, but I'm missing the second operand. Could you please tell me what number you want to multiply 42 by?"
User provides the missing parameter:
User: "The second number is 7."
Model uses the tool with complete parameters:
Assistant: "I've calculated that 42 multiplied by 7 equals 294."
This pattern allows for natural conversations where the model can request missing information and then use that information to complete the tool execution.
Creating a Custom Logger
You can create your own logger for other parts of your application:
import { createDefaultLogger, LogLevel } from "@juspay/bedrock-mcp-connector";
// Create a logger with a custom prefix
const logger = createDefaultLogger("MyComponent");
// Set the log level
logger.setLevel(LogLevel.INFO);
// Use the logger
logger.info("Application started");
logger.debug("This won't be shown unless log level is DEBUG");
logger.warn("Something might be wrong");
logger.error("An error occurred", errorObject);
License
MIT
840
Followers
265
Repositories
0
Gists
1
Total Contributions