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

Build an MCP server changeddocs/2026-07-28/develop/build-server

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

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

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

The whole hunk

from line 8, old and new numbered
/
lines

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

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