MCP-Based .NET Micro-Weather Wisdom SMS Service Using Twilio & OpenAI

September 16, 2025
Written by
Jacob Snipes
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

MCP-Based .NET Micro-Weather Wisdom SMS Service Using Twilio & OpenAI

This tutorial walks you step-by-step through building a production-minded but lightweight service that delivers personalized, actionable weather tips over SMS. Imagine users waking up to a short, friendly message like: “Good morning! 19°C and light rain in New York. Bring a light waterproof jacket and leave 10 mins early for your commute.”

That’s what you’ll build: a system that fetches weather forecasts through the MCP pattern, for easy data integration between different service providers and for testing. Then, the application asks OpenAI to craft crisp human-friendly advice, suggesting clothing, commute, and activities. It then delivers the advice via Twilio SMS, either on a schedule or on demand when somebody texts your number.

Prerequisites

Building the Application

Find the full source code on GitHub for reference.

Setting up the Working Environment

Start by initializing and creating the project structure. Run the command below to fire up the process.

dotnet new web -n MicroWeatherWisdom
cd MicroWeatherWisdom
# Create directories
mkdir Controllers Services Models

These commands create a clean, organized project structure following .NET conventions. The dotnet new web command creates a minimal web API template, perfect for webhook endpoints.

Next, install the required packages. To do so, copy and paste the commands below in your terminal and run them.

dotnet add package Twilio 
dotnet add package Newtonsoft.Json  
dotnet add package Microsoft.Extensions.Http 
dotnet add package Microsoft.AspNetCore.OpenApi

These packages provide essential functionality: Twilio SDK for SMS operations, JSON serialization for API responses, HTTP client for external API calls, and OpenAPI for intelligent operations.

Before you dive into the coding bit, you need to set up your configuration file with API credentials. Update your appsettings.json as shown below.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Twilio": {
    "AccountSid": "ACxxxxxxxxxxxxxxxxxxxxx",
    "AuthToken": "your-auth-token-here",
    "PhoneNumber": "+1234567890"
  },
  "OpenAI": {
    "ApiKey": "sk-xxxxxxxxxxxxxxxxxxxxx"
  },
  "Meteomatics": {
    "Username": "your-meteomatics-username",
    "Password": "your-meteomatics-password"
  }
}

This configuration file stores all your API credentials securely. Replace the placeholder values with your actual API keys. The structure follows .NET's configuration conventions, making it easy to inject these values into your services.

Data Models Definition

Now start coding by defining your data models. These represent the structure of our weather data and user preferences.

Create Models/WeatherData.cs and update it with the code below:

