Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [Unreleased]
### Added
- Personal access tokens (`auth.mode = pat`, now the default): `emcp_tokens` table, `Authorization: Bearer emcp_...` endpoint at `/{api_prefix}/{server}` without sApi, self-service **Tools → MCP tokens** manager page, `emcp:token:create|list|revoke` commands.
- API requests now impersonate the token/JWT owner (`ImpersonateManagerUser`): `evo()->isLoggedIn('mgr')`, `hasPermission()`, document groups and locks reflect that user; the `emcp` permission is required in API mode too.
- Content read tools respect document groups (`use_udperms`) and `view_unpublished`; model catalog reads require the permission of the matching manager screen.
- `evo.elements.list|get` and write tools `evo.write.content.update|create|publish`, `evo.write.elements.save`, `evo.write.cache.clear` (behind `security.enable_write_tools` and the `mcp:write` scope).
- `docker/` compose setup with a preinstalled site and smoke test.


All notable changes to this project will be documented in this file.

## [Unreleased]
Expand Down
6 changes: 2 additions & 4 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,8 @@ Never log raw values of:
- `api_key`

## 14) Multilingual Support
Required language files:
- `lang/en/global.php`
- `lang/uk/global.php`
- `lang/ru/global.php`
Required language files (`lang/{locale}/global.php`) for all supported locales:
`az`, `be`, `bg`, `cs`, `da`, `de`, `en`, `es`, `fa`, `fi`, `fr`, `he`, `it`, `ja`, `nl`, `nn`, `pl`, `pt`, `sk`, `sv`, `uk`, `zh`.

Minimum keys:
- `title`
Expand Down
4 changes: 2 additions & 2 deletions PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
- AC11: Worker `emcp_dispatch` авто-реєструється, якщо `sTask` встановлений.
- AC12: Async payload містить actor/context/trace/idempotency поля.
- AC13: `queue.failover=sync` дає синхронний fallback при відсутності `sTask`.
- AC14: Локалізації `en/uk/ru` покривають manager/error/permissions ключі.
- AC14: Локалізації всіх підтримуваних мов (`az/be/bg/cs/da/de/en/es/fa/fi/fr/he/it/ja/nl/nn/pl/pt/sk/sv/uk/zh`) покривають manager/error/permissions ключі.
- AC15: Audit log не містить raw bearer token та секретів.

### 11.3 Domain/orchestration contract
Expand Down Expand Up @@ -332,7 +332,7 @@ Extension compliance boundary:
- FR12: Safe audit log with redaction.
- FR13: Rate limits + payload size limits.
- FR14: Idempotency for async dispatch with `409` conflict semantics.
- FR15: Multilingual manager/error/permissions keys (`en/uk/ru`).
- FR15: Multilingual manager/error/permissions keys across all supported locales (`az/be/bg/cs/da/de/en/es/fa/fi/fr/he/it/ja/nl/nn/pl/pt/sk/sv/uk/zh`).

### 15.2 Domain/Extension (FR16-FR26)
- FR16: Canonical `evo.content.*` tool profile (`search/get/root_tree/descendants/ancestors/children/siblings`).
Expand Down
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Operations runbook: `OPERATIONS.md`.
- Evolution CMS 3.5.2+
- PHP 8.3+
- Composer 2.2+
- `seiger/sapi` 1.x (installed as dependency)
- `seiger/sapi` 1.x (installed as dependency; only used when `auth.mode = sapi_jwt`)
- `seiger/stask` 1.x (installed as dependency)

## Install
Expand Down Expand Up @@ -80,6 +80,65 @@ Generated classes are placed in `core/custom/app/Mcp/...`.
5. Optional async:
- set `queue.driver=stask`, ensure `sTask` installed, use dispatch endpoint for long-running jobs.

## Personal access tokens (default API auth)
Since this version the API endpoint needs **no extra packages**: a manager user creates a
personal access token and every request runs **as that user**, with that user's role,
permissions and document groups — exactly as if they were logged into the manager.

1. Give the role the `emcp` permission (admins have it after `migrate`).
2. Open **Tools → MCP tokens** in the manager (`{manager_url}/emcp/tokens`), or run
`php artisan emcp:token:create <username> --scopes=mcp:read,mcp:call --expires=90`.
3. Connect the agent:

```bash
claude mcp add --transport http evo https://example.com/mcp/content --header "Authorization: Bearer emcp_..."
```

```toml
# Codex ~/.codex/config.toml
[mcp_servers.evo]
url = "https://example.com/mcp/content"
bearer_token_env_var = "EVO_MCP_TOKEN"
```

