Repository navigation
MCP integration: laravel/mcp, shared entry title rule and AI assistant MCP tests - #668
uldisrudzitis wants to merge 25 commits into
Conversation
laravel/mcp moves from a dev-only dependency to require, so production installs can serve the AI assistant plugin's MCP endpoint. Adds tests for the MCP endpoint auth (site JWT in X-Authorization), the list-sections tool, SiteContentReader and the chat prompt's sections text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Tip 🎯 Customize your risk assessments
✅ Automatic approvals, with your rules
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
SectionEntryRenderService::showsEntryTitles() holds the rule "entry titles are shown on the default template and in portfolio sections", used by the renderer and the AI assistant plugin. Tests cover the chat's entries tool output, SiteContentReader entries (title rule, unknown and ../ section names) and the list-entries tool. New linkBertaEngine() and writeTemplate() test helpers; the shop test root now uses linkBertaEngine() too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the chat's GetEntryGalleryTool output, covers SiteContentReader::entryGallery() (untyped files, entries without a gallery, unknown or ../ sections and entries) and the get-entry-gallery tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the GetEntryContentTool output on the default template, checks that titles are left out on templates without entry titles, and that entryContent() matches the entry list and rejects unknown or ../ sections and entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…text Covers SiteContentReader::siteSettings() (titles, unknown keys, list items, internal group left out, siteTexts kept) and the get-site-settings tool. A snapshot pins the AI chat's current site settings prompt text ahead of moving it onto the shared reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phpunit.xml and Pest pick up app/Plugins/*/tests, so the AI assistant plugin's tests move to its repository. Core gets its own test for the entry title rule, which only the plugin's tests covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
givePlanFeatures() puts a test website on a hosting plan with the given features and limits, as UserModel reads them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor's settings and design endpoints saved any value they were sent. They now check it against the setting's definition with the editor's own rules (allowed values, colors, CSS units, blank values, validation kinds) and the plan feature a setting needs, and answer 422 with error_message otherwise. Entries of list settings are left as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The specs used pointerId 1, the real mouse's id in Chrome, so moving the mouse over a visible test page could join a synthetic drag and, rarely, stop the auto-scroll spec from scrolling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every editor API response carries the website's latest change (ChangeMarker, in Laravel's cache). A save from an editor window that missed a change made elsewhere - in another window or by an AI agent - is refused with 409, and the editor asks to reload, also when the tab comes back into view. Login, logout and an empty marker never ask. The session token is renewed at most hourly while the editor is used, keeping its claims, for up to 30 days after login and only while the account hasn't changed since (password change, website transfer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # _api_app/tests/Pest.php
Removes script elements, event handler attributes, iframe documents and javascript:/vbscript: links, as the editor's rich text field does, and keeps everything else (forms, embeds, styling). HTML with nothing to remove is returned unchanged. Used for HTML that AI agents write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…renamed or deleted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- UndoFiles: one folder per change in storage/_undo_files, where the AI plugin keeps what an agent deletes - Every web request removes kept files older than 30 days, at most once a day; without kept files the check is a single stat call - .htaccess and LocalValetDriver.php refuse the folder, and site clones and previews skip it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…files when one is deleted - EntryGallerySettings mirrors the editor's gallery editor: each setting's values, input rules and the gallery types and templates it's shown for, plus the file fields (caption, video autoplay) - SettingValueValidator checks toggles against their values, like selects - Deleting a gallery file whose folder another gallery refers to keeps the file, so that gallery doesn't lose it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…image's own copies - SectionBackgroundSettings mirrors the editor's background gallery editor: each setting's values, input rules and the templates that show backgrounds (messy), which the background renderer now uses too - Deleting a background image whose folder another gallery refers to keeps the file, as for entry galleries - One exact rule for an image's resized copies (Storage::resizedCopies()): deleting 1.jpg no longer deletes the thumbnails and sizes of 11.jpg, nor a.jpg those of banana.jpg, in galleries, backgrounds and settings images - Deleting a video also deletes its poster image, unless another file of the gallery uses it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngs file right away - SitesDataService::restore() adds back the record of a deleted subsite whose folder is in place again, the counterpart of delete(), so undoing a subsite delete is a core operation - delete() refuses the main site, whose empty name would point at the folder of all subsites - A new, empty site gets settings.xml with the default template when it is created, as the engine wrote it on first load, so its setup can be finished without opening it first Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or the chat's undo address - EditorSync takes a MARKED_BY_HANDLER parameter for routes whose handler marks the changes it makes, e.g. the AI chat: a window that missed a change is still refused, but the route marks no change of its own, and the answer carries the latest marker - The state lists the AI chat's undo address (aiUndo) when the plugin offers it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts changes natively - The chat sends the message, the conversation and the site; each reply keeps the changes it made, with an Undo button that undoes them on the server without the AI, and says why when they were changed since - AiChangesMirrorService swaps the fresh data the server sends into the store and re-renders the preview with the triggers the editor's own edits use, reloading it only where they do; the old client-side applying is removed - New store actions replace a site's sections and a site's section entries; SiteContentRefreshedAction re-renders the menus and entries in all four templates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A reply that wants to delete something shows its summary with Approve and Reject buttons; approving runs the delete on the server without the AI or quota, shows it in the editor and preview like any chat change, and lets the reply's Undo restore it; a request that expired says so - The state lists the chat's approve address (aiApprove) when the plugin offers it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of the berta.me MCP integration. AI agents (Claude, ChatGPT, Cursor…) connect to the hosting app's MCP gateway (berta-cms/berta-hosting#42), which forwards site requests to each site's own MCP server. That server lives in the AI assistant plugin (companion PR in
berta-cms/ai-assistant-plugin, branchmcp-integration).Draft: the
mcp-integrationbranch keeps growing until the integration is finished.Changes
laravel/mcp^1.0 moves torequire(v1.1.1). It was only a dev dependency, so production installs couldn't serve the plugin's MCP endpoint.SectionEntryRenderService::showsEntryTitles($templateName, $sectionType). Titles show on thedefaulttemplate and inportfoliosections. The renderer now uses it (rendering is unchanged), and so does the plugin'slist-entriestool.SectionEntryRenderService::acceptsEntries($sectionType): every section type exceptexternal_linkandshopping_cart. The editor's "Move entry to" list now uses it (the list is unchanged), and so do the plugin'screate-entryandmove-entrytools.phpunit.xmlhas aPluginstest suite forapp/Plugins/*/tests, andtests/Pest.phpbindsTestCasethere too. So each plugin ships its tests, and an install without plugins simply has none (checked: the core suites run on their own without warnings).SiteContentReader, chat tools and prompt, quota) moved to its repository, together with the chat's settings prompt snapshot.givePlanFeatures()(intests/Pest.php) puts a test website on a hosting plan with given features and limits, asUserModelreads them. The plugin's multisite and AI chat tests use it.linkBertaEngine()andwriteTemplate()stay intests/Pest.php: they set up a temporary Berta root, and plugin tests use them.setUpShopTestRoot()now useslinkBertaEngine().mcp-integrationbranch makes itssetUpShopTestRoot()use this branch'slinkBertaEngine(), so it must be merged together with this PR.SiteSettingsController::update(),SiteTemplateSettingsController::update()) saved any value they were sent, so a crafted request could store a value outside a select's list, or a setting that needs a plan feature the website doesn't have (e.g. thecustom_javascriptHTML fields).App\Configuration\SettingValueValidatorchecks a value against its definition with the editor's own rules (validation.service.ts,setting.component.ts): allowed values of select, font and toggle settings, colors (hex,rgb(), and thergba()the color picker saves), blank values unlessallow_blank, CSS units and thevalidationkinds, plusrequires_feature. Formats the editor doesn't validate (url, longtext, richtext, files, editor actions) take any value.error_message, the editor's existing error shape: it logs the error and keeps the field as it was. Entries of list settings and paths without a definition are saved as before.App\Shared\ChangeMarkerkeeps the website's latest change (a token, the editor window that made it, and its source) in Laravel's cache, not in the site'sstorage/folder that backups and transfers copy. It never expires. A cache that doesn't work (e.g. the first request after updating an install from beforeCACHE_STORE) turns the checks off instead of breaking the editor.EditorSyncmiddleware on the editor's route groups: every response carries the latest change (X-Berta-Change,-Writer,-Source). A save from a window (X-Berta-Editor, hex id) that hasn't seen a change someone else made since is refused with 409. A window's own quick saves pass. Successful saves mark a change; responses witherror_messagedon't. Password change and theme preview opt out, since they change no site data. When someone else marks a change while a save runs, the window is told about it, so it reloads instead of missing it.GET v1/change-markerfor the editor's check when a tab comes back into view.EditorSync::MARKED_BY_HANDLER: a window that missed a change is still refused, but the route marks no change of its own, and the answer carries the latest marker. The state lists the chat's undo address (aiUndo) when the plugin offers it.shared/editor-sync.ts): an interceptor sends the window id and the last seen change to the editor API only (same origin), and reads the answers. A change by someone else shows a modal warn popup, "This website was changed by an AI agent / in another editor window. Reload to see the changes.", that comes back if another popup replaces it. Login and logout start over (the login loads all data fresh), and an empty marker (e.g. a cleared cache) never asks. The 409 skips the generic error toast.App\User\EditorTokenissues the token (used byAuthController).RenewEditorTokenmiddleware renews a valid token older than an hour, at most hourly, and returns it inX-Berta-Token(withCache-Control: no-store) and in thetokencookie. The editor stores it (UserTokenRenewedAction).uid, and are signed with the same key, so hosting-issued and renewed tokens stay interchangeable.auth_time), and none for sessions that started before the account last changed (engine/config/inc.conf.phpis rewritten by a password change, and by hosting on a website transfer or email change). So a previous owner's or a stolen session still ends within 24 hours, as before.App\Shared\HtmlCleanerremoves what can run scripts: script elements (also in SVG),on…event attributes, iframesrcdocandjavascript:/vbscript:links (also disguised with tabs or entities). It keeps everything else, as the editor's rich text field (TinyMCE) does: forms and PayPal buttons, iframe embeds, styling. HTML with nothing to remove is returned unchanged. Built on PHP 8.4's own HTML5 parser, no new dependency. The AI plugin cleans the HTML agents write with it; the editor's own endpoints still trust the editor's client-side cleaning and could adopt it later.<name><id>; before, the names grew with every entry (works1,works12,works123…). A name that exists or another gallery refers to is skipped, so two entries never end up sharing a folder (renaming onto an empty folder used to replace it). The background folder keeps its old name when it can't be renamed, instead of the section pointing at a folder that doesn't exist.works-1with all its folders.Storage::mediaFoldersReferencedOutside()), so folders that older sites already share never lose files.copyFolder()reports failures. An emptymediafolderno longer makes a move or a section copy duplicate the whole media folder. A failed section copy returns the editor's usualerror_messageinstead of a server error.Storage::resizedCopies():_smallthumb_,_bg_,_grid_and_<w>x<h>_plus the exact file name). Before, deleting1.jpgalso deleted the thumbnails and sizes of11.jpg, and replacinga.jpgthose ofbanana.jpg, in galleries, backgrounds, crops, video posters and settings images, so images lost their editor thumbnails or backgrounds. The two copies ofdeleteMedia()are gone; everything usesremoveImageWithThumbnails().App\Shared\UndoFiles, one folder per change instorage/_undo_files..htaccessandLocalValetDriver.phprefuse/storage/_undo_files/, so a deleted image is gone from the site. Site clones and previews skip the folder, as they skip_shop_files..htaccessbefore the plugin's delete tools are used.App\Sites\Sections\Entries\Galleries\EntryGallerySettingsmirrors the editor's gallery editor (entry-gallery-editor.component.ts): each setting's values and input rules, the gallery types and templates it's shown for, and the file fields (caption, video autoplay). The AI plugin's gallery tools check against it; the editor keeps its own copy for now.App\Sites\Sections\SectionBackgroundSettingsmirrors the editor's background gallery editor (background-gallery-editor.component.ts), with the templates that show backgrounds (messy);SectionBackgroundGalleryRenderServiceuses it instead of its own list.SitesDataService::restore()adds back the record of a deleted subsite whose folder is in place again, the counterpart ofdelete(), so the AI plugin undoes a subsite delete through core.delete()refuses the main site, whose empty name would point at the folder of all subsites. A new, empty site getssettings.xmlwith the default template when it's created (SiteSettingsDataService::createDefaultFile()), as the engine wrote it on first load, so its setup can be finished without opening it first.editor/src/app/ai-assistant/): it sends the message, the conversation and the site to the plugin's new chat, which works through the MCP tools. Each reply keeps the changes it made, with an Undo button that undoes them on the server without the AI (and shows why, when they were changed since).AiChangesMirrorServiceswaps the fresh data the server sends into the store and re-renders the preview with the same triggers the editor's own edits use (HandleSiteSettingsChildrenChangesAction,HandleSiteTemplateSettingsAction, the newSiteContentRefreshedActionin all four templates), reloading it only where the editor's own edits do. New store actions replace a site's sections (ReplaceSiteSectionsAction) and section entries (ReplaceSectionEntriesAction). A reply that wants to delete something shows its summary with Approve and Reject buttons; approving runs the delete on the server without the AI or quota, mirrors it like any change, and the reply's Undo can restore it. The state listsaiApprovenext toaiUndo. The old client-side applying of AI change lists is removed.laravel/mcp1.1.2 andlaravel/ai1.3.0.tests/Unit/SectionEntryRenderServiceTest.phpcoversshowsEntryTitles(), which only the plugin's tests covered before, andacceptsEntries().Merge order
Merge this first, then the plugin PR, because the plugin needs
laravel/mcpinstalled and its tests need thePluginstest suite. Merge the shop plugin'smcp-integrationbranch (Bitbucketberta-shop-laravel-plugin) at the same time, since its tests call this branch'slinkBertaEngine().Notes
Test plan
php artisan test --compactpasses with the plugins installed: 725 tests, none skippedng test, incl. the chat state, the change mirroring, the replace actions and the content re-render), production build succeedstests/Feature/EditorSyncTest.php(checks, own saves, agent and other-window changes, no marker, failing cache, invalid window ids, refused saves, opted-out routes, setup wizard, changes during a save),tests/Feature/RenewEditorTokenTest.php(renewal, claims kept, 30-day limit, account change, no-store),shared/editor-sync.spec.tstests/Feature/MediaFoldersTest.php(copying a section with a folder not named after it, moving, a failed move keeping the entry, renaming, taken names, a title case change, the background, deletes keeping shared folders, gallery and background files kept when their folder is shared, an image's own copies deleted but not those of similar names, and a video deleted with its poster); 8 of its first 10 tests failed on the code before the fixtests/Feature/SitesTest.php(restoring a deleted subsite, refused for a taken name or missing folder, the main site never deleted, a new site's settings file)tests/Unit/HtmlCleanerTest.php(what's removed, and real entry HTML with embeds and shop forms kept unchanged)tests/Unit/SettingValueValidatorTest.php(rules per format) andtests/Feature/SettingsValidationTest.php(both endpoints save valid values, refuse invalid and missing-feature values with 422 without writing, and still save list entries)http://berta.test→list-sections,list-entries,get-entry-gallery,get-settingsandget-design-settingsreturn the website's content. On themessytemplate, entries come back withouttitleand withentry_titles: false.🤖 Generated with Claude Code