The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 24
2424
2525This tutorial will primarily focus on tools.
2626
27**Python**
28
29Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30
31### Prerequisite knowledge
32
33This quickstart assumes you have familiarity with:
34
35* Python
36* LLMs like Claude
37
38### Logging in MCP Servers
39
40When implementing MCP servers, be careful about how you handle logging:
41
42**For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, 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)
184 """
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](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/cli.md) in the `modern` [protocol era](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/protocol-eras.md):
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 ->
1217 """
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 ->
1233 """
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.";
1570 }
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)"
1823 }
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"
1861 }
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"
1944 }
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"
1956 }
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(),
2226 }
2227 }
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 }
2251 }
2252 Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2253 }
2254 }
2255
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()
2300 }
2301 }
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"
2358 }
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"
2368 }
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"
2785 }
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"
2795 }
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**.
27> [tab] Python
28>
29> Let's get started with building our weather server! [You can find the complete code for what we'll be building here.](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/weather-server-python)
30>
31> ### Prerequisite knowledge
32>
33> This quickstart assumes you have familiarity with:
34>
35> * Python
36> * LLMs like Claude
37>
38> ### Logging in MCP Servers
39>
40> When implementing MCP servers, be careful about how you handle logging:
41>
42> **For STDIO-based servers:** Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The `print()` function writes to stdout by default, 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}
54> import logging
55>
56> logger = logging.getLogger(__name__)
57>
58> # ❌ Bad (STDIO)
59> print("Processing request")
60>
61> # ✅ Good (STDIO)
62> logger.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>
72> First, let's install `uv` and set up our Python project and environment:
73>
74> ```bash macOS/Linux theme={null}
75> curl -LsSf https://astral.sh/uv/install.sh | sh
76> ```
77>
78> ```powershell Windows theme={null}
79> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
80> ```
81>
82> Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
83>
84> Now, let's create and set up our project:
85>
86> ```bash macOS/Linux theme={null}
87> # Create a new directory for our project
88> uv init weather
89> cd weather
90>
91> # Create virtual environment and activate it
92> uv venv
93> source .venv/bin/activate
94>
95> # Install dependencies
96> uv add "mcp[cli]"
97>
98> # Create our server file
99> touch weather.py
100> ```
101>
102> ```powershell Windows theme={null}
103> # Create a new directory for our project
104> uv init weather
105> cd weather
106>
107> # Create virtual environment and activate it
108> uv venv
109> .venv\Scripts\activate
110>
111> # Install dependencies
112> uv add mcp[cli]
113>
114> # Create our server file
115> new-item weather.py
116> ```
117>
118> Now let's dive into building your server.
119>
120> ## Building your server
121>
122> ### Importing packages and setting up the instance
123>
124> Add these to the top of your `weather.py`:
125>
126> ```python theme={null}
127> from typing import Any
128>
129> import httpx2
130> from mcp.server import MCPServer
131>
132> # Initialize MCPServer
133> mcp = MCPServer("weather")
134>
135> # Constants
136> NWS_API_BASE = "https://api.weather.gov"
137> USER_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>
142> The 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>
146> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
147>
148> ```python theme={null}
149> async 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>
161> def format_alert(feature: dict) -> str:
162> """Format an alert feature into a readable string."""
163> props = feature["properties"]
164> return f"""
165> Event: {props.get("event", "Unknown")}
166> Area: {props.get("areaDesc", "Unknown")}
167> Severity: {props.get("severity", "Unknown")}
168> Description: {props.get("description", "No description available")}
169> Instructions: {props.get("instruction", "No specific instructions provided")}
170> """
171> ```
172>
173> ### Implementing tool execution
174>
175> The 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()
179> async 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)
184> """
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()
199> async 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"]}:
226> Temperature: {period["temperature"]}°{period["temperatureUnit"]}
227> Wind: {period["windSpeed"]} {period["windDirection"]}
228> Forecast: {period["detailedForecast"]}
229> """
230> forecasts.append(forecast)
231>
232> return "\n---\n".join(forecasts)
233> ```
234>
235> ### Running the server
236>
237> Finally, let's initialize and run the server:
238>
239> ```python theme={null}
240> if __name__ == "__main__":
241> mcp.run(transport="stdio")
242> ```
243>
244> Your server is complete! Run `uv run weather.py` to start the MCP server, which will listen for messages from MCP hosts.
245>
246> Let's now test your server from an existing MCP host, Claude for Desktop.
247>
248> ## Testing your server with Claude for Desktop
249>
250> First, make sure you have Claude for Desktop installed. [You can install the latest version
251> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
252>
253> 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.
254>
255> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
256>
257> ```bash Linux theme={null}
258> code ~/.config/Claude/claude_desktop_config.json
259> ```
260>
261> ```bash macOS theme={null}
262> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
263> ```
264>
265> ```powershell Windows theme={null}
266> code $env:AppData\Claude\claude_desktop_config.json
267> ```
268>
269> 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.
270>
271> In 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>
313> This tells Claude for Desktop:
314>
315> 1. There's an MCP server named "weather"
316> 2. To launch it by running `uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py`
317>
318> Save the file, and restart **Claude for Desktop**.
319
320> [tab] TypeScript
321>
322> 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)
323>
324> ### Prerequisite knowledge
325>
326> This quickstart assumes you have familiarity with:
327>
328> * TypeScript
329> * LLMs like Claude
330>
331> ### Logging in MCP Servers
332>
333> When 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)
347> console.log("Server started");
348>
349> // ✅ Good (STDIO)
350> console.error("Server started"); // stderr is safe
351> ```
352>
353> ### System requirements
354>
355> For TypeScript, make sure you have the latest version of Node installed.
356>
357> ### Set up your environment
358>
359> First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
360> Verify your Node.js installation:
361>
362> ```bash theme={null}
363> node --version
364> npm --version
365> ```
366>
367> For this tutorial, you'll need Node.js version 20 or higher.
368>
369> Now, let's create and set up our project:
370>
371> ```bash macOS/Linux theme={null}
372> # Create a new directory for our project
373> mkdir weather
374> cd weather
375>
376> # Initialize a new npm project
377> npm init -y
378>
379> # Install dependencies
380> npm install @modelcontextprotocol/server zod
381> npm install -D @types/node typescript
382>
383> # Create our files
384> mkdir src
385> touch src/index.ts
386> ```
387>
388> ```powershell Windows theme={null}
389> # Create a new directory for our project
390> md weather
391> cd weather
392>
393> # Initialize a new npm project
394> npm init -y
395>
396> # Install dependencies
397> npm install @modelcontextprotocol/server zod
398> npm install -D @types/node typescript
399>
400> # Create our files
401> md src
402> new-item src\index.ts
403> ```
404>
405> Update 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>
420> Create 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>
441> Now let's dive into building your server.
442>
443> ## Building your server
444>
445> ### Importing packages and setting up constants
446>
447> Add these to the top of your `src/index.ts`:
448>
449> ```typescript theme={null}
450> import { McpServer } from "@modelcontextprotocol/server";
451> import { serveStdio } from "@modelcontextprotocol/server/stdio";
452> import { z } from "zod";
453>
454> const NWS_API_BASE = "https://api.weather.gov";
455> const USER_AGENT = "weather-app/1.0";
456> ```
457>
458> ### Helper functions
459>
460> Next, 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
464> async 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>
482> interface 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
493> function 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>
505> interface ForecastPeriod {
506> name?: string;
507> temperature?: number;
508> temperatureUnit?: string;
509> windSpeed?: string;
510> windDirection?: string;
511> shortForecast?: string;
512> }
513>
514> interface AlertsResponse {
515> features: AlertFeature[];
516> }
517>
518> interface PointsResponse {
519> properties: {
520> forecast?: string;
521> };
522> }
523>
524> interface ForecastResponse {
525> properties: {
526> periods: ForecastPeriod[];
527> };
528> }
529> ```
530>
531> ### Implementing tool execution
532>
533> 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:
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.
538> function 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>
697> Finally, serve the server over stdio:
698>
699> ```typescript theme={null}
700> serveStdio(createServer, {
701> onerror: (error) => console.error("Weather MCP Server error:", error),
702> });
703> console.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>
708> Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
709>
710> To check that the server answers 2026-07-28 clients, list its tools with the [Inspector CLI](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/cli.md) in the `modern` [protocol era](https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector/protocol-eras.md):
711>
712> ```bash theme={null}
713> npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list
714> ```
715>
716> 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`.
717>
718> Let's now test your server from an existing MCP host, Claude for Desktop.
719>
720> ## Testing your server with Claude for Desktop
721>
722> First, make sure you have Claude for Desktop installed. [You can install the latest version
723> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
724>
725> We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
726>
727> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
728>
729> ```bash Linux theme={null}
730> code ~/.config/Claude/claude_desktop_config.json
731> ```
732>
733> ```bash macOS theme={null}
734> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
735> ```
736>
737> ```powershell Windows theme={null}
738> code $env:AppData\Claude\claude_desktop_config.json
739> ```
740>
741> 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.
742>
743> In 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>
767> This tells Claude for Desktop:
768>
769> 1. There's an MCP server named "weather"
770> 2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
771>
772> Save the file, and restart **Claude for Desktop**.
773
774> [tab] 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>
781> Let'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>
784> 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.
785> For manual MCP Server implementation, refer to the [MCP Server Java SDK documentation](https://java.sdk.modelcontextprotocol.io/).
786>
787> ### Logging in MCP Servers
788>
789> When 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>
807> Use the [Spring Initializer](https://start.spring.io/) to bootstrap the project.
808>
809> You 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}
826> dependencies {
827> implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
828> implementation platform("org.springframework:spring-web")
829> }
830> ```
831>
832> Then configure your application by setting the application properties:
833>
834> ```bash application.properties theme={null}
835> spring.main.bannerMode=off
836> logging.pattern.console=
837> ```
838>
839> ```yaml application.yml theme={null}
840> logging:
841> pattern:
842> console:
843> spring:
844> main:
845> banner-mode: off
846> ```
847>
848> 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.
849>
850> Now let's dive into building your server.
851>
852> ## Building your server
853>
854> ### Weather Service
855>
856> 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:
857>
858> ```java theme={null}
859> @Service
860> public 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>
899> The `@Service` annotation will auto-register the service in your application context.
900> The Spring AI `@Tool` annotation makes it easy to create and maintain MCP tools.
901>
902> The auto-configuration will automatically register these tools with the MCP server.
903>
904> ### Create your Boot Application
905>
906> ```java theme={null}
907> @SpringBootApplication
908> public 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>
921> Uses the `MethodToolCallbackProvider` utils to convert the `@Tools` into actionable callbacks used by the MCP server.
922>
923> ### Running the server
924>
925> Finally, let's build the server:
926>
927> ```bash theme={null}
928> ./mvnw clean install
929> ```
930>
931> This will generate an `mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar` file within the `target` folder.
932>
933> Let's now test your server from an existing MCP host, Claude for Desktop.
934>
935> ## Testing your server with Claude for Desktop
936>
937> First, make sure you have Claude for Desktop installed.
938> [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
939>
940> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
941> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
942> Make sure to create the file if it doesn't exist.
943>
944> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
945>
946> ```bash Linux theme={null}
947> code ~/.config/Claude/claude_desktop_config.json
948> ```
949>
950> ```bash macOS theme={null}
951> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
952> ```
953>
954> ```powershell Windows theme={null}
955> code $env:AppData\Claude\claude_desktop_config.json
956> ```
957>
958> You'll then add your servers in the `mcpServers` key.
959> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
960>
961> In 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>
997> This tells Claude for Desktop:
998>
999> 1. There's an MCP server named "my-weather-server"
1000> 2. To launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar`
1001>
1002> Save the file, and restart **Claude for Desktop**.
1003>
1004> ## Testing your server with Java client
1005>
1006> ### Create an MCP Client manually
1007>
1008> Use the `McpClient` to connect to the server:
1009>
1010> ```java theme={null}
1011> var stdioParams = ServerParameters.builder("java")
1012> .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
1013> .build();
1014>
1015> var stdioTransport = new StdioClientTransport(stdioParams);
1016>
1017> var mcpClient = McpClient.sync(stdioTransport).build();
1018>
1019> mcpClient.initialize();
1020>
1021> ListToolsResult toolsList = mcpClient.listTools();
1022>
1023> CallToolResult weather = mcpClient.callTool(
1024> new CallToolRequest("getWeatherForecastByLocation",
1025> Map.of("latitude", "47.6062", "longitude", "-122.3321")));
1026>
1027> CallToolResult alert = mcpClient.callTool(
1028> new CallToolRequest("getAlerts", Map.of("state", "NY")));
1029>
1030> mcpClient.closeGracefully();
1031> ```
1032>
1033> ### Use MCP Client Boot Starter
1034>
1035> Create 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>
1044> and set the `spring.ai.mcp.client.stdio.servers-configuration` property to point to your `claude_desktop_config.json`.
1045> You can reuse the existing Anthropic Desktop configuration:
1046>
1047> ```properties theme={null}
1048> spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json
1049> ```
1050>
1051> When you start your client application, the auto-configuration will automatically create MCP clients from the claude\_desktop\_config.json.
1052>
1053> 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.
1054>
1055> ## More Java MCP Server examples
1056>
1057> 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.
1058> Set the `spring.ai.mcp.server.protocol=STREAMABLE` property to serve it over Streamable HTTP.
1059> It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot's auto-configuration capabilities.
1060
1061> [tab] Kotlin
1062>
1063> 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)
1064>
1065> ### Prerequisite knowledge
1066>
1067> This quickstart assumes you have familiarity with:
1068>
1069> * Kotlin
1070> * LLMs like Claude
1071>
1072> ### Logging in MCP Servers
1073>
1074> When 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>
1090> First, let's install `java` and `gradle` if you haven't already.
1091> You can download `java` from [official Oracle JDK website](https://www.oracle.com/java/technologies/downloads/).
1092> Verify your `java` installation:
1093>
1094> ```bash theme={null}
1095> java --version
1096> ```
1097>
1098> Now, let's create and set up your project:
1099>
1100> ```bash macOS/Linux theme={null}
1101> # Create a new directory for our project
1102> mkdir weather
1103> cd weather
1104>
1105> # Initialize a new kotlin project
1106> gradle init
1107> ```
1108>
1109> ```powershell Windows theme={null}
1110> # Create a new directory for our project
1111> md weather
1112> cd weather
1113>
1114> # Initialize a new kotlin project
1115> gradle init
1116> ```
1117>
1118> After running `gradle init`, select **Application** as the project type, **Kotlin** as the programming language.
1119>
1120> Alternatively, you can create a Kotlin application using the [IntelliJ IDEA project wizard](https://kotlinlang.org/docs/jvm-get-started.html).
1121>
1122> After 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
1126> val mcpVersion = "0.9.0"
1127> val ktorVersion = "3.2.3"
1128> val slf4jVersion = "2.0.17"
1129>
1130> plugins {
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>
1137> application {
1138> mainClass.set("MainKt")
1139> }
1140>
1141> dependencies {
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>
1150> Verify that everything is set up correctly:
1151>
1152> ```bash theme={null}
1153> ./gradlew build
1154> ```
1155>
1156> Now let's dive into building your server.
1157>
1158> ## Building your server
1159>
1160> ### Setting up the instance
1161>
1162> Add a server initialization function:
1163>
1164> ```kotlin theme={null}
1165> fun 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>
1196> Next, let's add functions and data classes for querying and converting responses from the National Weather Service API:
1197>
1198> ```kotlin theme={null}
1199> val 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
1214> suspend fun HttpClient.getAlerts(state: String): List<String> {
1215> val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
1216> return alerts.features.map { feature ->
1217> """
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
1228> suspend 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 ->
1233> """
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
1243> data class PointsResponse(val properties: PointsProperties)
1244>
1245> @Serializable
1246> data class PointsProperties(val forecast: String? = null)
1247>
1248> @Serializable
1249> data class ForecastResponse(val properties: ForecastProperties)
1250>
1251> @Serializable
1252> data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())
1253>
1254> @Serializable
1255> data 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
1265> data class AlertsResponse(val features: List<AlertFeature> = emptyList())
1266>
1267> @Serializable
1268> data class AlertFeature(val properties: AlertProperties)
1269>
1270> @Serializable
1271> data 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>
1282> The 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>
1287> server.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>
1309> server.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>
1341> Finally, implement the main function to run the server:
1342>
1343> ```kotlin theme={null}
1344> fun main() = runMcpServer()
1345> ```
1346>
1347> You can run the server directly during development:
1348>
1349> ```bash theme={null}
1350> ./gradlew run
1351> ```
1352>
1353> For production use, build the shadow JAR:
1354>
1355> ```bash theme={null}
1356> ./gradlew build
1357> java -jar build/libs/weather-0.1.0-all.jar
1358> ```
1359>
1360> Let's now test your server from an existing MCP host, Claude for Desktop.
1361>
1362> ## Testing your server with Claude for Desktop
1363>
1364> First, make sure you have Claude for Desktop installed. [You can install the latest version
1365> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1366>
1367> We'll need to configure Claude for Desktop for whichever MCP servers you want to use.
1368> To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
1369> Make sure to create the file if it doesn't exist.
1370>
1371> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1372>
1373> ```bash Linux theme={null}
1374> code ~/.config/Claude/claude_desktop_config.json
1375> ```
1376>
1377> ```bash macOS theme={null}
1378> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1379> ```
1380>
1381> ```powershell Windows theme={null}
1382> code $env:AppData\Claude\claude_desktop_config.json
1383> ```
1384>
1385> You'll then add your servers in the `mcpServers` key.
1386> The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
1387>
1388> In 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>
1418> This tells Claude for Desktop:
1419>
1420> 1. There's an MCP server named "weather"
1421> 2. Launch it by running `java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar`
1422>
1423> Save the file, and restart **Claude for Desktop**.
1424
1425> [tab] C#
1426>
1427> 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)
1428>
1429> ### Prerequisite knowledge
1430>
1431> This 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>
1439> When 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>
1455> 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:
1456>
1457> ```bash theme={null}
1458> dotnet --version
1459> ```
1460>
1461> Now, let's create and set up your project:
1462>
1463> ```bash macOS/Linux theme={null}
1464> # Create a new directory for our project
1465> mkdir weather
1466> cd weather
1467> # Initialize a new C# project
1468> dotnet new console
1469> ```
1470>
1471> ```powershell Windows theme={null}
1472> # Create a new directory for our project
1473> mkdir weather
1474> cd weather
1475> # Initialize a new C# project
1476> dotnet new console
1477> ```
1478>
1479> After running `dotnet new console`, you will be presented with a new C# project.
1480> You can open the project in your favorite IDE, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Rider](https://www.jetbrains.com/rider/).
1481> 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).
1482> After 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
1486> dotnet add package ModelContextProtocol --prerelease
1487> # Add the .NET Hosting NuGet package
1488> dotnet add package Microsoft.Extensions.Hosting
1489> ```
1490>
1491> Now let’s dive into building your server.
1492>
1493> ## Building your server
1494>
1495> Open the `Program.cs` file in your project and replace its contents with the following code:
1496>
1497> ```csharp theme={null}
1498> using Microsoft.Extensions.DependencyInjection;
1499> using Microsoft.Extensions.Hosting;
1500> using ModelContextProtocol;
1501> using System.Net.Http.Headers;
1502>
1503> var builder = Host.CreateEmptyApplicationBuilder(settings: null);
1504>
1505> builder.Services.AddMcpServer()
1506> .WithStdioServerTransport()
1507> .WithToolsFromAssembly();
1508>
1509> builder.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>
1516> var app = builder.Build();
1517>
1518> await 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>
1525> 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.
1526>
1527> ### Weather API helper functions
1528>
1529> Create an extension class for `HttpClient` which helps simplify JSON request handling:
1530>
1531> ```csharp theme={null}
1532> using System.Text.Json;
1533>
1534> internal 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>
1545> Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
1546>
1547> ```csharp theme={null}
1548> using ModelContextProtocol.Server;
1549> using System.ComponentModel;
1550> using System.Globalization;
1551> using System.Text.Json;
1552>
1553> namespace QuickstartWeatherServer.Tools;
1554>
1555> [McpServerToolType]
1556> public 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.";
1570> }
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>
1611> Finally, run the server using the following command:
1612>
1613> ```bash theme={null}
1614> dotnet run
1615> ```
1616>
1617> This will start the server and listen for incoming requests on standard input/output.
1618>
1619> ## Testing your server with Claude for Desktop
1620>
1621> First, make sure you have Claude for Desktop installed. [You can install the latest version
1622> here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
1623> 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.
1624> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1625>
1626> ```bash Linux theme={null}
1627> code ~/.config/Claude/claude_desktop_config.json
1628> ```
1629>
1630> ```bash macOS theme={null}
1631> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1632> ```
1633>
1634> ```powershell Windows theme={null}
1635> code $env:AppData\Claude\claude_desktop_config.json
1636> ```
1637>
1638> 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.
1639> In 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>
1668> This tells Claude for Desktop:
1669>
1670> 1. There's an MCP server named "weather"
1671> 2. Launch it by running `dotnet run /ABSOLUTE/PATH/TO/PROJECT`
1672> Save the file, and restart **Claude for Desktop**.
1673
1674> [tab] Ruby
1675>
1676> 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)
1677>
1678> ### Prerequisite knowledge
1679>
1680> This quickstart assumes you have familiarity with:
1681>
1682> * Ruby
1683> * LLMs like Claude
1684>
1685> ### Logging in MCP Servers
1686>
1687> When 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)
1701> puts "Processing request"
1702>
1703> # ✅ Good (STDIO)
1704> require "logger"
1705> logger = Logger.new($stderr)
1706> logger.info("Processing request")
1707> ```
1708>
1709> ### System requirements
1710>
1711> * Ruby 2.7 or higher installed.
1712>
1713> ### Set up your environment
1714>
1715> First, let's make sure you have Ruby installed. You can check by running:
1716>
1717> ```bash theme={null}
1718> ruby --version
1719> ```
1720>
1721> Now, let's create and set up our project:
1722>
1723> ```bash macOS/Linux theme={null}
1724> # Create a new directory for our project
1725> mkdir weather
1726> cd weather
1727>
1728> # Create a Gemfile
1729> bundle init
1730>
1731> # Add the MCP SDK dependency
1732> bundle add mcp
1733>
1734> # Create our server file
1735> touch weather.rb
1736> ```
1737>
1738> ```powershell Windows theme={null}
1739> # Create a new directory for our project
1740> mkdir weather
1741> cd weather
1742>
1743> # Create a Gemfile
1744> bundle init
1745>
1746> # Add the MCP SDK dependency
1747> bundle add mcp
1748>
1749> # Create our server file
1750> new-item weather.rb
1751> ```
1752>
1753> Now let's dive into building your server.
1754>
1755> ## Building your server
1756>
1757> ### Importing packages and setting up constants
1758>
1759> Open `weather.rb` and add these requires and constants at the top:
1760>
1761> ```ruby theme={null}
1762> require "json"
1763> require "mcp"
1764> require "net/http"
1765> require "uri"
1766>
1767> NWS_API_BASE = "https://api.weather.gov"
1768> USER_AGENT = "weather-app/1.0"
1769> ```
1770>
1771> The `mcp` gem provides the Model Context Protocol SDK for Ruby, with classes for server implementation and stdio transport.
1772>
1773> ### Helper methods
1774>
1775> Next, let's add helper methods for querying and formatting data from the National Weather Service API:
1776>
1777> ```ruby theme={null}
1778> module 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
1805> end
1806> ```
1807>
1808> ### Implementing tool execution
1809>
1810> Now let's define our tool classes. Each tool subclasses `MCP::Tool` and implements the tool logic:
1811>
1812> ```ruby theme={null}
1813> class 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)"
1823> }
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
1845> end
1846>
1847> class 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"
1861> }
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
1891> end
1892> ```
1893>
1894> ### Running the server
1895>
1896> Finally, initialize and run the server:
1897>
1898> ```ruby theme={null}
1899> server = MCP::Server.new(
1900> name: "weather",
1901> version: "1.0.0",
1902> tools: [GetAlerts, GetForecast]
1903> )
1904>
1905> transport = MCP::Server::Transports::StdioTransport.new(server)
1906> transport.open
1907> ```
1908>
1909> Your server is complete! Run `bundle exec ruby weather.rb` to start the MCP server, which will listen for messages from MCP hosts.
1910>
1911> Let's now test your server from an existing MCP host, Claude for Desktop.
1912>
1913> ## Testing your server with Claude for Desktop
1914>
1915> 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.**
1916>
1917> 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.
1918>
1919> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
1920>
1921> ```bash Linux theme={null}
1922> code ~/.config/Claude/claude_desktop_config.json
1923> ```
1924>
1925> ```bash macOS theme={null}
1926> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
1927> ```
1928>
1929> ```powershell Windows theme={null}
1930> code $env:AppData\Claude\claude_desktop_config.json
1931> ```
1932>
1933> 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.
1934>
1935> In 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"
1944> }
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"
1956> }
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>
1965> This tells Claude for Desktop:
1966>
1967> 1. There's an MCP server named "weather"
1968> 2. Launch it by running `bundle exec ruby weather.rb` in the specified directory
1969>
1970> Save the file, and restart **Claude for Desktop**.
1971
1972> [tab] Rust
1973>
1974> 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)
1975>
1976> ### Prerequisite knowledge
1977>
1978> This 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>
1986> When 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)
2001> println!("Processing request");
2002>
2003> // ✅ Good (STDIO)
2004> eprintln!("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>
2014> 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):
2015>
2016> ```bash macOS/Linux theme={null}
2017> curl --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>
2024> Verify your Rust installation:
2025>
2026> ```bash theme={null}
2027> rustc --version
2028> cargo --version
2029> ```
2030>
2031> Now, let's create and set up our project:
2032>
2033> ```bash macOS/Linux theme={null}
2034> # Create a new Rust project
2035> cargo new weather
2036> cd weather
2037> ```
2038>
2039> ```powershell Windows theme={null}
2040> # Create a new Rust project
2041> cargo new weather
2042> cd weather
2043> ```
2044>
2045> Update your `Cargo.toml` to add the required dependencies:
2046>
2047> ```toml Cargo.toml theme={null}
2048> [package]
2049> name = "weather"
2050> version = "0.1.0"
2051> edition = "2024"
2052>
2053> [dependencies]
2054> rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
2055> tokio = { version = "1.46", features = ["full"] }
2056> reqwest = { version = "0.12", features = ["json"] }
2057> serde = { version = "1.0", features = ["derive"] }
2058> serde_json = "1.0"
2059> anyhow = "1.0"
2060> tracing = "0.1"
2061> tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
2062> ```
2063>
2064> Now let's dive into building your server.
2065>
2066> ## Building your server
2067>
2068> ### Importing packages and constants
2069>
2070> Open `src/main.rs` and add these imports and constants at the top:
2071>
2072> ```rust theme={null}
2073> use anyhow::Result;
2074> use rmcp::{
2075> ServerHandler, ServiceExt,
2076> handler::server::{router::tool::ToolRouter, tool::Parameters},
2077> model::*,
2078> schemars, tool, tool_handler, tool_router,
2079> };
2080> use serde::Deserialize;
2081> use serde::de::DeserializeOwned;
2082>
2083> const NWS_API_BASE: &str = "https://api.weather.gov";
2084> const USER_AGENT: &str = "weather-app/1.0";
2085> ```
2086>
2087> The `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>
2091> Next, let's define the data structures for deserializing responses from the National Weather Service API:
2092>
2093> ```rust theme={null}
2094> #[derive(Debug, Deserialize)]
2095> struct AlertsResponse {
2096> features: Vec<AlertFeature>,
2097> }
2098>
2099> #[derive(Debug, Deserialize)]
2100> struct AlertFeature {
2101> properties: AlertProperties,
2102> }
2103>
2104> #[derive(Debug, Deserialize)]
2105> struct 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)]
2115> struct PointsResponse {
2116> properties: PointsProperties,
2117> }
2118>
2119> #[derive(Debug, Deserialize)]
2120> struct PointsProperties {
2121> forecast: String,
2122> }
2123>
2124> #[derive(Debug, Deserialize)]
2125> struct ForecastResponse {
2126> properties: ForecastProperties,
2127> }
2128>
2129> #[derive(Debug, Deserialize)]
2130> struct ForecastProperties {
2131> periods: Vec<ForecastPeriod>,
2132> }
2133>
2134> #[derive(Debug, Deserialize)]
2135> struct 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>
2149> Now define the request types that MCP clients will send:
2150>
2151> ```rust theme={null}
2152> #[derive(serde::Deserialize, schemars::JsonSchema)]
2153> pub struct MCPForecastRequest {
2154> latitude: f32,
2155> longitude: f32,
2156> }
2157>
2158> #[derive(serde::Deserialize, schemars::JsonSchema)]
2159> pub struct MCPAlertRequest {
2160> state: String,
2161> }
2162> ```
2163>
2164> ### Helper functions
2165>
2166> Add helper functions for making API requests and formatting responses:
2167>
2168> ```rust theme={null}
2169> async 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>
2181> fn 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>
2199> fn 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>
2214> Now let's implement the main Weather server struct with the tool handlers:
2215>
2216> ```rust theme={null}
2217> pub struct Weather {
2218> tool_router: ToolRouter<Weather>,
2219> }
2220>
2221> #[tool_router]
2222> impl Weather {
2223> fn new() -> Self {
2224> Self {
2225> tool_router: Self::tool_router(),
2226> }
2227> }
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> }
2251> }
2252> Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
2253> }
2254> }
2255>
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>
2287> The `#[tool_router]` macro automatically generates the routing logic, and the `#[tool]` attribute marks methods as MCP tools.
2288>
2289> ### Implementing the ServerHandler
2290>
2291> Implement the `ServerHandler` trait to define server capabilities:
2292>
2293> ```rust theme={null}
2294> #[tool_handler]
2295> impl ServerHandler for Weather {
2296> fn get_info(&self) -> ServerInfo {
2297> ServerInfo {
2298> capabilities: ServerCapabilities::builder().enable_tools().build(),
2299> ..Default::default()
2300> }
2301> }
2302> }
2303> ```
2304>
2305> ### Running the server
2306>
2307> Finally, implement the main function to run the server with stdio transport:
2308>
2309> ```rust theme={null}
2310> #[tokio::main]
2311> async 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>
2319> Build your server with:
2320>
2321> ```bash theme={null}
2322> cargo build --release
2323> ```
2324>
2325> The compiled binary will be in `target/release/weather`.
2326>
2327> Let's now test your server from an existing MCP host, Claude for Desktop.
2328>
2329> ## Testing your server with Claude for Desktop
2330>
2331> 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.**
2332>
2333> 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.
2334>
2335> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2336>
2337> ```bash Linux theme={null}
2338> code ~/.config/Claude/claude_desktop_config.json
2339> ```
2340>
2341> ```bash macOS theme={null}
2342> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2343> ```
2344>
2345> ```powershell Windows theme={null}
2346> code $env:AppData\Claude\claude_desktop_config.json
2347> ```
2348>
2349> 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.
2350>
2351> In 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"
2358> }
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"
2368> }
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>
2377> This tells Claude for Desktop:
2378>
2379> 1. There's an MCP server named "weather"
2380> 2. Launch it by running the compiled binary at the specified path
2381>
2382> Save the file, and restart **Claude for Desktop**.
2383
2384> [tab] Go
2385>
2386> 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)
2387>
2388> ### Prerequisite knowledge
2389>
2390> This quickstart assumes you have familiarity with:
2391>
2392> * Go
2393> * LLMs like Claude
2394>
2395> ### Logging in MCP Servers
2396>
2397> When 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)
2412> fmt.Println("Processing request")
2413>
2414> // ✅ Good (STDIO)
2415> log.Println("Processing request") // defaults to stderr
2416>
2417> // ✅ Good (STDIO)
2418> fmt.Fprintln(os.Stderr, "Processing request")
2419> ```
2420>
2421> ### System requirements
2422>
2423> * Go 1.25 or higher installed.
2424>
2425> ### Set up your environment
2426>
2427> First, let's install Go if you haven't already. You can download and install Go from [go.dev](https://go.dev/dl/).
2428>
2429> Verify your Go installation:
2430>
2431> ```bash theme={null}
2432> go version
2433> ```
2434>
2435> Now, let's create and set up our project:
2436>
2437> ```bash macOS/Linux theme={null}
2438> # Create a new directory for our project
2439> mkdir weather
2440> cd weather
2441>
2442> # Initialize Go module
2443> go mod init weather
2444>
2445> # Install dependencies
2446> go get github.com/modelcontextprotocol/go-sdk/mcp
2447>
2448> # Create our server file
2449> touch main.go
2450> ```
2451>
2452> ```powershell Windows theme={null}
2453> # Create a new directory for our project
2454> md weather
2455> cd weather
2456>
2457> # Initialize Go module
2458> go mod init weather
2459>
2460> # Install dependencies
2461> go get github.com/modelcontextprotocol/go-sdk/mcp
2462>
2463> # Create our server file
2464> new-item main.go
2465> ```
2466>
2467> Now let's dive into building your server.
2468>
2469> ## Building your server
2470>
2471> ### Importing packages and constants
2472>
2473> Add these to the top of your `main.go`:
2474>
2475> ```go theme={null}
2476> package main
2477>
2478> import (
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>
2491> const (
2492> NWSAPIBase = "https://api.weather.gov"
2493> UserAgent = "weather-app/1.0"
2494> )
2495> ```
2496>
2497> ### Data structures
2498>
2499> Next, let's define the data structures used by our tools:
2500>
2501> ```go theme={null}
2502> type PointsResponse struct {
2503> Properties struct {
2504> Forecast string `json:"forecast"`
2505> } `json:"properties"`
2506> }
2507>
2508> type ForecastResponse struct {
2509> Properties struct {
2510> Periods []ForecastPeriod `json:"periods"`
2511> } `json:"properties"`
2512> }
2513>
2514> type 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>
2523> type AlertsResponse struct {
2524> Features []AlertFeature `json:"features"`
2525> }
2526>
2527> type AlertFeature struct {
2528> Properties AlertProperties `json:"properties"`
2529> }
2530>
2531> type 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>
2539> type ForecastInput struct {
2540> Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
2541> Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
2542> }
2543>
2544> type AlertsInput struct {
2545> State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
2546> }
2547> ```
2548>
2549> ### Helper functions
2550>
2551> Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
2552>
2553> ```go theme={null}
2554> func 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>
2583> func 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(`
2592> Event: %s
2593> Area: %s
2594> Severity: %s
2595> Description: %s
2596> Instructions: %s
2597> `, event, areaDesc, severity, description, instruction)
2598> }
2599>
2600> func formatPeriod(period ForecastPeriod) string {
2601> return fmt.Sprintf(`
2602> %s:
2603> Temperature: %d°%s
2604> Wind: %s %s
2605> Forecast: %s
2606> `, period.Name, period.Temperature, period.TemperatureUnit,
2607> period.WindSpeed, period.WindDirection, period.DetailedForecast)
2608> }
2609> ```
2610>
2611> ### Implementing tool execution
2612>
2613> The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
2614>
2615> ```go theme={null}
2616> func 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>
2674> func 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>
2717> Finally, implement the main function to run the server:
2718>
2719> ```go theme={null}
2720> func 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>
2746> Build your server with:
2747>
2748> ```bash theme={null}
2749> go build -o weather .
2750> ```
2751>
2752> The compiled binary will be in `./weather`.
2753>
2754> Let's now test your server from an existing MCP host, Claude for Desktop.
2755>
2756> ## Testing your server with Claude for Desktop
2757>
2758> 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.**
2759>
2760> 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.
2761>
2762> For example, if you have [VS Code](https://code.visualstudio.com/) installed:
2763>
2764> ```bash Linux theme={null}
2765> code ~/.config/Claude/claude_desktop_config.json
2766> ```
2767>
2768> ```bash macOS theme={null}
2769> code ~/Library/Application\ Support/Claude/claude_desktop_config.json
2770> ```
2771>
2772> ```powershell Windows theme={null}
2773> code $env:AppData\Claude\claude_desktop_config.json
2774> ```
2775>
2776> 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.
2777>
2778> In 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"
2785> }
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"
2795> }
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>
2804> This tells Claude for Desktop:
2805>
2806> 1. There's an MCP server named "weather"
2807> 2. Launch it by running the compiled binary at the specified path
2808>
2809> Save the file, and restart **Claude for Desktop**.
28102810
28112811### Test with commands
28122812
from line 2846
28462846
28472847## Troubleshooting
28482848
2849> **Claude for Desktop Integration Issues**
2849> [accordion] Claude for Desktop Integration Issues
28502850>
28512851> **Getting logs from Claude for Desktop**
28522852>
from line 2897
28972897>
28982898> Please refer to our [debugging guide](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging.md) for better debugging tools and more detailed guidance.
28992899
2900> **Weather API Issues**
2900> [accordion] Weather API Issues
29012901>
29022902> **Error: Failed to retrieve grid point data**
29032903>
from line 2923
29232923
29242924## Next steps
29252925
2926> **[Building a client](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-client.md)**
2926> [card] [Building a client](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-client.md)
29272927>
29282928> Learn how to build your own MCP client that can connect to your server
29292929
2930> **[Example servers](https://modelcontextprotocol.io/examples.md)**
2930> [card] [Example servers](https://modelcontextprotocol.io/examples.md)
29312931>
29322932> Check out our gallery of official MCP servers and implementations
29332933
2934> **[Debugging Guide](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging.md)**
2934> [card] [Debugging Guide](https://modelcontextprotocol.io/docs/2026-07-28/tools/debugging.md)
29352935>
29362936> Learn how to effectively debug MCP servers and integrations
29372937
2938> **[Build with Agent Skills](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-with-agent-skills.md)**
2938> [card] [Build with Agent Skills](https://modelcontextprotocol.io/docs/2026-07-28/develop/build-with-agent-skills.md)
29392939>
29402940> Use agent skills to guide AI coding assistants through server design
29412941