Picture of Stefan Lieser
Stefan Lieser

Implement MCP Server in C#

MCP servers can be used to connect programs and tools to the AI. With an MCP server for your accounting software, for example, you can use OpenCode to find out whether documents are missing. Or have diagrams drawn with an MCP server for Excalidraw. MCP servers are therefore not only useful for integrating developer tools, but also for many other tasks.

Creating an MCP server in C# is basically easy, because Microsoft provides the NuGet package ModelContextProtocol provides everything you need. The package is currently still a prerelease, i.e. you may have to take this into account when adding it by ticking the โ€žPrereleaseโ€œ box. An MCP server can be implemented with this package as a stand-alone application or integrated into an existing application. The following example shows the standalone version. Integration into an existing application is very similar.

MCP server connection

Put simply, MCP servers can be addressed by the AI in two ways:

  • Command line (in OpenCode with local designated)
  • http (in OpenCode with remote even if the server can run locally)

The following example shows an MCP server that is accessed via http. This requires the NuGet package ModelContextProtocol.AspNetCore to be integrated.

Two different tools are offered in the example:

  • Echo
  • Monkey

The Echo The example shows the basic structure. The tool returns the string passed as a parameter to the caller so that you can see that the call has actually taken place.

The Monkey example is an MCP server for the URL https://www.montemagno.com/monkeys.json It virtually simulates a remote API call, which is then provided as an MCP tool.

As a starting point, I began with a console application that takes over the ASP.NET core hosting. The connection to OpenCode, for example, is made via http. An MCP tool is implemented in a class, which is defined with the attribute [McpServerToolType] is provided. As a convention, I name such classes with the suffix Tool.

				
					using System.ComponentModel;
using Microsoft.AspNetCore.Mvc;
using ModelContextProtocol.Server;

namespace mymcp;

[McpServerToolType]
public class EchoTool
{
    [HttpGet("echo"), McpServerTool, Description("Echoes the message back to the client.")]
    public string Echo([FromQuery] string message) {
        return $ "Hello from C#: {message}";
    }

    [HttpGet("reverse"), McpServerTool, Description("Echoes in reverse the message sent by the client.")]
    public string ReverseEcho([FromQuery] string message) {
        return new string(message.Reverse().ToArray());
    }
}
				
			

Methods are then provided within the class, each with the attribute [McpServerTool] are provided. This means that they are listed in the list of tools provided at runtime. So if the AI asks the MCP server for a list of tools, all classes with the attribute McpServerToolType and all of them with McpServerTool are each listed as a tool. The attributes are therefore used to provide the metadata required to list the MCP tools.

Each tool must have a description that can be assigned the attribute Description is provided. The text is used by the AI to decide whether it should use the tool. It is therefore essential to provide a good description of the functionality provided, which should not be too long.

The third component is the parameters. If an MCP tool requires parameters, these can be defined as parameters of the method. With a tool as simple as Echo, it is already clear from the description and the parameter name that a โ€žmessageโ€œ must be provided. In real MCP servers, the parameters are also provided with a description, as we will see later in the Monkey example.

Initialization of the MCP server

To start the MCP server, the usual initialization must be carried out.

				
					namespace mymcp;

internal class Program
{
    private async static Task Main(string[] args) {
        var builder = WebApplication.CreateBuilder(args);
        builder.Logging.AddConsole(consoleLogOptions => {
            consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
        });

        builder.Services.AddHttpClient();
        builder.Services.AddSingleton();
        builder.Services.AddControllers();

        builder.Services
            .AddMcpServer()
            .WithHttpTransport(options => options.Stateless = true)
            .WithToolsFromAssembly();

        var app = builder.Build();
        app.MapControllers();
        app.MapMcp("/mcp");

        await app.RunAsync("http://localhost:3001");
    }
}
				
			

The first step is to initialize the logging, in this case quite simply on the console. Then some services are added to the DI container. This is followed by the MCP server-specific part: an MCP server is added and configured. This demo MCP server is defined as stateless. This disables some features of the MCP server implementation that are only required for stateful servers. Among other things, the endpoint /sse is not provided. With app.MapMcp(โ€ž/mcpโ€œ) the MCP server is then provided on the desired endpoint.

Set up in OpenCode and Claude Code

In OpenCode, for example, the server can be provided by editing the file ~/.config/opencode.json:

				
					{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "mo-mcp": {
      "type": "remote",
      "url": "http://127.0.0.1:3001/mcp"
    }
  }
}
				
			