public class WeatherData
{
    public string Location { get; set; } = "New York";
    public double Temperature { get; set; }
    public double Humidity { get; set; }
    public double WindSpeed { get; set; }
    public string Conditions { get; set; } = "";
    public double PrecipitationProbability { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.Now;
}
public class UserPreferences
{
    public string PhoneNumber { get; set; } = "";
    public List<string> Interests { get; set; } = new();
    public string PreferredTime { get; set; } = "08:00";
    public bool DailyUpdates { get; set; } = true;
}

These models define the structure for weather data you'll receive from the Meteomatics API and user preferences for personalized experiences. The WeatherData class contains all essential weather metrics, while UserPreferences allows customization of when and what type of weather insights users want to receive.

Weather Service Implementation

Now, implement the weather service that fetches real-time weather data and provides fallback mock data.

Create Services/WeatherService.cs then update it with the code below.

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
public class WeatherService
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _config;
    private const string METEOMATICS_BASE_URL = "https://api.meteomatics.com";
    public WeatherService(HttpClient httpClient, IConfiguration config)
    {
        _httpClient = httpClient;
        _config = config;
        // Setup Basic Auth for Meteomatics
        var username = _config["Meteomatics:Username"] ?? "demo";
        var password = _config["Meteomatics:Password"] ?? "demo";
        var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
        _httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authValue);
    }
    public async Task<WeatherData> GetCurrentWeatherAsync(string location = "New York")
    {
        try
        {
            // Meteomatics API endpoint format
            var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
            var parameters = "t_2m:C,relative_humidity_2m:p,wind_speed_10m:ms,weather_symbol_1h:idx,precip_1h:mm";
            var coordinates = "40.7128,-74.0060"; // New York coordinates
            var url = $"{METEOMATICS_BASE_URL}/{now}/{parameters}/{coordinates}/json";
            var response = await _httpClient.GetAsync(url);
            if (!response.IsSuccessStatusCode)
            {
                // Fallback to mock data for demo
                return GetMockWeatherData();
            }
            var content = await response.Content.ReadAsStringAsync();
            var weatherResponse = JsonConvert.DeserializeObject<dynamic>(content);
            return new WeatherData
            {
                Location = location,
                Temperature = weatherResponse.data[0].coordinates[0].dates[0].value ?? 20.0,
                Humidity = weatherResponse.data[1].coordinates[0].dates[0].value ?? 65.0,
                WindSpeed = weatherResponse.data[2].coordinates[0].dates[0].value ?? 5.0,
                Conditions = GetWeatherCondition((int)(weatherResponse.data[3].coordinates[0].dates[0].value ?? 1)),
                PrecipitationProbability = (weatherResponse.data[4].coordinates[0].dates[0].value ?? 0) * 10,
                Timestamp = DateTime.Now
            };
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Weather API Error: {ex.Message}");
            return GetMockWeatherData();
        }
    }
    private WeatherData GetMockWeatherData()
    {
        var random = new Random();
        return new WeatherData
        {
            Location = "New York",
            Temperature = random.Next(15, 30),
            Humidity = random.Next(40, 80),
            WindSpeed = random.Next(0, 15),
            Conditions = new[] { "Sunny", "Partly Cloudy", "Cloudy", "Light Rain" }[random.Next(4)],
            PrecipitationProbability = random.Next(0, 100),
            Timestamp = DateTime.Now
        };
    }
    private string GetWeatherCondition(int symbolCode)
    {
        return symbolCode switch
        {
            1 => "Clear Sky",
            2 => "Partly Cloudy",
            3 => "Cloudy",
            4 => "Overcast",
            5 => "Light Rain",
            6 => "Rain",
            7 => "Heavy Rain",
            _ => "Sunny"
        };
    }
}

The WeatherService implements a robust pattern for fetching weather data. It attempts to get real data from the Meteomatics API using Basic Authentication, but gracefully falls back to mock data if the API is unavailable. This ensures your service works even during development without API keys. The service uses dependency injection for the HTTP client and configuration, following .NET best practices.

This demo is hardcoded to use New York City as the default location, to avoid needing to make dynamic location requests. To use your own locale: change the coordinates variable inside GetCurrentWeatherAsync to your city’s latitude and longitude in lat, lon format (decimal degrees, no spaces) before running the app. Take for example 1.2921,36.8219 for Nairobi.

Intelligent Operations Service

Next, create the OpenAI service that transforms raw weather data into actionable insights.

Create Services/OpenAIService.cs then update it with the code below.

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
public class OpenAIService
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _config;
    public OpenAIService(HttpClient httpClient, IConfiguration config)
    {
        _httpClient = httpClient;
        _config = config;
        var apiKey = _config["OpenAI:ApiKey"] ?? "your-openai-api-key";
        _httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
    }
    public async Task<string> GenerateWeatherInsightAsync(WeatherData weather, string userContext = "general")
    {
        try
        {
            var prompt = $@"Generate a brief, actionable weather insight for {weather.Location}:
- Temperature: {weather.Temperature}°C
- Humidity: {weather.Humidity}%  
- Wind: {weather.WindSpeed} m/s
- Conditions: {weather.Conditions}
- Rain chance: {weather.PrecipitationProbability}%
Context: {userContext}
Provide: clothing recommendation, transport advice, and one activity suggestion. Keep it under 140 characters for SMS.";
            var requestBody = new
            {
                model = "gpt-3.5-turbo",
                messages = new[]
                {
                    new { role = "system", content = "You are a helpful weather assistant providing concise, actionable advice via SMS." },
                    new { role = "user", content = prompt }
                },
                max_tokens = 150,
                temperature = 0.7
            };
            var json = JsonConvert.SerializeObject(requestBody);
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            var response = await _httpClient.PostAsync("https://api.openai.com/v1/chat/completions", content);
            if (!response.IsSuccessStatusCode)
            {
                return GenerateFallbackInsight(weather);
            }
            var responseContent = await response.Content.ReadAsStringAsync();
            var aiResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);
            return aiResponse.choices[0].message.content.ToString().Trim();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OpenAI Error: {ex.Message}");
            return GenerateFallbackInsight(weather);
        }
    }
    private string GenerateFallbackInsight(WeatherData weather)
    {
        var clothingAdvice = weather.Temperature > 20 ? "Light clothing" : 
                           weather.Temperature > 10 ? "Layer up" : "Warm coat needed";
        var transportAdvice = weather.PrecipitationProbability > 70 ? "Take umbrella/drive" : "Any transport fine";
        return $"🌡️{weather.Temperature}°C - {clothingAdvice}. {transportAdvice}. Conditions: {weather.Conditions}";
    }
}

