Two months ago, the story was innovation arriving on top of quiet quality work, with the release of the MCP server. This window, that innovation grew up. Across v5.49 through v5.52, the MCP server stopped being something you use and became something you build on: typed builders for plugin authors, and an audit trail for everything an agent touches.
Underneath it, the quality work kept going. Across these releases we merged 366 pull requests, and more than seventy people are named in the thank-you sections.
Here's what changed.
The MCP server became something you can extend
Plugin authors get typed builders
Shipping an MCP server is one thing. Letting plugin authors add to it is another, and until v5.49.0 you couldn't do it comfortably: defining a tool meant hand-typing definitions against Modules.MCP.* from @strapi/types, with no inference and no narrowing. v5.49.0 exposes ai.mcp.defineTool, ai.mcp.defineResource and ai.mcp.definePrompt from @strapi/strapi under an ai namespace, mirroring the runtime strapi.ai.mcp API (#26603). You define a tool with real TypeScript inference, then register it in your plugin's bootstrap with strapi.ai.mcp.registerTool().
MCP actions now land in your audit logs As of v5.52.0, if an agent is writing to your content through MCP, that's now traceable alongside everything a human does (#27151), which is the difference between an interesting feature and one you can put in front of a compliance review.
The surrounding work matters too. MCP relation output is now an identity-only shape, so responses stop carrying entire related documents. Two content types in the same API folder no longer resolve to the same tool name, which used to throw on startup and take the whole app down with it. And the MCP server now has contributor documentation, if you want to add to it (#26560, #26710, #27357, #27160). One related fix: AI-generated localizations now respect field length constraints instead of overflowing them, in v5.51.1 (#26880).
You can finally see who is logged in
Active devices
If an admin account was compromised, your only real option used to be a global logout. Active devices arrived under the profile menu in v5.50.0 (#26628). It lists every session on your account with a device label, when it started and when it was last used, marks the one you're on, and lets you revoke one or all of them. Users & Permissions apps running in refresh-token mode get the same through GET /auth/sessions and DELETE /auth/sessions/:sessionId. SSO logins are covered as of v5.51.0 (#26872, #27100): those sessions carry a device name and login time too, so they sit in the list identifiable alongside password logins, and revoking one genuinely ends it rather than leaving a ghost row behind.
Admin password-reset tokens now expire
Before v5.51.0 they never did, and resetPassword never checked a token's age (#27020). A reset link that leaked through an intercepted email, a log file, or an inbox someone else could read stayed valid indefinitely. Tokens now carry an expiry, one hour by default and configurable through admin.forgotPassword.expiresIn. Tokens issued before the change are rejected and cleared, so an old link can't be retried. Thanks to jasleenkaur-qed42 for this one.
In the Content Manager
Publishing an entry that points at unpublished entries now warns you first v5.50.0 gives you a dialog telling you the draft relations won't be included in the published version, and you can cancel or publish without them. It works from the edit view and from bulk publish in the list view. Later releases narrowed it to the cases that deserve a warning, so the dialog now stays quiet when the relation is already published, when you disconnected it earlier in the same edit, or when it errored (#26736, #26858, #26871, #26900).
Relation ordering got a sustained pass Reordering an item to the start of a list now holds. Many-to-many order survives publishing, and self-referential order and state survive a child publish. Reordering and removing a relation in the same save no longer crashes, order stops drifting after you save dynamic-zone components, and a reorder that saved the wrong position outright is fixed (#26112, #26791, #26838, #26932, #26210, #27135, #27444).
Creating a relation no longer eats your unsaved work Add a component to a dynamic zone, type a title into it, then create a new related entry from that component's relation modal before saving the parent. The parent was written back using the form data it had when you opened it, so the component you'd just added, and everything you typed into it, quietly disappeared. It survives now, as of v5.51.2 (#27081).
List rows are real links Rows navigated through a JavaScript click handler with no link underneath them, so cmd-click, middle-click and right-click into "Open in new tab" all did nothing. As of v5.50.1 the first text column of each row is an actual link, which brings all of that back and shows the destination on hover, and there's an explicit "Open in new tab" item in the row's three-dots menu (#26853).
Components in the dynamic zone picker can carry a screenshot
There's no Content-Type Builder field for it yet, so it happens in the schema. Add a preview key to a component's info block in its schema.json, pointing at an image committed with the app or at a CDN URL, and the picker shows a thumbnail in place of the component icon, with a larger preview on hover (v5.51.2, #26863). Components without one keep their icon, so you can do this for the handful your editors mix up and leave the rest alone. Thanks to dominik-juriga for this one.
Keyboard shortcuts changed In v5.49.0, Cmd/Ctrl+Enter now saves a draft. To publish, use Cmd/Ctrl+Shift+Enter (#26621). Since v5.31.3, plain Cmd/Ctrl+Enter published immediately, so if that's in your muscle memory, retrain it now rather than during a live edit.
The new Media Library is in beta
v5.52.2 ships a full rework of the interface rather than another pass of fixes. The parts you notice first:
Folders live in a sidebar tree. Create, rename and navigate them there, and drag assets onto a folder to file them, including deep onto a nested one.
Search, sort and filter arrive, across assets and folders alike, so finding something stops depending on remembering where you put it.
Assets open in a modal, where you can look at the image properly, crop it and set a focal point.
The upload dialog follows you through the app. Start a large batch, move to another screen, and it keeps going with the queue and progress still visible.
It works on a tablet or a phone, though some image-editing gestures aren't there yet.
It sits behind a flag until GA on 16 September. Set betaMediaLibrary: true under future in config/features and restart. The legacy Media Library stays the default, and stays available behind a flag after GA.
Uploads
Replacing an image now actually replaces it
If your files sit behind a CDN, replacing one in the Media Library could leave the old version being served until the cache expired, with the admin panel and your API both showing the file you just got rid of. Any CDN with a cache in front of it could do this; DigitalOcean Spaces is the sharpest case, because it forces a one-hour cache you can't shorten. The cause was that a replacement reached the provider as a delete followed by an upload, with nothing to signal the two were one operation, so a provider that could have invalidated its cache never got the chance. As of v5.49.0, providers can implement replace, and all three first-party ones do: Cloudinary overwrites and invalidates in a single call, S3 reuses the key, local writes in place (#26582). Providers that don't implement it fall back to the old delete-and-upload.
Your upload size limit was not enforced on replace
sizeLimit was only checked when a file was created. Replacing an existing file's binary skipped the check entirely, through both media libraries and the content API, so a file over your configured limit was written to the provider and persisted anyway. In v5.52.2 the check runs before anything is written, and an oversized replacement fails with a 413 leaving the original in place (#27414).
Large uploads stop failing during type detection Checking a file's type meant reading the whole file into memory first, which could exceed Node's buffer limit and fail the upload before validation had a chance to run. Since v5.49.0, Strapi reads only the first 4100 bytes, which is all the check ever needed (#26678). Thanks to singhvishalkr for this one.
Uploads survive a credential rotation Plenty of AWS setups deliberately avoid permanent access keys and use short-lived credentials that expire and get reissued on a schedule. Strapi read those once when it started and never looked again, so the moment they rotated, every upload failed with Access Denied until somebody restarted the app. As of v5.50.0 you can hand the S3 provider a function that fetches credentials rather than a fixed pair, and it fetches again whenever they change (#26796). Permanent keys keep working exactly as before. Thanks to ivseb for this one.
Roles and permissions
A missing permission condition no longer locks people out Conditions narrow a permission to a subset of entries: only the ones you created, say, or only the locales you have access to. They're registered in code, mostly by plugins, and once you tick one on in Settings, the role holds a reference to it by name. Disable that plugin and the reference points at nothing. Strapi rebuilds what you're allowed to do on every admin request, so a reference it couldn't resolve didn't break one screen, it broke all of them: everyone holding that role was locked out of the admin, while super admins, who skip permission checks, saw nothing wrong. As of v5.52.0, conditions that no longer resolve are skipped, and a permission left with none is denied (#27282). You lose that one permission instead of the whole panel.
One conditional permission no longer takes the others down with it A role usually holds several permissions on the same content type, and typically only some of them carry a condition. You might grant read on everything, for instance, while gating update behind one. To decide whether you could open that content type at all, Strapi collapsed the whole set into a single verdict, so one condition evaluating false failed the lot. An Editor with unconditional read would open Articles and get the full-page "You don't have the permission to access that content" screen, purely because a conditioned update sitting alongside it didn't apply to them. In v5.52.1, each permission is judged on its own (#27328).
Behaviour changes worth reading twice
Four changes in this window happen to you whether you act or not. Nothing here needs work on upgrade unless the description matches your setup, but the first two are the ones that catch people.
An empty gallery now comes back as [] rather than null
Ask the API for an entry whose multiple-media field has nothing in it and you used to get null, so any client calling .length or .map on it crashed. As of v5.51.1 it's an empty array, the same as every other to-many relation has always returned (#27090). If a client, webhook or integration checks field === null to mean "no images", change it to !field?.length. The same applies to any custom morphMany attribute in your schemas. This one arrives on upgrade regardless of what you configure.
Percent signs and underscores in filters are now taken literally
$contains, $startsWith, $endsWith and their case-insensitive variants passed your value straight into a SQL LIKE, where % means "anything" and _ means "any single character". So a search for 50% quietly matched every value containing 50, and a value ending in a backslash could come back as a 500 from Postgres instead of results. In v5.51.2 those characters are escaped, so they match themselves, and $eqi and $nei stopped using LIKE altogether in favour of real case-insensitive equality (#26476). If anything you run puts % in a filter value as a deliberate wildcard, that stops working. Thanks to masonmcelvain for this one.
Invalid sort parameters now return a 400 instead of a 500 The response code on an existing endpoint changed in v5.50.1, so anything that treats a 500 as retryable and a 400 as final will behave differently against the same bad request (#26907). Thanks to santichausis for this one.
String fields default to a maximum length of 255 in v5.51.0 (#26128), and a custom status field alongside Draft & Publish now logs a startup warning instead of failing boot in v5.50.2 (#26890). The Content-Type Builder still blocks the combination when you try to create it. The maxLength default came from Akash504-ai.
Deploying and upgrading
New projects reject SVG uploads
The generated upload policy allows image/*, which quietly includes SVG, and an SVG is markup that can carry scripts. Serve one from your own domain and it runs with your site's privileges. New projects created on v5.52.2 deny image/svg+xml explicitly (#27360). Existing projects are untouched, and you can drop that entry if you do want SVG. It joins the rest of the scaffold hardening from v5.50.0 (#26737): refresh-token auth with httpOnly cookies, upload allow and deny lists, webhooks that no longer populate relations, a generated JWT_SECRET, and strict parameter handling.
A missing secret now stops startup instead of surfacing later
Deploy a new environment, forget one variable, and Strapi booted regardless. The variable came through as undefined, config load carried on, and the app looked healthy until something that needed that secret failed in a way that never mentioned it. Wrap it in env.required(), or env.array.required() for a list, and Strapi stops at config load and names the variable that's missing (v5.50.0, #26830). New projects already do this for the JWT salt, the encryption key and app keys, and you can add it to a config you already have.
You can rename the admin access-token cookie via admin.auth.cookie.name (v5.50.2, #26931). This unblocks the case where another app on a shared parent domain sets its own jwtToken cookie and breaks admin login. Thanks to aliataf for this one. In the same release, @strapi/upgrade started offering to pin ranged @strapi/* versions before upgrading, so an upgrade stops reporting "already up-to-date" when node_modules had resolved ahead of package.json (#26929).
For developers
Required relations and media are now enforced on publish
A relation or media field marked required was never actually enforced, so an entry could be created and published with it empty. It's enforced now, at publish time only, which keeps drafts free to be incomplete while you work, and required is supported on relation attributes in the Content-Type Builder (v5.51.1, #27028; v5.51.2, #27080). The switch is strictRelations, set under documents in config/api, and because it sits on the Document Service it covers REST, GraphQL and the Content Manager alike. New projects have it on. Existing projects are untouched until you turn it on yourself. We'll have more to say about this one shortly.
If you run MySQL, two admin screens could refuse to load Audit log rows and history versions each carry a JSON payload, and selecting that payload while sorting by date made MySQL and MariaDB pull the payloads into the sort buffer. Past the default 256KB buffer the query died outright and the screen showed an error rather than a list. In v5.52.2, both queries fetch ids first and reassemble the rows afterwards (#27410, #27394). Thanks to lazerg for this one.
SendGrid works from the EU now
SendGrid issues EU-residency API keys that only authenticate against its EU endpoint, and Strapi's provider always talked to the global one, so those keys came back Unauthorized with no way to fix it. Set region: 'eu' in the provider options (v5.50.0, #25907). Thanks to arthurhamon for this one.
A handful of smaller ones: new Strapi apps scaffold with strict TypeScript enabled, generated APIs land in named directories, the data transfer CLI gained --exclude and --only content-type filters, GraphQL mutation inputs now include private fields, and database lifecycle event types are exported (#26779, #26354, #26915, #26489, #25637). And if you turned on the OpenAPI spec endpoints that landed in v5.48.0, v5.50.2 added bearerAuth and bracket-style pagination query parameters to the generated spec (#26948).
The people who shipped this
Thank you to everyone who filed an issue, reviewed a pull request, or shipped a translation across this stretch. Japanese and Korean admin translations were completed, Polish was updated, and locale codes arrived for Corsican, Abkhazian, Adyghe and Kabardian (#26855, #26941, #26592, #27099, #26255). Those are contributions from people who wanted Strapi to work properly in their own language, and they're among the most-used changes we ship.
More of this window came from outside the core team than the paragraphs above can carry. Alongside those already named: tewson, akash-dabhi-qed, MaksZhukov, abaza738, mturac, dijedontahiri, TheMatrixan, moduvoice, PetruMagdeleine and lazerg.
This post covers the changes most likely to matter in your day, but it's a curated slice. To see the full list of changes, visit the release notes:
- v5.49.0 · 24 June 2026
- v5.50.0 · 2 July 2026
- v5.50.1 · 8 July 2026
- v5.50.2 · 15 July 2026
- v5.51.0 · 23 July 2026
- v5.51.1 · 29 July 2026
- v5.51.2 · 5 August 2026
- v5.52.0 · 12 August 2026
- v5.52.1 · 19 August 2026
- v5.52.2 · 26 August 2026 · Media Library beta
- v4.26.2 · 9 June 2026 · final Strapi 4 release

