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/draft/develop/build-client).
17</Note>
11
12
13> [!NOTE]
14>
15> Servers can connect to any client. We've chosen Claude for Desktop here for simplicity, but we also have a guide on [building your own client](/docs/draft/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/draft/tools/inspector/cli) in the `modern` [protocol era](/docs/draft/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")
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/draft/tools/inspector/cli) in the `modern` [protocol era](/docs/draft/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")
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 /"  icon:
2814
2815
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
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
2829
2830
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 <CodeGroup>
2927 ```bash macOS theme={null}
2928 # Check Claude's logs for errors
2929 tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
2930 ```
2931
2932 ```bash Linux theme={null}
2933 # Check Claude's logs for errors
2934 tail -n 20 -f ~/.config/Claude/logs/mcp*.log
2935 ```
2936 </CodeGroup>
2937
2938 **Server not showing up in Claude**
2939
2940 1. Check your `claude_desktop_config.json` file syntax
2941 2. Make sure the path to your project is absolute and not relative
2942 3. Restart Claude for Desktop completely
2943
2944 <Warning>
2945 To properly restart Claude for Desktop, you must fully quit the application:
2946
2947 * **Windows**: Right-click the Claude icon in the system tray (which may be hidden in the "hidden icons" menu) and select "Quit" or "Exit".
2948 * **macOS**: Use Cmd+Q or select "Quit Claude" from the menu bar.
2949 * **Linux**: Right-click the Claude icon in the system tray and select "Quit", or run `pkill -f claude-desktop` from a terminal.
2950
2951 Simply closing the window does not fully quit the application, and your MCP server configuration changes will not take effect.
2952 </Warning>
2953
2954 **Tool calls failing silently**
2955
2956 If Claude attempts to use the tools but they fail:
2957
2958 1. Check Claude's logs for errors
2959 2. Verify your server builds and runs without errors
2960 3. Try restarting Claude for Desktop
2961
2962 **None of this is working. What do I do?**
2963
2964 Please refer to our [debugging guide](/docs/draft/tools/debugging) for better debugging tools and more detailed guidance.
2965 </Accordion>
2966
2967 <Accordion title="Weather API Issues">
2968 **Error: Failed to retrieve grid point data**
2969
2970 This usually means either:
2971
2972 1. The coordinates are outside the US
2973 2. The NWS API is having issues
2974 3. You're being rate limited
2975
2976 Fix:
2977
2978 * Verify you're using US coordinates
2979 * Add a small delay between requests
2980 * Check the NWS API status page
2981
2982 **Error: No active alerts for \[STATE]**
2983
2984 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.
2985 </Accordion>
2986</AccordionGroup>
2987
2988<Note>
2989 For more advanced troubleshooting, check out our guide on [Debugging MCP](/docs/draft/tools/debugging)
2990</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/draft/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/draft/tools/debugging)
29912923
29922924## Next steps
29932925
2994<CardGroup cols={2}>
2995 <Card title="Building a client" icon="outlet" href="/docs/draft/develop/build-client">
2996 Learn how to build your own MCP client that can connect to your server
2997 </Card>
2998
2999 <Card title="Example servers" icon="grid" href="/examples">
3000 Check out our gallery of official MCP servers and implementations
3001 </Card>
3002
3003 <Card title="Debugging Guide" icon="bug" href="/docs/draft/tools/debugging">
3004 Learn how to effectively debug MCP servers and integrations
3005 </Card>
3006
3007 <Card title="Build with Agent Skills" icon="comments" href="/docs/draft/develop/build-with-agent-skills">
3008 Use agent skills to guide AI coding assistants through server design
3009 </Card>
3010</CardGroup>
2926> **[Building a client](/docs/draft/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/draft/tools/debugging)**
2935>
2936> Learn how to effectively debug MCP servers and integrations
2937
2938> **[Build with Agent Skills](/docs/draft/develop/build-with-agent-skills)**
2939>
2940> Use agent skills to guide AI coding assistants through server design
30112941
No line in this hunk matches that.