The OpenAIService takes raw weather data and transforms it into personalized, actionable advice. It sends a carefully crafted prompt to OpenAI's GPT-3.5 model, requesting concise advice suitable for SMS. The service includes intelligent fallback logic that creates basic but useful weather insights even when the AI API is unavailable, ensuring reliability. Feel free to swap with any LLM provider.

SMS Communication Service

Now proceed to implement the Twilio service for sending and receiving SMS messages.

Create Services/TwilioService.cs then update it with the code below.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
public class TwilioService
{
    private readonly IConfiguration _config;
    public TwilioService(IConfiguration config)
    {
        _config = config;
        var accountSid = _config["Twilio:AccountSid"] ?? "your-twilio-account-sid";
        var authToken = _config["Twilio:AuthToken"] ?? "your-twilio-auth-token";
        TwilioClient.Init(accountSid, authToken);
    }
    public async Task SendWeatherSMSAsync(string toPhoneNumber, string weatherMessage)
    {
        try
        {
            var fromNumber = _config["Twilio:PhoneNumber"] ?? "your-twilio-phone-number";
            var message = await MessageResource.CreateAsync(
                body: $"🌤️ Micro-Weather Wisdom:\n{weatherMessage}",
                from: new PhoneNumber(fromNumber),
                to: new PhoneNumber(toPhoneNumber)
            );
            Console.WriteLine($"✅ SMS sent to {toPhoneNumber}: {message.Sid}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"❌ SMS Error: {ex.Message}");
        }
    }
}

The TwilioService handles all SMS communication using Twilio's REST API. It initializes the Twilio client with your credentials and provides methods to send weather insights via SMS. The service includes helpful simulation mode when credentials aren't configured, logging messages to the console instead of sending SMS, which is perfect for development and testing.

Background Scheduling Service

Finally, create the service that will handle scheduled daily weather updates for users.

Create Services/WeatherSchedulerService.cs and update it with the code below.

public class WeatherSchedulerService
{
    private readonly WeatherService _weatherService;
    private readonly OpenAIService _openAIService;
    private readonly TwilioService _twilioService;
    // Demo user - In production, this would come from a database
    private readonly List<UserPreferences> _users = new()
    {
        new UserPreferences 
        { 
            PhoneNumber = "+1234567890", // Replace with your phone number for testing
            PreferredTime = "08:00",
            Interests = new List<string> { "commute", "outdoor" }
        }
    };
    public WeatherSchedulerService(WeatherService weatherService, OpenAIService openAIService, TwilioService twilioService)
    {
        _weatherService = weatherService;
        _openAIService = openAIService;
        _twilioService = twilioService;
    }
    public async Task StartScheduledUpdates()
    {
        while (true)
        {
            try
            {
                var currentTime = DateTime.Now.ToString("HH:mm");
                foreach (var user in _users.Where(u => u.DailyUpdates && u.PreferredTime == currentTime))
                {
                    await SendScheduledWeatherUpdate(user);
                }
                // Check every minute
                await Task.Delay(TimeSpan.FromMinutes(1));
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Scheduler Error: {ex.Message}");
                await Task.Delay(TimeSpan.FromMinutes(5)); // Wait longer on error
            }
        }
    }
    private async Task SendScheduledWeatherUpdate(UserPreferences user)
    {
        var weather = await _weatherService.GetCurrentWeatherAsync();
        var context = string.Join(", ", user.Interests);
        var insight = await _openAIService.GenerateWeatherInsightAsync(weather, context);
        await _twilioService.SendWeatherSMSAsync(user.PhoneNumber, insight);
    }
}

The WeatherSchedulerService runs continuously in the background, checking every minute for users who should receive their daily weather update. When it finds a user whose preferred time matches the current time, it generates a personalized weather insight and sends it via SMS. This service demonstrates how to implement background tasks in .NET applications and includes proper error handling to maintain reliability.

Webhook Controller Implementation

Next, create the webhook controller that handles incoming SMS messages from Twilio.

Create Controllers/WebhookController.cs and update it with the code below.

using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
    private readonly WeatherService _weatherService;
    private readonly OpenAIService _openAIService;
    private readonly TwilioService _twilioService;
    public WebhookController(WeatherService weatherService, OpenAIService openAIService, TwilioService twilioService)
    {
        _weatherService = weatherService;
        _openAIService = openAIService;
        _twilioService = twilioService;
    }
    [HttpPost("sms")]
    public async Task<IActionResult> ReceiveSMS([FromForm] string Body, [FromForm] string From)
    {
        try
        {
            Console.WriteLine($"📨 Received SMS from {From}: {Body}");
            // Process incoming SMS and generate weather response
            var weather = await _weatherService.GetCurrentWeatherAsync();
            var context = ExtractContextFromMessage(Body);
            var response = await _openAIService.GenerateWeatherInsightAsync(weather, context);
            // Send response back (Note: Twilio trial only supports verified numbers)
            await _twilioService.SendWeatherSMSAsync(From, response);
            return Ok("SMS processed successfully");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Webhook Error: {ex.Message}");
            return StatusCode(500, "Error processing SMS");
        }
    }
    private string ExtractContextFromMessage(string message)
    {
        var lowerMessage = message.ToLower();
        if (lowerMessage.Contains("commute") || lowerMessage.Contains("travel"))
            return "commute";
        if (lowerMessage.Contains("outfit") || lowerMessage.Contains("clothing"))
            return "clothing";
        if (lowerMessage.Contains("outdoor") || lowerMessage.Contains("activity"))
            return "outdoor activity";
        return "general";
    }
}

