Lab 021: Build an MCP Server in CΒΆ
What You'll LearnΒΆ
- Create an MCP server using the official ModelContextProtocol .NET SDK
- Expose tools, resources, and prompts from C#
- Test the server with MCP Inspector
- Connect it to GitHub Copilot Agent Mode via
mcp.json
IntroductionΒΆ
Python is great for rapid MCP prototyping, but .NET is common in enterprise environments. The official ModelContextProtocol NuGet package makes building MCP servers in C# first-class.
PrerequisitesΒΆ
- .NET 8 SDK or later β free
- Lab 012: What is MCP? recommended
- Node.js (for MCP Inspector) β free
π¦ Supporting FilesΒΆ
Download these files before starting the lab
Save all files to a lab-021/ folder in your working directory.
| File | Description | Download |
|---|---|---|
BrokenMcpServer.cs |
Bug-fix exercise (3 bugs + self-tests) | π₯ Download |
Lab ExerciseΒΆ
Step 1: Create the projectΒΆ
mkdir mcp-csharp-demo && cd mcp-csharp-demo
dotnet new console -o ProductServer
cd ProductServer
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
Step 2: Build the MCP serverΒΆ
Replace Program.cs with:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<ProductTools>();
await builder.Build().RunAsync();
Create ProductTools.cs:
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Text.Json;
[McpServerToolType]
public class ProductTools
{
private static readonly List<Product> _products = new()
{
new("P001", "TrailBlazer X200", "footwear", 189.99m, true),
new("P002", "Summit Pro Tent", "camping", 349.00m, true),
new("P003", "HydroFlow Bottle", "hydration", 34.99m, false),
new("P004", "ClimbTech Harness","climbing", 129.99m, true),
};
[McpServerTool, Description("Search products by name or category keyword.")]
public static string SearchProducts(
[Description("Keyword to search in product name or category")] string query)
{
var q = query.ToLowerInvariant();
var matches = _products
.Where(p => p.Name.Contains(q, StringComparison.OrdinalIgnoreCase)
|| p.Category.Contains(q, StringComparison.OrdinalIgnoreCase))
.ToList();
return matches.Count == 0
? "No products found."
: JsonSerializer.Serialize(matches);
}
[McpServerTool, Description("Get details for a specific product by ID.")]
public static string GetProduct(
[Description("Product ID, e.g. P001")] string productId)
{
var product = _products.FirstOrDefault(p =>
p.Id.Equals(productId, StringComparison.OrdinalIgnoreCase));
return product is null
? $"Product '{productId}' not found."
: JsonSerializer.Serialize(product);
}
[McpServerTool, Description("List all product categories.")]
public static string ListCategories()
{
var categories = _products.Select(p => p.Category).Distinct().OrderBy(c => c);
return string.Join(", ", categories);
}
}
public record Product(string Id, string Name, string Category, decimal Price, bool InStock);
Step 3: Run and test with MCP InspectorΒΆ
Terminal 1 β build the server:
Test with MCP Inspector:
In Inspector, click Tools and test search_products with query "camping". You should see the tent returned.
Step 4: Add a ResourceΒΆ
Resources expose read-only data (files, database views, etc.). Add to ProductTools.cs:
[McpServerResourceType]
public class ProductResources
{
[McpServerResource(UriTemplate = "products://catalog", Name = "Full Catalog",
Description = "Complete product catalog as JSON", MimeType = "application/json")]
public static string GetCatalog()
{
return JsonSerializer.Serialize(_products, new JsonSerializerOptions { WriteIndented = true });
}
}
Update Program.cs to register resources:
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<ProductTools>()
.WithResources<ProductResources>(); // β add this
Step 5: Connect to GitHub CopilotΒΆ
Add to .vscode/mcp.json in your workspace:
{
"servers": {
"product-server-csharp": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "--project", "/path/to/ProductServer"]
}
}
}
Enable Agent Mode in VS Code, then ask: "What camping products are in stock?"
Key Differences vs Python SDKΒΆ
| Python | C# | |
|---|---|---|
| Decorator | @mcp.tool() |
[McpServerTool] |
| Description | docstring | [Description("...")] |
| Resources | @mcp.resource() |
[McpServerResource(...)] |
| Transport | mcp.run(transport="stdio") |
.WithStdioServerTransport() |
| DI container | β | Microsoft.Extensions.Hosting |
π Bug-Fix Exercise: Fix the Broken MCP ServerΒΆ
This lab includes a deliberately broken C# MCP server file. Your challenge: find and fix 3 bugs.
Setup:
mkdir mcp-bugfix && cd mcp-bugfix
dotnet new console -o BugFixServer
cd BugFixServer
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
# Copy the broken file over Program.cs
cp ../lab-021/BrokenMcpServer.cs Program.cs
dotnet run
The 3 bugs:
| # | Tool | Symptom | Type |
|---|---|---|---|
| 1 | list_categories |
NullReferenceException on startup |
Null initialization |
| 2 | search_products |
Always returns empty list [] |
Logic inversion (!) |
| 3 | get_product_details |
Returns "not found" for lowercase IDs | Case-sensitive comparison |
Verify your fixes: After fixing all 3 bugs, connect with the MCP Inspector and run:
list_categories()β should return["Backpacks", "Sleeping Bags", "Tents"]search_products(keyword: "tent")β should return P001, P002, P003get_product_details(productId: "p001")β should return TrailBlazer Tent 2P details
π§ Knowledge CheckΒΆ
Q1 (Run the Lab): After fixing all 3 bugs and calling list_categories(), what does the tool return? List the categories in the order they appear in the output.
Fix the bugs, start the server, connect with MCP Inspector, and call list_categories().
β Reveal Answer
["Backpacks", "Sleeping Bags", "Tents"]
The categories are returned in alphabetical order because the original code uses a sorted List<string>. Bug #1 (categories = null) caused a NullReferenceException before returning anything β fixing it reveals the properly sorted list.
Q2 (Run the Lab): After fixing bug #3 (the case-sensitive comparison bug), what StringComparison value replaces StringComparison.Ordinal in the fix?
Read the bug #3 description carefully, then look at the fix you applied in π₯ BrokenMcpServer.cs.
β Reveal Answer
StringComparison.OrdinalIgnoreCase
The original code used StringComparison.Ordinal which is case-sensitive, so get_product_details("p001") failed because the stored IDs are uppercase ("P001"). Replacing it with OrdinalIgnoreCase makes ID lookups work regardless of the case the client sends.
Q3 (Multiple Choice): Bug #2 in search_products caused it to always return an empty list. What was the root cause?
- A) The keyword parameter was null
- B) The
Contains()call was inverted with!β it filtered OUT matches instead of keeping them - C) The product list was not initialized
- D) The search was case-sensitive and no products matched
β Reveal Answer
Correct: B β Logic inversion
The code had !product.Name.Contains(keyword) β the ! negated the condition, so products that DID contain the keyword were excluded, and products that did NOT contain the keyword were returned. With an empty results list, there were no non-matching products either. Removing the ! fixes the logic.
Next StepsΒΆ
- Deploy this server to the cloud: β Lab 028 β Deploy MCP to Azure Container Apps
- Python version of MCP server: β Lab 020 β MCP Server in Python