DEV Community

Peter Saktor
Peter Saktor

Posted on

Build a Simple MCP Server and Client in C#

What is MCP?

The Model Context Protocol (MCP) is a standardized way for AI systems to access and interact with various external resources file systems, databases, APIs, and other services. Instead of writing unique connectors for each integration, MCP provides a unified interface that any compliant system can use.

By using MCP, developers can empower AI agents to discover, retrieve, and act upon information regardless of the underlying technology. This abstraction allows developers to focus more on what the system does rather than how it connects to the data.

Core MCP Primitives

MCP is built around three key concepts:

1. Resources

These represent any external data the AI agent might need to access: documents, images, files, databases, and so on.

  • Consistent Access: Agents use a unified interface to request any resource.
  • Always Up to Date: The system ensures access to the latest information.
  • Abstracted Complexity: Developers don’t need to write custom code for every data source.

2. Tools

Tools represent the functions or operations that the AI agent can invoke to perform specific tasks like fetching data, editing files, processing content, and more.

  • Action Oriented: Agents don’t just read data, they can also modify or trigger workflows.
  • Dynamic Selection: Agents can dynamically discover and choose the right tool based on context.

3. Prompts

Prompts are structured templates or instructions that define how AI agents interact with tools and resources.

  • Guided Interactions: Prompts ensure all requests are formatted correctly.
  • Context Management: Agents maintain coherent interaction across multiple operations.
  • Clarity: Standardized prompts reduce ambiguity and misinterpretation.

Together, these primitives allow agents to operate intelligently and autonomously in a modular system.

MCP Architecture: Client-Server Model

MCP is designed using a client-server model:

  • The Client (typically the AI agent) sends structured requests, which include prompts and resource identifiers.
  • The MCP Server processes these requests, maintains context, and returns results.

This separation ensures scalability, security, and flexibility across various applications.

C# SDK for MCP

To support .NET developers, there is a C# SDK available on GitHub:

πŸ‘‰ MCP SDK for C#

This SDK simplifies the process of integrating your AI agents with an MCP server, handling serialization, request/response formats, and context management.

Sample Solution: Simple MCP Server and Client

To demonstrate how MCP works in practice, I’ve created a minimalistic MCP solution in C#:

πŸ“ GitHub Repository: petersaktor/mcp

The solution includes both a server and a client, showing how to:

  • Define and expose tools on the server side
  • Send prompts and interact with resources
  • Log and handle results returned from the MCP server

MCP Server

Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

Console.WriteLine("Hello, MCP Simple server!");

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
    // Configure all logs to go to stderr
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithPromptsFromAssembly()
    .WithToolsFromAssembly()
    .WithResourcesFromAssembly();

await builder.Build()
    .RunAsync();
Enter fullscreen mode Exit fullscreen mode

SimplePrompt.cs

using Microsoft.Extensions.AI;
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace SimpleServer.Prompts;

[McpServerPromptType]
public static class SimplePrompt
{
    [McpServerPrompt, Description("Creates a prompt to summarize the provided message.")]
    public static ChatMessage Summarize(string content) =>
        new(ChatRole.User, $"Please summarize this content into a single sentence: {content}");
}

Enter fullscreen mode Exit fullscreen mode

SimpleTool.cs

using ModelContextProtocol.Server;
using System.ComponentModel;

namespace SimpleServer.Prompts;

[McpServerToolType]
public static class SimpleTool
{
    [McpServerTool, Description("Returns the current time for the specified time zone.")]
    public static string GetCurrentTime(string timeZone)
    {
        try
        {
            var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
            var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
            return now.ToString("o"); // ISO 8601 format
        }
        catch (TimeZoneNotFoundException)
        {
            return $"Time zone '{timeZone}' not found.";
        }
        catch (InvalidTimeZoneException)
        {
            return $"Time zone '{timeZone}' is invalid.";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

SimpleResource.cs

using ModelContextProtocol.Server;
using System.ComponentModel;

namespace SimpleServer.Prompts;

[McpServerResourceType]
public static class SimpleResource
{
    [McpServerResource, Description("Returns the Bing endpoint URL.")]
    public static string GetBingEndpoint() => "https://bing.com";
}
Enter fullscreen mode Exit fullscreen mode

MCP Client

Program.cs

using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;

Console.WriteLine($"Hello, MCP Simple client!");

// Create the stdio transport (using current process's stdin/stdout)
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Name = "MCP Simple Client",
    Command = "..\\..\\..\\..\\SimpleServer\\bin\\Debug\\net8.0\\SimpleServer.exe",
    // The command to start the MCP server process
    // If the server is already running, you can use the same command as the server
    // If you want to use a different command, specify it here
});

try
{
    // Create the MCP client
    var client = await McpClientFactory.CreateAsync(cancellationToken: default, clientTransport: transport);

    // List available prompts from the server
    foreach (var pr in await client.ListPromptsAsync())
    {
        Console.WriteLine($"Available prompt: {pr.Name} - {pr.Description}");
    }

    // Specify the prompt you want to use
    var promptName = "Summarize";

    // Create a dictionary of arguments for the prompt
    IReadOnlyDictionary<string, object> promptArguments = new Dictionary<string, object>()
    {
       { "content", "ModelContextProtocol enables structured communication." }
    };

    // Get the prompt from the server using the specified name and arguments
    var prompt = await client.GetPromptAsync(promptName, promptArguments!);
    if (prompt == null)
    {
        Console.WriteLine($"Prompt '{promptName}' not found.");
        return;
    }

    // Print the prompt details
    foreach (var message in prompt.Messages)
    {
        Console.WriteLine($"Message Role: {message.Role}, Content: {message.Content?.Text}");
    }

    // List available tools from the server
    foreach (var to in await client.ListToolsAsync())
    {
        Console.WriteLine($"Available tool: {to.Name} - {to.Description}");
    }

    // Specify the tool you want to use
    var toolName = "GetCurrentTime";

    // Create a dictionary of arguments for the tool
    IReadOnlyDictionary<string, object> toolArguments = new Dictionary<string, object>()
    {
        { "timeZone", "Pacific Standard Time" } // Example time zone
    };

    // Call tool from the server using the specified name and arguments
    var result = await client.CallToolAsync(toolName, toolArguments!);
    if (result == null)
    {
        Console.WriteLine($"Tool '{toolName}' not found.");
        return;
    }

    // Print the tool result
    foreach (var res in result.Content)
    {
        Console.WriteLine($"Tool Result: {res.Text}");
    }

    // List available resources from the server
    foreach (var res in await client.ListResourcesAsync())
    {
        Console.WriteLine($"Available resource: {res.Name} - {res.Description}");
    }

    // Specify the resource you want to use
    var resourceName = "resource://GetBingEndpoint";

    // Get the resource from the server using the specified name
    var resource = await client.ReadResourceAsync(resourceName);
    if (resource == null)
    {
        Console.WriteLine($"Resource '{resourceName}' not found.");
        return;
    }

    // Print the resource details
    foreach (var res in resource.Contents)
    {
        Console.WriteLine($"Resource Content: {((TextResourceContents)res)?.Text}");
    }
}
catch (Exception ex)
{
    Console.WriteLine("Error creating MCP client: " + ex.Message);
    return;
}
Enter fullscreen mode Exit fullscreen mode

Summary

MCP brings a new level of consistency and modularity to AI integrations. With a simple but powerful abstraction using Resources, Prompts, and Tools, and a clean client-server model, you can build intelligent agents that interact seamlessly with the outside world.

Explore more:

Top comments (0)