Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c756087
docs: define thin Joomla integration and shared component contract
Llewellynvdm Sep 17, 2026
ebd1e15
docs: define exact console identity, full companion parity and truste…
Llewellynvdm Sep 17, 2026
68df9cd
feat: add Joomla console adapters, dependency-aware installation and …
Llewellynvdm Sep 17, 2026
bc3bcf0
fix: use native Joomla installer DI and preserve clean MCP output thr…
Llewellynvdm Sep 17, 2026
fb6a72a
fix: align the console manifest description with its shipped language…
Llewellynvdm Sep 17, 2026
e7fb901
docs: require full JCB console coverage and separate external client …
Llewellynvdm Sep 18, 2026
7177870
Preserve native console lifecycle and verify typed adapters
Llewellynvdm Sep 21, 2026
c0b02e6
Exercise installed plugin command and MCP stdio entrypoints
Llewellynvdm Sep 21, 2026
51b855a
Gate console releases on installed tests and verified update assets
Llewellynvdm Sep 21, 2026
b2ee0ae
Expose explicit local JCB catalogue synchronization and record accept…
Llewellynvdm Sep 21, 2026
cce3d3d
Require the JCB-capable component runtime and normalize archive modes
Llewellynvdm Sep 21, 2026
873c467
Build the locked distribution for installed package lifecycle acceptance
Llewellynvdm Sep 21, 2026
fcfa91a
Keep post-merge installed checks aligned and refresh acceptance evidence
Llewellynvdm Sep 22, 2026
e9a97f2
Clarify implemented scope and link revision-bound acceptance evidence
Llewellynvdm Sep 22, 2026
3526cae
Cover NDJSON wire bounds through installed Joomla console
Llewellynvdm Sep 24, 2026
1daba4e
Record verified installed console and coordinated core acceptance
Llewellynvdm Sep 24, 2026
fe387b9
Keep coordinated JCB acceptance evidence linked to the live checklist
Llewellynvdm Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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, 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
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/plg_console_joomengine_mcp-*.zip
build/plg_console_joomengine_mcp-*.zip.sha256
if-no-files-found: error
retention-days: 7
83 changes: 83 additions & 0 deletions .github/workflows/installed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 || (github.ref_name == 'main' && 'main' || '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
bash tools/build-distribution.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
74 changes: 74 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/build/
/vendor/
/.reference/
/.phpunit.cache/
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Agent contract — console plugin

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.

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`, 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.

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 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.

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.

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.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

## 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.

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.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# mcp_plugin
# JoomEngine MCP console plugin

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`
**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`, `cli-inventory` and `jcb-sync` adapters. It does not contain a second MCP catalogue or an HTTP webservices plugin.

## Three repository boundaries

- [`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.

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.

## 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). 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

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.

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.
76 changes: 76 additions & 0 deletions build.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
/**
* @package JoomEngine.Mcp
* @created 17 September 2026
* @author Llewellyn van der Merwe <https://dev.vdm.io>
* @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)
|| !$zip->setExternalAttributesName($file, ZipArchive::OPSYS_UNIX, 0100644 << 16))
{
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;
Loading
Loading