The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 6
66
77Before you begin, it helps to have gone through our [Build an MCP Server](/docs/2024-11-05/develop/build-server) tutorial so you can understand how clients and servers communicate.
88
9<Tabs>
10 <Tab title="Python">
11 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)
12
13 ## System Requirements
14
15 Before starting, ensure your system meets these requirements:
16
17 * Mac or Windows computer
18 * Latest Python version installed
19 * Latest version of `uv` installed
20
21 ## Setting Up Your Environment
22
23 First, create a new Python project with `uv`:
24
25 <CodeGroup>
26 ```bash macOS/Linux theme={null}
27 # Create project directory
28 uv init mcp-client
29 cd mcp-client
30
31 # Create virtual environment
32 uv venv
33
34 # Activate virtual environment
35 source .venv/bin/activate
36
37 # Install required packages
38 uv add mcp anthropic python-dotenv
39
40 # Remove boilerplate files
41 rm main.py
42
43 # Create our main file
44 touch client.py
45 ```
46
47 ```powershell Windows theme={null}
48 # Create project directory
49 uv init mcp-client
50 cd mcp-client
51
52 # Create virtual environment
53 uv venv
54
55 # Activate virtual environment
56 .venv\Scripts\activate
57
58 # Install required packages
59 uv add mcp anthropic python-dotenv
60
61 # Remove boilerplate files
62 del main.py
63
64 # Create our main file
65 new-item client.py
66 ```
67 </CodeGroup>
68
69 ## Setting Up Your API Key
70
71 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
72
73 Create a `.env` file to store it:
74
75 ```bash theme={null}
76 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
77 ```
78
79 Add `.env` to your `.gitignore`:
80
81 ```bash theme={null}
82 echo ".env" >> .gitignore
83 ```
84
85 <Warning>
86 Make sure you keep your `ANTHROPIC_API_KEY` secure!
87 </Warning>
88
89 ## Creating the Client
90
91 ### Basic Client Structure
92
93 First, let's set up our imports and create the basic client class:
94
95 ```python theme={null}
96 import asyncio
97 from typing import Optional
98 from contextlib import AsyncExitStack
99
100 from mcp import ClientSession, StdioServerParameters
101 from mcp.client.stdio import stdio_client
102
103 from anthropic import Anthropic
104 from dotenv import load_dotenv
105
106 load_dotenv() # load environment variables from .env
107
108 class MCPClient:
109 def __init__(self):
110 # Initialize session and client objects
111 self.session: Optional[ClientSession] = None
112 self.exit_stack = AsyncExitStack()
113 self.anthropic = Anthropic()
114 # methods will go here
115 ```
116
117 ### Server Connection Management
118
119 Next, we'll implement the method to connect to an MCP server:
120
121 ```python theme={null}
122 async def connect_to_server(self, server_script_path: str):
123 """Connect to an MCP server
124
125 Args:
126 server_script_path: Path to the server script (.py or .js)
127 """
128 is_python = server_script_path.endswith('.py')
129 is_js = server_script_path.endswith('.js')
130 if not (is_python or is_js):
131 raise ValueError("Server script must be a .py or .js file")
132
133 command = "python" if is_python else "node"
134 server_params = StdioServerParameters(
135 command=command,
136 args=[server_script_path],
137 env=None
138 )
139
140 stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
141 self.stdio, self.write = stdio_transport
142 self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
143
144 await self.session.initialize()
145
146 # List available tools
147 response = await self.session.list_tools()
148 tools = response.tools
149 print("\nConnected to server with tools:", [tool.name for tool in tools])
150 ```
151
152 ### Query Processing Logic
153
154 Now let's add the core functionality for processing queries and handling tool calls:
155
156 ```python theme={null}
157 async def process_query(self, query: str) -> str:
158 """Process a query using Claude and available tools"""
159 messages = [
160 {
9**Python**
10
11[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)
12
13## System Requirements
14
15Before starting, ensure your system meets these requirements:
16
17* Mac or Windows computer
18* Latest Python version installed
19* Latest version of `uv` installed
20
21## Setting Up Your Environment
22
23First, create a new Python project with `uv`:
24
25```bash macOS/Linux theme={null}
26# Create project directory
27uv init mcp-client
28cd mcp-client
29
30# Create virtual environment
31uv venv
32
33# Activate virtual environment
34source .venv/bin/activate
35
36# Install required packages
37uv add mcp anthropic python-dotenv
38
39# Remove boilerplate files
40rm main.py
41
42# Create our main file
43touch client.py
44```
45
46```powershell Windows theme={null}
47# Create project directory
48uv init mcp-client
49cd mcp-client
50
51# Create virtual environment
52uv venv
53
54# Activate virtual environment
55.venv\Scripts\activate
56
57# Install required packages
58uv add mcp anthropic python-dotenv
59
60# Remove boilerplate files
61del main.py
62
63# Create our main file
64new-item client.py
65```
66
67## Setting Up Your API Key
68
69You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
70
71Create a `.env` file to store it:
72
73```bash theme={null}
74echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
75```
76
77Add `.env` to your `.gitignore`:
78
79```bash theme={null}
80echo ".env" >> .gitignore
81```
82
83> [!WARNING]
84>
85> Make sure you keep your `ANTHROPIC_API_KEY` secure!
86
87## Creating the Client
88
89### Basic Client Structure
90
91First, let's set up our imports and create the basic client class:
92
93```python theme={null}
94import asyncio
95from typing import Optional
96from contextlib import AsyncExitStack
97
98from mcp import ClientSession, StdioServerParameters
99from mcp.client.stdio import stdio_client
100
101from anthropic import Anthropic
102from dotenv import load_dotenv
103
104load_dotenv() # load environment variables from .env
105
106class MCPClient:
107 def __init__(self):
108 # Initialize session and client objects
109 self.session: Optional[ClientSession] = None
110 self.exit_stack = AsyncExitStack()
111 self.anthropic = Anthropic()
112 # methods will go here
113```
114
115### Server Connection Management
116
117Next, we'll implement the method to connect to an MCP server:
118
119```python theme={null}
120async def connect_to_server(self, server_script_path: str):
121 """Connect to an MCP server
122
123 Args:
124 server_script_path: Path to the server script (.py or .js)
125 """
126 is_python = server_script_path.endswith('.py')
127 is_js = server_script_path.endswith('.js')
128 if not (is_python or is_js):
129 raise ValueError("Server script must be a .py or .js file")
130
131 command = "python" if is_python else "node"
132 server_params = StdioServerParameters(
133 command=command,
134 args=[server_script_path],
135 env=None
136 )
137
138 stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
139 self.stdio, self.write = stdio_transport
140 self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
141
142 await self.session.initialize()
143
144 # List available tools
145 response = await self.session.list_tools()
146 tools = response.tools
147 print("\nConnected to server with tools:", [tool.name for tool in tools])
148```
149
150### Query Processing Logic
151
152Now let's add the core functionality for processing queries and handling tool calls:
153
154```python theme={null}
155async def process_query(self, query: str) -> str:
156 """Process a query using Claude and available tools"""
157 messages = [
158 {
159 "role": "user",
160 "content": query
161 }
162 ]
163
164 response = await self.session.list_tools()
165 available_tools = [{
166 "name": tool.name,
167 "description": tool.description,
168 "input_schema": tool.inputSchema
169 } for tool in response.tools]
170
171 # Initial Claude API call
172 response = self.anthropic.messages.create(
173 model="claude-sonnet-4-20250514",
174 max_tokens=1000,
175 messages=messages,
176 tools=available_tools
177 )
178
179 # Process response and handle tool calls
180 final_text = []
181
182 assistant_message_content = []
183 for content in response.content:
184 if content.type == 'text':
185 final_text.append(content.text)
186 assistant_message_content.append(content)
187 elif content.type == 'tool_use':
188 tool_name = content.name
189 tool_args = content.input
190
191 # Execute tool call
192 result = await self.session.call_tool(tool_name, tool_args)
193 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
194
195 assistant_message_content.append(content)
196 messages.append({
197 "role": "assistant",
198 "content": assistant_message_content
199 })
200 messages.append({
161201 "role": "user",
162 "content": query
163 }
164 ]
165
166 response = await self.session.list_tools()
167 available_tools = [{
168 "name": tool.name,
169 "description": tool.description,
170 "input_schema": tool.inputSchema
171 } for tool in response.tools]
172
173 # Initial Claude API call
174 response = self.anthropic.messages.create(
175 model="claude-sonnet-4-20250514",
176 max_tokens=1000,
177 messages=messages,
178 tools=available_tools
179 )
180
181 # Process response and handle tool calls
182 final_text = []
183
184 assistant_message_content = []
185 for content in response.content:
186 if content.type == 'text':
187 final_text.append(content.text)
188 assistant_message_content.append(content)
189 elif content.type == 'tool_use':
190 tool_name = content.name
191 tool_args = content.input
192
193 # Execute tool call
194 result = await self.session.call_tool(tool_name, tool_args)
195 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
196
197 assistant_message_content.append(content)
198 messages.append({
199 "role": "assistant",
200 "content": assistant_message_content
201 })
202 messages.append({
203 "role": "user",
204 "content": [
205 {
206 "type": "tool_result",
207 "tool_use_id": content.id,
208 "content": result.content
209 }
210 ]
211 })
212
213 # Get next response from Claude
214 response = self.anthropic.messages.create(
215 model="claude-sonnet-4-20250514",
216 max_tokens=1000,
217 messages=messages,
218 tools=available_tools
219 )
220
221 final_text.append(response.content[0].text)
222
223 return "\n".join(final_text)
224 ```
225
226 ### Interactive Chat Interface
227
228 Now we'll add the chat loop and cleanup functionality:
229
230 ```python theme={null}
231 async def chat_loop(self):
232 """Run an interactive chat loop"""
233 print("\nMCP Client Started!")
234 print("Type your queries or 'quit' to exit.")
235
236 while True:
237 try:
238 query = input("\nQuery: ").strip()
239
240 if query.lower() == 'quit':
241 break
242
243 response = await self.process_query(query)
244 print("\n" + response)
245
246 except Exception as e:
247 print(f"\nError: {str(e)}")
248
249 async def cleanup(self):
250 """Clean up resources"""
251 await self.exit_stack.aclose()
252 ```
253
254 ### Main Entry Point
255
256 Finally, we'll add the main execution logic:
257
258 ```python theme={null}
259 async def main():
260 if len(sys.argv) < 2:
261 print("Usage: python client.py <path_to_server_script>")
262 sys.exit(1)
263
264 client = MCPClient()
202 "content": [
203 {
204 "type": "tool_result",
205 "tool_use_id": content.id,
206 "content": result.content
207 }
208 ]
209 })
210
211 # Get next response from Claude
212 response = self.anthropic.messages.create(
213 model="claude-sonnet-4-20250514",
214 max_tokens=1000,
215 messages=messages,
216 tools=available_tools
217 )
218
219 final_text.append(response.content[0].text)
220
221 return "\n".join(final_text)
222```
223
224### Interactive Chat Interface
225
226Now we'll add the chat loop and cleanup functionality:
227
228```python theme={null}
229async def chat_loop(self):
230 """Run an interactive chat loop"""
231 print("\nMCP Client Started!")
232 print("Type your queries or 'quit' to exit.")
233
234 while True:
265235 try:
266 await client.connect_to_server(sys.argv[1])
267 await client.chat_loop()
268 finally:
269 await client.cleanup()
270
271 if __name__ == "__main__":
272 import sys
273 asyncio.run(main())
274 ```
275
276 You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).
277
278 ## Key Components Explained
279
280 ### 1. Client Initialization
281
282 * The `MCPClient` class initializes with session management and API clients
283 * Uses `AsyncExitStack` for proper resource management
284 * Configures the Anthropic client for Claude interactions
285
286 ### 2. Server Connection
287
288 * Supports both Python and Node.js servers
289 * Validates server script type
290 * Sets up proper communication channels
291 * Initializes the session and lists available tools
292
293 ### 3. Query Processing
294
295 * Maintains conversation context
296 * Handles Claude's responses and tool calls
297 * Manages the message flow between Claude and tools
298 * Combines results into a coherent response
299
300 ### 4. Interactive Interface
301
302 * Provides a simple command-line interface
303 * Handles user input and displays responses
304 * Includes basic error handling
305 * Allows graceful exit
306
307 ### 5. Resource Management
308
309 * Proper cleanup of resources
310 * Error handling for connection issues
311 * Graceful shutdown procedures
312
313 ## Common Customization Points
314
315 1. **Tool Handling**
316 * Modify `process_query()` to handle specific tool types
317 * Add custom error handling for tool calls
318 * Implement tool-specific response formatting
319
320 2. **Response Processing**
321 * Customize how tool results are formatted
322 * Add response filtering or transformation
323 * Implement custom logging
324
325 3. **User Interface**
326 * Add a GUI or web interface
327 * Implement rich console output
328 * Add command history or auto-completion
329
330 ## Running the Client
331
332 To run your client with any MCP server:
333
334 ```bash theme={null}
335 uv run client.py path/to/server.py # python server
336 uv run client.py path/to/build/index.js # node server
337 ```
338
339 <Note>
340 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python), your command might look something like this: `python client.py .../quickstart-resources/weather-server-python/weather.py`
341 </Note>
342
343 The client will:
344
345 1. Connect to the specified server
346 2. List available tools
347 3. Start an interactive chat session where you can:
348 * Enter queries
349 * See tool executions
350 * Get responses from Claude
351
352 Here's an example of what it should look like if connected to the weather server from the server quickstart:
353
354 <Frame>
355 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/client-claude-cli-python.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=686d6e0ae7c54f807827db111eaed7d4" width="1932" height="1739" data-path="images/client-claude-cli-python.png" />
356 </Frame>
357
358 ## How It Works
359
360 When you submit a query:
361
362 1. The client gets the list of available tools from the server
363 2. Your query is sent to Claude along with tool descriptions
364 3. Claude decides which tools (if any) to use
365 4. The client executes any requested tool calls through the server
366 5. Results are sent back to Claude
367 6. Claude provides a natural language response
368 7. The response is displayed to you
369
370 ## Best practices
371
372 1. **Error Handling**
373 * Always wrap tool calls in try-catch blocks
374 * Provide meaningful error messages
375 * Gracefully handle connection issues
376
377 2. **Resource Management**
378 * Use `AsyncExitStack` for proper cleanup
379 * Close connections when done
380 * Handle server disconnections
381
382 3. **Security**
383 * Store API keys securely in `.env`
384 * Validate server responses
385 * Be cautious with tool permissions
386
387 ## Troubleshooting
388
389 ### Server Path Issues
390
391 * Double-check the path to your server script is correct
392 * Use the absolute path if the relative path isn't working
393 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
394 * Verify the server file has the correct extension (.py for Python or .js for Node.js)
395
396 Example of correct path usage:
397
398 ```bash theme={null}
399 # Relative path
400 uv run client.py ./server/weather.py
401
402 # Absolute path
403 uv run client.py /Users/username/projects/mcp-server/weather.py
404
405 # Windows path (either format works)
406 uv run client.py C:/projects/mcp-server/weather.py
407 uv run client.py C:\\projects\\mcp-server\\weather.py
408 ```
409
410 ### Response Timing
411
412 * The first response might take up to 30 seconds to return
413 * This is normal and happens while:
414 * The server initializes
415 * Claude processes the query
416 * Tools are being executed
417 * Subsequent responses are typically faster
418 * Don't interrupt the process during this initial waiting period
419
420 ### Common Error Messages
421
422 If you see:
423
424 * `FileNotFoundError`: Check your server path
425 * `Connection refused`: Ensure the server is running and the path is correct
426 * `Tool execution failed`: Verify the tool's required environment variables are set
427 * `Timeout error`: Consider increasing the timeout in your client configuration
428 </Tab>
429
430 <Tab title="TypeScript">
431 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
432
433 ## System Requirements
434
435 Before starting, ensure your system meets these requirements:
436
437 * Mac or Windows computer
438 * Node.js 17 or higher installed
439 * Latest version of `npm` installed
440 * Anthropic API key (Claude)
441
442 ## Setting Up Your Environment
443
444 First, let's create and set up our project:
445
446 <CodeGroup>
447 ```bash macOS/Linux theme={null}
448 # Create project directory
449 mkdir mcp-client-typescript
450 cd mcp-client-typescript
451
452 # Initialize npm project
453 npm init -y
454
455 # Install dependencies
456 npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
457
458 # Install dev dependencies
459 npm install -D @types/node typescript
460
461 # Create source file
462 touch index.ts
463 ```
464
465 ```powershell Windows theme={null}
466 # Create project directory
467 md mcp-client-typescript
468 cd mcp-client-typescript
469
470 # Initialize npm project
471 npm init -y
472
473 # Install dependencies
474 npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
475
476 # Install dev dependencies
477 npm install -D @types/node typescript
478
479 # Create source file
480 new-item index.ts
481 ```
482 </CodeGroup>
483
484 Update your `package.json` to set `type: "module"` and a build script:
485
486 ```json package.json theme={null}
236 query = input("\nQuery: ").strip()
237
238 if query.lower() == 'quit':
239 break
240
241 response = await self.process_query(query)
242 print("\n" + response)
243
244 except Exception as e:
245 print(f"\nError: {str(e)}")
246
247async def cleanup(self):
248 """Clean up resources"""
249 await self.exit_stack.aclose()
250```
251
252### Main Entry Point
253
254Finally, we'll add the main execution logic:
255
256```python theme={null}
257async def main():
258 if len(sys.argv) < 2:
259 print("Usage: python client.py <path_to_server_script>")
260 sys.exit(1)
261
262 client = MCPClient()
263 try:
264 await client.connect_to_server(sys.argv[1])
265 await client.chat_loop()
266 finally:
267 await client.cleanup()
268
269if __name__ == "__main__":
270 import sys
271 asyncio.run(main())
272```
273
274You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).
275
276## Key Components Explained
277
278### 1. Client Initialization
279
280* The `MCPClient` class initializes with session management and API clients
281* Uses `AsyncExitStack` for proper resource management
282* Configures the Anthropic client for Claude interactions
283
284### 2. Server Connection
285
286* Supports both Python and Node.js servers
287* Validates server script type
288* Sets up proper communication channels
289* Initializes the session and lists available tools
290
291### 3. Query Processing
292
293* Maintains conversation context
294* Handles Claude's responses and tool calls
295* Manages the message flow between Claude and tools
296* Combines results into a coherent response
297
298### 4. Interactive Interface
299
300* Provides a simple command-line interface
301* Handles user input and displays responses
302* Includes basic error handling
303* Allows graceful exit
304
305### 5. Resource Management
306
307* Proper cleanup of resources
308* Error handling for connection issues
309* Graceful shutdown procedures
310
311## Common Customization Points
312
3131. **Tool Handling**
314 * Modify `process_query()` to handle specific tool types
315 * Add custom error handling for tool calls
316 * Implement tool-specific response formatting
317
3182. **Response Processing**
319 * Customize how tool results are formatted
320 * Add response filtering or transformation
321 * Implement custom logging
322
3233. **User Interface**
324 * Add a GUI or web interface
325 * Implement rich console output
326 * Add command history or auto-completion
327
328## Running the Client
329
330To run your client with any MCP server:
331
332```bash theme={null}
333uv run client.py path/to/server.py # python server
334uv run client.py path/to/build/index.js # node server
335```
336
337> [!NOTE]
338>
339> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python), your command might look something like this: `python client.py .../quickstart-resources/weather-server-python/weather.py`
340
341The client will:
342
3431. Connect to the specified server
3442. List available tools
3453. Start an interactive chat session where you can:
346 * Enter queries
347 * See tool executions
348 * Get responses from Claude
349
350Here's an example of what it should look like if connected to the weather server from the server quickstart:
351
352
353
354## How It Works
355
356When you submit a query:
357
3581. The client gets the list of available tools from the server
3592. Your query is sent to Claude along with tool descriptions
3603. Claude decides which tools (if any) to use
3614. The client executes any requested tool calls through the server
3625. Results are sent back to Claude
3636. Claude provides a natural language response
3647. The response is displayed to you
365
366## Best practices
367
3681. **Error Handling**
369 * Always wrap tool calls in try-catch blocks
370 * Provide meaningful error messages
371 * Gracefully handle connection issues
372
3732. **Resource Management**
374 * Use `AsyncExitStack` for proper cleanup
375 * Close connections when done
376 * Handle server disconnections
377
3783. **Security**
379 * Store API keys securely in `.env`
380 * Validate server responses
381 * Be cautious with tool permissions
382
383## Troubleshooting
384
385### Server Path Issues
386
387* Double-check the path to your server script is correct
388* Use the absolute path if the relative path isn't working
389* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
390* Verify the server file has the correct extension (.py for Python or .js for Node.js)
391
392Example of correct path usage:
393
394```bash theme={null}
395# Relative path
396uv run client.py ./server/weather.py
397
398# Absolute path
399uv run client.py /Users/username/projects/mcp-server/weather.py
400
401# Windows path (either format works)
402uv run client.py C:/projects/mcp-server/weather.py
403uv run client.py C:\\projects\\mcp-server\\weather.py
404```
405
406### Response Timing
407
408* The first response might take up to 30 seconds to return
409* This is normal and happens while:
410 * The server initializes
411 * Claude processes the query
412 * Tools are being executed
413* Subsequent responses are typically faster
414* Don't interrupt the process during this initial waiting period
415
416### Common Error Messages
417
418If you see:
419
420* `FileNotFoundError`: Check your server path
421* `Connection refused`: Ensure the server is running and the path is correct
422* `Tool execution failed`: Verify the tool's required environment variables are set
423* `Timeout error`: Consider increasing the timeout in your client configuration
424
425**TypeScript**
426
427[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
428
429## System Requirements
430
431Before starting, ensure your system meets these requirements:
432
433* Mac or Windows computer
434* Node.js 17 or higher installed
435* Latest version of `npm` installed
436* Anthropic API key (Claude)
437
438## Setting Up Your Environment
439
440First, let's create and set up our project:
441
442```bash macOS/Linux theme={null}
443# Create project directory
444mkdir mcp-client-typescript
445cd mcp-client-typescript
446
447# Initialize npm project
448npm init -y
449
450# Install dependencies
451npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
452
453# Install dev dependencies
454npm install -D @types/node typescript
455
456# Create source file
457touch index.ts
458```
459
460```powershell Windows theme={null}
461# Create project directory
462md mcp-client-typescript
463cd mcp-client-typescript
464
465# Initialize npm project
466npm init -y
467
468# Install dependencies
469npm install @anthropic-ai/sdk @modelcontextprotocol/sdk dotenv
470
471# Install dev dependencies
472npm install -D @types/node typescript
473
474# Create source file
475new-item index.ts
476```
477
478Update your `package.json` to set `type: "module"` and a build script:
479
480```json package.json theme={null}
481{
482 "type": "module",
483 "scripts": {
484 "build": "tsc && chmod 755 build/index.js"
485 }
486}
487```
488
489Create a `tsconfig.json` in the root of your project:
490
491```json tsconfig.json theme={null}
492{
493 "compilerOptions": {
494 "target": "ES2022",
495 "module": "Node16",
496 "moduleResolution": "Node16",
497 "outDir": "./build",
498 "rootDir": "./",
499 "strict": true,
500 "esModuleInterop": true,
501 "skipLibCheck": true,
502 "forceConsistentCasingInFileNames": true
503 },
504 "include": ["index.ts"],
505 "exclude": ["node_modules"]
506}
507```
508
509## Setting Up Your API Key
510
511You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
512
513Create a `.env` file to store it:
514
515```bash theme={null}
516echo "ANTHROPIC_API_KEY=<your key here>" > .env
517```
518
519Add `.env` to your `.gitignore`:
520
521```bash theme={null}
522echo ".env" >> .gitignore
523```
524
525> [!WARNING]
526>
527> Make sure you keep your `ANTHROPIC_API_KEY` secure!
528
529## Creating the Client
530
531### Basic Client Structure
532
533First, let's set up our imports and create the basic client class in `index.ts`:
534
535```typescript theme={null}
536import { Anthropic } from "@anthropic-ai/sdk";
537import {
538 MessageParam,
539 Tool,
540} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
541import { Client } from "@modelcontextprotocol/sdk/client/index.js";
542import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
543import readline from "readline/promises";
544import dotenv from "dotenv";
545
546dotenv.config();
547
548const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
549if (!ANTHROPIC_API_KEY) {
550 throw new Error("ANTHROPIC_API_KEY is not set");
551}
552
553class MCPClient {
554 private mcp: Client;
555 private anthropic: Anthropic;
556 private transport: StdioClientTransport | null = null;
557 private tools: Tool[] = [];
558
559 constructor() {
560 this.anthropic = new Anthropic({
561 apiKey: ANTHROPIC_API_KEY,
562 });
563 this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
564 }
565 // methods will go here
566}
567```
568
569### Server Connection Management
570
571Next, we'll implement the method to connect to an MCP server:
572
573```typescript theme={null}
574async connectToServer(serverScriptPath: string) {
575 try {
576 const isJs = serverScriptPath.endsWith(".js");
577 const isPy = serverScriptPath.endsWith(".py");
578 if (!isJs && !isPy) {
579 throw new Error("Server script must be a .js or .py file");
580 }
581 const command = isPy
582 ? process.platform === "win32"
583 ? "python"
584 : "python3"
585 : process.execPath;
586
587 this.transport = new StdioClientTransport({
588 command,
589 args: [serverScriptPath],
590 });
591 await this.mcp.connect(this.transport);
592
593 const toolsResult = await this.mcp.listTools();
594 this.tools = toolsResult.tools.map((tool) => {
595 return {
596 name: tool.name,
597 description: tool.description,
598 input_schema: tool.inputSchema,
599 };
600 });
601 console.log(
602 "Connected to server with tools:",
603 this.tools.map(({ name }) => name)
604 );
605 } catch (e) {
606 console.log("Failed to connect to MCP server: ", e);
607 throw e;
608 }
609}
610```
611
612### Query Processing Logic
613
614Now let's add the core functionality for processing queries and handling tool calls:
615
616```typescript theme={null}
617async processQuery(query: string) {
618 const messages: MessageParam[] = [
487619 {
488 "type": "module",
489 "scripts": {
490 "build": "tsc && chmod 755 build/index.js"
491 }
492 }
493 ```
494
495 Create a `tsconfig.json` in the root of your project:
496
497 ```json tsconfig.json theme={null}
498 {
499 "compilerOptions": {
500 "target": "ES2022",
501 "module": "Node16",
502 "moduleResolution": "Node16",
503 "outDir": "./build",
504 "rootDir": "./",
505 "strict": true,
506 "esModuleInterop": true,
507 "skipLibCheck": true,
508 "forceConsistentCasingInFileNames": true
509 },
510 "include": ["index.ts"],
511 "exclude": ["node_modules"]
512 }
513 ```
514
515 ## Setting Up Your API Key
516
517 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
518
519 Create a `.env` file to store it:
520
521 ```bash theme={null}
522 echo "ANTHROPIC_API_KEY=<your key here>" > .env
523 ```
524
525 Add `.env` to your `.gitignore`:
526
527 ```bash theme={null}
528 echo ".env" >> .gitignore
529 ```
530
531 <Warning>
532 Make sure you keep your `ANTHROPIC_API_KEY` secure!
533 </Warning>
534
535 ## Creating the Client
536
537 ### Basic Client Structure
538
539 First, let's set up our imports and create the basic client class in `index.ts`:
540
541 ```typescript theme={null}
542 import { Anthropic } from "@anthropic-ai/sdk";
543 import {
544 MessageParam,
545 Tool,
546 } from "@anthropic-ai/sdk/resources/messages/messages.mjs";
547 import { Client } from "@modelcontextprotocol/sdk/client/index.js";
548 import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
549 import readline from "readline/promises";
550 import dotenv from "dotenv";
551
552 dotenv.config();
553
554 const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
555 if (!ANTHROPIC_API_KEY) {
556 throw new Error("ANTHROPIC_API_KEY is not set");
557 }
558
559 class MCPClient {
560 private mcp: Client;
561 private anthropic: Anthropic;
562 private transport: StdioClientTransport | null = null;
563 private tools: Tool[] = [];
564
565 constructor() {
566 this.anthropic = new Anthropic({
567 apiKey: ANTHROPIC_API_KEY,
568 });
569 this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
570 }
571 // methods will go here
572 }
573 ```
574
575 ### Server Connection Management
576
577 Next, we'll implement the method to connect to an MCP server:
578
579 ```typescript theme={null}
580 async connectToServer(serverScriptPath: string) {
581 try {
582 const isJs = serverScriptPath.endsWith(".js");
583 const isPy = serverScriptPath.endsWith(".py");
584 if (!isJs && !isPy) {
585 throw new Error("Server script must be a .js or .py file");
586 }
587 const command = isPy
588 ? process.platform === "win32"
589 ? "python"
590 : "python3"
591 : process.execPath;
592
593 this.transport = new StdioClientTransport({
594 command,
595 args: [serverScriptPath],
596 });
597 await this.mcp.connect(this.transport);
598
599 const toolsResult = await this.mcp.listTools();
600 this.tools = toolsResult.tools.map((tool) => {
601 return {
602 name: tool.name,
603 description: tool.description,
604 input_schema: tool.inputSchema,
605 };
606 });
607 console.log(
608 "Connected to server with tools:",
609 this.tools.map(({ name }) => name)
610 );
611 } catch (e) {
612 console.log("Failed to connect to MCP server: ", e);
613 throw e;
614 }
615 }
616 ```
617
618 ### Query Processing Logic
619
620 Now let's add the core functionality for processing queries and handling tool calls:
621
622 ```typescript theme={null}
623 async processQuery(query: string) {
624 const messages: MessageParam[] = [
625 {
626 role: "user",
627 content: query,
628 },
629 ];
620 role: "user",
621 content: query,
622 },
623 ];
624
625 const response = await this.anthropic.messages.create({
626 model: "claude-sonnet-4-20250514",
627 max_tokens: 1000,
628 messages,
629 tools: this.tools,
630 });
631
632 const finalText = [];
633
634 for (const content of response.content) {
635 if (content.type === "text") {
636 finalText.push(content.text);
637 } else if (content.type === "tool_use") {
638 const toolName = content.name;
639 const toolArgs = content.input as { [x: string]: unknown } | undefined;
640
641 const result = await this.mcp.callTool({
642 name: toolName,
643 arguments: toolArgs,
644 });
645 finalText.push(
646 `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
647 );
648
649 messages.push({
650 role: "user",
651 content: result.content as string,
652 });
630653
631654 const response = await this.anthropic.messages.create({
632655 model: "claude-sonnet-4-20250514",
633656 max_tokens: 1000,
634657 messages,
635 tools: this.tools,
636658 });
637659
638 const finalText = [];
639
640 for (const content of response.content) {
641 if (content.type === "text") {
642 finalText.push(content.text);
643 } else if (content.type === "tool_use") {
644 const toolName = content.name;
645 const toolArgs = content.input as { [x: string]: unknown } | undefined;
646
647 const result = await this.mcp.callTool({
648 name: toolName,
649 arguments: toolArgs,
650 });
651 finalText.push(
652 `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
653 );
654
655 messages.push({
656 role: "user",
657 content: result.content as string,
658 });
659
660 const response = await this.anthropic.messages.create({
661 model: "claude-sonnet-4-20250514",
662 max_tokens: 1000,
663 messages,
664 });
665
666 finalText.push(
667 response.content[0].type === "text" ? response.content[0].text : ""
668 );
669 }
660 finalText.push(
661 response.content[0].type === "text" ? response.content[0].text : ""
662 );
663 }
664 }
665
666 return finalText.join("\n");
667}
668```
669
670### Interactive Chat Interface
671
672Now we'll add the chat loop and cleanup functionality:
673
674```typescript theme={null}
675async chatLoop() {
676 const rl = readline.createInterface({
677 input: process.stdin,
678 output: process.stdout,
679 });
680
681 try {
682 console.log("\nMCP Client Started!");
683 console.log("Type your queries or 'quit' to exit.");
684
685 while (true) {
686 const message = await rl.question("\nQuery: ");
687 if (message.toLowerCase() === "quit") {
688 break;
670689 }
671
672 return finalText.join("\n");
690 const response = await this.processQuery(message);
691 console.log("\n" + response);
673692 }
674 ```
675
676 ### Interactive Chat Interface
677
678 Now we'll add the chat loop and cleanup functionality:
679
680 ```typescript theme={null}
681 async chatLoop() {
682 const rl = readline.createInterface({
683 input: process.stdin,
684 output: process.stdout,
685 });
686
687 try {
688 console.log("\nMCP Client Started!");
689 console.log("Type your queries or 'quit' to exit.");
690
691 while (true) {
692 const message = await rl.question("\nQuery: ");
693 if (message.toLowerCase() === "quit") {
694 break;
695 }
696 const response = await this.processQuery(message);
697 console.log("\n" + response);
698 }
699 } finally {
700 rl.close();
693 } finally {
694 rl.close();
695 }
696}
697
698async cleanup() {
699 await this.mcp.close();
700}
701```
702
703### Main Entry Point
704
705Finally, we'll add the main execution logic:
706
707```typescript theme={null}
708async function main() {
709 if (process.argv.length < 3) {
710 console.log("Usage: node index.ts <path_to_server_script>");
711 return;
712 }
713 const mcpClient = new MCPClient();
714 try {
715 await mcpClient.connectToServer(process.argv[2]);
716 await mcpClient.chatLoop();
717 } catch (e) {
718 console.error("Error:", e);
719 await mcpClient.cleanup();
720 process.exit(1);
721 } finally {
722 await mcpClient.cleanup();
723 process.exit(0);
724 }
725}
726
727main();
728```
729
730## Running the Client
731
732To run your client with any MCP server:
733
734```bash theme={null}
735# Build TypeScript
736npm run build
737
738# Run the client
739node build/index.js path/to/server.py # python server
740node build/index.js path/to/build/index.js # node server
741```
742
743> [!NOTE]
744>
745> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript), your command might look something like this: `node build/index.js .../quickstart-resources/weather-server-typescript/build/index.js`
746
747**The client will:**
748
7491. Connect to the specified server
7502. List available tools
7513. Start an interactive chat session where you can:
752 * Enter queries
753 * See tool executions
754 * Get responses from Claude
755
756## How It Works
757
758When you submit a query:
759
7601. The client gets the list of available tools from the server
7612. Your query is sent to Claude along with tool descriptions
7623. Claude decides which tools (if any) to use
7634. The client executes any requested tool calls through the server
7645. Results are sent back to Claude
7656. Claude provides a natural language response
7667. The response is displayed to you
767
768## Best practices
769
7701. **Error Handling**
771 * Use TypeScript's type system for better error detection
772 * Wrap tool calls in try-catch blocks
773 * Provide meaningful error messages
774 * Gracefully handle connection issues
775
7762. **Security**
777 * Store API keys securely in `.env`
778 * Validate server responses
779 * Be cautious with tool permissions
780
781## Troubleshooting
782
783### Server Path Issues
784
785* Double-check the path to your server script is correct
786* Use the absolute path if the relative path isn't working
787* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
788* Verify the server file has the correct extension (.js for Node.js or .py for Python)
789
790Example of correct path usage:
791
792```bash theme={null}
793# Relative path
794node build/index.js ./server/build/index.js
795
796# Absolute path
797node build/index.js /Users/username/projects/mcp-server/build/index.js
798
799# Windows path (either format works)
800node build/index.js C:/projects/mcp-server/build/index.js
801node build/index.js C:\\projects\\mcp-server\\build\\index.js
802```
803
804### Response Timing
805
806* The first response might take up to 30 seconds to return
807* This is normal and happens while:
808 * The server initializes
809 * Claude processes the query
810 * Tools are being executed
811* Subsequent responses are typically faster
812* Don't interrupt the process during this initial waiting period
813
814### Common Error Messages
815
816If you see:
817
818* `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded
819* `Connection refused`: Ensure the server is running and the path is correct
820* `Tool execution failed`: Verify the tool's required environment variables are set
821* `ANTHROPIC_API_KEY is not set`: Check your .env file and environment variables
822* `TypeError`: Ensure you're using the correct types for tool arguments
823* `BadRequestError`: Ensure you have enough credits to access the Anthropic API
824
825**Java**
826
827> [!NOTE]
828>
829> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
830> To learn how to create sync and async MCP Clients manually, consult the [Java SDK Client](https://java.sdk.modelcontextprotocol.io/) documentation
831
832This example demonstrates how to build an interactive chatbot that combines Spring AI's Model Context Protocol (MCP) with the [Brave Search MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search). The application creates a conversational interface powered by Anthropic's Claude AI model that can perform internet searches through Brave Search, enabling natural language interactions with real-time web data.
833[You can find the complete code for this tutorial here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/web-search/brave-chatbot)
834
835## System Requirements
836
837Before starting, ensure your system meets these requirements:
838
839* Java 17 or higher
840* Maven 3.6+
841* npx package manager
842* Anthropic API key (Claude)
843* Brave Search API key
844
845## Setting Up Your Environment
846
8471. Install npx (Node Package eXecute):
848 First, make sure to install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
849 and then run:
850
851 ```bash theme={null}
852 npm install -g npx
853 ```
854
8552. Clone the repository:
856
857 ```bash theme={null}
858 git clone https://github.com/spring-projects/spring-ai-examples.git
859 cd model-context-protocol/web-search/brave-chatbot
860 ```
861
8623. Set up your API keys:
863
864 ```bash theme={null}
865 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
866 export BRAVE_API_KEY='your-brave-api-key-here'
867 ```
868
8694. Build the application:
870
871 ```bash theme={null}
872 ./mvnw clean install
873 ```
874
8755. Run the application using Maven:
876 ```bash theme={null}
877 ./mvnw spring-boot:run
878 ```
879
880> [!WARNING]
881>
882> Make sure you keep your `ANTHROPIC_API_KEY` and `BRAVE_API_KEY` keys secure!
883
884## How it Works
885
886The application integrates Spring AI with the Brave Search MCP server through several components:
887
888### MCP Client Configuration
889
8901. Required dependencies in pom.xml:
891
892```xml theme={null}
893<dependency>
894 <groupId>org.springframework.ai</groupId>
895 <artifactId>spring-ai-starter-mcp-client</artifactId>
896</dependency>
897<dependency>
898 <groupId>org.springframework.ai</groupId>
899 <artifactId>spring-ai-starter-model-anthropic</artifactId>
900</dependency>
901```
902
9032. Application properties (application.yml):
904
905```yml theme={null}
906spring:
907 ai:
908 mcp:
909 client:
910 enabled: true
911 name: brave-search-client
912 version: 1.0.0
913 type: SYNC
914 request-timeout: 20s
915 stdio:
916 root-change-notification: true
917 servers-configuration: classpath:/mcp-servers-config.json
918 toolcallback:
919 enabled: true
920 anthropic:
921 api-key: ${ANTHROPIC_API_KEY}
922```
923
924This activates the `spring-ai-starter-mcp-client` to create one or more `McpClient`s based on the provided server configuration.
925The `spring.ai.mcp.client.toolcallback.enabled=true` property enables the tool callback mechanism, that automatically registers all MCP tool as spring ai tools.
926It is disabled by default.
927
9283. MCP Server Configuration (`mcp-servers-config.json`):
929
930```json theme={null}
931{
932 "mcpServers": {
933 "brave-search": {
934 "command": "npx",
935 "args": ["-y", "@modelcontextprotocol/server-brave-search"],
936 "env": {
937 "BRAVE_API_KEY": "<PUT YOUR BRAVE API KEY>"
701938 }
702939 }
703
704 async cleanup() {
705 await this.mcp.close();
940 }
941}
942```
943
944### Chat Implementation
945
946The chatbot is implemented using Spring AI's ChatClient with MCP tool integration:
947
948```java theme={null}
949var chatClient = chatClientBuilder
950 .defaultSystem("You are useful assistant, expert in AI and Java.")
951 .defaultToolCallbacks((Object[]) mcpToolAdapter.toolCallbacks())
952 .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
953 .build();
954```
955
956Key features:
957
958* Uses Claude AI model for natural language understanding
959* Integrates Brave Search through MCP for real-time web search capabilities
960* Maintains conversation memory using InMemoryChatMemory
961* Runs as an interactive command-line application
962
963### Build and run
964
965```bash theme={null}
966./mvnw clean install
967java -jar ./target/ai-mcp-brave-chatbot-0.0.1-SNAPSHOT.jar
968```
969
970or
971
972```bash theme={null}
973./mvnw spring-boot:run
974```
975
976The application will start an interactive chat session where you can ask questions. The chatbot will use Brave Search when it needs to find information from the internet to answer your queries.
977
978The chatbot can:
979
980* Answer questions using its built-in knowledge
981* Perform web searches when needed using Brave Search
982* Remember context from previous messages in the conversation
983* Combine information from multiple sources to provide comprehensive answers
984
985### Advanced Configuration
986
987The MCP client supports additional configuration options:
988
989* Client customization through `McpSyncClientCustomizer` or `McpAsyncClientCustomizer`
990* Multiple clients with multiple transport types: `STDIO` and `SSE` (Server-Sent Events)
991* Integration with Spring AI's tool execution framework
992* Automatic client initialization and lifecycle management
993
994For WebFlux-based applications, you can use the WebFlux starter instead:
995
996```xml theme={null}
997<dependency>
998 <groupId>org.springframework.ai</groupId>
999 <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
1000</dependency>
1001```
1002
1003This provides similar functionality but uses a WebFlux-based SSE transport implementation, recommended for production deployments.
1004
1005**Kotlin**
1006
1007[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/kotlin-mcp-client)
1008
1009## System Requirements
1010
1011Before starting, ensure your system meets these requirements:
1012
1013* JDK 11 or higher
1014* Anthropic API key (Claude)
1015
1016## Setting up your environment
1017
1018First, let's install `java` and `gradle` if you haven't already.
1019You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1020Verify your `java` installation:
1021
1022```bash theme={null}
1023java --version
1024```
1025
1026Now, let's create and set up your project:
1027
1028```bash macOS/Linux theme={null}
1029# Create a new directory for our project
1030mkdir kotlin-mcp-client
1031cd kotlin-mcp-client
1032
1033# Initialize a new kotlin project
1034gradle init
1035```
1036
1037```powershell Windows theme={null}
1038# Create a new directory for our project
1039md kotlin-mcp-client
1040cd kotlin-mcp-client
1041# Initialize a new kotlin project
1042gradle init
1043```
1044
1045After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1046
1047Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1048
1049After creating the project, replace the contents of your `build.gradle.kts` with:
1050
1051```kotlin build.gradle.kts theme={null}
1052// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1053val mcpVersion = "0.9.0"
1054val ktorVersion = "3.2.3"
1055val anthropicVersion = "2.15.0"
1056val slf4jVersion = "2.0.17"
1057
1058plugins {
1059 kotlin("jvm") version "2.3.20"
1060 id("com.gradleup.shadow") version "8.3.9"
1061 application
1062}
1063
1064application {
1065 mainClass.set("MainKt")
1066}
1067
1068dependencies {
1069 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1070 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1071 implementation("com.anthropic:anthropic-java:$anthropicVersion")
1072 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1073}
1074```
1075
1076Verify that everything is set up correctly:
1077
1078```bash theme={null}
1079./gradlew build
1080```
1081
1082## Setting up your API key
1083
1084You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1085
1086Set up your API key:
1087
1088```bash theme={null}
1089export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
1090```
1091
1092> [!WARNING]
1093>
1094> Make sure you keep your `ANTHROPIC_API_KEY` secure!
1095
1096## Creating the Client
1097
1098### Basic Client Structure
1099
1100First, let's create the basic client class:
1101
1102```kotlin theme={null}
1103class MCPClient(apiKey: String) : AutoCloseable {
1104 private val anthropic = AnthropicOkHttpClient.builder()
1105 .apiKey(apiKey)
1106 .build()
1107
1108 private val mcp: Client = Client(
1109 clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")
1110 )
1111 private var serverProcess: Process? = null
1112 private lateinit var tools: List<ToolUnion>
1113
1114 // methods will go here
1115
1116 override fun close() {
1117 runBlocking {
1118 mcp.close()
1119 }
1120 serverProcess?.destroy()
1121 anthropic.close()
7061122 }
707 ```
708
709 ### Main Entry Point
710
711 Finally, we'll add the main execution logic:
712
713 ```typescript theme={null}
714 async function main() {
715 if (process.argv.length < 3) {
716 console.log("Usage: node index.ts <path_to_server_script>");
717 return;
718 }
719 const mcpClient = new MCPClient();
720 try {
721 await mcpClient.connectToServer(process.argv[2]);
722 await mcpClient.chatLoop();
723 } catch (e) {
724 console.error("Error:", e);
725 await mcpClient.cleanup();
726 process.exit(1);
727 } finally {
728 await mcpClient.cleanup();
729 process.exit(0);
730 }
1123}
1124```
1125
1126### Server connection management
1127
1128Next, we'll implement the method to connect to an MCP server:
1129
1130```kotlin theme={null}
1131suspend fun connectToServer(serverScriptPath: String) {
1132 val command = buildList {
1133 when (serverScriptPath.substringAfterLast(".")) {
1134 "js" -> add("node")
1135 "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
1136 "jar" -> addAll(listOf("java", "-jar"))
1137 else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
1138 }
1139 add(serverScriptPath)
7311140 }
7321141
733 main();
734 ```
735
736 ## Running the Client
737
738 To run your client with any MCP server:
739
740 ```bash theme={null}
741 # Build TypeScript
742 npm run build
743
744 # Run the client
745 node build/index.js path/to/server.py # python server
746 node build/index.js path/to/build/index.js # node server
747 ```
748
749 <Note>
750 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript), your command might look something like this: `node build/index.js .../quickstart-resources/weather-server-typescript/build/index.js`
751 </Note>
752
753 **The client will:**
754
755 1. Connect to the specified server
756 2. List available tools
757 3. Start an interactive chat session where you can:
758 * Enter queries
759 * See tool executions
760 * Get responses from Claude
761
762 ## How It Works
763
764 When you submit a query:
765
766 1. The client gets the list of available tools from the server
767 2. Your query is sent to Claude along with tool descriptions
768 3. Claude decides which tools (if any) to use
769 4. The client executes any requested tool calls through the server
770 5. Results are sent back to Claude
771 6. Claude provides a natural language response
772 7. The response is displayed to you
773
774 ## Best practices
775
776 1. **Error Handling**
777 * Use TypeScript's type system for better error detection
778 * Wrap tool calls in try-catch blocks
779 * Provide meaningful error messages
780 * Gracefully handle connection issues
781
782 2. **Security**
783 * Store API keys securely in `.env`
784 * Validate server responses
785 * Be cautious with tool permissions
786
787 ## Troubleshooting
788
789 ### Server Path Issues
790
791 * Double-check the path to your server script is correct
792 * Use the absolute path if the relative path isn't working
793 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
794 * Verify the server file has the correct extension (.js for Node.js or .py for Python)
795
796 Example of correct path usage:
797
798 ```bash theme={null}
799 # Relative path
800 node build/index.js ./server/build/index.js
801
802 # Absolute path
803 node build/index.js /Users/username/projects/mcp-server/build/index.js
804
805 # Windows path (either format works)
806 node build/index.js C:/projects/mcp-server/build/index.js
807 node build/index.js C:\\projects\\mcp-server\\build\\index.js
808 ```
809
810 ### Response Timing
811
812 * The first response might take up to 30 seconds to return
813 * This is normal and happens while:
814 * The server initializes
815 * Claude processes the query
816 * Tools are being executed
817 * Subsequent responses are typically faster
818 * Don't interrupt the process during this initial waiting period
819
820 ### Common Error Messages
821
822 If you see:
823
824 * `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded
825 * `Connection refused`: Ensure the server is running and the path is correct
826 * `Tool execution failed`: Verify the tool's required environment variables are set
827 * `ANTHROPIC_API_KEY is not set`: Check your .env file and environment variables
828 * `TypeError`: Ensure you're using the correct types for tool arguments
829 * `BadRequestError`: Ensure you have enough credits to access the Anthropic API
830 </Tab>
831
832 <Tab title="Java">
833 <Note>
834 This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
835 To learn how to create sync and async MCP Clients manually, consult the [Java SDK Client](https://java.sdk.modelcontextprotocol.io/) documentation
836 </Note>
837
838 This example demonstrates how to build an interactive chatbot that combines Spring AI's Model Context Protocol (MCP) with the [Brave Search MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search). The application creates a conversational interface powered by Anthropic's Claude AI model that can perform internet searches through Brave Search, enabling natural language interactions with real-time web data.
839 [You can find the complete code for this tutorial here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/web-search/brave-chatbot)
840
841 ## System Requirements
842
843 Before starting, ensure your system meets these requirements:
844
845 * Java 17 or higher
846 * Maven 3.6+
847 * npx package manager
848 * Anthropic API key (Claude)
849 * Brave Search API key
850
851 ## Setting Up Your Environment
852
853 1. Install npx (Node Package eXecute):
854 First, make sure to install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
855 and then run:
856
857 ```bash theme={null}
858 npm install -g npx
859 ```
860
861 2. Clone the repository:
862
863 ```bash theme={null}
864 git clone https://github.com/spring-projects/spring-ai-examples.git
865 cd model-context-protocol/web-search/brave-chatbot
866 ```
867
868 3. Set up your API keys:
869
870 ```bash theme={null}
871 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
872 export BRAVE_API_KEY='your-brave-api-key-here'
873 ```
874
875 4. Build the application:
876
877 ```bash theme={null}
878 ./mvnw clean install
879 ```
880
881 5. Run the application using Maven:
882 ```bash theme={null}
883 ./mvnw spring-boot:run
884 ```
885
886 <Warning>
887 Make sure you keep your `ANTHROPIC_API_KEY` and `BRAVE_API_KEY` keys secure!
888 </Warning>
889
890 ## How it Works
891
892 The application integrates Spring AI with the Brave Search MCP server through several components:
893
894 ### MCP Client Configuration
895
896 1. Required dependencies in pom.xml:
897
898 ```xml theme={null}
899 <dependency>
900 <groupId>org.springframework.ai</groupId>
901 <artifactId>spring-ai-starter-mcp-client</artifactId>
902 </dependency>
903 <dependency>
904 <groupId>org.springframework.ai</groupId>
905 <artifactId>spring-ai-starter-model-anthropic</artifactId>
906 </dependency>
907 ```
908
909 2. Application properties (application.yml):
910
911 ```yml theme={null}
912 spring:
913 ai:
914 mcp:
915 client:
916 enabled: true
917 name: brave-search-client
918 version: 1.0.0
919 type: SYNC
920 request-timeout: 20s
921 stdio:
922 root-change-notification: true
923 servers-configuration: classpath:/mcp-servers-config.json
924 toolcallback:
925 enabled: true
926 anthropic:
927 api-key: ${ANTHROPIC_API_KEY}
928 ```
929
930 This activates the `spring-ai-starter-mcp-client` to create one or more `McpClient`s based on the provided server configuration.
931 The `spring.ai.mcp.client.toolcallback.enabled=true` property enables the tool callback mechanism, that automatically registers all MCP tool as spring ai tools.
932 It is disabled by default.
933
934 3. MCP Server Configuration (`mcp-servers-config.json`):
935
936 ```json theme={null}
937 {
938 "mcpServers": {
939 "brave-search": {
940 "command": "npx",
941 "args": ["-y", "@modelcontextprotocol/server-brave-search"],
942 "env": {
943 "BRAVE_API_KEY": "<PUT YOUR BRAVE API KEY>"
944 }
945 }
946 }
947 }
948 ```
949
950 ### Chat Implementation
951
952 The chatbot is implemented using Spring AI's ChatClient with MCP tool integration:
953
954 ```java theme={null}
955 var chatClient = chatClientBuilder
956 .defaultSystem("You are useful assistant, expert in AI and Java.")
957 .defaultToolCallbacks((Object[]) mcpToolAdapter.toolCallbacks())
958 .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
959 .build();
960 ```
961
962 Key features:
963
964 * Uses Claude AI model for natural language understanding
965 * Integrates Brave Search through MCP for real-time web search capabilities
966 * Maintains conversation memory using InMemoryChatMemory
967 * Runs as an interactive command-line application
968
969 ### Build and run
970
971 ```bash theme={null}
972 ./mvnw clean install
973 java -jar ./target/ai-mcp-brave-chatbot-0.0.1-SNAPSHOT.jar
974 ```
975
976 or
977
978 ```bash theme={null}
979 ./mvnw spring-boot:run
980 ```
981
982 The application will start an interactive chat session where you can ask questions. The chatbot will use Brave Search when it needs to find information from the internet to answer your queries.
983
984 The chatbot can:
985
986 * Answer questions using its built-in knowledge
987 * Perform web searches when needed using Brave Search
988 * Remember context from previous messages in the conversation
989 * Combine information from multiple sources to provide comprehensive answers
990
991 ### Advanced Configuration
992
993 The MCP client supports additional configuration options:
994
995 * Client customization through `McpSyncClientCustomizer` or `McpAsyncClientCustomizer`
996 * Multiple clients with multiple transport types: `STDIO` and `SSE` (Server-Sent Events)
997 * Integration with Spring AI's tool execution framework
998 * Automatic client initialization and lifecycle management
999
1000 For WebFlux-based applications, you can use the WebFlux starter instead:
1001
1002 ```xml theme={null}
1003 <dependency>
1004 <groupId>org.springframework.ai</groupId>
1005 <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
1006 </dependency>
1007 ```
1008
1009 This provides similar functionality but uses a WebFlux-based SSE transport implementation, recommended for production deployments.
1010 </Tab>
1011
1012 <Tab title="Kotlin">
1013 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/kotlin-mcp-client)
1014
1015 ## System Requirements
1016
1017 Before starting, ensure your system meets these requirements:
1018
1019 * JDK 11 or higher
1020 * Anthropic API key (Claude)
1021
1022 ## Setting up your environment
1023
1024 First, let's install `java` and `gradle` if you haven't already.
1025 You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1026 Verify your `java` installation:
1027
1028 ```bash theme={null}
1029 java --version
1030 ```
1031
1032 Now, let's create and set up your project:
1033
1034 <CodeGroup>
1035 ```bash macOS/Linux theme={null}
1036 # Create a new directory for our project
1037 mkdir kotlin-mcp-client
1038 cd kotlin-mcp-client
1039
1040 # Initialize a new kotlin project
1041 gradle init
1042 ```
1043
1044 ```powershell Windows theme={null}
1045 # Create a new directory for our project
1046 md kotlin-mcp-client
1047 cd kotlin-mcp-client
1048 # Initialize a new kotlin project
1049 gradle init
1050 ```
1051 </CodeGroup>
1052
1053 After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1054
1055 Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1056
1057 After creating the project, replace the contents of your `build.gradle.kts` with:
1058
1059 ```kotlin build.gradle.kts theme={null}
1060 // Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1061 val mcpVersion = "0.9.0"
1062 val ktorVersion = "3.2.3"
1063 val anthropicVersion = "2.15.0"
1064 val slf4jVersion = "2.0.17"
1065
1066 plugins {
1067 kotlin("jvm") version "2.3.20"
1068 id("com.gradleup.shadow") version "8.3.9"
1069 application
1070 }
1071
1072 application {
1073 mainClass.set("MainKt")
1074 }
1075
1076 dependencies {
1077 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1078 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1079 implementation("com.anthropic:anthropic-java:$anthropicVersion")
1080 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1081 }
1082 ```
1083
1084 Verify that everything is set up correctly:
1085
1086 ```bash theme={null}
1087 ./gradlew build
1088 ```
1089
1090 ## Setting up your API key
1091
1092 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1093
1094 Set up your API key:
1095
1096 ```bash theme={null}
1097 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
1098 ```
1099
1100 <Warning>
1101 Make sure you keep your `ANTHROPIC_API_KEY` secure!
1102 </Warning>
1103
1104 ## Creating the Client
1105
1106 ### Basic Client Structure
1107
1108 First, let's create the basic client class:
1109
1110 ```kotlin theme={null}
1111 class MCPClient(apiKey: String) : AutoCloseable {
1112 private val anthropic = AnthropicOkHttpClient.builder()
1113 .apiKey(apiKey)
1114 .build()
1115
1116 private val mcp: Client = Client(
1117 clientInfo = Implementation(name = "mcp-client-cli", version = "1.0.0")
1118 )
1119 private var serverProcess: Process? = null
1120 private lateinit var tools: List<ToolUnion>
1121
1122 // methods will go here
1123
1124 override fun close() {
1125 runBlocking {
1126 mcp.close()
1127 }
1128 serverProcess?.destroy()
1129 anthropic.close()
1130 }
1131 }
1132 ```
1133
1134 ### Server connection management
1135
1136 Next, we'll implement the method to connect to an MCP server:
1137
1138 ```kotlin theme={null}
1139 suspend fun connectToServer(serverScriptPath: String) {
1140 val command = buildList {
1141 when (serverScriptPath.substringAfterLast(".")) {
1142 "js" -> add("node")
1143 "py" -> add(if (System.getProperty("os.name").lowercase().contains("win")) "python" else "python3")
1144 "jar" -> addAll(listOf("java", "-jar"))
1145 else -> throw IllegalArgumentException("Server script must be a .js, .py or .jar file")
1146 }
1147 add(serverScriptPath)
1148 }
1149
1150 val process = ProcessBuilder(command).start()
1151 serverProcess = process
1152
1153 val transport = StdioClientTransport(
1154 input = process.inputStream.asSource().buffered(),
1155 output = process.outputStream.asSink().buffered(),
1156 )
1157
1158 mcp.connect(transport)
1159
1160 val toolsResult = mcp.listTools()
1161 tools = toolsResult.tools.map { tool ->
1162 ToolUnion.ofTool(
1163 Tool.builder()
1164 .name(tool.name)
1165 .description(tool.description ?: "")
1166 .inputSchema(
1167 Tool.InputSchema.builder()
1168 .type(JsonValue.from(tool.inputSchema.type))
1169 .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue())
1170 .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
1171 .build(),
1172 )
1173 .build(),
1174 )
1175 }
1176 println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
1177 }
1178 ```
1179
1180 <Accordion title="JsonObject.toJsonValue() helper">
1181 This helper converts a kotlinx.serialization `JsonObject` to an Anthropic SDK `JsonValue` using Jackson:
1182
1183 ```kotlin theme={null}
1184 private fun JsonObject.toJsonValue(): JsonValue {
1185 val mapper = ObjectMapper()
1186 val node = mapper.readTree(this.toString())
1187 return JsonValue.fromJsonNode(node)
1188 }
1189 ```
1190 </Accordion>
1191
1192 ### Query processing logic
1193
1194 Now let's add the core functionality for processing queries and handling tool calls:
1195
1196 ```kotlin theme={null}
1197 suspend fun processQuery(query: String): String {
1198 val messages = mutableListOf(
1199 MessageParam.builder()
1200 .role(MessageParam.Role.USER)
1201 .content(query)
1142 val process = ProcessBuilder(command).start()
1143 serverProcess = process
1144
1145 val transport = StdioClientTransport(
1146 input = process.inputStream.asSource().buffered(),
1147 output = process.outputStream.asSink().buffered(),
1148 )
1149
1150 mcp.connect(transport)
1151
1152 val toolsResult = mcp.listTools()
1153 tools = toolsResult.tools.map { tool ->
1154 ToolUnion.ofTool(
1155 Tool.builder()
1156 .name(tool.name)
1157 .description(tool.description ?: "")
1158 .inputSchema(
1159 Tool.InputSchema.builder()
1160 .type(JsonValue.from(tool.inputSchema.type))
1161 .properties(tool.inputSchema.properties?.toJsonValue() ?: EmptyJsonObject.toJsonValue())
1162 .putAdditionalProperty("required", JsonValue.from(tool.inputSchema.required))
1163 .build(),
1164 )
12021165 .build(),
12031166 )
1204
1205 val response = anthropic.messages().create(
1206 MessageCreateParams.builder()
1207 .model("claude-sonnet-4-20250514")
1208 .maxTokens(1024)
1209 .messages(messages)
1210 .tools(tools)
1211 .build(),
1212 )
1213
1214 val finalText = mutableListOf<String>()
1215 response.content().forEach { content ->
1216 when {
1217 content.isText() -> finalText.add(content.text().get().text())
1218
1219 content.isToolUse() -> {
1220 val toolName = content.toolUse().get().name()
1221 val toolArgs =
1222 content.toolUse().get()._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
1223
1224 val result = mcp.callTool(
1225 name = toolName,
1226 arguments = toolArgs ?: emptyMap(),
1227 )
1228 finalText.add("[Calling tool $toolName with args $toolArgs]")
1229
1230 messages.add(
1231 MessageParam.builder()
1232 .role(MessageParam.Role.USER)
1233 .content(
1234 result.content
1235 .filterIsInstance<TextContent>()
1236 .joinToString("\n") { it.text }
1237 )
1238 .build(),
1239 )
1240
1241 val aiResponse = anthropic.messages().create(
1242 MessageCreateParams.builder()
1243 .model("claude-sonnet-4-20250514")
1244 .maxTokens(1024)
1245 .messages(messages)
1246 .build(),
1247 )
1248
1249 finalText.add(aiResponse.content().first().text().get().text())
1250 }
1251 }
1252 }
1253
1254 return finalText.joinToString("\n")
12551167 }
1256 ```
1257
1258 ### Interactive chat
1259
1260 We'll add the chat loop:
1261
1262 ```kotlin theme={null}
1263 suspend fun chatLoop() {
1264 println("\nMCP Client Started!")
1265 println("Type your queries or 'quit' to exit.")
1266
1267 while (true) {
1268 print("\nQuery: ")
1269 val message = readlnOrNull() ?: break
1270 if (message.trim().lowercase() == "quit") break
1271
1272 try {
1273 val response = processQuery(message)
1274 println("\n$response")
1275 } catch (e: Exception) {
1276 println("\nError: ${e.message}")
1168 println("Connected to server with tools: ${tools.joinToString(", ") { it.tool().get().name() }}")
1169}
1170```
1171
1172> **JsonObject.toJsonValue() helper**
1173>
1174> This helper converts a kotlinx.serialization `JsonObject` to an Anthropic SDK `JsonValue` using Jackson:
1175>
1176> ```kotlin theme={null}
1177> private fun JsonObject.toJsonValue(): JsonValue {
1178> val mapper = ObjectMapper()
1179> val node = mapper.readTree(this.toString())
1180> return JsonValue.fromJsonNode(node)
1181> }
1182> ```
1183
1184### Query processing logic
1185
1186Now let's add the core functionality for processing queries and handling tool calls:
1187
1188```kotlin theme={null}
1189suspend fun processQuery(query: String): String {
1190 val messages = mutableListOf(
1191 MessageParam.builder()
1192 .role(MessageParam.Role.USER)
1193 .content(query)
1194 .build(),
1195 )
1196
1197 val response = anthropic.messages().create(
1198 MessageCreateParams.builder()
1199 .model("claude-sonnet-4-20250514")
1200 .maxTokens(1024)
1201 .messages(messages)
1202 .tools(tools)
1203 .build(),
1204 )
1205
1206 val finalText = mutableListOf<String>()
1207 response.content().forEach { content ->
1208 when {
1209 content.isText() -> finalText.add(content.text().get().text())
1210
1211 content.isToolUse() -> {
1212 val toolName = content.toolUse().get().name()
1213 val toolArgs =
1214 content.toolUse().get()._input().convert(object : TypeReference<Map<String, JsonValue>>() {})
1215
1216 val result = mcp.callTool(
1217 name = toolName,
1218 arguments = toolArgs ?: emptyMap(),
1219 )
1220 finalText.add("[Calling tool $toolName with args $toolArgs]")
1221
1222 messages.add(
1223 MessageParam.builder()
1224 .role(MessageParam.Role.USER)
1225 .content(
1226 result.content
1227 .filterIsInstance<TextContent>()
1228 .joinToString("\n") { it.text }
1229 )
1230 .build(),
1231 )
1232
1233 val aiResponse = anthropic.messages().create(
1234 MessageCreateParams.builder()
1235 .model("claude-sonnet-4-20250514")
1236 .maxTokens(1024)
1237 .messages(messages)
1238 .build(),
1239 )
1240
1241 finalText.add(aiResponse.content().first().text().get().text())
12771242 }
12781243 }
12791244 }
1280 ```
1281
1282 ### Main entry point
1283
1284 Finally, we'll add the main execution function:
1285
1286 ```kotlin theme={null}
1287 fun main(args: Array<String>) = runBlocking {
1288 require(args.isNotEmpty()) { "Usage: java -jar <path> <path_to_server_script>" }
1289
1290 val apiKey = System.getenv("ANTHROPIC_API_KEY")
1291 require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" }
1292
1293 val client = MCPClient(apiKey)
1294 client.use {
1295 client.connectToServer(args.first())
1296 client.chatLoop()
1245
1246 return finalText.joinToString("\n")
1247}
1248```
1249
1250### Interactive chat
1251
1252We'll add the chat loop:
1253
1254```kotlin theme={null}
1255suspend fun chatLoop() {
1256 println("\nMCP Client Started!")
1257 println("Type your queries or 'quit' to exit.")
1258
1259 while (true) {
1260 print("\nQuery: ")
1261 val message = readlnOrNull() ?: break
1262 if (message.trim().lowercase() == "quit") break
1263
1264 try {
1265 val response = processQuery(message)
1266 println("\n$response")
1267 } catch (e: Exception) {
1268 println("\nError: ${e.message}")
12971269 }
12981270 }
1299 ```
1300
1301 ## Running the client
1302
1303 To run your client with any MCP server:
1304
1305 ```bash theme={null}
1306 ./gradlew build
1307
1308 # Run the client
1309 java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.jar # JVM server
1310 java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.py # Python server
1311 java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/build/index.js # Node server
1312 ```
1313
1314 Alternatively, you can run directly with Gradle:
1315
1316 ```bash theme={null}
1317 ./gradlew run --args="path/to/server.jar"
1318 ```
1319
1320 <Note>
1321 If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar .../samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar`
1322 </Note>
1323
1324 **The client will:**
1325
1326 1. Connect to the specified server
1327 2. List available tools
1328 3. Start an interactive chat session where you can:
1329 * Enter queries
1330 * See tool executions
1331 * Get responses from Claude
1332
1333 ## How it works
1334
1335 Here's a high-level workflow schema:
1336
1337 ```mermaid theme={null}
1338 ---
1339 config:
1340 theme: neutral
1341 ---
1342 sequenceDiagram
1343 actor User
1344 participant Client
1345 participant Claude
1346 participant MCP_Server as MCP Server
1347 participant Tools
1348
1349 User->>Client: Send query
1350 Client<<->>MCP_Server: Get available tools
1351 Client->>Claude: Send query with tool descriptions
1352 Claude-->>Client: Decide tool execution
1353 Client->>MCP_Server: Request tool execution
1354 MCP_Server->>Tools: Execute chosen tools
1355 Tools-->>MCP_Server: Return results
1356 MCP_Server-->>Client: Send results
1357 Client->>Claude: Send tool results
1358 Claude-->>Client: Provide final response
1359 Client-->>User: Display response
1360 ```
1361
1362 When you submit a query:
1363
1364 1. The client gets the list of available tools from the server
1365 2. Your query is sent to Claude along with tool descriptions
1366 3. Claude decides which tools (if any) to use
1367 4. The client executes any requested tool calls through the server
1368 5. Results are sent back to Claude
1369 6. Claude provides a natural language response
1370 7. The response is displayed to you
1371
1372 ## Best practices
1373
1374 1. **Error Handling**
1375 * Leverage Kotlin's type system to model errors explicitly
1376 * Wrap external tool and API calls in `try-catch` blocks when exceptions are possible
1377 * Provide clear and meaningful error messages
1378 * Handle network timeouts and connection issues gracefully
1379
1380 2. **Security**
1381 * Store API keys and secrets securely in `local.properties`, environment variables, or secret managers
1382 * Validate all external responses to avoid unexpected or unsafe data usage
1383 * Be cautious with permissions and trust boundaries when using tools
1384
1385 3. **Environment**
1386 * Set `ANTHROPIC_API_KEY` through environment variables rather than hardcoding
1387 * Use `.env` files with appropriate `.gitignore` rules for local development
1388
1389 ## Troubleshooting
1390
1391 ### Server Path Issues
1392
1393 * Double-check the path to your server script is correct
1394 * Use the absolute path if the relative path isn't working
1395 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1396 * Make sure that the required runtime is installed (java for Java, npm for Node.js, or uv for Python)
1397 * Verify the server file has the correct extension (.jar for Java, .js for Node.js or .py for Python)
1398
1399 Example of correct path usage:
1400
1401 ```bash theme={null}
1402 # Relative path
1403 java -jar build/libs/client.jar ./server/build/libs/server.jar
1404
1405 # Absolute path
1406 java -jar build/libs/client.jar /Users/username/projects/mcp-server/build/libs/server.jar
1407
1408 # Windows path (either format works)
1409 java -jar build/libs/client.jar C:/projects/mcp-server/build/libs/server.jar
1410 java -jar build/libs/client.jar C:\\projects\\mcp-server\\build\\libs\\server.jar
1411 ```
1412
1413 ### Build Issues
1414
1415 * Use `./gradlew build` or `./gradlew shadowJar` (not `./gradlew jar`) to create the shadow JAR with all dependencies
1416 * If you get JDK version errors, ensure your installed JDK version matches or exceeds the `jvmToolchain` setting in `build.gradle.kts`
1417
1418 ### Response Timing
1419
1420 * The first response might take up to 30 seconds to return
1421 * This is normal and happens while:
1422 * The server initializes
1423 * Claude processes the query
1424 * Tools are being executed
1425 * Subsequent responses are typically faster
1426 * Don't interrupt the process during this initial waiting period
1427
1428 ### Common Error Messages
1429
1430 If you see:
1431
1432 * `Connection refused`: Ensure the server is running and the path is correct
1433 * `Tool execution failed`: Verify the tool's required environment variables are set
1434 * `ANTHROPIC_API_KEY is not set`: Check your environment variables
1435 </Tab>
1436
1437 <Tab title="C#">
1438 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartClient)
1439
1440 ## System Requirements
1441
1442 Before starting, ensure your system meets these requirements:
1443
1444 * .NET 8.0 or higher
1445 * Anthropic API key (Claude)
1446 * Windows, Linux, or macOS
1447
1448 ## Setting up your environment
1449
1450 First, create a new .NET project:
1451
1452 ```bash theme={null}
1453 dotnet new console -n QuickstartClient
1454 cd QuickstartClient
1455 ```
1456
1457 Then, add the required dependencies to your project:
1458
1459 ```bash theme={null}
1460 dotnet add package ModelContextProtocol --prerelease
1461 dotnet add package Anthropic.SDK
1462 dotnet add package Microsoft.Extensions.Hosting
1463 dotnet add package Microsoft.Extensions.AI
1464 ```
1465
1466 ## Setting up your API key
1467
1468 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1469
1470 ```bash theme={null}
1471 dotnet user-secrets init
1472 dotnet user-secrets set "ANTHROPIC_API_KEY" "<your key here>"
1473 ```
1474
1475 ## Creating the Client
1476
1477 ### Basic Client Structure
1478
1479 First, let's setup the basic client class in the file `Program.cs`:
1480
1481 ```csharp theme={null}
1482 using Anthropic.SDK;
1483 using Microsoft.Extensions.AI;
1484 using Microsoft.Extensions.Configuration;
1485 using Microsoft.Extensions.Hosting;
1486 using ModelContextProtocol.Client;
1487 using ModelContextProtocol.Protocol.Transport;
1488
1489 var builder = Host.CreateApplicationBuilder(args);
1490
1491 builder.Configuration
1492 .AddEnvironmentVariables()
1493 .AddUserSecrets<Program>();
1494 ```
1495
1496 This creates the beginnings of a .NET console application that can read the API key from user secrets.
1497
1498 Next, we'll setup the MCP Client:
1499
1500 ```csharp theme={null}
1501 var (command, arguments) = GetCommandAndArguments(args);
1502
1503 var clientTransport = new StdioClientTransport(new()
1271}
1272```
1273
1274### Main entry point
1275
1276Finally, we'll add the main execution function:
1277
1278```kotlin theme={null}
1279fun main(args: Array<String>) = runBlocking {
1280 require(args.isNotEmpty()) { "Usage: java -jar <path> <path_to_server_script>" }
1281
1282 val apiKey = System.getenv("ANTHROPIC_API_KEY")
1283 require(!apiKey.isNullOrBlank()) { "ANTHROPIC_API_KEY environment variable is not set" }
1284
1285 val client = MCPClient(apiKey)
1286 client.use {
1287 client.connectToServer(args.first())
1288 client.chatLoop()
1289 }
1290}
1291```
1292
1293## Running the client
1294
1295To run your client with any MCP server:
1296
1297```bash theme={null}
1298./gradlew build
1299
1300# Run the client
1301java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.jar # JVM server
1302java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/server.py # Python server
1303java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar path/to/build/index.js # Node server
1304```
1305
1306Alternatively, you can run directly with Gradle:
1307
1308```bash theme={null}
1309./gradlew run --args="path/to/server.jar"
1310```
1311
1312> [!NOTE]
1313>
1314> If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `java -jar build/libs/kotlin-mcp-client-0.1.0-all.jar .../samples/weather-stdio-server/build/libs/weather-stdio-server-0.1.0-all.jar`
1315
1316**The client will:**
1317
13181. Connect to the specified server
13192. List available tools
13203. Start an interactive chat session where you can:
1321 * Enter queries
1322 * See tool executions
1323 * Get responses from Claude
1324
1325## How it works
1326
1327Here's a high-level workflow schema:
1328
1329```mermaid theme={null}
1330---
1331config:
1332 theme: neutral
1333---
1334sequenceDiagram
1335 actor User
1336 participant Client
1337 participant Claude
1338 participant MCP_Server as MCP Server
1339 participant Tools
1340
1341 User->>Client: Send query
1342 Client<<->>MCP_Server: Get available tools
1343 Client->>Claude: Send query with tool descriptions
1344 Claude-->>Client: Decide tool execution
1345 Client->>MCP_Server: Request tool execution
1346 MCP_Server->>Tools: Execute chosen tools
1347 Tools-->>MCP_Server: Return results
1348 MCP_Server-->>Client: Send results
1349 Client->>Claude: Send tool results
1350 Claude-->>Client: Provide final response
1351 Client-->>User: Display response
1352```
1353
1354When you submit a query:
1355
13561. The client gets the list of available tools from the server
13572. Your query is sent to Claude along with tool descriptions
13583. Claude decides which tools (if any) to use
13594. The client executes any requested tool calls through the server
13605. Results are sent back to Claude
13616. Claude provides a natural language response
13627. The response is displayed to you
1363
1364## Best practices
1365
13661. **Error Handling**
1367 * Leverage Kotlin's type system to model errors explicitly
1368 * Wrap external tool and API calls in `try-catch` blocks when exceptions are possible
1369 * Provide clear and meaningful error messages
1370 * Handle network timeouts and connection issues gracefully
1371
13722. **Security**
1373 * Store API keys and secrets securely in `local.properties`, environment variables, or secret managers
1374 * Validate all external responses to avoid unexpected or unsafe data usage
1375 * Be cautious with permissions and trust boundaries when using tools
1376
13773. **Environment**
1378 * Set `ANTHROPIC_API_KEY` through environment variables rather than hardcoding
1379 * Use `.env` files with appropriate `.gitignore` rules for local development
1380
1381## Troubleshooting
1382
1383### Server Path Issues
1384
1385* Double-check the path to your server script is correct
1386* Use the absolute path if the relative path isn't working
1387* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1388* Make sure that the required runtime is installed (java for Java, npm for Node.js, or uv for Python)
1389* Verify the server file has the correct extension (.jar for Java, .js for Node.js or .py for Python)
1390
1391Example of correct path usage:
1392
1393```bash theme={null}
1394# Relative path
1395java -jar build/libs/client.jar ./server/build/libs/server.jar
1396
1397# Absolute path
1398java -jar build/libs/client.jar /Users/username/projects/mcp-server/build/libs/server.jar
1399
1400# Windows path (either format works)
1401java -jar build/libs/client.jar C:/projects/mcp-server/build/libs/server.jar
1402java -jar build/libs/client.jar C:\\projects\\mcp-server\\build\\libs\\server.jar
1403```
1404
1405### Build Issues
1406
1407* Use `./gradlew build` or `./gradlew shadowJar` (not `./gradlew jar`) to create the shadow JAR with all dependencies
1408* If you get JDK version errors, ensure your installed JDK version matches or exceeds the `jvmToolchain` setting in `build.gradle.kts`
1409
1410### Response Timing
1411
1412* The first response might take up to 30 seconds to return
1413* This is normal and happens while:
1414 * The server initializes
1415 * Claude processes the query
1416 * Tools are being executed
1417* Subsequent responses are typically faster
1418* Don't interrupt the process during this initial waiting period
1419
1420### Common Error Messages
1421
1422If you see:
1423
1424* `Connection refused`: Ensure the server is running and the path is correct
1425* `Tool execution failed`: Verify the tool's required environment variables are set
1426* `ANTHROPIC_API_KEY is not set`: Check your environment variables
1427
1428**C#**
1429
1430[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartClient)
1431
1432## System Requirements
1433
1434Before starting, ensure your system meets these requirements:
1435
1436* .NET 8.0 or higher
1437* Anthropic API key (Claude)
1438* Windows, Linux, or macOS
1439
1440## Setting up your environment
1441
1442First, create a new .NET project:
1443
1444```bash theme={null}
1445dotnet new console -n QuickstartClient
1446cd QuickstartClient
1447```
1448
1449Then, add the required dependencies to your project:
1450
1451```bash theme={null}
1452dotnet add package ModelContextProtocol --prerelease
1453dotnet add package Anthropic.SDK
1454dotnet add package Microsoft.Extensions.Hosting
1455dotnet add package Microsoft.Extensions.AI
1456```
1457
1458## Setting up your API key
1459
1460You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1461
1462```bash theme={null}
1463dotnet user-secrets init
1464dotnet user-secrets set "ANTHROPIC_API_KEY" "<your key here>"
1465```
1466
1467## Creating the Client
1468
1469### Basic Client Structure
1470
1471First, let's setup the basic client class in the file `Program.cs`:
1472
1473```csharp theme={null}
1474using Anthropic.SDK;
1475using Microsoft.Extensions.AI;
1476using Microsoft.Extensions.Configuration;
1477using Microsoft.Extensions.Hosting;
1478using ModelContextProtocol.Client;
1479using ModelContextProtocol.Protocol.Transport;
1480
1481var builder = Host.CreateApplicationBuilder(args);
1482
1483builder.Configuration
1484 .AddEnvironmentVariables()
1485 .AddUserSecrets<Program>();
1486```
1487
1488This creates the beginnings of a .NET console application that can read the API key from user secrets.
1489
1490Next, we'll setup the MCP Client:
1491
1492```csharp theme={null}
1493var (command, arguments) = GetCommandAndArguments(args);
1494
1495var clientTransport = new StdioClientTransport(new()
1496{
1497 Name = "Demo Server",
1498 Command = command,
1499 Arguments = arguments,
1500});
1501
1502await using var mcpClient = await McpClient.CreateAsync(clientTransport);
1503
1504var tools = await mcpClient.ListToolsAsync();
1505foreach (var tool in tools)
1506{
1507 Console.WriteLine($"Connected to server with tools: {tool.Name}");
1508}
1509```
1510
1511Add this function at the end of the `Program.cs` file:
1512
1513```csharp theme={null}
1514static (string command, string[] arguments) GetCommandAndArguments(string[] args)
1515{
1516 return args switch
15041517 {
1505 Name = "Demo Server",
1506 Command = command,
1507 Arguments = arguments,
1508 });
1509
1510 await using var mcpClient = await McpClient.CreateAsync(clientTransport);
1511
1512 var tools = await mcpClient.ListToolsAsync();
1513 foreach (var tool in tools)
1518 [var script] when script.EndsWith(".py") => ("python", args),
1519 [var script] when script.EndsWith(".js") => ("node", args),
1520 [var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", ["run", "--project", script, "--no-build"]),
1521 _ => throw new NotSupportedException("An unsupported server script was provided. Supported scripts are .py, .js, or .csproj")
1522 };
1523}
1524```
1525
1526This creates an MCP client that will connect to a server that is provided as a command line argument. It then lists the available tools from the connected server.
1527
1528### Query processing logic
1529
1530Now let's add the core functionality for processing queries and handling tool calls:
1531
1532```csharp theme={null}
1533using var anthropicClient = new AnthropicClient(new APIAuthentication(builder.Configuration["ANTHROPIC_API_KEY"]))
1534 .Messages
1535 .AsBuilder()
1536 .UseFunctionInvocation()
1537 .Build();
1538
1539var options = new ChatOptions
1540{
1541 MaxOutputTokens = 1000,
1542 ModelId = "claude-sonnet-4-20250514",
1543 Tools = [.. tools]
1544};
1545
1546Console.ForegroundColor = ConsoleColor.Green;
1547Console.WriteLine("MCP Client Started!");
1548Console.ResetColor();
1549
1550PromptForInput();
1551while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
1552{
1553 if (string.IsNullOrWhiteSpace(query))
15141554 {
1515 Console.WriteLine($"Connected to server with tools: {tool.Name}");
1555 PromptForInput();
1556 continue;
15161557 }
1517 ```
1518
1519 Add this function at the end of the `Program.cs` file:
1520
1521 ```csharp theme={null}
1522 static (string command, string[] arguments) GetCommandAndArguments(string[] args)
1558
1559 await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options))
15231560 {
1524 return args switch
1525 {
1526 [var script] when script.EndsWith(".py") => ("python", args),
1527 [var script] when script.EndsWith(".js") => ("node", args),
1528 [var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", ["run", "--project", script, "--no-build"]),
1529 _ => throw new NotSupportedException("An unsupported server script was provided. Supported scripts are .py, .js, or .csproj")
1530 };
1561 Console.Write(message);
15311562 }
1532 ```
1533
1534 This creates an MCP client that will connect to a server that is provided as a command line argument. It then lists the available tools from the connected server.
1535
1536 ### Query processing logic
1537
1538 Now let's add the core functionality for processing queries and handling tool calls:
1539
1540 ```csharp theme={null}
1541 using var anthropicClient = new AnthropicClient(new APIAuthentication(builder.Configuration["ANTHROPIC_API_KEY"]))
1542 .Messages
1543 .AsBuilder()
1544 .UseFunctionInvocation()
1545 .Build();
1546
1547 var options = new ChatOptions
1548 {
1549 MaxOutputTokens = 1000,
1550 ModelId = "claude-sonnet-4-20250514",
1551 Tools = [.. tools]
1552 };
1553
1554 Console.ForegroundColor = ConsoleColor.Green;
1555 Console.WriteLine("MCP Client Started!");
1563 Console.WriteLine();
1564
1565 PromptForInput();
1566}
1567
1568static void PromptForInput()
1569{
1570 Console.WriteLine("Enter a command (or 'exit' to quit):");
1571 Console.ForegroundColor = ConsoleColor.Cyan;
1572 Console.Write("> ");
15561573 Console.ResetColor();
1557
1558 PromptForInput();
1559 while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
1560 {
1561 if (string.IsNullOrWhiteSpace(query))
1562 {
1563 PromptForInput();
1564 continue;
1565 }
1566
1567 await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options))
1568 {
1569 Console.Write(message);
1570 }
1571 Console.WriteLine();
1572
1573 PromptForInput();
1574 }
1575
1576 static void PromptForInput()
1577 {
1578 Console.WriteLine("Enter a command (or 'exit' to quit):");
1579 Console.ForegroundColor = ConsoleColor.Cyan;
1580 Console.Write("> ");
1581 Console.ResetColor();
1582 }
1583 ```
1584
1585 ## Key Components Explained
1586
1587 ### 1. Client Initialization
1588
1589 * The client is initialized using `McpClient.CreateAsync()`, which sets up the transport type and command to run the server.
1590
1591 ### 2. Server Connection
1592
1593 * Supports Python, Node.js, and .NET servers.
1594 * The server is started using the command specified in the arguments.
1595 * Configures to use stdio for communication with the server.
1596 * Initializes the session and available tools.
1597
1598 ### 3. Query Processing
1599
1600 * Leverages [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/ai-extensions) for the chat client.
1601 * Configures the `IChatClient` to use automatic tool (function) invocation.
1602 * The client reads user input and sends it to the server.
1603 * The server processes the query and returns a response.
1604 * The response is displayed to the user.
1605
1606 ## Running the Client
1607
1608 To run your client with any MCP server:
1609
1610 ```bash theme={null}
1611 dotnet run -- path/to/server.csproj # dotnet server
1612 dotnet run -- path/to/server.py # python server
1613 dotnet run -- path/to/server.js # node server
1614 ```
1615
1616 <Note>
1617 If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `dotnet run -- path/to/QuickstartWeatherServer`.
1618 </Note>
1619
1620 The client will:
1621
1622 1. Connect to the specified server
1623 2. List available tools
1624 3. Start an interactive chat session where you can:
1625 * Enter queries
1626 * See tool executions
1627 * Get responses from Claude
1628 4. Exit the session when done
1629
1630 Here's an example of what it should look like if connected to the weather server quickstart:
1631
1632 <Frame>
1633 <img src="https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/quickstart-dotnet-client.png?fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=fcf28dde150d6db879402ad8150c6b23" width="1115" height="666" data-path="images/quickstart-dotnet-client.png" />
1634 </Frame>
1635 </Tab>
1636
1637 <Tab title="Ruby">
1638 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-ruby)
1639
1640 ## System Requirements
1641
1642 Before starting, ensure your system meets these requirements:
1643
1644 * Mac or Windows computer
1645 * Ruby 3.2.0 or higher installed (required by the [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-ruby))
1646 * Anthropic API key (Claude)
1647
1648 ## Setting Up Your Environment
1649
1650 First, create a new Ruby project:
1651
1652 <CodeGroup>
1653 ```bash macOS/Linux theme={null}
1654 # Create project directory
1655 mkdir mcp-client
1656 cd mcp-client
1657
1658 # Create a Gemfile
1659 bundle init
1660
1661 # Add required dependencies
1662 bundle add anthropic base64 dotenv mcp
1663
1664 # Create our main file
1665 touch client.rb
1666 ```
1667
1668 ```powershell Windows theme={null}
1669 # Create project directory
1670 mkdir mcp-client
1671 cd mcp-client
1672
1673 # Create a Gemfile
1674 bundle init
1675
1676 # Add required dependencies
1677 bundle add anthropic base64 dotenv mcp
1678
1679 # Create our main file
1680 new-item client.rb
1681 ```
1682 </CodeGroup>
1683
1684 ## Setting Up Your API Key
1685
1686 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1687
1688 Create a `.env` file to store it:
1689
1690 ```bash theme={null}
1691 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
1692 ```
1693
1694 Add `.env` to your `.gitignore`:
1695
1696 ```bash theme={null}
1697 echo ".env" >> .gitignore
1698 ```
1699
1700 <Warning>
1701 Make sure you keep your `ANTHROPIC_API_KEY` secure!
1702 </Warning>
1703
1704 ## Creating the Client
1705
1706 ### Basic Client Structure
1707
1708 First, let's set up our requires and create the basic client class:
1709
1710 ```ruby theme={null}
1711 require "anthropic"
1712 require "dotenv/load"
1713 require "json"
1714 require "mcp"
1715
1716 class MCPClient
1717 ANTHROPIC_MODEL = "claude-sonnet-4-20250514"
1718
1719 def initialize
1720 @mcp_client = nil
1721 @transport = nil
1722 @anthropic_client = nil
1723 end
1724
1725 # methods will go here
1726 end
1727 ```
1728
1729 ### Server Connection Management
1730
1731 Next, we'll implement the method to connect to an MCP server:
1732
1733 ```ruby theme={null}
1734 def connect_to_server(server_script_path)
1735 command = case File.extname(server_script_path)
1736 when ".rb"
1737 "ruby"
1738 when ".py"
1739 "python3"
1740 when ".js"
1741 "node"
1742 else
1743 raise ArgumentError, "Server script must be a .rb, .py, or .js file."
1744 end
1745
1746 @transport = MCP::Client::Stdio.new(command: command, args: [server_script_path])
1747 @mcp_client = MCP::Client.new(transport: @transport)
1748 @mcp_client.connect
1749
1750 tool_names = @mcp_client.tools.map(&:name)
1751 puts "\nConnected to server with tools: #{tool_names}"
1752 end
1753 ```
1754
1755 ### Query Processing Logic
1756
1757 Now let's add the core functionality for processing queries and handling tool calls:
1758
1759 ```ruby theme={null}
1760 private
1761
1762 def process_query(query)
1763 messages = [{ role: "user", content: query }]
1764
1765 available_tools = @mcp_client.tools.map do |tool|
1766 { name: tool.name, description: tool.description, input_schema: tool.input_schema }
1767 end
1768
1769 # Initial Claude API call.
1770 response = chat(messages, tools: available_tools)
1771
1772 # Process response and handle tool calls.
1773 if response.content.any?(Anthropic::Models::ToolUseBlock)
1774 assistant_content = response.content.filter_map do |content_block|
1775 case content_block
1776 when Anthropic::Models::TextBlock
1777 { type: "text", text: content_block.text }
1778 when Anthropic::Models::ToolUseBlock
1779 { type: "tool_use", id: content_block.id, name: content_block.name, input: content_block.input }
1780 end
1781 end
1782 messages << { role: "assistant", content: assistant_content }
1783 end
1784
1785 response.content.each_with_object([]) do |content, response_parts|
1786 case content
1787 when Anthropic::Models::TextBlock
1788 response_parts << content.text
1789 when Anthropic::Models::ToolUseBlock
1790 # Execute tool call via MCP.
1791 result = @mcp_client.call_tool(name: content.name, arguments: content.input)
1792 response_parts << "[Calling tool #{content.name} with args #{content.input.to_json}]"
1793
1794 tool_result_content = result.dig("result", "content")
1795 result_text = if tool_result_content.is_a?(Array)
1796 tool_result_content.filter_map { |content_item| content_item["text"] }.join("\n")
1797 else
1798 tool_result_content.to_s
1799 end
1800
1801 messages << {
1802 role: "user",
1803 content: [{
1804 type: "tool_result",
1805 tool_use_id: content.id,
1806 content: result_text
1807 }]
1808 }
1809
1810 # Get next response from Claude.
1811 response = chat(messages)
1812
1813 response.content.each do |content_block|
1814 response_parts << content_block.text if content_block.is_a?(Anthropic::Models::TextBlock)
1815 end
1816 end
1817 end.join("\n")
1818 end
1819
1820 def chat(messages, tools: nil)
1821 params = { model: ANTHROPIC_MODEL, max_tokens: 1000, messages: messages }
1822 params[:tools] = tools if tools
1823
1824 anthropic_client.messages.create(**params)
1825 end
1826
1827 def anthropic_client
1828 @anthropic_client ||= Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"])
1829 end
1830 ```
1831
1832 ### Interactive Chat Interface
1833
1834 Now we'll add the chat loop and cleanup functionality:
1835
1836 ```ruby theme={null}
1837 def chat_loop
1838 puts <<~MESSAGE
1839 MCP Client Started!
1840 Type your queries or 'quit' to exit.
1841 MESSAGE
1842
1843 loop do
1844 print "\nQuery: "
1845 line = $stdin.gets
1846 break if line.nil?
1847
1848 query = line.chomp.strip
1849 break if query.downcase == "quit"
1850 next if query.empty?
1851
1852 begin
1853 response = process_query(query)
1854 puts "\n#{response}"
1855 rescue => e
1856 puts "\nError: #{e.message}"
1857 end
1574}
1575```
1576
1577## Key Components Explained
1578
1579### 1. Client Initialization
1580
1581* The client is initialized using `McpClient.CreateAsync()`, which sets up the transport type and command to run the server.
1582
1583### 2. Server Connection
1584
1585* Supports Python, Node.js, and .NET servers.
1586* The server is started using the command specified in the arguments.
1587* Configures to use stdio for communication with the server.
1588* Initializes the session and available tools.
1589
1590### 3. Query Processing
1591
1592* Leverages [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/ai-extensions) for the chat client.
1593* Configures the `IChatClient` to use automatic tool (function) invocation.
1594* The client reads user input and sends it to the server.
1595* The server processes the query and returns a response.
1596* The response is displayed to the user.
1597
1598## Running the Client
1599
1600To run your client with any MCP server:
1601
1602```bash theme={null}
1603dotnet run -- path/to/server.csproj # dotnet server
1604dotnet run -- path/to/server.py # python server
1605dotnet run -- path/to/server.js # node server
1606```
1607
1608> [!NOTE]
1609>
1610> If you're continuing the weather tutorial from the server quickstart, your command might look something like this: `dotnet run -- path/to/QuickstartWeatherServer`.
1611
1612The client will:
1613
16141. Connect to the specified server
16152. List available tools
16163. Start an interactive chat session where you can:
1617 * Enter queries
1618 * See tool executions
1619 * Get responses from Claude
16204. Exit the session when done
1621
1622Here's an example of what it should look like if connected to the weather server quickstart:
1623
1624
1625
1626**Ruby**
1627
1628[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-ruby)
1629
1630## System Requirements
1631
1632Before starting, ensure your system meets these requirements:
1633
1634* Mac or Windows computer
1635* Ruby 3.2.0 or higher installed (required by the [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-ruby))
1636* Anthropic API key (Claude)
1637
1638## Setting Up Your Environment
1639
1640First, create a new Ruby project:
1641
1642```bash macOS/Linux theme={null}
1643# Create project directory
1644mkdir mcp-client
1645cd mcp-client
1646
1647# Create a Gemfile
1648bundle init
1649
1650# Add required dependencies
1651bundle add anthropic base64 dotenv mcp
1652
1653# Create our main file
1654touch client.rb
1655```
1656
1657```powershell Windows theme={null}
1658# Create project directory
1659mkdir mcp-client
1660cd mcp-client
1661
1662# Create a Gemfile
1663bundle init
1664
1665# Add required dependencies
1666bundle add anthropic base64 dotenv mcp
1667
1668# Create our main file
1669new-item client.rb
1670```
1671
1672## Setting Up Your API Key
1673
1674You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
1675
1676Create a `.env` file to store it:
1677
1678```bash theme={null}
1679echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
1680```
1681
1682Add `.env` to your `.gitignore`:
1683
1684```bash theme={null}
1685echo ".env" >> .gitignore
1686```
1687
1688> [!WARNING]
1689>
1690> Make sure you keep your `ANTHROPIC_API_KEY` secure!
1691
1692## Creating the Client
1693
1694### Basic Client Structure
1695
1696First, let's set up our requires and create the basic client class:
1697
1698```ruby theme={null}
1699require "anthropic"
1700require "dotenv/load"
1701require "json"
1702require "mcp"
1703
1704class MCPClient
1705 ANTHROPIC_MODEL = "claude-sonnet-4-20250514"
1706
1707 def initialize
1708 @mcp_client = nil
1709 @transport = nil
1710 @anthropic_client = nil
1711 end
1712
1713 # methods will go here
1714end
1715```
1716
1717### Server Connection Management
1718
1719Next, we'll implement the method to connect to an MCP server:
1720
1721```ruby theme={null}
1722def connect_to_server(server_script_path)
1723 command = case File.extname(server_script_path)
1724 when ".rb"
1725 "ruby"
1726 when ".py"
1727 "python3"
1728 when ".js"
1729 "node"
1730 else
1731 raise ArgumentError, "Server script must be a .rb, .py, or .js file."
1732 end
1733
1734 @transport = MCP::Client::Stdio.new(command: command, args: [server_script_path])
1735 @mcp_client = MCP::Client.new(transport: @transport)
1736 @mcp_client.connect
1737
1738 tool_names = @mcp_client.tools.map(&:name)
1739 puts "\nConnected to server with tools: #{tool_names}"
1740end
1741```
1742
1743### Query Processing Logic
1744
1745Now let's add the core functionality for processing queries and handling tool calls:
1746
1747```ruby theme={null}
1748private
1749
1750def process_query(query)
1751 messages = [{ role: "user", content: query }]
1752
1753 available_tools = @mcp_client.tools.map do |tool|
1754 { name: tool.name, description: tool.description, input_schema: tool.input_schema }
1755 end
1756
1757 # Initial Claude API call.
1758 response = chat(messages, tools: available_tools)
1759
1760 # Process response and handle tool calls.
1761 if response.content.any?(Anthropic::Models::ToolUseBlock)
1762 assistant_content = response.content.filter_map do |content_block|
1763 case content_block
1764 when Anthropic::Models::TextBlock
1765 { type: "text", text: content_block.text }
1766 when Anthropic::Models::ToolUseBlock
1767 { type: "tool_use", id: content_block.id, name: content_block.name, input: content_block.input }
18581768 end
18591769 end
1860
1861 def cleanup
1862 @transport&.close
1770 messages << { role: "assistant", content: assistant_content }
1771 end
1772
1773 response.content.each_with_object([]) do |content, response_parts|
1774 case content
1775 when Anthropic::Models::TextBlock
1776 response_parts << content.text
1777 when Anthropic::Models::ToolUseBlock
1778 # Execute tool call via MCP.
1779 result = @mcp_client.call_tool(name: content.name, arguments: content.input)
1780 response_parts << "[Calling tool #{content.name} with args #{content.input.to_json}]"
1781
1782 tool_result_content = result.dig("result", "content")
1783 result_text = if tool_result_content.is_a?(Array)
1784 tool_result_content.filter_map { |content_item| content_item["text"] }.join("\n")
1785 else
1786 tool_result_content.to_s
1787 end
1788
1789 messages << {
1790 role: "user",
1791 content: [{
1792 type: "tool_result",
1793 tool_use_id: content.id,
1794 content: result_text
1795 }]
1796 }
1797
1798 # Get next response from Claude.
1799 response = chat(messages)
1800
1801 response.content.each do |content_block|
1802 response_parts << content_block.text if content_block.is_a?(Anthropic::Models::TextBlock)
1803 end
18631804 end
1864 ```
1865
1866 ### Main Entry Point
1867
1868 Finally, we'll add the main execution logic:
1869
1870 ```ruby theme={null}
1871 if ARGV.empty?
1872 puts "Usage: ruby client.rb <path_to_server_script>"
1873 exit 1
1805 end.join("\n")
1806end
1807
1808def chat(messages, tools: nil)
1809 params = { model: ANTHROPIC_MODEL, max_tokens: 1000, messages: messages }
1810 params[:tools] = tools if tools
1811
1812 anthropic_client.messages.create(**params)
1813end
1814
1815def anthropic_client
1816 @anthropic_client ||= Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"])
1817end
1818```
1819
1820### Interactive Chat Interface
1821
1822Now we'll add the chat loop and cleanup functionality:
1823
1824```ruby theme={null}
1825def chat_loop
1826 puts <<~MESSAGE
1827 MCP Client Started!
1828 Type your queries or 'quit' to exit.
1829 MESSAGE
1830
1831 loop do
1832 print "\nQuery: "
1833 line = $stdin.gets
1834 break if line.nil?
1835
1836 query = line.chomp.strip
1837 break if query.downcase == "quit"
1838 next if query.empty?
1839
1840 begin
1841 response = process_query(query)
1842 puts "\n#{response}"
1843 rescue => e
1844 puts "\nError: #{e.message}"
18741845 end
1875
1876 client = MCPClient.new
1877
1878 begin
1879 client.connect_to_server(ARGV[0])
1880
1881 api_key = ENV["ANTHROPIC_API_KEY"]
1882 if api_key.nil? || api_key.empty?
1883 puts <<~MESSAGE
1884 No ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:
1885 export ANTHROPIC_API_KEY=your-api-key-here
1886 MESSAGE
1887 exit
1888 end
1889
1890 client.chat_loop
1891 rescue => e
1892 puts "Error: #{e.message}"
1893 exit 1
1894 ensure
1895 client.cleanup
1896 end
1897 ```
1898
1899 You can find the complete `client.rb` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-ruby/client.rb).
1900
1901 ## Key Components Explained
1902
1903 ### 1. Client Initialization
1904
1905 * The `MCPClient` class initializes with nil references for lazy setup
1906 * The Anthropic client is lazily initialized via the `anthropic_client` method
1907 * Uses `dotenv` to load environment variables from `.env`
1908
1909 ### 2. Server Connection
1910
1911 * Supports Ruby, Python, and Node.js servers
1912 * Uses `File.extname` to determine the server script type
1913 * Uses `MCP::Client::Stdio` for stdio transport
1914 * Initializes the MCP client and lists available tools
1915
1916 ### 3. Query Processing
1917
1918 * Maps MCP tools to Anthropic tool format (`name`, `description`, `input_schema`)
1919 * Uses `Anthropic::Models::TextBlock` and `Anthropic::Models::ToolUseBlock` for pattern matching
1920 * Builds assistant content once before iterating tool calls
1921 * Executes tool calls via `@mcp_client.call_tool`
1922 * Uses `chat` helper method to wrap Anthropic API calls
1923 * Extracts tool result content with `result.dig("result", "content")`
1924 * Passes tool results back to Claude for a final response
1925
1926 ### 4. Interactive Interface
1927
1928 * Provides a simple command-line interface
1929 * Handles user input and displays responses
1930 * Skips empty queries
1931 * Includes basic error handling
1932
1933 ### 5. Resource Management
1934
1935 * Proper cleanup of the transport via `begin`...`ensure`
1936 * Top-level `rescue` for error handling
1937 * API key validation after server connection
1938
1939 ## Running the Client
1940
1941 To run your client with any MCP server:
1942
1943 ```bash theme={null}
1944 bundle exec ruby client.rb path/to/server.rb # ruby server
1945 bundle exec ruby client.rb path/to/server.py # python server
1946 bundle exec ruby client.rb path/to/build/index.js # node server
1947 ```
1948
1949 <Note>
1950 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby), your command might look something like this: `bundle exec ruby client.rb /path/to/weather-server-ruby/weather.rb`
1951 </Note>
1952
1953 The client will:
1954
1955 1. Connect to the specified server
1956 2. List available tools
1957 3. Start an interactive chat session where you can:
1958 * Enter queries
1959 * See tool executions
1960 * Get responses from Claude
1961
1962 ## How It Works
1963
1964 When you submit a query:
1965
1966 1. The client gets the list of available tools from the server
1967 2. Your query is sent to Claude along with tool descriptions
1968 3. Claude decides which tools (if any) to use
1969 4. The client executes any requested tool calls through the server
1970 5. Results are sent back to Claude
1971 6. Claude provides a natural language response
1972 7. The response is displayed to you
1973
1974 ## Best practices
1975
1976 1. **Error Handling**
1977 * Wrap tool calls in `begin`...`rescue` blocks
1978 * Provide meaningful error messages
1979 * Gracefully handle connection issues
1980
1981 2. **Resource Management**
1982 * Always close the transport when done
1983 * Use `begin`...`ensure` for proper cleanup
1984 * Handle server disconnections
1985
1986 3. **Security**
1987 * Store API keys securely in `.env`
1988 * Validate server responses
1989 * Be cautious with tool permissions
1990
1991 ## Troubleshooting
1992
1993 ### Server Path Issues
1994
1995 * Double-check the path to your server script is correct
1996 * Use the absolute path if the relative path isn't working
1997 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1998 * Verify the server file has the correct extension (.py for Python, .js for Node.js, or .rb for Ruby)
1999
2000 Example of correct path usage:
2001
2002 ```bash theme={null}
2003 # Relative path
2004 bundle exec ruby client.rb ./server/weather.rb
2005
2006 # Absolute path
2007 bundle exec ruby client.rb /Users/username/projects/mcp-server/weather.rb
2008
2009 # Windows path (either format works)
2010 bundle exec ruby client.rb C:/projects/mcp-server/weather.rb
2011 bundle exec ruby client.rb C:\\projects\\mcp-server\\weather.rb
2012 ```
2013
2014 ### Response Timing
2015
2016 * The first response might take up to 30 seconds to return
2017 * This is normal and happens while:
2018 * The server initializes
2019 * Claude processes the query
2020 * Tools are being executed
2021 * Subsequent responses are typically faster
2022 * Don't interrupt the process during this initial waiting period
2023
2024 ### Common Error Messages
2025
2026 If you see:
2027
2028 * `Errno::ENOENT`: Check your server path and ensure the command (`ruby`, `python3`, `node`) is available
2029 * `Connection refused`: Ensure the server is running and the path is correct
2030 * `Tool execution failed`: Verify the tool's required environment variables are set
2031 * `Anthropic::Errors::AuthenticationError`: Check your `.env` file has a valid `ANTHROPIC_API_KEY`
2032 </Tab>
2033
2034 <Tab title="Rust">
2035 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-rust)
2036
2037 ## System Requirements
2038
2039 Before starting, ensure your Linux system meets these requirements:
2040
2041 * Latest stable version of [Rust and Cargo](https://www.rust-lang.org/tools/install)
2042 * Anthropic API key (Claude)
2043 * A Python, Node.js, or executable MCP server to connect to
2044
2045 ## Setting Up Your Environment
2046
2047 First, create a new Rust project:
2048
2049 ```bash theme={null}
2050 cargo new mcp-client-rust
2051 cd mcp-client-rust
2052 ```
2053
2054 Replace the contents of `Cargo.toml` with the following:
2055
2056 ```toml Cargo.toml theme={null}
2057 [package]
2058 name = "mcp-client-rust"
2059 version = "0.1.0"
2060 edition = "2024"
2061
2062 [dependencies]
2063 anyhow = "1.0.100"
2064 genai = "0.4.2"
2065 rmcp = { version = "0.8.0", features = ["server", "client", "transport-io", "transport-child-process"] }
2066 tokio = { version = "1.47.1", features = ["full"] }
2067 tracing = "0.1.41"
2068 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2069 serde_json = "1.0.128"
2070 dotenvy = "0.15.7"
2071 reqwest = "0.12.23"
2072 ```
2073
2074 The [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk) crate provides the Rust MCP SDK and child-process transport. This example uses the [`genai`](https://github.com/jeremychone/rust-genai) crate to send requests to Claude and represent tools in the model request.
2075
2076 ## Setting Up Your API Key
2077
2078 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
2079
2080 Create a `.env` file to store it:
2081
2082 ```bash theme={null}
2083 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
2084 ```
2085
2086 Add `.env` to your `.gitignore`:
2087
2088 ```bash theme={null}
2089 echo ".env" >> .gitignore
2090 ```
2091
2092 <Warning>
2093 Make sure you keep your `ANTHROPIC_API_KEY` secure!
2094 </Warning>
2095
2096 ## Creating the Client
2097
2098 Open `src/main.rs` and replace its contents as you work through the following sections.
2099
2100 ### Imports and Client Structure
2101
2102 First, add the imports, model constant, and basic client structure:
2103
2104 ```rust theme={null}
2105 use anyhow::{Context, Result, bail};
2106 use genai::Client;
2107 use genai::chat::{
2108 ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse,
2109 };
2110 use rmcp::model::{CallToolRequestParam, Tool as McpTool};
2111 use rmcp::service::{RoleClient, RunningService, ServiceExt};
2112 use rmcp::transport::TokioChildProcess;
2113 use serde_json::Value;
2114 use tokio::io::{self, AsyncBufReadExt, BufReader};
2115 use tokio::process::Command;
2116
2117 const MODEL_ANTHROPIC: &str = "claude-sonnet-4-20250514";
2118
2119 struct MCPClient {
2120 anthropic: Client,
2121 session: Option<RunningService<RoleClient, ()>>,
2122 tools: Vec<GenaiTool>,
1846 end
1847end
1848
1849def cleanup
1850 @transport&.close
1851end
1852```
1853
1854### Main Entry Point
1855
1856Finally, we'll add the main execution logic:
1857
1858```ruby theme={null}
1859if ARGV.empty?
1860 puts "Usage: ruby client.rb <path_to_server_script>"
1861 exit 1
1862end
1863
1864client = MCPClient.new
1865
1866begin
1867 client.connect_to_server(ARGV[0])
1868
1869 api_key = ENV["ANTHROPIC_API_KEY"]
1870 if api_key.nil? || api_key.empty?
1871 puts <<~MESSAGE
1872 No ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:
1873 export ANTHROPIC_API_KEY=your-api-key-here
1874 MESSAGE
1875 exit
1876 end
1877
1878 client.chat_loop
1879rescue => e
1880 puts "Error: #{e.message}"
1881 exit 1
1882ensure
1883 client.cleanup
1884end
1885```
1886
1887You can find the complete `client.rb` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-ruby/client.rb).
1888
1889## Key Components Explained
1890
1891### 1. Client Initialization
1892
1893* The `MCPClient` class initializes with nil references for lazy setup
1894* The Anthropic client is lazily initialized via the `anthropic_client` method
1895* Uses `dotenv` to load environment variables from `.env`
1896
1897### 2. Server Connection
1898
1899* Supports Ruby, Python, and Node.js servers
1900* Uses `File.extname` to determine the server script type
1901* Uses `MCP::Client::Stdio` for stdio transport
1902* Initializes the MCP client and lists available tools
1903
1904### 3. Query Processing
1905
1906* Maps MCP tools to Anthropic tool format (`name`, `description`, `input_schema`)
1907* Uses `Anthropic::Models::TextBlock` and `Anthropic::Models::ToolUseBlock` for pattern matching
1908* Builds assistant content once before iterating tool calls
1909* Executes tool calls via `@mcp_client.call_tool`
1910* Uses `chat` helper method to wrap Anthropic API calls
1911* Extracts tool result content with `result.dig("result", "content")`
1912* Passes tool results back to Claude for a final response
1913
1914### 4. Interactive Interface
1915
1916* Provides a simple command-line interface
1917* Handles user input and displays responses
1918* Skips empty queries
1919* Includes basic error handling
1920
1921### 5. Resource Management
1922
1923* Proper cleanup of the transport via `begin`...`ensure`
1924* Top-level `rescue` for error handling
1925* API key validation after server connection
1926
1927## Running the Client
1928
1929To run your client with any MCP server:
1930
1931```bash theme={null}
1932bundle exec ruby client.rb path/to/server.rb # ruby server
1933bundle exec ruby client.rb path/to/server.py # python server
1934bundle exec ruby client.rb path/to/build/index.js # node server
1935```
1936
1937> [!NOTE]
1938>
1939> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby), your command might look something like this: `bundle exec ruby client.rb /path/to/weather-server-ruby/weather.rb`
1940
1941The client will:
1942
19431. Connect to the specified server
19442. List available tools
19453. Start an interactive chat session where you can:
1946 * Enter queries
1947 * See tool executions
1948 * Get responses from Claude
1949
1950## How It Works
1951
1952When you submit a query:
1953
19541. The client gets the list of available tools from the server
19552. Your query is sent to Claude along with tool descriptions
19563. Claude decides which tools (if any) to use
19574. The client executes any requested tool calls through the server
19585. Results are sent back to Claude
19596. Claude provides a natural language response
19607. The response is displayed to you
1961
1962## Best practices
1963
19641. **Error Handling**
1965 * Wrap tool calls in `begin`...`rescue` blocks
1966 * Provide meaningful error messages
1967 * Gracefully handle connection issues
1968
19692. **Resource Management**
1970 * Always close the transport when done
1971 * Use `begin`...`ensure` for proper cleanup
1972 * Handle server disconnections
1973
19743. **Security**
1975 * Store API keys securely in `.env`
1976 * Validate server responses
1977 * Be cautious with tool permissions
1978
1979## Troubleshooting
1980
1981### Server Path Issues
1982
1983* Double-check the path to your server script is correct
1984* Use the absolute path if the relative path isn't working
1985* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
1986* Verify the server file has the correct extension (.py for Python, .js for Node.js, or .rb for Ruby)
1987
1988Example of correct path usage:
1989
1990```bash theme={null}
1991# Relative path
1992bundle exec ruby client.rb ./server/weather.rb
1993
1994# Absolute path
1995bundle exec ruby client.rb /Users/username/projects/mcp-server/weather.rb
1996
1997# Windows path (either format works)
1998bundle exec ruby client.rb C:/projects/mcp-server/weather.rb
1999bundle exec ruby client.rb C:\\projects\\mcp-server\\weather.rb
2000```
2001
2002### Response Timing
2003
2004* The first response might take up to 30 seconds to return
2005* This is normal and happens while:
2006 * The server initializes
2007 * Claude processes the query
2008 * Tools are being executed
2009* Subsequent responses are typically faster
2010* Don't interrupt the process during this initial waiting period
2011
2012### Common Error Messages
2013
2014If you see:
2015
2016* `Errno::ENOENT`: Check your server path and ensure the command (`ruby`, `python3`, `node`) is available
2017* `Connection refused`: Ensure the server is running and the path is correct
2018* `Tool execution failed`: Verify the tool's required environment variables are set
2019* `Anthropic::Errors::AuthenticationError`: Check your `.env` file has a valid `ANTHROPIC_API_KEY`
2020
2021**Rust**
2022
2023[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-rust)
2024
2025## System Requirements
2026
2027Before starting, ensure your Linux system meets these requirements:
2028
2029* Latest stable version of [Rust and Cargo](https://www.rust-lang.org/tools/install)
2030* Anthropic API key (Claude)
2031* A Python, Node.js, or executable MCP server to connect to
2032
2033## Setting Up Your Environment
2034
2035First, create a new Rust project:
2036
2037```bash theme={null}
2038cargo new mcp-client-rust
2039cd mcp-client-rust
2040```
2041
2042Replace the contents of `Cargo.toml` with the following:
2043
2044```toml Cargo.toml theme={null}
2045[package]
2046name = "mcp-client-rust"
2047version = "0.1.0"
2048edition = "2024"
2049
2050[dependencies]
2051anyhow = "1.0.100"
2052genai = "0.4.2"
2053rmcp = { version = "0.8.0", features = ["server", "client", "transport-io", "transport-child-process"] }
2054tokio = { version = "1.47.1", features = ["full"] }
2055tracing = "0.1.41"
2056tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2057serde_json = "1.0.128"
2058dotenvy = "0.15.7"
2059reqwest = "0.12.23"
2060```
2061
2062The [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk) crate provides the Rust MCP SDK and child-process transport. This example uses the [`genai`](https://github.com/jeremychone/rust-genai) crate to send requests to Claude and represent tools in the model request.
2063
2064## Setting Up Your API Key
2065
2066You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
2067
2068Create a `.env` file to store it:
2069
2070```bash theme={null}
2071echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
2072```
2073
2074Add `.env` to your `.gitignore`:
2075
2076```bash theme={null}
2077echo ".env" >> .gitignore
2078```
2079
2080> [!WARNING]
2081>
2082> Make sure you keep your `ANTHROPIC_API_KEY` secure!
2083
2084## Creating the Client
2085
2086Open `src/main.rs` and replace its contents as you work through the following sections.
2087
2088### Imports and Client Structure
2089
2090First, add the imports, model constant, and basic client structure:
2091
2092```rust theme={null}
2093use anyhow::{Context, Result, bail};
2094use genai::Client;
2095use genai::chat::{
2096 ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse,
2097};
2098use rmcp::model::{CallToolRequestParam, Tool as McpTool};
2099use rmcp::service::{RoleClient, RunningService, ServiceExt};
2100use rmcp::transport::TokioChildProcess;
2101use serde_json::Value;
2102use tokio::io::{self, AsyncBufReadExt, BufReader};
2103use tokio::process::Command;
2104
2105const MODEL_ANTHROPIC: &str = "claude-sonnet-4-20250514";
2106
2107struct MCPClient {
2108 anthropic: Client,
2109 session: Option<RunningService<RoleClient, ()>>,
2110 tools: Vec<GenaiTool>,
2111}
2112```
2113
2114The client keeps the model API client, the active MCP session, and the tools advertised by the connected server.
2115
2116### Client Initialization
2117
2118Next, initialize the model client and start without an MCP session or tools:
2119
2120```rust theme={null}
2121impl MCPClient {
2122 fn new() -> Result<Self> {
2123 Ok(MCPClient {
2124 anthropic: Client::default(),
2125 session: None,
2126 tools: Vec::new(),
2127 })
21232128 }
2124 ```
2125
2126 The client keeps the model API client, the active MCP session, and the tools advertised by the connected server.
2127
2128 ### Client Initialization
2129
2130 Next, initialize the model client and start without an MCP session or tools:
2131
2132 ```rust theme={null}
2133 impl MCPClient {
2134 fn new() -> Result<Self> {
2135 Ok(MCPClient {
2136 anthropic: Client::default(),
2137 session: None,
2138 tools: Vec::new(),
2139 })
2129
2130 // Additional methods will go here.
2131}
2132```
2133
2134`genai::Client::default()` reads the `ANTHROPIC_API_KEY` environment variable when it sends a request.
2135
2136### Server Connection Management
2137
2138Add this method inside the `impl MCPClient` block:
2139
2140```rust theme={null}
2141async fn connect_to_server(&mut self, server_args: &[String]) -> Result<()> {
2142 if self.session.is_some() {
2143 bail!("Client is already connected to a server");
2144 }
2145
2146 let mut command = Command::new(&server_args[0]);
2147 command.args(&server_args[1..]);
2148
2149 let process = TokioChildProcess::new(command)
2150 .with_context(|| format!("Failed to spawn server process for {:?}", server_args))?;
2151
2152 let session = ().serve(process).await?;
2153
2154 let rmcp_tools = session
2155 .list_all_tools()
2156 .await
2157 .context("Unable to list tools from server")?;
2158
2159 let tool_names: Vec<String> = rmcp_tools
2160 .iter()
2161 .map(|tool| tool.name.to_string())
2162 .collect();
2163
2164 println!("Connected to server with tools: {tool_names:?}");
2165
2166 self.tools = convert_tools(&rmcp_tools);
2167 self.session = Some(session);
2168 Ok(())
2169}
2170```
2171
2172This method:
2173
21741. Starts the server as a child process using the command and arguments supplied on the command line
21752. Establishes an MCP session over stdio
21763. Lists all tools advertised by the server
21774. Converts those tools into the format used in model requests
2178
2179### Converting MCP Tools
2180
2181Add this function outside the `impl MCPClient` block:
2182
2183```rust theme={null}
2184fn convert_tools(tools: &[McpTool]) -> Vec<GenaiTool> {
2185 tools
2186 .iter()
2187 .map(|tool| GenaiTool {
2188 name: tool.name.to_string(),
2189 description: tool.description.as_deref().map(str::to_string),
2190 schema: Some(Value::Object(tool.input_schema.as_ref().clone())),
2191 config: None,
2192 })
2193 .collect()
2194}
2195```
2196
2197MCP and model APIs describe tools with similar information but different Rust types. `convert_tools` maps each MCP tool's name, description, and input schema into a `genai` tool definition.
2198
2199### Sending Model Requests
2200
2201Add this helper method inside `impl MCPClient`:
2202
2203```rust theme={null}
2204async fn request_model(&self, chat_req: &ChatRequest) -> Result<ChatResponse> {
2205 let response = self
2206 .anthropic
2207 .exec_chat(MODEL_ANTHROPIC, chat_req.clone(), None)
2208 .await
2209 .context("Anthropic chat request failed")?;
2210
2211 Ok(response)
2212}
2213```
2214
2215This keeps model request handling in one place and adds useful context if the API request fails.
2216
2217### Query Processing Logic
2218
2219Now add the core query-processing method inside `impl MCPClient`:
2220
2221```rust theme={null}
2222async fn process_query(&mut self, query: &str) -> Result<String> {
2223 let session = self
2224 .session
2225 .as_ref()
2226 .context("Client is not connected to any server")?;
2227
2228 let mut messages = vec![ChatMessage::user(query)];
2229 let mut final_text = Vec::new();
2230
2231 // Initial Claude API call with tools
2232 let mut chat_req = ChatRequest::new(messages.clone()).with_tools(self.tools.clone());
2233 let mut chat_rsp = self.request_model(&chat_req).await?;
2234
2235 // Process response content - collect text and handle tool calls
2236 for text in chat_rsp.texts() {
2237 final_text.push(text.to_string());
2238 }
2239
2240 let tool_calls = chat_rsp.tool_calls();
2241 if !tool_calls.is_empty() {
2242 // Append assistant's response to message history
2243 messages.push(ChatMessage::assistant(chat_rsp.content.clone()));
2244
2245 // Execute each tool call and collect responses
2246 let mut tool_results = Vec::new();
2247 for tool_call in tool_calls {
2248 // Add information about the tool call to final text
2249 let tool_args_str = serde_json::to_string(&tool_call.fn_arguments)
2250 .unwrap_or_else(|_| "{}".to_string());
2251
2252 final_text.push(format!(
2253 "[Calling tool {} with args {}]",
2254 tool_call.fn_name, tool_args_str
2255 ));
2256
2257 // Query the MCP server
2258 let tool_result = session
2259 .call_tool(CallToolRequestParam {
2260 name: tool_call.fn_name.clone().into(),
2261 arguments: tool_call.fn_arguments.as_object().cloned(),
2262 })
2263 .await
2264 .with_context(|| format!("Tool call {} failed", tool_call.fn_name))?;
2265
2266 let payload = serde_json::to_string(&tool_result)
2267 .context("Failed to serialize tool result")?;
2268
2269 tool_results.push(ContentPart::ToolResponse(ToolResponse::new(
2270 tool_call.call_id.clone(),
2271 payload,
2272 )));
21402273 }
21412274
2142 // Additional methods will go here.
2143 }
2144 ```
2145
2146 `genai::Client::default()` reads the `ANTHROPIC_API_KEY` environment variable when it sends a request.
2147
2148 ### Server Connection Management
2149
2150 Add this method inside the `impl MCPClient` block:
2151
2152 ```rust theme={null}
2153 async fn connect_to_server(&mut self, server_args: &[String]) -> Result<()> {
2154 if self.session.is_some() {
2155 bail!("Client is already connected to a server");
2156 }
2157
2158 let mut command = Command::new(&server_args[0]);
2159 command.args(&server_args[1..]);
2160
2161 let process = TokioChildProcess::new(command)
2162 .with_context(|| format!("Failed to spawn server process for {:?}", server_args))?;
2163
2164 let session = ().serve(process).await?;
2165
2166 let rmcp_tools = session
2167 .list_all_tools()
2168 .await
2169 .context("Unable to list tools from server")?;
2170
2171 let tool_names: Vec<String> = rmcp_tools
2172 .iter()
2173 .map(|tool| tool.name.to_string())
2174 .collect();
2175
2176 println!("Connected to server with tools: {tool_names:?}");
2177
2178 self.tools = convert_tools(&rmcp_tools);
2179 self.session = Some(session);
2180 Ok(())
2181 }
2182 ```
2183
2184 This method:
2185
2186 1. Starts the server as a child process using the command and arguments supplied on the command line
2187 2. Establishes an MCP session over stdio
2188 3. Lists all tools advertised by the server
2189 4. Converts those tools into the format used in model requests
2190
2191 ### Converting MCP Tools
2192
2193 Add this function outside the `impl MCPClient` block:
2194
2195 ```rust theme={null}
2196 fn convert_tools(tools: &[McpTool]) -> Vec<GenaiTool> {
2197 tools
2198 .iter()
2199 .map(|tool| GenaiTool {
2200 name: tool.name.to_string(),
2201 description: tool.description.as_deref().map(str::to_string),
2202 schema: Some(Value::Object(tool.input_schema.as_ref().clone())),
2203 config: None,
2204 })
2205 .collect()
2206 }
2207 ```
2208
2209 MCP and model APIs describe tools with similar information but different Rust types. `convert_tools` maps each MCP tool's name, description, and input schema into a `genai` tool definition.
2210
2211 ### Sending Model Requests
2212
2213 Add this helper method inside `impl MCPClient`:
2214
2215 ```rust theme={null}
2216 async fn request_model(&self, chat_req: &ChatRequest) -> Result<ChatResponse> {
2217 let response = self
2218 .anthropic
2219 .exec_chat(MODEL_ANTHROPIC, chat_req.clone(), None)
2220 .await
2221 .context("Anthropic chat request failed")?;
2222
2223 Ok(response)
2224 }
2225 ```
2226
2227 This keeps model request handling in one place and adds useful context if the API request fails.
2228
2229 ### Query Processing Logic
2230
2231 Now add the core query-processing method inside `impl MCPClient`:
2232
2233 ```rust theme={null}
2234 async fn process_query(&mut self, query: &str) -> Result<String> {
2235 let session = self
2236 .session
2237 .as_ref()
2238 .context("Client is not connected to any server")?;
2239
2240 let mut messages = vec![ChatMessage::user(query)];
2241 let mut final_text = Vec::new();
2242
2243 // Initial Claude API call with tools
2244 let mut chat_req = ChatRequest::new(messages.clone()).with_tools(self.tools.clone());
2245 let mut chat_rsp = self.request_model(&chat_req).await?;
2246
2247 // Process response content - collect text and handle tool calls
2275 // Append tool responses to message history
2276 messages.push(ChatMessage::user(tool_results));
2277
2278 // Build the next request and query model
2279 chat_req = ChatRequest::new(messages.clone());
2280 chat_rsp = self.request_model(&chat_req).await?;
2281
2282 // Collect text from response
22482283 for text in chat_rsp.texts() {
22492284 final_text.push(text.to_string());
22502285 }
2251
2252 let tool_calls = chat_rsp.tool_calls();
2253 if !tool_calls.is_empty() {
2254 // Append assistant's response to message history
2255 messages.push(ChatMessage::assistant(chat_rsp.content.clone()));
2256
2257 // Execute each tool call and collect responses
2258 let mut tool_results = Vec::new();
2259 for tool_call in tool_calls {
2260 // Add information about the tool call to final text
2261 let tool_args_str = serde_json::to_string(&tool_call.fn_arguments)
2262 .unwrap_or_else(|_| "{}".to_string());
2263
2264 final_text.push(format!(
2265 "[Calling tool {} with args {}]",
2266 tool_call.fn_name, tool_args_str
2267 ));
2268
2269 // Query the MCP server
2270 let tool_result = session
2271 .call_tool(CallToolRequestParam {
2272 name: tool_call.fn_name.clone().into(),
2273 arguments: tool_call.fn_arguments.as_object().cloned(),
2274 })
2275 .await
2276 .with_context(|| format!("Tool call {} failed", tool_call.fn_name))?;
2277
2278 let payload = serde_json::to_string(&tool_result)
2279 .context("Failed to serialize tool result")?;
2280
2281 tool_results.push(ContentPart::ToolResponse(ToolResponse::new(
2282 tool_call.call_id.clone(),
2283 payload,
2284 )));
2285 }
2286
2287 // Append tool responses to message history
2288 messages.push(ChatMessage::user(tool_results));
2289
2290 // Build the next request and query model
2291 chat_req = ChatRequest::new(messages.clone());
2292 chat_rsp = self.request_model(&chat_req).await?;
2293
2294 // Collect text from response
2295 for text in chat_rsp.texts() {
2296 final_text.push(text.to_string());
2297 }
2286 }
2287
2288 Ok(final_text.join("\n"))
2289}
2290```
2291
2292The method first sends the user's query and available tools to Claude. When Claude requests tools, the client executes each request through the MCP session, sends the results back to Claude, and collects the final text response.
2293
2294### Interactive Chat Interface
2295
2296Add the interactive terminal loop inside `impl MCPClient`:
2297
2298```rust theme={null}
2299async fn chat_loop(&mut self) -> Result<()> {
2300 println!("\nMCP Client Started!");
2301 println!("Type your queries or 'quit' to exit.");
2302
2303 let mut stdin = BufReader::new(io::stdin());
2304 let mut input = String::new();
2305
2306 loop {
2307 print!("\nQuery: ");
2308 std::io::Write::flush(&mut std::io::stdout())?;
2309
2310 input.clear();
2311 if stdin.read_line(&mut input).await? == 0 {
2312 break; // EOF
22982313 }
22992314
2300 Ok(final_text.join("\n"))
2315 let query = input.trim();
2316 if query.eq_ignore_ascii_case("quit") {
2317 break;
2318 }
2319 if query.is_empty() {
2320 continue;
2321 }
2322
2323 match self.process_query(query).await {
2324 Ok(response) => println!("\n{}", response),
2325 Err(err) => println!("\nError: {}", err),
2326 }
23012327 }
2302 ```
2303
2304 The method first sends the user's query and available tools to Claude. When Claude requests tools, the client executes each request through the MCP session, sends the results back to Claude, and collects the final text response.
2305
2306 ### Interactive Chat Interface
2307
2308 Add the interactive terminal loop inside `impl MCPClient`:
2309
2310 ```rust theme={null}
2311 async fn chat_loop(&mut self) -> Result<()> {
2312 println!("\nMCP Client Started!");
2313 println!("Type your queries or 'quit' to exit.");
2314
2315 let mut stdin = BufReader::new(io::stdin());
2316 let mut input = String::new();
2317
2318 loop {
2319 print!("\nQuery: ");
2320 std::io::Write::flush(&mut std::io::stdout())?;
2321
2322 input.clear();
2323 if stdin.read_line(&mut input).await? == 0 {
2324 break; // EOF
2325 }
2326
2327 let query = input.trim();
2328 if query.eq_ignore_ascii_case("quit") {
2329 break;
2330 }
2331 if query.is_empty() {
2332 continue;
2333 }
2334
2335 match self.process_query(query).await {
2336 Ok(response) => println!("\n{}", response),
2337 Err(err) => println!("\nError: {}", err),
2338 }
2339 }
2340
2341 Ok(())
2328
2329 Ok(())
2330}
2331```
2332
2333The loop accepts queries until the user types `quit` or closes standard input. Query errors are printed without terminating the client.
2334
2335### Cleanup
2336
2337Add this method inside `impl MCPClient` to stop the MCP session and child process:
2338
2339```rust theme={null}
2340async fn cleanup(&mut self) -> Result<()> {
2341 if let Some(session) = self.session.take() {
2342 let _ = session.cancel().await;
23422343 }
2343 ```
2344
2345 The loop accepts queries until the user types `quit` or closes standard input. Query errors are printed without terminating the client.
2346
2347 ### Cleanup
2348
2349 Add this method inside `impl MCPClient` to stop the MCP session and child process:
2350
2351 ```rust theme={null}
2352 async fn cleanup(&mut self) -> Result<()> {
2353 if let Some(session) = self.session.take() {
2354 let _ = session.cancel().await;
2355 }
2356 Ok(())
2344 Ok(())
2345}
2346```
2347
2348### Main Entry Point
2349
2350Finally, add the asynchronous entry point outside the `impl MCPClient` block:
2351
2352```rust theme={null}
2353#[tokio::main]
2354async fn main() -> Result<()> {
2355 dotenvy::dotenv().context("Failed to load env file")?;
2356
2357 let mut args = std::env::args();
2358 let _ = args.next();
2359 let server_args: Vec<String> = args.collect();
2360
2361 if server_args.is_empty() {
2362 eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
2363 std::process::exit(1);
23572364 }
2358 ```
2359
2360 ### Main Entry Point
2361
2362 Finally, add the asynchronous entry point outside the `impl MCPClient` block:
2363
2364 ```rust theme={null}
2365 #[tokio::main]
2366 async fn main() -> Result<()> {
2367 dotenvy::dotenv().context("Failed to load env file")?;
2368
2369 let mut args = std::env::args();
2370 let _ = args.next();
2371 let server_args: Vec<String> = args.collect();
2372
2373 if server_args.is_empty() {
2374 eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
2375 std::process::exit(1);
2376 }
2377
2378 let mut client = MCPClient::new()?;
2379
2380 let result = async {
2381 client.connect_to_server(&server_args).await?;
2382 client.chat_loop().await
2383 }
2384 .await;
2385
2386 let cleanup_result = client.cleanup().await;
2387
2388 result?;
2389 cleanup_result?;
2390
2391 Ok(())
2365
2366 let mut client = MCPClient::new()?;
2367
2368 let result = async {
2369 client.connect_to_server(&server_args).await?;
2370 client.chat_loop().await
23922371 }
2393 ```
2394
2395 The entry point loads `.env`, treats all remaining command-line arguments as the server command, connects the client, starts the chat loop, and ensures cleanup runs before exiting.
2396
2397 ### Verify the Complete File
2398
2399 Before running the client, confirm the items in `src/main.rs` are placed at the correct scope:
2400
2401 * `new`, `connect_to_server`, `process_query`, `request_model`, `chat_loop`, and `cleanup` are methods inside the single `impl MCPClient` block.
2402 * `main` and `convert_tools` are functions outside the `impl MCPClient` block.
2403
2404 Rust does not require these items to appear in a particular order, but methods and free functions must be placed in the correct scope. Compare your file with the [complete `src/main.rs` example](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-rust/src/main.rs), then check that it compiles:
2405
2406 ```bash theme={null}
2407 cargo fmt --check
2408 cargo check
2409 ```
2410
2411 ## Running the Client
2412
2413 Use `cargo run --` followed by the command you would normally use to start the MCP server:
2414
2415 ```bash theme={null}
2416 # Python server
2417 cargo run -- python path/to/server.py
2418
2419 # Node.js server
2420 cargo run -- node path/to/build/index.js
2421
2422 # Executable server
2423 cargo run -- path/to/server-binary
2424 ```
2425
2426 Running bare `cargo run` without a server command prints the usage message and exits.
2427
2428 <Note>
2429 If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust), build the server first and then run a command similar to: `cargo run -- ../weather-server-rust/target/debug/weather`
2430 </Note>
2431
2432 The client will:
2433
2434 1. Start and connect to the specified MCP server
2435 2. List the tools available from that server
2436 3. Start an interactive chat session where you can:
2437 * Enter queries
2438 * See tool executions
2439 * Get responses from Claude
2440
2441 ## How It Works
2442
2443 When you submit a query:
2444
2445 1. The client sends your query and the server's available tools to Claude
2446 2. Claude decides which tools, if any, to use
2447 3. The client executes requested tools through the MCP session
2448 4. Tool results are sent back to Claude
2449 5. Claude provides a natural language response
2450 6. The response is displayed in the terminal
2451
2452 ## Best Practices
2453
2454 1. **Error Handling**
2455 * Add context to errors at process, MCP, model API, and serialization boundaries
2456 * Report individual query errors without terminating the interactive session
2457 * Validate server commands before running them
2458
2459 2. **Resource Management**
2460 * Always cancel the MCP session during cleanup
2461 * Ensure cleanup runs even when connection or chat-loop operations fail
2462 * Avoid starting a second server while a session is active
2463
2464 3. **Security**
2465 * Store API keys securely in `.env`
2466 * Review the tools exposed by a server before allowing model-driven calls
2467 * Connect only to servers and executable commands you trust
2468
2469 ## Troubleshooting
2470
2471 ### Server Command Issues
2472
2473 The arguments after `cargo run --` must form a complete command. Interpreted server scripts need their runtime:
2474
2475 ```bash theme={null}
2476 # Correct
2477 cargo run -- python ./server/weather.py
2478 cargo run -- node ./server/build/index.js
2479
2480 # Incorrect: a Python script is not necessarily executable by itself
2481 cargo run -- ./server/weather.py
2482 ```
2483
2484 If a command cannot be found, use its absolute path or verify it is available in your `PATH`.
2485
2486 ### Environment File Issues
2487
2488 If you see `Failed to load env file`, ensure `.env` exists in the directory where you run the client.
2489
2490 If the model request reports a missing API key, confirm that `.env` contains:
2491
2492 ```text theme={null}
2493 ANTHROPIC_API_KEY=your-api-key-goes-here
2494 ```
2495
2496 ### Tool and Response Errors
2497
2498 * `Unable to list tools from server`: Verify the server starts successfully and communicates over stdio
2499 * `Tool call ... failed`: Verify the server tool's required arguments and environment variables
2500 * `Failed to serialize tool result`: Inspect the server's response for unsupported or malformed content
2501 </Tab>
2502</Tabs>
2372 .await;
2373
2374 let cleanup_result = client.cleanup().await;
2375
2376 result?;
2377 cleanup_result?;
2378
2379 Ok(())
2380}
2381```
2382
2383The entry point loads `.env`, treats all remaining command-line arguments as the server command, connects the client, starts the chat loop, and ensures cleanup runs before exiting.
2384
2385### Verify the Complete File
2386
2387Before running the client, confirm the items in `src/main.rs` are placed at the correct scope:
2388
2389* `new`, `connect_to_server`, `process_query`, `request_model`, `chat_loop`, and `cleanup` are methods inside the single `impl MCPClient` block.
2390* `main` and `convert_tools` are functions outside the `impl MCPClient` block.
2391
2392Rust does not require these items to appear in a particular order, but methods and free functions must be placed in the correct scope. Compare your file with the [complete `src/main.rs` example](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-rust/src/main.rs), then check that it compiles:
2393
2394```bash theme={null}
2395cargo fmt --check
2396cargo check
2397```
2398
2399## Running the Client
2400
2401Use `cargo run --` followed by the command you would normally use to start the MCP server:
2402
2403```bash theme={null}
2404# Python server
2405cargo run -- python path/to/server.py
2406
2407# Node.js server
2408cargo run -- node path/to/build/index.js
2409
2410# Executable server
2411cargo run -- path/to/server-binary
2412```
2413
2414Running bare `cargo run` without a server command prints the usage message and exits.
2415
2416> [!NOTE]
2417>
2418> If you're continuing [the weather tutorial from the server quickstart](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust), build the server first and then run a command similar to: `cargo run -- ../weather-server-rust/target/debug/weather`
2419
2420The client will:
2421
24221. Start and connect to the specified MCP server
24232. List the tools available from that server
24243. Start an interactive chat session where you can:
2425 * Enter queries
2426 * See tool executions
2427 * Get responses from Claude
2428
2429## How It Works
2430
2431When you submit a query:
2432
24331. The client sends your query and the server's available tools to Claude
24342. Claude decides which tools, if any, to use
24353. The client executes requested tools through the MCP session
24364. Tool results are sent back to Claude
24375. Claude provides a natural language response
24386. The response is displayed in the terminal
2439
2440## Best Practices
2441
24421. **Error Handling**
2443 * Add context to errors at process, MCP, model API, and serialization boundaries
2444 * Report individual query errors without terminating the interactive session
2445 * Validate server commands before running them
2446
24472. **Resource Management**
2448 * Always cancel the MCP session during cleanup
2449 * Ensure cleanup runs even when connection or chat-loop operations fail
2450 * Avoid starting a second server while a session is active
2451
24523. **Security**
2453 * Store API keys securely in `.env`
2454 * Review the tools exposed by a server before allowing model-driven calls
2455 * Connect only to servers and executable commands you trust
2456
2457## Troubleshooting
2458
2459### Server Command Issues
2460
2461The arguments after `cargo run --` must form a complete command. Interpreted server scripts need their runtime:
2462
2463```bash theme={null}
2464# Correct
2465cargo run -- python ./server/weather.py
2466cargo run -- node ./server/build/index.js
2467
2468# Incorrect: a Python script is not necessarily executable by itself
2469cargo run -- ./server/weather.py
2470```
2471
2472If a command cannot be found, use its absolute path or verify it is available in your `PATH`.
2473
2474### Environment File Issues
2475
2476If you see `Failed to load env file`, ensure `.env` exists in the directory where you run the client.
2477
2478If the model request reports a missing API key, confirm that `.env` contains:
2479
2480```text theme={null}
2481ANTHROPIC_API_KEY=your-api-key-goes-here
2482```
2483
2484### Tool and Response Errors
2485
2486* `Unable to list tools from server`: Verify the server starts successfully and communicates over stdio
2487* `Tool call ... failed`: Verify the server tool's required arguments and environment variables
2488* `Failed to serialize tool result`: Inspect the server's response for unsupported or malformed content
25032489
25042490## Next steps
25052491
2506<CardGroup cols={2}>
2507 <Card title="Example servers" icon="grid" href="/examples">
2508 Check out our gallery of official MCP servers and implementations
2509 </Card>
2510</CardGroup>
2492> **[Example servers](/examples)**
2493>
2494> Check out our gallery of official MCP servers and implementations
25112495
No line in this hunk matches that.