Node System Requirements Before starting, ensure your system meets these requirements:
Mac or Windows computer
Node.js version 16 or higher installed
npm (comes with Node.js)
Setting Up Your Environment First, create a new Node.js project:
# Create project directory
mkdir mcp-client
cd mcp-client
# Initialize npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk @anthropic-ai/sdk dotenv
npm install -D typescript @types/node
# Create TypeScript config
npx tsc --init
# Create necessary files
mkdir src
touch src/client.ts
touch .env
Update your package.json
to add necessary configuration:
{
"type" : "module" ,
"scripts" : {
"build" : "tsc" ,
"start" : "node build/client.js"
}
}
Update your tsconfig.json
with appropriate settings:
{
"compilerOptions" : {
"target" : "ES2022" ,
"module" : "Node16" ,
"moduleResolution" : "Node16" ,
"outDir" : "./build" ,
"rootDir" : "./src" ,
"strict" : true ,
"esModuleInterop" : true ,
"skipLibCheck" : true ,
"forceConsistentCasingInFileNames" : true
},
"include" : [ "src/**/*" ]
}
Setting Up Your API Key You’ll need an Anthropic API key from the Anthropic Console .
Create a .env
file:
ANTHROPIC_API_KEY = your_key_here
Add .env
to your .gitignore
:
echo ".env" >> .gitignore
Creating the Client First, let’s set up our imports and create the basic client class in src/client.ts
:
import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" ;
import Anthropic from "@anthropic-ai/sdk" ;
import dotenv from "dotenv" ;
import {
CallToolResultSchema ,
ListToolsResultSchema ,
} from "@modelcontextprotocol/sdk/types.js" ;
import * as readline from "node:readline" ;
dotenv . config ();
interface MCPClientConfig {
name ?: string ;
version ?: string ;
}
class MCPClient {
private client : Client | null = null ;
private anthropic : Anthropic ;
private transport : StdioClientTransport | null = null ;
constructor ( config : MCPClientConfig = {}) {
this . anthropic = new Anthropic ();
}
// Methods will go here
}
Server Connection Management Next, implement the method to connect to an MCP server:
async connectToServer ( serverScriptPath : string ): Promise < void > {
const isPython = serverScriptPath . endsWith ( ".py" );
const isJs = serverScriptPath . endsWith ( ".js" );
if (! isPython && ! isJs ) {
throw new Error ( "Server script must be a .py or .js file" );
}
const command = isPython ? "python" : "node" ;
this. transport = new StdioClientTransport ({
command ,
args: [ serverScriptPath ],
});
this. client = new Client (
{
name: "mcp-client" ,
version: "1.0.0" ,
},
{
capabilities: {},
}
);
await this.client.connect(this.transport);
// List available tools
const response = await this . client . request (
{ method: "tools/list" },
ListToolsResultSchema
);
console.log(
" \n Connected to server with tools:" ,
response.tools.map(( tool: any ) => tool . name )
);
}
Query Processing Logic Now add the core functionality for processing queries and handling tool calls:
async processQuery ( query : string ): Promise < string > {
if (!this.client) {
throw new Error ( "Client not connected" );
}
// Initialize messages array with user query
let messages: Anthropic . MessageParam [] = [
{
role: "user" ,
content: query ,
},
];
// Get available tools
const toolsResponse = await this . client . request (
{ method: "tools/list" },
ListToolsResultSchema
);
const availableTools = toolsResponse . tools . map (( tool : any ) => ({
name: tool . name ,
description: tool . description ,
input_schema: tool . inputSchema ,
}));
const finalText : string [] = [];
let currentResponse = await this . anthropic . messages . create ({
model: "claude-3-5-sonnet-20241022" ,
max_tokens: 1000 ,
messages ,
tools: availableTools ,
});
// Process the response and any tool calls
while ( true ) {
// Add Claude's response to final text and messages
for ( const content of currentResponse.content) {
if ( content . type === "text" ) {
finalText . push ( content . text );
} else if ( content . type === "tool_use" ) {
const toolName = content . name ;
const toolArgs = content . input ;
// Execute tool call
const result = await this . client . request (
{
method: "tools/call" ,
params: {
name: toolName ,
arguments: toolArgs ,
},
},
CallToolResultSchema
);
finalText . push (
`[Calling tool ${ toolName } with args ${ JSON . stringify ( toolArgs ) } ]`
);
// Add Claude's response (including tool use) to messages
messages . push ({
role: "assistant" ,
content: currentResponse . content ,
});
// Add tool result to messages
messages . push ({
role: "user" ,
content: [
{
type: "tool_result" ,
tool_use_id: content . id ,
content: [
{ type: "text" , text: JSON . stringify ( result . content ) },
],
},
],
});
// Get next response from Claude with tool results
currentResponse = await this . anthropic . messages . create ({
model: "claude-3-5-sonnet-20241022" ,
max_tokens: 1000 ,
messages ,
tools: availableTools ,
});
// Add Claude's interpretation of the tool results to final text
if ( currentResponse . content [ 0 ]?. type === "text" ) {
finalText . push ( currentResponse . content [ 0 ]. text );
}
// Continue the loop to process any additional tool calls
continue ;
}
}
// If we reach here, there were no tool calls in the response
break;
}
return finalText . join ( " \n " );
}
Interactive Chat Interface Add the chat loop and cleanup functionality:
async chatLoop (): Promise < void > {
console.log( " \n MCP Client Started!" );
console.log( "Type your queries or 'quit' to exit." );
// Using Node's readline for console input
const rl = readline.createInterface({
input: process . stdin ,
output: process . stdout ,
});
const askQuestion = () => {
rl . question ( " \n Query: " , async ( query : string ) => {
try {
if ( query . toLowerCase () === "quit" ) {
await this . cleanup ();
rl . close ();
return ;
}
const response = await this . processQuery ( query );
console . log ( " \n " + response );
askQuestion ();
} catch ( error ) {
console . error ( " \n Error:" , error );
askQuestion ();
}
});
};
askQuestion ();
}
async cleanup (): Promise < void > {
if (this.transport) {
await this . transport . close ();
}
}
Main Entry Point Finally, add the main execution logic outside the class:
// Main execution
async function main () {
if ( process . argv . length < 3 ) {
console . log ( "Usage: ts-node client.ts <path_to_server_script>" );
process . exit ( 1 );
}
const client = new MCPClient ();
try {
await client . connectToServer ( process . argv [ 2 ]);
await client . chatLoop ();
} catch ( error ) {
console . error ( "Error:" , error );
await client . cleanup ();
process . exit ( 1 );
}
}
// Run main if this is the main module
if ( import . meta . url === new URL ( process . argv [ 1 ], "file:" ). href ) {
main ();
}
export default MCPClient ;
Running the Client To run your client with any MCP server:
# Build the TypeScript code. Make sure to rerun this every time you update `client.ts`!
npm run build
# Run the client
node build/client.js path/to/server.py # for Python servers
node build/client.js path/to/server.js # for Node.js servers
The client will:
Connect to the specified server
List available tools
Start an interactive chat session where you can:
Enter queries
See tool executions
Get responses from Claude
Key Components Explained 1. Client Initialization
The MCPClient
class initializes with session management and API clients
Sets up the MCP client with basic capabilities
Configures the Anthropic client for Claude interactions
2. Server Connection
Supports both Python and Node.js servers
Validates server script type
Sets up proper communication channels
Lists available tools on connection
3. Query Processing
Maintains conversation context
Handles Claude’s responses and tool calls
Manages the message flow between Claude and tools
Combines results into a coherent response
4. Interactive Interface
Provides a simple command-line interface
Handles user input and displays responses
Includes basic error handling
Allows graceful exit
5. Resource Management
Proper cleanup of resources
Error handling for connection issues
Graceful shutdown procedures
Common Customization Points
Tool Handling
Modify processQuery()
to handle specific tool types
Add custom error handling for tool calls
Implement tool-specific response formatting
Response Processing
Customize how tool results are formatted
Add response filtering or transformation
Implement custom logging
User Interface
Add a GUI or web interface
Implement rich console output
Add command history or auto-completion
Best Practices
Error Handling
Always wrap tool calls in try-catch blocks
Provide meaningful error messages
Gracefully handle connection issues
Resource Management
Use proper cleanup methods
Close connections when done
Handle server disconnections
Security
Store API keys securely in .env
Validate server responses
Be cautious with tool permissions
Troubleshooting Server Path Issues
Double-check the path to your server script
Use absolute paths if relative paths aren’t working
For Windows users, use forward slashes (/) or escaped backslashes (\)
Verify the server file has the correct extension (.py or .js)
Example of correct path usage:
# Relative path
node build/client.js ./server/weather.js
# Absolute path
node build/client.js /Users/username/projects/mcp-server/weather.js
# Windows path (either format works)
node build/client.js C:/projects/mcp-server/weather.js
node build/client.js C: \\ projects \\ mcp-server \\ weather.js
Connection Issues
Verify the server script exists and has correct permissions
Check that the server script is executable
Ensure the server script’s dependencies are installed
Try running the server script directly to check for errors
Check server logs for error messages
Verify tool input arguments match the schema
Ensure tool dependencies are available
Add debug logging to track execution flow