
Promo Core admins can now manage their website content by talking to Claude, ChatGPT or Cursor. They don't need to click through the dashboard, and the AI never gets more access than the admin who connected it.
This article follows up on my MCP presentation. It covers what MCP is, why we built it into Promo Core, how it works from the admin's side and the backend side, and how we keep it safe. At the end I've added a few lessons from building it that didn't fit in the talk.
MCP (Model Context Protocol) is an open standard that lets AI apps use outside systems as tools. Anthropic introduced it in November 2024. Claude, ChatGPT, Cursor and VS Code now support it.
On its own, an AI model can only write text. It can draft an article for you, but you still have to copy it into the dashboard yourself. MCP lets the AI take real actions instead, like creating that article as a draft in our CMS.
Before MCP, every AI app had its own plugin system, so each tool had to be integrated separately with each app. With a shared standard, we write one server and it works in every app that supports MCP. People often call it "USB-C for AI".
There are three players:
Each tool has a name (like create_article), a plain-English description, and the inputs it expects. The model never sees our code. It only reads those descriptions and picks the right tool, so writing good descriptions is a big part of the work.
MCP is used well beyond CMSs: developers connect it to repositories and CI, companies connect it to Jira, Slack and Notion, and support teams use it to fetch tickets and draft replies. Our use case is content management.
We gave our CMS a new interface, not a new backend. Four reasons drove it:
Connecting takes about a minute and happens once. After that, the admin just talks to the AI.
/mcp door, it can't be used to log in to the dashboard, and it expires and refreshes on its own.Here is what that looks like in practice:
| The admin types | The AI calls |
|---|---|
| "List draft articles" | list_articles |
| "Create a FAQ about pricing" | create_faq |
| "Upload this image to media" | upload_media |
| "Show submissions for form 7" | list_form_submissions |
| "Publish project #12" | publish_project |
| "Update the About page title" | update_page |
The admin never needs to know these tool names. New content is created as a draft by default, so a person still reviews it before it goes live. Publishing is always a separate, explicit step.
The MCP layer is a new door into our existing NestJS backend, not a second backend. The dashboard and the AI end up calling the same service methods, with the same validation and the same permission checks. What differs is the entrance (/api/admin/... with a dashboard token, or /mcp with an MCP token) and who decides what to call: a person clicking buttons, or the model reading the tool list.
/api/admin/... with a dashboard token → validation and permission check.POST /mcp with an MCP token, checked by McpAuthGuard → createMcpServer(userId) with only the tools this role allows → validation and permission check.articleService.create()) → Prisma → PostgreSQL.The only extra step on the AI side is building a per-admin MCP server. From the permission check down, both paths run the same code.
One tool call goes through five steps:
POST /mcp. Everything goes through a single endpoint, built with the official MCP TypeScript SDK and its Streamable HTTP transport. A tool call is an ordinary HTTP POST with a JSON-RPC body.McpAuthGuard. A dedicated Passport strategy verifies the token. Its audience must be our MCP URL, its scope must include mcp, and its mcpTokenVersion must match the user's current value in the database.createMcpServer(userId). We build a fresh MCP server for this admin and register only the tools their role allows. If they have no delete permission, the delete tools are never registered, so the AI doesn't know they exist.permissionService.assert() checks the specific permission, such as CREATE_ARTICLE.articleService.create(), which writes to PostgreSQL through Prisma.This was the most common question after the talk. There are three reasons:
userId it was built with, so nothing can leak between users.sessionIdGenerator: undefined). In that mode the SDK expects a new server per request, because two clients can send the same JSON-RPC message ID at the same time and would collide on a shared server.Building the server is cheap: registering tools only creates objects in memory. The real cost is a few database lookups for permissions, which is fine at today's AI traffic levels.
We didn't hand-write every tool. Each content module is described by one config object, and a generic registrar turns it into up to seven tools: list, get, create, update, publish, unpublish and delete.
{
resource: 'article',
permissions: { view, create, update, delete },
service: articleService,
createSchema, updateSchema,
publishField: 'published',
}26 modules are configured this way, which gives up to 174 generated tools. Around 90 more are hand-written for work that isn't plain create/read/update/delete: media upload, form versions, email templates, push notifications, roles and admins, settings, and import/export jobs. Adding a new content module to MCP is mostly a matter of adding its config entry and schemas.
The AI gets exactly the access the admin gives it, and the admin can take it back at any time. Four guarantees make that true:
| Guarantee | What it means | How it's enforced |
|---|---|---|
| Consent required | No AI can connect until an admin logs in and approves it | OAuth 2.1 with PKCE (S256 only), plus an allowlist of redirect URLs |
| Separate MCP token | A dashboard token can't reach /mcp, and an MCP token can't call the dashboard API | MCP tokens carry their own audience, an mcp scope and their own version counter |
| Role permissions apply | If your role can't create articles in the dashboard, the AI can't either | Every tool checks its permission before running; tools you can't use aren't registered |
| Easy to revoke | One click disconnects every AI client without logging the admin out of the dashboard | Revoking increments mcpTokenVersion, which invalidates all existing MCP tokens for that admin |
We also seed an AI Editor role with view, create and update permissions on content but no delete permissions. Giving an AI connection this role means it can draft and edit but cannot destroy anything. Tools that do delete are marked as destructive, so AI apps can ask the user to confirm before calling them. Tool calls are logged with the tool name and the user ID.
The AI never touches the database directly. It can only call the same functions our own UI calls, and every call passes through the same layers as a dashboard request:
The MCP layer contains no raw SQL at all, so there is no query string for SQL injection to target. It also contains no business logic of its own. When someone fixes a bug or adds a rule in a service, the dashboard and the AI both get the change automatically.
These didn't fit in the presentation, but they'll help anyone who builds an MCP server.
list_options first to get valid category types". Then the AI looks up valid values itself instead of guessing.MCP is a safe bridge between AI and our CMS. The AI works as the admin who connected it, with that admin's consent and permissions, and access can be revoked at any time. It isn't a second CMS: it uses the same services and follows the same rules.
To try it, open the Connectors page in the Promo Core dashboard and follow the guide for Claude, ChatGPT or Cursor. Questions and ideas are welcome, so reach out to me any time.