MCP - From Development to Azure Governance Series: Part 1 - Exploring MCP Servers with .NET

• MCP, .NET, Servers, Azure, Development, Governance • 11 min read

Welcome to the start of a hands-on series on MCP development and governance. Throughout these articles, we’ll explore how to build, deploy, and manage MCP servers using .NET and Azure. In this first part, you’ll learn how to create an MCP server with tools in .NET—laying the groundwork for future Azure deployment and governance.

Introduction to MCP Servers

MCP (Model Context Protocol)1 enables AI developers to extend the capabilities of AI models by integrating them with external servers. These servers can be hosted on various platforms, including cloud services like Azure.

While the basics of MCP have been widely covered in technical articles and documentation, here’s a concise overview. For more in-depth information, check the official documentation.

Depending on your use case, MCP servers can use several transport mechanisms:

  • STDIO (Standard Input/Output): Communication occurs via standard input and output streams, commonly used for local or CLI-based integrations.
  • Streamable HTTP Responses: Allows clients to receive data incrementally as it becomes available, improving responsiveness for large or long-running operations.
  • SSE (Server-Sent Events): Enables servers to push real-time updates to clients over HTTP, ideal for streaming data or events.
mcp-transport-protocols-azure

Note: Sometimes, API endpoints are organized by transport type, such as /mcp for standard HTTP or /sse for Server-Sent Events. These URL paths are conventions to help clients select the appropriate transport, but the underlying protocol (streamable, SSE, etc.) is determined by the server’s implementation and response handling, not by the URL alone.


Setting Up an MCP Server with .NET

In this section, we’ll create an MCP server step by step. By following these instructions, you’ll build a simple restaurant picker called LunchTimeMCP using .NET as your foundation.

1. Create the Console App

First, create a new folder and initialize a console project:

1
2
3
mkdir LunchTime
cd LunchTime
dotnet new console -n LunchTimeMCP

2. Install Required NuGet Packages

Add the essential packages for MCP server development:

1
2
dotnet add package Microsoft.Extensions.Hosting --version 9.0.6
dotnet add package ModelContextProtocol --version 0.3.0-preview.1

3. Program.cs Setup

Configure the host builder, dependency injection, and MCP server transport in Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport();

await builder.Build().RunAsync();

The MCP library handles the server setup and protocol implementation. You can register your own services and tools as needed.

4. Implement the RestaurantService

Create a new file named RestaurantService.cs in the LunchTimeMCP project. This service manages restaurant data, visit tracking, and statistics.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// RestaurantService.cs
using System.Text.Json;
using System.Text.Json.Serialization;

namespace LunchTimeMCP;

public class RestaurantService
{
    private readonly string dataFilePath;
    private List<Restaurant> restaurants = new();
    private Dictionary<string, int> visitCounts = new();

    public RestaurantService()
    {
        // Store data in user's app data directory
        var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        var appDir = Path.Combine(appDataPath, "LunchTimeMCP");
        Directory.CreateDirectory(appDir);
        
        dataFilePath = Path.Combine(appDir, "restaurants.json");
        LoadData();
        
        // Initialize with trendy West Hollywood restaurants if empty
        if (restaurants.Count == 0)
        {
            InitializeWithTrendyRestaurants();
            SaveData();
        }
    }

    public async Task<List<Restaurant>> GetRestaurantsAsync()
    {
        return await Task.FromResult(restaurants.ToList());
    }

    public async Task<Restaurant> AddRestaurantAsync(string name, string location, string foodType)
    {
        var restaurant = new Restaurant
        {
            Id = Guid.NewGuid().ToString(),
            Name = name,
            Location = location,
            FoodType = foodType,
            DateAdded = DateTime.UtcNow
        };

        restaurants.Add(restaurant);
        SaveData();
        
        return await Task.FromResult(restaurant);
    }

    public async Task<Restaurant?> PickRandomRestaurantAsync()
    {
        if (restaurants.Count == 0)
            return null;

        var random = new Random();
        var selectedRestaurant = restaurants[random.Next(restaurants.Count)];
        
        // Track the visit
        if (visitCounts.ContainsKey(selectedRestaurant.Id))
            visitCounts[selectedRestaurant.Id]++;
        else
            visitCounts[selectedRestaurant.Id] = 1;

        SaveData();
        
        return await Task.FromResult(selectedRestaurant);
    }

    public async Task<Dictionary<string, RestaurantVisitInfo>> GetVisitStatsAsync()
    {
        var stats = new Dictionary<string, RestaurantVisitInfo>();
        
        foreach (var restaurant in restaurants)
        {
            var visitCount = visitCounts.GetValueOrDefault(restaurant.Id, 0);
            stats[restaurant.Name] = new RestaurantVisitInfo
            {
                Restaurant = restaurant,
                VisitCount = visitCount,
                LastVisited = visitCount > 0 ? DateTime.UtcNow : null // In a real app, you'd track actual visit dates
            };
        }

        return await Task.FromResult(stats);
    }

