@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/portfolio/lh-mcp-buddypress-actions/>
  a sioc:Post ;
  dc:title "Notes" ;
  dcterms:identifier 153120 ;
  dc:modified "2026-11-01T22:12:09Z"^^xsd:dateTime ;
  dc:created "2026-11-01T22:12:09Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-mcp-buddypress-actions/notes/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-mcp-buddypress-actions/">LH MCP BuddyPress Actions</a></li></ul>
<h2 class="wp-block-heading">Who may change what</h2>



<ul class="wp-block-list">

<li>Every ability needs BuddyPress&#8217;s <code>bp_moderate</code>, or being an admin of the group named in the call, so a team captain can manage their own team. BuddyPress re-checks the same thing itself on remove, ban, promote and demote.</li>


<li>Only members of the site the call runs on can be added. With LH Membership active that means an approved member, so pending applicants and unclaimed accounts are refused as <code>not_site_member</code>.</li>


<li>Callers with <code>install_plugins</code> are unrestricted: the site member requirement, site rules and the unloaded-plugin refusal are skipped for them, with a warning when site plugins were not loaded.</li>


<li>For everyone, a group always keeps an admin, and an admin must be demoted before being banned.</li>

</ul>



<h2 class="wp-block-heading">Site rules come first</h2>



<p class="wp-block-paragraph">A site&#8217;s own plugins can refuse any change through the <code>lh_mcp_buddypress_actions_refuse_change</code> filter: return a non-empty reason to refuse. It runs on dry runs too, so a dry run shows what a real run would refuse. Princes Park Touch uses it (in ppt-buddypress-extender) for its one team per format per night rule.</p>



<p class="wp-block-paragraph">Because those rules live in plugins active on that site only, a <code>site_id</code> call from another site&#8217;s connector would skip them. A real change is therefore refused when the target site has plugins this call did not load, and a dry run only warns: use the site&#8217;s own connector for such changes.</p>



<h2 class="wp-block-heading">Filters</h2>



<ul class="wp-block-list">

<li><code>lh_mcp_buddypress_actions_refuse_change</code> ( <code>$reason</code>, <code>$action</code>, <code>$user_id</code>, <code>$group_id</code>, <code>$role</code> ): <code>$action</code> is add, remove, ban or set_role; return a reason string to refuse.</li>


<li><code>lh_mcp_buddypress_actions_is_site_member</code> ( <code>$is_member</code>, <code>$user_id</code>, <code>$site_id</code> ): whether a user counts as a member of the site.</li>

</ul>



<h2 class="wp-block-heading">How changes are made</h2>



<ul class="wp-block-list">

<li>Every change goes through BuddyPress&#8217;s own functions so group hooks run: <code>groups_join_group</code>, <code>groups_accept_membership_request</code>, <code>groups_accept_invite</code>, <code>groups_remove_member</code>, <code>groups_ban_member</code>, <code>groups_promote_member</code> and <code>groups_demote_member</code>. The caller is passed as the group admin, as BuddyPress&#8217;s REST API does.</li>


<li>A pending membership request or invitation is accepted rather than duplicated. An unconfirmed legacy membership row is refused rather than joined over.</li>


<li>After each change the membership is read back, so a join hook that backs a join out is reported as <code>refused</code>.</li>


<li>The optional <code>note</code> is set with <code>lh_audit_trail_set_note()</code> just before each change and cleared after, so it attaches only to that change&#8217;s LH Audit Trail entry.</li>

</ul>



<h2 class="wp-block-heading">Related plugins</h2>



<ul class="wp-block-list">

<li><a href="/portfolio/lh-mcp-buddypress-queries/">lh-mcp-buddypress-queries</a>: the read-only companion (group members, membership status, groups, activity)</li>


<li>LH MCP Adapter Extensions: adds <code>site_id</code> to site-scoped abilities and returns <code>site_notice</code></li>

</ul>
"""^^rdf:XMLLiteral ;
  sioc:content """Part of: LH MCP BuddyPress Actions
Who may change what





Every ability needs BuddyPress&#8217;s bp_moderate, or being an admin of the group named in the call, so a team captain can manage their own team. BuddyPress re-checks the same thing itself on remove, ban, promote and demote.


Only members of the site the call runs on can be added. With LH Membership active that means an approved member, so pending applicants and unclaimed accounts are refused as not_site_member.


Callers with install_plugins are unrestricted: the site member requirement, site rules and the unloaded-plugin refusal are skipped for them, with a warning when site plugins were not loaded.


For everyone, a group always keeps an admin, and an admin must be demoted before being banned.





Site rules come first



A site&#8217;s own plugins can refuse any change through the lh_mcp_buddypress_actions_refuse_change filter: return a non-empty reason to refuse. It runs on dry runs too, so a dry run shows what a real run would refuse. Princes Park Touch uses it (in ppt-buddypress-extender) for its one team per format per night rule.



Because those rules live in plugins active on that site only, a site_id call from another site&#8217;s connector would skip them. A real change is therefore refused when the target site has plugins this call did not load, and a dry run only warns: use the site&#8217;s own connector for such changes.



Filters





lh_mcp_buddypress_actions_refuse_change ( $reason, $action, $user_id, $group_id, $role ): $action is add, remove, ban or set_role; return a reason string to refuse.


lh_mcp_buddypress_actions_is_site_member ( $is_member, $user_id, $site_id ): whether a user counts as a member of the site.





How changes are made





Every change goes through BuddyPress&#8217;s own functions so group hooks run: groups_join_group, groups_accept_membership_request, groups_accept_invite, groups_remove_member, groups_ban_member, groups_promote_member and groups_demote_member. The caller is passed as the group admin, as BuddyPress&#8217;s REST API does.


A pending membership request or invitation is accepted rather than duplicated. An unconfirmed legacy membership row is refused rather than joined over.


After each change the membership is read back, so a join hook that backs a join out is reported as refused.


The optional note is set with lh_audit_trail_set_note() just before each change and cleared after, so it attaches only to that change&#8217;s LH Audit Trail entry.





Related plugins





lh-mcp-buddypress-queries: the read-only companion (group members, membership status, groups, activity)


LH MCP Adapter Extensions: adds site_id to site-scoped abilities and returns site_notice


""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1>, <https://lhero.org/?taxonomy=lh_object_key&term=lh-portfolio_doc_20261101t111500-bpa-notes> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=lh_object_key&term=lh-portfolio_doc_20261101t111500-bpa-notes> rdfs:seeAlso <https://lhero.org/?taxonomy=lh_object_key&term=lh-portfolio_doc_20261101t111500-bpa-notes&feed=lhrdf&format=turtle> .
