Skip to content

Lab 021: Build an MCP Server in CΒΆ

Level: L200 Path: MCP Time: ~45 min πŸ’° Cost: Free (local + Ollama)

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ΒΆ


πŸ“¦ 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:

dotnet build

Test with MCP Inspector:

npx @modelcontextprotocol/inspector dotnet run

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.

lab-021/
└── BrokenMcpServer.cs    ← 3 intentional bugs to find and fix

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, P003
  • get_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ΒΆ