DEV Community

Timo Tielens-Jansen
Timo Tielens-Jansen

Posted on

Building an MCP Server in Umbraco: My Journey into AI-Powered Content Management

Why I Decided to Build an MCP Server in Umbraco

I've been working with Umbraco for years, and while it's a fantastic CMS, I always wondered how I could make it smarter. When I learned about Model Context Protocol (MCP), I saw an opportunity to bridge the gap between traditional content management and AI-powered tools. The idea of giving AI assistants direct access to my Umbraco data was both exciting and a bit scary—but mostly exciting.

Note: I'm working on a Windows machine for this tutorial, so some commands might vary if you're on a different OS.

What You'll Need Before We Start

Before diving in, make sure you have:

  • A basic understanding of what Model Context Protocol is (if not, it's worth reading up on it first)
  • Ollama installed from ollama.com
  • Go 1.23 or later
  • Some .NET development experience

Getting Umbraco Up and Running

Let's start with the basics. I always like to begin with a fresh Umbraco installation to avoid any conflicts with existing projects.

First, install the Umbraco template:

dotnet new install Umbraco.Templates
Enter fullscreen mode Exit fullscreen mode

Then create a new project:

dotnet new umbraco --name umbracoMcpServer
Enter fullscreen mode Exit fullscreen mode

Navigate to the project directory and run it:

cd umbracoMcpServer
dotnet run
Enter fullscreen mode Exit fullscreen mode

When you run the project, the console will print the port it's listening on. In my case, it was https://localhost:44395. Navigate to this URL in your browser, and you should see the Umbraco installation screen. Go ahead and complete the installation—we'll need an active user and a fully configured Umbraco instance.

Once you're logged into the back-end, you can stop the server with Ctrl + C.

Adding the MCP Magic

Now comes the interesting part. We need to add the Model Context Protocol SDK. At the time of writing, this is still in preview (version 0.3.0-preview.1), which means things might change, but that's part of the fun of working with cutting-edge technology.

dotnet add package ModelContextProtocol --prerelease
dotnet add package ModelContextProtocol.AspNetCore --prerelease
Enter fullscreen mode Exit fullscreen mode

I also needed to add the .NET Hosting package:

dotnet add package Microsoft.Extensions.Hosting
Enter fullscreen mode Exit fullscreen mode

Organizing Our MCP Components

I like to keep things organized, so let's create a proper folder structure for our MCP implementation:

mkdir mcp
cd mcp
mkdir tools
mkdir models
Enter fullscreen mode Exit fullscreen mode

This structure will help us separate our concerns and make the code more maintainable as we add more features.

Creating the Data Transfer Object

Now that we have our folder structure in place, let's create our first component. We'll start with a data transfer object (DTO) that will help us shape the member data for our MCP server.

Create a new file called MemberMcpDto.cs in the mcp/models folder:

using System.Diagnostics.CodeAnalysis;
using Umbraco.Cms.Core.Models;

namespace umbracoMcpServer.mcp.models;

public class MemberMcpDto
{
    public int Identifier { get; set; }
    public string? Name { get; set; }
    public required string Username { get; set; }
    public required string Email { get; set; }
    public DateTime? CreateDate { get; set; }
    public DateTime? UpdateDate { get; set; }
    public bool IsApproved { get; set; }
    public bool IsLockedOut { get; set; }
    public required string MemberTypeAlias { get; set; }

    [SetsRequiredMembers]
    public MemberMcpDto(IMember member)
    {
        Identifier = member.Id;
        Name = member.Name;
        Username = member.Username;
        Email = member.Email;
        CreateDate = member.CreateDate;
        UpdateDate = member.UpdateDate;
        IsApproved = member.IsApproved;
        IsLockedOut = member.IsLockedOut;
        MemberTypeAlias = member.ContentType.Alias;
    }
}
Enter fullscreen mode Exit fullscreen mode

This DTO serves as a clean interface between Umbraco's internal member model and what we want to expose through our MCP server. I've included the most useful properties that an AI assistant might need when working with members.

Building the MCP Tool

Here's where things get really interesting. The MCP tool is what actually provides functionality to AI assistants. Create a new file called MemberTool.cs in the mcp/tools folder:

using System.ComponentModel;
using System.Text.Json;
using ModelContextProtocol.Server;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Models;
using umbracoMcpServer.mcp.models;

namespace umbracoMcpServer.mcp.tools;

[McpServerToolType]
public sealed class MemberTool
{
    private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
    {
        WriteIndented = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    [McpServerTool, Description("Get all members from the Umbraco database. Returns a list of members in JSON format.")]
    public Task<string> GetMembers(IMemberService memberService)
    {
        try
        {
            var members = memberService.GetAllMembers();

            var enumerable = members.ToList();
            if (enumerable.Count == 0)
                return Task.FromResult("{}");

            var membersList = enumerable.Select(member => new MemberMcpDto(member)).ToList();

            var json = JsonSerializer.Serialize(membersList, SerializerOptions);

            return Task.FromResult(json);
        }
        catch
        {
            // Return empty JSON object on error
            return Task.FromResult("{}");
        }
    }

    [McpServerTool, Description("Create a new member in the Umbraco database. Returns the created member details in JSON format.")]
    public Task<string> CreateMember(
        IMemberService memberService, 
        [Description("The username for the new member. This will be used for login purposes.")]
        string username, 
        [Description("The email address for the new member. Must be a valid email format.")]
        string email, 
        [Description("The display name for the new member. This is typically the full name or display name.")]
        string name, 
        [Description("The password for the new member. If empty, the member will be created without a password.")]
        string password = "")
    {
        try
        {
            var member = memberService.CreateMember(username, email, name, "Member");

            if (!string.IsNullOrEmpty(password))
                member.RawPasswordValue = password;

            memberService.Save(member);
            var json = JsonSerializer.Serialize(new MemberMcpDto(member), SerializerOptions);

            return Task.FromResult(json);
        }
        catch (Exception ex)
        {
            var errorResponse = new
            {
                success = false,
                error = ex.Message
            };

            var errorJson = JsonSerializer.Serialize(errorResponse, SerializerOptions);

            return Task.FromResult(errorJson);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What I love about this approach is how the MCP framework uses dependency injection to provide the IMemberService automatically. The Description attributes are crucial—they help AI assistants understand what each tool does and how to use the parameters correctly.

Wiring Everything Together

Now we need to update our Program.cs file to register our MCP server and tools. This is where the magic happens:

using ModelContextProtocol.AspNetCore;
using umbracoMcpServer.mcp.tools;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithTools<MemberTool>();

builder.CreateUmbracoBuilder()
    .AddBackOffice()
    .AddWebsite()
    .AddComposers()
    .Build();

var app = builder.Build();

app.MapMcp("/mcp");

await app.BootUmbracoAsync();

app.UseUmbraco()
    .WithMiddleware(u =>
    {
        u.UseBackOffice();
        u.UseWebsite();
    })
    .WithEndpoints(u =>
    {
        u.UseBackOfficeEndpoints();
        u.UseWebsiteEndpoints();
    });

await app.RunAsync();
Enter fullscreen mode Exit fullscreen mode

The key line here is app.MapMcp("/mcp"), which exposes our MCP server at the /mcp endpoint.

Testing Our MCP Server

Start the application and navigate to https://localhost:44395/mcp. You should see a "Session not found" error message, which actually means everything is working correctly! The MCP server is running and waiting for a proper MCP client to connect.

Things to keep in mind

Since this is a simple implementation, there are a few things to consider:

  • Security: This implementation does not include any authentication or authorization. In a production environment, you should secure your MCP endpoints to prevent unauthorized access.
  • Error Handling: The error handling in the CreateMember method is basic. You might want to improve it to handle specific exceptions or validation errors more gracefully.
  • Comments and Documentation: While I've added some comments, you might want to expand on them to make the code more understandable for others (or yourself in the future).
  • Testing: This example doesn't include unit tests or integration tests. Consider adding tests to ensure the reliability of your MCP server.
  • Logging: Implementing logging would be beneficial for debugging and monitoring the MCP server's activity.
  • Cancellation Tokens: For long-running operations, consider using cancellation tokens to allow graceful shutdowns or cancellations. More on Cancellation Tokens in .NET.
  • Serperation It would be a good idea to seperate the AI request and the handling in different classes. This would make it easier to add more tools in the future.
  • Data validation: The CreateMember method does not validate the input data. You might want to add checks to ensure that the username and email are valid before creating a member.

Connecting the LLM to Our MCP Server

Now comes the exciting part—actually using our MCP server! To connect an LLM like Ollama to our MCP server, I discovered MCPHost by Mark3Labs. This tool makes the connection process surprisingly straightforward.

First, install MCPHost:

go install github.com/mark3labs/mcphost@latest
Enter fullscreen mode Exit fullscreen mode

Configuration

Create a new file called mcphost.yaml in your project root:

{
  "mcpServers": {
    "umbraco": {
      "type": "remote", 
      "url": "https://localhost:44395/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

I chose to test this with the qwen2.5:1.5b model, but you can use any model that supports tool calling. You can find compatible models on the Ollama website.

Start the MCP host with:

mcphost -m ollama:qwen2.5:1.5b --config "what/ever/your/path/is/umbracoMcpServer/mcphost.yaml"
Enter fullscreen mode Exit fullscreen mode

Seeing It in Action

The moment of truth! I asked the AI to create a new member:

"Create a new member for me. The username and name are Timo, the email is development@timotielens.nl and the password is something secure."

And it worked! The AI understood my request, called the CreateMember tool with the appropriate parameters, and successfully created a new member in Umbraco. When I checked the Umbraco back-office, there was the new member, complete with all the details I'd specified.

A screenshot of the new member conversation

I could also ask it to list all members, and it would return a nicely formatted JSON response with all the member data. The AI assistant now has direct access to my Umbraco data and can perform operations on it—it's like having a conversational interface to my CMS.

Final Thoughts

Building an MCP server in Umbraco has been one of those projects that feels like a glimpse into the future. It's not just about adding AI features to a CMS—it's about fundamentally changing how we interact with our content management systems.

The idea that I can now have a conversation with my Umbraco instance, asking it to create members, query data, or potentially manage content, feels almost magical. And this is just the beginning. With this foundation, I could easily add tools for content management, media handling, or even complex workflow automation.

What excites me most is how this bridges the gap between technical and non-technical users. Imagine content editors being able to ask an AI assistant to "create a new blog post about our latest product launch" and having it actually do it, complete with proper categorization and metadata.

If you're curious about MCP and want to explore further, I'd recommend checking out the official MCP documentation. The protocol is still evolving, but the potential is enormous. We're just scratching the surface of what's possible when we give AI assistants direct access to our systems and data.

Top comments (0)