    public async Task<FormattedRestaurantStats> GetFormattedVisitStatsAsync()
    {
        var stats = await GetVisitStatsAsync();
        
        var formattedStats = stats.Values
            .OrderByDescending(x => x.VisitCount)
            .Select(stat => new FormattedRestaurantStat
            {
                Restaurant = stat.Restaurant.Name,
                Location = stat.Restaurant.Location,
                FoodType = stat.Restaurant.FoodType,
                VisitCount = stat.VisitCount,
                TimesEaten = stat.VisitCount == 0 ? "Never" : 
                            stat.VisitCount == 1 ? "Once" : 
                            $"{stat.VisitCount} times"
            })
            .ToList();

        return new FormattedRestaurantStats
        {
            Message = "Restaurant visit statistics:",
            Statistics = formattedStats,
            TotalRestaurants = stats.Count,
            TotalVisits = stats.Values.Sum(x => x.VisitCount)
        };
    }

    private void LoadData()
    {
        if (!File.Exists(dataFilePath))
            return;

        try
        {
            var json = File.ReadAllText(dataFilePath);
            var data = JsonSerializer.Deserialize<RestaurantData>(json, RestaurantContext.Default.RestaurantData);
            
            if (data != null)
            {
                restaurants = data.Restaurants ?? new List<Restaurant>();
                visitCounts = data.VisitCounts ?? new Dictionary<string, int>();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error loading data: {ex.Message}");
        }
    }

    private void SaveData()
    {
        try
        {
            var data = new RestaurantData
            {
                Restaurants = restaurants,
                VisitCounts = visitCounts
            };
            
            var json = JsonSerializer.Serialize(data, RestaurantContext.Default.RestaurantData);
            File.WriteAllText(dataFilePath, json);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error saving data: {ex.Message}");
        }
    }

    private void InitializeWithTrendyRestaurants()
    {
        var trendyRestaurants = new List<Restaurant>
        {
            new() { Id = Guid.NewGuid().ToString(), Name = "Guelaguetza", Location = "3014 W Olympic Blvd", FoodType = "Oaxacan Mexican", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Republique", Location = "624 S La Brea Ave", FoodType = "French Bistro", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Night + Market WeHo", Location = "9041 Sunset Blvd", FoodType = "Thai Street Food", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Gracias Madre", Location = "8905 Melrose Ave", FoodType = "Vegan Mexican", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "The Ivy", Location = "113 N Robertson Blvd", FoodType = "Californian", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Catch LA", Location = "8715 Melrose Ave", FoodType = "Seafood", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Cecconi's", Location = "8764 Melrose Ave", FoodType = "Italian", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Earls Kitchen + Bar", Location = "8730 W Sunset Blvd", FoodType = "Global Comfort Food", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Pump Restaurant", Location = "8948 Santa Monica Blvd", FoodType = "Mediterranean", DateAdded = DateTime.UtcNow },
            new() { Id = Guid.NewGuid().ToString(), Name = "Craig's", Location = "8826 Melrose Ave", FoodType = "American Contemporary", DateAdded = DateTime.UtcNow }
        };

        restaurants.AddRange(trendyRestaurants);
    }
}

public partial class Restaurant
{
    public string Id { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string Location { get; set; } = string.Empty;
    public string FoodType { get; set; } = string.Empty;
    public DateTime DateAdded { get; set; }
}

public partial class RestaurantVisitInfo
{
    public Restaurant Restaurant { get; set; } = new();
    public int VisitCount { get; set; }
    public DateTime? LastVisited { get; set; }
}

public partial class RestaurantData
{
    public List<Restaurant> Restaurants { get; set; } = new();
    public Dictionary<string, int> VisitCounts { get; set; } = new();
}

public class FormattedRestaurantStat
{
    public string Restaurant { get; set; } = string.Empty;
    public string Location { get; set; } = string.Empty;
    public string FoodType { get; set; } = string.Empty;
    public int VisitCount { get; set; }
    public string TimesEaten { get; set; } = string.Empty;
}

public class FormattedRestaurantStats
{
    public string Message { get; set; } = string.Empty;
    public List<FormattedRestaurantStat> Statistics { get; set; } = new();
    public int TotalRestaurants { get; set; }
    public int TotalVisits { get; set; }
}

[JsonSerializable(typeof(List<Restaurant>))]
[JsonSerializable(typeof(Restaurant))]
[JsonSerializable(typeof(RestaurantVisitInfo))]
[JsonSerializable(typeof(Dictionary<string, RestaurantVisitInfo>))]
[JsonSerializable(typeof(RestaurantData))]
[JsonSerializable(typeof(FormattedRestaurantStat))]
[JsonSerializable(typeof(FormattedRestaurantStats))]
internal sealed partial class RestaurantContext : JsonSerializerContext
{
}

The [JsonSerializable(...)] attributes and the RestaurantContext class enable source generation for System.Text.Json in .NET. This is especially valuable for high-performance applications, AOT deployments, or when working with APIs and tools that frequently serialize/deserialize objects.

5. Implementing MCP Tools

Next, expose your restaurant functionality to AI assistants by implementing MCP tools in RestaurantTools.cs.

RestaurantTools.cs File Structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// RestaurantTools.cs
using System.ComponentModel;
using System.Text.Json;
using ModelContextProtocol.Server;

namespace LunchTimeMCP;

[McpServerToolType]
public sealed class RestaurantTools
{
    private readonly RestaurantService restaurantService;

    public RestaurantTools(RestaurantService restaurantService)
    {
        this.restaurantService = restaurantService;
    }

    // All tools implemented here...
}
  • [McpServerToolType] marks the class as containing MCP tools.
  • Constructor injection provides access to RestaurantService.

Tool Implementation Overview

Each tool follows the same pattern:

  • Use [McpServerTool] to register with MCP.
  • Add descriptive [Description] attributes for the AI to understand the tool’s purpose.
  • Use dependency injection to access the RestaurantService.
  • Return JSON-serialized responses using the RestaurantContext.

Tool 1: Get All Restaurants

1
2
3
4
5
6
[McpServerTool, Description("Get a list of all restaurants available for lunch.")]
public async Task<string> GetRestaurants()
{
    var restaurants = await restaurantService.GetRestaurantsAsync();
    return JsonSerializer.Serialize(restaurants, RestaurantContext.Default.ListRestaurant);
}
  • Simple method with no parameters.
  • Returns JSON-serialized list of restaurants.

Tool 2: Add New Restaurant

1
2
3
4
5
6
7
8
9
[McpServerTool, Description("Add a new restaurant to the lunch options.")]
public async Task<string> AddRestaurant(
    [Description("The name of the restaurant")] string name,
    [Description("The location/address of the restaurant")] string location,
    [Description("The type of food served (e.g., Italian, Mexican, Thai, etc.)")] string foodType)
{
    var restaurant = await restaurantService.AddRestaurantAsync(name, location, foodType);
    return JsonSerializer.Serialize(restaurant, RestaurantContext.Default.Restaurant);
}
  • Each parameter has a descriptive [Description] attribute.
  • Returns the newly created restaurant object as JSON.

Tool 3: Pick Random Restaurant

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[McpServerTool, Description("Pick a random restaurant from the available options for lunch.")]
public async Task<string> PickRandomRestaurant()
{
    var selectedRestaurant = await restaurantService.PickRandomRestaurantAsync();
    
    if (selectedRestaurant == null)
    {
        return JsonSerializer.Serialize(new { 
            message = "No restaurants available. Please add some restaurants first!" 
        });
    }

    return JsonSerializer.Serialize(new { 
        message = $"🍽️ Time for lunch at {selectedRestaurant.Name}!",
        restaurant = selectedRestaurant 
    });
}
  • Handles the case when no restaurants are available.
  • Returns a friendly message with emoji and the selected restaurant data.

Tool 4: Get Visit Statistics

1
2
3
4
5
6
[McpServerTool, Description("Get statistics about how many times each restaurant has been visited.")]
public async Task<string> GetVisitStatistics()
{
    var formattedStats = await restaurantService.GetFormattedVisitStatsAsync();
    return JsonSerializer.Serialize(formattedStats, RestaurantContext.Default.FormattedRestaurantStats);
}
  • Returns formatted statistics about restaurant visits.

With these tools, your MCP server exposes a complete set of restaurant management operations to AI assistants.


6. Register Tools & Services in Program.cs

Finally, register your tools and the service in Program.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
using LunchTimeMCP;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithTools<RestaurantTools>();

builder.Services.AddSingleton<RestaurantService>();

await builder.Build().RunAsync();
  • AddSingleton<RestaurantService>() registers your service for dependency injection.
  • .WithTools<RestaurantTools>() makes your tools available to the MCP server.

7. Testing Your MCP Server

You can test your LunchTimeMCP server locally using the MCP Inspector tool.

Step 1: Navigate to Your Project

1
cd LunchTimeMCP

Step 2: Build and Test Your Server

1
dotnet build

Step 3: Install and Run MCP Inspector

Install MCP Inspector globally:

1
npm install -g @modelcontextprotocol/inspector

Or run it directly with npx:

1
npx @modelcontextprotocol/inspector dotnet run

When you run the inspector, you’ll receive a URL with a pre-filled token to open in your browser.

Step 4: Connect to Your MCP Server

In the MCP Inspector web interface:

  • Transport Type: Select “STDIO”
  • Command: dotnet
  • Arguments: run
  • Click Connect to establish the connection.
  • Click List Tools.

You should see your four tools appear in the inspector:

  • GetRestaurants
  • AddRestaurant
  • PickRandomRestaurant
  • GetVisitStatistics

Step 5: Test Each Tool

  • GetRestaurants: Returns the 10 seeded restaurants.
  • AddRestaurant: Add a new restaurant with name, location, and food type.
  • PickRandomRestaurant: Selects and tracks a random restaurant.
  • GetVisitStatistics: Shows access counts for visited restaurants.
mcp-inspector

Your LunchTimeMCP server is now ready for local testing and further development. In the next part of this series, we’ll explore how to build and deploy MCP servers on Azure.


  1. Microsoft Learn MCP Server overview Microsoft Learn MCP Server overview ↩︎

Comments & Discussion

Join the conversation! Share your thoughts and connect with other readers.