The WebhookControlleris the heart of the SMS interaction system. When someone sends an SMS to your Twilio number, Twilio forwards that message to this webhook endpoint. The controller analyzes the message content to understand what the user is asking about. It then generates a personalized weather response and sends it back. The controller returns proper TwiML XML responses that Twilio expects, ensuring reliable communication.

Main Application Setup

Finally, let's create the main application entry point that ties everything together.

Replace Program.cs with the code below.

// Program.cs - Main Entry Point
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Text;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddHttpClient();
builder.Services.AddSingleton<WeatherService>();
builder.Services.AddSingleton<OpenAIService>();
builder.Services.AddSingleton<TwilioService>();
var app = builder.Build();
// Configure pipeline
app.UseRouting();
app.MapControllers();
// Start background service for scheduled weather updates
var backgroundService = new WeatherSchedulerService(
    app.Services.GetRequiredService<WeatherService>(),
    app.Services.GetRequiredService<OpenAIService>(),
    app.Services.GetRequiredService<TwilioService>()
);
_ = Task.Run(backgroundService.StartScheduledUpdates);
Console.WriteLine("🌤️ Micro-Weather Wisdom SMS Service Started!");
Console.WriteLine("📱 Webhook endpoint: http://localhost:5000/webhook/sms");
Console.WriteLine("🚀 Use ngrok to expose: ngrok http 5000");
app.Run("http://localhost:5000");

The Program.cs file is the application's entry point. It configures dependency injection for all your services, sets up the web server with routing, and starts the background scheduler service. The application listens on port 5000 and provides helpful console output showing important URLs. This setup follows modern .NET patterns and ensures all services are properly initialized and connected.