Scopes narrow a token, never widen the user: `mcp:read` (list/read), `mcp:call` (read-only
tools), `mcp:write` (`evo.write.*` tools, also gated by `security.enable_write_tools`),
`mcp:admin`. Tokens are stored hashed, can expire, and are revoked from the same page or with
`emcp:token:revoke`. `emcp:token:list` shows what exists.

`auth.mode` in `core/custom/config/cms/settings/eMCP.php` selects `pat` (default), `sapi_jwt`
(seiger/sapi JWT, now also impersonating the JWT user) or `none`.

### Write tools
`evo.write.content.update|create|publish`, `evo.write.elements.save`, `evo.write.cache.clear`
plus the read tools `evo.elements.list|get`. Each one re-checks the manager permission of the
matching manager action (`save_document`, `publish_document`, `save_chunk`, `new_snippet`, ...),
document-group access, element locks, fires the same `OnBefore*FormSave`/`On*FormSave` events
and writes a `manager_log` row, so an admin sees API changes next to browser ones.

### Tools from other extras
An extra contributes tools by implementing `EvolutionCMS\eMCP\Contracts\ToolProvider` and
registering it in its service provider's `boot()`:

```php
if (class_exists(\EvolutionCMS\eMCP\Services\ToolRegistry::class)) {
app(\EvolutionCMS\eMCP\Services\ToolRegistry::class)->register(new MyToolProvider());
}
```

Tools are plain `Laravel\Mcp\Server\Tool` classes; mark the ones that change the site with
`EvolutionCMS\eMCP\Contracts\WritesSite` so `security.enable_write_tools` and the `mcp:write`
scope apply to them. Tool code runs as the impersonated manager, so `evo()->hasPermission()` and
the extra's own guards behave as on the page. Alternatively list classes under
`mcp.servers[].extra_tools`. Example: `elcreator/aimage` ships `aimage.*` tools this way.

### Try it in Docker
```bash
cd docker && docker compose up --build # prints the site URL and a ready-made token
EVO_EXTRAS=elcreator/aimage AIMAGE_API_KEY=... docker compose up --build # with extras
docker/smoke.sh # runs an end-to-end check against it
```

## Design Philosophy (Optional Reading)
### Why This Product Exists (4 Core Questions, Aristotle)
This is the shortest way to understand eMCP as a product, not just a package.
Expand Down
10 changes: 3 additions & 7 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,7 @@ eMCP/
│ ├─ *_add_emcp_permissions.php
│ └─ *_add_emcp_role_permissions.php (optional if split)
├─ lang/
│ ├─ en/global.php
│ ├─ uk/global.php
│ └─ ru/global.php
│ └─ {az,be,bg,cs,da,de,en,es,fa,fi,fr,he,it,ja,nl,nn,pl,pt,sk,sv,uk,zh}/global.php
├─ plugins/
│ └─ eMCPPlugin.php
├─ src/
Expand Down Expand Up @@ -797,10 +795,8 @@ Recommended orchestration fields (Post-MVP SHOULD):
- `authorization`, `token`, `jwt`, `secret`, `cookie`, `password`, `api_key`.

## 14. Multilingual contract
Mandatory translation files:
- `lang/en/global.php`
- `lang/uk/global.php`
- `lang/ru/global.php`
Mandatory translation files (`lang/{locale}/global.php`) for all supported locales:
`az`, `be`, `bg`, `cs`, `da`, `de`, `en`, `es`, `fa`, `fi`, `fr`, `he`, `it`, `ja`, `nl`, `nn`, `pl`, `pt`, `sk`, `sv`, `uk`, `zh`.

Required keys:
- `title`
Expand Down
16 changes: 15 additions & 1 deletion config/eMCPSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@
],

'auth' => [
'mode' => 'sapi_jwt',
// pat - personal access tokens issued in the manager / by artisan (no extra packages)
// sapi_jwt - JWT issued by seiger/sapi
// none - no API authentication (development only)
// In every mode the request runs as the authenticated manager user, with that user's permissions.
'mode' => 'pat',
'require_scopes' => true,
// Extra scope a token needs to call evo.write.* tools.
'write_scope' => 'mcp:write',
'scope_map' => [
'mcp:read' => [
'initialize',
Expand All @@ -27,12 +33,20 @@
'prompts/list',
'prompts/get',
'completion/complete',
'resources/templates/list',
// JSON-RPC notifications (notifications/initialized, notifications/cancelled, ...)
'notifications/*',
],
'mcp:call' => ['tools/call'],
'mcp:admin' => ['admin/*'],
],
],

'tokens' => [
// Manager page where a user creates their own tokens: {manager_url}/{manager_prefix}/tokens
'self_service' => true,
],

