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

Build an MCP server changeddocs/2025-03-26/develop/build-server

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

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

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

### 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 whole hunk

from line 8, old and new numbered
/
lines

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

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