Running and Testing the Application

Start Your Application

To fire up your application, run this command.

# Restore packages and build
dotnet restore
dotnet build
# Run the application
dotnet run

The application will start and display URLs in the console.

To expose your local server, open a new terminal window and run ngrok:

ngrok http 5000

Copy the HTTPS URL provided (e.g., https://example123.ngrok.io). What this does: ngrok creates a secure tunnel from the internet to your local development server. This is necessary because Twilio needs a publicly accessible URL to send webhook requests to your application.

Configure Twilio Webhook

  1. Go to the Twilio Console (https://console.twilio.com/)
  2. Navigate to Phone Numbers → Manage → Active numbers
  3. Click on your Twilio phone number
  4. In the Messaging section, set the webhook URL to: https://example123.ngrok.io/webhook/sms
  5. Set HTTP method to POST
  6. Save the configuration

Simple use flow: SMS → Twilio → Webhook → Your Local App → AI Response → SMS Back

This tells Twilio where to forward incoming SMS messages. When someone texts your Twilio number, Twilio will send the message data to your webhook endpoint.

Testing Your Service

Send an SMS to your Twilio virtual phone number from a verified number. You can use a phone you have access to, or the Twilio Virtual phone. Try messages like:

  • "What should I wear today?"
  • "How's the weather for my commute?"
  • "Good day for outdoor activities?"

See the screenshots below on how the service responds to the requests.

A smartphone screen showing a text conversation about weather from a virtual phone using Twilio.

See below in the screenshot how the service responds. Feel free to try other organic queries to test the application's potential and limitations.

Screenshot of a phone displaying two messages sent from a Twilio trial account about SMS success and weather update.

The application works as intended. You can now take this service further as discussed in the next steps.

Troubleshooting Steps

If you run into any errors getting the application running as described, there are a few troubleshooting steps that you can take:

  • Credentials & config
    • Confirm appsettings.json contains your real values (not placeholders) and that they’re in E.164 format for phone numbers (e.g. +14155554321).
    • Verify Twilio:AccountSid and Twilio:AuthToken are correct.
    • Confirm OpenAI:ApiKey and any weather API credentials are present and valid.
  • Twilio trial / verified numbers
    • If you’re on a Twilio trial, you can only send SMS to numbers you’ve verified in the Console (trial restriction). Upgrade the account to remove this limit.
    • If your number is registered in the US, automated SMS may be restricted to verified 10DLC or verified toll-free numbers only.
  • Webhook & tunnelling
    • Confirm your ngrok (or other tunnel) URL matches what you entered in Twilio’s phone-number Messaging webhook, and that the webhook path is /webhook/sms (POST).
  • Monitor OpenAI usage/quotas in your OpenAI Console (if the AI call fails or you’re on a limited trial, AI responses will fall back).
  • Inspect Twilio MessageResource responses (Sid, Status, ErrorCode) — log them for postmortem analysis.

How to debug quickly

  • Re-run locally with dotnet run and trigger a send; watch console logs for Twilio SDK errors.
  • Check Twilio Console → Messaging → Logs (and TrustHub / Regulatory Compliance) for deliverability errors and status codes.

Next Steps and Enhancements

Now that you have a working weather SMS service, consider these improvements:

  • Database Integration: Replace the in-memory user list with a proper database
  • User Management: Add SMS commands for users to update their preferences
  • Multiple Locations: Allow users to set custom locations
  • Rich Messaging: Add emojis and better formatting to responses
  • Analytics: Track usage patterns and popular request types

Your Micro-Weather Wisdom SMS service is now ready to provide personalized weather insights to users via SMS! The service demonstrates proper webhook handling, API integration, background processing, and follows Twilio's best practices for SMS applications.

Additional Resources for Continued Learning

Jacob Snipes is a seasoned AI Engineer who transforms complex communication challenges into seamless, intelligent solutions. With a strong foundation in full-stack development and a keen eye for innovation, he crafts applications that enhance user experiences and streamline operations. Jacob's work bridges the gap between cutting-edge technology and real-world applications, delivering impactful results across various industries.