cd /news/ai-agents/beyond-localhost-running-an-mcp-serv… · home › topics › ai-agents › article
[ARTICLE · art-147074] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Beyond Localhost: Running an MCP Server for Kentico Xperience on AWS with Docker

A developer built and deployed a hosted Model Context Protocol (MCP) server on AWS that lets an AI assistant create draft landing pages directly in a live Xperience by Kentico CMS, using ECS, RDS, Docker and an ALB. The proof-of-concept pipeline handles authentication and networking end-to-end so an AI can take a real CMS action without a human clicking through admin screens, though it remains an MVP limited to draft pages with a title, tag, headline and call-to-action button. The author notes Kentico's own KentiCopilot MCP servers are designed to run locally, whereas this project targets a cloud-hosted server any MCP client can reach securely.

by read12 min views4 publishedOct 7, 2026

A few weeks ago I got curious about something: could I actually let an AI assistant create real content on a real website, safely, end-to-end, without a human clicking through admin screens? Not a mockup. Not a "wouldn't it be cool if" thought experiment. An actual working pipeline, in production, on AWS.

This post is the full story of how I built that - the wins, the two-in-the-morning bugs, the moment I almost destroyed my own infrastructure by accident, and the final demo where it all clicked into place. If you work with Xperience by Kentico, MCP, or you're just curious what it takes to wire an AI up to a real CMS, this one's for you.

I'd already built and shipped a Xperience by Kentico site (Dancing Goat, the standard Kentico sample project) on AWS - ECS, RDS, an ALB, the whole production setup. That project was done and live.

Read more about previous article - https://medium.com/@pawansharma_25255/from-localhost-to-live-on-aws-deploying-kenticos-dancing-goat-with-docker-terraform-and-github-14f129734481

But I kept coming back to a question: everyone's talking about "AI agents" doing real work, but what does that actually require under the hood, technically, for a real CMS?

MCP (Model Context Protocol) kept coming up as the emerging standard for this - a way for AI assistants to discover and call "tools" that do real things in the real world, instead of just generating text. I didn't want to just read about it. I wanted to build one, badly, get it wrong a bunch of times, and come out the other side actually understanding the pieces.

So I picked a concrete, scoped goal: let an AI create a draft landing page in my Kentico site. Small enough to be achievable. Real enough to force me through every layer - auth, networking, the CMS's own internals - that a "toy demo" would let me skip.

Here's the honest pitch, no hype: right now, if a marketing person wants a new landing page, someone has to log into the CMS admin, click "New Page," type in the fields, pick a template, save. Every single time. It's not hard, but it's manual, and it doesn't scale, and it can't be delegated to an AI assistant working alongside a human.

This project is a proof of concept for a different model: a marketing person (or an AI acting on their behalf) describes what they want in plain language, and it gets created directly in the CMS - safely, with proper authentication, without anyone hand-typing into admin forms.

I want to be upfront about scope: this is an MVP, not a finished product. It creates draft pages with a title, a tag, and a simple headline and call-to-action button. It doesn't write full page copy, it doesn't handle every content type, and it has some rough edges I'll get into. But the hard part - the plumbing that lets an AI safely reach into a CMS and take a real action - works, end-to-end, in production. That's what this post is really about.

