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