yotech-ai / twill-cms-ai-assistant
An AI content assistant and MCP connector for Twill CMS.
Requires
- php: ^8.3
- area17/twill: ^3.5
- illuminate/support: ^12.0 || ^13.0
- laravel/ai: ^0.3.2 || ^0.10.0 || ^0.11.0
Requires (Dev)
- laravel/mcp: ^0.9
- laravel/passport: ^13.7.1
- laravel/pint: ^1.24
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpoffice/phpword: ^1.4
- yotech-ai/twill-cms-seo-suite: ^1.0
Suggests
- laravel/mcp: Required for the MCP connector (^0.5.9 || ^0.9).
- laravel/passport: Required for the MCP connector's OAuth 2.1 flow (^13.7.1 — 13.7.0 and below carry CVE-2026-39976 in the token guard).
- phpoffice/phpword: Required to read .docx chat attachments (^1.4).
Provides
None
Conflicts
- laravel/mcp: <0.5.9 || >=0.6,<0.9 || >=0.10
- laravel/passport: <13.7.1 || >=14.0
Replaces
None
- dev-main
- v1.4.0
- v1.3.0
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.0
- v1.0.0-beta.4
- dev-feature/self-service-connectors
- dev-fix/host-oauth-test-mcp-05
- dev-fix/mcp-doctor-chain-app-url
- dev-mcp-own-authorization-server
- dev-laravel-ai-0.11-compat
- dev-fix/release-workflow-tag-message
- dev-fix/mcp-doctor-and-gate-message
- dev-fix/widget-chat-titles
- dev-fix/agent-conversation-tables
- dev-laravel-ai-0.10-compat
- dev-feature/seo-suite-integration
- dev-fix/no-main-nav-entry
This package is auto-updated.
Last update: 2026-11-13 15:56:21 UTC
README
An AI content assistant embedded in the Twill CMS admin, with an optional MCP connector that exposes the same content tools to external clients such as Claude.
Editors chat with it in the admin; it reads your module structure, proposes a plan, and writes drafts for review. It can never publish and can never delete — those are product guarantees enforced in code, not settings.
Requires PHP 8.3+, Laravel 12 or 13, Twill 3.5+.
Installation
composer require yotech-ai/twill-cms-ai-assistant
php artisan twill-ai:install && php artisan migrate
Then describe your modules in config/twill-ai.php, run a queue worker, and save a provider API key on the assistant's Settings page. php artisan twill-ai:doctor checks all of it.
There is no provider to register, no view to override and no asset to publish. The package registers its own routes, views, migrations, private upload disk and job queue from your existing Twill configuration, so it adapts to a custom admin path automatically.
Editors reach the assistant two ways: the Plugins page, and the floating widget on every admin screen. It deliberately adds no entry of its own to the main navigation — set twill-ai.ui.navigation_link to true if your editors want one.
The module registry
config/twill-ai.php is the one file you must fill in. Only modules listed there are reachable by the agent — everything else in your CMS (users, orders, application data) does not exist from its point of view.
'modules' => [ 'pages' => [ 'label' => 'Pages', 'description' => 'Standard site pages, served from /{slug}.', 'model' => App\Models\Page::class, 'repository' => App\Repositories\PageRepository::class, 'route' => 'pages', 'operations' => ['read', 'create', 'update'], // never 'delete' 'block_editors' => [ 'default' => ['content-hero', 'content-text', 'content-faq'], ], 'browsers' => [], 'sync_fields' => [], 'extra_fields' => [], ], ],
| Key | Meaning |
|---|---|
label / description |
Shown to the model. The description is the most effective place to teach it how a module should be written. |
model / repository |
The Twill module classes. |
route |
The Twill route name segment, as passed to TwillRoutes::module(). |
singleton |
true for singleton modules — update-only, never created. |
operations |
Subset of read, create, update. There is no delete operation anywhere in this package. |
block_editors |
Editor name => allowed block names, mirroring getForm(). An unnamed BlockEditor::make() is the editor called default. |
browsers |
Twill related browsers, saved through twill_related. |
sync_fields |
Plain belongsToMany id-array fields synced in afterSave(). |
extra_fields |
Whitelisted non-translated columns the agent may set. Anything not listed is stripped from its payloads — needed for models without HasTranslation. |
An empty registry is safe: the assistant runs and answers questions but has no content it may touch.
Prompts describe your CMS
The tool descriptions and system prompt contain worked examples — a JSON payload, an editor name, a relation. These are generated from your registry at runtime, so the model learns your block names and locales rather than another project's. Override any fragment via twill-ai.prompts.*, and set twill-ai.site_description to tell external MCP clients what the site is.
Queue worker
Agent runs execute in a queued job so they can take minutes without hitting execution limits:
php artisan queue:work twill-ai --queue=twill-ai --timeout=620
queue:work caches code in memory — restart it after any deploy (php artisan queue:restart) or the agent runs stale tools. In development prefer queue:listen, which reloads per job.
The MCP connector (optional)
Off by default. It exposes the same content tools to an external MCP client over OAuth 2.1, and stays completely dormant unless both twill-ai.mcp.enabled is true and laravel/mcp is installed — so a site can run the assistant with no OAuth stack at all.
composer require laravel/mcp "laravel/passport:^13.7.1"
Security: use Passport 13.7.1 or newer. Versions 13.0.0–13.7.0 carry CVE-2026-39976, a high-severity flaw in the token guard — the exact component this connector authenticates on.
Then set TWILL_AI_MCP_ENABLED=true and:
php artisan migratephp artisan passport:keys- Point
twill.models.userat a user model that implementsLaravel\Passport\Contracts\OAuthenticatable— eitherTwillAi\Models\TwillUser, or addTwillAi\Concerns\ActsAsOAuthUserto your own Twill user subclass.
That is all a server needs: Twill users connect their own AI apps from there. php artisan twill-ai:doctor verifies the setup, and php artisan mcp:doctor follows the whole sign-in chain the way Claude does. The connector authenticates on its own twill-mcp guard rather than claiming api, which in most applications belongs to Sanctum.
Connect an AI app
A Twill user connects the AI app they use, with no server access and no client ID or secret:
- In the app, add a custom connector (an MCP server) with only the URL
https://<your-site>/mcp/twill. Claude, ChatGPT and Mistral Le Chat all take just the URL, as does any MCP client that supports OAuth. - The app sends you to the site. Log in to Twill if you aren't already; you come back to the same screen.
- The screen shows the name the app gave itself, where its access goes (
claude.ai,chatgpt.com,chat.mistral.ai, orlocalhostfor a desktop app) and what it may do: create and edit drafts, never publish or delete. Choose Approve.
That approval registers the connector. Its drafts are attributed to a Twill user of its own, <app> (approved by <you>), never to you.
Only users who pass Twill's edit gate may approve (publishers, admins and superadmins). A view-only user is asked to get an editor or admin to connect the app, and someone without a Twill account can't get past the login. With Twill's permission management on, that gate does not exist; approving then takes the global "Edit settings" permission.
Approving also revokes any token the app was given before it was a connector, so access only ever starts with this approval. A connector app that asks on Passport's own /oauth/authorize is sent to this screen, so a host's customer login can never approve one.
| Config | Default | |
|---|---|---|
twill-ai.mcp.self_service (TWILL_AI_MCP_SELF_SERVICE) |
true |
Off, an app works only once a server admin registers it with mcp:client-create, as in 1.3. |
twill-ai.mcp.allowed_redirect_hosts |
null: any app |
The hosts an app's access may go to, e.g. ['claude.ai', 'chatgpt.com']. An app for another host gets no Approve button. List a desktop app on its own scheme by the scheme, e.g. cursor. |
Dynamic client registration (/oauth/register) accepts only the redirect URIs OAuth 2.1 allows: https, http on a loopback address (localhost, 127.0.0.1, [::1]) for a desktop app, or an app's private-use scheme. Anything else, a host with characters outside ASCII included, is refused with invalid_redirect_uri. Private-use schemes need laravel/mcp 0.9, and if you set mcp.custom_schemes yourself, that list decides which ones register. The endpoint is shared with your application, so while self-service is on these rules apply to your own MCP server's clients too.
The Connectors page
Twill AI → Connectors (/<admin path>/ai/connectors), for users who may approve, lists every connector: its name, the host its access goes to, who approved it and when (or "artisan"), when it was last used, and its live tokens.
- Revoke does what
mcp:client-revokedoes: the app's tokens stop working at once, and it can only connect again through a new approval. - Add an app by hand is for an app that can't register itself. Give it a name and its redirect URI(s), and the page shows its client ID and secret, once. The app then asks for approval like any other.
For server admins
php artisan mcp:client-create "<name>" still registers a connector from the command line and prints its client ID and secret once; with self-service off it is the only way in. mcp:client-list and mcp:client-revoke list connectors and cut them off, and mcp:doctor shows self-service, the allowed apps and the apps waiting for approval.
Its own sign-in, beside yours
The connector runs its own OAuth authorization server, so it sits next to an application that uses Passport for its own customers (a customer API, or its own MCP server) without changing anything global:
- Its own approval screen, at
/twill-ai/oauth/authorize(twill-ai.mcp.oauth_prefix), behind the CMS login.passport.guardstays whatever your application needs; leave it atwebfor a customer login. - Its own discovery documents. Claude reads the connector's protected-resource document and its issuer's metadata, which name that approval screen. Every other discovery request, including your own MCP server's, is answered by laravel/mcp as before. The connector answers its addresses in global middleware, before routing, so route order and
route:cachecannot change the result. - Your approval screen is left alone. The package only replaces Passport's global approval view on the old setup below.
- Connector clients are bound to the CMS user provider (
oauth_clients.provider), so their tokens are refused by any other Passport guard.mcp:client-createdoes this;mcp:doctorflags an older client that is not bound. - Tested versions only. The package declares conflicts with untested laravel/mcp (anything but 0.5.9+ and 0.9) and Passport (anything but 13.7.1 to 13.x), so an upgrade the connector has not been tested against is refused at install time instead of failing at sign-in.
Token issuing (/oauth/token) and dynamic registration (/oauth/register) are shared with the application; neither depends on who is logged in.
Upgrading from the old setup. Earlier versions asked you to set passport.guard to twill_users. A site that did keeps working: Passport's /oauth/authorize still shows the connector's screen there, and new connections go to the connector's own screen. You can set passport.guard back to what your application needs; existing connector tokens are unaffected.
Registering a client grants nothing on its own: dynamic client registration lets anyone create one, but a client with no row in mcp_clients is refused until a Twill editor approves it (with self-service on) or a server admin registers it with mcp:client-create. Only a public client that no other user provider claimed, the kind /oauth/register makes, is registered by approving it. Your application's confidential clients and clients bound to its own providers never become connectors; a client your own MCP server registered is public too, so it would, if an editor approved it on the connector's screen.
Safety guarantees
These are enforced in PayloadBuilder and the tool list, not in config, and no setting widens them:
- Drafts only. The agent cannot publish, and cannot change the publish state of anything.
- No deletion. No delete tool exists in either the chat agent or the MCP server.
- Registry-bound. Only registered modules, and only the operations listed for each.
- Field whitelist. Non-translated columns not in
extra_fieldsare stripped from agent payloads.
allow_updating_published only controls whether the agent may edit an entry a human already published; it still cannot alter its publish state. It ships as null — see SEO Suite integration — and false pins it closed.
Updates change only what they send
update_content, in the Twill AI chat and over MCP alike, is partial:
- Translated fields are merged per locale. A locale the payload leaves out keeps its text, so an update with only
enleavesnlanddeas they are. A locale sent as""ornullis cleared. - Blocks are rebuilt per editor, from the full list sent for it. A block sent with the
idthatget_contentgave it keeps the locales it leaves out, the same way, and so do its children. A block without an id is new, so send every locale for it. Ids change with every update, so read the entry again before the next one: an id an earlier update replaced is refused, rather than its locales dropped. - Which languages are active stays as it is. Switching a language on is publishing, so
localesin an update payload is ignored.
The Plugins page
This package adds a Plugins entry to the admin navigation, next to the Media Library, listing every installed Yotech plugin with a link to its own screen. Nothing to configure.
The page is created by whichever Yotech plugin loads first; the rest just add themselves to it. That coordination happens through two container keys — yotech.twill-plugins.registry and yotech.twill-plugins.page-owner — which carry only PHP built-ins, so each plugin ships its own copy of the code under its own namespace and they still interoperate. Install one Yotech plugin or five: you get exactly one Plugins page, and no plugin depends on any other.
SEO Suite integration (optional)
With yotech-ai/twill-cms-seo-suite installed, the assistant gains three tools on both surfaces — the admin chat and the MCP connector:
| Tool | Does |
|---|---|
get_seo |
Reads an entry's current metadata, its score, and the assessments explaining that score. |
analyze_seo_text |
Scores proposed copy without saving anything, so a rewrite can be checked before it is written. |
update_seo |
Writes the metadata, through the Suite's own writer, and refreshes its score and sitemap caches. |
Nothing to configure: the integration turns itself on when the Suite is present. TWILL_AI_SEO_ENABLED=false turns it off again, and on a site without the Suite none of it exists — no tools, no registry key, and no SEO wording in the prompt.
What it will not touch
update_seo writes seven fields — seo_title, seo_description, focus_keyphrase, og_title, og_description, twitter_title, twitter_description.
The indexing controls are refused in code, not by config: robots_noindex, robots_nofollow, canonical_url, cornerstone and schema_type_override. Deindexing a page or reassigning its canonical is quietly destructive in the same way publishing and deleting are, and this package's rule is that the agent never does the destructive thing.
Editing published entries
Installing the Suite changes one default. allow_updating_published ships as null, meaning follow the Suite: with it installed the agent may edit entries a human already published, without it the answer stays no.
allow_updating_published |
Effect |
|---|---|
null (default) |
Published entries editable only when the SEO Suite is installed. |
true |
Always editable. |
false |
Never; drafts only, whatever else is installed. |
Two things hold regardless. The agent still cannot change any entry's publish state, and new content is always a draft — the loosened rule is about improving live copy, never about putting new copy live. And every edit to a published entry comes back with was_published: true and a warning that the agent is instructed to relay, so a live change is never silent.
Commands
| Command | Purpose |
|---|---|
twill-ai:install |
Publish the config and report the remaining setup steps. |
twill-ai:doctor |
Diagnose block registration, host wiring, the API key, queue, SEO and MCP setup. |
twill-ai:refresh-models |
Refresh the provider's model list for the picker. |
mcp:client-create / mcp:client-list / mcp:client-revoke |
Manage MCP connectors from the command line. The Connectors page in Twill does the same for editors. |
mcp:doctor |
Diagnose the MCP endpoint, its tools and its clients. |
Frontend assets
The built Vue app ships in resources/dist and is served from a package route with an ETag and a far-future cache header, so it can never go stale after an upgrade. php artisan vendor:publish --tag=twill-ai-assets is optional; the views prefer a published copy when one exists.
To rebuild from source: npm install && npm run build in resources/js.
License
MIT