eMCP is the Evolution CMS integration layer for laravel/mcp.
It adapts Laravel MCP to Evo runtime with:
- Evo-native config publishing
- manager ACL and sApi scope controls
- optional async dispatch through sTask
- no Laravel app skeleton requirement
- Evo domain MCP tools for document tree (
SiteContent+ TVs)
Implementation starts with a strict MVP gate:
- web transport
- manager mode
initialize+tools/list
Design style:
- contract-first (
TOOLSET.md+ validators) - declarative config-first server registry (
config/mcp.php) - explicit handler pipeline (
validate -> authorize -> query -> map -> paginate)
If you need full architecture and contracts, see DOCS.md (EN) or DOCS.uk.md (UA).
Public canonical tool contract: TOOLSET.md.
Versioning and BC policy: PRD.md (API Stability Policy section).
Operations runbook: OPERATIONS.md.
- Evolution CMS 3.5.2+
- PHP 8.4+
- Composer 2.2+
seiger/sapi1.x (installed as dependency)seiger/stask1.x (installed as dependency)
From your Evo core directory:
cd core
php artisan package:installrequire evolution-cms/emcp "*"
php artisan migratephp artisan vendor:publish --provider="EvolutionCMS\\eMCP\\eMCPServiceProvider" --tag=emcp-config
php artisan vendor:publish --provider="EvolutionCMS\\eMCP\\eMCPServiceProvider" --tag=emcp-mcp-config
php artisan vendor:publish --provider="EvolutionCMS\\eMCP\\eMCPServiceProvider" --tag=emcp-stubsPublished files:
core/custom/config/cms/settings/eMCP.phpcore/custom/config/mcp.phpcore/stubs/mcp-*.stub
The default contract is concept-agnostic and follows Laravel MCP behavior first.
- Create your MCP server/tool classes:
php artisan make:mcp-server ContentServer
php artisan make:mcp-tool HealthToolGenerated classes are placed in core/custom/app/Mcp/....
- Register server in
core/custom/config/mcp.php(servers[]). - Test manager/internal route:
POST /{manager_prefix}/{handle}with manager session + permissionemcp.
- Enable external API mode (if
sApiinstalled):
- keep
mode.api=trueincore/custom/config/cms/settings/eMCP.php - call
POST /{SAPI_BASE_PATH}/{SAPI_VERSION}/mcp/{handle}with Bearer JWT and requiredmcp:*scopes. - get JWT from
POST /{SAPI_BASE_PATH}/{SAPI_VERSION}/token(sApi token endpoint).
- Optional async:
- set
queue.driver=stask, ensuresTaskinstalled, use dispatch endpoint for long-running jobs.
This is the shortest way to understand eMCP as a product, not just a package.
- Material cause: what it consists of (hard boundaries):
- protocol/runtime from
laravel/mcp - Evo adapter layer (
ServiceProvider, registry, routes, middleware, publish) - optional access/async integrations (
sApi,sTask) - canonical contracts (
SPEC.md,TOOLSET.md)
- Formal cause: what form makes it a product (not components):
- one execution contract from request to audited response
- one policy model for manager/API access (
ACL + scopes + limits) - one versioned public tool contract for ecosystem consumers
- one stable extension model for third-party packages
- Efficient cause: what puts it in motion (workflows + triggers):
- internal trigger: manager MCP call (
/{manager_prefix}/{handle}) - external trigger: API MCP call (
/{SAPI_BASE_PATH}/{SAPI_VERSION}/mcp/{handle}) - async trigger: dispatch into
sTaskworker for long operations - lifecycle trigger: package install/publish/register/test
- Final cause: why it is built this way:
- keep Laravel MCP semantics intact
- keep Evo integration explicit and operable
- support both internal and external MCP usage
- allow multiple orchestration strategies on top of one neutral MCP foundation
This lens helps explain architecture decisions:
- set theory: CMS data is structured sets (site -> nodes -> attributes)
- Peano sequence: workflows are ordered state transitions
- Godel limits: self-referential rule systems need strict boundaries
Practical implication:
- eMCP stays as a contract/runtime layer
- orchestration logic stays in consuming packages
- policy/audit/human gates prevent recursive rule loops from becoming unsafe
For Gate A, use manager endpoint /{manager_prefix}/{server_handle} (default: /emcp/content).
Gate A is manager ACL protected, so run checks as a logged-in manager with emcp permission (session cookie required).
- Verify
GETreturns405on MCP endpoint:
curl -i -X GET http://localhost/<MANAGER_PREFIX>/<SERVER_HANDLE> \
-H 'Cookie: evo_session=<MANAGER_SESSION_COOKIE>'- Verify JSON-RPC
initialize:
curl -i -X POST http://localhost/<MANAGER_PREFIX>/<SERVER_HANDLE> \
-H 'Cookie: evo_session=<MANAGER_SESSION_COOKIE>' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"1.0.0"}}}'Expected:
- HTTP
200for validinitialize. MCP-Session-Idpresent in response headers.- stable HTTP
405onGET.
Unlike Laravel default routes/ai.php, eMCP registers servers from config.
Example in core/custom/config/mcp.php:
return [
'redirect_domains' => ['*'],
'servers' => [
[
'handle' => 'content',
'transport' => 'web',
'route' => '/mcp/content',
'class' => EvolutionCMS\eMCP\Servers\ContentServer::class,
'enabled' => true,
'auth' => 'sapi_jwt',
'scopes' => ['mcp:read', 'mcp:call'],
],
[
'handle' => 'content-local',
'transport' => 'local',
'class' => EvolutionCMS\eMCP\Servers\ContentServer::class,
'enabled' => false,
],
],
];Notes:
- Gate A manager endpoint is still
/{manager_prefix}/{handle}(for example/emcp/content). servers[*].routeis used by web transport registration and becomes externally relevant in API mode (Gate B+).content-localis disabled by default to avoid duplicate tool-name registration conflict withcontent.
- Manager/internal access: Evo permission
emcp - API access (via sApi): JWT scopes (
mcp:read,mcp:call,mcp:admin) - Domain reads (
evo.content.*,evo.model.*) are read-only by default
eMCP is the MCP platform layer for the Evo ecosystem:
LaravelMcp: upstream protocol/runtime contract (kept intact).sApi: external API kernel + JWT route-provider discovery.sTask: async worker/task execution and progress.eAi: AI runtime can call MCP tools through manager or API mode.dAi: manager-side orchestration UI can consume eMCP tools as a stable contract.
This keeps the core declarative and neutral: one MCP foundation for multiple orchestration concepts.
- Implemented now:
evo.content.search|get|root_tree|descendants|ancestors|children|siblings - Optional (implemented):
evo.content.neighbors|prev_siblings|next_siblings|children_range|siblings_range - TV-aware queries via structured
with_tvs,tv_filters,tv_order evo.model.list|getimplemented with per-model explicit allowlist projection and sensitive-field defense-in-depth blacklist
From Laravel MCP (available via eMCP adapter):
php artisan make:mcp-server ContentServer
php artisan make:mcp-tool ListResourcesTool
php artisan make:mcp-resource DocsResource
php artisan make:mcp-prompt SummaryPrompt
php artisan mcp:start content-localFor mcp:start content-local, first enable content-local in core/custom/config/mcp.php and disable conflicting server entries if they expose identical tool names.
eMCP operational commands:
php artisan emcp:testphp artisan emcp:list-serversphp artisan emcp:sync-workerscomposer run governance:update-lockcomposer run ci:checkcomposer run benchmark:runcomposer run benchmark:leaderboardcomposer run test:integration:clean-install
If you are validating this repository directly:
composer run check
make test
composer run ci:check
make benchmark
make leaderboardThese checks validate composer.json and run PHP syntax lint across package sources.
One-click demo + full MCP verification:
make demo-allThis target installs demo Evo, starts php -S, issues sApi JWT, runs php artisan emcp:test, then runs composer run test with HTTP runtime integration enabled.
After run, detailed evidence is written to:
demo/logs.md(token/masked auth info, MCP request payloads, HTTP statuses, responses, manual verification commands, plus negative probes: 401/403/413/415/409/429 andevo.model.get(User)sanity)demo/logs.mdalso includes localsTasklifecycle proof (queued -> completed) viaphp artisan stask:workerin demo runtime./tmp/emcp-demo-php-server.log(php built-in server log)
If GitHub API auth is needed during install, pass token via ENV (same pattern as evolution):
GITHUB_PAT=ghp_xxx make demo-allFallback ENV names are also supported: GITHUB_TOKEN, GH_TOKEN.
Manual content-read MCP examples (same calls used in demo/logs.md):
# list tools
curl -sS -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' \
-d '{"jsonrpc":"2.0","id":"tools-1","method":"tools/list","params":{}}' \
'http://127.0.0.1:8787/api/v1/mcp/content'
# read content slice from DB
curl -sS -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' \
-d '{"jsonrpc":"2.0","id":"search-1","method":"tools/call","params":{"name":"evo.content.search","arguments":{"limit":3,"offset":0}}}' \
'http://127.0.0.1:8787/api/v1/mcp/content'
# read one document
curl -sS -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' \
-d '{"jsonrpc":"2.0","id":"get-1","method":"tools/call","params":{"name":"evo.content.get","arguments":{"id":1}}}' \
'http://127.0.0.1:8787/api/v1/mcp/content'Optional runtime integration check (against deployed environment):
EMCP_INTEGRATION_ENABLED=1 \
EMCP_BASE_URL="https://example.org" \
EMCP_API_PATH="/api/v1/mcp/{server}" \
EMCP_API_TOKEN="<jwt>" \
EMCP_SERVER_HANDLE="content" \
EMCP_DISPATCH_CHECK=1 \
composer run test:integration:runtimeCI release note:
.github/workflows/ci.ymlrunsdemo-runtime-proof,runtime-integration, andmigration-matrix(sqlite/mysql/pgsql) onrelease/*pushes.- Configure branch protection to make these jobs required for RC/release merges.
If queue.driver=stask and sTask is installed, eMCP can run long MCP calls via worker emcp_dispatch.
If sTask is missing, fallback behavior follows queue.failover (sync or fail).
- Keep secrets in
.envorcore/custom/config/*. - Audit logs must redact tokens/secrets.
- Use tool denylist and server allowlist for production hardening.
- deny-by-default for manager/API without explicit access.
security.enable_write_tools=falseby default.- sensitive key redaction in logs is mandatory.
- API scope checks (
mcp:read|call|admin) are required in Gate B+. depth/limit/payloadlimits should stay enabled.
Security release checklist: SECURITY_CHECKLIST.md.
Threat model: THREAT_MODEL.md.
Architecture freeze: ARCHITECTURE_FREEZE_CHECKLIST.md.
MIT (LICENSE).