It is integrated into Claude Code via the following command line call:

				
					claude mcp add --scope user --transport http mymcp http://localhost:3001/mcp
				
			

The MCP server can now be used.

MCP Server echo

Current Clean Code trainings

We also run all seminars as closed company courses for you.
If you are interested or have any questions please contact us.

Connecting a remote API

If a remote API does not provide an MCP server, you can easily implement one yourself. The following example binds the URL https://www.montemagno.com/monkeys.json as an API, so to speak. The URL returns a JSON result, just like a โ€žrealโ€œ API would.

The first step is to implement a service that connects to this API:

				
					namespace mymcp;

public class Monkey
{
    public string? Name { get; set; }

    public string? Location { get; set; }

    public string? Details { get; set; }

    public string? Image { get; set; }

    public int Population { get; set; }

    public double Latitude { get; set; }

    public double Longitude { get; set; }
}
				
			
				
					using System.Text.Json.Serialization;

namespace mymcp;

public class MonkeyService(IHttpClientFactory httpClientFactory)
{
    private readonly HttpClient _httpClient = httpClientFactory.CreateClient();

    private List _monkeyList = new();

    public async Task<List> GetMonkeys() {
        if (_monkeyList.Count > 0) {
            return _monkeyList;
        }

        var response = await _httpClient.GetAsync("https://www.montemagno.com/monkeys.json");
        if (response.IsSuccessStatusCode) {
            _monkeyList = await response.Content.ReadFromJsonAsync(MonkeyContext.Default.ListMonkey) ?? [];
        }

        _monkeyList ??= [];

        return _monkeyList;
    }

    public async Task GetMonkey(string name) {
        var monkeys = await GetMonkeys();
        return monkeys.FirstOrDefault(m => m.Name?.Equals(name, StringComparison.OrdinalIgnoreCase) == true);
    }
}

[JsonSerializable(typeof(List))]
internal sealed partial class MonkeyContext : JsonSerializerContext
{
}
				
			

The two methods GetMonkeys() and GetMonkey(string name) represent the functions provided by the API. GetMonkeys() reads the JSON data from the URL. The result is a list of monkeys. As the API does not offer the option of filtering by name directly on the server side, this is done by the GetMonkey(string name) is adopted. It first retrieves all monkeys from the URL and then searches for the first element with the transferred name.

Now that we have connected the external API, all we have to do is create the MCP tools again.

				
					using System.ComponentModel;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using ModelContextProtocol.Server;

namespace mymcp;

[McpServerToolType]
public class MonkeyTools(MonkeyService monkeyService)
{
    [McpServerTool, Description("Get a list of monkeys.")]
    public async Task GetMonkeys() {
        var monkeys = await monkeyService.GetMonkeys();
        return JsonSerializer.Serialize(monkeys);
    }

    [McpServerTool, Description("Get a monkey by name.")]
    public async Task GetMonkey([FromRoute, Description("The name of the monkey to get details for")] string name) {
        var monkey = await monkeyService.GetMonkey(name);
        return JsonSerializer.Serialize(monkey);
    }
}
				
			

Here we see in the method GetMonkey an example of a parameter description. The Description attribute can be used here to give the AI information about the meaning of the parameter. This is obvious with the name of the monkey. With my MCP server for accounting, however, these descriptions are helpful so that the AI knows when and how it can use the parameters.

The following illustration shows the use of the Monkey Tool in OpenCode.

Implement GetMonkey's mcp server
Implement GetMonkey mcp server

Conclusion

An MCP server can be configured using the ModelContextProtocol NuGet packages can be created quickly. In practice, it takes some time and trial and error until the server runs smoothly. Since it makes sense to get help from Claude Code or OpenCode when writing the MCP server, they can also be helpful when troubleshooting. With my accounting connection, for example, there was the problem that the accounting service was overrun by too many requests in quick succession. Claude Code then helped me to install a delay and thus make the connection stable.

If you would like to learn more about MCP Server and Claude Code, our seminar Introduction to Claude Code the right one for you.

You can find my MCP server for ProSaldo Accounting Monkey Office at https://gitlab.com/slieser/mo-mcp.

Our seminars

course
Clean Code Developer Basics

Principles and tests - The seminar is aimed at software developers who are just starting to deal with the topic of software quality. The most important principles and practices of the Clean Code Developer Initiative are taught.

to the seminar "
course
Clean Code Developer Trainer

Conducting seminars as a trainer - This seminar is aimed at software developers who would like to pass on their knowledge of Clean Code Developer principles and practices or Flow Design to others as a trainer.

to the seminar "

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish