Remote clients¶
The Site Agent MCP submodule (site_agent_mcp, experimental) offers an
account's toolsets to remote MCP clients, such as Claude's custom connectors,
Claude Desktop and Claude Code, so an agent outside the site changes its
content and structure through the same checks, approvals and audit log as the
chat. The client's own model makes the calls; Site Agent's model and runner
aren't involved. It is a submodule because it needs
MCP Server, which serves the
protocol at /mcp over Streamable HTTP and over STDIO with
drush mcp:server ACCOUNT, and the chat doesn't.
MCP Server has no stable release, so a site allows its stability as it does Tool API's:
Tools¶
- An account gets the tools of every toolset it may use, merged as the chat
merges them, each named
site_agent__FUNCTION, with the same refined schema.drush site-agent:toolslists the same functions. - Each call runs through the runner's checks: refinements, the structure
gate, text formats and alt text, the tool's permission, validation and
access(), as the requesting account, in the thread's workspace scope. - The server's instructions carry the site conventions, a section per toolset with its prompt fragment and, where configuration is locked, the note that structure can't change.
- Tool annotations come from the classification: reads are read-only, destructive tools destructive.
Threads¶
Each MCP session writes into one thread, with the origin mcp, titled by its
client and the date. Its calls leave tool-call records, and revision logs name
it as "AGENT thread ID: CLIENT, DATE". A remote thread has no transcript and
isn't listed in the chat's history. A stateless client, which sends no
session, gets one thread a day. A thread idle for longer than the transcript
retention is closed, which cancels its calls waiting for a decision.
Where content is staged, a session's first thread has no workspace, so its
content writes are refused until the client calls
site_agent_start_thread with a staging workspace, which starts a new
thread that writes content there.
Approvals¶
Approval modes apply as in the chat. A call that needs approval is decided in the client, never on the site:
- A client that renders MCP Apps,
such as claude.ai and Claude Desktop, shows the call's card, the same card
the chat shows, rendered on the server from the payload. Its Approve and
Reject buttons call
site_agent_decide, and a result card's Publish button callssite_agent_publish. Both are listed withvisibility: ["app"], so the client keeps them from its model, and they're listed only to clients that declare MCP Apps; a call from any other client is refused. After a decision the card tells the model what happened, as a message in the conversation. The app loads the MCP Apps SDK from unpkg.com, which its content security policy allows, with the site's own origin for images. - A client that supports elicitation, such as Claude Code, asks its user with the card as text. Accepting runs the call; declining rejects it with the user's reason. After a write that leaves a draft the account may publish, pre-approved or approved, a second prompt shows the result card and asks whether to publish it now; declining keeps the draft. The answer goes to the model with the call's result.
- A client with neither can't approve, and the call is refused with a message saying so.
A decision is bound to the record, the thread's owner and the payload hash, and approving checks the call again, as in the chat.
The site can't tell that a person clicked a card's button or answered a
prompt: it trusts the client to show them to its user, and to keep
site_agent_decide and site_agent_publish from its model. A compromised or
malicious client signed in as an account can approve and publish whatever
that account may. Connect only clients trusted as much as the account itself,
and where an approval must be a person's, make those changes in the chat. A
publish names the version of the content its card showed, and is refused
when the content changed after.
Connecting Claude¶
Claude's connectors sign in with OAuth 2.1. With MCP Server OAuth, Simple OAuth 6 and Simple OAuth 2.1's server metadata, client registration and PKCE submodules:
- Create a directory outside the web root, generate the signing keys in it with
drush simple-oauth:generate-keys DIRECTORY, which needs the directory to exist, and set the keys' paths at/admin/config/people/simple_oauth. - Grant
access mcp serverand the permissions of the toolsets they use to the roles that connect. - At
/admin/config/people/simple_oauth/oauth2_scope/dynamic, add a scope for each of those roles, with the role granularity and the authorization code and refresh token grants. A token carries the roles its scopes name that the account has, so it acts with the account's own permissions within those roles. - In Claude, add a custom connector with the URL
https://SITE/mcp. Claude registers itself through dynamic client registration and sends the user to the site to sign in and consent.
Simple OAuth refuses an authorization without scopes, and a client that registered itself has no default scopes, so the endpoint's metadata lists every scope of the authorization code grant for the client to ask for.
Claude requires the protected resource metadata to name the URL it connects
to. Simple OAuth's names the site, and MCP Server OAuth's challenge has no
resource_metadata parameter, so this submodule serves the metadata of
/mcp at /.well-known/oauth-protected-resource/mcp and names it in the
challenge, until
MCP Server OAuth #3604054
lands.
Claude Code connects the same way:
Where the client runs beside the site, it can run drush mcp:server ACCOUNT
over STDIO instead, without OAuth: whoever can run Drush already has the
site.
Coding agents¶
The submodule ships an Agent Skill,
modules/site_agent_mcp/skills/site-agent, that tells coding agents, such as
Claude Code, Codex, Gemini CLI and Cursor, how to use a site through this
server: how to connect, that its tools come from the account's toolsets, how
approvals reach each kind of client, that content writes are drafts the user
publishes, where configuration writes run and how staging starts, and to read
before writing. The site's conventions and the toolsets' sections stay in the
server's instructions, which differ per site.
drush site-agent:setup-ai copies the skill into the Composer project root:
| Host | Directory | Read by |
|---|---|---|
claude |
.claude/skills/site-agent |
Claude Code |
agents |
.agents/skills/site-agent |
Codex, Gemini CLI, Cursor and other Agent Skills clients |
all, the default |
Both |
--host picks one. --mcp also adds the site to the project's .mcp.json
as the site-agent server, keeping the file's other servers:
--mcp=http: Streamable HTTP at the site's/mcp, at the URL Drush has for the site, given with--uri. Claude Code signs in from/mcp.--mcp=stdio --account=ACCOUNT:vendor/bin/drush mcp:server ACCOUNT, orddev drush mcp:server ACCOUNTwhen the command runs in DDEV.
The STDIO entry it writes in DDEV:
{
"mcpServers": {
"site-agent": {
"command": "ddev",
"args": ["drush", "mcp:server", "editor"]
}
}
}
A file, or a site-agent entry, that differs from the one the command writes
is kept and listed as "kept", so changes made in the project aren't lost;
--force replaces it. After updating the module, compare the kept files with
the module's copies and run the command again with --force. It is a Console
command, which needs Drush 13.7 or later.
Limits¶
- Over HTTP, MCP Server holds the session's lock while a call waits for an elicitation answer, so the answer, a second request of the same session, waits for the lock's 30 second timeout before it is read: MCP Server #3585940.
visibility: ["app"]is honored by the client. The server can't tell a call from the card from a call from the model; it refuses the card's tools to clients that don't declare MCP Apps.