Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · mcp

Build an MCP client changeddocs/2025-11-25/develop/build-client

Upstream edited this page at 28 Jul 2026 15:42 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 20:07 UTC.

Upstream edited
Recorded here
Lines+2,444added
Lines−2,460removed
From line 6 where the diff opens
First seen 26 Aug 2026 this site's first read of the page
Recorded edits2to this page, all time

## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Basic Client Structure ### Server Connection Management ### Query Processing Logic ### Interactive Chat Interface ### Main Entry Point ## Key Components Explained ### 1. Client Initialization ### 2. Server Connection ### 3. Query Processing ### 4. Interactive Interface ### 5. Resource Management ## Common Customization Points ## Running the Client ## How It Works ## Best practices ## Troubleshooting ### Server Path Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Basic Client Structure ### Server Connection Management ### Query Processing Logic ### Interactive Chat Interface ### Main Entry Point ## Running the Client ## How It Works ## Best practices ## Troubleshooting ### Server Path Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting Up Your Environment ## How it Works ### MCP Client Configuration ### Chat Implementation ### Build and run ### Advanced Configuration ## System Requirements ## Setting up your environment ## Setting up your API key ## Creating the Client ### Basic Client Structure ### Server connection management ### Query processing logic ### Interactive chat ### Main entry point ## Running the client ## How it works ## Best practices ## Troubleshooting ### Server Path Issues ### Build Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting up your environment ## Setting up your API key ## Creating the Client ### Basic Client Structure ### Query processing logic ## Key Components Explained ### 1. Client Initialization ### 2. Server Connection ### 3. Query Processing ## Running the Client ## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Basic Client Structure ### Server Connection Management ### Query Processing Logic ### Interactive Chat Interface ### Main Entry Point ## Key Components Explained ### 1. Client Initialization ### 2. Server Connection ### 3. Query Processing ### 4. Interactive Interface ### 5. Resource Management ## Running the Client ## How It Works ## Best practices ## Troubleshooting ### Server Path Issues ### Response Timing ### Common Error Messages ## System Requirements ## Setting Up Your Environment ## Setting Up Your API Key ## Creating the Client ### Imports and Client Structure ### Client Initialization ### Server Connection Management ### Converting MCP Tools ### Sending Model Requests ### Query Processing Logic ### Interactive Chat Interface ### Cleanup ### Main Entry Point ### Verify the Complete File ## Running the Client ## How It Works ## Best Practices ## Troubleshooting ### Server Command Issues ### Environment File Issues ### Tool and Response Errors

The whole hunk

from line 6, old and new numbered
/
lines

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