## 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: ```bash # 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: ```json { "type": "module", "scripts": { "build": "tsc", "start": "node build/client.js" } } ``` Update your `tsconfig.json` with appropriate settings: ```json { "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](https://console.anthropic.com/settings/keys). Create a `.env` file: ```bash ANTHROPIC_API_KEY=your_key_here ``` Add `.env` to your `.gitignore`: ```bash echo ".env" >> .gitignore ``` ## Creating the Client First, let's set up our imports and create the basic client class in `src/client.ts`: ```typescript 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: ```typescript async connectToServer(serverScriptPath: string): Promise { 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( "\nConnected 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: ```typescript async processQuery(query: string): Promise { 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: ```typescript async chatLoop(): Promise { console.log("\nMCP 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("\nQuery: ", 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("\nError:", error); askQuestion(); } }); }; askQuestion(); } async cleanup(): Promise { if (this.transport) { await this.transport.close(); } } ``` ## Main Entry Point Finally, add the main execution logic outside the class: ```typescript // Main execution async function main() { if (process.argv.length < 3) { console.log("Usage: ts-node client.ts "); 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: ```bash # 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: 1. Connect to the specified server 2. List available tools 3. 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 1. **Tool Handling** - Modify `processQuery()` to handle specific tool types - Add custom error handling for tool calls - Implement tool-specific response formatting 2. **Response Processing** - Customize how tool results are formatted - Add response filtering or transformation - Implement custom logging 3. **User Interface** - Add a GUI or web interface - Implement rich console output - Add command history or auto-completion ### Best Practices 1. **Error Handling** - Always wrap tool calls in try-catch blocks - Provide meaningful error messages - Gracefully handle connection issues 2. **Resource Management** - Use proper cleanup methods - Close connections when done - Handle server disconnections 3. **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: ```bash # 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 #### Tool Execution Issues - 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