System Architecture
System Architecture
The MCP Weather Server follows a clean, SOLID-compliant architecture that prioritizes maintainability, testability, and extensibility. This document provides a comprehensive overview of the system design and architectural decisions.
Architecture Overview
graph TB
Client[MCP Client] --> Tools[MCP Tools Layer]
Tools --> Services[Service Layer]
Services --> Config[Configuration]
Services --> Logging[Logging System]
subgraph "MCP Tools Layer"
RT[RandomNumberTools]
WT[WeatherTools]
end
subgraph "Service Layer"
RNS[RandomNumberService]
WS[WeatherService]
end
subgraph "Abstractions"
IRNS[IRandomNumberService]
IWS[IWeatherService]
end
RT --> IRNS
WT --> IWS
RNS --> IRNS
WS --> IWS
SOLID Principles Implementation
Single Responsibility Principle (SRP)
Each class has a single, well-defined responsibility:
// ✅ GOOD: Single responsibility - random number generation
public class RandomNumberService : IRandomNumberService
{
public int GetRandomNumber(int min = 0, int max = 100)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(min, max, nameof(min));
return _random.Next(min, max);
}
}
// ✅ GOOD: Single responsibility - weather information
public class WeatherService : IWeatherService
{
public string GetCityWeather(string city)
{
ArgumentException.ThrowIfNullOrWhiteSpace(city, nameof(city));
var weatherIndex = _randomNumberService.GetRandomNumber(0, _weatherChoices.Length);
return $"The weather in {city} is {_weatherChoices[weatherIndex]}.";
}
}
Open/Closed Principle (OCP)
The system is open for extension but closed for modification:
// Extension point - new weather providers can be added
public interface IWeatherService
{
string GetCityWeather(string city);
}
// Easy to extend with new implementations
public class ApiWeatherService : IWeatherService
{
public string GetCityWeather(string city)
{
// Implementation using external API
}
}
public class DatabaseWeatherService : IWeatherService
{
public string GetCityWeather(string city)
{
// Implementation using database
}
}
Liskov Substitution Principle (LSP)
All implementations are fully substitutable:
// Any implementation can be substituted without breaking functionality
IRandomNumberService randomService = new RandomNumberService();
// OR
IRandomNumberService randomService = new CryptoRandomService();
// OR
IRandomNumberService randomService = new MockRandomService(); // For testing
Interface Segregation Principle (ISP)
Focused, minimal interfaces:
// ✅ GOOD: Focused interface
public interface IRandomNumberService
{
int GetRandomNumber(int min = 0, int max = 100);
}
// ✅ GOOD: Separate concern
public interface IWeatherService
{
string GetCityWeather(string city);
}
// ❌ BAD: Would violate ISP
// public interface IGeneralService
// {
// int GetRandomNumber(int min, int max);
// string GetCityWeather(string city);
// void SendEmail(string to, string subject, string body);
// void WriteToDatabase(object data);
// }
Dependency Inversion Principle (DIP)
High-level modules depend on abstractions:
// ✅ GOOD: Depends on abstraction
public class WeatherTools
{
private readonly IWeatherService _weatherService;
public WeatherTools(IWeatherService weatherService)
{
_weatherService = weatherService ?? throw new ArgumentNullException(nameof(weatherService));
}
}
// ✅ GOOD: Service composition through abstractions
public class WeatherService : IWeatherService
{
private readonly IRandomNumberService _randomNumberService;
private readonly IConfiguration _configuration;
public WeatherService(IRandomNumberService randomNumberService, IConfiguration configuration)
{
_randomNumberService = randomNumberService ?? throw new ArgumentNullException(nameof(randomNumberService));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
}
Layer Architecture
1. Presentation Layer (MCP Tools)
Purpose: Handle MCP protocol interactions and expose tools to clients.
[McpServerTool]
[Description("Generates a random number between the specified minimum and maximum values.")]
public int GetRandomNumber(
[Description("Minimum value (inclusive)")] int min = 0,
[Description("Maximum value (exclusive)")] int max = 100)
{
return _randomNumberService.GetRandomNumber(min, max);
}
Responsibilities:
- MCP protocol compliance
- Input parameter validation
- Tool metadata and descriptions
- Delegation to service layer
2. Service Layer
Purpose: Contain business logic and coordinate application operations.
public class WeatherService : IWeatherService
{
private readonly IRandomNumberService _randomNumberService;
private readonly string[] _weatherChoices;
public WeatherService(IRandomNumberService randomNumberService, IConfiguration configuration)
{
_randomNumberService = randomNumberService ?? throw new ArgumentNullException(nameof(randomNumberService));
var weatherConfig = configuration["WeatherChoices"] ?? "balmy,rainy,stormy";
_weatherChoices = weatherConfig.Split(',', StringSplitOptions.RemoveEmptyEntries);
if (_weatherChoices.Length == 0)
{
_weatherChoices = ["balmy", "rainy", "stormy"];
}
}
public string GetCityWeather(string city)
{
ArgumentException.ThrowIfNullOrWhiteSpace(city, nameof(city));
var selectedWeatherIndex = _randomNumberService.GetRandomNumber(0, _weatherChoices.Length);
return $"The weather in {city} is {_weatherChoices[selectedWeatherIndex]}.";
}
}
Responsibilities:
- Business rule implementation
- Data transformation
- Coordination between dependencies
- Error handling and validation
3. Infrastructure Layer
Purpose: Handle cross-cutting concerns and external dependencies.
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Register core services
services.AddSingleton<IRandomNumberService, RandomNumberService>();
services.AddSingleton<IWeatherService, WeatherService>();
return services;
}
}
Responsibilities:
- Dependency injection configuration
- External service integrations
- Configuration management
- Logging and monitoring
Design Patterns Used
1. Dependency Injection Pattern
// Program.cs
builder.Services.AddApplicationServices();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<RandomNumberTools>()
.WithTools<WeatherTools>();
Benefits:
- Loose coupling
- Easy testing with mocks
- Runtime dependency resolution
- Configuration flexibility
2. Service Locator Pattern (via DI Container)
// Automatic resolution of dependency tree
var weatherTools = serviceProvider.GetRequiredService<WeatherTools>();
// Automatically resolves: WeatherTools -> IWeatherService -> IRandomNumberService
3. Template Method Pattern (in Testing)
public abstract class ServiceTestBase<TService>
{
protected TService Service { get; private set; }
protected virtual void SetupService()
{
// Template method for service setup
}
[SetUp]
public void Setup()
{
SetupService();
}
}
4. Factory Pattern (for Random Number Generation)
public class RandomNumberService : IRandomNumberService
{
private readonly Random _random;
public RandomNumberService(Random? random = null)
{
_random = random ?? Random.Shared; // Factory-like creation
}
}
Error Handling Strategy
1. Defensive Programming
public string GetCityWeather(string city)
{
// Guard clauses
ArgumentException.ThrowIfNullOrWhiteSpace(city, nameof(city));
// Validation
if (city.Length > 100)
throw new ArgumentException("City name too long", nameof(city));
// Business logic
var selectedWeatherIndex = _randomNumberService.GetRandomNumber(0, _weatherChoices.Length);
return $"The weather in {city} is {_weatherChoices[selectedWeatherIndex]}.";
}
2. Fail-Fast Principle
public RandomNumberService(IRandomNumberService randomNumberService)
{
// Fail immediately if dependency is null
_randomNumberService = randomNumberService ?? throw new ArgumentNullException(nameof(randomNumberService));
}
3. Graceful Degradation
public WeatherService(IRandomNumberService randomNumberService, IConfiguration configuration)
{
_randomNumberService = randomNumberService ?? throw new ArgumentNullException(nameof(randomNumberService));
var weatherConfig = configuration["WeatherChoices"] ?? "balmy,rainy,stormy";
_weatherChoices = weatherConfig.Split(',', StringSplitOptions.RemoveEmptyEntries);
// Graceful fallback to defaults
if (_weatherChoices.Length == 0)
{
_weatherChoices = ["balmy", "rainy", "stormy"];
}
}
Configuration Architecture
1. Hierarchical Configuration
{
"WeatherChoices": "sunny,cloudy,rainy,stormy,snowy,windy,foggy",
"RandomNumber": {
"DefaultMin": 0,
"DefaultMax": 100,
"AllowNegative": true
},
"Logging": {
"LogLevel": {
"Default": "Information",
"SampleMcpServer": "Debug"
}
}
}
2. Environment-Specific Settings
// appsettings.json (base)
// appsettings.Development.json (development overrides)
// appsettings.Production.json (production overrides)
// Environment variables (runtime overrides)
var builder = Host.CreateApplicationBuilder(args);
// Automatically loads configuration hierarchy
Scalability Considerations
1. Stateless Services
// All services are stateless and thread-safe
[Singleton] // Can be safely shared across requests
public class WeatherService : IWeatherService
{
// No mutable state - thread-safe by design
}
2. Immutable Configuration
public class WeatherService : IWeatherService
{
private readonly string[] _weatherChoices; // Immutable after construction
public WeatherService(IRandomNumberService randomNumberService, IConfiguration configuration)
{
// Configuration read once and cached
var weatherConfig = configuration["WeatherChoices"] ?? "balmy,rainy,stormy";
_weatherChoices = weatherConfig.Split(',', StringSplitOptions.RemoveEmptyEntries);
}
}
3. Resource Efficiency
// Singleton services reduce memory footprint
services.AddSingleton<IRandomNumberService, RandomNumberService>();
services.AddSingleton<IWeatherService, WeatherService>();
// Efficient string operations
return $"The weather in {city} is {_weatherChoices[selectedWeatherIndex]}.";
Testing Architecture Integration
The architecture is designed with testability as a first-class concern:
1. Dependency Injection Enables Testing
// Easy to mock dependencies
var mockRandomService = new Mock<IRandomNumberService>();
var mockConfiguration = new Mock<IConfiguration>();
var weatherService = new WeatherService(mockRandomService.Object, mockConfiguration.Object);
2. Clear Boundaries for Unit Testing
// Each layer can be tested in isolation
[Test]
public void WeatherService_GetCityWeather_ReturnsFormattedString()
{
// Arrange - Mock only the direct dependencies
mockRandomService.Setup(x => x.GetRandomNumber(0, 3)).Returns(1);
// Act - Test only the weather service logic
var result = weatherService.GetCityWeather("TestCity");
// Assert - Verify the expected behavior
result.Should().Be("The weather in TestCity is rainy.");
}
3. Integration Testing Support
// Real services can be easily composed for integration testing
services.AddApplicationServices();
var serviceProvider = services.BuildServiceProvider();
var weatherTools = serviceProvider.GetRequiredService<WeatherTools>();
Performance Characteristics
1. Memory Efficiency
- Singleton services reduce object allocation
- Immutable configurations prevent memory leaks
- String interpolation optimized for common scenarios
2. CPU Efficiency
- Minimal computational overhead
- No complex algorithms or heavy processing
- Fast dependency resolution through DI container
3. Scalability
- Stateless design allows horizontal scaling
- No shared mutable state prevents concurrency issues
- Lightweight services suitable for containerization
Architecture Evolution
The current architecture provides a solid foundation for future enhancements:
- Add New Tools: Implement new MCP tools by following the same pattern
- External Integrations: Replace services with external API implementations
- Caching Layer: Add caching services without changing existing code
- Monitoring: Inject telemetry services through the existing DI infrastructure
Next Steps
- Review Production-Ready Settings for deployment configuration
- Explore CI/CD Maintainability for deployment automation
- Check Testing Integration for quality assurance processes