Before I go further, an important note. Kentico now ships its own MCP servers as part of KentiCopilot: a Documentation MCP server (read-only, searches Kentico's docs), a Content Modeling MCP server, and a Management MCP server that can actually create and edit pages and content through the management API - still in preview as I write this. They're good, and for day-to-day development I'd use them.

But they're designed to run next to a local Xperience instance, started by your IDE or AI client. I wanted something different: a hosted MCP server, running in the cloud, that any MCP client can reach securely, so an AI can create pages on a live site without anyone running a local project.

That's what this post is about. It's a proof of concept, not a replacement for the official tooling. I built one tool (create_draft_landing_page) that creates a real page in a real Kentico instance, and I put a secured, containerized path in front of it on AWS.

Within that, there was a second fork: should the MCP server talk to Kentico directly using its content/page management APIs (IContentItemManager, IWebPageManager), or should it call something else?

I initially assumed I could embed those Kentico APIs straight into the standalone MCP server project. That turned out to be wrong - those APIs need to run inside a real, fully-initialized Xperience by Kentico application (DI container, licensing, the works). A separate .NET console-style MCP server can't just import them and go.

So the actual architecture became:

The MCP server doesn't touch Kentico directly at all. It calls a plain internal HTTP endpoint I built inside the Dancing Goat application, and that endpoint is the one running inside the real Xperience by Kentico app, using the real IWebPageManager APIs. This distinction cost me some wasted time early on, but it's the right shape for anyone doing something similar.

This is the part most tutorials skip. Here's what actually went wrong, in the order it happened:

The "confused deputy" IAM trust policy issue. When I gave the AgentCore Gateway's IAM role a plain Service principal, AWS rejected it with "not authorized to assume the execution role." Turns out you need explicit Condition blocks (aws:SourceAccount, aws:SourceArn) - undocumented in the basic examples I found. Took real digging to track down.

A silently-wrong Gateway target URL. Pointing the Gateway target at the bare API Gateway root URL (no /mcp suffix) caused it to enter a FAILED state with a 404 - not obvious from the error alone.

An invisible page-template bug. Pages created through my code looked fine in the content tree, but threw InvalidOperationException the moment anyone tried to open them: "the web page does not have a page template assigned." There was no documented way to fix this through the public API. I ended up creating a page manually through the admin UI, then literally querying the database directly to diff the working page against the broken one, and found the difference sitting in one JSON column (ContentItemCommonDataVisualBuilderTemplateConfiguration). That column wasn't exposed anywhere in the public C# API — I had to set it through IInfoProvider directly.

A near-disaster with Terraform. At one point, a CI pipeline ran terraform plan and showed "22 resources to destroy" - my entire MCP infrastructure: the Gateway, Cognito, API Gateway, the lot. My stomach dropped. It turned out the plan failed before it could apply (a provider schema error saved me), and a later, successful run showed "0 destroyed" — nothing was actually lost. But it was a real reminder: always scope Terraform applies with target, and never assume a green checkmark in CI means "safe," read the actual plan output.

PowerShell quoting and encoding gremlins. A UTF-8 BOM silently added by Out-File broke JSON parsing on the receiving end. Running commands in the wrong order (pasting a whole block where later lines depended on earlier ones executing first) produced confusing, unrelated-looking errors.

A production-only 60-second hang. Everything worked locally. In production, calling the tool would just... hang, for exactly 60 seconds, then fail with a 504. It took pulling the actual application logs (not the MCP server's logs - the Kentico app's logs) to find the real cause: Kentico's Continuous Integration feature was trying to bulk-insert file-tracking metadata into the database as a side effect of page creation, and that insert was timing out. Disabling Continuous Integration fixed it instantly.

AI billing. For the demo chat interface, I originally wired it up to call Claude's API directly — then hit "your credit balance is too low" the first time I actually tested it. For an MVP, I ended up building a free fallback that parses simple quoted phrases out of a sentence instead of calling an AI model at all, just to prove the pipeline without needing to set up billing.

None of these were exotic. They were the normal, grinding kind of bugs that eat a whole afternoon - and I think that's worth writing down honestly, instead of pretending it was all smooth.

Here's everything that ended up in the stack, at a glance:

Amazon ECS (Fargate) - running both the Dancing Goat website itself and the new MCP server, as two separate services in the same cluster

Amazon ECR - two repositories, one per container image

Application Load Balancer (ALB) - public-facing, in front of Dancing Goat

Network Load Balancer (NLB) - internal, in front of the MCP server (API Gateway's VPC Link can only target an NLB or Cloud Map, not a bare ECS service)

Amazon API Gateway (HTTP API) - the internal hop between the Gateway and the NLB, secured with IAM/SigV4

VPC Link - bridges API Gateway into the private VPC

Amazon Cognito - a user pool + app client issuing OAuth client-credentials tokens, used as the inbound authorizer for the Gateway

Amazon Bedrock AgentCore Gateway - the actual front door that MCP clients connect to; validates the Cognito JWT, then signs and forwards requests on with IAM

Amazon RDS (SQL Server) - the database behind Dancing Goat (from the earlier, separate hosting project)

Amazon CloudWatch Logs - for both services; this is what ultimately cracked the production-only hang

AWS Secrets Manager - holding the DB connection string

Amazon S3 - used later to host the free, no-backend HTML demo page as a static website for chat

IAM - roles and trust policies wiring all of the above together securely

Everything's managed as Terraform, organized into small, focused modules rather than one giant file:

modules/networking - VPC, subnets, NAT, security groups (including a new dedicated one for the MCP server, allowing inbound 8080 only from within the VPC)

modules/ecr / modules/ecr_mcp - one repo each for Dancing Goat and the MCP server

modules/ecs / modules/ecs_mcp - two ECS services sharing one cluster; I gave the MCP server its own module rather than reusing the existing one, since the existing one hardcoded an ALB dependency I didn't want here

modules/nlb_mcp - internal NLB + target group + listener for the MCP server

modules/vpclink_mcp - the VPC Link resource connecting API Gateway into the VPC

modules/apigw_mcp - the HTTP API Gateway itself, IAM-authorized, proxying through to the NLB

modules/cognito_mcp - the user pool, resource server, app client (client-credentials flow), and hosted domain

modules/agentcore_gateway_mcp - the Bedrock AgentCore Gateway resource and its target, plus the IAM role with that confused-deputy trust policy fix

One practical lesson from the Terraform side: I bumped the AWS provider from ~> 5.0 to ~> 6.0 partway through (needed for the newer AgentCore resource types), and from that point on, every apply got scoped with -target, specifically to avoid touching the existing, tightly-coupled module.ecs/module.rds resources by accident. Given the near-miss I described above, I'd treat that as a hard rule for anyone doing something similar, not just a nice-to-have.

Phase 1 - MCP server, local only. Built with ModelContextProtocol.AspNetCore, one tool (CreateDraftLandingPage), verified with the MCP Inspector's CLI mode (its web UI had its own unrelated bug, so CLI became the standard way to test throughout).

Phase 2 - containerize and deploy. Standard multi-stage Dockerfile, new ECR repo, new ECS service in the existing cluster, confirmed reachable over the internal network from a bastion host.

Phase 3 - outbound auth (Gateway → MCP server). Chose IAM/SigV4 over rolling my own OAuth validation inside the MCP server - built the NLB, VPC Link, and IAM-authorized API Gateway, confirmed the full chain with a SigV4-signed curl request.

Phase 4 - inbound auth (caller → Gateway). Added Cognito for JWT-based authorization on the Gateway itself, plus the AgentCore Gateway resource and target. Tested with a real Cognito client-credentials token, all the way through to a real tools/call response.

Phase 5 - making the tool do something real. This was the biggest phase. Added an internal controller inside Dancing Goat itself, wired up real page creation via IWebPageManager, hit and fixed the invisible template-assignment bug, wired the MCP server's tool to call this new endpoint over HTTP, deployed both pieces to production, and finally root-caused and fixed the Continuous Integration hang that was silently blocking everything in production only.

Phase 6 - making it demoable. Built a small single-file HTML chat page, first wired to Claude's API (blocked by billing on a fresh account), then reworked into a free version that parses quoted phrases directly, so the whole thing could be demoed without any AI billing setup at all. Extended the tool further to add real page content (a headline and a CTA button, using Kentico's Page Builder widget structure) instead of just an empty templated shell.

(This section is where I'm dropping in the concrete artifacts — swap in your own screenshots from your run-through at each marked point.)

Building and pushing the MCP server image:

docker build -t dancing-goat-mcp:v2 .
aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin <your-account-id>.dkr.ecr.ap-south-1.amazonaws.com
docker tag dancing-goat-mcp:v2 <your-account-id>.dkr.ecr.ap-south-1.amazonaws.com/dancing-goat-mcp:v2
docker push <your-account-id>.dkr.ecr.ap-south-1.amazonaws.com/dancing-goat-mcp:v2

Fetching a Cognito token (client-credentials flow):

$pair = "$clientId`:$clientSecret"
$basicAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($pair))
$headers = @{ Authorization = "Basic $basicAuth" }
$body = "grant_type=client_credentials&scope=mcp-gateway/invoke"
$tokenResponse = Invoke-RestMethod -Uri $tokenEndpoint -Method Post -Headers $headers -Body $body -ContentType "application/x-www-form-urlencoded"

Calling the tool through the Gateway:

curl.exe -X POST $gatewayUrl -H "Content-Type: application/json" -H "Authorization: Bearer $accessToken" -d "@call_request.json"

The template-assignment fix, inside the internal controller (C#):

var commonData = contentItemCommonDataInfoProvider.Get()
    .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentItemID), newWebPageItem.WebPageItemContentItemID)
    .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataIsLatest), true)
    .TopN(1)
    .FirstOrDefault();

