Overview
This article demonstrates how to build a basic MCP server and client using ASP.NET and Server Sent Events (SSE). The MCP server exposes tools that can be discovered and used by LLMs, while the client application connects these tools to an AI service. GitHub AI models are used on the client application.
Server Sent Events (SSE)
SSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication. This approach facilitates communication between frontend microservices and backend business contexts through the MCP server.
Prerequisites
- Docker Desktop
- GitHub account
- Visual Studio Code
- GitHub Copilot Chat Extension
- .NET 9.0 (or later)
Setup
We will create an .NET solution comprising of an ASP.NET web server project; a WebAPI client project; and add the required packages with these terminal window commands:
mkdir AspMCP
cd AspMCP
dotnet new sln
dotnet new web -n ServerMCP
dotnet sln add ./ServerMCP/ServerMCP.csproj
cd ServerMCP
dotnet add package Azure.AI.OpenAI
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease
dotnet add package ModelContextProtocol --prerelease
dotnet add package ModelContextProtocol.AspNetCore --prerelease
cd ..
dotnet new webapi --use-controllers -n ClientMCP
dotnet sln add ./ClientMCP/ClientMCP.csproj
cd ClientMCP
dotnet add package Azure.AI.OpenAI
dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease
dotnet add package ModelContextProtocol --prerelease
dotnet add package Swashbuckle.AspNetCore
cd ..
Open the solution in VS Code. To do that, you can enter this command in a terminal a terminal window:
code .
Build ASP.NET Server
In the ServerMCP project, add this service to Program.cs before “var app = builder.Build();”:
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
In the same server Program.cs file, add this code just before “app.Run();”:
// Add MCP middleware
app.MapMcp();
Delete (or comment out) this code in the Program.cs file:
In the server project create a folder named McpTools folder and add to it a GreetingTool class with this code:
[McpServerToolType]
public sealed class GreetingTool {
public GreetingTool() { }
[McpServerTool, Description("Says Hello to a user")]
public static string Echo(string username) {
return "Hello " + username;
}
}
Test Server
In a terminal window inside the server project, run this command to start the server:
dotnet watch
Point your browser to https://localhost:????/sse.
Note : replace ???? with your port number.
This is what you will see:
Build Client
In the client project......
1) Add these settings to the appsettings.json file.
"AI": {
"ModelName": "gpt-4o-mini",
"Endpoint": "https://models.inference.ai.azure.com",
"ApiKey": "PUT-GITHUB-TOKEN-HERE",
"MCPServiceUri": "http://localhost:5053/sse"
}
2) Register Services via Dependency Injection. Add the following to your client Program.cs:NOTE: adjust values for ApiKey and MCPServiceUri accordingly.
var endpoint = builder.Configuration["AI:Endpoint"];
var apiKey = builder.Configuration["AI:ApiKey"];
var model = builder.Configuration["AI:ModelName"];
builder.Services.AddChatClient(services =>
new ChatClientBuilder(
(
!string.IsNullOrEmpty(apiKey)
? new AzureOpenAIClient(new Uri(endpoint!), new AzureKeyCredential(apiKey))
: new AzureOpenAIClient(new Uri(endpoint!), new DefaultAzureCredential())
).GetChatClient("gpt-4o").AsIChatClient()
)
.UseFunctionInvocation()
.Build());
app.UseSwaggerUI(options => {
options.SwaggerEndpoint("/openapi/v1.json", "MCP Server");
options.RoutePrefix = "";
});
4) Edit Properties/launchSettings.json, change launchBrowser to true under http and https. This is so that the app automatically launches in a browser when you run the project with “dotnet watch”.
In the Controllers folder, create a ChatController class with the following code:
[ApiController]
[Route("[controller]")]
public class ChatController : ControllerBase {
private readonly ILogger<ChatController> _logger;
private readonly IChatClient _chatClient;
private readonly IConfiguration? _configuration;
public ChatController(
ILogger<ChatController> logger,
IChatClient chatClient,
IConfiguration configuration
) {
_logger = logger;
_chatClient = chatClient;
_configuration = configuration;
}
[HttpPost(Name = "Chat")]
public async Task<string> Chat([FromBody] string message) {
// Create MCP client connecting to our MCP server
var mcpClient = await McpClientFactory.CreateAsync(
new SseClientTransport(
new SseClientTransportOptions {
Endpoint = new Uri(_configuration?["AI:MCPServiceUri"] ?? throw new InvalidOperationException("MCPServiceUri is not configured"))
}
)
);
// Get available tools from the MCP server
var tools = await mcpClient.ListToolsAsync();
// Set up the chat messages
var messages = new List<ChatMessage> {
new ChatMessage(ChatRole.System, "You are a helpful assistant.")
};
messages.Add(new(ChatRole.User, message));
// Get streaming response and collect updates
List<ChatResponseUpdate> updates = [];
StringBuilder result = new StringBuilder();
await foreach (var update in _chatClient.GetStreamingResponseAsync(
messages,
new() { Tools = [.. tools] }
)) {
result.Append(update);
updates.Add(update);
}
// Add the assistant's responses to the message history
messages.AddMessages(updates);
return result.ToString();
}
}
Test server and client
1) Start the server in a terminal window with: dotnet watch2) Start the client in another terminal window also with: dotnet watch
The following swagger page will load in your browser. Click on POST, then “Try it out”.3) string with your name, then click on Execute.
4) You will receive a response like below.
A more realistic solution
To develop a more realistic solution, we will add a database to the server project. Thereafter, our MCP client can query the database using natural language. This is very compelling for line-of-business applications.In the server project, make these changes.
1) Add these packages:
dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.Sqlite dotnet add package Microsoft.EntityFrameworkCore.SQLite.Design dotnet add package Microsoft.EntityFrameworkCore.Tools dotnet add package CsvHelper
2) Add this to appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "DataSource=beverages.sqlite;Cache=Shared"
}
3) Copy CSV data from this link. Then, save the content is a file named beverages.csv and put that file in a wwwroot folder in tour project.
4) In a Models folder, add a class named Beverage with this code:
public class Beverage {
[Required]
public int BeverageId { get; set; }
public string? Name { get; set; }
public string? Type { get; set; }
public string? MainIngredient { get; set; }
public string? Origin { get; set; }
public int? CaloriesPerServing { get; set; }
public void DisplayInfo() {
Console.WriteLine($"{Name} is a {Type} from {Origin} made with {MainIngredient}. It has {CaloriesPerServing} calories per serving.");
}
}
5) In a Data folder, add this ApplicationDbContext class:
public class ApplicationDbContext : DbContext {
public DbSet<Beverage> Beverages => Set<Beverage>();
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Beverage>().HasData(GetBeverages());
}
private static IEnumerable<Beverage> GetBeverages() {
string[] p = { Directory.GetCurrentDirectory(), "wwwroot", "beverages.csv" };
var csvFilePath = Path.Combine(p);
var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
Encoding = Encoding.UTF8,
PrepareHeaderForMatch = args => args.Header.ToLower(),
};
var data = new List<Beverage>().AsEnumerable();
using (var reader = new StreamReader(csvFilePath)) {
using (var csvReader = new CsvReader(reader, config)) {
data = csvReader.GetRecords<Beverage>().ToList();
}
}
return data;
}
}
6) Add this service to Program.cs:
var connStr = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(
option => option.UseSqlite(connStr)
);
7) In the same Program.cs file, add this code right before “app.Run();”:
// Apply database migrations on startup
using (var scope = app.Services.CreateScope()) {
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
}
8) Now, we can add and apply database migrations with:
dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update
At this stage, the beverages.sqlite database is populated with real data:
9) Let us add a class that queries the SQLite database. Inside of a Services folder, add a class named BeverageService with this content:
public class BeverageService(ApplicationDbContext db) {
public async Task<string> GetBeveragesJson() {
var beverages = await db.Beverages.ToListAsync();
return System.Text.Json.JsonSerializer.Serialize(beverages);
}
public async Task<string> GetBeverageByIdJson(int id) {
var beverage = await db.Beverages.FindAsync(id);
return System.Text.Json.JsonSerializer.Serialize(beverage);
}
public async Task<string> GetBeveragesContainingNameJson(string name) {
var beverages = await db.Beverages
.Where(b => b.Name!.Contains(name))
.ToListAsync();
return System.Text.Json.JsonSerializer.Serialize(beverages);
}
public async Task<string> GetBeveragesContainingTypeJson(string type) {
var beverages = await db.Beverages
.Where(b => b.Type!.Contains(type))
.ToListAsync();
return System.Text.Json.JsonSerializer.Serialize(beverages);
}
public async Task<string> GetBeveragesByIngredientJson(string ingredient) {
var beverages = await db.Beverages
.Where(b => b.MainIngredient!.Contains(ingredient))
.ToListAsync();
return System.Text.Json.JsonSerializer.Serialize(beverages);
}
public async Task<string> GetBeveragesByCaloriesLessThanOrEqualJson(int calories) {
var beverages = await db.Beverages
.Where(b => b.CaloriesPerServing <= calories)
.ToListAsync();
return System.Text.Json.JsonSerializer.Serialize(beverages);
}
public async Task<string> GetBeveragesByOriginJson(string origin) {
var beverages = await db.Beverages
.Where(b => b.Origin!.Contains(origin))
.ToListAsync();
return System.Text.Json.JsonSerializer.Serialize(beverages);
}
}
10) Next, expose MCP tooling that interacts with the SQLite data. In the McpTools folder, add a class named StudentTools with this code:
[McpServerToolType]
public static class StudentTools {
private static readonly StudentService _studentService = new StudentService();
[McpServerTool, Description("Get a list of students and return as JSON array")]
public static string GetStudentsJson() {
var task = _studentService.GetStudentsJson();
return task.GetAwaiter().GetResult();
}
[McpServerTool, Description("Get a student by name and return as JSON")]
public static string GetStudentJson([Description("The name of the student to get details for")] string name) {
var task = _studentService.GetStudentByFullName(name);
var student = task.GetAwaiter().GetResult();
if (student == null) {
return "Student not found";
}
return System.Text.Json.JsonSerializer.Serialize(student, StudentContext.Default.Student);
}
[McpServerTool, Description("Get a student by ID and return as JSON")]
public static string GetStudentByIdJson([Description("The ID of the student to get details for")] int id) {
var task = _studentService.GetStudentById(id);
var student = task.GetAwaiter().GetResult();
if (student == null) {
return "Student not found";
}
return System.Text.Json.JsonSerializer.Serialize(student, StudentContext.Default.Student);
}
[McpServerTool, Description("Get students by school and return as JSON")]
public static string GetStudentsBySchoolJson([Description("The name of the school to filter students by")] string school) {
var task = _studentService.GetStudentsBySchoolJson(school);
var students = task.GetAwaiter().GetResult();
return System.Text.Json.JsonSerializer.Serialize(students, StudentContext.Default.ListStudent);
}
[McpServerTool, Description("Get students by last name and return as JSON")]
public static string GetStudentsByLastNameJson([Description("The last name of the student to filter by")] string lastName) {
var task = _studentService.GetStudentsByLastName(lastName);
var students = task.GetAwaiter().GetResult();
return System.Text.Json.JsonSerializer.Serialize(students, StudentContext.Default.ListStudent);
}
[McpServerTool, Description("Get students by first name and return as JSON")]
public static string GetStudentsByFirstNameJson([Description("The first name of the student to filter by")] string firstName) {
var task = _studentService.GetStudentsByFirstName(firstName);
var students = task.GetAwaiter().GetResult();
return System.Text.Json.JsonSerializer.Serialize(students, StudentContext.Default.ListStudent);
}
[McpServerTool, Description("Get count of total students")]
public static int GetStudentCount() {
var task = _studentService.GetStudents();
var students = task.GetAwaiter().GetResult();
return students.Count;
}
}
You can now test the Student MCP service. Start the server project followed by the client project. In the client app, enter prompt: beverages high in calories.
Here is the output:
No comments:
Post a Comment