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-11-25/develop/build-client).
17</Note>
11
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-11-25/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-11-25/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-11-25/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")
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-11-25/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-11-25/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-11-25/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-11-25/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-11-25/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")
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-11-25/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-11-25/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-11-25/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-11-25/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-11-25/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-11-25/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-11-25/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 /"  icon:
2809
2810
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
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
2824
2825
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-11-25/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-11-25/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-11-25/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-11-25/tools/debugging)
29782912
29792913## Next steps
29802914
2981<CardGroup cols={2}>
2982 <Card title="Building a client" icon="outlet" href="/docs/2025-11-25/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-11-25/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-11-25/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-11-25/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-11-25/tools/debugging)**
2924>
2925> Learn how to effectively debug MCP servers and integrations
2926
2927> **[Build with Agent Skills](/docs/2025-11-25/develop/build-with-agent-skills)**
2928>
2929> Use agent skills to guide AI coding assistants through server design
29982930
No line in this hunk matches that.