Remote clients¶
The Site Agent MCP submodule (site_agent_mcp, experimental) offers an
account's toolset 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 toolset a new chat thread would start with, and the
tools that toolset offers it in the chat, 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, the toolset's 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 toolset's permission 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.
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.