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 capture · mcp

One read of Model Context Protocolmcp-20261030T183718Z

91 pages moved out of 359 read.

Pages moved 91 significant first
Pages read 359 in this capture
Captured 18:37 UTC
Corpus hash 972f3bd44853 corpus-hash

What this read moved

1-25 of 91, page 1 of 4

This capture is too large to show at once. Changes 1-25 of 91 are below, significant first; the rest are on the following screens.

community/contributing Changed · +19 / -19 lines

from line 427
427427 
428428### SDK Repositories
429429 
430> **[TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)**
430> [card] [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
431431 
432> **[Python SDK](https://github.com/modelcontextprotocol/python-sdk)**
432> [card] [Python SDK](https://github.com/modelcontextprotocol/python-sdk)
433433 
434> **[Go SDK](https://github.com/modelcontextprotocol/go-sdk)**
434> [card] [Go SDK](https://github.com/modelcontextprotocol/go-sdk)
435435 
436> **[Kotlin SDK](https://github.com/modelcontextprotocol/kotlin-sdk)**
436> [card] [Kotlin SDK](https://github.com/modelcontextprotocol/kotlin-sdk)
437437 
438> **[Java SDK](https://github.com/modelcontextprotocol/java-sdk)**
438> [card] [Java SDK](https://github.com/modelcontextprotocol/java-sdk)
439439 
440> **[C# SDK](https://github.com/modelcontextprotocol/csharp-sdk)**
440> [card] [C# SDK](https://github.com/modelcontextprotocol/csharp-sdk)
441441 
442> **[Swift SDK](https://github.com/modelcontextprotocol/swift-sdk)**
442> [card] [Swift SDK](https://github.com/modelcontextprotocol/swift-sdk)
443443 
444> **[Rust SDK](https://github.com/modelcontextprotocol/rust-sdk)**
444> [card] [Rust SDK](https://github.com/modelcontextprotocol/rust-sdk)
445445 
446> **[Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk)**
446> [card] [Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk)
447447 
448> **[PHP SDK](https://github.com/modelcontextprotocol/php-sdk)**
448> [card] [PHP SDK](https://github.com/modelcontextprotocol/php-sdk)
449449 
450450## Getting Help
451451 

docs/2024-11-05/develop/build-client Changed · +2464 / -2475 lines

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

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

docs/2024-11-05/develop/build-server Changed · +2784 / -2784 lines

### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather Service ### Create your Boot Application ### Running the server ## Testing your server with Claude for Desktop ## Testing your server with Java client ### Create an MCP Client manually ### Use MCP Client Boot Starter ## More Java MCP Server examples ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Setting up the instance ### Weather API helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather API helper functions ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up constants ### Helper methods ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing the Weather server and tools ### Implementing the ServerHandler ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop

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 24
2424 
2525This tutorial will primarily focus on tools.
2626 
27**Python**
28 
29Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30 
31### Prerequisite knowledge
32 
33This quickstart assumes you have familiarity with:
34 
35* Python
36* LLMs like Claude
37 
38### Logging in MCP Servers
39 
40When implementing MCP servers, be careful about how you handle logging:
41 
42**For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, but can be used safely with `file=sys.stderr`.
43 
44**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45 
46### Best Practices
47 
48* Use a logging library that writes to stderr or files.
49 
50### Quick Examples
51 
52```python theme={null}
53import sys
54import logging
55 
56# ❌ Bad (STDIO)
57print("Processing request")
58 
59# ✅ Good (STDIO)
60print("Processing request", file=sys.stderr)
61 
62# ✅ Good (STDIO)
63logging.info("Processing request")
64```
65 
66### System requirements
67 
68* Python 3.10 or higher installed.
69* You must use the Python MCP SDK 1.2.0 or higher.
70 
71### Set up your environment
72 
73First, let's install `uv` and set up our Python project and environment:
74 
75```bash macOS/Linux theme={null}
76curl -LsSf https://astral.sh/uv/install.sh | sh
77```
78 
79```powershell Windows theme={null}
80powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81```
82 
83Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84 
85Now, let's create and set up our project:
86 
87```bash macOS/Linux theme={null}
88# Create a new directory for our project
89uv init weather
90cd weather
91 
92# Create virtual environment and activate it
93uv venv
94source .venv/bin/activate
95 
96# Install dependencies
97uv add "mcp[cli]" httpx
98 
99# Create our server file
100touch weather.py
101```
102 
103```powershell Windows theme={null}
104# Create a new directory for our project
105uv init weather
106cd weather
107 
108# Create virtual environment and activate it
109uv venv
110.venv\Scripts\activate
111 
112# Install dependencies
113uv add mcp[cli] httpx
114 
115# Create our server file
116new-item weather.py
117```
118 
119Now let's dive into building your server.
120 
121## Building your server
122 
123### Importing packages and setting up the instance
124 
125Add these to the top of your `weather.py`:
126 
127```python theme={null}
128from typing import Any
129 
130import httpx
131from mcp.server.fastmcp import FastMCP
132 
133# Initialize FastMCP server
134mcp = FastMCP("weather")
135 
136# Constants
137NWS_API_BASE = "https://api.weather.gov"
138USER_AGENT = "weather-app/1.0"
139```
140 
141The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
142 
143### Helper functions
144 
145Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146 
147```python theme={null}
148async def make_nws_request(url: str) -> dict[str, Any] | None:
149 """Make a request to the NWS API with proper error handling."""
150 headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
151 async with httpx.AsyncClient() as client:
152 try:
153 response = await client.get(url, headers=headers, timeout=30.0)
154 response.raise_for_status()
155 return response.json()
156 except Exception:
157 return None
158 
159 
160def format_alert(feature: dict) -> str:
161 """Format an alert feature into a readable string."""
162 props = feature["properties"]
163 return f"""
164Event: {props.get("event", "Unknown")}
165Area: {props.get("areaDesc", "Unknown")}
166Severity: {props.get("severity", "Unknown")}
167Description: {props.get("description", "No description available")}
168Instructions: {props.get("instruction", "No specific instructions provided")}
169"""
170```
171 
172### Implementing tool execution
173 
174The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
175 
176```python theme={null}
177@mcp.tool()
178async def get_alerts(state: str) -> str:
179 """Get weather alerts for a US state.
180 
181 Args:
182 state: Two-letter US state code (e.g. CA, NY)
183 """
184 url = f"{NWS_API_BASE}/alerts/active/area/{state}"
185 data = await make_nws_request(url)
186 
187 if not data or "features" not in data:
188 return "Unable to fetch alerts or no alerts found."
189 
190 if not data["features"]:
191 return "No active alerts for this state."
192 
193 alerts = [format_alert(feature) for feature in data["features"]]
194 return "\n---\n".join(alerts)
195 
196 
197@mcp.tool()
198async def get_forecast(latitude: float, longitude: float) -> str:
199 """Get weather forecast for a location.
200 
201 Args:
202 latitude: Latitude of the location
203 longitude: Longitude of the location
204 """
205 # First get the forecast grid endpoint
206 points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
207 points_data = await make_nws_request(points_url)
208 
209 if not points_data:
210 return "Unable to fetch forecast data for this location."
211 
212 # Get the forecast URL from the points response
213 forecast_url = points_data["properties"]["forecast"]
214 forecast_data = await make_nws_request(forecast_url)
215 
216 if not forecast_data:
217 return "Unable to fetch detailed forecast."
218 
219 # Format the periods into a readable forecast
220 periods = forecast_data["properties"]["periods"]
221 forecasts = []
222 for period in periods[:5]: # Only show next 5 periods
223 forecast = f"""
224{period["name"]}:
225Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226Wind: {period["windSpeed"]} {period["windDirection"]}
227Forecast: {period["detailedForecast"]}
228"""
229 forecasts.append(forecast)
230 
231 return "\n---\n".join(forecasts)
232```
233 
234### Running the server
235 
236Finally, let's initialize and run the server:
237 
238```python theme={null}
239def main():
240 # Initialize and run the server
241 mcp.run(transport="stdio")
242 
243 
244if __name__ == "__main__":
245 main()
246```
247 
248Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249 
250Let's now test your server from an existing MCP host, Claude for Desktop.
251 
252## Testing your server with Claude for Desktop
253 
254> [!NOTE]
255>
256> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257 
258First, make sure you have Claude for Desktop installed. [You can install the latest version
259here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260 
261We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
262 
263For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264 
265```bash macOS/Linux theme={null}
266code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267```
268 
269```powershell Windows theme={null}
270code $env:AppData\Claude\claude_desktop_config.json
271```
272 
273You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
274 
275In this case, we'll add our single weather server like so:
276 
277```json macOS/Linux theme={null}
278{
279 "mcpServers": {
280 "weather": {
281 "command": "uv",
282 "args": [
283 "--directory",
284 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
285 "run",
286 "weather.py"
287 ]
288 }
289 }
290}
291```
292 
293```json Windows theme={null}
294{
295 "mcpServers": {
296 "weather": {
297 "command": "uv",
298 "args": [
299 "--directory",
300 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
301 "run",
302 "weather.py"
303 ]
304 }
305 }
306}
307```
308 
309> [!WARNING]
310>
311> You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
312 
313> [!NOTE]
314>
315> Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
316 
317This tells Claude for Desktop:
318 
3191. There's an MCP server named "weather"
3202. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321 
322Save the file, and restart **Claude for Desktop**.
323 
324**TypeScript**
325 
326Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
327 
328### Prerequisite knowledge
329 
330This quickstart assumes you have familiarity with:
331 
332* TypeScript
333* LLMs like Claude
334 
335### Logging in MCP Servers
336 
337When implementing MCP servers, be careful about how you handle logging:
338 
339**For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
340 
341**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
342 
343### Best Practices
344 
345* Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
346 
347### Quick Examples
348 
349```javascript theme={null}
350// ❌ Bad (STDIO)
351console.log("Server started");
352 
353// ✅ Good (STDIO)
354console.error("Server started"); // stderr is safe
355```
356 
357### System requirements
358 
359For TypeScript, make sure you have the latest version of Node installed.
360 
361### Set up your environment
362 
363First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364Verify your Node.js installation:
365 
366```bash theme={null}
367node --version
368npm --version
369```
370 
371For this tutorial, you'll need Node.js version 16 or higher.
372 
373Now, let's create and set up our project:
374 
375```bash macOS/Linux theme={null}
376# Create a new directory for our project
377mkdir weather
378cd weather
379 
380# Initialize a new npm project
381npm init -y
382 
383# Install dependencies
384npm install @modelcontextprotocol/sdk zod@3
385npm install -D @types/node typescript
386 
387# Create our files
388mkdir src
389touch src/index.ts
390```
391 
392```powershell Windows theme={null}
393# Create a new directory for our project
394md weather
395cd weather
396 
397# Initialize a new npm project
398npm init -y
399 
400# Install dependencies
401npm install @modelcontextprotocol/sdk zod@3
402npm install -D @types/node typescript
403 
404# Create our files
405md src
406new-item src\index.ts
407```
408 
409Update your package.json to add type: "module" and a build script:
410 
411```json package.json theme={null}
412{
413 "type": "module",
414 "bin": {
415 "weather": "./build/index.js"
416 },
417 "scripts": {
418 "build": "tsc && chmod 755 build/index.js"
419 },
420 "files": ["build"]
421}
422```
423 
424Create a `tsconfig.json` in the root of your project:
425 
426```json tsconfig.json theme={null}
427{
428 "compilerOptions": {
429 "target": "ES2022",
430 "module": "Node16",
431 "moduleResolution": "Node16",
432 "outDir": "./build",
433 "rootDir": "./src",
434 "strict": true,
435 "esModuleInterop": true,
436 "skipLibCheck": true,
437 "forceConsistentCasingInFileNames": true
438 },
439 "include": ["src/**/*"],
440 "exclude": ["node_modules"]
441}
442```
443 
444Now let's dive into building your server.
445 
446## Building your server
447 
448### Importing packages and setting up the instance
449 
450Add these to the top of your `src/index.ts`:
451 
452```typescript theme={null}
453import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455import { z } from "zod";
456 
457const NWS_API_BASE = "https://api.weather.gov";
458const USER_AGENT = "weather-app/1.0";
459 
460// Create server instance
461const server = new McpServer({
462 name: "weather",
463 version: "1.0.0",
464});
465```
466 
467### Helper functions
468 
469Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
470 
471```typescript theme={null}
472// Helper function for making NWS API requests
473async function makeNWSRequest<T>(url: string): Promise<T | null> {
474 const headers = {
475 "User-Agent": USER_AGENT,
476 Accept: "application/geo+json",
477 };
478 
479 try {
480 const response = await fetch(url, { headers });
481 if (!response.ok) {
482 throw new Error(`HTTP error! status: ${response.status}`);
483 }
484 return (await response.json()) as T;
485 } catch (error) {
486 console.error("Error making NWS request:", error);
487 return null;
488 }
489}
490 
491interface AlertFeature {
492 properties: {
493 event?: string;
494 areaDesc?: string;
495 severity?: string;
496 status?: string;
497 headline?: string;
498 };
499}
500 
501// Format alert data
502function formatAlert(feature: AlertFeature): string {
503 const props = feature.properties;
504 return [
505 `Event: ${props.event || "Unknown"}`,
506 `Area: ${props.areaDesc || "Unknown"}`,
507 `Severity: ${props.severity || "Unknown"}`,
508 `Status: ${props.status || "Unknown"}`,
509 `Headline: ${props.headline || "No headline"}`,
510 "---",
511 ].join("\n");
512}
513 
514interface ForecastPeriod {
515 name?: string;
516 temperature?: number;
517 temperatureUnit?: string;
518 windSpeed?: string;
519 windDirection?: string;
520 shortForecast?: string;
521}
522 
523interface AlertsResponse {
524 features: AlertFeature[];
525}
526 
527interface PointsResponse {
528 properties: {
529 forecast?: string;
530 };
531}
532 
533interface ForecastResponse {
534 properties: {
535 periods: ForecastPeriod[];
536 };
537}
538```
539 
540### Implementing tool execution
541 
542The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
543 
544```typescript theme={null}
545// Register weather tools
546 
547server.registerTool(
548 "get_alerts",
549 {
550 description: "Get weather alerts for a state",
551 inputSchema: {
552 state: z
553 .string()
554 .length(2)
555 .describe("Two-letter state code (e.g. CA, NY)"),
556 },
557 },
558 async ({ state }) => {
559 const stateCode = state.toUpperCase();
560 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
561 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
562 
563 if (!alertsData) {
564 return {
565 content: [
566 {
567 type: "text",
568 text: "Failed to retrieve alerts data",
569 },
570 ],
571 };
572 }
573 
574 const features = alertsData.features || [];
575 if (!features.length) {
576 return {
577 content: [
578 {
579 type: "text",
580 text: `No active alerts for ${stateCode}`,
581 },
582 ],
583 };
584 }
585 
586 const formattedAlerts = features.map(formatAlert);
587 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
588 
589 return {
590 content: [
591 {
592 type: "text",
593 text: alertsText,
594 },
595 ],
596 };
597 },
598);
599 
600server.registerTool(
601 "get_forecast",
602 {
603 description: "Get weather forecast for a location",
604 inputSchema: {
605 latitude: z
606 .number()
607 .min(-90)
608 .max(90)
609 .describe("Latitude of the location"),
610 longitude: z
611 .number()
612 .min(-180)
613 .max(180)
614 .describe("Longitude of the location"),
615 },
616 },
617 async ({ latitude, longitude }) => {
618 // Get grid point data
619 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
620 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
621 
622 if (!pointsData) {
623 return {
624 content: [
625 {
626 type: "text",
627 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
628 },
629 ],
630 };
631 }
632 
633 const forecastUrl = pointsData.properties?.forecast;
634 if (!forecastUrl) {
635 return {
636 content: [
637 {
638 type: "text",
639 text: "Failed to get forecast URL from grid point data",
640 },
641 ],
642 };
643 }
644 
645 // Get forecast data
646 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
647 if (!forecastData) {
648 return {
649 content: [
650 {
651 type: "text",
652 text: "Failed to retrieve forecast data",
653 },
654 ],
655 };
656 }
657 
658 const periods = forecastData.properties?.periods || [];
659 if (periods.length === 0) {
660 return {
661 content: [
662 {
663 type: "text",
664 text: "No forecast periods available",
665 },
666 ],
667 };
668 }
669 
670 // Format forecast periods
671 const formattedForecast = periods.map((period: ForecastPeriod) =>
672 [
673 `${period.name || "Unknown"}:`,
674 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
675 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
676 `${period.shortForecast || "No forecast available"}`,
677 "---",
678 ].join("\n"),
679 );
680 
681 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
682 
683 return {
684 content: [
685 {
686 type: "text",
687 text: forecastText,
688 },
689 ],
690 };
691 },
692);
693```
694 
695### Running the server
696 
697Finally, implement the main function to run the server:
698 
699```typescript theme={null}
700async function main() {
701 const transport = new StdioServerTransport();
702 await server.connect(transport);
703 console.error("Weather MCP Server running on stdio");
704}
705 
706main().catch((error) => {
707 console.error("Fatal error in main():", error);
708 process.exit(1);
709});
710```
711 
712Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713 
714Let's now test your server from an existing MCP host, Claude for Desktop.
715 
716## Testing your server with Claude for Desktop
717 
718> [!NOTE]
719>
720> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721 
722First, make sure you have Claude for Desktop installed. [You can install the latest version
723here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724 
725We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726 
727For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728 
729```bash macOS/Linux theme={null}
730code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731```
732 
733```powershell Windows theme={null}
734code $env:AppData\Claude\claude_desktop_config.json
735```
736 
737You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
738 
739In this case, we'll add our single weather server like so:
740 
741```json macOS/Linux theme={null}
742{
743 "mcpServers": {
744 "weather": {
745 "command": "node",
746 "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
747 }
748 }
749}
750```
751 
752```json Windows theme={null}
753{
754 "mcpServers": {
755 "weather": {
756 "command": "node",
757 "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
758 }
759 }
760}
761```
762 
763This tells Claude for Desktop:
764 
7651. There's an MCP server named "weather"
7662. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767 
768Save the file, and restart **Claude for Desktop**.
769 
770**Java**
771 
772> [!NOTE]
773>
774> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
775> To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
776 
777Let's get started with building our weather server!
778[You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
779 
780For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
781For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782 
783### Logging in MCP Servers
784 
785When implementing MCP servers, be careful about how you handle logging:
786 
787**For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
788 
789**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
790 
791### Best Practices
792 
793* Use a logging library that writes to stderr or files.
794* Ensure any configured logging library will not write to stdout.
795 
796### System requirements
797 
798* Java 17 or higher installed.
799* [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
800 
801### Set up your environment
802 
803Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804 
805You will need to add the following dependencies:
806 
807```xml Maven theme={null}
808<dependencies>
809 <dependency>
810 <groupId>org.springframework.ai</groupId>
811 <artifactId>spring-ai-starter-mcp-server</artifactId>
812 </dependency>
813 
814 <dependency>
815 <groupId>org.springframework</groupId>
816 <artifactId>spring-web</artifactId>
817 </dependency>
818</dependencies>
819```
820 
821```groovy Gradle theme={null}
822dependencies {
823 implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824 implementation platform("org.springframework:spring-web")
825}
826```
827 
828Then configure your application by setting the application properties:
829 
830```bash application.properties theme={null}
831spring.main.bannerMode=off
832logging.pattern.console=
833```
834 
835```yaml application.yml theme={null}
836logging:
837 pattern:
838 console:
839spring:
840 main:
841 banner-mode: off
842```
843 
844The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
845 
846Now let's dive into building your server.
847 
848## Building your server
849 
850### Weather Service
851 
852Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
853 
854```java theme={null}
855@Service
856public class WeatherService {
857 
858 private final RestClient restClient;
859 
860 public WeatherService() {
861 this.restClient = RestClient.builder()
862 .baseUrl("https://api.weather.gov")
863 .defaultHeader("Accept", "application/geo+json")
864 .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
865 .build();
866 }
867 
868 @Tool(description = "Get weather forecast for a specific latitude/longitude")
869 public String getWeatherForecastByLocation(
870 double latitude, // Latitude coordinate
871 double longitude // Longitude coordinate
872 ) {
873 // Returns detailed forecast including:
874 // - Temperature and unit
875 // - Wind speed and direction
876 // - Detailed forecast description
877 }
878 
879 @Tool(description = "Get weather alerts for a US state")
880 public String getAlerts(
881 @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
882 ) {
883 // Returns active alerts including:
884 // - Event type
885 // - Affected area
886 // - Severity
887 // - Description
888 // - Safety instructions
889 }
890 
891 // ......
892}
893```
894 
895The `@Service` annotation will auto-register the service in your application context.
896The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897 
898The auto-configuration will automatically register these tools with the MCP server.
899 
900### Create your Boot Application
901 
902```java theme={null}
903@SpringBootApplication
904public class McpServerApplication {
905 
906 public static void main(String[] args) {
907 SpringApplication.run(McpServerApplication.class, args);
908 }
909 
910 @Bean
911 public ToolCallbackProvider weatherTools(WeatherService weatherService) {
912 return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
913 }
914}
915```
916 
917Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918 
919### Running the server
920 
921Finally, let's build the server:
922 
923```bash theme={null}
924./mvnw clean install
925```
926 
927This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928 
929Let's now test your server from an existing MCP host, Claude for Desktop.
930 
931## Testing your server with Claude for Desktop
932 
933> [!NOTE]
934>
935> Claude for Desktop is not yet available on Linux.
936 
937First, make sure you have Claude for Desktop installed.
938[You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939 
940We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942Make sure to create the file if it doesn't exist.
943 
944For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945 
946```bash macOS/Linux theme={null}
947code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948```
949 
950```powershell Windows theme={null}
951code $env:AppData\Claude\claude_desktop_config.json
952```
953 
954You'll then add your servers in the `mcpServers` key.
955The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956 
957In this case, we'll add our single weather server like so:
958 
959```json macOS/Linux theme={null}
960{
961 "mcpServers": {
962 "spring-ai-mcp-weather": {
963 "command": "java",
964 "args": [
965 "-Dspring.ai.mcp.server.stdio=true",
966 "-jar",
967 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
968 ]
969 }
970 }
971}
972```
973 
974```json Windows theme={null}
975{
976 "mcpServers": {
977 "spring-ai-mcp-weather": {
978 "command": "java",
979 "args": [
980 "-Dspring.ai.mcp.server.transport=STDIO",
981 "-jar",
982 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
983 ]
984 }
985 }
986}
987```
988 
989> [!NOTE]
990>
991> Make sure you pass in the absolute path to your server.
992 
993This tells Claude for Desktop:
994 
9951. There's an MCP server named "my-weather-server"
9962. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997 
998Save the file, and restart **Claude for Desktop**.
999 
1000## Testing your server with Java client
1001 
1002### Create an MCP Client manually
1003 
1004Use the `McpClient` to connect to the server:
1005 
1006```java theme={null}
1007var stdioParams = ServerParameters.builder("java")
1008 .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009 .build();
1010 
1011var stdioTransport = new StdioClientTransport(stdioParams);
1012 
1013var mcpClient = McpClient.sync(stdioTransport).build();
1014 
1015mcpClient.initialize();
1016 
1017ListToolsResult toolsList = mcpClient.listTools();
1018 
1019CallToolResult weather = mcpClient.callTool(
1020 new CallToolRequest("getWeatherForecastByLocation",
1021 Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022 
1023CallToolResult alert = mcpClient.callTool(
1024 new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025 
1026mcpClient.closeGracefully();
1027```
1028 
1029### Use MCP Client Boot Starter
1030 
1031Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1032 
1033```xml theme={null}
1034<dependency>
1035 <groupId>org.springframework.ai</groupId>
1036 <artifactId>spring-ai-starter-mcp-client</artifactId>
1037</dependency>
1038```
1039 
1040and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041You can reuse the existing Anthropic Desktop configuration:
1042 
1043```properties theme={null}
1044spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045```
1046 
1047When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048 
1049For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1050 
1051## More Java MCP Server examples
1052 
1053The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an MCP server using SSE transport.
1054It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056**Kotlin**
1057 
1058Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1059 
1060### Prerequisite knowledge
1061 
1062This quickstart assumes you have familiarity with:
1063 
1064* Kotlin
1065* LLMs like Claude
1066 
1067### Logging in MCP Servers
1068 
1069When implementing MCP servers, be careful about how you handle logging:
1070 
1071**For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1072 
1073**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1074 
1075### Best Practices
1076 
1077* Use a logging library that writes to stderr or files.
1078 
1079### System requirements
1080 
1081* JDK 11 or higher installed.
1082 
1083### Set up your environment
1084 
1085First, let's install `java` and `gradle` if you haven't already.
1086You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087Verify your `java` installation:
1088 
1089```bash theme={null}
1090java --version
1091```
1092 
1093Now, let's create and set up your project:
1094 
1095```bash macOS/Linux theme={null}
1096# Create a new directory for our project
1097mkdir weather
1098cd weather
1099 
1100# Initialize a new kotlin project
1101gradle init
1102```
1103 
1104```powershell Windows theme={null}
1105# Create a new directory for our project
1106md weather
1107cd weather
1108 
1109# Initialize a new kotlin project
1110gradle init
1111```
1112 
1113After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114 
1115Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116 
1117After creating the project, replace the contents of your `build.gradle.kts` with:
1118 
1119```kotlin build.gradle.kts theme={null}
1120// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1121val mcpVersion = "0.9.0"
1122val ktorVersion = "3.2.3"
1123val slf4jVersion = "2.0.17"
1124 
1125plugins {
1126 kotlin("jvm") version "2.3.20"
1127 kotlin("plugin.serialization") version "2.3.20"
1128 id("com.gradleup.shadow") version "8.3.9"
1129 application
1130}
1131 
1132application {
1133 mainClass.set("MainKt")
1134}
1135 
1136dependencies {
1137 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1138 implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1139 implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1140 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1141 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1142}
1143```
1144 
1145Verify that everything is set up correctly:
1146 
1147```bash theme={null}
1148./gradlew build
1149```
1150 
1151Now let's dive into building your server.
1152 
1153## Building your server
1154 
1155### Setting up the instance
1156 
1157Add a server initialization function:
1158 
1159```kotlin theme={null}
1160fun runMcpServer() {
1161 val server = Server(
1162 Implementation(
1163 name = "weather",
1164 version = "1.0.0",
1165 ),
1166 ServerOptions(
1167 capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1168 ),
1169 )
1170 
1171 // register tools on server here
1172 
1173 val transport = StdioServerTransport(
1174 System.`in`.asInput(),
1175 System.out.asSink().buffered(),
1176 )
1177 
1178 runBlocking {
1179 val session = server.createSession(transport)
1180 val done = Job()
1181 session.onClose {
1182 done.complete()
1183 }
1184 done.join()
1185 }
1186}
1187```
1188 
1189### Weather API helper functions
1190 
1191Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192 
1193```kotlin theme={null}
1194val httpClient = HttpClient(CIO) {
1195 defaultRequest {
1196 url("https://api.weather.gov")
1197 headers {
1198 append("Accept", "application/geo+json")
1199 append("User-Agent", "WeatherApiClient/1.0")
1200 }
1201 contentType(ContentType.Application.Json)
1202 }
1203 install(ContentNegotiation) {
1204 json(Json { ignoreUnknownKeys = true })
1205 }
1206}
1207 
1208// Extension function to fetch weather alerts for a given state
1209suspend fun HttpClient.getAlerts(state: String): List<String> {
1210 val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211 return alerts.features.map { feature ->
1212 """
1213 Event: ${feature.properties.event}
1214 Area: ${feature.properties.areaDesc}
1215 Severity: ${feature.properties.severity}
1216 Status: ${feature.properties.status}
1217 Headline: ${feature.properties.headline}
1218 """.trimIndent()
1219 }
1220}
1221 
1222// Extension function to fetch forecast information for given latitude and longitude
1223suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1224 val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1225 val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1226 val forecast = this.get(forecastUrl).body<ForecastResponse>()
1227 return forecast.properties.periods.map { period ->
1228 """
1229 ${period.name}:
1230 Temperature: ${period.temperature}°${period.temperatureUnit}
1231 Wind: ${period.windSpeed} ${period.windDirection}
1232 ${period.shortForecast}
1233 """.trimIndent()
1234 }
1235}
1236 
1237@Serializable
1238data class PointsResponse(val properties: PointsProperties)
1239 
1240@Serializable
1241data class PointsProperties(val forecast: String? = null)
1242 
1243@Serializable
1244data class ForecastResponse(val properties: ForecastProperties)
1245 
1246@Serializable
1247data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248 
1249@Serializable
1250data class ForecastPeriod(
1251 val name: String? = null,
1252 val temperature: Int? = null,
1253 val temperatureUnit: String? = null,
1254 val windSpeed: String? = null,
1255 val windDirection: String? = null,
1256 val shortForecast: String? = null,
1257)
1258 
1259@Serializable
1260data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261 
1262@Serializable
1263data class AlertFeature(val properties: AlertProperties)
1264 
1265@Serializable
1266data class AlertProperties(
1267 val event: String? = null,
1268 val areaDesc: String? = null,
1269 val severity: String? = null,
1270 val status: String? = null,
1271 val headline: String? = null,
1272)
1273```
1274 
1275### Implementing tool execution
1276 
1277The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1278 
1279```kotlin theme={null}
1280// Register weather tools
1281 
1282server.addTool(
1283 name = "get_alerts",
1284 description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1285 inputSchema = ToolSchema(
1286 properties = buildJsonObject {
1287 putJsonObject("state") {
1288 put("type", "string")
1289 put("description", "Two-letter US state code (e.g. CA, NY)")
1290 }
1291 },
1292 required = listOf("state"),
1293 ),
1294) { request ->
1295 val state = request.arguments?.get("state")?.jsonPrimitive?.content
1296 ?: return@addTool CallToolResult(
1297 content = listOf(TextContent("The 'state' parameter is required.")),
1298 )
1299 
1300 val alerts = httpClient.getAlerts(state)
1301 CallToolResult(content = alerts.map { TextContent(it) })
1302}
1303 
1304server.addTool(
1305 name = "get_forecast",
1306 description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1307 inputSchema = ToolSchema(
1308 properties = buildJsonObject {
1309 putJsonObject("latitude") {
1310 put("type", "number")
1311 put("description", "Latitude of the location")
1312 }
1313 putJsonObject("longitude") {
1314 put("type", "number")
1315 put("description", "Longitude of the location")
1316 }
1317 },
1318 required = listOf("latitude", "longitude"),
1319 ),
1320) { request ->
1321 val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1322 val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1323 if (latitude == null || longitude == null) {
1324 return@addTool CallToolResult(
1325 content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1326 )
1327 }
1328 
1329 val forecast = httpClient.getForecast(latitude, longitude)
1330 CallToolResult(content = forecast.map { TextContent(it) })
1331}
1332```
1333 
1334### Running the server
1335 
1336Finally, implement the main function to run the server:
1337 
1338```kotlin theme={null}
1339fun main() = runMcpServer()
1340```
1341 
1342You can run the server directly during development:
1343 
1344```bash theme={null}
1345./gradlew run
1346```
1347 
1348For production use, build the shadow JAR:
1349 
1350```bash theme={null}
1351./gradlew build
1352java -jar build/libs/weather-0.1.0-all.jar
1353```
1354 
1355Let's now test your server from an existing MCP host, Claude for Desktop.
1356 
1357## Testing your server with Claude for Desktop
1358 
1359> [!NOTE]
1360>
1361> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362 
1363First, make sure you have Claude for Desktop installed. [You can install the latest version
1364here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365 
1366We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368Make sure to create the file if it doesn't exist.
1369 
1370For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371 
1372```bash macOS/Linux theme={null}
1373code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374```
1375 
1376```powershell Windows theme={null}
1377code $env:AppData\Claude\claude_desktop_config.json
1378```
1379 
1380You'll then add your servers in the `mcpServers` key.
1381The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382 
1383In this case, we'll add our single weather server like so:
1384 
1385```json macOS/Linux theme={null}
1386{
1387 "mcpServers": {
1388 "weather": {
1389 "command": "java",
1390 "args": [
1391 "-jar",
1392 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1393 ]
1394 }
1395 }
1396}
1397```
1398 
1399```json Windows theme={null}
1400{
1401 "mcpServers": {
1402 "weather": {
1403 "command": "java",
1404 "args": [
1405 "-jar",
1406 "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1407 ]
1408 }
1409 }
1410}
1411```
1412 
1413This tells Claude for Desktop:
1414 
14151. There's an MCP server named "weather"
14162. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417 
1418Save the file, and restart **Claude for Desktop**.
1419 
1420**C#**
1421 
1422Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1423 
1424### Prerequisite knowledge
1425 
1426This quickstart assumes you have familiarity with:
1427 
1428* C#
1429* LLMs like Claude
1430* .NET 8 or higher
1431 
1432### Logging in MCP Servers
1433 
1434When implementing MCP servers, be careful about how you handle logging:
1435 
1436**For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1437 
1438**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1439 
1440### Best Practices
1441 
1442* Use a logging library that writes to stderr or files.
1443 
1444### System requirements
1445 
1446* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1447 
1448### Set up your environment
1449 
1450First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1451 
1452```bash theme={null}
1453dotnet --version
1454```
1455 
1456Now, let's create and set up your project:
1457 
1458```bash macOS/Linux theme={null}
1459# Create a new directory for our project
1460mkdir weather
1461cd weather
1462# Initialize a new C# project
1463dotnet new console
1464```
1465 
1466```powershell Windows theme={null}
1467# Create a new directory for our project
1468mkdir weather
1469cd weather
1470# Initialize a new C# project
1471dotnet new console
1472```
1473 
1474After running `dotnet new console`, you will be presented with a new C# project.
1475You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1477After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1478 
1479```bash theme={null}
1480# Add the Model Context Protocol SDK NuGet package
1481dotnet add package ModelContextProtocol --prerelease
1482# Add the .NET Hosting NuGet package
1483dotnet add package Microsoft.Extensions.Hosting
1484```
1485 
1486Now let’s dive into building your server.
1487 
1488## Building your server
1489 
1490Open the `Program.cs` file in your project and replace its contents with the following code:
1491 
1492```csharp theme={null}
1493using Microsoft.Extensions.DependencyInjection;
1494using Microsoft.Extensions.Hosting;
1495using ModelContextProtocol;
1496using System.Net.Http.Headers;
1497 
1498var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499 
1500builder.Services.AddMcpServer()
1501 .WithStdioServerTransport()
1502 .WithToolsFromAssembly();
1503 
1504builder.Services.AddSingleton(_ =>
1505{
1506 var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1507 client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1508 return client;
1509});
1510 
1511var app = builder.Build();
1512 
1513await app.RunAsync();
1514```
1515 
1516> [!NOTE]
1517>
1518> When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1519 
1520This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1521 
1522### Weather API helper functions
1523 
1524Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525 
1526```csharp theme={null}
1527using System.Text.Json;
1528 
1529internal static class HttpClientExt
1530{
1531 public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1532 {
1533 using var response = await client.GetAsync(requestUri);
1534 response.EnsureSuccessStatusCode();
1535 return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1536 }
1537}
1538```
1539 
1540Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541 
1542```csharp theme={null}
1543using ModelContextProtocol.Server;
1544using System.ComponentModel;
1545using System.Globalization;
1546using System.Text.Json;
1547 
1548namespace QuickstartWeatherServer.Tools;
1549 
1550[McpServerToolType]
1551public static class WeatherTools
1552{
1553 [McpServerTool, Description("Get weather alerts for a US state code.")]
1554 public static async Task<string> GetAlerts(
1555 HttpClient client,
1556 [Description("The US state code to get alerts for.")] string state)
1557 {
1558 using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1559 var jsonElement = jsonDocument.RootElement;
1560 var alerts = jsonElement.GetProperty("features").EnumerateArray();
1561 
1562 if (!alerts.Any())
1563 {
1564 return "No active alerts for this state.";
1565 }
1566 
1567 return string.Join("\n--\n", alerts.Select(alert =>
1568 {
1569 JsonElement properties = alert.GetProperty("properties");
1570 return $"""
1571 Event: {properties.GetProperty("event").GetString()}
1572 Area: {properties.GetProperty("areaDesc").GetString()}
1573 Severity: {properties.GetProperty("severity").GetString()}
1574 Description: {properties.GetProperty("description").GetString()}
1575 Instruction: {properties.GetProperty("instruction").GetString()}
1576 """;
1577 }));
1578 }
1579 
1580 [McpServerTool, Description("Get weather forecast for a location.")]
1581 public static async Task<string> GetForecast(
1582 HttpClient client,
1583 [Description("Latitude of the location.")] double latitude,
1584 [Description("Longitude of the location.")] double longitude)
1585 {
1586 var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1587 using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1588 var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1589 ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1590 
1591 using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1592 var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1593 
1594 return string.Join("\n---\n", periods.Select(period => $"""
1595 {period.GetProperty("name").GetString()}
1596 Temperature: {period.GetProperty("temperature").GetInt32()}°F
1597 Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1598 Forecast: {period.GetProperty("detailedForecast").GetString()}
1599 """));
1600 }
1601}
1602```
1603 
1604### Running the server
1605 
1606Finally, run the server using the following command:
1607 
1608```bash theme={null}
1609dotnet run
1610```
1611 
1612This will start the server and listen for incoming requests on standard input/output.
1613 
1614## Testing your server with Claude for Desktop
1615 
1616> [!NOTE]
1617>
1618> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619 
1620First, make sure you have Claude for Desktop installed. [You can install the latest version
1621here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1623For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624 
1625```bash macOS/Linux theme={null}
1626code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627```
1628 
1629```powershell Windows theme={null}
1630code $env:AppData\Claude\claude_desktop_config.json
1631```
1632 
1633You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1634In this case, we'll add our single weather server like so:
1635 
1636```json macOS/Linux theme={null}
1637{
1638 "mcpServers": {
1639 "weather": {
1640 "command": "dotnet",
1641 "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1642 }
1643 }
1644}
1645```
1646 
1647```json Windows theme={null}
1648{
1649 "mcpServers": {
1650 "weather": {
1651 "command": "dotnet",
1652 "args": [
1653 "run",
1654 "--project",
1655 "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1656 "--no-build"
1657 ]
1658 }
1659 }
1660}
1661```
1662 
1663This tells Claude for Desktop:
1664 
16651. There's an MCP server named "weather"
16662. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667 Save the file, and restart **Claude for Desktop**.
1668 
1669**Ruby**
1670 
1671Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1672 
1673### Prerequisite knowledge
1674 
1675This quickstart assumes you have familiarity with:
1676 
1677* Ruby
1678* LLMs like Claude
1679 
1680### Logging in MCP Servers
1681 
1682When implementing MCP servers, be careful about how you handle logging:
1683 
1684**For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1685 
1686**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1687 
1688### Best Practices
1689 
1690* Use a logging library that writes to stderr or files.
1691 
1692### Quick Examples
1693 
1694```ruby theme={null}
1695# ❌ Bad (STDIO)
1696puts "Processing request"
1697 
1698# ✅ Good (STDIO)
1699require "logger"
1700logger = Logger.new($stderr)
1701logger.info("Processing request")
1702```
1703 
1704### System requirements
1705 
1706* Ruby 2.7 or higher installed.
1707 
1708### Set up your environment
1709 
1710First, let's make sure you have Ruby installed. You can check by running:
1711 
1712```bash theme={null}
1713ruby --version
1714```
1715 
1716Now, let's create and set up our project:
1717 
1718```bash macOS/Linux theme={null}
1719# Create a new directory for our project
1720mkdir weather
1721cd weather
1722 
1723# Create a Gemfile
1724bundle init
1725 
1726# Add the MCP SDK dependency
1727bundle add mcp
1728 
1729# Create our server file
1730touch weather.rb
1731```
1732 
1733```powershell Windows theme={null}
1734# Create a new directory for our project
1735mkdir weather
1736cd weather
1737 
1738# Create a Gemfile
1739bundle init
1740 
1741# Add the MCP SDK dependency
1742bundle add mcp
1743 
1744# Create our server file
1745new-item weather.rb
1746```
1747 
1748Now let's dive into building your server.
1749 
1750## Building your server
1751 
1752### Importing packages and setting up constants
1753 
1754Open `weather.rb` and add these requires and constants at the top:
1755 
1756```ruby theme={null}
1757require "json"
1758require "mcp"
1759require "net/http"
1760require "uri"
1761 
1762NWS_API_BASE = "https://api.weather.gov"
1763USER_AGENT = "weather-app/1.0"
1764```
1765 
1766The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767 
1768### Helper methods
1769 
1770Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771 
1772```ruby theme={null}
1773module HelperMethods
1774 def make_nws_request(url)
1775 uri = URI(url)
1776 request = Net::HTTP::Get.new(uri)
1777 request["User-Agent"] = USER_AGENT
1778 request["Accept"] = "application/geo+json"
1779 
1780 response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1781 http.request(request)
1782 end
1783 
1784 raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1785 
1786 JSON.parse(response.body)
1787 end
1788 
1789 def format_alert(feature)
1790 properties = feature["properties"]
1791 
1792 <<~ALERT
1793 Event: #{properties["event"] || "Unknown"}
1794 Area: #{properties["areaDesc"] || "Unknown"}
1795 Severity: #{properties["severity"] || "Unknown"}
1796 Description: #{properties["description"] || "No description available"}
1797 Instructions: #{properties["instruction"] || "No specific instructions provided"}
1798 ALERT
1799 end
1800end
1801```
1802 
1803### Implementing tool execution
1804 
1805Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806 
1807```ruby theme={null}
1808class GetAlerts < MCP::Tool
1809 extend HelperMethods
1810 
1811 tool_name "get_alerts"
1812 description "Get weather alerts for a US state"
1813 input_schema(
1814 properties: {
1815 state: {
1816 type: "string",
1817 description: "Two-letter US state code (e.g. CA, NY)"
1818 }
1819 },
1820 required: ["state"]
1821 )
1822 
1823 def self.call(state:)
1824 url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1825 data = make_nws_request(url)
1826 
1827 if data["features"].empty?
1828 return MCP::Tool::Response.new([{
1829 type: "text",
1830 text: "No active alerts for this state."
1831 }])
1832 end
1833 
1834 alerts = data["features"].map { |feature| format_alert(feature) }
1835 MCP::Tool::Response.new([{
1836 type: "text",
1837 text: alerts.join("\n---\n")
1838 }])
1839 end
1840end
1841 
1842class GetForecast < MCP::Tool
1843 extend HelperMethods
1844 
1845 tool_name "get_forecast"
1846 description "Get weather forecast for a location"
1847 input_schema(
1848 properties: {
1849 latitude: {
1850 type: "number",
1851 description: "Latitude of the location"
1852 },
1853 longitude: {
1854 type: "number",
1855 description: "Longitude of the location"
1856 }
1857 },
1858 required: ["latitude", "longitude"]
1859 )
1860 
1861 def self.call(latitude:, longitude:)
1862 # First get the forecast grid endpoint.
1863 points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1864 points_data = make_nws_request(points_url)
1865 
1866 # Get the forecast URL from the points response.
1867 forecast_url = points_data["properties"]["forecast"]
1868 forecast_data = make_nws_request(forecast_url)
1869 
1870 # Format the periods into a readable forecast.
1871 periods = forecast_data["properties"]["periods"]
1872 forecasts = periods.first(5).map do |period|
1873 <<~FORECAST
1874 #{period["name"]}:
1875 Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1876 Wind: #{period["windSpeed"]} #{period["windDirection"]}
1877 Forecast: #{period["detailedForecast"]}
1878 FORECAST
1879 end
1880 
1881 MCP::Tool::Response.new([{
1882 type: "text",
1883 text: forecasts.join("\n---\n")
1884 }])
1885 end
1886end
1887```
1888 
1889### Running the server
1890 
1891Finally, initialize and run the server:
1892 
1893```ruby theme={null}
1894server = MCP::Server.new(
1895 name: "weather",
1896 version: "1.0.0",
1897 tools: [GetAlerts, GetForecast]
1898)
1899 
1900transport = MCP::Server::Transports::StdioTransport.new(server)
1901transport.open
1902```
1903 
1904Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905 
1906Let's now test your server from an existing MCP host, Claude for Desktop.
1907 
1908## Testing your server with Claude for Desktop
1909 
1910> [!NOTE]
1911>
1912> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913 
1914First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1915 
1916We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1917 
1918For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919 
1920```bash macOS/Linux theme={null}
1921code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922```
1923 
1924```powershell Windows theme={null}
1925code $env:AppData\Claude\claude_desktop_config.json
1926```
1927 
1928You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1929 
1930In this case, we'll add our single weather server like so:
1931 
1932```json macOS/Linux theme={null}
1933{
1934 "mcpServers": {
1935 "weather": {
1936 "command": "bundle",
1937 "args": ["exec", "ruby", "weather.rb"],
1938 "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1939 }
1940 }
1941}
1942```
1943 
1944```json Windows theme={null}
1945{
1946 "mcpServers": {
1947 "weather": {
1948 "command": "bundle",
1949 "args": ["exec", "ruby", "weather.rb"],
1950 "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1951 }
1952 }
1953}
1954```
1955 
1956> [!NOTE]
1957>
1958> Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1959 
1960This tells Claude for Desktop:
1961 
19621. There's an MCP server named "weather"
19632. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964 
1965Save the file, and restart **Claude for Desktop**.
1966 
1967**Rust**
1968 
1969Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1970 
1971### Prerequisite knowledge
1972 
1973This quickstart assumes you have familiarity with:
1974 
1975* Rust programming language
1976* Async/await in Rust
1977* LLMs like Claude
1978 
1979### Logging in MCP Servers
1980 
1981When implementing MCP servers, be careful about how you handle logging:
1982 
1983**For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1984 
1985**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1986 
1987### Best Practices
1988 
1989* Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1990* Configure your logging framework to avoid stdout output.
1991 
1992### Quick Examples
1993 
1994```rust theme={null}
1995// ❌ Bad (STDIO)
1996println!("Processing request");
1997 
1998// ✅ Good (STDIO)
1999eprintln!("Processing request"); // writes to stderr
2000```
2001 
2002### System requirements
2003 
2004* Rust 1.70 or higher installed.
2005* Cargo (comes with Rust installation).
2006 
2007### Set up your environment
2008 
2009First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2010 
2011```bash macOS/Linux theme={null}
2012curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2013```
2014 
2015```powershell Windows theme={null}
2016# Download and run rustup-init.exe from https://rustup.rs/
2017```
2018 
2019Verify your Rust installation:
2020 
2021```bash theme={null}
2022rustc --version
2023cargo --version
2024```
2025 
2026Now, let's create and set up our project:
2027 
2028```bash macOS/Linux theme={null}
2029# Create a new Rust project
2030cargo new weather
2031cd weather
2032```
2033 
2034```powershell Windows theme={null}
2035# Create a new Rust project
2036cargo new weather
2037cd weather
2038```
2039 
2040Update your `Cargo.toml` to add the required dependencies:
2041 
2042```toml Cargo.toml theme={null}
2043[package]
2044name = "weather"
2045version = "0.1.0"
2046edition = "2024"
2047 
2048[dependencies]
2049rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050tokio = { version = "1.46", features = ["full"] }
2051reqwest = { version = "0.12", features = ["json"] }
2052serde = { version = "1.0", features = ["derive"] }
2053serde_json = "1.0"
2054anyhow = "1.0"
2055tracing = "0.1"
2056tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057```
2058 
2059Now let's dive into building your server.
2060 
2061## Building your server
2062 
2063### Importing packages and constants
2064 
2065Open `src/main.rs` and add these imports and constants at the top:
2066 
2067```rust theme={null}
2068use anyhow::Result;
2069use rmcp::{
2070 ServerHandler, ServiceExt,
2071 handler::server::{router::tool::ToolRouter, tool::Parameters},
2072 model::*,
2073 schemars, tool, tool_handler, tool_router,
2074};
2075use serde::Deserialize;
2076use serde::de::DeserializeOwned;
2077 
2078const NWS_API_BASE: &str = "https://api.weather.gov";
2079const USER_AGENT: &str = "weather-app/1.0";
2080```
2081 
2082The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2083 
2084### Data structures
2085 
2086Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087 
2088```rust theme={null}
2089#[derive(Debug, Deserialize)]
2090struct AlertsResponse {
2091 features: Vec<AlertFeature>,
2092}
2093 
2094#[derive(Debug, Deserialize)]
2095struct AlertFeature {
2096 properties: AlertProperties,
2097}
2098 
2099#[derive(Debug, Deserialize)]
2100struct AlertProperties {
2101 event: Option<String>,
2102 #[serde(rename = "areaDesc")]
2103 area_desc: Option<String>,
2104 severity: Option<String>,
2105 description: Option<String>,
2106 instruction: Option<String>,
2107}
2108 
2109#[derive(Debug, Deserialize)]
2110struct PointsResponse {
2111 properties: PointsProperties,
2112}
2113 
2114#[derive(Debug, Deserialize)]
2115struct PointsProperties {
2116 forecast: String,
2117}
2118 
2119#[derive(Debug, Deserialize)]
2120struct ForecastResponse {
2121 properties: ForecastProperties,
2122}
2123 
2124#[derive(Debug, Deserialize)]
2125struct ForecastProperties {
2126 periods: Vec<ForecastPeriod>,
2127}
2128 
2129#[derive(Debug, Deserialize)]
2130struct ForecastPeriod {
2131 name: String,
2132 temperature: i32,
2133 #[serde(rename = "temperatureUnit")]
2134 temperature_unit: String,
2135 #[serde(rename = "windSpeed")]
2136 wind_speed: String,
2137 #[serde(rename = "windDirection")]
2138 wind_direction: String,
2139 #[serde(rename = "detailedForecast")]
2140 detailed_forecast: String,
2141}
2142```
2143 
2144Now define the request types that MCP clients will send:
2145 
2146```rust theme={null}
2147#[derive(serde::Deserialize, schemars::JsonSchema)]
2148pub struct MCPForecastRequest {
2149 latitude: f32,
2150 longitude: f32,
2151}
2152 
2153#[derive(serde::Deserialize, schemars::JsonSchema)]
2154pub struct MCPAlertRequest {
2155 state: String,
2156}
2157```
2158 
2159### Helper functions
2160 
2161Add helper functions for making API requests and formatting responses:
2162 
2163```rust theme={null}
2164async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2165 let client = reqwest::Client::new();
2166 let rsp = client
2167 .get(url)
2168 .header(reqwest::header::USER_AGENT, USER_AGENT)
2169 .header(reqwest::header::ACCEPT, "application/geo+json")
2170 .send()
2171 .await?
2172 .error_for_status()?;
2173 Ok(rsp.json::<T>().await?)
2174}
2175 
2176fn format_alert(feature: &AlertFeature) -> String {
2177 let props = &feature.properties;
2178 format!(
2179 "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2180 props.event.as_deref().unwrap_or("Unknown"),
2181 props.area_desc.as_deref().unwrap_or("Unknown"),
2182 props.severity.as_deref().unwrap_or("Unknown"),
2183 props
2184 .description
2185 .as_deref()
2186 .unwrap_or("No description available"),
2187 props
2188 .instruction
2189 .as_deref()
2190 .unwrap_or("No specific instructions provided")
2191 )
2192}
2193 
2194fn format_period(period: &ForecastPeriod) -> String {
2195 format!(
2196 "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2197 period.name,
2198 period.temperature,
2199 period.temperature_unit,
2200 period.wind_speed,
2201 period.wind_direction,
2202 period.detailed_forecast
2203 )
2204}
2205```
2206 
2207### Implementing the Weather server and tools
2208 
2209Now let's implement the main Weather server struct with the tool handlers:
2210 
2211```rust theme={null}
2212pub struct Weather {
2213 tool_router: ToolRouter<Weather>,
2214}
2215 
2216#[tool_router]
2217impl Weather {
2218 fn new() -> Self {
2219 Self {
2220 tool_router: Self::tool_router(),
2221 }
2222 }
2223 
2224 #[tool(description = "Get weather alerts for a US state.")]
2225 async fn get_alerts(
2226 &self,
2227 Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2228 ) -> String {
2229 let url = format!(
2230 "{}/alerts/active/area/{}",
2231 NWS_API_BASE,
2232 state.to_uppercase()
2233 );
2234 
2235 match make_nws_request::<AlertsResponse>(&url).await {
2236 Ok(data) => {
2237 if data.features.is_empty() {
2238 "No active alerts for this state.".to_string()
2239 } else {
2240 data.features
2241 .iter()
2242 .map(format_alert)
2243 .collect::<Vec<_>>()
2244 .join("\n---\n")
2245 }
2246 }
2247 Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2248 }
2249 }
2250 
2251 #[tool(description = "Get weather forecast for a location.")]
2252 async fn get_forecast(
2253 &self,
2254 Parameters(MCPForecastRequest {
2255 latitude,
2256 longitude,
2257 }): Parameters<MCPForecastRequest>,
2258 ) -> String {
2259 let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2260 let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2261 return "Unable to fetch forecast data for this location.".to_string();
2262 };
2263 
2264 let forecast_url = points_data.properties.forecast;
2265 
2266 let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2267 return "Unable to fetch forecast data for this location.".to_string();
2268 };
2269 
2270 let periods = &forecast_data.properties.periods;
2271 let forecast_summary: String = periods
2272 .iter()
2273 .take(5) // Next 5 periods only
2274 .map(format_period)
2275 .collect::<Vec<String>>()
2276 .join("\n---\n");
2277 forecast_summary
2278 }
2279}
2280```
2281 
2282The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283 
2284### Implementing the ServerHandler
2285 
2286Implement the `ServerHandler` trait to define server capabilities:
2287 
2288```rust theme={null}
2289#[tool_handler]
2290impl ServerHandler for Weather {
2291 fn get_info(&self) -> ServerInfo {
2292 ServerInfo {
2293 capabilities: ServerCapabilities::builder().enable_tools().build(),
2294 ..Default::default()
2295 }
2296 }
2297}
2298```
2299 
2300### Running the server
2301 
2302Finally, implement the main function to run the server with stdio transport:
2303 
2304```rust theme={null}
2305#[tokio::main]
2306async fn main() -> Result<()> {
2307 let transport = (tokio::io::stdin(), tokio::io::stdout());
2308 let service = Weather::new().serve(transport).await?;
2309 service.waiting().await?;
2310 Ok(())
2311}
2312```
2313 
2314Build your server with:
2315 
2316```bash theme={null}
2317cargo build --release
2318```
2319 
2320The compiled binary will be in `target/release/weather`.
2321 
2322Let's now test your server from an existing MCP host, Claude for Desktop.
2323 
2324## Testing your server with Claude for Desktop
2325 
2326> [!NOTE]
2327>
2328> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329 
2330First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2331 
2332We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2333 
2334For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335 
2336```bash macOS/Linux theme={null}
2337code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338```
2339 
2340```powershell Windows theme={null}
2341code $env:AppData\Claude\claude_desktop_config.json
2342```
2343 
2344You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2345 
2346In this case, we'll add our single weather server like so:
2347 
2348```json macOS/Linux theme={null}
2349{
2350 "mcpServers": {
2351 "weather": {
2352 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2353 }
2354 }
2355}
2356```
2357 
2358```json Windows theme={null}
2359{
2360 "mcpServers": {
2361 "weather": {
2362 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2363 }
2364 }
2365}
2366```
2367 
2368> [!NOTE]
2369>
2370> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2371 
2372This tells Claude for Desktop:
2373 
23741. There's an MCP server named "weather"
23752. Launch it by running the compiled binary at the specified path
2376 
2377Save the file, and restart **Claude for Desktop**.
2378 
2379**Go**
2380 
2381Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2382 
2383### Prerequisite knowledge
2384 
2385This quickstart assumes you have familiarity with:
2386 
2387* Go
2388* LLMs like Claude
2389 
2390### Logging in MCP Servers
2391 
2392When implementing MCP servers, be careful about how you handle logging:
2393 
2394**For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2395 
2396**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2397 
2398### Best Practices
2399 
2400* Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2401* Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2402 
2403### Quick Examples
2404 
2405```go theme={null}
2406// ❌ Bad (STDIO)
2407fmt.Println("Processing request")
2408 
2409// ✅ Good (STDIO)
2410log.Println("Processing request") // defaults to stderr
2411 
2412// ✅ Good (STDIO)
2413fmt.Fprintln(os.Stderr, "Processing request")
2414```
2415 
2416### System requirements
2417 
2418* Go 1.24 or higher installed.
2419 
2420### Set up your environment
2421 
2422First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423 
2424Verify your Go installation:
2425 
2426```bash theme={null}
2427go version
2428```
2429 
2430Now, let's create and set up our project:
2431 
2432```bash macOS/Linux theme={null}
2433# Create a new directory for our project
2434mkdir weather
2435cd weather
2436 
2437# Initialize Go module
2438go mod init weather
2439 
2440# Install dependencies
2441go get github.com/modelcontextprotocol/go-sdk/mcp
2442 
2443# Create our server file
2444touch main.go
2445```
2446 
2447```powershell Windows theme={null}
2448# Create a new directory for our project
2449md weather
2450cd weather
2451 
2452# Initialize Go module
2453go mod init weather
2454 
2455# Install dependencies
2456go get github.com/modelcontextprotocol/go-sdk/mcp
2457 
2458# Create our server file
2459new-item main.go
2460```
2461 
2462Now let's dive into building your server.
2463 
2464## Building your server
2465 
2466### Importing packages and constants
2467 
2468Add these to the top of your `main.go`:
2469 
2470```go theme={null}
2471package main
2472 
2473import (
2474 "cmp"
2475 "context"
2476 "encoding/json"
2477 "fmt"
2478 "io"
2479 "log"
2480 "net/http"
2481 "strings"
2482 
2483 "github.com/modelcontextprotocol/go-sdk/mcp"
2484)
2485 
2486const (
2487 NWSAPIBase = "https://api.weather.gov"
2488 UserAgent = "weather-app/1.0"
2489)
2490```
2491 
2492### Data structures
2493 
2494Next, let's define the data structures used by our tools:
2495 
2496```go theme={null}
2497type PointsResponse struct {
2498 Properties struct {
2499 Forecast string `json:"forecast"`
2500 } `json:"properties"`
2501}
2502 
2503type ForecastResponse struct {
2504 Properties struct {
2505 Periods []ForecastPeriod `json:"periods"`
2506 } `json:"properties"`
2507}
2508 
2509type ForecastPeriod struct {
2510 Name string `json:"name"`
2511 Temperature int `json:"temperature"`
2512 TemperatureUnit string `json:"temperatureUnit"`
2513 WindSpeed string `json:"windSpeed"`
2514 WindDirection string `json:"windDirection"`
2515 DetailedForecast string `json:"detailedForecast"`
2516}
2517 
2518type AlertsResponse struct {
2519 Features []AlertFeature `json:"features"`
2520}
2521 
2522type AlertFeature struct {
2523 Properties AlertProperties `json:"properties"`
2524}
2525 
2526type AlertProperties struct {
2527 Event string `json:"event"`
2528 AreaDesc string `json:"areaDesc"`
2529 Severity string `json:"severity"`
2530 Description string `json:"description"`
2531 Instruction string `json:"instruction"`
2532}
2533 
2534type ForecastInput struct {
2535 Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536 Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537}
2538 
2539type AlertsInput struct {
2540 State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541}
2542```
2543 
2544### Helper functions
2545 
2546Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547 
2548```go theme={null}
2549func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2550 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2551 if err != nil {
2552 return nil, fmt.Errorf("failed to create request: %w", err)
2553 }
2554 
2555 req.Header.Set("User-Agent", UserAgent)
2556 req.Header.Set("Accept", "application/geo+json")
2557 
2558 client := http.DefaultClient
2559 resp, err := client.Do(req)
2560 if err != nil {
2561 return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2562 }
2563 defer resp.Body.Close()
2564 
2565 if resp.StatusCode != http.StatusOK {
2566 body, _ := io.ReadAll(resp.Body)
2567 return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2568 }
2569 
2570 var result T
2571 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2572 return nil, fmt.Errorf("failed to decode response: %w", err)
2573 }
2574 
2575 return &result, nil
2576}
2577 
2578func formatAlert(alert AlertFeature) string {
2579 props := alert.Properties
2580 event := cmp.Or(props.Event, "Unknown")
2581 areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2582 severity := cmp.Or(props.Severity, "Unknown")
2583 description := cmp.Or(props.Description, "No description available")
2584 instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2585 
2586 return fmt.Sprintf(`
2587Event: %s
2588Area: %s
2589Severity: %s
2590Description: %s
2591Instructions: %s
2592`, event, areaDesc, severity, description, instruction)
2593}
2594 
2595func formatPeriod(period ForecastPeriod) string {
2596 return fmt.Sprintf(`
2597%s:
2598Temperature: %d°%s
2599Wind: %s %s
2600Forecast: %s
2601`, period.Name, period.Temperature, period.TemperatureUnit,
2602 period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603}
2604```
2605 
2606### Implementing tool execution
2607 
2608The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609 
2610```go theme={null}
2611func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2612 *mcp.CallToolResult, any, error,
2613) {
2614 // Get points data
2615 pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2616 pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2617 if err != nil {
2618 return &mcp.CallToolResult{
2619 Content: []mcp.Content{
2620 &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2621 },
2622 }, nil, nil
2623 }
2624 
2625 // Get forecast data
2626 forecastURL := pointsData.Properties.Forecast
2627 if forecastURL == "" {
2628 return &mcp.CallToolResult{
2629 Content: []mcp.Content{
2630 &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2631 },
2632 }, nil, nil
2633 }
2634 
2635 forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2636 if err != nil {
2637 return &mcp.CallToolResult{
2638 Content: []mcp.Content{
2639 &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2640 },
2641 }, nil, nil
2642 }
2643 
2644 // Format the periods
2645 periods := forecastData.Properties.Periods
2646 if len(periods) == 0 {
2647 return &mcp.CallToolResult{
2648 Content: []mcp.Content{
2649 &mcp.TextContent{Text: "No forecast periods available."},
2650 },
2651 }, nil, nil
2652 }
2653 
2654 // Show next 5 periods
2655 var forecasts []string
2656 for i := range min(5, len(periods)) {
2657 forecasts = append(forecasts, formatPeriod(periods[i]))
2658 }
2659 
2660 result := strings.Join(forecasts, "\n---\n")
2661 
2662 return &mcp.CallToolResult{
2663 Content: []mcp.Content{
2664 &mcp.TextContent{Text: result},
2665 },
2666 }, nil, nil
2667}
2668 
2669func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2670 *mcp.CallToolResult, any, error,
2671) {
2672 // Build alerts URL
2673 stateCode := strings.ToUpper(input.State)
2674 alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2675 
2676 alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2677 if err != nil {
2678 return &mcp.CallToolResult{
2679 Content: []mcp.Content{
2680 &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2681 },
2682 }, nil, nil
2683 }
2684 
2685 // Check if there are any alerts
2686 if len(alertsData.Features) == 0 {
2687 return &mcp.CallToolResult{
2688 Content: []mcp.Content{
2689 &mcp.TextContent{Text: "No active alerts for this state."},
2690 },
2691 }, nil, nil
2692 }
2693 
2694 // Format alerts
2695 var alerts []string
2696 for _, feature := range alertsData.Features {
2697 alerts = append(alerts, formatAlert(feature))
2698 }
2699 
2700 result := strings.Join(alerts, "\n---\n")
2701 
2702 return &mcp.CallToolResult{
2703 Content: []mcp.Content{
2704 &mcp.TextContent{Text: result},
2705 },
2706 }, nil, nil
2707}
2708```
2709 
2710### Running the server
2711 
2712Finally, implement the main function to run the server:
2713 
2714```go theme={null}
2715func main() {
2716 // Create MCP server
2717 server := mcp.NewServer(&mcp.Implementation{
2718 Name: "weather",
2719 Version: "1.0.0",
2720 }, nil)
2721 
2722 // Add get_forecast tool
2723 mcp.AddTool(server, &mcp.Tool{
2724 Name: "get_forecast",
2725 Description: "Get weather forecast for a location",
2726 }, getForecast)
2727 
2728 // Add get_alerts tool
2729 mcp.AddTool(server, &mcp.Tool{
2730 Name: "get_alerts",
2731 Description: "Get weather alerts for a US state",
2732 }, getAlerts)
2733 
2734 // Run server on stdio transport
2735 if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2736 log.Fatal(err)
2737 }
2738}
2739```
2740 
2741Build your server with:
2742 
2743```bash theme={null}
2744go build -o weather .
2745```
2746 
2747The compiled binary will be in `./weather`.
2748 
2749Let's now test your server from an existing MCP host, Claude for Desktop.
2750 
2751## Testing your server with Claude for Desktop
2752 
2753> [!NOTE]
2754>
2755> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756 
2757First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2758 
2759We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2760 
2761For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762 
2763```bash macOS/Linux theme={null}
2764code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765```
2766 
2767```powershell Windows theme={null}
2768code $env:AppData\Claude\claude_desktop_config.json
2769```
2770 
2771You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2772 
2773In this case, we'll add our single weather server like so:
2774 
2775```json macOS/Linux theme={null}
2776{
2777 "mcpServers": {
2778 "weather": {
2779 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2780 }
2781 }
2782}
2783```
2784 
2785```json Windows theme={null}
2786{
2787 "mcpServers": {
2788 "weather": {
2789 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2790 }
2791 }
2792}
2793```
2794 
2795> [!NOTE]
2796>
2797> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2798 
2799This tells Claude for Desktop:
2800 
28011. There's an MCP server named "weather"
28022. Launch it by running the compiled binary at the specified path
2803 
2804Save the file, and restart **Claude for Desktop**.
27> [tab] Python
28>
29> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30>
31> ### Prerequisite knowledge
32>
33> This quickstart assumes you have familiarity with:
34>
35> * Python
36> * LLMs like Claude
37>
38> ### Logging in MCP Servers
39>
40> When implementing MCP servers, be careful about how you handle logging:
41>
42> **For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, but can be used safely with `file=sys.stderr`.
43>
44> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45>
46> ### Best Practices
47>
48> * Use a logging library that writes to stderr or files.
49>
50> ### Quick Examples
51>
52> ```python theme={null}
53> import sys
54> import logging
55>
56> # ❌ Bad (STDIO)
57> print("Processing request")
58>
59> # ✅ Good (STDIO)
60> print("Processing request", file=sys.stderr)
61>
62> # ✅ Good (STDIO)
63> logging.info("Processing request")
64> ```
65>
66> ### System requirements
67>
68> * Python 3.10 or higher installed.
69> * You must use the Python MCP SDK 1.2.0 or higher.
70>
71> ### Set up your environment
72>
73> First, let's install `uv` and set up our Python project and environment:
74>
75> ```bash macOS/Linux theme={null}
76> curl -LsSf https://astral.sh/uv/install.sh | sh
77> ```
78>
79> ```powershell Windows theme={null}
80> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81> ```
82>
83> Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84>
85> Now, let's create and set up our project:
86>
87> ```bash macOS/Linux theme={null}
88> # Create a new directory for our project
89> uv init weather
90> cd weather
91>
92> # Create virtual environment and activate it
93> uv venv
94> source .venv/bin/activate
95>
96> # Install dependencies
97> uv add "mcp[cli]" httpx
98>
99> # Create our server file
100> touch weather.py
101> ```
102>
103> ```powershell Windows theme={null}
104> # Create a new directory for our project
105> uv init weather
106> cd weather
107>
108> # Create virtual environment and activate it
109> uv venv
110> .venv\Scripts\activate
111>
112> # Install dependencies
113> uv add mcp[cli] httpx
114>
115> # Create our server file
116> new-item weather.py
117> ```
118>
119> Now let's dive into building your server.
120>
121> ## Building your server
122>
123> ### Importing packages and setting up the instance
124>
125> Add these to the top of your `weather.py`:
126>
127> ```python theme={null}
128> from typing import Any
129>
130> import httpx
131> from mcp.server.fastmcp import FastMCP
132>
133> # Initialize FastMCP server
134> mcp = FastMCP("weather")
135>
136> # Constants
137> NWS_API_BASE = "https://api.weather.gov"
138> USER_AGENT = "weather-app/1.0"
139> ```
140>
141> The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
142>
143> ### Helper functions
144>
145> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146>
147> ```python theme={null}
148> async def make_nws_request(url: str) -> dict[str, Any] | None:
149> """Make a request to the NWS API with proper error handling."""
150> headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
151> async with httpx.AsyncClient() as client:
152> try:
153> response = await client.get(url, headers=headers, timeout=30.0)
154> response.raise_for_status()
155> return response.json()
156> except Exception:
157> return None
158>
159>
160> def format_alert(feature: dict) -> str:
161> """Format an alert feature into a readable string."""
162> props = feature["properties"]
163> return f"""
164> Event: {props.get("event", "Unknown")}
165> Area: {props.get("areaDesc", "Unknown")}
166> Severity: {props.get("severity", "Unknown")}
167> Description: {props.get("description", "No description available")}
168> Instructions: {props.get("instruction", "No specific instructions provided")}
169> """
170> ```
171>
172> ### Implementing tool execution
173>
174> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
175>
176> ```python theme={null}
177> @mcp.tool()
178> async def get_alerts(state: str) -> str:
179> """Get weather alerts for a US state.
180>
181> Args:
182> state: Two-letter US state code (e.g. CA, NY)
183> """
184> url = f"{NWS_API_BASE}/alerts/active/area/{state}"
185> data = await make_nws_request(url)
186>
187> if not data or "features" not in data:
188> return "Unable to fetch alerts or no alerts found."
189>
190> if not data["features"]:
191> return "No active alerts for this state."
192>
193> alerts = [format_alert(feature) for feature in data["features"]]
194> return "\n---\n".join(alerts)
195>
196>
197> @mcp.tool()
198> async def get_forecast(latitude: float, longitude: float) -> str:
199> """Get weather forecast for a location.
200>
201> Args:
202> latitude: Latitude of the location
203> longitude: Longitude of the location
204> """
205> # First get the forecast grid endpoint
206> points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
207> points_data = await make_nws_request(points_url)
208>
209> if not points_data:
210> return "Unable to fetch forecast data for this location."
211>
212> # Get the forecast URL from the points response
213> forecast_url = points_data["properties"]["forecast"]
214> forecast_data = await make_nws_request(forecast_url)
215>
216> if not forecast_data:
217> return "Unable to fetch detailed forecast."
218>
219> # Format the periods into a readable forecast
220> periods = forecast_data["properties"]["periods"]
221> forecasts = []
222> for period in periods[:5]: # Only show next 5 periods
223> forecast = f"""
224> {period["name"]}:
225> Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226> Wind: {period["windSpeed"]} {period["windDirection"]}
227> Forecast: {period["detailedForecast"]}
228> """
229> forecasts.append(forecast)
230>
231> return "\n---\n".join(forecasts)
232> ```
233>
234> ### Running the server
235>
236> Finally, let's initialize and run the server:
237>
238> ```python theme={null}
239> def main():
240> # Initialize and run the server
241> mcp.run(transport="stdio")
242>
243>
244> if __name__ == "__main__":
245> main()
246> ```
247>
248> Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249>
250> Let's now test your server from an existing MCP host, Claude for Desktop.
251>
252> ## Testing your server with Claude for Desktop
253>
254> > [!NOTE]
255> >
256> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257>
258> First, make sure you have Claude for Desktop installed. [You can install the latest version
259> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260>
261> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
262>
263> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264>
265> ```bash macOS/Linux theme={null}
266> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267> ```
268>
269> ```powershell Windows theme={null}
270> code $env:AppData\Claude\claude_desktop_config.json
271> ```
272>
273> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
274>
275> In this case, we'll add our single weather server like so:
276>
277> ```json macOS/Linux theme={null}
278> {
279> "mcpServers": {
280> "weather": {
281> "command": "uv",
282> "args": [
283> "--directory",
284> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
285> "run",
286> "weather.py"
287> ]
288> }
289> }
290> }
291> ```
292>
293> ```json Windows theme={null}
294> {
295> "mcpServers": {
296> "weather": {
297> "command": "uv",
298> "args": [
299> "--directory",
300> "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
301> "run",
302> "weather.py"
303> ]
304> }
305> }
306> }
307> ```
308>
309> > [!WARNING]
310> >
311> > You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
312>
313> > [!NOTE]
314> >
315> > Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
316>
317> This tells Claude for Desktop:
318>
319> 1. There's an MCP server named "weather"
320> 2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321>
322> Save the file, and restart **Claude for Desktop**.
323 
324> [tab] TypeScript
325>
326> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
327>
328> ### Prerequisite knowledge
329>
330> This quickstart assumes you have familiarity with:
331>
332> * TypeScript
333> * LLMs like Claude
334>
335> ### Logging in MCP Servers
336>
337> When implementing MCP servers, be careful about how you handle logging:
338>
339> **For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
340>
341> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
342>
343> ### Best Practices
344>
345> * Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
346>
347> ### Quick Examples
348>
349> ```javascript theme={null}
350> // ❌ Bad (STDIO)
351> console.log("Server started");
352>
353> // ✅ Good (STDIO)
354> console.error("Server started"); // stderr is safe
355> ```
356>
357> ### System requirements
358>
359> For TypeScript, make sure you have the latest version of Node installed.
360>
361> ### Set up your environment
362>
363> First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364> Verify your Node.js installation:
365>
366> ```bash theme={null}
367> node --version
368> npm --version
369> ```
370>
371> For this tutorial, you'll need Node.js version 16 or higher.
372>
373> Now, let's create and set up our project:
374>
375> ```bash macOS/Linux theme={null}
376> # Create a new directory for our project
377> mkdir weather
378> cd weather
379>
380> # Initialize a new npm project
381> npm init -y
382>
383> # Install dependencies
384> npm install @modelcontextprotocol/sdk zod@3
385> npm install -D @types/node typescript
386>
387> # Create our files
388> mkdir src
389> touch src/index.ts
390> ```
391>
392> ```powershell Windows theme={null}
393> # Create a new directory for our project
394> md weather
395> cd weather
396>
397> # Initialize a new npm project
398> npm init -y
399>
400> # Install dependencies
401> npm install @modelcontextprotocol/sdk zod@3
402> npm install -D @types/node typescript
403>
404> # Create our files
405> md src
406> new-item src\index.ts
407> ```
408>
409> Update your package.json to add type: "module" and a build script:
410>
411> ```json package.json theme={null}
412> {
413> "type": "module",
414> "bin": {
415> "weather": "./build/index.js"
416> },
417> "scripts": {
418> "build": "tsc && chmod 755 build/index.js"
419> },
420> "files": ["build"]
421> }
422> ```
423>
424> Create a `tsconfig.json` in the root of your project:
425>
426> ```json tsconfig.json theme={null}
427> {
428> "compilerOptions": {
429> "target": "ES2022",
430> "module": "Node16",
431> "moduleResolution": "Node16",
432> "outDir": "./build",
433> "rootDir": "./src",
434> "strict": true,
435> "esModuleInterop": true,
436> "skipLibCheck": true,
437> "forceConsistentCasingInFileNames": true
438> },
439> "include": ["src/**/*"],
440> "exclude": ["node_modules"]
441> }
442> ```
443>
444> Now let's dive into building your server.
445>
446> ## Building your server
447>
448> ### Importing packages and setting up the instance
449>
450> Add these to the top of your `src/index.ts`:
451>
452> ```typescript theme={null}
453> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454> import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455> import { z } from "zod";
456>
457> const NWS_API_BASE = "https://api.weather.gov";
458> const USER_AGENT = "weather-app/1.0";
459>
460> // Create server instance
461> const server = new McpServer({
462> name: "weather",
463> version: "1.0.0",
464> });
465> ```
466>
467> ### Helper functions
468>
469> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
470>
471> ```typescript theme={null}
472> // Helper function for making NWS API requests
473> async function makeNWSRequest<T>(url: string): Promise<T | null> {
474> const headers = {
475> "User-Agent": USER_AGENT,
476> Accept: "application/geo+json",
477> };
478>
479> try {
480> const response = await fetch(url, { headers });
481> if (!response.ok) {
482> throw new Error(`HTTP error! status: ${response.status}`);
483> }
484> return (await response.json()) as T;
485> } catch (error) {
486> console.error("Error making NWS request:", error);
487> return null;
488> }
489> }
490>
491> interface AlertFeature {
492> properties: {
493> event?: string;
494> areaDesc?: string;
495> severity?: string;
496> status?: string;
497> headline?: string;
498> };
499> }
500>
501> // Format alert data
502> function formatAlert(feature: AlertFeature): string {
503> const props = feature.properties;
504> return [
505> `Event: ${props.event || "Unknown"}`,
506> `Area: ${props.areaDesc || "Unknown"}`,
507> `Severity: ${props.severity || "Unknown"}`,
508> `Status: ${props.status || "Unknown"}`,
509> `Headline: ${props.headline || "No headline"}`,
510> "---",
511> ].join("\n");
512> }
513>
514> interface ForecastPeriod {
515> name?: string;
516> temperature?: number;
517> temperatureUnit?: string;
518> windSpeed?: string;
519> windDirection?: string;
520> shortForecast?: string;
521> }
522>
523> interface AlertsResponse {
524> features: AlertFeature[];
525> }
526>
527> interface PointsResponse {
528> properties: {
529> forecast?: string;
530> };
531> }
532>
533> interface ForecastResponse {
534> properties: {
535> periods: ForecastPeriod[];
536> };
537> }
538> ```
539>
540> ### Implementing tool execution
541>
542> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
543>
544> ```typescript theme={null}
545> // Register weather tools
546>
547> server.registerTool(
548> "get_alerts",
549> {
550> description: "Get weather alerts for a state",
551> inputSchema: {
552> state: z
553> .string()
554> .length(2)
555> .describe("Two-letter state code (e.g. CA, NY)"),
556> },
557> },
558> async ({ state }) => {
559> const stateCode = state.toUpperCase();
560> const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
561> const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
562>
563> if (!alertsData) {
564> return {
565> content: [
566> {
567> type: "text",
568> text: "Failed to retrieve alerts data",
569> },
570> ],
571> };
572> }
573>
574> const features = alertsData.features || [];
575> if (!features.length) {
576> return {
577> content: [
578> {
579> type: "text",
580> text: `No active alerts for ${stateCode}`,
581> },
582> ],
583> };
584> }
585>
586> const formattedAlerts = features.map(formatAlert);
587> const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
588>
589> return {
590> content: [
591> {
592> type: "text",
593> text: alertsText,
594> },
595> ],
596> };
597> },
598> );
599>
600> server.registerTool(
601> "get_forecast",
602> {
603> description: "Get weather forecast for a location",
604> inputSchema: {
605> latitude: z
606> .number()
607> .min(-90)
608> .max(90)
609> .describe("Latitude of the location"),
610> longitude: z
611> .number()
612> .min(-180)
613> .max(180)
614> .describe("Longitude of the location"),
615> },
616> },
617> async ({ latitude, longitude }) => {
618> // Get grid point data
619> const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
620> const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
621>
622> if (!pointsData) {
623> return {
624> content: [
625> {
626> type: "text",
627> text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
628> },
629> ],
630> };
631> }
632>
633> const forecastUrl = pointsData.properties?.forecast;
634> if (!forecastUrl) {
635> return {
636> content: [
637> {
638> type: "text",
639> text: "Failed to get forecast URL from grid point data",
640> },
641> ],
642> };
643> }
644>
645> // Get forecast data
646> const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
647> if (!forecastData) {
648> return {
649> content: [
650> {
651> type: "text",
652> text: "Failed to retrieve forecast data",
653> },
654> ],
655> };
656> }
657>
658> const periods = forecastData.properties?.periods || [];
659> if (periods.length === 0) {
660> return {
661> content: [
662> {
663> type: "text",
664> text: "No forecast periods available",
665> },
666> ],
667> };
668> }
669>
670> // Format forecast periods
671> const formattedForecast = periods.map((period: ForecastPeriod) =>
672> [
673> `${period.name || "Unknown"}:`,
674> `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
675> `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
676> `${period.shortForecast || "No forecast available"}`,
677> "---",
678> ].join("\n"),
679> );
680>
681> const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
682>
683> return {
684> content: [
685> {
686> type: "text",
687> text: forecastText,
688> },
689> ],
690> };
691> },
692> );
693> ```
694>
695> ### Running the server
696>
697> Finally, implement the main function to run the server:
698>
699> ```typescript theme={null}
700> async function main() {
701> const transport = new StdioServerTransport();
702> await server.connect(transport);
703> console.error("Weather MCP Server running on stdio");
704> }
705>
706> main().catch((error) => {
707> console.error("Fatal error in main():", error);
708> process.exit(1);
709> });
710> ```
711>
712> Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713>
714> Let's now test your server from an existing MCP host, Claude for Desktop.
715>
716> ## Testing your server with Claude for Desktop
717>
718> > [!NOTE]
719> >
720> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721>
722> First, make sure you have Claude for Desktop installed. [You can install the latest version
723> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724>
725> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726>
727> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728>
729> ```bash macOS/Linux theme={null}
730> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731> ```
732>
733> ```powershell Windows theme={null}
734> code $env:AppData\Claude\claude_desktop_config.json
735> ```
736>
737> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
738>
739> In this case, we'll add our single weather server like so:
740>
741> ```json macOS/Linux theme={null}
742> {
743> "mcpServers": {
744> "weather": {
745> "command": "node",
746> "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
747> }
748> }
749> }
750> ```
751>
752> ```json Windows theme={null}
753> {
754> "mcpServers": {
755> "weather": {
756> "command": "node",
757> "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
758> }
759> }
760> }
761> ```
762>
763> This tells Claude for Desktop:
764>
765> 1. There's an MCP server named "weather"
766> 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767>
768> Save the file, and restart **Claude for Desktop**.
769 
770> [tab] Java
771>
772> > [!NOTE]
773> >
774> > This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
775> > To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
776>
777> Let's get started with building our weather server!
778> [You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
779>
780> For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
781> For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782>
783> ### Logging in MCP Servers
784>
785> When implementing MCP servers, be careful about how you handle logging:
786>
787> **For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
788>
789> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
790>
791> ### Best Practices
792>
793> * Use a logging library that writes to stderr or files.
794> * Ensure any configured logging library will not write to stdout.
795>
796> ### System requirements
797>
798> * Java 17 or higher installed.
799> * [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
800>
801> ### Set up your environment
802>
803> Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804>
805> You will need to add the following dependencies:
806>
807> ```xml Maven theme={null}
808> <dependencies>
809> <dependency>
810> <groupId>org.springframework.ai</groupId>
811> <artifactId>spring-ai-starter-mcp-server</artifactId>
812> </dependency>
813>
814> <dependency>
815> <groupId>org.springframework</groupId>
816> <artifactId>spring-web</artifactId>
817> </dependency>
818> </dependencies>
819> ```
820>
821> ```groovy Gradle theme={null}
822> dependencies {
823> implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824> implementation platform("org.springframework:spring-web")
825> }
826> ```
827>
828> Then configure your application by setting the application properties:
829>
830> ```bash application.properties theme={null}
831> spring.main.bannerMode=off
832> logging.pattern.console=
833> ```
834>
835> ```yaml application.yml theme={null}
836> logging:
837> pattern:
838> console:
839> spring:
840> main:
841> banner-mode: off
842> ```
843>
844> The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
845>
846> Now let's dive into building your server.
847>
848> ## Building your server
849>
850> ### Weather Service
851>
852> Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
853>
854> ```java theme={null}
855> @Service
856> public class WeatherService {
857>
858> private final RestClient restClient;
859>
860> public WeatherService() {
861> this.restClient = RestClient.builder()
862> .baseUrl("https://api.weather.gov")
863> .defaultHeader("Accept", "application/geo+json")
864> .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
865> .build();
866> }
867>
868> @Tool(description = "Get weather forecast for a specific latitude/longitude")
869> public String getWeatherForecastByLocation(
870> double latitude, // Latitude coordinate
871> double longitude // Longitude coordinate
872> ) {
873> // Returns detailed forecast including:
874> // - Temperature and unit
875> // - Wind speed and direction
876> // - Detailed forecast description
877> }
878>
879> @Tool(description = "Get weather alerts for a US state")
880> public String getAlerts(
881> @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
882> ) {
883> // Returns active alerts including:
884> // - Event type
885> // - Affected area
886> // - Severity
887> // - Description
888> // - Safety instructions
889> }
890>
891> // ......
892> }
893> ```
894>
895> The `@Service` annotation will auto-register the service in your application context.
896> The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897>
898> The auto-configuration will automatically register these tools with the MCP server.
899>
900> ### Create your Boot Application
901>
902> ```java theme={null}
903> @SpringBootApplication
904> public class McpServerApplication {
905>
906> public static void main(String[] args) {
907> SpringApplication.run(McpServerApplication.class, args);
908> }
909>
910> @Bean
911> public ToolCallbackProvider weatherTools(WeatherService weatherService) {
912> return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
913> }
914> }
915> ```
916>
917> Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918>
919> ### Running the server
920>
921> Finally, let's build the server:
922>
923> ```bash theme={null}
924> ./mvnw clean install
925> ```
926>
927> This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928>
929> Let's now test your server from an existing MCP host, Claude for Desktop.
930>
931> ## Testing your server with Claude for Desktop
932>
933> > [!NOTE]
934> >
935> > Claude for Desktop is not yet available on Linux.
936>
937> First, make sure you have Claude for Desktop installed.
938> [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939>
940> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942> Make sure to create the file if it doesn't exist.
943>
944> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945>
946> ```bash macOS/Linux theme={null}
947> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948> ```
949>
950> ```powershell Windows theme={null}
951> code $env:AppData\Claude\claude_desktop_config.json
952> ```
953>
954> You'll then add your servers in the `mcpServers` key.
955> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956>
957> In this case, we'll add our single weather server like so:
958>
959> ```json macOS/Linux theme={null}
960> {
961> "mcpServers": {
962> "spring-ai-mcp-weather": {
963> "command": "java",
964> "args": [
965> "-Dspring.ai.mcp.server.stdio=true",
966> "-jar",
967> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
968> ]
969> }
970> }
971> }
972> ```
973>
974> ```json Windows theme={null}
975> {
976> "mcpServers": {
977> "spring-ai-mcp-weather": {
978> "command": "java",
979> "args": [
980> "-Dspring.ai.mcp.server.transport=STDIO",
981> "-jar",
982> "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
983> ]
984> }
985> }
986> }
987> ```
988>
989> > [!NOTE]
990> >
991> > Make sure you pass in the absolute path to your server.
992>
993> This tells Claude for Desktop:
994>
995> 1. There's an MCP server named "my-weather-server"
996> 2. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997>
998> Save the file, and restart **Claude for Desktop**.
999>
1000> ## Testing your server with Java client
1001>
1002> ### Create an MCP Client manually
1003>
1004> Use the `McpClient` to connect to the server:
1005>
1006> ```java theme={null}
1007> var stdioParams = ServerParameters.builder("java")
1008> .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009> .build();
1010>
1011> var stdioTransport = new StdioClientTransport(stdioParams);
1012>
1013> var mcpClient = McpClient.sync(stdioTransport).build();
1014>
1015> mcpClient.initialize();
1016>
1017> ListToolsResult toolsList = mcpClient.listTools();
1018>
1019> CallToolResult weather = mcpClient.callTool(
1020> new CallToolRequest("getWeatherForecastByLocation",
1021> Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022>
1023> CallToolResult alert = mcpClient.callTool(
1024> new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025>
1026> mcpClient.closeGracefully();
1027> ```
1028>
1029> ### Use MCP Client Boot Starter
1030>
1031> Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1032>
1033> ```xml theme={null}
1034> <dependency>
1035> <groupId>org.springframework.ai</groupId>
1036> <artifactId>spring-ai-starter-mcp-client</artifactId>
1037> </dependency>
1038> ```
1039>
1040> and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041> You can reuse the existing Anthropic Desktop configuration:
1042>
1043> ```properties theme={null}
1044> spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045> ```
1046>
1047> When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048>
1049> For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1050>
1051> ## More Java MCP Server examples
1052>
1053> The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an MCP server using SSE transport.
1054> It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056> [tab] Kotlin
1057>
1058> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1059>
1060> ### Prerequisite knowledge
1061>
1062> This quickstart assumes you have familiarity with:
1063>
1064> * Kotlin
1065> * LLMs like Claude
1066>
1067> ### Logging in MCP Servers
1068>
1069> When implementing MCP servers, be careful about how you handle logging:
1070>
1071> **For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1072>
1073> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1074>
1075> ### Best Practices
1076>
1077> * Use a logging library that writes to stderr or files.
1078>
1079> ### System requirements
1080>
1081> * JDK 11 or higher installed.
1082>
1083> ### Set up your environment
1084>
1085> First, let's install `java` and `gradle` if you haven't already.
1086> You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087> Verify your `java` installation:
1088>
1089> ```bash theme={null}
1090> java --version
1091> ```
1092>
1093> Now, let's create and set up your project:
1094>
1095> ```bash macOS/Linux theme={null}
1096> # Create a new directory for our project
1097> mkdir weather
1098> cd weather
1099>
1100> # Initialize a new kotlin project
1101> gradle init
1102> ```
1103>
1104> ```powershell Windows theme={null}
1105> # Create a new directory for our project
1106> md weather
1107> cd weather
1108>
1109> # Initialize a new kotlin project
1110> gradle init
1111> ```
1112>
1113> After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114>
1115> Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116>
1117> After creating the project, replace the contents of your `build.gradle.kts` with:
1118>
1119> ```kotlin build.gradle.kts theme={null}
1120> // Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1121> val mcpVersion = "0.9.0"
1122> val ktorVersion = "3.2.3"
1123> val slf4jVersion = "2.0.17"
1124>
1125> plugins {
1126> kotlin("jvm") version "2.3.20"
1127> kotlin("plugin.serialization") version "2.3.20"
1128> id("com.gradleup.shadow") version "8.3.9"
1129> application
1130> }
1131>
1132> application {
1133> mainClass.set("MainKt")
1134> }
1135>
1136> dependencies {
1137> implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1138> implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1139> implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1140> implementation("io.ktor:ktor-client-cio:$ktorVersion")
1141> implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1142> }
1143> ```
1144>
1145> Verify that everything is set up correctly:
1146>
1147> ```bash theme={null}
1148> ./gradlew build
1149> ```
1150>
1151> Now let's dive into building your server.
1152>
1153> ## Building your server
1154>
1155> ### Setting up the instance
1156>
1157> Add a server initialization function:
1158>
1159> ```kotlin theme={null}
1160> fun runMcpServer() {
1161> val server = Server(
1162> Implementation(
1163> name = "weather",
1164> version = "1.0.0",
1165> ),
1166> ServerOptions(
1167> capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1168> ),
1169> )
1170>
1171> // register tools on server here
1172>
1173> val transport = StdioServerTransport(
1174> System.`in`.asInput(),
1175> System.out.asSink().buffered(),
1176> )
1177>
1178> runBlocking {
1179> val session = server.createSession(transport)
1180> val done = Job()
1181> session.onClose {
1182> done.complete()
1183> }
1184> done.join()
1185> }
1186> }
1187> ```
1188>
1189> ### Weather API helper functions
1190>
1191> Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192>
1193> ```kotlin theme={null}
1194> val httpClient = HttpClient(CIO) {
1195> defaultRequest {
1196> url("https://api.weather.gov")
1197> headers {
1198> append("Accept", "application/geo+json")
1199> append("User-Agent", "WeatherApiClient/1.0")
1200> }
1201> contentType(ContentType.Application.Json)
1202> }
1203> install(ContentNegotiation) {
1204> json(Json { ignoreUnknownKeys = true })
1205> }
1206> }
1207>
1208> // Extension function to fetch weather alerts for a given state
1209> suspend fun HttpClient.getAlerts(state: String): List<String> {
1210> val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211> return alerts.features.map { feature ->
1212> """
1213> Event: ${feature.properties.event}
1214> Area: ${feature.properties.areaDesc}
1215> Severity: ${feature.properties.severity}
1216> Status: ${feature.properties.status}
1217> Headline: ${feature.properties.headline}
1218> """.trimIndent()
1219> }
1220> }
1221>
1222> // Extension function to fetch forecast information for given latitude and longitude
1223> suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1224> val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1225> val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1226> val forecast = this.get(forecastUrl).body<ForecastResponse>()
1227> return forecast.properties.periods.map { period ->
1228> """
1229> ${period.name}:
1230> Temperature: ${period.temperature}°${period.temperatureUnit}
1231> Wind: ${period.windSpeed} ${period.windDirection}
1232> ${period.shortForecast}
1233> """.trimIndent()
1234> }
1235> }
1236>
1237> @Serializable
1238> data class PointsResponse(val properties: PointsProperties)
1239>
1240> @Serializable
1241> data class PointsProperties(val forecast: String? = null)
1242>
1243> @Serializable
1244> data class ForecastResponse(val properties: ForecastProperties)
1245>
1246> @Serializable
1247> data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248>
1249> @Serializable
1250> data class ForecastPeriod(
1251> val name: String? = null,
1252> val temperature: Int? = null,
1253> val temperatureUnit: String? = null,
1254> val windSpeed: String? = null,
1255> val windDirection: String? = null,
1256> val shortForecast: String? = null,
1257> )
1258>
1259> @Serializable
1260> data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261>
1262> @Serializable
1263> data class AlertFeature(val properties: AlertProperties)
1264>
1265> @Serializable
1266> data class AlertProperties(
1267> val event: String? = null,
1268> val areaDesc: String? = null,
1269> val severity: String? = null,
1270> val status: String? = null,
1271> val headline: String? = null,
1272> )
1273> ```
1274>
1275> ### Implementing tool execution
1276>
1277> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1278>
1279> ```kotlin theme={null}
1280> // Register weather tools
1281>
1282> server.addTool(
1283> name = "get_alerts",
1284> description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1285> inputSchema = ToolSchema(
1286> properties = buildJsonObject {
1287> putJsonObject("state") {
1288> put("type", "string")
1289> put("description", "Two-letter US state code (e.g. CA, NY)")
1290> }
1291> },
1292> required = listOf("state"),
1293> ),
1294> ) { request ->
1295> val state = request.arguments?.get("state")?.jsonPrimitive?.content
1296> ?: return@addTool CallToolResult(
1297> content = listOf(TextContent("The 'state' parameter is required.")),
1298> )
1299>
1300> val alerts = httpClient.getAlerts(state)
1301> CallToolResult(content = alerts.map { TextContent(it) })
1302> }
1303>
1304> server.addTool(
1305> name = "get_forecast",
1306> description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1307> inputSchema = ToolSchema(
1308> properties = buildJsonObject {
1309> putJsonObject("latitude") {
1310> put("type", "number")
1311> put("description", "Latitude of the location")
1312> }
1313> putJsonObject("longitude") {
1314> put("type", "number")
1315> put("description", "Longitude of the location")
1316> }
1317> },
1318> required = listOf("latitude", "longitude"),
1319> ),
1320> ) { request ->
1321> val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1322> val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1323> if (latitude == null || longitude == null) {
1324> return@addTool CallToolResult(
1325> content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1326> )
1327> }
1328>
1329> val forecast = httpClient.getForecast(latitude, longitude)
1330> CallToolResult(content = forecast.map { TextContent(it) })
1331> }
1332> ```
1333>
1334> ### Running the server
1335>
1336> Finally, implement the main function to run the server:
1337>
1338> ```kotlin theme={null}
1339> fun main() = runMcpServer()
1340> ```
1341>
1342> You can run the server directly during development:
1343>
1344> ```bash theme={null}
1345> ./gradlew run
1346> ```
1347>
1348> For production use, build the shadow JAR:
1349>
1350> ```bash theme={null}
1351> ./gradlew build
1352> java -jar build/libs/weather-0.1.0-all.jar
1353> ```
1354>
1355> Let's now test your server from an existing MCP host, Claude for Desktop.
1356>
1357> ## Testing your server with Claude for Desktop
1358>
1359> > [!NOTE]
1360> >
1361> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362>
1363> First, make sure you have Claude for Desktop installed. [You can install the latest version
1364> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365>
1366> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368> Make sure to create the file if it doesn't exist.
1369>
1370> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371>
1372> ```bash macOS/Linux theme={null}
1373> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374> ```
1375>
1376> ```powershell Windows theme={null}
1377> code $env:AppData\Claude\claude_desktop_config.json
1378> ```
1379>
1380> You'll then add your servers in the `mcpServers` key.
1381> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382>
1383> In this case, we'll add our single weather server like so:
1384>
1385> ```json macOS/Linux theme={null}
1386> {
1387> "mcpServers": {
1388> "weather": {
1389> "command": "java",
1390> "args": [
1391> "-jar",
1392> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1393> ]
1394> }
1395> }
1396> }
1397> ```
1398>
1399> ```json Windows theme={null}
1400> {
1401> "mcpServers": {
1402> "weather": {
1403> "command": "java",
1404> "args": [
1405> "-jar",
1406> "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1407> ]
1408> }
1409> }
1410> }
1411> ```
1412>
1413> This tells Claude for Desktop:
1414>
1415> 1. There's an MCP server named "weather"
1416> 2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417>
1418> Save the file, and restart **Claude for Desktop**.
1419 
1420> [tab] C#
1421>
1422> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1423>
1424> ### Prerequisite knowledge
1425>
1426> This quickstart assumes you have familiarity with:
1427>
1428> * C#
1429> * LLMs like Claude
1430> * .NET 8 or higher
1431>
1432> ### Logging in MCP Servers
1433>
1434> When implementing MCP servers, be careful about how you handle logging:
1435>
1436> **For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1437>
1438> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1439>
1440> ### Best Practices
1441>
1442> * Use a logging library that writes to stderr or files.
1443>
1444> ### System requirements
1445>
1446> * [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1447>
1448> ### Set up your environment
1449>
1450> First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1451>
1452> ```bash theme={null}
1453> dotnet --version
1454> ```
1455>
1456> Now, let's create and set up your project:
1457>
1458> ```bash macOS/Linux theme={null}
1459> # Create a new directory for our project
1460> mkdir weather
1461> cd weather
1462> # Initialize a new C# project
1463> dotnet new console
1464> ```
1465>
1466> ```powershell Windows theme={null}
1467> # Create a new directory for our project
1468> mkdir weather
1469> cd weather
1470> # Initialize a new C# project
1471> dotnet new console
1472> ```
1473>
1474> After running `dotnet new console`, you will be presented with a new C# project.
1475> You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476> Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1477> After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1478>
1479> ```bash theme={null}
1480> # Add the Model Context Protocol SDK NuGet package
1481> dotnet add package ModelContextProtocol --prerelease
1482> # Add the .NET Hosting NuGet package
1483> dotnet add package Microsoft.Extensions.Hosting
1484> ```
1485>
1486> Now let’s dive into building your server.
1487>
1488> ## Building your server
1489>
1490> Open the `Program.cs` file in your project and replace its contents with the following code:
1491>
1492> ```csharp theme={null}
1493> using Microsoft.Extensions.DependencyInjection;
1494> using Microsoft.Extensions.Hosting;
1495> using ModelContextProtocol;
1496> using System.Net.Http.Headers;
1497>
1498> var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499>
1500> builder.Services.AddMcpServer()
1501> .WithStdioServerTransport()
1502> .WithToolsFromAssembly();
1503>
1504> builder.Services.AddSingleton(_ =>
1505> {
1506> var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1507> client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1508> return client;
1509> });
1510>
1511> var app = builder.Build();
1512>
1513> await app.RunAsync();
1514> ```
1515>
1516> > [!NOTE]
1517> >
1518> > When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1519>
1520> This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1521>
1522> ### Weather API helper functions
1523>
1524> Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525>
1526> ```csharp theme={null}
1527> using System.Text.Json;
1528>
1529> internal static class HttpClientExt
1530> {
1531> public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1532> {
1533> using var response = await client.GetAsync(requestUri);
1534> response.EnsureSuccessStatusCode();
1535> return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1536> }
1537> }
1538> ```
1539>
1540> Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541>
1542> ```csharp theme={null}
1543> using ModelContextProtocol.Server;
1544> using System.ComponentModel;
1545> using System.Globalization;
1546> using System.Text.Json;
1547>
1548> namespace QuickstartWeatherServer.Tools;
1549>
1550> [McpServerToolType]
1551> public static class WeatherTools
1552> {
1553> [McpServerTool, Description("Get weather alerts for a US state code.")]
1554> public static async Task<string> GetAlerts(
1555> HttpClient client,
1556> [Description("The US state code to get alerts for.")] string state)
1557> {
1558> using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1559> var jsonElement = jsonDocument.RootElement;
1560> var alerts = jsonElement.GetProperty("features").EnumerateArray();
1561>
1562> if (!alerts.Any())
1563> {
1564> return "No active alerts for this state.";
1565> }
1566>
1567> return string.Join("\n--\n", alerts.Select(alert =>
1568> {
1569> JsonElement properties = alert.GetProperty("properties");
1570> return $"""
1571> Event: {properties.GetProperty("event").GetString()}
1572> Area: {properties.GetProperty("areaDesc").GetString()}
1573> Severity: {properties.GetProperty("severity").GetString()}
1574> Description: {properties.GetProperty("description").GetString()}
1575> Instruction: {properties.GetProperty("instruction").GetString()}
1576> """;
1577> }));
1578> }
1579>
1580> [McpServerTool, Description("Get weather forecast for a location.")]
1581> public static async Task<string> GetForecast(
1582> HttpClient client,
1583> [Description("Latitude of the location.")] double latitude,
1584> [Description("Longitude of the location.")] double longitude)
1585> {
1586> var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1587> using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1588> var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1589> ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1590>
1591> using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1592> var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1593>
1594> return string.Join("\n---\n", periods.Select(period => $"""
1595> {period.GetProperty("name").GetString()}
1596> Temperature: {period.GetProperty("temperature").GetInt32()}°F
1597> Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1598> Forecast: {period.GetProperty("detailedForecast").GetString()}
1599> """));
1600> }
1601> }
1602> ```
1603>
1604> ### Running the server
1605>
1606> Finally, run the server using the following command:
1607>
1608> ```bash theme={null}
1609> dotnet run
1610> ```
1611>
1612> This will start the server and listen for incoming requests on standard input/output.
1613>
1614> ## Testing your server with Claude for Desktop
1615>
1616> > [!NOTE]
1617> >
1618> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619>
1620> First, make sure you have Claude for Desktop installed. [You can install the latest version
1621> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1623> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624>
1625> ```bash macOS/Linux theme={null}
1626> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627> ```
1628>
1629> ```powershell Windows theme={null}
1630> code $env:AppData\Claude\claude_desktop_config.json
1631> ```
1632>
1633> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1634> In this case, we'll add our single weather server like so:
1635>
1636> ```json macOS/Linux theme={null}
1637> {
1638> "mcpServers": {
1639> "weather": {
1640> "command": "dotnet",
1641> "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1642> }
1643> }
1644> }
1645> ```
1646>
1647> ```json Windows theme={null}
1648> {
1649> "mcpServers": {
1650> "weather": {
1651> "command": "dotnet",
1652> "args": [
1653> "run",
1654> "--project",
1655> "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1656> "--no-build"
1657> ]
1658> }
1659> }
1660> }
1661> ```
1662>
1663> This tells Claude for Desktop:
1664>
1665> 1. There's an MCP server named "weather"
1666> 2. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667> Save the file, and restart **Claude for Desktop**.
1668 
1669> [tab] Ruby
1670>
1671> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1672>
1673> ### Prerequisite knowledge
1674>
1675> This quickstart assumes you have familiarity with:
1676>
1677> * Ruby
1678> * LLMs like Claude
1679>
1680> ### Logging in MCP Servers
1681>
1682> When implementing MCP servers, be careful about how you handle logging:
1683>
1684> **For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1685>
1686> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1687>
1688> ### Best Practices
1689>
1690> * Use a logging library that writes to stderr or files.
1691>
1692> ### Quick Examples
1693>
1694> ```ruby theme={null}
1695> # ❌ Bad (STDIO)
1696> puts "Processing request"
1697>
1698> # ✅ Good (STDIO)
1699> require "logger"
1700> logger = Logger.new($stderr)
1701> logger.info("Processing request")
1702> ```
1703>
1704> ### System requirements
1705>
1706> * Ruby 2.7 or higher installed.
1707>
1708> ### Set up your environment
1709>
1710> First, let's make sure you have Ruby installed. You can check by running:
1711>
1712> ```bash theme={null}
1713> ruby --version
1714> ```
1715>
1716> Now, let's create and set up our project:
1717>
1718> ```bash macOS/Linux theme={null}
1719> # Create a new directory for our project
1720> mkdir weather
1721> cd weather
1722>
1723> # Create a Gemfile
1724> bundle init
1725>
1726> # Add the MCP SDK dependency
1727> bundle add mcp
1728>
1729> # Create our server file
1730> touch weather.rb
1731> ```
1732>
1733> ```powershell Windows theme={null}
1734> # Create a new directory for our project
1735> mkdir weather
1736> cd weather
1737>
1738> # Create a Gemfile
1739> bundle init
1740>
1741> # Add the MCP SDK dependency
1742> bundle add mcp
1743>
1744> # Create our server file
1745> new-item weather.rb
1746> ```
1747>
1748> Now let's dive into building your server.
1749>
1750> ## Building your server
1751>
1752> ### Importing packages and setting up constants
1753>
1754> Open `weather.rb` and add these requires and constants at the top:
1755>
1756> ```ruby theme={null}
1757> require "json"
1758> require "mcp"
1759> require "net/http"
1760> require "uri"
1761>
1762> NWS_API_BASE = "https://api.weather.gov"
1763> USER_AGENT = "weather-app/1.0"
1764> ```
1765>
1766> The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767>
1768> ### Helper methods
1769>
1770> Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771>
1772> ```ruby theme={null}
1773> module HelperMethods
1774> def make_nws_request(url)
1775> uri = URI(url)
1776> request = Net::HTTP::Get.new(uri)
1777> request["User-Agent"] = USER_AGENT
1778> request["Accept"] = "application/geo+json"
1779>
1780> response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1781> http.request(request)
1782> end
1783>
1784> raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1785>
1786> JSON.parse(response.body)
1787> end
1788>
1789> def format_alert(feature)
1790> properties = feature["properties"]
1791>
1792> <<~ALERT
1793> Event: #{properties["event"] || "Unknown"}
1794> Area: #{properties["areaDesc"] || "Unknown"}
1795> Severity: #{properties["severity"] || "Unknown"}
1796> Description: #{properties["description"] || "No description available"}
1797> Instructions: #{properties["instruction"] || "No specific instructions provided"}
1798> ALERT
1799> end
1800> end
1801> ```
1802>
1803> ### Implementing tool execution
1804>
1805> Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806>
1807> ```ruby theme={null}
1808> class GetAlerts < MCP::Tool
1809> extend HelperMethods
1810>
1811> tool_name "get_alerts"
1812> description "Get weather alerts for a US state"
1813> input_schema(
1814> properties: {
1815> state: {
1816> type: "string",
1817> description: "Two-letter US state code (e.g. CA, NY)"
1818> }
1819> },
1820> required: ["state"]
1821> )
1822>
1823> def self.call(state:)
1824> url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1825> data = make_nws_request(url)
1826>
1827> if data["features"].empty?
1828> return MCP::Tool::Response.new([{
1829> type: "text",
1830> text: "No active alerts for this state."
1831> }])
1832> end
1833>
1834> alerts = data["features"].map { |feature| format_alert(feature) }
1835> MCP::Tool::Response.new([{
1836> type: "text",
1837> text: alerts.join("\n---\n")
1838> }])
1839> end
1840> end
1841>
1842> class GetForecast < MCP::Tool
1843> extend HelperMethods
1844>
1845> tool_name "get_forecast"
1846> description "Get weather forecast for a location"
1847> input_schema(
1848> properties: {
1849> latitude: {
1850> type: "number",
1851> description: "Latitude of the location"
1852> },
1853> longitude: {
1854> type: "number",
1855> description: "Longitude of the location"
1856> }
1857> },
1858> required: ["latitude", "longitude"]
1859> )
1860>
1861> def self.call(latitude:, longitude:)
1862> # First get the forecast grid endpoint.
1863> points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1864> points_data = make_nws_request(points_url)
1865>
1866> # Get the forecast URL from the points response.
1867> forecast_url = points_data["properties"]["forecast"]
1868> forecast_data = make_nws_request(forecast_url)
1869>
1870> # Format the periods into a readable forecast.
1871> periods = forecast_data["properties"]["periods"]
1872> forecasts = periods.first(5).map do |period|
1873> <<~FORECAST
1874> #{period["name"]}:
1875> Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1876> Wind: #{period["windSpeed"]} #{period["windDirection"]}
1877> Forecast: #{period["detailedForecast"]}
1878> FORECAST
1879> end
1880>
1881> MCP::Tool::Response.new([{
1882> type: "text",
1883> text: forecasts.join("\n---\n")
1884> }])
1885> end
1886> end
1887> ```
1888>
1889> ### Running the server
1890>
1891> Finally, initialize and run the server:
1892>
1893> ```ruby theme={null}
1894> server = MCP::Server.new(
1895> name: "weather",
1896> version: "1.0.0",
1897> tools: [GetAlerts, GetForecast]
1898> )
1899>
1900> transport = MCP::Server::Transports::StdioTransport.new(server)
1901> transport.open
1902> ```
1903>
1904> Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905>
1906> Let's now test your server from an existing MCP host, Claude for Desktop.
1907>
1908> ## Testing your server with Claude for Desktop
1909>
1910> > [!NOTE]
1911> >
1912> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913>
1914> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1915>
1916> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1917>
1918> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919>
1920> ```bash macOS/Linux theme={null}
1921> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922> ```
1923>
1924> ```powershell Windows theme={null}
1925> code $env:AppData\Claude\claude_desktop_config.json
1926> ```
1927>
1928> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1929>
1930> In this case, we'll add our single weather server like so:
1931>
1932> ```json macOS/Linux theme={null}
1933> {
1934> "mcpServers": {
1935> "weather": {
1936> "command": "bundle",
1937> "args": ["exec", "ruby", "weather.rb"],
1938> "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1939> }
1940> }
1941> }
1942> ```
1943>
1944> ```json Windows theme={null}
1945> {
1946> "mcpServers": {
1947> "weather": {
1948> "command": "bundle",
1949> "args": ["exec", "ruby", "weather.rb"],
1950> "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1951> }
1952> }
1953> }
1954> ```
1955>
1956> > [!NOTE]
1957> >
1958> > Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1959>
1960> This tells Claude for Desktop:
1961>
1962> 1. There's an MCP server named "weather"
1963> 2. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964>
1965> Save the file, and restart **Claude for Desktop**.
1966 
1967> [tab] Rust
1968>
1969> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1970>
1971> ### Prerequisite knowledge
1972>
1973> This quickstart assumes you have familiarity with:
1974>
1975> * Rust programming language
1976> * Async/await in Rust
1977> * LLMs like Claude
1978>
1979> ### Logging in MCP Servers
1980>
1981> When implementing MCP servers, be careful about how you handle logging:
1982>
1983> **For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1984>
1985> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1986>
1987> ### Best Practices
1988>
1989> * Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1990> * Configure your logging framework to avoid stdout output.
1991>
1992> ### Quick Examples
1993>
1994> ```rust theme={null}
1995> // ❌ Bad (STDIO)
1996> println!("Processing request");
1997>
1998> // ✅ Good (STDIO)
1999> eprintln!("Processing request"); // writes to stderr
2000> ```
2001>
2002> ### System requirements
2003>
2004> * Rust 1.70 or higher installed.
2005> * Cargo (comes with Rust installation).
2006>
2007> ### Set up your environment
2008>
2009> First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2010>
2011> ```bash macOS/Linux theme={null}
2012> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2013> ```
2014>
2015> ```powershell Windows theme={null}
2016> # Download and run rustup-init.exe from https://rustup.rs/
2017> ```
2018>
2019> Verify your Rust installation:
2020>
2021> ```bash theme={null}
2022> rustc --version
2023> cargo --version
2024> ```
2025>
2026> Now, let's create and set up our project:
2027>
2028> ```bash macOS/Linux theme={null}
2029> # Create a new Rust project
2030> cargo new weather
2031> cd weather
2032> ```
2033>
2034> ```powershell Windows theme={null}
2035> # Create a new Rust project
2036> cargo new weather
2037> cd weather
2038> ```
2039>
2040> Update your `Cargo.toml` to add the required dependencies:
2041>
2042> ```toml Cargo.toml theme={null}
2043> [package]
2044> name = "weather"
2045> version = "0.1.0"
2046> edition = "2024"
2047>
2048> [dependencies]
2049> rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050> tokio = { version = "1.46", features = ["full"] }
2051> reqwest = { version = "0.12", features = ["json"] }
2052> serde = { version = "1.0", features = ["derive"] }
2053> serde_json = "1.0"
2054> anyhow = "1.0"
2055> tracing = "0.1"
2056> tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057> ```
2058>
2059> Now let's dive into building your server.
2060>
2061> ## Building your server
2062>
2063> ### Importing packages and constants
2064>
2065> Open `src/main.rs` and add these imports and constants at the top:
2066>
2067> ```rust theme={null}
2068> use anyhow::Result;
2069> use rmcp::{
2070> ServerHandler, ServiceExt,
2071> handler::server::{router::tool::ToolRouter, tool::Parameters},
2072> model::*,
2073> schemars, tool, tool_handler, tool_router,
2074> };
2075> use serde::Deserialize;
2076> use serde::de::DeserializeOwned;
2077>
2078> const NWS_API_BASE: &str = "https://api.weather.gov";
2079> const USER_AGENT: &str = "weather-app/1.0";
2080> ```
2081>
2082> The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2083>
2084> ### Data structures
2085>
2086> Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087>
2088> ```rust theme={null}
2089> #[derive(Debug, Deserialize)]
2090> struct AlertsResponse {
2091> features: Vec<AlertFeature>,
2092> }
2093>
2094> #[derive(Debug, Deserialize)]
2095> struct AlertFeature {
2096> properties: AlertProperties,
2097> }
2098>
2099> #[derive(Debug, Deserialize)]
2100> struct AlertProperties {
2101> event: Option<String>,
2102> #[serde(rename = "areaDesc")]
2103> area_desc: Option<String>,
2104> severity: Option<String>,
2105> description: Option<String>,
2106> instruction: Option<String>,
2107> }
2108>
2109> #[derive(Debug, Deserialize)]
2110> struct PointsResponse {
2111> properties: PointsProperties,
2112> }
2113>
2114> #[derive(Debug, Deserialize)]
2115> struct PointsProperties {
2116> forecast: String,
2117> }
2118>
2119> #[derive(Debug, Deserialize)]
2120> struct ForecastResponse {
2121> properties: ForecastProperties,
2122> }
2123>
2124> #[derive(Debug, Deserialize)]
2125> struct ForecastProperties {
2126> periods: Vec<ForecastPeriod>,
2127> }
2128>
2129> #[derive(Debug, Deserialize)]
2130> struct ForecastPeriod {
2131> name: String,
2132> temperature: i32,
2133> #[serde(rename = "temperatureUnit")]
2134> temperature_unit: String,
2135> #[serde(rename = "windSpeed")]
2136> wind_speed: String,
2137> #[serde(rename = "windDirection")]
2138> wind_direction: String,
2139> #[serde(rename = "detailedForecast")]
2140> detailed_forecast: String,
2141> }
2142> ```
2143>
2144> Now define the request types that MCP clients will send:
2145>
2146> ```rust theme={null}
2147> #[derive(serde::Deserialize, schemars::JsonSchema)]
2148> pub struct MCPForecastRequest {
2149> latitude: f32,
2150> longitude: f32,
2151> }
2152>
2153> #[derive(serde::Deserialize, schemars::JsonSchema)]
2154> pub struct MCPAlertRequest {
2155> state: String,
2156> }
2157> ```
2158>
2159> ### Helper functions
2160>
2161> Add helper functions for making API requests and formatting responses:
2162>
2163> ```rust theme={null}
2164> async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2165> let client = reqwest::Client::new();
2166> let rsp = client
2167> .get(url)
2168> .header(reqwest::header::USER_AGENT, USER_AGENT)
2169> .header(reqwest::header::ACCEPT, "application/geo+json")
2170> .send()
2171> .await?
2172> .error_for_status()?;
2173> Ok(rsp.json::<T>().await?)
2174> }
2175>
2176> fn format_alert(feature: &AlertFeature) -> String {
2177> let props = &feature.properties;
2178> format!(
2179> "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2180> props.event.as_deref().unwrap_or("Unknown"),
2181> props.area_desc.as_deref().unwrap_or("Unknown"),
2182> props.severity.as_deref().unwrap_or("Unknown"),
2183> props
2184> .description
2185> .as_deref()
2186> .unwrap_or("No description available"),
2187> props
2188> .instruction
2189> .as_deref()
2190> .unwrap_or("No specific instructions provided")
2191> )
2192> }
2193>
2194> fn format_period(period: &ForecastPeriod) -> String {
2195> format!(
2196> "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2197> period.name,
2198> period.temperature,
2199> period.temperature_unit,
2200> period.wind_speed,
2201> period.wind_direction,
2202> period.detailed_forecast
2203> )
2204> }
2205> ```
2206>
2207> ### Implementing the Weather server and tools
2208>
2209> Now let's implement the main Weather server struct with the tool handlers:
2210>
2211> ```rust theme={null}
2212> pub struct Weather {
2213> tool_router: ToolRouter<Weather>,
2214> }
2215>
2216> #[tool_router]
2217> impl Weather {
2218> fn new() -> Self {
2219> Self {
2220> tool_router: Self::tool_router(),
2221> }
2222> }
2223>
2224> #[tool(description = "Get weather alerts for a US state.")]
2225> async fn get_alerts(
2226> &self,
2227> Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2228> ) -> String {
2229> let url = format!(
2230> "{}/alerts/active/area/{}",
2231> NWS_API_BASE,
2232> state.to_uppercase()
2233> );
2234>
2235> match make_nws_request::<AlertsResponse>(&url).await {
2236> Ok(data) => {
2237> if data.features.is_empty() {
2238> "No active alerts for this state.".to_string()
2239> } else {
2240> data.features
2241> .iter()
2242> .map(format_alert)
2243> .collect::<Vec<_>>()
2244> .join("\n---\n")
2245> }
2246> }
2247> Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2248> }
2249> }
2250>
2251> #[tool(description = "Get weather forecast for a location.")]
2252> async fn get_forecast(
2253> &self,
2254> Parameters(MCPForecastRequest {
2255> latitude,
2256> longitude,
2257> }): Parameters<MCPForecastRequest>,
2258> ) -> String {
2259> let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2260> let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2261> return "Unable to fetch forecast data for this location.".to_string();
2262> };
2263>
2264> let forecast_url = points_data.properties.forecast;
2265>
2266> let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2267> return "Unable to fetch forecast data for this location.".to_string();
2268> };
2269>
2270> let periods = &forecast_data.properties.periods;
2271> let forecast_summary: String = periods
2272> .iter()
2273> .take(5) // Next 5 periods only
2274> .map(format_period)
2275> .collect::<Vec<String>>()
2276> .join("\n---\n");
2277> forecast_summary
2278> }
2279> }
2280> ```
2281>
2282> The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283>
2284> ### Implementing the ServerHandler
2285>
2286> Implement the `ServerHandler` trait to define server capabilities:
2287>
2288> ```rust theme={null}
2289> #[tool_handler]
2290> impl ServerHandler for Weather {
2291> fn get_info(&self) -> ServerInfo {
2292> ServerInfo {
2293> capabilities: ServerCapabilities::builder().enable_tools().build(),
2294> ..Default::default()
2295> }
2296> }
2297> }
2298> ```
2299>
2300> ### Running the server
2301>
2302> Finally, implement the main function to run the server with stdio transport:
2303>
2304> ```rust theme={null}
2305> #[tokio::main]
2306> async fn main() -> Result<()> {
2307> let transport = (tokio::io::stdin(), tokio::io::stdout());
2308> let service = Weather::new().serve(transport).await?;
2309> service.waiting().await?;
2310> Ok(())
2311> }
2312> ```
2313>
2314> Build your server with:
2315>
2316> ```bash theme={null}
2317> cargo build --release
2318> ```
2319>
2320> The compiled binary will be in `target/release/weather`.
2321>
2322> Let's now test your server from an existing MCP host, Claude for Desktop.
2323>
2324> ## Testing your server with Claude for Desktop
2325>
2326> > [!NOTE]
2327> >
2328> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329>
2330> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2331>
2332> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2333>
2334> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335>
2336> ```bash macOS/Linux theme={null}
2337> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338> ```
2339>
2340> ```powershell Windows theme={null}
2341> code $env:AppData\Claude\claude_desktop_config.json
2342> ```
2343>
2344> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2345>
2346> In this case, we'll add our single weather server like so:
2347>
2348> ```json macOS/Linux theme={null}
2349> {
2350> "mcpServers": {
2351> "weather": {
2352> "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2353> }
2354> }
2355> }
2356> ```
2357>
2358> ```json Windows theme={null}
2359> {
2360> "mcpServers": {
2361> "weather": {
2362> "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2363> }
2364> }
2365> }
2366> ```
2367>
2368> > [!NOTE]
2369> >
2370> > Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2371>
2372> This tells Claude for Desktop:
2373>
2374> 1. There's an MCP server named "weather"
2375> 2. Launch it by running the compiled binary at the specified path
2376>
2377> Save the file, and restart **Claude for Desktop**.
2378 
2379> [tab] Go
2380>
2381> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2382>
2383> ### Prerequisite knowledge
2384>
2385> This quickstart assumes you have familiarity with:
2386>
2387> * Go
2388> * LLMs like Claude
2389>
2390> ### Logging in MCP Servers
2391>
2392> When implementing MCP servers, be careful about how you handle logging:
2393>
2394> **For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2395>
2396> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2397>
2398> ### Best Practices
2399>
2400> * Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2401> * Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2402>
2403> ### Quick Examples
2404>
2405> ```go theme={null}
2406> // ❌ Bad (STDIO)
2407> fmt.Println("Processing request")
2408>
2409> // ✅ Good (STDIO)
2410> log.Println("Processing request") // defaults to stderr
2411>
2412> // ✅ Good (STDIO)
2413> fmt.Fprintln(os.Stderr, "Processing request")
2414> ```
2415>
2416> ### System requirements
2417>
2418> * Go 1.24 or higher installed.
2419>
2420> ### Set up your environment
2421>
2422> First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423>
2424> Verify your Go installation:
2425>
2426> ```bash theme={null}
2427> go version
2428> ```
2429>
2430> Now, let's create and set up our project:
2431>
2432> ```bash macOS/Linux theme={null}
2433> # Create a new directory for our project
2434> mkdir weather
2435> cd weather
2436>
2437> # Initialize Go module
2438> go mod init weather
2439>
2440> # Install dependencies
2441> go get github.com/modelcontextprotocol/go-sdk/mcp
2442>
2443> # Create our server file
2444> touch main.go
2445> ```
2446>
2447> ```powershell Windows theme={null}
2448> # Create a new directory for our project
2449> md weather
2450> cd weather
2451>
2452> # Initialize Go module
2453> go mod init weather
2454>
2455> # Install dependencies
2456> go get github.com/modelcontextprotocol/go-sdk/mcp
2457>
2458> # Create our server file
2459> new-item main.go
2460> ```
2461>
2462> Now let's dive into building your server.
2463>
2464> ## Building your server
2465>
2466> ### Importing packages and constants
2467>
2468> Add these to the top of your `main.go`:
2469>
2470> ```go theme={null}
2471> package main
2472>
2473> import (
2474> "cmp"
2475> "context"
2476> "encoding/json"
2477> "fmt"
2478> "io"
2479> "log"
2480> "net/http"
2481> "strings"
2482>
2483> "github.com/modelcontextprotocol/go-sdk/mcp"
2484> )
2485>
2486> const (
2487> NWSAPIBase = "https://api.weather.gov"
2488> UserAgent = "weather-app/1.0"
2489> )
2490> ```
2491>
2492> ### Data structures
2493>
2494> Next, let's define the data structures used by our tools:
2495>
2496> ```go theme={null}
2497> type PointsResponse struct {
2498> Properties struct {
2499> Forecast string `json:"forecast"`
2500> } `json:"properties"`
2501> }
2502>
2503> type ForecastResponse struct {
2504> Properties struct {
2505> Periods []ForecastPeriod `json:"periods"`
2506> } `json:"properties"`
2507> }
2508>
2509> type ForecastPeriod struct {
2510> Name string `json:"name"`
2511> Temperature int `json:"temperature"`
2512> TemperatureUnit string `json:"temperatureUnit"`
2513> WindSpeed string `json:"windSpeed"`
2514> WindDirection string `json:"windDirection"`
2515> DetailedForecast string `json:"detailedForecast"`
2516> }
2517>
2518> type AlertsResponse struct {
2519> Features []AlertFeature `json:"features"`
2520> }
2521>
2522> type AlertFeature struct {
2523> Properties AlertProperties `json:"properties"`
2524> }
2525>
2526> type AlertProperties struct {
2527> Event string `json:"event"`
2528> AreaDesc string `json:"areaDesc"`
2529> Severity string `json:"severity"`
2530> Description string `json:"description"`
2531> Instruction string `json:"instruction"`
2532> }
2533>
2534> type ForecastInput struct {
2535> Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536> Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537> }
2538>
2539> type AlertsInput struct {
2540> State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541> }
2542> ```
2543>
2544> ### Helper functions
2545>
2546> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547>
2548> ```go theme={null}
2549> func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2550> req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2551> if err != nil {
2552> return nil, fmt.Errorf("failed to create request: %w", err)
2553> }
2554>
2555> req.Header.Set("User-Agent", UserAgent)
2556> req.Header.Set("Accept", "application/geo+json")
2557>
2558> client := http.DefaultClient
2559> resp, err := client.Do(req)
2560> if err != nil {
2561> return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2562> }
2563> defer resp.Body.Close()
2564>
2565> if resp.StatusCode != http.StatusOK {
2566> body, _ := io.ReadAll(resp.Body)
2567> return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2568> }
2569>
2570> var result T
2571> if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2572> return nil, fmt.Errorf("failed to decode response: %w", err)
2573> }
2574>
2575> return &result, nil
2576> }
2577>
2578> func formatAlert(alert AlertFeature) string {
2579> props := alert.Properties
2580> event := cmp.Or(props.Event, "Unknown")
2581> areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2582> severity := cmp.Or(props.Severity, "Unknown")
2583> description := cmp.Or(props.Description, "No description available")
2584> instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2585>
2586> return fmt.Sprintf(`
2587> Event: %s
2588> Area: %s
2589> Severity: %s
2590> Description: %s
2591> Instructions: %s
2592> `, event, areaDesc, severity, description, instruction)
2593> }
2594>
2595> func formatPeriod(period ForecastPeriod) string {
2596> return fmt.Sprintf(`
2597> %s:
2598> Temperature: %d°%s
2599> Wind: %s %s
2600> Forecast: %s
2601> `, period.Name, period.Temperature, period.TemperatureUnit,
2602> period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603> }
2604> ```
2605>
2606> ### Implementing tool execution
2607>
2608> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609>
2610> ```go theme={null}
2611> func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2612> *mcp.CallToolResult, any, error,
2613> ) {
2614> // Get points data
2615> pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2616> pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2617> if err != nil {
2618> return &mcp.CallToolResult{
2619> Content: []mcp.Content{
2620> &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2621> },
2622> }, nil, nil
2623> }
2624>
2625> // Get forecast data
2626> forecastURL := pointsData.Properties.Forecast
2627> if forecastURL == "" {
2628> return &mcp.CallToolResult{
2629> Content: []mcp.Content{
2630> &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2631> },
2632> }, nil, nil
2633> }
2634>
2635> forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2636> if err != nil {
2637> return &mcp.CallToolResult{
2638> Content: []mcp.Content{
2639> &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2640> },
2641> }, nil, nil
2642> }
2643>
2644> // Format the periods
2645> periods := forecastData.Properties.Periods
2646> if len(periods) == 0 {
2647> return &mcp.CallToolResult{
2648> Content: []mcp.Content{
2649> &mcp.TextContent{Text: "No forecast periods available."},
2650> },
2651> }, nil, nil
2652> }
2653>
2654> // Show next 5 periods
2655> var forecasts []string
2656> for i := range min(5, len(periods)) {
2657> forecasts = append(forecasts, formatPeriod(periods[i]))
2658> }
2659>
2660> result := strings.Join(forecasts, "\n---\n")
2661>
2662> return &mcp.CallToolResult{
2663> Content: []mcp.Content{
2664> &mcp.TextContent{Text: result},
2665> },
2666> }, nil, nil
2667> }
2668>
2669> func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2670> *mcp.CallToolResult, any, error,
2671> ) {
2672> // Build alerts URL
2673> stateCode := strings.ToUpper(input.State)
2674> alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2675>
2676> alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2677> if err != nil {
2678> return &mcp.CallToolResult{
2679> Content: []mcp.Content{
2680> &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2681> },
2682> }, nil, nil
2683> }
2684>
2685> // Check if there are any alerts
2686> if len(alertsData.Features) == 0 {
2687> return &mcp.CallToolResult{
2688> Content: []mcp.Content{
2689> &mcp.TextContent{Text: "No active alerts for this state."},
2690> },
2691> }, nil, nil
2692> }
2693>
2694> // Format alerts
2695> var alerts []string
2696> for _, feature := range alertsData.Features {
2697> alerts = append(alerts, formatAlert(feature))
2698> }
2699>
2700> result := strings.Join(alerts, "\n---\n")
2701>
2702> return &mcp.CallToolResult{
2703> Content: []mcp.Content{
2704> &mcp.TextContent{Text: result},
2705> },
2706> }, nil, nil
2707> }
2708> ```
2709>
2710> ### Running the server
2711>
2712> Finally, implement the main function to run the server:
2713>
2714> ```go theme={null}
2715> func main() {
2716> // Create MCP server
2717> server := mcp.NewServer(&mcp.Implementation{
2718> Name: "weather",
2719> Version: "1.0.0",
2720> }, nil)
2721>
2722> // Add get_forecast tool
2723> mcp.AddTool(server, &mcp.Tool{
2724> Name: "get_forecast",
2725> Description: "Get weather forecast for a location",
2726> }, getForecast)
2727>
2728> // Add get_alerts tool
2729> mcp.AddTool(server, &mcp.Tool{
2730> Name: "get_alerts",
2731> Description: "Get weather alerts for a US state",
2732> }, getAlerts)
2733>
2734> // Run server on stdio transport
2735> if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2736> log.Fatal(err)
2737> }
2738> }
2739> ```
2740>
2741> Build your server with:
2742>
2743> ```bash theme={null}
2744> go build -o weather .
2745> ```
2746>
2747> The compiled binary will be in `./weather`.
2748>
2749> Let's now test your server from an existing MCP host, Claude for Desktop.
2750>
2751> ## Testing your server with Claude for Desktop
2752>
2753> > [!NOTE]
2754> >
2755> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756>
2757> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2758>
2759> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2760>
2761> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762>
2763> ```bash macOS/Linux theme={null}
2764> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765> ```
2766>
2767> ```powershell Windows theme={null}
2768> code $env:AppData\Claude\claude_desktop_config.json
2769> ```
2770>
2771> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2772>
2773> In this case, we'll add our single weather server like so:
2774>
2775> ```json macOS/Linux theme={null}
2776> {
2777> "mcpServers": {
2778> "weather": {
2779> "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2780> }
2781> }
2782> }
2783> ```
2784>
2785> ```json Windows theme={null}
2786> {
2787> "mcpServers": {
2788> "weather": {
2789> "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2790> }
2791> }
2792> }
2793> ```
2794>
2795> > [!NOTE]
2796> >
2797> > Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2798>
2799> This tells Claude for Desktop:
2800>
2801> 1. There's an MCP server named "weather"
2802> 2. Launch it by running the compiled binary at the specified path
2803>
2804> Save the file, and restart **Claude for Desktop**.
28052805 
28062806### Test with commands
28072807 
from line 2841
28412841 
28422842## Troubleshooting
28432843 
2844> **Claude for Desktop Integration Issues**
2844> [accordion] Claude for Desktop Integration Issues
28452845>
28462846> **Getting logs from Claude for Desktop**
28472847>
from line 2886
28862886>
28872887> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2024-11-05/tools/debugging.md) for better debugging tools and more detailed guidance.
28882888 
2889> **Weather API Issues**
2889> [accordion] Weather API Issues
28902890>
28912891> **Error: Failed to retrieve grid point data**
28922892>
from line 2912
29122912 
29132913## Next steps
29142914 
2915> **[Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md)**
2915> [card] [Building a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md)
29162916>
29172917> Learn how to build your own MCP client that can connect to your server
29182918 
2919> **[Example servers](https://modelcontextprotocol.io/examples.md)**
2919> [card] [Example servers](https://modelcontextprotocol.io/examples.md)
29202920>
29212921> Check out our gallery of official MCP servers and implementations
29222922 
2923> **[Debugging Guide](https://modelcontextprotocol.io/docs/2024-11-05/tools/debugging.md)**
2923> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2024-11-05/tools/debugging.md)
29242924>
29252925> Learn how to effectively debug MCP servers and integrations
29262926 
2927> **[Build with Agent Skills](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-with-agent-skills.md)**
2927> [card] [Build with Agent Skills](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-with-agent-skills.md)
29282928>
29292929> Use agent skills to guide AI coding assistants through server design
29302930 

docs/2024-11-05/develop/build-with-agent-skills Changed · +3 / -3 lines

from line 83
8383Once your agent scaffolds the server, iterate on tool descriptions and error
8484handling, then test and ship:
8585 
86> **[MCP Inspector](https://modelcontextprotocol.io/docs/2024-11-05/tools/inspector.md)**
86> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2024-11-05/tools/inspector.md)
8787>
8888> Test your server's tools, resources, and prompts interactively
8989 
90> **[Connect to a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-local-servers.md)**
90> [card] [Connect to a client](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-local-servers.md)
9191>
9292> Wire your server into an MCP client via local or remote configuration
9393 
94> **[Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)**
94> [card] [Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)
9595>
9696> Make your server discoverable in the MCP Registry
9797 

docs/2024-11-05/develop/connect-local-servers Changed · +9 / -9 lines

from line 156
156156 
157157If you encounter issues setting up or using the Filesystem Server, these solutions address common problems:
158158 
159> **Server not showing up in Claude / hammer icon missing**
159> [accordion] Server not showing up in Claude / hammer icon missing
160160>
161161> 1. Restart Claude Desktop completely
162162> 2. Check your `claude_desktop_config.json` file syntax
from line 172
172172> npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
173173> ```
174174 
175> **Getting logs from Claude Desktop**
175> [accordion] Getting logs from Claude Desktop
176176>
177177> Claude.app logging related to MCP is written to log files in:
178178>
from line 194
194194> type "%APPDATA%\Claude\logs\mcp*.log"
195195> ```
196196 
197> **Tool calls failing silently**
197> [accordion] Tool calls failing silently
198198>
199199> If Claude attempts to use the tools but they fail:
200200>
from line 202
202202> 2. Verify your server builds and runs without errors
203203> 3. Try restarting Claude Desktop
204204 
205> **None of this is working. What do I do?**
205> [accordion] None of this is working. What do I do?
206206>
207207> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2024-11-05/tools/debugging.md) for better debugging tools and more detailed guidance.
208208 
209> **ENOENT error and `$\{APPDATA\}` in paths on Windows**
209> [accordion] ENOENT error and `$\{APPDATA\}` in paths on Windows
210210>
211211> If your configured server fails to load, and you see within its logs an error referring to `${APPDATA}` within a path, you may need to add the expanded value of `%APPDATA%` to your `env` key in `claude_desktop_config.json`:
212212>
from line 239
239239 
240240Now that you've successfully connected Claude Desktop to a local MCP server, explore these options to expand your setup:
241241 
242> **[Explore other servers](https://github.com/modelcontextprotocol/servers)**
242> [card] [Explore other servers](https://github.com/modelcontextprotocol/servers)
243243>
244244> Browse our collection of official and community-created MCP servers for
245245> additional capabilities
246246 
247> **[Build your own server](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)**
247> [card] [Build your own server](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)
248248>
249249> Create custom MCP servers tailored to your specific workflows and
250250> integrations
251251 
252> **[Connect to remote servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-remote-servers.md)**
252> [card] [Connect to remote servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-remote-servers.md)
253253>
254254> Learn how to connect Claude to remote MCP servers for cloud-based tools and
255255> services
256256 
257> **[Understand the protocol](https://modelcontextprotocol.io/docs/2024-11-05/learn/architecture.md)**
257> [card] [Understand the protocol](https://modelcontextprotocol.io/docs/2024-11-05/learn/architecture.md)
258258>
259259> Dive deeper into how MCP works and its architecture
260260 

docs/2024-11-05/develop/connect-remote-servers Changed · +4 / -4 lines

from line 81
8181 
8282Now that you've connected Claude to a remote MCP server, you can explore its capabilities in your conversations. Try using the connected tools to automate tasks, access external data, or integrate with your existing workflows.
8383 
84> **[Build your own remote server](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)**
84> [card] [Build your own remote server](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)
8585>
8686> Create custom remote MCP servers to integrate with proprietary tools and
8787> services
8888 
89> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
89> [card] [Explore available servers](https://github.com/modelcontextprotocol/servers)
9090>
9191> Browse our collection of official and community-created MCP servers
9292 
93> **[Connect local servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-local-servers.md)**
93> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-local-servers.md)
9494>
9595> Learn how to connect Claude Desktop to local MCP servers for direct system
9696> access
9797 
98> **[Understand the architecture](https://modelcontextprotocol.io/docs/2024-11-05/learn/architecture.md)**
98> [card] [Understand the architecture](https://modelcontextprotocol.io/docs/2024-11-05/learn/architecture.md)
9999>
100100> Dive deeper into how MCP works and its architecture
101101 

docs/2024-11-05/getting-started/intro Changed · +6 / -6 lines

from line 29
2929 
3030## Start Building
3131 
32> **[Build servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)**
32> [card] [Build servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)
3333>
3434> Create MCP servers to expose your data and tools
3535 
36> **[Build clients](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md)**
36> [card] [Build clients](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-client.md)
3737>
3838> Develop applications that connect to MCP servers
3939 
40> **[Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)**
40> [card] [Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)
4141>
4242> Build interactive apps that run inside AI clients
4343 
4444## Learn more
4545 
46> **[Architecture](https://modelcontextprotocol.io/docs/2024-11-05/learn/architecture.md)**
46> [card] [Architecture](https://modelcontextprotocol.io/docs/2024-11-05/learn/architecture.md)
4747>
4848> Learn the core concepts and architecture of MCP
4949 
50> **[Security](https://modelcontextprotocol.io/docs/2024-11-05/tutorials/security/security_best_practices.md)**
50> [card] [Security](https://modelcontextprotocol.io/docs/2024-11-05/tutorials/security/security_best_practices.md)
5151>
5252> Understand the security considerations and best practices for MCP
5353 
5454## Community
5555 
56> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
56> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
5757>
5858> Learn how to get involved and contribute to MCP
5959 

docs/2024-11-05/tools/debugging Changed · +3 / -3 lines

from line 324
324324 
325325## Next steps
326326 
327> **[MCP Inspector](https://modelcontextprotocol.io/docs/2024-11-05/tools/inspector.md)**
327> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2024-11-05/tools/inspector.md)
328328>
329329> Learn to use the MCP Inspector
330330 
331> **[Build an MCP server](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)**
331> [card] [Build an MCP server](https://modelcontextprotocol.io/docs/2024-11-05/develop/build-server.md)
332332>
333333> Walk through building a server from scratch
334334 
335> **[Connect local servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-local-servers.md)**
335> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2024-11-05/develop/connect-local-servers.md)
336336>
337337> Full claude\_desktop\_config.json reference and troubleshooting
338338 

docs/2024-11-05/tools/inspector Changed · +31 / -31 lines

from line 22
2222 
2323A common way to start server packages from [npm](https://npmjs.com) or [PyPI](https://pypi.org).
2424 
25**npm package**
25> [tab] npm package
26>
27> ```bash theme={null}
28> npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29> # For example
30> npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31> ```
2632 
27```bash theme={null}
28npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29# For example
30npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31```
33> [tab] PyPI package
34>
35> ```bash theme={null}
36> npx @modelcontextprotocol/inspector uvx <package-name> <args>
37> # For example
38> npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39> ```
3240 
33**PyPI package**
34 
35```bash theme={null}
36npx @modelcontextprotocol/inspector uvx <package-name> <args>
37# For example
38npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39```
40 
4141#### Inspecting locally developed servers
4242 
4343To inspect servers locally developed or downloaded as a repository, the most common
4444way is:
4545 
46**TypeScript**
46> [tab] TypeScript
47>
48> ```bash theme={null}
49> npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50> ```
4751 
48```bash theme={null}
49npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50```
52> [tab] Python
53>
54> ```bash theme={null}
55> npx @modelcontextprotocol/inspector \
56> uv \
57> --directory path/to/server \
58> run \
59> package-name \
60> args...
61> ```
5162 
52**Python**
53 
54```bash theme={null}
55npx @modelcontextprotocol/inspector \
56 uv \
57 --directory path/to/server \
58 run \
59 package-name \
60 args...
61```
62 
6363Please carefully read any attached README for the most accurate instructions.
6464 
6565## Feature overview
from line 125
125125 
126126## Next steps
127127 
128> **[Inspector Repository](https://github.com/modelcontextprotocol/inspector)**
128> [card] [Inspector Repository](https://github.com/modelcontextprotocol/inspector)
129129>
130130> Check out the MCP Inspector source code
131131 
132> **[Debugging Guide](https://modelcontextprotocol.io/docs/2024-11-05/tools/debugging.md)**
132> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2024-11-05/tools/debugging.md)
133133>
134134> Learn about broader debugging strategies
135135 

docs/2024-11-05/tutorials/security/authorization Changed · +727 / -727 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 246
246246 
247247For our testing purposes, we will create an extremely simple MCP server that exposes two tools - one for addition and another for multiplication. The server will require authorization to access these.
248248 
249**TypeScript**
250 
251You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252 
253Prior to running the code below, ensure that you have a `.env` file with the following content:
254 
255```env theme={null}
256# Server host/port
257HOST=localhost
258PORT=3000
259 
260# Auth server location
261AUTH_HOST=localhost
262AUTH_PORT=8080
263AUTH_REALM=master
264 
265# Keycloak OAuth client credentials
266OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268```
269 
270`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271 
272In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273 
274```typescript theme={null}
275import "dotenv/config";
276import express from "express";
277import { randomUUID } from "node:crypto";
278import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281import { z } from "zod";
282import cors from "cors";
283import {
284 mcpAuthMetadataRouter,
285 getOAuthProtectedResourceMetadataUrl,
286} from "@modelcontextprotocol/sdk/server/auth/router.js";
287import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290const CONFIG = {
291 host: process.env.HOST || "localhost",
292 port: Number(process.env.PORT) || 3000,
293 auth: {
294 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295 port: Number(process.env.AUTH_PORT) || 8080,
296 realm: process.env.AUTH_REALM || "master",
297 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299 },
300};
301 
302function createOAuthUrls() {
303 const authBaseUrl = new URL(
304 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305 );
306 return {
307 issuer: authBaseUrl.toString(),
308 introspection_endpoint: new URL(
309 "protocol/openid-connect/token/introspect",
310 authBaseUrl,
311 ).toString(),
312 authorization_endpoint: new URL(
313 "protocol/openid-connect/auth",
314 authBaseUrl,
315 ).toString(),
316 token_endpoint: new URL(
317 "protocol/openid-connect/token",
318 authBaseUrl,
319 ).toString(),
320 };
321}
322 
323function createRequestLogger() {
324 return (req: any, res: any, next: any) => {
325 const start = Date.now();
326 res.on("finish", () => {
327 const ms = Date.now() - start;
328 console.log(
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
330 );
331 });
332 next();
333 };
334}
335 
336const app = express();
337 
338app.use(
339 express.json({
340 verify: (req: any, _res, buf) => {
341 req.rawBody = buf?.toString() ?? "";
342 },
343 }),
344);
345 
346app.use(
347 cors({
348 origin: "*",
349 exposedHeaders: ["Mcp-Session-Id"],
350 }),
351);
352 
353app.use(createRequestLogger());
354 
355const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356const oauthUrls = createOAuthUrls();
357 
358const oauthMetadata: OAuthMetadata = {
359 ...oauthUrls,
360 response_types_supported: ["code"],
361};
362 
363const tokenVerifier = {
364 verifyAccessToken: async (token: string) => {
365 const endpoint = oauthMetadata.introspection_endpoint;
366 
367 if (!endpoint) {
368 console.error("[auth] no introspection endpoint in metadata");
369 throw new Error("No token verification endpoint available in metadata");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) =>
430 checkResourceAllowed({
431 requestedResource: a,
432 configuredResource: mcpServerUrl,
433 }),
434 );
435 if (!allowed) {
436 throw new Error(
437 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
438 );
439 }
440 
441 return {
442 token,
443 clientId: data.client_id,
444 scopes: data.scope ? data.scope.split(" ") : [],
445 expiresAt: data.exp,
446 };
447 },
448};
449app.use(
450 mcpAuthMetadataRouter({
451 oauthMetadata,
452 resourceServerUrl: mcpServerUrl,
453 scopesSupported: ["mcp:tools"],
454 resourceName: "MCP Demo Server",
455 }),
456);
457 
458const authMiddleware = requireBearerAuth({
459 verifier: tokenVerifier,
460 requiredScopes: [],
461 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462});
463 
464const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465 
466function createMcpServer() {
467 const server = new McpServer({
468 name: "example-server",
469 version: "1.0.0",
470 });
471 
472 server.registerTool(
473 "add",
474 {
475 title: "Addition Tool",
476 description: "Add two numbers together",
477 inputSchema: {
478 a: z.number().describe("First number to add"),
479 b: z.number().describe("Second number to add"),
480 },
481 },
482 async ({ a, b }) => ({
483 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
484 }),
485 );
486 
487 server.registerTool(
488 "multiply",
489 {
490 title: "Multiplication Tool",
491 description: "Multiply two numbers together",
492 inputSchema: {
493 x: z.number().describe("First number to multiply"),
494 y: z.number().describe("Second number to multiply"),
495 },
496 },
497 async ({ x, y }) => ({
498 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
499 }),
500 );
501 
502 return server;
503}
504 
505const mcpPostHandler = async (req: express.Request, res: express.Response) => {
506 const sessionId = req.headers["mcp-session-id"] as string | undefined;
507 let transport: StreamableHTTPServerTransport;
508 
509 if (sessionId && transports[sessionId]) {
510 transport = transports[sessionId];
511 } else if (!sessionId && isInitializeRequest(req.body)) {
512 transport = new StreamableHTTPServerTransport({
513 sessionIdGenerator: () => randomUUID(),
514 onsessioninitialized: (sessionId) => {
515 transports[sessionId] = transport;
516 },
517 });
518 
519 transport.onclose = () => {
520 if (transport.sessionId) {
521 delete transports[transport.sessionId];
522 }
523 };
524 
525 const server = createMcpServer();
526 await server.connect(transport);
527 } else {
528 res.status(400).json({
529 jsonrpc: "2.0",
530 error: {
531 code: -32000,
532 message: "Bad Request: No valid session ID provided",
533 },
534 id: null,
535 });
536 return;
537 }
538 
539 await transport.handleRequest(req, res, req.body);
540};
541 
542const handleSessionRequest = async (
543 req: express.Request,
544 res: express.Response,
545) => {
546 const sessionId = req.headers["mcp-session-id"] as string | undefined;
547 if (!sessionId || !transports[sessionId]) {
548 res.status(400).send("Invalid or missing session ID");
549 return;
550 }
551 
552 const transport = transports[sessionId];
553 await transport.handleRequest(req, res);
554};
555 
556app.post("/", authMiddleware, mcpPostHandler);
557app.get("/", authMiddleware, handleSessionRequest);
558app.delete("/", authMiddleware, handleSessionRequest);
559 
560app.listen(CONFIG.port, CONFIG.host, () => {
561 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
562 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
563 console.log(
564 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
565 );
566});
567```
568 
569When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570 
571For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573**Python**
574 
575You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576 
577To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
578 
579Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
580 
581```python theme={null}
582"""Configuration settings for the MCP auth server."""
583 
584import os
585from typing import Optional
586 
587 
588class Config:
589 """Configuration class that loads from environment variables with sensible defaults."""
590 
591 # Server settings
592 HOST: str = os.getenv("HOST", "localhost")
593 PORT: int = int(os.getenv("PORT", "3000"))
594 
595 # Auth server settings
596 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
597 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
598 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
599 
600 # OAuth client settings
601 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
602 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
603 
604 # Server settings
605 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
606 OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
607 TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
608 
609 @property
610 def server_url(self) -> str:
611 """Build the server URL."""
612 return f"http://{self.HOST}:{self.PORT}"
613 
614 @property
615 def auth_base_url(self) -> str:
616 """Build the auth server base URL."""
617 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
618 
619 def validate(self) -> None:
620 """Validate configuration."""
621 if self.TRANSPORT not in ["sse", "streamable-http"]:
622 raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
623 
624 
625# Global configuration instance
626config = Config()
627 
628```
629 
630The server implementation is as follows:
631 
632```python theme={null}
633import datetime
634import logging
635from typing import Any
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server.auth.settings import AuthSettings
640from mcp.server.fastmcp.server import FastMCP
641 
642from .config import config
643from .token_verifier import IntrospectionTokenVerifier
644 
645logger = logging.getLogger(__name__)
646 
647 
648def create_oauth_urls() -> dict[str, str]:
649 """Create OAuth URLs based on configuration (Keycloak-style)."""
650 from urllib.parse import urljoin
651 
652 auth_base_url = config.auth_base_url
653 
654 return {
655 "issuer": auth_base_url,
656 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
657 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
658 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
659 }
660 
661 
662def create_server() -> FastMCP:
663 """Create and configure the FastMCP server."""
664 
665 config.validate()
666 
667 oauth_urls = create_oauth_urls()
668 
669 token_verifier = IntrospectionTokenVerifier(
670 introspection_endpoint=oauth_urls["introspection_endpoint"],
671 server_url=config.server_url,
672 client_id=config.OAUTH_CLIENT_ID,
673 client_secret=config.OAUTH_CLIENT_SECRET,
674 )
675 
676 app = FastMCP(
677 name="MCP Resource Server",
678 instructions="Resource Server that validates tokens via Authorization Server introspection",
679 host=config.HOST,
680 port=config.PORT,
681 debug=True,
682 streamable_http_path="/",
683 token_verifier=token_verifier,
684 auth=AuthSettings(
685 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
686 required_scopes=[config.MCP_SCOPE],
687 resource_server_url=AnyHttpUrl(config.server_url),
688 ),
689 )
690 
691 @app.tool()
692 async def add_numbers(a: float, b: float) -> dict[str, Any]:
693 """
694 Add two numbers together.
695 This tool demonstrates basic arithmetic operations with OAuth authentication.
696 
697 Args:
698 a: The first number to add
699 b: The second number to add
700 """
701 result = a + b
702 return {
703 "operation": "addition",
704 "operand_a": a,
705 "operand_b": b,
706 "result": result,
707 "timestamp": datetime.datetime.now().isoformat()
708 }
709 
710 @app.tool()
711 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
712 """
713 Multiply two numbers together.
714 This tool demonstrates basic arithmetic operations with OAuth authentication.
715 
716 Args:
717 x: The first number to multiply
718 y: The second number to multiply
719 """
720 result = x * y
721 return {
722 "operation": "multiplication",
723 "operand_x": x,
724 "operand_y": y,
725 "result": result,
726 "timestamp": datetime.datetime.now().isoformat()
727 }
728 
729 return app
730 
731 
732def main() -> int:
733 """
734 Run the MCP Resource Server.
735 
736 This server:
737 - Provides RFC 9728 Protected Resource Metadata
738 - Validates tokens via Authorization Server introspection
739 - Serves MCP tools requiring authentication
740 
741 Configuration is loaded from config.py and environment variables.
742 """
743 logging.basicConfig(level=logging.INFO)
744 
745 try:
746 config.validate()
747 oauth_urls = create_oauth_urls()
748 
749 except ValueError as e:
750 logger.error("Configuration error: %s", e)
751 return 1
752 
753 try:
754 mcp_server = create_server()
755 
756 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
757 logger.info("Authorization Server: %s", oauth_urls["issuer"])
758 logger.info("Transport: %s", config.TRANSPORT)
759 
760 mcp_server.run(transport=config.TRANSPORT)
761 return 0
762 
763 except Exception:
764 logger.exception("Server error")
765 return 1
766 
767 
768if __name__ == "__main__":
769 exit(main())
770```
771 
772Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts
773 
774```python theme={null}
775"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776 
777import logging
778from typing import Any
779 
780from mcp.server.auth.provider import AccessToken, TokenVerifier
781from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782 
783logger = logging.getLogger(__name__)
784 
785 
786class IntrospectionTokenVerifier(TokenVerifier):
787 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
788 """
789 
790 def __init__(
791 self,
792 introspection_endpoint: str,
793 server_url: str,
794 client_id: str,
795 client_secret: str,
796 ):
797 self.introspection_endpoint = introspection_endpoint
798 self.server_url = server_url
799 self.client_id = client_id
800 self.client_secret = client_secret
801 self.resource_url = resource_url_from_server_url(server_url)
802 
803 async def verify_token(self, token: str) -> AccessToken | None:
804 """Verify token via introspection endpoint."""
805 import httpx
806 
807 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
808 return None
809 
810 timeout = httpx.Timeout(10.0, connect=5.0)
811 limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
812 
813 async with httpx.AsyncClient(
814 timeout=timeout,
815 limits=limits,
816 verify=True,
817 ) as client:
818 try:
819 form_data = {
820 "token": token,
821 "client_id": self.client_id,
822 "client_secret": self.client_secret,
823 }
824 headers = {"Content-Type": "application/x-www-form-urlencoded"}
825 
826 response = await client.post(
827 self.introspection_endpoint,
828 data=form_data,
829 headers=headers,
830 )
831 
832 if response.status_code != 200:
833 return None
834 
835 data = response.json()
836 if not data.get("active", False):
837 return None
838 
839 if not self._validate_resource(data):
840 return None
841 
842 return AccessToken(
843 token=token,
844 client_id=data.get("client_id", "unknown"),
845 scopes=data.get("scope", "").split() if data.get("scope") else [],
846 expires_at=data.get("exp"),
847 resource=data.get("aud"), # Include resource in token
848 )
849 
850 except Exception as e:
851 return None
852 
853 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
854 """Validate token was issued for this resource server.
855 
856 Rules:
857 - Reject if 'aud' missing.
858 - Accept if any audience entry matches the derived resource URL.
859 - Supports string or list forms per JWT spec.
860 """
861 if not self.server_url or not self.resource_url:
862 return False
863 
864 aud: list[str] | str | None = token_data.get("aud")
865 if isinstance(aud, list):
866 return any(self._is_valid_resource(a) for a in aud)
867 if isinstance(aud, str):
868 return self._is_valid_resource(aud)
869 return False
870 
871 def _is_valid_resource(self, resource: str) -> bool:
872 """Check if the given resource matches our server."""
873 return check_resource_allowed(self.resource_url, resource)
874```
875 
876For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878**C#**
879 
880You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881 
882To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
883 
884```csharp theme={null}
885using Microsoft.AspNetCore.Authentication.JwtBearer;
886using Microsoft.IdentityModel.Tokens;
887using ModelContextProtocol.AspNetCore.Authentication;
888using ProtectedMcpServer.Tools;
889using System.Security.Claims;
890 
891var builder = WebApplication.CreateBuilder(args);
892 
893var serverUrl = "http://localhost:3000/";
894var authorizationServerUrl = "http://localhost:8080/realms/master/";
895 
896builder.Services.AddAuthentication(options =>
897{
898 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
899 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
900})
901.AddJwtBearer(options =>
902{
903 options.Authority = authorizationServerUrl;
904 var normalizedServerAudience = serverUrl.TrimEnd('/');
905 options.TokenValidationParameters = new TokenValidationParameters
906 {
907 ValidIssuer = authorizationServerUrl,
908 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
909 AudienceValidator = (audiences, securityToken, validationParameters) =>
910 {
911 if (audiences == null) return false;
912 foreach (var aud in audiences)
913 {
914 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
915 {
916 return true;
917 }
918 }
919 return false;
920 }
921 };
922 
923 options.RequireHttpsMetadata = false; // Set to true in production
924 
925 options.Events = new JwtBearerEvents
926 {
927 OnTokenValidated = context =>
928 {
929 var name = context.Principal?.Identity?.Name ?? "unknown";
930 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
931 Console.WriteLine($"Token validated for: {name} ({email})");
932 return Task.CompletedTask;
933 },
934 OnAuthenticationFailed = context =>
935 {
936 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937 return Task.CompletedTask;
938 },
939 };
940})
941.AddMcp(options =>
942{
943 options.ResourceMetadata = new()
944 {
945 Resource = new Uri(serverUrl),
946 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
947 AuthorizationServers = { new Uri(authorizationServerUrl) },
948 ScopesSupported = ["mcp:tools"]
949 };
950});
951 
952builder.Services.AddAuthorization();
953 
954builder.Services.AddHttpContextAccessor();
955builder.Services.AddMcpServer()
956 .WithTools<MathTools>()
957 .WithHttpTransport();
958 
959var app = builder.Build();
960 
961app.UseAuthentication();
962app.UseAuthorization();
963 
964app.MapMcp().RequireAuthorization();
965 
966Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969Console.WriteLine("Exposed Math tools: Add, Multiply");
970Console.WriteLine("Press Ctrl+C to stop the server");
971 
972app.Run(serverUrl);
973```
974 
975For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
249> [tab] TypeScript
250>
251> You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252>
253> Prior to running the code below, ensure that you have a `.env` file with the following content:
254>
255> ```env theme={null}
256> # Server host/port
257> HOST=localhost
258> PORT=3000
259>
260> # Auth server location
261> AUTH_HOST=localhost
262> AUTH_PORT=8080
263> AUTH_REALM=master
264>
265> # Keycloak OAuth client credentials
266> OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267> OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268> ```
269>
270> `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271>
272> In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273>
274> ```typescript theme={null}
275> import "dotenv/config";
276> import express from "express";
277> import { randomUUID } from "node:crypto";
278> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279> import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280> import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281> import { z } from "zod";
282> import cors from "cors";
283> import {
284> mcpAuthMetadataRouter,
285> getOAuthProtectedResourceMetadataUrl,
286> } from "@modelcontextprotocol/sdk/server/auth/router.js";
287> import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288> import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289> import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290> const CONFIG = {
291> host: process.env.HOST || "localhost",
292> port: Number(process.env.PORT) || 3000,
293> auth: {
294> host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295> port: Number(process.env.AUTH_PORT) || 8080,
296> realm: process.env.AUTH_REALM || "master",
297> clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298> clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299> },
300> };
301>
302> function createOAuthUrls() {
303> const authBaseUrl = new URL(
304> `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305> );
306> return {
307> issuer: authBaseUrl.toString(),
308> introspection_endpoint: new URL(
309> "protocol/openid-connect/token/introspect",
310> authBaseUrl,
311> ).toString(),
312> authorization_endpoint: new URL(
313> "protocol/openid-connect/auth",
314> authBaseUrl,
315> ).toString(),
316> token_endpoint: new URL(
317> "protocol/openid-connect/token",
318> authBaseUrl,
319> ).toString(),
320> };
321> }
322>
323> function createRequestLogger() {
324> return (req: any, res: any, next: any) => {
325> const start = Date.now();
326> res.on("finish", () => {
327> const ms = Date.now() - start;
328> console.log(
329> `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
330> );
331> });
332> next();
333> };
334> }
335>
336> const app = express();
337>
338> app.use(
339> express.json({
340> verify: (req: any, _res, buf) => {
341> req.rawBody = buf?.toString() ?? "";
342> },
343> }),
344> );
345>
346> app.use(
347> cors({
348> origin: "*",
349> exposedHeaders: ["Mcp-Session-Id"],
350> }),
351> );
352>
353> app.use(createRequestLogger());
354>
355> const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356> const oauthUrls = createOAuthUrls();
357>
358> const oauthMetadata: OAuthMetadata = {
359> ...oauthUrls,
360> response_types_supported: ["code"],
361> };
362>
363> const tokenVerifier = {
364> verifyAccessToken: async (token: string) => {
365> const endpoint = oauthMetadata.introspection_endpoint;
366>
367> if (!endpoint) {
368> console.error("[auth] no introspection endpoint in metadata");
369> throw new Error("No token verification endpoint available in metadata");
370> }
371>
372> const params = new URLSearchParams({
373> token: token,
374> client_id: CONFIG.auth.clientId,
375> });
376>
377> if (CONFIG.auth.clientSecret) {
378> params.set("client_secret", CONFIG.auth.clientSecret);
379> }
380>
381> let response: Response;
382> try {
383> response = await fetch(endpoint, {
384> method: "POST",
385> headers: {
386> "Content-Type": "application/x-www-form-urlencoded",
387> },
388> body: params.toString(),
389> });
390> } catch (e) {
391> console.error("[auth] introspection fetch threw", e);
392> throw e;
393> }
394>
395> if (!response.ok) {
396> const txt = await response.text();
397> console.error("[auth] introspection non-OK", { status: response.status });
398>
399> try {
400> const obj = JSON.parse(txt);
401> console.log(JSON.stringify(obj, null, 2));
402> } catch {
403> console.error(txt);
404> }
405> throw new Error(`Invalid or expired token: ${txt}`);
406> }
407>
408> let data: any;
409> try {
410> data = await response.json();
411> } catch (e) {
412> const txt = await response.text();
413> console.error("[auth] failed to parse introspection JSON", {
414> error: String(e),
415> body: txt,
416> });
417> throw e;
418> }
419>
420> if (data.active === false) {
421> throw new Error("Inactive token");
422> }
423>
424> if (!data.aud) {
425> throw new Error("Resource indicator (aud) missing");
426> }
427>
428> const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429> const allowed = audiences.some((a) =>
430> checkResourceAllowed({
431> requestedResource: a,
432> configuredResource: mcpServerUrl,
433> }),
434> );
435> if (!allowed) {
436> throw new Error(
437> `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
438> );
439> }
440>
441> return {
442> token,
443> clientId: data.client_id,
444> scopes: data.scope ? data.scope.split(" ") : [],
445> expiresAt: data.exp,
446> };
447> },
448> };
449> app.use(
450> mcpAuthMetadataRouter({
451> oauthMetadata,
452> resourceServerUrl: mcpServerUrl,
453> scopesSupported: ["mcp:tools"],
454> resourceName: "MCP Demo Server",
455> }),
456> );
457>
458> const authMiddleware = requireBearerAuth({
459> verifier: tokenVerifier,
460> requiredScopes: [],
461> resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462> });
463>
464> const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465>
466> function createMcpServer() {
467> const server = new McpServer({
468> name: "example-server",
469> version: "1.0.0",
470> });
471>
472> server.registerTool(
473> "add",
474> {
475> title: "Addition Tool",
476> description: "Add two numbers together",
477> inputSchema: {
478> a: z.number().describe("First number to add"),
479> b: z.number().describe("Second number to add"),
480> },
481> },
482> async ({ a, b }) => ({
483> content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
484> }),
485> );
486>
487> server.registerTool(
488> "multiply",
489> {
490> title: "Multiplication Tool",
491> description: "Multiply two numbers together",
492> inputSchema: {
493> x: z.number().describe("First number to multiply"),
494> y: z.number().describe("Second number to multiply"),
495> },
496> },
497> async ({ x, y }) => ({
498> content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
499> }),
500> );
501>
502> return server;
503> }
504>
505> const mcpPostHandler = async (req: express.Request, res: express.Response) => {
506> const sessionId = req.headers["mcp-session-id"] as string | undefined;
507> let transport: StreamableHTTPServerTransport;
508>
509> if (sessionId && transports[sessionId]) {
510> transport = transports[sessionId];
511> } else if (!sessionId && isInitializeRequest(req.body)) {
512> transport = new StreamableHTTPServerTransport({
513> sessionIdGenerator: () => randomUUID(),
514> onsessioninitialized: (sessionId) => {
515> transports[sessionId] = transport;
516> },
517> });
518>
519> transport.onclose = () => {
520> if (transport.sessionId) {
521> delete transports[transport.sessionId];
522> }
523> };
524>
525> const server = createMcpServer();
526> await server.connect(transport);
527> } else {
528> res.status(400).json({
529> jsonrpc: "2.0",
530> error: {
531> code: -32000,
532> message: "Bad Request: No valid session ID provided",
533> },
534> id: null,
535> });
536> return;
537> }
538>
539> await transport.handleRequest(req, res, req.body);
540> };
541>
542> const handleSessionRequest = async (
543> req: express.Request,
544> res: express.Response,
545> ) => {
546> const sessionId = req.headers["mcp-session-id"] as string | undefined;
547> if (!sessionId || !transports[sessionId]) {
548> res.status(400).send("Invalid or missing session ID");
549> return;
550> }
551>
552> const transport = transports[sessionId];
553> await transport.handleRequest(req, res);
554> };
555>
556> app.post("/", authMiddleware, mcpPostHandler);
557> app.get("/", authMiddleware, handleSessionRequest);
558> app.delete("/", authMiddleware, handleSessionRequest);
559>
560> app.listen(CONFIG.port, CONFIG.host, () => {
561> console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
562> console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
563> console.log(
564> `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
565> );
566> });
567> ```
568>
569> When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570>
571> For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573> [tab] Python
574>
575> You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576>
577> To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
578>
579> Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
580>
581> ```python theme={null}
582> """Configuration settings for the MCP auth server."""
583>
584> import os
585> from typing import Optional
586>
587>
588> class Config:
589> """Configuration class that loads from environment variables with sensible defaults."""
590>
591> # Server settings
592> HOST: str = os.getenv("HOST", "localhost")
593> PORT: int = int(os.getenv("PORT", "3000"))
594>
595> # Auth server settings
596> AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
597> AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
598> AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
599>
600> # OAuth client settings
601> OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
602> OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
603>
604> # Server settings
605> MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
606> OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
607> TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
608>
609> @property
610> def server_url(self) -> str:
611> """Build the server URL."""
612> return f"http://{self.HOST}:{self.PORT}"
613>
614> @property
615> def auth_base_url(self) -> str:
616> """Build the auth server base URL."""
617> return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
618>
619> def validate(self) -> None:
620> """Validate configuration."""
621> if self.TRANSPORT not in ["sse", "streamable-http"]:
622> raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
623>
624>
625> # Global configuration instance
626> config = Config()
627>
628> ```
629>
630> The server implementation is as follows:
631>
632> ```python theme={null}
633> import datetime
634> import logging
635> from typing import Any
636>
637> from pydantic import AnyHttpUrl
638>
639> from mcp.server.auth.settings import AuthSettings
640> from mcp.server.fastmcp.server import FastMCP
641>
642> from .config import config
643> from .token_verifier import IntrospectionTokenVerifier
644>
645> logger = logging.getLogger(__name__)
646>
647>
648> def create_oauth_urls() -> dict[str, str]:
649> """Create OAuth URLs based on configuration (Keycloak-style)."""
650> from urllib.parse import urljoin
651>
652> auth_base_url = config.auth_base_url
653>
654> return {
655> "issuer": auth_base_url,
656> "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
657> "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
658> "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
659> }
660>
661>
662> def create_server() -> FastMCP:
663> """Create and configure the FastMCP server."""
664>
665> config.validate()
666>
667> oauth_urls = create_oauth_urls()
668>
669> token_verifier = IntrospectionTokenVerifier(
670> introspection_endpoint=oauth_urls["introspection_endpoint"],
671> server_url=config.server_url,
672> client_id=config.OAUTH_CLIENT_ID,
673> client_secret=config.OAUTH_CLIENT_SECRET,
674> )
675>
676> app = FastMCP(
677> name="MCP Resource Server",
678> instructions="Resource Server that validates tokens via Authorization Server introspection",
679> host=config.HOST,
680> port=config.PORT,
681> debug=True,
682> streamable_http_path="/",
683> token_verifier=token_verifier,
684> auth=AuthSettings(
685> issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
686> required_scopes=[config.MCP_SCOPE],
687> resource_server_url=AnyHttpUrl(config.server_url),
688> ),
689> )
690>
691> @app.tool()
692> async def add_numbers(a: float, b: float) -> dict[str, Any]:
693> """
694> Add two numbers together.
695> This tool demonstrates basic arithmetic operations with OAuth authentication.
696>
697> Args:
698> a: The first number to add
699> b: The second number to add
700> """
701> result = a + b
702> return {
703> "operation": "addition",
704> "operand_a": a,
705> "operand_b": b,
706> "result": result,
707> "timestamp": datetime.datetime.now().isoformat()
708> }
709>
710> @app.tool()
711> async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
712> """
713> Multiply two numbers together.
714> This tool demonstrates basic arithmetic operations with OAuth authentication.
715>
716> Args:
717> x: The first number to multiply
718> y: The second number to multiply
719> """
720> result = x * y
721> return {
722> "operation": "multiplication",
723> "operand_x": x,
724> "operand_y": y,
725> "result": result,
726> "timestamp": datetime.datetime.now().isoformat()
727> }
728>
729> return app
730>
731>
732> def main() -> int:
733> """
734> Run the MCP Resource Server.
735>
736> This server:
737> - Provides RFC 9728 Protected Resource Metadata
738> - Validates tokens via Authorization Server introspection
739> - Serves MCP tools requiring authentication
740>
741> Configuration is loaded from config.py and environment variables.
742> """
743> logging.basicConfig(level=logging.INFO)
744>
745> try:
746> config.validate()
747> oauth_urls = create_oauth_urls()
748>
749> except ValueError as e:
750> logger.error("Configuration error: %s", e)
751> return 1
752>
753> try:
754> mcp_server = create_server()
755>
756> logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
757> logger.info("Authorization Server: %s", oauth_urls["issuer"])
758> logger.info("Transport: %s", config.TRANSPORT)
759>
760> mcp_server.run(transport=config.TRANSPORT)
761> return 0
762>
763> except Exception:
764> logger.exception("Server error")
765> return 1
766>
767>
768> if __name__ == "__main__":
769> exit(main())
770> ```
771>
772> Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts
773>
774> ```python theme={null}
775> """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776>
777> import logging
778> from typing import Any
779>
780> from mcp.server.auth.provider import AccessToken, TokenVerifier
781> from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782>
783> logger = logging.getLogger(__name__)
784>
785>
786> class IntrospectionTokenVerifier(TokenVerifier):
787> """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
788> """
789>
790> def __init__(
791> self,
792> introspection_endpoint: str,
793> server_url: str,
794> client_id: str,
795> client_secret: str,
796> ):
797> self.introspection_endpoint = introspection_endpoint
798> self.server_url = server_url
799> self.client_id = client_id
800> self.client_secret = client_secret
801> self.resource_url = resource_url_from_server_url(server_url)
802>
803> async def verify_token(self, token: str) -> AccessToken | None:
804> """Verify token via introspection endpoint."""
805> import httpx
806>
807> if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
808> return None
809>
810> timeout = httpx.Timeout(10.0, connect=5.0)
811> limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
812>
813> async with httpx.AsyncClient(
814> timeout=timeout,
815> limits=limits,
816> verify=True,
817> ) as client:
818> try:
819> form_data = {
820> "token": token,
821> "client_id": self.client_id,
822> "client_secret": self.client_secret,
823> }
824> headers = {"Content-Type": "application/x-www-form-urlencoded"}
825>
826> response = await client.post(
827> self.introspection_endpoint,
828> data=form_data,
829> headers=headers,
830> )
831>
832> if response.status_code != 200:
833> return None
834>
835> data = response.json()
836> if not data.get("active", False):
837> return None
838>
839> if not self._validate_resource(data):
840> return None
841>
842> return AccessToken(
843> token=token,
844> client_id=data.get("client_id", "unknown"),
845> scopes=data.get("scope", "").split() if data.get("scope") else [],
846> expires_at=data.get("exp"),
847> resource=data.get("aud"), # Include resource in token
848> )
849>
850> except Exception as e:
851> return None
852>
853> def _validate_resource(self, token_data: dict[str, Any]) -> bool:
854> """Validate token was issued for this resource server.
855>
856> Rules:
857> - Reject if 'aud' missing.
858> - Accept if any audience entry matches the derived resource URL.
859> - Supports string or list forms per JWT spec.
860> """
861> if not self.server_url or not self.resource_url:
862> return False
863>
864> aud: list[str] | str | None = token_data.get("aud")
865> if isinstance(aud, list):
866> return any(self._is_valid_resource(a) for a in aud)
867> if isinstance(aud, str):
868> return self._is_valid_resource(aud)
869> return False
870>
871> def _is_valid_resource(self, resource: str) -> bool:
872> """Check if the given resource matches our server."""
873> return check_resource_allowed(self.resource_url, resource)
874> ```
875>
876> For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878> [tab] C#
879>
880> You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881>
882> To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
883>
884> ```csharp theme={null}
885> using Microsoft.AspNetCore.Authentication.JwtBearer;
886> using Microsoft.IdentityModel.Tokens;
887> using ModelContextProtocol.AspNetCore.Authentication;
888> using ProtectedMcpServer.Tools;
889> using System.Security.Claims;
890>
891> var builder = WebApplication.CreateBuilder(args);
892>
893> var serverUrl = "http://localhost:3000/";
894> var authorizationServerUrl = "http://localhost:8080/realms/master/";
895>
896> builder.Services.AddAuthentication(options =>
897> {
898> options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
899> options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
900> })
901> .AddJwtBearer(options =>
902> {
903> options.Authority = authorizationServerUrl;
904> var normalizedServerAudience = serverUrl.TrimEnd('/');
905> options.TokenValidationParameters = new TokenValidationParameters
906> {
907> ValidIssuer = authorizationServerUrl,
908> ValidAudiences = new[] { normalizedServerAudience, serverUrl },
909> AudienceValidator = (audiences, securityToken, validationParameters) =>
910> {
911> if (audiences == null) return false;
912> foreach (var aud in audiences)
913> {
914> if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
915> {
916> return true;
917> }
918> }
919> return false;
920> }
921> };
922>
923> options.RequireHttpsMetadata = false; // Set to true in production
924>
925> options.Events = new JwtBearerEvents
926> {
927> OnTokenValidated = context =>
928> {
929> var name = context.Principal?.Identity?.Name ?? "unknown";
930> var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
931> Console.WriteLine($"Token validated for: {name} ({email})");
932> return Task.CompletedTask;
933> },
934> OnAuthenticationFailed = context =>
935> {
936> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937> return Task.CompletedTask;
938> },
939> };
940> })
941> .AddMcp(options =>
942> {
943> options.ResourceMetadata = new()
944> {
945> Resource = new Uri(serverUrl),
946> ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
947> AuthorizationServers = { new Uri(authorizationServerUrl) },
948> ScopesSupported = ["mcp:tools"]
949> };
950> });
951>
952> builder.Services.AddAuthorization();
953>
954> builder.Services.AddHttpContextAccessor();
955> builder.Services.AddMcpServer()
956> .WithTools<MathTools>()
957> .WithHttpTransport();
958>
959> var app = builder.Build();
960>
961> app.UseAuthentication();
962> app.UseAuthorization();
963>
964> app.MapMcp().RequireAuthorization();
965>
966> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969> Console.WriteLine("Exposed Math tools: Add, Multiply");
970> Console.WriteLine("Press Ctrl+C to stop the server");
971>
972> app.Run(serverUrl);
973> ```
974>
975> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
976976 
977977## Testing the MCP Server
978978 
979979 

docs/2025-03-26/develop/build-client Changed · +2464 / -2475 lines

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

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

docs/2025-03-26/develop/build-server Changed · +2784 / -2784 lines

### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather Service ### Create your Boot Application ### Running the server ## Testing your server with Claude for Desktop ## Testing your server with Java client ### Create an MCP Client manually ### Use MCP Client Boot Starter ## More Java MCP Server examples ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Setting up the instance ### Weather API helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather API helper functions ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up constants ### Helper methods ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing the Weather server and tools ### Implementing the ServerHandler ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop

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 24
2424 
2525This tutorial will primarily focus on tools.
2626 
27**Python**
28 
29Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30 
31### Prerequisite knowledge
32 
33This quickstart assumes you have familiarity with:
34 
35* Python
36* LLMs like Claude
37 
38### Logging in MCP Servers
39 
40When implementing MCP servers, be careful about how you handle logging:
41 
42**For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, but can be used safely with `file=sys.stderr`.
43 
44**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45 
46### Best Practices
47 
48* Use a logging library that writes to stderr or files.
49 
50### Quick Examples
51 
52```python theme={null}
53import sys
54import logging
55 
56# ❌ Bad (STDIO)
57print("Processing request")
58 
59# ✅ Good (STDIO)
60print("Processing request", file=sys.stderr)
61 
62# ✅ Good (STDIO)
63logging.info("Processing request")
64```
65 
66### System requirements
67 
68* Python 3.10 or higher installed.
69* You must use the Python MCP SDK 1.2.0 or higher.
70 
71### Set up your environment
72 
73First, let's install `uv` and set up our Python project and environment:
74 
75```bash macOS/Linux theme={null}
76curl -LsSf https://astral.sh/uv/install.sh | sh
77```
78 
79```powershell Windows theme={null}
80powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81```
82 
83Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84 
85Now, let's create and set up our project:
86 
87```bash macOS/Linux theme={null}
88# Create a new directory for our project
89uv init weather
90cd weather
91 
92# Create virtual environment and activate it
93uv venv
94source .venv/bin/activate
95 
96# Install dependencies
97uv add "mcp[cli]" httpx
98 
99# Create our server file
100touch weather.py
101```
102 
103```powershell Windows theme={null}
104# Create a new directory for our project
105uv init weather
106cd weather
107 
108# Create virtual environment and activate it
109uv venv
110.venv\Scripts\activate
111 
112# Install dependencies
113uv add mcp[cli] httpx
114 
115# Create our server file
116new-item weather.py
117```
118 
119Now let's dive into building your server.
120 
121## Building your server
122 
123### Importing packages and setting up the instance
124 
125Add these to the top of your `weather.py`:
126 
127```python theme={null}
128from typing import Any
129 
130import httpx
131from mcp.server.fastmcp import FastMCP
132 
133# Initialize FastMCP server
134mcp = FastMCP("weather")
135 
136# Constants
137NWS_API_BASE = "https://api.weather.gov"
138USER_AGENT = "weather-app/1.0"
139```
140 
141The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
142 
143### Helper functions
144 
145Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146 
147```python theme={null}
148async def make_nws_request(url: str) -> dict[str, Any] | None:
149 """Make a request to the NWS API with proper error handling."""
150 headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
151 async with httpx.AsyncClient() as client:
152 try:
153 response = await client.get(url, headers=headers, timeout=30.0)
154 response.raise_for_status()
155 return response.json()
156 except Exception:
157 return None
158 
159 
160def format_alert(feature: dict) -> str:
161 """Format an alert feature into a readable string."""
162 props = feature["properties"]
163 return f"""
164Event: {props.get("event", "Unknown")}
165Area: {props.get("areaDesc", "Unknown")}
166Severity: {props.get("severity", "Unknown")}
167Description: {props.get("description", "No description available")}
168Instructions: {props.get("instruction", "No specific instructions provided")}
169"""
170```
171 
172### Implementing tool execution
173 
174The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
175 
176```python theme={null}
177@mcp.tool()
178async def get_alerts(state: str) -> str:
179 """Get weather alerts for a US state.
180 
181 Args:
182 state: Two-letter US state code (e.g. CA, NY)
183 """
184 url = f"{NWS_API_BASE}/alerts/active/area/{state}"
185 data = await make_nws_request(url)
186 
187 if not data or "features" not in data:
188 return "Unable to fetch alerts or no alerts found."
189 
190 if not data["features"]:
191 return "No active alerts for this state."
192 
193 alerts = [format_alert(feature) for feature in data["features"]]
194 return "\n---\n".join(alerts)
195 
196 
197@mcp.tool()
198async def get_forecast(latitude: float, longitude: float) -> str:
199 """Get weather forecast for a location.
200 
201 Args:
202 latitude: Latitude of the location
203 longitude: Longitude of the location
204 """
205 # First get the forecast grid endpoint
206 points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
207 points_data = await make_nws_request(points_url)
208 
209 if not points_data:
210 return "Unable to fetch forecast data for this location."
211 
212 # Get the forecast URL from the points response
213 forecast_url = points_data["properties"]["forecast"]
214 forecast_data = await make_nws_request(forecast_url)
215 
216 if not forecast_data:
217 return "Unable to fetch detailed forecast."
218 
219 # Format the periods into a readable forecast
220 periods = forecast_data["properties"]["periods"]
221 forecasts = []
222 for period in periods[:5]: # Only show next 5 periods
223 forecast = f"""
224{period["name"]}:
225Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226Wind: {period["windSpeed"]} {period["windDirection"]}
227Forecast: {period["detailedForecast"]}
228"""
229 forecasts.append(forecast)
230 
231 return "\n---\n".join(forecasts)
232```
233 
234### Running the server
235 
236Finally, let's initialize and run the server:
237 
238```python theme={null}
239def main():
240 # Initialize and run the server
241 mcp.run(transport="stdio")
242 
243 
244if __name__ == "__main__":
245 main()
246```
247 
248Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249 
250Let's now test your server from an existing MCP host, Claude for Desktop.
251 
252## Testing your server with Claude for Desktop
253 
254> [!NOTE]
255>
256> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257 
258First, make sure you have Claude for Desktop installed. [You can install the latest version
259here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260 
261We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
262 
263For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264 
265```bash macOS/Linux theme={null}
266code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267```
268 
269```powershell Windows theme={null}
270code $env:AppData\Claude\claude_desktop_config.json
271```
272 
273You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
274 
275In this case, we'll add our single weather server like so:
276 
277```json macOS/Linux theme={null}
278{
279 "mcpServers": {
280 "weather": {
281 "command": "uv",
282 "args": [
283 "--directory",
284 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
285 "run",
286 "weather.py"
287 ]
288 }
289 }
290}
291```
292 
293```json Windows theme={null}
294{
295 "mcpServers": {
296 "weather": {
297 "command": "uv",
298 "args": [
299 "--directory",
300 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
301 "run",
302 "weather.py"
303 ]
304 }
305 }
306}
307```
308 
309> [!WARNING]
310>
311> You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
312 
313> [!NOTE]
314>
315> Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
316 
317This tells Claude for Desktop:
318 
3191. There's an MCP server named "weather"
3202. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321 
322Save the file, and restart **Claude for Desktop**.
323 
324**TypeScript**
325 
326Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
327 
328### Prerequisite knowledge
329 
330This quickstart assumes you have familiarity with:
331 
332* TypeScript
333* LLMs like Claude
334 
335### Logging in MCP Servers
336 
337When implementing MCP servers, be careful about how you handle logging:
338 
339**For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
340 
341**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
342 
343### Best Practices
344 
345* Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
346 
347### Quick Examples
348 
349```javascript theme={null}
350// ❌ Bad (STDIO)
351console.log("Server started");
352 
353// ✅ Good (STDIO)
354console.error("Server started"); // stderr is safe
355```
356 
357### System requirements
358 
359For TypeScript, make sure you have the latest version of Node installed.
360 
361### Set up your environment
362 
363First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364Verify your Node.js installation:
365 
366```bash theme={null}
367node --version
368npm --version
369```
370 
371For this tutorial, you'll need Node.js version 16 or higher.
372 
373Now, let's create and set up our project:
374 
375```bash macOS/Linux theme={null}
376# Create a new directory for our project
377mkdir weather
378cd weather
379 
380# Initialize a new npm project
381npm init -y
382 
383# Install dependencies
384npm install @modelcontextprotocol/sdk zod@3
385npm install -D @types/node typescript
386 
387# Create our files
388mkdir src
389touch src/index.ts
390```
391 
392```powershell Windows theme={null}
393# Create a new directory for our project
394md weather
395cd weather
396 
397# Initialize a new npm project
398npm init -y
399 
400# Install dependencies
401npm install @modelcontextprotocol/sdk zod@3
402npm install -D @types/node typescript
403 
404# Create our files
405md src
406new-item src\index.ts
407```
408 
409Update your package.json to add type: "module" and a build script:
410 
411```json package.json theme={null}
412{
413 "type": "module",
414 "bin": {
415 "weather": "./build/index.js"
416 },
417 "scripts": {
418 "build": "tsc && chmod 755 build/index.js"
419 },
420 "files": ["build"]
421}
422```
423 
424Create a `tsconfig.json` in the root of your project:
425 
426```json tsconfig.json theme={null}
427{
428 "compilerOptions": {
429 "target": "ES2022",
430 "module": "Node16",
431 "moduleResolution": "Node16",
432 "outDir": "./build",
433 "rootDir": "./src",
434 "strict": true,
435 "esModuleInterop": true,
436 "skipLibCheck": true,
437 "forceConsistentCasingInFileNames": true
438 },
439 "include": ["src/**/*"],
440 "exclude": ["node_modules"]
441}
442```
443 
444Now let's dive into building your server.
445 
446## Building your server
447 
448### Importing packages and setting up the instance
449 
450Add these to the top of your `src/index.ts`:
451 
452```typescript theme={null}
453import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455import { z } from "zod";
456 
457const NWS_API_BASE = "https://api.weather.gov";
458const USER_AGENT = "weather-app/1.0";
459 
460// Create server instance
461const server = new McpServer({
462 name: "weather",
463 version: "1.0.0",
464});
465```
466 
467### Helper functions
468 
469Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
470 
471```typescript theme={null}
472// Helper function for making NWS API requests
473async function makeNWSRequest<T>(url: string): Promise<T | null> {
474 const headers = {
475 "User-Agent": USER_AGENT,
476 Accept: "application/geo+json",
477 };
478 
479 try {
480 const response = await fetch(url, { headers });
481 if (!response.ok) {
482 throw new Error(`HTTP error! status: ${response.status}`);
483 }
484 return (await response.json()) as T;
485 } catch (error) {
486 console.error("Error making NWS request:", error);
487 return null;
488 }
489}
490 
491interface AlertFeature {
492 properties: {
493 event?: string;
494 areaDesc?: string;
495 severity?: string;
496 status?: string;
497 headline?: string;
498 };
499}
500 
501// Format alert data
502function formatAlert(feature: AlertFeature): string {
503 const props = feature.properties;
504 return [
505 `Event: ${props.event || "Unknown"}`,
506 `Area: ${props.areaDesc || "Unknown"}`,
507 `Severity: ${props.severity || "Unknown"}`,
508 `Status: ${props.status || "Unknown"}`,
509 `Headline: ${props.headline || "No headline"}`,
510 "---",
511 ].join("\n");
512}
513 
514interface ForecastPeriod {
515 name?: string;
516 temperature?: number;
517 temperatureUnit?: string;
518 windSpeed?: string;
519 windDirection?: string;
520 shortForecast?: string;
521}
522 
523interface AlertsResponse {
524 features: AlertFeature[];
525}
526 
527interface PointsResponse {
528 properties: {
529 forecast?: string;
530 };
531}
532 
533interface ForecastResponse {
534 properties: {
535 periods: ForecastPeriod[];
536 };
537}
538```
539 
540### Implementing tool execution
541 
542The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
543 
544```typescript theme={null}
545// Register weather tools
546 
547server.registerTool(
548 "get_alerts",
549 {
550 description: "Get weather alerts for a state",
551 inputSchema: {
552 state: z
553 .string()
554 .length(2)
555 .describe("Two-letter state code (e.g. CA, NY)"),
556 },
557 },
558 async ({ state }) => {
559 const stateCode = state.toUpperCase();
560 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
561 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
562 
563 if (!alertsData) {
564 return {
565 content: [
566 {
567 type: "text",
568 text: "Failed to retrieve alerts data",
569 },
570 ],
571 };
572 }
573 
574 const features = alertsData.features || [];
575 if (!features.length) {
576 return {
577 content: [
578 {
579 type: "text",
580 text: `No active alerts for ${stateCode}`,
581 },
582 ],
583 };
584 }
585 
586 const formattedAlerts = features.map(formatAlert);
587 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
588 
589 return {
590 content: [
591 {
592 type: "text",
593 text: alertsText,
594 },
595 ],
596 };
597 },
598);
599 
600server.registerTool(
601 "get_forecast",
602 {
603 description: "Get weather forecast for a location",
604 inputSchema: {
605 latitude: z
606 .number()
607 .min(-90)
608 .max(90)
609 .describe("Latitude of the location"),
610 longitude: z
611 .number()
612 .min(-180)
613 .max(180)
614 .describe("Longitude of the location"),
615 },
616 },
617 async ({ latitude, longitude }) => {
618 // Get grid point data
619 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
620 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
621 
622 if (!pointsData) {
623 return {
624 content: [
625 {
626 type: "text",
627 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
628 },
629 ],
630 };
631 }
632 
633 const forecastUrl = pointsData.properties?.forecast;
634 if (!forecastUrl) {
635 return {
636 content: [
637 {
638 type: "text",
639 text: "Failed to get forecast URL from grid point data",
640 },
641 ],
642 };
643 }
644 
645 // Get forecast data
646 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
647 if (!forecastData) {
648 return {
649 content: [
650 {
651 type: "text",
652 text: "Failed to retrieve forecast data",
653 },
654 ],
655 };
656 }
657 
658 const periods = forecastData.properties?.periods || [];
659 if (periods.length === 0) {
660 return {
661 content: [
662 {
663 type: "text",
664 text: "No forecast periods available",
665 },
666 ],
667 };
668 }
669 
670 // Format forecast periods
671 const formattedForecast = periods.map((period: ForecastPeriod) =>
672 [
673 `${period.name || "Unknown"}:`,
674 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
675 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
676 `${period.shortForecast || "No forecast available"}`,
677 "---",
678 ].join("\n"),
679 );
680 
681 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
682 
683 return {
684 content: [
685 {
686 type: "text",
687 text: forecastText,
688 },
689 ],
690 };
691 },
692);
693```
694 
695### Running the server
696 
697Finally, implement the main function to run the server:
698 
699```typescript theme={null}
700async function main() {
701 const transport = new StdioServerTransport();
702 await server.connect(transport);
703 console.error("Weather MCP Server running on stdio");
704}
705 
706main().catch((error) => {
707 console.error("Fatal error in main():", error);
708 process.exit(1);
709});
710```
711 
712Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713 
714Let's now test your server from an existing MCP host, Claude for Desktop.
715 
716## Testing your server with Claude for Desktop
717 
718> [!NOTE]
719>
720> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721 
722First, make sure you have Claude for Desktop installed. [You can install the latest version
723here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724 
725We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726 
727For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728 
729```bash macOS/Linux theme={null}
730code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731```
732 
733```powershell Windows theme={null}
734code $env:AppData\Claude\claude_desktop_config.json
735```
736 
737You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
738 
739In this case, we'll add our single weather server like so:
740 
741```json macOS/Linux theme={null}
742{
743 "mcpServers": {
744 "weather": {
745 "command": "node",
746 "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
747 }
748 }
749}
750```
751 
752```json Windows theme={null}
753{
754 "mcpServers": {
755 "weather": {
756 "command": "node",
757 "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
758 }
759 }
760}
761```
762 
763This tells Claude for Desktop:
764 
7651. There's an MCP server named "weather"
7662. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767 
768Save the file, and restart **Claude for Desktop**.
769 
770**Java**
771 
772> [!NOTE]
773>
774> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
775> To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
776 
777Let's get started with building our weather server!
778[You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
779 
780For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
781For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782 
783### Logging in MCP Servers
784 
785When implementing MCP servers, be careful about how you handle logging:
786 
787**For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
788 
789**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
790 
791### Best Practices
792 
793* Use a logging library that writes to stderr or files.
794* Ensure any configured logging library will not write to stdout.
795 
796### System requirements
797 
798* Java 17 or higher installed.
799* [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
800 
801### Set up your environment
802 
803Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804 
805You will need to add the following dependencies:
806 
807```xml Maven theme={null}
808<dependencies>
809 <dependency>
810 <groupId>org.springframework.ai</groupId>
811 <artifactId>spring-ai-starter-mcp-server</artifactId>
812 </dependency>
813 
814 <dependency>
815 <groupId>org.springframework</groupId>
816 <artifactId>spring-web</artifactId>
817 </dependency>
818</dependencies>
819```
820 
821```groovy Gradle theme={null}
822dependencies {
823 implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824 implementation platform("org.springframework:spring-web")
825}
826```
827 
828Then configure your application by setting the application properties:
829 
830```bash application.properties theme={null}
831spring.main.bannerMode=off
832logging.pattern.console=
833```
834 
835```yaml application.yml theme={null}
836logging:
837 pattern:
838 console:
839spring:
840 main:
841 banner-mode: off
842```
843 
844The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
845 
846Now let's dive into building your server.
847 
848## Building your server
849 
850### Weather Service
851 
852Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
853 
854```java theme={null}
855@Service
856public class WeatherService {
857 
858 private final RestClient restClient;
859 
860 public WeatherService() {
861 this.restClient = RestClient.builder()
862 .baseUrl("https://api.weather.gov")
863 .defaultHeader("Accept", "application/geo+json")
864 .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
865 .build();
866 }
867 
868 @Tool(description = "Get weather forecast for a specific latitude/longitude")
869 public String getWeatherForecastByLocation(
870 double latitude, // Latitude coordinate
871 double longitude // Longitude coordinate
872 ) {
873 // Returns detailed forecast including:
874 // - Temperature and unit
875 // - Wind speed and direction
876 // - Detailed forecast description
877 }
878 
879 @Tool(description = "Get weather alerts for a US state")
880 public String getAlerts(
881 @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
882 ) {
883 // Returns active alerts including:
884 // - Event type
885 // - Affected area
886 // - Severity
887 // - Description
888 // - Safety instructions
889 }
890 
891 // ......
892}
893```
894 
895The `@Service` annotation will auto-register the service in your application context.
896The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897 
898The auto-configuration will automatically register these tools with the MCP server.
899 
900### Create your Boot Application
901 
902```java theme={null}
903@SpringBootApplication
904public class McpServerApplication {
905 
906 public static void main(String[] args) {
907 SpringApplication.run(McpServerApplication.class, args);
908 }
909 
910 @Bean
911 public ToolCallbackProvider weatherTools(WeatherService weatherService) {
912 return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
913 }
914}
915```
916 
917Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918 
919### Running the server
920 
921Finally, let's build the server:
922 
923```bash theme={null}
924./mvnw clean install
925```
926 
927This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928 
929Let's now test your server from an existing MCP host, Claude for Desktop.
930 
931## Testing your server with Claude for Desktop
932 
933> [!NOTE]
934>
935> Claude for Desktop is not yet available on Linux.
936 
937First, make sure you have Claude for Desktop installed.
938[You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939 
940We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942Make sure to create the file if it doesn't exist.
943 
944For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945 
946```bash macOS/Linux theme={null}
947code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948```
949 
950```powershell Windows theme={null}
951code $env:AppData\Claude\claude_desktop_config.json
952```
953 
954You'll then add your servers in the `mcpServers` key.
955The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956 
957In this case, we'll add our single weather server like so:
958 
959```json macOS/Linux theme={null}
960{
961 "mcpServers": {
962 "spring-ai-mcp-weather": {
963 "command": "java",
964 "args": [
965 "-Dspring.ai.mcp.server.stdio=true",
966 "-jar",
967 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
968 ]
969 }
970 }
971}
972```
973 
974```json Windows theme={null}
975{
976 "mcpServers": {
977 "spring-ai-mcp-weather": {
978 "command": "java",
979 "args": [
980 "-Dspring.ai.mcp.server.transport=STDIO",
981 "-jar",
982 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
983 ]
984 }
985 }
986}
987```
988 
989> [!NOTE]
990>
991> Make sure you pass in the absolute path to your server.
992 
993This tells Claude for Desktop:
994 
9951. There's an MCP server named "my-weather-server"
9962. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997 
998Save the file, and restart **Claude for Desktop**.
999 
1000## Testing your server with Java client
1001 
1002### Create an MCP Client manually
1003 
1004Use the `McpClient` to connect to the server:
1005 
1006```java theme={null}
1007var stdioParams = ServerParameters.builder("java")
1008 .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009 .build();
1010 
1011var stdioTransport = new StdioClientTransport(stdioParams);
1012 
1013var mcpClient = McpClient.sync(stdioTransport).build();
1014 
1015mcpClient.initialize();
1016 
1017ListToolsResult toolsList = mcpClient.listTools();
1018 
1019CallToolResult weather = mcpClient.callTool(
1020 new CallToolRequest("getWeatherForecastByLocation",
1021 Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022 
1023CallToolResult alert = mcpClient.callTool(
1024 new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025 
1026mcpClient.closeGracefully();
1027```
1028 
1029### Use MCP Client Boot Starter
1030 
1031Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1032 
1033```xml theme={null}
1034<dependency>
1035 <groupId>org.springframework.ai</groupId>
1036 <artifactId>spring-ai-starter-mcp-client</artifactId>
1037</dependency>
1038```
1039 
1040and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041You can reuse the existing Anthropic Desktop configuration:
1042 
1043```properties theme={null}
1044spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045```
1046 
1047When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048 
1049For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1050 
1051## More Java MCP Server examples
1052 
1053The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an MCP server using SSE transport.
1054It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056**Kotlin**
1057 
1058Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1059 
1060### Prerequisite knowledge
1061 
1062This quickstart assumes you have familiarity with:
1063 
1064* Kotlin
1065* LLMs like Claude
1066 
1067### Logging in MCP Servers
1068 
1069When implementing MCP servers, be careful about how you handle logging:
1070 
1071**For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1072 
1073**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1074 
1075### Best Practices
1076 
1077* Use a logging library that writes to stderr or files.
1078 
1079### System requirements
1080 
1081* JDK 11 or higher installed.
1082 
1083### Set up your environment
1084 
1085First, let's install `java` and `gradle` if you haven't already.
1086You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087Verify your `java` installation:
1088 
1089```bash theme={null}
1090java --version
1091```
1092 
1093Now, let's create and set up your project:
1094 
1095```bash macOS/Linux theme={null}
1096# Create a new directory for our project
1097mkdir weather
1098cd weather
1099 
1100# Initialize a new kotlin project
1101gradle init
1102```
1103 
1104```powershell Windows theme={null}
1105# Create a new directory for our project
1106md weather
1107cd weather
1108 
1109# Initialize a new kotlin project
1110gradle init
1111```
1112 
1113After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114 
1115Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116 
1117After creating the project, replace the contents of your `build.gradle.kts` with:
1118 
1119```kotlin build.gradle.kts theme={null}
1120// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1121val mcpVersion = "0.9.0"
1122val ktorVersion = "3.2.3"
1123val slf4jVersion = "2.0.17"
1124 
1125plugins {
1126 kotlin("jvm") version "2.3.20"
1127 kotlin("plugin.serialization") version "2.3.20"
1128 id("com.gradleup.shadow") version "8.3.9"
1129 application
1130}
1131 
1132application {
1133 mainClass.set("MainKt")
1134}
1135 
1136dependencies {
1137 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1138 implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1139 implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1140 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1141 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1142}
1143```
1144 
1145Verify that everything is set up correctly:
1146 
1147```bash theme={null}
1148./gradlew build
1149```
1150 
1151Now let's dive into building your server.
1152 
1153## Building your server
1154 
1155### Setting up the instance
1156 
1157Add a server initialization function:
1158 
1159```kotlin theme={null}
1160fun runMcpServer() {
1161 val server = Server(
1162 Implementation(
1163 name = "weather",
1164 version = "1.0.0",
1165 ),
1166 ServerOptions(
1167 capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1168 ),
1169 )
1170 
1171 // register tools on server here
1172 
1173 val transport = StdioServerTransport(
1174 System.`in`.asInput(),
1175 System.out.asSink().buffered(),
1176 )
1177 
1178 runBlocking {
1179 val session = server.createSession(transport)
1180 val done = Job()
1181 session.onClose {
1182 done.complete()
1183 }
1184 done.join()
1185 }
1186}
1187```
1188 
1189### Weather API helper functions
1190 
1191Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192 
1193```kotlin theme={null}
1194val httpClient = HttpClient(CIO) {
1195 defaultRequest {
1196 url("https://api.weather.gov")
1197 headers {
1198 append("Accept", "application/geo+json")
1199 append("User-Agent", "WeatherApiClient/1.0")
1200 }
1201 contentType(ContentType.Application.Json)
1202 }
1203 install(ContentNegotiation) {
1204 json(Json { ignoreUnknownKeys = true })
1205 }
1206}
1207 
1208// Extension function to fetch weather alerts for a given state
1209suspend fun HttpClient.getAlerts(state: String): List<String> {
1210 val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211 return alerts.features.map { feature ->
1212 """
1213 Event: ${feature.properties.event}
1214 Area: ${feature.properties.areaDesc}
1215 Severity: ${feature.properties.severity}
1216 Status: ${feature.properties.status}
1217 Headline: ${feature.properties.headline}
1218 """.trimIndent()
1219 }
1220}
1221 
1222// Extension function to fetch forecast information for given latitude and longitude
1223suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1224 val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1225 val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1226 val forecast = this.get(forecastUrl).body<ForecastResponse>()
1227 return forecast.properties.periods.map { period ->
1228 """
1229 ${period.name}:
1230 Temperature: ${period.temperature}°${period.temperatureUnit}
1231 Wind: ${period.windSpeed} ${period.windDirection}
1232 ${period.shortForecast}
1233 """.trimIndent()
1234 }
1235}
1236 
1237@Serializable
1238data class PointsResponse(val properties: PointsProperties)
1239 
1240@Serializable
1241data class PointsProperties(val forecast: String? = null)
1242 
1243@Serializable
1244data class ForecastResponse(val properties: ForecastProperties)
1245 
1246@Serializable
1247data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248 
1249@Serializable
1250data class ForecastPeriod(
1251 val name: String? = null,
1252 val temperature: Int? = null,
1253 val temperatureUnit: String? = null,
1254 val windSpeed: String? = null,
1255 val windDirection: String? = null,
1256 val shortForecast: String? = null,
1257)
1258 
1259@Serializable
1260data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261 
1262@Serializable
1263data class AlertFeature(val properties: AlertProperties)
1264 
1265@Serializable
1266data class AlertProperties(
1267 val event: String? = null,
1268 val areaDesc: String? = null,
1269 val severity: String? = null,
1270 val status: String? = null,
1271 val headline: String? = null,
1272)
1273```
1274 
1275### Implementing tool execution
1276 
1277The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1278 
1279```kotlin theme={null}
1280// Register weather tools
1281 
1282server.addTool(
1283 name = "get_alerts",
1284 description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1285 inputSchema = ToolSchema(
1286 properties = buildJsonObject {
1287 putJsonObject("state") {
1288 put("type", "string")
1289 put("description", "Two-letter US state code (e.g. CA, NY)")
1290 }
1291 },
1292 required = listOf("state"),
1293 ),
1294) { request ->
1295 val state = request.arguments?.get("state")?.jsonPrimitive?.content
1296 ?: return@addTool CallToolResult(
1297 content = listOf(TextContent("The 'state' parameter is required.")),
1298 )
1299 
1300 val alerts = httpClient.getAlerts(state)
1301 CallToolResult(content = alerts.map { TextContent(it) })
1302}
1303 
1304server.addTool(
1305 name = "get_forecast",
1306 description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1307 inputSchema = ToolSchema(
1308 properties = buildJsonObject {
1309 putJsonObject("latitude") {
1310 put("type", "number")
1311 put("description", "Latitude of the location")
1312 }
1313 putJsonObject("longitude") {
1314 put("type", "number")
1315 put("description", "Longitude of the location")
1316 }
1317 },
1318 required = listOf("latitude", "longitude"),
1319 ),
1320) { request ->
1321 val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1322 val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1323 if (latitude == null || longitude == null) {
1324 return@addTool CallToolResult(
1325 content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1326 )
1327 }
1328 
1329 val forecast = httpClient.getForecast(latitude, longitude)
1330 CallToolResult(content = forecast.map { TextContent(it) })
1331}
1332```
1333 
1334### Running the server
1335 
1336Finally, implement the main function to run the server:
1337 
1338```kotlin theme={null}
1339fun main() = runMcpServer()
1340```
1341 
1342You can run the server directly during development:
1343 
1344```bash theme={null}
1345./gradlew run
1346```
1347 
1348For production use, build the shadow JAR:
1349 
1350```bash theme={null}
1351./gradlew build
1352java -jar build/libs/weather-0.1.0-all.jar
1353```
1354 
1355Let's now test your server from an existing MCP host, Claude for Desktop.
1356 
1357## Testing your server with Claude for Desktop
1358 
1359> [!NOTE]
1360>
1361> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362 
1363First, make sure you have Claude for Desktop installed. [You can install the latest version
1364here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365 
1366We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368Make sure to create the file if it doesn't exist.
1369 
1370For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371 
1372```bash macOS/Linux theme={null}
1373code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374```
1375 
1376```powershell Windows theme={null}
1377code $env:AppData\Claude\claude_desktop_config.json
1378```
1379 
1380You'll then add your servers in the `mcpServers` key.
1381The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382 
1383In this case, we'll add our single weather server like so:
1384 
1385```json macOS/Linux theme={null}
1386{
1387 "mcpServers": {
1388 "weather": {
1389 "command": "java",
1390 "args": [
1391 "-jar",
1392 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1393 ]
1394 }
1395 }
1396}
1397```
1398 
1399```json Windows theme={null}
1400{
1401 "mcpServers": {
1402 "weather": {
1403 "command": "java",
1404 "args": [
1405 "-jar",
1406 "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1407 ]
1408 }
1409 }
1410}
1411```
1412 
1413This tells Claude for Desktop:
1414 
14151. There's an MCP server named "weather"
14162. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417 
1418Save the file, and restart **Claude for Desktop**.
1419 
1420**C#**
1421 
1422Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1423 
1424### Prerequisite knowledge
1425 
1426This quickstart assumes you have familiarity with:
1427 
1428* C#
1429* LLMs like Claude
1430* .NET 8 or higher
1431 
1432### Logging in MCP Servers
1433 
1434When implementing MCP servers, be careful about how you handle logging:
1435 
1436**For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1437 
1438**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1439 
1440### Best Practices
1441 
1442* Use a logging library that writes to stderr or files.
1443 
1444### System requirements
1445 
1446* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1447 
1448### Set up your environment
1449 
1450First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1451 
1452```bash theme={null}
1453dotnet --version
1454```
1455 
1456Now, let's create and set up your project:
1457 
1458```bash macOS/Linux theme={null}
1459# Create a new directory for our project
1460mkdir weather
1461cd weather
1462# Initialize a new C# project
1463dotnet new console
1464```
1465 
1466```powershell Windows theme={null}
1467# Create a new directory for our project
1468mkdir weather
1469cd weather
1470# Initialize a new C# project
1471dotnet new console
1472```
1473 
1474After running `dotnet new console`, you will be presented with a new C# project.
1475You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1477After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1478 
1479```bash theme={null}
1480# Add the Model Context Protocol SDK NuGet package
1481dotnet add package ModelContextProtocol --prerelease
1482# Add the .NET Hosting NuGet package
1483dotnet add package Microsoft.Extensions.Hosting
1484```
1485 
1486Now let’s dive into building your server.
1487 
1488## Building your server
1489 
1490Open the `Program.cs` file in your project and replace its contents with the following code:
1491 
1492```csharp theme={null}
1493using Microsoft.Extensions.DependencyInjection;
1494using Microsoft.Extensions.Hosting;
1495using ModelContextProtocol;
1496using System.Net.Http.Headers;
1497 
1498var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499 
1500builder.Services.AddMcpServer()
1501 .WithStdioServerTransport()
1502 .WithToolsFromAssembly();
1503 
1504builder.Services.AddSingleton(_ =>
1505{
1506 var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1507 client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1508 return client;
1509});
1510 
1511var app = builder.Build();
1512 
1513await app.RunAsync();
1514```
1515 
1516> [!NOTE]
1517>
1518> When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1519 
1520This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1521 
1522### Weather API helper functions
1523 
1524Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525 
1526```csharp theme={null}
1527using System.Text.Json;
1528 
1529internal static class HttpClientExt
1530{
1531 public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1532 {
1533 using var response = await client.GetAsync(requestUri);
1534 response.EnsureSuccessStatusCode();
1535 return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1536 }
1537}
1538```
1539 
1540Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541 
1542```csharp theme={null}
1543using ModelContextProtocol.Server;
1544using System.ComponentModel;
1545using System.Globalization;
1546using System.Text.Json;
1547 
1548namespace QuickstartWeatherServer.Tools;
1549 
1550[McpServerToolType]
1551public static class WeatherTools
1552{
1553 [McpServerTool, Description("Get weather alerts for a US state code.")]
1554 public static async Task<string> GetAlerts(
1555 HttpClient client,
1556 [Description("The US state code to get alerts for.")] string state)
1557 {
1558 using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1559 var jsonElement = jsonDocument.RootElement;
1560 var alerts = jsonElement.GetProperty("features").EnumerateArray();
1561 
1562 if (!alerts.Any())
1563 {
1564 return "No active alerts for this state.";
1565 }
1566 
1567 return string.Join("\n--\n", alerts.Select(alert =>
1568 {
1569 JsonElement properties = alert.GetProperty("properties");
1570 return $"""
1571 Event: {properties.GetProperty("event").GetString()}
1572 Area: {properties.GetProperty("areaDesc").GetString()}
1573 Severity: {properties.GetProperty("severity").GetString()}
1574 Description: {properties.GetProperty("description").GetString()}
1575 Instruction: {properties.GetProperty("instruction").GetString()}
1576 """;
1577 }));
1578 }
1579 
1580 [McpServerTool, Description("Get weather forecast for a location.")]
1581 public static async Task<string> GetForecast(
1582 HttpClient client,
1583 [Description("Latitude of the location.")] double latitude,
1584 [Description("Longitude of the location.")] double longitude)
1585 {
1586 var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1587 using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1588 var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1589 ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1590 
1591 using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1592 var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1593 
1594 return string.Join("\n---\n", periods.Select(period => $"""
1595 {period.GetProperty("name").GetString()}
1596 Temperature: {period.GetProperty("temperature").GetInt32()}°F
1597 Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1598 Forecast: {period.GetProperty("detailedForecast").GetString()}
1599 """));
1600 }
1601}
1602```
1603 
1604### Running the server
1605 
1606Finally, run the server using the following command:
1607 
1608```bash theme={null}
1609dotnet run
1610```
1611 
1612This will start the server and listen for incoming requests on standard input/output.
1613 
1614## Testing your server with Claude for Desktop
1615 
1616> [!NOTE]
1617>
1618> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619 
1620First, make sure you have Claude for Desktop installed. [You can install the latest version
1621here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1623For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624 
1625```bash macOS/Linux theme={null}
1626code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627```
1628 
1629```powershell Windows theme={null}
1630code $env:AppData\Claude\claude_desktop_config.json
1631```
1632 
1633You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1634In this case, we'll add our single weather server like so:
1635 
1636```json macOS/Linux theme={null}
1637{
1638 "mcpServers": {
1639 "weather": {
1640 "command": "dotnet",
1641 "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1642 }
1643 }
1644}
1645```
1646 
1647```json Windows theme={null}
1648{
1649 "mcpServers": {
1650 "weather": {
1651 "command": "dotnet",
1652 "args": [
1653 "run",
1654 "--project",
1655 "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1656 "--no-build"
1657 ]
1658 }
1659 }
1660}
1661```
1662 
1663This tells Claude for Desktop:
1664 
16651. There's an MCP server named "weather"
16662. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667 Save the file, and restart **Claude for Desktop**.
1668 
1669**Ruby**
1670 
1671Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1672 
1673### Prerequisite knowledge
1674 
1675This quickstart assumes you have familiarity with:
1676 
1677* Ruby
1678* LLMs like Claude
1679 
1680### Logging in MCP Servers
1681 
1682When implementing MCP servers, be careful about how you handle logging:
1683 
1684**For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1685 
1686**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1687 
1688### Best Practices
1689 
1690* Use a logging library that writes to stderr or files.
1691 
1692### Quick Examples
1693 
1694```ruby theme={null}
1695# ❌ Bad (STDIO)
1696puts "Processing request"
1697 
1698# ✅ Good (STDIO)
1699require "logger"
1700logger = Logger.new($stderr)
1701logger.info("Processing request")
1702```
1703 
1704### System requirements
1705 
1706* Ruby 2.7 or higher installed.
1707 
1708### Set up your environment
1709 
1710First, let's make sure you have Ruby installed. You can check by running:
1711 
1712```bash theme={null}
1713ruby --version
1714```
1715 
1716Now, let's create and set up our project:
1717 
1718```bash macOS/Linux theme={null}
1719# Create a new directory for our project
1720mkdir weather
1721cd weather
1722 
1723# Create a Gemfile
1724bundle init
1725 
1726# Add the MCP SDK dependency
1727bundle add mcp
1728 
1729# Create our server file
1730touch weather.rb
1731```
1732 
1733```powershell Windows theme={null}
1734# Create a new directory for our project
1735mkdir weather
1736cd weather
1737 
1738# Create a Gemfile
1739bundle init
1740 
1741# Add the MCP SDK dependency
1742bundle add mcp
1743 
1744# Create our server file
1745new-item weather.rb
1746```
1747 
1748Now let's dive into building your server.
1749 
1750## Building your server
1751 
1752### Importing packages and setting up constants
1753 
1754Open `weather.rb` and add these requires and constants at the top:
1755 
1756```ruby theme={null}
1757require "json"
1758require "mcp"
1759require "net/http"
1760require "uri"
1761 
1762NWS_API_BASE = "https://api.weather.gov"
1763USER_AGENT = "weather-app/1.0"
1764```
1765 
1766The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767 
1768### Helper methods
1769 
1770Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771 
1772```ruby theme={null}
1773module HelperMethods
1774 def make_nws_request(url)
1775 uri = URI(url)
1776 request = Net::HTTP::Get.new(uri)
1777 request["User-Agent"] = USER_AGENT
1778 request["Accept"] = "application/geo+json"
1779 
1780 response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1781 http.request(request)
1782 end
1783 
1784 raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1785 
1786 JSON.parse(response.body)
1787 end
1788 
1789 def format_alert(feature)
1790 properties = feature["properties"]
1791 
1792 <<~ALERT
1793 Event: #{properties["event"] || "Unknown"}
1794 Area: #{properties["areaDesc"] || "Unknown"}
1795 Severity: #{properties["severity"] || "Unknown"}
1796 Description: #{properties["description"] || "No description available"}
1797 Instructions: #{properties["instruction"] || "No specific instructions provided"}
1798 ALERT
1799 end
1800end
1801```
1802 
1803### Implementing tool execution
1804 
1805Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806 
1807```ruby theme={null}
1808class GetAlerts < MCP::Tool
1809 extend HelperMethods
1810 
1811 tool_name "get_alerts"
1812 description "Get weather alerts for a US state"
1813 input_schema(
1814 properties: {
1815 state: {
1816 type: "string",
1817 description: "Two-letter US state code (e.g. CA, NY)"
1818 }
1819 },
1820 required: ["state"]
1821 )
1822 
1823 def self.call(state:)
1824 url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1825 data = make_nws_request(url)
1826 
1827 if data["features"].empty?
1828 return MCP::Tool::Response.new([{
1829 type: "text",
1830 text: "No active alerts for this state."
1831 }])
1832 end
1833 
1834 alerts = data["features"].map { |feature| format_alert(feature) }
1835 MCP::Tool::Response.new([{
1836 type: "text",
1837 text: alerts.join("\n---\n")
1838 }])
1839 end
1840end
1841 
1842class GetForecast < MCP::Tool
1843 extend HelperMethods
1844 
1845 tool_name "get_forecast"
1846 description "Get weather forecast for a location"
1847 input_schema(
1848 properties: {
1849 latitude: {
1850 type: "number",
1851 description: "Latitude of the location"
1852 },
1853 longitude: {
1854 type: "number",
1855 description: "Longitude of the location"
1856 }
1857 },
1858 required: ["latitude", "longitude"]
1859 )
1860 
1861 def self.call(latitude:, longitude:)
1862 # First get the forecast grid endpoint.
1863 points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1864 points_data = make_nws_request(points_url)
1865 
1866 # Get the forecast URL from the points response.
1867 forecast_url = points_data["properties"]["forecast"]
1868 forecast_data = make_nws_request(forecast_url)
1869 
1870 # Format the periods into a readable forecast.
1871 periods = forecast_data["properties"]["periods"]
1872 forecasts = periods.first(5).map do |period|
1873 <<~FORECAST
1874 #{period["name"]}:
1875 Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1876 Wind: #{period["windSpeed"]} #{period["windDirection"]}
1877 Forecast: #{period["detailedForecast"]}
1878 FORECAST
1879 end
1880 
1881 MCP::Tool::Response.new([{
1882 type: "text",
1883 text: forecasts.join("\n---\n")
1884 }])
1885 end
1886end
1887```
1888 
1889### Running the server
1890 
1891Finally, initialize and run the server:
1892 
1893```ruby theme={null}
1894server = MCP::Server.new(
1895 name: "weather",
1896 version: "1.0.0",
1897 tools: [GetAlerts, GetForecast]
1898)
1899 
1900transport = MCP::Server::Transports::StdioTransport.new(server)
1901transport.open
1902```
1903 
1904Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905 
1906Let's now test your server from an existing MCP host, Claude for Desktop.
1907 
1908## Testing your server with Claude for Desktop
1909 
1910> [!NOTE]
1911>
1912> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913 
1914First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1915 
1916We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1917 
1918For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919 
1920```bash macOS/Linux theme={null}
1921code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922```
1923 
1924```powershell Windows theme={null}
1925code $env:AppData\Claude\claude_desktop_config.json
1926```
1927 
1928You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1929 
1930In this case, we'll add our single weather server like so:
1931 
1932```json macOS/Linux theme={null}
1933{
1934 "mcpServers": {
1935 "weather": {
1936 "command": "bundle",
1937 "args": ["exec", "ruby", "weather.rb"],
1938 "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1939 }
1940 }
1941}
1942```
1943 
1944```json Windows theme={null}
1945{
1946 "mcpServers": {
1947 "weather": {
1948 "command": "bundle",
1949 "args": ["exec", "ruby", "weather.rb"],
1950 "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1951 }
1952 }
1953}
1954```
1955 
1956> [!NOTE]
1957>
1958> Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1959 
1960This tells Claude for Desktop:
1961 
19621. There's an MCP server named "weather"
19632. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964 
1965Save the file, and restart **Claude for Desktop**.
1966 
1967**Rust**
1968 
1969Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1970 
1971### Prerequisite knowledge
1972 
1973This quickstart assumes you have familiarity with:
1974 
1975* Rust programming language
1976* Async/await in Rust
1977* LLMs like Claude
1978 
1979### Logging in MCP Servers
1980 
1981When implementing MCP servers, be careful about how you handle logging:
1982 
1983**For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1984 
1985**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1986 
1987### Best Practices
1988 
1989* Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1990* Configure your logging framework to avoid stdout output.
1991 
1992### Quick Examples
1993 
1994```rust theme={null}
1995// ❌ Bad (STDIO)
1996println!("Processing request");
1997 
1998// ✅ Good (STDIO)
1999eprintln!("Processing request"); // writes to stderr
2000```
2001 
2002### System requirements
2003 
2004* Rust 1.70 or higher installed.
2005* Cargo (comes with Rust installation).
2006 
2007### Set up your environment
2008 
2009First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2010 
2011```bash macOS/Linux theme={null}
2012curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2013```
2014 
2015```powershell Windows theme={null}
2016# Download and run rustup-init.exe from https://rustup.rs/
2017```
2018 
2019Verify your Rust installation:
2020 
2021```bash theme={null}
2022rustc --version
2023cargo --version
2024```
2025 
2026Now, let's create and set up our project:
2027 
2028```bash macOS/Linux theme={null}
2029# Create a new Rust project
2030cargo new weather
2031cd weather
2032```
2033 
2034```powershell Windows theme={null}
2035# Create a new Rust project
2036cargo new weather
2037cd weather
2038```
2039 
2040Update your `Cargo.toml` to add the required dependencies:
2041 
2042```toml Cargo.toml theme={null}
2043[package]
2044name = "weather"
2045version = "0.1.0"
2046edition = "2024"
2047 
2048[dependencies]
2049rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050tokio = { version = "1.46", features = ["full"] }
2051reqwest = { version = "0.12", features = ["json"] }
2052serde = { version = "1.0", features = ["derive"] }
2053serde_json = "1.0"
2054anyhow = "1.0"
2055tracing = "0.1"
2056tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057```
2058 
2059Now let's dive into building your server.
2060 
2061## Building your server
2062 
2063### Importing packages and constants
2064 
2065Open `src/main.rs` and add these imports and constants at the top:
2066 
2067```rust theme={null}
2068use anyhow::Result;
2069use rmcp::{
2070 ServerHandler, ServiceExt,
2071 handler::server::{router::tool::ToolRouter, tool::Parameters},
2072 model::*,
2073 schemars, tool, tool_handler, tool_router,
2074};
2075use serde::Deserialize;
2076use serde::de::DeserializeOwned;
2077 
2078const NWS_API_BASE: &str = "https://api.weather.gov";
2079const USER_AGENT: &str = "weather-app/1.0";
2080```
2081 
2082The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2083 
2084### Data structures
2085 
2086Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087 
2088```rust theme={null}
2089#[derive(Debug, Deserialize)]
2090struct AlertsResponse {
2091 features: Vec<AlertFeature>,
2092}
2093 
2094#[derive(Debug, Deserialize)]
2095struct AlertFeature {
2096 properties: AlertProperties,
2097}
2098 
2099#[derive(Debug, Deserialize)]
2100struct AlertProperties {
2101 event: Option<String>,
2102 #[serde(rename = "areaDesc")]
2103 area_desc: Option<String>,
2104 severity: Option<String>,
2105 description: Option<String>,
2106 instruction: Option<String>,
2107}
2108 
2109#[derive(Debug, Deserialize)]
2110struct PointsResponse {
2111 properties: PointsProperties,
2112}
2113 
2114#[derive(Debug, Deserialize)]
2115struct PointsProperties {
2116 forecast: String,
2117}
2118 
2119#[derive(Debug, Deserialize)]
2120struct ForecastResponse {
2121 properties: ForecastProperties,
2122}
2123 
2124#[derive(Debug, Deserialize)]
2125struct ForecastProperties {
2126 periods: Vec<ForecastPeriod>,
2127}
2128 
2129#[derive(Debug, Deserialize)]
2130struct ForecastPeriod {
2131 name: String,
2132 temperature: i32,
2133 #[serde(rename = "temperatureUnit")]
2134 temperature_unit: String,
2135 #[serde(rename = "windSpeed")]
2136 wind_speed: String,
2137 #[serde(rename = "windDirection")]
2138 wind_direction: String,
2139 #[serde(rename = "detailedForecast")]
2140 detailed_forecast: String,
2141}
2142```
2143 
2144Now define the request types that MCP clients will send:
2145 
2146```rust theme={null}
2147#[derive(serde::Deserialize, schemars::JsonSchema)]
2148pub struct MCPForecastRequest {
2149 latitude: f32,
2150 longitude: f32,
2151}
2152 
2153#[derive(serde::Deserialize, schemars::JsonSchema)]
2154pub struct MCPAlertRequest {
2155 state: String,
2156}
2157```
2158 
2159### Helper functions
2160 
2161Add helper functions for making API requests and formatting responses:
2162 
2163```rust theme={null}
2164async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2165 let client = reqwest::Client::new();
2166 let rsp = client
2167 .get(url)
2168 .header(reqwest::header::USER_AGENT, USER_AGENT)
2169 .header(reqwest::header::ACCEPT, "application/geo+json")
2170 .send()
2171 .await?
2172 .error_for_status()?;
2173 Ok(rsp.json::<T>().await?)
2174}
2175 
2176fn format_alert(feature: &AlertFeature) -> String {
2177 let props = &feature.properties;
2178 format!(
2179 "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2180 props.event.as_deref().unwrap_or("Unknown"),
2181 props.area_desc.as_deref().unwrap_or("Unknown"),
2182 props.severity.as_deref().unwrap_or("Unknown"),
2183 props
2184 .description
2185 .as_deref()
2186 .unwrap_or("No description available"),
2187 props
2188 .instruction
2189 .as_deref()
2190 .unwrap_or("No specific instructions provided")
2191 )
2192}
2193 
2194fn format_period(period: &ForecastPeriod) -> String {
2195 format!(
2196 "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2197 period.name,
2198 period.temperature,
2199 period.temperature_unit,
2200 period.wind_speed,
2201 period.wind_direction,
2202 period.detailed_forecast
2203 )
2204}
2205```
2206 
2207### Implementing the Weather server and tools
2208 
2209Now let's implement the main Weather server struct with the tool handlers:
2210 
2211```rust theme={null}
2212pub struct Weather {
2213 tool_router: ToolRouter<Weather>,
2214}
2215 
2216#[tool_router]
2217impl Weather {
2218 fn new() -> Self {
2219 Self {
2220 tool_router: Self::tool_router(),
2221 }
2222 }
2223 
2224 #[tool(description = "Get weather alerts for a US state.")]
2225 async fn get_alerts(
2226 &self,
2227 Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2228 ) -> String {
2229 let url = format!(
2230 "{}/alerts/active/area/{}",
2231 NWS_API_BASE,
2232 state.to_uppercase()
2233 );
2234 
2235 match make_nws_request::<AlertsResponse>(&url).await {
2236 Ok(data) => {
2237 if data.features.is_empty() {
2238 "No active alerts for this state.".to_string()
2239 } else {
2240 data.features
2241 .iter()
2242 .map(format_alert)
2243 .collect::<Vec<_>>()
2244 .join("\n---\n")
2245 }
2246 }
2247 Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2248 }
2249 }
2250 
2251 #[tool(description = "Get weather forecast for a location.")]
2252 async fn get_forecast(
2253 &self,
2254 Parameters(MCPForecastRequest {
2255 latitude,
2256 longitude,
2257 }): Parameters<MCPForecastRequest>,
2258 ) -> String {
2259 let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2260 let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2261 return "Unable to fetch forecast data for this location.".to_string();
2262 };
2263 
2264 let forecast_url = points_data.properties.forecast;
2265 
2266 let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2267 return "Unable to fetch forecast data for this location.".to_string();
2268 };
2269 
2270 let periods = &forecast_data.properties.periods;
2271 let forecast_summary: String = periods
2272 .iter()
2273 .take(5) // Next 5 periods only
2274 .map(format_period)
2275 .collect::<Vec<String>>()
2276 .join("\n---\n");
2277 forecast_summary
2278 }
2279}
2280```
2281 
2282The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283 
2284### Implementing the ServerHandler
2285 
2286Implement the `ServerHandler` trait to define server capabilities:
2287 
2288```rust theme={null}
2289#[tool_handler]
2290impl ServerHandler for Weather {
2291 fn get_info(&self) -> ServerInfo {
2292 ServerInfo {
2293 capabilities: ServerCapabilities::builder().enable_tools().build(),
2294 ..Default::default()
2295 }
2296 }
2297}
2298```
2299 
2300### Running the server
2301 
2302Finally, implement the main function to run the server with stdio transport:
2303 
2304```rust theme={null}
2305#[tokio::main]
2306async fn main() -> Result<()> {
2307 let transport = (tokio::io::stdin(), tokio::io::stdout());
2308 let service = Weather::new().serve(transport).await?;
2309 service.waiting().await?;
2310 Ok(())
2311}
2312```
2313 
2314Build your server with:
2315 
2316```bash theme={null}
2317cargo build --release
2318```
2319 
2320The compiled binary will be in `target/release/weather`.
2321 
2322Let's now test your server from an existing MCP host, Claude for Desktop.
2323 
2324## Testing your server with Claude for Desktop
2325 
2326> [!NOTE]
2327>
2328> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329 
2330First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2331 
2332We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2333 
2334For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335 
2336```bash macOS/Linux theme={null}
2337code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338```
2339 
2340```powershell Windows theme={null}
2341code $env:AppData\Claude\claude_desktop_config.json
2342```
2343 
2344You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2345 
2346In this case, we'll add our single weather server like so:
2347 
2348```json macOS/Linux theme={null}
2349{
2350 "mcpServers": {
2351 "weather": {
2352 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2353 }
2354 }
2355}
2356```
2357 
2358```json Windows theme={null}
2359{
2360 "mcpServers": {
2361 "weather": {
2362 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2363 }
2364 }
2365}
2366```
2367 
2368> [!NOTE]
2369>
2370> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2371 
2372This tells Claude for Desktop:
2373 
23741. There's an MCP server named "weather"
23752. Launch it by running the compiled binary at the specified path
2376 
2377Save the file, and restart **Claude for Desktop**.
2378 
2379**Go**
2380 
2381Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2382 
2383### Prerequisite knowledge
2384 
2385This quickstart assumes you have familiarity with:
2386 
2387* Go
2388* LLMs like Claude
2389 
2390### Logging in MCP Servers
2391 
2392When implementing MCP servers, be careful about how you handle logging:
2393 
2394**For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2395 
2396**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2397 
2398### Best Practices
2399 
2400* Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2401* Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2402 
2403### Quick Examples
2404 
2405```go theme={null}
2406// ❌ Bad (STDIO)
2407fmt.Println("Processing request")
2408 
2409// ✅ Good (STDIO)
2410log.Println("Processing request") // defaults to stderr
2411 
2412// ✅ Good (STDIO)
2413fmt.Fprintln(os.Stderr, "Processing request")
2414```
2415 
2416### System requirements
2417 
2418* Go 1.24 or higher installed.
2419 
2420### Set up your environment
2421 
2422First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423 
2424Verify your Go installation:
2425 
2426```bash theme={null}
2427go version
2428```
2429 
2430Now, let's create and set up our project:
2431 
2432```bash macOS/Linux theme={null}
2433# Create a new directory for our project
2434mkdir weather
2435cd weather
2436 
2437# Initialize Go module
2438go mod init weather
2439 
2440# Install dependencies
2441go get github.com/modelcontextprotocol/go-sdk/mcp
2442 
2443# Create our server file
2444touch main.go
2445```
2446 
2447```powershell Windows theme={null}
2448# Create a new directory for our project
2449md weather
2450cd weather
2451 
2452# Initialize Go module
2453go mod init weather
2454 
2455# Install dependencies
2456go get github.com/modelcontextprotocol/go-sdk/mcp
2457 
2458# Create our server file
2459new-item main.go
2460```
2461 
2462Now let's dive into building your server.
2463 
2464## Building your server
2465 
2466### Importing packages and constants
2467 
2468Add these to the top of your `main.go`:
2469 
2470```go theme={null}
2471package main
2472 
2473import (
2474 "cmp"
2475 "context"
2476 "encoding/json"
2477 "fmt"
2478 "io"
2479 "log"
2480 "net/http"
2481 "strings"
2482 
2483 "github.com/modelcontextprotocol/go-sdk/mcp"
2484)
2485 
2486const (
2487 NWSAPIBase = "https://api.weather.gov"
2488 UserAgent = "weather-app/1.0"
2489)
2490```
2491 
2492### Data structures
2493 
2494Next, let's define the data structures used by our tools:
2495 
2496```go theme={null}
2497type PointsResponse struct {
2498 Properties struct {
2499 Forecast string `json:"forecast"`
2500 } `json:"properties"`
2501}
2502 
2503type ForecastResponse struct {
2504 Properties struct {
2505 Periods []ForecastPeriod `json:"periods"`
2506 } `json:"properties"`
2507}
2508 
2509type ForecastPeriod struct {
2510 Name string `json:"name"`
2511 Temperature int `json:"temperature"`
2512 TemperatureUnit string `json:"temperatureUnit"`
2513 WindSpeed string `json:"windSpeed"`
2514 WindDirection string `json:"windDirection"`
2515 DetailedForecast string `json:"detailedForecast"`
2516}
2517 
2518type AlertsResponse struct {
2519 Features []AlertFeature `json:"features"`
2520}
2521 
2522type AlertFeature struct {
2523 Properties AlertProperties `json:"properties"`
2524}
2525 
2526type AlertProperties struct {
2527 Event string `json:"event"`
2528 AreaDesc string `json:"areaDesc"`
2529 Severity string `json:"severity"`
2530 Description string `json:"description"`
2531 Instruction string `json:"instruction"`
2532}
2533 
2534type ForecastInput struct {
2535 Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536 Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537}
2538 
2539type AlertsInput struct {
2540 State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541}
2542```
2543 
2544### Helper functions
2545 
2546Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547 
2548```go theme={null}
2549func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2550 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2551 if err != nil {
2552 return nil, fmt.Errorf("failed to create request: %w", err)
2553 }
2554 
2555 req.Header.Set("User-Agent", UserAgent)
2556 req.Header.Set("Accept", "application/geo+json")
2557 
2558 client := http.DefaultClient
2559 resp, err := client.Do(req)
2560 if err != nil {
2561 return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2562 }
2563 defer resp.Body.Close()
2564 
2565 if resp.StatusCode != http.StatusOK {
2566 body, _ := io.ReadAll(resp.Body)
2567 return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2568 }
2569 
2570 var result T
2571 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2572 return nil, fmt.Errorf("failed to decode response: %w", err)
2573 }
2574 
2575 return &result, nil
2576}
2577 
2578func formatAlert(alert AlertFeature) string {
2579 props := alert.Properties
2580 event := cmp.Or(props.Event, "Unknown")
2581 areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2582 severity := cmp.Or(props.Severity, "Unknown")
2583 description := cmp.Or(props.Description, "No description available")
2584 instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2585 
2586 return fmt.Sprintf(`
2587Event: %s
2588Area: %s
2589Severity: %s
2590Description: %s
2591Instructions: %s
2592`, event, areaDesc, severity, description, instruction)
2593}
2594 
2595func formatPeriod(period ForecastPeriod) string {
2596 return fmt.Sprintf(`
2597%s:
2598Temperature: %d°%s
2599Wind: %s %s
2600Forecast: %s
2601`, period.Name, period.Temperature, period.TemperatureUnit,
2602 period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603}
2604```
2605 
2606### Implementing tool execution
2607 
2608The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609 
2610```go theme={null}
2611func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2612 *mcp.CallToolResult, any, error,
2613) {
2614 // Get points data
2615 pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2616 pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2617 if err != nil {
2618 return &mcp.CallToolResult{
2619 Content: []mcp.Content{
2620 &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2621 },
2622 }, nil, nil
2623 }
2624 
2625 // Get forecast data
2626 forecastURL := pointsData.Properties.Forecast
2627 if forecastURL == "" {
2628 return &mcp.CallToolResult{
2629 Content: []mcp.Content{
2630 &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2631 },
2632 }, nil, nil
2633 }
2634 
2635 forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2636 if err != nil {
2637 return &mcp.CallToolResult{
2638 Content: []mcp.Content{
2639 &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2640 },
2641 }, nil, nil
2642 }
2643 
2644 // Format the periods
2645 periods := forecastData.Properties.Periods
2646 if len(periods) == 0 {
2647 return &mcp.CallToolResult{
2648 Content: []mcp.Content{
2649 &mcp.TextContent{Text: "No forecast periods available."},
2650 },
2651 }, nil, nil
2652 }
2653 
2654 // Show next 5 periods
2655 var forecasts []string
2656 for i := range min(5, len(periods)) {
2657 forecasts = append(forecasts, formatPeriod(periods[i]))
2658 }
2659 
2660 result := strings.Join(forecasts, "\n---\n")
2661 
2662 return &mcp.CallToolResult{
2663 Content: []mcp.Content{
2664 &mcp.TextContent{Text: result},
2665 },
2666 }, nil, nil
2667}
2668 
2669func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2670 *mcp.CallToolResult, any, error,
2671) {
2672 // Build alerts URL
2673 stateCode := strings.ToUpper(input.State)
2674 alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2675 
2676 alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2677 if err != nil {
2678 return &mcp.CallToolResult{
2679 Content: []mcp.Content{
2680 &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2681 },
2682 }, nil, nil
2683 }
2684 
2685 // Check if there are any alerts
2686 if len(alertsData.Features) == 0 {
2687 return &mcp.CallToolResult{
2688 Content: []mcp.Content{
2689 &mcp.TextContent{Text: "No active alerts for this state."},
2690 },
2691 }, nil, nil
2692 }
2693 
2694 // Format alerts
2695 var alerts []string
2696 for _, feature := range alertsData.Features {
2697 alerts = append(alerts, formatAlert(feature))
2698 }
2699 
2700 result := strings.Join(alerts, "\n---\n")
2701 
2702 return &mcp.CallToolResult{
2703 Content: []mcp.Content{
2704 &mcp.TextContent{Text: result},
2705 },
2706 }, nil, nil
2707}
2708```
2709 
2710### Running the server
2711 
2712Finally, implement the main function to run the server:
2713 
2714```go theme={null}
2715func main() {
2716 // Create MCP server
2717 server := mcp.NewServer(&mcp.Implementation{
2718 Name: "weather",
2719 Version: "1.0.0",
2720 }, nil)
2721 
2722 // Add get_forecast tool
2723 mcp.AddTool(server, &mcp.Tool{
2724 Name: "get_forecast",
2725 Description: "Get weather forecast for a location",
2726 }, getForecast)
2727 
2728 // Add get_alerts tool
2729 mcp.AddTool(server, &mcp.Tool{
2730 Name: "get_alerts",
2731 Description: "Get weather alerts for a US state",
2732 }, getAlerts)
2733 
2734 // Run server on stdio transport
2735 if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2736 log.Fatal(err)
2737 }
2738}
2739```
2740 
2741Build your server with:
2742 
2743```bash theme={null}
2744go build -o weather .
2745```
2746 
2747The compiled binary will be in `./weather`.
2748 
2749Let's now test your server from an existing MCP host, Claude for Desktop.
2750 
2751## Testing your server with Claude for Desktop
2752 
2753> [!NOTE]
2754>
2755> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756 
2757First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2758 
2759We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2760 
2761For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762 
2763```bash macOS/Linux theme={null}
2764code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765```
2766 
2767```powershell Windows theme={null}
2768code $env:AppData\Claude\claude_desktop_config.json
2769```
2770 
2771You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2772 
2773In this case, we'll add our single weather server like so:
2774 
2775```json macOS/Linux theme={null}
2776{
2777 "mcpServers": {
2778 "weather": {
2779 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2780 }
2781 }
2782}
2783```
2784 
2785```json Windows theme={null}
2786{
2787 "mcpServers": {
2788 "weather": {
2789 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2790 }
2791 }
2792}
2793```
2794 
2795> [!NOTE]
2796>
2797> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2798 
2799This tells Claude for Desktop:
2800 
28011. There's an MCP server named "weather"
28022. Launch it by running the compiled binary at the specified path
2803 
2804Save the file, and restart **Claude for Desktop**.
27> [tab] Python
28>
29> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30>
31> ### Prerequisite knowledge
32>
33> This quickstart assumes you have familiarity with:
34>
35> * Python
36> * LLMs like Claude
37>
38> ### Logging in MCP Servers
39>
40> When implementing MCP servers, be careful about how you handle logging:
41>
42> **For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, but can be used safely with `file=sys.stderr`.
43>
44> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45>
46> ### Best Practices
47>
48> * Use a logging library that writes to stderr or files.
49>
50> ### Quick Examples
51>
52> ```python theme={null}
53> import sys
54> import logging
55>
56> # ❌ Bad (STDIO)
57> print("Processing request")
58>
59> # ✅ Good (STDIO)
60> print("Processing request", file=sys.stderr)
61>
62> # ✅ Good (STDIO)
63> logging.info("Processing request")
64> ```
65>
66> ### System requirements
67>
68> * Python 3.10 or higher installed.
69> * You must use the Python MCP SDK 1.2.0 or higher.
70>
71> ### Set up your environment
72>
73> First, let's install `uv` and set up our Python project and environment:
74>
75> ```bash macOS/Linux theme={null}
76> curl -LsSf https://astral.sh/uv/install.sh | sh
77> ```
78>
79> ```powershell Windows theme={null}
80> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81> ```
82>
83> Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84>
85> Now, let's create and set up our project:
86>
87> ```bash macOS/Linux theme={null}
88> # Create a new directory for our project
89> uv init weather
90> cd weather
91>
92> # Create virtual environment and activate it
93> uv venv
94> source .venv/bin/activate
95>
96> # Install dependencies
97> uv add "mcp[cli]" httpx
98>
99> # Create our server file
100> touch weather.py
101> ```
102>
103> ```powershell Windows theme={null}
104> # Create a new directory for our project
105> uv init weather
106> cd weather
107>
108> # Create virtual environment and activate it
109> uv venv
110> .venv\Scripts\activate
111>
112> # Install dependencies
113> uv add mcp[cli] httpx
114>
115> # Create our server file
116> new-item weather.py
117> ```
118>
119> Now let's dive into building your server.
120>
121> ## Building your server
122>
123> ### Importing packages and setting up the instance
124>
125> Add these to the top of your `weather.py`:
126>
127> ```python theme={null}
128> from typing import Any
129>
130> import httpx
131> from mcp.server.fastmcp import FastMCP
132>
133> # Initialize FastMCP server
134> mcp = FastMCP("weather")
135>
136> # Constants
137> NWS_API_BASE = "https://api.weather.gov"
138> USER_AGENT = "weather-app/1.0"
139> ```
140>
141> The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
142>
143> ### Helper functions
144>
145> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146>
147> ```python theme={null}
148> async def make_nws_request(url: str) -> dict[str, Any] | None:
149> """Make a request to the NWS API with proper error handling."""
150> headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
151> async with httpx.AsyncClient() as client:
152> try:
153> response = await client.get(url, headers=headers, timeout=30.0)
154> response.raise_for_status()
155> return response.json()
156> except Exception:
157> return None
158>
159>
160> def format_alert(feature: dict) -> str:
161> """Format an alert feature into a readable string."""
162> props = feature["properties"]
163> return f"""
164> Event: {props.get("event", "Unknown")}
165> Area: {props.get("areaDesc", "Unknown")}
166> Severity: {props.get("severity", "Unknown")}
167> Description: {props.get("description", "No description available")}
168> Instructions: {props.get("instruction", "No specific instructions provided")}
169> """
170> ```
171>
172> ### Implementing tool execution
173>
174> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
175>
176> ```python theme={null}
177> @mcp.tool()
178> async def get_alerts(state: str) -> str:
179> """Get weather alerts for a US state.
180>
181> Args:
182> state: Two-letter US state code (e.g. CA, NY)
183> """
184> url = f"{NWS_API_BASE}/alerts/active/area/{state}"
185> data = await make_nws_request(url)
186>
187> if not data or "features" not in data:
188> return "Unable to fetch alerts or no alerts found."
189>
190> if not data["features"]:
191> return "No active alerts for this state."
192>
193> alerts = [format_alert(feature) for feature in data["features"]]
194> return "\n---\n".join(alerts)
195>
196>
197> @mcp.tool()
198> async def get_forecast(latitude: float, longitude: float) -> str:
199> """Get weather forecast for a location.
200>
201> Args:
202> latitude: Latitude of the location
203> longitude: Longitude of the location
204> """
205> # First get the forecast grid endpoint
206> points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
207> points_data = await make_nws_request(points_url)
208>
209> if not points_data:
210> return "Unable to fetch forecast data for this location."
211>
212> # Get the forecast URL from the points response
213> forecast_url = points_data["properties"]["forecast"]
214> forecast_data = await make_nws_request(forecast_url)
215>
216> if not forecast_data:
217> return "Unable to fetch detailed forecast."
218>
219> # Format the periods into a readable forecast
220> periods = forecast_data["properties"]["periods"]
221> forecasts = []
222> for period in periods[:5]: # Only show next 5 periods
223> forecast = f"""
224> {period["name"]}:
225> Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226> Wind: {period["windSpeed"]} {period["windDirection"]}
227> Forecast: {period["detailedForecast"]}
228> """
229> forecasts.append(forecast)
230>
231> return "\n---\n".join(forecasts)
232> ```
233>
234> ### Running the server
235>
236> Finally, let's initialize and run the server:
237>
238> ```python theme={null}
239> def main():
240> # Initialize and run the server
241> mcp.run(transport="stdio")
242>
243>
244> if __name__ == "__main__":
245> main()
246> ```
247>
248> Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249>
250> Let's now test your server from an existing MCP host, Claude for Desktop.
251>
252> ## Testing your server with Claude for Desktop
253>
254> > [!NOTE]
255> >
256> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257>
258> First, make sure you have Claude for Desktop installed. [You can install the latest version
259> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260>
261> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
262>
263> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264>
265> ```bash macOS/Linux theme={null}
266> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267> ```
268>
269> ```powershell Windows theme={null}
270> code $env:AppData\Claude\claude_desktop_config.json
271> ```
272>
273> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
274>
275> In this case, we'll add our single weather server like so:
276>
277> ```json macOS/Linux theme={null}
278> {
279> "mcpServers": {
280> "weather": {
281> "command": "uv",
282> "args": [
283> "--directory",
284> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
285> "run",
286> "weather.py"
287> ]
288> }
289> }
290> }
291> ```
292>
293> ```json Windows theme={null}
294> {
295> "mcpServers": {
296> "weather": {
297> "command": "uv",
298> "args": [
299> "--directory",
300> "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
301> "run",
302> "weather.py"
303> ]
304> }
305> }
306> }
307> ```
308>
309> > [!WARNING]
310> >
311> > You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
312>
313> > [!NOTE]
314> >
315> > Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
316>
317> This tells Claude for Desktop:
318>
319> 1. There's an MCP server named "weather"
320> 2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321>
322> Save the file, and restart **Claude for Desktop**.
323 
324> [tab] TypeScript
325>
326> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
327>
328> ### Prerequisite knowledge
329>
330> This quickstart assumes you have familiarity with:
331>
332> * TypeScript
333> * LLMs like Claude
334>
335> ### Logging in MCP Servers
336>
337> When implementing MCP servers, be careful about how you handle logging:
338>
339> **For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
340>
341> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
342>
343> ### Best Practices
344>
345> * Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
346>
347> ### Quick Examples
348>
349> ```javascript theme={null}
350> // ❌ Bad (STDIO)
351> console.log("Server started");
352>
353> // ✅ Good (STDIO)
354> console.error("Server started"); // stderr is safe
355> ```
356>
357> ### System requirements
358>
359> For TypeScript, make sure you have the latest version of Node installed.
360>
361> ### Set up your environment
362>
363> First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364> Verify your Node.js installation:
365>
366> ```bash theme={null}
367> node --version
368> npm --version
369> ```
370>
371> For this tutorial, you'll need Node.js version 16 or higher.
372>
373> Now, let's create and set up our project:
374>
375> ```bash macOS/Linux theme={null}
376> # Create a new directory for our project
377> mkdir weather
378> cd weather
379>
380> # Initialize a new npm project
381> npm init -y
382>
383> # Install dependencies
384> npm install @modelcontextprotocol/sdk zod@3
385> npm install -D @types/node typescript
386>
387> # Create our files
388> mkdir src
389> touch src/index.ts
390> ```
391>
392> ```powershell Windows theme={null}
393> # Create a new directory for our project
394> md weather
395> cd weather
396>
397> # Initialize a new npm project
398> npm init -y
399>
400> # Install dependencies
401> npm install @modelcontextprotocol/sdk zod@3
402> npm install -D @types/node typescript
403>
404> # Create our files
405> md src
406> new-item src\index.ts
407> ```
408>
409> Update your package.json to add type: "module" and a build script:
410>
411> ```json package.json theme={null}
412> {
413> "type": "module",
414> "bin": {
415> "weather": "./build/index.js"
416> },
417> "scripts": {
418> "build": "tsc && chmod 755 build/index.js"
419> },
420> "files": ["build"]
421> }
422> ```
423>
424> Create a `tsconfig.json` in the root of your project:
425>
426> ```json tsconfig.json theme={null}
427> {
428> "compilerOptions": {
429> "target": "ES2022",
430> "module": "Node16",
431> "moduleResolution": "Node16",
432> "outDir": "./build",
433> "rootDir": "./src",
434> "strict": true,
435> "esModuleInterop": true,
436> "skipLibCheck": true,
437> "forceConsistentCasingInFileNames": true
438> },
439> "include": ["src/**/*"],
440> "exclude": ["node_modules"]
441> }
442> ```
443>
444> Now let's dive into building your server.
445>
446> ## Building your server
447>
448> ### Importing packages and setting up the instance
449>
450> Add these to the top of your `src/index.ts`:
451>
452> ```typescript theme={null}
453> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454> import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455> import { z } from "zod";
456>
457> const NWS_API_BASE = "https://api.weather.gov";
458> const USER_AGENT = "weather-app/1.0";
459>
460> // Create server instance
461> const server = new McpServer({
462> name: "weather",
463> version: "1.0.0",
464> });
465> ```
466>
467> ### Helper functions
468>
469> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
470>
471> ```typescript theme={null}
472> // Helper function for making NWS API requests
473> async function makeNWSRequest<T>(url: string): Promise<T | null> {
474> const headers = {
475> "User-Agent": USER_AGENT,
476> Accept: "application/geo+json",
477> };
478>
479> try {
480> const response = await fetch(url, { headers });
481> if (!response.ok) {
482> throw new Error(`HTTP error! status: ${response.status}`);
483> }
484> return (await response.json()) as T;
485> } catch (error) {
486> console.error("Error making NWS request:", error);
487> return null;
488> }
489> }
490>
491> interface AlertFeature {
492> properties: {
493> event?: string;
494> areaDesc?: string;
495> severity?: string;
496> status?: string;
497> headline?: string;
498> };
499> }
500>
501> // Format alert data
502> function formatAlert(feature: AlertFeature): string {
503> const props = feature.properties;
504> return [
505> `Event: ${props.event || "Unknown"}`,
506> `Area: ${props.areaDesc || "Unknown"}`,
507> `Severity: ${props.severity || "Unknown"}`,
508> `Status: ${props.status || "Unknown"}`,
509> `Headline: ${props.headline || "No headline"}`,
510> "---",
511> ].join("\n");
512> }
513>
514> interface ForecastPeriod {
515> name?: string;
516> temperature?: number;
517> temperatureUnit?: string;
518> windSpeed?: string;
519> windDirection?: string;
520> shortForecast?: string;
521> }
522>
523> interface AlertsResponse {
524> features: AlertFeature[];
525> }
526>
527> interface PointsResponse {
528> properties: {
529> forecast?: string;
530> };
531> }
532>
533> interface ForecastResponse {
534> properties: {
535> periods: ForecastPeriod[];
536> };
537> }
538> ```
539>
540> ### Implementing tool execution
541>
542> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
543>
544> ```typescript theme={null}
545> // Register weather tools
546>
547> server.registerTool(
548> "get_alerts",
549> {
550> description: "Get weather alerts for a state",
551> inputSchema: {
552> state: z
553> .string()
554> .length(2)
555> .describe("Two-letter state code (e.g. CA, NY)"),
556> },
557> },
558> async ({ state }) => {
559> const stateCode = state.toUpperCase();
560> const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
561> const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
562>
563> if (!alertsData) {
564> return {
565> content: [
566> {
567> type: "text",
568> text: "Failed to retrieve alerts data",
569> },
570> ],
571> };
572> }
573>
574> const features = alertsData.features || [];
575> if (!features.length) {
576> return {
577> content: [
578> {
579> type: "text",
580> text: `No active alerts for ${stateCode}`,
581> },
582> ],
583> };
584> }
585>
586> const formattedAlerts = features.map(formatAlert);
587> const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
588>
589> return {
590> content: [
591> {
592> type: "text",
593> text: alertsText,
594> },
595> ],
596> };
597> },
598> );
599>
600> server.registerTool(
601> "get_forecast",
602> {
603> description: "Get weather forecast for a location",
604> inputSchema: {
605> latitude: z
606> .number()
607> .min(-90)
608> .max(90)
609> .describe("Latitude of the location"),
610> longitude: z
611> .number()
612> .min(-180)
613> .max(180)
614> .describe("Longitude of the location"),
615> },
616> },
617> async ({ latitude, longitude }) => {
618> // Get grid point data
619> const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
620> const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
621>
622> if (!pointsData) {
623> return {
624> content: [
625> {
626> type: "text",
627> text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
628> },
629> ],
630> };
631> }
632>
633> const forecastUrl = pointsData.properties?.forecast;
634> if (!forecastUrl) {
635> return {
636> content: [
637> {
638> type: "text",
639> text: "Failed to get forecast URL from grid point data",
640> },
641> ],
642> };
643> }
644>
645> // Get forecast data
646> const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
647> if (!forecastData) {
648> return {
649> content: [
650> {
651> type: "text",
652> text: "Failed to retrieve forecast data",
653> },
654> ],
655> };
656> }
657>
658> const periods = forecastData.properties?.periods || [];
659> if (periods.length === 0) {
660> return {
661> content: [
662> {
663> type: "text",
664> text: "No forecast periods available",
665> },
666> ],
667> };
668> }
669>
670> // Format forecast periods
671> const formattedForecast = periods.map((period: ForecastPeriod) =>
672> [
673> `${period.name || "Unknown"}:`,
674> `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
675> `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
676> `${period.shortForecast || "No forecast available"}`,
677> "---",
678> ].join("\n"),
679> );
680>
681> const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
682>
683> return {
684> content: [
685> {
686> type: "text",
687> text: forecastText,
688> },
689> ],
690> };
691> },
692> );
693> ```
694>
695> ### Running the server
696>
697> Finally, implement the main function to run the server:
698>
699> ```typescript theme={null}
700> async function main() {
701> const transport = new StdioServerTransport();
702> await server.connect(transport);
703> console.error("Weather MCP Server running on stdio");
704> }
705>
706> main().catch((error) => {
707> console.error("Fatal error in main():", error);
708> process.exit(1);
709> });
710> ```
711>
712> Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713>
714> Let's now test your server from an existing MCP host, Claude for Desktop.
715>
716> ## Testing your server with Claude for Desktop
717>
718> > [!NOTE]
719> >
720> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721>
722> First, make sure you have Claude for Desktop installed. [You can install the latest version
723> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724>
725> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726>
727> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728>
729> ```bash macOS/Linux theme={null}
730> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731> ```
732>
733> ```powershell Windows theme={null}
734> code $env:AppData\Claude\claude_desktop_config.json
735> ```
736>
737> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
738>
739> In this case, we'll add our single weather server like so:
740>
741> ```json macOS/Linux theme={null}
742> {
743> "mcpServers": {
744> "weather": {
745> "command": "node",
746> "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
747> }
748> }
749> }
750> ```
751>
752> ```json Windows theme={null}
753> {
754> "mcpServers": {
755> "weather": {
756> "command": "node",
757> "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
758> }
759> }
760> }
761> ```
762>
763> This tells Claude for Desktop:
764>
765> 1. There's an MCP server named "weather"
766> 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767>
768> Save the file, and restart **Claude for Desktop**.
769 
770> [tab] Java
771>
772> > [!NOTE]
773> >
774> > This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
775> > To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
776>
777> Let's get started with building our weather server!
778> [You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
779>
780> For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
781> For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782>
783> ### Logging in MCP Servers
784>
785> When implementing MCP servers, be careful about how you handle logging:
786>
787> **For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
788>
789> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
790>
791> ### Best Practices
792>
793> * Use a logging library that writes to stderr or files.
794> * Ensure any configured logging library will not write to stdout.
795>
796> ### System requirements
797>
798> * Java 17 or higher installed.
799> * [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
800>
801> ### Set up your environment
802>
803> Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804>
805> You will need to add the following dependencies:
806>
807> ```xml Maven theme={null}
808> <dependencies>
809> <dependency>
810> <groupId>org.springframework.ai</groupId>
811> <artifactId>spring-ai-starter-mcp-server</artifactId>
812> </dependency>
813>
814> <dependency>
815> <groupId>org.springframework</groupId>
816> <artifactId>spring-web</artifactId>
817> </dependency>
818> </dependencies>
819> ```
820>
821> ```groovy Gradle theme={null}
822> dependencies {
823> implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824> implementation platform("org.springframework:spring-web")
825> }
826> ```
827>
828> Then configure your application by setting the application properties:
829>
830> ```bash application.properties theme={null}
831> spring.main.bannerMode=off
832> logging.pattern.console=
833> ```
834>
835> ```yaml application.yml theme={null}
836> logging:
837> pattern:
838> console:
839> spring:
840> main:
841> banner-mode: off
842> ```
843>
844> The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
845>
846> Now let's dive into building your server.
847>
848> ## Building your server
849>
850> ### Weather Service
851>
852> Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
853>
854> ```java theme={null}
855> @Service
856> public class WeatherService {
857>
858> private final RestClient restClient;
859>
860> public WeatherService() {
861> this.restClient = RestClient.builder()
862> .baseUrl("https://api.weather.gov")
863> .defaultHeader("Accept", "application/geo+json")
864> .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
865> .build();
866> }
867>
868> @Tool(description = "Get weather forecast for a specific latitude/longitude")
869> public String getWeatherForecastByLocation(
870> double latitude, // Latitude coordinate
871> double longitude // Longitude coordinate
872> ) {
873> // Returns detailed forecast including:
874> // - Temperature and unit
875> // - Wind speed and direction
876> // - Detailed forecast description
877> }
878>
879> @Tool(description = "Get weather alerts for a US state")
880> public String getAlerts(
881> @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
882> ) {
883> // Returns active alerts including:
884> // - Event type
885> // - Affected area
886> // - Severity
887> // - Description
888> // - Safety instructions
889> }
890>
891> // ......
892> }
893> ```
894>
895> The `@Service` annotation will auto-register the service in your application context.
896> The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897>
898> The auto-configuration will automatically register these tools with the MCP server.
899>
900> ### Create your Boot Application
901>
902> ```java theme={null}
903> @SpringBootApplication
904> public class McpServerApplication {
905>
906> public static void main(String[] args) {
907> SpringApplication.run(McpServerApplication.class, args);
908> }
909>
910> @Bean
911> public ToolCallbackProvider weatherTools(WeatherService weatherService) {
912> return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
913> }
914> }
915> ```
916>
917> Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918>
919> ### Running the server
920>
921> Finally, let's build the server:
922>
923> ```bash theme={null}
924> ./mvnw clean install
925> ```
926>
927> This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928>
929> Let's now test your server from an existing MCP host, Claude for Desktop.
930>
931> ## Testing your server with Claude for Desktop
932>
933> > [!NOTE]
934> >
935> > Claude for Desktop is not yet available on Linux.
936>
937> First, make sure you have Claude for Desktop installed.
938> [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939>
940> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942> Make sure to create the file if it doesn't exist.
943>
944> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945>
946> ```bash macOS/Linux theme={null}
947> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948> ```
949>
950> ```powershell Windows theme={null}
951> code $env:AppData\Claude\claude_desktop_config.json
952> ```
953>
954> You'll then add your servers in the `mcpServers` key.
955> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956>
957> In this case, we'll add our single weather server like so:
958>
959> ```json macOS/Linux theme={null}
960> {
961> "mcpServers": {
962> "spring-ai-mcp-weather": {
963> "command": "java",
964> "args": [
965> "-Dspring.ai.mcp.server.stdio=true",
966> "-jar",
967> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
968> ]
969> }
970> }
971> }
972> ```
973>
974> ```json Windows theme={null}
975> {
976> "mcpServers": {
977> "spring-ai-mcp-weather": {
978> "command": "java",
979> "args": [
980> "-Dspring.ai.mcp.server.transport=STDIO",
981> "-jar",
982> "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
983> ]
984> }
985> }
986> }
987> ```
988>
989> > [!NOTE]
990> >
991> > Make sure you pass in the absolute path to your server.
992>
993> This tells Claude for Desktop:
994>
995> 1. There's an MCP server named "my-weather-server"
996> 2. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997>
998> Save the file, and restart **Claude for Desktop**.
999>
1000> ## Testing your server with Java client
1001>
1002> ### Create an MCP Client manually
1003>
1004> Use the `McpClient` to connect to the server:
1005>
1006> ```java theme={null}
1007> var stdioParams = ServerParameters.builder("java")
1008> .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009> .build();
1010>
1011> var stdioTransport = new StdioClientTransport(stdioParams);
1012>
1013> var mcpClient = McpClient.sync(stdioTransport).build();
1014>
1015> mcpClient.initialize();
1016>
1017> ListToolsResult toolsList = mcpClient.listTools();
1018>
1019> CallToolResult weather = mcpClient.callTool(
1020> new CallToolRequest("getWeatherForecastByLocation",
1021> Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022>
1023> CallToolResult alert = mcpClient.callTool(
1024> new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025>
1026> mcpClient.closeGracefully();
1027> ```
1028>
1029> ### Use MCP Client Boot Starter
1030>
1031> Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1032>
1033> ```xml theme={null}
1034> <dependency>
1035> <groupId>org.springframework.ai</groupId>
1036> <artifactId>spring-ai-starter-mcp-client</artifactId>
1037> </dependency>
1038> ```
1039>
1040> and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041> You can reuse the existing Anthropic Desktop configuration:
1042>
1043> ```properties theme={null}
1044> spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045> ```
1046>
1047> When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048>
1049> For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1050>
1051> ## More Java MCP Server examples
1052>
1053> The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an MCP server using SSE transport.
1054> It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056> [tab] Kotlin
1057>
1058> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1059>
1060> ### Prerequisite knowledge
1061>
1062> This quickstart assumes you have familiarity with:
1063>
1064> * Kotlin
1065> * LLMs like Claude
1066>
1067> ### Logging in MCP Servers
1068>
1069> When implementing MCP servers, be careful about how you handle logging:
1070>
1071> **For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1072>
1073> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1074>
1075> ### Best Practices
1076>
1077> * Use a logging library that writes to stderr or files.
1078>
1079> ### System requirements
1080>
1081> * JDK 11 or higher installed.
1082>
1083> ### Set up your environment
1084>
1085> First, let's install `java` and `gradle` if you haven't already.
1086> You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087> Verify your `java` installation:
1088>
1089> ```bash theme={null}
1090> java --version
1091> ```
1092>
1093> Now, let's create and set up your project:
1094>
1095> ```bash macOS/Linux theme={null}
1096> # Create a new directory for our project
1097> mkdir weather
1098> cd weather
1099>
1100> # Initialize a new kotlin project
1101> gradle init
1102> ```
1103>
1104> ```powershell Windows theme={null}
1105> # Create a new directory for our project
1106> md weather
1107> cd weather
1108>
1109> # Initialize a new kotlin project
1110> gradle init
1111> ```
1112>
1113> After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114>
1115> Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116>
1117> After creating the project, replace the contents of your `build.gradle.kts` with:
1118>
1119> ```kotlin build.gradle.kts theme={null}
1120> // Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1121> val mcpVersion = "0.9.0"
1122> val ktorVersion = "3.2.3"
1123> val slf4jVersion = "2.0.17"
1124>
1125> plugins {
1126> kotlin("jvm") version "2.3.20"
1127> kotlin("plugin.serialization") version "2.3.20"
1128> id("com.gradleup.shadow") version "8.3.9"
1129> application
1130> }
1131>
1132> application {
1133> mainClass.set("MainKt")
1134> }
1135>
1136> dependencies {
1137> implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1138> implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1139> implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1140> implementation("io.ktor:ktor-client-cio:$ktorVersion")
1141> implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1142> }
1143> ```
1144>
1145> Verify that everything is set up correctly:
1146>
1147> ```bash theme={null}
1148> ./gradlew build
1149> ```
1150>
1151> Now let's dive into building your server.
1152>
1153> ## Building your server
1154>
1155> ### Setting up the instance
1156>
1157> Add a server initialization function:
1158>
1159> ```kotlin theme={null}
1160> fun runMcpServer() {
1161> val server = Server(
1162> Implementation(
1163> name = "weather",
1164> version = "1.0.0",
1165> ),
1166> ServerOptions(
1167> capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1168> ),
1169> )
1170>
1171> // register tools on server here
1172>
1173> val transport = StdioServerTransport(
1174> System.`in`.asInput(),
1175> System.out.asSink().buffered(),
1176> )
1177>
1178> runBlocking {
1179> val session = server.createSession(transport)
1180> val done = Job()
1181> session.onClose {
1182> done.complete()
1183> }
1184> done.join()
1185> }
1186> }
1187> ```
1188>
1189> ### Weather API helper functions
1190>
1191> Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192>
1193> ```kotlin theme={null}
1194> val httpClient = HttpClient(CIO) {
1195> defaultRequest {
1196> url("https://api.weather.gov")
1197> headers {
1198> append("Accept", "application/geo+json")
1199> append("User-Agent", "WeatherApiClient/1.0")
1200> }
1201> contentType(ContentType.Application.Json)
1202> }
1203> install(ContentNegotiation) {
1204> json(Json { ignoreUnknownKeys = true })
1205> }
1206> }
1207>
1208> // Extension function to fetch weather alerts for a given state
1209> suspend fun HttpClient.getAlerts(state: String): List<String> {
1210> val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211> return alerts.features.map { feature ->
1212> """
1213> Event: ${feature.properties.event}
1214> Area: ${feature.properties.areaDesc}
1215> Severity: ${feature.properties.severity}
1216> Status: ${feature.properties.status}
1217> Headline: ${feature.properties.headline}
1218> """.trimIndent()
1219> }
1220> }
1221>
1222> // Extension function to fetch forecast information for given latitude and longitude
1223> suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1224> val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1225> val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1226> val forecast = this.get(forecastUrl).body<ForecastResponse>()
1227> return forecast.properties.periods.map { period ->
1228> """
1229> ${period.name}:
1230> Temperature: ${period.temperature}°${period.temperatureUnit}
1231> Wind: ${period.windSpeed} ${period.windDirection}
1232> ${period.shortForecast}
1233> """.trimIndent()
1234> }
1235> }
1236>
1237> @Serializable
1238> data class PointsResponse(val properties: PointsProperties)
1239>
1240> @Serializable
1241> data class PointsProperties(val forecast: String? = null)
1242>
1243> @Serializable
1244> data class ForecastResponse(val properties: ForecastProperties)
1245>
1246> @Serializable
1247> data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248>
1249> @Serializable
1250> data class ForecastPeriod(
1251> val name: String? = null,
1252> val temperature: Int? = null,
1253> val temperatureUnit: String? = null,
1254> val windSpeed: String? = null,
1255> val windDirection: String? = null,
1256> val shortForecast: String? = null,
1257> )
1258>
1259> @Serializable
1260> data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261>
1262> @Serializable
1263> data class AlertFeature(val properties: AlertProperties)
1264>
1265> @Serializable
1266> data class AlertProperties(
1267> val event: String? = null,
1268> val areaDesc: String? = null,
1269> val severity: String? = null,
1270> val status: String? = null,
1271> val headline: String? = null,
1272> )
1273> ```
1274>
1275> ### Implementing tool execution
1276>
1277> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1278>
1279> ```kotlin theme={null}
1280> // Register weather tools
1281>
1282> server.addTool(
1283> name = "get_alerts",
1284> description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1285> inputSchema = ToolSchema(
1286> properties = buildJsonObject {
1287> putJsonObject("state") {
1288> put("type", "string")
1289> put("description", "Two-letter US state code (e.g. CA, NY)")
1290> }
1291> },
1292> required = listOf("state"),
1293> ),
1294> ) { request ->
1295> val state = request.arguments?.get("state")?.jsonPrimitive?.content
1296> ?: return@addTool CallToolResult(
1297> content = listOf(TextContent("The 'state' parameter is required.")),
1298> )
1299>
1300> val alerts = httpClient.getAlerts(state)
1301> CallToolResult(content = alerts.map { TextContent(it) })
1302> }
1303>
1304> server.addTool(
1305> name = "get_forecast",
1306> description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1307> inputSchema = ToolSchema(
1308> properties = buildJsonObject {
1309> putJsonObject("latitude") {
1310> put("type", "number")
1311> put("description", "Latitude of the location")
1312> }
1313> putJsonObject("longitude") {
1314> put("type", "number")
1315> put("description", "Longitude of the location")
1316> }
1317> },
1318> required = listOf("latitude", "longitude"),
1319> ),
1320> ) { request ->
1321> val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1322> val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1323> if (latitude == null || longitude == null) {
1324> return@addTool CallToolResult(
1325> content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1326> )
1327> }
1328>
1329> val forecast = httpClient.getForecast(latitude, longitude)
1330> CallToolResult(content = forecast.map { TextContent(it) })
1331> }
1332> ```
1333>
1334> ### Running the server
1335>
1336> Finally, implement the main function to run the server:
1337>
1338> ```kotlin theme={null}
1339> fun main() = runMcpServer()
1340> ```
1341>
1342> You can run the server directly during development:
1343>
1344> ```bash theme={null}
1345> ./gradlew run
1346> ```
1347>
1348> For production use, build the shadow JAR:
1349>
1350> ```bash theme={null}
1351> ./gradlew build
1352> java -jar build/libs/weather-0.1.0-all.jar
1353> ```
1354>
1355> Let's now test your server from an existing MCP host, Claude for Desktop.
1356>
1357> ## Testing your server with Claude for Desktop
1358>
1359> > [!NOTE]
1360> >
1361> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362>
1363> First, make sure you have Claude for Desktop installed. [You can install the latest version
1364> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365>
1366> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368> Make sure to create the file if it doesn't exist.
1369>
1370> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371>
1372> ```bash macOS/Linux theme={null}
1373> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374> ```
1375>
1376> ```powershell Windows theme={null}
1377> code $env:AppData\Claude\claude_desktop_config.json
1378> ```
1379>
1380> You'll then add your servers in the `mcpServers` key.
1381> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382>
1383> In this case, we'll add our single weather server like so:
1384>
1385> ```json macOS/Linux theme={null}
1386> {
1387> "mcpServers": {
1388> "weather": {
1389> "command": "java",
1390> "args": [
1391> "-jar",
1392> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1393> ]
1394> }
1395> }
1396> }
1397> ```
1398>
1399> ```json Windows theme={null}
1400> {
1401> "mcpServers": {
1402> "weather": {
1403> "command": "java",
1404> "args": [
1405> "-jar",
1406> "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1407> ]
1408> }
1409> }
1410> }
1411> ```
1412>
1413> This tells Claude for Desktop:
1414>
1415> 1. There's an MCP server named "weather"
1416> 2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417>
1418> Save the file, and restart **Claude for Desktop**.
1419 
1420> [tab] C#
1421>
1422> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1423>
1424> ### Prerequisite knowledge
1425>
1426> This quickstart assumes you have familiarity with:
1427>
1428> * C#
1429> * LLMs like Claude
1430> * .NET 8 or higher
1431>
1432> ### Logging in MCP Servers
1433>
1434> When implementing MCP servers, be careful about how you handle logging:
1435>
1436> **For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1437>
1438> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1439>
1440> ### Best Practices
1441>
1442> * Use a logging library that writes to stderr or files.
1443>
1444> ### System requirements
1445>
1446> * [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1447>
1448> ### Set up your environment
1449>
1450> First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1451>
1452> ```bash theme={null}
1453> dotnet --version
1454> ```
1455>
1456> Now, let's create and set up your project:
1457>
1458> ```bash macOS/Linux theme={null}
1459> # Create a new directory for our project
1460> mkdir weather
1461> cd weather
1462> # Initialize a new C# project
1463> dotnet new console
1464> ```
1465>
1466> ```powershell Windows theme={null}
1467> # Create a new directory for our project
1468> mkdir weather
1469> cd weather
1470> # Initialize a new C# project
1471> dotnet new console
1472> ```
1473>
1474> After running `dotnet new console`, you will be presented with a new C# project.
1475> You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476> Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1477> After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1478>
1479> ```bash theme={null}
1480> # Add the Model Context Protocol SDK NuGet package
1481> dotnet add package ModelContextProtocol --prerelease
1482> # Add the .NET Hosting NuGet package
1483> dotnet add package Microsoft.Extensions.Hosting
1484> ```
1485>
1486> Now let’s dive into building your server.
1487>
1488> ## Building your server
1489>
1490> Open the `Program.cs` file in your project and replace its contents with the following code:
1491>
1492> ```csharp theme={null}
1493> using Microsoft.Extensions.DependencyInjection;
1494> using Microsoft.Extensions.Hosting;
1495> using ModelContextProtocol;
1496> using System.Net.Http.Headers;
1497>
1498> var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499>
1500> builder.Services.AddMcpServer()
1501> .WithStdioServerTransport()
1502> .WithToolsFromAssembly();
1503>
1504> builder.Services.AddSingleton(_ =>
1505> {
1506> var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1507> client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1508> return client;
1509> });
1510>
1511> var app = builder.Build();
1512>
1513> await app.RunAsync();
1514> ```
1515>
1516> > [!NOTE]
1517> >
1518> > When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1519>
1520> This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1521>
1522> ### Weather API helper functions
1523>
1524> Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525>
1526> ```csharp theme={null}
1527> using System.Text.Json;
1528>
1529> internal static class HttpClientExt
1530> {
1531> public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1532> {
1533> using var response = await client.GetAsync(requestUri);
1534> response.EnsureSuccessStatusCode();
1535> return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1536> }
1537> }
1538> ```
1539>
1540> Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541>
1542> ```csharp theme={null}
1543> using ModelContextProtocol.Server;
1544> using System.ComponentModel;
1545> using System.Globalization;
1546> using System.Text.Json;
1547>
1548> namespace QuickstartWeatherServer.Tools;
1549>
1550> [McpServerToolType]
1551> public static class WeatherTools
1552> {
1553> [McpServerTool, Description("Get weather alerts for a US state code.")]
1554> public static async Task<string> GetAlerts(
1555> HttpClient client,
1556> [Description("The US state code to get alerts for.")] string state)
1557> {
1558> using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1559> var jsonElement = jsonDocument.RootElement;
1560> var alerts = jsonElement.GetProperty("features").EnumerateArray();
1561>
1562> if (!alerts.Any())
1563> {
1564> return "No active alerts for this state.";
1565> }
1566>
1567> return string.Join("\n--\n", alerts.Select(alert =>
1568> {
1569> JsonElement properties = alert.GetProperty("properties");
1570> return $"""
1571> Event: {properties.GetProperty("event").GetString()}
1572> Area: {properties.GetProperty("areaDesc").GetString()}
1573> Severity: {properties.GetProperty("severity").GetString()}
1574> Description: {properties.GetProperty("description").GetString()}
1575> Instruction: {properties.GetProperty("instruction").GetString()}
1576> """;
1577> }));
1578> }
1579>
1580> [McpServerTool, Description("Get weather forecast for a location.")]
1581> public static async Task<string> GetForecast(
1582> HttpClient client,
1583> [Description("Latitude of the location.")] double latitude,
1584> [Description("Longitude of the location.")] double longitude)
1585> {
1586> var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1587> using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1588> var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1589> ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1590>
1591> using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1592> var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1593>
1594> return string.Join("\n---\n", periods.Select(period => $"""
1595> {period.GetProperty("name").GetString()}
1596> Temperature: {period.GetProperty("temperature").GetInt32()}°F
1597> Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1598> Forecast: {period.GetProperty("detailedForecast").GetString()}
1599> """));
1600> }
1601> }
1602> ```
1603>
1604> ### Running the server
1605>
1606> Finally, run the server using the following command:
1607>
1608> ```bash theme={null}
1609> dotnet run
1610> ```
1611>
1612> This will start the server and listen for incoming requests on standard input/output.
1613>
1614> ## Testing your server with Claude for Desktop
1615>
1616> > [!NOTE]
1617> >
1618> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619>
1620> First, make sure you have Claude for Desktop installed. [You can install the latest version
1621> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1623> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624>
1625> ```bash macOS/Linux theme={null}
1626> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627> ```
1628>
1629> ```powershell Windows theme={null}
1630> code $env:AppData\Claude\claude_desktop_config.json
1631> ```
1632>
1633> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1634> In this case, we'll add our single weather server like so:
1635>
1636> ```json macOS/Linux theme={null}
1637> {
1638> "mcpServers": {
1639> "weather": {
1640> "command": "dotnet",
1641> "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1642> }
1643> }
1644> }
1645> ```
1646>
1647> ```json Windows theme={null}
1648> {
1649> "mcpServers": {
1650> "weather": {
1651> "command": "dotnet",
1652> "args": [
1653> "run",
1654> "--project",
1655> "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1656> "--no-build"
1657> ]
1658> }
1659> }
1660> }
1661> ```
1662>
1663> This tells Claude for Desktop:
1664>
1665> 1. There's an MCP server named "weather"
1666> 2. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667> Save the file, and restart **Claude for Desktop**.
1668 
1669> [tab] Ruby
1670>
1671> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1672>
1673> ### Prerequisite knowledge
1674>
1675> This quickstart assumes you have familiarity with:
1676>
1677> * Ruby
1678> * LLMs like Claude
1679>
1680> ### Logging in MCP Servers
1681>
1682> When implementing MCP servers, be careful about how you handle logging:
1683>
1684> **For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1685>
1686> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1687>
1688> ### Best Practices
1689>
1690> * Use a logging library that writes to stderr or files.
1691>
1692> ### Quick Examples
1693>
1694> ```ruby theme={null}
1695> # ❌ Bad (STDIO)
1696> puts "Processing request"
1697>
1698> # ✅ Good (STDIO)
1699> require "logger"
1700> logger = Logger.new($stderr)
1701> logger.info("Processing request")
1702> ```
1703>
1704> ### System requirements
1705>
1706> * Ruby 2.7 or higher installed.
1707>
1708> ### Set up your environment
1709>
1710> First, let's make sure you have Ruby installed. You can check by running:
1711>
1712> ```bash theme={null}
1713> ruby --version
1714> ```
1715>
1716> Now, let's create and set up our project:
1717>
1718> ```bash macOS/Linux theme={null}
1719> # Create a new directory for our project
1720> mkdir weather
1721> cd weather
1722>
1723> # Create a Gemfile
1724> bundle init
1725>
1726> # Add the MCP SDK dependency
1727> bundle add mcp
1728>
1729> # Create our server file
1730> touch weather.rb
1731> ```
1732>
1733> ```powershell Windows theme={null}
1734> # Create a new directory for our project
1735> mkdir weather
1736> cd weather
1737>
1738> # Create a Gemfile
1739> bundle init
1740>
1741> # Add the MCP SDK dependency
1742> bundle add mcp
1743>
1744> # Create our server file
1745> new-item weather.rb
1746> ```
1747>
1748> Now let's dive into building your server.
1749>
1750> ## Building your server
1751>
1752> ### Importing packages and setting up constants
1753>
1754> Open `weather.rb` and add these requires and constants at the top:
1755>
1756> ```ruby theme={null}
1757> require "json"
1758> require "mcp"
1759> require "net/http"
1760> require "uri"
1761>
1762> NWS_API_BASE = "https://api.weather.gov"
1763> USER_AGENT = "weather-app/1.0"
1764> ```
1765>
1766> The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767>
1768> ### Helper methods
1769>
1770> Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771>
1772> ```ruby theme={null}
1773> module HelperMethods
1774> def make_nws_request(url)
1775> uri = URI(url)
1776> request = Net::HTTP::Get.new(uri)
1777> request["User-Agent"] = USER_AGENT
1778> request["Accept"] = "application/geo+json"
1779>
1780> response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1781> http.request(request)
1782> end
1783>
1784> raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1785>
1786> JSON.parse(response.body)
1787> end
1788>
1789> def format_alert(feature)
1790> properties = feature["properties"]
1791>
1792> <<~ALERT
1793> Event: #{properties["event"] || "Unknown"}
1794> Area: #{properties["areaDesc"] || "Unknown"}
1795> Severity: #{properties["severity"] || "Unknown"}
1796> Description: #{properties["description"] || "No description available"}
1797> Instructions: #{properties["instruction"] || "No specific instructions provided"}
1798> ALERT
1799> end
1800> end
1801> ```
1802>
1803> ### Implementing tool execution
1804>
1805> Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806>
1807> ```ruby theme={null}
1808> class GetAlerts < MCP::Tool
1809> extend HelperMethods
1810>
1811> tool_name "get_alerts"
1812> description "Get weather alerts for a US state"
1813> input_schema(
1814> properties: {
1815> state: {
1816> type: "string",
1817> description: "Two-letter US state code (e.g. CA, NY)"
1818> }
1819> },
1820> required: ["state"]
1821> )
1822>
1823> def self.call(state:)
1824> url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1825> data = make_nws_request(url)
1826>
1827> if data["features"].empty?
1828> return MCP::Tool::Response.new([{
1829> type: "text",
1830> text: "No active alerts for this state."
1831> }])
1832> end
1833>
1834> alerts = data["features"].map { |feature| format_alert(feature) }
1835> MCP::Tool::Response.new([{
1836> type: "text",
1837> text: alerts.join("\n---\n")
1838> }])
1839> end
1840> end
1841>
1842> class GetForecast < MCP::Tool
1843> extend HelperMethods
1844>
1845> tool_name "get_forecast"
1846> description "Get weather forecast for a location"
1847> input_schema(
1848> properties: {
1849> latitude: {
1850> type: "number",
1851> description: "Latitude of the location"
1852> },
1853> longitude: {
1854> type: "number",
1855> description: "Longitude of the location"
1856> }
1857> },
1858> required: ["latitude", "longitude"]
1859> )
1860>
1861> def self.call(latitude:, longitude:)
1862> # First get the forecast grid endpoint.
1863> points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1864> points_data = make_nws_request(points_url)
1865>
1866> # Get the forecast URL from the points response.
1867> forecast_url = points_data["properties"]["forecast"]
1868> forecast_data = make_nws_request(forecast_url)
1869>
1870> # Format the periods into a readable forecast.
1871> periods = forecast_data["properties"]["periods"]
1872> forecasts = periods.first(5).map do |period|
1873> <<~FORECAST
1874> #{period["name"]}:
1875> Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1876> Wind: #{period["windSpeed"]} #{period["windDirection"]}
1877> Forecast: #{period["detailedForecast"]}
1878> FORECAST
1879> end
1880>
1881> MCP::Tool::Response.new([{
1882> type: "text",
1883> text: forecasts.join("\n---\n")
1884> }])
1885> end
1886> end
1887> ```
1888>
1889> ### Running the server
1890>
1891> Finally, initialize and run the server:
1892>
1893> ```ruby theme={null}
1894> server = MCP::Server.new(
1895> name: "weather",
1896> version: "1.0.0",
1897> tools: [GetAlerts, GetForecast]
1898> )
1899>
1900> transport = MCP::Server::Transports::StdioTransport.new(server)
1901> transport.open
1902> ```
1903>
1904> Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905>
1906> Let's now test your server from an existing MCP host, Claude for Desktop.
1907>
1908> ## Testing your server with Claude for Desktop
1909>
1910> > [!NOTE]
1911> >
1912> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913>
1914> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1915>
1916> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1917>
1918> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919>
1920> ```bash macOS/Linux theme={null}
1921> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922> ```
1923>
1924> ```powershell Windows theme={null}
1925> code $env:AppData\Claude\claude_desktop_config.json
1926> ```
1927>
1928> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1929>
1930> In this case, we'll add our single weather server like so:
1931>
1932> ```json macOS/Linux theme={null}
1933> {
1934> "mcpServers": {
1935> "weather": {
1936> "command": "bundle",
1937> "args": ["exec", "ruby", "weather.rb"],
1938> "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1939> }
1940> }
1941> }
1942> ```
1943>
1944> ```json Windows theme={null}
1945> {
1946> "mcpServers": {
1947> "weather": {
1948> "command": "bundle",
1949> "args": ["exec", "ruby", "weather.rb"],
1950> "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1951> }
1952> }
1953> }
1954> ```
1955>
1956> > [!NOTE]
1957> >
1958> > Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1959>
1960> This tells Claude for Desktop:
1961>
1962> 1. There's an MCP server named "weather"
1963> 2. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964>
1965> Save the file, and restart **Claude for Desktop**.
1966 
1967> [tab] Rust
1968>
1969> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1970>
1971> ### Prerequisite knowledge
1972>
1973> This quickstart assumes you have familiarity with:
1974>
1975> * Rust programming language
1976> * Async/await in Rust
1977> * LLMs like Claude
1978>
1979> ### Logging in MCP Servers
1980>
1981> When implementing MCP servers, be careful about how you handle logging:
1982>
1983> **For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1984>
1985> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1986>
1987> ### Best Practices
1988>
1989> * Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1990> * Configure your logging framework to avoid stdout output.
1991>
1992> ### Quick Examples
1993>
1994> ```rust theme={null}
1995> // ❌ Bad (STDIO)
1996> println!("Processing request");
1997>
1998> // ✅ Good (STDIO)
1999> eprintln!("Processing request"); // writes to stderr
2000> ```
2001>
2002> ### System requirements
2003>
2004> * Rust 1.70 or higher installed.
2005> * Cargo (comes with Rust installation).
2006>
2007> ### Set up your environment
2008>
2009> First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2010>
2011> ```bash macOS/Linux theme={null}
2012> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2013> ```
2014>
2015> ```powershell Windows theme={null}
2016> # Download and run rustup-init.exe from https://rustup.rs/
2017> ```
2018>
2019> Verify your Rust installation:
2020>
2021> ```bash theme={null}
2022> rustc --version
2023> cargo --version
2024> ```
2025>
2026> Now, let's create and set up our project:
2027>
2028> ```bash macOS/Linux theme={null}
2029> # Create a new Rust project
2030> cargo new weather
2031> cd weather
2032> ```
2033>
2034> ```powershell Windows theme={null}
2035> # Create a new Rust project
2036> cargo new weather
2037> cd weather
2038> ```
2039>
2040> Update your `Cargo.toml` to add the required dependencies:
2041>
2042> ```toml Cargo.toml theme={null}
2043> [package]
2044> name = "weather"
2045> version = "0.1.0"
2046> edition = "2024"
2047>
2048> [dependencies]
2049> rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050> tokio = { version = "1.46", features = ["full"] }
2051> reqwest = { version = "0.12", features = ["json"] }
2052> serde = { version = "1.0", features = ["derive"] }
2053> serde_json = "1.0"
2054> anyhow = "1.0"
2055> tracing = "0.1"
2056> tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057> ```
2058>
2059> Now let's dive into building your server.
2060>
2061> ## Building your server
2062>
2063> ### Importing packages and constants
2064>
2065> Open `src/main.rs` and add these imports and constants at the top:
2066>
2067> ```rust theme={null}
2068> use anyhow::Result;
2069> use rmcp::{
2070> ServerHandler, ServiceExt,
2071> handler::server::{router::tool::ToolRouter, tool::Parameters},
2072> model::*,
2073> schemars, tool, tool_handler, tool_router,
2074> };
2075> use serde::Deserialize;
2076> use serde::de::DeserializeOwned;
2077>
2078> const NWS_API_BASE: &str = "https://api.weather.gov";
2079> const USER_AGENT: &str = "weather-app/1.0";
2080> ```
2081>
2082> The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2083>
2084> ### Data structures
2085>
2086> Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087>
2088> ```rust theme={null}
2089> #[derive(Debug, Deserialize)]
2090> struct AlertsResponse {
2091> features: Vec<AlertFeature>,
2092> }
2093>
2094> #[derive(Debug, Deserialize)]
2095> struct AlertFeature {
2096> properties: AlertProperties,
2097> }
2098>
2099> #[derive(Debug, Deserialize)]
2100> struct AlertProperties {
2101> event: Option<String>,
2102> #[serde(rename = "areaDesc")]
2103> area_desc: Option<String>,
2104> severity: Option<String>,
2105> description: Option<String>,
2106> instruction: Option<String>,
2107> }
2108>
2109> #[derive(Debug, Deserialize)]
2110> struct PointsResponse {
2111> properties: PointsProperties,
2112> }
2113>
2114> #[derive(Debug, Deserialize)]
2115> struct PointsProperties {
2116> forecast: String,
2117> }
2118>
2119> #[derive(Debug, Deserialize)]
2120> struct ForecastResponse {
2121> properties: ForecastProperties,
2122> }
2123>
2124> #[derive(Debug, Deserialize)]
2125> struct ForecastProperties {
2126> periods: Vec<ForecastPeriod>,
2127> }
2128>
2129> #[derive(Debug, Deserialize)]
2130> struct ForecastPeriod {
2131> name: String,
2132> temperature: i32,
2133> #[serde(rename = "temperatureUnit")]
2134> temperature_unit: String,
2135> #[serde(rename = "windSpeed")]
2136> wind_speed: String,
2137> #[serde(rename = "windDirection")]
2138> wind_direction: String,
2139> #[serde(rename = "detailedForecast")]
2140> detailed_forecast: String,
2141> }
2142> ```
2143>
2144> Now define the request types that MCP clients will send:
2145>
2146> ```rust theme={null}
2147> #[derive(serde::Deserialize, schemars::JsonSchema)]
2148> pub struct MCPForecastRequest {
2149> latitude: f32,
2150> longitude: f32,
2151> }
2152>
2153> #[derive(serde::Deserialize, schemars::JsonSchema)]
2154> pub struct MCPAlertRequest {
2155> state: String,
2156> }
2157> ```
2158>
2159> ### Helper functions
2160>
2161> Add helper functions for making API requests and formatting responses:
2162>
2163> ```rust theme={null}
2164> async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2165> let client = reqwest::Client::new();
2166> let rsp = client
2167> .get(url)
2168> .header(reqwest::header::USER_AGENT, USER_AGENT)
2169> .header(reqwest::header::ACCEPT, "application/geo+json")
2170> .send()
2171> .await?
2172> .error_for_status()?;
2173> Ok(rsp.json::<T>().await?)
2174> }
2175>
2176> fn format_alert(feature: &AlertFeature) -> String {
2177> let props = &feature.properties;
2178> format!(
2179> "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2180> props.event.as_deref().unwrap_or("Unknown"),
2181> props.area_desc.as_deref().unwrap_or("Unknown"),
2182> props.severity.as_deref().unwrap_or("Unknown"),
2183> props
2184> .description
2185> .as_deref()
2186> .unwrap_or("No description available"),
2187> props
2188> .instruction
2189> .as_deref()
2190> .unwrap_or("No specific instructions provided")
2191> )
2192> }
2193>
2194> fn format_period(period: &ForecastPeriod) -> String {
2195> format!(
2196> "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2197> period.name,
2198> period.temperature,
2199> period.temperature_unit,
2200> period.wind_speed,
2201> period.wind_direction,
2202> period.detailed_forecast
2203> )
2204> }
2205> ```
2206>
2207> ### Implementing the Weather server and tools
2208>
2209> Now let's implement the main Weather server struct with the tool handlers:
2210>
2211> ```rust theme={null}
2212> pub struct Weather {
2213> tool_router: ToolRouter<Weather>,
2214> }
2215>
2216> #[tool_router]
2217> impl Weather {
2218> fn new() -> Self {
2219> Self {
2220> tool_router: Self::tool_router(),
2221> }
2222> }
2223>
2224> #[tool(description = "Get weather alerts for a US state.")]
2225> async fn get_alerts(
2226> &self,
2227> Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2228> ) -> String {
2229> let url = format!(
2230> "{}/alerts/active/area/{}",
2231> NWS_API_BASE,
2232> state.to_uppercase()
2233> );
2234>
2235> match make_nws_request::<AlertsResponse>(&url).await {
2236> Ok(data) => {
2237> if data.features.is_empty() {
2238> "No active alerts for this state.".to_string()
2239> } else {
2240> data.features
2241> .iter()
2242> .map(format_alert)
2243> .collect::<Vec<_>>()
2244> .join("\n---\n")
2245> }
2246> }
2247> Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2248> }
2249> }
2250>
2251> #[tool(description = "Get weather forecast for a location.")]
2252> async fn get_forecast(
2253> &self,
2254> Parameters(MCPForecastRequest {
2255> latitude,
2256> longitude,
2257> }): Parameters<MCPForecastRequest>,
2258> ) -> String {
2259> let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2260> let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2261> return "Unable to fetch forecast data for this location.".to_string();
2262> };
2263>
2264> let forecast_url = points_data.properties.forecast;
2265>
2266> let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2267> return "Unable to fetch forecast data for this location.".to_string();
2268> };
2269>
2270> let periods = &forecast_data.properties.periods;
2271> let forecast_summary: String = periods
2272> .iter()
2273> .take(5) // Next 5 periods only
2274> .map(format_period)
2275> .collect::<Vec<String>>()
2276> .join("\n---\n");
2277> forecast_summary
2278> }
2279> }
2280> ```
2281>
2282> The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283>
2284> ### Implementing the ServerHandler
2285>
2286> Implement the `ServerHandler` trait to define server capabilities:
2287>
2288> ```rust theme={null}
2289> #[tool_handler]
2290> impl ServerHandler for Weather {
2291> fn get_info(&self) -> ServerInfo {
2292> ServerInfo {
2293> capabilities: ServerCapabilities::builder().enable_tools().build(),
2294> ..Default::default()
2295> }
2296> }
2297> }
2298> ```
2299>
2300> ### Running the server
2301>
2302> Finally, implement the main function to run the server with stdio transport:
2303>
2304> ```rust theme={null}
2305> #[tokio::main]
2306> async fn main() -> Result<()> {
2307> let transport = (tokio::io::stdin(), tokio::io::stdout());
2308> let service = Weather::new().serve(transport).await?;
2309> service.waiting().await?;
2310> Ok(())
2311> }
2312> ```
2313>
2314> Build your server with:
2315>
2316> ```bash theme={null}
2317> cargo build --release
2318> ```
2319>
2320> The compiled binary will be in `target/release/weather`.
2321>
2322> Let's now test your server from an existing MCP host, Claude for Desktop.
2323>
2324> ## Testing your server with Claude for Desktop
2325>
2326> > [!NOTE]
2327> >
2328> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329>
2330> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2331>
2332> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2333>
2334> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335>
2336> ```bash macOS/Linux theme={null}
2337> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338> ```
2339>
2340> ```powershell Windows theme={null}
2341> code $env:AppData\Claude\claude_desktop_config.json
2342> ```
2343>
2344> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2345>
2346> In this case, we'll add our single weather server like so:
2347>
2348> ```json macOS/Linux theme={null}
2349> {
2350> "mcpServers": {
2351> "weather": {
2352> "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2353> }
2354> }
2355> }
2356> ```
2357>
2358> ```json Windows theme={null}
2359> {
2360> "mcpServers": {
2361> "weather": {
2362> "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2363> }
2364> }
2365> }
2366> ```
2367>
2368> > [!NOTE]
2369> >
2370> > Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2371>
2372> This tells Claude for Desktop:
2373>
2374> 1. There's an MCP server named "weather"
2375> 2. Launch it by running the compiled binary at the specified path
2376>
2377> Save the file, and restart **Claude for Desktop**.
2378 
2379> [tab] Go
2380>
2381> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2382>
2383> ### Prerequisite knowledge
2384>
2385> This quickstart assumes you have familiarity with:
2386>
2387> * Go
2388> * LLMs like Claude
2389>
2390> ### Logging in MCP Servers
2391>
2392> When implementing MCP servers, be careful about how you handle logging:
2393>
2394> **For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2395>
2396> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2397>
2398> ### Best Practices
2399>
2400> * Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2401> * Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2402>
2403> ### Quick Examples
2404>
2405> ```go theme={null}
2406> // ❌ Bad (STDIO)
2407> fmt.Println("Processing request")
2408>
2409> // ✅ Good (STDIO)
2410> log.Println("Processing request") // defaults to stderr
2411>
2412> // ✅ Good (STDIO)
2413> fmt.Fprintln(os.Stderr, "Processing request")
2414> ```
2415>
2416> ### System requirements
2417>
2418> * Go 1.24 or higher installed.
2419>
2420> ### Set up your environment
2421>
2422> First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423>
2424> Verify your Go installation:
2425>
2426> ```bash theme={null}
2427> go version
2428> ```
2429>
2430> Now, let's create and set up our project:
2431>
2432> ```bash macOS/Linux theme={null}
2433> # Create a new directory for our project
2434> mkdir weather
2435> cd weather
2436>
2437> # Initialize Go module
2438> go mod init weather
2439>
2440> # Install dependencies
2441> go get github.com/modelcontextprotocol/go-sdk/mcp
2442>
2443> # Create our server file
2444> touch main.go
2445> ```
2446>
2447> ```powershell Windows theme={null}
2448> # Create a new directory for our project
2449> md weather
2450> cd weather
2451>
2452> # Initialize Go module
2453> go mod init weather
2454>
2455> # Install dependencies
2456> go get github.com/modelcontextprotocol/go-sdk/mcp
2457>
2458> # Create our server file
2459> new-item main.go
2460> ```
2461>
2462> Now let's dive into building your server.
2463>
2464> ## Building your server
2465>
2466> ### Importing packages and constants
2467>
2468> Add these to the top of your `main.go`:
2469>
2470> ```go theme={null}
2471> package main
2472>
2473> import (
2474> "cmp"
2475> "context"
2476> "encoding/json"
2477> "fmt"
2478> "io"
2479> "log"
2480> "net/http"
2481> "strings"
2482>
2483> "github.com/modelcontextprotocol/go-sdk/mcp"
2484> )
2485>
2486> const (
2487> NWSAPIBase = "https://api.weather.gov"
2488> UserAgent = "weather-app/1.0"
2489> )
2490> ```
2491>
2492> ### Data structures
2493>
2494> Next, let's define the data structures used by our tools:
2495>
2496> ```go theme={null}
2497> type PointsResponse struct {
2498> Properties struct {
2499> Forecast string `json:"forecast"`
2500> } `json:"properties"`
2501> }
2502>
2503> type ForecastResponse struct {
2504> Properties struct {
2505> Periods []ForecastPeriod `json:"periods"`
2506> } `json:"properties"`
2507> }
2508>
2509> type ForecastPeriod struct {
2510> Name string `json:"name"`
2511> Temperature int `json:"temperature"`
2512> TemperatureUnit string `json:"temperatureUnit"`
2513> WindSpeed string `json:"windSpeed"`
2514> WindDirection string `json:"windDirection"`
2515> DetailedForecast string `json:"detailedForecast"`
2516> }
2517>
2518> type AlertsResponse struct {
2519> Features []AlertFeature `json:"features"`
2520> }
2521>
2522> type AlertFeature struct {
2523> Properties AlertProperties `json:"properties"`
2524> }
2525>
2526> type AlertProperties struct {
2527> Event string `json:"event"`
2528> AreaDesc string `json:"areaDesc"`
2529> Severity string `json:"severity"`
2530> Description string `json:"description"`
2531> Instruction string `json:"instruction"`
2532> }
2533>
2534> type ForecastInput struct {
2535> Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536> Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537> }
2538>
2539> type AlertsInput struct {
2540> State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541> }
2542> ```
2543>
2544> ### Helper functions
2545>
2546> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547>
2548> ```go theme={null}
2549> func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2550> req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2551> if err != nil {
2552> return nil, fmt.Errorf("failed to create request: %w", err)
2553> }
2554>
2555> req.Header.Set("User-Agent", UserAgent)
2556> req.Header.Set("Accept", "application/geo+json")
2557>
2558> client := http.DefaultClient
2559> resp, err := client.Do(req)
2560> if err != nil {
2561> return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2562> }
2563> defer resp.Body.Close()
2564>
2565> if resp.StatusCode != http.StatusOK {
2566> body, _ := io.ReadAll(resp.Body)
2567> return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2568> }
2569>
2570> var result T
2571> if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2572> return nil, fmt.Errorf("failed to decode response: %w", err)
2573> }
2574>
2575> return &result, nil
2576> }
2577>
2578> func formatAlert(alert AlertFeature) string {
2579> props := alert.Properties
2580> event := cmp.Or(props.Event, "Unknown")
2581> areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2582> severity := cmp.Or(props.Severity, "Unknown")
2583> description := cmp.Or(props.Description, "No description available")
2584> instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2585>
2586> return fmt.Sprintf(`
2587> Event: %s
2588> Area: %s
2589> Severity: %s
2590> Description: %s
2591> Instructions: %s
2592> `, event, areaDesc, severity, description, instruction)
2593> }
2594>
2595> func formatPeriod(period ForecastPeriod) string {
2596> return fmt.Sprintf(`
2597> %s:
2598> Temperature: %d°%s
2599> Wind: %s %s
2600> Forecast: %s
2601> `, period.Name, period.Temperature, period.TemperatureUnit,
2602> period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603> }
2604> ```
2605>
2606> ### Implementing tool execution
2607>
2608> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609>
2610> ```go theme={null}
2611> func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2612> *mcp.CallToolResult, any, error,
2613> ) {
2614> // Get points data
2615> pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2616> pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2617> if err != nil {
2618> return &mcp.CallToolResult{
2619> Content: []mcp.Content{
2620> &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2621> },
2622> }, nil, nil
2623> }
2624>
2625> // Get forecast data
2626> forecastURL := pointsData.Properties.Forecast
2627> if forecastURL == "" {
2628> return &mcp.CallToolResult{
2629> Content: []mcp.Content{
2630> &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2631> },
2632> }, nil, nil
2633> }
2634>
2635> forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2636> if err != nil {
2637> return &mcp.CallToolResult{
2638> Content: []mcp.Content{
2639> &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2640> },
2641> }, nil, nil
2642> }
2643>
2644> // Format the periods
2645> periods := forecastData.Properties.Periods
2646> if len(periods) == 0 {
2647> return &mcp.CallToolResult{
2648> Content: []mcp.Content{
2649> &mcp.TextContent{Text: "No forecast periods available."},
2650> },
2651> }, nil, nil
2652> }
2653>
2654> // Show next 5 periods
2655> var forecasts []string
2656> for i := range min(5, len(periods)) {
2657> forecasts = append(forecasts, formatPeriod(periods[i]))
2658> }
2659>
2660> result := strings.Join(forecasts, "\n---\n")
2661>
2662> return &mcp.CallToolResult{
2663> Content: []mcp.Content{
2664> &mcp.TextContent{Text: result},
2665> },
2666> }, nil, nil
2667> }
2668>
2669> func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2670> *mcp.CallToolResult, any, error,
2671> ) {
2672> // Build alerts URL
2673> stateCode := strings.ToUpper(input.State)
2674> alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2675>
2676> alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2677> if err != nil {
2678> return &mcp.CallToolResult{
2679> Content: []mcp.Content{
2680> &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2681> },
2682> }, nil, nil
2683> }
2684>
2685> // Check if there are any alerts
2686> if len(alertsData.Features) == 0 {
2687> return &mcp.CallToolResult{
2688> Content: []mcp.Content{
2689> &mcp.TextContent{Text: "No active alerts for this state."},
2690> },
2691> }, nil, nil
2692> }
2693>
2694> // Format alerts
2695> var alerts []string
2696> for _, feature := range alertsData.Features {
2697> alerts = append(alerts, formatAlert(feature))
2698> }
2699>
2700> result := strings.Join(alerts, "\n---\n")
2701>
2702> return &mcp.CallToolResult{
2703> Content: []mcp.Content{
2704> &mcp.TextContent{Text: result},
2705> },
2706> }, nil, nil
2707> }
2708> ```
2709>
2710> ### Running the server
2711>
2712> Finally, implement the main function to run the server:
2713>
2714> ```go theme={null}
2715> func main() {
2716> // Create MCP server
2717> server := mcp.NewServer(&mcp.Implementation{
2718> Name: "weather",
2719> Version: "1.0.0",
2720> }, nil)
2721>
2722> // Add get_forecast tool
2723> mcp.AddTool(server, &mcp.Tool{
2724> Name: "get_forecast",
2725> Description: "Get weather forecast for a location",
2726> }, getForecast)
2727>
2728> // Add get_alerts tool
2729> mcp.AddTool(server, &mcp.Tool{
2730> Name: "get_alerts",
2731> Description: "Get weather alerts for a US state",
2732> }, getAlerts)
2733>
2734> // Run server on stdio transport
2735> if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2736> log.Fatal(err)
2737> }
2738> }
2739> ```
2740>
2741> Build your server with:
2742>
2743> ```bash theme={null}
2744> go build -o weather .
2745> ```
2746>
2747> The compiled binary will be in `./weather`.
2748>
2749> Let's now test your server from an existing MCP host, Claude for Desktop.
2750>
2751> ## Testing your server with Claude for Desktop
2752>
2753> > [!NOTE]
2754> >
2755> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756>
2757> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2758>
2759> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2760>
2761> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762>
2763> ```bash macOS/Linux theme={null}
2764> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765> ```
2766>
2767> ```powershell Windows theme={null}
2768> code $env:AppData\Claude\claude_desktop_config.json
2769> ```
2770>
2771> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2772>
2773> In this case, we'll add our single weather server like so:
2774>
2775> ```json macOS/Linux theme={null}
2776> {
2777> "mcpServers": {
2778> "weather": {
2779> "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2780> }
2781> }
2782> }
2783> ```
2784>
2785> ```json Windows theme={null}
2786> {
2787> "mcpServers": {
2788> "weather": {
2789> "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2790> }
2791> }
2792> }
2793> ```
2794>
2795> > [!NOTE]
2796> >
2797> > Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2798>
2799> This tells Claude for Desktop:
2800>
2801> 1. There's an MCP server named "weather"
2802> 2. Launch it by running the compiled binary at the specified path
2803>
2804> Save the file, and restart **Claude for Desktop**.
28052805 
28062806### Test with commands
28072807 
from line 2841
28412841 
28422842## Troubleshooting
28432843 
2844> **Claude for Desktop Integration Issues**
2844> [accordion] Claude for Desktop Integration Issues
28452845>
28462846> **Getting logs from Claude for Desktop**
28472847>
from line 2886
28862886>
28872887> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2025-03-26/tools/debugging.md) for better debugging tools and more detailed guidance.
28882888 
2889> **Weather API Issues**
2889> [accordion] Weather API Issues
28902890>
28912891> **Error: Failed to retrieve grid point data**
28922892>
from line 2912
29122912 
29132913## Next steps
29142914 
2915> **[Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md)**
2915> [card] [Building a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md)
29162916>
29172917> Learn how to build your own MCP client that can connect to your server
29182918 
2919> **[Example servers](https://modelcontextprotocol.io/examples.md)**
2919> [card] [Example servers](https://modelcontextprotocol.io/examples.md)
29202920>
29212921> Check out our gallery of official MCP servers and implementations
29222922 
2923> **[Debugging Guide](https://modelcontextprotocol.io/docs/2025-03-26/tools/debugging.md)**
2923> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2025-03-26/tools/debugging.md)
29242924>
29252925> Learn how to effectively debug MCP servers and integrations
29262926 
2927> **[Build with Agent Skills](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-with-agent-skills.md)**
2927> [card] [Build with Agent Skills](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-with-agent-skills.md)
29282928>
29292929> Use agent skills to guide AI coding assistants through server design
29302930 

docs/2025-03-26/develop/build-with-agent-skills Changed · +3 / -3 lines

from line 83
8383Once your agent scaffolds the server, iterate on tool descriptions and error
8484handling, then test and ship:
8585 
86> **[MCP Inspector](https://modelcontextprotocol.io/docs/2025-03-26/tools/inspector.md)**
86> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2025-03-26/tools/inspector.md)
8787>
8888> Test your server's tools, resources, and prompts interactively
8989 
90> **[Connect to a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-local-servers.md)**
90> [card] [Connect to a client](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-local-servers.md)
9191>
9292> Wire your server into an MCP client via local or remote configuration
9393 
94> **[Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)**
94> [card] [Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)
9595>
9696> Make your server discoverable in the MCP Registry
9797 

docs/2025-03-26/develop/connect-local-servers Changed · +9 / -9 lines

from line 156
156156 
157157If you encounter issues setting up or using the Filesystem Server, these solutions address common problems:
158158 
159> **Server not showing up in Claude / hammer icon missing**
159> [accordion] Server not showing up in Claude / hammer icon missing
160160>
161161> 1. Restart Claude Desktop completely
162162> 2. Check your `claude_desktop_config.json` file syntax
from line 172
172172> npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
173173> ```
174174 
175> **Getting logs from Claude Desktop**
175> [accordion] Getting logs from Claude Desktop
176176>
177177> Claude.app logging related to MCP is written to log files in:
178178>
from line 194
194194> type "%APPDATA%\Claude\logs\mcp*.log"
195195> ```
196196 
197> **Tool calls failing silently**
197> [accordion] Tool calls failing silently
198198>
199199> If Claude attempts to use the tools but they fail:
200200>
from line 202
202202> 2. Verify your server builds and runs without errors
203203> 3. Try restarting Claude Desktop
204204 
205> **None of this is working. What do I do?**
205> [accordion] None of this is working. What do I do?
206206>
207207> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2025-03-26/tools/debugging.md) for better debugging tools and more detailed guidance.
208208 
209> **ENOENT error and `$\{APPDATA\}` in paths on Windows**
209> [accordion] ENOENT error and `$\{APPDATA\}` in paths on Windows
210210>
211211> If your configured server fails to load, and you see within its logs an error referring to `${APPDATA}` within a path, you may need to add the expanded value of `%APPDATA%` to your `env` key in `claude_desktop_config.json`:
212212>
from line 239
239239 
240240Now that you've successfully connected Claude Desktop to a local MCP server, explore these options to expand your setup:
241241 
242> **[Explore other servers](https://github.com/modelcontextprotocol/servers)**
242> [card] [Explore other servers](https://github.com/modelcontextprotocol/servers)
243243>
244244> Browse our collection of official and community-created MCP servers for
245245> additional capabilities
246246 
247> **[Build your own server](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)**
247> [card] [Build your own server](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)
248248>
249249> Create custom MCP servers tailored to your specific workflows and
250250> integrations
251251 
252> **[Connect to remote servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-remote-servers.md)**
252> [card] [Connect to remote servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-remote-servers.md)
253253>
254254> Learn how to connect Claude to remote MCP servers for cloud-based tools and
255255> services
256256 
257> **[Understand the protocol](https://modelcontextprotocol.io/docs/2025-03-26/learn/architecture.md)**
257> [card] [Understand the protocol](https://modelcontextprotocol.io/docs/2025-03-26/learn/architecture.md)
258258>
259259> Dive deeper into how MCP works and its architecture
260260 

docs/2025-03-26/develop/connect-remote-servers Changed · +4 / -4 lines

from line 81
8181 
8282Now that you've connected Claude to a remote MCP server, you can explore its capabilities in your conversations. Try using the connected tools to automate tasks, access external data, or integrate with your existing workflows.
8383 
84> **[Build your own remote server](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)**
84> [card] [Build your own remote server](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)
8585>
8686> Create custom remote MCP servers to integrate with proprietary tools and
8787> services
8888 
89> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
89> [card] [Explore available servers](https://github.com/modelcontextprotocol/servers)
9090>
9191> Browse our collection of official and community-created MCP servers
9292 
93> **[Connect local servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-local-servers.md)**
93> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-local-servers.md)
9494>
9595> Learn how to connect Claude Desktop to local MCP servers for direct system
9696> access
9797 
98> **[Understand the architecture](https://modelcontextprotocol.io/docs/2025-03-26/learn/architecture.md)**
98> [card] [Understand the architecture](https://modelcontextprotocol.io/docs/2025-03-26/learn/architecture.md)
9999>
100100> Dive deeper into how MCP works and its architecture
101101 

docs/2025-03-26/getting-started/intro Changed · +6 / -6 lines

from line 29
2929 
3030## Start Building
3131 
32> **[Build servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)**
32> [card] [Build servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)
3333>
3434> Create MCP servers to expose your data and tools
3535 
36> **[Build clients](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md)**
36> [card] [Build clients](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-client.md)
3737>
3838> Develop applications that connect to MCP servers
3939 
40> **[Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)**
40> [card] [Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)
4141>
4242> Build interactive apps that run inside AI clients
4343 
4444## Learn more
4545 
46> **[Architecture](https://modelcontextprotocol.io/docs/2025-03-26/learn/architecture.md)**
46> [card] [Architecture](https://modelcontextprotocol.io/docs/2025-03-26/learn/architecture.md)
4747>
4848> Learn the core concepts and architecture of MCP
4949 
50> **[Security](https://modelcontextprotocol.io/docs/2025-03-26/tutorials/security/security_best_practices.md)**
50> [card] [Security](https://modelcontextprotocol.io/docs/2025-03-26/tutorials/security/security_best_practices.md)
5151>
5252> Understand the security considerations and best practices for MCP
5353 
5454## Community
5555 
56> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
56> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
5757>
5858> Learn how to get involved and contribute to MCP
5959 

docs/2025-03-26/tools/debugging Changed · +3 / -3 lines

from line 326
326326 
327327## Next steps
328328 
329> **[MCP Inspector](https://modelcontextprotocol.io/docs/2025-03-26/tools/inspector.md)**
329> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2025-03-26/tools/inspector.md)
330330>
331331> Learn to use the MCP Inspector
332332 
333> **[Build an MCP server](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)**
333> [card] [Build an MCP server](https://modelcontextprotocol.io/docs/2025-03-26/develop/build-server.md)
334334>
335335> Walk through building a server from scratch
336336 
337> **[Connect local servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-local-servers.md)**
337> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2025-03-26/develop/connect-local-servers.md)
338338>
339339> Full claude\_desktop\_config.json reference and troubleshooting
340340 

docs/2025-03-26/tools/inspector Changed · +31 / -31 lines

from line 22
2222 
2323A common way to start server packages from [npm](https://npmjs.com) or [PyPI](https://pypi.org).
2424 
25**npm package**
25> [tab] npm package
26>
27> ```bash theme={null}
28> npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29> # For example
30> npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31> ```
2632 
27```bash theme={null}
28npx -y @modelcontextprotocol/inspector npx <package-name> <args>
29# For example
30npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /Users/username/Desktop
31```
33> [tab] PyPI package
34>
35> ```bash theme={null}
36> npx @modelcontextprotocol/inspector uvx <package-name> <args>
37> # For example
38> npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39> ```
3240 
33**PyPI package**
34 
35```bash theme={null}
36npx @modelcontextprotocol/inspector uvx <package-name> <args>
37# For example
38npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
39```
40 
4141#### Inspecting locally developed servers
4242 
4343To inspect servers locally developed or downloaded as a repository, the most common
4444way is:
4545 
46**TypeScript**
46> [tab] TypeScript
47>
48> ```bash theme={null}
49> npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50> ```
4751 
48```bash theme={null}
49npx @modelcontextprotocol/inspector node path/to/server/index.js args...
50```
52> [tab] Python
53>
54> ```bash theme={null}
55> npx @modelcontextprotocol/inspector \
56> uv \
57> --directory path/to/server \
58> run \
59> package-name \
60> args...
61> ```
5162 
52**Python**
53 
54```bash theme={null}
55npx @modelcontextprotocol/inspector \
56 uv \
57 --directory path/to/server \
58 run \
59 package-name \
60 args...
61```
62 
6363Please carefully read any attached README for the most accurate instructions.
6464 
6565## Feature overview
from line 125
125125 
126126## Next steps
127127 
128> **[Inspector Repository](https://github.com/modelcontextprotocol/inspector)**
128> [card] [Inspector Repository](https://github.com/modelcontextprotocol/inspector)
129129>
130130> Check out the MCP Inspector source code
131131 
132> **[Debugging Guide](https://modelcontextprotocol.io/docs/2025-03-26/tools/debugging.md)**
132> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2025-03-26/tools/debugging.md)
133133>
134134> Learn about broader debugging strategies
135135 

docs/2025-03-26/tutorials/security/authorization Changed · +727 / -727 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 246
246246 
247247For our testing purposes, we will create an extremely simple MCP server that exposes two tools - one for addition and another for multiplication. The server will require authorization to access these.
248248 
249**TypeScript**
250 
251You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252 
253Prior to running the code below, ensure that you have a `.env` file with the following content:
254 
255```env theme={null}
256# Server host/port
257HOST=localhost
258PORT=3000
259 
260# Auth server location
261AUTH_HOST=localhost
262AUTH_PORT=8080
263AUTH_REALM=master
264 
265# Keycloak OAuth client credentials
266OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268```
269 
270`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271 
272In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273 
274```typescript theme={null}
275import "dotenv/config";
276import express from "express";
277import { randomUUID } from "node:crypto";
278import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281import { z } from "zod";
282import cors from "cors";
283import {
284 mcpAuthMetadataRouter,
285 getOAuthProtectedResourceMetadataUrl,
286} from "@modelcontextprotocol/sdk/server/auth/router.js";
287import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290const CONFIG = {
291 host: process.env.HOST || "localhost",
292 port: Number(process.env.PORT) || 3000,
293 auth: {
294 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295 port: Number(process.env.AUTH_PORT) || 8080,
296 realm: process.env.AUTH_REALM || "master",
297 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299 },
300};
301 
302function createOAuthUrls() {
303 const authBaseUrl = new URL(
304 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305 );
306 return {
307 issuer: authBaseUrl.toString(),
308 introspection_endpoint: new URL(
309 "protocol/openid-connect/token/introspect",
310 authBaseUrl,
311 ).toString(),
312 authorization_endpoint: new URL(
313 "protocol/openid-connect/auth",
314 authBaseUrl,
315 ).toString(),
316 token_endpoint: new URL(
317 "protocol/openid-connect/token",
318 authBaseUrl,
319 ).toString(),
320 };
321}
322 
323function createRequestLogger() {
324 return (req: any, res: any, next: any) => {
325 const start = Date.now();
326 res.on("finish", () => {
327 const ms = Date.now() - start;
328 console.log(
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
330 );
331 });
332 next();
333 };
334}
335 
336const app = express();
337 
338app.use(
339 express.json({
340 verify: (req: any, _res, buf) => {
341 req.rawBody = buf?.toString() ?? "";
342 },
343 }),
344);
345 
346app.use(
347 cors({
348 origin: "*",
349 exposedHeaders: ["Mcp-Session-Id"],
350 }),
351);
352 
353app.use(createRequestLogger());
354 
355const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356const oauthUrls = createOAuthUrls();
357 
358const oauthMetadata: OAuthMetadata = {
359 ...oauthUrls,
360 response_types_supported: ["code"],
361};
362 
363const tokenVerifier = {
364 verifyAccessToken: async (token: string) => {
365 const endpoint = oauthMetadata.introspection_endpoint;
366 
367 if (!endpoint) {
368 console.error("[auth] no introspection endpoint in metadata");
369 throw new Error("No token verification endpoint available in metadata");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) =>
430 checkResourceAllowed({
431 requestedResource: a,
432 configuredResource: mcpServerUrl,
433 }),
434 );
435 if (!allowed) {
436 throw new Error(
437 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
438 );
439 }
440 
441 return {
442 token,
443 clientId: data.client_id,
444 scopes: data.scope ? data.scope.split(" ") : [],
445 expiresAt: data.exp,
446 };
447 },
448};
449app.use(
450 mcpAuthMetadataRouter({
451 oauthMetadata,
452 resourceServerUrl: mcpServerUrl,
453 scopesSupported: ["mcp:tools"],
454 resourceName: "MCP Demo Server",
455 }),
456);
457 
458const authMiddleware = requireBearerAuth({
459 verifier: tokenVerifier,
460 requiredScopes: [],
461 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462});
463 
464const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465 
466function createMcpServer() {
467 const server = new McpServer({
468 name: "example-server",
469 version: "1.0.0",
470 });
471 
472 server.registerTool(
473 "add",
474 {
475 title: "Addition Tool",
476 description: "Add two numbers together",
477 inputSchema: {
478 a: z.number().describe("First number to add"),
479 b: z.number().describe("Second number to add"),
480 },
481 },
482 async ({ a, b }) => ({
483 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
484 }),
485 );
486 
487 server.registerTool(
488 "multiply",
489 {
490 title: "Multiplication Tool",
491 description: "Multiply two numbers together",
492 inputSchema: {
493 x: z.number().describe("First number to multiply"),
494 y: z.number().describe("Second number to multiply"),
495 },
496 },
497 async ({ x, y }) => ({
498 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
499 }),
500 );
501 
502 return server;
503}
504 
505const mcpPostHandler = async (req: express.Request, res: express.Response) => {
506 const sessionId = req.headers["mcp-session-id"] as string | undefined;
507 let transport: StreamableHTTPServerTransport;
508 
509 if (sessionId && transports[sessionId]) {
510 transport = transports[sessionId];
511 } else if (!sessionId && isInitializeRequest(req.body)) {
512 transport = new StreamableHTTPServerTransport({
513 sessionIdGenerator: () => randomUUID(),
514 onsessioninitialized: (sessionId) => {
515 transports[sessionId] = transport;
516 },
517 });
518 
519 transport.onclose = () => {
520 if (transport.sessionId) {
521 delete transports[transport.sessionId];
522 }
523 };
524 
525 const server = createMcpServer();
526 await server.connect(transport);
527 } else {
528 res.status(400).json({
529 jsonrpc: "2.0",
530 error: {
531 code: -32000,
532 message: "Bad Request: No valid session ID provided",
533 },
534 id: null,
535 });
536 return;
537 }
538 
539 await transport.handleRequest(req, res, req.body);
540};
541 
542const handleSessionRequest = async (
543 req: express.Request,
544 res: express.Response,
545) => {
546 const sessionId = req.headers["mcp-session-id"] as string | undefined;
547 if (!sessionId || !transports[sessionId]) {
548 res.status(400).send("Invalid or missing session ID");
549 return;
550 }
551 
552 const transport = transports[sessionId];
553 await transport.handleRequest(req, res);
554};
555 
556app.post("/", authMiddleware, mcpPostHandler);
557app.get("/", authMiddleware, handleSessionRequest);
558app.delete("/", authMiddleware, handleSessionRequest);
559 
560app.listen(CONFIG.port, CONFIG.host, () => {
561 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
562 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
563 console.log(
564 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
565 );
566});
567```
568 
569When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570 
571For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573**Python**
574 
575You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576 
577To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
578 
579Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
580 
581```python theme={null}
582"""Configuration settings for the MCP auth server."""
583 
584import os
585from typing import Optional
586 
587 
588class Config:
589 """Configuration class that loads from environment variables with sensible defaults."""
590 
591 # Server settings
592 HOST: str = os.getenv("HOST", "localhost")
593 PORT: int = int(os.getenv("PORT", "3000"))
594 
595 # Auth server settings
596 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
597 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
598 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
599 
600 # OAuth client settings
601 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
602 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
603 
604 # Server settings
605 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
606 OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
607 TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
608 
609 @property
610 def server_url(self) -> str:
611 """Build the server URL."""
612 return f"http://{self.HOST}:{self.PORT}"
613 
614 @property
615 def auth_base_url(self) -> str:
616 """Build the auth server base URL."""
617 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
618 
619 def validate(self) -> None:
620 """Validate configuration."""
621 if self.TRANSPORT not in ["sse", "streamable-http"]:
622 raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
623 
624 
625# Global configuration instance
626config = Config()
627 
628```
629 
630The server implementation is as follows:
631 
632```python theme={null}
633import datetime
634import logging
635from typing import Any
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server.auth.settings import AuthSettings
640from mcp.server.fastmcp.server import FastMCP
641 
642from .config import config
643from .token_verifier import IntrospectionTokenVerifier
644 
645logger = logging.getLogger(__name__)
646 
647 
648def create_oauth_urls() -> dict[str, str]:
649 """Create OAuth URLs based on configuration (Keycloak-style)."""
650 from urllib.parse import urljoin
651 
652 auth_base_url = config.auth_base_url
653 
654 return {
655 "issuer": auth_base_url,
656 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
657 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
658 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
659 }
660 
661 
662def create_server() -> FastMCP:
663 """Create and configure the FastMCP server."""
664 
665 config.validate()
666 
667 oauth_urls = create_oauth_urls()
668 
669 token_verifier = IntrospectionTokenVerifier(
670 introspection_endpoint=oauth_urls["introspection_endpoint"],
671 server_url=config.server_url,
672 client_id=config.OAUTH_CLIENT_ID,
673 client_secret=config.OAUTH_CLIENT_SECRET,
674 )
675 
676 app = FastMCP(
677 name="MCP Resource Server",
678 instructions="Resource Server that validates tokens via Authorization Server introspection",
679 host=config.HOST,
680 port=config.PORT,
681 debug=True,
682 streamable_http_path="/",
683 token_verifier=token_verifier,
684 auth=AuthSettings(
685 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
686 required_scopes=[config.MCP_SCOPE],
687 resource_server_url=AnyHttpUrl(config.server_url),
688 ),
689 )
690 
691 @app.tool()
692 async def add_numbers(a: float, b: float) -> dict[str, Any]:
693 """
694 Add two numbers together.
695 This tool demonstrates basic arithmetic operations with OAuth authentication.
696 
697 Args:
698 a: The first number to add
699 b: The second number to add
700 """
701 result = a + b
702 return {
703 "operation": "addition",
704 "operand_a": a,
705 "operand_b": b,
706 "result": result,
707 "timestamp": datetime.datetime.now().isoformat()
708 }
709 
710 @app.tool()
711 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
712 """
713 Multiply two numbers together.
714 This tool demonstrates basic arithmetic operations with OAuth authentication.
715 
716 Args:
717 x: The first number to multiply
718 y: The second number to multiply
719 """
720 result = x * y
721 return {
722 "operation": "multiplication",
723 "operand_x": x,
724 "operand_y": y,
725 "result": result,
726 "timestamp": datetime.datetime.now().isoformat()
727 }
728 
729 return app
730 
731 
732def main() -> int:
733 """
734 Run the MCP Resource Server.
735 
736 This server:
737 - Provides RFC 9728 Protected Resource Metadata
738 - Validates tokens via Authorization Server introspection
739 - Serves MCP tools requiring authentication
740 
741 Configuration is loaded from config.py and environment variables.
742 """
743 logging.basicConfig(level=logging.INFO)
744 
745 try:
746 config.validate()
747 oauth_urls = create_oauth_urls()
748 
749 except ValueError as e:
750 logger.error("Configuration error: %s", e)
751 return 1
752 
753 try:
754 mcp_server = create_server()
755 
756 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
757 logger.info("Authorization Server: %s", oauth_urls["issuer"])
758 logger.info("Transport: %s", config.TRANSPORT)
759 
760 mcp_server.run(transport=config.TRANSPORT)
761 return 0
762 
763 except Exception:
764 logger.exception("Server error")
765 return 1
766 
767 
768if __name__ == "__main__":
769 exit(main())
770```
771 
772Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts
773 
774```python theme={null}
775"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776 
777import logging
778from typing import Any
779 
780from mcp.server.auth.provider import AccessToken, TokenVerifier
781from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782 
783logger = logging.getLogger(__name__)
784 
785 
786class IntrospectionTokenVerifier(TokenVerifier):
787 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
788 """
789 
790 def __init__(
791 self,
792 introspection_endpoint: str,
793 server_url: str,
794 client_id: str,
795 client_secret: str,
796 ):
797 self.introspection_endpoint = introspection_endpoint
798 self.server_url = server_url
799 self.client_id = client_id
800 self.client_secret = client_secret
801 self.resource_url = resource_url_from_server_url(server_url)
802 
803 async def verify_token(self, token: str) -> AccessToken | None:
804 """Verify token via introspection endpoint."""
805 import httpx
806 
807 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
808 return None
809 
810 timeout = httpx.Timeout(10.0, connect=5.0)
811 limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
812 
813 async with httpx.AsyncClient(
814 timeout=timeout,
815 limits=limits,
816 verify=True,
817 ) as client:
818 try:
819 form_data = {
820 "token": token,
821 "client_id": self.client_id,
822 "client_secret": self.client_secret,
823 }
824 headers = {"Content-Type": "application/x-www-form-urlencoded"}
825 
826 response = await client.post(
827 self.introspection_endpoint,
828 data=form_data,
829 headers=headers,
830 )
831 
832 if response.status_code != 200:
833 return None
834 
835 data = response.json()
836 if not data.get("active", False):
837 return None
838 
839 if not self._validate_resource(data):
840 return None
841 
842 return AccessToken(
843 token=token,
844 client_id=data.get("client_id", "unknown"),
845 scopes=data.get("scope", "").split() if data.get("scope") else [],
846 expires_at=data.get("exp"),
847 resource=data.get("aud"), # Include resource in token
848 )
849 
850 except Exception as e:
851 return None
852 
853 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
854 """Validate token was issued for this resource server.
855 
856 Rules:
857 - Reject if 'aud' missing.
858 - Accept if any audience entry matches the derived resource URL.
859 - Supports string or list forms per JWT spec.
860 """
861 if not self.server_url or not self.resource_url:
862 return False
863 
864 aud: list[str] | str | None = token_data.get("aud")
865 if isinstance(aud, list):
866 return any(self._is_valid_resource(a) for a in aud)
867 if isinstance(aud, str):
868 return self._is_valid_resource(aud)
869 return False
870 
871 def _is_valid_resource(self, resource: str) -> bool:
872 """Check if the given resource matches our server."""
873 return check_resource_allowed(self.resource_url, resource)
874```
875 
876For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878**C#**
879 
880You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881 
882To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
883 
884```csharp theme={null}
885using Microsoft.AspNetCore.Authentication.JwtBearer;
886using Microsoft.IdentityModel.Tokens;
887using ModelContextProtocol.AspNetCore.Authentication;
888using ProtectedMcpServer.Tools;
889using System.Security.Claims;
890 
891var builder = WebApplication.CreateBuilder(args);
892 
893var serverUrl = "http://localhost:3000/";
894var authorizationServerUrl = "http://localhost:8080/realms/master/";
895 
896builder.Services.AddAuthentication(options =>
897{
898 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
899 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
900})
901.AddJwtBearer(options =>
902{
903 options.Authority = authorizationServerUrl;
904 var normalizedServerAudience = serverUrl.TrimEnd('/');
905 options.TokenValidationParameters = new TokenValidationParameters
906 {
907 ValidIssuer = authorizationServerUrl,
908 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
909 AudienceValidator = (audiences, securityToken, validationParameters) =>
910 {
911 if (audiences == null) return false;
912 foreach (var aud in audiences)
913 {
914 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
915 {
916 return true;
917 }
918 }
919 return false;
920 }
921 };
922 
923 options.RequireHttpsMetadata = false; // Set to true in production
924 
925 options.Events = new JwtBearerEvents
926 {
927 OnTokenValidated = context =>
928 {
929 var name = context.Principal?.Identity?.Name ?? "unknown";
930 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
931 Console.WriteLine($"Token validated for: {name} ({email})");
932 return Task.CompletedTask;
933 },
934 OnAuthenticationFailed = context =>
935 {
936 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937 return Task.CompletedTask;
938 },
939 };
940})
941.AddMcp(options =>
942{
943 options.ResourceMetadata = new()
944 {
945 Resource = new Uri(serverUrl),
946 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
947 AuthorizationServers = { new Uri(authorizationServerUrl) },
948 ScopesSupported = ["mcp:tools"]
949 };
950});
951 
952builder.Services.AddAuthorization();
953 
954builder.Services.AddHttpContextAccessor();
955builder.Services.AddMcpServer()
956 .WithTools<MathTools>()
957 .WithHttpTransport();
958 
959var app = builder.Build();
960 
961app.UseAuthentication();
962app.UseAuthorization();
963 
964app.MapMcp().RequireAuthorization();
965 
966Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969Console.WriteLine("Exposed Math tools: Add, Multiply");
970Console.WriteLine("Press Ctrl+C to stop the server");
971 
972app.Run(serverUrl);
973```
974 
975For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
249> [tab] TypeScript
250>
251> You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252>
253> Prior to running the code below, ensure that you have a `.env` file with the following content:
254>
255> ```env theme={null}
256> # Server host/port
257> HOST=localhost
258> PORT=3000
259>
260> # Auth server location
261> AUTH_HOST=localhost
262> AUTH_PORT=8080
263> AUTH_REALM=master
264>
265> # Keycloak OAuth client credentials
266> OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267> OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268> ```
269>
270> `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271>
272> In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273>
274> ```typescript theme={null}
275> import "dotenv/config";
276> import express from "express";
277> import { randomUUID } from "node:crypto";
278> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279> import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280> import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281> import { z } from "zod";
282> import cors from "cors";
283> import {
284> mcpAuthMetadataRouter,
285> getOAuthProtectedResourceMetadataUrl,
286> } from "@modelcontextprotocol/sdk/server/auth/router.js";
287> import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288> import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289> import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290> const CONFIG = {
291> host: process.env.HOST || "localhost",
292> port: Number(process.env.PORT) || 3000,
293> auth: {
294> host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295> port: Number(process.env.AUTH_PORT) || 8080,
296> realm: process.env.AUTH_REALM || "master",
297> clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298> clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299> },
300> };
301>
302> function createOAuthUrls() {
303> const authBaseUrl = new URL(
304> `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305> );
306> return {
307> issuer: authBaseUrl.toString(),
308> introspection_endpoint: new URL(
309> "protocol/openid-connect/token/introspect",
310> authBaseUrl,
311> ).toString(),
312> authorization_endpoint: new URL(
313> "protocol/openid-connect/auth",
314> authBaseUrl,
315> ).toString(),
316> token_endpoint: new URL(
317> "protocol/openid-connect/token",
318> authBaseUrl,
319> ).toString(),
320> };
321> }
322>
323> function createRequestLogger() {
324> return (req: any, res: any, next: any) => {
325> const start = Date.now();
326> res.on("finish", () => {
327> const ms = Date.now() - start;
328> console.log(
329> `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
330> );
331> });
332> next();
333> };
334> }
335>
336> const app = express();
337>
338> app.use(
339> express.json({
340> verify: (req: any, _res, buf) => {
341> req.rawBody = buf?.toString() ?? "";
342> },
343> }),
344> );
345>
346> app.use(
347> cors({
348> origin: "*",
349> exposedHeaders: ["Mcp-Session-Id"],
350> }),
351> );
352>
353> app.use(createRequestLogger());
354>
355> const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356> const oauthUrls = createOAuthUrls();
357>
358> const oauthMetadata: OAuthMetadata = {
359> ...oauthUrls,
360> response_types_supported: ["code"],
361> };
362>
363> const tokenVerifier = {
364> verifyAccessToken: async (token: string) => {
365> const endpoint = oauthMetadata.introspection_endpoint;
366>
367> if (!endpoint) {
368> console.error("[auth] no introspection endpoint in metadata");
369> throw new Error("No token verification endpoint available in metadata");
370> }
371>
372> const params = new URLSearchParams({
373> token: token,
374> client_id: CONFIG.auth.clientId,
375> });
376>
377> if (CONFIG.auth.clientSecret) {
378> params.set("client_secret", CONFIG.auth.clientSecret);
379> }
380>
381> let response: Response;
382> try {
383> response = await fetch(endpoint, {
384> method: "POST",
385> headers: {
386> "Content-Type": "application/x-www-form-urlencoded",
387> },
388> body: params.toString(),
389> });
390> } catch (e) {
391> console.error("[auth] introspection fetch threw", e);
392> throw e;
393> }
394>
395> if (!response.ok) {
396> const txt = await response.text();
397> console.error("[auth] introspection non-OK", { status: response.status });
398>
399> try {
400> const obj = JSON.parse(txt);
401> console.log(JSON.stringify(obj, null, 2));
402> } catch {
403> console.error(txt);
404> }
405> throw new Error(`Invalid or expired token: ${txt}`);
406> }
407>
408> let data: any;
409> try {
410> data = await response.json();
411> } catch (e) {
412> const txt = await response.text();
413> console.error("[auth] failed to parse introspection JSON", {
414> error: String(e),
415> body: txt,
416> });
417> throw e;
418> }
419>
420> if (data.active === false) {
421> throw new Error("Inactive token");
422> }
423>
424> if (!data.aud) {
425> throw new Error("Resource indicator (aud) missing");
426> }
427>
428> const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429> const allowed = audiences.some((a) =>
430> checkResourceAllowed({
431> requestedResource: a,
432> configuredResource: mcpServerUrl,
433> }),
434> );
435> if (!allowed) {
436> throw new Error(
437> `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
438> );
439> }
440>
441> return {
442> token,
443> clientId: data.client_id,
444> scopes: data.scope ? data.scope.split(" ") : [],
445> expiresAt: data.exp,
446> };
447> },
448> };
449> app.use(
450> mcpAuthMetadataRouter({
451> oauthMetadata,
452> resourceServerUrl: mcpServerUrl,
453> scopesSupported: ["mcp:tools"],
454> resourceName: "MCP Demo Server",
455> }),
456> );
457>
458> const authMiddleware = requireBearerAuth({
459> verifier: tokenVerifier,
460> requiredScopes: [],
461> resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
462> });
463>
464> const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
465>
466> function createMcpServer() {
467> const server = new McpServer({
468> name: "example-server",
469> version: "1.0.0",
470> });
471>
472> server.registerTool(
473> "add",
474> {
475> title: "Addition Tool",
476> description: "Add two numbers together",
477> inputSchema: {
478> a: z.number().describe("First number to add"),
479> b: z.number().describe("Second number to add"),
480> },
481> },
482> async ({ a, b }) => ({
483> content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
484> }),
485> );
486>
487> server.registerTool(
488> "multiply",
489> {
490> title: "Multiplication Tool",
491> description: "Multiply two numbers together",
492> inputSchema: {
493> x: z.number().describe("First number to multiply"),
494> y: z.number().describe("Second number to multiply"),
495> },
496> },
497> async ({ x, y }) => ({
498> content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
499> }),
500> );
501>
502> return server;
503> }
504>
505> const mcpPostHandler = async (req: express.Request, res: express.Response) => {
506> const sessionId = req.headers["mcp-session-id"] as string | undefined;
507> let transport: StreamableHTTPServerTransport;
508>
509> if (sessionId && transports[sessionId]) {
510> transport = transports[sessionId];
511> } else if (!sessionId && isInitializeRequest(req.body)) {
512> transport = new StreamableHTTPServerTransport({
513> sessionIdGenerator: () => randomUUID(),
514> onsessioninitialized: (sessionId) => {
515> transports[sessionId] = transport;
516> },
517> });
518>
519> transport.onclose = () => {
520> if (transport.sessionId) {
521> delete transports[transport.sessionId];
522> }
523> };
524>
525> const server = createMcpServer();
526> await server.connect(transport);
527> } else {
528> res.status(400).json({
529> jsonrpc: "2.0",
530> error: {
531> code: -32000,
532> message: "Bad Request: No valid session ID provided",
533> },
534> id: null,
535> });
536> return;
537> }
538>
539> await transport.handleRequest(req, res, req.body);
540> };
541>
542> const handleSessionRequest = async (
543> req: express.Request,
544> res: express.Response,
545> ) => {
546> const sessionId = req.headers["mcp-session-id"] as string | undefined;
547> if (!sessionId || !transports[sessionId]) {
548> res.status(400).send("Invalid or missing session ID");
549> return;
550> }
551>
552> const transport = transports[sessionId];
553> await transport.handleRequest(req, res);
554> };
555>
556> app.post("/", authMiddleware, mcpPostHandler);
557> app.get("/", authMiddleware, handleSessionRequest);
558> app.delete("/", authMiddleware, handleSessionRequest);
559>
560> app.listen(CONFIG.port, CONFIG.host, () => {
561> console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
562> console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
563> console.log(
564> `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
565> );
566> });
567> ```
568>
569> When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
570>
571> For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
572 
573> [tab] Python
574>
575> You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
576>
577> To simplify our authorization interaction, in Python scenarios we rely on [FastMCP](https://gofastmcp.com/getting-started/welcome). Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
578>
579> Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
580>
581> ```python theme={null}
582> """Configuration settings for the MCP auth server."""
583>
584> import os
585> from typing import Optional
586>
587>
588> class Config:
589> """Configuration class that loads from environment variables with sensible defaults."""
590>
591> # Server settings
592> HOST: str = os.getenv("HOST", "localhost")
593> PORT: int = int(os.getenv("PORT", "3000"))
594>
595> # Auth server settings
596> AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
597> AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
598> AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
599>
600> # OAuth client settings
601> OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "mcp-server")
602> OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "UO3rmozkFFkXr0QxPTkzZ0LMXDidIikB")
603>
604> # Server settings
605> MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
606> OAUTH_STRICT: bool = os.getenv("OAUTH_STRICT", "false").lower() in ("true", "1", "yes")
607> TRANSPORT: str = os.getenv("TRANSPORT", "streamable-http")
608>
609> @property
610> def server_url(self) -> str:
611> """Build the server URL."""
612> return f"http://{self.HOST}:{self.PORT}"
613>
614> @property
615> def auth_base_url(self) -> str:
616> """Build the auth server base URL."""
617> return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
618>
619> def validate(self) -> None:
620> """Validate configuration."""
621> if self.TRANSPORT not in ["sse", "streamable-http"]:
622> raise ValueError(f"Invalid transport: {self.TRANSPORT}. Must be 'sse' or 'streamable-http'")
623>
624>
625> # Global configuration instance
626> config = Config()
627>
628> ```
629>
630> The server implementation is as follows:
631>
632> ```python theme={null}
633> import datetime
634> import logging
635> from typing import Any
636>
637> from pydantic import AnyHttpUrl
638>
639> from mcp.server.auth.settings import AuthSettings
640> from mcp.server.fastmcp.server import FastMCP
641>
642> from .config import config
643> from .token_verifier import IntrospectionTokenVerifier
644>
645> logger = logging.getLogger(__name__)
646>
647>
648> def create_oauth_urls() -> dict[str, str]:
649> """Create OAuth URLs based on configuration (Keycloak-style)."""
650> from urllib.parse import urljoin
651>
652> auth_base_url = config.auth_base_url
653>
654> return {
655> "issuer": auth_base_url,
656> "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
657> "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
658> "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
659> }
660>
661>
662> def create_server() -> FastMCP:
663> """Create and configure the FastMCP server."""
664>
665> config.validate()
666>
667> oauth_urls = create_oauth_urls()
668>
669> token_verifier = IntrospectionTokenVerifier(
670> introspection_endpoint=oauth_urls["introspection_endpoint"],
671> server_url=config.server_url,
672> client_id=config.OAUTH_CLIENT_ID,
673> client_secret=config.OAUTH_CLIENT_SECRET,
674> )
675>
676> app = FastMCP(
677> name="MCP Resource Server",
678> instructions="Resource Server that validates tokens via Authorization Server introspection",
679> host=config.HOST,
680> port=config.PORT,
681> debug=True,
682> streamable_http_path="/",
683> token_verifier=token_verifier,
684> auth=AuthSettings(
685> issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
686> required_scopes=[config.MCP_SCOPE],
687> resource_server_url=AnyHttpUrl(config.server_url),
688> ),
689> )
690>
691> @app.tool()
692> async def add_numbers(a: float, b: float) -> dict[str, Any]:
693> """
694> Add two numbers together.
695> This tool demonstrates basic arithmetic operations with OAuth authentication.
696>
697> Args:
698> a: The first number to add
699> b: The second number to add
700> """
701> result = a + b
702> return {
703> "operation": "addition",
704> "operand_a": a,
705> "operand_b": b,
706> "result": result,
707> "timestamp": datetime.datetime.now().isoformat()
708> }
709>
710> @app.tool()
711> async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
712> """
713> Multiply two numbers together.
714> This tool demonstrates basic arithmetic operations with OAuth authentication.
715>
716> Args:
717> x: The first number to multiply
718> y: The second number to multiply
719> """
720> result = x * y
721> return {
722> "operation": "multiplication",
723> "operand_x": x,
724> "operand_y": y,
725> "result": result,
726> "timestamp": datetime.datetime.now().isoformat()
727> }
728>
729> return app
730>
731>
732> def main() -> int:
733> """
734> Run the MCP Resource Server.
735>
736> This server:
737> - Provides RFC 9728 Protected Resource Metadata
738> - Validates tokens via Authorization Server introspection
739> - Serves MCP tools requiring authentication
740>
741> Configuration is loaded from config.py and environment variables.
742> """
743> logging.basicConfig(level=logging.INFO)
744>
745> try:
746> config.validate()
747> oauth_urls = create_oauth_urls()
748>
749> except ValueError as e:
750> logger.error("Configuration error: %s", e)
751> return 1
752>
753> try:
754> mcp_server = create_server()
755>
756> logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
757> logger.info("Authorization Server: %s", oauth_urls["issuer"])
758> logger.info("Transport: %s", config.TRANSPORT)
759>
760> mcp_server.run(transport=config.TRANSPORT)
761> return 0
762>
763> except Exception:
764> logger.exception("Server error")
765> return 1
766>
767>
768> if __name__ == "__main__":
769> exit(main())
770> ```
771>
772> Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts
773>
774> ```python theme={null}
775> """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
776>
777> import logging
778> from typing import Any
779>
780> from mcp.server.auth.provider import AccessToken, TokenVerifier
781> from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
782>
783> logger = logging.getLogger(__name__)
784>
785>
786> class IntrospectionTokenVerifier(TokenVerifier):
787> """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
788> """
789>
790> def __init__(
791> self,
792> introspection_endpoint: str,
793> server_url: str,
794> client_id: str,
795> client_secret: str,
796> ):
797> self.introspection_endpoint = introspection_endpoint
798> self.server_url = server_url
799> self.client_id = client_id
800> self.client_secret = client_secret
801> self.resource_url = resource_url_from_server_url(server_url)
802>
803> async def verify_token(self, token: str) -> AccessToken | None:
804> """Verify token via introspection endpoint."""
805> import httpx
806>
807> if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
808> return None
809>
810> timeout = httpx.Timeout(10.0, connect=5.0)
811> limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
812>
813> async with httpx.AsyncClient(
814> timeout=timeout,
815> limits=limits,
816> verify=True,
817> ) as client:
818> try:
819> form_data = {
820> "token": token,
821> "client_id": self.client_id,
822> "client_secret": self.client_secret,
823> }
824> headers = {"Content-Type": "application/x-www-form-urlencoded"}
825>
826> response = await client.post(
827> self.introspection_endpoint,
828> data=form_data,
829> headers=headers,
830> )
831>
832> if response.status_code != 200:
833> return None
834>
835> data = response.json()
836> if not data.get("active", False):
837> return None
838>
839> if not self._validate_resource(data):
840> return None
841>
842> return AccessToken(
843> token=token,
844> client_id=data.get("client_id", "unknown"),
845> scopes=data.get("scope", "").split() if data.get("scope") else [],
846> expires_at=data.get("exp"),
847> resource=data.get("aud"), # Include resource in token
848> )
849>
850> except Exception as e:
851> return None
852>
853> def _validate_resource(self, token_data: dict[str, Any]) -> bool:
854> """Validate token was issued for this resource server.
855>
856> Rules:
857> - Reject if 'aud' missing.
858> - Accept if any audience entry matches the derived resource URL.
859> - Supports string or list forms per JWT spec.
860> """
861> if not self.server_url or not self.resource_url:
862> return False
863>
864> aud: list[str] | str | None = token_data.get("aud")
865> if isinstance(aud, list):
866> return any(self._is_valid_resource(a) for a in aud)
867> if isinstance(aud, str):
868> return self._is_valid_resource(aud)
869> return False
870>
871> def _is_valid_resource(self, resource: str) -> bool:
872> """Check if the given resource matches our server."""
873> return check_resource_allowed(self.resource_url, resource)
874> ```
875>
876> For more details, see the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
877 
878> [tab] C#
879>
880> You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
881>
882> To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
883>
884> ```csharp theme={null}
885> using Microsoft.AspNetCore.Authentication.JwtBearer;
886> using Microsoft.IdentityModel.Tokens;
887> using ModelContextProtocol.AspNetCore.Authentication;
888> using ProtectedMcpServer.Tools;
889> using System.Security.Claims;
890>
891> var builder = WebApplication.CreateBuilder(args);
892>
893> var serverUrl = "http://localhost:3000/";
894> var authorizationServerUrl = "http://localhost:8080/realms/master/";
895>
896> builder.Services.AddAuthentication(options =>
897> {
898> options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
899> options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
900> })
901> .AddJwtBearer(options =>
902> {
903> options.Authority = authorizationServerUrl;
904> var normalizedServerAudience = serverUrl.TrimEnd('/');
905> options.TokenValidationParameters = new TokenValidationParameters
906> {
907> ValidIssuer = authorizationServerUrl,
908> ValidAudiences = new[] { normalizedServerAudience, serverUrl },
909> AudienceValidator = (audiences, securityToken, validationParameters) =>
910> {
911> if (audiences == null) return false;
912> foreach (var aud in audiences)
913> {
914> if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
915> {
916> return true;
917> }
918> }
919> return false;
920> }
921> };
922>
923> options.RequireHttpsMetadata = false; // Set to true in production
924>
925> options.Events = new JwtBearerEvents
926> {
927> OnTokenValidated = context =>
928> {
929> var name = context.Principal?.Identity?.Name ?? "unknown";
930> var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
931> Console.WriteLine($"Token validated for: {name} ({email})");
932> return Task.CompletedTask;
933> },
934> OnAuthenticationFailed = context =>
935> {
936> Console.WriteLine($"Authentication failed: {context.Exception.Message}");
937> return Task.CompletedTask;
938> },
939> };
940> })
941> .AddMcp(options =>
942> {
943> options.ResourceMetadata = new()
944> {
945> Resource = new Uri(serverUrl),
946> ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
947> AuthorizationServers = { new Uri(authorizationServerUrl) },
948> ScopesSupported = ["mcp:tools"]
949> };
950> });
951>
952> builder.Services.AddAuthorization();
953>
954> builder.Services.AddHttpContextAccessor();
955> builder.Services.AddMcpServer()
956> .WithTools<MathTools>()
957> .WithHttpTransport();
958>
959> var app = builder.Build();
960>
961> app.UseAuthentication();
962> app.UseAuthorization();
963>
964> app.MapMcp().RequireAuthorization();
965>
966> Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
967> Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
968> Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
969> Console.WriteLine("Exposed Math tools: Add, Multiply");
970> Console.WriteLine("Press Ctrl+C to stop the server");
971>
972> app.Run(serverUrl);
973> ```
974>
975> For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
976976 
977977## Testing the MCP Server
978978 
979979 

docs/2025-06-18/develop/build-client Changed · +2464 / -2475 lines

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

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

docs/2025-06-18/develop/build-server Changed · +2784 / -2784 lines

### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up the instance ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather Service ### Create your Boot Application ### Running the server ## Testing your server with Claude for Desktop ## Testing your server with Java client ### Create an MCP Client manually ### Use MCP Client Boot Starter ## More Java MCP Server examples ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Setting up the instance ### Weather API helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### System requirements ### Set up your environment ## Building your server ### Weather API helper functions ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and setting up constants ### Helper methods ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing the Weather server and tools ### Implementing the ServerHandler ### Running the server ## Testing your server with Claude for Desktop ### Prerequisite knowledge ### Logging in MCP Servers ### Best Practices ### Quick Examples ### System requirements ### Set up your environment ## Building your server ### Importing packages and constants ### Data structures ### Helper functions ### Implementing tool execution ### Running the server ## Testing your server with Claude for Desktop

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 24
2424 
2525This tutorial will primarily focus on tools.
2626 
27**Python**
28 
29Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30 
31### Prerequisite knowledge
32 
33This quickstart assumes you have familiarity with:
34 
35* Python
36* LLMs like Claude
37 
38### Logging in MCP Servers
39 
40When implementing MCP servers, be careful about how you handle logging:
41 
42**For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, but can be used safely with `file=sys.stderr`.
43 
44**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45 
46### Best Practices
47 
48* Use a logging library that writes to stderr or files.
49 
50### Quick Examples
51 
52```python theme={null}
53import sys
54import logging
55 
56# ❌ Bad (STDIO)
57print("Processing request")
58 
59# ✅ Good (STDIO)
60print("Processing request", file=sys.stderr)
61 
62# ✅ Good (STDIO)
63logging.info("Processing request")
64```
65 
66### System requirements
67 
68* Python 3.10 or higher installed.
69* You must use the Python MCP SDK 1.2.0 or higher.
70 
71### Set up your environment
72 
73First, let's install `uv` and set up our Python project and environment:
74 
75```bash macOS/Linux theme={null}
76curl -LsSf https://astral.sh/uv/install.sh | sh
77```
78 
79```powershell Windows theme={null}
80powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81```
82 
83Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84 
85Now, let's create and set up our project:
86 
87```bash macOS/Linux theme={null}
88# Create a new directory for our project
89uv init weather
90cd weather
91 
92# Create virtual environment and activate it
93uv venv
94source .venv/bin/activate
95 
96# Install dependencies
97uv add "mcp[cli]" httpx
98 
99# Create our server file
100touch weather.py
101```
102 
103```powershell Windows theme={null}
104# Create a new directory for our project
105uv init weather
106cd weather
107 
108# Create virtual environment and activate it
109uv venv
110.venv\Scripts\activate
111 
112# Install dependencies
113uv add mcp[cli] httpx
114 
115# Create our server file
116new-item weather.py
117```
118 
119Now let's dive into building your server.
120 
121## Building your server
122 
123### Importing packages and setting up the instance
124 
125Add these to the top of your `weather.py`:
126 
127```python theme={null}
128from typing import Any
129 
130import httpx
131from mcp.server.fastmcp import FastMCP
132 
133# Initialize FastMCP server
134mcp = FastMCP("weather")
135 
136# Constants
137NWS_API_BASE = "https://api.weather.gov"
138USER_AGENT = "weather-app/1.0"
139```
140 
141The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
142 
143### Helper functions
144 
145Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146 
147```python theme={null}
148async def make_nws_request(url: str) -> dict[str, Any] | None:
149 """Make a request to the NWS API with proper error handling."""
150 headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
151 async with httpx.AsyncClient() as client:
152 try:
153 response = await client.get(url, headers=headers, timeout=30.0)
154 response.raise_for_status()
155 return response.json()
156 except Exception:
157 return None
158 
159 
160def format_alert(feature: dict) -> str:
161 """Format an alert feature into a readable string."""
162 props = feature["properties"]
163 return f"""
164Event: {props.get("event", "Unknown")}
165Area: {props.get("areaDesc", "Unknown")}
166Severity: {props.get("severity", "Unknown")}
167Description: {props.get("description", "No description available")}
168Instructions: {props.get("instruction", "No specific instructions provided")}
169"""
170```
171 
172### Implementing tool execution
173 
174The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
175 
176```python theme={null}
177@mcp.tool()
178async def get_alerts(state: str) -> str:
179 """Get weather alerts for a US state.
180 
181 Args:
182 state: Two-letter US state code (e.g. CA, NY)
183 """
184 url = f"{NWS_API_BASE}/alerts/active/area/{state}"
185 data = await make_nws_request(url)
186 
187 if not data or "features" not in data:
188 return "Unable to fetch alerts or no alerts found."
189 
190 if not data["features"]:
191 return "No active alerts for this state."
192 
193 alerts = [format_alert(feature) for feature in data["features"]]
194 return "\n---\n".join(alerts)
195 
196 
197@mcp.tool()
198async def get_forecast(latitude: float, longitude: float) -> str:
199 """Get weather forecast for a location.
200 
201 Args:
202 latitude: Latitude of the location
203 longitude: Longitude of the location
204 """
205 # First get the forecast grid endpoint
206 points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
207 points_data = await make_nws_request(points_url)
208 
209 if not points_data:
210 return "Unable to fetch forecast data for this location."
211 
212 # Get the forecast URL from the points response
213 forecast_url = points_data["properties"]["forecast"]
214 forecast_data = await make_nws_request(forecast_url)
215 
216 if not forecast_data:
217 return "Unable to fetch detailed forecast."
218 
219 # Format the periods into a readable forecast
220 periods = forecast_data["properties"]["periods"]
221 forecasts = []
222 for period in periods[:5]: # Only show next 5 periods
223 forecast = f"""
224{period["name"]}:
225Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226Wind: {period["windSpeed"]} {period["windDirection"]}
227Forecast: {period["detailedForecast"]}
228"""
229 forecasts.append(forecast)
230 
231 return "\n---\n".join(forecasts)
232```
233 
234### Running the server
235 
236Finally, let's initialize and run the server:
237 
238```python theme={null}
239def main():
240 # Initialize and run the server
241 mcp.run(transport="stdio")
242 
243 
244if __name__ == "__main__":
245 main()
246```
247 
248Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249 
250Let's now test your server from an existing MCP host, Claude for Desktop.
251 
252## Testing your server with Claude for Desktop
253 
254> [!NOTE]
255>
256> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257 
258First, make sure you have Claude for Desktop installed. [You can install the latest version
259here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260 
261We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
262 
263For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264 
265```bash macOS/Linux theme={null}
266code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267```
268 
269```powershell Windows theme={null}
270code $env:AppData\Claude\claude_desktop_config.json
271```
272 
273You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
274 
275In this case, we'll add our single weather server like so:
276 
277```json macOS/Linux theme={null}
278{
279 "mcpServers": {
280 "weather": {
281 "command": "uv",
282 "args": [
283 "--directory",
284 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
285 "run",
286 "weather.py"
287 ]
288 }
289 }
290}
291```
292 
293```json Windows theme={null}
294{
295 "mcpServers": {
296 "weather": {
297 "command": "uv",
298 "args": [
299 "--directory",
300 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
301 "run",
302 "weather.py"
303 ]
304 }
305 }
306}
307```
308 
309> [!WARNING]
310>
311> You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
312 
313> [!NOTE]
314>
315> Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
316 
317This tells Claude for Desktop:
318 
3191. There's an MCP server named "weather"
3202. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321 
322Save the file, and restart **Claude for Desktop**.
323 
324**TypeScript**
325 
326Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
327 
328### Prerequisite knowledge
329 
330This quickstart assumes you have familiarity with:
331 
332* TypeScript
333* LLMs like Claude
334 
335### Logging in MCP Servers
336 
337When implementing MCP servers, be careful about how you handle logging:
338 
339**For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
340 
341**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
342 
343### Best Practices
344 
345* Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
346 
347### Quick Examples
348 
349```javascript theme={null}
350// ❌ Bad (STDIO)
351console.log("Server started");
352 
353// ✅ Good (STDIO)
354console.error("Server started"); // stderr is safe
355```
356 
357### System requirements
358 
359For TypeScript, make sure you have the latest version of Node installed.
360 
361### Set up your environment
362 
363First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364Verify your Node.js installation:
365 
366```bash theme={null}
367node --version
368npm --version
369```
370 
371For this tutorial, you'll need Node.js version 16 or higher.
372 
373Now, let's create and set up our project:
374 
375```bash macOS/Linux theme={null}
376# Create a new directory for our project
377mkdir weather
378cd weather
379 
380# Initialize a new npm project
381npm init -y
382 
383# Install dependencies
384npm install @modelcontextprotocol/sdk zod@3
385npm install -D @types/node typescript
386 
387# Create our files
388mkdir src
389touch src/index.ts
390```
391 
392```powershell Windows theme={null}
393# Create a new directory for our project
394md weather
395cd weather
396 
397# Initialize a new npm project
398npm init -y
399 
400# Install dependencies
401npm install @modelcontextprotocol/sdk zod@3
402npm install -D @types/node typescript
403 
404# Create our files
405md src
406new-item src\index.ts
407```
408 
409Update your package.json to add type: "module" and a build script:
410 
411```json package.json theme={null}
412{
413 "type": "module",
414 "bin": {
415 "weather": "./build/index.js"
416 },
417 "scripts": {
418 "build": "tsc && chmod 755 build/index.js"
419 },
420 "files": ["build"]
421}
422```
423 
424Create a `tsconfig.json` in the root of your project:
425 
426```json tsconfig.json theme={null}
427{
428 "compilerOptions": {
429 "target": "ES2022",
430 "module": "Node16",
431 "moduleResolution": "Node16",
432 "outDir": "./build",
433 "rootDir": "./src",
434 "strict": true,
435 "esModuleInterop": true,
436 "skipLibCheck": true,
437 "forceConsistentCasingInFileNames": true
438 },
439 "include": ["src/**/*"],
440 "exclude": ["node_modules"]
441}
442```
443 
444Now let's dive into building your server.
445 
446## Building your server
447 
448### Importing packages and setting up the instance
449 
450Add these to the top of your `src/index.ts`:
451 
452```typescript theme={null}
453import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455import { z } from "zod";
456 
457const NWS_API_BASE = "https://api.weather.gov";
458const USER_AGENT = "weather-app/1.0";
459 
460// Create server instance
461const server = new McpServer({
462 name: "weather",
463 version: "1.0.0",
464});
465```
466 
467### Helper functions
468 
469Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
470 
471```typescript theme={null}
472// Helper function for making NWS API requests
473async function makeNWSRequest<T>(url: string): Promise<T | null> {
474 const headers = {
475 "User-Agent": USER_AGENT,
476 Accept: "application/geo+json",
477 };
478 
479 try {
480 const response = await fetch(url, { headers });
481 if (!response.ok) {
482 throw new Error(`HTTP error! status: ${response.status}`);
483 }
484 return (await response.json()) as T;
485 } catch (error) {
486 console.error("Error making NWS request:", error);
487 return null;
488 }
489}
490 
491interface AlertFeature {
492 properties: {
493 event?: string;
494 areaDesc?: string;
495 severity?: string;
496 status?: string;
497 headline?: string;
498 };
499}
500 
501// Format alert data
502function formatAlert(feature: AlertFeature): string {
503 const props = feature.properties;
504 return [
505 `Event: ${props.event || "Unknown"}`,
506 `Area: ${props.areaDesc || "Unknown"}`,
507 `Severity: ${props.severity || "Unknown"}`,
508 `Status: ${props.status || "Unknown"}`,
509 `Headline: ${props.headline || "No headline"}`,
510 "---",
511 ].join("\n");
512}
513 
514interface ForecastPeriod {
515 name?: string;
516 temperature?: number;
517 temperatureUnit?: string;
518 windSpeed?: string;
519 windDirection?: string;
520 shortForecast?: string;
521}
522 
523interface AlertsResponse {
524 features: AlertFeature[];
525}
526 
527interface PointsResponse {
528 properties: {
529 forecast?: string;
530 };
531}
532 
533interface ForecastResponse {
534 properties: {
535 periods: ForecastPeriod[];
536 };
537}
538```
539 
540### Implementing tool execution
541 
542The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
543 
544```typescript theme={null}
545// Register weather tools
546 
547server.registerTool(
548 "get_alerts",
549 {
550 description: "Get weather alerts for a state",
551 inputSchema: {
552 state: z
553 .string()
554 .length(2)
555 .describe("Two-letter state code (e.g. CA, NY)"),
556 },
557 },
558 async ({ state }) => {
559 const stateCode = state.toUpperCase();
560 const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
561 const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
562 
563 if (!alertsData) {
564 return {
565 content: [
566 {
567 type: "text",
568 text: "Failed to retrieve alerts data",
569 },
570 ],
571 };
572 }
573 
574 const features = alertsData.features || [];
575 if (!features.length) {
576 return {
577 content: [
578 {
579 type: "text",
580 text: `No active alerts for ${stateCode}`,
581 },
582 ],
583 };
584 }
585 
586 const formattedAlerts = features.map(formatAlert);
587 const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
588 
589 return {
590 content: [
591 {
592 type: "text",
593 text: alertsText,
594 },
595 ],
596 };
597 },
598);
599 
600server.registerTool(
601 "get_forecast",
602 {
603 description: "Get weather forecast for a location",
604 inputSchema: {
605 latitude: z
606 .number()
607 .min(-90)
608 .max(90)
609 .describe("Latitude of the location"),
610 longitude: z
611 .number()
612 .min(-180)
613 .max(180)
614 .describe("Longitude of the location"),
615 },
616 },
617 async ({ latitude, longitude }) => {
618 // Get grid point data
619 const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
620 const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
621 
622 if (!pointsData) {
623 return {
624 content: [
625 {
626 type: "text",
627 text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
628 },
629 ],
630 };
631 }
632 
633 const forecastUrl = pointsData.properties?.forecast;
634 if (!forecastUrl) {
635 return {
636 content: [
637 {
638 type: "text",
639 text: "Failed to get forecast URL from grid point data",
640 },
641 ],
642 };
643 }
644 
645 // Get forecast data
646 const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
647 if (!forecastData) {
648 return {
649 content: [
650 {
651 type: "text",
652 text: "Failed to retrieve forecast data",
653 },
654 ],
655 };
656 }
657 
658 const periods = forecastData.properties?.periods || [];
659 if (periods.length === 0) {
660 return {
661 content: [
662 {
663 type: "text",
664 text: "No forecast periods available",
665 },
666 ],
667 };
668 }
669 
670 // Format forecast periods
671 const formattedForecast = periods.map((period: ForecastPeriod) =>
672 [
673 `${period.name || "Unknown"}:`,
674 `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
675 `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
676 `${period.shortForecast || "No forecast available"}`,
677 "---",
678 ].join("\n"),
679 );
680 
681 const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
682 
683 return {
684 content: [
685 {
686 type: "text",
687 text: forecastText,
688 },
689 ],
690 };
691 },
692);
693```
694 
695### Running the server
696 
697Finally, implement the main function to run the server:
698 
699```typescript theme={null}
700async function main() {
701 const transport = new StdioServerTransport();
702 await server.connect(transport);
703 console.error("Weather MCP Server running on stdio");
704}
705 
706main().catch((error) => {
707 console.error("Fatal error in main():", error);
708 process.exit(1);
709});
710```
711 
712Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713 
714Let's now test your server from an existing MCP host, Claude for Desktop.
715 
716## Testing your server with Claude for Desktop
717 
718> [!NOTE]
719>
720> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721 
722First, make sure you have Claude for Desktop installed. [You can install the latest version
723here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724 
725We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726 
727For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728 
729```bash macOS/Linux theme={null}
730code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731```
732 
733```powershell Windows theme={null}
734code $env:AppData\Claude\claude_desktop_config.json
735```
736 
737You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
738 
739In this case, we'll add our single weather server like so:
740 
741```json macOS/Linux theme={null}
742{
743 "mcpServers": {
744 "weather": {
745 "command": "node",
746 "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
747 }
748 }
749}
750```
751 
752```json Windows theme={null}
753{
754 "mcpServers": {
755 "weather": {
756 "command": "node",
757 "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
758 }
759 }
760}
761```
762 
763This tells Claude for Desktop:
764 
7651. There's an MCP server named "weather"
7662. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767 
768Save the file, and restart **Claude for Desktop**.
769 
770**Java**
771 
772> [!NOTE]
773>
774> This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
775> To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
776 
777Let's get started with building our weather server!
778[You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
779 
780For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
781For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782 
783### Logging in MCP Servers
784 
785When implementing MCP servers, be careful about how you handle logging:
786 
787**For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
788 
789**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
790 
791### Best Practices
792 
793* Use a logging library that writes to stderr or files.
794* Ensure any configured logging library will not write to stdout.
795 
796### System requirements
797 
798* Java 17 or higher installed.
799* [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
800 
801### Set up your environment
802 
803Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804 
805You will need to add the following dependencies:
806 
807```xml Maven theme={null}
808<dependencies>
809 <dependency>
810 <groupId>org.springframework.ai</groupId>
811 <artifactId>spring-ai-starter-mcp-server</artifactId>
812 </dependency>
813 
814 <dependency>
815 <groupId>org.springframework</groupId>
816 <artifactId>spring-web</artifactId>
817 </dependency>
818</dependencies>
819```
820 
821```groovy Gradle theme={null}
822dependencies {
823 implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824 implementation platform("org.springframework:spring-web")
825}
826```
827 
828Then configure your application by setting the application properties:
829 
830```bash application.properties theme={null}
831spring.main.bannerMode=off
832logging.pattern.console=
833```
834 
835```yaml application.yml theme={null}
836logging:
837 pattern:
838 console:
839spring:
840 main:
841 banner-mode: off
842```
843 
844The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
845 
846Now let's dive into building your server.
847 
848## Building your server
849 
850### Weather Service
851 
852Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
853 
854```java theme={null}
855@Service
856public class WeatherService {
857 
858 private final RestClient restClient;
859 
860 public WeatherService() {
861 this.restClient = RestClient.builder()
862 .baseUrl("https://api.weather.gov")
863 .defaultHeader("Accept", "application/geo+json")
864 .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
865 .build();
866 }
867 
868 @Tool(description = "Get weather forecast for a specific latitude/longitude")
869 public String getWeatherForecastByLocation(
870 double latitude, // Latitude coordinate
871 double longitude // Longitude coordinate
872 ) {
873 // Returns detailed forecast including:
874 // - Temperature and unit
875 // - Wind speed and direction
876 // - Detailed forecast description
877 }
878 
879 @Tool(description = "Get weather alerts for a US state")
880 public String getAlerts(
881 @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
882 ) {
883 // Returns active alerts including:
884 // - Event type
885 // - Affected area
886 // - Severity
887 // - Description
888 // - Safety instructions
889 }
890 
891 // ......
892}
893```
894 
895The `@Service` annotation will auto-register the service in your application context.
896The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897 
898The auto-configuration will automatically register these tools with the MCP server.
899 
900### Create your Boot Application
901 
902```java theme={null}
903@SpringBootApplication
904public class McpServerApplication {
905 
906 public static void main(String[] args) {
907 SpringApplication.run(McpServerApplication.class, args);
908 }
909 
910 @Bean
911 public ToolCallbackProvider weatherTools(WeatherService weatherService) {
912 return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
913 }
914}
915```
916 
917Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918 
919### Running the server
920 
921Finally, let's build the server:
922 
923```bash theme={null}
924./mvnw clean install
925```
926 
927This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928 
929Let's now test your server from an existing MCP host, Claude for Desktop.
930 
931## Testing your server with Claude for Desktop
932 
933> [!NOTE]
934>
935> Claude for Desktop is not yet available on Linux.
936 
937First, make sure you have Claude for Desktop installed.
938[You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939 
940We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942Make sure to create the file if it doesn't exist.
943 
944For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945 
946```bash macOS/Linux theme={null}
947code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948```
949 
950```powershell Windows theme={null}
951code $env:AppData\Claude\claude_desktop_config.json
952```
953 
954You'll then add your servers in the `mcpServers` key.
955The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956 
957In this case, we'll add our single weather server like so:
958 
959```json macOS/Linux theme={null}
960{
961 "mcpServers": {
962 "spring-ai-mcp-weather": {
963 "command": "java",
964 "args": [
965 "-Dspring.ai.mcp.server.stdio=true",
966 "-jar",
967 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
968 ]
969 }
970 }
971}
972```
973 
974```json Windows theme={null}
975{
976 "mcpServers": {
977 "spring-ai-mcp-weather": {
978 "command": "java",
979 "args": [
980 "-Dspring.ai.mcp.server.transport=STDIO",
981 "-jar",
982 "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
983 ]
984 }
985 }
986}
987```
988 
989> [!NOTE]
990>
991> Make sure you pass in the absolute path to your server.
992 
993This tells Claude for Desktop:
994 
9951. There's an MCP server named "my-weather-server"
9962. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997 
998Save the file, and restart **Claude for Desktop**.
999 
1000## Testing your server with Java client
1001 
1002### Create an MCP Client manually
1003 
1004Use the `McpClient` to connect to the server:
1005 
1006```java theme={null}
1007var stdioParams = ServerParameters.builder("java")
1008 .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009 .build();
1010 
1011var stdioTransport = new StdioClientTransport(stdioParams);
1012 
1013var mcpClient = McpClient.sync(stdioTransport).build();
1014 
1015mcpClient.initialize();
1016 
1017ListToolsResult toolsList = mcpClient.listTools();
1018 
1019CallToolResult weather = mcpClient.callTool(
1020 new CallToolRequest("getWeatherForecastByLocation",
1021 Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022 
1023CallToolResult alert = mcpClient.callTool(
1024 new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025 
1026mcpClient.closeGracefully();
1027```
1028 
1029### Use MCP Client Boot Starter
1030 
1031Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1032 
1033```xml theme={null}
1034<dependency>
1035 <groupId>org.springframework.ai</groupId>
1036 <artifactId>spring-ai-starter-mcp-client</artifactId>
1037</dependency>
1038```
1039 
1040and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041You can reuse the existing Anthropic Desktop configuration:
1042 
1043```properties theme={null}
1044spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045```
1046 
1047When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048 
1049For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1050 
1051## More Java MCP Server examples
1052 
1053The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an MCP server using SSE transport.
1054It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056**Kotlin**
1057 
1058Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1059 
1060### Prerequisite knowledge
1061 
1062This quickstart assumes you have familiarity with:
1063 
1064* Kotlin
1065* LLMs like Claude
1066 
1067### Logging in MCP Servers
1068 
1069When implementing MCP servers, be careful about how you handle logging:
1070 
1071**For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1072 
1073**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1074 
1075### Best Practices
1076 
1077* Use a logging library that writes to stderr or files.
1078 
1079### System requirements
1080 
1081* JDK 11 or higher installed.
1082 
1083### Set up your environment
1084 
1085First, let's install `java` and `gradle` if you haven't already.
1086You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087Verify your `java` installation:
1088 
1089```bash theme={null}
1090java --version
1091```
1092 
1093Now, let's create and set up your project:
1094 
1095```bash macOS/Linux theme={null}
1096# Create a new directory for our project
1097mkdir weather
1098cd weather
1099 
1100# Initialize a new kotlin project
1101gradle init
1102```
1103 
1104```powershell Windows theme={null}
1105# Create a new directory for our project
1106md weather
1107cd weather
1108 
1109# Initialize a new kotlin project
1110gradle init
1111```
1112 
1113After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114 
1115Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116 
1117After creating the project, replace the contents of your `build.gradle.kts` with:
1118 
1119```kotlin build.gradle.kts theme={null}
1120// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1121val mcpVersion = "0.9.0"
1122val ktorVersion = "3.2.3"
1123val slf4jVersion = "2.0.17"
1124 
1125plugins {
1126 kotlin("jvm") version "2.3.20"
1127 kotlin("plugin.serialization") version "2.3.20"
1128 id("com.gradleup.shadow") version "8.3.9"
1129 application
1130}
1131 
1132application {
1133 mainClass.set("MainKt")
1134}
1135 
1136dependencies {
1137 implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1138 implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1139 implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1140 implementation("io.ktor:ktor-client-cio:$ktorVersion")
1141 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1142}
1143```
1144 
1145Verify that everything is set up correctly:
1146 
1147```bash theme={null}
1148./gradlew build
1149```
1150 
1151Now let's dive into building your server.
1152 
1153## Building your server
1154 
1155### Setting up the instance
1156 
1157Add a server initialization function:
1158 
1159```kotlin theme={null}
1160fun runMcpServer() {
1161 val server = Server(
1162 Implementation(
1163 name = "weather",
1164 version = "1.0.0",
1165 ),
1166 ServerOptions(
1167 capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1168 ),
1169 )
1170 
1171 // register tools on server here
1172 
1173 val transport = StdioServerTransport(
1174 System.`in`.asInput(),
1175 System.out.asSink().buffered(),
1176 )
1177 
1178 runBlocking {
1179 val session = server.createSession(transport)
1180 val done = Job()
1181 session.onClose {
1182 done.complete()
1183 }
1184 done.join()
1185 }
1186}
1187```
1188 
1189### Weather API helper functions
1190 
1191Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192 
1193```kotlin theme={null}
1194val httpClient = HttpClient(CIO) {
1195 defaultRequest {
1196 url("https://api.weather.gov")
1197 headers {
1198 append("Accept", "application/geo+json")
1199 append("User-Agent", "WeatherApiClient/1.0")
1200 }
1201 contentType(ContentType.Application.Json)
1202 }
1203 install(ContentNegotiation) {
1204 json(Json { ignoreUnknownKeys = true })
1205 }
1206}
1207 
1208// Extension function to fetch weather alerts for a given state
1209suspend fun HttpClient.getAlerts(state: String): List<String> {
1210 val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211 return alerts.features.map { feature ->
1212 """
1213 Event: ${feature.properties.event}
1214 Area: ${feature.properties.areaDesc}
1215 Severity: ${feature.properties.severity}
1216 Status: ${feature.properties.status}
1217 Headline: ${feature.properties.headline}
1218 """.trimIndent()
1219 }
1220}
1221 
1222// Extension function to fetch forecast information for given latitude and longitude
1223suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1224 val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1225 val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1226 val forecast = this.get(forecastUrl).body<ForecastResponse>()
1227 return forecast.properties.periods.map { period ->
1228 """
1229 ${period.name}:
1230 Temperature: ${period.temperature}°${period.temperatureUnit}
1231 Wind: ${period.windSpeed} ${period.windDirection}
1232 ${period.shortForecast}
1233 """.trimIndent()
1234 }
1235}
1236 
1237@Serializable
1238data class PointsResponse(val properties: PointsProperties)
1239 
1240@Serializable
1241data class PointsProperties(val forecast: String? = null)
1242 
1243@Serializable
1244data class ForecastResponse(val properties: ForecastProperties)
1245 
1246@Serializable
1247data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248 
1249@Serializable
1250data class ForecastPeriod(
1251 val name: String? = null,
1252 val temperature: Int? = null,
1253 val temperatureUnit: String? = null,
1254 val windSpeed: String? = null,
1255 val windDirection: String? = null,
1256 val shortForecast: String? = null,
1257)
1258 
1259@Serializable
1260data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261 
1262@Serializable
1263data class AlertFeature(val properties: AlertProperties)
1264 
1265@Serializable
1266data class AlertProperties(
1267 val event: String? = null,
1268 val areaDesc: String? = null,
1269 val severity: String? = null,
1270 val status: String? = null,
1271 val headline: String? = null,
1272)
1273```
1274 
1275### Implementing tool execution
1276 
1277The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1278 
1279```kotlin theme={null}
1280// Register weather tools
1281 
1282server.addTool(
1283 name = "get_alerts",
1284 description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1285 inputSchema = ToolSchema(
1286 properties = buildJsonObject {
1287 putJsonObject("state") {
1288 put("type", "string")
1289 put("description", "Two-letter US state code (e.g. CA, NY)")
1290 }
1291 },
1292 required = listOf("state"),
1293 ),
1294) { request ->
1295 val state = request.arguments?.get("state")?.jsonPrimitive?.content
1296 ?: return@addTool CallToolResult(
1297 content = listOf(TextContent("The 'state' parameter is required.")),
1298 )
1299 
1300 val alerts = httpClient.getAlerts(state)
1301 CallToolResult(content = alerts.map { TextContent(it) })
1302}
1303 
1304server.addTool(
1305 name = "get_forecast",
1306 description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1307 inputSchema = ToolSchema(
1308 properties = buildJsonObject {
1309 putJsonObject("latitude") {
1310 put("type", "number")
1311 put("description", "Latitude of the location")
1312 }
1313 putJsonObject("longitude") {
1314 put("type", "number")
1315 put("description", "Longitude of the location")
1316 }
1317 },
1318 required = listOf("latitude", "longitude"),
1319 ),
1320) { request ->
1321 val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1322 val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1323 if (latitude == null || longitude == null) {
1324 return@addTool CallToolResult(
1325 content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1326 )
1327 }
1328 
1329 val forecast = httpClient.getForecast(latitude, longitude)
1330 CallToolResult(content = forecast.map { TextContent(it) })
1331}
1332```
1333 
1334### Running the server
1335 
1336Finally, implement the main function to run the server:
1337 
1338```kotlin theme={null}
1339fun main() = runMcpServer()
1340```
1341 
1342You can run the server directly during development:
1343 
1344```bash theme={null}
1345./gradlew run
1346```
1347 
1348For production use, build the shadow JAR:
1349 
1350```bash theme={null}
1351./gradlew build
1352java -jar build/libs/weather-0.1.0-all.jar
1353```
1354 
1355Let's now test your server from an existing MCP host, Claude for Desktop.
1356 
1357## Testing your server with Claude for Desktop
1358 
1359> [!NOTE]
1360>
1361> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362 
1363First, make sure you have Claude for Desktop installed. [You can install the latest version
1364here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365 
1366We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368Make sure to create the file if it doesn't exist.
1369 
1370For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371 
1372```bash macOS/Linux theme={null}
1373code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374```
1375 
1376```powershell Windows theme={null}
1377code $env:AppData\Claude\claude_desktop_config.json
1378```
1379 
1380You'll then add your servers in the `mcpServers` key.
1381The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382 
1383In this case, we'll add our single weather server like so:
1384 
1385```json macOS/Linux theme={null}
1386{
1387 "mcpServers": {
1388 "weather": {
1389 "command": "java",
1390 "args": [
1391 "-jar",
1392 "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1393 ]
1394 }
1395 }
1396}
1397```
1398 
1399```json Windows theme={null}
1400{
1401 "mcpServers": {
1402 "weather": {
1403 "command": "java",
1404 "args": [
1405 "-jar",
1406 "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1407 ]
1408 }
1409 }
1410}
1411```
1412 
1413This tells Claude for Desktop:
1414 
14151. There's an MCP server named "weather"
14162. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417 
1418Save the file, and restart **Claude for Desktop**.
1419 
1420**C#**
1421 
1422Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1423 
1424### Prerequisite knowledge
1425 
1426This quickstart assumes you have familiarity with:
1427 
1428* C#
1429* LLMs like Claude
1430* .NET 8 or higher
1431 
1432### Logging in MCP Servers
1433 
1434When implementing MCP servers, be careful about how you handle logging:
1435 
1436**For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1437 
1438**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1439 
1440### Best Practices
1441 
1442* Use a logging library that writes to stderr or files.
1443 
1444### System requirements
1445 
1446* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1447 
1448### Set up your environment
1449 
1450First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1451 
1452```bash theme={null}
1453dotnet --version
1454```
1455 
1456Now, let's create and set up your project:
1457 
1458```bash macOS/Linux theme={null}
1459# Create a new directory for our project
1460mkdir weather
1461cd weather
1462# Initialize a new C# project
1463dotnet new console
1464```
1465 
1466```powershell Windows theme={null}
1467# Create a new directory for our project
1468mkdir weather
1469cd weather
1470# Initialize a new C# project
1471dotnet new console
1472```
1473 
1474After running `dotnet new console`, you will be presented with a new C# project.
1475You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1477After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1478 
1479```bash theme={null}
1480# Add the Model Context Protocol SDK NuGet package
1481dotnet add package ModelContextProtocol --prerelease
1482# Add the .NET Hosting NuGet package
1483dotnet add package Microsoft.Extensions.Hosting
1484```
1485 
1486Now let’s dive into building your server.
1487 
1488## Building your server
1489 
1490Open the `Program.cs` file in your project and replace its contents with the following code:
1491 
1492```csharp theme={null}
1493using Microsoft.Extensions.DependencyInjection;
1494using Microsoft.Extensions.Hosting;
1495using ModelContextProtocol;
1496using System.Net.Http.Headers;
1497 
1498var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499 
1500builder.Services.AddMcpServer()
1501 .WithStdioServerTransport()
1502 .WithToolsFromAssembly();
1503 
1504builder.Services.AddSingleton(_ =>
1505{
1506 var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1507 client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1508 return client;
1509});
1510 
1511var app = builder.Build();
1512 
1513await app.RunAsync();
1514```
1515 
1516> [!NOTE]
1517>
1518> When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1519 
1520This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1521 
1522### Weather API helper functions
1523 
1524Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525 
1526```csharp theme={null}
1527using System.Text.Json;
1528 
1529internal static class HttpClientExt
1530{
1531 public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1532 {
1533 using var response = await client.GetAsync(requestUri);
1534 response.EnsureSuccessStatusCode();
1535 return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1536 }
1537}
1538```
1539 
1540Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541 
1542```csharp theme={null}
1543using ModelContextProtocol.Server;
1544using System.ComponentModel;
1545using System.Globalization;
1546using System.Text.Json;
1547 
1548namespace QuickstartWeatherServer.Tools;
1549 
1550[McpServerToolType]
1551public static class WeatherTools
1552{
1553 [McpServerTool, Description("Get weather alerts for a US state code.")]
1554 public static async Task<string> GetAlerts(
1555 HttpClient client,
1556 [Description("The US state code to get alerts for.")] string state)
1557 {
1558 using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1559 var jsonElement = jsonDocument.RootElement;
1560 var alerts = jsonElement.GetProperty("features").EnumerateArray();
1561 
1562 if (!alerts.Any())
1563 {
1564 return "No active alerts for this state.";
1565 }
1566 
1567 return string.Join("\n--\n", alerts.Select(alert =>
1568 {
1569 JsonElement properties = alert.GetProperty("properties");
1570 return $"""
1571 Event: {properties.GetProperty("event").GetString()}
1572 Area: {properties.GetProperty("areaDesc").GetString()}
1573 Severity: {properties.GetProperty("severity").GetString()}
1574 Description: {properties.GetProperty("description").GetString()}
1575 Instruction: {properties.GetProperty("instruction").GetString()}
1576 """;
1577 }));
1578 }
1579 
1580 [McpServerTool, Description("Get weather forecast for a location.")]
1581 public static async Task<string> GetForecast(
1582 HttpClient client,
1583 [Description("Latitude of the location.")] double latitude,
1584 [Description("Longitude of the location.")] double longitude)
1585 {
1586 var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1587 using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1588 var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1589 ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1590 
1591 using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1592 var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1593 
1594 return string.Join("\n---\n", periods.Select(period => $"""
1595 {period.GetProperty("name").GetString()}
1596 Temperature: {period.GetProperty("temperature").GetInt32()}°F
1597 Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1598 Forecast: {period.GetProperty("detailedForecast").GetString()}
1599 """));
1600 }
1601}
1602```
1603 
1604### Running the server
1605 
1606Finally, run the server using the following command:
1607 
1608```bash theme={null}
1609dotnet run
1610```
1611 
1612This will start the server and listen for incoming requests on standard input/output.
1613 
1614## Testing your server with Claude for Desktop
1615 
1616> [!NOTE]
1617>
1618> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619 
1620First, make sure you have Claude for Desktop installed. [You can install the latest version
1621here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1623For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624 
1625```bash macOS/Linux theme={null}
1626code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627```
1628 
1629```powershell Windows theme={null}
1630code $env:AppData\Claude\claude_desktop_config.json
1631```
1632 
1633You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1634In this case, we'll add our single weather server like so:
1635 
1636```json macOS/Linux theme={null}
1637{
1638 "mcpServers": {
1639 "weather": {
1640 "command": "dotnet",
1641 "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1642 }
1643 }
1644}
1645```
1646 
1647```json Windows theme={null}
1648{
1649 "mcpServers": {
1650 "weather": {
1651 "command": "dotnet",
1652 "args": [
1653 "run",
1654 "--project",
1655 "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1656 "--no-build"
1657 ]
1658 }
1659 }
1660}
1661```
1662 
1663This tells Claude for Desktop:
1664 
16651. There's an MCP server named "weather"
16662. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667 Save the file, and restart **Claude for Desktop**.
1668 
1669**Ruby**
1670 
1671Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1672 
1673### Prerequisite knowledge
1674 
1675This quickstart assumes you have familiarity with:
1676 
1677* Ruby
1678* LLMs like Claude
1679 
1680### Logging in MCP Servers
1681 
1682When implementing MCP servers, be careful about how you handle logging:
1683 
1684**For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1685 
1686**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1687 
1688### Best Practices
1689 
1690* Use a logging library that writes to stderr or files.
1691 
1692### Quick Examples
1693 
1694```ruby theme={null}
1695# ❌ Bad (STDIO)
1696puts "Processing request"
1697 
1698# ✅ Good (STDIO)
1699require "logger"
1700logger = Logger.new($stderr)
1701logger.info("Processing request")
1702```
1703 
1704### System requirements
1705 
1706* Ruby 2.7 or higher installed.
1707 
1708### Set up your environment
1709 
1710First, let's make sure you have Ruby installed. You can check by running:
1711 
1712```bash theme={null}
1713ruby --version
1714```
1715 
1716Now, let's create and set up our project:
1717 
1718```bash macOS/Linux theme={null}
1719# Create a new directory for our project
1720mkdir weather
1721cd weather
1722 
1723# Create a Gemfile
1724bundle init
1725 
1726# Add the MCP SDK dependency
1727bundle add mcp
1728 
1729# Create our server file
1730touch weather.rb
1731```
1732 
1733```powershell Windows theme={null}
1734# Create a new directory for our project
1735mkdir weather
1736cd weather
1737 
1738# Create a Gemfile
1739bundle init
1740 
1741# Add the MCP SDK dependency
1742bundle add mcp
1743 
1744# Create our server file
1745new-item weather.rb
1746```
1747 
1748Now let's dive into building your server.
1749 
1750## Building your server
1751 
1752### Importing packages and setting up constants
1753 
1754Open `weather.rb` and add these requires and constants at the top:
1755 
1756```ruby theme={null}
1757require "json"
1758require "mcp"
1759require "net/http"
1760require "uri"
1761 
1762NWS_API_BASE = "https://api.weather.gov"
1763USER_AGENT = "weather-app/1.0"
1764```
1765 
1766The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767 
1768### Helper methods
1769 
1770Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771 
1772```ruby theme={null}
1773module HelperMethods
1774 def make_nws_request(url)
1775 uri = URI(url)
1776 request = Net::HTTP::Get.new(uri)
1777 request["User-Agent"] = USER_AGENT
1778 request["Accept"] = "application/geo+json"
1779 
1780 response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1781 http.request(request)
1782 end
1783 
1784 raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1785 
1786 JSON.parse(response.body)
1787 end
1788 
1789 def format_alert(feature)
1790 properties = feature["properties"]
1791 
1792 <<~ALERT
1793 Event: #{properties["event"] || "Unknown"}
1794 Area: #{properties["areaDesc"] || "Unknown"}
1795 Severity: #{properties["severity"] || "Unknown"}
1796 Description: #{properties["description"] || "No description available"}
1797 Instructions: #{properties["instruction"] || "No specific instructions provided"}
1798 ALERT
1799 end
1800end
1801```
1802 
1803### Implementing tool execution
1804 
1805Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806 
1807```ruby theme={null}
1808class GetAlerts < MCP::Tool
1809 extend HelperMethods
1810 
1811 tool_name "get_alerts"
1812 description "Get weather alerts for a US state"
1813 input_schema(
1814 properties: {
1815 state: {
1816 type: "string",
1817 description: "Two-letter US state code (e.g. CA, NY)"
1818 }
1819 },
1820 required: ["state"]
1821 )
1822 
1823 def self.call(state:)
1824 url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1825 data = make_nws_request(url)
1826 
1827 if data["features"].empty?
1828 return MCP::Tool::Response.new([{
1829 type: "text",
1830 text: "No active alerts for this state."
1831 }])
1832 end
1833 
1834 alerts = data["features"].map { |feature| format_alert(feature) }
1835 MCP::Tool::Response.new([{
1836 type: "text",
1837 text: alerts.join("\n---\n")
1838 }])
1839 end
1840end
1841 
1842class GetForecast < MCP::Tool
1843 extend HelperMethods
1844 
1845 tool_name "get_forecast"
1846 description "Get weather forecast for a location"
1847 input_schema(
1848 properties: {
1849 latitude: {
1850 type: "number",
1851 description: "Latitude of the location"
1852 },
1853 longitude: {
1854 type: "number",
1855 description: "Longitude of the location"
1856 }
1857 },
1858 required: ["latitude", "longitude"]
1859 )
1860 
1861 def self.call(latitude:, longitude:)
1862 # First get the forecast grid endpoint.
1863 points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1864 points_data = make_nws_request(points_url)
1865 
1866 # Get the forecast URL from the points response.
1867 forecast_url = points_data["properties"]["forecast"]
1868 forecast_data = make_nws_request(forecast_url)
1869 
1870 # Format the periods into a readable forecast.
1871 periods = forecast_data["properties"]["periods"]
1872 forecasts = periods.first(5).map do |period|
1873 <<~FORECAST
1874 #{period["name"]}:
1875 Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1876 Wind: #{period["windSpeed"]} #{period["windDirection"]}
1877 Forecast: #{period["detailedForecast"]}
1878 FORECAST
1879 end
1880 
1881 MCP::Tool::Response.new([{
1882 type: "text",
1883 text: forecasts.join("\n---\n")
1884 }])
1885 end
1886end
1887```
1888 
1889### Running the server
1890 
1891Finally, initialize and run the server:
1892 
1893```ruby theme={null}
1894server = MCP::Server.new(
1895 name: "weather",
1896 version: "1.0.0",
1897 tools: [GetAlerts, GetForecast]
1898)
1899 
1900transport = MCP::Server::Transports::StdioTransport.new(server)
1901transport.open
1902```
1903 
1904Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905 
1906Let's now test your server from an existing MCP host, Claude for Desktop.
1907 
1908## Testing your server with Claude for Desktop
1909 
1910> [!NOTE]
1911>
1912> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913 
1914First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1915 
1916We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1917 
1918For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919 
1920```bash macOS/Linux theme={null}
1921code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922```
1923 
1924```powershell Windows theme={null}
1925code $env:AppData\Claude\claude_desktop_config.json
1926```
1927 
1928You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1929 
1930In this case, we'll add our single weather server like so:
1931 
1932```json macOS/Linux theme={null}
1933{
1934 "mcpServers": {
1935 "weather": {
1936 "command": "bundle",
1937 "args": ["exec", "ruby", "weather.rb"],
1938 "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1939 }
1940 }
1941}
1942```
1943 
1944```json Windows theme={null}
1945{
1946 "mcpServers": {
1947 "weather": {
1948 "command": "bundle",
1949 "args": ["exec", "ruby", "weather.rb"],
1950 "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1951 }
1952 }
1953}
1954```
1955 
1956> [!NOTE]
1957>
1958> Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1959 
1960This tells Claude for Desktop:
1961 
19621. There's an MCP server named "weather"
19632. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964 
1965Save the file, and restart **Claude for Desktop**.
1966 
1967**Rust**
1968 
1969Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1970 
1971### Prerequisite knowledge
1972 
1973This quickstart assumes you have familiarity with:
1974 
1975* Rust programming language
1976* Async/await in Rust
1977* LLMs like Claude
1978 
1979### Logging in MCP Servers
1980 
1981When implementing MCP servers, be careful about how you handle logging:
1982 
1983**For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1984 
1985**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1986 
1987### Best Practices
1988 
1989* Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1990* Configure your logging framework to avoid stdout output.
1991 
1992### Quick Examples
1993 
1994```rust theme={null}
1995// ❌ Bad (STDIO)
1996println!("Processing request");
1997 
1998// ✅ Good (STDIO)
1999eprintln!("Processing request"); // writes to stderr
2000```
2001 
2002### System requirements
2003 
2004* Rust 1.70 or higher installed.
2005* Cargo (comes with Rust installation).
2006 
2007### Set up your environment
2008 
2009First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2010 
2011```bash macOS/Linux theme={null}
2012curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2013```
2014 
2015```powershell Windows theme={null}
2016# Download and run rustup-init.exe from https://rustup.rs/
2017```
2018 
2019Verify your Rust installation:
2020 
2021```bash theme={null}
2022rustc --version
2023cargo --version
2024```
2025 
2026Now, let's create and set up our project:
2027 
2028```bash macOS/Linux theme={null}
2029# Create a new Rust project
2030cargo new weather
2031cd weather
2032```
2033 
2034```powershell Windows theme={null}
2035# Create a new Rust project
2036cargo new weather
2037cd weather
2038```
2039 
2040Update your `Cargo.toml` to add the required dependencies:
2041 
2042```toml Cargo.toml theme={null}
2043[package]
2044name = "weather"
2045version = "0.1.0"
2046edition = "2024"
2047 
2048[dependencies]
2049rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050tokio = { version = "1.46", features = ["full"] }
2051reqwest = { version = "0.12", features = ["json"] }
2052serde = { version = "1.0", features = ["derive"] }
2053serde_json = "1.0"
2054anyhow = "1.0"
2055tracing = "0.1"
2056tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057```
2058 
2059Now let's dive into building your server.
2060 
2061## Building your server
2062 
2063### Importing packages and constants
2064 
2065Open `src/main.rs` and add these imports and constants at the top:
2066 
2067```rust theme={null}
2068use anyhow::Result;
2069use rmcp::{
2070 ServerHandler, ServiceExt,
2071 handler::server::{router::tool::ToolRouter, tool::Parameters},
2072 model::*,
2073 schemars, tool, tool_handler, tool_router,
2074};
2075use serde::Deserialize;
2076use serde::de::DeserializeOwned;
2077 
2078const NWS_API_BASE: &str = "https://api.weather.gov";
2079const USER_AGENT: &str = "weather-app/1.0";
2080```
2081 
2082The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2083 
2084### Data structures
2085 
2086Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087 
2088```rust theme={null}
2089#[derive(Debug, Deserialize)]
2090struct AlertsResponse {
2091 features: Vec<AlertFeature>,
2092}
2093 
2094#[derive(Debug, Deserialize)]
2095struct AlertFeature {
2096 properties: AlertProperties,
2097}
2098 
2099#[derive(Debug, Deserialize)]
2100struct AlertProperties {
2101 event: Option<String>,
2102 #[serde(rename = "areaDesc")]
2103 area_desc: Option<String>,
2104 severity: Option<String>,
2105 description: Option<String>,
2106 instruction: Option<String>,
2107}
2108 
2109#[derive(Debug, Deserialize)]
2110struct PointsResponse {
2111 properties: PointsProperties,
2112}
2113 
2114#[derive(Debug, Deserialize)]
2115struct PointsProperties {
2116 forecast: String,
2117}
2118 
2119#[derive(Debug, Deserialize)]
2120struct ForecastResponse {
2121 properties: ForecastProperties,
2122}
2123 
2124#[derive(Debug, Deserialize)]
2125struct ForecastProperties {
2126 periods: Vec<ForecastPeriod>,
2127}
2128 
2129#[derive(Debug, Deserialize)]
2130struct ForecastPeriod {
2131 name: String,
2132 temperature: i32,
2133 #[serde(rename = "temperatureUnit")]
2134 temperature_unit: String,
2135 #[serde(rename = "windSpeed")]
2136 wind_speed: String,
2137 #[serde(rename = "windDirection")]
2138 wind_direction: String,
2139 #[serde(rename = "detailedForecast")]
2140 detailed_forecast: String,
2141}
2142```
2143 
2144Now define the request types that MCP clients will send:
2145 
2146```rust theme={null}
2147#[derive(serde::Deserialize, schemars::JsonSchema)]
2148pub struct MCPForecastRequest {
2149 latitude: f32,
2150 longitude: f32,
2151}
2152 
2153#[derive(serde::Deserialize, schemars::JsonSchema)]
2154pub struct MCPAlertRequest {
2155 state: String,
2156}
2157```
2158 
2159### Helper functions
2160 
2161Add helper functions for making API requests and formatting responses:
2162 
2163```rust theme={null}
2164async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2165 let client = reqwest::Client::new();
2166 let rsp = client
2167 .get(url)
2168 .header(reqwest::header::USER_AGENT, USER_AGENT)
2169 .header(reqwest::header::ACCEPT, "application/geo+json")
2170 .send()
2171 .await?
2172 .error_for_status()?;
2173 Ok(rsp.json::<T>().await?)
2174}
2175 
2176fn format_alert(feature: &AlertFeature) -> String {
2177 let props = &feature.properties;
2178 format!(
2179 "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2180 props.event.as_deref().unwrap_or("Unknown"),
2181 props.area_desc.as_deref().unwrap_or("Unknown"),
2182 props.severity.as_deref().unwrap_or("Unknown"),
2183 props
2184 .description
2185 .as_deref()
2186 .unwrap_or("No description available"),
2187 props
2188 .instruction
2189 .as_deref()
2190 .unwrap_or("No specific instructions provided")
2191 )
2192}
2193 
2194fn format_period(period: &ForecastPeriod) -> String {
2195 format!(
2196 "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2197 period.name,
2198 period.temperature,
2199 period.temperature_unit,
2200 period.wind_speed,
2201 period.wind_direction,
2202 period.detailed_forecast
2203 )
2204}
2205```
2206 
2207### Implementing the Weather server and tools
2208 
2209Now let's implement the main Weather server struct with the tool handlers:
2210 
2211```rust theme={null}
2212pub struct Weather {
2213 tool_router: ToolRouter<Weather>,
2214}
2215 
2216#[tool_router]
2217impl Weather {
2218 fn new() -> Self {
2219 Self {
2220 tool_router: Self::tool_router(),
2221 }
2222 }
2223 
2224 #[tool(description = "Get weather alerts for a US state.")]
2225 async fn get_alerts(
2226 &self,
2227 Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2228 ) -> String {
2229 let url = format!(
2230 "{}/alerts/active/area/{}",
2231 NWS_API_BASE,
2232 state.to_uppercase()
2233 );
2234 
2235 match make_nws_request::<AlertsResponse>(&url).await {
2236 Ok(data) => {
2237 if data.features.is_empty() {
2238 "No active alerts for this state.".to_string()
2239 } else {
2240 data.features
2241 .iter()
2242 .map(format_alert)
2243 .collect::<Vec<_>>()
2244 .join("\n---\n")
2245 }
2246 }
2247 Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2248 }
2249 }
2250 
2251 #[tool(description = "Get weather forecast for a location.")]
2252 async fn get_forecast(
2253 &self,
2254 Parameters(MCPForecastRequest {
2255 latitude,
2256 longitude,
2257 }): Parameters<MCPForecastRequest>,
2258 ) -> String {
2259 let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2260 let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2261 return "Unable to fetch forecast data for this location.".to_string();
2262 };
2263 
2264 let forecast_url = points_data.properties.forecast;
2265 
2266 let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2267 return "Unable to fetch forecast data for this location.".to_string();
2268 };
2269 
2270 let periods = &forecast_data.properties.periods;
2271 let forecast_summary: String = periods
2272 .iter()
2273 .take(5) // Next 5 periods only
2274 .map(format_period)
2275 .collect::<Vec<String>>()
2276 .join("\n---\n");
2277 forecast_summary
2278 }
2279}
2280```
2281 
2282The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283 
2284### Implementing the ServerHandler
2285 
2286Implement the `ServerHandler` trait to define server capabilities:
2287 
2288```rust theme={null}
2289#[tool_handler]
2290impl ServerHandler for Weather {
2291 fn get_info(&self) -> ServerInfo {
2292 ServerInfo {
2293 capabilities: ServerCapabilities::builder().enable_tools().build(),
2294 ..Default::default()
2295 }
2296 }
2297}
2298```
2299 
2300### Running the server
2301 
2302Finally, implement the main function to run the server with stdio transport:
2303 
2304```rust theme={null}
2305#[tokio::main]
2306async fn main() -> Result<()> {
2307 let transport = (tokio::io::stdin(), tokio::io::stdout());
2308 let service = Weather::new().serve(transport).await?;
2309 service.waiting().await?;
2310 Ok(())
2311}
2312```
2313 
2314Build your server with:
2315 
2316```bash theme={null}
2317cargo build --release
2318```
2319 
2320The compiled binary will be in `target/release/weather`.
2321 
2322Let's now test your server from an existing MCP host, Claude for Desktop.
2323 
2324## Testing your server with Claude for Desktop
2325 
2326> [!NOTE]
2327>
2328> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329 
2330First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2331 
2332We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2333 
2334For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335 
2336```bash macOS/Linux theme={null}
2337code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338```
2339 
2340```powershell Windows theme={null}
2341code $env:AppData\Claude\claude_desktop_config.json
2342```
2343 
2344You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2345 
2346In this case, we'll add our single weather server like so:
2347 
2348```json macOS/Linux theme={null}
2349{
2350 "mcpServers": {
2351 "weather": {
2352 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2353 }
2354 }
2355}
2356```
2357 
2358```json Windows theme={null}
2359{
2360 "mcpServers": {
2361 "weather": {
2362 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2363 }
2364 }
2365}
2366```
2367 
2368> [!NOTE]
2369>
2370> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2371 
2372This tells Claude for Desktop:
2373 
23741. There's an MCP server named "weather"
23752. Launch it by running the compiled binary at the specified path
2376 
2377Save the file, and restart **Claude for Desktop**.
2378 
2379**Go**
2380 
2381Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2382 
2383### Prerequisite knowledge
2384 
2385This quickstart assumes you have familiarity with:
2386 
2387* Go
2388* LLMs like Claude
2389 
2390### Logging in MCP Servers
2391 
2392When implementing MCP servers, be careful about how you handle logging:
2393 
2394**For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2395 
2396**For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2397 
2398### Best Practices
2399 
2400* Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2401* Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2402 
2403### Quick Examples
2404 
2405```go theme={null}
2406// ❌ Bad (STDIO)
2407fmt.Println("Processing request")
2408 
2409// ✅ Good (STDIO)
2410log.Println("Processing request") // defaults to stderr
2411 
2412// ✅ Good (STDIO)
2413fmt.Fprintln(os.Stderr, "Processing request")
2414```
2415 
2416### System requirements
2417 
2418* Go 1.24 or higher installed.
2419 
2420### Set up your environment
2421 
2422First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423 
2424Verify your Go installation:
2425 
2426```bash theme={null}
2427go version
2428```
2429 
2430Now, let's create and set up our project:
2431 
2432```bash macOS/Linux theme={null}
2433# Create a new directory for our project
2434mkdir weather
2435cd weather
2436 
2437# Initialize Go module
2438go mod init weather
2439 
2440# Install dependencies
2441go get github.com/modelcontextprotocol/go-sdk/mcp
2442 
2443# Create our server file
2444touch main.go
2445```
2446 
2447```powershell Windows theme={null}
2448# Create a new directory for our project
2449md weather
2450cd weather
2451 
2452# Initialize Go module
2453go mod init weather
2454 
2455# Install dependencies
2456go get github.com/modelcontextprotocol/go-sdk/mcp
2457 
2458# Create our server file
2459new-item main.go
2460```
2461 
2462Now let's dive into building your server.
2463 
2464## Building your server
2465 
2466### Importing packages and constants
2467 
2468Add these to the top of your `main.go`:
2469 
2470```go theme={null}
2471package main
2472 
2473import (
2474 "cmp"
2475 "context"
2476 "encoding/json"
2477 "fmt"
2478 "io"
2479 "log"
2480 "net/http"
2481 "strings"
2482 
2483 "github.com/modelcontextprotocol/go-sdk/mcp"
2484)
2485 
2486const (
2487 NWSAPIBase = "https://api.weather.gov"
2488 UserAgent = "weather-app/1.0"
2489)
2490```
2491 
2492### Data structures
2493 
2494Next, let's define the data structures used by our tools:
2495 
2496```go theme={null}
2497type PointsResponse struct {
2498 Properties struct {
2499 Forecast string `json:"forecast"`
2500 } `json:"properties"`
2501}
2502 
2503type ForecastResponse struct {
2504 Properties struct {
2505 Periods []ForecastPeriod `json:"periods"`
2506 } `json:"properties"`
2507}
2508 
2509type ForecastPeriod struct {
2510 Name string `json:"name"`
2511 Temperature int `json:"temperature"`
2512 TemperatureUnit string `json:"temperatureUnit"`
2513 WindSpeed string `json:"windSpeed"`
2514 WindDirection string `json:"windDirection"`
2515 DetailedForecast string `json:"detailedForecast"`
2516}
2517 
2518type AlertsResponse struct {
2519 Features []AlertFeature `json:"features"`
2520}
2521 
2522type AlertFeature struct {
2523 Properties AlertProperties `json:"properties"`
2524}
2525 
2526type AlertProperties struct {
2527 Event string `json:"event"`
2528 AreaDesc string `json:"areaDesc"`
2529 Severity string `json:"severity"`
2530 Description string `json:"description"`
2531 Instruction string `json:"instruction"`
2532}
2533 
2534type ForecastInput struct {
2535 Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536 Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537}
2538 
2539type AlertsInput struct {
2540 State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541}
2542```
2543 
2544### Helper functions
2545 
2546Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547 
2548```go theme={null}
2549func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2550 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2551 if err != nil {
2552 return nil, fmt.Errorf("failed to create request: %w", err)
2553 }
2554 
2555 req.Header.Set("User-Agent", UserAgent)
2556 req.Header.Set("Accept", "application/geo+json")
2557 
2558 client := http.DefaultClient
2559 resp, err := client.Do(req)
2560 if err != nil {
2561 return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2562 }
2563 defer resp.Body.Close()
2564 
2565 if resp.StatusCode != http.StatusOK {
2566 body, _ := io.ReadAll(resp.Body)
2567 return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2568 }
2569 
2570 var result T
2571 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2572 return nil, fmt.Errorf("failed to decode response: %w", err)
2573 }
2574 
2575 return &result, nil
2576}
2577 
2578func formatAlert(alert AlertFeature) string {
2579 props := alert.Properties
2580 event := cmp.Or(props.Event, "Unknown")
2581 areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2582 severity := cmp.Or(props.Severity, "Unknown")
2583 description := cmp.Or(props.Description, "No description available")
2584 instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2585 
2586 return fmt.Sprintf(`
2587Event: %s
2588Area: %s
2589Severity: %s
2590Description: %s
2591Instructions: %s
2592`, event, areaDesc, severity, description, instruction)
2593}
2594 
2595func formatPeriod(period ForecastPeriod) string {
2596 return fmt.Sprintf(`
2597%s:
2598Temperature: %d°%s
2599Wind: %s %s
2600Forecast: %s
2601`, period.Name, period.Temperature, period.TemperatureUnit,
2602 period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603}
2604```
2605 
2606### Implementing tool execution
2607 
2608The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609 
2610```go theme={null}
2611func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2612 *mcp.CallToolResult, any, error,
2613) {
2614 // Get points data
2615 pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2616 pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2617 if err != nil {
2618 return &mcp.CallToolResult{
2619 Content: []mcp.Content{
2620 &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2621 },
2622 }, nil, nil
2623 }
2624 
2625 // Get forecast data
2626 forecastURL := pointsData.Properties.Forecast
2627 if forecastURL == "" {
2628 return &mcp.CallToolResult{
2629 Content: []mcp.Content{
2630 &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2631 },
2632 }, nil, nil
2633 }
2634 
2635 forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2636 if err != nil {
2637 return &mcp.CallToolResult{
2638 Content: []mcp.Content{
2639 &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2640 },
2641 }, nil, nil
2642 }
2643 
2644 // Format the periods
2645 periods := forecastData.Properties.Periods
2646 if len(periods) == 0 {
2647 return &mcp.CallToolResult{
2648 Content: []mcp.Content{
2649 &mcp.TextContent{Text: "No forecast periods available."},
2650 },
2651 }, nil, nil
2652 }
2653 
2654 // Show next 5 periods
2655 var forecasts []string
2656 for i := range min(5, len(periods)) {
2657 forecasts = append(forecasts, formatPeriod(periods[i]))
2658 }
2659 
2660 result := strings.Join(forecasts, "\n---\n")
2661 
2662 return &mcp.CallToolResult{
2663 Content: []mcp.Content{
2664 &mcp.TextContent{Text: result},
2665 },
2666 }, nil, nil
2667}
2668 
2669func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2670 *mcp.CallToolResult, any, error,
2671) {
2672 // Build alerts URL
2673 stateCode := strings.ToUpper(input.State)
2674 alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2675 
2676 alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2677 if err != nil {
2678 return &mcp.CallToolResult{
2679 Content: []mcp.Content{
2680 &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2681 },
2682 }, nil, nil
2683 }
2684 
2685 // Check if there are any alerts
2686 if len(alertsData.Features) == 0 {
2687 return &mcp.CallToolResult{
2688 Content: []mcp.Content{
2689 &mcp.TextContent{Text: "No active alerts for this state."},
2690 },
2691 }, nil, nil
2692 }
2693 
2694 // Format alerts
2695 var alerts []string
2696 for _, feature := range alertsData.Features {
2697 alerts = append(alerts, formatAlert(feature))
2698 }
2699 
2700 result := strings.Join(alerts, "\n---\n")
2701 
2702 return &mcp.CallToolResult{
2703 Content: []mcp.Content{
2704 &mcp.TextContent{Text: result},
2705 },
2706 }, nil, nil
2707}
2708```
2709 
2710### Running the server
2711 
2712Finally, implement the main function to run the server:
2713 
2714```go theme={null}
2715func main() {
2716 // Create MCP server
2717 server := mcp.NewServer(&mcp.Implementation{
2718 Name: "weather",
2719 Version: "1.0.0",
2720 }, nil)
2721 
2722 // Add get_forecast tool
2723 mcp.AddTool(server, &mcp.Tool{
2724 Name: "get_forecast",
2725 Description: "Get weather forecast for a location",
2726 }, getForecast)
2727 
2728 // Add get_alerts tool
2729 mcp.AddTool(server, &mcp.Tool{
2730 Name: "get_alerts",
2731 Description: "Get weather alerts for a US state",
2732 }, getAlerts)
2733 
2734 // Run server on stdio transport
2735 if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2736 log.Fatal(err)
2737 }
2738}
2739```
2740 
2741Build your server with:
2742 
2743```bash theme={null}
2744go build -o weather .
2745```
2746 
2747The compiled binary will be in `./weather`.
2748 
2749Let's now test your server from an existing MCP host, Claude for Desktop.
2750 
2751## Testing your server with Claude for Desktop
2752 
2753> [!NOTE]
2754>
2755> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756 
2757First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2758 
2759We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2760 
2761For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762 
2763```bash macOS/Linux theme={null}
2764code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765```
2766 
2767```powershell Windows theme={null}
2768code $env:AppData\Claude\claude_desktop_config.json
2769```
2770 
2771You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2772 
2773In this case, we'll add our single weather server like so:
2774 
2775```json macOS/Linux theme={null}
2776{
2777 "mcpServers": {
2778 "weather": {
2779 "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2780 }
2781 }
2782}
2783```
2784 
2785```json Windows theme={null}
2786{
2787 "mcpServers": {
2788 "weather": {
2789 "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2790 }
2791 }
2792}
2793```
2794 
2795> [!NOTE]
2796>
2797> Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2798 
2799This tells Claude for Desktop:
2800 
28011. There's an MCP server named "weather"
28022. Launch it by running the compiled binary at the specified path
2803 
2804Save the file, and restart **Claude for Desktop**.
27> [tab] Python
28>
29> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30>
31> ### Prerequisite knowledge
32>
33> This quickstart assumes you have familiarity with:
34>
35> * Python
36> * LLMs like Claude
37>
38> ### Logging in MCP Servers
39>
40> When implementing MCP servers, be careful about how you handle logging:
41>
42> **For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, but can be used safely with `file=sys.stderr`.
43>
44> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
45>
46> ### Best Practices
47>
48> * Use a logging library that writes to stderr or files.
49>
50> ### Quick Examples
51>
52> ```python theme={null}
53> import sys
54> import logging
55>
56> # ❌ Bad (STDIO)
57> print("Processing request")
58>
59> # ✅ Good (STDIO)
60> print("Processing request", file=sys.stderr)
61>
62> # ✅ Good (STDIO)
63> logging.info("Processing request")
64> ```
65>
66> ### System requirements
67>
68> * Python 3.10 or higher installed.
69> * You must use the Python MCP SDK 1.2.0 or higher.
70>
71> ### Set up your environment
72>
73> First, let's install `uv` and set up our Python project and environment:
74>
75> ```bash macOS/Linux theme={null}
76> curl -LsSf https://astral.sh/uv/install.sh | sh
77> ```
78>
79> ```powershell Windows theme={null}
80> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
81> ```
82>
83> Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
84>
85> Now, let's create and set up our project:
86>
87> ```bash macOS/Linux theme={null}
88> # Create a new directory for our project
89> uv init weather
90> cd weather
91>
92> # Create virtual environment and activate it
93> uv venv
94> source .venv/bin/activate
95>
96> # Install dependencies
97> uv add "mcp[cli]" httpx
98>
99> # Create our server file
100> touch weather.py
101> ```
102>
103> ```powershell Windows theme={null}
104> # Create a new directory for our project
105> uv init weather
106> cd weather
107>
108> # Create virtual environment and activate it
109> uv venv
110> .venv\Scripts\activate
111>
112> # Install dependencies
113> uv add mcp[cli] httpx
114>
115> # Create our server file
116> new-item weather.py
117> ```
118>
119> Now let's dive into building your server.
120>
121> ## Building your server
122>
123> ### Importing packages and setting up the instance
124>
125> Add these to the top of your `weather.py`:
126>
127> ```python theme={null}
128> from typing import Any
129>
130> import httpx
131> from mcp.server.fastmcp import FastMCP
132>
133> # Initialize FastMCP server
134> mcp = FastMCP("weather")
135>
136> # Constants
137> NWS_API_BASE = "https://api.weather.gov"
138> USER_AGENT = "weather-app/1.0"
139> ```
140>
141> The FastMCP class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.
142>
143> ### Helper functions
144>
145> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
146>
147> ```python theme={null}
148> async def make_nws_request(url: str) -> dict[str, Any] | None:
149> """Make a request to the NWS API with proper error handling."""
150> headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
151> async with httpx.AsyncClient() as client:
152> try:
153> response = await client.get(url, headers=headers, timeout=30.0)
154> response.raise_for_status()
155> return response.json()
156> except Exception:
157> return None
158>
159>
160> def format_alert(feature: dict) -> str:
161> """Format an alert feature into a readable string."""
162> props = feature["properties"]
163> return f"""
164> Event: {props.get("event", "Unknown")}
165> Area: {props.get("areaDesc", "Unknown")}
166> Severity: {props.get("severity", "Unknown")}
167> Description: {props.get("description", "No description available")}
168> Instructions: {props.get("instruction", "No specific instructions provided")}
169> """
170> ```
171>
172> ### Implementing tool execution
173>
174> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
175>
176> ```python theme={null}
177> @mcp.tool()
178> async def get_alerts(state: str) -> str:
179> """Get weather alerts for a US state.
180>
181> Args:
182> state: Two-letter US state code (e.g. CA, NY)
183> """
184> url = f"{NWS_API_BASE}/alerts/active/area/{state}"
185> data = await make_nws_request(url)
186>
187> if not data or "features" not in data:
188> return "Unable to fetch alerts or no alerts found."
189>
190> if not data["features"]:
191> return "No active alerts for this state."
192>
193> alerts = [format_alert(feature) for feature in data["features"]]
194> return "\n---\n".join(alerts)
195>
196>
197> @mcp.tool()
198> async def get_forecast(latitude: float, longitude: float) -> str:
199> """Get weather forecast for a location.
200>
201> Args:
202> latitude: Latitude of the location
203> longitude: Longitude of the location
204> """
205> # First get the forecast grid endpoint
206> points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
207> points_data = await make_nws_request(points_url)
208>
209> if not points_data:
210> return "Unable to fetch forecast data for this location."
211>
212> # Get the forecast URL from the points response
213> forecast_url = points_data["properties"]["forecast"]
214> forecast_data = await make_nws_request(forecast_url)
215>
216> if not forecast_data:
217> return "Unable to fetch detailed forecast."
218>
219> # Format the periods into a readable forecast
220> periods = forecast_data["properties"]["periods"]
221> forecasts = []
222> for period in periods[:5]: # Only show next 5 periods
223> forecast = f"""
224> {period["name"]}:
225> Temperature: {period["temperature"]}°{period["temperatureUnit"]}
226> Wind: {period["windSpeed"]} {period["windDirection"]}
227> Forecast: {period["detailedForecast"]}
228> """
229> forecasts.append(forecast)
230>
231> return "\n---\n".join(forecasts)
232> ```
233>
234> ### Running the server
235>
236> Finally, let's initialize and run the server:
237>
238> ```python theme={null}
239> def main():
240> # Initialize and run the server
241> mcp.run(transport="stdio")
242>
243>
244> if __name__ == "__main__":
245> main()
246> ```
247>
248> Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
249>
250> Let's now test your server from an existing MCP host, Claude for Desktop.
251>
252> ## Testing your server with Claude for Desktop
253>
254> > [!NOTE]
255> >
256> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
257>
258> First, make sure you have Claude for Desktop installed. [You can install the latest version
259> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
260>
261> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
262>
263> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
264>
265> ```bash macOS/Linux theme={null}
266> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
267> ```
268>
269> ```powershell Windows theme={null}
270> code $env:AppData\Claude\claude_desktop_config.json
271> ```
272>
273> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
274>
275> In this case, we'll add our single weather server like so:
276>
277> ```json macOS/Linux theme={null}
278> {
279> "mcpServers": {
280> "weather": {
281> "command": "uv",
282> "args": [
283> "--directory",
284> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
285> "run",
286> "weather.py"
287> ]
288> }
289> }
290> }
291> ```
292>
293> ```json Windows theme={null}
294> {
295> "mcpServers": {
296> "weather": {
297> "command": "uv",
298> "args": [
299> "--directory",
300> "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
301> "run",
302> "weather.py"
303> ]
304> }
305> }
306> }
307> ```
308>
309> > [!WARNING]
310> >
311> > You may need to put the full path to the `uv` executable in the `command` field. You can get this by running `which uv` on macOS/Linux or `where uv` on Windows.
312>
313> > [!NOTE]
314> >
315> > Make sure you pass in the absolute path to your server. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
316>
317> This tells Claude for Desktop:
318>
319> 1. There's an MCP server named "weather"
320> 2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
321>
322> Save the file, and restart **Claude for Desktop**.
323 
324> [tab] TypeScript
325>
326> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-typescript)
327>
328> ### Prerequisite knowledge
329>
330> This quickstart assumes you have familiarity with:
331>
332> * TypeScript
333> * LLMs like Claude
334>
335> ### Logging in MCP Servers
336>
337> When implementing MCP servers, be careful about how you handle logging:
338>
339> **For STDIO-based servers:** Never use `console.log()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
340>
341> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
342>
343> ### Best Practices
344>
345> * Use `console.error()` which writes to stderr, or use a logging library that writes to stderr or files.
346>
347> ### Quick Examples
348>
349> ```javascript theme={null}
350> // ❌ Bad (STDIO)
351> console.log("Server started");
352>
353> // ✅ Good (STDIO)
354> console.error("Server started"); // stderr is safe
355> ```
356>
357> ### System requirements
358>
359> For TypeScript, make sure you have the latest version of Node installed.
360>
361> ### Set up your environment
362>
363> First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
364> Verify your Node.js installation:
365>
366> ```bash theme={null}
367> node --version
368> npm --version
369> ```
370>
371> For this tutorial, you'll need Node.js version 16 or higher.
372>
373> Now, let's create and set up our project:
374>
375> ```bash macOS/Linux theme={null}
376> # Create a new directory for our project
377> mkdir weather
378> cd weather
379>
380> # Initialize a new npm project
381> npm init -y
382>
383> # Install dependencies
384> npm install @modelcontextprotocol/sdk zod@3
385> npm install -D @types/node typescript
386>
387> # Create our files
388> mkdir src
389> touch src/index.ts
390> ```
391>
392> ```powershell Windows theme={null}
393> # Create a new directory for our project
394> md weather
395> cd weather
396>
397> # Initialize a new npm project
398> npm init -y
399>
400> # Install dependencies
401> npm install @modelcontextprotocol/sdk zod@3
402> npm install -D @types/node typescript
403>
404> # Create our files
405> md src
406> new-item src\index.ts
407> ```
408>
409> Update your package.json to add type: "module" and a build script:
410>
411> ```json package.json theme={null}
412> {
413> "type": "module",
414> "bin": {
415> "weather": "./build/index.js"
416> },
417> "scripts": {
418> "build": "tsc && chmod 755 build/index.js"
419> },
420> "files": ["build"]
421> }
422> ```
423>
424> Create a `tsconfig.json` in the root of your project:
425>
426> ```json tsconfig.json theme={null}
427> {
428> "compilerOptions": {
429> "target": "ES2022",
430> "module": "Node16",
431> "moduleResolution": "Node16",
432> "outDir": "./build",
433> "rootDir": "./src",
434> "strict": true,
435> "esModuleInterop": true,
436> "skipLibCheck": true,
437> "forceConsistentCasingInFileNames": true
438> },
439> "include": ["src/**/*"],
440> "exclude": ["node_modules"]
441> }
442> ```
443>
444> Now let's dive into building your server.
445>
446> ## Building your server
447>
448> ### Importing packages and setting up the instance
449>
450> Add these to the top of your `src/index.ts`:
451>
452> ```typescript theme={null}
453> import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
454> import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
455> import { z } from "zod";
456>
457> const NWS_API_BASE = "https://api.weather.gov";
458> const USER_AGENT = "weather-app/1.0";
459>
460> // Create server instance
461> const server = new McpServer({
462> name: "weather",
463> version: "1.0.0",
464> });
465> ```
466>
467> ### Helper functions
468>
469> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
470>
471> ```typescript theme={null}
472> // Helper function for making NWS API requests
473> async function makeNWSRequest<T>(url: string): Promise<T | null> {
474> const headers = {
475> "User-Agent": USER_AGENT,
476> Accept: "application/geo+json",
477> };
478>
479> try {
480> const response = await fetch(url, { headers });
481> if (!response.ok) {
482> throw new Error(`HTTP error! status: ${response.status}`);
483> }
484> return (await response.json()) as T;
485> } catch (error) {
486> console.error("Error making NWS request:", error);
487> return null;
488> }
489> }
490>
491> interface AlertFeature {
492> properties: {
493> event?: string;
494> areaDesc?: string;
495> severity?: string;
496> status?: string;
497> headline?: string;
498> };
499> }
500>
501> // Format alert data
502> function formatAlert(feature: AlertFeature): string {
503> const props = feature.properties;
504> return [
505> `Event: ${props.event || "Unknown"}`,
506> `Area: ${props.areaDesc || "Unknown"}`,
507> `Severity: ${props.severity || "Unknown"}`,
508> `Status: ${props.status || "Unknown"}`,
509> `Headline: ${props.headline || "No headline"}`,
510> "---",
511> ].join("\n");
512> }
513>
514> interface ForecastPeriod {
515> name?: string;
516> temperature?: number;
517> temperatureUnit?: string;
518> windSpeed?: string;
519> windDirection?: string;
520> shortForecast?: string;
521> }
522>
523> interface AlertsResponse {
524> features: AlertFeature[];
525> }
526>
527> interface PointsResponse {
528> properties: {
529> forecast?: string;
530> };
531> }
532>
533> interface ForecastResponse {
534> properties: {
535> periods: ForecastPeriod[];
536> };
537> }
538> ```
539>
540> ### Implementing tool execution
541>
542> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
543>
544> ```typescript theme={null}
545> // Register weather tools
546>
547> server.registerTool(
548> "get_alerts",
549> {
550> description: "Get weather alerts for a state",
551> inputSchema: {
552> state: z
553> .string()
554> .length(2)
555> .describe("Two-letter state code (e.g. CA, NY)"),
556> },
557> },
558> async ({ state }) => {
559> const stateCode = state.toUpperCase();
560> const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
561> const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
562>
563> if (!alertsData) {
564> return {
565> content: [
566> {
567> type: "text",
568> text: "Failed to retrieve alerts data",
569> },
570> ],
571> };
572> }
573>
574> const features = alertsData.features || [];
575> if (!features.length) {
576> return {
577> content: [
578> {
579> type: "text",
580> text: `No active alerts for ${stateCode}`,
581> },
582> ],
583> };
584> }
585>
586> const formattedAlerts = features.map(formatAlert);
587> const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
588>
589> return {
590> content: [
591> {
592> type: "text",
593> text: alertsText,
594> },
595> ],
596> };
597> },
598> );
599>
600> server.registerTool(
601> "get_forecast",
602> {
603> description: "Get weather forecast for a location",
604> inputSchema: {
605> latitude: z
606> .number()
607> .min(-90)
608> .max(90)
609> .describe("Latitude of the location"),
610> longitude: z
611> .number()
612> .min(-180)
613> .max(180)
614> .describe("Longitude of the location"),
615> },
616> },
617> async ({ latitude, longitude }) => {
618> // Get grid point data
619> const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
620> const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
621>
622> if (!pointsData) {
623> return {
624> content: [
625> {
626> type: "text",
627> text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
628> },
629> ],
630> };
631> }
632>
633> const forecastUrl = pointsData.properties?.forecast;
634> if (!forecastUrl) {
635> return {
636> content: [
637> {
638> type: "text",
639> text: "Failed to get forecast URL from grid point data",
640> },
641> ],
642> };
643> }
644>
645> // Get forecast data
646> const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
647> if (!forecastData) {
648> return {
649> content: [
650> {
651> type: "text",
652> text: "Failed to retrieve forecast data",
653> },
654> ],
655> };
656> }
657>
658> const periods = forecastData.properties?.periods || [];
659> if (periods.length === 0) {
660> return {
661> content: [
662> {
663> type: "text",
664> text: "No forecast periods available",
665> },
666> ],
667> };
668> }
669>
670> // Format forecast periods
671> const formattedForecast = periods.map((period: ForecastPeriod) =>
672> [
673> `${period.name || "Unknown"}:`,
674> `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
675> `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
676> `${period.shortForecast || "No forecast available"}`,
677> "---",
678> ].join("\n"),
679> );
680>
681> const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
682>
683> return {
684> content: [
685> {
686> type: "text",
687> text: forecastText,
688> },
689> ],
690> };
691> },
692> );
693> ```
694>
695> ### Running the server
696>
697> Finally, implement the main function to run the server:
698>
699> ```typescript theme={null}
700> async function main() {
701> const transport = new StdioServerTransport();
702> await server.connect(transport);
703> console.error("Weather MCP Server running on stdio");
704> }
705>
706> main().catch((error) => {
707> console.error("Fatal error in main():", error);
708> process.exit(1);
709> });
710> ```
711>
712> Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
713>
714> Let's now test your server from an existing MCP host, Claude for Desktop.
715>
716> ## Testing your server with Claude for Desktop
717>
718> > [!NOTE]
719> >
720> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
721>
722> First, make sure you have Claude for Desktop installed. [You can install the latest version
723> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724>
725> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726>
727> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728>
729> ```bash macOS/Linux theme={null}
730> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
731> ```
732>
733> ```powershell Windows theme={null}
734> code $env:AppData\Claude\claude_desktop_config.json
735> ```
736>
737> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
738>
739> In this case, we'll add our single weather server like so:
740>
741> ```json macOS/Linux theme={null}
742> {
743> "mcpServers": {
744> "weather": {
745> "command": "node",
746> "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
747> }
748> }
749> }
750> ```
751>
752> ```json Windows theme={null}
753> {
754> "mcpServers": {
755> "weather": {
756> "command": "node",
757> "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
758> }
759> }
760> }
761> ```
762>
763> This tells Claude for Desktop:
764>
765> 1. There's an MCP server named "weather"
766> 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
767>
768> Save the file, and restart **Claude for Desktop**.
769 
770> [tab] Java
771>
772> > [!NOTE]
773> >
774> > This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
775> > To learn how to create sync and async MCP Servers, manually, consult the [Java SDK Server](https://java.sdk.modelcontextprotocol.io/) documentation.
776>
777> Let's get started with building our weather server!
778> [You can find the complete code for what we'll be building here.](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-stdio-server)
779>
780> For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation.
781> For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
782>
783> ### Logging in MCP Servers
784>
785> When implementing MCP servers, be careful about how you handle logging:
786>
787> **For STDIO-based servers:** Never use `System.out.println()` or `System.out.print()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
788>
789> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
790>
791> ### Best Practices
792>
793> * Use a logging library that writes to stderr or files.
794> * Ensure any configured logging library will not write to stdout.
795>
796> ### System requirements
797>
798> * Java 17 or higher installed.
799> * [Spring Boot 3.3.x](https://docs.spring.io/spring-boot/installing.html) or higher
800>
801> ### Set up your environment
802>
803> Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
804>
805> You will need to add the following dependencies:
806>
807> ```xml Maven theme={null}
808> <dependencies>
809> <dependency>
810> <groupId>org.springframework.ai</groupId>
811> <artifactId>spring-ai-starter-mcp-server</artifactId>
812> </dependency>
813>
814> <dependency>
815> <groupId>org.springframework</groupId>
816> <artifactId>spring-web</artifactId>
817> </dependency>
818> </dependencies>
819> ```
820>
821> ```groovy Gradle theme={null}
822> dependencies {
823> implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
824> implementation platform("org.springframework:spring-web")
825> }
826> ```
827>
828> Then configure your application by setting the application properties:
829>
830> ```bash application.properties theme={null}
831> spring.main.bannerMode=off
832> logging.pattern.console=
833> ```
834>
835> ```yaml application.yml theme={null}
836> logging:
837> pattern:
838> console:
839> spring:
840> main:
841> banner-mode: off
842> ```
843>
844> The [Server Configuration Properties](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_configuration_properties) documents all available properties.
845>
846> Now let's dive into building your server.
847>
848> ## Building your server
849>
850> ### Weather Service
851>
852> Let's implement a [WeatherService.java](https://github.com/spring-projects/spring-ai-examples/blob/main/model-context-protocol/weather/starter-stdio-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java) that uses a REST client to query the data from the National Weather Service API:
853>
854> ```java theme={null}
855> @Service
856> public class WeatherService {
857>
858> private final RestClient restClient;
859>
860> public WeatherService() {
861> this.restClient = RestClient.builder()
862> .baseUrl("https://api.weather.gov")
863> .defaultHeader("Accept", "application/geo+json")
864> .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
865> .build();
866> }
867>
868> @Tool(description = "Get weather forecast for a specific latitude/longitude")
869> public String getWeatherForecastByLocation(
870> double latitude, // Latitude coordinate
871> double longitude // Longitude coordinate
872> ) {
873> // Returns detailed forecast including:
874> // - Temperature and unit
875> // - Wind speed and direction
876> // - Detailed forecast description
877> }
878>
879> @Tool(description = "Get weather alerts for a US state")
880> public String getAlerts(
881> @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
882> ) {
883> // Returns active alerts including:
884> // - Event type
885> // - Affected area
886> // - Severity
887> // - Description
888> // - Safety instructions
889> }
890>
891> // ......
892> }
893> ```
894>
895> The `@Service` annotation will auto-register the service in your application context.
896> The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
897>
898> The auto-configuration will automatically register these tools with the MCP server.
899>
900> ### Create your Boot Application
901>
902> ```java theme={null}
903> @SpringBootApplication
904> public class McpServerApplication {
905>
906> public static void main(String[] args) {
907> SpringApplication.run(McpServerApplication.class, args);
908> }
909>
910> @Bean
911> public ToolCallbackProvider weatherTools(WeatherService weatherService) {
912> return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
913> }
914> }
915> ```
916>
917> Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
918>
919> ### Running the server
920>
921> Finally, let's build the server:
922>
923> ```bash theme={null}
924> ./mvnw clean install
925> ```
926>
927> This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
928>
929> Let's now test your server from an existing MCP host, Claude for Desktop.
930>
931> ## Testing your server with Claude for Desktop
932>
933> > [!NOTE]
934> >
935> > Claude for Desktop is not yet available on Linux.
936>
937> First, make sure you have Claude for Desktop installed.
938> [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939>
940> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942> Make sure to create the file if it doesn't exist.
943>
944> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945>
946> ```bash macOS/Linux theme={null}
947> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
948> ```
949>
950> ```powershell Windows theme={null}
951> code $env:AppData\Claude\claude_desktop_config.json
952> ```
953>
954> You'll then add your servers in the `mcpServers` key.
955> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
956>
957> In this case, we'll add our single weather server like so:
958>
959> ```json macOS/Linux theme={null}
960> {
961> "mcpServers": {
962> "spring-ai-mcp-weather": {
963> "command": "java",
964> "args": [
965> "-Dspring.ai.mcp.server.stdio=true",
966> "-jar",
967> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
968> ]
969> }
970> }
971> }
972> ```
973>
974> ```json Windows theme={null}
975> {
976> "mcpServers": {
977> "spring-ai-mcp-weather": {
978> "command": "java",
979> "args": [
980> "-Dspring.ai.mcp.server.transport=STDIO",
981> "-jar",
982> "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
983> ]
984> }
985> }
986> }
987> ```
988>
989> > [!NOTE]
990> >
991> > Make sure you pass in the absolute path to your server.
992>
993> This tells Claude for Desktop:
994>
995> 1. There's an MCP server named "my-weather-server"
996> 2. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
997>
998> Save the file, and restart **Claude for Desktop**.
999>
1000> ## Testing your server with Java client
1001>
1002> ### Create an MCP Client manually
1003>
1004> Use the `McpClient` to connect to the server:
1005>
1006> ```java theme={null}
1007> var stdioParams = ServerParameters.builder("java")
1008> .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1009> .build();
1010>
1011> var stdioTransport = new StdioClientTransport(stdioParams);
1012>
1013> var mcpClient = McpClient.sync(stdioTransport).build();
1014>
1015> mcpClient.initialize();
1016>
1017> ListToolsResult toolsList = mcpClient.listTools();
1018>
1019> CallToolResult weather = mcpClient.callTool(
1020> new CallToolRequest("getWeatherForecastByLocation",
1021> Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1022>
1023> CallToolResult alert = mcpClient.callTool(
1024> new CallToolRequest("getAlerts", Map.of("state", "NY")));
1025>
1026> mcpClient.closeGracefully();
1027> ```
1028>
1029> ### Use MCP Client Boot Starter
1030>
1031> Create a new boot starter application using the `spring-ai-starter-mcp-client` dependency:
1032>
1033> ```xml theme={null}
1034> <dependency>
1035> <groupId>org.springframework.ai</groupId>
1036> <artifactId>spring-ai-starter-mcp-client</artifactId>
1037> </dependency>
1038> ```
1039>
1040> and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1041> You can reuse the existing Anthropic Desktop configuration:
1042>
1043> ```properties theme={null}
1044> spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1045> ```
1046>
1047> When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1048>
1049> For more information, see the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) reference documentation.
1050>
1051> ## More Java MCP Server examples
1052>
1053> The [starter-webflux-server](https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server) demonstrates how to create an MCP server using SSE transport.
1054> It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1055 
1056> [tab] Kotlin
1057>
1058> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/kotlin-sdk/tree/main/samples/weather-stdio-server)
1059>
1060> ### Prerequisite knowledge
1061>
1062> This quickstart assumes you have familiarity with:
1063>
1064> * Kotlin
1065> * LLMs like Claude
1066>
1067> ### Logging in MCP Servers
1068>
1069> When implementing MCP servers, be careful about how you handle logging:
1070>
1071> **For STDIO-based servers:** Never use `println()`, as it writes to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1072>
1073> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1074>
1075> ### Best Practices
1076>
1077> * Use a logging library that writes to stderr or files.
1078>
1079> ### System requirements
1080>
1081> * JDK 11 or higher installed.
1082>
1083> ### Set up your environment
1084>
1085> First, let's install `java` and `gradle` if you haven't already.
1086> You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1087> Verify your `java` installation:
1088>
1089> ```bash theme={null}
1090> java --version
1091> ```
1092>
1093> Now, let's create and set up your project:
1094>
1095> ```bash macOS/Linux theme={null}
1096> # Create a new directory for our project
1097> mkdir weather
1098> cd weather
1099>
1100> # Initialize a new kotlin project
1101> gradle init
1102> ```
1103>
1104> ```powershell Windows theme={null}
1105> # Create a new directory for our project
1106> md weather
1107> cd weather
1108>
1109> # Initialize a new kotlin project
1110> gradle init
1111> ```
1112>
1113> After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1114>
1115> Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1116>
1117> After creating the project, replace the contents of your `build.gradle.kts` with:
1118>
1119> ```kotlin build.gradle.kts theme={null}
1120> // Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
1121> val mcpVersion = "0.9.0"
1122> val ktorVersion = "3.2.3"
1123> val slf4jVersion = "2.0.17"
1124>
1125> plugins {
1126> kotlin("jvm") version "2.3.20"
1127> kotlin("plugin.serialization") version "2.3.20"
1128> id("com.gradleup.shadow") version "8.3.9"
1129> application
1130> }
1131>
1132> application {
1133> mainClass.set("MainKt")
1134> }
1135>
1136> dependencies {
1137> implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
1138> implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
1139> implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
1140> implementation("io.ktor:ktor-client-cio:$ktorVersion")
1141> implementation("org.slf4j:slf4j-simple:$slf4jVersion")
1142> }
1143> ```
1144>
1145> Verify that everything is set up correctly:
1146>
1147> ```bash theme={null}
1148> ./gradlew build
1149> ```
1150>
1151> Now let's dive into building your server.
1152>
1153> ## Building your server
1154>
1155> ### Setting up the instance
1156>
1157> Add a server initialization function:
1158>
1159> ```kotlin theme={null}
1160> fun runMcpServer() {
1161> val server = Server(
1162> Implementation(
1163> name = "weather",
1164> version = "1.0.0",
1165> ),
1166> ServerOptions(
1167> capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
1168> ),
1169> )
1170>
1171> // register tools on server here
1172>
1173> val transport = StdioServerTransport(
1174> System.`in`.asInput(),
1175> System.out.asSink().buffered(),
1176> )
1177>
1178> runBlocking {
1179> val session = server.createSession(transport)
1180> val done = Job()
1181> session.onClose {
1182> done.complete()
1183> }
1184> done.join()
1185> }
1186> }
1187> ```
1188>
1189> ### Weather API helper functions
1190>
1191> Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1192>
1193> ```kotlin theme={null}
1194> val httpClient = HttpClient(CIO) {
1195> defaultRequest {
1196> url("https://api.weather.gov")
1197> headers {
1198> append("Accept", "application/geo+json")
1199> append("User-Agent", "WeatherApiClient/1.0")
1200> }
1201> contentType(ContentType.Application.Json)
1202> }
1203> install(ContentNegotiation) {
1204> json(Json { ignoreUnknownKeys = true })
1205> }
1206> }
1207>
1208> // Extension function to fetch weather alerts for a given state
1209> suspend fun HttpClient.getAlerts(state: String): List<String> {
1210> val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1211> return alerts.features.map { feature ->
1212> """
1213> Event: ${feature.properties.event}
1214> Area: ${feature.properties.areaDesc}
1215> Severity: ${feature.properties.severity}
1216> Status: ${feature.properties.status}
1217> Headline: ${feature.properties.headline}
1218> """.trimIndent()
1219> }
1220> }
1221>
1222> // Extension function to fetch forecast information for given latitude and longitude
1223> suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
1224> val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
1225> val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
1226> val forecast = this.get(forecastUrl).body<ForecastResponse>()
1227> return forecast.properties.periods.map { period ->
1228> """
1229> ${period.name}:
1230> Temperature: ${period.temperature}°${period.temperatureUnit}
1231> Wind: ${period.windSpeed} ${period.windDirection}
1232> ${period.shortForecast}
1233> """.trimIndent()
1234> }
1235> }
1236>
1237> @Serializable
1238> data class PointsResponse(val properties: PointsProperties)
1239>
1240> @Serializable
1241> data class PointsProperties(val forecast: String? = null)
1242>
1243> @Serializable
1244> data class ForecastResponse(val properties: ForecastProperties)
1245>
1246> @Serializable
1247> data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1248>
1249> @Serializable
1250> data class ForecastPeriod(
1251> val name: String? = null,
1252> val temperature: Int? = null,
1253> val temperatureUnit: String? = null,
1254> val windSpeed: String? = null,
1255> val windDirection: String? = null,
1256> val shortForecast: String? = null,
1257> )
1258>
1259> @Serializable
1260> data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1261>
1262> @Serializable
1263> data class AlertFeature(val properties: AlertProperties)
1264>
1265> @Serializable
1266> data class AlertProperties(
1267> val event: String? = null,
1268> val areaDesc: String? = null,
1269> val severity: String? = null,
1270> val status: String? = null,
1271> val headline: String? = null,
1272> )
1273> ```
1274>
1275> ### Implementing tool execution
1276>
1277> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
1278>
1279> ```kotlin theme={null}
1280> // Register weather tools
1281>
1282> server.addTool(
1283> name = "get_alerts",
1284> description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
1285> inputSchema = ToolSchema(
1286> properties = buildJsonObject {
1287> putJsonObject("state") {
1288> put("type", "string")
1289> put("description", "Two-letter US state code (e.g. CA, NY)")
1290> }
1291> },
1292> required = listOf("state"),
1293> ),
1294> ) { request ->
1295> val state = request.arguments?.get("state")?.jsonPrimitive?.content
1296> ?: return@addTool CallToolResult(
1297> content = listOf(TextContent("The 'state' parameter is required.")),
1298> )
1299>
1300> val alerts = httpClient.getAlerts(state)
1301> CallToolResult(content = alerts.map { TextContent(it) })
1302> }
1303>
1304> server.addTool(
1305> name = "get_forecast",
1306> description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
1307> inputSchema = ToolSchema(
1308> properties = buildJsonObject {
1309> putJsonObject("latitude") {
1310> put("type", "number")
1311> put("description", "Latitude of the location")
1312> }
1313> putJsonObject("longitude") {
1314> put("type", "number")
1315> put("description", "Longitude of the location")
1316> }
1317> },
1318> required = listOf("latitude", "longitude"),
1319> ),
1320> ) { request ->
1321> val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
1322> val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
1323> if (latitude == null || longitude == null) {
1324> return@addTool CallToolResult(
1325> content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
1326> )
1327> }
1328>
1329> val forecast = httpClient.getForecast(latitude, longitude)
1330> CallToolResult(content = forecast.map { TextContent(it) })
1331> }
1332> ```
1333>
1334> ### Running the server
1335>
1336> Finally, implement the main function to run the server:
1337>
1338> ```kotlin theme={null}
1339> fun main() = runMcpServer()
1340> ```
1341>
1342> You can run the server directly during development:
1343>
1344> ```bash theme={null}
1345> ./gradlew run
1346> ```
1347>
1348> For production use, build the shadow JAR:
1349>
1350> ```bash theme={null}
1351> ./gradlew build
1352> java -jar build/libs/weather-0.1.0-all.jar
1353> ```
1354>
1355> Let's now test your server from an existing MCP host, Claude for Desktop.
1356>
1357> ## Testing your server with Claude for Desktop
1358>
1359> > [!NOTE]
1360> >
1361> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1362>
1363> First, make sure you have Claude for Desktop installed. [You can install the latest version
1364> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1365>
1366> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1367> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1368> Make sure to create the file if it doesn't exist.
1369>
1370> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1371>
1372> ```bash macOS/Linux theme={null}
1373> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1374> ```
1375>
1376> ```powershell Windows theme={null}
1377> code $env:AppData\Claude\claude_desktop_config.json
1378> ```
1379>
1380> You'll then add your servers in the `mcpServers` key.
1381> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1382>
1383> In this case, we'll add our single weather server like so:
1384>
1385> ```json macOS/Linux theme={null}
1386> {
1387> "mcpServers": {
1388> "weather": {
1389> "command": "java",
1390> "args": [
1391> "-jar",
1392> "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
1393> ]
1394> }
1395> }
1396> }
1397> ```
1398>
1399> ```json Windows theme={null}
1400> {
1401> "mcpServers": {
1402> "weather": {
1403> "command": "java",
1404> "args": [
1405> "-jar",
1406> "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
1407> ]
1408> }
1409> }
1410> }
1411> ```
1412>
1413> This tells Claude for Desktop:
1414>
1415> 1. There's an MCP server named "weather"
1416> 2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1417>
1418> Save the file, and restart **Claude for Desktop**.
1419 
1420> [tab] C#
1421>
1422> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/QuickstartWeatherServer)
1423>
1424> ### Prerequisite knowledge
1425>
1426> This quickstart assumes you have familiarity with:
1427>
1428> * C#
1429> * LLMs like Claude
1430> * .NET 8 or higher
1431>
1432> ### Logging in MCP Servers
1433>
1434> When implementing MCP servers, be careful about how you handle logging:
1435>
1436> **For STDIO-based servers:** Never use `Console.WriteLine()` or `Console.Write()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1437>
1438> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1439>
1440> ### Best Practices
1441>
1442> * Use a logging library that writes to stderr or files.
1443>
1444> ### System requirements
1445>
1446> * [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher installed.
1447>
1448> ### Set up your environment
1449>
1450> First, let's install `dotnet` if you haven't already. You can download `dotnet` from [official Microsoft .NET website](https://dotnet.microsoft.com/download/). Verify your `dotnet` installation:
1451>
1452> ```bash theme={null}
1453> dotnet --version
1454> ```
1455>
1456> Now, let's create and set up your project:
1457>
1458> ```bash macOS/Linux theme={null}
1459> # Create a new directory for our project
1460> mkdir weather
1461> cd weather
1462> # Initialize a new C# project
1463> dotnet new console
1464> ```
1465>
1466> ```powershell Windows theme={null}
1467> # Create a new directory for our project
1468> mkdir weather
1469> cd weather
1470> # Initialize a new C# project
1471> dotnet new console
1472> ```
1473>
1474> After running `dotnet new console`, you will be presented with a new C# project.
1475> You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1476> Alternatively, you can create a C# application using the [Visual Studio project wizard](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console?view=vs-2022).
1477> After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
1478>
1479> ```bash theme={null}
1480> # Add the Model Context Protocol SDK NuGet package
1481> dotnet add package ModelContextProtocol --prerelease
1482> # Add the .NET Hosting NuGet package
1483> dotnet add package Microsoft.Extensions.Hosting
1484> ```
1485>
1486> Now let’s dive into building your server.
1487>
1488> ## Building your server
1489>
1490> Open the `Program.cs` file in your project and replace its contents with the following code:
1491>
1492> ```csharp theme={null}
1493> using Microsoft.Extensions.DependencyInjection;
1494> using Microsoft.Extensions.Hosting;
1495> using ModelContextProtocol;
1496> using System.Net.Http.Headers;
1497>
1498> var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1499>
1500> builder.Services.AddMcpServer()
1501> .WithStdioServerTransport()
1502> .WithToolsFromAssembly();
1503>
1504> builder.Services.AddSingleton(_ =>
1505> {
1506> var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
1507> client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
1508> return client;
1509> });
1510>
1511> var app = builder.Build();
1512>
1513> await app.RunAsync();
1514> ```
1515>
1516> > [!NOTE]
1517> >
1518> > When creating the `ApplicationHostBuilder`, ensure you use `CreateEmptyApplicationBuilder` instead of `CreateDefaultBuilder`. This ensures that the server does not write any additional messages to the console. This is only necessary for servers using STDIO transport.
1519>
1520> This code sets up a basic console application that uses the Model Context Protocol SDK to create an MCP server with standard I/O transport.
1521>
1522> ### Weather API helper functions
1523>
1524> Create an extension class for `HttpClient` which helps simplify JSON request handling:
1525>
1526> ```csharp theme={null}
1527> using System.Text.Json;
1528>
1529> internal static class HttpClientExt
1530> {
1531> public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
1532> {
1533> using var response = await client.GetAsync(requestUri);
1534> response.EnsureSuccessStatusCode();
1535> return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
1536> }
1537> }
1538> ```
1539>
1540> Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1541>
1542> ```csharp theme={null}
1543> using ModelContextProtocol.Server;
1544> using System.ComponentModel;
1545> using System.Globalization;
1546> using System.Text.Json;
1547>
1548> namespace QuickstartWeatherServer.Tools;
1549>
1550> [McpServerToolType]
1551> public static class WeatherTools
1552> {
1553> [McpServerTool, Description("Get weather alerts for a US state code.")]
1554> public static async Task<string> GetAlerts(
1555> HttpClient client,
1556> [Description("The US state code to get alerts for.")] string state)
1557> {
1558> using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
1559> var jsonElement = jsonDocument.RootElement;
1560> var alerts = jsonElement.GetProperty("features").EnumerateArray();
1561>
1562> if (!alerts.Any())
1563> {
1564> return "No active alerts for this state.";
1565> }
1566>
1567> return string.Join("\n--\n", alerts.Select(alert =>
1568> {
1569> JsonElement properties = alert.GetProperty("properties");
1570> return $"""
1571> Event: {properties.GetProperty("event").GetString()}
1572> Area: {properties.GetProperty("areaDesc").GetString()}
1573> Severity: {properties.GetProperty("severity").GetString()}
1574> Description: {properties.GetProperty("description").GetString()}
1575> Instruction: {properties.GetProperty("instruction").GetString()}
1576> """;
1577> }));
1578> }
1579>
1580> [McpServerTool, Description("Get weather forecast for a location.")]
1581> public static async Task<string> GetForecast(
1582> HttpClient client,
1583> [Description("Latitude of the location.")] double latitude,
1584> [Description("Longitude of the location.")] double longitude)
1585> {
1586> var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
1587> using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
1588> var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
1589> ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");
1590>
1591> using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
1592> var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();
1593>
1594> return string.Join("\n---\n", periods.Select(period => $"""
1595> {period.GetProperty("name").GetString()}
1596> Temperature: {period.GetProperty("temperature").GetInt32()}°F
1597> Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
1598> Forecast: {period.GetProperty("detailedForecast").GetString()}
1599> """));
1600> }
1601> }
1602> ```
1603>
1604> ### Running the server
1605>
1606> Finally, run the server using the following command:
1607>
1608> ```bash theme={null}
1609> dotnet run
1610> ```
1611>
1612> This will start the server and listen for incoming requests on standard input/output.
1613>
1614> ## Testing your server with Claude for Desktop
1615>
1616> > [!NOTE]
1617> >
1618> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1619>
1620> First, make sure you have Claude for Desktop installed. [You can install the latest version
1621> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1622> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1623> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1624>
1625> ```bash macOS/Linux theme={null}
1626> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1627> ```
1628>
1629> ```powershell Windows theme={null}
1630> code $env:AppData\Claude\claude_desktop_config.json
1631> ```
1632>
1633> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1634> In this case, we'll add our single weather server like so:
1635>
1636> ```json macOS/Linux theme={null}
1637> {
1638> "mcpServers": {
1639> "weather": {
1640> "command": "dotnet",
1641> "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
1642> }
1643> }
1644> }
1645> ```
1646>
1647> ```json Windows theme={null}
1648> {
1649> "mcpServers": {
1650> "weather": {
1651> "command": "dotnet",
1652> "args": [
1653> "run",
1654> "--project",
1655> "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
1656> "--no-build"
1657> ]
1658> }
1659> }
1660> }
1661> ```
1662>
1663> This tells Claude for Desktop:
1664>
1665> 1. There's an MCP server named "weather"
1666> 2. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1667> Save the file, and restart **Claude for Desktop**.
1668 
1669> [tab] Ruby
1670>
1671> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-ruby)
1672>
1673> ### Prerequisite knowledge
1674>
1675> This quickstart assumes you have familiarity with:
1676>
1677> * Ruby
1678> * LLMs like Claude
1679>
1680> ### Logging in MCP Servers
1681>
1682> When implementing MCP servers, be careful about how you handle logging:
1683>
1684> **For STDIO-based servers:** Never use `puts` or `print`, as they write to standard output (stdout) by default. Writing to stdout will corrupt the JSON-RPC messages and break your server.
1685>
1686> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1687>
1688> ### Best Practices
1689>
1690> * Use a logging library that writes to stderr or files.
1691>
1692> ### Quick Examples
1693>
1694> ```ruby theme={null}
1695> # ❌ Bad (STDIO)
1696> puts "Processing request"
1697>
1698> # ✅ Good (STDIO)
1699> require "logger"
1700> logger = Logger.new($stderr)
1701> logger.info("Processing request")
1702> ```
1703>
1704> ### System requirements
1705>
1706> * Ruby 2.7 or higher installed.
1707>
1708> ### Set up your environment
1709>
1710> First, let's make sure you have Ruby installed. You can check by running:
1711>
1712> ```bash theme={null}
1713> ruby --version
1714> ```
1715>
1716> Now, let's create and set up our project:
1717>
1718> ```bash macOS/Linux theme={null}
1719> # Create a new directory for our project
1720> mkdir weather
1721> cd weather
1722>
1723> # Create a Gemfile
1724> bundle init
1725>
1726> # Add the MCP SDK dependency
1727> bundle add mcp
1728>
1729> # Create our server file
1730> touch weather.rb
1731> ```
1732>
1733> ```powershell Windows theme={null}
1734> # Create a new directory for our project
1735> mkdir weather
1736> cd weather
1737>
1738> # Create a Gemfile
1739> bundle init
1740>
1741> # Add the MCP SDK dependency
1742> bundle add mcp
1743>
1744> # Create our server file
1745> new-item weather.rb
1746> ```
1747>
1748> Now let's dive into building your server.
1749>
1750> ## Building your server
1751>
1752> ### Importing packages and setting up constants
1753>
1754> Open `weather.rb` and add these requires and constants at the top:
1755>
1756> ```ruby theme={null}
1757> require "json"
1758> require "mcp"
1759> require "net/http"
1760> require "uri"
1761>
1762> NWS_API_BASE = "https://api.weather.gov"
1763> USER_AGENT = "weather-app/1.0"
1764> ```
1765>
1766> The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1767>
1768> ### Helper methods
1769>
1770> Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1771>
1772> ```ruby theme={null}
1773> module HelperMethods
1774> def make_nws_request(url)
1775> uri = URI(url)
1776> request = Net::HTTP::Get.new(uri)
1777> request["User-Agent"] = USER_AGENT
1778> request["Accept"] = "application/geo+json"
1779>
1780> response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
1781> http.request(request)
1782> end
1783>
1784> raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
1785>
1786> JSON.parse(response.body)
1787> end
1788>
1789> def format_alert(feature)
1790> properties = feature["properties"]
1791>
1792> <<~ALERT
1793> Event: #{properties["event"] || "Unknown"}
1794> Area: #{properties["areaDesc"] || "Unknown"}
1795> Severity: #{properties["severity"] || "Unknown"}
1796> Description: #{properties["description"] || "No description available"}
1797> Instructions: #{properties["instruction"] || "No specific instructions provided"}
1798> ALERT
1799> end
1800> end
1801> ```
1802>
1803> ### Implementing tool execution
1804>
1805> Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1806>
1807> ```ruby theme={null}
1808> class GetAlerts < MCP::Tool
1809> extend HelperMethods
1810>
1811> tool_name "get_alerts"
1812> description "Get weather alerts for a US state"
1813> input_schema(
1814> properties: {
1815> state: {
1816> type: "string",
1817> description: "Two-letter US state code (e.g. CA, NY)"
1818> }
1819> },
1820> required: ["state"]
1821> )
1822>
1823> def self.call(state:)
1824> url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
1825> data = make_nws_request(url)
1826>
1827> if data["features"].empty?
1828> return MCP::Tool::Response.new([{
1829> type: "text",
1830> text: "No active alerts for this state."
1831> }])
1832> end
1833>
1834> alerts = data["features"].map { |feature| format_alert(feature) }
1835> MCP::Tool::Response.new([{
1836> type: "text",
1837> text: alerts.join("\n---\n")
1838> }])
1839> end
1840> end
1841>
1842> class GetForecast < MCP::Tool
1843> extend HelperMethods
1844>
1845> tool_name "get_forecast"
1846> description "Get weather forecast for a location"
1847> input_schema(
1848> properties: {
1849> latitude: {
1850> type: "number",
1851> description: "Latitude of the location"
1852> },
1853> longitude: {
1854> type: "number",
1855> description: "Longitude of the location"
1856> }
1857> },
1858> required: ["latitude", "longitude"]
1859> )
1860>
1861> def self.call(latitude:, longitude:)
1862> # First get the forecast grid endpoint.
1863> points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
1864> points_data = make_nws_request(points_url)
1865>
1866> # Get the forecast URL from the points response.
1867> forecast_url = points_data["properties"]["forecast"]
1868> forecast_data = make_nws_request(forecast_url)
1869>
1870> # Format the periods into a readable forecast.
1871> periods = forecast_data["properties"]["periods"]
1872> forecasts = periods.first(5).map do |period|
1873> <<~FORECAST
1874> #{period["name"]}:
1875> Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
1876> Wind: #{period["windSpeed"]} #{period["windDirection"]}
1877> Forecast: #{period["detailedForecast"]}
1878> FORECAST
1879> end
1880>
1881> MCP::Tool::Response.new([{
1882> type: "text",
1883> text: forecasts.join("\n---\n")
1884> }])
1885> end
1886> end
1887> ```
1888>
1889> ### Running the server
1890>
1891> Finally, initialize and run the server:
1892>
1893> ```ruby theme={null}
1894> server = MCP::Server.new(
1895> name: "weather",
1896> version: "1.0.0",
1897> tools: [GetAlerts, GetForecast]
1898> )
1899>
1900> transport = MCP::Server::Transports::StdioTransport.new(server)
1901> transport.open
1902> ```
1903>
1904> Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1905>
1906> Let's now test your server from an existing MCP host, Claude for Desktop.
1907>
1908> ## Testing your server with Claude for Desktop
1909>
1910> > [!NOTE]
1911> >
1912> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
1913>
1914> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1915>
1916> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
1917>
1918> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1919>
1920> ```bash macOS/Linux theme={null}
1921> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1922> ```
1923>
1924> ```powershell Windows theme={null}
1925> code $env:AppData\Claude\claude_desktop_config.json
1926> ```
1927>
1928> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1929>
1930> In this case, we'll add our single weather server like so:
1931>
1932> ```json macOS/Linux theme={null}
1933> {
1934> "mcpServers": {
1935> "weather": {
1936> "command": "bundle",
1937> "args": ["exec", "ruby", "weather.rb"],
1938> "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
1939> }
1940> }
1941> }
1942> ```
1943>
1944> ```json Windows theme={null}
1945> {
1946> "mcpServers": {
1947> "weather": {
1948> "command": "bundle",
1949> "args": ["exec", "ruby", "weather.rb"],
1950> "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
1951> }
1952> }
1953> }
1954> ```
1955>
1956> > [!NOTE]
1957> >
1958> > Make sure you pass in the absolute path to your project directory in the `cwd` field. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path.
1959>
1960> This tells Claude for Desktop:
1961>
1962> 1. There's an MCP server named "weather"
1963> 2. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1964>
1965> Save the file, and restart **Claude for Desktop**.
1966 
1967> [tab] Rust
1968>
1969> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-rust)
1970>
1971> ### Prerequisite knowledge
1972>
1973> This quickstart assumes you have familiarity with:
1974>
1975> * Rust programming language
1976> * Async/await in Rust
1977> * LLMs like Claude
1978>
1979> ### Logging in MCP Servers
1980>
1981> When implementing MCP servers, be careful about how you handle logging:
1982>
1983> **For STDIO-based servers:** Never use `println!()` or `print!()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
1984>
1985> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
1986>
1987> ### Best Practices
1988>
1989> * Use a logging library that writes to stderr or files, such as `tracing` or `log` in Rust.
1990> * Configure your logging framework to avoid stdout output.
1991>
1992> ### Quick Examples
1993>
1994> ```rust theme={null}
1995> // ❌ Bad (STDIO)
1996> println!("Processing request");
1997>
1998> // ✅ Good (STDIO)
1999> eprintln!("Processing request"); // writes to stderr
2000> ```
2001>
2002> ### System requirements
2003>
2004> * Rust 1.70 or higher installed.
2005> * Cargo (comes with Rust installation).
2006>
2007> ### Set up your environment
2008>
2009> First, let's install Rust if you haven't already. You can install Rust from [rust-lang.org](https://www.rust-lang.org/tools/install):
2010>
2011> ```bash macOS/Linux theme={null}
2012> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2013> ```
2014>
2015> ```powershell Windows theme={null}
2016> # Download and run rustup-init.exe from https://rustup.rs/
2017> ```
2018>
2019> Verify your Rust installation:
2020>
2021> ```bash theme={null}
2022> rustc --version
2023> cargo --version
2024> ```
2025>
2026> Now, let's create and set up our project:
2027>
2028> ```bash macOS/Linux theme={null}
2029> # Create a new Rust project
2030> cargo new weather
2031> cd weather
2032> ```
2033>
2034> ```powershell Windows theme={null}
2035> # Create a new Rust project
2036> cargo new weather
2037> cd weather
2038> ```
2039>
2040> Update your `Cargo.toml` to add the required dependencies:
2041>
2042> ```toml Cargo.toml theme={null}
2043> [package]
2044> name = "weather"
2045> version = "0.1.0"
2046> edition = "2024"
2047>
2048> [dependencies]
2049> rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2050> tokio = { version = "1.46", features = ["full"] }
2051> reqwest = { version = "0.12", features = ["json"] }
2052> serde = { version = "1.0", features = ["derive"] }
2053> serde_json = "1.0"
2054> anyhow = "1.0"
2055> tracing = "0.1"
2056> tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2057> ```
2058>
2059> Now let's dive into building your server.
2060>
2061> ## Building your server
2062>
2063> ### Importing packages and constants
2064>
2065> Open `src/main.rs` and add these imports and constants at the top:
2066>
2067> ```rust theme={null}
2068> use anyhow::Result;
2069> use rmcp::{
2070> ServerHandler, ServiceExt,
2071> handler::server::{router::tool::ToolRouter, tool::Parameters},
2072> model::*,
2073> schemars, tool, tool_handler, tool_router,
2074> };
2075> use serde::Deserialize;
2076> use serde::de::DeserializeOwned;
2077>
2078> const NWS_API_BASE: &str = "https://api.weather.gov";
2079> const USER_AGENT: &str = "weather-app/1.0";
2080> ```
2081>
2082> The `rmcp` crate provides the Model Context Protocol SDK for Rust, with features for server implementation, procedural macros, and stdio transport.
2083>
2084> ### Data structures
2085>
2086> Next, let's define the data structures for deserializing responses from the National Weather Service API:
2087>
2088> ```rust theme={null}
2089> #[derive(Debug, Deserialize)]
2090> struct AlertsResponse {
2091> features: Vec<AlertFeature>,
2092> }
2093>
2094> #[derive(Debug, Deserialize)]
2095> struct AlertFeature {
2096> properties: AlertProperties,
2097> }
2098>
2099> #[derive(Debug, Deserialize)]
2100> struct AlertProperties {
2101> event: Option<String>,
2102> #[serde(rename = "areaDesc")]
2103> area_desc: Option<String>,
2104> severity: Option<String>,
2105> description: Option<String>,
2106> instruction: Option<String>,
2107> }
2108>
2109> #[derive(Debug, Deserialize)]
2110> struct PointsResponse {
2111> properties: PointsProperties,
2112> }
2113>
2114> #[derive(Debug, Deserialize)]
2115> struct PointsProperties {
2116> forecast: String,
2117> }
2118>
2119> #[derive(Debug, Deserialize)]
2120> struct ForecastResponse {
2121> properties: ForecastProperties,
2122> }
2123>
2124> #[derive(Debug, Deserialize)]
2125> struct ForecastProperties {
2126> periods: Vec<ForecastPeriod>,
2127> }
2128>
2129> #[derive(Debug, Deserialize)]
2130> struct ForecastPeriod {
2131> name: String,
2132> temperature: i32,
2133> #[serde(rename = "temperatureUnit")]
2134> temperature_unit: String,
2135> #[serde(rename = "windSpeed")]
2136> wind_speed: String,
2137> #[serde(rename = "windDirection")]
2138> wind_direction: String,
2139> #[serde(rename = "detailedForecast")]
2140> detailed_forecast: String,
2141> }
2142> ```
2143>
2144> Now define the request types that MCP clients will send:
2145>
2146> ```rust theme={null}
2147> #[derive(serde::Deserialize, schemars::JsonSchema)]
2148> pub struct MCPForecastRequest {
2149> latitude: f32,
2150> longitude: f32,
2151> }
2152>
2153> #[derive(serde::Deserialize, schemars::JsonSchema)]
2154> pub struct MCPAlertRequest {
2155> state: String,
2156> }
2157> ```
2158>
2159> ### Helper functions
2160>
2161> Add helper functions for making API requests and formatting responses:
2162>
2163> ```rust theme={null}
2164> async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
2165> let client = reqwest::Client::new();
2166> let rsp = client
2167> .get(url)
2168> .header(reqwest::header::USER_AGENT, USER_AGENT)
2169> .header(reqwest::header::ACCEPT, "application/geo+json")
2170> .send()
2171> .await?
2172> .error_for_status()?;
2173> Ok(rsp.json::<T>().await?)
2174> }
2175>
2176> fn format_alert(feature: &AlertFeature) -> String {
2177> let props = &feature.properties;
2178> format!(
2179> "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
2180> props.event.as_deref().unwrap_or("Unknown"),
2181> props.area_desc.as_deref().unwrap_or("Unknown"),
2182> props.severity.as_deref().unwrap_or("Unknown"),
2183> props
2184> .description
2185> .as_deref()
2186> .unwrap_or("No description available"),
2187> props
2188> .instruction
2189> .as_deref()
2190> .unwrap_or("No specific instructions provided")
2191> )
2192> }
2193>
2194> fn format_period(period: &ForecastPeriod) -> String {
2195> format!(
2196> "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
2197> period.name,
2198> period.temperature,
2199> period.temperature_unit,
2200> period.wind_speed,
2201> period.wind_direction,
2202> period.detailed_forecast
2203> )
2204> }
2205> ```
2206>
2207> ### Implementing the Weather server and tools
2208>
2209> Now let's implement the main Weather server struct with the tool handlers:
2210>
2211> ```rust theme={null}
2212> pub struct Weather {
2213> tool_router: ToolRouter<Weather>,
2214> }
2215>
2216> #[tool_router]
2217> impl Weather {
2218> fn new() -> Self {
2219> Self {
2220> tool_router: Self::tool_router(),
2221> }
2222> }
2223>
2224> #[tool(description = "Get weather alerts for a US state.")]
2225> async fn get_alerts(
2226> &self,
2227> Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
2228> ) -> String {
2229> let url = format!(
2230> "{}/alerts/active/area/{}",
2231> NWS_API_BASE,
2232> state.to_uppercase()
2233> );
2234>
2235> match make_nws_request::<AlertsResponse>(&url).await {
2236> Ok(data) => {
2237> if data.features.is_empty() {
2238> "No active alerts for this state.".to_string()
2239> } else {
2240> data.features
2241> .iter()
2242> .map(format_alert)
2243> .collect::<Vec<_>>()
2244> .join("\n---\n")
2245> }
2246> }
2247> Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2248> }
2249> }
2250>
2251> #[tool(description = "Get weather forecast for a location.")]
2252> async fn get_forecast(
2253> &self,
2254> Parameters(MCPForecastRequest {
2255> latitude,
2256> longitude,
2257> }): Parameters<MCPForecastRequest>,
2258> ) -> String {
2259> let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
2260> let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
2261> return "Unable to fetch forecast data for this location.".to_string();
2262> };
2263>
2264> let forecast_url = points_data.properties.forecast;
2265>
2266> let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
2267> return "Unable to fetch forecast data for this location.".to_string();
2268> };
2269>
2270> let periods = &forecast_data.properties.periods;
2271> let forecast_summary: String = periods
2272> .iter()
2273> .take(5) // Next 5 periods only
2274> .map(format_period)
2275> .collect::<Vec<String>>()
2276> .join("\n---\n");
2277> forecast_summary
2278> }
2279> }
2280> ```
2281>
2282> The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2283>
2284> ### Implementing the ServerHandler
2285>
2286> Implement the `ServerHandler` trait to define server capabilities:
2287>
2288> ```rust theme={null}
2289> #[tool_handler]
2290> impl ServerHandler for Weather {
2291> fn get_info(&self) -> ServerInfo {
2292> ServerInfo {
2293> capabilities: ServerCapabilities::builder().enable_tools().build(),
2294> ..Default::default()
2295> }
2296> }
2297> }
2298> ```
2299>
2300> ### Running the server
2301>
2302> Finally, implement the main function to run the server with stdio transport:
2303>
2304> ```rust theme={null}
2305> #[tokio::main]
2306> async fn main() -> Result<()> {
2307> let transport = (tokio::io::stdin(), tokio::io::stdout());
2308> let service = Weather::new().serve(transport).await?;
2309> service.waiting().await?;
2310> Ok(())
2311> }
2312> ```
2313>
2314> Build your server with:
2315>
2316> ```bash theme={null}
2317> cargo build --release
2318> ```
2319>
2320> The compiled binary will be in `target/release/weather`.
2321>
2322> Let's now test your server from an existing MCP host, Claude for Desktop.
2323>
2324> ## Testing your server with Claude for Desktop
2325>
2326> > [!NOTE]
2327> >
2328> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2329>
2330> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2331>
2332> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2333>
2334> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2335>
2336> ```bash macOS/Linux theme={null}
2337> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2338> ```
2339>
2340> ```powershell Windows theme={null}
2341> code $env:AppData\Claude\claude_desktop_config.json
2342> ```
2343>
2344> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2345>
2346> In this case, we'll add our single weather server like so:
2347>
2348> ```json macOS/Linux theme={null}
2349> {
2350> "mcpServers": {
2351> "weather": {
2352> "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
2353> }
2354> }
2355> }
2356> ```
2357>
2358> ```json Windows theme={null}
2359> {
2360> "mcpServers": {
2361> "weather": {
2362> "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
2363> }
2364> }
2365> }
2366> ```
2367>
2368> > [!NOTE]
2369> >
2370> > Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2371>
2372> This tells Claude for Desktop:
2373>
2374> 1. There's an MCP server named "weather"
2375> 2. Launch it by running the compiled binary at the specified path
2376>
2377> Save the file, and restart **Claude for Desktop**.
2378 
2379> [tab] Go
2380>
2381> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-go)
2382>
2383> ### Prerequisite knowledge
2384>
2385> This quickstart assumes you have familiarity with:
2386>
2387> * Go
2388> * LLMs like Claude
2389>
2390> ### Logging in MCP Servers
2391>
2392> When implementing MCP servers, be careful about how you handle logging:
2393>
2394> **For STDIO-based servers:** Never use `fmt.Println()` or `fmt.Printf()`, as they write to standard output (stdout). Writing to stdout will corrupt the JSON-RPC messages and break your server.
2395>
2396> **For HTTP-based servers:** Standard output logging is fine since it doesn't interfere with HTTP responses.
2397>
2398> ### Best Practices
2399>
2400> * Use `log.Println()` (which defaults to stderr) or a logging library that writes to stderr or files.
2401> * Use `fmt.Fprintf(os.Stderr, ...)` to write to stderr explicitly.
2402>
2403> ### Quick Examples
2404>
2405> ```go theme={null}
2406> // ❌ Bad (STDIO)
2407> fmt.Println("Processing request")
2408>
2409> // ✅ Good (STDIO)
2410> log.Println("Processing request") // defaults to stderr
2411>
2412> // ✅ Good (STDIO)
2413> fmt.Fprintln(os.Stderr, "Processing request")
2414> ```
2415>
2416> ### System requirements
2417>
2418> * Go 1.24 or higher installed.
2419>
2420> ### Set up your environment
2421>
2422> First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2423>
2424> Verify your Go installation:
2425>
2426> ```bash theme={null}
2427> go version
2428> ```
2429>
2430> Now, let's create and set up our project:
2431>
2432> ```bash macOS/Linux theme={null}
2433> # Create a new directory for our project
2434> mkdir weather
2435> cd weather
2436>
2437> # Initialize Go module
2438> go mod init weather
2439>
2440> # Install dependencies
2441> go get github.com/modelcontextprotocol/go-sdk/mcp
2442>
2443> # Create our server file
2444> touch main.go
2445> ```
2446>
2447> ```powershell Windows theme={null}
2448> # Create a new directory for our project
2449> md weather
2450> cd weather
2451>
2452> # Initialize Go module
2453> go mod init weather
2454>
2455> # Install dependencies
2456> go get github.com/modelcontextprotocol/go-sdk/mcp
2457>
2458> # Create our server file
2459> new-item main.go
2460> ```
2461>
2462> Now let's dive into building your server.
2463>
2464> ## Building your server
2465>
2466> ### Importing packages and constants
2467>
2468> Add these to the top of your `main.go`:
2469>
2470> ```go theme={null}
2471> package main
2472>
2473> import (
2474> "cmp"
2475> "context"
2476> "encoding/json"
2477> "fmt"
2478> "io"
2479> "log"
2480> "net/http"
2481> "strings"
2482>
2483> "github.com/modelcontextprotocol/go-sdk/mcp"
2484> )
2485>
2486> const (
2487> NWSAPIBase = "https://api.weather.gov"
2488> UserAgent = "weather-app/1.0"
2489> )
2490> ```
2491>
2492> ### Data structures
2493>
2494> Next, let's define the data structures used by our tools:
2495>
2496> ```go theme={null}
2497> type PointsResponse struct {
2498> Properties struct {
2499> Forecast string `json:"forecast"`
2500> } `json:"properties"`
2501> }
2502>
2503> type ForecastResponse struct {
2504> Properties struct {
2505> Periods []ForecastPeriod `json:"periods"`
2506> } `json:"properties"`
2507> }
2508>
2509> type ForecastPeriod struct {
2510> Name string `json:"name"`
2511> Temperature int `json:"temperature"`
2512> TemperatureUnit string `json:"temperatureUnit"`
2513> WindSpeed string `json:"windSpeed"`
2514> WindDirection string `json:"windDirection"`
2515> DetailedForecast string `json:"detailedForecast"`
2516> }
2517>
2518> type AlertsResponse struct {
2519> Features []AlertFeature `json:"features"`
2520> }
2521>
2522> type AlertFeature struct {
2523> Properties AlertProperties `json:"properties"`
2524> }
2525>
2526> type AlertProperties struct {
2527> Event string `json:"event"`
2528> AreaDesc string `json:"areaDesc"`
2529> Severity string `json:"severity"`
2530> Description string `json:"description"`
2531> Instruction string `json:"instruction"`
2532> }
2533>
2534> type ForecastInput struct {
2535> Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2536> Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2537> }
2538>
2539> type AlertsInput struct {
2540> State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2541> }
2542> ```
2543>
2544> ### Helper functions
2545>
2546> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2547>
2548> ```go theme={null}
2549> func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
2550> req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
2551> if err != nil {
2552> return nil, fmt.Errorf("failed to create request: %w", err)
2553> }
2554>
2555> req.Header.Set("User-Agent", UserAgent)
2556> req.Header.Set("Accept", "application/geo+json")
2557>
2558> client := http.DefaultClient
2559> resp, err := client.Do(req)
2560> if err != nil {
2561> return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
2562> }
2563> defer resp.Body.Close()
2564>
2565> if resp.StatusCode != http.StatusOK {
2566> body, _ := io.ReadAll(resp.Body)
2567> return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
2568> }
2569>
2570> var result T
2571> if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
2572> return nil, fmt.Errorf("failed to decode response: %w", err)
2573> }
2574>
2575> return &result, nil
2576> }
2577>
2578> func formatAlert(alert AlertFeature) string {
2579> props := alert.Properties
2580> event := cmp.Or(props.Event, "Unknown")
2581> areaDesc := cmp.Or(props.AreaDesc, "Unknown")
2582> severity := cmp.Or(props.Severity, "Unknown")
2583> description := cmp.Or(props.Description, "No description available")
2584> instruction := cmp.Or(props.Instruction, "No specific instructions provided")
2585>
2586> return fmt.Sprintf(`
2587> Event: %s
2588> Area: %s
2589> Severity: %s
2590> Description: %s
2591> Instructions: %s
2592> `, event, areaDesc, severity, description, instruction)
2593> }
2594>
2595> func formatPeriod(period ForecastPeriod) string {
2596> return fmt.Sprintf(`
2597> %s:
2598> Temperature: %d°%s
2599> Wind: %s %s
2600> Forecast: %s
2601> `, period.Name, period.Temperature, period.TemperatureUnit,
2602> period.WindSpeed, period.WindDirection, period.DetailedForecast)
2603> }
2604> ```
2605>
2606> ### Implementing tool execution
2607>
2608> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2609>
2610> ```go theme={null}
2611> func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
2612> *mcp.CallToolResult, any, error,
2613> ) {
2614> // Get points data
2615> pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
2616> pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
2617> if err != nil {
2618> return &mcp.CallToolResult{
2619> Content: []mcp.Content{
2620> &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
2621> },
2622> }, nil, nil
2623> }
2624>
2625> // Get forecast data
2626> forecastURL := pointsData.Properties.Forecast
2627> if forecastURL == "" {
2628> return &mcp.CallToolResult{
2629> Content: []mcp.Content{
2630> &mcp.TextContent{Text: "Unable to fetch forecast URL."},
2631> },
2632> }, nil, nil
2633> }
2634>
2635> forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
2636> if err != nil {
2637> return &mcp.CallToolResult{
2638> Content: []mcp.Content{
2639> &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
2640> },
2641> }, nil, nil
2642> }
2643>
2644> // Format the periods
2645> periods := forecastData.Properties.Periods
2646> if len(periods) == 0 {
2647> return &mcp.CallToolResult{
2648> Content: []mcp.Content{
2649> &mcp.TextContent{Text: "No forecast periods available."},
2650> },
2651> }, nil, nil
2652> }
2653>
2654> // Show next 5 periods
2655> var forecasts []string
2656> for i := range min(5, len(periods)) {
2657> forecasts = append(forecasts, formatPeriod(periods[i]))
2658> }
2659>
2660> result := strings.Join(forecasts, "\n---\n")
2661>
2662> return &mcp.CallToolResult{
2663> Content: []mcp.Content{
2664> &mcp.TextContent{Text: result},
2665> },
2666> }, nil, nil
2667> }
2668>
2669> func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
2670> *mcp.CallToolResult, any, error,
2671> ) {
2672> // Build alerts URL
2673> stateCode := strings.ToUpper(input.State)
2674> alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
2675>
2676> alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
2677> if err != nil {
2678> return &mcp.CallToolResult{
2679> Content: []mcp.Content{
2680> &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
2681> },
2682> }, nil, nil
2683> }
2684>
2685> // Check if there are any alerts
2686> if len(alertsData.Features) == 0 {
2687> return &mcp.CallToolResult{
2688> Content: []mcp.Content{
2689> &mcp.TextContent{Text: "No active alerts for this state."},
2690> },
2691> }, nil, nil
2692> }
2693>
2694> // Format alerts
2695> var alerts []string
2696> for _, feature := range alertsData.Features {
2697> alerts = append(alerts, formatAlert(feature))
2698> }
2699>
2700> result := strings.Join(alerts, "\n---\n")
2701>
2702> return &mcp.CallToolResult{
2703> Content: []mcp.Content{
2704> &mcp.TextContent{Text: result},
2705> },
2706> }, nil, nil
2707> }
2708> ```
2709>
2710> ### Running the server
2711>
2712> Finally, implement the main function to run the server:
2713>
2714> ```go theme={null}
2715> func main() {
2716> // Create MCP server
2717> server := mcp.NewServer(&mcp.Implementation{
2718> Name: "weather",
2719> Version: "1.0.0",
2720> }, nil)
2721>
2722> // Add get_forecast tool
2723> mcp.AddTool(server, &mcp.Tool{
2724> Name: "get_forecast",
2725> Description: "Get weather forecast for a location",
2726> }, getForecast)
2727>
2728> // Add get_alerts tool
2729> mcp.AddTool(server, &mcp.Tool{
2730> Name: "get_alerts",
2731> Description: "Get weather alerts for a US state",
2732> }, getAlerts)
2733>
2734> // Run server on stdio transport
2735> if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
2736> log.Fatal(err)
2737> }
2738> }
2739> ```
2740>
2741> Build your server with:
2742>
2743> ```bash theme={null}
2744> go build -o weather .
2745> ```
2746>
2747> The compiled binary will be in `./weather`.
2748>
2749> Let's now test your server from an existing MCP host, Claude for Desktop.
2750>
2751> ## Testing your server with Claude for Desktop
2752>
2753> > [!NOTE]
2754> >
2755> > Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md) tutorial to build an MCP client that connects to the server we just built.
2756>
2757> First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
2758>
2759> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
2760>
2761> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2762>
2763> ```bash macOS/Linux theme={null}
2764> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2765> ```
2766>
2767> ```powershell Windows theme={null}
2768> code $env:AppData\Claude\claude_desktop_config.json
2769> ```
2770>
2771> You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
2772>
2773> In this case, we'll add our single weather server like so:
2774>
2775> ```json macOS/Linux theme={null}
2776> {
2777> "mcpServers": {
2778> "weather": {
2779> "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
2780> }
2781> }
2782> }
2783> ```
2784>
2785> ```json Windows theme={null}
2786> {
2787> "mcpServers": {
2788> "weather": {
2789> "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
2790> }
2791> }
2792> }
2793> ```
2794>
2795> > [!NOTE]
2796> >
2797> > Make sure you pass in the absolute path to your compiled binary. You can get this by running `pwd` on macOS/Linux or `cd` on Windows Command Prompt from your project directory. On Windows, remember to use double backslashes (`\\`) or forward slashes (`/`) in the JSON path, and add the `.exe` extension.
2798>
2799> This tells Claude for Desktop:
2800>
2801> 1. There's an MCP server named "weather"
2802> 2. Launch it by running the compiled binary at the specified path
2803>
2804> Save the file, and restart **Claude for Desktop**.
28052805 
28062806### Test with commands
28072807 
from line 2841
28412841 
28422842## Troubleshooting
28432843 
2844> **Claude for Desktop Integration Issues**
2844> [accordion] Claude for Desktop Integration Issues
28452845>
28462846> **Getting logs from Claude for Desktop**
28472847>
from line 2886
28862886>
28872887> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2025-06-18/tools/debugging.md) for better debugging tools and more detailed guidance.
28882888 
2889> **Weather API Issues**
2889> [accordion] Weather API Issues
28902890>
28912891> **Error: Failed to retrieve grid point data**
28922892>
from line 2912
29122912 
29132913## Next steps
29142914 
2915> **[Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md)**
2915> [card] [Building a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md)
29162916>
29172917> Learn how to build your own MCP client that can connect to your server
29182918 
2919> **[Example servers](https://modelcontextprotocol.io/examples.md)**
2919> [card] [Example servers](https://modelcontextprotocol.io/examples.md)
29202920>
29212921> Check out our gallery of official MCP servers and implementations
29222922 
2923> **[Debugging Guide](https://modelcontextprotocol.io/docs/2025-06-18/tools/debugging.md)**
2923> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2025-06-18/tools/debugging.md)
29242924>
29252925> Learn how to effectively debug MCP servers and integrations
29262926 
2927> **[Build with Agent Skills](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-with-agent-skills.md)**
2927> [card] [Build with Agent Skills](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-with-agent-skills.md)
29282928>
29292929> Use agent skills to guide AI coding assistants through server design
29302930 

docs/2025-06-18/develop/build-with-agent-skills Changed · +3 / -3 lines

from line 85
8585Once your agent scaffolds the server, iterate on tool descriptions and error
8686handling, then test and ship:
8787 
88> **[MCP Inspector](https://modelcontextprotocol.io/docs/2025-06-18/tools/inspector.md)**
88> [card] [MCP Inspector](https://modelcontextprotocol.io/docs/2025-06-18/tools/inspector.md)
8989>
9090> Test your server's tools, resources, and prompts interactively
9191 
92> **[Connect to a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-local-servers.md)**
92> [card] [Connect to a client](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-local-servers.md)
9393>
9494> Wire your server into an MCP client via local or remote configuration
9595 
96> **[Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)**
96> [card] [Publish to the Registry](https://modelcontextprotocol.io/registry/quickstart.md)
9797>
9898> Make your server discoverable in the MCP Registry
9999 

docs/2025-06-18/develop/connect-local-servers Changed · +9 / -9 lines

from line 156
156156 
157157If you encounter issues setting up or using the Filesystem Server, these solutions address common problems:
158158 
159> **Server not showing up in Claude / hammer icon missing**
159> [accordion] Server not showing up in Claude / hammer icon missing
160160>
161161> 1. Restart Claude Desktop completely
162162> 2. Check your `claude_desktop_config.json` file syntax
from line 172
172172> npx -y @modelcontextprotocol/server-filesystem C:\Users\username\Desktop C:\Users\username\Downloads
173173> ```
174174 
175> **Getting logs from Claude Desktop**
175> [accordion] Getting logs from Claude Desktop
176176>
177177> Claude.app logging related to MCP is written to log files in:
178178>
from line 194
194194> type "%APPDATA%\Claude\logs\mcp*.log"
195195> ```
196196 
197> **Tool calls failing silently**
197> [accordion] Tool calls failing silently
198198>
199199> If Claude attempts to use the tools but they fail:
200200>
from line 202
202202> 2. Verify your server builds and runs without errors
203203> 3. Try restarting Claude Desktop
204204 
205> **None of this is working. What do I do?**
205> [accordion] None of this is working. What do I do?
206206>
207207> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2025-06-18/tools/debugging.md) for better debugging tools and more detailed guidance.
208208 
209> **ENOENT error and `$\{APPDATA\}` in paths on Windows**
209> [accordion] ENOENT error and `$\{APPDATA\}` in paths on Windows
210210>
211211> If your configured server fails to load, and you see within its logs an error referring to `${APPDATA}` within a path, you may need to add the expanded value of `%APPDATA%` to your `env` key in `claude_desktop_config.json`:
212212>
from line 239
239239 
240240Now that you've successfully connected Claude Desktop to a local MCP server, explore these options to expand your setup:
241241 
242> **[Explore other servers](https://github.com/modelcontextprotocol/servers)**
242> [card] [Explore other servers](https://github.com/modelcontextprotocol/servers)
243243>
244244> Browse our collection of official and community-created MCP servers for
245245> additional capabilities
246246 
247> **[Build your own server](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)**
247> [card] [Build your own server](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)
248248>
249249> Create custom MCP servers tailored to your specific workflows and
250250> integrations
251251 
252> **[Connect to remote servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-remote-servers.md)**
252> [card] [Connect to remote servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-remote-servers.md)
253253>
254254> Learn how to connect Claude to remote MCP servers for cloud-based tools and
255255> services
256256 
257> **[Understand the protocol](https://modelcontextprotocol.io/docs/2025-06-18/learn/architecture.md)**
257> [card] [Understand the protocol](https://modelcontextprotocol.io/docs/2025-06-18/learn/architecture.md)
258258>
259259> Dive deeper into how MCP works and its architecture
260260 

docs/2025-06-18/develop/connect-remote-servers Changed · +4 / -4 lines

from line 81
8181 
8282Now that you've connected Claude to a remote MCP server, you can explore its capabilities in your conversations. Try using the connected tools to automate tasks, access external data, or integrate with your existing workflows.
8383 
84> **[Build your own remote server](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)**
84> [card] [Build your own remote server](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)
8585>
8686> Create custom remote MCP servers to integrate with proprietary tools and
8787> services
8888 
89> **[Explore available servers](https://github.com/modelcontextprotocol/servers)**
89> [card] [Explore available servers](https://github.com/modelcontextprotocol/servers)
9090>
9191> Browse our collection of official and community-created MCP servers
9292 
93> **[Connect local servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-local-servers.md)**
93> [card] [Connect local servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/connect-local-servers.md)
9494>
9595> Learn how to connect Claude Desktop to local MCP servers for direct system
9696> access
9797 
98> **[Understand the architecture](https://modelcontextprotocol.io/docs/2025-06-18/learn/architecture.md)**
98> [card] [Understand the architecture](https://modelcontextprotocol.io/docs/2025-06-18/learn/architecture.md)
9999>
100100> Dive deeper into how MCP works and its architecture
101101 

docs/2025-06-18/getting-started/intro Changed · +6 / -6 lines

from line 29
2929 
3030## Start Building
3131 
32> **[Build servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)**
32> [card] [Build servers](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-server.md)
3333>
3434> Create MCP servers to expose your data and tools
3535 
36> **[Build clients](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md)**
36> [card] [Build clients](https://modelcontextprotocol.io/docs/2025-06-18/develop/build-client.md)
3737>
3838> Develop applications that connect to MCP servers
3939 
40> **[Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)**
40> [card] [Build MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview.md)
4141>
4242> Build interactive apps that run inside AI clients
4343 
4444## Learn more
4545 
46> **[Architecture](https://modelcontextprotocol.io/docs/2025-06-18/learn/architecture.md)**
46> [card] [Architecture](https://modelcontextprotocol.io/docs/2025-06-18/learn/architecture.md)
4747>
4848> Learn the core concepts and architecture of MCP
4949 
50> **[Security](https://modelcontextprotocol.io/docs/2025-06-18/tutorials/security/security_best_practices.md)**
50> [card] [Security](https://modelcontextprotocol.io/docs/2025-06-18/tutorials/security/security_best_practices.md)
5151>
5252> Understand the security considerations and best practices for MCP
5353 
5454## Community
5555 
56> **[Contributing](https://modelcontextprotocol.io/community/contributing.md)**
56> [card] [Contributing](https://modelcontextprotocol.io/community/contributing.md)
5757>
5858> Learn how to get involved and contribute to MCP
5959 
Feedback