Blog

A Step-by-Step Guide to OAuth Authorization for MCP Servers

More and more software solutions make use of AI technology to implement certain functionality. Large Language Models (LLMs) are often used. In a lot of cases it is required that certain (company) data is exposed to the LLM to let the model give the correct response. In this blogpost I will explain how this can be achieved using an MCP (Model Context Protocol) server. I will also explain how to add authentication to this service.

🌟 What is MCP?

There are different ways to expose (company) data to a LLM. One option is to implement a technique called ‘function calling’. Earlier I wrote a blog about this. See: https://mstack.nl/blogs/integrating-your-own-business-logic-and-data-with-an-llm/.
In that blogpost functions embedded in an application are exposed to the LLM. To take this a step further you often hear the term MCP. MCP stands for Model Context Protocol. This is an open standard that allows LLMs to interact with and use external tools. Those tools do not need to be embedded in the calling application but can be implemented as a (reusable) service on its own. It is often referred to as the USB-C port for AI models.

 

💡 Build your first MCP server in .NET

To build an MCP Server in .NET there is a NuGet package called ModelContextProtocol https://www.nuget.org/packages/ModelContextProtocol) available. At the time of writing this blogpost it is still in preview. Using this NuGet package it is fairly easy to build your own MCP service. Let’s first set up our project.

  1. In Visual Studio create a new project using the ‘ASP.NET Core Empty’ template
  2. When the project is created add the NuGet package ModelContextProtocol. For this demo I used version Latest prerelease 0.3.0-preview.4
    dotnet add package ModelContextProtocol --version 0.4.0-preview.3
    
  3. Next replace the content of the Program.cs file with the following lines of code:
    var builder = Host.CreateApplicationBuilder(args);
    builder.Services.AddMcpServer()
        .WithStdioServerTransport();
    
    var host = builder.Build();
    await host.RunAsync();
    
  4. Press Ctrl + F5 and you have your first MCP server running

Now you have a MCP server running but it has no functionality at all. To register functionality that a LLM can use, you have to register a so called tool. A tool is a class decorated with some attributes on the class and its methods. Lets write an example tool that has functionality to get what employees are working at mstack.

using ModelContextProtocol.Server;
using System.ComponentModel;

namespace McpServerDemo.Tools;

[McpServerToolType]
public class MstackTools()
{
    [McpServerTool, Description("Returns the mstack employees")]
    public string GetMstackEmployees()
    {
        return $"Hans, Mike, Gabor, Stef, Jasper, Brenda";
    }
}

Looking at the class above you see the class ‘MstackTools’ is decorated with the attribute McpServerToolType. Because of this attribute the MCP extension method that will add the tools (we see that later in this blog) can recognize this code as a class that wants to expose some functionality. Inside the class you see the method ‘GetMstackEmployees’. This method is decorated with the McpServerToolType and Description attributes. Those attributes are also used to register each method as a tool for the MCP server. In this example the method simply returns a comma separated string of hardcoded values.

Now we have the tool ready, we need to register it. Below is the program.cs content again with one line of code added to register the tools in the MCP server.

using McpServerDemo.Tools;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly(typeof(MstackTools).Assembly);

var host = builder.Build();
await host.RunAsync();

When we start the MCP server the functionality to get the mstack employees is exposed. To demonstrate this I will now describe how you can build a simple chat client that registers this MCP server so we can use the functionality to be called by a LLM.

💡 Build a chat client that uses the MCP server

In .NET SemanticKernel is a NuGet package from Microsoft that is very handy to interact with LLM’s. In this demo I will use SemanticKernel to talk to a GPT4.1 model deployed in Azure AI Foundry. One of the advantages of SemanticKernel is that you can write plugins (as seen in the blogpost I mentioned earlier) to expose functionality to the LLM. There is a NuGet package (written by my colleague Stef Heyenrath) available named Stef.ModelContextProtocol.SemanticKernel. This is a very handy NuGet package that acts as the glue between the plugins in SemanticKernel and MCP servers. This NuGet package has methods that will automatically map all the MCP tools to plugins in SemanticKernel. In that way you can add MCP server tools to your client in just one line of code.

await kernel.Plugins.AddMcpFunctionsFromStdioServerAsync("McpServerDemo", "..\\..\\..\\..\\..\\McpServerDemo\\McpServerDemo\\bin\\Debug\\net9.0\\McpServerDemo.exe");

Or (when using server send events)

await kernel.Plugins.AddMcpFunctionsFromSseServerAsync("McpServerWithAuth", new Uri("http://localhost:5000"));

Now let’s write our chat client using this one-liner to integrate it with our MCP server. Create a new console application and replace the program.cs file with the following code.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using ModelContextProtocol.SemanticKernel.Extensions;

Console.WriteLine("Press enter to start");
Console.ReadLine();

// credentials
var deploymentName = "gpt-4.1"; // your model name
var endpoint = "";
var apiKey = "";

// create the kernel
var builder = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey);

// build the kernel
Kernel kernel = builder.Build();
await kernel.Plugins.AddMcpFunctionsFromStdioServerAsync("McpServerDemo", "..\\..\\..\\..\\..\\McpServerDemo\\McpServerDemo\\bin\\Debug\\net9.0\\McpServerDemo.exe");

// Set parameters so that plugin methods are automatically called
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
};

