Get started building your own server to use in Claude for Desktop and other clients.
In this tutorial, we’ll build a simple MCP weather server and connect it to a host, Claude for Desktop. We’ll start with a basic setup, and then progress to more complex use cases.
Many LLMs do not currently have the ability to fetch the forecast and severe weather alerts. Let’s use MCP to solve that!
We’ll build a server that exposes two tools: get-alerts and get-forecast. Then we’ll connect the server to an MCP host (in this case, Claude for Desktop):
The tool execution handler is responsible for actually executing the logic of each tool. Let’s add it:
@mcp.tool()async def get_alerts(state: str) -> str: """Get weather alerts for a US state. Args: state: Two-letter US state code (e.g. CA, NY) """ url = f"{NWS_API_BASE}/alerts/active/area/{state}" data = await make_nws_request(url) if not data or "features" not in data: return "Unable to fetch alerts or no alerts found." if not data["features"]: return "No active alerts for this state." alerts = [format_alert(feature) for feature in data["features"]] return "\n---\n".join(alerts)@mcp.tool()async def get_forecast(latitude: float, longitude: float) -> str: """Get weather forecast for a location. Args: latitude: Latitude of the location longitude: Longitude of the location """ # First get the forecast grid endpoint points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" points_data = await make_nws_request(points_url) if not points_data: return "Unable to fetch forecast data for this location." # Get the forecast URL from the points response forecast_url = points_data["properties"]["forecast"] forecast_data = await make_nws_request(forecast_url) if not forecast_data: return "Unable to fetch detailed forecast." # Format the periods into a readable forecast periods = forecast_data["properties"]["periods"] forecasts = [] for period in periods[:5]: # Only show next 5 periods forecast = f"""{period['name']}:Temperature: {period['temperature']}°{period['temperatureUnit']}Wind: {period['windSpeed']} {period['windDirection']}Forecast: {period['detailedForecast']}""" forecasts.append(forecast) return "\n---\n".join(forecasts)
Claude for Desktop is not yet available on Linux. Linux users can proceed to the Building a client tutorial to build an MCP client that connects to the server we just built.
First, make sure you have Claude for Desktop installed. You can install the latest version
here. If you already have Claude for Desktop, make sure it’s updated to the latest version.
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.
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.
In this case, we’ll add our single weather server like so:
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.
Make sure you pass in the absolute path to your server.
This tells Claude for Desktop:
There’s an MCP server named “weather”
To launch it by running uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py
The tool execution handler is responsible for actually executing the logic of each tool. Let’s add it:
@mcp.tool()async def get_alerts(state: str) -> str: """Get weather alerts for a US state. Args: state: Two-letter US state code (e.g. CA, NY) """ url = f"{NWS_API_BASE}/alerts/active/area/{state}" data = await make_nws_request(url) if not data or "features" not in data: return "Unable to fetch alerts or no alerts found." if not data["features"]: return "No active alerts for this state." alerts = [format_alert(feature) for feature in data["features"]] return "\n---\n".join(alerts)@mcp.tool()async def get_forecast(latitude: float, longitude: float) -> str: """Get weather forecast for a location. Args: latitude: Latitude of the location longitude: Longitude of the location """ # First get the forecast grid endpoint points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" points_data = await make_nws_request(points_url) if not points_data: return "Unable to fetch forecast data for this location." # Get the forecast URL from the points response forecast_url = points_data["properties"]["forecast"] forecast_data = await make_nws_request(forecast_url) if not forecast_data: return "Unable to fetch detailed forecast." # Format the periods into a readable forecast periods = forecast_data["properties"]["periods"] forecasts = [] for period in periods[:5]: # Only show next 5 periods forecast = f"""{period['name']}:Temperature: {period['temperature']}°{period['temperatureUnit']}Wind: {period['windSpeed']} {period['windDirection']}Forecast: {period['detailedForecast']}""" forecasts.append(forecast) return "\n---\n".join(forecasts)
Claude for Desktop is not yet available on Linux. Linux users can proceed to the Building a client tutorial to build an MCP client that connects to the server we just built.
First, make sure you have Claude for Desktop installed. You can install the latest version
here. If you already have Claude for Desktop, make sure it’s updated to the latest version.
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.
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.
In this case, we’ll add our single weather server like so:
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.
Make sure you pass in the absolute path to your server.
This tells Claude for Desktop:
There’s an MCP server named “weather”
To launch it by running uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py
Finally, implement the main function to run the server:
async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Weather MCP Server running on stdio");}main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1);});
Make sure to run npm run build to build your server! This is a very important step in getting your server to connect.
Let’s now test your server from an existing MCP host, Claude for Desktop.
Claude for Desktop is not yet available on Linux. Linux users can proceed to the Building a client tutorial to build an MCP client that connects to the server we just built.
First, make sure you have Claude for Desktop installed. You can install the latest version
here. If you already have Claude for Desktop, make sure it’s updated to the latest version.
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.
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.
In this case, we’ll add our single weather server like so:
Launch it by running node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js
Save the file, and restart Claude for Desktop.
This is a quickstart demo based on Spring AI MCP auto-configuration and boot starters.
To learn how to create sync and async MCP Servers, manually, consult the Java SDK Server documentation.
Let’s implement a WeatherService.java that uses a REST client to query the data from the National Weather Service API:
@Servicepublic class WeatherService { private final RestClient restClient; public WeatherService() { this.restClient = RestClient.builder() .baseUrl("https://api.weather.gov") .defaultHeader("Accept", "application/geo+json") .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])") .build(); } @Tool(description = "Get weather forecast for a specific latitude/longitude") public String getWeatherForecastByLocation( double latitude, // Latitude coordinate double longitude // Longitude coordinate ) { // Returns detailed forecast including: // - Temperature and unit // - Wind speed and direction // - Detailed forecast description } @Tool(description = "Get weather alerts for a US state") public String getAlerts( @ToolParam(description = "Two-letter US state code (e.g. CA, NY)" String state ) { // Returns active alerts including: // - Event type // - Affected area // - Severity // - Description // - Safety instructions } // ......}
The @Service annotation with auto-register the service in your application context.
The Spring AI @Tool annotation, making it easy to create and maintain MCP tools.
The auto-configuration will automatically register these tools with the MCP server.
First, make sure you have Claude for Desktop installed.
You can install the latest version here. If you already have Claude for Desktop, make sure it’s updated to the latest version.
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.
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.
In this case, we’ll add our single weather server like so:
and set the spring.ai.mcp.client.stdio.servers-configuration property to point to your claude_desktop_config.json.
You can re-use the existing Anthropic Desktop configuration:
The starter-webflux-server demonstrates how to create a MCP server using SSE transport.
It showcases how to define and register MCP Tools, Resources, and Prompts, using the Spring Boot’s auto-configuration capabilities.
First, let’s install java and gradle if you haven’t already.
You can download java from official Oracle JDK website.
Verify your java installation:
java --version
Now, let’s create and set up your project:
# Create a new directory for our projectmkdir weathercd weather# Initialize a new kotlin projectgradle init
After running gradle init, you will be presented with options for creating your project.
Select Application as the project type, Kotlin as the programming language, and Java 17 as the Java version.
// Main function to run the MCP serverfun `run mcp server`() { // Create the MCP Server instance with a basic implementation val server = Server( Implementation( name = "weather", // Tool name is "weather" version = "1.0.0" // Version of the implementation ), ServerOptions( capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)) ) ) // Create a transport using standard IO for server communication val transport = StdioServerTransport( System.`in`.asInput(), System.out.asSink().buffered() ) runBlocking { server.connect(transport) val done = Job() server.onClose { done.complete() } done.join() }}
Next, let’s add functions and data classes for querying and converting responses from the National Weather Service API:
// Extension function to fetch forecast information for given latitude and longitudesuspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> { val points = this.get("/points/$latitude,$longitude").body<Points>() val forecast = this.get(points.properties.forecast).body<Forecast>() return forecast.properties.periods.map { period -> """ ${period.name}: Temperature: ${period.temperature} ${period.temperatureUnit} Wind: ${period.windSpeed} ${period.windDirection} Forecast: ${period.detailedForecast} """.trimIndent() }}// Extension function to fetch weather alerts for a given statesuspend fun HttpClient.getAlerts(state: String): List<String> { val alerts = this.get("/alerts/active/area/$state").body<Alert>() return alerts.features.map { feature -> """ Event: ${feature.properties.event} Area: ${feature.properties.areaDesc} Severity: ${feature.properties.severity} Description: ${feature.properties.description} Instruction: ${feature.properties.instruction} """.trimIndent() }}@Serializabledata class Points( val properties: Properties) { @Serializable data class Properties(val forecast: String)}@Serializabledata class Forecast( val properties: Properties) { @Serializable data class Properties(val periods: List<Period>) @Serializable data class Period( val number: Int, val name: String, val startTime: String, val endTime: String, val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String, val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject, val windSpeed: String, val windDirection: String, val shortForecast: String, val detailedForecast: String, )}@Serializabledata class Alert( val features: List<Feature>) { @Serializable data class Feature( val properties: Properties ) @Serializable data class Properties( val event: String, val areaDesc: String, val severity: String, val description: String, val instruction: String?, )}
Claude for Desktop is not yet available on Linux. Linux users can proceed to the Building a client tutorial to build an MCP client that connects to the server we just built.
First, make sure you have Claude for Desktop installed. You can install the latest version
here. If you already have Claude for Desktop, make sure it’s updated to the latest version.
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.
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.
In this case, we’ll add our single weather server like so:
First, let’s install dotnet if you haven’t already. You can download dotnet from official Microsoft .NET website. Verify your dotnet installation:
dotnet --version
Now, let’s create and set up your project:
# Create a new directory for our projectmkdir weathercd weather# Initialize a new C# projectdotnet new console
After running dotnet new console, you will be presented with a new C# project.
You can open the project in your favorite IDE, such as Visual Studio or Rider.
Alternatively, you can create a C# application using the Visual Studio project wizard.
After creating the project, add NuGet package for the Model Context Protocol SDK and hosting:
# Add the Model Context Protocol SDK NuGet packagedotnet add package ModelContextProtocol --prerelease# Add the .NET Hosting NuGet packagedotnet add package Microsoft.Extensions.Hosting
Open the Program.cs file in your project and replace its contents with the following code:
using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using ModelContextProtocol;using System.Net.Http.Headers;var builder = Host.CreateEmptyApplicationBuilder(settings: null);builder.Services.AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly();builder.Services.AddSingleton(_ =>{ var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") }; client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); return client;});var app = builder.Build();await app.RunAsync();
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 neccessary for servers using STDIO transport.
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.
Create an extension class for HttpClient which helps simplify JSON request handling:
using System.Text.Json;internal static class HttpClientExt{ public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri) { using var response = await client.GetAsync(requestUri); response.EnsureSuccessStatusCode(); return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); }}
Next, define a class with the tool execution handlers for querying and converting responses from the National Weather Service API:
using ModelContextProtocol.Server;using System.ComponentModel;using System.Globalization;using System.Text.Json;namespace QuickstartWeatherServer.Tools;[McpServerToolType]public static class WeatherTools{ [McpServerTool, Description("Get weather alerts for a US state.")] public static async Task<string> GetAlerts( HttpClient client, [Description("The US state to get alerts for.")] string state) { using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}"); var jsonElement = jsonDocument.RootElement; var alerts = jsonElement.GetProperty("features").EnumerateArray(); if (!alerts.Any()) { return "No active alerts for this state."; } return string.Join("\n--\n", alerts.Select(alert => { JsonElement properties = alert.GetProperty("properties"); return $""" Event: {properties.GetProperty("event").GetString()} Area: {properties.GetProperty("areaDesc").GetString()} Severity: {properties.GetProperty("severity").GetString()} Description: {properties.GetProperty("description").GetString()} Instruction: {properties.GetProperty("instruction").GetString()} """; })); } [McpServerTool, Description("Get weather forecast for a location.")] public static async Task<string> GetForecast( HttpClient client, [Description("Latitude of the location.")] double latitude, [Description("Longitude of the location.")] double longitude) { var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}"); using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl); var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString() ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}"); using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl); var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray(); return string.Join("\n---\n", periods.Select(period => $""" {period.GetProperty("name").GetString()} Temperature: {period.GetProperty("temperature").GetInt32()}°F Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()} Forecast: {period.GetProperty("detailedForecast").GetString()} """)); }}
Claude for Desktop is not yet available on Linux. Linux users can proceed to the Building a client tutorial to build an MCP client that connects to the server we just built.
First, make sure you have Claude for Desktop installed. You can install the latest version
here. If you already have Claude for Desktop, make sure it’s updated to the latest version.
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.
For example, if you have VS Code installed:
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.
In this case, we’ll add our single weather server like so:
Let’s make sure Claude for Desktop is picking up the two tools we’ve exposed in our weather server. You can do this by looking for the “Search and tools” icon:
After clicking on the slider icon, you should see two tools listed:
If your server isn’t being picked up by Claude for Desktop, proceed to the Troubleshooting section for debugging tips.
If the tool settings icon has shown up, you can now test your server by running the following commands in Claude for Desktop:
What’s the weather in Sacramento?
What are the active weather alerts in Texas?
Since this is the US National Weather service, the queries will only work for US locations.