Skip to content

phpClaw for Drupal

The Drupal adapter is a module. It adds a chat page and admin pages under Configuration, two Drush commands, two REST routes, three permissions, and up to 15 Drupal tools.


RequirementVersion
PHP8.1 or later
Drupal10 or 11
Terminal window
composer require phpclaw/phpclaw-drupal
drush en phpclaw

Enabling the module creates phpclaw_conversations, phpclaw_messages and phpclaw_memory from hook_schema(). After updating the module, run drush updb for pending updates.

Open Configuration → phpClaw AI Agent → Settings, choose a provider, enter the API key and save. Test Connection checks it.

phpClaw in a Drupal site

To update the module later:

Terminal window
composer update phpclaw/phpclaw-drupal --with-dependencies
drush updb -y
drush cr

The settings live in the phpclaw.settings config object. The Settings form has:

FieldNotes
Provider, Model, API Keythe provider to use; the key is not needed for Ollama
Base URLfor the Custom provider; https://, or http:// on localhost, 127.0.0.1 or ::1
System Promptadded to every request
Store Messageson by default
Max Iterationsthe agent loop limit; 0 or empty uses 20
Remote Skill URLscomma-separated HTTPS URLs of skill files; the Guide page shows which loaded
Cloud Key, Cloud Signing Secret, Cloud Disableconfigure phpClaw Cloud; cloud is used only while Store Messages is on

Drush writes the same config:

Terminal window
drush config:set phpclaw.settings provider anthropic
drush config:set phpclaw.settings api_key sk-ant-...

These keys have no form field. Edit them with drush config:edit phpclaw.settings:

KeyDefaultNotes
shell_allowlistnot setempty uses core’s default allowlist
tool_deny[]tool names or groups to remove
guards[]guard class entries
hooks[]event, handler and priority entries; the handler must be callable
skills[]skill definitions
api_auth_providers[basic_auth, cookie]authentication providers accepted by the REST routes

With Store Messages off, the agent stores no message content. See Memory.


PermissionAllows
use phpclaw chatthe Chat, Analytics, Guide and About pages, the REST routes, and every Drupal tool
manage all phpclaw conversationsseeing every user’s conversations and counts on Chat and Analytics
administer phpclawthe Settings page and Test Connection

All three are marked restricted.

A tool call without the permission returns a FORBIDDEN error. Tools run from Drush, including the MCP server, skip the check.

The phpClaw menu item is attached to the Chat route, so a role with use phpclaw chat but not administer phpclaw still sees the menu. To reach it from the admin menu, the role also needs Drupal’s own Use the administration pages and View the administration theme permissions.


The adapter builds two agents with different tool sets:

WhereTools
Chat page and REST APIthe Drupal tools only
Drush phpclaw:run, the phpclaw.agent service, and the MCP serverthe Drupal tools plus core’s shell_exec, http_request, file_read, file_write, file_edit, code_search and project_info

So the chat page and the REST API cannot run shell commands, make HTTP requests or touch files.

Eight are always registered:

ToolWhat it reads
drupal_entitynodes, users and taxonomy terms, with filters and field selection; each row passes Drupal’s own view access check, and the response reports total_before_access_filter
drupal_configa value from an allowlisted set of core configuration objects, by name and optional key
db_queryone read-only SQL SELECT
drupal_modulesinstalled modules with status, version and package
drupal_cronlast cron run, queue sizes and queue workers
drupal_rolesroles with their permissions and user counts
drupal_path_aliasespath aliases: list them, find the alias for a path, or search alias text
drupal_cachecache bins with approximate row counts; it never flushes

Seven are registered only when the module they read is installed:

ToolNeeds moduleWhat it reads
read_logdblogrecent watchdog entries by type, severity and time; message bodies only with include_messages: true; never uid, hostname, location, link or referer
drupal_menusmenu_link_contentmenus and their links, with hierarchy
drupal_mediafilefiles and media by type, with size and usage
drupal_viewsviewsViews with their displays, paths and status
drupal_blocksblockblock instances with region, visibility and status
drupal_moderationcontent_moderationmoderation states, pending reviews and recent changes
drupal_webformwebformwebforms with submission counts; submitted values are never returned

db_query refuses users, users_field_data, sessions, key_value, key_value_expire, flood, batch, config, watchdog, node_field_data, taxonomy_term_field_data, file_managed, webform_submission, webform_submission_data, phpclaw_conversations and phpclaw_messages. The refusal is QUERY_NOT_PERMITTED and names only the table. Some of that data is readable through other tools, within their own rules: drupal_entity for users, nodes and terms, read_log for watchdog, and drupal_config for allowlisted configuration.

