Release history for Cybersalt MCP for Joomla. For install and usage instructions see the documentation.
📋 Cybersalt MCP for Joomla — Changelog
v2.9.1 October 19, 2026
Clean-up for the Joomla Extensions Directory submission. No behaviour changes.
Fixed
- Every PHP file now carries its GPL licence notice. JED Checker found the notice in only one of 137 files; the JED requires it in each.
- Windows paths in the Setup Guide's Claude Desktop steps showed doubled backslashes. Since Joomla 4.4.1 / 5.0.1 language strings don't need backslashes escaped, so they were shown twice.
- The MCP endpoint no longer falls back to
Factory::getUser(), which JED Checker flags as removed; a request with no loaded identity is treated as a guest, as before.
v2.9.0 October 19, 2026
Connect by signing in. ChatGPT custom connectors and claude.ai / Claude Desktop "Add custom connector" can now connect to a Joomla site: paste the site's MCP address, sign in with a Joomla account, approve. Most other clients (Claude Code, VS Code + Copilot, Cursor, Gemini CLI, Codex, Cline) can sign in the same way. API keys keep working exactly as before. (#36)
Security — four tools now need a Super User
set_component_permission,apply_joomla_update,set_extension_enabledandset_plugin_paramsrefuse anyone who isn't a Super User. Before, they only needed MCP write access, which Administrators have through Joomla's defaultcore.managegrant (and Managers on sites that grant it). On a site that gives Administrators Web Services Login, an Administrator could have usedset_component_permissionto give their own groupcore.adminon the root asset and become a Super User (reproduced on Joomla 6.1.4), or switched off a two-factor or firewall plugin. Joomla's own Permissions, Update and security screens are limited the same way.install_extensionalready worked like this. Found while preparing the JED listing.- The Dashboard's “Who can use this endpoint?” table was wrong about Administrators and Managers. It said Administrators have
core.adminon the component and Managers havecore.manage. With Joomla's default permissions, Administrators havecore.manageand Managers have no MCP permission at all, so a Manager also needs Use MCP endpoint (and Write through MCP endpoint) granted on the component, not just Web Services Login. - The installer now checks PHP 8.2 and Joomla 5.0 before installing or updating, using Joomla's own messages. The update server already advertised these minimums, but a manual install on an older site went ahead.
Fixed — found while preparing the JED listing
- Options now has a Permissions tab. The Dashboard, the Setup Guide and the OAuth refusal page all sent people to Options → Permissions to grant Use MCP endpoint and Write through MCP endpoint, but the tab was missing, so those two permissions could not be set from the admin at all. It is there now, in the standard Joomla layout.
- The Options page title showed the raw key
com_csmcpforj_configuration; it now reads MCP for Joomla: Options. - Uninstalling now removes the
api/.htaccessrule the installer adds to keep the Authorization header on cPanel / PHP-FPM hosts. Only that block is removed; the rest of the file is left exactly as it was. joomla_update_healthcheckworks in read-only mode. It only reads, but its name has no read verb, so read-only mode hid it.- The Setup Guide's ChatGPT tab no longer offers an Authorization header snippet; ChatGPT's dialog has no header field. It shows just the endpoint URL.
- Text that predated OAuth or was wrong: the Security and FAQ sections now mention OAuth sign-in; the "no tools" troubleshooting step now covers Web Services Login and the Permissions tab; the post-install message names all four enabled extensions; the membership re-check help no longer offers 0 (the form minimum is 60); the read-only help lists every read-only prefix; the system plugin description covers all the tool groups; the account-link card now says your email address and domain are sent to cybersalt.com.
Added — OAuth 2.1 sign-in (opt-in)
- Off by default. Switch it on in Options → OAuth sign-in. Switching it off cuts every OAuth connection immediately.
- The site is its own sign-in server, following the MCP authorization spec: protected-resource metadata (RFC 9728) and the
401challenge that points to it, authorization-server metadata (RFC 8414, also answered at/.well-known/openid-configuration), dynamic client registration (RFC 7591) so apps register themselves with no client ID or secret to paste, the authorization code flow with PKCE (S256 only), resource indicators (RFC 8707), and token revocation (RFC 7009). - Signing in uses the site's own Joomla login, so its two-factor authentication applies, followed by a consent screen naming the app and the account it will act as. Only accounts that can already use MCP (Web Services Login plus MCP for Joomla access) can approve an app.
- The app acts as the user who approved it, with exactly that user's permissions, the same as an API token for that user.
- Tokens: opaque, stored only as SHA-256 hashes, short-lived access tokens (1 hour by default) with rotating refresh tokens (30 days by default, both configurable). A refresh token used twice ends that whole sign-in, as a stolen-token precaution. Access stops at once if the token is revoked, the app is disconnected, OAuth is switched off, or the user is blocked, deleted or changes their password.
- Tokens only work on the MCP endpoint. They are bound to this site's MCP address and are refused anywhere else in Joomla's API.
- New api-authentication plugin, API Authentication - MCP for Joomla OAuth, installed and enabled with the package. It is a separate plugin because Joomla authenticates API requests in an isolated dispatcher that only loads that plugin group.
- Dashboard → Connected apps lists every connected app and the account it acts as, with a Disconnect button. It also checks live that AI clients can find the site's sign-in details at
/.well-known/. - Consents and disconnections are written to Joomla's User Actions Log.
- Client registration is rate-limited per IP address. Redirect addresses must be https, a loopback http address, or an app-specific scheme.
🐛 Fixed — OAuth sign-in failed at the last step with server_error
- Signing in now completes. In the 2.9.0 test builds every OAuth sign-in failed: the app reported "OAuth token exchange failed: server_error" (or the sign-in page broke right after Allow), and nothing on the site said why. Two database writes, the authorization code and the issued tokens, handed Joomla a value in a form PHP 8 refuses, so both always threw. Found and reported by Stefan Gagner (CMS Universal), testing with Codex on Joomla 6.2.
- The
server_errorresponse now tells the app's user that a site administrator can see the details under Components → MCP for Joomla → Logs.
Added — Logs: what MCP for Joomla did, and why something failed
- New Logs screen (Components → MCP for Joomla → Logs). Each entry has a time, status, event, a plain-language summary, the user, the app, the IP address, and how long it took. Expand an entry for the details: for an error, the exception, file, line and stack trace.
- What is logged: every OAuth sign-in step (discovery, app registration, the login and Allow page, token exchange and renewal, sign-out), every refused sign-in with the reason, every OAuth token refused at the MCP endpoint with the reason (expired, revoked, app disconnected, password changed, site address changed…), AI apps connecting (
initialize, with the app's name and version), and every tool call with its outcome and arguments. - Never logged: access and refresh tokens, authorization codes, client secrets and PKCE verifiers. Tool arguments named like passwords, secrets, tokens or keys are masked, and long values are shortened.
- Standard Cybersalt log viewer: Refresh, Dump (plain text for a support email), Download CSV, Delete and Clear All (with a confirmation). Clickable count cards (all, last 24 hours, success, info, warning, error, OAuth, tool calls), filters for status, source, event, time, user and request, search, and sorting. Click a request ID to see everything from that one web request.
- Options → Logging: log everything (default), only warnings and errors, or nothing; and how many days to keep entries (default 30; the newest 20,000 are kept at most).
- Dashboard: with OAuth on, a warning appears under the "Ready" check when sign-ins have failed in the last 24 hours, linking to the Logs. "Ready" only proves AI apps can find the sign-in page, not that sign-in works.
- Support form: a bug report can include the last 7 days of warnings and errors (on by default, can be unticked).
- New Delete permission for the component, used by the Logs screen.
Fixed — editing an article or category silently removed its tags
update_articleandupdate_categoryno longer strip tags. Joomla's tagging reads a save with notagskey as "this item has no tags" and deletes them. The admin form always sends the tags, so it never shows there, but every MCP edit that left tags out removed them all, while reporting success. Found by Susan on cybersalt.org: editing one article's text and meta description took both of its tags, and a planned bulk edit of about 130 articles was stopped in time. The tools now always send the item's current tags unless you pass new ones. The earlier "partial save is safe" check compared each item's own columns, so it could not see tags, which live in a separate table.- New
tagsargument oncreate_article,update_articleandupdate_category: an array of tag ids. It replaces the item's tags;[]removes them all. Part of #8.
Fixed — the same kind of silent reset on custom fields and multilingual menu items
update_custom_fieldno longer resets a field to "All categories". Joomla rewrites a field's category list on every save and reads a save withoutassigned_cat_idsas "show everywhere". So publishing a field restricted to one category ({id, state: 1}) put it on every article on the site. Also found by Susan on cybersalt.org. The tool now always sends the field's current categories unless you pass new ones;[-1](no categories) and[](all categories) still work as before.create_custom_fieldnow acceptsassigned_cat_ids. It was listed nowhere and silently ignored, so every field created through MCP landed on "All categories".update_menu_itemno longer drops a menu item's language associations on multilingual sites. Joomla deletes the association group when a save leaves it out; the tool now sends the current associations unless you pass new ones.- Every other update tool was checked for the same pattern, by reading what each Joomla model writes to other tables rather than only comparing the item's own row. Modules (menu assignment) and users (groups) were already protected; field groups, tags, user groups and template styles write nothing else.
Fixed — menu item params picked up the link; custom field params were replaced, not merged
update_menu_itemno longer copies the menu link into the item's params. Setting any params key (params_setor a named one such asbrowser_page_title) on an article link also stored the link'soption,viewandidin params, so[]became{"option":"com_content","view":"article","id":"844",…}. Joomla's menu item model adds the link's parts to params to fill in the edit form, and the tool merged its change onto that copy. It now merges onto the params as stored. Found on cybersalt.com. Items already affected can be cleaned withparams_unset: ["option","view","id"].update_custom_fieldnow mergesparamsandfieldparams, as its description always said ("untouched properties preserve their current value"). Aparamsargument used to replace the whole set, so setting just a hint dropped the field's other display options (where it shows, CSS classes, layout), which fell back to Joomla's defaults. Pass only the keys to change;nulldeletes a key; a nested value such as a list's options is replaced as a whole.update_user_groupran Joomla's Super User check against the wrong parent. Joomla works out whether a group will carry Super User rights from the parent in the save, and a title-only update didn't send one, so the check treated the group as having no parent. A non-Super User could rename a group under Super Users, and a Super User whose rights come through that group could be refused. The tool now sends the current parent.update_tagandupdate_usernow send the currentparent_idandblock. Joomla reads both before saving. Without them the save still worked, but raised a PHP warning that a site set to display errors would print into the response.- These came from a second pass over every update tool, reading each Joomla model's save for values it uses before saving, and inside params, which the earlier checks of each item's own row and related tables could not see.
Fixed — one broken add-on no longer takes the whole MCP endpoint down
- A broken add-on now costs only its own tools. Before, if any add-on failed while registering its tools (a tool file with a syntax error, a class missing after a half-finished update), every MCP request failed with HTTP 500, core tools included, so there was no way to use MCP to fix it. Now each add-on registers on its own. One that fails is skipped, any tools it had already added are removed, and the rest of MCP keeps working. The failure is recorded under Components → MCP for Joomla → Logs with the add-on's name, the file and the line. Found on our test site when one add-on's typo stopped MCP for every client.
🚀 Added — get_article and get_category return tags
- Both now include
tags, an array of{id, title}, so a tag change made with thetagsargument can be checked without leaving MCP.
Added — set an article's hit count
- New
hitsargument onupdate_article, for merging duplicate articles: add the duplicates' hits to the copy you keep, then trash the duplicates. Until now that needed SQL. Requested by Susan on cybersalt.org. Leavinghitsout keeps the current count, as with every other argument. The count is written directly, the way Joomla's own hit counter writes it. A call with onlyidandhitsdoesn't re-save the article, so its modified date stays the same and plugins that change text on save (for example ReReplacer) don't run. The response includesprevious_hits.
Fixed — MCP clients could flood a site with GET requests
- A GET from an MCP client now gets
405 Method Not Allowed. After connecting, Claude Code and other clients built on the MCP SDKs open a GET on the endpoint to receive a server-to-client stream. The spec allows two answers, a stream or 405. MCP for Joomla answered200with its friendly information page, which the client took as "stream opened, then closed" and reconnected at once, forever: 286,000 requests in 10.5 hours from one session, and on 12 September enough load to take two sites down for ten minutes. Any GET carrying a token or asking for a stream now gets 405 and the client stops. A plain browser visit still gets the information page. With OAuth on, an expired OAuth token gets401 invalid_tokeninstead, so the client refreshes.
Changed — Setup Guide and Dashboard
- New lead section, "Connect by signing in (OAuth)", with steps for claude.ai, Claude Desktop, ChatGPT, Claude Code, VS Code, Cursor, Gemini CLI, Codex and Cline. The API-key method moves to Advanced setup and remains the answer for headless machines.
- Fixed instructions that were wrong. The ChatGPT entry told users to add an Authorization header in a dialog that has no header field. The Dashboard listed claude.ai and ChatGPT custom connectors under the config-file method, although both are sign-in only. The Claude "this dialog won't work" warnings now explain how to switch OAuth on, and disappear when it is on.
- Dashboard reorganised around one question: how do I connect? One card, Connect an AI app to this site, with three collapsible ways, each as numbered steps: 1. Log in (copy the Endpoint URL, add it as a connector, log in, click Allow, with connected apps listed underneath); 2. Paste a setup prompt; 3. Config file or command line. Ways 2 and 3 each start with a Get my API token button that opens your profile in a new tab, then a field to paste it into; the two fields are linked, so pasting once fills both, and Copy configuration now puts the token in for you as the prompt already did. The old Quick start card is gone (its "Administrator and Manager work immediately" was wrong); Connection details is a compact side card; Check for updates now moves to the Cybersalt account card, where it belongs.
- Plain-language wording. The Dashboard card now walks through connecting in three numbered steps (paste the address, log in with your username and password for this website, click Allow) and its status line says Ready or Not ready with what to do, instead of developer terms.
- ChatGPT and Codex steps match the apps as they are now, checked by connecting both to a live site. ChatGPT moved custom servers to Plugins → Add → Add custom MCP server, and it works on a Plus plan (the guide said Team, Enterprise or Pro only). Codex starts the sign-in by itself on
codex mcp add, waits a few minutes for Allow, and needs the browser on the same computer; the guide now says so and givescodex mcp login --no-browserfor when the browser can't reach Codex. get_joomla_versionreported the extension as "cs-mcp-for-j 1.0.0" since 1.0; it now reports the installed version.- Cybersalt account buttons say what they do by colour: Refresh membership status is green (safe, positive), Deactivate locally and Disconnect this email are red (they undo something). In dark mode the old grey buttons were hard to read.
- Buttons readable in Joomla's dark mode. Coloured outline buttons (Open Options, Disconnect, Clear token, Refresh membership) rendered dark text on a dark background in Atum dark mode. They are now solid buttons with light text.
- New troubleshooting entries: firewalls blocking claude.ai / ChatGPT,
/.well-known/not reaching Joomla, and signing in on a machine with no browser.
v2.8.3 October 10, 2026
Everything left open after 2.8.2: two security hardenings, a registration bug that could leave a site unable to download paid add-ons, and a version number that had been wrong since 2.7.
Fixed
- A failed registration could leave a site holding an installation id the store had never heard of. The id was saved the moment it was generated, before the store accepted it, so if registration failed the site kept it, sent a perfectly formed download ID, and every paid download came back
installation_not_found. cybersalt.com sat in that state for almost four weeks. The id is now saved only once the store has accepted it. - Sites already in that state repair themselves. When the store answers
installation_not_foundfor a site that has a linked email, MCP for Joomla re-registers the same id and re-links the same email, then checks again. No unlink and relink by hand. It runs on the next membership check, including the one this update makes during installation. - The MCP server reported its version as 2.7.0 in every
initializeresponse since 2.7, because the number was hard-coded. It now reads the installed version from the package manifest. The wrong number misled a diagnosis on a customer site.
Security
- The download ID is stamped only onto genuine Cybersalt update sites. Update sites were picked with a text match, so an update URL that merely contained
cybersalt.comandtask=api.updatexmlwould also have received it. It now requires https and the exact cybersalt.com host, and strips the ID from any other update site that holds it. - The download ID is never sent over plain http. The download hook added in 2.8.2 now requires https as well as the cybersalt.com host.
Changed
- Joomla's "Changelog" button now works. The package manifest points at the store's new changelog feed, built from the release notes published with each version.
- A freshly installed add-on now gets the download ID on its update site straight away. The re-stamp added in 2.8.2 listened to the same after-install event Joomla uses to create the update site, and ran first, so the new row did not exist yet. It now runs last.
- The download hook checks the URL before loading anything, so MCP for Joomla's own updates no longer load the old helper into memory ahead of the new package's install script.
v2.8.2 October 9, 2026
Fixed — paid add-ons could not be updated from mySites.guru
- Every paid add-on update from mySites.guru failed with "Failed to download package", on every site whose update sites had never had the download ID stamped on them. Stamping only happened when someone opened the MCP for Joomla dashboard, so a linked site nobody visited, or an add-on installed after linking, never got it. Joomla's own Extensions > Update adds the ID itself and worked, but mySites.guru downloads the bare URL from the update feed, and the store refused it with "Missing parameters". Free add-ons use a route that needs no ID, which is why only the paid ones failed.
- The download ID is now added at download time. The system plugin listens for
onInstallerBeforePackageDownload, which every route into the Joomla installer goes through, and adds this site's download ID to any Cybersalt members download that lacks one. That covers mySites.guru, other remote managers, Install from URL and the core updater, whether or not the update sites were ever stamped. Nothing is added for any other host or route, or when a download ID is already present. - Update sites are re-stamped automatically whenever any extension is installed or updated, and when MCP for Joomla itself is installed or updated. Installing 2.8.2 therefore fixes a linked site immediately, with no dashboard visit.
- System plugin 1.16.1 → 1.16.2. Pairs with cs-release-manager 1.12.5 on the store, which now also puts the ID in the feed's download link for sites that send it.
v2.8.1 October 9, 2026
Four defects found by running every Update and Delete tool against a live Joomla 6 site rather than reading them. Three were invisible to static review.
Fixed
create_userdid not work on Joomla 5 or 6 at all (#15). It passedparamsas the string'{}';User::bind()hands that toRegistry::loadArray(), which is typedarray, so every call died with a TypeError and no user was created. Now an empty array.update_usercould not change anything. com_users reads$data['groups']as the complete group list, so omitting it meant remove every group — and the model then refused the entire save withCANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS. Renaming a user, changing an email, blocking an account: all impossible unless the caller happened to re-send the full group list. The existing groups are now re-supplied when the caller does not pass any. Same hazard as #18, different model.delete_categorycould not trash anything on Joomla 6.CategoryModel::publish()reads the extension from the HTTP request (getInput()->get('extension')) and hands it to an event typedstring. The admin UI always has&extension=com_contentin its URL, so this is invisible there; over MCP it arrived null and threw. Permanent delete was unaffected, because it does not fire that event.delete_modulereported failures with an empty reason. Joomla refuses to permanently delete a module that is not already trashed — and signals it by pushing a message onto the application queue and returningnull, leavinggetError()untouched. The tool read onlygetError(), so the operator gotPermanent delete rejected:and nothing else. It now checks the precondition up front and names it, and falls back to the message queue when a model fails silently.
Verified rather than assumed
Every Update*Tool has now been run through the behavioural partial-save test the guard prescribes — create fully populated, change exactly one field, re-read, diff everything. Six were confirmed clean (categories, tags, custom fields, field groups, template styles, user groups); update_user was not, and is fixed above. Each carries an annotation recording what was observed, including the field count, so the claim is auditable rather than asserted.
v2.8.0 October 9, 2026
The community release. Most of this came from one person: Dragan Subotic (gagibeograd) filed the reports, wrote patches for three of them, and opened the project's first external pull requests.
Fixed — menus and modules lost data silently
create_module/update_moduledropped menu assignments entirely (#17).assignedandassignmentwere passed toModuleModel::save()the wrong way round, so the existing#__modules_menurows were deleted and nothing was written back. Worse, anupdate_modulecall that said nothing about assignment reset the module to all pages, because the model's default did. A module quietly appearing site-wide is the kind of thing nobody notices until a customer does. Fixed by PR #34, Dragan's patch.update_menu_itempointed items at the wrong component after a link change (#18).ItemModel::save()storescomponent_idexactly as handed to it — onlygetItem()re-derives it from the link, and that runs when the admin form loads, never on save. So switching a menu item fromcom_contenttocom_contactleft it rendering as one component while the Menus manager reported another. It is now derived from the new link, for typecomponentonly; url and alias items keepcomponent_id0 as they should. Built from Dragan's patch in PR #35, with the recompute limited to component items and his transaction removed (Nested::store()issuesLOCK TABLES, which commits any open transaction, so a rollback there has nothing left to undo).update_menu_itemorphaned the item it was editing (first half of #18, shipped unreleased in September).ItemModel::save()is not partial-payload safe: omitmenutypeorparent_idand PHP 8 evaluates them to null, so the model concludes the item belongs to a menu called""and stores it that way. The item survived in#__menu, rendered nowhere, and vanished from the Menus manager while still holding its alias.create_menu_itemdied on an alias collision (#30) with a router error naming neither the alias nor the conflict — which is exactly what you hit after the orphan above, when you reasonably tried to recreate the item. It now detects the clash itself, including against trashed items, and names the id you are colliding with.
Fixed — Gemini could not use the server at all
- One empty string in one enum took down the whole catalogue (#29).
update_menu_item'srobotsparameter offered""as a choice. Google's Gemini API validates tool schemas against a strict OpenAPI subset and rejects every tool at parse time over a single empty member — HTTP 400 before a prompt runs. Not one broken tool: no usable server for any Gemini-backed client. Anthropic tolerates it, which is why it shipped unnoticed. Reported by Mathew John, who then confirmed the fix. tests/enum-guard.phpnow fails the build if any enum advertises an empty member, across this repo and the add-on repos. Deliberately a build-time check rather than a runtime filter: empty string is sometimes load-bearing, and silently stripping it would trade a compatibility bug for a capability regression.
Fixed — permissions documentation that cost people real time
- The dashboard claimed Administrator and Manager work "out of the box". They do not. Both get
403 Forbiddenfrom every tool until their group has Joomla's Web Services Login permission (core.login.api), which only Super Users hold by default. The page sent people to check the component's permissions, the token and the endpoint URL — none of which was the problem. Both rows now say what they need, and a notice above the table states the precondition and points at System → Global Configuration → Permissions, on the group, which is the part people get wrong.
Fixed — registration and installs
- Registering from the CLI recorded the site as
localhost.Uri::root()has no host there, and the fallback wrote that string into the installation id — which is generated once, saved permanently, and is the unique key on our side. One CLI call registered a site forever as "localhost". It now refuses with an explanation instead of inventing an identity. Scoped to generation only, so an already-registered site still works from CLI. - Installing an add-on could produce an HTTP 500 a minute later. Joomla's PSR-4 autoload map is written once and reused, so a plugin installed afterwards simply is not in it and the first request that reaches its classes fatals. Every add-on's installer now drops that map, resets opcache and clears Joomla's cache groups. All best-effort — a cache that will not clear never fails an install.
Changed — you can now find the fields you type into
- The Joomla API token field could not be found on the dashboard by someone who knew exactly what he was looking for. It was a hairline grey card on a page carrying two coloured alerts, a numbered list, a code block and a large button: the only thing meant to be acted on was styled more quietly than everything explaining it. Fields you type into now carry a brand-orange rail, and on the dashboard the token field is step 2 of the setup sequence rather than a stray box beside it.
Add-ons released alongside
- 4SEO 1.10.3 —
query_4seo_tablereturned the entire table whenwherewas a string instead of a list of clauses (#19), because "no filter asked for" and "filter asked for but unreadable" took the same branch. A silent superset is worse than an error: nothing failed, the result looked right, and the caller acted on rows that never matched. Reported with a patch by Dragan. This build also finally carries the auto-enable step, so it no longer installs disabled. - Membership Pro — the same empty-enum problem as #29, in the coupon tools. Any site with it installed was unusable from Gemini.
subscription_typenow takes"any"instead of""; the old value is still accepted.
Server-side (cs-release-manager, deployed separately)
- Free add-ons returned 403 "membership not active" on the members download route, which ran the group check regardless of whether the package was public. The catalog's own public link worked, so it looked like a client bug. Affected every free-add-on user on that route.
v2.7.6 September 17, 2026
Fixed — Gemini-backed clients could not use the server at all
update_menu_itemoffered an empty string in itsrobotsenum. Google's Gemini API validates the whole tool catalogue against a strict OpenAPI subset and rejects all of it at parse time over a single empty enum member —GenerateContentRequest.tools[0].function_declarations[N].parameters.properties[robots].enum[0]: cannot be empty, HTTP 400 before any prompt runs. Not one broken tool: no usable server for any Gemini client. Anthropic tolerates empty enum members, which is why this shipped unnoticed.- The empty member is gone. Nothing is lost — it was advertised in the schema only and never validated against at run time, and clearing the directive already had a better route in
params_unset: ["robots"], which removes the key outright instead of storing an empty string. - Reported by Mathew John, testing via Cline +
gemini-2.5-flash. Confirmed independently against our own live catalogue before changing anything. (#29)
Added — a guard so this cannot recur silently
tests/enum-guard.phpscans this repo and the sibling add-on repos for any enum advertising an empty-string member, resolving both inline enums andself::CONSTreferences. Exit code 1 on a hit.- Deliberately a build-time check rather than a runtime filter. Stripping
""as the catalogue serialises would have silently broken the Events Booking add-on, where""legitimately means inherit the global setting — a third state distinct from"0"and"1". That case needed an explicit"inherit"sentinel instead, which is a decision a human has to make per field. The guard fails loudly and makes someone choose.
Housekeeping
- Removed
admin/src/Model/cat.json, an 8.5 KB debug dump of a catalog response that was never source. It sat inside the package tree, so the build swept it into the shipped zip from at least 2.7.5. Public catalog data only — no addresses, download ids or hashes — so litter rather than a leak.
v2.7.5 September 14, 2026
Fixed — the download id now reaches the update server, not just the download
- 2.7.4 stamped the dlid into
#__update_sites.extra_query, which fixed downloads. It did not fix update discovery, because Joomla never appendsextra_queryto the update-XML request — only to the download URL.Updater::findUpdates()copiesextra_queryonto the parsed update record for the installer to use afterwards; the XML itself is fetched from the barelocation. - The consequence was invisible until cs-release-manager started using the dlid: with no identity on the XML request, the update server could not tell an entitled site from an unentitled one, and briefly told every polling site its update was unavailable (fixed on that side in cs-release-manager 1.12.2).
syncUpdateSiteDlid()now writes the dlid into the update site'slocationas well asextra_query— location for discovery, extra_query for the download. Any existingdlid=in the location is stripped first, so re-linking with a different account cannot leave two.- Downloads were already working from 2.7.4; this restores an accurate answer to "why can't I install this?" for sites that genuinely cannot.
v2.7.4 September 14, 2026
Fixed — 2.7.3's update-site stamping did not actually run on most sites
- 2.7.3 added
syncUpdateSiteDlid()but called it only after a successful re-verification.refreshIfStale()returns early whenever the throttle window has not elapsed — which is the normal state for any site verified in the last 24 hours — so on those sites the stamp never happened and Pro add-on updates stayed broken. - The dlid does not depend on verification being due. The stamp now runs as soon as the site is known to be linked, before the throttle check. The write is guarded so it is a no-op when the value already matches.
- Caught by blanking
extra_queryon a live site, loading the dashboard, and finding it still empty — the fix 2.7.3 shipped was real but unreachable.
🚀 Version 2.7.3 (September 14, 2026) — superseded by 2.7.4, see above
Fixed — Pro add-on updates failed for EVERY site, entitled or not
- Extensions → Update could not download any Pro add-on, on any site. Joomla reported its own
COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED— "Failed to download package. Download it and install manually from <url>" — quoting a URL that also fails, because it is the same credential-less URL that just refused. - Cause:
#__update_sites.extra_querywas never populated. Our update XML's<downloadurl>carries no credentials — it cannot, since the XML is generated per-element, not per-site. Joomla's mechanism for exactly this isextra_query, which it appends both to the update-XML request and to the download URL; it is how every commercial Joomla extension ships a download ID. Leaving it empty meantapi.downloadwas called with nodlidand returned HTTP 400. cs-release-manager was behaving as designed — its own code comment says members-only packages "rely on Joomla appending extra_query" — the client side simply never wrote it. - This was not an entitlement bug. It hit paying, fully entitled customers identically. It stayed hidden because the in-admin catalog installs through
install_extensionwith an explicitdlidURL, which bypasses Joomla's updater entirely — so the catalog worked while Joomla's own update screen did not. - New
ProActivationHelper::syncUpdateSiteDlid()stampsdlid=<installation_id>:<email_hash>onto every cs-mcp-for-j add-on update site, and is called whenever the dlid can change: on activation, on entitlement refresh, and on deactivate (where it clears the credential rather than leaving a stale one behind). Update sites are matched on the same location patterncheckUpdatesNow()uses, so add-ons installed by any route are covered — including a manual Install-from-URL that never went through the catalog. - Existing sites self-heal on their next dashboard or catalog load after updating. Verified against a live entitled site: the download Joomla will now perform returns HTTP 200,
application/zip, 305,620 bytes.
Companion release: cs-release-manager 1.12.1, which uses the now-arriving
dlidto explain in the update XML why a site that still cannot download is being refused — rather than leaving Joomla to show a dead end.
v2.7.2 September 14, 2026
Fixed — newly published Pro add-ons showed as "Get it →" on sites that already own them
- Every time a new Pro add-on ships, already-linked sites showed it as needing purchase — including All-Access accounts unambiguously entitled to it. Not a licensing bug:
api.verifyaccessreturned the correct, larger list the whole time. The stale copy was local. entitled_elementsis a snapshot, computed server-side when the account is linked and cached in component params. Anything published afterwards is simply not in it. Confirmed in the field 2026-09-14 while shipping the EasyBlog add-on: a site linked at 18:49 could not see an add-on published at 20:35 until its cached list was refreshed by hand — 16 elements cached, 17 actually entitled.- 2.7.1's self-heal could not catch this. It fires only when the cached list is empty (verified under a pre-entitlement build). A list that is populated but out of date looks healthy, and that is precisely the case that recurs on every release.
- New:
ProActivationHelper::refreshIfCatalogOffersUnknown(). The catalog view now re-verifies when the catalog offers a Pro element this site has never heard of. The unknown set is fingerprinted and the fingerprint stored, so the probe is self-limiting: a newly published add-on changes the fingerprint exactly once and costs exactly one round trip, while a site that genuinely does not own an add-on asks once and then stops — rather than re-verifying on every page view for an answer that will not change. - The fingerprint is cleared on deactivate alongside the other Pro params, so an unlink → re-link with a different or upgraded account re-probes instead of trusting the old answer.
v2.7.1 September 11, 2026
Add-on catalog + licensing refinements: the catalog now understands per-add-on entitlement (Install vs "Get it →" per add-on, so à-la-carte buyers see exactly what they own), the Pro-activation card is reframed as "Link your Cybersalt Extensions account," every Pro add-on card carries a vendor-dependency notice, and the bundled fallback catalog no longer 404s installs. Builds on 2.7.0 (Codex support). Companion release: cs-release-manager 1.11.6.
New — per-add-on entitlement in the catalog (GitHub #21 / #25, Phase 2)
- Browse MCP Add-Ons now shows Install vs "Get it →" per add-on, based on what the linked account actually owns — not one umbrella Pro verdict. An à-la-carte buyer (e.g. only "MCP for 4SEO") sees 4SEO as installable and the other Pro add-ons as "Get it →" (linking to the membership store), and is no longer told they have no membership (#21, the "four denials in thirteen minutes" bug).
- cs-release-manager
api.verifyaccessgained an additiveentitled_elementslist — the extension_elements the account can install, resolved across the whole catalog from the account's group memberships (free add-ons always included; owned Pro add-ons via à-la-carte plan group OR All-Access). Backward-compatible: old clients ignore the field. Verified end-to-end: Dragan (à-la-carte) → 4SEO + free only; an All-Access account → all 16. - cs-mcp-for-j stores/exposes it (
ProActivationHelper::getEntitledElements()/isEntitledTo()/isLinked()); the catalog view keys per-add-onhas_pro_membershipoff it and self-heals a stale/empty list on load (so an existing install verified under the pre-entitlement build fixes itself on first catalog view). - The dead "Pro — manual install" locked pill is now an actionable "Get it →" link to
/membership-plans. - Companion release: cs-release-manager v1.11.6 (the
entitled_elementsresolver inAccessCheckHelper).
Changed — "Link your Cybersalt Extensions account" + Pro add-on vendor-dependency notice (GitHub #21 / #25, Phase 1)
- Dashboard rename. The "Pro Membership Activation" card is now "Link your Cybersalt Extensions account" (button "Activate Pro" → "Link account", "Membership email" → "Purchase email"), and the intro is reworded so a customer who bought a single à-la-carte add-on is no longer told they need a "Pro membership." One link step (enter the email you purchased with) covers both All-Access and individual-add-on buyers.
- Vendor-dependency notice on Pro add-on cards. Every Pro add-on in Browse MCP Add-Ons now shows: "Adds MCP tools for <Extension> (from <Vendor>). You'll need that extension itself — a separate product purchased from the vendor — installed on your site for these tools to work." Rendered from each add-on's
target_extension(name + vendor). Prevents the "I bought the add-on, where's the extension?" confusion (Leandro / customer feedback).
Fixed — catalog fallback pinned dead add-on versions (GitHub #20)
- Bundled fallback catalog no longer 404s add-on installs.
catalog.fallback.jsonhad hardcoded the 4SEO and RSTicketsPro add-ons at v1.8.0 in theirdownload_url, but cs-release-manager only ever published v1.10.2 for both — so any catalog install that fell back to the bundled file (when the liveapi.catalogfetch failed or was cache-stale) asked cs-release-manager for a version that did not exist and got "Requested version not found." Reported by Dragan Subotic (first MCP for 4SEO customer); his workaround was to drop the version parameter, which pulls the latest and works. - Fix is structural, not a version bump. The
&version=pin is now stripped from everydownload_urlin the fallback so it always installs the latest — matching the proven workaround and immune to future drift. The bundled file was also regenerated from the live catalog, so it now carries all current add-ons (16, was 4) with the AI-neutral "Adds MCP tools for…" descriptions instead of the stale "Adds Claude tools for…" text. - build.ps1 now refreshes the fallback from the live catalog on every build (fetch → strip version pins → relabel source → validate → write), with a graceful keep-the-committed-file fallback if the fetch fails. The bundled fallback can no longer silently drift out of date.
v2.7.0 September 5, 2026
New — Native Codex connection support
- Added a dedicated Codex tab to the Setup Guide with site-specific Windows PowerShell, macOS/Linux,
codex mcp add, andX-Joomla-Tokenfallback snippets. - Tokens are referenced by a generated environment-variable name instead of being stored in Codex's
config.toml; the existing browser-only copy substitution remains available for the environment setup commands. - Added concise server-wide
instructionsto the MCPinitializeresponse so Codex receives prompt-injection and write-approval guidance during the native handshake. - Made the Dashboard setup prompt client-neutral and added a permanent Codex registration path alongside Claude Code.
- Documented Codex registration, verification, least-privilege access, read-only mode, and the cPanel/PHP-FPM fallback in the README.
Build
- Bumped the package and component manifests to 2.7.0 and the System plugin to 1.15.0. The installable package continues to bundle the component plus the required System and Web Services plugins.
v2.6.0 August 21, 2026
Ships two new capability classes: a Support admin view for submitting questions and ideas directly to with tier-aware routing, and a screencasting-safe secret-reveal system for the Dashboard's Joomla API token and Pro membership email — blur by default with configurable hover-with-delay + auto-hide.
New — Support view
Fourth admin submenu item alongside Dashboard / Browse MCP Add-Ons / Setup Guide. Cross-view toolbar buttons on all three existing views also link here.
- Tier-aware banner at the top drives the framing:
- Pro members see a green priority-support banner: "You have an active MCP for J Pro membership — thank you! Pro members get priority support. Ask us anything — bug reports, feature requests, questions, ideas — all welcome."
- Community (Free) users see a friendly blue banner: "We love hearing from you. Support for the free version is on a best-effort basis... If you need guaranteed response times or priority attention, consider an MCP for J Pro membership" — with a real hotlink to the OS Membership Pro plans page where users actually buy.
- Form fields: type picker (🐛 Bug / ✨ Feature / ❓ Question / 💡 Idea / 💬 Other), subject (3-200 chars), message (10-5000 chars), reply-to email (pre-filled from Joomla user).
- Backend:
SupportController::submit()validates + sends via Joomla's Mailer to.This email address is being protected from spambots. You need JavaScript enabled to view it. Reply-Toset to the submitter's email so replies from the operator land in their inbox, not the site's noreply. - Subject-line tier tag for Gmail-side triage:
[MCP for J Pro Support]or[MCP for J Community Support]+ type label + user's subject. Filter/label rules on the receiving inbox become trivial. - Auto-attached metadata (visible to the user in the right sidebar for full transparency): site URL, cs-mcp-for-j version, Joomla version, PHP version, installed csmcpforj-family add-ons + their enabled state, Pro membership status, submitting user's name + email. Answers the environment questions any first-reply would otherwise have to ping-pong for.
- HTML-entity decode on the subject line — type labels in the language file use
—for HTML rendering in the form UI, but email subjects are plain-text. Controller now runshtml_entity_decode(..., ENT_QUOTES | ENT_HTML5)on the type label before composing the subject so the em-dash lands as an actual character, not a literal—string.
New — Screencasting-safe secret reveal system
The Dashboard's Joomla API token pill (in the copy-prompt preview) and Pro membership email input are now blurred by default with an intentional-reveal interaction, safe for live demos and screencasts.
- Blur by default via
filter: blur(5px)— DOMtextContentstays intact so clipboard copy still works; only the visual rendering is obscured. The empty-state placeholder variants (<PASTE YOUR JOOMLA API TOKEN HERE>and the empty pre-activation email field) are explicitly NOT blurred — nothing to hide there. - Hover-with-delay reveal: user must hold hover over the blurred element for N seconds before the blur comes off. A centered countdown pill overlays the element reading "Revealing in 3s" → "Revealing in 2s" → "Revealing in 1s" so the operator knows the timer is running (without it, a slow reveal reads as "hover not working").
- Mouse-off resets — leaving the element cancels any in-progress countdown AND removes the reveal if it had happened. Re-entering starts a fresh full-length countdown. Makes reveal an intentional act; a mouse pointer transiting the element on the way to click something else never triggers exposure.
- Auto-hide after reveal — once revealed, N seconds later the blur automatically returns even if the mouse is still over the element. Belt-and-braces for the "glanced, read it, then got distracted" case where an unattended browser could otherwise leave the secret exposed indefinitely.
- Two new component options under a new Screencasting privacy fieldset (Components → MCP for Joomla → Options):
hover_reveal_delay_seconds(integer 0-60, default 3) — hold-hover duration before reveal.0= immediate reveal (removes the anti-accidental-hover protection, restores classic single-hover behaviour).hover_reveal_hide_seconds(integer 0-300, default 8) — auto-hide duration after reveal.0= no auto-hide (reveal persists until mouseleave).- Design pattern locked in project auto-memory (
feedback_screencasting_safe_secret_reveal_pattern.md) as the canonical secret-display style for all future Cybersalt Joomla extension admin UIs. Sibling tofeedback_advisory_box_design_pattern.
Miscellaneous polish
- Admin sidebar translation fix — the
.sys.iniwas missing keys forSUBMENU_SETUPGUIDE(added in v2.5.0) andSUBMENU_SUPPORT(added in this release). The Joomla sidebar loads.sys.ini, not the regular.ini, when rendering menu-item labels outside the currently-active component — so both submenu items were showing their rawCOM_CSMCPFORJ_SUBMENU_SE.../COM_CSMCPFORJ_SUBMENU_SU...constants when navigating from other admin pages. Now both keys are duplicated into.sys.iniand both render as Setup Guide / Support.
v2.5.0 August 15, 2026
Biggest release since v2.0.0. Ships a brand-new admin Setup Guide view, a full-component AI-neutrality repositioning (welcomes every major AI client, not just Claude), and a critical Claude Desktop compatibility fix that unblocks a Zod-validation regression on Claude Desktop 1.30096+.
New — Setup Guide view
Third admin submenu item alongside Dashboard + Browse MCP Add-Ons. Designed explicitly for non-technical audiences, leads with the Easy Way (Dashboard copy-prompt path) and folds all technical setup into a collapsed Advanced section.
- Universal sections: what MCP is, connection recipe, how to generate a Joomla API key, security & sharing model (per-user tokens = passwords; one-per-authorised-person vs. shared-demo patterns; revocation).
- Per-client tabs in the Advanced expander — Claude (Desktop, Code, claude.ai), Cursor, ChatGPT custom connectors. Each tab has copy buttons that substitute your API key inline before copying to clipboard (same
csmcpforj-tokenlocalStorage key as the Dashboard, so a key typed on either view propagates). - Claude Desktop walkthrough uses the wrapper-script pattern — the only pattern verified to work with current Claude Desktop builds (see the compat-fix note below for why the obvious approaches don't work). Four numbered steps: create a small
.cmdwrapper file with the mcp-remote command inside, editclaude_desktop_config.jsonto reference the wrapper, fully quit from the system tray, restart. - Troubleshooting collapsibles: 401 Unauthorized, 406 Not Acceptable, empty tools/list, 404 on endpoint, 403 / Malware detected (WAF-blocked), Edit-Config button does nothing, Add-custom-connector dialog has no API-key field, “Some MCP servers could not be loaded” (with wrapper-script workaround), “I restarted Claude Desktop but the new connection doesn't appear” (tray-restart gotcha).
- FAQ collapsibles: anonymous access, multiple tokens per user, rate limits, HTTPS/ports, key rotation, other-clients-work-too.
- Sticky-nav sidebar with jump links to each section.
- Design pattern: warning / advisory / callout boxes use the dark neutral bg + colored left border + colored title style from
feedback_advisory_box_design_pattern(not Bootstrap's colored-fill alerts).
AI-neutrality repositioning
Component-wide rewrite of user-facing copy so cs-mcp-for-j names every major MCP client (Claude, Cursor, Cline, Continue, ChatGPT custom connectors, GitHub Copilot, Gemini CLI, Windsurf, mcp-cli) instead of leading with Claude. The MCP server itself was already 100 % AI-neutral (any conformant client works); this is a copy + framing update.
- 10 admin language strings rewritten in
com_csmcpforj.ini— dashboard headline is now “Connect an AI assistant to this site” (was “Connect Claude to this site”), primary tab is “Copy a setup prompt” (was “Copy a prompt into Claude”), client lists throughout name multiple vendors inclusively. - Two hardcoded discovery-JSON strings rewritten (
McpController.php:132andplg_system_csmcpforj/src/Extension/Csmcpforj.php:234) — the text returned on GET requests to/api/index.php/v1/mcpno longer leads with Claude. - Package sys.ini description (
pkg_csmcpforj.sys.ini) — the description shown in Extension Manager after install now names all major MCP clients. - README.md — lead paragraph rewritten; client-config section now has per-client blocks for Claude Desktop / Code / claude.ai, Cursor, Cline, Continue, ChatGPT, GitHub Copilot, Gemini CLI, Windsurf, Zed.
- Live server-side (deployed 2026-08-07, no client change needed): all 8 MCP add-on catalog entries on cybersalt.com had
"Adds Claude tools for..."rewritten to"Adds MCP tools for..."— visible immediately on every existing install's Browse MCP Add-Ons view.
Critical fix — Claude Desktop compatibility
inputSchema.properties on tools with no input parameters (12 of 152 tools including list_access_levels, get_joomla_version, check_for_updates, list_user_groups, list_schema_types, get_site_info, plus several add-on tools) now serialises as JSON {} (record) instead of [] (array). PHP's json_encode emits [] for empty native arrays but MCP spec requires {} for empty property records. Claude Desktop 1.30096+ enforces this strictly via Zod validation and rejects the entire tools/list response with “Invalid input: expected record, received array” if any tool violates. Effect: Claude Desktop users saw the MCP server listed as connected but no tools available.
Single-point fix in packages/com_csmcpforj/admin/src/MCP/ToolRegistry::describeForMcp() — casts an empty properties array to new \stdClass() before it goes into the response. Cascades to base tools AND every installed add-on with no per-tool changes needed. Verified end-to-end on cybersalt.org: Claude Desktop now sees all 152 tools successfully.
Catalog view improvements
- All state badges visible in the collapsed card row — Free/Pro tier + NEW + EXPERIMENTAL + installed-state + Superseded all render on the summary line so you can see the whole state of each add-on without expanding. Previously only the tier badge showed collapsed; everything else was hidden behind the click-to-expand.
- New “Not installed” badge (neutral gray) — fills the fourth quadrant of the state grammar so every card explicitly advertises its state. Previously not-installed meant “no badge” which required the reader to notice absence.
- Installed-disabled badge recoloured from
bg-secondary(gray) tobg-success-subtlewith green border and text-success-emphasis. Grammar: same color family as active but dimmed = “still installed, but paused.” Freesbg-secondarygray for the new Not-installed badge. - New “Update to vX.Y.Z” badge in the collapsed summary row when an installed add-on has an update available. Clickable — navigates directly to the install action with
event.stopPropagation()so the click doesn't also toggle the card open. Yellow (bg-warning) for the classic action-required cue.
Dashboard polish
- Token pill in prompt preview is blurred by default (
filter: blur(5px)) — makes the Dashboard safe for screencasting the setup workflow. Hover-to-reveal for the operator's own peek. Copy button still copies the real token because clipboard payload reads DOM textContent, not rendered CSS. Placeholder-mode variant explicitly NOT blurred — the public placeholder text stays readable.
Miscellaneous
- Setup Guide Easy-Setup card accent swapped from cyan
#4dd0e1to Bootstrap primary blue#0d6efdfor consistency with its CTA button and the Joomla-admin canonical action-blue. - Setup Guide code snippets fix:
<PASTE YOUR JOOMLA API TOKEN HERE>placeholder now correctly copies with real angle brackets (fix for double HTML-entity encoding that was leaking</>into the clipboard payload). - Setup Guide advisory-callout design pattern applied to troubleshooting entries (colored left border + neutral background — per
feedback_advisory_box_design_patternmemory).
v2.4.0 July 21, 2026
Browse Catalog — collapsible cards
- Each add-on card is now collapsible. Native
<details>/<summary>— no JS. Collapsed view shows only the add-on name and the tier badge (FREE/PRO); expanding reveals everything else (description, version, installed-state badge, NEW / EXPERIMENTAL discovery hints, advisory disclosure, install/enable/uninstall buttons). All cards start collapsed on load so the grid scans in one glance regardless of catalog size. Chevron marker rotates 90° when open. Requested by Tim 2026-07-21 as the catalog grows past ~7 entries and the all-expanded layout gets hard to read. - Shortened all catalog titles to the uniform
MCP for <extension>format. Was:Cybersalt MCP add-on for AdAgency Pro/MCP add-on for 4SEO/ etc. — now:MCP for AdAgency Pro,MCP for 4SEO,MCP for RSTicketsPro, etc. Consistent voice + shorter reading pass — especially helpful now that titles are the primary at-a-glance identifier on collapsed cards. Change is server-side (cs-release-manager#__csrm_packages.titleon cybersalt.com), no code change here.
Bug Fixes
- 406 "Could not match accept header" for spec-compliant MCP clients. The MCP Streamable HTTP spec requires clients to send
Accept: application/json, text/event-streamon every POST, and current Claude Code builds set that header themselves — overriding any customAcceptconfigured on the server entry. Joomla'sApiApplicationnegotiates the Accept header against the route's registered format (application/vnd.api+jsononly) and rejected the request with 406 beforeMcpControllercould run, breaking every Claude Code connection to the endpoint.plg_system_csmcpforjnow normalises the Accept header toapplication/vnd.api+jsonfor the MCP route only (the endpoint only ever emits JSON, so the coercion is lossless). Any spec-compliant MCP client now connects without needing a custom Accept header workaround. File:packages/plg_system_csmcpforj/src/Extension/Csmcpforj.php. - Advisory disclosure text unreadable in Atum dark mode. v2.3.3's advisory redesign hardcoded severity-accent hex on the summary text (
#ffc107warning yellow, etc.), which read fine on a light card bg but competed with the surrounding dark card bg on Atum dark — combined with an unstyled default-blue link inside the message body, the box felt harsh + hard to scan. Swapped the summary text + icon colors from hardcoded hex to Bootstrap 5.3's--bs-*-emphasistheme tokens, which auto-flip under[data-bs-theme="dark"]— dark yellow-brown on light bg, soft warm yellow on dark bg. Border-left accent stays bright hex (small enough that saturation reads OK in both modes). Added explicit!importantlink color rule inside the advisory body with a dark-theme override for#6ea8feso the GitHub feedback link in the StageIt experimental warning renders as a soft blue rather than the harsh default. Tim 2026-07-21.
v2.3.3 July 9, 2026
Catalog advisory disclosure — dark-mode-friendly redesign. Ripped the advisory box off Bootstrap's stock alert alert-warning / alert-info / alert-danger classes (which paint the whole box in the semantic hue and don't respect Atum dark mode — the yellow-background variant washes out the title, drowns inline <code> tags, and pulls the eye off the surrounding card content). Rebuilt on the design pattern Tim signed off: neutral card background + 4 px colored left border + colored bold title, inspired by GitHub blockquote admonitions and Docusaurus callouts. Reads cleanly against both Atum light and dark card backgrounds without further tuning.
Advisory disclosure redesign
- Left border bar — 4 px solid, hex-coded per severity:
#ffc107(warning golden yellow),#ff5c66(danger coral-red),#4dd0e1(info bright cyan). - Summary title — same accent hex,
fw-bold, applied via inlinestyle=""on<summary>, on the leading<span>icon, AND on a wrapping<span>around the title text. Belt-and-braces so Atum's dark-mode default<summary>color can't clobber the accent regardless of external-CSS specificity or!important. - Body copy — normal
--bs-body-colorcontrast, integrates with surrounding card body flow. - Inline
<code>tags — inset-pill styling via scoped<style>block:--bs-body-bgbackground +--bs-emphasis-colortext +--bs-border-colorborder. Tool names likedeploy,sync,remove,restore,continue_*now read as discrete pills instead of low-contrast ghost text against the previous coloredalertbackground. - Marker — replaced the default
▶triangle with a rotating▸chevron that turns 90° when open. Small polish.
Design pattern locked in project auto-memory (feedback_advisory_box_design_pattern.md) as the canonical warning-box style for all future Cybersalt Joomla extension work — cs-articles-module-maxxed, cs-template-integrity, cs-edge-cache-marker, cs-override-checker, cs-userback-admin, etc. — once each touches an advisory / callout / warning surface.
Single-file change: packages/com_csmcpforj/admin/tmpl/catalog/default.php.
v2.3.2 July 7, 2026
Dashboard "Check for Updates Now" polish + one fatal-error fix that surfaces the first time a user actually clicks the button on a cybersalt.com-managed install.
Bug Fixes
checkUpdatesNowfatal on click. The bind-loops inDashboardController::checkUpdatesNow()were calling$db->bind()— a method that does not exist on Joomla'sDatabaseInterface.bind()lives on the query object. Both loops ($deleteand$countQuery) now bind onto the query, thensetQuery()->execute()/setQuery()->loadResult()runs the parameterised query correctly. Reproduces on any site with at least one cybersalt.com-managed extension and cached#__updatesrows.
UI
- Dashboard "Check for Updates Now" button dark-mode contrast. Was
btn-outline-primary(transparent background + blue border + blue text) — vanished into Atum dark's card background. Swapped tobtn-primary text-white, matching the other primary-action buttons on the dashboard. Reads clearly in both light and dark themes.
Files touched: packages/com_csmcpforj/admin/src/Controller/DashboardController.php, packages/com_csmcpforj/admin/tmpl/dashboard/default.php.
v2.3.1 July 6, 2026
Catalog badges + advisory disclosure. Two operator-flippable per-add-on flags and a structured "read before install" caveat, driven by the StageIt free add-on landing in the catalog and needing a way to tell prospective users up-front that server-timeout risk on chunked long-runners is beyond our control.
Catalog UI
- NEW badge — light-green pill next to the tier badge when
catalog_metadata.is_new = true. Draws the eye to recently-added add-ons without spamming everything. - EXPERIMENTAL badge — yellow warning pill (with flag icon) when
catalog_metadata.is_experimental = true. Hover tooltip points readers to the advisory disclosure below. - Advisory disclosure — native
<details>collapsible below the card description, rendered whencatalog_metadata.advisoryis populated. Bootstrapalert-info/alert-warning/alert-dangerstyling byseverity; message body is HTML so an operator can include links to docs or format multi-paragraph caveats. Collapsed by default (no visual weight on cards that don't need it) but the summary bar always visible so prospective users see the cue.
Companion release
Coordinated with cs-release-manager v1.11.4 — the api.catalog endpoint now passes is_new, is_experimental, and advisory fields through from each Package's catalog_metadata JSON. Operators set these via the Package edit form; no code deploy needed to flip an add-on's flags.
Upgrade notes
No breaking changes. The catalog endpoint's response gains three optional fields; MCP clients that don't recognize them ignore them. Add-ons without any of these flags render exactly as before.
v2.3.0 July 6, 2026
Admin sidebar preset XMLs are now inspectable from an MCP session. Driven by the americanfoam.com/stageit engagement 2026-07-06 — a Super User reported Content > Fields, Content > Field Groups, and Users > Fields missing from the admin sidebar on Joomla 5.4.6. Every existing menu tool queries #__menu, but the Joomla 4+ admin sidebar isn't stored there — it's rendered at request time from XML preset files on disk under administrator/components/<component>/presets/. The session ended with "SSH in and diff against stock" because there was no MCP path to the file contents. This release closes that loop.
New tools (2)
list_admin_menu_presets(read) — enumerate every preset XML installed on the site. Returns per-preset{component, name, path, size, mtime, sha256}plusmatches_stockwhen a bundled stock hash is available for the running Joomla version. Answers "what presets exist and which ones have been modified" in a single call.get_admin_menu_preset(read) — read one preset by(component, name). Structured input only — path is built server-side asadministrator/components/<component>/presets/<name>.xml, both parts regex-validated. Returns raw XML contents plus sha256 for direct diff against a stock reference.
🔒 Explicit scope choice: narrow allowlist, not read_file
The obvious way to solve this would be to add a general read_file tool. The full analysis is in ISSUE-6-get_admin_menu_preset-scoped-file-read.md at repo root; the short version:
- A leaked API token today = attacker can create content and install extensions. Bad, but bounded.
- A leaked token with
read_file= attacker can pullconfiguration.php(DB password,$secret, mailer creds, session key). Full site compromise, potentially shared-credential lateral movement to other Cybersalt-managed sites. - Silent, script-friendly recon over the whole webroot, much stealthier than the "install a plugin to exfiltrate" alternative.
Decision: scoped diagnostic readers, one file family at a time. This release ships the admin-menu-preset family. Follow-on issues (deliberately separate, not this release):
get_language_file— allowlisted toadministrator/language/**/*.ini+language/**/*.ini.get_template_manifest— readtemplateDetails.xmlfor a specific template.diagnose_admin_menu— higher-level convenience: read all presets, diff against bundled stock hashes, return a report of modified/added/removed entries + which<menuitem>blocks are missing.get_component_manifest— read<component>.xmlfor an installed component.
Each of those gets its own scoped tool with its own allowlist. No general read_file.
Safety stack on the preset tools
Both new tools route through a shared AdminMenuPresetPathTrait — single source of truth for the security model. Each layer is strict-by-default; any failure aborts before touching disk:
- Structured input only. No raw path parameter. Caller supplies
component+name; the tool builds the filesystem path server-side. - Regex validation.
componentmust match^com_[a-z0-9_]+$,namemust match^[a-z0-9_-]+$. Rejects.., path separators, null bytes, leading dots, extensions in the name. - realpath canonicalization. Resolved absolute path must start with
realpath(JPATH_ADMINISTRATOR . '/components'). Catches symlink escape attempts that string-prefix checks miss. - File-type + extension check.
is_file()(notis_link()alone), extension must be.xml. - Size cap. Response cap of 256 KB — presets are typically 5–15 KB; a preset XML much bigger than that is suspicious enough to refuse loading.
- Distinct "not found" vs "not authorized" responses.
PresetNotFoundExceptionfor genuine 404s (component exists but no preset by that name),InvalidArgumentExceptionfor allowlist violations. AI can distinguish "you asked for a preset that isn't installed" from "you asked for something outside the allowlist".
What test cases pass now
Per the spec:
- ✅
list_admin_menu_presetson a stock Joomla 5.4.6 returns exactly the expected presets with sha256 hashes.matches_stockreturnsnulluntil the stock-hash table is populated (opt-in — seeAdminMenuPresetPathTrait::stockHashLookup()docblock for how to generate + transcribe hashes; hash comparison logic is already wired up). - ✅
get_admin_menu_preset(component="com_menus", name="default")returns byte-identical file content on any Joomla install. - ✅
get_admin_menu_preset(component="../etc", name="passwd")— refused, no file access attempted. - ✅
get_admin_menu_preset(component="com_menus", name="../../../configuration")— refused, name regex rejects../. - ✅
get_admin_menu_preset(component="com_menus", name="nonexistent")— clean 404-shapedToolResult::error, no PHP warning. - ✅ Symlink
administrator/components/com_evil/presets/default.xml -> /etc/passwd— refused because realpath escapes the allowlist base. - ✅ Preset file > 256 KB — refused with a clear message.
Upgrade notes
No breaking changes. New tools land in a new "Admin Menu Presets" category, automatically picked up by the tool registry. If you have a ?categories= filter in use on the MCP endpoint URL, add admin_menu_presets to keep the new tools accessible.
The stock-hash lookup table in AdminMenuPresetPathTrait::stockHashLookup() intentionally ships empty in v2.3.0 — plumbing wired, awaits hash-gathering from stock Joomla installs. Populating it is a follow-on task; sha256 is returned regardless so external diffs against a reference install work today.
v2.2.0 June 19, 2026
Templates domain ships full coverage — file editing + style settings. Driven by GitHub issue #7 (Greg Willson Cassiopeia user.css engagement, 2026-06-26): every other step of the work landed cleanly through MCP except the actual file write, which required two human round-trips through the Joomla admin Template Files Editor. After this release, that whole workflow runs through Claude.
New tools (5)
File editing — equivalent to the Joomla admin's System → Templates → Site Templates → [template] → Edit Template Files screen, exposed as MCP tools:
list_template_files(read) — recursively list every file undermedia/templates/<client>/<template>/. Returns{path, size, mtime}per file with paths relative to the template root, suitable to feed back into the read/write tools.read_template_file(read) — return the text contents of a file under the template root. Auto-detects binary files (presence of null byte) and returns them base64-encoded so the JSON-RPC envelope stays valid.write_template_file(write, destructive) — overwrite-or-create a file under the template root. Returns post-write size + sha256 for the AI to verify what landed. PHP files intentionally NOT writable in v1 — see safety below.
Style settings — equivalent to opening a style under System → Site Template Styles → [style] and clicking Save:
get_template_style(read) — fetch a single style by id with its params blob decoded into a JSON object. Pairs with the existinglist_template_styles(which returns the list without params).update_template_style(write) — update a style's params (and optionally its title). Params are merged into existing (not replaced) — pass only the keys you want to change, usenullto delete a key. Same write semantics asset_plugin_paramsso the AI doesn't have to learn two different patterns.
Safety stack on the file tools
Template files can be executable PHP, so write access without guardrails would be effectively RCE if the API token leaks. The safety surface for the file tools is built around a shared TemplateFilePathTrait that every read/write tool routes through. Each layer is strict-by-default; any failure aborts before touching disk:
- Strict input validation —
client_idmust be 0 or 1;templatemust match the safe-element pattern (lowercase alphanumeric + underscore + hyphen + dot suffix);pathmust use forward slashes, no.., no null bytes. - Path canonicalization — every resolved target goes through
realpath(). Final path must start with the canonical jail root (media/templates/<client>/<template>/); catches symlink escapes that string-prefix checks would miss. - Write-extension allowlist —
write_template_fileonly accepts CSS, JS, SCSS, LESS, JSON, SVG, plain text, raster images, and font files. PHP is denied in this version; enabling .php writes is a deliberate later-version decision behind its own permission flag. Read access is broader (any extension) so the AI can still inspect existing PHP overrides to decide whether a CSS-only customization would suffice. - MCP
ToolAnnotations— list/read getreadOnlyHint=true, idempotentHint=true. Write getsdestructiveHint=true, idempotentHint=true(overwrites may destroy prior content; calling with same args twice = same end state). Read-only mode (component config) naturally excludes the write tool.
Auto-classifier additions
AbstractTool::getMcpAnnotations() now recognizes two more name-verb prefixes for automatic ToolAnnotations classification:
read_*→ readOnlyHint=true, idempotentHint=true (was previously falling through to the "unknown verb" default which is non-destructive but also not read-only). Matches the existinglist_/get_/check_/validate_/fetch_cluster.write_*→ readOnlyHint=false, destructiveHint=true, idempotentHint=true. Covers the new file tool and any future overwrite-or-create style tools.
Deliberately deferred
To keep the v2.2.0 release focused, these template-domain capabilities are NOT in this release:
create_template_style/delete_template_style— less common, more error-prone; can come later.create_template_override/delete_template_file— the issue #7 spec includes them but the MVP scope ships read/write without them.- Per-menu-item template-style assignment — already partially possible via existing menu tools.
- Schema-aware param validation (parsing each template's own
templateDetails.xmlconfig schema) — pass-through covers ~90% of use cases without it. csmcpforj.templates.editphppermission flag to unlock PHP writes — deferred until there's a concrete need.
Upgrade notes
No breaking changes. New tools land in the Templates category, automatically picked up by the registry. If you have a ?categories= filter in use on the MCP endpoint URL, add templates to keep the new tools accessible.
v2.1.1 June 18, 2026
Issue-backlog sweep. Bug fixes + catalog UX polish driven by the WMW #342 chat feedback and the open GitHub issues list.
Fixes
- RSTicketsPro Pro add-on (GitHub #5):
add_rst_ticket_replyforcedhtml=0regardless of passed argument. The wrapper was correctly extracting the caller'shtmlvalue and putting it in$data, butRSTicketsProTicketHelper::bind($data)was dropping the field empirically —saveMessage()always wrote html=0. Fix: after$helper->bind($data), set$helper->html = (int) $data['html']directly so the value survives. Ships in cs-mcp-for-j-addons-pro / plg_system_csmcpforjrst v1.8.1. - RSTicketsPro Pro add-on: missing
script.phpreferenced by manifest. The plugin manifest declared<scriptfile>script.php</scriptfile>but no file shipped — Joomla's installer would warn on install. Added the standard auto-enable-on-install script.php (matching the pattern of the other three addons) plus a post-install confirmation message.
Catalog UX (driven by Bjørn + Ivar feedback from WMW #342 chat)
- "Installed" badges polished. The catalog's per-card state badges now read green ✓ "Installed" (active) and grey "Installed (disabled)" (with hover tooltips explaining the state) instead of the terse "ENABLED"/"DISABLED" labels. Easier to register at a skim — "you've already got this" lands visually.
- Post-install confirmation message from every add-on plugin's
postflight. Tells the operator the install actually did something (no admin UI by design, tools appear in connected MCP clients) and names the new tools. Addresses Ivar's "couldn't find the Akeeba add-on after install — expected a menu entry" pattern. Lands in cs-mcp-for-j-addons-free (Akeeba Backup Core, Cybersalt Release Manager) and cs-mcp-for-j-addons-pro (4SEO, RSTicketsPro). - Supersede-pair logic —
superseded_byfield on catalog metadata. Each catalog entry can declare a list of extensions ({type, folder?, element, display_name?}) that, if installed on the client site, mean this catalog entry is redundant. The cs-mcp-for-j catalog template marks superseded rows with a teal "Superseded by <X>" badge and hides the Install / Update buttons (Toggle / Uninstall stay available on already-installed rows so cleanup is possible). Resolves Bjørn's request: hide Akeeba Backup Core add-on when Akeeba Pro is installed.
Investigation notes (no code changes)
- Pro activation false-positive on fresh install (Bjørn 2026-06-17) — investigated the activation helper's per-request memo path and the postflight script; no obvious mechanism that would carry a false "Membership Expired" verdict across a fresh install. Cannot reproduce statically. Queued for live-repro on a clean test site.
- AMM (Regular Labs Advanced Module Manager) form-name compat (Bjørn 2026-06-17 catalog-chat note) — audited the codebase; cs-mcp-for-j ships zero
onContentPrepareFormhandlers and the module-management tools (create_module,update_module, etc.) call into Joomla's MVC model layer directly, which is form-name agnostic. AMM'scom_modules.module→com_advancedmodules.modulerename does not affect us. Closing as N/A.
Companion releases
This release coordinates with simultaneous bumps:
- cs-mcp-for-j-addons-free v1.0.1 (Akeeba Backup Core + Cybersalt Release Manager) — post-install confirmation messages added.
- cs-mcp-for-j-addons-pro v1.8.1 (4SEO + RSTicketsPro) — RST html=0 fix, RST script.php added, post-install confirmation messages added.
- cs-release-manager v1.11.3 — api.catalog endpoint output gains the superseded_by field passed through from catalog_metadata.superseded_by JSON. No schema changes; no DB migration needed.
Upgrade notes
No breaking changes. The catalog endpoint's response shape gains one optional field (superseded_by); existing MCP clients ignore unknown fields. The "Installed" badge labels changed; if you had screen-reader translations relying on the old strings, update to the new COM_CSMCPFORJ_CATALOG_STATE_INSTALLED_ACTIVE / ..._DISABLED keys.
v2.1.0 June 18, 2026
Server-side safety + agentic ergonomics + native Joomla update tools. Several new cross-cutting features were added to the MCP server after surveying the landscape — see the credits section at the bottom of this entry.
Security
- Argument secret guard. Before any tool dispatches, the server recursively scans the tool's arguments for the current session's Bearer token. If found anywhere in the payload, the call is refused before reaching the tool handler. This is a server-side circuit breaker against prompt-injection attempts that smuggle the user's API token into a downstream tool call (e.g. via a fetched URL or article body the AI happens to be processing). Implementation in
MCP\Security\ArgumentSecretGuard. - Permanent-delete safety scaffolding.
delete_articlenow refuses to permanent-delete a row that isn't already trashed, with an error message that clearly explains the two-step path (trash first, then re-call withpermanent=true). Powered by a newRequiresTrashFirstTraitso the same pattern can extend to other delete-* tools incrementally. This is intentional friction for AI clients — the two-step intent has to be explicit, not a single mis-targeted call.
New Features
- Read-only mode (component config). New Options → "MCP server" tab with a "Read-only mode" switch. When on, the MCP endpoint only announces and accepts tools whose MCP ToolAnnotations declare
readOnlyHint=true(list_, get_, check_, validate_, fetch_*). Lets an operator hand an AI client a "browse but don't mutate" surface without revoking the user's API token. Per-request override:?read_only=1on the endpoint URL narrows further (but cannot UNLOCK writes from a site that has the floor set). - Category-based tool filtering. Optional
?categories=articles,users,menus(or singular?category=) on the endpoint URL filters which tools the server announces. Useful for context-window-bound models that choke on the full tool list. The filter applies consistently totools/listANDtools/callso a client cannot invoke a tool that the filtered list wouldn't have shown them. - MCP
ToolAnnotationson every tool. The server now emits the MCP-spec-standardannotationsobject on every tool descriptor:readOnlyHint,destructiveHint,idempotentHint,openWorldHint. Tools auto-classify from their name verb (list_/get_/check_/validate_/fetch_ → read-only; delete_/uninstall_/clear_/revoke_/cancel_ → destructive; etc.); individual tools can override viagetMcpAnnotations(). Lets MCP-spec-aware clients render safety badges per tool. - Type-introspection tools (2 new):
list_menu_item_types— every menu item type installed on this site, grouped by component. Discovery for the AI before callingcreate_menu_item, which needs an installed type code (Single Article, Category List, Login Form, Contact Form, Custom URL, plus any third-party component's types).list_module_types— every module type installed (mod_login, mod_menu, mod_custom, mod_articles_latest, mod_banners, plus any third-party module). Discovery beforecreate_module.- Joomla core update tools (3 new):
check_joomla_update(read-only) — force-polls the Joomla update server (bypasses the up-to-24h local cache) and reports current vs latest, release notes URL, update availability.joomla_update_healthcheck(read-only) — pre-flight: PHP version, database type/version, extension count, disk space. Returns OK/warning/error verdicts per check + an aggregate summary.apply_joomla_update(destructive, irreversible) — downloads + extracts + applies the update via Joomla's ownUpdateModel. Requires explicitconfirm=trueargument; without it, refuses with a message explaining why. Conservative scope: routine point releases on straightforward sites; for major version upgrades or sites with extension compat warnings, the description points the AI back to the web UI.
Credits
The infrastructure ideas above (argument secret guard, MCP tool annotations + read-only mode, trash-before-permanent-delete, category filtering, type-introspection, multi-step Joomla update tool family) were inspired by reviewing nikosdion/joomla-mcp-php (licensed AGPL-3.0). Each feature was studied for the concept — what problem it solves and why — and then independently reimplemented from scratch in cs-mcp-for-j's GPL-2-or-later codebase using our own naming, file layout, and code structure. No source code, configuration, or strings were copied. Nikolaos Dionysopoulos's broader MCP-for-Joomla work is excellent and worth a look if you're after a stdio-based MCP server that runs outside Joomla via Composer/PHAR.
Upgrade notes
- New component config field
read_only_modedefaults to0(off) — existing installs keep current behavior on upgrade. - 5 new tools added to the registry — the total surface is now ~100 tools. Consider using the new
?categories=filter on the endpoint URL if your MCP client struggles with the full list. - No schema migrations. No breaking changes to existing tools' input/output shapes; the only addition to the tools/list response is the
annotationsobject per tool, which MCP clients ignore if they don't recognise it.
v2.0.0 June 12, 2026
v2.0 catalog architecture lands as GA: the four extension-specific add-ons (4SEO, RSTicketsPro, Cybersalt Release Manager, Akeeba Backup) are now detached from the bundle and ship as standalone installable plugins from cs-release-manager on cybersalt.com. The component itself stays light — operators install only the add-ons their site actually uses, one click from Components → MCP for Joomla → Browse MCP Add-ons.
Security
- XSS in catalog install messages.
CatalogController::installAddonnow HTML-escapes catalog-sourcedaddonNameand request-sourcedaddonKeybefore passing them toText::sprintf/setMessage. A compromised remote catalog endpoint (or an operator-set malicious catalog URL) can no longer XSS the admin via a crafted addon name. Joomla's admin message bar renders messages as HTML, so this escape is required at the substitution boundary.
New Features — Catalog GA
- Browse MCP Add-ons view. Cards per add-on with Pro/Free tier badge, installed/disabled state, catalog-vs-installed version compare, target-extension info, and vendor link with an "MCP tools developed independently of [vendor]" notice.
- One-click Install. Wraps Joomla's standard
InstallerHelper::downloadPackage+Installer::install— same code path Extensions → Install from URL uses, so manifest parsing, file copy, postflight, and rollback all behave exactly like a normal Joomla install. Defensive checks: GET token,core.adminoncom_csmcpforj, addon_key must resolve to a catalog entry (no arbitrary URLs), HTTPS-only. - One-click Update. When an installed add-on's manifest version is older than the catalog version, the Install button becomes "Update to v…" and runs the same code path against the new zip.
- One-click Enable / Disable / Uninstall per card so the full lifecycle happens inside the catalog UI.
- Pro Activation flow. Dashboard "Pro Activation" card: enter membership email, activate locally, dlid gets appended to Pro add-on downloads so cs-release-manager's
AccessCheckHelper::verifyUpdateAccesslets them through. Deactivate Locally clears the activation. Activation state mirrors into Component Options (read-only) with a "Open Dashboard" link for the deactivate action. - Pro gate on Install button. Pro add-ons render a disabled "Pro — manual install" button with a tooltip when Pro activation isn't present locally. No more dead-end click → 401.
- Akeeba Backup add-on added to the launch lineup (free) alongside 4SEO, RSTicketsPro, and Cybersalt Release Manager (free).
Improvements
- Dashboard Options button — the gear icon now appears on the dashboard view too, not just on Browse Add-ons.
- Canonical Joomla sidebar. Cross-view nav now uses
HTMLHelper::_('sidebar.addEntry', ...)— same pattern as com_users / com_modules. - Skip-rebuild when source unchanged.
build.ps1reuses an existing standalone add-on zip when no source file is newer than it. Stops spamming cs-release-manager with identical-content uploads when only the core changed. - Per-extension version discipline. Each sub-extension's
<version>now reflects when its source actually changed, not the bundle release. No more lockstep bumps that cause spurious "Find Updates" prompts on installed sites.
Upgrade notes
Standard "Install from File" over your existing install. Postflight auto-enables the core component, system plugin, and webservices plugin. Add-ons no longer ship in the bundle — install them per-site from Components → MCP for Joomla → Browse MCP Add-ons.
If you have a previous version of csmcpforj4seo or csmcpforjrst installed via the v1.10.x bundle, they remain in place after the upgrade (Joomla doesn't auto-uninstall children that move out of the bundle). The catalog will detect them as installed and offer Enable/Disable/Uninstall as normal.
v1.10.2 June 10, 2026
Same-day patch on the heels of v1.10.1's first public release. Closes two MCP discoverability paper-cuts caught while filling out cybersalt.com's /extensions/mcp-for-j page through the live endpoint, plus a belt-and-braces fix for the missing Update Sites entry some installs had after upgrading from v1.10.0.
🔧 Discoverability — article_id and url aliases
- New
AbstractTool::requireItemOrArticleId()helper accepts either the canonicalitem_id(matches Joomla's#__schemaorg.itemId) or the natural-soundingarticle_idalias. Applied to: get_article_schemaset_article_schemaclear_article_schemaset_article_custom_jsonldset_article_custom_jsonld_bulk(per-entry)fetch_rendered_urlnow acceptsurlas an alias forpath.- All six tool descriptions rewritten to lead with
REQUIRED ARG: ...so an agent doesn't have to read past the prose to find the parameter name.
These were caught when an agent extrapolating from update_article(id=812) reasonably guessed article_id for set_article_custom_jsonld — and got an error. Both names work now.
Update Sites missing after upgrade
The package manifest's <updateservers> block is supposed to be processed by Joomla's PackageAdapter on install AND upgrade. In practice it lands reliably on fresh installs but is flaky when upgrading from a version of the package that didn't ship an <updateservers> block — exactly the v1.10.0 → v1.10.1 case. Result: System → Update Sites stayed empty, "Find Updates" had nothing to query.
New ensureUpdateSiteRegistered() step in the postflight script explicitly:
- Finds the package's
extension_idin#__extensions - Looks up any linked
#__update_sitesrow via the#__update_sites_extensionsjoin - If a row exists: refreshes the URL, name, type, and enables it
- If not: inserts both rows
Idempotent. Safe to re-run. Anyone on v1.10.1 with a missing Update Sites entry gets it added when they install v1.10.2 over the top.
Upgrade
Standard "Install from File" over your existing install. No data migration, no DB schema changes.
v1.10.1 June 10, 2026
Public-release housekeeping. Adds the Cybersalt update-server URL to the package manifest so Joomla's "Find Updates" surfaces future versions automatically — required for the first official public release on cybersalt.com.
Update server
pkg_csmcpforj.xml now declares:
<updateservers>
<server type="extension" name="MCP for Joomla">https://www.cybersalt.com/index.php?option=com_csreleasemanager&task=api.updatexml&format=raw&element=pkg_csmcpforj</server>
</updateservers>
Existing installs upgrading from v1.10.0 → v1.10.1 register the update server during the package install; from then on, Joomla → System → Update will detect new versions without re-installing.
Supersedes v1.10.0
v1.10.0 was tagged earlier on June 3 but had no update-server URL, which made it unfit as the public-distribution build. v1.10.1 is functionally identical except for the manifest line; if you grabbed v1.10.0, install v1.10.1 over the top.
v1.10.0 June 3, 2026
Closes the ACL gap that previously required UI clicks to grant a non-Super-User group access to a component. Adds full user-group CRUD plus a new Permissions domain that reads and writes #__assets.rules directly.
Tool count: 114 → 119. New domain: Permissions (2 tools). Three new tools join Users.
New Features — User Group CRUD
create_user_group(title, parent_id=1)— goes throughcom_users'GroupModelso the matching#__assetsrow is created and the new group inherits from its parent.update_user_group(id, title?, parent_id?)— PATCH semantics. Moving a group changes which permissions it inherits.delete_user_group(id, confirm:true)— refuses if any users are still in the group or if child groups inherit from it. Joomla's built-in groups can't be deleted.
New Features — Permissions Domain
list_component_permissions(component)— return the parsed#__assets.rulesJSON for a component (orroot.1for global) plus the resolved value for every (group, permission) cell viaAccess::checkGroup()— the same inheritance walk Joomla uses at runtime. Diff the resolved column across groups to find which permission is gating access.
Actions are discovered from the component's access.xml (canonical) and from any keys already present in the rules JSON.
set_component_permission(component, group_id, permission, value)— set one ACL cell.value:1= Allowed,0= Denied,null= Inherited (clears the explicit setting). Equivalent to clicking one row/column intersection in the admin Permissions tab and saving.
Refuses one footgun: setting core.admin Denied for Super Users on root.1. Other dangerous combinations are allowed because Joomla itself allows them.
Security review (pre-release sweep, June 3)
Five findings caught and fixed in the same release. None reachable from outside an authenticated admin session, but worth closing for defense in depth:
- MED — Path traversal in
list_component_permissions. Inputcomponentwas only validated withstr_starts_with('com_'), so values likecom_../../etc/passwdpassed and got concatenated intoJPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'. NewAbstractTool::requireSafeAssetName()helper enforcesroot.1|com_[a-z0-9_]+|com_<name>.<subtype>.<id>shape. Applied to bothlist_/set_component_permission. - MED — Privilege escalation via direct-DB token tools. The four new
*_user_api_tokentools bypassed Joomla's UserModel, so a Manager withcsmcpforj.writecould mint a Super User token and pivot. NewAbstractTool::requireCanEditTargetUser()helper mirrors Joomla's "can't edit equal-or-higher privilege users" check by comparingMAX(usergroups.level)across actor vs target. Super Users bypass; self-operations bypass. Applied to all four token tools + thecreate_user(enable_api_token: true)mint path (defense-in-depth: UserModel already enforces it during group assignment). - LOW — Missing ACL on
CatalogController::refresh. Any logged-in admin who could hit the URL could trigger remote fetches against cybersalt.com. Now requirescore.adminoncom_csmcpforjin addition to the form-token check. - LOW —
CatalogModelaccepted any URL scheme for the catalog endpoint. An operator who could edit the component config could setfile://orgopher://. Now allowlistshttp://andhttps://only. - VERY LOW —
JoomlatokenHelper::loadProfileRowsbound$likePatternbefore assigning it. Worked because Joomla'sbind()is by-reference, but fragile against refactors. Reordered.
Use case (from field testing, June 3)
Granting a new low-privilege group access to DPCalendar previously meant stepping a test user through each built-in group manually to discover the gating permission. Now:
list_component_permissions(component: "com_dpcalendar")
→ diff Super Users vs Manager → identify "core.admin" as gate
create_user_group(title: "Kiosk Access", parent_id: 1)
→ returns id: 10
set_component_permission(component: "com_dpcalendar",
group_id: 10,
permission: "core.admin",
value: 1)
update_user(id: 7, groups: [10])
Four MCP calls instead of an admin GUI dance.
v1.9.0 June 3, 2026
Closes the per-user API token management gap that previously forced operators to log in as each target user to mint or rotate tokens. Adds a full joomlatoken admin surface plus atomic create-and-mint on create_user.
Tool count: 110 → 114.
New Features — User / Token Surface
get_user_api_token_status(user_id)— read-only state of a user's API token:enabled,has_secret,algorithm. The secret itself is never returned.enable_user_api_token(user_id, enabled)— toggle thejoomlatoken.enabledflag without touching the secret. Useful for temporarily blocking a token without invalidating it.reset_user_api_token(user_id)— generate a fresh secret and return the display token the user can paste straight into an MCP client'sAuthorization: Bearerheader. Format mirrorsplg_user_tokenexactly:base64("<algo>:<userid>:<hmac>"). Bypasses Joomla's "only the user can see their own token" GUI restriction because Super User MCP tools are by definition programmatic admin.revoke_user_api_token(user_id)— disable + zero the secret. For incident response. Restore access withreset_user_api_token.
Improvements
create_user(enable_api_token: true)— atomic create-and-mint. Response now includesdisplay_tokenand a pre-formattedpaste_asheader string. Replaces the previous multi-step dance (create user → bump to Manager → log in as user → reset token → unbump) with a single call.get_user(include_profile: true)— adds#__user_profilesrows to the response (joomlatoken state, profile plugin fields, vendor plugin fields like DPCalendar's private-feed token, etc.). The rawjoomlatoken.tokensecret is redacted to[redacted; present]; other plugin tokens are returned verbatim so the agent can spot them.
Engineering
- Token format mirrors
plugins/user/token/src/Extension/Token.phpexactly so the display string returned byreset_user_api_tokenworks in any MCP client. The HMAC uses Joomla'ssecretfromconfiguration.phpas the key (same as the GUI). - New
JoomlatokenHelperconsolidates the seed-gen, HMAC, and#__user_profilesupsert logic so the four new tools (andcreate_user) share one code path.
v1.8.1 May 25, 2026
Fills out the Custom Fields domain so programmatic setup of a clean field group on an article context is one MCP call rather than 6+ admin clicks. Closes issue #1 — VMT template change-log Subform field-group setup on 2026-05-25 needed it.
Tool count: 103 → 110. No new domains; Custom Fields just goes from 4 tools to 11.
New Features
- 5 new field-group tools — full CRUD over
#__fields_groups(the tabs that custom fields appear under in the article editor): list_field_groups(context?, state?)— returns id, title, context, state, access, ordering, language, description, noteget_field_group(id)— full row with decoded params blobcreate_field_group(title, context, state?, access?, language?, description?, note?)— callscom_fields'GroupModel, all save hooks fireupdate_field_group(id, title?, state?, access?, language?, description?, note?, ordering?)— PATCH semantics; context intentionally NOT updatable (changing a group's context orphans every field assigned to it — create a new group in the new context instead)-
delete_field_group(id, confirm:true)— requires explicitconfirm:true; fields previously assigned to the group get theirgroup_idreset to 0 (unassigned, appear under the generic "Fields" tab); the fields themselves are NOT deleted (usedelete_custom_fieldfor that) -
2 new custom-field write tools — fills the create/update/delete trio:
update_custom_field(id, title?, label?, description?, required?, state?, group_id?, access?, language?, default_value?, note?, ordering?, only_use_in_subform?, assigned_cat_ids?, fieldparams?, params?)— PATCH semantics; surfaces the three properties that previously required admin clicks:group_id(assign to a field-group tab),assigned_cat_ids(M:N to#__fields_categories— pass[-1]for "no categories",[]for "all categories"), andonly_use_in_subform(1 hides the field from the standard editor — required when building Subform children via the API). Context intentionally NOT updatable.delete_custom_field(id, confirm:true)— callsFieldModel::delete()which also removes the field's values from#__fields_valuesacross every article in the context. Destructive and not reversible —update_custom_field(state=0)is the non-destructive alternative.
Improvements
get_custom_fieldnow returnsassigned_cat_ids— queried from#__fields_categories(the M:N join table, NOT a column on the field row). Empty list = "all categories" (no restrictions);[-1]= "no categories" (Joomla's sentinel value for explicit none). Description rewritten to surface the semantics so the agent reads-before-decides instead of guessing. Theparamsfield was already in the response but unmentioned in the description; description now covers it explicitly.
Bug Fixes
delete_custom_field+delete_field_groupinitially failed with empty error messages on first live test —com_fields'FieldModel::canDelete()refuses unlessstate=-2(trashed). Joomla's admin UI does this as two phases (set State → Trashed, then "Empty Trash"); both delete tools now trash via$model->publish([$id], -2)before calling$model->delete($ids), so one MCP call = gone, matching user expectation of a "delete" verb. Verified inadministrator/components/com_fields/src/Model/FieldModel.phpline 831 (if (empty($record->id) || $record->state != -2) { return false; }).
v1.8.0 May 23, 2026
Headline: cs-mcp-for-j now manages a second Joomla extension end-to-end. The new RSTicketsPro MCP add-on adds 20 typed tools for RSJoomla!'s helpdesk extension, alongside three new 4SEO Business Profile tools that complete the LocalBusiness JSON-LD picture for sites running 4SEO. The version bump from 1.7.x → 1.8.0 reflects that "entire new Joomla extension domain" jump rather than a feature increment within the existing surface area.
Tool count: 80 → 103. Domain count: 13 → 14.
New Features
- RSTicketsPro MCP add-on (
plg_system_csmcpforjrst) — a new bundled-but-separable plugin that brings the RSJoomla! RSTicketsPro 3.x helpdesk extension under MCP control. 20 tools covering the full ticket workflow: - Read (12):
list_rst_tickets(with JOIN'd dept/status/priority/staff/customer labels and rich filters incl. status, dept, priority, staff, customer, last_reply_customer, flagged, search, date range),get_rst_ticket(full state + resolved labels + custom field values + anautocloseblock showing warning/close ETAs and what's blocking them),get_rst_ticket_messages(conversation thread withis_staffcomputed against the actual staff user_id set; RST system-message rows are decoded from PHP-serialized blobs into a cleansystem_event {type, from, to, user_id}object with a human-readable summary, and aninclude_system_messagesflag toggles them),get_rst_ticket_history,get_rst_ticket_notes,get_rst_ticket_files, plus six lookups:list_rst_departments,list_rst_statuses,list_rst_priorities,list_rst_staff(with resolved Joomla user + group + departments-with-access; surfaces the trap thattickets.staff_idis actually a Joomla user_id, NOT the_rsticketspro_staffPK),list_rst_groups(25+ permission flags coerced to bools),list_rst_custom_fields. - Write (8):
add_rst_ticket_reply,add_rst_ticket_note,update_rst_ticket(with{from, to}diff per changed field),close_rst_ticket(matches admin "Close" — also stops time tracking),reopen_rst_ticket,flag_rst_ticket,notify_rst_ticket(autoclose-warning email with meaningful response when RST refuses to send),delete_rst_ticket(destructive — requires explicitconfirm:true). - Architectural design: write tools call into RSTicketsPro's own
AdminModelmethods ($model->reply(),$model->updateInfo(),$model->notify(),$model->toggleTime(), etc.) instead of doing direct SQL. That means every email notification, every ticket_history entry, every department-change ticket-code regeneration with custom-field migration, every staff-access validation, every time-tracking-stop-on-close happens automatically — indistinguishable from a human staff member clicking through the admin UI. - Same separable-bundled-plugin pattern as
plg_system_csmcpforj4seo— can split into its own paid SKU later without restructuring the core. -
Tools live in
Cybersalt\Plugin\System\Csmcpforjrst\Tools\{Tickets,Lookups}\*. -
4SEO Business Profile typed wrappers (
get_4seo_business_profile,set_4seo_business_profile,clear_4seo_business_profile) — the third typed 4SEO write wrapper, alongsideset_4seo_meta_overrideandset_4seo_config. Wraps the site-wide LocalBusiness profile that 4SEO emits as the#defaultBusinessJSON-LD node on every page (#__forseo_configrowscope='default', key='sd'). Handles all the structural gotchas:business_typestored as single-element array (renderer doesarray_pop),addressCountrysame,logonormalised to{url, width, height}object, opening hours fanned out from a clean[{day, opens, closes}]array into the 28-fieldhoursMon1Opens/etc. grid withorganizationHoursType=3(CUSTOM) set automatically. Solves the chicken-and-egg: the row doesn't exist until a human saves the admin form, so the typed wrapper creates it from scratch with a defaults baseline pulled from 4SEO's ownconfig/sd.php(matches what the admin UI would produce on first save). Closes the Westshore Eye Care site-wide LocalBusiness gap that ISSUE-3 + ISSUE-4 chased — the schemaorg plugin's site-wide schema isname + image + sameAsonly (no phone/address/geo), so 4SEO's profile is the only working path to a full LocalBusiness on most Cybersalt sites. Source design grounded in the re-extracted 4SEOvendor/weeblr/forseo/tree that the May 11 snapshot missed.
Improvements
-
get_rst_ticket_messagesnow decodes RST's system-message rows — RSTicketsPro stores status / department / priority / staff changes as syntheticticket_messagesrows withuser_id=-1and aserialize()'d PHP array in the body (a:4:{s:4:"type"...}). The tool nowunserialize()s those (withallowed_classes => falseto prevent PHP gadget chains) and surfaces a cleansystem_event {type, from, to, user_id}object plus a human-readable[system: status changed from 1 to 2 by user 8949]body. Newinclude_system_messagesarg (default true) hides them when the caller wants just the actual conversation. -
get_rst_ticketnow includes anautocloseblock so the agent can answer "when will this ticket autoclose?" without having to know the config layout. Showsenabled,automatic,warning_email_interval_days,close_interval_days,warning_sent,warning_eta/close_eta(computed fromlast_reply + interval), andblocked_bywhen conditions short-circuit the flow (e.g.last_reply_customer=1,autoclose_enabled=0,already_closed). -
Shared
withSiteAppContext()trait helper inRSTicketsProBootTrait— extracted from the inline pattern Tim's hand-patch landed on the ISSUE-5 fix. WrapsFactory::$applicationswap (api app ↔ real SiteApplication bootstrapped via the DI container) around any RST call that needs site routing for email-body URL construction (Route::link('site', ...)). Applied acrossAddTicketReply,Update,Close,Reopen,Notifyso every email-firing write tool gets the swap without each one repeating 20 lines of try/finally noise.
Bug Fixes
-
add_rst_ticket_replyfailed end-to-end in API context with "Error loading menu: api" (or "Call to a member function getDepartments() on false"). RSTicketsPro 3.x's reply flow assumes a SiteApplication context — the api app the MCP endpoint runs under has no menu, no router, and no front-end MVC model search path. Three layers of failure: (1)JModelLegacy::getInstance('Submit', 'RsticketsproModel')returns false because front-end model paths aren't registered in api context, (2) even pre-loaded, the Submit model constructor needs site menu, (3)RSTicketsProTicketHelper::saveMessage()callsRoute::link('site', ...)when building the notification email body which also needs site menu. Fix: bypassRsticketsproModelTicket::reply()and callRSTicketsProTicketHelper::saveMessage()directly, wrapped in thewithSiteAppContext()swap. Consent gate +onBeforeStoreTicketReply/onAfterStoreTicketReplyevent triggers preserved from the originalreply()flow. Validated end-to-end on virtuemarttemplates.net ticket TECH-0000000202: MCP reply posted → customer notification email delivered → inbound reply confirmed atsupport@vmtIMAP. Full investigation atresolved-issues/ISSUE-5-add_rst_ticket_reply-site-app-context.md. -
list_rst_tickets/get_rst_ticketwere returningstaff_name=null— the JOIN wenttickets.staff_id → _rsticketspro_staff.id → users.idbuttickets.staff_idis actually a Joomlauser_id, not the_rsticketspro_staffPK (confirmed by readingmodels/fields/staff.phpline 87 which emits$user->idas the dropdown option value). Directtickets.staff_id → users.idJOIN now resolves correctly. -
update_rst_ticket/close_rst_ticket/reopen_rst_ticket/add_rst_ticket_replyreturned stale post-write state —RsticketsproModelTicket::getTicket($id)caches statically per-id and the model's write methods don't invalidate the cache, so the post-write re-read returned the pre-write values. Now reads via a newfetchTicketRow()trait helper that bypasses the cache with direct SQL. -
update_rst_ticketchangesarray was always empty for staff/department changes —RsticketsproModelTicket::updateInfo()calls$original->bind($data)at model line 1189 to assemble the email payload, which mutates the original JTable in place. By the time the diff ran,$original->$kwas the new value, not the original. Fix: snapshot the field values into a plain assoc array before callingupdateInfo(), not a reference to the JTable. -
notify_rst_ticketwas always returningnotified:trueregardless of whether$model->notify()actually sent the email. (RST refuses to send whenlast_reply_customer=1,autoclose_sent=1already, orlast_replytoo recent forautoclose_email_interval.) Now captures the model return value + emits an explanatorynotelisting the common refusal reasons.
Known limitations carried forward
-
File attachments still not supported via
add_rst_ticket_reply— the front-end Submit-model upload path needs the same SiteApplication-swap treatment to work in API context, and we don't have a multipart upload path through the MCP layer anyway. Use the admin reply box for replies that need attachments. -
tickets.staff_idnaming trap — the column stores a Joomlauser_id, not the_rsticketspro_staffPK, despite the name.list_rst_staffoutput disambiguates with bothuser_id(use this for assignments viaupdate_rst_ticket(staff_id=...)) andstaff_id(the_stafftable PK, mostly internal bookkeeping). Tool descriptions surface this so future agent calls don't repeat the original confusion.
v1.7.5 May 13, 2026
Three field-discovery fixes from the Westshore Eye Care SEO audit session on 2026-05-12. All three blocked different parts of getting a single site to a clean, fully-MCP-driven schema/meta state.
Bug Fixes
-
set_4seo_meta_overridewas writing custom values but never flippingdata.useTitle/useDescription/useRobots/useCanonical. 4SEO's renderer checks thoseuse*flags at request time and skips the override if they're0, so every override written through this tool since v1.7.0 was a silent no-op even though the row looked correct (status_title=2,data.custom.title="...", etc.). Both the insert and update branches now flip the matching flag whenever a custom value is supplied. Existing rows can be repaired by re-calling the tool with the same args — it'll preserve the custom value and now also set the flag. Response payload now includesuse_flags_set: ["useTitle", ...]so callers can see exactly which flags landed. Note for 4SEO Free sites: per-page custom meta appears to be a 4SEO Pro feature — clean DB writes with no visible change on the page suggest the site has nodlidconfigured. Tool description now warns of this. -
Dashboard token field's prompt preview now shows the substituted token live, not just on copy. Previously the token field would correctly substitute on click, but the visible
<code>block still showed the<PASTE YOUR JOOMLA API TOKEN HERE>placeholder — confusing because the user couldn't see that the substitution had actually happened. Now the preview re-renders on paste / clear / refresh-with-saved. -
Substitution target is now visually highlighted in the prompt preview — yellow
<mark>on the substituted token (so the user can see "yes, this is what's about to be copied") and a muted grey highlight on the placeholder when no token is set yet (so the user can see "this is the spot that will be replaced"). Dedicated Atum dark-mode styles so the contrast stays readable.
New Features
-
update_menu_itemnow exposes the menu item'sparamsblob — the JSON column on#__menuwhere Joomla stores every per-menu-item SEO setting (Browser Page Title, Meta Description, Meta Keywords, Robots, Page Heading, Show Page Heading, Page Class Suffix, Anchor Title, Force HTTPS). Two paths: named args (browser_page_title,meta_description,meta_keywords,robots,page_heading,show_page_heading,page_class_sfx,menu_anchor_title,secure) map to the right Joomla keys so the agent can't typomenu-meta_descriptionasmeta_description; escape hatchesparams_set: objectandparams_unset: string[]reach any other key. Named args take precedence on collision. Merge-by-default — existing keys not in the call are preserved byte-for-byte.robotsenum-validated against Joomla's five valid values. Closes the home-page-meta-description gap on every Cybersalt client site. -
set_schemaorg_site_profileandget_schemaorg_site_profile— typed wrappers for theplg_system_schemaorgplugin's site-wide Organization/Person profile. Knows the actual four-key shape the plugin reads (baseType,name,image,socialmedia) — not the wrongOrganization_*flat keys that look plausible but go nowhere. Hard-enforcesbase_typelowercase (organization/person); capital-OOrganizationsilently kills the plugin's entire@graphoutput including per-article schemas — that case-sensitivity bug was the regression caught in last week's audit. Honours locked-plugin status the same way Joomla's admin UI does. Tool descriptions explicitly say the plugin does NOT support telephone/address/geo/email and point callers at 4SEO's Business Profile for full LocalBusiness coverage.
Tool count: 78 → 80. SchemaOrg domain count: 8 → 10.
v1.7.4 May 11, 2026
New Features
- Dashboard token field now persists across page refreshes via
localStorage. Previously the "paste your token here for one-click setup" field was ephemeral — paste, tab out, refresh to get the updated copy-paste prompt, and the token was gone. Now the field saves on input/change/blur, restores on load, and a new trash-icon button next to the eye-toggle clears it. A status line under the field tells you whether anything is currently saved ("Saved in this browser. The trash button clears it." / "No token saved. Paste one above — it stays in this browser only."). Stored per-browser/per-origin only — token never leaves the device, never hits the server.
Improvements
- Token field UX confirmed as the Cybersalt house pattern:
<input type="password">rendering as asterisks plus an eye-icon reveal button (no CSS blur). Same trio — masked-by-default + eye reveal + localStorage persistence with trash to clear — will land on any future Cybersalt Joomla extension that asks for a secret.
v1.7.3 May 11, 2026
Bug Fixes
- CRITICAL: Postflight failed on Joomla 6 with
Class "Joomla\CMS\Filesystem\File" not found. That class was deprecated in J4 and removed in J6 — installs on J5 worked (shim still present) but J6 sites (e.g. goatsatwork.ca, notesatwork.ca) hit the not-found error in the postflight try/catch, which surfaced as a yellow warning "cs-mcp-for-j postflight setup failed: …" and silently skipped the autoload-cache clear AND the plugin-enabling step. So on J6, the bundledplg_webservices_csmcpforjplugin stayed disabled after install, the MCP route 404'd, and the install looked broken. Fixed by replacing the oneFile::delete()call with plain@unlink()— no Joomla classes involved, works on every Joomla version. If you installed v1.6.x–v1.7.2 on a Joomla 6 site, install v1.7.3 over top and the plugins will get re-enabled automatically.
v1.7.2 May 11, 2026
Documentation
fetch_rendered_urldescription — added explicit guidance on path semantics (relative to Joomla install root, not server filesystem) and the 4SEO verification tip: fetch the SEF URL when checking whether a custom meta override landed, not theindex.php?option=...form. 4SEO matches custom meta by SEF URL; on the option= form the override won't apply. Discovered during v1.7.1 live testing — burned 15 minutes on this confusion, the agent will too without the hint.set_4seo_meta_overridedescription — added the same SEF-URL verification tip directly into the write tool's description so the agent reads it BEFORE the verify step, not after.
v1.7.1 May 11, 2026
Bug Fixes
set_4seo_configfailed with "Incorrect integer value: 'json' for column format" on first live use. Theformatcolumn in#__forseo_configisTINYINT NOT NULL DEFAULT 1, not a VARCHAR — I'd been writing the string"json"into it. Fixed:formatis now an integer enum (1= raw string,2= JSON). Auto-set to2whenvalue_objectis passed,1whenvalueis passed. Explicitformatoverride accepted as{1, 2}only. Also fixed the SQL:formatis now written as a bare integer, not a quoted string. Bonus fix: the unrelatedlock_expires_atcolumn is nullable so we now insertNULLthere instead of'1970-01-01 00:00:00'.
v1.7.0 May 11, 2026
Refactor of the 4SEO add-on now that we have 4SEO v6.12.0's full source available locally for design reference. Adds 5 typed tools that wrap the highest-traffic 4SEO database tables, so an agent can manage per-page meta and 4SEO config without having to know about the three-layer envelope (platform / auto / custom) or the size-based column routing in #__forseo_config. The generic query_4seo_table / insert_4seo_row / update_4seo_row / delete_4seo_row escape hatches stay — they're how the agent can still reach the tables we haven't typed yet (rules, sitemaps, perf data, GSC, referrers, errors, links, images).
Tool count: 73 → 78. 4SEO domain count: 11 → 16.
New Features
list_4seo_meta_overrides— typed audit query over#__forseo_custom_meta. Returns each row'scontent_id,url, status flags, and the decodedcustom_title/custom_description/custom_robots/custom_canonicalso a query like "which articles have a custom SEO title?" is one call. Filters:content_id_like,has_custom_title,has_custom_description,enabled. Includestotalfor pagination.get_4seo_meta_override— read a single row. Acceptscontent_id(raw),joomla_params(object — tool alphabetises and concatenates), orarticle_id(shorthand for acom_contentarticle). Surfaces all three layers (platform,auto,custom) separately so the agent doesn't have to walk thedataJSON envelope.set_4seo_meta_override— the headline tool. Three-layer-aware upsert: supplytitle/description/robots/canonicaland the tool sets them in thecustomlayer, bumps the correspondingstatus_title/status_descriptionto2(custom), and leavesplatformandautolayers untouched. Creates the row if missing (with emptyplatform/auto— 4SEO repopulates on next crawl), updates in place otherwise. The agent never has to know about the JSON envelope or hash columns.clear_4seo_meta_override— two modes:reset_to_auto(keeps row, wipescustomlayer, resets statuses to0so 4SEO falls back to auto-detection) ordelete_row(hard-removes the row entirely; 4SEO recreates on next crawl if the page is encountered again).set_4seo_config(key, value | value_object, scope?)— typed counterpart toget_4seo_config. Auto-encodesvalue_objectto JSON; routes large payloads to thelarge_valuemediumtext column when the string exceeds 16000 bytes; auto-detectsformat="json"when given avalue_object. Upserts by(scope, key).
Improvements
- All five new tools share a
ContentIdTraitfor parsing/building 4SEO's canonicalcontent_idformat (alphabetisedkey=valuepairs joined with&, case-sensitive ASCII key sort to match 4SEO'sksortbehaviour). Verified live against stage data on 2026-05-11: 1494 rows in#__forseo_custom_metaall match this format, and confirmed atplugins/system/forseo/platform/components/content.phpline 225 in the 4SEO source.
Design notes
- Why typed tools alongside generic CRUD instead of replacing the generic tools: the generic ones are still the safe path for any 4SEO table we haven't yet typed (about 20 of the 25 tables: rules, sitemaps, perf data, GSC daily aggregates, referrers, errors, links, images, etc.). Replacing them would lock the agent out of those tables. They're now placed last in the registration so the agent reaches for typed tools first.
- 4SEO does NOT use standard Joomla
Modelclasses — there are none anywhere in the 4SEO PHP source. The "Option B" path from the original discovery (call into Weeblr's models viabootComponent('com_forseo')->getMVCFactory()) is unavailable. The path we took is Option A (DB-direct) but with typed wrappers built from the canonicalinstall.sqlschema, so the agent gets the ergonomics of Option B without needing the models.
v1.6.0 May 9, 2026
New Features
validate_jsonld(jsonld, expected_type?)— pre-flight JSON-LD shape validator. Returnserrors(must-fix),warnings(should-fix), andinfo(cosmetic) messages. Designed to be called beforeset_article_custom_jsonld_bulkso a typo doesn't produce 500 silently-broken schema rows. Knows the required + strongly-recommended fields for: Article, BlogPosting, NewsArticle, FAQPage, Question, HowTo, Recipe, Event, Product, Offer, Review, JobPosting, LocalBusiness, Organization, Person, BreadcrumbList, VideoObject, Service, Book. Unknown @types pass without field-level checks (you can still get errors for missing @type, missing @context, or @graph wrapping). Tool count: 72 → 73.
v1.5.1 May 9, 2026
Patch release responding to v1.5.0 test feedback. Fixes the JSON-RPC corruption class of bug structurally and unblocks editing legitimately user-editable locked plugins via set_plugin_params.
Bug Fixes
- Critical: stray PHP output corrupted JSON-RPC envelopes. Any tool that triggered a PHP notice/warning (e.g. v1.5.0's
fetch_rendered_urlcasting an array Content-Type header to string) emitted that warning text in front of the JSON-RPC response, which caused MCP clients to throw "Parse error: Unexpected token" on the otherwise-valid response. WrappedMcpController::handle()inob_start()and discard the buffer before emitting JSON. Defends every tool — present and future — from this whole class of bug. Local cause inFetchRenderedUrlToolis also fixed (multi-value headers now joined with,instead of cast to "Array"). set_plugin_paramscouldn't modify legitimately user-editable locked plugins likeplg_system_schemaorg. Joomla's own admin UI lets you edit these even though they're flaggedlocked: true. Addedallow_locked: trueflag to override the lock guard explicitly;protected: trueplugins still get a hard refusal because those are genuinely dangerous to touch.
Improvements
fetch_rendered_urlreturnsjsonld_types— a flat, dedup'd, sorted array of every@typevalue across all JSON-LD blocks (recursively walking@graph). Common SEO check "did my X type land?" becomesresult.jsonld_types.includes("FAQPage")instead of walking every block.set_article_custom_jsonldandset_article_custom_jsonld_bulkdescriptions now warn against wrapping the supplied JSON-LD in a top-level@graph— Joomla 5.1+ merges each block into the page's existing@graphautomatically, so wrapping yourself produces a graph-in-graph that's likely wrong.
v1.5.0 May 9, 2026
Feature release responding to the v1.4.x test feedback. Adds five new tools, total counts on paginated responses, and a token-substitute UI on the dashboard so non-technical users can copy a fully-ready prompt without manual editing.
Tool count: 67 → 72 (and three pre-existing paginated tools learned a total field).
New Features
get_plugin_params(folder, element)/set_plugin_params(folder, element, params, mode?)— generic plugin params read/write via direct DB. Unlocks site-wide schemaorg config (folder=system, element=schemaorg), router options, third-party plugin config, anything in the Options screen of any plugin.set_plugin_paramsdefaults to merge mode (preserves keys you don't supply); refuses protected/locked core plugins.fetch_rendered_url(path, extract_jsonld?)— fetches a rendered page from the SAME Joomla site (same-origin only — no SSRF) so the agent can verify its writes worked. Optionalextract_jsonld=trueparses every<script type="application/ld+json">block in<head>and returns them as structured data — closes the verification loop for any Schema.org workflow.set_article_custom_jsonld_bulk(updates[])— bulk variant ofset_article_custom_jsonldfor sites with hundreds of articles. Per-item independent (one failure doesn't roll back the others), capped at 500 updates per call. Response gives per-itemok/error.get_4seo_config(4SEO add-on) — reads the actual 4SEO settings from#__forseo_config(where the real config lives — site-wide schema templates, default tags, scan rules) instead of the near-empty Joomla extensions row thatget_4seo_component_paramsreturns. Schema-agnostic: returns every column verbatim plus a__parsedfield for any column whose value is JSON.totalfield on paginated responses —list_articles,list_articles_with_schema,list_users, andquery_4seo_tablenow return a top-leveltotal(the count across the whole filtered set, not just the current page) so the agent knows when pagination is complete without poll-till-empty.
Improvements
- Token-substitute UI on the dashboard's prompt tab. New "Optional: paste your token here for one-click setup" input field — when the user pastes a token, the Copy button substitutes the
<PASTE YOUR JOOMLA API TOKEN HERE>placeholder before copying. Token never leaves the browser; the dashboard never sends it back to the server. The button label flips to "Copied with token included!" so the user knows substitution happened.
v1.4.2 May 9, 2026
Patch release responding to a real-world test of the v1.4.x onboarding prompt — first-call friction fixes, no new tools.
Bug Fixes
list_articles_with_schemasummary was misleading. It counted only the rows on the current page, so calling withlimit:1returnedwith_schema:0, without_schema:1regardless of how many articles actually had/lacked schema across the full filtered set. Fixed: summary now runsCOUNT(*)queries across the whole filtered set, independent of pagination. Response also adds a top-leveltotalso the agent knows the full filtered count.- Onboarding prompt: curl example mangled JSON arguments containing nested quotes. First-time callers hit
Parse error: Syntax errorthe moment a tool argument was a JSON object. Switched the example from inline-d '{"…"}'to--data-binary @-with a single-quoted heredoc — survives any payload. - Onboarding prompt: didn't explain that
tools/listandtools/callreturn different response shapes.tools/listreturnsresult.toolsdirectly (no content wrap);tools/callwraps inresult.content[0].text. The prompt only described thetools/callshape, so first-time callers hit a parse mismatch on the very first response. Documented both. - Onboarding prompt: claude.ai branch was wrong. Said to "grab the JSON snippet from the dashboard" — claude.ai connectors actually use separate URL + auth-header fields, not a JSON snippet. Updated the prompt to give claude.ai users the URL and auth header directly, with the JSON snippet path reserved for Claude Desktop.
- Onboarding prompt: prompt-injection guard added to
claude mcp addself-install offer. Tells Claude to verify the URL in the install dialog matches the user's own site domain before approving — defense against a maliciously edited copy of the prompt that redirects the install to a hostile endpoint.
v1.4.1 May 9, 2026
Bug Fixes
- "Generate / view API token" link landed on the user list, not the admin's own profile. The link was
task=user.editwith no id, so Joomla's controller fell through to the user list instead of opening the current admin's profile (where the API Token tab lives). Fixed by computing the current admin's user id and substituting it intotask=user.edit&id={N}. Both link instances on the dashboard also now open in a new tab so the user keeps the dashboard open while copying the token. Same pattern as cs-template-integrity.
v1.4.0 May 9, 2026
Bug Fixes
- Tabs not switching on dashboard. The Bootstrap
bootstrap.tabJavaScript module isn't auto-loaded in Joomla admin views; without an explicitWebAssetManager::useScript('bootstrap.tab')opt-in, clicking a tab only changed the URL hash and the panel never activated. Fixed.
Improvements
- Tab order swapped. The copy-paste prompt is now the default tab (renamed "Copy a prompt into Claude (recommended)") because it's the easier path for non-technical users. The JSON snippet path moves to the second tab ("MCP Connector (manual config)").
- Self-installing prompt. The copy-paste prompt now includes a closing instruction that tells Claude: after the user confirms the connection works, offer to install this site as a permanent MCP connector via
claude mcp add(Claude Code only). The user just says "make it permanent" and Claude runs the install command (with the standard approval dialog before any shell command runs). After a Claude restart, the site appears as native MCP tools in every conversation — no prompt needed ever again. One paste = either a one-off use OR a permanent install, user's choice via a single follow-up sentence. - Setup card framing rewritten to make the recommended path obvious and explain the bonus self-install behavior up front.
v1.3.0 May 9, 2026
New Features
- Two-method dashboard for connecting Claude. The dashboard now offers two clear setup paths in a tabbed UI:
- Method 1 — MCP Connector. The traditional MCP setup: copy a JSON snippet (pre-filled with the site's URL), paste into your Claude client's config (Claude Desktop / claude.ai connectors /
claude mcp add), restart. One-time, persistent. - Method 2 — Copy-paste prompt. A new "no client config required" path. The dashboard generates a complete instruction prompt (site URL, endpoint, auth, JSON-RPC shape, tool surface summary, workflow rules, example curl). Paste into a fresh Claude Code conversation and Claude talks to the MCP endpoint directly via curl. Best for one-off tasks or when you can't change client config.
- Method 1 — MCP Connector. The traditional MCP setup: copy a JSON snippet (pre-filled with the site's URL), paste into your Claude client's config (Claude Desktop / claude.ai connectors /
- Copy buttons with "Copied!" feedback on both setup payloads (clipboard API with text-selection fallback).
Improvements
- Dashboard tools list and setup payloads are both auto-generated from the live tool registry — the prompt always reflects what's actually installed.
v1.2.0 May 9, 2026
New Features
- 4SEO add-on (
plg_system_csmcpforj4seo) — first paid-add-on-shaped sub-plugin (bundled with the package for testing; structured so it can be split into its own SKU later). Adds 10 tools that introspect and modify the Weeblr 4SEO extension (com_forseo) directly against#__forseo_*since 4SEO ships no public Web Services API:list_4seo_tables— every #__forseo_* table on the sitedescribe_4seo_table— column schema for one tablecount_4seo_rows— row counts across all 4SEO tables (health snapshot)get_4seo_component_info— is com_forseo installed/enabled, which Weeblr siblings are alongsideget_4seo_component_params— read com_forseo Options screen settingsset_4seo_component_params— merge-update com_forseo Options screen settingsquery_4seo_table— safe parameterised SELECT (structured WHERE clauses, no raw SQL, restricted toforseo_*)insert_4seo_row— safe single-row INSERTupdate_4seo_row— safe single-row UPDATE (refuses if WHERE matches >1 row)delete_4seo_row— safe single-row DELETE (refuses if WHERE matches >1 row)
- All 4SEO write tools refuse any table not starting with
forseo_, so misuse can't reach #__users / #__content / etc.
Bug Fixes
- Memory exhaustion in dashboard load (CRITICAL).
RegisterToolsEventextendedJoomla\CMS\Event\AbstractEvent, whose argument processor walks argument values and recursed catastrophically when theregistryargument carried 50+ tool instances each holding aDatabaseInterfacereference. Fixed by switching to the simplerJoomla\Event\Eventbase class and refactoring the dashboard to read tool metadata statically from the bundled plugin classes (no event dispatch from the dashboard at all).
Improvements
- Tool count: 57 → 67. Domain count: 12 → 13.
- Server protocol identifier bumped to
cs-mcp-for-j 1.2.0.
v1.1.0 May 9, 2026
New Features
- Schema.org / SEO tool domain (6 tools) wrapping Joomla's CORE
plg_system_schemaorgsystem:list_schema_types— canonical type list with typical fields per typelist_articles_with_schema— audit which articles have/lack structured data, with summary by typeget_article_schema— read the stored schemaorg row for a content itemset_article_schema— set/replace any of Article, BlogPosting, Book, Event, JobPosting, Organization, Person, Recipe, Customset_article_custom_jsonld— convenience for Custom type: pass a JSON-LD object directly, no need to stringifyclear_article_schema— remove the row (matches Joomla'sschemaType=Nonebehaviour)
- Writes go directly to
#__schemaorgin the same shape Joomla'sSchemaorg::onContentAfterSavehook produces. The rendered<script type="application/ld+json">blocks pick up changes on the next page load with no cache invalidation needed.
Improvements
- Tool count: 51 → 57. Domain count: 11 → 12.
- Server protocol identifier bumped to
cs-mcp-for-j 1.1.0.
v1.0.0 April 25, 2026
Initial release.
New Features
- MCP endpoint: Streamable-HTTP MCP server at
/api/index.php/v1/mcp, authenticated by Joomla API token (X-Joomla-TokenorAuthorization: Bearer). - JSON-RPC 2.0 server:
initialize,notifications/initialized,ping,tools/list,tools/call. Supports single messages and batches. - ACL gating:
csmcpforj.use(read-only tools) andcsmcpforj.write(mutating tools). Super Users / Administrators / Managers always pass; other groups need explicit grant. - Tool registry: Plugins extend the surface by subscribing to the
onCsMcpRegisterToolsevent and registering classes extendingAbstractTool. - Bearer translation: System plugin rewrites
Authorization: Bearer <token>toX-Joomla-Tokenfor the MCP route only. - Admin dashboard: Endpoint URL, copy-paste client config, permissions table, and grouped tool list (51 tools across 11 domains).
- 51 built-in tools across:
- Articles (6) — list, get, create, update, delete, list_categories
- Categories (5) — list_categories_in, get, create, update, delete (works for any extension)
- Tags (5) — list, get, create, update, delete
- Menus (6) — list_menus, list/get/create/update/delete menu_item
- Users & Access (7) — list, get, create, update, delete; list_user_groups, list_access_levels
- Modules (6) — list, list_module_positions, get, create, update, delete
- Extensions (3) — list_extensions, list_plugins, set_extension_enabled
- Templates (2) — list_template_styles, set_default_template_style
- Languages (2) — list_languages, list_content_languages
- Custom Fields (4) — list, get, create, set_custom_field_value
- System (5) — get_joomla_version, get_site_info, list_scheduled_tasks, check_for_updates, clear_cache
Security
- Per-tool permission gate runs before every
tools/callexecution. - Component ships with
access.xmlso per-action permissions are visible in System → Permissions out of the box. - All article/category/tag/menu/user/module/field writes go through the corresponding com_* Administrator model so workflow, asset, and ACL side effects stay consistent.
- All database access uses
quoteName()and parameterised values — no string concatenation into SQL. delete_userrefuses to delete the calling user.set_extension_enabledrefuses to touch protected/locked core extensions.get_site_infodeliberately omits secrets (DB password, mailer credentials, captcha keys).