
AI chat inside the Strapi admin that reads and edits your content via MCP, with voice and a side-by-side live preview.
released June 24, 2026
npm install strapi-plugin-mcp-chat
AI chat inside the Strapi 5 admin that actually reads and edits your content โ including fields nested in components and dynamic zones โ through MCP. Comes with voice (speech-to-text / text-to-speech) and a side-by-side live preview of your frontend.
Ask in plain language ("change the homepage hero title to โฆ") and the assistant finds the text across all content-types, edits it, and publishes โ then reloads the preview on the very page you were looking at.
https://github.com/raulbalestra/strapi-plugin-mcp-chat
buscar_texto finds a phrase across all content-types, single types, components and dynamic zones (recursive); editar_campo updates it (preserving the other components); publicar publishes.strapi.manifest.json; the plugin validates it, creates all content-types, seeds content and wires the preview. The manifest can also be inferred from the code (incl. data arrays hardcoded inside components โ collection types), and provisioning then auto-connects the frontend to Strapi via live REST fetch (generates a data layer + rewires components, with a hardcoded fallback). Never runs code from the upload โ it acts only on the validated manifest.MCP Chat is built on Strapi 5's native MCP server and focuses on letting an assistant operate your content end-to-end โ find text anywhere (including fields nested in components and dynamic zones), edit it as a draft, publish it, and translate it โ while you watch the result in a live preview right next to the editor.
A few things that set it apart:
strapi.manifest.json (Next.js / TanStack Start): the
plugin infers the content model, creates the content-types, seeds the data and wires the
preview for you.It's a complement to the rest of Strapi's AI ecosystem, not a replacement โ reach for it when you want the "chat that edits your content with a live preview" workflow.
>= 5.47.0 โ required for the built-in native MCP server that this plugin consumes.npm install strapi-plugin-mcp-chat
# or pull the latest unreleased code straight from GitHub:
# npm install github:raulbalestra/strapi-plugin-mcp-chatOr just try the ready-to-run Launchpad demo (the plugin is vendored there).
config/plugins.ts (or .js):
export default () => ({
'mcp-chat': {
enabled: true,
},
});On register(), the plugin registers its content tools (mcp_chat_buscar_texto,
mcp_chat_editar_campo, mcp_chat_publicar) into Strapi's native MCP server via
strapi.ai.mcp.registerTool. The in-admin chat calls the same functions in-process โ
no admin token or HTTP round-trip needed for the chat to work.
The chat can send a screenshot of your screen (base64) in the request body, so the
default ~100 kb limit must be raised. If you use the live preview, also allow the
frontend origin to be framed. config/middlewares.ts:
export default [
'strapi::logger',
'strapi::errors',
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'frame-src': ["'self'", process.env.CLIENT_URL],
'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io'],
'media-src': ["'self'", 'data:', 'blob:'],
upgradeInsecureRequests: null,
},
},
},
},
'strapi::cors',
'strapi::poweredBy',
'strapi::query',
{ name: 'strapi::body', config: { jsonLimit: '15mb', formLimit: '15mb', textLimit: '15mb' } },
'strapi::session',
'strapi::favicon',
'strapi::public',
];# Required โ the chat and voice features fail without it.
OPENAI_API_KEY=sk-...
# Optional.
OPENAI_CHAT_MODEL=gpt-4o # default: gpt-4o
PLAYWRIGHT_MCP_URL=http://localhost:8931/mcp # enables browser control
STRAPI_ADMIN_URL=http://localhost:1337/admin # used by browser control
CLIENT_URL=http://localhost:3000 # frontend origin (for the preview iframe CSP)The OpenAI key lives only in the server
.envand is never exposed to the browser. The chat endpoints require an authenticated admin session.
The plugin always registers its tools; if you also enable Strapi's native MCP server,
those tools become available to external MCP clients (e.g. Cursor) at
/mcp. In config/server.ts:
export default ({ env }) => ({
// ...your existing server config
mcp: { enabled: true }, // serves /mcp (Streamable HTTP, Admin-token authenticated)
});External clients authenticate with an Admin token (Settings โ Admin Tokens); the MCP session is scoped to that token's permissions. The in-admin chat does not need this โ it's only for letting other AI clients use the same tools.
npm run build && npm run developThe floating chat appears on every admin screen, and MCP Chat is added to the menu.
Set your frontend URL once in the preview panel's address bar โ it's remembered in localStorage.
The preview iframe is cross-origin, so the admin can't see where you navigated inside
it. Drop this tiny client component into your frontend and it will (a) report the
current URL to the admin so reloads return to the same page, and (b) save/restore the
scroll position per page. For Next.js, create components/preview-bridge.tsx:
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export function PreviewBridge() {
const pathname = usePathname();
useEffect(() => {
if (typeof window === 'undefined' || window.self === window.top) return; // only inside an iframe
const key = `preview-scroll:${pathname}`;
try { window.parent.postMessage({ type: 'preview:location', href: window.location.href }, '*'); } catch {}
const saved = sessionStorage.getItem(key);
if (saved != null) {
const y = parseInt(saved, 10) || 0;
[0, 60, 180, 400, 800].forEach((t) => setTimeout(() => window.scrollTo(0, y), t));
}
const save = () => { try { sessionStorage.setItem(key, String(window.scrollY)); } catch {} };
window.addEventListener('scroll', save, { passive: true });
window.addEventListener('beforeunload', save);
return () => { save(); window.removeEventListener('scroll', save); window.removeEventListener('beforeunload', save); };
}, [pathname]);
return null;
}Then render <PreviewBridge /> once in your root layout. The same idea works in any
framework โ just post { type: 'preview:location', href: location.href } to window.parent.
Strapi's Draft & Publish lets editors stage changes before they go live. MCP Chat respects that:
editar_campo always writes the
draft version. By default the agent stops there and tells you it saved a draft โ
it only calls publicar when you explicitly ask ("publish this", "make it live") or
when you turn Auto-publish ON in the chat toolbar. The setting is remembered
(localStorage mcp-chat-autopublish, default off).publish() only exists when it's enabled (calling it otherwise throws, per the Strapi 5 docs). When a type has no Draft & Publish, the edit is the live value โ so the plugin skips publishing (no error) and the assistant tells you the change is already live.?preview=1; provisioned frontends
read that flag and fetch status=draft from the Content API, so you see unpublished
changes exactly as they'll look โ without publishing. Flip to Live to compare with
what's currently public.Draft preview contract (for custom / non-provisioned frontends): when the iframe URL
carries ?preview=1 (or ?status=draft), fetch Strapi with status: 'draft' and an API
token that can read drafts (STRAPI_API_TOKEN). Provisioned frontends get this wired
automatically via the generated data layer (src/lib/strapi.ts + src/hooks/useStrapi.ts),
which reads the ?preview=1/?status=draft flag from the URL. Note: draft fetching keys off
the URL on the client, so with SSR the first server render may show published content
until hydration โ fine for preview, but don't rely on it for production rendering.
register() โโบ strapi.ai.mcp.registerTool โโบ native MCP server (/mcp)
mcp_chat_buscar_texto / _editar_campo / _publicar
(also available to external MCP clients, e.g. Cursor)
Admin (floating chat / full page)
โโ POST /mcp-chat/message โโบ chat service (agent loop, OpenAI)
โโ content tools โโบ same functions, called IN-PROCESS (no HTTP, no token)
โ buscar_texto โ deep, recursive search (components + dynamic zones), returns a `path`
โ editar_campo โ edits the field at that `path`, re-saving the whole top attribute
โ publicar โ publishes the entry
โโ (optional) browser_* โโบ Playwright MCP
โโ POST /mcp-chat/stt ยท /mcp-chat/tts โโบ Whisper / OpenAI TTSThe plugin extends Strapi's native MCP server: in register() it calls
strapi.ai.mcp.registerTool to add its deep search/edit/publish tools, so any MCP
client gets them. The same functions are shared with the in-admin chat, which calls
them in-process โ so the chat needs no admin token and no HTTP round-trip.
buscar_texto returns matches with a path like ["dynamic_zone", 0, "heading"].
editar_campo takes that same path, deep-fetches the entry, mutates the leaf, and
writes the whole top-level attribute back โ keeping component ids (so they're updated
in place, not recreated) and reducing media/relations to ids.
Note:
blocks-type rich text is intentionally not edited (it's structured JSON); string / text / richtext fields at any depth are.
Prerequisites: the plugin installed & enabled,
OPENAI_API_KEYset, and Strapi running indevelop(schema generation is dev-only). See Install.
frontend/โฆ). You can include a strapi.manifest.json at its root, or let
the plugin infer one from the code. Exclude node_modules (the plugin installs deps
when it runs the dev server)..zip โ click Analyze project. The plugin extracts it to a sibling
folder and proposes a strapi.manifest.json (existing one, or inferred from the code โ
including data arrays inside components โ collection types).CLIENT_URL), and
wires the frontend to Strapi via live REST fetch. Don't close the page โ it polls
until done.No
strapi.manifest.jsonand noOPENAI_API_KEY? Provisioning still models the loose UI text as single-types and wires the preview, but it can't infer data collections or rewire components without the key. Add the key for the full "upload โ live" flow.
Bring a "blessed-stack" frontend (Next.js or TanStack Start) carrying a
strapi.manifest.json. The plugin never executes code from the upload โ it
reads and validates the manifest (Zod) and, from it, provisions the backend:
upload โ validate manifest โ extract to ../<frontend>
โ generate src/api/**/schema.json (additive) โ Strapi restarts
โ seed content (Document Service) โ grant public read
โ wire .env + types + preview (admin.ts + CSP + CLIENT_URL)
โ wire frontend to Strapi (live REST fetch)Safety rails: schema generation runs only in develop (a Content-Type
Builder limitation); generation is additive (never drops/alters an existing
type); the frontend always lands in a sibling folder, never inside Strapi's
src/. Ready-to-use starters live in starters/. The manifest can
also be inferred from the frontend code (e.g. Figma/Lovable exports) โ including
data arrays hardcoded inside components, which are modeled as collection types (a
deterministic guard drops any value that isn't found verbatim in the source, so the
AI can only transcribe, never invent).
The last step connects the frontend to Strapi by live REST fetch (not a snapshot), so editing in Strapi reflects in the page:
src/lib/strapi.ts (REST helpers, flat โ no populate/nesting, light calls) and
src/hooks/useStrapi.ts (useSection / useCollection, preview/draft aware).{c.field ?? "original text"}), leaving icons/assets/layout untouched..bak; on any doubt the file is left as-is, so the frontend
never fails to compile because of the plugin. It only writes inside the frontend
folder โ never touches Strapi.POST /mcp-chat/frontend/wire. It's idempotent (components already wired are skipped).Needs
OPENAI_API_KEYfor the two AI steps (inferring collections + rewiring components). Without the key, provisioning still creates the text single-types and generates the data layer, but does not rewire the components (the frontend stays hardcoded).
Ask the chat to translate and it creates the locales and translates every localized field, using Strapi 5's native i18n โ without the two failure modes of typical translation plugins:
Tools (in the chat and via the native MCP server): criar_locale,
listar_locales, traduzir, habilitar_i18n, plus a locale option on
editar_campo/publicar. In a manifest, mark localized: true on the
content-type and the fields to translate; the generator emits
pluginOptions.i18n.localized at both the content-type and attribute level.
Validated end-to-end against a real Strapi 5 (14 locales, long multi-chunk text
and translation inside repeatable components).
The plugin follows the documented Strapi 5 plugin APIs:
server/src/index.ts) exports the documented shape
(register / bootstrap / destroy / config / controllers / services / routes).
Routes are declared with type: 'admin', so the chat/STT/TTS endpoints require an
authenticated admin session.defineTool helper
(server/src/mcp/define.ts) and registered from an array via
strapi.ai.mcp.registerTool during register(), using z from @strapi/utils for the
schemas and auth.policies (content-manager read/update/publish) for RBAC. The
defineTool/defineResource/definePrompt identity functions infer each handler's
args from its input schema (no any) and keep the definitions side-effect-free โ
mirroring the direction of Strapi's PR #26603
(ai.mcp.defineTool + the import { ai } from "@strapi/strapi" namespace). When that API
ships stable, migrating is a one-line import swap; until then the plugin stays on the
released registerTool so it runs on any Strapi โฅ 5.47 (no experimental build required).register() uses only documented APIs (app.addMenuLink, app.registerPlugin).One intentional deviation: the global floating chat is mounted via its own React root
in bootstrap(). Strapi's documented injection zones are Content-Manager-specific, and there
is no official zone for an admin-wide overlay โ so this is the only way to render a widget on
every screen. It's isolated and idempotent (guarded by an element id) and contained to that
single spot.
auth.policies (content-manager read/update/publish).
When exposed to external MCP clients, the session is scoped to the connecting Admin
token's permissions โ scope the token to only what those clients should change.This plugin is built to be a good citizen: installing it must never slow down or take down the host app. Concrete guarantees:
register() degrades gracefully if the native MCP server / i18n /
OpenAI key are absent (logs a warning, disables the feature). MCP tools register inside a
per-tool try/catch, so a single bad tool can never abort the others or the boot.
destroy() is best-effort.MIT ยฉ Raul Balestra
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
