The connector is supervised, not autonomous: an AI drafts and edits through these tools, but it is bound by the partner's capabilities and per-file ACLs, the deny-list (it can never read or write delivery secrets), and - for write actions - the AI client's own per-call approval. A read is not audited unless it is refused or fails; writes are recorded as material events.
The connector is a single MCP endpoint, Streamable HTTP / JSON-RPC:
https://YOUR-SITE/cgi-bin/lazysite-mcp.pl
initialize and tools/list are open (discovery); a tools/call requires
authentication. An unauthenticated tool call returns HTTP 401 with a
WWW-Authenticate challenge so an OAuth client starts the sign-in flow.
Two credential shapes, same capability + ACL enforcement:
Authorization: Bearer <partner-id>:<lzs_ token> - the token comes from
Generate credential on the Users page.
whoami returns an auth block - { method: "oauth"|"bearer", expires_at } - so
the agent can see how the session is authenticated and when it lapses.
A partner's grant (visible in whoami.capabilities) gates the tools:
manage_content - read/write content pages and use the file tools (most tools).
Defaults to the webdav grant; set off for a theme-only partner.manage_themes - activate themes.manage_layouts - activate layouts.manage_domains - manage the domains this instance serves, and move whole
sites between them (the site_backup / site_apply tools; the domain-*
control-API actions). A token reaches only the domains its scope names, and
a token with no scope reaches none: the operator gives it one by naming its
group in a domain's allowed_groups. The same holds for every tool that
names a host - domain_set, preview_domain, read_nav, set_nav, the
host form of activate_theme and activate_layout - and list_domains
shows only the domains the grant reaches. A domain added or repointed must
have its content inside the scope; a grant with a scope does not reach the
primary site. Nor does it change anything the whole instance reads:
activate_theme and activate_layout without a host, install_layout,
delete_layout, the handler and schedule tools, create_theme with
activate, and on the control API the site configuration, extensions,
connectors and instance backups are refused to it; it changes its own
domains through the host forms. A grant with no scope keeps them: it
reaches the primary, whose settings they are. A theme or layout a site
serves is that site's: a confined grant writes one only when no site serves
it or only its own domains do (copy it, edit the copy, bind the copy with
host). A form is reached through the pages that bind it (form: NAME),
and a form no page binds is the primary's; a brief is its page's, or its
row's table's; form_list and list_briefs show only those the grant
reaches. A safety export is its table's domain's, and one written before
exports recorded a domain is the primary's. A page a confined grant writes,
moves, copies, uploads, restores or applies may not newly bind a form it does
not reach; editing a page that already binds one, and removing a binding,
still work.webdav - the WebDAV transport / file-API mechanism flag.manage_config - site configuration (control API, not exposed as MCP tools).manage_connectors - where site data may be sent: define connectors, set their
secrets, read the call record (control API only in this release; see
/docs/connectors). Calling a connector as a logged-in user needs no
capability - the connector's own callers groups decide.analytics - read the visitor-log analysis. Off by default; an explicit grant,
since it exposes (aggregated, IP-anonymised, path-free) log data. Visitor analysis
is available both as the MCP analyse_visitors tool AND as the control-API
analyse_visitors action (?action=analyse_visitors&window=N), so an API-channel
client gets it too.audit - read the audit trail (the in-page Audit view plus the control-API
audit action). A separate capability from analytics; off by default.Per-file ACLs (owner + read/write lists, with @groups) bind a token client
exactly as over WebDAV - a tool call is refused if the partner lacks access to the
target, regardless of capability.
The connector deliberately cannot reach operator-only surfaces. Attempts return a
machine-readable kind:
lazysite/forms/*.conf and other config - blocked-config (use the handler
tools below - save_handler, bind_form, save_schedule - rather than the
files; SMTP credentials are operator-only).lazysite/auth/*, .pl scripts, the manager - blocked.37 tools. Reads are not audited (a refused or failed read is); writes are
recorded in the audit log as material events and may trigger the AI client's per-call approval. All file tools
need manage_content unless noted.
user), capabilities, account, active layout/theme, the
full tools manifest, and the auth block (method + expiry). No capability
required. Call it first. capabilities holds exactly the capability keys, as
booleans - ui among them is manager access. account holds what the account
is rather than what it may do: groups, email, display_name, provenance
(created_by, created_at, managed_by), token and MFA state, scopes, and
interactive_login (whether it may sign in to the manager at all). The control
API's whoami carries the same two blocks from the same builder (SM821).
holds - what THIS account
currently has. No capability required. The task recipes are the sanctioned
sequences: follow them (e.g. switch-layout, restore-from-history) rather
than improvising an order.
{ path }/) with size, mtime, ext
and generated. Brief presence left the listing with SM245 - ask list_briefs.
{ path }kind: binary) and files over 512 KB
(kind: too-large).
{ path }kind:
page-encoding-refused) with the first invalid byte's offset and how to fix it;
read_file, preview_page, preview_public_page, validate_page, view_version,
read_brief and create_form answer the same.
truncated says whether it stopped there, with page_cap.
{ path }{ query, path }{ path }{ path | content }truncated
says whether it stopped there, with page_cap.
{ window }analytics
capability). Returns per-day totals, a people/AI-assistant/bot/noise breakdown,
top pages, referrers, status codes, and a capped recent event stream over the
last window days (1-365, default 30). Never the raw log, any filesystem path,
or a visitor IP. Read /docs/ai-briefing-stats for how to interpret it and what
may/may not be reported.
{ path }set_permissions.
cap) and what uses it (used_by); plus every form's handler list (forms)
and the fields each type takes (types). Opens for manage_forms,
manage_data or manage_connectors.
{ id, type, name, ... } / delete_handler { id }table
handler needs manage_data, a connector handler manage_connectors, smtp
and file need manage_forms; a refusal names the capability that would
work. A handler still used by a form or schedule entry is not deleted - the
answer names them. There is no webhook type: outbound HTTP is a connector.
{ id, handler, every, payload } / delete_schedule { id }host
for the primary site. A nav.conf that is not valid UTF-8 is refused (kind:
page-encoding-refused), naming the file, the first invalid byte's offset and
how to fix it.
manage_themes.
install_layout can pull without downloading anything. Needs manage_layouts.
When the operator has enabled the Content history plugin, every save
(manager, WebDAV, or this connector) is recorded as a version, and these tools
let you inspect and undo content changes. If list_versions returns
enabled: false, versions are not being recorded - ask the operator to enable
the plugin; do not try to build your own history. All three need
manage_content.
{ path, limit }{ path, version }{ path, version }kind: page-encoding-refused,
with the offset) and the file is left as it is.
Remote sync of the history (push/pull to a git host) is operator-only by design - it is configured and driven from the manager UI (Remote sync plugin) and is not exposed over the connector or the control API.
{ path, content }created (1 new / 0 overwrite) and runs
validate_page on the content, returning any warnings/issues. Audited as
create or edit.
{ slug, title, subtitle, body, register }{ slug }still_referenced_in (nav, other pages) for cleanup; generated indexes
refresh automatically.
{ old, new, update_links }update_links, rewrites
internal links to the old path across pages (nav.conf is not rewritten) - at most
1,000 pages, and truncated with page_cap says when it stopped there.
{ path, old, new }old is absent (no silent clobber); reports the replacement count.
{ from, to }{ from, to }{ path }delete.
{ path, read, write, draft }@groups), and the draft flag. A partial update: omitted fields keep their
current value. Setting a read list on a draft section leaves it draft -
still a 404 - so the API equivalent of the Publish button is
{"draft": false} alongside your grants. A field agent replicated the UI
flow, granted access, and the section kept 404ing; least surprise says the
trap gets named where the tool is learned.
{ form, handler } or { form, handlers: [...] }list_handlers.
Needs manage_forms only - and nothing but handler ids: to deliver
somewhere new, create the handler first.
{ items }items is an ordered list of { label, url }
(a children list becomes a sub-menu; an item with no url is a section header).
Writes nav.conf and rebuilds the cache. Needs manage_nav.
{ theme }manage_themes.
{ layout, theme }manage_layouts.
{ layout, theme, all, update, activate }manage_layouts. To switch the site to a different
layout, this one call is the whole switch - it installs AND activates.
Only delete the old layout afterwards, if at all. Use
list_layout_catalogue first to see names.
{ layout }list_layout_catalogue -> install_layout -> (optionally) delete_layout.
A recovery snapshot is kept. Needs manage_layouts.
{ summary, good, bad, rating, context }{ path }"*" for all). A normal write
already clears its own page; use this for pages that embed another.
A site package is a portable snapshot of ONE domain's site: its content, its
navigation, the theme and layout it uses, and its presentation settings. It
deliberately excludes plugins, instance settings and any secrets, so it is safe
to hand to another instance. Use it to turn an agency demo into a client's live
site, or to copy a site to another domain. Both tools need manage_domains and
access to the domain.
{ host }lazysite-site-<host>-<stamp>.tar.gz in the backups area (fetch it with the
backup download if you need to move it to another instance). Returns the
package name.
{ name, host, clean }host to apply to the default site; clean: true clears
the target content first. Apply overwrites the target - take a backup first
if you want a rollback point. The target domain must already be registered
(domain-add on the control API) unless you are applying to the default site.
The end-to-end recipe (migrate-site) is in describe_capabilities under
tasks: site_backup on the source -> (download + site-backup-upload if a
different instance) -> site_apply on the target -> verify.
A failed tool result is { ok: 0, error, kind }. The kind lets an agent tell
causes apart: blocked, blocked-config, not-found, permission, binary,
too-large, invalid-path. A 401 carries error.data.reason -
sign-in-incomplete (no credential reached the server - re-authorise the
connector) vs credential-invalid (expired/revoked - reconnect).
Over the control API (/cgi-bin/lazysite-manager-api.pl) the same refusal
also answers with an HTTP status taken from its kind (SM670): forbidden,
permission, blocked, disabled 403; not-found 404 (an unknown action too);
exists, in-use, confirm 409; too-large 413; rate 429; a failure on the
server 500; partial - part of the write happened - 207; cannot-tell 503;
anything else, and a refusal with no kind, 400. The body keeps ok: false either
way, so a client may key on either. MCP tool results are JSON-RPC and stay 200.
cannot-tell is not a refusal of your account. It means an auth store the
engine needs to answer "what may this account do" exists and the web server
cannot read it - a file permission fault on the host. Your credential verified;
nothing was decided about your grants, so do not ask for one. The body names the
store (store, a docroot-relative path such as
lazysite/auth/groups-settings.json) and the repair (repair: lazysite check
--docroot <docroot> --fix, run by the operator). Every call answers this until
the store is readable, whoami included, rather than reporting an empty grant.
Over MCP the same answer is JSON-RPC error -32003 with HTTP 503 and kind,
store and repair in error.data.
When the account store itself cannot be read, the engine cannot check your
credential at all, so it has not verified you: you get cannot-tell 503 with
no store and no repair, and a message saying it is not a verdict on your
credential. Do not reissue the credential; the operator's log names the cause.
whoami - confirm identity, capabilities, and that tools are loaded.list_files / list_pages / read_page - orient; read before you edit.validate_page (with content) - catch front-matter / form / public-data
issues before writing.write_file or replace_text - make one change at a time.preview_page - confirm the render in-channel (not a web fetch). page_status
confirms it will reach visitors.audit_site after a set of changes - catch broken links / orphans / duplicates.Pages are Markdown files served at their path (about.md → /about); a page that
registers in a feed lists it in front matter (register: [sitemap, llms]).