Yr weather Tool

The Yr tool is defined in the same way as the Nominatim tool, it is a sealed class decorated with the [McpServerToolType] attribute and it defines methods decorated with the [McpServerTool] attribute.

The tool has got instructions in the Description that tell the tool to use the Nominatim tool to look up coordinates for the place that is to be checked for weather prediction.

We also use async here since we communicate with the remote service.

It is an enlightening experience to debug the serverside to watch how the LLM uses the tools given.

When debugging, you will find that if a relevant question that fulfills the conditions for the tool to be used and contacted by MCP from the client using an LLM and the given context will provide the information given to the parameters of the method for the tool.

Key takeaway : MCP can populate parameters in tool methods and this is done via good description. But it is the LLM that decides (orchestrator)

MCP populates tool parameters by combining:

namespace WeatherServer.Tools;

using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using System.Text.Json;
using WeatherServer.Common;
using WeatherServer.Models;

[McpServerToolType]
public sealed class YrTools
{
    public string ToolId => "Yr tool";

    [McpServerTool(Name = "YrWeatherCurrentWeather")]
    [Description(
 $@"""
     Description of this tool method:
     Retrieves the current weather conditions for a specified location using the YrTools CurrentWeather API.

    Usage Instructions:
    1. Use the 'NominatimLookupLatLongForPlace' tool to resolve the latitude and longitude of the provided location.
    2. Pass the resolved coordinates from the tool above and pass them into to this method.
    3. If coordinates cannot be resolved, use latitude = 0 and longitude = 0. In this case, the method will return a message indicating no results were found.
    4. In case the place passed in is for a place in United States, use instead the tool 'UsWeatherForecastLocation'.
    5. This method is to be used when asked about the current weather right now.
    6. Use the system clock to check the date of today.

    Response Requirements:
    - It is very important that the correct url is used, longitude and latitude here will be provided . $""weatherapi/locationforecast/2.0/compact?lat=&lon=""
    - Always include the latitude and longitude used.
    - Always inform about which url was used to get the data here.
    - Inform about the time when the weather is.
    - Always include the 'time' field from the result to indicate when the weather data is valid.
    - Clearly state that the data was retrieved using 'YrWeatherCurrentWeather'.
""")]
    public static async Task<string> GetCurrentWeatherForecast(
        IHttpClientFactory clientFactory,
        ILogger<YrTools> logger,
        [Description("Provide current weather. State the location, latitude and longitude used. Return precisely the data given. Return ALL the data you were given.")] string location, decimal latitude, decimal longitude)
    {
        if (latitude == 0 && longitude == 0)
        {
            return $"No current weather data found for '{location}'. Try another location to query?";
        }

        var client = clientFactory.CreateClient(WeatherServerApiClientNames.YrApiClientName);
        string url = $"weatherapi/locationforecast/2.0/compact?lat={latitude.ToString(CultureInfo.InvariantCulture)}&lon={longitude.ToString(CultureInfo.InvariantCulture)}";

        logger.LogWarning($"Accessing Yr Current Weather with url: {url} with client base address {client.BaseAddress}");

        using var jsonDocument = await client.ReadJsonDocumentAsync(url);
        var timeseries = jsonDocument.RootElement.GetProperty("properties").GetProperty("timeseries").EnumerateArray();

        if (!timeseries.Any())
        {
            return $"No current weather data found for '{location}'. Try another place to query?";
        }

        var currentWeatherInfos = GetInformationForTimeSeries(timeseries, onlyFirst: true);

        var sb = new StringBuilder();
        foreach (var info in currentWeatherInfos)
        {
            sb.AppendLine(info.ToString());
        }

        return sb.ToString();
    }

    [McpServerTool(Name = "YrWeatherTenDayForecast")]
    public static async Task<string> GetTenDaysWeatherForecast(
     IHttpClientFactory clientFactory,
     ILogger<YrTools> logger,
     [Description("Provide ten day forecast weather. State the location, latitude and longitude used. Return the data given. Return ALL the data you were given.")] string location, decimal latitude, decimal longitude)
    {
        if (latitude == 0 && longitude == 0)
        {
            return $"No current weather data found for '{location}'. Try another location to query?";
        }

        var client = clientFactory.CreateClient(WeatherServerApiClientNames.YrApiClientName);
        var url = $"/weatherapi/locationforecast/2.0/compact?lat={latitude.ToString(CultureInfo.InvariantCulture)}&lon={longitude.ToString(CultureInfo.InvariantCulture)}";

        logger.LogWarning($"Accessing Yr Current Weather with url: {url} with client base address {client.BaseAddress}");

        using var jsonDocument = await client.ReadJsonDocumentAsync(url);
        var timeseries = jsonDocument.RootElement.GetProperty("properties").GetProperty("timeseries").EnumerateArray();

        if (!timeseries.Any())
        {
            return $"No current weather data found for '{location}'. Try another place to query?";
        }

        var currentWeatherInfos = GetInformationForTimeSeries(timeseries, onlyFirst: false);

        var sb = new StringBuilder();
        foreach (var info in currentWeatherInfos)
        {
            sb.AppendLine(info.ToString());
        }

        return sb.ToString();
    }
}