'acl' => [
'permission' => 'emcp',
],
Expand Down
5 changes: 4 additions & 1 deletion config/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
'auth' => 'sapi_jwt',
'scopes' => ['mcp:read', 'mcp:call'],
'scope_map' => [
'mcp:read' => ['initialize', 'tools/list', 'resources/read'],
'mcp:read' => ['initialize', 'ping', 'tools/list', 'resources/list', 'resources/read', 'resources/templates/list', 'prompts/list', 'prompts/get', 'notifications/*'],
'mcp:call' => ['tools/call'],
],
'limits' => [
Expand All @@ -28,6 +28,9 @@
'security' => [
'deny_tools' => [],
],
// Tool classes from other packages to expose on this server (alternative to
// registering an EvolutionCMS\eMCP\Contracts\ToolProvider at runtime).
'extra_tools' => [],
],
[
'handle' => 'content-local',
Expand Down
33 changes: 33 additions & 0 deletions database/migrations/2026_09_15_000004_create_emcp_tokens_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
public function up(): void
{
if (Schema::hasTable('emcp_tokens')) {
return;
}

Schema::create('emcp_tokens', function (Blueprint $table): void {
$table->increments('id');
$table->unsignedInteger('user_id')->index();
$table->string('name', 100);
// First characters of the plaintext token, shown in lists so a user can tell tokens apart.
$table->string('token_prefix', 16);
$table->string('token_hash', 64)->unique();
$table->text('scopes')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('revoked_at')->nullable();
$table->timestamps();
});
}

public function down(): void
{
Schema::dropIfExists('emcp_tokens');
}
};
41 changes: 41 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Evolution CMS 3.5.x + eMCP from the working copy, for testing the MCP API end to end.
#
# cd docker && docker compose up --build
#
# Site: http://localhost:8080/ manager: http://localhost:8080/manager/ (admin / 123456)
# MCP: http://localhost:8080/mcp/content token: printed at first start, also in /var/www/html/core/storage/emcp-token.txt
FROM php:8.3-apache

ARG EVO_BRANCH=3.5.x

RUN apt-get update && apt-get install -y --no-install-recommends \
git unzip curl libzip-dev libpng-dev libjpeg-dev libwebp-dev libfreetype6-dev libicu-dev \
&& docker-php-ext-configure gd --with-jpeg --with-webp --with-freetype \
&& docker-php-ext-install -j"$(nproc)" gd zip intl pdo_mysql opcache \
&& a2enmod rewrite \
&& rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

ENV COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_MEMORY_LIMIT=-1 \
APACHE_DOCUMENT_ROOT=/var/www/html

RUN sed -ri 's!AllowOverride None!AllowOverride All!g' /etc/apache2/apache2.conf \
# ServerName silences the FQDN warning; SetEnvIf hands Bearer tokens to PHP under mod_php
&& printf 'ServerName localhost\nSetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1\n' > /etc/apache2/conf-enabled/evo-mcp.conf \
&& printf 'memory_limit=512M\nupload_max_filesize=64M\npost_max_size=64M\n' > /usr/local/etc/php/conf.d/evo.ini

WORKDIR /var/www/html

# Core checkout + core dependencies are baked into the image; site install happens at first start.
RUN git clone --depth 1 --branch "${EVO_BRANCH}" https://github.com/evolution-cms/evolution.git /tmp/evo \
&& cp -a /tmp/evo/. /var/www/html/ && rm -rf /tmp/evo \
&& cp ht.access .htaccess \
&& cd core && composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

COPY entrypoint.sh /usr/local/bin/evo-entrypoint.sh
RUN chmod +x /usr/local/bin/evo-entrypoint.sh

ENTRYPOINT ["/usr/local/bin/evo-entrypoint.sh"]
CMD ["apache2-foreground"]
29 changes: 29 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
services:
evo:
build:
context: .
args:
EVO_BRANCH: ${EVO_BRANCH:-3.5.x}
ports:
- "${EVO_PUBLIC_PORT:-8080}:80"
environment:
EVO_ADMIN_USERNAME: ${EVO_ADMIN_USERNAME:-admin}
EVO_ADMIN_PASSWORD: ${EVO_ADMIN_PASSWORD:-123456}
EVO_PUBLIC_PORT: ${EVO_PUBLIC_PORT:-8080}
# Comma-separated composer packages to install at first start (none by default)
EVO_EXTRAS: ${EVO_EXTRAS:-}
# Passed through for extras that read it (e.g. aIMage's site-wide key)
AIMAGE_API_KEY: ${AIMAGE_API_KEY:-}
volumes:
# The package under test, symlinked into core/vendor so edits are live (no rebuild needed).
- ..:/package
# Installed site survives container restarts; `docker compose down -v` starts from scratch.
- evo-site:/var/www/html
healthcheck:
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost/"]
interval: 15s
timeout: 5s
retries: 20

volumes:
evo-site:
Loading