From c7560877e998db40ac7f802c3330e3f944396f73 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Thu, 17 Sep 2026 11:58:06 +0200
Subject: [PATCH 01/17] docs: define thin Joomla integration and shared
component contract
---
docs/ARCHITECTURE.md | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 docs/ARCHITECTURE.md
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..9882b4f
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,20 @@
+# MCP plugin architecture
+
+This repository contains the Joomla integration plugin for `joomengine/mcp_component`. Joomla's current plugin, event, routing, authentication and installer contracts are authoritative. Follow JCB-generated plugin layout: root extension manifest, `services/provider.php`, `src/Extension`, language files, installer and update/changelog metadata. Do not nest the installable project inside a second plugin directory.
+
+The plugin is a thin adapter: register supported endpoints and connect Joomla lifecycle events to the component. Application logic, policy evaluation, consent, protocol processing and audit belong to the component. No duplicated runtime or alternate security decisions belong here. Missing or incompatible component dependencies must fail closed without breaking unrelated Joomla requests.
+
+The existing reference is `joomengine/joomla-mcp` and its Joomla-native action/security contracts. This is JCB-aligned hand-authored source, not a claim that a JCB blueprint already exists. Both new repositories started with README and licence files only.
+
+## Delivery requirements
+
+- Explicit extension element/group and namespaced dependency-injected plugin.
+- No public execution routes or authentication bypass.
+- No automatic trust in arbitrary client confirmation flags.
+- Installer/version checks that preserve operator choices during upgrades.
+- Matching component/plugin/package versions, update and changelog XML.
+- Component-owned `.octojpack` distribution configuration referencing this repository.
+- Contract, installation and request-level verification, reported honestly.
+
+Component implementation: https://github.com/joomengine/mcp_component/pull/1
+JCB reference: https://github.com/joomengine/Joomla-Component-Builder/tree/6.x
From ebd1e159a70c43294be4096f477fbe6cacdf63ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Thu, 17 Sep 2026 12:02:32 +0200
Subject: [PATCH 02/17] docs: define exact console identity, full companion
parity and trusted CLI boundary
---
AGENTS.md | 15 +++++++++++++++
CHANGELOG.md | 8 ++++++++
README.md | 19 ++++++++++++++++++-
docs/ARCHITECTURE.md | 41 +++++++++++++++++++++++++++--------------
docs/IMPLEMENTATION.md | 5 +++++
5 files changed, 73 insertions(+), 15 deletions(-)
create mode 100644 AGENTS.md
create mode 100644 CHANGELOG.md
create mode 100644 docs/IMPLEMENTATION.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e2d6191
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,15 @@
+# Agent contract — console plugin
+
+Complete the migration of the original PHP companion and CLI track from `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36` without dropping commands, schemas, supported native operations, dry-run/plan, approval, verification or recovery behaviour. Share the database-driven component runtime rather than duplicating its catalogue, protocol, policy or audit implementation.
+
+Use the existing `feature/jcb-mcp-runtime` branch and PR #1. Push cohesive commits, update `docs/IMPLEMENTATION.md` with exact outcomes/remaining work, and keep the PR draft until runtime implementation and required tests are complete. Do not create replacement branches, force-push, merge, publish releases or edit the original TypeScript repository.
+
+Use element `joomengine_mcp`, group `console`, extension `plg_console_joomengine_mcp`, namespace `VDM\Plugin\Console\JoomEngineMcp`. The dependency is `com_joomengine_mcp`, namespace `VDM\Component\JoomEngineMcp`. HTTP webservices glue belongs to the component distribution, not this console plugin.
+
+Joomla 6 native plugin/event/DI/console contracts are authoritative. Follow the JCB generated plugin-root layout: manifest, installer, `services/provider.php`, `src/Extension`, `src/Console`, language files, changelog and update metadata. The PHP style authority is https://github.com/extension-builder/joomla/blob/main/docs/development/php-code-style.md: tabs, LF, Allman braces, explicit typed properties/constructor injection, meaningful docblocks, no closing PHP tags or isolated strict_types/property promotion. Preserve Joomla inherited signatures. This is hand-authored JCB-aligned code, not an already imported JCB blueprint.
+
+Only the real Joomla console application under CLI may create the privileged local execution context. JSON input, headers, tokens and database rows may not manufacture it. Local execution does not require a Joomla token/row view-level check, but still validates inputs and handler bindings and retains audit/verification/recovery. Never expose this path via an HTTP controller or spawn arbitrary row-supplied shell commands.
+
+Stdio stdout is exclusively newline-delimited JSON-RPC. Send diagnostics to stderr and preserve nonzero failures. Bound input size/time and handle malformed messages/EOF without corrupting the next message. Missing/incompatible component dependencies should produce a clear command failure without breaking unrelated Joomla console commands.
+
+Tests must cover service registration, installation/dependency checks, original companion contract parity, stdio protocol, malformed input, real Joomla reads/writes/read-back/cleanup, and local-vs-HTTP authority separation. Do not report mocked or syntax tests as live passes. Package versions align with the component; update feeds must not advertise unpublished artifacts. Retain source licences and notices.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4665892
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## Unreleased
+
+- Document the exact `joomengine_mcp` console-plugin identity, trusted local boundary and shared component-runtime contract.
+- Define complete original companion migration, direct PHP stdio, installation and verification objectives.
+
+No production release is advertised.
diff --git a/README.md b/README.md
index 101eb1e..2e9cbeb 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,18 @@
-# mcp_plugin
\ No newline at end of file
+# JoomEngine MCP console plugin
+
+PHP-only Joomla console integration for [`com_joomengine_mcp`](https://github.com/joomengine/mcp_component).
+
+**Element:** `joomengine_mcp`
+**Group:** `console`
+**Extension:** `plg_console_joomengine_mcp`
+**Namespace:** `VDM\Plugin\Console\JoomEngineMcp`
+
+This repository migrates the existing `joomengine/joomla-mcp` PHP companion into a separately installable plugin. It connects Joomla's console application to the component's shared database-driven MCP engine and provides direct PHP stdio serving. It is not the HTTP webservices routing plugin; that small adapter is distributed with the component.
+
+## Authority and status
+
+Local console execution is the trusted-server track requested by the project owner. It does not need a Joomla API token or row viewing-level authorization. Input validation, explicit action semantics, bounded execution, audit, verification and recovery remain mandatory. No HTTP request or database row can opt into this local privilege.
+
+Work remains on `feature/jcb-mcp-runtime`, draft PR #1. Architecture precedes runtime. See [implementation status](docs/IMPLEMENTATION.md), [architecture](docs/ARCHITECTURE.md) and [agent instructions](AGENTS.md). No production certification is implied by an install manifest, a catalogue entry or a syntax-only test.
+
+Original migration source: `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36`, especially `companion/plugin`. Preserve all original licences, supported commands, request/result contracts and native operation behaviour. The original repository is not modified.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 9882b4f..edd689c 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,20 +1,33 @@
-# MCP plugin architecture
+# Console plugin architecture
-This repository contains the Joomla integration plugin for `joomengine/mcp_component`. Joomla's current plugin, event, routing, authentication and installer contracts are authoritative. Follow JCB-generated plugin layout: root extension manifest, `services/provider.php`, `src/Extension`, language files, installer and update/changelog metadata. Do not nest the installable project inside a second plugin directory.
+## Identity and responsibility
-The plugin is a thin adapter: register supported endpoints and connect Joomla lifecycle events to the component. Application logic, policy evaluation, consent, protocol processing and audit belong to the component. No duplicated runtime or alternate security decisions belong here. Missing or incompatible component dependencies must fail closed without breaking unrelated Joomla requests.
+`plg_console_joomengine_mcp`, element `joomengine_mcp`, group `console`, namespace `VDM\Plugin\Console\JoomEngineMcp`. Joomla-native plugin-root source uses `joomengine_mcp.xml`, installer, `services/provider.php`, `src/Extension`, `src/Console` and language/update/changelog files. Do not wrap it inside another plugin directory.
-The existing reference is `joomengine/joomla-mcp` and its Joomla-native action/security contracts. This is JCB-aligned hand-authored source, not a claim that a JCB blueprint already exists. Both new repositories started with README and licence files only.
+The plugin is the local CLI adapter to `com_joomengine_mcp`. It is not the component's HTTP webservices adapter. The component owns database catalogue/schema/binding resolution, protocol processing, reusable native and API handlers, durable plans/grants/idempotency/locks, verification and audit. No duplicated action catalogue or alternative policy engine belongs here.
-## Delivery requirements
+## Execution boundary
-- Explicit extension element/group and namespaced dependency-injected plugin.
-- No public execution routes or authentication bypass.
-- No automatic trust in arbitrary client confirmation flags.
-- Installer/version checks that preserve operator choices during upgrades.
-- Matching component/plugin/package versions, update and changelog XML.
-- Component-owned `.octojpack` distribution configuration referencing this repository.
-- Contract, installation and request-level verification, reported honestly.
+Joomla console application boots enabled console plugins through its native event lifecycle. The plugin registers named commands using Joomla Console/Symfony Console contracts. Command execution verifies that the application is Joomla's console application and PHP is running in CLI before creating trusted-local context. Owning the server is the user's requested authority boundary; no API token or Joomla row view-level checks are required for this track. HTTP cannot select it.
-Component implementation: https://github.com/joomengine/mcp_component/pull/1
-JCB reference: https://github.com/joomengine/Joomla-Component-Builder/tree/6.x
+The privileged track still validates all schemas and declarative handler bindings, uses registered native actions/stock command mappings, bounds input and execution, retains explicit destructive-action semantics and records local provenance. It does not run arbitrary PHP, SQL, class names or shell supplied by database rows. A remote PHP stdio-to-HTTP bridge remains an HTTP-token-restricted client and is not trusted CLI.
+
+## Compatibility
+
+Inventory and migrate every original companion entry point and action contract from `companion/plugin` at `2cff50f4f6b440da3c684f9995a77efad32e1a36`. Preserve the existing `joomla:mcp:describe`, `joomla:mcp:dispatch`, `joomla:mcp:self-test` and `joomla:mcp:cli-inventory` interfaces where confirmed in the pinned source; add a direct MCP stdio command without requiring the TypeScript process. Keep aliases explicit and tested. Preserve native-model events, filters, dry-run/preflight, partial-apply diagnostics and structured result/error shapes.
+
+Stdio emits only JSON-RPC frames to stdout; banners/notices/logs go to stderr or are captured as diagnostics. Requests are bounded newline-delimited UTF-8 JSON. Errors cannot silently become successful empty results. The shared engine reads installed database definitions, so extensions installed by rows become available to CLI without editing this plugin.
+
+## Installation and distribution
+
+Require a compatible Joomla 6/PHP baseline and installed compatible component. Installation/update preserves enabled state and existing settings. Detect missing dependencies on invocation without crashing unrelated Joomla commands. Provide standalone plugin archive and join the component's `.octojpack` package assembly. Component/plugin versions are pinned together for release. Ship changelog/update metadata but no feed entry for an unpublished release. Preserve original licence notices during migration.
+
+## Acceptance
+
+Test namespace/autoload/manifest and DI/event contracts, old companion request/result compatibility, actual console command execution and protocol framing. On disposable Joomla, exercise native reads/writes with persisted read-back and cleanup, CLI inventory, error/partial-apply behaviour and inability for an HTTP request to manufacture local context. Record real commands/results and missing evidence in `docs/IMPLEMENTATION.md`; syntax/unit tests alone are not live certification.
+
+## References
+
+- Component plan: https://github.com/joomengine/mcp_component/pull/1
+- Original companion: https://github.com/joomengine/joomla-mcp/tree/2cff50f4f6b440da3c684f9995a77efad32e1a36/companion/plugin
+- JCB source/style: https://github.com/extension-builder/joomla/blob/main/docs/development/php-code-style.md
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
new file mode 100644
index 0000000..6686eb8
--- /dev/null
+++ b/docs/IMPLEMENTATION.md
@@ -0,0 +1,5 @@
+# Implementation status
+
+Documentation-first draft PR #1 and branch `feature/jcb-mcp-runtime` already exist and are retained. Exact console identity and responsibility are now documented; previous generic HTTP-plugin wording is superseded.
+
+Runtime is not yet verified at this documentation commit. Next: inspect pinned companion source, implement the Joomla console service provider/extension/commands against the shared component contracts, migrate confirmed legacy interfaces, then execute unit, package and real Joomla integration tests. Continue until all source parity and component acceptance requirements are met. Record actual outcomes here rather than marking declaration-only work complete.
From 68df9cdbed71a4b64f9de74a50035aae82df0d52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Thu, 17 Sep 2026 17:09:23 +0200
Subject: [PATCH 03/17] feat: add Joomla console adapters, dependency-aware
installation and reproducible plugin packaging
---
.editorconfig | 12 ++
.github/workflows/ci.yml | 42 +++++
.gitignore | 4 +
build.php | 75 +++++++++
docs/IMPLEMENTATION.md | 17 +-
joomengine_mcp.xml | 30 ++++
joomengine_mcp_changelog.xml | 4 +
joomengine_mcp_update_server.xml | 4 +
language/en-GB/plg_console_joomengine_mcp.ini | 2 +
.../en-GB/plg_console_joomengine_mcp.sys.ini | 2 +
script.php | 90 +++++++++++
services/provider.php | 44 +++++
src/Console/McpCommand.php | 152 ++++++++++++++++++
src/Extension/JoomEngineMcpPlugin.php | 85 ++++++++++
tests/run.php | 67 ++++++++
15 files changed, 628 insertions(+), 2 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .github/workflows/ci.yml
create mode 100644 .gitignore
create mode 100644 build.php
create mode 100644 joomengine_mcp.xml
create mode 100644 joomengine_mcp_changelog.xml
create mode 100644 joomengine_mcp_update_server.xml
create mode 100644 language/en-GB/plg_console_joomengine_mcp.ini
create mode 100644 language/en-GB/plg_console_joomengine_mcp.sys.ini
create mode 100644 script.php
create mode 100644 services/provider.php
create mode 100644 src/Console/McpCommand.php
create mode 100644 src/Extension/JoomEngineMcpPlugin.php
create mode 100644 tests/run.php
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5dfcc62
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+
+[*.{yml,yaml,json,xml,md}]
+indent_style = space
+indent_size = 2
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c48a0da
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+name: PHP console plugin
+
+on:
+ pull_request:
+ push:
+ branches: [main, feature/jcb-mcp-runtime]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: plugin-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ package:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['8.3', '8.4']
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, simplexml, zip
+ coverage: none
+ - name: PHP syntax
+ run: find . -type f -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l
+ - name: Manifest and reproducible package contracts
+ run: php tests/run.php
+ - uses: actions/upload-artifact@v7
+ if: matrix.php == '8.3'
+ with:
+ name: console-plugin-development-package
+ path: build/
+ if-no-files-found: error
+ retention-days: 7
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6b3e47d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/build/
+/vendor/
+/.reference/
+/.phpunit.cache/
diff --git a/build.php b/build.php
new file mode 100644
index 0000000..2ae9bd1
--- /dev/null
+++ b/build.php
@@ -0,0 +1,75 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+if (PHP_SAPI !== 'cli' || !class_exists(ZipArchive::class))
+{
+ fwrite(STDERR, "Build requires PHP CLI with the zip extension.\n");
+ exit(1);
+}
+
+$root = __DIR__;
+$manifest = simplexml_load_file($root . '/joomengine_mcp.xml');
+
+if ($manifest === false || preg_match('/\A\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?\z/D', (string) $manifest->version) !== 1)
+{
+ throw new RuntimeException('Invalid plugin manifest version.');
+}
+
+$files = ['joomengine_mcp.xml', 'script.php', 'LICENSE'];
+
+foreach (['src', 'services', 'language'] as $directory)
+{
+ foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/' . $directory, FilesystemIterator::SKIP_DOTS)) as $file)
+ {
+ if ($file->isLink())
+ {
+ throw new RuntimeException('Plugin archives may not contain symbolic links.');
+ }
+
+ if ($file->isFile())
+ {
+ $files[] = substr($file->getPathname(), strlen($root) + 1);
+ }
+ }
+}
+
+sort($files, SORT_STRING);
+$output = $root . '/build';
+
+if (!is_dir($output) && !mkdir($output, 0775, true))
+{
+ throw new RuntimeException('Cannot create the build directory.');
+}
+
+$path = $output . '/plg_console_joomengine_mcp-' . (string) $manifest->version . '.zip';
+$zip = new ZipArchive();
+
+if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true)
+{
+ throw new RuntimeException('Cannot create the plugin archive.');
+}
+
+$epoch = getenv('SOURCE_DATE_EPOCH');
+$mtime = $epoch !== false && ctype_digit($epoch) ? max(315532800, (int) $epoch) : 1789603200;
+
+foreach ($files as $file)
+{
+ if (!$zip->addFile($root . '/' . $file, $file) || !$zip->setMtimeName($file, $mtime))
+ {
+ throw new RuntimeException('Cannot add a file to the plugin archive.');
+ }
+}
+
+if (!$zip->close())
+{
+ throw new RuntimeException('Cannot finalize the plugin archive.');
+}
+
+file_put_contents($path . '.sha256', hash_file('sha256', $path) . ' ' . basename($path) . "\n");
+echo $path . PHP_EOL;
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index 6686eb8..0d807c2 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -1,5 +1,18 @@
# Implementation status
-Documentation-first draft PR #1 and branch `feature/jcb-mcp-runtime` already exist and are retained. Exact console identity and responsibility are now documented; previous generic HTTP-plugin wording is superseded.
+## Branch and review
-Runtime is not yet verified at this documentation commit. Next: inspect pinned companion source, implement the Joomla console service provider/extension/commands against the shared component contracts, migrate confirmed legacy interfaces, then execute unit, package and real Joomla integration tests. Continue until all source parity and component acceptance requirements are met. Record actual outcomes here rather than marking declaration-only work complete.
+The existing `feature/jcb-mcp-runtime` branch is retained. PR #1 has now actually been created; earlier documentation incorrectly assumed that it already existed.
+
+## Runtime implemented
+
+- Exact plugin element/group/namespace, Joomla console lifecycle and dependency-injected provider.
+- Lazy command adapters for `joomla:mcp:serve`, `describe`, `dispatch`, `self-test` and `cli-inventory`.
+- Local-only entry checks; missing component failures do not eagerly break unrelated Joomla commands.
+- Original companion protocol framing delegated to the component-owned implementation, with PHP MCP stdio as a separate mode.
+- Installer dependency checks, first-install enabling with upgrade state preservation, language/update/changelog metadata.
+- Reproducible PHP-only ZIP builder and manifest/package contracts on PHP 8.3 and 8.4.
+
+## Verification
+
+CI results must be inspected for the current commit. The end-to-end Joomla runtime is tested with the coordinated component implementation; package tests alone are not proof of runtime parity or production readiness. The component's `ConsoleRuntimeInterface` and `ConsoleRuntimeProviderInterface` are required before these commands can execute.
diff --git a/joomengine_mcp.xml b/joomengine_mcp.xml
new file mode 100644
index 0000000..4a94752
--- /dev/null
+++ b/joomengine_mcp.xml
@@ -0,0 +1,30 @@
+
+
+ plg_console_joomengine_mcp
+ Llewellyn van der Merwe
+ September 2026
+ Copyright (C) 2026 Vast Development Method. All rights reserved.
+ GNU General Public License version 3 or later; see LICENSE
+ https://dev.vdm.io/
+ 0.1.0
+ PLG_CONSOLE_JOOMENGINE_MCP_XML_DESCRIPTION
+ VDM\Plugin\Console\JoomEngineMcp
+ script.php
+
+ services
+ src
+ language
+ script.php
+ LICENSE
+
+
+ en-GB/plg_console_joomengine_mcp.ini
+ en-GB/plg_console_joomengine_mcp.sys.ini
+
+
+ https://raw.githubusercontent.com/joomengine/mcp_plugin/main/joomengine_mcp_update_server.xml
+
+ https://raw.githubusercontent.com/joomengine/mcp_plugin/main/joomengine_mcp_changelog.xml
+
+ 8.3.0
+
diff --git a/joomengine_mcp_changelog.xml b/joomengine_mcp_changelog.xml
new file mode 100644
index 0000000..15ee61a
--- /dev/null
+++ b/joomengine_mcp_changelog.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/joomengine_mcp_update_server.xml b/joomengine_mcp_update_server.xml
new file mode 100644
index 0000000..4ed8d98
--- /dev/null
+++ b/joomengine_mcp_update_server.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/language/en-GB/plg_console_joomengine_mcp.ini b/language/en-GB/plg_console_joomengine_mcp.ini
new file mode 100644
index 0000000..fe29834
--- /dev/null
+++ b/language/en-GB/plg_console_joomengine_mcp.ini
@@ -0,0 +1,2 @@
+PLG_CONSOLE_JOOMENGINE_MCP="Console - JoomEngine MCP"
+PLG_CONSOLE_JOOMENGINE_MCP_XML_DESCRIPTION="Provides local Joomla MCP stdio and companion commands through the shared JoomEngine MCP component. Local console execution is trusted server access and is never available to HTTP requests."
diff --git a/language/en-GB/plg_console_joomengine_mcp.sys.ini b/language/en-GB/plg_console_joomengine_mcp.sys.ini
new file mode 100644
index 0000000..fe29834
--- /dev/null
+++ b/language/en-GB/plg_console_joomengine_mcp.sys.ini
@@ -0,0 +1,2 @@
+PLG_CONSOLE_JOOMENGINE_MCP="Console - JoomEngine MCP"
+PLG_CONSOLE_JOOMENGINE_MCP_XML_DESCRIPTION="Provides local Joomla MCP stdio and companion commands through the shared JoomEngine MCP component. Local console execution is trusted server access and is never available to HTTP requests."
diff --git a/script.php b/script.php
new file mode 100644
index 0000000..7a61776
--- /dev/null
+++ b/script.php
@@ -0,0 +1,90 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Installer\InstallerAdapter;
+use Joomla\CMS\Version;
+use Joomla\Database\DatabaseInterface;
+
+
+defined('_JEXEC') or die;
+
+/**
+ * Validate dependencies and preserve operator settings on upgrades.
+ *
+ * @since 0.1.0
+ */
+class PlgConsoleJoomengine_mcpInstallerScript
+{
+ /**
+ * Reject unsupported PHP/Joomla or a missing component dependency.
+ *
+ * @param string $type Installer operation.
+ * @param InstallerAdapter $parent Joomla installer adapter.
+ * @return bool
+ * @since 0.1.0
+ */
+ public function preflight(string $type, InstallerAdapter $parent): bool
+ {
+ if ($type === 'uninstall')
+ {
+ return true;
+ }
+
+ if (version_compare(PHP_VERSION, '8.3.0', '<') || version_compare((new Version())->getShortVersion(), '6.1.0', '<'))
+ {
+ Factory::getApplication()->enqueueMessage('JoomEngine MCP requires Joomla 6.1 or later and PHP 8.3 or later.', 'error');
+
+ return false;
+ }
+
+ $database = Factory::getContainer()->get(DatabaseInterface::class);
+ $query = $database->getQuery(true)
+ ->select($database->quoteName(['enabled', 'manifest_cache']))
+ ->from($database->quoteName('#__extensions'))
+ ->where($database->quoteName('type') . ' = ' . $database->quote('component'))
+ ->where($database->quoteName('element') . ' = ' . $database->quote('com_joomengine_mcp'));
+ $component = $database->setQuery($query)->loadAssoc();
+ $manifest = json_decode($component['manifest_cache'] ?? '{}', true);
+
+ if (!$component || (int) $component['enabled'] !== 1 || version_compare((string) ($manifest['version'] ?? '0.0.0'), '0.1.0', '<'))
+ {
+ Factory::getApplication()->enqueueMessage('Install and enable the JoomEngine MCP component before its console plugin.', 'error');
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Enable a newly installed console adapter, without changing upgrade choices.
+ *
+ * @param string $type Installer operation.
+ * @param InstallerAdapter $parent Joomla installer adapter.
+ * @return void
+ * @since 0.1.0
+ */
+ public function postflight(string $type, InstallerAdapter $parent): void
+ {
+ if ($type !== 'install')
+ {
+ return;
+ }
+
+ $database = Factory::getContainer()->get(DatabaseInterface::class);
+ $query = $database->getQuery(true)
+ ->update($database->quoteName('#__extensions'))
+ ->set($database->quoteName('enabled') . ' = 1')
+ ->where($database->quoteName('type') . ' = ' . $database->quote('plugin'))
+ ->where($database->quoteName('folder') . ' = ' . $database->quote('console'))
+ ->where($database->quoteName('element') . ' = ' . $database->quote('joomengine_mcp'));
+ $database->setQuery($query)->execute();
+ }
+}
diff --git a/services/provider.php b/services/provider.php
new file mode 100644
index 0000000..9a4e1f6
--- /dev/null
+++ b/services/provider.php
@@ -0,0 +1,44 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+use Joomla\CMS\Extension\PluginInterface;
+use Joomla\CMS\Factory;
+use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\DI\Container;
+use Joomla\DI\ServiceProviderInterface;
+use Joomla\Event\DispatcherInterface;
+use VDM\Plugin\Console\JoomEngineMcp\Extension\JoomEngineMcpPlugin;
+
+
+defined('_JEXEC') or die;
+
+/** Compose the plugin at Joomla's extension-service boundary. */
+return new class implements ServiceProviderInterface
+{
+ /**
+ * Register the console plugin without booting the MCP component eagerly.
+ *
+ * @param Container $container Joomla extension container.
+ * @return void
+ * @since 0.1.0
+ */
+ public function register(Container $container): void
+ {
+ $container->set(PluginInterface::class, static function (Container $container): PluginInterface
+ {
+ $plugin = new JoomEngineMcpPlugin(
+ $container->get(DispatcherInterface::class),
+ (array) PluginHelper::getPlugin('console', 'joomengine_mcp')
+ );
+ $plugin->setApplication(Factory::getApplication());
+
+ return $plugin;
+ });
+ }
+};
diff --git a/src/Console/McpCommand.php b/src/Console/McpCommand.php
new file mode 100644
index 0000000..2d21efb
--- /dev/null
+++ b/src/Console/McpCommand.php
@@ -0,0 +1,152 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+namespace VDM\Plugin\Console\JoomEngineMcp\Console;
+
+
+use Closure;
+use InvalidArgumentException;
+use Joomla\CMS\Application\ConsoleApplication;
+use Joomla\Console\Command\AbstractCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
+use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeInterface;
+
+
+/**
+ * Lazy console adapter preserving legacy framing and adding native MCP stdio.
+ *
+ * @since 0.1.0
+ */
+final class McpCommand extends AbstractCommand
+{
+ /**
+ * Fixed command operation selected by the plugin, never by a request body.
+ *
+ * @var string
+ * @since 0.1.0
+ */
+ private string $operation;
+
+ /**
+ * Component runtime resolver invoked only when this command executes.
+ *
+ * @var Closure():ConsoleRuntimeInterface
+ * @since 0.1.0
+ */
+ private Closure $resolveRuntime;
+
+ /**
+ * Bind one known command to the component composition root.
+ *
+ * @param string $operation Known console operation.
+ * @param callable $resolveRuntime Lazy component runtime resolver.
+ * @throws InvalidArgumentException For an unknown command operation.
+ * @since 0.1.0
+ */
+ public function __construct(string $operation, callable $resolveRuntime)
+ {
+ if (!in_array($operation, ['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'], true))
+ {
+ throw new InvalidArgumentException('Unknown Joomla MCP console operation.');
+ }
+
+ $this->operation = $operation;
+ $this->resolveRuntime = Closure::fromCallable($resolveRuntime);
+ parent::__construct('joomla:mcp:' . $operation);
+ }
+
+ /**
+ * Preserve the companion's public options; stdio has no alternate framing.
+ *
+ * @return void
+ * @since 0.1.0
+ */
+ protected function configure(): void
+ {
+ $this->setDescription($this->operation === 'serve'
+ ? 'Serve the installed database-driven Joomla MCP over PHP stdio.'
+ : 'Run the shared JoomEngine MCP ' . $this->operation . ' console operation.');
+
+ if ($this->operation !== 'serve')
+ {
+ $this->addOption('format', null, InputOption::VALUE_REQUIRED, 'Output format; dispatch also accepts ndjson.', 'json');
+ }
+
+ if ($this->operation === 'dispatch')
+ {
+ $this->addOption('input', null, InputOption::VALUE_REQUIRED, 'Only stdin (-) is supported.', '-');
+ }
+ }
+
+ /**
+ * Resolve the local component runtime and execute without a shell process.
+ *
+ * @param InputInterface $input Joomla console input.
+ * @param OutputInterface $output Joomla console output.
+ * @return int Zero only for successful command execution.
+ * @since 0.1.0
+ */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ if (PHP_SAPI !== 'cli' || !$this->getApplication() instanceof ConsoleApplication)
+ {
+ return $this->failure('LOCAL_CONSOLE_REQUIRED', 'This command requires the local Joomla console.');
+ }
+
+ try
+ {
+ $runtime = ($this->resolveRuntime)();
+
+ if (!$runtime instanceof ConsoleRuntimeInterface)
+ {
+ return $this->failure('DEPENDENCY_UNAVAILABLE', 'A compatible JoomEngine MCP component is required.');
+ }
+
+ if ($this->operation === 'serve')
+ {
+ return $runtime->serveStdio();
+ }
+
+ return $runtime->executeCommand($this->operation, $input, $output);
+ }
+ catch (Throwable)
+ {
+ return $this->failure('MCP_COMMAND_FAILED', 'The JoomEngine MCP command could not complete; check the component installation and server logs.');
+ }
+ }
+
+ /**
+ * Keep diagnostics out of MCP stdout and retain legacy structured errors.
+ *
+ * @param string $code Stable error code.
+ * @param string $message Non-sensitive operator diagnostic.
+ * @return int
+ * @since 0.1.0
+ */
+ private function failure(string $code, string $message): int
+ {
+ if ($this->operation === 'serve')
+ {
+ fwrite(STDERR, $message . PHP_EOL);
+ }
+ else
+ {
+ fwrite(STDOUT, json_encode([
+ 'protocol' => 'joomla-mcp/1',
+ 'id' => null,
+ 'ok' => false,
+ 'error' => ['code' => $code, 'message' => $message],
+ ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL);
+ }
+
+ return 1;
+ }
+}
diff --git a/src/Extension/JoomEngineMcpPlugin.php b/src/Extension/JoomEngineMcpPlugin.php
new file mode 100644
index 0000000..782bc9a
--- /dev/null
+++ b/src/Extension/JoomEngineMcpPlugin.php
@@ -0,0 +1,85 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+namespace VDM\Plugin\Console\JoomEngineMcp\Extension;
+
+
+use Joomla\Application\ApplicationEvents;
+use Joomla\CMS\Application\ConsoleApplication;
+use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\Event\SubscriberInterface;
+use RuntimeException;
+use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeInterface;
+use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeProviderInterface;
+use VDM\Plugin\Console\JoomEngineMcp\Console\McpCommand;
+
+
+/**
+ * Registers local-only commands while keeping the component runtime shared.
+ *
+ * @since 0.1.0
+ */
+final class JoomEngineMcpPlugin extends CMSPlugin implements SubscriberInterface
+{
+ /**
+ * Subscribe to Joomla's console lifecycle, not web request events.
+ *
+ * @return array
+ * @since 0.1.0
+ */
+ public static function getSubscribedEvents(): array
+ {
+ return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands'];
+ }
+
+ /**
+ * Register lazy adapters without affecting unrelated commands or web traffic.
+ *
+ * @return void
+ * @throws RuntimeException When a different extension owns an MCP command.
+ * @since 0.1.0
+ */
+ public function registerCommands(): void
+ {
+ $application = $this->getApplication();
+
+ if (PHP_SAPI !== 'cli' || !$application instanceof ConsoleApplication)
+ {
+ return;
+ }
+
+ $resolve = static function () use ($application): ConsoleRuntimeInterface
+ {
+ $component = $application->bootComponent('com_joomengine_mcp');
+
+ if (!$component instanceof ConsoleRuntimeProviderInterface)
+ {
+ throw new RuntimeException('A compatible JoomEngine MCP component must be installed and enabled.');
+ }
+
+ return $component->getConsoleRuntime($application);
+ };
+
+ foreach (['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'] as $operation)
+ {
+ $name = 'joomla:mcp:' . $operation;
+
+ if ($application->hasCommand($name))
+ {
+ if ($application->getCommand($name) instanceof McpCommand)
+ {
+ continue;
+ }
+
+ throw new RuntimeException('Refusing to replace an existing Joomla MCP console command.');
+ }
+
+ $application->addCommand(new McpCommand($operation, $resolve));
+ }
+ }
+}
diff --git a/tests/run.php b/tests/run.php
new file mode 100644
index 0000000..030e0ea
--- /dev/null
+++ b/tests/run.php
@@ -0,0 +1,67 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+$root = dirname(__DIR__);
+$manifest = simplexml_load_file($root . '/joomengine_mcp.xml');
+
+if ($manifest === false || (string) $manifest['group'] !== 'console'
+ || (string) $manifest->namespace !== 'VDM\\Plugin\\Console\\JoomEngineMcp'
+ || (string) $manifest->files->folder[0]['plugin'] !== 'joomengine_mcp')
+{
+ throw new RuntimeException('Plugin identity or namespace contract is invalid.');
+}
+
+$language = parse_ini_file($root . '/language/en-GB/plg_console_joomengine_mcp.sys.ini');
+
+if ($language === false || !isset($language[(string) $manifest->description]))
+{
+ throw new RuntimeException('The plugin manifest description has no language value.');
+}
+
+$feed = simplexml_load_file($root . '/joomengine_mcp_update_server.xml');
+$changelog = simplexml_load_file($root . '/joomengine_mcp_changelog.xml');
+
+if ($feed === false || $changelog === false)
+{
+ throw new RuntimeException('Update or changelog XML is invalid.');
+}
+
+require $root . '/build.php';
+$archive = $root . '/build/plg_console_joomengine_mcp-' . (string) $manifest->version . '.zip';
+$firstHash = hash_file('sha256', $archive);
+require $root . '/build.php';
+
+if (!hash_equals($firstHash, hash_file('sha256', $archive)))
+{
+ throw new RuntimeException('The same plugin source did not produce a reproducible archive.');
+}
+
+$zip = new ZipArchive();
+$zip->open($archive);
+
+foreach (['joomengine_mcp.xml', 'services/provider.php', 'src/Extension/JoomEngineMcpPlugin.php', 'src/Console/McpCommand.php', 'script.php', 'LICENSE'] as $required)
+{
+ if ($zip->locateName($required) === false)
+ {
+ throw new RuntimeException('The plugin archive is missing an installation dependency.');
+ }
+}
+
+for ($index = 0; $index < $zip->numFiles; $index++)
+{
+ $name = $zip->getNameIndex($index);
+
+ if (str_starts_with($name, '/') || str_contains($name, '..') || str_starts_with($name, 'tests/') || str_ends_with($name, '.ts'))
+ {
+ throw new RuntimeException('The plugin archive contains a forbidden path.');
+ }
+}
+
+$zip->close();
+echo json_encode(['manifest' => 'passed', 'languages' => 'passed', 'package' => 'passed', 'reproducible' => true, 'liveJoomla' => 'covered by the component integration workflow'], JSON_PRETTY_PRINT) . PHP_EOL;
From bc3bcf0eea2b2bcde8aa4e617f16996862012d88 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Fri, 18 Sep 2026 00:01:54 +0200
Subject: [PATCH 04/17] fix: use native Joomla installer DI and preserve clean
MCP output through console shutdown
---
joomengine_mcp.xml | 19 ++---
script.php | 88 ++++----------------
services/provider.php | 2 -
src/Console/OutputGuard.php | 91 +++++++++++++++++++++
src/Extension/JoomEngineMcpPlugin.php | 32 +++++++-
src/Installer/InstallerScript.php | 113 ++++++++++++++++++++++++++
6 files changed, 262 insertions(+), 83 deletions(-)
create mode 100644 src/Console/OutputGuard.php
create mode 100644 src/Installer/InstallerScript.php
diff --git a/joomengine_mcp.xml b/joomengine_mcp.xml
index 4a94752..aee997a 100644
--- a/joomengine_mcp.xml
+++ b/joomengine_mcp.xml
@@ -1,30 +1,29 @@
- plg_console_joomengine_mcp
- Llewellyn van der Merwe
+ PLG_CONSOLE_JOOMENGINE_MCP
+ Vast Development Method
September 2026
Copyright (C) 2026 Vast Development Method. All rights reserved.
GNU General Public License version 3 or later; see LICENSE
- https://dev.vdm.io/
+ joomla@vdm.io
+ https://dev.vdm.io
0.1.0
- PLG_CONSOLE_JOOMENGINE_MCP_XML_DESCRIPTION
+ PLG_CONSOLE_JOOMENGINE_MCP_DESCRIPTION
VDM\Plugin\Console\JoomEngineMcp
script.php
services
src
language
- script.php
LICENSE
-
- en-GB/plg_console_joomengine_mcp.ini
- en-GB/plg_console_joomengine_mcp.sys.ini
+
+ language/en-GB/plg_console_joomengine_mcp.ini
+ language/en-GB/plg_console_joomengine_mcp.sys.ini
https://raw.githubusercontent.com/joomengine/mcp_plugin/main/joomengine_mcp_update_server.xml
https://raw.githubusercontent.com/joomengine/mcp_plugin/main/joomengine_mcp_changelog.xml
-
- 8.3.0
+
diff --git a/script.php b/script.php
index 7a61776..4bed967 100644
--- a/script.php
+++ b/script.php
@@ -6,85 +6,33 @@
* @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
-
use Joomla\CMS\Factory;
-use Joomla\CMS\Installer\InstallerAdapter;
-use Joomla\CMS\Version;
+use Joomla\CMS\Installer\InstallerScriptInterface;
use Joomla\Database\DatabaseInterface;
+use Joomla\DI\Container;
+use Joomla\DI\ServiceProviderInterface;
+use VDM\Plugin\Console\JoomEngineMcp\Installer\InstallerScript;
+\defined('_JEXEC') or die;
-defined('_JEXEC') or die;
+if (!class_exists(InstallerScript::class, false))
+{
+ require_once __DIR__ . '/src/Installer/InstallerScript.php';
+}
/**
- * Validate dependencies and preserve operator settings on upgrades.
+ * Explicitly injected native Joomla installer provider.
*
- * @since 0.1.0
+ * @since 0.1.0
*/
-class PlgConsoleJoomengine_mcpInstallerScript
+return new class implements ServiceProviderInterface
{
- /**
- * Reject unsupported PHP/Joomla or a missing component dependency.
- *
- * @param string $type Installer operation.
- * @param InstallerAdapter $parent Joomla installer adapter.
- * @return bool
- * @since 0.1.0
- */
- public function preflight(string $type, InstallerAdapter $parent): bool
+ /** @inheritDoc */
+ public function register(Container $container): void
{
- if ($type === 'uninstall')
- {
- return true;
- }
-
- if (version_compare(PHP_VERSION, '8.3.0', '<') || version_compare((new Version())->getShortVersion(), '6.1.0', '<'))
- {
- Factory::getApplication()->enqueueMessage('JoomEngine MCP requires Joomla 6.1 or later and PHP 8.3 or later.', 'error');
-
- return false;
- }
-
- $database = Factory::getContainer()->get(DatabaseInterface::class);
- $query = $database->getQuery(true)
- ->select($database->quoteName(['enabled', 'manifest_cache']))
- ->from($database->quoteName('#__extensions'))
- ->where($database->quoteName('type') . ' = ' . $database->quote('component'))
- ->where($database->quoteName('element') . ' = ' . $database->quote('com_joomengine_mcp'));
- $component = $database->setQuery($query)->loadAssoc();
- $manifest = json_decode($component['manifest_cache'] ?? '{}', true);
-
- if (!$component || (int) $component['enabled'] !== 1 || version_compare((string) ($manifest['version'] ?? '0.0.0'), '0.1.0', '<'))
+ $container->set(InstallerScriptInterface::class, static function (Container $container): InstallerScriptInterface
{
- Factory::getApplication()->enqueueMessage('Install and enable the JoomEngine MCP component before its console plugin.', 'error');
-
- return false;
- }
-
- return true;
- }
-
- /**
- * Enable a newly installed console adapter, without changing upgrade choices.
- *
- * @param string $type Installer operation.
- * @param InstallerAdapter $parent Joomla installer adapter.
- * @return void
- * @since 0.1.0
- */
- public function postflight(string $type, InstallerAdapter $parent): void
- {
- if ($type !== 'install')
- {
- return;
- }
-
- $database = Factory::getContainer()->get(DatabaseInterface::class);
- $query = $database->getQuery(true)
- ->update($database->quoteName('#__extensions'))
- ->set($database->quoteName('enabled') . ' = 1')
- ->where($database->quoteName('type') . ' = ' . $database->quote('plugin'))
- ->where($database->quoteName('folder') . ' = ' . $database->quote('console'))
- ->where($database->quoteName('element') . ' = ' . $database->quote('joomengine_mcp'));
- $database->setQuery($query)->execute();
+ return new InstallerScript($container->get(DatabaseInterface::class), Factory::getApplication());
+ });
}
-}
+};
diff --git a/services/provider.php b/services/provider.php
index 9a4e1f6..b35175a 100644
--- a/services/provider.php
+++ b/services/provider.php
@@ -12,7 +12,6 @@
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
-use Joomla\Event\DispatcherInterface;
use VDM\Plugin\Console\JoomEngineMcp\Extension\JoomEngineMcpPlugin;
@@ -33,7 +32,6 @@ public function register(Container $container): void
$container->set(PluginInterface::class, static function (Container $container): PluginInterface
{
$plugin = new JoomEngineMcpPlugin(
- $container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('console', 'joomengine_mcp')
);
$plugin->setApplication(Factory::getApplication());
diff --git a/src/Console/OutputGuard.php b/src/Console/OutputGuard.php
new file mode 100644
index 0000000..19eb867
--- /dev/null
+++ b/src/Console/OutputGuard.php
@@ -0,0 +1,91 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+namespace VDM\Plugin\Console\JoomEngineMcp\Console;
+
+
+use Joomla\CMS\Application\ConsoleApplication;
+use Symfony\Component\Console\Output\ConsoleOutputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Output\StreamOutput;
+
+
+/**
+ * Keep Joomla's post-command messages out of the newline-delimited MCP stream.
+ *
+ * Protocol frames write directly to STDOUT. Joomla's console formatter is muted
+ * until AFTER_EXECUTE, while native exception diagnostics still use STDERR.
+ * All formatting state is restored after Joomla has flushed its message queue.
+ *
+ * @since 0.1.0
+ */
+final class OutputGuard
+{
+ /** @var OutputInterface Native console formatter, not the protocol stream. @since 0.1.0 */
+ private OutputInterface $output;
+ /** @var ?OutputInterface Previous exception output. @since 0.1.0 */
+ private ?OutputInterface $errorOutput = null;
+ /** @var int Previous console verbosity. @since 0.1.0 */
+ private int $verbosity;
+ /** @var string|false Previous PHP notice-output mode. @since 0.1.0 */
+ private string|false $displayErrors;
+ /** @var bool Whether the guard still owns the formatter state. @since 0.1.0 */
+ private bool $active = true;
+
+ /** @param ConsoleApplication $application Actual local console. @since 0.1.0 */
+ public function __construct(ConsoleApplication $application)
+ {
+ $this->output = $application->getConsoleOutput();
+ $this->verbosity = $this->output->getVerbosity();
+ $this->displayErrors = ini_get('display_errors');
+
+ if ($this->output instanceof ConsoleOutputInterface)
+ {
+ $this->errorOutput = $this->output->getErrorOutput();
+ }
+
+ $this->output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
+
+ if ($this->output instanceof ConsoleOutputInterface)
+ {
+ $this->output->setErrorOutput(new StreamOutput(STDERR, $this->verbosity, false));
+ }
+
+ ini_set('display_errors', 'stderr');
+ }
+
+ /** @param ConsoleApplication $application Finished command application. @return void Restore after Joomla's post-command output. @since 0.1.0 */
+ public function restore(ConsoleApplication $application): void
+ {
+ if (!$this->active)
+ {
+ return;
+ }
+
+ foreach ($application->getMessageQueue() as $type => $messages)
+ {
+ // Message bodies can contain untrusted content or secrets. Operation
+ // results carry their own safe diagnostics; report only queue counts.
+ fwrite(STDERR, 'Joomla queued ' . count($messages) . ' ' . preg_replace('/[^a-z_-]/i', '', (string) $type) . ' message(s) during the MCP command.' . PHP_EOL);
+ }
+
+ if ($this->errorOutput !== null && $this->output instanceof ConsoleOutputInterface)
+ {
+ $this->output->setErrorOutput($this->errorOutput);
+ }
+
+ $this->output->setVerbosity($this->verbosity);
+
+ if ($this->displayErrors !== false)
+ {
+ ini_set('display_errors', $this->displayErrors);
+ }
+
+ $this->active = false;
+ }
+}
diff --git a/src/Extension/JoomEngineMcpPlugin.php b/src/Extension/JoomEngineMcpPlugin.php
index 782bc9a..6116d70 100644
--- a/src/Extension/JoomEngineMcpPlugin.php
+++ b/src/Extension/JoomEngineMcpPlugin.php
@@ -11,12 +11,14 @@
use Joomla\Application\ApplicationEvents;
use Joomla\CMS\Application\ConsoleApplication;
+use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use RuntimeException;
use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeInterface;
use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeProviderInterface;
use VDM\Plugin\Console\JoomEngineMcp\Console\McpCommand;
+use VDM\Plugin\Console\JoomEngineMcp\Console\OutputGuard;
/**
@@ -26,6 +28,9 @@
*/
final class JoomEngineMcpPlugin extends CMSPlugin implements SubscriberInterface
{
+ /** @var ?OutputGuard Scoped console formatter protection. @since 0.1.0 */
+ private ?OutputGuard $outputGuard = null;
+
/**
* Subscribe to Joomla's console lifecycle, not web request events.
*
@@ -34,7 +39,7 @@ final class JoomEngineMcpPlugin extends CMSPlugin implements SubscriberInterface
*/
public static function getSubscribedEvents(): array
{
- return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands'];
+ return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands', ApplicationEvents::AFTER_EXECUTE => 'restoreOutput'];
}
/**
@@ -53,8 +58,21 @@ public function registerCommands(): void
return;
}
+ $selected = $application->getConsoleInput()->getFirstArgument();
+
+ if (is_string($selected) && str_starts_with($selected, 'joomla:mcp:')
+ && !$application->getConsoleInput()->hasParameterOption(['--help', '-h', '--version', '-V']))
+ {
+ $this->outputGuard ??= new OutputGuard($application);
+ }
+
$resolve = static function () use ($application): ConsoleRuntimeInterface
{
+ if (!ComponentHelper::isEnabled('com_joomengine_mcp'))
+ {
+ throw new RuntimeException('The JoomEngine MCP component is disabled or not installed.');
+ }
+
$component = $application->bootComponent('com_joomengine_mcp');
if (!$component instanceof ConsoleRuntimeProviderInterface)
@@ -82,4 +100,16 @@ public function registerCommands(): void
$application->addCommand(new McpCommand($operation, $resolve));
}
}
+
+ /** @return void Restore native formatter state after Joomla flushes its queue. @since 0.1.0 */
+ public function restoreOutput(): void
+ {
+ $application = $this->getApplication();
+
+ if ($this->outputGuard !== null && $application instanceof ConsoleApplication)
+ {
+ $this->outputGuard->restore($application);
+ $this->outputGuard = null;
+ }
+ }
}
diff --git a/src/Installer/InstallerScript.php b/src/Installer/InstallerScript.php
new file mode 100644
index 0000000..f86530f
--- /dev/null
+++ b/src/Installer/InstallerScript.php
@@ -0,0 +1,113 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+namespace VDM\Plugin\Console\JoomEngineMcp\Installer;
+
+
+use Joomla\CMS\Application\CMSApplicationInterface;
+use Joomla\CMS\Installer\InstallerAdapter;
+use Joomla\CMS\Installer\InstallerScriptInterface;
+use Joomla\CMS\Version;
+use Joomla\Database\DatabaseInterface;
+use RuntimeException;
+use Throwable;
+
+
+/**
+ * Joomla-native dependency and version checks for the independent console adapter.
+ *
+ * @since 0.1.0
+ */
+final class InstallerScript implements InstallerScriptInterface
+{
+ /** @var DatabaseInterface Joomla extension registry. @since 0.1.0 */
+ private DatabaseInterface $database;
+ /** @var CMSApplicationInterface Native installer application. @since 0.1.0 */
+ private CMSApplicationInterface $application;
+
+ /** @param DatabaseInterface $database Native database. @param CMSApplicationInterface $application Actual installer. @since 0.1.0 */
+ public function __construct(DatabaseInterface $database, CMSApplicationInterface $application)
+ {
+ $this->database = $database;
+ $this->application = $application;
+ }
+
+ /** @inheritDoc */
+ public function preflight(string $type, InstallerAdapter $adapter): bool
+ {
+ if ($type === 'uninstall')
+ {
+ return true;
+ }
+
+ try
+ {
+ $version = (new Version())->getShortVersion();
+
+ if (version_compare(PHP_VERSION, '8.3.0', '<') || version_compare($version, '6.1.0', '<') || version_compare($version, '7.0.0', '>='))
+ {
+ throw new RuntimeException('JoomEngine MCP requires Joomla 6.1–6.x and PHP 8.3 or later.');
+ }
+
+ $db = $this->database;
+ $query = $db->createQuery()->select($db->quoteName(['enabled', 'manifest_cache']))->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomengine_mcp'));
+ $row = $db->setQuery($query)->loadAssoc();
+ $manifest = json_decode($row['manifest_cache'] ?? '{}', true);
+ $componentVersion = (string) ($manifest['version'] ?? '0');
+ $pluginVersion = (string) $adapter->getManifest()->version;
+
+ if ($row === null || (int) $row['enabled'] !== 1 || version_compare($componentVersion, '0.1.0', '<')
+ || explode('.', $componentVersion)[0] !== explode('.', $pluginVersion)[0]
+ || !is_file(JPATH_ADMINISTRATOR . '/components/com_joomengine_mcp/vendor/autoload.php'))
+ {
+ throw new RuntimeException('Install and enable the compatible built JoomEngine MCP component before its console plugin.');
+ }
+
+ return true;
+ }
+ catch (Throwable $error)
+ {
+ $this->application->enqueueMessage($error instanceof RuntimeException ? $error->getMessage() : 'The console plugin dependency check failed.', 'error');
+
+ return false;
+ }
+ }
+
+ /** @inheritDoc */
+ public function install(InstallerAdapter $adapter): bool
+ {
+ $db = $this->database;
+ $query = $db->createQuery()->update($db->quoteName('#__extensions'))->set($db->quoteName('enabled') . ' = 1')
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('console'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('joomengine_mcp'));
+ $db->setQuery($query)->execute();
+
+ return true;
+ }
+
+ /** @inheritDoc */
+ public function update(InstallerAdapter $adapter): bool
+ {
+ return true;
+ }
+
+ /** @inheritDoc */
+ public function uninstall(InstallerAdapter $adapter): bool
+ {
+ return true;
+ }
+
+ /** @inheritDoc */
+ public function postflight(string $type, InstallerAdapter $adapter): bool
+ {
+ return true;
+ }
+}
From fb6a72a515859d198b47acb6fdac932e5dae92ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Fri, 18 Sep 2026 00:11:59 +0200
Subject: [PATCH 05/17] fix: align the console manifest description with its
shipped language key
---
joomengine_mcp.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/joomengine_mcp.xml b/joomengine_mcp.xml
index aee997a..70d5984 100644
--- a/joomengine_mcp.xml
+++ b/joomengine_mcp.xml
@@ -8,7 +8,7 @@
joomla@vdm.io
https://dev.vdm.io
0.1.0
- PLG_CONSOLE_JOOMENGINE_MCP_DESCRIPTION
+ PLG_CONSOLE_JOOMENGINE_MCP_XML_DESCRIPTION
VDM\Plugin\Console\JoomEngineMcp
script.php
From e7fb901cb0475abc56b40c65a02430eaa5cc426f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Fri, 18 Sep 2026 16:26:25 +0200
Subject: [PATCH 06/17] docs: require full JCB console coverage and separate
external client ownership
---
AGENTS.md | 16 +++++++++-------
CHANGELOG.md | 8 +++++---
README.md | 26 +++++++++++++++++++------
docs/ARCHITECTURE.md | 32 ++++++++++++++-----------------
docs/IMPLEMENTATION.md | 29 ++++++++++++++++------------
docs/JCB-INTEGRATION.md | 42 +++++++++++++++++++++++++++++++++++++++++
6 files changed, 107 insertions(+), 46 deletions(-)
create mode 100644 docs/JCB-INTEGRATION.md
diff --git a/AGENTS.md b/AGENTS.md
index e2d6191..d963a7c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,15 +1,17 @@
# Agent contract — console plugin
-Complete the migration of the original PHP companion and CLI track from `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36` without dropping commands, schemas, supported native operations, dry-run/plan, approval, verification or recovery behaviour. Share the database-driven component runtime rather than duplicating its catalogue, protocol, policy or audit implementation.
+Complete the original companion/CLI migration from `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36` without losing supported commands, schemas, native actions, preview/plan, approval, verification or recovery. **Also complete the JCB CLI integration required by docs/JCB-INTEGRATION.md and the component's canonical JCB roadmap.** Core-only support is not the completed objective.
-Use the existing `feature/jcb-mcp-runtime` branch and PR #1. Push cohesive commits, update `docs/IMPLEMENTATION.md` with exact outcomes/remaining work, and keep the PR draft until runtime implementation and required tests are complete. Do not create replacement branches, force-push, merge, publish releases or edit the original TypeScript repository.
+Stay on `feature/jcb-mcp-runtime` / PR #1. Push coherent commits and update docs/IMPLEMENTATION.md with actual tests and remaining work. Do not replace branches, force-push, merge, publish or alter the original MCP/JCB source repositories without separate instruction.
-Use element `joomengine_mcp`, group `console`, extension `plg_console_joomengine_mcp`, namespace `VDM\Plugin\Console\JoomEngineMcp`. The dependency is `com_joomengine_mcp`, namespace `VDM\Component\JoomEngineMcp`. HTTP webservices glue belongs to the component distribution, not this console plugin.
+Use element `joomengine_mcp`, group `console`, namespace `VDM\Plugin\Console\JoomEngineMcp`; dependency `com_joomengine_mcp`, namespace `VDM\Component\JoomEngineMcp`. Joomla 6 native plugin/event/DI/console contracts are authoritative. Follow JCB's plugin-root manifest/installer/services/src/language/update layout. PHP style authority: https://github.com/extension-builder/joomla/blob/main/docs/development/php-code-style.md (tabs, LF, Allman braces, explicit typed properties/constructor injection, meaningful docblocks, no closing tags or isolated strict_types/promotion/readonly changes). Preserve inherited signatures.
-Joomla 6 native plugin/event/DI/console contracts are authoritative. Follow the JCB generated plugin-root layout: manifest, installer, `services/provider.php`, `src/Extension`, `src/Console`, language files, changelog and update metadata. The PHP style authority is https://github.com/extension-builder/joomla/blob/main/docs/development/php-code-style.md: tabs, LF, Allman braces, explicit typed properties/constructor injection, meaningful docblocks, no closing PHP tags or isolated strict_types/property promotion. Preserve Joomla inherited signatures. This is hand-authored JCB-aligned code, not an already imported JCB blueprint.
+External Composer client and remote stdio bridge belong only to `joomengine/mcp_client`, package `joomengine/mcp-client`. Do not implement or depend on them here. The component owns HTTP routing glue, catalogue/schema resolution, business handlers, permissions/plans, jobs/artifacts, verification and audit. The plugin supplies local entry and adaptation only.
-Only the real Joomla console application under CLI may create the privileged local execution context. JSON input, headers, tokens and database rows may not manufacture it. Local execution does not require a Joomla token/row view-level check, but still validates inputs and handler bindings and retains audit/verification/recovery. Never expose this path via an HTTP controller or spawn arbitrary row-supplied shell commands.
+Only the real console application under CLI can establish server-owner authority. JSON, headers, tokens and database rows cannot do so. Local requests still validate inputs and bindings and retain audit/recovery; HTTP-originated jobs never acquire unrestricted authority just because a local worker executes them.
-Stdio stdout is exclusively newline-delimited JSON-RPC. Send diagnostics to stderr and preserve nonzero failures. Bound input size/time and handle malformed messages/EOF without corrupting the next message. Missing/incompatible component dependencies should produce a clear command failure without breaking unrelated Joomla console commands.
+JCB's installed command plugin owns `componentbuilder:*` registration. Inventory it after registration, verify exact command identity/InputDefinitions, and invoke only reviewed database-selected mappings. Do not generate all family/entity combinations from the 45-entity factory map, replace JCB commands, dynamically instantiate arbitrary classes or spawn row-supplied shell programs. Preserve local file-input forms, effective global/environment options, dependencies, stdout/stderr, exit codes and partial effects. JCB package get is not an ordinary read-only lookup. Long operations use shared durable jobs, not uncontrolled timeouts/retries.
-Tests must cover service registration, installation/dependency checks, original companion contract parity, stdio protocol, malformed input, real Joomla reads/writes/read-back/cleanup, and local-vs-HTTP authority separation. Do not report mocked or syntax tests as live passes. Package versions align with the component; update feeds must not advertise unpublished artifacts. Retain source licences and notices.
+Stdio stdout contains only JSON-RPC. Keep banners/notices/logs off it; preserve nonzero failures and EOF/byte bounds. Missing/incompatible component or JCB dependencies must fail the affected operation clearly without breaking unrelated Joomla/core commands. Restore native identity/input/factory state or use isolated job workers so consecutive requests cannot contaminate one another.
+
+Run syntax, provider/registration, manifest/package tests and coordinated installed Joomla/JCB API/CLI/stdio tests. Exercise true writes/read-back/cleanup, dependency queues, compile/install artifacts, command ordering, concurrency, cancellation, errors and HTTP/local-authority separation. Package checks are not live passes. Align server package versions/update feeds, retain licences and never advertise unpublished artifacts or completed JCB coverage without evidence.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4665892..d7373c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
## Unreleased
-- Document the exact `joomengine_mcp` console-plugin identity, trusted local boundary and shared component-runtime contract.
-- Define complete original companion migration, direct PHP stdio, installation and verification objectives.
+- Establish the exact joomengine_mcp console plugin identity, local-server authority and shared component contract.
+- Add native plugin/provider/lazy command adapters, output isolation, installer checks, languages/update metadata and PHP-only reproducible packaging.
+- Separate external Composer-client/remote-bridge ownership into `joomengine/mcp_client`; no server/plugin dependency on that package.
+- Require complete first-class JCB API/CLI coverage and document native command registration, compiler/package semantics, shared jobs and installed acceptance responsibilities.
-No production release is advertised.
+JCB execution expansion and complete live combined installation remain pending. No production release is advertised.
diff --git a/README.md b/README.md
index 2e9cbeb..d36542b 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,32 @@
# JoomEngine MCP console plugin
-PHP-only Joomla console integration for [`com_joomengine_mcp`](https://github.com/joomengine/mcp_component).
+PHP-only local Joomla console integration for `com_joomengine_mcp`.
**Element:** `joomengine_mcp`
**Group:** `console`
**Extension:** `plg_console_joomengine_mcp`
**Namespace:** `VDM\Plugin\Console\JoomEngineMcp`
-This repository migrates the existing `joomengine/joomla-mcp` PHP companion into a separately installable plugin. It connects Joomla's console application to the component's shared database-driven MCP engine and provides direct PHP stdio serving. It is not the HTTP webservices routing plugin; that small adapter is distributed with the component.
+The plugin connects Joomla's console lifecycle to the component-owned database catalogue and execution engine. It provides the `joomla:mcp:serve`, `describe`, `dispatch`, `self-test` and `cli-inventory` adapters. It does not contain a second MCP catalogue or an HTTP webservices plugin.
-## Authority and status
+## Three repository boundaries
-Local console execution is the trusted-server track requested by the project owner. It does not need a Joomla API token or row viewing-level authorization. Input validation, explicit action semantics, bounded execution, audit, verification and recovery remain mandatory. No HTTP request or database row can opt into this local privilege.
+- [`mcp_component`](https://github.com/joomengine/mcp_component): installed server, database definitions, HTTP authentication/ACL/routing, administrator application, API/native handlers, durable plans/jobs and verification.
+- This repository: trusted local console entry, typed command/runtime integration, protocol output isolation and plugin distribution.
+- [`mcp_client`](https://github.com/joomengine/mcp_client): external Composer client `joomengine/mcp-client` and remote stdio bridge. Neither component nor plugin depends on it.
-Work remains on `feature/jcb-mcp-runtime`, draft PR #1. Architecture precedes runtime. See [implementation status](docs/IMPLEMENTATION.md), [architecture](docs/ARCHITECTURE.md) and [agent instructions](AGENTS.md). No production certification is implied by an install manifest, a catalogue entry or a syntax-only test.
+Direct local server stdio is not the remote bridge. A client talking over HTTP remains restricted by its Joomla API token regardless of whether it speaks stdio to an AI application on the workstation.
-Original migration source: `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36`, especially `companion/plugin`. Preserve all original licences, supported commands, request/result contracts and native operation behaviour. The original repository is not modified.
+## Required JCB coverage
+
+The server and this plugin must support **all actual Joomla Component Builder API and registered CLI capabilities**, alongside Joomla core. For this plugin that includes correct discovery/invocation of JCB's compiler and package get/init/pull/push/reset commands, all registered entity/area variants, native options, output/exit semantics and long-operation handling through the shared component.
+
+Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). Existing CLI inventory or a list of command names is not proof that those operations execute. JCB handlers and database bindings remain component-owned; do not copy JCB's compiler or register duplicate `componentbuilder:*` commands here. The JCB integration is required roadmap work, not already completed by this documentation change.
+
+## Status and local authority
+
+Continue `feature/jcb-mcp-runtime`, draft PR #1. The plugin manifest/provider/lazy command adapters/output guard/installer and PHP-only package builder exist, with PHP 8.3/8.4 packaging contracts. Complete installed Joomla/JCB execution and combined component/package acceptance are still pending. See [implementation status](docs/IMPLEMENTATION.md).
+
+Local execution uses the genuine Joomla console application under CLI SAPI, without a Joomla API token or row-viewing-level restriction. Input validation, explicit action semantics, grants/plans, bounded output, audit, verification and recovery still apply. HTTP requests and database values cannot manufacture this local privilege.
+
+Original migration source: `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36`, especially companion/plugin. Preserve licences and all supported request/result/command behaviours. The source repository is unchanged.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index edd689c..e790625 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,33 +1,29 @@
# Console plugin architecture
-## Identity and responsibility
+## Identity and repository boundary
-`plg_console_joomengine_mcp`, element `joomengine_mcp`, group `console`, namespace `VDM\Plugin\Console\JoomEngineMcp`. Joomla-native plugin-root source uses `joomengine_mcp.xml`, installer, `services/provider.php`, `src/Extension`, `src/Console` and language/update/changelog files. Do not wrap it inside another plugin directory.
+`plg_console_joomengine_mcp`: element `joomengine_mcp`, group `console`, namespace `VDM\Plugin\Console\JoomEngineMcp`. Root manifest/installer/services/src/language/update files form a Joomla-native, JCB-aligned plugin project. Do not nest it in another installable plugin root.
-The plugin is the local CLI adapter to `com_joomengine_mcp`. It is not the component's HTTP webservices adapter. The component owns database catalogue/schema/binding resolution, protocol processing, reusable native and API handlers, durable plans/grants/idempotency/locks, verification and audit. No duplicated action catalogue or alternative policy engine belongs here.
+The plugin adapts the real Joomla console to the shared `com_joomengine_mcp` runtime. It owns neither HTTP webservices routing nor an external Composer client. HTTP routing glue belongs to the component; external client/remote stdio belongs to `joomengine/mcp_client`. No server-to-client package dependency is permitted.
-## Execution boundary
+## Local execution and wire framing
-Joomla console application boots enabled console plugins through its native event lifecycle. The plugin registers named commands using Joomla Console/Symfony Console contracts. Command execution verifies that the application is Joomla's console application and PHP is running in CLI before creating trusted-local context. Owning the server is the user's requested authority boundary; no API token or Joomla row view-level checks are required for this track. HTTP cannot select it.
+The native Joomla console lifecycle registers lazy `joomla:mcp:serve`, `describe`, `dispatch`, `self-test` and `cli-inventory` adapters. Invocation verifies the real console application/CLI SAPI and resolves the component's typed ConsoleRuntimeProviderInterface/ConsoleRuntimeInterface. Missing component dependencies produce a command failure without eagerly breaking unrelated commands.
-The privileged track still validates all schemas and declarative handler bindings, uses registered native actions/stock command mappings, bounds input and execution, retains explicit destructive-action semantics and records local provenance. It does not run arbitrary PHP, SQL, class names or shell supplied by database rows. A remote PHP stdio-to-HTTP bridge remains an HTTP-token-restricted client and is not trusted CLI.
+Local server ownership is the requested authority boundary: no API token or row viewing permission is needed, but schema validation, explicit effects/grants/plans, verification and recovery remain. The authority cannot be requested from remote JSON, database content or tokens. A remote stdio-to-HTTP client is a different product and remains API-ACL-restricted.
-## Compatibility
+MCP stdout contains JSON-RPC only. Legacy command JSON/NDJSON framing, input bounds, EOF and nonzero outcomes are preserved independently. Isolate Joomla banners, ANSI messages and native command diagnostics; do not convert warnings/partial mutation into an empty success envelope.
-Inventory and migrate every original companion entry point and action contract from `companion/plugin` at `2cff50f4f6b440da3c684f9995a77efad32e1a36`. Preserve the existing `joomla:mcp:describe`, `joomla:mcp:dispatch`, `joomla:mcp:self-test` and `joomla:mcp:cli-inventory` interfaces where confirmed in the pinned source; add a direct MCP stdio command without requiring the TypeScript process. Keep aliases explicit and tested. Preserve native-model events, filters, dry-run/preflight, partial-apply diagnostics and structured result/error shapes.
+## Required JCB integration
-Stdio emits only JSON-RPC frames to stdout; banners/notices/logs go to stderr or are captured as diagnostics. Requests are bounded newline-delimited UTF-8 JSON. Errors cannot silently become successful empty results. The shared engine reads installed database definitions, so extensions installed by rows become available to CLI without editing this plugin.
+Full JCB API and CLI support is a first-class server objective; this plugin supplies its local console adaptation. See JCB-INTEGRATION.md and the canonical component roadmap. JCB's own command plugin remains responsible for registering native compiler/package commands. Inventory actual registered names/aliases/arguments/options after all relevant plugins load; do not duplicate command registrations or invent names from entity counts.
-## Installation and distribution
+Shared component services implement JCB provider/schema/action/binding/target resolution, reviewed native/API adapters, durable jobs/artifacts and verification. The plugin must support invoking those bindings while preserving native compiler/global/environment/file/dependency/output semantics. No separate catalogue or JCB compiler copy belongs here.
-Require a compatible Joomla 6/PHP baseline and installed compatible component. Installation/update preserves enabled state and existing settings. Detect missing dependencies on invocation without crashing unrelated Joomla commands. Provide standalone plugin archive and join the component's `.octojpack` package assembly. Component/plugin versions are pinned together for release. Ship changelog/update metadata but no feed entry for an unpublished release. Preserve original licence notices during migration.
+Job workers need explicit authority provenance. A job requested over HTTP must retain the initiating Joomla user's permission limits, even when processed by a local worker; only jobs requested through the trusted local track have unrestricted server-owner authority. State/identity/input/factory isolation and cancellation/reconciliation are part of the handler contract.
-## Acceptance
+## Distribution and acceptance
-Test namespace/autoload/manifest and DI/event contracts, old companion request/result compatibility, actual console command execution and protocol framing. On disposable Joomla, exercise native reads/writes with persisted read-back and cleanup, CLI inventory, error/partial-apply behaviour and inability for an HTTP request to manufacture local context. Record real commands/results and missing evidence in `docs/IMPLEMENTATION.md`; syntax/unit tests alone are not live certification.
+Require compatible Joomla/PHP and component dependencies. Preserve installation enablement/settings through updates, provide a standalone PHP-built plugin archive and join the component's .octojpack package assembly. Versioned update feeds may reference only published archives. Retain original notices.
-## References
-
-- Component plan: https://github.com/joomengine/mcp_component/pull/1
-- Original companion: https://github.com/joomengine/joomla-mcp/tree/2cff50f4f6b440da3c684f9995a77efad32e1a36/companion/plugin
-- JCB source/style: https://github.com/extension-builder/joomla/blob/main/docs/development/php-code-style.md
+Validate native DI/event/namespace/manifest contracts and actual legacy/stdio command execution in disposable Joomla. Extend that matrix to JCB absent/disabled/installed/upgraded; registered compiler/package operations, persisted entity/repository changes, output archives, partial failures and cleanup. Source/packaging tests alone are not installed-runtime certification. Track current evidence and remaining work in IMPLEMENTATION.md.
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index 0d807c2..d878274 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -1,18 +1,23 @@
-# Implementation status
+# Implementation status — 18 September 2026
-## Branch and review
+## Branch
-The existing `feature/jcb-mcp-runtime` branch is retained. PR #1 has now actually been created; earlier documentation incorrectly assumed that it already existed.
+Continue `feature/jcb-mcp-runtime`, draft PR #1. The implementation baseline inspected for this documentation update is `fb6a72a515859d198b47acb6fdac932e5dae92ab`; do not replace the existing runtime branch.
-## Runtime implemented
+## Implemented runtime
-- Exact plugin element/group/namespace, Joomla console lifecycle and dependency-injected provider.
-- Lazy command adapters for `joomla:mcp:serve`, `describe`, `dispatch`, `self-test` and `cli-inventory`.
-- Local-only entry checks; missing component failures do not eagerly break unrelated Joomla commands.
-- Original companion protocol framing delegated to the component-owned implementation, with PHP MCP stdio as a separate mode.
-- Installer dependency checks, first-install enabling with upgrade state preservation, language/update/changelog metadata.
-- Reproducible PHP-only ZIP builder and manifest/package contracts on PHP 8.3 and 8.4.
+Exact plugin element/group/namespace, Joomla DI/event integration, lazy adapters for serve/describe/dispatch/self-test/cli-inventory, local-only checks and shared typed component runtime resolution are present. The output guard isolates protocol framing from Joomla diagnostics. Installer checks, initial enablement with update-state preservation, languages/update/changelog metadata and reproducible PHP ZIP building exist.
-## Verification
+The component now supplies ConsoleRuntimeInterface/ConsoleRuntimeProviderInterface and the runtime composition; older statements that those interfaces have not yet been written are superseded. Complete combined installation and live command execution still need verification.
-CI results must be inspected for the current commit. The end-to-end Joomla runtime is tested with the coordinated component implementation; package tests alone are not proof of runtime parity or production readiness. The component's `ConsoleRuntimeInterface` and `ConsoleRuntimeProviderInterface` are required before these commands can execute.
+## Current scope update
+
+External Composer-client/remote-stdio ownership is exclusively in `joomengine/mcp_client`; the server and plugin do not depend on it. Full JCB API/CLI support is now mandatory in README, AGENTS and architecture, with concrete plugin tasks in JCB-INTEGRATION.md linked to the component's pinned roadmap/inventory.
+
+This update documents the extension work; it does **not** implement new JCB handlers, active target rows or compiler/package job execution. Installed JCB command registration and actual API routes still need exact inventories before executable bindings are enabled.
+
+## Evidence and remaining work
+
+The inspected baseline's CI passed PHP 8.3/8.4 syntax and manifest/reproducible-package contracts. Consult the current PR head's checks for this documentation update. Neither those tests nor a CLI inventory proves installed Joomla/JCB execution.
+
+Finish all plugin responsibilities in JCB-INTEGRATION.md and the component's complete installed-server acceptance: actual core/JCB operations, options/dependencies, stdio output/exit behaviour, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Record exact source/runtime versions and evidence before marking this PR ready. Client interoperability and independent release work are tracked in mcp_client.
diff --git a/docs/JCB-INTEGRATION.md b/docs/JCB-INTEGRATION.md
new file mode 100644
index 0000000..2dce219
--- /dev/null
+++ b/docs/JCB-INTEGRATION.md
@@ -0,0 +1,42 @@
+# Required JCB console integration
+
+## Canonical scope
+
+The authoritative cross-repository roadmap and planning inventory live in the component:
+
+- https://github.com/joomengine/mcp_component/blob/feature/jcb-mcp-runtime/docs/integrations/JCB.md
+- https://github.com/joomengine/mcp_component/blob/feature/jcb-mcp-runtime/docs/integrations/jcb-surface.json
+
+JCB source is pinned to `extension-builder/joomla@5ee658dd07eb749dca43ed4722f6cca7eb8208cf`; CLI documentation to `joomengine/jcb-documentation@ecd3670232d344295fc4f673b2d3dc40a64b3bf6`, english/CLI-Command-Suite.md. Full JCB support is mandatory alongside Joomla core. This page assigns plugin work; it is not evidence that JCB execution is already implemented.
+
+## Native registration and invocation
+
+JCB's package identifies a separate `ComponentBuilderCommands` console plugin. Keep its command ownership intact. Capture the installed registry after registration: exact names, aliases, InputDefinitions, implementation identity, arguments, defaults and option modes. The documented pattern is componentbuilder::, with componentbuilder:compile:component explicitly documented, but the 45-entity factory map does not prove that every verb/area combination is registered. Additional actual commands are also in scope.
+
+Let the component's JCB handler/provider and database targets select reviewed command objects or native services. Do not add a plugin hard-coded duplicate list, arbitrary class resolver or generic process/shell endpoint. Command discovery alone does not establish execution parity. Preserve unrelated Joomla/core commands when JCB is missing or disabled.
+
+## Families and semantics to preserve
+
+Cover compiler and every actually registered get/init/pull/push/reset package command. Package get synchronizes definitions/dependencies and can write; it is not a normal read-only entity getter. Init/pull/reset have their own initialization/overwrite/tracking/dependency semantics. Push publishes configured remote definition graphs synchronously and can have partial external effects despite a command returning normally.
+
+Retain GUID/identifier validation, CSV/newline/JSON, native local @file/--items-file forms, repository/force/resolve options where supported, global/environment fallbacks and exact defaults. Do not silently discard an option that needs an explicitly designed HTTP counterpart. Never forward a workstation token or URL as authority to run unrestricted local operations.
+
+Compiler parity includes selectors, options bundles, backup/local repository export, placeholders/debug/minify/powers/power repository, target Joomla 3/4/5/6, indentation/build dates, and compile-install. Host Joomla compatibility and output target compatibility differ. Omission preserves native GLOBAL behaviour; freeze the reviewed effective options/environment and reject stale plans.
+
+## Output, jobs and state isolation
+
+The compiler deliberately separates machine paths on stdout from human diagnostics on stderr. Preserve that distinction, all native exit codes and per-component results; do not let either stream corrupt MCP framing. Verify output/archive existence and hashes through the component's artifact service. Redact credential-bearing repository messages and sensitive paths for remote job consumers.
+
+JCB factories, message buses and dependency queues contain mutable operation state. Resolve related services from the same factory, reset them correctly or use isolated workers, and restore Joomla identity/input after invocation. Test consecutive requests for state leakage.
+
+Long work uses the component-owned durable execution/job/lease/artifact protocol. A client disconnect is not a rollback or permission to replay. Cancellation/restart/expired leases need honest partial/uncertain outcomes. HTTP-originated jobs retain the requester's Joomla ACL and scoped consent; local worker execution must not turn them into server-owner jobs. No blanket HTTP-to-CLI privilege bridge is allowed.
+
+## Plugin implementation and acceptance tasks
+
+1. Extend installed registry inspection/availability checks and typed invocation contracts for JCB without shadowing its plugin.
+2. Connect shared component JCB bindings/jobs to the existing lazy MCP command runtime, preserving original core behaviours and namespace identities.
+3. Add command-level tests for registration order, options/environment, framing/EOF/bounds, nonzero exits, no-op/partial results and identity/factory restoration.
+4. Run real JCB package operations against temporary repositories and compiler/install cases against disposable Joomla, with persisted read-back, artifacts and cleanup. Cover JCB absent/disabled/version mismatch and preserve core functionality.
+5. Verify cross-principal job/artifact isolation, grants/revocation, concurrent commands, cancellation and recovery; keep exact executed source/version evidence in IMPLEMENTATION.md.
+
+The external `joomengine/mcp-client` only discovers and consumes the server contract. This plugin must never depend on its client package or duplicate its remote bridge.
From 71778709bfe927b611046513caf305709b4027a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Mon, 21 Sep 2026 16:31:23 +0200
Subject: [PATCH 07/17] Preserve native console lifecycle and verify typed
adapters
---
src/Extension/JoomEngineMcpPlugin.php | 54 +++++---
tests/native.php | 177 ++++++++++++++++++++++++++
tests/prepare-native.sh | 12 ++
tests/run.php | 2 +-
4 files changed, 227 insertions(+), 18 deletions(-)
create mode 100644 tests/native.php
create mode 100644 tests/prepare-native.sh
diff --git a/src/Extension/JoomEngineMcpPlugin.php b/src/Extension/JoomEngineMcpPlugin.php
index 6116d70..7b0ac51 100644
--- a/src/Extension/JoomEngineMcpPlugin.php
+++ b/src/Extension/JoomEngineMcpPlugin.php
@@ -13,8 +13,10 @@
use Joomla\CMS\Application\ConsoleApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\Console\ConsoleEvents;
use Joomla\Event\SubscriberInterface;
use RuntimeException;
+use Symfony\Component\Console\Exception\ExceptionInterface;
use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeInterface;
use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeProviderInterface;
use VDM\Plugin\Console\JoomEngineMcp\Console\McpCommand;
@@ -39,7 +41,8 @@ final class JoomEngineMcpPlugin extends CMSPlugin implements SubscriberInterface
*/
public static function getSubscribedEvents(): array
{
- return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands', ApplicationEvents::AFTER_EXECUTE => 'restoreOutput'];
+ return [ApplicationEvents::BEFORE_EXECUTE => 'registerCommands',
+ ApplicationEvents::AFTER_EXECUTE => 'restoreOutput', ConsoleEvents::APPLICATION_ERROR => 'restoreOutput'];
}
/**
@@ -58,14 +61,6 @@ public function registerCommands(): void
return;
}
- $selected = $application->getConsoleInput()->getFirstArgument();
-
- if (is_string($selected) && str_starts_with($selected, 'joomla:mcp:')
- && !$application->getConsoleInput()->hasParameterOption(['--help', '-h', '--version', '-V']))
- {
- $this->outputGuard ??= new OutputGuard($application);
- }
-
$resolve = static function () use ($application): ConsoleRuntimeInterface
{
if (!ComponentHelper::isEnabled('com_joomengine_mcp'))
@@ -83,21 +78,46 @@ public function registerCommands(): void
return $component->getConsoleRuntime($application);
};
- foreach (['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'] as $operation)
+ $operations = ['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'];
+
+ // Validate the complete namespace before changing the registry or formatter.
+ foreach ($operations as $operation)
{
$name = 'joomla:mcp:' . $operation;
- if ($application->hasCommand($name))
+ if ($application->hasCommand($name) && !$application->getCommand($name) instanceof McpCommand)
{
- if ($application->getCommand($name) instanceof McpCommand)
- {
- continue;
- }
-
throw new RuntimeException('Refusing to replace an existing Joomla MCP console command.');
}
+ }
+
+ foreach ($operations as $operation)
+ {
+ if (!$application->hasCommand('joomla:mcp:' . $operation))
+ {
+ $application->addCommand(new McpCommand($operation, $resolve));
+ }
+ }
+
+ $input = $application->getConsoleInput();
+
+ try
+ {
+ // As in Joomla's dispatcher, bind global options before determining the
+ // command. Otherwise --live-site URL may be mistaken for its name.
+ $input->bind($application->getDefinition());
+ }
+ catch (ExceptionInterface)
+ {
+ // Command-specific options are validated by the actual command later.
+ }
+
+ $selected = $input->getFirstArgument();
- $application->addCommand(new McpCommand($operation, $resolve));
+ if (is_string($selected) && str_starts_with($selected, 'joomla:mcp:')
+ && !$input->hasParameterOption(['--help', '-h', '--version', '-V'], true))
+ {
+ $this->outputGuard ??= new OutputGuard($application);
}
}
diff --git a/tests/native.php b/tests/native.php
new file mode 100644
index 0000000..bb308b3
--- /dev/null
+++ b/tests/native.php
@@ -0,0 +1,177 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+use Joomla\Application\ApplicationEvents;
+use Joomla\CMS\Application\ConsoleApplication;
+use Joomla\CMS\Language\Language;
+use Joomla\Console\Command\AbstractCommand;
+use Joomla\Console\ConsoleEvents;
+use Joomla\DI\Container;
+use Joomla\Event\Dispatcher;
+use Joomla\Event\Event;
+use Joomla\Registry\Registry;
+use Symfony\Component\Console\Input\ArgvInput;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Output\StreamOutput;
+use VDM\Component\JoomEngineMcp\Administrator\Contract\ConsoleRuntimeInterface;
+use VDM\Plugin\Console\JoomEngineMcp\Console\McpCommand;
+use VDM\Plugin\Console\JoomEngineMcp\Extension\JoomEngineMcpPlugin;
+
+$joomla = realpath((string) getenv('JOOMLA_ROOT'));
+$component = realpath((string) getenv('MCP_COMPONENT_SOURCE'));
+
+if (PHP_SAPI !== 'cli' || $joomla === false || $component === false
+ || !is_file($joomla . '/libraries/vendor/autoload.php')
+ || !is_file($component . '/admin/src/Contract/ConsoleRuntimeInterface.php'))
+{
+ throw new RuntimeException('Set JOOMLA_ROOT to the full Joomla distribution and MCP_COMPONENT_SOURCE to its matching component checkout.');
+}
+
+define('_JEXEC', 1);
+define('JPATH_BASE', $joomla);
+require $joomla . '/includes/defines.php';
+require $joomla . '/libraries/bootstrap.php';
+require $component . '/admin/src/Contract/ConsoleRuntimeInterface.php';
+require dirname(__DIR__) . '/src/Console/McpCommand.php';
+require dirname(__DIR__) . '/src/Console/OutputGuard.php';
+require dirname(__DIR__) . '/src/Extension/JoomEngineMcpPlugin.php';
+
+/** Native console behaviour without adding the database-backed core commands. */
+final class FixtureConsole extends ConsoleApplication
+{
+ /** @inheritDoc */
+ protected function getDefaultCommands(): array
+ {
+ return [];
+ }
+}
+
+$checks = 0;
+$check = static function (bool $condition, string $message) use (&$checks): void
+{
+ if (!$condition)
+ {
+ throw new RuntimeException($message);
+ }
+
+ $checks++;
+ echo 'PASS ' . $message . PHP_EOL;
+};
+$make = static function (array $arguments): array
+{
+ $output = new ConsoleOutput(OutputInterface::VERBOSITY_VERBOSE, false);
+ $output->setErrorOutput(new StreamOutput(fopen('php://memory', 'w+'), OutputInterface::VERBOSITY_VERBOSE));
+ $dispatcher = new Dispatcher();
+ $app = new FixtureConsole(new Registry(), $dispatcher, new Container(), new Language('en-GB'), new ArgvInput($arguments), $output);
+ $plugin = new JoomEngineMcpPlugin(['name' => 'joomengine_mcp', 'type' => 'console']);
+ $plugin->setApplication($app);
+ $dispatcher->addSubscriber($plugin);
+
+ return [$app, $plugin, $output, $dispatcher];
+};
+
+foreach ([['joomla.php', 'joomla:mcp:serve'], ['joomla.php', '--live-site', 'https://example.test', 'joomla:mcp:serve']] as $arguments)
+{
+ [$app, $plugin, $output, $dispatcher] = $make($arguments);
+ $previousErrors = ini_get('display_errors');
+ $previousStderr = $output->getErrorOutput();
+ $dispatcher->dispatch(ApplicationEvents::BEFORE_EXECUTE, new Event(ApplicationEvents::BEFORE_EXECUTE));
+ $check($output->isQuiet() && ini_get('display_errors') === 'stderr', 'MCP entry isolates formatter and PHP diagnostics with global options');
+ $first = $app->getCommand('joomla:mcp:serve');
+ $plugin->registerCommands();
+ $check(count($app->getAllCommands()) === 5 && $first === $app->getCommand('joomla:mcp:serve'), 'Repeated registration is idempotent without booting the component');
+ $dispatcher->dispatch(ConsoleEvents::APPLICATION_ERROR, new Event(ConsoleEvents::APPLICATION_ERROR));
+ $check($output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE && $output->getErrorOutput() === $previousStderr
+ && ini_get('display_errors') === $previousErrors, 'Native error events restore the exact formatter and PHP diagnostic state');
+ $plugin->restoreOutput();
+ $check($output->getErrorOutput() === $previousStderr, 'Repeated output restoration is harmless');
+}
+
+foreach ([['joomla.php', 'list'], ['joomla.php', 'joomla:mcp:serve', '--help'], ['joomla.php', 'joomla:mcp:serve', '--version']] as $arguments)
+{
+ [$app, $plugin, $output] = $make($arguments);
+ $plugin->registerCommands();
+ $check($output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE, 'Core commands, help and version retain normal output');
+}
+
+[$app, $plugin, $output] = $make(['joomla.php', 'joomla:mcp:serve']);
+$conflicting = new class('joomla:mcp:cli-inventory') extends AbstractCommand
+{
+ /** @inheritDoc */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ return 0;
+ }
+};
+$app->addCommand($conflicting);
+$rejected = false;
+
+try
+{
+ $plugin->registerCommands();
+}
+catch (RuntimeException)
+{
+ $rejected = true;
+}
+
+$check($rejected && count($app->getAllCommands()) === 1 && $app->getCommand('joomla:mcp:cli-inventory') === $conflicting
+ && $output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE, 'Registration conflicts preserve the existing registry and output state');
+
+$runtime = new class implements ConsoleRuntimeInterface
+{
+ /** @var array Recorded typed calls. */
+ public array $calls = [];
+
+ /** @inheritDoc */
+ public function serveStdio(): int
+ {
+ $this->calls[] = ['serve'];
+
+ return 23;
+ }
+
+ /** @inheritDoc */
+ public function executeCommand(string $operation, InputInterface $input, OutputInterface $output): int
+ {
+ $this->calls[] = [$operation, $input, $output];
+
+ return 17;
+ }
+};
+[$app, $plugin, $output] = $make(['joomla.php', 'list']);
+$resolved = 0;
+$resolve = static function () use ($runtime, &$resolved): ConsoleRuntimeInterface
+{
+ $resolved++;
+
+ return $runtime;
+};
+
+foreach (['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'] as $operation)
+{
+ $command = new McpCommand($operation, $resolve);
+ $app->addCommand($command);
+ $check($resolved === count($runtime->calls), 'Constructing adapters never resolves component runtime eagerly');
+ $input = new ArrayInput($operation === 'dispatch' ? ['--input' => '-', '--format' => 'ndjson'] : []);
+ $status = $command->execute($input, $output);
+ $call = $runtime->calls[array_key_last($runtime->calls)];
+ $check($status === ($operation === 'serve' ? 23 : 17) && $call[0] === $operation, 'Adapters preserve selected operation and nonzero native status');
+
+ if ($operation !== 'serve')
+ {
+ $check($call[1] === $input && $call[2] === $output && $input->getOption('format') === ($operation === 'dispatch' ? 'ndjson' : 'json'),
+ 'Typed native input and output reach the shared runtime without reconstruction');
+ }
+}
+
+echo json_encode(['checks' => $checks, 'joomla' => JVERSION, 'nativeConsoleClasses' => true, 'installedRuntime' => false], JSON_THROW_ON_ERROR) . PHP_EOL;
diff --git a/tests/prepare-native.sh b/tests/prepare-native.sh
new file mode 100644
index 0000000..416919a
--- /dev/null
+++ b/tests/prepare-native.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+: "${JOOMLA_ROOT:?Set an empty directory for the pinned Joomla distribution}"
+[[ ! -e "$JOOMLA_ROOT" ]]
+work="$(mktemp -d)"
+trap 'rm -rf -- "$work"' EXIT
+curl --fail --location --proto '=https' --tlsv1.2 --retry 3 --connect-timeout 30 --max-time 180 \
+ https://github.com/joomla/joomla-cms/releases/download/6.1.3/Joomla_6.1.3-Stable-Full_Package.tar.gz \
+ --output "$work/joomla.tar.gz"
+printf '%s %s\n' '184f8c582cde5981693de7c28547c6e834c48c50cb377c7b8421bbfd33bbdf6f' "$work/joomla.tar.gz" | sha256sum --check
+mkdir -p -- "$JOOMLA_ROOT"
+tar --no-same-owner -xzf "$work/joomla.tar.gz" -C "$JOOMLA_ROOT"
diff --git a/tests/run.php b/tests/run.php
index 030e0ea..81dfb20 100644
--- a/tests/run.php
+++ b/tests/run.php
@@ -64,4 +64,4 @@
}
$zip->close();
-echo json_encode(['manifest' => 'passed', 'languages' => 'passed', 'package' => 'passed', 'reproducible' => true, 'liveJoomla' => 'covered by the component integration workflow'], JSON_PRETTY_PRINT) . PHP_EOL;
+echo json_encode(['manifest' => 'passed', 'languages' => 'passed', 'package' => 'passed', 'reproducible' => true, 'installedRuntime' => 'Run tests/installed.php separately.'], JSON_PRETTY_PRINT) . PHP_EOL;
From c0b02e6ad901e7fc7b2299c662243cb11e0bf3d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Mon, 21 Sep 2026 16:31:31 +0200
Subject: [PATCH 08/17] Exercise installed plugin command and MCP stdio
entrypoints
---
.github/workflows/installed.yml | 81 +++++++++++++
tests/installed.php | 196 ++++++++++++++++++++++++++++++++
2 files changed, 277 insertions(+)
create mode 100644 .github/workflows/installed.yml
create mode 100644 tests/installed.php
diff --git a/.github/workflows/installed.yml b/.github/workflows/installed.yml
new file mode 100644
index 0000000..038a034
--- /dev/null
+++ b/.github/workflows/installed.yml
@@ -0,0 +1,81 @@
+name: Installed Joomla console plugin
+
+on:
+ workflow_call:
+ inputs:
+ component_ref:
+ type: string
+ default: main
+ pull_request:
+ push:
+ branches: [main, feature/jcb-mcp-runtime]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: plugin-installed-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ installed:
+ runs-on: ubuntu-latest
+ timeout-minutes: 25
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['8.3', '8.4']
+ services:
+ mysql:
+ image: mysql:8.4
+ env:
+ MYSQL_DATABASE: mcp_fixture
+ MYSQL_USER: mcp_test
+ MYSQL_PASSWORD: disposable-test-password
+ MYSQL_ROOT_PASSWORD: disposable-root-password
+ ports: ['3306:3306']
+ options: >-
+ --health-cmd "mysqladmin ping -h localhost -pdisposable-root-password"
+ --health-interval 5s --health-timeout 5s --health-retries 15
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ path: plugin
+ persist-credentials: false
+ - uses: actions/checkout@v7
+ with:
+ repository: joomengine/mcp_component
+ ref: ${{ inputs.component_ref || 'feature/jcb-mcp-runtime' }}
+ path: component
+ persist-credentials: false
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: curl, dom, fileinfo, intl, json, mbstring, mysqli, pdo_mysql, simplexml, sodium, xml, zip
+ tools: composer:v2
+ coverage: none
+ - name: Build the matching component
+ working-directory: component
+ run: bash tools/build.sh
+ - name: Install and exercise this plugin checkout through native Joomla CLI
+ working-directory: component
+ env:
+ MCP_TEST_ALLOW_DESTRUCTIVE: '1'
+ JOOMLA_ROOT: ${{ runner.temp }}/joomla
+ MCP_PLUGIN_SOURCE: ${{ github.workspace }}/plugin
+ MCP_TEST_DB_TYPE: mysqli
+ MCP_TEST_DB_HOST: 127.0.0.1:3306
+ MCP_TEST_DB_USER: mcp_test
+ MCP_TEST_DB_PASS: disposable-test-password
+ MCP_TEST_DB_NAME: mcp_fixture
+ run: |
+ bash tests/integration/run.sh
+ test -s build/evidence/live-console-plugin.log
+ - name: Preserve installed console evidence
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: installed-console-php-${{ matrix.php }}
+ path: component/build/evidence/
+ if-no-files-found: error
+ retention-days: 7
diff --git a/tests/installed.php b/tests/installed.php
new file mode 100644
index 0000000..d40ec89
--- /dev/null
+++ b/tests/installed.php
@@ -0,0 +1,196 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+use Joomla\Database\DatabaseInterface;
+use Mcp\Client;
+use Mcp\Client\Transport\StdioTransport;
+
+$component = realpath((string) getenv('MCP_COMPONENT_SOURCE'));
+
+if ($component === false || !is_file($component . '/tests/integration/bootstrap.php'))
+{
+ throw new RuntimeException('Set MCP_COMPONENT_SOURCE to the matching component checkout.');
+}
+
+// This bootstrap requires explicit destructive-test consent and a fixture marker.
+require $component . '/tests/integration/bootstrap.php';
+$app->bootComponent('com_joomengine_mcp');
+$db = $container->get(DatabaseInterface::class);
+$arguments = [];
+
+if (is_string(php_ini_loaded_file()))
+{
+ $arguments = ['-c', php_ini_loaded_file()];
+}
+
+$arguments = array_merge($arguments, ['-d', 'extension_dir=' . ini_get('extension_dir'), '-d', 'display_errors=stderr', JPATH_ROOT . '/cli/joomla.php']);
+$checks = 0;
+$check = static function (bool $condition, string $message) use (&$checks): void
+{
+ if (!$condition)
+ {
+ throw new RuntimeException($message);
+ }
+
+ $checks++;
+ echo 'PASS ' . $message . PHP_EOL;
+};
+
+/** Run the real installed CLI with bounded pipes, a deadline and no shell. */
+$run = static function (array $command, string $input = '') use ($arguments): array
+{
+ $process = proc_open(array_merge([PHP_BINARY], $arguments, $command),
+ [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, JPATH_ROOT, null, ['bypass_shell' => true]);
+
+ if (!is_resource($process))
+ {
+ throw new RuntimeException('Cannot start the installed Joomla console.');
+ }
+
+ foreach ($pipes as $pipe)
+ {
+ stream_set_blocking($pipe, false);
+ }
+
+ $stdout = '';
+ $stderr = '';
+ $offset = 0;
+ $deadline = hrtime(true) + 60000000000;
+
+ try
+ {
+ while (true)
+ {
+ if (hrtime(true) >= $deadline)
+ {
+ throw new RuntimeException('Installed console command exceeded its test deadline.');
+ }
+
+ if (isset($pipes[0]))
+ {
+ if ($offset < strlen($input))
+ {
+ $written = fwrite($pipes[0], substr($input, $offset, 8192));
+
+ if ($written === false)
+ {
+ throw new RuntimeException('Installed console input pipe failed.');
+ }
+
+ $offset += $written;
+ }
+
+ if ($offset === strlen($input))
+ {
+ fclose($pipes[0]);
+ unset($pipes[0]);
+ }
+ }
+
+ $stdout .= stream_get_contents($pipes[1]);
+ $stderr .= stream_get_contents($pipes[2]);
+
+ if (strlen($stdout) + strlen($stderr) > 16777216)
+ {
+ throw new RuntimeException('Installed console output exceeded its test bound.');
+ }
+
+ $status = proc_get_status($process);
+
+ if (!$status['running'])
+ {
+ return [(int) $status['exitcode'], $stdout . stream_get_contents($pipes[1]), $stderr . stream_get_contents($pipes[2])];
+ }
+
+ usleep(10000);
+ }
+ }
+ finally
+ {
+ if (proc_get_status($process)['running'])
+ {
+ proc_terminate($process, 9);
+ }
+
+ foreach ($pipes as $pipe)
+ {
+ fclose($pipe);
+ }
+
+ proc_close($process);
+ }
+};
+$decode = static fn (string $text): array => json_decode(trim($text), true, 128, JSON_THROW_ON_ERROR);
+$request = json_encode(['protocol' => 'joomla-mcp/1', 'id' => 'system', 'action' => 'system.info', 'input' => (object) []], JSON_THROW_ON_ERROR);
+
+foreach (['describe', 'self-test', 'cli-inventory'] as $operation)
+{
+ [$status, $stdout] = $run(['joomla:mcp:' . $operation, '--no-ansi', '--no-interaction']);
+ $result = $decode($stdout);
+ $check($status === 0 && ($result['protocol'] ?? '') === 'joomla-mcp/1' && ($result['ok'] ?? false),
+ 'Installed ' . $operation . ' returns one clean native protocol response');
+}
+
+[$status, $stdout] = $run(['--live-site', 'https://example.test', 'joomla:mcp:dispatch', '--input=-'], $request);
+$result = $decode($stdout);
+$check($status === 0 && ($result['id'] ?? '') === 'system' && ($result['result']['joomlaVersion'] ?? '') === JVERSION,
+ 'Installed dispatch preserves global options and executes the actual Joomla handler');
+
+[$status, $stdout] = $run(['joomla:mcp:dispatch', '--format=ndjson'], "{invalid}\n" . $request . "\n");
+$lines = array_map($decode, explode("\n", trim($stdout)));
+$check($status !== 0 && count($lines) === 2 && !$lines[0]['ok'] && $lines[1]['ok'] && $lines[1]['id'] === 'system',
+ 'NDJSON retains failed-frame status and continues with the next valid request');
+
+foreach ([['joomla:mcp:dispatch', '--format=xml'], ['joomla:mcp:dispatch', '--input=unsupported']] as $command)
+{
+ [$status, $stdout] = $run($command, $request);
+ $result = $decode($stdout);
+ $check($status !== 0 && !$result['ok'], 'Unsupported framing or input source fails with a structured response');
+}
+
+[$status, $stdout] = $run(['joomla:mcp:dispatch']);
+$check($status !== 0 && !$decode($stdout)['ok'], 'EOF without a JSON request fails promptly');
+[$status, $stdout] = $run(['joomla:mcp:dispatch'], str_repeat(' ', 1048577));
+$check($status !== 0 && ($decode($stdout)['error']['code'] ?? '') === 'REQUEST_TOO_LARGE', 'Oversized JSON is rejected at the native input bound');
+
+$client = Client::builder()->setClientInfo('installed-plugin-fixture', '1.0.0')->setInitTimeout(15)->setRequestTimeout(30)->setMaxRetries(0)->build();
+
+try
+{
+ $client->connect(new StdioTransport(PHP_BINARY, array_merge($arguments, ['joomla:mcp:serve']), JPATH_ROOT, maxBufferSize: 16777216));
+ $names = array_map(static fn ($tool): string => $tool->name, $client->listTools()->tools);
+ $check(in_array('joomla_action_read', $names, true), 'Installed plugin serve completes MCP handshake and discovery');
+ $result = $client->callTool('joomla_action_read', ['action' => 'system.info', 'transport' => 'cli']);
+ $content = json_decode(json_encode($result->structuredContent, JSON_THROW_ON_ERROR), true, 128, JSON_THROW_ON_ERROR);
+ $check(!$result->isError && ($content['response']['data']['joomlaVersion'] ?? '') === JVERSION, 'Installed plugin serve executes the component-owned native action');
+ $client->ping();
+ $check($client->isConnected(), 'Native diagnostics leave subsequent protocol frames usable');
+}
+finally
+{
+ $client->disconnect();
+}
+
+$where = $db->quoteName('type') . ' = ' . $db->quote('component') . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_joomengine_mcp');
+$previous = (int) $db->setQuery('SELECT enabled FROM ' . $db->quoteName('#__extensions') . ' WHERE ' . $where)->loadResult();
+
+try
+{
+ $db->setQuery('UPDATE ' . $db->quoteName('#__extensions') . ' SET enabled = 0 WHERE ' . $where)->execute();
+ [$status, $stdout] = $run(['joomla:mcp:describe']);
+ $check($status !== 0 && !$decode($stdout)['ok'], 'Unavailable component fails only the affected MCP command');
+ [$status] = $run(['list', '--no-ansi', '--no-interaction']);
+ $check($status === 0, 'Unrelated Joomla commands remain available with the component disabled');
+}
+finally
+{
+ $db->setQuery('UPDATE ' . $db->quoteName('#__extensions') . ' SET enabled = ' . $previous . ' WHERE ' . $where)->execute();
+}
+
+echo json_encode(['checks' => $checks, 'joomla' => JVERSION, 'database' => $db->getServerType(), 'actualPluginEntrypoints' => true], JSON_THROW_ON_ERROR) . PHP_EOL;
From 51b855acc353b3e3e2eb1c060a3e1905e88febaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Mon, 21 Sep 2026 16:31:36 +0200
Subject: [PATCH 09/17] Gate console releases on installed tests and verified
update assets
---
.github/workflows/ci.yml | 23 ++++++-
.github/workflows/release.yml | 74 ++++++++++++++++++++
CHANGELOG.md | 6 ++
README.md | 8 ++-
docs/IMPLEMENTATION.md | 18 +++--
tests/release.php | 102 +++++++++++++++++++++++++++
tools/update-feed.php | 125 ++++++++++++++++++++++++++++++++++
7 files changed, 346 insertions(+), 10 deletions(-)
create mode 100644 .github/workflows/release.yml
create mode 100644 tests/release.php
create mode 100644 tools/update-feed.php
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c48a0da..1bcff2a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,16 +27,33 @@ jobs:
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- extensions: dom, simplexml, zip
+ extensions: dom, intl, mbstring, simplexml, zip
coverage: none
- name: PHP syntax
run: find . -type f -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l
- name: Manifest and reproducible package contracts
- run: php tests/run.php
+ run: |
+ php tests/run.php
+ php tests/release.php
+ - uses: actions/checkout@v7
+ with:
+ repository: joomengine/mcp_component
+ ref: feature/jcb-mcp-runtime
+ path: build/component-contract
+ persist-credentials: false
+ - name: Native Joomla console registration and runtime contracts
+ env:
+ JOOMLA_ROOT: ${{ runner.temp }}/native-joomla
+ MCP_COMPONENT_SOURCE: ${{ github.workspace }}/build/component-contract
+ run: |
+ bash tests/prepare-native.sh
+ php tests/native.php
- uses: actions/upload-artifact@v7
if: matrix.php == '8.3'
with:
name: console-plugin-development-package
- path: build/
+ path: |
+ build/plg_console_joomengine_mcp-*.zip
+ build/plg_console_joomengine_mcp-*.zip.sha256
if-no-files-found: error
retention-days: 7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..4f75dda
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,74 @@
+name: Publish console plugin release
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: console-plugin-release
+ cancel-in-progress: false
+
+jobs:
+ installed:
+ if: github.ref == 'refs/heads/main'
+ uses: ./.github/workflows/installed.yml
+ with:
+ component_ref: main
+ publish:
+ needs: installed
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: dom, simplexml, zip
+ coverage: none
+ - name: Validate immutable version and rebuild package
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ php tests/run.php
+ php tests/release.php
+ version="$(php -r 'echo (string) simplexml_load_file("joomengine_mcp.xml")->version;')"
+ [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
+ tag="v$version"
+ git fetch origin main --tags
+ [[ "$(git rev-parse origin/main)" == "$GITHUB_SHA" ]]
+ if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
+ echo 'This immutable version already exists; bump the manifest in a reviewed change.' >&2
+ exit 1
+ fi
+ printf 'PLUGIN_VERSION=%s\nPLUGIN_TAG=%s\n' "$version" "$tag" >> "$GITHUB_ENV"
+ - name: Publish verified versioned archive and checksum
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ archive="build/plg_console_joomengine_mcp-$PLUGIN_VERSION.zip"
+ gh release create "$PLUGIN_TAG" "$archive" "$archive.sha256" \
+ --target "$GITHUB_SHA" --title "JoomEngine MCP console $PLUGIN_VERSION" \
+ --notes-file CHANGELOG.md --draft
+ gh release edit "$PLUGIN_TAG" --draft=false
+ mkdir -p build/published
+ gh release download "$PLUGIN_TAG" --dir build/published --pattern '*.zip' --pattern '*.sha256'
+ cmp "$archive" "build/published/$(basename "$archive")"
+ gh api "repos/$GITHUB_REPOSITORY/releases/tags/$PLUGIN_TAG" > build/published/release.json
+ php tools/update-feed.php build/published/release.json "build/published/$(basename "$archive")"
+ - name: Commit feed only after publication succeeds
+ run: |
+ set -euo pipefail
+ git config user.name 'github-actions[bot]'
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+ git add joomengine_mcp_update_server.xml
+ git commit -m "Publish console plugin $PLUGIN_VERSION update metadata"
+ git push origin HEAD:main
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7373c1..7ea4825 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
## Unreleased
+- Preserve native global options, atomic command registration and output restoration after console errors.
+- Verify native Joomla console contracts and actual installed plugin command/stdio entrypoints.
+- Add explicit main-only release publication with verified versioned archives, checksums and post-publication update metadata.
+
+## Unreleased
+
- Establish the exact joomengine_mcp console plugin identity, local-server authority and shared component contract.
- Add native plugin/provider/lazy command adapters, output isolation, installer checks, languages/update metadata and PHP-only reproducible packaging.
- Separate external Composer-client/remote-bridge ownership into `joomengine/mcp_client`; no server/plugin dependency on that package.
diff --git a/README.md b/README.md
index d36542b..c9d1d0c 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,14 @@ Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). Existing CLI i
## Status and local authority
-Continue `feature/jcb-mcp-runtime`, draft PR #1. The plugin manifest/provider/lazy command adapters/output guard/installer and PHP-only package builder exist, with PHP 8.3/8.4 packaging contracts. Complete installed Joomla/JCB execution and combined component/package acceptance are still pending. See [implementation status](docs/IMPLEMENTATION.md).
+Continue `feature/jcb-mcp-runtime`, draft PR #1. The native provider, lazy command adapters, output guard, installer and PHP-only package builder are implemented. Native Joomla console tests cover registration, global options, typed runtime delegation and output restoration; the installed workflow builds the matching component and tests this plugin checkout through the actual Joomla CLI. Complete JCB execution acceptance is coordinated with the component. See [implementation status](docs/IMPLEMENTATION.md) for actual evidence.
Local execution uses the genuine Joomla console application under CLI SAPI, without a Joomla API token or row-viewing-level restriction. Input validation, explicit action semantics, grants/plans, bounded output, audit, verification and recovery still apply. HTTP requests and database values cannot manufacture this local privilege.
Original migration source: `joomengine/joomla-mcp@2cff50f4f6b440da3c684f9995a77efad32e1a36`, especially companion/plugin. Preserve licences and all supported request/result/command behaviours. The source repository is unchanged.
+
+## Verification and release
+
+Run `php tests/run.php` and `php tests/release.php` for packaging and publication metadata checks. With a full Joomla distribution in `JOOMLA_ROOT` and the component checkout in `MCP_COMPONENT_SOURCE`, run `php tests/native.php` for actual Joomla class contracts. Installed acceptance requires the component's disposable fixture and `MCP_PLUGIN_SOURCE` pointing to this checkout; its runner installs the plugin and executes `tests/installed.php` before teardown.
+
+Release publication is an explicit manual workflow on `main`, after merge and review. It runs installed acceptance against the component's `main`, refuses an existing version tag, publishes the versioned archive and checksum, downloads and verifies those assets, then commits the update feed. The feed remains empty until an archive is published. The component owns combined server package assembly.
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index d878274..58a5e53 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -1,23 +1,29 @@
-# Implementation status — 18 September 2026
+# Implementation status — 21 September 2026
## Branch
-Continue `feature/jcb-mcp-runtime`, draft PR #1. The implementation baseline inspected for this documentation update is `fb6a72a515859d198b47acb6fdac932e5dae92ab`; do not replace the existing runtime branch.
+Continue `feature/jcb-mcp-runtime`, draft PR #1. Preserve this branch and its component-owned shared runtime contract.
## Implemented runtime
Exact plugin element/group/namespace, Joomla DI/event integration, lazy adapters for serve/describe/dispatch/self-test/cli-inventory, local-only checks and shared typed component runtime resolution are present. The output guard isolates protocol framing from Joomla diagnostics. Installer checks, initial enablement with update-state preservation, languages/update/changelog metadata and reproducible PHP ZIP building exist.
-The component now supplies ConsoleRuntimeInterface/ConsoleRuntimeProviderInterface and the runtime composition; older statements that those interfaces have not yet been written are superseded. Complete combined installation and live command execution still need verification.
+The component supplies ConsoleRuntimeInterface/ConsoleRuntimeProviderInterface and the runtime composition. The plugin forwards native input/output objects and exact exit status; it contains no JCB catalogue or business handlers. Registration checks all MCP names before mutation, binds native global options before selecting protocol output protection, and restores formatter state after successful execution and native application errors.
+
+The component's existing installed stdio suite exercises its shared runtime directly. This repository now also has an installed workflow which installs this plugin checkout and tests `cli/joomla.php joomla:mcp:*`, including the real `serve` adapter. These are distinct evidence layers.
## Current scope update
External Composer-client/remote-stdio ownership is exclusively in `joomengine/mcp_client`; the server and plugin do not depend on it. Full JCB API/CLI support is now mandatory in README, AGENTS and architecture, with concrete plugin tasks in JCB-INTEGRATION.md linked to the component's pinned roadmap/inventory.
-This update documents the extension work; it does **not** implement new JCB handlers, active target rows or compiler/package job execution. Installed JCB command registration and actual API routes still need exact inventories before executable bindings are enabled.
+JCB handlers, reviewed command/API bindings, input freezing, jobs and artifacts belong to the component. The plugin consumes them through its existing shared runtime. Its console adapters do not re-register JCB's native commands or depend on the external client. The canonical JCB acceptance matrix remains docs/JCB-INTEGRATION.md and the component roadmap.
## Evidence and remaining work
-The inspected baseline's CI passed PHP 8.3/8.4 syntax and manifest/reproducible-package contracts. Consult the current PR head's checks for this documentation update. Neither those tests nor a CLI inventory proves installed Joomla/JCB execution.
+Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package checks; 26 assertions using genuine Joomla 6.1.3 console/plugin/input/output classes; and five release metadata assertions. The native class suite verifies idempotent/atomic registration, global-option handling, help/core output preservation, error restoration, lazy runtime resolution, native input forwarding and nonzero statuses. It uses a substitutable shared-runtime implementation and is not an installed JCB execution test.
+
+The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. CI runs this against PHP 8.3/8.4 and MySQL using the component's disposable installation and upgrade/teardown fixture. Record actual CI outcomes before treating this newly added suite as passed.
+
+Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.
-Finish all plugin responsibilities in JCB-INTEGRATION.md and the component's complete installed-server acceptance: actual core/JCB operations, options/dependencies, stdio output/exit behaviour, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Record exact source/runtime versions and evidence before marking this PR ready. Client interoperability and independent release work are tracked in mcp_client.
+Complete coordinated installed JCB operations through the shared runtime, including native options/dependencies, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Confirm current installed workflow results and record exact component/JCB/plugin sources before marking this PR ready. Client interoperability is tracked in mcp_client.
diff --git a/tests/release.php b/tests/release.php
new file mode 100644
index 0000000..8ca6fc9
--- /dev/null
+++ b/tests/release.php
@@ -0,0 +1,102 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+$root = dirname(__DIR__);
+$version = (string) simplexml_load_file($root . '/joomengine_mcp.xml')->version;
+$name = 'plg_console_joomengine_mcp-' . $version . '.zip';
+$directory = sys_get_temp_dir() . '/mcp-plugin-release-' . bin2hex(random_bytes(8));
+mkdir($directory . '/tools', 0700, true);
+$checks = 0;
+$check = static function (bool $condition, string $message) use (&$checks): void
+{
+ if (!$condition)
+ {
+ throw new RuntimeException($message);
+ }
+
+ $checks++;
+ echo 'PASS ' . $message . PHP_EOL;
+};
+$run = static function (string $directory, string $name): bool
+{
+ $argv = [$directory . '/tools/update-feed.php', $directory . '/release.json', $directory . '/' . $name];
+ ob_start();
+
+ try
+ {
+ require $argv[0];
+
+ return true;
+ }
+ catch (RuntimeException)
+ {
+ return false;
+ }
+ finally
+ {
+ ob_end_clean();
+ }
+};
+
+try
+{
+ copy($root . '/tools/update-feed.php', $directory . '/tools/update-feed.php');
+ copy($root . '/joomengine_mcp.xml', $directory . '/joomengine_mcp.xml');
+ copy($root . '/joomengine_mcp_update_server.xml', $directory . '/joomengine_mcp_update_server.xml');
+ copy($root . '/build/' . $name, $directory . '/' . $name);
+ copy($root . '/build/' . $name . '.sha256', $directory . '/' . $name . '.sha256');
+ $tag = 'v' . $version;
+ $base = 'https://github.com/joomengine/mcp_plugin/releases/';
+ $release = ['tag_name' => $tag, 'draft' => true, 'prerelease' => false, 'published_at' => '2026-09-21T00:00:00Z',
+ 'html_url' => $base . 'tag/' . $tag, 'assets' => []];
+
+ foreach ([$name, $name . '.sha256'] as $asset)
+ {
+ $release['assets'][] = ['name' => $asset, 'state' => 'uploaded', 'size' => filesize($directory . '/' . $asset),
+ 'browser_download_url' => $base . 'download/' . $tag . '/' . $asset];
+ }
+
+ $write = static fn () => file_put_contents($directory . '/release.json', json_encode($release, JSON_THROW_ON_ERROR));
+ $before = file_get_contents($directory . '/joomengine_mcp_update_server.xml');
+ $write();
+ $check(!$run($directory, $name) && file_get_contents($directory . '/joomengine_mcp_update_server.xml') === $before,
+ 'Unpublished releases cannot advertise an update');
+ $release['draft'] = false;
+ file_put_contents($directory . '/release.json', json_encode($release, JSON_THROW_ON_ERROR));
+ $check($run($directory, $name), 'Published matching release generates an update');
+ $feed = simplexml_load_file($directory . '/joomengine_mcp_update_server.xml');
+ $entries = $feed->xpath('update[version="' . $version . '"]');
+ $check(count($entries) === 1 && (string) $entries[0]->sha256 === hash_file('sha256', $directory . '/' . $name)
+ && (string) $entries[0]->downloads->downloadurl === $release['assets'][0]['browser_download_url'],
+ 'Published feed binds the correct archive URL, version and checksum');
+ $before = file_get_contents($directory . '/joomengine_mcp_update_server.xml');
+ $check($run($directory, $name) && file_get_contents($directory . '/joomengine_mcp_update_server.xml') === $before,
+ 'Repeating verified metadata generation is idempotent');
+ file_put_contents($directory . '/' . $name . '.sha256', str_repeat('0', 64) . ' ' . $name . "\n");
+ $check(!$run($directory, $name) && file_get_contents($directory . '/joomengine_mcp_update_server.xml') === $before,
+ 'Mismatched downloaded checksums leave the published feed unchanged');
+}
+finally
+{
+ foreach (glob($directory . '/tools/*') as $file)
+ {
+ unlink($file);
+ }
+
+ rmdir($directory . '/tools');
+
+ foreach (glob($directory . '/*') as $file)
+ {
+ unlink($file);
+ }
+
+ rmdir($directory);
+}
+
+echo json_encode(['checks' => $checks, 'releaseMetadata' => 'passed'], JSON_THROW_ON_ERROR) . PHP_EOL;
diff --git a/tools/update-feed.php b/tools/update-feed.php
new file mode 100644
index 0000000..d3076d6
--- /dev/null
+++ b/tools/update-feed.php
@@ -0,0 +1,125 @@
+
+ * @copyright Copyright (C) 2026 Vast Development Method. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+/** Generate Joomla update metadata only for an already published verified release. */
+$root = dirname(__DIR__);
+$manifest = simplexml_load_file($root . '/joomengine_mcp.xml');
+$version = (string) $manifest->version;
+$metadata = $argv[1] ?? '';
+$archive = $argv[2] ?? '';
+
+if (PHP_SAPI !== 'cli' || !is_file($metadata) || !is_file($archive)
+ || preg_match('/\A\d+\.\d+\.\d+\z/D', $version) !== 1)
+{
+ throw new RuntimeException('Supply published GitHub release JSON and its downloaded plugin ZIP for a stable manifest version.');
+}
+
+$release = json_decode(file_get_contents($metadata), true, 64, JSON_THROW_ON_ERROR);
+$tag = 'v' . $version;
+$filename = 'plg_console_joomengine_mcp-' . $version . '.zip';
+$base = 'https://github.com/joomengine/mcp_plugin/releases/';
+$url = $base . 'download/' . $tag . '/' . $filename;
+
+if (($release['tag_name'] ?? '') !== $tag || ($release['draft'] ?? true) || ($release['prerelease'] ?? true)
+ || ($release['html_url'] ?? '') !== $base . 'tag/' . $tag || empty($release['published_at']) || basename($archive) !== $filename)
+{
+ throw new RuntimeException('Update feeds require the matching published stable GitHub release.');
+}
+
+$assets = [];
+
+foreach ($release['assets'] ?? [] as $asset)
+{
+ $assets[$asset['name']] = $asset;
+}
+
+foreach ([$filename, $filename . '.sha256'] as $asset)
+{
+ if (($assets[$asset]['state'] ?? '') !== 'uploaded'
+ || ($assets[$asset]['browser_download_url'] ?? '') !== $base . 'download/' . $tag . '/' . $asset)
+ {
+ throw new RuntimeException('A release archive or checksum has not been published at its immutable version URL.');
+ }
+}
+
+$checksum = hash_file('sha256', $archive);
+$expected = is_file($archive . '.sha256') ? trim(file_get_contents($archive . '.sha256')) : '';
+
+if (!hash_equals($checksum . ' ' . $filename, $expected) || (int) ($assets[$filename]['size'] ?? -1) !== filesize($archive))
+{
+ throw new RuntimeException('The downloaded release archive does not match its published checksum or asset size.');
+}
+
+$zip = new ZipArchive();
+
+if ($zip->open($archive) !== true)
+{
+ throw new RuntimeException('The published archive is not a ZIP.');
+}
+
+$packaged = simplexml_load_string((string) $zip->getFromName('joomengine_mcp.xml'));
+$zip->close();
+
+if ($packaged === false || (string) $packaged->version !== $version
+ || (string) $packaged['group'] !== 'console' || (string) $packaged->namespace !== (string) $manifest->namespace)
+{
+ throw new RuntimeException('The published archive has a different extension identity or version.');
+}
+
+$document = new DOMDocument('1.0', 'utf-8');
+$document->preserveWhiteSpace = false;
+$document->formatOutput = true;
+
+if (!$document->load($root . '/joomengine_mcp_update_server.xml', LIBXML_NONET) || $document->documentElement->nodeName !== 'updates')
+{
+ throw new RuntimeException('The existing update feed is invalid.');
+}
+
+$query = new DOMXPath($document);
+
+foreach ($query->query('/updates/update[version="' . $version . '"]') as $old)
+{
+ $old->parentNode->removeChild($old);
+}
+
+$update = $document->createElement('update');
+$append = static function (DOMNode $parent, string $name, string $value) use ($document): DOMElement
+{
+ $element = $document->createElement($name);
+ $element->appendChild($document->createTextNode($value));
+ $parent->appendChild($element);
+
+ return $element;
+};
+$append($update, 'name', 'JoomEngine MCP Console');
+$append($update, 'description', 'Local Joomla console integration for JoomEngine MCP.');
+$append($update, 'element', 'joomengine_mcp');
+$append($update, 'type', 'plugin');
+$append($update, 'folder', 'console');
+$append($update, 'version', $version);
+$downloads = $document->createElement('downloads');
+$update->appendChild($downloads);
+$download = $append($downloads, 'downloadurl', $url);
+$download->setAttribute('type', 'full');
+$download->setAttribute('format', 'zip');
+$append($update, 'sha256', $checksum);
+$append($update, 'tags', '')->appendChild($document->createElement('tag', 'stable'));
+$target = $append($update, 'targetplatform', '');
+$target->setAttribute('name', 'joomla');
+$target->setAttribute('version', '6\\.[1-9][0-9]*');
+$append($update, 'php_minimum', '8.3.0');
+$append($update, 'detailsurl', $release['html_url']);
+$document->documentElement->appendChild($update);
+
+if ($document->save($root . '/joomengine_mcp_update_server.xml') === false)
+{
+ throw new RuntimeException('Cannot save verified release metadata.');
+}
+
+echo 'Published update metadata for ' . $tag . PHP_EOL;
From b2ee0aea1270fbf1937899ae5206c8374a5675fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Mon, 21 Sep 2026 16:36:22 +0200
Subject: [PATCH 10/17] Expose explicit local JCB catalogue synchronization and
record acceptance
---
README.md | 4 +++-
docs/ARCHITECTURE.md | 2 +-
docs/IMPLEMENTATION.md | 15 +++++++++++----
src/Console/McpCommand.php | 2 +-
src/Extension/JoomEngineMcpPlugin.php | 2 +-
tests/native.php | 4 ++--
6 files changed, 19 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index c9d1d0c..7442bd8 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ PHP-only local Joomla console integration for `com_joomengine_mcp`.
**Extension:** `plg_console_joomengine_mcp`
**Namespace:** `VDM\Plugin\Console\JoomEngineMcp`
-The plugin connects Joomla's console lifecycle to the component-owned database catalogue and execution engine. It provides the `joomla:mcp:serve`, `describe`, `dispatch`, `self-test` and `cli-inventory` adapters. It does not contain a second MCP catalogue or an HTTP webservices plugin.
+The plugin connects Joomla's console lifecycle to the component-owned database catalogue and execution engine. It provides the `joomla:mcp:serve`, `describe`, `dispatch`, `self-test`, `cli-inventory` and `jcb-sync` adapters. It does not contain a second MCP catalogue or an HTTP webservices plugin.
## Three repository boundaries
@@ -23,6 +23,8 @@ The server and this plugin must support **all actual Joomla Component Builder AP
Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). Existing CLI inventory or a list of command names is not proof that those operations execute. JCB handlers and database bindings remain component-owned; do not copy JCB's compiler or register duplicate `componentbuilder:*` commands here. The JCB integration is required roadmap work, not already completed by this documentation change.
+After installing or upgrading JCB, the server owner runs `php cli/joomla.php joomla:mcp:jcb-sync` to synchronize reviewed installed JCB definitions through the component. The plugin only forwards this explicit local operation; schema discovery, identity validation and persisted catalogue updates remain component-owned.
+
## Status and local authority
Continue `feature/jcb-mcp-runtime`, draft PR #1. The native provider, lazy command adapters, output guard, installer and PHP-only package builder are implemented. Native Joomla console tests cover registration, global options, typed runtime delegation and output restoration; the installed workflow builds the matching component and tests this plugin checkout through the actual Joomla CLI. Complete JCB execution acceptance is coordinated with the component. See [implementation status](docs/IMPLEMENTATION.md) for actual evidence.
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index e790625..2033af0 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -8,7 +8,7 @@ The plugin adapts the real Joomla console to the shared `com_joomengine_mcp` run
## Local execution and wire framing
-The native Joomla console lifecycle registers lazy `joomla:mcp:serve`, `describe`, `dispatch`, `self-test` and `cli-inventory` adapters. Invocation verifies the real console application/CLI SAPI and resolves the component's typed ConsoleRuntimeProviderInterface/ConsoleRuntimeInterface. Missing component dependencies produce a command failure without eagerly breaking unrelated commands.
+The native Joomla console lifecycle registers lazy `joomla:mcp:serve`, `describe`, `dispatch`, `self-test`, `cli-inventory` and `jcb-sync` adapters. Invocation verifies the real console application/CLI SAPI and resolves the component's typed ConsoleRuntimeProviderInterface/ConsoleRuntimeInterface. Missing component dependencies produce a command failure without eagerly breaking unrelated commands.
Local server ownership is the requested authority boundary: no API token or row viewing permission is needed, but schema validation, explicit effects/grants/plans, verification and recovery remain. The authority cannot be requested from remote JSON, database content or tokens. A remote stdio-to-HTTP client is a different product and remains API-ACL-restricted.
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index 58a5e53..c6a1d57 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -6,7 +6,7 @@ Continue `feature/jcb-mcp-runtime`, draft PR #1. Preserve this branch and its co
## Implemented runtime
-Exact plugin element/group/namespace, Joomla DI/event integration, lazy adapters for serve/describe/dispatch/self-test/cli-inventory, local-only checks and shared typed component runtime resolution are present. The output guard isolates protocol framing from Joomla diagnostics. Installer checks, initial enablement with update-state preservation, languages/update/changelog metadata and reproducible PHP ZIP building exist.
+Exact plugin element/group/namespace, Joomla DI/event integration, lazy adapters for serve/describe/dispatch/self-test/cli-inventory/jcb-sync, local-only checks and shared typed component runtime resolution are present. The output guard isolates protocol framing from Joomla diagnostics. Installer checks, initial enablement with update-state preservation, languages/update/changelog metadata and reproducible PHP ZIP building exist.
The component supplies ConsoleRuntimeInterface/ConsoleRuntimeProviderInterface and the runtime composition. The plugin forwards native input/output objects and exact exit status; it contains no JCB catalogue or business handlers. Registration checks all MCP names before mutation, binds native global options before selecting protocol output protection, and restores formatter state after successful execution and native application errors.
@@ -20,10 +20,17 @@ JCB handlers, reviewed command/API bindings, input freezing, jobs and artifacts
## Evidence and remaining work
-Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package checks; 26 assertions using genuine Joomla 6.1.3 console/plugin/input/output classes; and five release metadata assertions. The native class suite verifies idempotent/atomic registration, global-option handling, help/core output preservation, error restoration, lazy runtime resolution, native input forwarding and nonzero statuses. It uses a substitutable shared-runtime implementation and is not an installed JCB execution test.
+Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package checks; 29 assertions using genuine Joomla 6.1.3 console/plugin/input/output classes; and five release metadata assertions. The native class suite verifies idempotent/atomic registration, global-option handling, help/core output preservation, error restoration, lazy runtime resolution, native input forwarding and nonzero statuses. It uses a substitutable shared-runtime implementation and is not an installed JCB execution test.
-The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. CI runs this against PHP 8.3/8.4 and MySQL using the component's disposable installation and upgrade/teardown fixture. Record actual CI outcomes before treating this newly added suite as passed.
+The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. All 14 installed assertions passed on Joomla 6.1.3 / MySQL with PHP 8.3 and 8.4, together with the component's existing installation, administrator, HTTP, native CRUD, upgrade and uninstall suites.
+
+Evidence for plugin source `51b855acc353b3e3e2eb1c060a3e1905e88febaf`, paired with component source `b3a75714eeadea35fbed102e4b5ba3ce021334cc`:
+
+- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35612756830): success on PHP 8.3 and 8.4.
+- [Actual installed console plugin CI](https://github.com/joomengine/mcp_plugin/actions/runs/35612756511): success on PHP 8.3 and 8.4.
+
+These installed fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence.
Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.
-Complete coordinated installed JCB operations through the shared runtime, including native options/dependencies, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Confirm current installed workflow results and record exact component/JCB/plugin sources before marking this PR ready. Client interoperability is tracked in mcp_client.
+Complete coordinated installed JCB operations through the shared runtime, including native options/dependencies, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Record exact component/JCB/plugin sources and the golden-image results before marking this PR ready. Client interoperability is tracked in mcp_client.
diff --git a/src/Console/McpCommand.php b/src/Console/McpCommand.php
index 2d21efb..a66ec45 100644
--- a/src/Console/McpCommand.php
+++ b/src/Console/McpCommand.php
@@ -53,7 +53,7 @@ final class McpCommand extends AbstractCommand
*/
public function __construct(string $operation, callable $resolveRuntime)
{
- if (!in_array($operation, ['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'], true))
+ if (!in_array($operation, ['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory', 'jcb-sync'], true))
{
throw new InvalidArgumentException('Unknown Joomla MCP console operation.');
}
diff --git a/src/Extension/JoomEngineMcpPlugin.php b/src/Extension/JoomEngineMcpPlugin.php
index 7b0ac51..2570a25 100644
--- a/src/Extension/JoomEngineMcpPlugin.php
+++ b/src/Extension/JoomEngineMcpPlugin.php
@@ -78,7 +78,7 @@ public function registerCommands(): void
return $component->getConsoleRuntime($application);
};
- $operations = ['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'];
+ $operations = ['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory', 'jcb-sync'];
// Validate the complete namespace before changing the registry or formatter.
foreach ($operations as $operation)
diff --git a/tests/native.php b/tests/native.php
index bb308b3..997a0fb 100644
--- a/tests/native.php
+++ b/tests/native.php
@@ -88,7 +88,7 @@ protected function getDefaultCommands(): array
$check($output->isQuiet() && ini_get('display_errors') === 'stderr', 'MCP entry isolates formatter and PHP diagnostics with global options');
$first = $app->getCommand('joomla:mcp:serve');
$plugin->registerCommands();
- $check(count($app->getAllCommands()) === 5 && $first === $app->getCommand('joomla:mcp:serve'), 'Repeated registration is idempotent without booting the component');
+ $check(count($app->getAllCommands()) === 6 && $first === $app->getCommand('joomla:mcp:serve'), 'Repeated registration is idempotent without booting the component');
$dispatcher->dispatch(ConsoleEvents::APPLICATION_ERROR, new Event(ConsoleEvents::APPLICATION_ERROR));
$check($output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE && $output->getErrorOutput() === $previousStderr
&& ini_get('display_errors') === $previousErrors, 'Native error events restore the exact formatter and PHP diagnostic state');
@@ -157,7 +157,7 @@ public function executeCommand(string $operation, InputInterface $input, OutputI
return $runtime;
};
-foreach (['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory'] as $operation)
+foreach (['serve', 'describe', 'dispatch', 'self-test', 'cli-inventory', 'jcb-sync'] as $operation)
{
$command = new McpCommand($operation, $resolve);
$app->addCommand($command);
From cce3d3d0d510e306097f0c4c2ad091fdac3a0813 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Mon, 21 Sep 2026 16:41:09 +0200
Subject: [PATCH 11/17] Require the JCB-capable component runtime and normalize
archive modes
---
README.md | 2 ++
build.php | 3 ++-
docs/IMPLEMENTATION.md | 2 ++
src/Installer/InstallerScript.php | 4 ++--
tests/run.php | 8 ++++++++
5 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 7442bd8..6af6a9f 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
PHP-only local Joomla console integration for `com_joomengine_mcp`.
+Requires the built component version **0.1.1 or later in the same major version**, including its explicit JCB synchronization runtime.
+
**Element:** `joomengine_mcp`
**Group:** `console`
**Extension:** `plg_console_joomengine_mcp`
diff --git a/build.php b/build.php
index 2ae9bd1..39c4115 100644
--- a/build.php
+++ b/build.php
@@ -60,7 +60,8 @@
foreach ($files as $file)
{
- if (!$zip->addFile($root . '/' . $file, $file) || !$zip->setMtimeName($file, $mtime))
+ if (!$zip->addFile($root . '/' . $file, $file) || !$zip->setMtimeName($file, $mtime)
+ || !$zip->setExternalAttributesName($file, ZipArchive::OPSYS_UNIX, 0100644 << 16))
{
throw new RuntimeException('Cannot add a file to the plugin archive.');
}
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index c6a1d57..c0d8ec6 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -4,6 +4,8 @@
Continue `feature/jcb-mcp-runtime`, draft PR #1. Preserve this branch and its component-owned shared runtime contract.
+Plugin version 0.1.0 requires component version 0.1.1 or later within the same major version, because the explicit JCB synchronization operation is part of that runtime contract.
+
## Implemented runtime
Exact plugin element/group/namespace, Joomla DI/event integration, lazy adapters for serve/describe/dispatch/self-test/cli-inventory/jcb-sync, local-only checks and shared typed component runtime resolution are present. The output guard isolates protocol framing from Joomla diagnostics. Installer checks, initial enablement with update-state preservation, languages/update/changelog metadata and reproducible PHP ZIP building exist.
diff --git a/src/Installer/InstallerScript.php b/src/Installer/InstallerScript.php
index f86530f..6fc70fb 100644
--- a/src/Installer/InstallerScript.php
+++ b/src/Installer/InstallerScript.php
@@ -63,11 +63,11 @@ public function preflight(string $type, InstallerAdapter $adapter): bool
$componentVersion = (string) ($manifest['version'] ?? '0');
$pluginVersion = (string) $adapter->getManifest()->version;
- if ($row === null || (int) $row['enabled'] !== 1 || version_compare($componentVersion, '0.1.0', '<')
+ if ($row === null || (int) $row['enabled'] !== 1 || version_compare($componentVersion, '0.1.1', '<')
|| explode('.', $componentVersion)[0] !== explode('.', $pluginVersion)[0]
|| !is_file(JPATH_ADMINISTRATOR . '/components/com_joomengine_mcp/vendor/autoload.php'))
{
- throw new RuntimeException('Install and enable the compatible built JoomEngine MCP component before its console plugin.');
+ throw new RuntimeException('Install and enable the built JoomEngine MCP component version 0.1.1 or later in the same major version before its console plugin.');
}
return true;
diff --git a/tests/run.php b/tests/run.php
index 81dfb20..f867a3a 100644
--- a/tests/run.php
+++ b/tests/run.php
@@ -56,6 +56,14 @@
for ($index = 0; $index < $zip->numFiles; $index++)
{
$name = $zip->getNameIndex($index);
+ $system = 0;
+ $attributes = 0;
+
+ if (!$zip->getExternalAttributesIndex($index, $system, $attributes)
+ || $system !== ZipArchive::OPSYS_UNIX || ($attributes >> 16) !== 0100644)
+ {
+ throw new RuntimeException('The plugin archive does not normalize source file permissions.');
+ }
if (str_starts_with($name, '/') || str_contains($name, '..') || str_starts_with($name, 'tests/') || str_ends_with($name, '.ts'))
{
From 873c46742d1ee95d661779e73649fcedaf069ff4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Mon, 21 Sep 2026 16:49:11 +0200
Subject: [PATCH 12/17] Build the locked distribution for installed package
lifecycle acceptance
---
.github/workflows/installed.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/installed.yml b/.github/workflows/installed.yml
index 038a034..1c3b5ae 100644
--- a/.github/workflows/installed.yml
+++ b/.github/workflows/installed.yml
@@ -56,7 +56,9 @@ jobs:
coverage: none
- name: Build the matching component
working-directory: component
- run: bash tools/build.sh
+ run: |
+ bash tools/build.sh
+ bash tools/build-distribution.sh
- name: Install and exercise this plugin checkout through native Joomla CLI
working-directory: component
env:
From fcfa91a026fedae9d785bad4447eb0855ddee1cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Tue, 22 Sep 2026 18:11:33 +0200
Subject: [PATCH 13/17] Keep post-merge installed checks aligned and refresh
acceptance evidence
---
.github/workflows/installed.yml | 2 +-
README.md | 2 +-
docs/IMPLEMENTATION.md | 13 +++++++------
docs/JCB-INTEGRATION.md | 12 +++++-------
4 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/installed.yml b/.github/workflows/installed.yml
index 1c3b5ae..6cbf19f 100644
--- a/.github/workflows/installed.yml
+++ b/.github/workflows/installed.yml
@@ -45,7 +45,7 @@ jobs:
- uses: actions/checkout@v7
with:
repository: joomengine/mcp_component
- ref: ${{ inputs.component_ref || 'feature/jcb-mcp-runtime' }}
+ ref: ${{ inputs.component_ref || (github.ref_name == 'main' && 'main' || 'feature/jcb-mcp-runtime') }}
path: component
persist-credentials: false
- uses: shivammathur/setup-php@v2
diff --git a/README.md b/README.md
index 6af6a9f..09fe8ae 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Direct local server stdio is not the remote bridge. A client talking over HTTP r
The server and this plugin must support **all actual Joomla Component Builder API and registered CLI capabilities**, alongside Joomla core. For this plugin that includes correct discovery/invocation of JCB's compiler and package get/init/pull/push/reset commands, all registered entity/area variants, native options, output/exit semantics and long-operation handling through the shared component.
-Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). Existing CLI inventory or a list of command names is not proof that those operations execute. JCB handlers and database bindings remain component-owned; do not copy JCB's compiler or register duplicate `componentbuilder:*` commands here. The JCB integration is required roadmap work, not already completed by this documentation change.
+Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). JCB handlers, database synchronization and durable jobs are implemented in the component and consumed by this plugin. Complete installed compiler/package/job acceptance is still being verified there; command inventory alone is not proof of successful execution. The plugin does not copy JCB's compiler or register duplicate `componentbuilder:*` commands.
After installing or upgrading JCB, the server owner runs `php cli/joomla.php joomla:mcp:jcb-sync` to synchronize reviewed installed JCB definitions through the component. The plugin only forwards this explicit local operation; schema discovery, identity validation and persisted catalogue updates remain component-owned.
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index c0d8ec6..d5452de 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -1,4 +1,4 @@
-# Implementation status — 21 September 2026
+# Implementation status — 22 September 2026
## Branch
@@ -16,7 +16,7 @@ The component's existing installed stdio suite exercises its shared runtime dire
## Current scope update
-External Composer-client/remote-stdio ownership is exclusively in `joomengine/mcp_client`; the server and plugin do not depend on it. Full JCB API/CLI support is now mandatory in README, AGENTS and architecture, with concrete plugin tasks in JCB-INTEGRATION.md linked to the component's pinned roadmap/inventory.
+External Composer-client/remote-stdio ownership is exclusively in `joomengine/mcp_client`; the server and plugin do not depend on it. Full JCB API/CLI support remains mandatory, with the plugin boundary documented in JCB-INTEGRATION.md and actual source/execution evidence maintained by the component.
JCB handlers, reviewed command/API bindings, input freezing, jobs and artifacts belong to the component. The plugin consumes them through its existing shared runtime. Its console adapters do not re-register JCB's native commands or depend on the external client. The canonical JCB acceptance matrix remains docs/JCB-INTEGRATION.md and the component roadmap.
@@ -26,13 +26,14 @@ Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package che
The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. All 14 installed assertions passed on Joomla 6.1.3 / MySQL with PHP 8.3 and 8.4, together with the component's existing installation, administrator, HTTP, native CRUD, upgrade and uninstall suites.
-Evidence for plugin source `51b855acc353b3e3e2eb1c060a3e1905e88febaf`, paired with component source `b3a75714eeadea35fbed102e4b5ba3ce021334cc`:
+Verified plugin source `873c46742d1ee95d661779e73649fcedaf069ff4`:
-- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35612756830): success on PHP 8.3 and 8.4.
-- [Actual installed console plugin CI](https://github.com/joomengine/mcp_plugin/actions/runs/35612756511): success on PHP 8.3 and 8.4.
+- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35614736871): passed on PHP 8.3 and 8.4.
+- [Coordinated installed client CI](https://github.com/joomengine/mcp_client/actions/runs/35614914117): passed on PHP 8.3 and 8.4, including all 14 actual plugin-entrypoint assertions and complete component/package lifecycle. Its source log records this plugin and component `080e189aec094b5c1f883926498623ef1f16099d`.
+- [This repository's earlier installed run](https://github.com/joomengine/mcp_plugin/actions/runs/35614736847) failed a component package-ownership assertion before that component correction. It must be superseded by passing current-head installed checks; the coordinated run already passed the corrected lifecycle.
These installed fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence.
-Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.
+Ordinary installed CI pairs the feature branches before merge and uses the component's `main` for plugin `main`. Reusable callers can select an explicit component revision. Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.
Complete coordinated installed JCB operations through the shared runtime, including native options/dependencies, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Record exact component/JCB/plugin sources and the golden-image results before marking this PR ready. Client interoperability is tracked in mcp_client.
diff --git a/docs/JCB-INTEGRATION.md b/docs/JCB-INTEGRATION.md
index 2dce219..0917a38 100644
--- a/docs/JCB-INTEGRATION.md
+++ b/docs/JCB-INTEGRATION.md
@@ -7,7 +7,7 @@ The authoritative cross-repository roadmap and planning inventory live in the co
- https://github.com/joomengine/mcp_component/blob/feature/jcb-mcp-runtime/docs/integrations/JCB.md
- https://github.com/joomengine/mcp_component/blob/feature/jcb-mcp-runtime/docs/integrations/jcb-surface.json
-JCB source is pinned to `extension-builder/joomla@5ee658dd07eb749dca43ed4722f6cca7eb8208cf`; CLI documentation to `joomengine/jcb-documentation@ecd3670232d344295fc4f673b2d3dc40a64b3bf6`, english/CLI-Command-Suite.md. Full JCB support is mandatory alongside Joomla core. This page assigns plugin work; it is not evidence that JCB execution is already implemented.
+JCB source is pinned to `extension-builder/joomla@5ee658dd07eb749dca43ed4722f6cca7eb8208cf`; CLI documentation to `joomengine/jcb-documentation@ecd3670232d344295fc4f673b2d3dc40a64b3bf6`, english/CLI-Command-Suite.md. Full JCB support is mandatory alongside Joomla core. The shared component implementation now provides installed discovery, planning and job execution; final compiler/package/job acceptance remains in progress. This page defines the plugin contract, while IMPLEMENTATION.md records verified evidence.
## Native registration and invocation
@@ -31,12 +31,10 @@ JCB factories, message buses and dependency queues contain mutable operation sta
Long work uses the component-owned durable execution/job/lease/artifact protocol. A client disconnect is not a rollback or permission to replay. Cancellation/restart/expired leases need honest partial/uncertain outcomes. HTTP-originated jobs retain the requester's Joomla ACL and scoped consent; local worker execution must not turn them into server-owner jobs. No blanket HTTP-to-CLI privilege bridge is allowed.
-## Plugin implementation and acceptance tasks
+## Plugin implementation and acceptance
-1. Extend installed registry inspection/availability checks and typed invocation contracts for JCB without shadowing its plugin.
-2. Connect shared component JCB bindings/jobs to the existing lazy MCP command runtime, preserving original core behaviours and namespace identities.
-3. Add command-level tests for registration order, options/environment, framing/EOF/bounds, nonzero exits, no-op/partial results and identity/factory restoration.
-4. Run real JCB package operations against temporary repositories and compiler/install cases against disposable Joomla, with persisted read-back, artifacts and cleanup. Cover JCB absent/disabled/version mismatch and preserve core functionality.
-5. Verify cross-principal job/artifact isolation, grants/revocation, concurrent commands, cancellation and recovery; keep exact executed source/version evidence in IMPLEMENTATION.md.
+The implemented lazy adapters forward native input/output and exit status to the component without shadowing JCB's command plugin. Native and installed tests cover registration order, global options, framing/EOF/bounds, nonzero exits and output restoration; the explicit `jcb-sync` entrypoint delegates inventory and persistence to the component.
+
+Before coordinated readiness, complete real package/compiler workflows in disposable Joomla, with persisted read-back, artifacts, cleanup and the original options/environment semantics. The component matrix must also prove missing/disabled JCB behavior, dependency handling, state isolation, principal/grant boundaries, concurrent jobs, cancellation and recovery. Record exact component/JCB/plugin revisions and successful final results in IMPLEMENTATION.md.
The external `joomengine/mcp-client` only discovers and consumes the server contract. This plugin must never depend on its client package or duplicate its remote bridge.
From e9a97f2b0871058522670b28485ff19f68102b1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Tue, 22 Sep 2026 18:37:55 +0200
Subject: [PATCH 14/17] Clarify implemented scope and link revision-bound
acceptance evidence
---
README.md | 4 ++--
docs/IMPLEMENTATION.md | 13 ++++++-------
docs/JCB-INTEGRATION.md | 4 ++--
3 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 09fe8ae..8d1e9d2 100644
--- a/README.md
+++ b/README.md
@@ -23,13 +23,13 @@ Direct local server stdio is not the remote bridge. A client talking over HTTP r
The server and this plugin must support **all actual Joomla Component Builder API and registered CLI capabilities**, alongside Joomla core. For this plugin that includes correct discovery/invocation of JCB's compiler and package get/init/pull/push/reset commands, all registered entity/area variants, native options, output/exit semantics and long-operation handling through the shared component.
-Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). JCB handlers, database synchronization and durable jobs are implemented in the component and consumed by this plugin. Complete installed compiler/package/job acceptance is still being verified there; command inventory alone is not proof of successful execution. The plugin does not copy JCB's compiler or register duplicate `componentbuilder:*` commands.
+Read [JCB integration responsibilities](docs/JCB-INTEGRATION.md). JCB handlers, database synchronization and durable jobs are implemented in the component and consumed by this plugin. The [component acceptance checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349) records installed compiler/package/job results and native limitations; command inventory alone is not proof of successful execution. The plugin does not copy JCB's compiler or register duplicate `componentbuilder:*` commands.
After installing or upgrading JCB, the server owner runs `php cli/joomla.php joomla:mcp:jcb-sync` to synchronize reviewed installed JCB definitions through the component. The plugin only forwards this explicit local operation; schema discovery, identity validation and persisted catalogue updates remain component-owned.
## Status and local authority
-Continue `feature/jcb-mcp-runtime`, draft PR #1. The native provider, lazy command adapters, output guard, installer and PHP-only package builder are implemented. Native Joomla console tests cover registration, global options, typed runtime delegation and output restoration; the installed workflow builds the matching component and tests this plugin checkout through the actual Joomla CLI. Complete JCB execution acceptance is coordinated with the component. See [implementation status](docs/IMPLEMENTATION.md) for actual evidence.
+Implementation is on `feature/jcb-mcp-runtime` / [PR #1](https://github.com/joomengine/mcp_plugin/pull/1). The native provider, lazy command adapters, output guard, installer and PHP-only package builder are implemented. Native Joomla console tests cover registration, global options, typed runtime delegation and output restoration; installed workflows exercise this checkout through the actual Joomla CLI and the shared JCB runtime. The PR records current check results and review status; [implementation evidence](docs/IMPLEMENTATION.md) describes the verification layers.
Local execution uses the genuine Joomla console application under CLI SAPI, without a Joomla API token or row-viewing-level restriction. Input validation, explicit action semantics, grants/plans, bounded output, audit, verification and recovery still apply. HTTP requests and database values cannot manufacture this local privilege.
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index d5452de..17a6cf7 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -2,7 +2,7 @@
## Branch
-Continue `feature/jcb-mcp-runtime`, draft PR #1. Preserve this branch and its component-owned shared runtime contract.
+Implementation is on `feature/jcb-mcp-runtime` / [PR #1](https://github.com/joomengine/mcp_plugin/pull/1). The PR records current checks and review status; the [component acceptance checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349) tracks coordinated Joomla/JCB execution evidence.
Plugin version 0.1.0 requires component version 0.1.1 or later within the same major version, because the explicit JCB synchronization operation is part of that runtime contract.
@@ -20,20 +20,19 @@ External Composer-client/remote-stdio ownership is exclusively in `joomengine/mc
JCB handlers, reviewed command/API bindings, input freezing, jobs and artifacts belong to the component. The plugin consumes them through its existing shared runtime. Its console adapters do not re-register JCB's native commands or depend on the external client. The canonical JCB acceptance matrix remains docs/JCB-INTEGRATION.md and the component roadmap.
-## Evidence and remaining work
+## Verification layers
Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package checks; 29 assertions using genuine Joomla 6.1.3 console/plugin/input/output classes; and five release metadata assertions. The native class suite verifies idempotent/atomic registration, global-option handling, help/core output preservation, error restoration, lazy runtime resolution, native input forwarding and nonzero statuses. It uses a substitutable shared-runtime implementation and is not an installed JCB execution test.
The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. All 14 installed assertions passed on Joomla 6.1.3 / MySQL with PHP 8.3 and 8.4, together with the component's existing installation, administrator, HTTP, native CRUD, upgrade and uninstall suites.
-Verified plugin source `873c46742d1ee95d661779e73649fcedaf069ff4`:
+Historical passing baseline, plugin source `fcfa91a026fedae9d785bad4447eb0855ddee1cf`:
-- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35614736871): passed on PHP 8.3 and 8.4.
-- [Coordinated installed client CI](https://github.com/joomengine/mcp_client/actions/runs/35614914117): passed on PHP 8.3 and 8.4, including all 14 actual plugin-entrypoint assertions and complete component/package lifecycle. Its source log records this plugin and component `080e189aec094b5c1f883926498623ef1f16099d`.
-- [This repository's earlier installed run](https://github.com/joomengine/mcp_plugin/actions/runs/35614736847) failed a component package-ownership assertion before that component correction. It must be superseded by passing current-head installed checks; the coordinated run already passed the corrected lifecycle.
+- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35752470759): passed on PHP 8.3 and 8.4.
+- [Installed console plugin CI](https://github.com/joomengine/mcp_plugin/actions/runs/35752470754): passed on PHP 8.3 and 8.4, including actual plugin entrypoints and the component/package lifecycle. Exact checked-out revisions are retained in that run's logs.
These installed fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence.
Ordinary installed CI pairs the feature branches before merge and uses the component's `main` for plugin `main`. Reusable callers can select an explicit component revision. Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.
-Complete coordinated installed JCB operations through the shared runtime, including native options/dependencies, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Record exact component/JCB/plugin sources and the golden-image results before marking this PR ready. Client interoperability is tracked in mcp_client.
+The component golden-image suite exercises shared JCB operations, native options/dependencies, persisted read-back, generated/install artifacts, state isolation, long jobs/cancellation/recovery and cleanup. Each result belongs to its recorded component/JCB/plugin revisions. The linked PR and acceptance checklist are authoritative for current completion; historical runs do not certify later runtime changes. External-client interoperability is tracked in `mcp_client` and the coordinated component suite. Review/merge and deliberate release publication remain separate actions.
diff --git a/docs/JCB-INTEGRATION.md b/docs/JCB-INTEGRATION.md
index 0917a38..7dadb3e 100644
--- a/docs/JCB-INTEGRATION.md
+++ b/docs/JCB-INTEGRATION.md
@@ -7,7 +7,7 @@ The authoritative cross-repository roadmap and planning inventory live in the co
- https://github.com/joomengine/mcp_component/blob/feature/jcb-mcp-runtime/docs/integrations/JCB.md
- https://github.com/joomengine/mcp_component/blob/feature/jcb-mcp-runtime/docs/integrations/jcb-surface.json
-JCB source is pinned to `extension-builder/joomla@5ee658dd07eb749dca43ed4722f6cca7eb8208cf`; CLI documentation to `joomengine/jcb-documentation@ecd3670232d344295fc4f673b2d3dc40a64b3bf6`, english/CLI-Command-Suite.md. Full JCB support is mandatory alongside Joomla core. The shared component implementation now provides installed discovery, planning and job execution; final compiler/package/job acceptance remains in progress. This page defines the plugin contract, while IMPLEMENTATION.md records verified evidence.
+JCB source is pinned to `extension-builder/joomla@5ee658dd07eb749dca43ed4722f6cca7eb8208cf`; CLI documentation to `joomengine/jcb-documentation@ecd3670232d344295fc4f673b2d3dc40a64b3bf6`, english/CLI-Command-Suite.md. Full JCB support is mandatory alongside Joomla core. The shared component implements installed discovery, planning and job execution. This page defines the plugin contract; IMPLEMENTATION.md describes verification layers, and the [component acceptance checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349) records current compiler/package/job results and native limitations.
## Native registration and invocation
@@ -35,6 +35,6 @@ Long work uses the component-owned durable execution/job/lease/artifact protocol
The implemented lazy adapters forward native input/output and exit status to the component without shadowing JCB's command plugin. Native and installed tests cover registration order, global options, framing/EOF/bounds, nonzero exits and output restoration; the explicit `jcb-sync` entrypoint delegates inventory and persistence to the component.
-Before coordinated readiness, complete real package/compiler workflows in disposable Joomla, with persisted read-back, artifacts, cleanup and the original options/environment semantics. The component matrix must also prove missing/disabled JCB behavior, dependency handling, state isolation, principal/grant boundaries, concurrent jobs, cancellation and recovery. Record exact component/JCB/plugin revisions and successful final results in IMPLEMENTATION.md.
+The component's disposable Joomla matrix exercises real package/compiler workflows, persisted read-back, artifacts, cleanup and original options/environment semantics. Its acceptance contract also covers missing/disabled JCB behavior, dependency handling, state isolation, principal/grant boundaries, concurrent jobs, cancellation and recovery. Exact component/JCB/plugin revisions and current results belong to the linked acceptance checklist and run artifacts, including explicit inherited native limitations.
The external `joomengine/mcp-client` only discovers and consumes the server contract. This plugin must never depend on its client package or duplicate its remote bridge.
From 3526cae818803a02971374c044a2e2184f1c2c61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Thu, 24 Sep 2026 11:44:03 +0200
Subject: [PATCH 15/17] Cover NDJSON wire bounds through installed Joomla
console
---
docs/IMPLEMENTATION.md | 4 +++-
tests/installed.php | 12 ++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index 17a6cf7..05fb838 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -1,4 +1,4 @@
-# Implementation status — 22 September 2026
+# Implementation status — 24 September 2026
## Branch
@@ -26,6 +26,8 @@ Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package che
The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. All 14 installed assertions passed on Joomla 6.1.3 / MySQL with PHP 8.3 and 8.4, together with the component's existing installation, administrator, HTTP, native CRUD, upgrade and uninstall suites.
+The current suite adds four NDJSON boundary assertions: oversized spaces, tabs and non-JSON input must each return `REQUEST_TOO_LARGE`, while bounded blank frames and a valid request exactly at the one-MiB wire limit remain accepted. These exercise the shared component runtime through the installed plugin, including the component fix that checks frame size before ignoring whitespace. The previous 14-assertion runs do not certify these new checks; their installed results belong to the current coordinated workflow revision. The 24 September local review reran the 29 native Joomla assertions, reproducible package checks and five release metadata assertions successfully.
+
Historical passing baseline, plugin source `fcfa91a026fedae9d785bad4447eb0855ddee1cf`:
- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35752470759): passed on PHP 8.3 and 8.4.
diff --git a/tests/installed.php b/tests/installed.php
index d40ec89..329e9cb 100644
--- a/tests/installed.php
+++ b/tests/installed.php
@@ -159,6 +159,18 @@
[$status, $stdout] = $run(['joomla:mcp:dispatch'], str_repeat(' ', 1048577));
$check($status !== 0 && ($decode($stdout)['error']['code'] ?? '') === 'REQUEST_TOO_LARGE', 'Oversized JSON is rejected at the native input bound');
+foreach ([' ', "\t", 'x'] as $fill)
+{
+ [$status, $stdout] = $run(['joomla:mcp:dispatch', '--format=ndjson'], str_repeat($fill, 1048577));
+ $check($status !== 0 && ($decode($stdout)['error']['code'] ?? '') === 'REQUEST_TOO_LARGE',
+ 'Oversized NDJSON frames fail before blank-frame or JSON-content handling');
+}
+
+[$status, $stdout] = $run(['joomla:mcp:dispatch', '--format=ndjson'], " \t\n" . $request . str_repeat(' ', 1048575 - strlen($request)) . "\n");
+$result = $decode($stdout);
+$check($status === 0 && ($result['id'] ?? '') === 'system' && ($result['ok'] ?? false),
+ 'Bounded blank NDJSON frames are ignored and a request exactly at the byte bound remains valid');
+
$client = Client::builder()->setClientInfo('installed-plugin-fixture', '1.0.0')->setInitTimeout(15)->setRequestTimeout(30)->setMaxRetries(0)->build();
try
From 1daba4e24c218616e841a27b4bdeb796ca97088a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Thu, 24 Sep 2026 11:52:45 +0200
Subject: [PATCH 16/17] Record verified installed console and coordinated core
acceptance
---
CHANGELOG.md | 13 ++++++-------
docs/IMPLEMENTATION.md | 13 +++++++------
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ea4825..c428673 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,15 +2,14 @@
## Unreleased
-- Preserve native global options, atomic command registration and output restoration after console errors.
-- Verify native Joomla console contracts and actual installed plugin command/stdio entrypoints.
-- Add explicit main-only release publication with verified versioned archives, checksums and post-publication update metadata.
-
-## Unreleased
-
- Establish the exact joomengine_mcp console plugin identity, local-server authority and shared component contract.
- Add native plugin/provider/lazy command adapters, output isolation, installer checks, languages/update metadata and PHP-only reproducible packaging.
+- Expose explicit local JCB catalogue synchronization through the component-owned runtime without replacing JCB's commands.
+- Preserve native global options, atomic command registration and output restoration after console errors.
+- Verify native Joomla console contracts and 18 actual installed command/stdio assertions, including whitespace and exact-limit NDJSON frames, on PHP 8.3 and 8.4.
+- Verify coordinated installed component/plugin/client execution on MySQL and PostgreSQL; retain separate JCB golden-image evidence in the component acceptance checklist.
+- Add explicit main-only release publication with verified versioned archives, checksums and post-publication update metadata.
- Separate external Composer-client/remote-bridge ownership into `joomengine/mcp_client`; no server/plugin dependency on that package.
- Require complete first-class JCB API/CLI coverage and document native command registration, compiler/package semantics, shared jobs and installed acceptance responsibilities.
-JCB execution expansion and complete live combined installation remain pending. No production release is advertised.
+Exact tested revisions and workflow results are recorded in [implementation evidence](docs/IMPLEMENTATION.md). Coordinated JCB compiler/package/job acceptance is tracked in the [component checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349). No release has been published by this implementation work.
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index 05fb838..4316457 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -24,16 +24,17 @@ JCB handlers, reviewed command/API bindings, input freezing, jobs and artifacts
Local PHP 8.3.6 verification: syntax, manifest/language/reproducible package checks; 29 assertions using genuine Joomla 6.1.3 console/plugin/input/output classes; and five release metadata assertions. The native class suite verifies idempotent/atomic registration, global-option handling, help/core output preservation, error restoration, lazy runtime resolution, native input forwarding and nonzero statuses. It uses a substitutable shared-runtime implementation and is not an installed JCB execution test.
-The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. All 14 installed assertions passed on Joomla 6.1.3 / MySQL with PHP 8.3 and 8.4, together with the component's existing installation, administrator, HTTP, native CRUD, upgrade and uninstall suites.
+The installed suite covers the actual plugin's describe/self-test/inventory/dispatch adapters, JSON/NDJSON framing and nonzero outcomes, EOF/byte bounds, MCP handshake/discovery/native action execution through `serve`, and component-disabled/core-command isolation. All 18 installed assertions passed on Joomla 6.1.3 / MySQL with PHP 8.3 and 8.4, together with the component's installation, administrator, HTTP, native CRUD, upgrade and uninstall suites.
-The current suite adds four NDJSON boundary assertions: oversized spaces, tabs and non-JSON input must each return `REQUEST_TOO_LARGE`, while bounded blank frames and a valid request exactly at the one-MiB wire limit remain accepted. These exercise the shared component runtime through the installed plugin, including the component fix that checks frame size before ignoring whitespace. The previous 14-assertion runs do not certify these new checks; their installed results belong to the current coordinated workflow revision. The 24 September local review reran the 29 native Joomla assertions, reproducible package checks and five release metadata assertions successfully.
+Four of these assertions cover NDJSON boundaries: oversized spaces, tabs and non-JSON input each return `REQUEST_TOO_LARGE`, while bounded blank frames and a valid request exactly at the one-MiB wire limit remain accepted. These exercise the shared component runtime through the installed plugin, including the component fix that checks frame size before ignoring whitespace. The 24 September local review also reran the 29 native Joomla assertions, reproducible package checks and five release metadata assertions successfully.
-Historical passing baseline, plugin source `fcfa91a026fedae9d785bad4447eb0855ddee1cf`:
+Verified runtime and test revision: plugin `3526cae818803a02971374c044a2e2184f1c2c61`.
-- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35752470759): passed on PHP 8.3 and 8.4.
-- [Installed console plugin CI](https://github.com/joomengine/mcp_plugin/actions/runs/35752470754): passed on PHP 8.3 and 8.4, including actual plugin entrypoints and the component/package lifecycle. Exact checked-out revisions are retained in that run's logs.
+- [Native console, package and release metadata CI](https://github.com/joomengine/mcp_plugin/actions/runs/35983036798): passed on PHP 8.3 and 8.4.
+- [Installed console plugin CI](https://github.com/joomengine/mcp_plugin/actions/runs/35983036883): all 18 plugin assertions passed on PHP 8.3 and 8.4, including actual plugin entrypoints and the component/package lifecycle. Exact checked-out revisions are retained in that run's logs.
+- [Coordinated installed core matrix](https://github.com/joomengine/mcp_component/actions/runs/35983426742): all four PHP 8.3/8.4 and MySQL 8.4/PostgreSQL 16 jobs passed. Component `75d9685268332241de846935aad2edc2e92c8459` pins this plugin revision and client `72d02491fe80dadc581d3d9d67b1dfbc917d095d`; the matrix exercises the installed plugin and authenticated remote client alongside the component.
-These installed fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence.
+These installed core fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence. The current golden-image run is still pending at this evidence update. Its result and any inherited native limitations belong to the [component acceptance checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349).
Ordinary installed CI pairs the feature branches before merge and uses the component's `main` for plugin `main`. Reusable callers can select an explicit component revision. Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.
From fe387b962dab605191ef18efe4d5cb94821165d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com>
Date: Thu, 24 Sep 2026 11:53:24 +0200
Subject: [PATCH 17/17] Keep coordinated JCB acceptance evidence linked to the
live checklist
---
docs/IMPLEMENTATION.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
index 4316457..9fb1854 100644
--- a/docs/IMPLEMENTATION.md
+++ b/docs/IMPLEMENTATION.md
@@ -34,7 +34,7 @@ Verified runtime and test revision: plugin `3526cae818803a02971374c044a2e2184f1c
- [Installed console plugin CI](https://github.com/joomengine/mcp_plugin/actions/runs/35983036883): all 18 plugin assertions passed on PHP 8.3 and 8.4, including actual plugin entrypoints and the component/package lifecycle. Exact checked-out revisions are retained in that run's logs.
- [Coordinated installed core matrix](https://github.com/joomengine/mcp_component/actions/runs/35983426742): all four PHP 8.3/8.4 and MySQL 8.4/PostgreSQL 16 jobs passed. Component `75d9685268332241de846935aad2edc2e92c8459` pins this plugin revision and client `72d02491fe80dadc581d3d9d67b1dfbc917d095d`; the matrix exercises the installed plugin and authenticated remote client alongside the component.
-These installed core fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence. The current golden-image run is still pending at this evidence update. Its result and any inherited native limitations belong to the [component acceptance checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349).
+These installed core fixtures have no JCB installation. The component's golden-image workflow installs a pinned version of this plugin alongside JCB and runs the same actual-entrypoint suite; its JCB operation matrix supplies the separate compiler/package/job evidence. The verified golden-image revisions, results and inherited native limitations are recorded in the [component acceptance checklist](https://github.com/joomengine/mcp_component/pull/1#issuecomment-5732685349).
Ordinary installed CI pairs the feature branches before merge and uses the component's `main` for plugin `main`. Reusable callers can select an explicit component revision. Manual main-only publication runs installed acceptance first, refuses reused version tags, publishes immutable versioned ZIP/checksum assets, verifies downloaded bytes and updates the feed only after publication. No release has been published by this work.