var chatCompletionService = kernel.GetRequiredService();
var history = new ChatHistory();
string? userInput;
do
{
    Console.Write("User > ");
    userInput = Console.ReadLine();
    if (!string.IsNullOrEmpty(userInput))
    {
        // Add input to the history
        history.AddUserMessage(userInput!);
        var result = await chatCompletionService.GetChatMessageContentAsync(
            history,
            executionSettings: openAIPromptExecutionSettings,
            kernel: kernel);
        Console.WriteLine("Assistant > " + result);

        // Add answer to the history
        history.AddMessage(result.Role, result.Content ?? string.Empty);
    }
} while (!String.IsNullOrEmpty(userInput));

When you read trough the code above you see that we first set up the SemanticKernel client. After that there is a simple do-while loop that will ask the user for input and then send the input to the LLM. When running this application and asking a question like ‘Who works at mstack?’ the LLM will decide that the client needs to call the MCP server to get the mstack employees. The response of that call is send back to the LLM and the LLM will then create its final response. All this logic is abstracted by SemanticKernel (see my earlier mentioned blog about plugins). Because of the one-liner AddMcpFunctionsFromStdioServerAsync the MCP tools are available as plugins in SemanticKernel.

🛂 How to add authorization?

Now we have a chat client and a MCP server there is one important topic I want to address in this blogpost. It is nice that you can write a MCP server that exposes data but you don’t want to expose this to everyone. Authentication and authorization is needed!

Adding authentication and authorization to our MCP server

For this blog post we will add oauth authentication to our MCP server. To set this up we first need to switch the transport protocol the MCP service uses. In our previous demo we configured this to use the standard I/O as transport protocol. This is possible because everything runs on one machine. Because everything runs locally authentication is not implemented here. Now we want to add authentication we switch to HttpTransport. The code looks as the following.

builder.Services.AddMcpServer()
    .WithTools()
    .WithHttpTransport();

Our MCP service is in basis a normal ASP.NET core web application. We can authenticate and authorize to it just as we know it.

var duendeOAuthServerUrl = "https://localhost:6001"; // duende demo service
builder.Services.AddAuthentication(options =>
{
    options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    // Configure to validate tokens from our in-memory OAuth server
    options.Authority = duendeOAuthServerUrl;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidAudience = "mcp",
        ValidIssuer = duendeOAuthServerUrl,
        NameClaimType = ClaimTypes.Name, 
        RoleClaimType = "roles",        
    };  
})
.AddMcp();

Next to that we need to add the authentication and authorization to the ASP.NET core pipeline.

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// call RequireAuthorization to protect the MCP endpoints
app.MapMcp().RequireAuthorization();

Notice the last line. Here we add MCP to the pipeline and call the RequireAuthorization method so that authorization is required for all MCP methods.
The MCP service is now ready and protected. We now have to make some changes to our ChatClient to use HttpTransport and to add an authentication header.

Adding authentication to our ChatClient

Now our MCP server requires authentication we need to change our demo ChatClient so that it will send an authentication header in it’s requests to the MCP server. Remember that it is always the client application that calls the MCP server. The AI model only decides that it wants the client to call it. This means authentication needs to be implemented in the ChatClient. To achieve this I added a dummy Duende IdentityServer project (see the full code base here). In the ChatClient I then call this server to authenticate a user. Next to that I create a HttpClient where I set the authentication header with the token I got from the Duende IdentityServer. The code looks like this.

var token = await LoginHelper.Login("alice", "password");
builder.Services.AddHttpClient(); // default client
builder.Services.PostConfigure(options =>
{
    options.HttpClientActions.Add(client =>
    {
        client.BaseAddress = new Uri("http://localhost:5000/");
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);
    });
});
var httpClient = builder.Services.BuildServiceProvider().GetService();

// build the kernel and add the MCP functions as SemanticKernel plugins
Kernel kernel = builder.Build();
await kernel.Plugins.AddMcpFunctionsFromSseServerAsync("McpServerWithAuth", new Uri("http://localhost:5000"), null, httpClient);

As you can see I pass in the configured httpClient to the AddMcpFunctionsFromSseServerAsync method. In this way all calls to the MCP server will use this HttpClient and therefor each HTTP request will contain the authentication header.
The full code base of this demo application (ChatClient, Duende IdentityServer and the MCP server) can be found here.

👉 Conclusion

Looking at the demo applications we just created you can see that setting up an MCP server using the ModelContextProtocol NuGet package is fairly easy. Just a few lines of code and you have your MCP server running. Using the NuGet package ModelContextProtocol-SemanticKernel makes it very easy to register MCP functionality as plugins in SemanticKernel. In that way you can connect MCP servers to your clients with just one line of code. Last but not least we have seen that you can add authentication on an MCP server in a way that is for a large part the same as authentication on a normal ASP.NET core web application.

Hope you liked this blog post. If you have any questions or suggestions feel free to contact me!

Links:

Hans Enthoven

Hans kent .NET en C# als geen ander. In zijn blogs neemt hij je onder andere mee door te nieuwste feature van onze favoriete ontwikkelstack.

Written by: Hans Enthoven

Hans is a software developer with over 20 years of experience. As a software developer he has lots of experience developing for the Microsoft platform. In these 20 years Hans has carried out many different projects in different environments. Hans describes himself as a professional software engineer with a lot of experience within Microsoft technology. Next to developing Hans also coaches people on the job and likes to give presentations to share his knowledge with other people.

Mission: Building structured, reusable and maintainable software that that matters in this society.

Want to know more about our experts? Contact us!