diff --git a/BuildWithAspire.slnx b/BuildWithAspire.slnx
index 7fd00f5..41b5c2a 100644
--- a/BuildWithAspire.slnx
+++ b/BuildWithAspire.slnx
@@ -5,12 +5,14 @@
+
+
diff --git a/MCP_INTEGRATION.md b/MCP_INTEGRATION.md
new file mode 100644
index 0000000..7aed2f4
--- /dev/null
+++ b/MCP_INTEGRATION.md
@@ -0,0 +1,287 @@
+# Model Context Protocol (MCP) Integration in Aspire
+
+## Overview
+
+This project demonstrates the integration of Model Context Protocol (MCP) with .NET Aspire, providing a standardized way for AI models to discover and use tools dynamically. MCP enables interoperable AI model hosting and serving by providing a consistent interface for tool discovery and execution.
+
+## What is MCP?
+
+Model Context Protocol (MCP) is a standard that:
+- Enables AI models to discover available tools dynamically
+- Provides a standardized interface for calling tools
+- Allows for interoperable AI model hosting and serving
+- Supports tool composition and chaining
+
+## Benefits of Using MCP in Aspire
+
+### 1. **Dynamic Tool Discovery**
+- AI models can discover available tools at runtime
+- No need to hardcode tool endpoints or interfaces
+- Tools can be added or removed without changing AI model code
+
+### 2. **Service Orchestration**
+- Aspire manages MCP server lifecycle and discovery
+- Built-in health checks and monitoring
+- Automatic service resolution and load balancing
+
+### 3. **Scalability**
+- MCP servers can be scaled independently
+- Multiple tool instances can be deployed
+- Built-in telemetry and observability
+
+### 4. **Developer Experience**
+- Standardized tool development patterns
+- Easy testing and debugging
+- Consistent API documentation
+
+## Architecture
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ AI Service │────│ MCP Server │────│ Tool Suite │
+│ │ │ │ │ │
+│ - Chat Service │ │ - Tool Registry │ │ - Weather Tool │
+│ - AI Models │ │ - Request/Resp │ │ - Time Tool │
+│ - MCP Client │ │ - Validation │ │ - Custom Tools │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+ │ │ │
+ └───────────────────────┼───────────────────────┘
+ │
+ ┌─────────────────┐
+ │ Aspire AppHost │
+ │ │
+ │ - Service Disc. │
+ │ - Health Checks │
+ │ - Telemetry │
+ └─────────────────┘
+```
+
+## Implementation Details
+
+### MCP Server (`BuildWithAspire.McpServer`)
+
+The MCP Server hosts available tools and provides the following endpoints:
+
+#### Available Tools
+```http
+GET /mcp/tools
+```
+Returns a list of all available tools with their descriptions and schemas.
+
+#### Call Tool
+```http
+POST /mcp/tools/call
+Content-Type: application/json
+
+{
+ "name": "get_weather",
+ "arguments": {
+ "location": "New York",
+ "days": 3
+ }
+}
+```
+
+### Available Tools
+
+#### 1. Weather Tool (`get_weather`)
+Provides weather forecasts for specified locations.
+
+**Parameters:**
+- `location` (string, required): The location to get weather for
+- `days` (integer, optional): Number of days to forecast (1-7, default: 5)
+
+**Example Usage:**
+```json
+{
+ "name": "get_weather",
+ "arguments": {
+ "location": "San Francisco",
+ "days": 3
+ }
+}
+```
+
+#### 2. Time Tool (`get_time`)
+Provides current date and time information with timezone support.
+
+**Parameters:**
+- `timezone` (string, optional): Timezone name (default: "UTC")
+- `format` (string, optional): Format type - "iso", "local", "unix", or "detailed" (default: "iso")
+
+**Example Usage:**
+```json
+{
+ "name": "get_time",
+ "arguments": {
+ "timezone": "America/New_York",
+ "format": "detailed"
+ }
+}
+```
+
+### AI Service Integration
+
+The API Service (`BuildWithAspire.ApiService`) includes:
+
+1. **MCP Client Service**: Communicates with the MCP Server
+2. **Smart Tool Detection**: Automatically detects when users ask for weather or time information
+3. **Context Enrichment**: Adds tool results to AI conversation context
+
+#### Intelligent Tool Usage
+
+The AI service analyzes user messages and automatically calls appropriate MCP tools:
+
+- **Weather queries**: "What's the weather in Paris?" → calls `get_weather` tool
+- **Time queries**: "What time is it?" → calls `get_time` tool
+- **Regular chat**: Processes normally without tool calls
+
+### Aspire Integration
+
+In `BuildWithAspire.AppHost`, the MCP Server is registered as:
+
+```csharp
+// Add MCP Server service
+var mcpServer = builder.AddProject("mcpserver")
+ .WithExternalHttpEndpoints();
+
+// Reference MCP Server from API Service
+var apiService = builder.AddProject("apiservice")
+ .WithExternalHttpEndpoints()
+ .WithAIModel(aiService)
+ .WithReference(chatDb)
+ .WithReference(mcpServer) // Enables service discovery
+ .WaitFor(chatDb)
+ .WaitFor(mcpServer);
+```
+
+## Testing the MCP Implementation
+
+### 1. Demo Endpoint
+Visit `/mcp/demo` to see all available tools and example calls:
+
+```http
+GET https://localhost:5001/mcp/demo
+```
+
+### 2. Chat Integration
+Create a conversation and ask for weather or time:
+
+```http
+POST https://localhost:5001/conversations/{id}/messages
+Content-Type: application/json
+
+{
+ "message": "What's the weather like in London for the next 3 days?"
+}
+```
+
+### 3. Direct Tool Calls
+Call MCP tools directly through the MCP server:
+
+```http
+POST https://localhost:5124/mcp/tools/call
+Content-Type: application/json
+
+{
+ "name": "get_weather",
+ "arguments": {
+ "location": "Tokyo",
+ "days": 5
+ }
+}
+```
+
+## Extending the MCP Implementation
+
+### Adding New Tools
+
+1. **Create Tool Class**: Implement the tool interface in `Tools/` directory
+2. **Register Tool**: Add to `McpServer` constructor
+3. **Update Documentation**: Add tool description and examples
+
+Example tool structure:
+```csharp
+public class CustomTool
+{
+ public string Name => "tool_name";
+ public string Description => "Tool description";
+
+ public object GetParametersSchema() => new { /* schema */ };
+
+ public async Task ExecuteAsync(Dictionary parameters, CancellationToken cancellationToken = default)
+ {
+ // Tool implementation
+ }
+}
+```
+
+### AI Model Integration
+
+To integrate with different AI providers:
+
+1. **Tool Calling**: Use AI model's native tool calling capabilities
+2. **Schema Conversion**: Convert MCP schemas to AI-specific formats
+3. **Response Processing**: Parse tool results and integrate into conversations
+
+## Best Practices
+
+### 1. **Tool Design**
+- Keep tools focused and single-purpose
+- Provide comprehensive parameter validation
+- Include detailed error handling
+- Use appropriate timeout values
+
+### 2. **Error Handling**
+- Graceful degradation when tools are unavailable
+- Meaningful error messages for users
+- Proper logging and telemetry
+
+### 3. **Performance**
+- Cache tool metadata when possible
+- Use async patterns throughout
+- Implement proper timeout and cancellation support
+
+### 4. **Security**
+- Validate all tool parameters
+- Implement rate limiting if needed
+- Sanitize tool outputs before returning to AI
+
+## Monitoring and Observability
+
+The MCP implementation includes:
+
+- **Health Checks**: Built-in Aspire health monitoring
+- **Telemetry**: OpenTelemetry integration for tracing
+- **Logging**: Structured logging throughout the pipeline
+- **Metrics**: Tool call success rates and performance metrics
+
+## Migration from Hardcoded Tools
+
+The original weather endpoint (`/weatherforecast`) remains available, demonstrating both approaches:
+
+- **Legacy**: Direct API endpoints with hardcoded functionality
+- **MCP**: Dynamic tool discovery and execution
+
+This allows for gradual migration and comparison of approaches.
+
+## Future Enhancements
+
+Potential extensions to the MCP implementation:
+
+1. **Tool Chaining**: Allow tools to call other tools
+2. **Dynamic Loading**: Load tools from external assemblies
+3. **Version Management**: Support multiple versions of tools
+4. **Tool Marketplace**: Discover and install community tools
+5. **Advanced Schemas**: Support complex parameter types and validation
+
+## Conclusion
+
+The MCP integration demonstrates how modern AI applications can benefit from standardized tool protocols. By combining MCP with .NET Aspire, we achieve:
+
+- **Discoverability**: AI models can find and use tools dynamically
+- **Interoperability**: Standard interfaces enable tool reuse
+- **Scalability**: Aspire provides enterprise-grade hosting
+- **Maintainability**: Clear separation of concerns and testable components
+
+This implementation serves as a foundation for building sophisticated AI applications with rich tool ecosystems.
\ No newline at end of file
diff --git a/src/BuildWithAspire.ApiService/Program.cs b/src/BuildWithAspire.ApiService/Program.cs
index dac1201..a2e1229 100644
--- a/src/BuildWithAspire.ApiService/Program.cs
+++ b/src/BuildWithAspire.ApiService/Program.cs
@@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.AI;
using Scalar.AspNetCore;
+using System.Text.Json;
using ChatRole = Microsoft.Extensions.AI.ChatRole;
var builder = WebApplication.CreateBuilder(args);
@@ -35,6 +36,13 @@
builder.Services.AddTransient();
+// Add MCP Client Service to communicate with MCP Server
+builder.Services.AddHttpClient(client =>
+{
+ // This will be configured by Aspire service discovery to point to the MCP server
+ client.BaseAddress = new Uri("http://mcpserver");
+});
+
var app = builder.Build();
app.MapDefaultEndpoints();
@@ -199,7 +207,7 @@ static async Task GetWeatherSummary(IChatClient client, int temp)
.WithName("CreateConversation")
.WithOpenApi();
-app.MapPost("/conversations/{id}/messages", async (Guid id, [FromBody] SendMessageRequest request, ChatService chatService, ChatDbContext db) =>
+app.MapPost("/conversations/{id}/messages", async (Guid id, [FromBody] SendMessageRequest request, ChatService chatService, McpClientService mcpClient, ChatDbContext db) =>
{
try
{
@@ -235,27 +243,138 @@ static async Task GetWeatherSummary(IChatClient client, int temp)
// Save user message first
await db.SaveChangesAsync().ConfigureAwait(false);
- // Prepare history for AI - get all messages for this conversation including the new one
- var messages = await db.Messages
- .Where(m => m.ConversationId == id)
- .OrderBy(m => m.CreatedAt)
- .Select(m => new ChatMessageRequest
- {
- Role = m.Role,
- Content = m.Content
- })
- .ToListAsync().ConfigureAwait(false);
-
- // Get AI response with timeout handling
+ // Check if the user is asking for weather or time information
+ var message = request.Message.ToLowerInvariant();
string aiResponse;
- try
+
+ if (message.Contains("weather") || message.Contains("forecast"))
+ {
+ // Try to call MCP weather tool
+ try
+ {
+ // Extract location from user message (simple approach)
+ var location = "New York"; // Default location
+ if (message.Contains(" in "))
+ {
+ var locationPart = message.Substring(message.IndexOf(" in ") + 4);
+ var words = locationPart.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (words.Length > 0)
+ {
+ location = string.Join(" ", words.Take(2)); // Take up to 2 words for location
+ }
+ }
+
+ var weatherResult = await mcpClient.CallToolAsync("get_weather", new Dictionary
+ {
+ ["location"] = location,
+ ["days"] = 3
+ }).ConfigureAwait(false);
+
+ if (weatherResult != null)
+ {
+ var weatherJson = System.Text.Json.JsonSerializer.Serialize(weatherResult, new JsonSerializerOptions { WriteIndented = true });
+
+ // Prepare history for AI - get all messages for this conversation including the new one
+ var messages = await db.Messages
+ .Where(m => m.ConversationId == id)
+ .OrderBy(m => m.CreatedAt)
+ .Select(m => new ChatMessageRequest
+ {
+ Role = m.Role,
+ Content = m.Content
+ })
+ .ToListAsync().ConfigureAwait(false);
+
+ // Add the weather data to the context
+ messages.Add(new ChatMessageRequest
+ {
+ Role = "system",
+ Content = $"Weather data from MCP tool: {weatherJson}. Use this data to provide a helpful weather summary to the user."
+ });
+
+ aiResponse = await chatService.ProcessMessagesWithHistory(messages).ConfigureAwait(false);
+ }
+ else
+ {
+ aiResponse = "I'm sorry, I couldn't get the weather information right now. Please try again later.";
+ }
+ }
+ catch (Exception ex)
+ {
+ app.Logger.LogWarning(ex, "Failed to get weather from MCP tool");
+ aiResponse = "I'm sorry, I encountered an error while trying to get weather information.";
+ }
+ }
+ else if (message.Contains("time") || message.Contains("date"))
{
- aiResponse = await chatService.ProcessMessagesWithHistory(messages).ConfigureAwait(false);
+ // Try to call MCP time tool
+ try
+ {
+ var timeResult = await mcpClient.CallToolAsync("get_time", new Dictionary
+ {
+ ["timezone"] = "UTC",
+ ["format"] = "detailed"
+ }).ConfigureAwait(false);
+
+ if (timeResult != null)
+ {
+ var timeJson = System.Text.Json.JsonSerializer.Serialize(timeResult, new JsonSerializerOptions { WriteIndented = true });
+
+ // Prepare history for AI - get all messages for this conversation including the new one
+ var messages = await db.Messages
+ .Where(m => m.ConversationId == id)
+ .OrderBy(m => m.CreatedAt)
+ .Select(m => new ChatMessageRequest
+ {
+ Role = m.Role,
+ Content = m.Content
+ })
+ .ToListAsync().ConfigureAwait(false);
+
+ // Add the time data to the context
+ messages.Add(new ChatMessageRequest
+ {
+ Role = "system",
+ Content = $"Current time data from MCP tool: {timeJson}. Use this data to provide helpful time/date information to the user."
+ });
+
+ aiResponse = await chatService.ProcessMessagesWithHistory(messages).ConfigureAwait(false);
+ }
+ else
+ {
+ aiResponse = "I'm sorry, I couldn't get the current time information right now.";
+ }
+ }
+ catch (Exception ex)
+ {
+ app.Logger.LogWarning(ex, "Failed to get time from MCP tool");
+ aiResponse = "I'm sorry, I encountered an error while trying to get time information.";
+ }
}
- catch (Exception ex)
+ else
{
- app.Logger.LogError(ex, "AI service error for conversation {ConversationId}", id);
- return Results.Problem("Failed to get AI response", statusCode: 500);
+ // Regular chat processing
+ // Prepare history for AI - get all messages for this conversation including the new one
+ var messages = await db.Messages
+ .Where(m => m.ConversationId == id)
+ .OrderBy(m => m.CreatedAt)
+ .Select(m => new ChatMessageRequest
+ {
+ Role = m.Role,
+ Content = m.Content
+ })
+ .ToListAsync().ConfigureAwait(false);
+
+ // Get AI response with timeout handling
+ try
+ {
+ aiResponse = await chatService.ProcessMessagesWithHistory(messages).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ app.Logger.LogError(ex, "AI service error for conversation {ConversationId}", id);
+ return Results.Problem("Failed to get AI response", statusCode: 500);
+ }
}
// Add assistant message
@@ -304,6 +423,41 @@ static async Task GetWeatherSummary(IChatClient client, int temp)
.WithName("GetChat")
.WithOpenApi();
+// Add MCP tools demonstration endpoint
+app.MapGet("/mcp/demo", async (McpClientService mcpClient) =>
+{
+ try
+ {
+ var tools = await mcpClient.GetAvailableToolsAsync().ConfigureAwait(false);
+
+ var weatherDemo = await mcpClient.CallToolAsync("get_weather", new Dictionary
+ {
+ ["location"] = "San Francisco",
+ ["days"] = 2
+ }).ConfigureAwait(false);
+
+ var timeDemo = await mcpClient.CallToolAsync("get_time", new Dictionary
+ {
+ ["timezone"] = "UTC",
+ ["format"] = "detailed"
+ }).ConfigureAwait(false);
+
+ return Results.Ok(new
+ {
+ message = "MCP Tools Demo",
+ available_tools = tools?.Tools?.Select(t => new { t.Name, t.Description }),
+ weather_demo = weatherDemo,
+ time_demo = timeDemo
+ });
+ }
+ catch (Exception ex)
+ {
+ return Results.Problem($"MCP Demo failed: {ex.Message}");
+ }
+})
+.WithName("McpDemo")
+.WithOpenApi();
+
app.Run();
public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
diff --git a/src/BuildWithAspire.ApiService/Services/McpClientService.cs b/src/BuildWithAspire.ApiService/Services/McpClientService.cs
new file mode 100644
index 0000000..cf8443b
--- /dev/null
+++ b/src/BuildWithAspire.ApiService/Services/McpClientService.cs
@@ -0,0 +1,106 @@
+using System.Text.Json;
+
+namespace BuildWithAspire.ApiService.Services;
+
+///
+/// Service to interact with MCP tools.
+///
+public class McpClientService
+{
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ public McpClientService(HttpClient httpClient, ILogger logger)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+ }
+
+ ///
+ /// Get available MCP tools.
+ ///
+ public async Task GetAvailableToolsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ _logger.LogDebug("Fetching available MCP tools");
+ var response = await _httpClient.GetAsync("/mcp/tools", cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ var result = JsonSerializer.Deserialize(content, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ _logger.LogInformation("Retrieved {ToolCount} MCP tools", result?.Tools?.Count ?? 0);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get available MCP tools");
+ return null;
+ }
+ }
+
+ ///
+ /// Call an MCP tool.
+ ///
+ public async Task CallToolAsync(string toolName, Dictionary arguments, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ _logger.LogDebug("Calling MCP tool: {ToolName} with {ArgumentCount} arguments", toolName, arguments.Count);
+
+ var request = new McpToolCallRequest
+ {
+ Name = toolName,
+ Arguments = arguments
+ };
+
+ var json = JsonSerializer.Serialize(request);
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync("/mcp/tools/call", content, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ var result = JsonSerializer.Deserialize(responseContent);
+
+ _logger.LogInformation("Successfully called MCP tool: {ToolName}", toolName);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to call MCP tool: {ToolName}", toolName);
+ throw new InvalidOperationException($"Failed to call MCP tool '{toolName}': {ex.Message}", ex);
+ }
+ }
+}
+
+///
+/// Response structure for MCP tools listing.
+///
+public class McpToolsResponse
+{
+ public List Tools { get; set; } = new();
+}
+
+///
+/// Information about an MCP tool.
+///
+public class McpToolInfo
+{
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public object? InputSchema { get; set; }
+}
+
+///
+/// Request structure for calling MCP tools.
+///
+public class McpToolCallRequest
+{
+ public string Name { get; set; } = string.Empty;
+ public Dictionary Arguments { get; set; } = new();
+}
\ No newline at end of file
diff --git a/src/BuildWithAspire.AppHost/BuildWithAspire.AppHost.csproj b/src/BuildWithAspire.AppHost/BuildWithAspire.AppHost.csproj
index 0ec973d..cfe27d4 100644
--- a/src/BuildWithAspire.AppHost/BuildWithAspire.AppHost.csproj
+++ b/src/BuildWithAspire.AppHost/BuildWithAspire.AppHost.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/BuildWithAspire.AppHost/Program.cs b/src/BuildWithAspire.AppHost/Program.cs
index db9d17b..507c0eb 100644
--- a/src/BuildWithAspire.AppHost/Program.cs
+++ b/src/BuildWithAspire.AppHost/Program.cs
@@ -32,16 +32,24 @@
// Add API service with AI model configuration
var aiService = builder.AddAIModel();
+// Add MCP Server service
+var mcpServer = builder.AddProject("mcpserver")
+ .WithExternalHttpEndpoints();
+
var apiService = builder.AddProject("apiservice")
.WithExternalHttpEndpoints()
.WithAIModel(aiService)
.WithReference(chatDb)
- .WaitFor(chatDb);
+ .WithReference(mcpServer)
+ .WaitFor(chatDb)
+ .WaitFor(mcpServer);
// Add Web service
var _ = builder.AddProject("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService)
- .WaitFor(apiService);
+ .WithReference(mcpServer)
+ .WaitFor(apiService)
+ .WaitFor(mcpServer);
builder.Build().Run();
diff --git a/src/BuildWithAspire.McpServer/BuildWithAspire.McpServer.csproj b/src/BuildWithAspire.McpServer/BuildWithAspire.McpServer.csproj
new file mode 100644
index 0000000..b65a777
--- /dev/null
+++ b/src/BuildWithAspire.McpServer/BuildWithAspire.McpServer.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/BuildWithAspire.McpServer/McpServer.cs b/src/BuildWithAspire.McpServer/McpServer.cs
new file mode 100644
index 0000000..0b10a95
--- /dev/null
+++ b/src/BuildWithAspire.McpServer/McpServer.cs
@@ -0,0 +1,59 @@
+using BuildWithAspire.McpServer.Tools;
+
+namespace BuildWithAspire.McpServer;
+
+///
+/// MCP tool call request structure.
+///
+public class McpToolCallRequest
+{
+ public string Name { get; set; } = string.Empty;
+ public Dictionary Arguments { get; set; } = new();
+}
+
+///
+/// MCP Server that manages tool calls.
+///
+public class McpServer
+{
+ private readonly Dictionary, CancellationToken, Task>> _tools;
+ private readonly Dictionary _toolMetadata;
+
+ public McpServer(WeatherTool weatherTool, TimeTool timeTool)
+ {
+ _tools = new Dictionary, CancellationToken, Task>>
+ {
+ [weatherTool.Name] = weatherTool.ExecuteAsync,
+ [timeTool.Name] = timeTool.ExecuteAsync
+ };
+
+ _toolMetadata = new Dictionary
+ {
+ [weatherTool.Name] = (weatherTool.Description, weatherTool.GetParametersSchema()),
+ [timeTool.Name] = (timeTool.Description, timeTool.GetParametersSchema())
+ };
+ }
+
+ public async Task CallToolAsync(McpToolCallRequest request)
+ {
+ if (!_tools.TryGetValue(request.Name, out var tool))
+ {
+ throw new InvalidOperationException($"Tool '{request.Name}' not found");
+ }
+
+ return await tool(request.Arguments, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ public object GetAvailableTools()
+ {
+ return new
+ {
+ tools = _toolMetadata.Select(kvp => new
+ {
+ name = kvp.Key,
+ description = kvp.Value.Description,
+ inputSchema = kvp.Value.Schema
+ }).ToList()
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/BuildWithAspire.McpServer/Program.cs b/src/BuildWithAspire.McpServer/Program.cs
new file mode 100644
index 0000000..055209f
--- /dev/null
+++ b/src/BuildWithAspire.McpServer/Program.cs
@@ -0,0 +1,52 @@
+using BuildWithAspire.McpServer;
+using BuildWithAspire.McpServer.Tools;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+// Add services to the container.
+builder.Services.AddOpenApi();
+
+// Add MCP Server
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+app.UseHttpsRedirection();
+
+app.MapGet("/", () => "MCP Server is running");
+
+// Add MCP endpoints
+app.MapPost("/mcp/tools/call", async (McpToolCallRequest request, McpServer mcpServer) =>
+{
+ try
+ {
+ var result = await mcpServer.CallToolAsync(request).ConfigureAwait(false);
+ return Results.Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return Results.Problem($"Tool call failed: {ex.Message}");
+ }
+});
+
+app.MapGet("/mcp/tools", (McpServer mcpServer) =>
+{
+ return Results.Ok(mcpServer.GetAvailableTools());
+});
+
+app.Run();
+
+// Make Program class accessible for testing
+public partial class Program { }
\ No newline at end of file
diff --git a/src/BuildWithAspire.McpServer/Tools/TimeTool.cs b/src/BuildWithAspire.McpServer/Tools/TimeTool.cs
new file mode 100644
index 0000000..996eb33
--- /dev/null
+++ b/src/BuildWithAspire.McpServer/Tools/TimeTool.cs
@@ -0,0 +1,110 @@
+namespace BuildWithAspire.McpServer.Tools;
+
+///
+/// Time tool for MCP that provides current date and time information.
+///
+public class TimeTool
+{
+ public string Name => "get_time";
+ public string Description => "Get current date and time information in various formats";
+
+ public object GetParametersSchema()
+ {
+ return new
+ {
+ type = "object",
+ properties = new
+ {
+ timezone = new
+ {
+ type = "string",
+ description = "Timezone name (e.g., 'UTC', 'America/New_York', 'Europe/London') - defaults to UTC",
+ @default = "UTC"
+ },
+ format = new
+ {
+ type = "string",
+ description = "Date/time format - 'iso', 'local', 'unix', or 'detailed'",
+ @default = "iso"
+ }
+ },
+ required = System.Array.Empty() // No required parameters
+ };
+ }
+
+ public async Task ExecuteAsync(Dictionary parameters, CancellationToken cancellationToken = default)
+ {
+ var timezone = parameters.GetValueOrDefault("timezone", "UTC")?.ToString() ?? "UTC";
+ var format = parameters.GetValueOrDefault("format", "iso")?.ToString() ?? "iso";
+
+ // Handle JsonElement properly for timezone
+ if (parameters.GetValueOrDefault("timezone") is System.Text.Json.JsonElement timezoneElement)
+ {
+ if (timezoneElement.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ timezone = timezoneElement.GetString() ?? "UTC";
+ }
+ }
+
+ // Handle JsonElement properly for format
+ if (parameters.GetValueOrDefault("format") is System.Text.Json.JsonElement formatElement)
+ {
+ if (formatElement.ValueKind == System.Text.Json.JsonValueKind.String)
+ {
+ format = formatElement.GetString() ?? "iso";
+ }
+ }
+
+ var utcNow = DateTime.UtcNow;
+ var localNow = DateTime.Now;
+
+ // Try to get timezone specific time
+ DateTime targetTime = utcNow;
+ string timezoneDisplay = timezone;
+
+ try
+ {
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timezone);
+ targetTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, timeZoneInfo);
+ timezoneDisplay = timeZoneInfo.DisplayName;
+ }
+ catch
+ {
+ // Fallback to UTC if timezone not found
+ timezone = "UTC";
+ timezoneDisplay = "Coordinated Universal Time";
+ }
+
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false); // Simulate some processing time
+
+ var result = new
+ {
+ timezone = timezone,
+ timezone_display = timezoneDisplay,
+ format_requested = format,
+ current_time = format.ToLowerInvariant() switch
+ {
+ "iso" => (object)targetTime.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture),
+ "local" => (object)targetTime.ToString("F", System.Globalization.CultureInfo.InvariantCulture),
+ "unix" => (object)((DateTimeOffset)targetTime).ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture),
+ "detailed" => (object)new
+ {
+ iso = targetTime.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture),
+ local = targetTime.ToString("F", System.Globalization.CultureInfo.InvariantCulture),
+ unix_timestamp = ((DateTimeOffset)targetTime).ToUnixTimeSeconds(),
+ day_of_week = targetTime.DayOfWeek.ToString(),
+ day_of_year = targetTime.DayOfYear,
+ week_of_year = System.Globalization.CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(
+ targetTime,
+ System.Globalization.CalendarWeekRule.FirstDay,
+ DayOfWeek.Sunday)
+ },
+ _ => (object)targetTime.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture)
+ },
+ utc_time = utcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture),
+ generated_at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture)
+ };
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/BuildWithAspire.McpServer/Tools/WeatherTool.cs b/src/BuildWithAspire.McpServer/Tools/WeatherTool.cs
new file mode 100644
index 0000000..ef9690b
--- /dev/null
+++ b/src/BuildWithAspire.McpServer/Tools/WeatherTool.cs
@@ -0,0 +1,94 @@
+using System.ComponentModel;
+using System.Text.Json;
+
+namespace BuildWithAspire.McpServer.Tools;
+
+///
+/// Weather tool for MCP that provides weather forecasts.
+///
+public class WeatherTool
+{
+ public string Name => "get_weather";
+ public string Description => "Get weather forecast for a specified location and number of days";
+
+ public object GetParametersSchema()
+ {
+ return new
+ {
+ type = "object",
+ properties = new
+ {
+ location = new
+ {
+ type = "string",
+ description = "The location to get weather for (city, country)"
+ },
+ days = new
+ {
+ type = "integer",
+ description = "Number of days to forecast (1-7, default: 5)",
+ minimum = 1,
+ maximum = 7,
+ @default = 5
+ }
+ },
+ required = new[] { "location" }
+ };
+ }
+
+ public async Task ExecuteAsync(Dictionary parameters, CancellationToken cancellationToken = default)
+ {
+ var location = parameters.GetValueOrDefault("location", "Unknown Location")?.ToString() ?? "Unknown Location";
+
+ // Handle JsonElement properly
+ var daysObj = parameters.GetValueOrDefault("days", 5);
+ int days = 5; // default
+
+ if (daysObj is System.Text.Json.JsonElement jsonElement)
+ {
+ if (jsonElement.ValueKind == System.Text.Json.JsonValueKind.Number)
+ {
+ days = jsonElement.GetInt32();
+ }
+ }
+ else
+ {
+ days = Convert.ToInt32(daysObj, System.Globalization.CultureInfo.InvariantCulture);
+ }
+
+ // Clamp days to valid range
+ days = Math.Max(1, Math.Min(7, days));
+
+ var forecasts = new List();
+ var random = new Random();
+ var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
+
+ for (int i = 0; i < days; i++)
+ {
+ var date = DateOnly.FromDateTime(DateTime.Now.AddDays(i + 1));
+ var tempC = random.Next(-20, 55);
+ var tempF = 32 + (int)(tempC / 0.5556);
+ var summary = summaries[random.Next(summaries.Length)];
+
+ forecasts.Add(new
+ {
+ date = date.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture),
+ location = location,
+ temperatureC = tempC,
+ temperatureF = tempF,
+ summary = summary,
+ description = $"{summary} weather expected in {location} with temperature around {tempC}°C ({tempF}°F)"
+ });
+ }
+
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false); // Simulate some processing time
+
+ return new
+ {
+ location = location,
+ forecast_days = days,
+ forecasts = forecasts,
+ generated_at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture)
+ };
+ }
+}
\ No newline at end of file
diff --git a/tests/BuildWithAspire.McpServer.UnitTests/BuildWithAspire.McpServer.UnitTests.csproj b/tests/BuildWithAspire.McpServer.UnitTests/BuildWithAspire.McpServer.UnitTests.csproj
new file mode 100644
index 0000000..b1e24bb
--- /dev/null
+++ b/tests/BuildWithAspire.McpServer.UnitTests/BuildWithAspire.McpServer.UnitTests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/BuildWithAspire.McpServer.UnitTests/McpServerTests.cs b/tests/BuildWithAspire.McpServer.UnitTests/McpServerTests.cs
new file mode 100644
index 0000000..42137cf
--- /dev/null
+++ b/tests/BuildWithAspire.McpServer.UnitTests/McpServerTests.cs
@@ -0,0 +1,319 @@
+using BuildWithAspire.McpServer.Tools;
+using System.Text.Json;
+using Xunit;
+
+namespace BuildWithAspire.McpServer.UnitTests;
+
+public class WeatherToolTests
+{
+ private readonly WeatherTool _weatherTool;
+
+ public WeatherToolTests()
+ {
+ _weatherTool = new WeatherTool();
+ }
+
+ [Fact]
+ public void Name_ShouldReturnExpectedValue()
+ {
+ // Act
+ var name = _weatherTool.Name;
+
+ // Assert
+ Assert.Equal("get_weather", name);
+ }
+
+ [Fact]
+ public void Description_ShouldReturnExpectedValue()
+ {
+ // Act
+ var description = _weatherTool.Description;
+
+ // Assert
+ Assert.Equal("Get weather forecast for a specified location and number of days", description);
+ }
+
+ [Fact]
+ public void GetParametersSchema_ShouldReturnValidSchema()
+ {
+ // Act
+ var schema = _weatherTool.GetParametersSchema();
+
+ // Assert
+ Assert.NotNull(schema);
+
+ var json = JsonSerializer.Serialize(schema);
+ Assert.Contains("location", json);
+ Assert.Contains("days", json);
+ Assert.Contains("required", json);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithValidParameters_ShouldReturnWeatherData()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["location"] = "New York",
+ ["days"] = 3
+ };
+
+ // Act
+ var result = await _weatherTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("New York", json);
+ Assert.Contains("forecasts", json);
+ Assert.Contains("location", json);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithJsonElements_ShouldHandleCorrectly()
+ {
+ // Arrange - Simulate JSON deserialized parameters
+ var daysElement = JsonSerializer.Deserialize("2");
+ var parameters = new Dictionary
+ {
+ ["location"] = "Paris",
+ ["days"] = daysElement
+ };
+
+ // Act
+ var result = await _weatherTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("Paris", json);
+ Assert.Contains("forecast_days", json);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithDefaultDays_ShouldUse5Days()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["location"] = "London"
+ };
+
+ // Act
+ var result = await _weatherTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var resultObj = JsonSerializer.Deserialize(JsonSerializer.Serialize(result));
+ var forecastDays = resultObj.GetProperty("forecast_days").GetInt32();
+ Assert.Equal(5, forecastDays);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithInvalidDaysRange_ShouldClampToValidRange()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["location"] = "Tokyo",
+ ["days"] = 10 // Should be clamped to 7
+ };
+
+ // Act
+ var result = await _weatherTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var resultObj = JsonSerializer.Deserialize(JsonSerializer.Serialize(result));
+ var forecastDays = resultObj.GetProperty("forecast_days").GetInt32();
+ Assert.Equal(7, forecastDays); // Should be clamped to max 7
+ }
+}
+
+public class TimeToolTests
+{
+ private readonly TimeTool _timeTool;
+
+ public TimeToolTests()
+ {
+ _timeTool = new TimeTool();
+ }
+
+ [Fact]
+ public void Name_ShouldReturnExpectedValue()
+ {
+ // Act
+ var name = _timeTool.Name;
+
+ // Assert
+ Assert.Equal("get_time", name);
+ }
+
+ [Fact]
+ public void Description_ShouldReturnExpectedValue()
+ {
+ // Act
+ var description = _timeTool.Description;
+
+ // Assert
+ Assert.Equal("Get current date and time information in various formats", description);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithUTCTimezone_ShouldReturnUTCTime()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["timezone"] = "UTC",
+ ["format"] = "iso"
+ };
+
+ // Act
+ var result = await _timeTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("UTC", json);
+ Assert.Contains("current_time", json);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithDetailedFormat_ShouldReturnDetailedInfo()
+ {
+ // Arrange
+ var parameters = new Dictionary
+ {
+ ["timezone"] = "UTC",
+ ["format"] = "detailed"
+ };
+
+ // Act
+ var result = await _timeTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("day_of_week", json);
+ Assert.Contains("unix_timestamp", json);
+ Assert.Contains("week_of_year", json);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithNoParameters_ShouldUseDefaults()
+ {
+ // Arrange
+ var parameters = new Dictionary();
+
+ // Act
+ var result = await _timeTool.ExecuteAsync(parameters);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var resultObj = JsonSerializer.Deserialize(JsonSerializer.Serialize(result));
+ var timezone = resultObj.GetProperty("timezone").GetString();
+ Assert.Equal("UTC", timezone);
+ }
+}
+
+public class McpServerTests
+{
+ private readonly McpServer _mcpServer;
+ private readonly WeatherTool _weatherTool;
+ private readonly TimeTool _timeTool;
+
+ public McpServerTests()
+ {
+ _weatherTool = new WeatherTool();
+ _timeTool = new TimeTool();
+ _mcpServer = new McpServer(_weatherTool, _timeTool);
+ }
+
+ [Fact]
+ public void GetAvailableTools_ShouldReturnAllTools()
+ {
+ // Act
+ var result = _mcpServer.GetAvailableTools();
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("get_weather", json);
+ Assert.Contains("get_time", json);
+ Assert.Contains("tools", json);
+ }
+
+ [Fact]
+ public async Task CallToolAsync_WithValidWeatherRequest_ShouldReturnWeatherData()
+ {
+ // Arrange
+ var request = new McpToolCallRequest
+ {
+ Name = "get_weather",
+ Arguments = new Dictionary
+ {
+ ["location"] = "Berlin",
+ ["days"] = 2
+ }
+ };
+
+ // Act
+ var result = await _mcpServer.CallToolAsync(request);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("Berlin", json);
+ }
+
+ [Fact]
+ public async Task CallToolAsync_WithValidTimeRequest_ShouldReturnTimeData()
+ {
+ // Arrange
+ var request = new McpToolCallRequest
+ {
+ Name = "get_time",
+ Arguments = new Dictionary
+ {
+ ["timezone"] = "UTC",
+ ["format"] = "iso"
+ }
+ };
+
+ // Act
+ var result = await _mcpServer.CallToolAsync(request);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var json = JsonSerializer.Serialize(result);
+ Assert.Contains("UTC", json);
+ }
+
+ [Fact]
+ public async Task CallToolAsync_WithInvalidToolName_ShouldThrowException()
+ {
+ // Arrange
+ var request = new McpToolCallRequest
+ {
+ Name = "invalid_tool",
+ Arguments = new Dictionary()
+ };
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ () => _mcpServer.CallToolAsync(request));
+
+ Assert.Contains("Tool 'invalid_tool' not found", exception.Message);
+ }
+}
\ No newline at end of file