Follow Discord
Sweep 30 Oct 2026 · 01:02Z Build v2.1.314 532 read Stable v2.1.308 Latest v2.1.314 Next v2.1.314 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · mcp

Build an MCP client changeddocs/draft/develop/build-client

Nearest release: v2.1.314, published 18 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 30 Oct 2026 18:13 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 30 Oct 2026 18:37 UTC.

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

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

The whole hunk

from line 6, old and new numbered
/
lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

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