if (commonData != null)
{
    commonData.ContentItemCommonDataVisualBuilderTemplateConfiguration =
        "{\"identifier\":\"DancingGoat.LandingPageSingleColumn\",\"properties\":null,\"fieldIdentifiers\":null}";
    contentItemCommonDataInfoProvider.Set(commonData);
}

I'm putting together a full screen-recorded walkthrough of this entire process — the code, the AWS console, the debugging, and the final live demo. Once it's up on YouTube, I'll link it right here:

Category What was used
CMS Xperience by Kentico (v31.6.x), Dancing Goat sample project
Container platform Docker, Amazon ECS (Fargate)
AWS networking VPC, private/public subnets, NAT Gateway, Security Groups, ALB, NLB, VPC Link
AWS auth/identity IAM (SigV4, trust policies), Amazon Cognito (OAuth client-credentials)
AWS AI infrastructure Amazon Bedrock AgentCore Gateway
AWS supporting services ECR, RDS, Secrets Manager, CloudWatch Logs, S3
AI / agent layer Model Context Protocol (MCP), Claude (for the AI-driven version of the demo)
Infra as code Terraform, modularized per component
Languages C# (.NET, ASP.NET Core), PowerShell (testing/tooling), HTML/JS (demo page)

If you're building something similar — an MCP server wired into a real CMS, or an AI agent that needs to safely take real actions in production - I hope the mistakes in section 4 save you a few hours. That was, honestly, most of the actual learning.

Thanks.....See you soon!

── more in #ai-agents 4 stories · sorted by recency
── more on @kentico 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/beyond-localhost-run…] indexed:0 read:12min 2026-10-07 · —