featured image

Building a Model Context Protocol Server with .NET and Semantic Kernel Integration

Learn how to implement a Model Context Protocol (MCP) server using C# and integrate it with Semantic Kernel to enhance AI assistants with external data and tools through a standardized protocol.

Prathamesh Dhande Prathamesh Dhande Thu Apr 10 2025 7 min read

Introduction

In the “What is MCP” section, we explored MCP and its core features. In this post, we’ll build a simple MCP server using the MCP C# SDK and integrate it with Semantic Kernel enabling AI assistants to tap into external data and tools via a standardized protocol.

Problems Before MCP

MCP Working Image From Source

AspectBefore MCPAfter MCP
IntegrationCustom, separate connectors for each data sourceOne universal standard that connects all data sources
Context AccessRelies on static training dataFetches real-time, dynamic external information
Security & PrivacyInconsistent security and data handling practicesUniform, standardized security and privacy measures
ScalabilityDifficult to add new tools and data sourcesSeamless integration with additional tools and sources

Integrating MCP with the Semantic Kernel

This blog is currently using the MCP C# SDK version v0.1.0-preview.2. It’s possible that some code may have changed since then.

Note: The MCP C# SDK is currently in preview, so breaking changes may occur. At present, it only supports exposing Tools. Prompt and Resource support may be introduced later. Also, the current implementation only supports the STDIO protocol, while the SSE protocol throws an exception. Therefore, to implement an MCP Server using SSE, we need to implement our own Server-Sent Events (SSE). For more information about the protocols supported by the MCP Server, please refer to protocol supported by MCP.

The STDIO protocol is used for local development. Most Model Context Protocol implementations are STDIO-based because they can be easily integrated by Claude Desktop and Cursor.

In this blog, we will implement an MCP Server using Server-Sent Events (SSE). Although the current method for adding SSE throws exceptions, we will create our own SSE for sending the Tools through JSON RPC.

Building the MCP Server

  1. Install the MCP C# SDK in Visual Studio:

    dotnet add package ModelContextProtocol --prerelease
    
  2. Creating the Server:

    // Program.cs
    using MCPTutorial.SSEServer.Extensions;
    using ModelContextProtocol;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddMcpServer().WithToolsFromAssembly();
    
    var app = builder.Build();
    app.MapGet("/", () => $"Hello World! {DateTime.Now}");
    app.MapMcpSse(); // Custom Server-Sent Events for sending the RPC messages through JSON RPC.
    app.Run();
    

    The above code adds the MCP Server to the program. The WithToolsFromAssembly() extension method adds all the tools decorated with McpServerToolType from the currently running assembly using reflection.

    The extension method MapMcpSse() implements our custom Server-Sent Events; it is located in the McpEndpointRouteBuilderExtension.cs file.

  3. Creating the Custom Tools:

    // Tools.cs
    [McpServerToolType]
    public static class GithubTool
    {
        [McpServerTool, Description("Returns the GitHub repository URL constructed from the specified username and repository name.")]
        public static string GetRepository(string repositoryname, string username)
        {
            return $"https://www.github.com/{username}/{repositoryname}";
        }
    }
    
    [McpServerToolType]
    public class JokeTool
    {
        private readonly HttpClient _httpClient;
        public JokeTool()
        {
            // Initialize HttpClient with the base address
            _httpClient = new HttpClient()
            {
                BaseAddress = new Uri("https://v2.jokeapi.dev/joke/"),
            };
        }
    
        [McpServerTool, Description("Fetches a collection of jokes from the specified category. Valid categories include 'Programming', 'Misc', and 'Christmas'. If no amount is specified, 10 jokes are returned by default.")]
        public async Task<string> GetJoke(
            [Description("The joke category to filter by (e.g., 'Programming', 'Misc', 'Christmas').")] string category,
            [Description("The number of jokes to retrieve. Defaults to 10 if not provided.")] int amountofjokes = 10)
        {
            try
            {
                var response = await _httpClient.GetAsync($"{category}?amount={amountofjokes}");
                return await response.Content.ReadAsStringAsync();
            }
            catch (Exception)
            {
                return "Error";
            }
        }
    }
    

    Create the tool by using the McpServerToolType attribute and decorating the methods with McpServerTool, providing an appropriate description for each parameter. In this example, we have added two tools:

    • JokeTool: Returns jokes based on the specified category and number of jokes.
    • GithubTool: Returns the repository URL by using the provided username and repository name.

    These are dummy tools for demonstration purposes; you can create as many tools or functions as needed.

  4. Implementing a Custom SSE Server for Sending All the Tools Through JSON RPC:

    The MCP C# GitHub repository provides a sample for creating the MCP Server using Server-Sent Events. We refer to that sample even though their method WithHttpListenerSseServerTransport() currently throws an exception when implementing the MCP server using SSE.

    // McpEndpointRouteBuilderExtensions.cs
    public static class McpEndpointRouteBuilderExtensions
    {
        public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints)
        {
            IMcpServer? server = null;
            SseResponseStreamTransport? transport = null;
            var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
            var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
    
            var routeGroup = endpoints.MapGroup("");
    
            routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) =>
            {
                await using var localTransport = transport = new SseResponseStreamTransport(response.Body);
                await using var localServer = server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
    
                await localServer.StartAsync(requestAborted);
    
                response.Headers.ContentType = "text/event-stream";
                response.Headers.CacheControl = "no-cache";
    
                try
                {
                    await transport.RunAsync(requestAborted);
                }
                catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
                {
                    // RequestAborted always triggers when the client disconnects before the complete response body is written,
                    // which is typical for SSE connections.
                }
            });
    
            routeGroup.MapPost("/message", async context =>
            {
                if (transport is null)
                {
                    await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context);
                    return;
                }
    
                var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(McpJsonUtilities.DefaultOptions, context.RequestAborted);
                if (message is null)
                {
                    await Results.BadRequest("No message in request body.").ExecuteAsync(context);
                    return;
                }
    
                await transport.OnMessageReceivedAsync(message, context.RequestAborted);
                context.Response.StatusCode = StatusCodes.Status202Accepted;
                await context.Response.WriteAsync("Accepted");
            });
            return routeGroup;
        }
    }
    

    Implementing an MCP server using Server-Sent Events requires two endpoints: /sse and /message. This implementation sends methods as JSON-RPC messages. In the future, the SSE implementation may be updated to support additional features, such as Prompts and Resources. At present, only the tools are supported.