The tool got some processing logic to extract the relevant information from the Yr API response and format it in a way that is useful for the client.

I have selected the most relevant weather information that Yr offers (it offers a lot of information).


private static List<YrWeatherInfoItem> GetInformationForTimeSeries(JsonElement.ArrayEnumerator timeseries, bool onlyFirst)
{
    var result = new List<YrWeatherInfoItem>();

    foreach (var timeseriesItem in timeseries)
    {
        var currentWeather = timeseriesItem;
        var currentWeatherData = currentWeather.GetProperty("data");
        var instant = currentWeatherData.GetProperty("instant");
        string? nextOneHourWeatherSymbol = null;
        double? nextOneHourPrecipitationAmount = null;
        if (currentWeatherData.TryGetProperty("next_1_hours", out JsonElement nextOneHours))
        {
            nextOneHourWeatherSymbol = nextOneHours.GetProperty("summary").GetProperty("symbol_code").GetString();
            nextOneHourPrecipitationAmount = nextOneHours.GetProperty("details").GetProperty("precipitation_amount").GetDouble();
        }

        string? nextSixHourWeatherSymbol = null;
        double? nextSixHourPrecipitationAmount = null;
        if (currentWeatherData.TryGetProperty("next_6_hours", out JsonElement nextSixHours))
        {
            nextSixHourWeatherSymbol = nextSixHours.GetProperty("summary").GetProperty("symbol_code").GetString();
            nextSixHourPrecipitationAmount = nextSixHours.GetProperty("details").GetProperty("precipitation_amount").GetDouble();
        }

        string? nextTwelveHourWeatherSymbol = null;
        if (currentWeatherData.TryGetProperty("next_12_hours", out JsonElement nextTwelveHours))
        {
            nextTwelveHourWeatherSymbol = nextTwelveHours.GetProperty("summary").GetProperty("symbol_code").GetString();
        }

        string timeRaw = currentWeather.GetProperty("time").GetString()!;
        DateTime parsedDate = DateTime.Parse(timeRaw, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal);
        var instantDetails = instant.GetProperty("details");

        var airPressureAtSeaLevel = instantDetails.GetProperty("air_pressure_at_sea_level");
        var airTemperature = instantDetails.GetProperty("air_temperature");
        var cloudAreaFraction = instantDetails.GetProperty("cloud_area_fraction");
        var relativeHumidity = instantDetails.GetProperty("relative_humidity");
        var windFromDirection = instantDetails.GetProperty("wind_from_direction");
        var windSpeed = instantDetails.GetProperty("wind_speed");

        var weatherItem = new YrWeatherInfoItem
        {
            AirPressureAtSeaLevel = airPressureAtSeaLevel.GetDouble(),
            AirTemperature = airTemperature.GetDouble(),
            CloudAreaFraction = cloudAreaFraction.GetDouble(),
            RelativeHumidity = relativeHumidity.GetDouble(),
            WindFromDirection = windFromDirection.GetDouble(),
            WindSpeed = windSpeed.GetDouble(),
            Time = parsedDate,
            NextHourPrecipitationAmount = nextOneHourPrecipitationAmount,
            NextHourWeatherSymbol = nextOneHourWeatherSymbol,
            NextSixHoursPrecipitationAmount = nextSixHourPrecipitationAmount,
            NextSixHoursWeatherSymbol = nextOneHourWeatherSymbol,
            NextTwelveHoursWeatherSymbol = nextTwelveHourWeatherSymbol
        };

        result.Add(weatherItem);

        if (onlyFirst)
        {
            break;
        }
    }

    return result;
}

        public override string ToString()
        {
            return
$@"""
Time = {Time},
AirpressureAtSeaLevel = {AirPressureAtSeaLevel},
AirTemperature = {AirTemperature},
CloudAreaFraction = {CloudAreaFraction},
RelativeHumidity = {RelativeHumidity},
WindFromDirection = {WindFromDirection},
WindSpeed = {WindSpeed}
NextHourWeatherSymbol = {NextHourWeatherSymbol}
NextHourPrecipitationAmount = {NextHourPrecipitationAmount}
NextSixHoursWeatherSymbol = {NextSixHoursWeatherSymbol}
NextSixHoursPrecipitationAmount = {NextSixHoursPrecipitationAmount}
NextTwelveHoursWeatherSymbol = {NextTwelveHoursWeatherSymbol}
""";
        } //tostring override