Skip to content

MCP Server

phpclaw/phpclaw-mcp serves a ToolRegistry over the Model Context Protocol, JSON-RPC 2.0, so an MCP client can list and call your phpClaw tools directly. It reports protocol version 2024-11-05 and the server name phpclaw-mcp.

It requires PHP 8.1 and phpclaw/phpclaw on the same line. Every adapter already depends on it. For core alone:

Terminal window
composer require phpclaw/phpclaw-mcp

MethodReturns
initializeprotocol version, capabilities, server info
tools/listthe registered tools, filtered by the allow and deny lists
tools/calla tool’s result
resources/listregistered resources
resources/readone resource
prompts/listregistered prompts
prompts/getone prompt, rendered with its arguments

Any other method returns -32601. Notifications receive no response.


AdapterCommandTransports
Laravelphp artisan phpclaw:mcp-server--transport=stdio (default) or http
Symfonybin/console phpclaw:mcp-server--transport=stdio (default) or http
WordPresswp phpclaw mcp-serverstdio
Drupaldrush phpclaw:mcp-serverstdio
Joomlaphp cli/joomla.php phpclaw:mcp-serverstdio
Magentobin/magento phpclaw:mcp-serverstdio
OpenCartphp cli/phpclaw.php mcp-serverstdio
PrestaShopphp modules/phpclaw/cli/phpclaw.php mcp-serverstdio

Which tools each adapter exposes is described on its page.


<?php
require 'vendor/autoload.php';
use PhpClaw\Mcp\Generic\CliRunner;
use PhpClaw\Tools\ToolRegistry;
$registry = new ToolRegistry;
$registry->register([/* your tools */]);
CliRunner::run($registry);

Point your MCP client at php mcp-server.php. For a client that reads .mcp.json, such as Claude Code, the entry looks like this:

{
"mcpServers": {
"phpclaw": {
"command": "php",
"args": ["mcp-server.php"]
}
}
}

On stdio, anything written to STDOUT that is not a JSON-RPC message breaks the client. CliRunner does not redirect PHP warnings or notices, so send them to STDERR in your script. The package’s StdoutPurity class does this, and PrestaShop’s CLI uses it.

CliRunner::serve($registry, 'http') starts the HTTP transport, or serve it from your own endpoint:

use PhpClaw\Mcp\PhpClawMcpServer;
use PhpClaw\Mcp\Transport\StreamableHttpTransport;
use PhpClaw\Tools\ToolRegistry;
$server = new PhpClawMcpServer($registry);
$server->serve(new StreamableHttpTransport((string) getenv('PHPCLAW_MCP_TOKEN')));

Each transport instance handles one request, so create one per HTTP request.


The HTTP transport checks every request, and refuses to start at all without a token.

CheckFailure
PHPCLAW_MCP_TOKEN is set and non-emptythe transport throws at startup
the client is 127.0.0.1 or ::1403
the request carries no Origin header403, so a browser page cannot call it
the method is POST, or DELETE to end the session405
Authorization: Bearer <token> matches, compared in constant time401
no more than 60 requests in 60 seconds for the token429
the body is present and at most 1 MB400 or 413

A DELETE returns 204 and closes the session. A body that is not valid JSON is the one gap: the request is dropped and the client gets 200 with an empty body instead of a -32700 error.

stdio has no network exposure and checks no token.


tools/call does not go through the agent loop. It runs these steps, in order:

  1. Allow and deny lists. A tool not permitted returns -32601.
  2. Guard scan of the arguments. Every string value in the arguments is scanned by the guard chain, except the three prompt-only guards: code_injection, pii_detection and message_length.
  3. tool.before hook, with source: mcp in the context.
  4. The tool runs, with its own rules: shell allowlist, SSRF checks, file sandbox, read-only SQL, and its capability check.
  5. tool.after hook, with source: mcp.

Three protections from the agent loop do not run on MCP:

Not appliedConsequence
the approval gatemutating tools run without confirmation
ToolOutputGuardinjection phrases in a tool’s output reach the client unredacted
the tool result capoutput is returned at full size

Adapter commands run from the console, so each adapter tool’s capability check passes on its console branch. Use the deny list to keep a tool off MCP.

OutcomeResponse
success{content: [{type: text, text: <result>}], isError: false}
a guard blocked the argumentsa result with isError: true and the text Blocked: <reason>; guard.blocked fires
the tool threw ToolExceptiona result with isError: true and the message
not permitted, or no such toolerror -32601
anything elseerror -32603, Internal error

PhpClawMcpServer reads four variables, each a JSON array.

PHPCLAW_TOOL_ALLOW and PHPCLAW_TOOL_DENY limit the tools:

Terminal window
PHPCLAW_TOOL_ALLOW='["http_request","code_search"]'
PHPCLAW_TOOL_DENY='["shell_exec","file_write"]'

An empty or unset allow list permits every tool. The deny list always wins. Both lists also filter tools/list. Use tool names as name() returns them, such as shell_exec, not catalogue keys.

PHPCLAW_GUARDS adds guards, and PHPCLAW_HOOKS adds hook listeners:

Terminal window
PHPCLAW_GUARDS='[{"class": "App\\Guards\\ProfanityGuard", "priority": 45}]'
PHPCLAW_HOOKS='[{"event": "tool.after", "handler": "App\\Hooks\\AuditHook", "priority": 10}]'

A class that does not implement GuardInterface or HookInterface is skipped with a logged warning. A PHPCLAW_GUARDS entry naming a class that does not exist is skipped with no warning at all, so check the spelling. The eight default guards are always registered.


use PhpClaw\Mcp\PromptRegistry;
use PhpClaw\Mcp\ResourceRegistry;
ResourceRegistry::register(
uri: 'file:///var/log/app.log',
name: 'Application Log',
description: 'Recent application errors',
reader: static fn (): string => (string) file_get_contents('/var/log/app.log'),
mimeType: 'text/plain',
);
PromptRegistry::register(
name: 'health-check',
description: 'Run an application health check',
arguments: [],
renderer: static fn (array $args): array => [
['role' => 'user', 'content' => ['type' => 'text', 'text' => 'Check disk, database and queues.']],
],
);

mimeType defaults to text/plain. An unknown resource URI or prompt name returns -32601.

Two classes define ready-made entries. Nothing registers them automatically; call them before serving if you want them:

use PhpClaw\Mcp\Defaults\DefaultPrompts;
use PhpClaw\Mcp\Defaults\DefaultResources;
DefaultResources::register($registry);
DefaultPrompts::register();
KindNameProvides
resourcephpclaw://configthe application configuration with API keys redacted
resourcephpclaw://toolsthe name and description of each registered tool
promptdebugasks the model to review logs, list tools and check memory
promptdb-schemaasks for every table and its columns, or one table with the optional table argument