Rationale and cross-ability notes behind the generated ability reference.
Most abilities are gated to edit_posts by a single plugin-wide permission check. Two differ: search-users requires list_users, because it returns email addresses, and get-site-info requires nothing at all. That gate is deliberately coarse: it establishes that the caller edits content on this site and nothing more, so any ability touching a specific object does its own per-object check on top of it (read_post in get-post, edit_post in update-post, the target resource’s own permission callback in read-mcp-resource).
It is a trivial ability anyone can use as a connection test. Everything it can return already appears in the markup of every front-end page. What keeps that safe is a fixed, deliberately non-filterable allowlist: requested fields are intersected against it before anything reaches get_bloginfo(), which would otherwise hand back admin_email, the WordPress version and the template paths. audit-abilities flags its __return_true permission callback; that warning is accepted.
get-post is constrained to the readable post-type allowlist (post and page by default). content_raw is always complete; content_markdown is the part that can be absent. markdown_source takes one of five values. lh-content-converter means the conversion ran (before 1.11.0 this value was lh-mcp-block-transformer). empty means the post has no content, so content_markdown is an empty string. classic means the post has no block markup to convert (classic-editor content, a bare shortcode or embed URL); it is expected rather than an error, and content_raw should be read and edited in whatever form it was saved. unavailable means LH Content Converter is not active, and failed means it ran and reported an error; both are genuine problems, and notice carries what to tell the user.
list-posts is deliberately not narrowed to the readable allowlist, since it returns titles, slugs, links and dates rather than content. Whether it should be narrowed is a real question, and an open one.
Both write abilities are draft-only by construction rather than by default: there is no status parameter in either input schema (inputs are closed, so passing one is refused), and the status is written as a literal. No call through either can publish, schedule, or set a member-visibility status. Publishing stays a human act.
create-post is restricted to the writable post-type allowlist (post and page by default, filterable, and deliberately separate from the readable one) and checks the type’s own create_posts capability. Everything is validated before the insert runs, so a failure on meta or terms leaves no empty draft behind. It is annotated non-idempotent: repeating a call creates a second draft.
update-post has patch semantics: a field is written only if it was supplied, so the natural way to retitle a post is to send a title alone. Which posts may be touched is decided by an allowlist of current statuses (draft, pending, auto-draft) rather than by excluding publish, so a status nobody anticipated is refused by default. Anything already live is refused with a pointer to lh-agora/propose-changes. It is annotated destructive, because supplied fields replace what was there and the post’s revisions are the only way back, and idempotent, because repeating the same call leaves the post in the same state.
search-users ranks across nine priorities: exact beats partial, and last name beats first name. Multi-word input runs an AND pass first and falls back to OR if that yields nothing. Each result reports which field and match type determined its rank.
list-mcp-resources and read-mcp-resource let a caller discover and read abilities registered as MCP resources rather than tools, instead of a human attaching them by hand. read-mcp-resource invokes the target exactly as the MCP resources/read handler does, and its content is whatever the target returns, typed as any JSON value. audit-abilities asks for an items schema on it because the type list includes array; that warning is accepted.