Inspecting/Debugging the Tools on the MCP Server

To inspect and debug the tools, use the MCP Inspector. The MCP Inspector helps debug and verify the tools before integrating them into Semantic Kernel. It is built using React and Next.js.

Below is a screenshot showing the currently running tools:

MCP Inspector

It lists all the tools and allows you to run them.

Integrating with the Semantic Kernel

Now that we have created a simple MCP Server, let’s integrate and use the tools available on the MCP Server with the Semantic Kernel.
If you’re not familiar with the Semantic Kernel, please refer to this blog: Getting Started With Semantic Kernel.

// Program.cs
// Providing the URL of the running MCP Server
HttpClient httpClient = new()
{
    BaseAddress = new Uri("http://localhost:5249/sse")
};

// Configuration settings for the server
McpServerConfig serverconfig = new()
{
    Id = "Sample",
    Name = "SampleSSEServer",
    TransportType = TransportTypes.Sse,
    // Providing the MCP server client location
    Location = httpClient.BaseAddress.ToString()
};

McpClientOptions clientoptions = new()
{
    ClientInfo = new Implementation() { Name = "Tool Client", Version = "1.0.0" }
};

// Connecting to the MCP Server
IMcpClient mcpclient = await McpClientFactory.CreateAsync(serverconfig, clientoptions);

In the above code, an HttpClient is created with the base address. This HttpClient is then used in the server configuration, establishing a connection with the MCP Server using the configuration settings in the McpServerConfig class.

IKernelBuilder kernelbuilder = Kernel.CreateBuilder();

kernelbuilder.AddAzureOpenAIChatCompletion(
    deploymentName: config["AzureOpenAI:ModelName"]!,
    endpoint: config["AzureOpenAI:Endpoint"]!,
    apiKey: config["AzureOpenAI:ApiKey"]!,
    modelId: config["AzureOpenAI:ModelVersion"]!,
    serviceId: "azureopenaimodel"
);

// Retrieve all the tools from the MCP client and insert them as plugins in the KernelBuilder
IList<AIFunction> aifunctions = await mcpclient.GetAIFunctionsAsync(new CancellationTokenSource().Token);
kernelbuilder.Plugins.AddFromFunctions("MCPTools", aifunctions.Select(func => func.AsKernelFunction()));

In this code, the AzureOpenAIChatCompletion method is added to the KernelBuilder. Then, the list of functions (tools) available on the MCP Server is retrieved. These functions follow the abstractions provided by Microsoft.Extensions.AI as AIFunction objects. Each AIFunction is converted to a KernelFunction using the AsKernelFunction() extension method provided by Semantic Kernel.

Kernel kernel = kernelbuilder.Build();
string systemprompt = @"""You are a helpful assistant bot. 
    Help the user solve the problem.
    Keep the conversation short and precise.
    Avoid using vulgar language or engaging in small talk.
    """;
ChatHistory chathistory = new ChatHistory(systemprompt);
while (true)
{
    IChatCompletionService chatcompletionservice = kernel.GetRequiredService<IChatCompletionService>(serviceKey: "azureopenaimodel");

    Console.Write("User: ");
    string userinput = Console.ReadLine()!;
    chathistory.AddUserMessage(userinput);

    AzureOpenAIPromptExecutionSettings promptexecutionsettings = new AzureOpenAIPromptExecutionSettings()
    {
        ToolCallBehavior = Microsoft.SemanticKernel.Connectors.OpenAI.ToolCallBehavior.AutoInvokeKernelFunctions,
        ServiceId = "azureopenaimodel"
    };

    StringBuilder stringbuilder = new StringBuilder();
    Console.Write("Assistant: ");
    await foreach (StreamingChatMessageContent chatcontent in chatcompletionservice.GetStreamingChatMessageContentsAsync(chathistory, promptexecutionsettings, kernel))
    {
        Console.Write(chatcontent.Content);
        stringbuilder.Append(chatcontent.Content);
    }
    Console.WriteLine();
    chathistory.AddAssistantMessage(stringbuilder.ToString());
}

This code initiates a chat loop and provides the PromptExecutionSettings to automatically invoke Kernel Functions.

Output:

Output

Notice that we haven’t explicitly defined any tools in the Semantic Kernel. This approach is similar to how Semantic Kernel Plugins work.

Conclusion

In summary, the Model Context Protocol (MCP) greatly streamlines the way AI systems access and interact with external data sources and tools. By utilizing a standardized client-server architecture, developers no longer need to create individual connectors for each data source, resulting in faster, more secure, and scalable integrations. This blog showcased the process of building an MCP Server with the MCP C# SDK using a custom Server-Sent Events implementation, along with creating example tools and integrating them with the Semantic Kernel.

Prev
Why Your AI Agent Isn't Calling Your Tools: Fixing Function Invocation Issues in Semantic Kernel
Next
Building AI Agent using Semantic Kernel Agent Framework