tool_deny removes tools by name or group:

GroupTools
group:contentdrupal_entity, drupal_moderation, drupal_media, drupal_webform
group:systemdrupal_modules, drupal_cron, drupal_cache, drupal_config
group:navdrupal_menus, drupal_path_aliases, drupal_blocks, drupal_views

group:system here holds Drupal tools only; it does not remove db_query or the core tools. Name those individually.

Every agent is built with CliApprovalGate. In a terminal you are asked before a tool changes something; elsewhere the change is refused. See Security: approval. The MCP server does not use the approval gate.

file_write may create .php, .phtml and .phar files only under Drush phpclaw:run, not from the MCP server.

With shell_allowlist empty, shell_exec allows core’s default commands, which include cat, head, tail and grep. Set your own list to narrow it.


Terminal window
drush phpclaw:run "list active modules"
drush pc "show cache bin sizes" --stream
drush phpclaw:mcp-server
CommandAliasDoes
phpclaw:run <message>pcsend a message; --stream prints tokens as they arrive
phpclaw:mcp-serverphpclaw-mcpserve the Drush tool set over stdio; see MCP

MethodRouteReturns
POST/api/phpclaw/sendthe reply as JSON
POST/api/phpclaw/chat/streamServer-Sent Events

Both are ordinary Drupal routes: Drupal authenticates the caller with the providers in api_auth_providers and checks use phpclaw chat before phpClaw runs. Both also carry Drupal’s _csrf_request_header_token requirement, so a browser session sends a token from /session/token as X-CSRF-Token. Drupal applies that check only to requests authenticated by a session cookie, so an HTTP Basic request needs no token.

To accept another authentication provider, add its id to the list:

Terminal window
drush config:set phpclaw.settings api_auth_providers.2 oauth2

To call them from a script with HTTP Basic, enable Drupal’s basic_auth module and use HTTPS:

Terminal window
drush en basic_auth
curl -u 'USERNAME:PASSWORD' \
-X POST https://example.com/api/phpclaw/send \
-H 'Content-Type: application/json' \
-d '{"message": "how many articles are published?"}'

The body is JSON with message and an optional conversation_id. send returns ok, text, provider, model, iterations, tokens, conversation_id and tool_calls. Errors return {"ok": false, "error": "..."}:

StatusWhen
400message is missing, empty or longer than 10,000 characters, or the body is not JSON
403the conversation belongs to another user
422a guard blocked the message
502the provider returned an error
500anything else failed
503the agent could not be built, for example with no provider configured

chat/stream sends tool_before, tool_after, chunk, done and error events.


Each conversation stores its creator’s Drupal user id in phpclaw_conversations.user_id. Conversations created from Drush store 0. A user sees and opens only their own conversations; opening another user’s returns 403. manage all phpclaw conversations shows every conversation, including Drush ones. Analytics counts follow the same rule.

A Drush run stores 0 whenever the Drupal console is active, whatever user the command line names. The rule is enforced in the memory driver, DrupalDbConversationMemory, which throws ConversationAccessDeniedException for another user’s conversation. On a site installed before the owner column existed, phpclaw_update_8004 adds it, so run drush updb; rows without an owner are visible only with manage all phpclaw conversations.

The agent always uses the database memory driver: conversations in the conversation tables, everything else in phpclaw_memory.


$agent = \Drupal::service('phpclaw.agent');
echo $agent->send('Summarise content published today.')->text;
mymodule.services.yml
services:
mymodule.reporter:
class: Drupal\mymodule\Reporter
arguments: ['@phpclaw.agent']

phpclaw.agent has the Drush tool set; phpclaw.agent.chat has the chat tool set. The factory behind each returns null when the agent cannot be built, for example with no provider configured.


Tag your services to register them:

TagRegisters
phpclaw.toola tool
phpclaw.guarda guard
phpclaw.skilla skill
phpclaw.providera provider
phpclaw.memory_drivera memory driver
phpclaw.hook_listenera listener; set event on the tag, and the service needs a handle() method
services:
mymodule.inventory_tool:
class: Drupal\mymodule\Tool\InventoryTool
tags:
- { name: phpclaw.tool }
mymodule.audit_listener:
class: Drupal\mymodule\AuditListener
tags:
- { name: phpclaw.hook_listener, event: agent.after }

Every lifecycle event is also dispatched on Drupal’s event dispatcher as phpclaw. followed by the event name. The event object is PhpClaw\Drupal\EventBridge\PhpClawEvent, whose context property holds the event data. The event names are listed on the Hooks page.