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/2026-07-28/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 * You must use the Python MCP SDK 2.0.0 or higher
21
22 ## Setting Up Your Environment
23
24 First, create a new Python project with `uv`:
25
26 <CodeGroup>
27 ```bash macOS/Linux theme={null}
28 # Create project directory
29 uv init mcp-client
30 cd mcp-client
31
32 # Create virtual environment
33 uv venv
34
35 # Activate virtual environment
36 source .venv/bin/activate
37
38 # Install required packages
39 uv add mcp anthropic python-dotenv
40
41 # Remove boilerplate files
42 rm main.py
43
44 # Create our main file
45 touch client.py
46 ```
47
48 ```powershell Windows theme={null}
49 # Create project directory
50 uv init mcp-client
51 cd mcp-client
52
53 # Create virtual environment
54 uv venv
55
56 # Activate virtual environment
57 .venv\Scripts\activate
58
59 # Install required packages
60 uv add mcp anthropic python-dotenv
61
62 # Remove boilerplate files
63 del main.py
64
65 # Create our main file
66 new-item client.py
67 ```
68 </CodeGroup>
69
70 ## Setting Up Your API Key
71
72 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
73
74 Create a `.env` file to store it:
75
76 ```bash theme={null}
77 echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
78 ```
79
80 Add `.env` to your `.gitignore`:
81
82 ```bash theme={null}
83 echo ".env" >> .gitignore
84 ```
85
86 <Warning>
87 Make sure you keep your `ANTHROPIC_API_KEY` secure!
88 </Warning>
89
90 ## Creating the Client
91
92 ### Imports and Setup
93
94 First, let's set up our imports and the pieces the rest of the file shares:
95
96 ```python theme={null}
97 import asyncio
98 import sys
99
100 from mcp import Client, StdioServerParameters
101 from mcp.client.stdio import stdio_client
102 from mcp_types import TextContent
103
104 from anthropic import Anthropic
105 from dotenv import load_dotenv
106
107 load_dotenv() # load environment variables from .env
108
109 MODEL = "claude-opus-5"
110 anthropic = Anthropic()
111 ```
112
113 `Client` is the single object your program talks to the server through. Listing the tools, calling one, reading a resource: each of those is a method on it.
114
115 ### Server Connection Management
116
117 Next, we'll work out which process to launch for a given server script:
118
119 ```python theme={null}
120 def server_params(server_script_path: str) -> StdioServerParameters:
121 """Describe the subprocess that runs an MCP server
122
123 Args:
124 server_script_path: Path to the server script (.py or .js)
125 """
126 if server_script_path.endswith(".py"):
127 command = "python"
128 elif server_script_path.endswith(".js"):
129 command = "node"
130 else:
131 raise ValueError("Server script must be a .py or .js file")
132
133 return StdioServerParameters(command=command, args=[server_script_path])
134 ```
135
136 `StdioServerParameters` is configuration, not a connection. `stdio_client()` turns it into a stdio transport, and `Client` opens that transport when you enter its `async with` block. We'll do both in `main()`.
137
138 ### Query Processing Logic
139
140 Now let's add the core functionality for processing queries and handling tool calls:
141
142 ```python theme={null}
143 async def process_query(client: Client, query: str) -> str:
144 """Process a query using Claude and available tools"""
145 messages = [
146 {
147 "role": "user",
148 "content": query
149 }
150 ]
151
152 tool_list = await client.list_tools()
153 available_tools = [{
154 "name": tool.name,
155 "description": tool.description,
156 "input_schema": tool.input_schema
157 } for tool in tool_list.tools]
158
159 # Initial Claude API call
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* You must use the Python MCP SDK 2.0.0 or higher
21
22## Setting Up Your Environment
23
24First, create a new Python project with `uv`:
25
26```bash macOS/Linux theme={null}
27# Create project directory
28uv init mcp-client
29cd mcp-client
30
31# Create virtual environment
32uv venv
33
34# Activate virtual environment
35source .venv/bin/activate
36
37# Install required packages
38uv add mcp anthropic python-dotenv
39
40# Remove boilerplate files
41rm main.py
42
43# Create our main file
44touch client.py
45```
46
47```powershell Windows theme={null}
48# Create project directory
49uv init mcp-client
50cd mcp-client
51
52# Create virtual environment
53uv venv
54
55# Activate virtual environment
56.venv\Scripts\activate
57
58# Install required packages
59uv add mcp anthropic python-dotenv
60
61# Remove boilerplate files
62del main.py
63
64# Create our main file
65new-item client.py
66```
67
68## Setting Up Your API Key
69
70You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
71
72Create a `.env` file to store it:
73
74```bash theme={null}
75echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
76```
77
78Add `.env` to your `.gitignore`:
79
80```bash theme={null}
81echo ".env" >> .gitignore
82```
83
84> [!WARNING]
85>
86> Make sure you keep your `ANTHROPIC_API_KEY` secure!
87
88## Creating the Client
89
90### Imports and Setup
91
92First, let's set up our imports and the pieces the rest of the file shares:
93
94```python theme={null}
95import asyncio
96import sys
97
98from mcp import Client, StdioServerParameters
99from mcp.client.stdio import stdio_client
100from mcp_types import TextContent
101
102from anthropic import Anthropic
103from dotenv import load_dotenv
104
105load_dotenv() # load environment variables from .env
106
107MODEL = "claude-opus-5"
108anthropic = Anthropic()
109```
110
111`Client` is the single object your program talks to the server through. Listing the tools, calling one, reading a resource: each of those is a method on it.
112
113### Server Connection Management
114
115Next, we'll work out which process to launch for a given server script:
116
117```python theme={null}
118def server_params(server_script_path: str) -> StdioServerParameters:
119 """Describe the subprocess that runs an MCP server
120
121 Args:
122 server_script_path: Path to the server script (.py or .js)
123 """
124 if server_script_path.endswith(".py"):
125 command = "python"
126 elif server_script_path.endswith(".js"):
127 command = "node"
128 else:
129 raise ValueError("Server script must be a .py or .js file")
130
131 return StdioServerParameters(command=command, args=[server_script_path])
132```
133
134`StdioServerParameters` is configuration, not a connection. `stdio_client()` turns it into a stdio transport, and `Client` opens that transport when you enter its `async with` block. We'll do both in `main()`.
135
136### Query Processing Logic
137
138Now let's add the core functionality for processing queries and handling tool calls:
139
140```python theme={null}
141async def process_query(client: Client, query: str) -> str:
142 """Process a query using Claude and available tools"""
143 messages = [
144 {
145 "role": "user",
146 "content": query
147 }
148 ]
149
150 tool_list = await client.list_tools()
151 available_tools = [{
152 "name": tool.name,
153 "description": tool.description,
154 "input_schema": tool.input_schema
155 } for tool in tool_list.tools]
156
157 # Initial Claude API call
158 response = anthropic.messages.create(
159 model=MODEL,
160 max_tokens=1000,
161 messages=messages,
162 tools=available_tools
163 )
164
165 # Process response and handle tool calls
166 final_text = []
167 tool_results = []
168
169 for content in response.content:
170 if content.type == 'text':
171 final_text.append(content.text)
172 elif content.type == 'tool_use':
173 tool_name = content.name
174 tool_args = content.input
175
176 # Execute tool call
177 result = await client.call_tool(tool_name, tool_args)
178 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
179
180 tool_results.append({
181 "type": "tool_result",
182 "tool_use_id": content.id,
183 "content": "\n".join(
184 block.text
185 for block in result.content
186 if isinstance(block, TextContent)
187 ),
188 "is_error": result.is_error
189 })
190
191 if tool_results:
192 messages.append({"role": "assistant", "content": response.content})
193 messages.append({"role": "user", "content": tool_results})
194
195 # Get next response from Claude
160196 response = anthropic.messages.create(
161197 model=MODEL,
162198 max_tokens=1000,
from line 200
164200 tools=available_tools
165201 )
166202
167 # Process response and handle tool calls
168 final_text = []
169 tool_results = []
170
171203 for content in response.content:
172204 if content.type == 'text':
173205 final_text.append(content.text)
174 elif content.type == 'tool_use':
175 tool_name = content.name
176 tool_args = content.input
177
178 # Execute tool call
179 result = await client.call_tool(tool_name, tool_args)
180 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
181
182 tool_results.append({
183 "type": "tool_result",
184 "tool_use_id": content.id,
185 "content": "\n".join(
186 block.text
187 for block in result.content
188 if isinstance(block, TextContent)
189 ),
190 "is_error": result.is_error
191 })
192
193 if tool_results:
194 messages.append({"role": "assistant", "content": response.content})
195 messages.append({"role": "user", "content": tool_results})
196
197 # Get next response from Claude
198 response = anthropic.messages.create(
199 model=MODEL,
200 max_tokens=1000,
201 messages=messages,
202 tools=available_tools
203 )
204
205 for content in response.content:
206 if content.type == 'text':
207 final_text.append(content.text)
208
209 return "\n".join(final_text)
210 ```
211
212 `call_tool` returns a `CallToolResult`. Its `content` is a list of blocks, which is why we narrow to `TextContent` before reading `.text`. A tool that raises does not raise here: it answers with `is_error` set, and passing that flag on lets Claude read the message and try something else.
213
214 ### Interactive Chat Interface
215
216 Now we'll add the chat loop:
217
218 ```python theme={null}
219 async def chat_loop(client: Client) -> None:
220 """Run an interactive chat loop"""
221 print("\nMCP Client Started!")
222 print("Type your queries or 'quit' to exit.")
223
224 while True:
225 try:
226 query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
227 except EOFError:
228 break
229
230 if query.lower() == 'quit':
231 break
232
233 try:
234 response = await process_query(client, query)
235 print("\n" + response)
236 except Exception as e:
237 print(f"\nError: {e}")
238 ```
239
240 `input()` blocks, so it runs on a worker thread. That keeps the event loop free to service the connection while you type.
241
242 ### Main Entry Point
243
244 Finally, we'll add the main execution logic:
245
246 ```python theme={null}
247 async def main() -> None:
248 if len(sys.argv) < 2:
249 print("Usage: python client.py <path_to_server_script>")
250 sys.exit(1)
251
252 async with Client(stdio_client(server_params(sys.argv[1]))) as client:
253 tool_list = await client.list_tools()
254 tool_names = [tool.name for tool in tool_list.tools]
255 print("\nConnected to server with tools:", tool_names)
256
257 await chat_loop(client)
258
259
260 if __name__ == "__main__":
261 asyncio.run(main())
262 ```
263
264 That `async with` is the entire connection lifecycle. Entering it launches the server and agrees a protocol version with it; leaving it disconnects and shuts the subprocess down. There is nothing to close by hand.
265
266 You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).
267
268 ## Key Components Explained
269
270 ### 1. Client Initialization
271
272 * A single `Client` carries the connection, and `async with` is its whole lifecycle
273 * There is no connect/close pair to call and nothing to clean up afterwards
274 * Configures the Anthropic client for Claude interactions
275
276 ### 2. Server Connection
277
278 * Supports both Python and Node.js servers
279 * Validates server script type
280 * Launches the server as a subprocess and speaks stdio to it
281 * Lists the available tools once the connection is open
282
283 ### 3. Query Processing
284
285 * Maintains conversation context
286 * Handles Claude's responses and tool calls
287 * Manages the message flow between Claude and tools
288 * Combines results into a coherent response
289
290 ### 4. Interactive Interface
291
292 * Provides a simple command-line interface
293 * Handles user input and displays responses
294 * Includes basic error handling
295 * Allows graceful exit
296
297 ### 5. Resource Management
298
299 * Leaving the `async with` block disconnects and shuts the server subprocess down
300 * A failing query is reported without ending the session
301 * Typing `quit`, or closing standard input, exits cleanly
302
303 ## Common Customization Points
304
305 1. **Tool Handling**
306 * Modify `process_query()` to handle specific tool types
307 * Add custom error handling for tool calls
308 * Implement tool-specific response formatting
309
310 2. **Response Processing**
311 * Customize how tool results are formatted
312 * Add response filtering or transformation
313 * Implement custom logging
314
315 3. **User Interface**
316 * Add a GUI or web interface
317 * Implement rich console output
318 * Add command history or auto-completion
319
320 ## Running the Client
321
322 To run your client with any MCP server:
323
324 ```bash theme={null}
325 uv run client.py path/to/server.py # python server
326 uv run client.py path/to/build/index.js # node server
327 ```
328
329 <Note>
330 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`
331 </Note>
332
333 The client will:
334
335 1. Connect to the specified server
336 2. List available tools
337 3. Start an interactive chat session where you can:
338 * Enter queries
339 * See tool executions
340 * Get responses from Claude
341
342 Here's an example of what it should look like if connected to the weather server from the server quickstart:
343
344 <Frame>
345 <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" />
346 </Frame>
347
348 ## How It Works
349
350 When you submit a query:
351
352 1. The client gets the list of available tools from the server
353 2. Your query is sent to Claude along with tool descriptions
354 3. Claude decides which tools (if any) to use
355 4. The client executes any requested tool calls through the server
356 5. Results are sent back to Claude
357 6. Claude provides a natural language response
358 7. The response is displayed to you
359
360 ## Best practices
361
362 1. **Error Handling**
363 * Check `result.is_error` rather than expecting a failing tool to raise
364 * Provide meaningful error messages
365 * Gracefully handle connection issues
366
367 2. **Resource Management**
368 * Let the `async with` block own the connection
369 * Keep it open for as long as you need the server
370 * Handle server disconnections
371
372 3. **Security**
373 * Store API keys securely in `.env`
374 * Validate server responses
375 * Be cautious with tool permissions
376
377 4. **Tool Names**
378 * Tool names can be validated according to the format specified [here](/specification/2026-07-28/server/tools#tool-names)
379 * If a tool name conforms to the specified format, it should not fail validation by an MCP client
380
381 ## Troubleshooting
382
383 ### Server Path Issues
384
385 * Double-check the path to your server script is correct
386 * Use the absolute path if the relative path isn't working
387 * For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
388 * Verify the server file has the correct extension (.py for Python or .js for Node.js)
389
390 Example of correct path usage:
391
392 ```bash theme={null}
393 # Relative path
394 uv run client.py ./server/weather.py
395
396 # Absolute path
397 uv run client.py /Users/username/projects/mcp-server/weather.py
398
399 # Windows path (either format works)
400 uv run client.py C:/projects/mcp-server/weather.py
401 uv run client.py C:\\projects\\mcp-server\\weather.py
402 ```
403
404 ### Response Timing
405
406 * The first response might take up to 30 seconds to return
407 * This is normal and happens while:
408 * The server initializes
409 * Claude processes the query
410 * Tools are being executed
411 * Subsequent responses are typically faster
412 * Don't interrupt the process during this initial waiting period
413
414 ### Common Error Messages
415
416 If you see:
417
418 * `FileNotFoundError`: Check your server path
419 * `Connection refused`: Ensure the server is running and the path is correct
420 * `Tool execution failed`: Verify the tool's required environment variables are set
421 * `Timeout error`: Consider raising `read_timeout_seconds` on the `Client`
422 </Tab>
423
424 <Tab title="TypeScript">
425 [You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
426
427 ## System Requirements
428
429 Before starting, ensure your system meets these requirements:
430
431 * Mac or Windows computer
432 * Node.js 20 or higher installed
433 * Latest version of `npm` installed
434 * Anthropic API key (Claude)
435
436 ## Setting Up Your Environment
437
438 First, let's create and set up our project:
439
440 <CodeGroup>
441 ```bash macOS/Linux theme={null}
442 # Create project directory
443 mkdir mcp-client-typescript
444 cd mcp-client-typescript
445
446 # Initialize npm project
447 npm init -y
448
449 # Install dependencies
450 npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
451
452 # Install dev dependencies
453 npm install -D @types/node typescript
454
455 # Create source file
456 touch index.ts
457 ```
458
459 ```powershell Windows theme={null}
460 # Create project directory
461 md mcp-client-typescript
462 cd mcp-client-typescript
463
464 # Initialize npm project
465 npm init -y
466
467 # Install dependencies
468 npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
469
470 # Install dev dependencies
471 npm install -D @types/node typescript
472
473 # Create source file
474 new-item index.ts
475 ```
476 </CodeGroup>
477
478 Update your `package.json` to set `type: "module"` and a build script:
479
480 ```json package.json theme={null}
206
207 return "\n".join(final_text)
208```
209
210`call_tool` returns a `CallToolResult`. Its `content` is a list of blocks, which is why we narrow to `TextContent` before reading `.text`. A tool that raises does not raise here: it answers with `is_error` set, and passing that flag on lets Claude read the message and try something else.
211
212### Interactive Chat Interface
213
214Now we'll add the chat loop:
215
216```python theme={null}
217async def chat_loop(client: Client) -> None:
218 """Run an interactive chat loop"""
219 print("\nMCP Client Started!")
220 print("Type your queries or 'quit' to exit.")
221
222 while True:
223 try:
224 query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
225 except EOFError:
226 break
227
228 if query.lower() == 'quit':
229 break
230
231 try:
232 response = await process_query(client, query)
233 print("\n" + response)
234 except Exception as e:
235 print(f"\nError: {e}")
236```
237
238`input()` blocks, so it runs on a worker thread. That keeps the event loop free to service the connection while you type.
239
240### Main Entry Point
241
242Finally, we'll add the main execution logic:
243
244```python theme={null}
245async def main() -> None:
246 if len(sys.argv) < 2:
247 print("Usage: python client.py <path_to_server_script>")
248 sys.exit(1)
249
250 async with Client(stdio_client(server_params(sys.argv[1]))) as client:
251 tool_list = await client.list_tools()
252 tool_names = [tool.name for tool in tool_list.tools]
253 print("\nConnected to server with tools:", tool_names)
254
255 await chat_loop(client)
256
257
258if __name__ == "__main__":
259 asyncio.run(main())
260```
261
262That `async with` is the entire connection lifecycle. Entering it launches the server and agrees a protocol version with it; leaving it disconnects and shuts the subprocess down. There is nothing to close by hand.
263
264You can find the complete `client.py` file [here](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py).
265
266## Key Components Explained
267
268### 1. Client Initialization
269
270* A single `Client` carries the connection, and `async with` is its whole lifecycle
271* There is no connect/close pair to call and nothing to clean up afterwards
272* Configures the Anthropic client for Claude interactions
273
274### 2. Server Connection
275
276* Supports both Python and Node.js servers
277* Validates server script type
278* Launches the server as a subprocess and speaks stdio to it
279* Lists the available tools once the connection is open
280
281### 3. Query Processing
282
283* Maintains conversation context
284* Handles Claude's responses and tool calls
285* Manages the message flow between Claude and tools
286* Combines results into a coherent response
287
288### 4. Interactive Interface
289
290* Provides a simple command-line interface
291* Handles user input and displays responses
292* Includes basic error handling
293* Allows graceful exit
294
295### 5. Resource Management
296
297* Leaving the `async with` block disconnects and shuts the server subprocess down
298* A failing query is reported without ending the session
299* Typing `quit`, or closing standard input, exits cleanly
300
301## Common Customization Points
302
3031. **Tool Handling**
304 * Modify `process_query()` to handle specific tool types
305 * Add custom error handling for tool calls
306 * Implement tool-specific response formatting
307
3082. **Response Processing**
309 * Customize how tool results are formatted
310 * Add response filtering or transformation
311 * Implement custom logging
312
3133. **User Interface**
314 * Add a GUI or web interface
315 * Implement rich console output
316 * Add command history or auto-completion
317
318## Running the Client
319
320To run your client with any MCP server:
321
322```bash theme={null}
323uv run client.py path/to/server.py # python server
324uv run client.py path/to/build/index.js # node server
325```
326
327> [!NOTE]
328>
329> 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`
330
331The client will:
332
3331. Connect to the specified server
3342. List available tools
3353. Start an interactive chat session where you can:
336 * Enter queries
337 * See tool executions
338 * Get responses from Claude
339
340Here's an example of what it should look like if connected to the weather server from the server quickstart:
341
342
343
344## How It Works
345
346When you submit a query:
347
3481. The client gets the list of available tools from the server
3492. Your query is sent to Claude along with tool descriptions
3503. Claude decides which tools (if any) to use
3514. The client executes any requested tool calls through the server
3525. Results are sent back to Claude
3536. Claude provides a natural language response
3547. The response is displayed to you
355
356## Best practices
357
3581. **Error Handling**
359 * Check `result.is_error` rather than expecting a failing tool to raise
360 * Provide meaningful error messages
361 * Gracefully handle connection issues
362
3632. **Resource Management**
364 * Let the `async with` block own the connection
365 * Keep it open for as long as you need the server
366 * Handle server disconnections
367
3683. **Security**
369 * Store API keys securely in `.env`
370 * Validate server responses
371 * Be cautious with tool permissions
372
3734. **Tool Names**
374 * Tool names can be validated according to the format specified [here](/specification/2026-07-28/server/tools#tool-names)
375 * If a tool name conforms to the specified format, it should not fail validation by an MCP client
376
377## Troubleshooting
378
379### Server Path Issues
380
381* Double-check the path to your server script is correct
382* Use the absolute path if the relative path isn't working
383* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
384* Verify the server file has the correct extension (.py for Python or .js for Node.js)
385
386Example of correct path usage:
387
388```bash theme={null}
389# Relative path
390uv run client.py ./server/weather.py
391
392# Absolute path
393uv run client.py /Users/username/projects/mcp-server/weather.py
394
395# Windows path (either format works)
396uv run client.py C:/projects/mcp-server/weather.py
397uv run client.py C:\\projects\\mcp-server\\weather.py
398```
399
400### Response Timing
401
402* The first response might take up to 30 seconds to return
403* This is normal and happens while:
404 * The server initializes
405 * Claude processes the query
406 * Tools are being executed
407* Subsequent responses are typically faster
408* Don't interrupt the process during this initial waiting period
409
410### Common Error Messages
411
412If you see:
413
414* `FileNotFoundError`: Check your server path
415* `Connection refused`: Ensure the server is running and the path is correct
416* `Tool execution failed`: Verify the tool's required environment variables are set
417* `Timeout error`: Consider raising `read_timeout_seconds` on the `Client`
418
419**TypeScript**
420
421[You can find the complete code for this tutorial here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-typescript)
422
423## System Requirements
424
425Before starting, ensure your system meets these requirements:
426
427* Mac or Windows computer
428* Node.js 20 or higher installed
429* Latest version of `npm` installed
430* Anthropic API key (Claude)
431
432## Setting Up Your Environment
433
434First, let's create and set up our project:
435
436```bash macOS/Linux theme={null}
437# Create project directory
438mkdir mcp-client-typescript
439cd mcp-client-typescript
440
441# Initialize npm project
442npm init -y
443
444# Install dependencies
445npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
446
447# Install dev dependencies
448npm install -D @types/node typescript
449
450# Create source file
451touch index.ts
452```
453
454```powershell Windows theme={null}
455# Create project directory
456md mcp-client-typescript
457cd mcp-client-typescript
458
459# Initialize npm project
460npm init -y
461
462# Install dependencies
463npm install @anthropic-ai/sdk @modelcontextprotocol/client dotenv
464
465# Install dev dependencies
466npm install -D @types/node typescript
467
468# Create source file
469new-item index.ts
470```
471
472Update your `package.json` to set `type: "module"` and a build script:
473
474```json package.json theme={null}
475{
476 "type": "module",
477 "scripts": {
478 "build": "tsc && chmod 755 build/index.js"
479 }
480}
481```
482
483Create a `tsconfig.json` in the root of your project:
484
485```json tsconfig.json theme={null}
486{
487 "compilerOptions": {
488 "target": "ES2022",
489 "module": "Node16",
490 "moduleResolution": "Node16",
491 "types": ["node"],
492 "outDir": "./build",
493 "rootDir": "./",
494 "strict": true,
495 "esModuleInterop": true,
496 "skipLibCheck": true,
497 "forceConsistentCasingInFileNames": true
498 },
499 "include": ["index.ts"],
500 "exclude": ["node_modules"]
501}
502```
503
504## Setting Up Your API Key
505
506You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
507
508Create a `.env` file to store it:
509
510```bash theme={null}
511echo "ANTHROPIC_API_KEY=<your key here>" > .env
512```
513
514Add `.env` to your `.gitignore`:
515
516```bash theme={null}
517echo ".env" >> .gitignore
518```
519
520> [!WARNING]
521>
522> Make sure you keep your `ANTHROPIC_API_KEY` secure!
523
524## Creating the Client
525
526### Basic Client Structure
527
528First, let's set up our imports and create the basic client class in `index.ts`:
529
530```typescript theme={null}
531import { Anthropic } from "@anthropic-ai/sdk";
532import {
533 MessageParam,
534 Tool,
535} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
536import { Client } from "@modelcontextprotocol/client";
537import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
538import readline from "readline/promises";
539import dotenv from "dotenv";
540
541dotenv.config();
542
543const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
544if (!ANTHROPIC_API_KEY) {
545 throw new Error("ANTHROPIC_API_KEY is not set");
546}
547
548class MCPClient {
549 private mcp: Client;
550 private anthropic: Anthropic;
551 private transport: StdioClientTransport | null = null;
552 private tools: Tool[] = [];
553
554 constructor() {
555 this.anthropic = new Anthropic({
556 apiKey: ANTHROPIC_API_KEY,
557 });
558 this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
559 }
560 // methods will go here
561}
562```
563
564### Server Connection Management
565
566Next, we'll implement the method to connect to an MCP server:
567
568```typescript theme={null}
569async connectToServer(serverScriptPath: string) {
570 try {
571 const isJs = serverScriptPath.endsWith(".js");
572 const isPy = serverScriptPath.endsWith(".py");
573 if (!isJs && !isPy) {
574 throw new Error("Server script must be a .js or .py file");
575 }
576 const command = isPy
577 ? process.platform === "win32"
578 ? "python"
579 : "python3"
580 : process.execPath;
581
582 this.transport = new StdioClientTransport({
583 command,
584 args: [serverScriptPath],
585 });
586 await this.mcp.connect(this.transport);
587
588 const toolsResult = await this.mcp.listTools();
589 this.tools = toolsResult.tools.map((tool) => {
590 return {
591 name: tool.name,
592 description: tool.description,
593 input_schema: tool.inputSchema,
594 };
595 });
596 console.log(
597 "Connected to server with tools:",
598 this.tools.map(({ name }) => name)
599 );
600 } catch (e) {
601 console.log("Failed to connect to MCP server: ", e);
602 throw e;
603 }
604}
605```
606
607### Query Processing Logic
608
609Now let's add the core functionality for processing queries and handling tool calls:
610
611```typescript theme={null}
612async processQuery(query: string) {
613 const messages: MessageParam[] = [
481614 {
482 "type": "module",
483 "scripts": {
484 "build": "tsc && chmod 755 build/index.js"
485 }
486 }
487 ```
488
489 Create a `tsconfig.json` in the root of your project:
490
491 ```json tsconfig.json theme={null}
492 {
493 "compilerOptions": {
494 "target": "ES2022",
495 "module": "Node16",
496 "moduleResolution": "Node16",
497 "types": ["node"],
498 "outDir": "./build",
499 "rootDir": "./",
500 "strict": true,
501 "esModuleInterop": true,
502 "skipLibCheck": true,
503 "forceConsistentCasingInFileNames": true
504 },
505 "include": ["index.ts"],
506 "exclude": ["node_modules"]
507 }
508 ```
509
510 ## Setting Up Your API Key
511
512 You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
513
514 Create a `.env` file to store it:
515
516 ```bash theme={null}
517 echo "ANTHROPIC_API_KEY=<your key here>" > .env
518 ```
519
520 Add `.env` to your `.gitignore`:
521
522 ```bash theme={null}
523 echo ".env" >> .gitignore
524 ```
525
526 <Warning>
527 Make sure you keep your `ANTHROPIC_API_KEY` secure!
528 </Warning>
529
530 ## Creating the Client
531
532 ### Basic Client Structure
533
534 First, let's set up our imports and create the basic client class in `index.ts`:
535
536 ```typescript theme={null}
537 import { Anthropic } from "@anthropic-ai/sdk";
538 import {
539 MessageParam,
540 Tool,
541 } from "@anthropic-ai/sdk/resources/messages/messages.mjs";
542 import { Client } from "@modelcontextprotocol/client";
543 import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
544 import readline from "readline/promises";
545 import dotenv from "dotenv";
546
547 dotenv.config();
548
549 const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
550 if (!ANTHROPIC_API_KEY) {
551 throw new Error("ANTHROPIC_API_KEY is not set");
552 }
553
554 class MCPClient {
555 private mcp: Client;
556 private anthropic: Anthropic;
557 private transport: StdioClientTransport | null = null;
558 private tools: Tool[] = [];
559
560 constructor() {
561 this.anthropic = new Anthropic({
562 apiKey: ANTHROPIC_API_KEY,
563 });
564 this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
565 }
566 // methods will go here
567 }
568 ```
569
570 ### Server Connection Management
571
572 Next, we'll implement the method to connect to an MCP server:
573
574 ```typescript theme={null}
575 async connectToServer(serverScriptPath: string) {
576 try {
577 const isJs = serverScriptPath.endsWith(".js");
578 const isPy = serverScriptPath.endsWith(".py");
579 if (!isJs && !isPy) {
580 throw new Error("Server script must be a .js or .py file");
581 }
582 const command = isPy
583 ? process.platform === "win32"
584 ? "python"
585 : "python3"
586 : process.execPath;
587
588 this.transport = new StdioClientTransport({
589 command,
590 args: [serverScriptPath],
591 });
592 await this.mcp.connect(this.transport);
593
594 const toolsResult = await this.mcp.listTools();
595 this.tools = toolsResult.tools.map((tool) => {
596 return {
597 name: tool.name,
598 description: tool.description,
599 input_schema: tool.inputSchema,
600 };
601 });
602 console.log(
603 "Connected to server with tools:",
604 this.tools.map(({ name }) => name)
605 );
606 } catch (e) {
607 console.log("Failed to connect to MCP server: ", e);
608 throw e;
609 }
610 }
611 ```
612
613 ### Query Processing Logic
614
615 Now let's add the core functionality for processing queries and handling tool calls:
616
617 ```typescript theme={null}
618 async processQuery(query: string) {
619 const messages: MessageParam[] = [
620 {
621 role: "user",
622 content: query,
623 },
624 ];
615 role: "user",
616 content: query,
617 },
618 ];
619
620 const response = await this.anthropic.messages.create({
621 model: "claude-opus-5",
622 max_tokens: 1000,
623 messages,
624 tools: this.tools,
625 });
626
627 const finalText = [];
628
629 for (const content of response.content) {
630 if (content.type === "text") {
631 finalText.push(content.text);
632 } else if (content.type === "tool_use") {
633 const toolName = content.name;
634 const toolArgs = content.input as { [x: string]: unknown } | undefined;
635
636 const result = await this.mcp.callTool({
637 name: toolName,
638 arguments: toolArgs,
639 });
640 finalText.push(
641 `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
642 );
643
644 messages.push({
645 role: "user",
646 content: result.content
647 .filter((block) => block.type === "text")
648 .map((block) => block.text)
649 .join("\n"),
650 });
625651
626652 const response = await this.anthropic.messages.create({
627653 model: "claude-opus-5",
628654 max_tokens: 1000,
629655 messages,
630 tools: this.tools,
631656 });
632657
633 const finalText = [];
634
635 for (const content of response.content) {
636 if (content.type === "text") {
637 finalText.push(content.text);
638 } else if (content.type === "tool_use") {
639 const toolName = content.name;
640 const toolArgs = content.input as { [x: string]: unknown } | undefined;
641
642 const result = await this.mcp.callTool({
643 name: toolName,
644 arguments: toolArgs,
645 });
646 finalText.push(
647 `[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
648 );
649
650 messages.push({
651 role: "user",
652 content: result.content
653 .filter((block) => block.type === "text")
654 .map((block) => block.text)
655 .join("\n"),
656 });
657
658 const response = await this.anthropic.messages.create({
659 model: "claude-opus-5",
660 max_tokens: 1000,
661 messages,
662 });
663
664 finalText.push(
665 response.content[0].type === "text" ? response.content[0].text : ""
666 );
667 }
658 finalText.push(
659 response.content[0].type === "text" ? response.content[0].text : ""
660 );
661 }
662 }
663
664 return finalText.join("\n");
665}
666```
667
668### Interactive Chat Interface
669
670Now we'll add the chat loop and cleanup functionality:
671
672```typescript theme={null}
673async chatLoop() {
674 const rl = readline.createInterface({
675 input: process.stdin,
676 output: process.stdout,
677 });
678
679 try {
680 console.log("\nMCP Client Started!");
681 console.log("Type your queries or 'quit' to exit.");
682
683 while (true) {
684 const message = await rl.question("\nQuery: ");
685 if (message.toLowerCase() === "quit") {
686 break;
668687 }
669
670 return finalText.join("\n");
688 const response = await this.processQuery(message);
689 console.log("\n" + response);
671690 }
672 ```
673
674 ### Interactive Chat Interface
675
676 Now we'll add the chat loop and cleanup functionality:
677
678 ```typescript theme={null}
679 async 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;
693 }
694 const response = await this.processQuery(message);
695 console.log("\n" + response);
696 }
697 } finally {
698 rl.close();
691 } finally {
692 rl.close();
693 }
694}
695
696async cleanup() {
697 await this.mcp.close();
698}
699```
700
701### Main Entry Point
702
703Finally, we'll add the main execution logic:
704
705```typescript theme={null}
706async function main() {
707 if (process.argv.length < 3) {
708 console.log("Usage: node index.ts <path_to_server_script>");
709 return;
710 }
711 const mcpClient = new MCPClient();
712 try {
713 await mcpClient.connectToServer(process.argv[2]);
714 await mcpClient.chatLoop();
715 } catch (e) {
716 console.error("Error:", e);
717 await mcpClient.cleanup();
718 process.exit(1);
719 } finally {
720 await mcpClient.cleanup();
721 process.exit(0);
722 }
723}
724
725main();
726```
727
728## Running the Client
729
730To run your client with any MCP server:
731
732```bash theme={null}
733# Build TypeScript
734npm run build
735
736# Run the client
737node build/index.js path/to/server.py # python server
738node build/index.js path/to/build/index.js # node server
739```
740
741> [!NOTE]
742>
743> 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`
744
745**The client will:**
746
7471. Connect to the specified server
7482. List available tools
7493. Start an interactive chat session where you can:
750 * Enter queries
751 * See tool executions
752 * Get responses from Claude
753
754## How It Works
755
756When you submit a query:
757
7581. The client gets the list of available tools from the server
7592. Your query is sent to Claude along with tool descriptions
7603. Claude decides which tools (if any) to use
7614. The client executes any requested tool calls through the server
7625. Results are sent back to Claude
7636. Claude provides a natural language response
7647. The response is displayed to you
765
766## Best practices
767
7681. **Error Handling**
769 * Use TypeScript's type system for better error detection
770 * Wrap tool calls in try-catch blocks
771 * Provide meaningful error messages
772 * Gracefully handle connection issues
773
7742. **Security**
775 * Store API keys securely in `.env`
776 * Validate server responses
777 * Be cautious with tool permissions
778
779## Troubleshooting
780
781### Server Path Issues
782
783* Double-check the path to your server script is correct
784* Use the absolute path if the relative path isn't working
785* For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
786* Verify the server file has the correct extension (.js for Node.js or .py for Python)
787
788Example of correct path usage:
789
790```bash theme={null}
791# Relative path
792node build/index.js ./server/build/index.js
793
794# Absolute path
795node build/index.js /Users/username/projects/mcp-server/build/index.js
796
797# Windows path (either format works)
798node build/index.js C:/projects/mcp-server/build/index.js
799node build/index.js C:\\projects\\mcp-server\\build\\index.js
800```
801
802### Response Timing
803
804* The first response might take up to 30 seconds to return
805* This is normal and happens while:
806 * The server initializes
807 * Claude processes the query
808 * Tools are being executed
809* Subsequent responses are typically faster
810* Don't interrupt the process during this initial waiting period
811
812### Common Error Messages
813
814If you see:
815
816* `Error: Cannot find module`: Check your build folder and ensure TypeScript compilation succeeded
817* `Connection refused`: Ensure the server is running and the path is correct
818* `Tool execution failed`: Verify the tool's required environment variables are set
819* `ANTHROPIC_API_KEY is not set`: Check your .env file and environment variables
820* `TypeError`: Ensure you're using the correct types for tool arguments
821* `BadRequestError`: Ensure you have enough credits to access the Anthropic API
822
823**Java**
824
825> [!NOTE]
826>
827> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
828> To learn how to create sync and async MCP Clients manually, consult the [Java SDK Client](https://java.sdk.modelcontextprotocol.io/) documentation.
829
830This 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.
831[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)
832
833## System Requirements
834
835Before starting, ensure your system meets these requirements:
836
837* Java 17 or higher
838* Maven 3.6+
839* npx package manager
840* Anthropic API key (Claude)
841* Brave Search API key
842
843## Setting Up Your Environment
844
8451. Install npx (Node Package eXecute):
846 First, make sure to install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
847 and then run:
848
849 ```bash theme={null}
850 npm install -g npx
851 ```
852
8532. Clone the repository:
854
855 ```bash theme={null}
856 git clone https://github.com/spring-projects/spring-ai-examples.git
857 cd model-context-protocol/web-search/brave-chatbot
858 ```
859
8603. Set up your API keys:
861
862 ```bash theme={null}
863 export ANTHROPIC_API_KEY='your-anthropic-api-key-here'
864 export BRAVE_API_KEY='your-brave-api-key-here'
865 ```
866
8674. Build the application:
868
869 ```bash theme={null}
870 ./mvnw clean install
871 ```
872
8735. Run the application using Maven:
874 ```bash theme={null}
875 ./mvnw spring-boot:run
876 ```
877
878> [!WARNING]
879>
880> Make sure you keep your `ANTHROPIC_API_KEY` and `BRAVE_API_KEY` keys secure!
881
882## How it Works
883
884The application integrates Spring AI with the Brave Search MCP server through several components:
885
886### MCP Client Configuration
887
8881. Required dependencies in pom.xml:
889
890```xml theme={null}
891<dependency>
892 <groupId>org.springframework.ai</groupId>
893 <artifactId>spring-ai-starter-mcp-client</artifactId>
894</dependency>
895<dependency>
896 <groupId>org.springframework.ai</groupId>
897 <artifactId>spring-ai-starter-model-anthropic</artifactId>
898</dependency>
899```
900
9012. Application properties (application.yml):
902
903```yml theme={null}
904spring:
905 ai:
906 mcp:
907 client:
908 enabled: true
909 name: brave-search-client
910 version: 1.0.0
911 type: SYNC
912 request-timeout: 20s
913 stdio:
914 root-change-notification: true
915 servers-configuration: classpath:/mcp-servers-config.json
916 toolcallback:
917 enabled: true
918 anthropic:
919 api-key: ${ANTHROPIC_API_KEY}
920```
921
922This activates the `spring-ai-starter-mcp-client` to create one or more `McpClient`s based on the provided server configuration.
923The `spring.ai.mcp.client.toolcallback.enabled=true` property enables the tool callback mechanism, that automatically registers all MCP tool as spring ai tools.
924It is disabled by default.
925
9263. MCP Server Configuration (`mcp-servers-config.json`):
927
928```json theme={null}
929{
930 "mcpServers": {
931 "brave-search": {
932 "command": "npx",
933 "args": ["-y", "@modelcontextprotocol/server-brave-search"],
934 "env": {
935 "BRAVE_API_KEY": "<PUT YOUR BRAVE API KEY>"
699936 }
700937 }
701
702 async cleanup() {
703 await this.mcp.close();
938 }
939}
940```
941
942### Chat Implementation
943
944The chatbot is implemented using Spring AI's ChatClient with MCP tool integration:
945
946```java theme={null}
947var chatClient = chatClientBuilder
948 .defaultSystem("You are useful assistant, expert in AI and Java.")
949 .defaultToolCallbacks((Object[]) mcpToolAdapter.toolCallbacks())
950 .defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
951 .build();
952```
953
954Key features:
955
956* Uses Claude AI model for natural language understanding
957* Integrates Brave Search through MCP for real-time web search capabilities
958* Maintains conversation memory using InMemoryChatMemory
959* Runs as an interactive command-line application
960
961### Build and run
962
963```bash theme={null}
964./mvnw clean install
965java -jar ./target/ai-mcp-brave-chatbot-0.0.1-SNAPSHOT.jar
966```
967
968or
969
970```bash theme={null}
971./mvnw spring-boot:run
972```
973
974The 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.
975
976The chatbot can:
977
978* Answer questions using its built-in knowledge
979* Perform web searches when needed using Brave Search
980* Remember context from previous messages in the conversation
981* Combine information from multiple sources to provide comprehensive answers
982
983### Advanced Configuration
984
985The MCP client supports additional configuration options:
986
987* Client customization through `McpClientCustomizer<McpClient.SyncSpec>` or `McpClientCustomizer<McpClient.AsyncSpec>` beans
988* Multiple clients with multiple transport types: `STDIO` and Streamable HTTP
989* Integration with Spring AI's tool execution framework
990* Automatic client initialization and lifecycle management
991
992To connect to a remote MCP server over Streamable HTTP, configure a connection URL:
993
994```properties theme={null}
995spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080
996```
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-starter-mcp-client-webflux</artifactId>
1004</dependency>
1005```
1006
1007This provides similar functionality but uses a WebFlux-based Streamable HTTP 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()
7041126 }
705 ```
706
707 ### Main Entry Point
708
709 Finally, we'll add the main execution logic:
710
711 ```typescript theme={null}
712 async 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 }
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)
7291144 }
7301145
731 main();
732 ```
733
734 ## Running the Client
735
736 To run your client with any MCP server:
737
738 ```bash theme={null}
739 # Build TypeScript
740 npm run build
741
742 # Run the client
743 node build/index.js path/to/server.py # python server
744 node build/index.js path/to/build/index.js # node server
745 ```
746
747 <Note>
748 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`
749 </Note>
750
751 **The client will:**
752
753 1. Connect to the specified server
754 2. List available tools
755 3. 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
762 When you submit a query:
763
764 1. The client gets the list of available tools from the server
765 2. Your query is sent to Claude along with tool descriptions
766 3. Claude decides which tools (if any) to use
767 4. The client executes any requested tool calls through the server
768 5. Results are sent back to Claude
769 6. Claude provides a natural language response
770 7. The response is displayed to you
771
772 ## Best practices
773
774 1. **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
780 2. **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
794 Example of correct path usage:
795
796 ```bash theme={null}
797 # Relative path
798 node build/index.js ./server/build/index.js
799
800 # Absolute path
801 node build/index.js /Users/username/projects/mcp-server/build/index.js
802
803 # Windows path (either format works)
804 node build/index.js C:/projects/mcp-server/build/index.js
805 node 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
820 If 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 </Tab>
829
830 <Tab title="Java">
831 <Note>
832 This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
833 To learn how to create sync and async MCP Clients manually, consult the [Java SDK Client](https://java.sdk.modelcontextprotocol.io/) documentation.
834 </Note>
835
836 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.
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
841 Before 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
851 1. 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
859 2. 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
866 3. 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
873 4. Build the application:
874
875 ```bash theme={null}
876 ./mvnw clean install
877 ```
878
879 5. Run the application using Maven:
880 ```bash theme={null}
881 ./mvnw spring-boot:run
882 ```
883
884 <Warning>
885 Make sure you keep your `ANTHROPIC_API_KEY` and `BRAVE_API_KEY` keys secure!
886 </Warning>
887
888 ## How it Works
889
890 The application integrates Spring AI with the Brave Search MCP server through several components:
891
892 ### MCP Client Configuration
893
894 1. 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
907 2. Application properties (application.yml):
908
909 ```yml theme={null}
910 spring:
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
928 This activates the `spring-ai-starter-mcp-client` to create one or more `McpClient`s based on the provided server configuration.
929 The `spring.ai.mcp.client.toolcallback.enabled=true` property enables the tool callback mechanism, that automatically registers all MCP tool as spring ai tools.
930 It is disabled by default.
931
932 3. 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>"
942 }
943 }
944 }
945 }
946 ```
947
948 ### Chat Implementation
949
950 The chatbot is implemented using Spring AI's ChatClient with MCP tool integration:
951
952 ```java theme={null}
953 var 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
960 Key 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
971 java -jar ./target/ai-mcp-brave-chatbot-0.0.1-SNAPSHOT.jar
972 ```
973
974 or
975
976 ```bash theme={null}
977 ./mvnw spring-boot:run
978 ```
979
980 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.
981
982 The 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
991 The MCP client supports additional configuration options:
992
993 * Client customization through `McpClientCustomizer<McpClient.SyncSpec>` or `McpClientCustomizer<McpClient.AsyncSpec>` beans
994 * Multiple clients with multiple transport types: `STDIO` and Streamable HTTP
995 * Integration with Spring AI's tool execution framework
996 * Automatic client initialization and lifecycle management
997
998 To connect to a remote MCP server over Streamable HTTP, configure a connection URL:
999
1000 ```properties theme={null}
1001 spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080
1002 ```
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-starter-mcp-client-webflux</artifactId>
1010 </dependency>
1011 ```
1012
1013 This provides similar functionality but uses a WebFlux-based Streamable HTTP 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-opus-5")
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-opus-5")
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-opus-5")
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-opus-5")
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-opus-5",
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-opus-5",
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-opus-5"
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
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-opus-5"
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/2026-07-28/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-opus-5";
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/2026-07-28/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-opus-5";
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
No line in this hunk matches that.