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:
composer require phpclaw/phpclaw-mcpMethods
Section titled “Methods”| Method | Returns |
|---|---|
initialize | protocol version, capabilities, server info |
tools/list | the registered tools, filtered by the allow and deny lists |
tools/call | a tool’s result |
resources/list | registered resources |
resources/read | one resource |
prompts/list | registered prompts |
prompts/get | one prompt, rendered with its arguments |
Any other method returns -32601. Notifications receive no response.
Running it with an adapter
Section titled “Running it with an adapter”| Adapter | Command | Transports |
|---|---|---|
| Laravel | php artisan phpclaw:mcp-server | --transport=stdio (default) or http |
| Symfony | bin/console phpclaw:mcp-server | --transport=stdio (default) or http |
| WordPress | wp phpclaw mcp-server | stdio |
| Drupal | drush phpclaw:mcp-server | stdio |
| Joomla | php cli/joomla.php phpclaw:mcp-server | stdio |
| Magento | bin/magento phpclaw:mcp-server | stdio |
| OpenCart | php cli/phpclaw.php mcp-server | stdio |
| PrestaShop | php modules/phpclaw/cli/phpclaw.php mcp-server | stdio |
Which tools each adapter exposes is described on its page.
Running it yourself
Section titled “Running it yourself”<?phprequire '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.
Streamable HTTP
Section titled “Streamable HTTP”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.
HTTP security
Section titled “HTTP security”The HTTP transport checks every request, and refuses to start at all without a token.
| Check | Failure |
|---|---|
PHPCLAW_MCP_TOKEN is set and non-empty | the transport throws at startup |
the client is 127.0.0.1 or ::1 | 403 |
the request carries no Origin header | 403, so a browser page cannot call it |
the method is POST, or DELETE to end the session | 405 |
Authorization: Bearer <token> matches, compared in constant time | 401 |
| no more than 60 requests in 60 seconds for the token | 429 |
| the body is present and at most 1 MB | 400 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.
What runs on a tool call
Section titled “What runs on a tool call”tools/call does not go through the agent loop. It runs these steps, in order:
- Allow and deny lists. A tool not permitted returns
-32601. - 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_detectionandmessage_length. tool.beforehook, withsource: mcpin the context.- The tool runs, with its own rules: shell allowlist, SSRF checks, file sandbox, read-only SQL, and its capability check.
tool.afterhook, withsource: mcp.
Three protections from the agent loop do not run on MCP:
| Not applied | Consequence |
|---|---|
| the approval gate | mutating tools run without confirmation |
ToolOutputGuard | injection phrases in a tool’s output reach the client unredacted |
| the tool result cap | output 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.
Results and errors
Section titled “Results and errors”| Outcome | Response |
|---|---|
| success | {content: [{type: text, text: <result>}], isError: false} |
| a guard blocked the arguments | a result with isError: true and the text Blocked: <reason>; guard.blocked fires |
the tool threw ToolException | a result with isError: true and the message |
| not permitted, or no such tool | error -32601 |
| anything else | error -32603, Internal error |
Configuring from the environment
Section titled “Configuring from the environment”PhpClawMcpServer reads four variables, each a JSON array.
PHPCLAW_TOOL_ALLOW and PHPCLAW_TOOL_DENY limit the tools:
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:
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.
Resources and prompts
Section titled “Resources and prompts”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.
Built-in resources and prompts
Section titled “Built-in resources and prompts”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();| Kind | Name | Provides |
|---|---|---|
| resource | phpclaw://config | the application configuration with API keys redacted |
| resource | phpclaw://tools | the name and description of each registered tool |
| prompt | debug | asks the model to review logs, list tools and check memory |
| prompt | db-schema | asks for every table and its columns, or one table with the optional table argument |