These hooks surround HappenBoard’s AI agent surface: the abilities that let an AI agent read and change your events through the WordPress Abilities API and HappenBoard’s built-in MCP server. Use them to add your own abilities behind the same guards, hide built-in ones, choose what MCP clients are served, forward the audit trail or tune the rate limits. The site owner’s side is in AI agents and the Abilities API. If you are new to hooks, start with Extending HappenBoard with hooks.
When the abilities exist
Nothing is registered while Expose HappenBoard to AI agents is off under HappenBoard → Settings → AI agents, which is the default. The surface also stays off when HAPPENBOARD_DISABLE_ABILITIES is true in wp-config.php, and on WordPress versions without the Abilities API (it arrived in 6.9). In those cases the registration hooks never fire and the MCP server does not exist. Only the log retention filter still runs, along with the two OAuth checks the AI agents settings screen reads.
The site owner gets one switch on purpose. A developer who needs finer control removes abilities with happenboard_registered_abilities.
What every ability gets
Abilities declared through \HappenBoard\Service\Abilities\Registry::ability(), HappenBoard’s and Pro’s alike, share the same guards:
- No anonymous access: a logged-out caller is always refused, even for a read.
- A capability check: the user needs the ability’s capability, or
manage_options. - A rate budget per user: 300 reads a minute, 60 writes a minute and 600 writes an hour.
- Abilities marked destructive, such as deletions, need
"confirm": truein their input. - Every write is announced on
happenboard_ability_executedand recorded in the agent log, refused calls included.
An ability registered directly with wp_register_ability() gets none of this.
Calling an ability over REST
AI clients such as Claude or ChatGPT connect through HappenBoard’s MCP server, described in the next section. Your own code and scripts can call abilities directly: they run through the core route /wp-json/wp-abilities/v1/abilities/{name}/run, with a signed-in user (an application password, for example). Every HappenBoard ability that is not a write is marked read-only.
Call read-only abilities with GET and their input in the query string, such as ?input[id]=42. Call writes with POST and a JSON body { "input": { ... } }. The wrong method gets HTTP 405.
The booking-type abilities also read drafts. happenboard/create-booking-type makes a draft, and happenboard/get-booking-type returns it with its status, so an agent can read back, configure and publish what it has just made.
What MCP clients are served
HappenBoard’s MCP server answers on POST /wp-json/happenboard/mcp/v1, with no other plugin needed. Each ability becomes a tool named after it, with __ in place of the slash: happenboard/list-events is the tool happenboard__list-events. A client signs in with OAuth or sends an application password, and each call goes through the ability’s own guards listed above.
The server narrows the tool list in this order, and each step can only remove:
- Only abilities declared through the
Registry, and never a destructive one. Pass'mcp_public' => falseto keep one of yours off the MCP server. - Only the abilities the user’s capabilities allow.
- A connection approved as Read only on the consent screen gets the read tools alone, and is refused a write tool called by name.
happenboard_mcp_read_only can make every connection read-only, and happenboard_mcp_served_abilities has the last word on the list. A tool that is not served cannot be called by name. These filters change the MCP server only, not the core REST route above.
What the agent log keeps
Each write gets a row: who acted, which ability, the input with secrets replaced by [redacted], the outcome and when. A refused or failed write also keeps its error code and the message the agent received, in the language of that request, up to 255 characters. The Agent activity panel under HappenBoard → Settings → AI agents shows them. A write refused for want of permission is listed as Not allowed.
happenboard_register_abilities
Fires on wp_abilities_api_init, after HappenBoard registers its own abilities. Register yours here so they follow the same switch: when the agent surface is off, it never fires.
Signature
do_action( 'happenboard_register_abilities' )
Example
add_action( 'happenboard_register_abilities', function () {
\HappenBoard\Service\Abilities\Registry::ability(
'list-sponsors',
array(
'namespace' => 'acme',
'label' => 'List sponsors',
'description' => 'Returns the sponsor names attached to an event.',
'category' => 'happenboard-events',
'cap' => 'happenboard_manage_events',
'input_schema' => array(
'type' => 'object',
'properties' => array( 'event_id' => array( 'type' => 'integer' ) ),
'required' => array( 'event_id' ),
),
'output_schema' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
'execute' => fn ( $input ) => acme_sponsors_for( (int) $input['event_id'] ),
)
);
} );
Notes
- No arguments. Both
wp_register_ability()and HappenBoard’sRegistryare available here. Registry::ability( $name, $args )takeslabel,description,category,cap,execute,input_schemaandoutput_schema, plus the optionalnamespace(defaulthappenboard),write,destructiveandmcp_public.- Use your own namespace, as the example does, and one of the free plugin’s categories:
happenboard-events,happenboard-booking,happenboard-attendeesorhappenboard-site. WordPress rejects an ability whose category does not exist. - HappenBoard Pro registers its abilities on this action at priority 10, then fires
happenboard_pro_register_abilities.
happenboard_pro_register_abilities
Fires after HappenBoard Pro registers its own abilities, inside the free plugin’s happenboard_register_abilities. Use it for add-ons that build on Pro features, such as tickets.
Signature
do_action( 'happenboard_pro_register_abilities' )
Example
add_action( 'happenboard_pro_register_abilities', function () {
\HappenBoard\Service\Abilities\Registry::ability(
'flag-sponsor-ticket',
array(
'namespace' => 'acme',
'label' => 'Flag a sponsor ticket',
'description' => 'Marks one ticket type as a sponsor ticket.',
'category' => 'happenboard-commerce',
'cap' => 'happenboard_manage_tickets',
'write' => true,
'input_schema' => array(
'type' => 'object',
'properties' => array( 'ticket_id' => array( 'type' => 'integer' ) ),
'required' => array( 'ticket_id' ),
),
'output_schema' => array( 'type' => 'object', 'properties' => array( 'ok' => array( 'type' => 'boolean' ) ) ),
'execute' => fn ( $input ) => array( 'ok' => acme_flag_sponsor_ticket( (int) $input['ticket_id'] ) ),
)
);
} );
Notes
- No arguments. It never fires when the agent surface is off or Pro is inactive.
- The
happenboard-commercecategory exists only when Pro is active, so abilities in it belong on this hook. - Set
writeto true for anything that changes data: the ability then uses the write budget, is announced onhappenboard_ability_executedand is called withPOST. Adddestructiveto requireconfirm: true. - Pro’s own abilities use the
happenboard-pronamespace. Keep yours in your own.
happenboard_registered_abilities
Filters each ability HappenBoard is about to register. Remove its name and the ability is never registered, so agents do not see it at all instead of being refused when they call it.
Signature
apply_filters( 'happenboard_registered_abilities', array $names )
Parameters
-
$namesarray<int,string> - A one-item array holding the full name of the ability being registered, for example
happenboard/delete-event.
Return value
array<int,string>
The names to keep. Return the array without the name to drop that ability.
Example
add_filter(
'happenboard_registered_abilities',
fn ( $names ) => array_diff(
$names,
array( 'happenboard/delete-event', 'happenboard/delete-booking-type' )
)
);
Notes
- It runs once per ability, not once for the whole list. Compare with the names you want to remove instead of expecting a full list.
- It is not a permission check: each ability already checks the user’s capability. Use it to make the surface smaller.
- It sees every ability declared through HappenBoard’s
Registry, Pro’s included (happenboard-pro/names), but not abilities registered directly withwp_register_ability(). - A removed ability is never registered, so it is gone for MCP clients too.
happenboard_ability_executed
Fires after a write ability is called, whatever the outcome, including calls refused before they ran. The agent log listens here; hook it to forward the trail to your own logging or alerts.
Signature
do_action( 'happenboard_ability_executed', string $name, array $input, mixed $result, string $status )
Parameters
-
$namestring - Full ability name, for example
happenboard/create-event. -
$inputarray<string,mixed> - The input, validated against the ability’s schema. It may contain credentials: redact it before storing it, as the agent log does.
-
$resultmixed - What the ability returned, or the
WP_Errorthe call was refused with. -
$statusstring ok,error(the ability said no, for example to a booking that is not pending),failed(an exception was caught),rate_limited,unconfirmed(a destructive call withoutconfirm: true) orforbidden(the user lacks the capability).
Example
add_action(
'happenboard_ability_executed',
function ( $name, $input, $result, $status ) {
if ( ! in_array( $status, array( 'forbidden', 'rate_limited' ), true ) ) {
return;
}
error_log( sprintf(
'Agent call %s by user %d: %s (%s)',
$name,
get_current_user_id(),
$status,
is_wp_error( $result ) ? $result->get_error_message() : ''
) );
},
10,
4
);
Notes
- Four arguments are passed, so register with
10, 4. - Reads never fire it. Only abilities declared with
writethrough HappenBoard’sRegistrydo. - A
forbiddencall fires it from the permission check, before the ability runs, once per ability per request and only for a signed-in user.$resultis then ahappenboard_forbiddenerror (HTTP 403). - An exception thrown by a listener is caught, so it cannot turn a completed write into an error. It still stops the listeners after it, the agent log at priority 10 included.
happenboard_abuse_guard_limits
Filters the rate budgets applied to every ability call, per user. They stop a looping agent, or a leaked application password, from exhausting the site: raise them with care.
Signature
apply_filters( 'happenboard_abuse_guard_limits', array $limits )
Parameters
-
$limitsarray<string,int> - Keys
read_per_minute(300),write_per_minute(60) andwrite_per_hour(600).
Return value
array<string,int>
The budgets to apply. A missing key falls back to its default, and every value is at least 1.
Example
add_filter(
'happenboard_abuse_guard_limits',
function ( $limits ) {
// A nightly sync job needs more write room than a chat agent.
$limits['write_per_hour'] = 2000;
return $limits;
}
);
Notes
- Budgets are counted per user, not per site, so one busy integration does not slow down the others.
- Over budget, the call answers HTTP 429 with
happenboard_rate_limitand aretry_afterof 60 or 3600 seconds. A write over budget is recorded asrate_limited. - Only abilities declared through HappenBoard’s
Registryare budgeted. The public RSVP and booking forms have their own limits and are not affected.
happenboard_abuse_guard_denial_threshold
Fires the first time a user reaches ten refused ability calls within five minutes. A stolen credential probing the surface produces refusals, so this is the signal to alert on.
Signature
do_action( 'happenboard_abuse_guard_denial_threshold', int $actor_id, int $count )
Parameters
-
$actor_idint - ID of the signed-in user. Logged-out callers are refused before anything is counted.
-
$countint - Refusals in the current window: always 10, the threshold.
Example
add_action(
'happenboard_abuse_guard_denial_threshold',
function ( $actor_id, $count ) {
wp_mail(
get_option( 'admin_email' ),
'HappenBoard: repeated agent refusals',
sprintf( 'User %d was refused %d times in five minutes.', $actor_id, $count )
);
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - Only permission refusals count, for reads and writes alike. A request that lists the abilities, as an MCP client does for its tools, counts at most one refusal, however many abilities the user may not use. Rate-limited and unconfirmed calls do not count.
- It fires once per window. Each refusal restarts the five-minute window, so a caller who keeps trying fires it again only after five quiet minutes and ten new refusals.
happenboard_agent_log_retention_days
Filters how many days agent log entries are kept before the daily clean-up deletes them.
Signature
apply_filters( 'happenboard_agent_log_retention_days', int $days )
Parameters
-
$daysint - Retention in days. Default
90.
Return value
int
Days to keep. Return 0 or less to turn the clean-up off and keep entries forever.
Example
// Keep a full year for a compliance requirement.
add_filter( 'happenboard_agent_log_retention_days', fn () => 365 );
Notes
- The Agent activity panel reads the same value to say how long entries are kept, so the screen matches the actual clean-up.
- The clean-up is a daily WP-Cron event,
happenboard_prune_agent_log, scheduled whether or not the agent surface is on. This filter runs even when the surface is off. - A WordPress personal data erasure removes the person’s email and details from the logged input of the entries that mention them, whatever the retention. In the logged error message, their email becomes
[erased]. The entries themselves stay.
happenboard_mcp_read_only
Filters whether the MCP connection being served may only read. Return true and the client gets the read tools alone, whatever was approved on the consent screen.
Signature
apply_filters( 'happenboard_mcp_read_only', bool $read_only )
Parameters
-
$read_onlybool - True when the connection’s OAuth token was approved for reading only. False for a full token and for an application password.
Return value
bool
True to limit the connection to the read tools. Returning false never widens a token approved for reading only.
Example
add_filter(
'happenboard_mcp_read_only',
function ( $read_only ) {
// Only administrators may let an AI client change data.
return $read_only || ! current_user_can( 'manage_options' );
}
);
Notes
- It runs after sign-in, so the current user is the account the client acts as. It also covers clients connected with an application password, which have no scope of their own.
- While it returns true for a user, their consent screen offers the Read only choice alone, since no write tool would be served.
- It narrows the MCP server only. The same account can still run a write ability through the core
/wp-abilities/v1route. To take a write away everywhere, usehappenboard_registered_abilitiesor the user’s capabilities.
happenboard_mcp_served_abilities
Filters the abilities the MCP server hands the current connection, after the Registry, capability and scope checks. A tool removed here is not listed, and a client that calls it by name gets an unknown tool error.
Signature
apply_filters( 'happenboard_mcp_served_abilities', array $served )
Parameters
-
$servedarray<string,WP_Ability> - Abilities about to be served, keyed by full name, for example
happenboard/list-attendees.
Return value
array<string,WP_Ability>
The abilities to serve. Anything other than an array serves nothing. On a read-only connection, a write ability you add is removed again.
Example
add_filter(
'happenboard_mcp_served_abilities',
function ( $served ) {
// Keep attendee data away from AI clients.
foreach ( array_keys( $served ) as $name ) {
if ( str_contains( $name, 'attendee' ) ) {
unset( $served[ $name ] );
}
}
return $served;
}
);
Notes
- The ability stays registered, unlike with
happenboard_registered_abilities: the core/wp-abilities/v1route and your own code can still run it. - You may add an ability from another plugin, a
WP_Abilityfromwp_get_ability(). It then runs with its own permission callback only, without HappenBoard’s rate budgets or agent log. - It runs several times in one request (the tool list, each call, the consent screen), so keep it cheap.
- The consent screen counts tools from this list, so the person approving sees what the client will get. The tool list on the AI agents settings screen does not go through it.
happenboard_mcp_allowed_origins
Filters the origins allowed to call the MCP endpoint from a web page. A request whose Origin header is not on the list is refused with HTTP 403 and happenboard_mcp_invalid_origin, which stops another site from driving the endpoint through a signed-in browser.
Signature
apply_filters( 'happenboard_mcp_allowed_origins', array $allowed )
Parameters
-
$allowedarray<int,string> - Absolute URLs. Default
home_url()andsite_url().
Return value
array<int,string>
The URLs to accept. Only the scheme, host and port are compared, and a default port is ignored.
Example
add_filter(
'happenboard_mcp_allowed_origins',
function ( $origins ) {
// A browser dashboard on another subdomain talks to the MCP server.
$origins[] = 'https://app.example.com';
return $origins;
}
);
Notes
- A request without an
Originheader is not checked. Browsers send one, while server-side connectors and desktop clients usually do not, so most clients never reach this filter. - The check runs before sign-in, on every request to the endpoint, so keep it cheap.
happenboard_mcp_oauth_enabled
Filters whether the MCP server offers OAuth sign-in. Return false and clients can only connect with an application password.
Signature
apply_filters( 'happenboard_mcp_oauth_enabled', bool $enabled )
Parameters
-
$enabledbool - True when WordPress has
wp_fast_hash()(6.8 and later), which HappenBoard uses to hash tokens.
Return value
bool
Whether the discovery, registration, token, revocation and consent endpoints answer.
Example
// Connections use application passwords only on this site.
add_filter( 'happenboard_mcp_oauth_enabled', '__return_false' );
Notes
- Off, tokens already issued stop working: every call made with one gets HTTP 401 with
happenboard_mcp_oauth_disabled. - The Connect an AI client card reads the same value. Off, it says OAuth sign-in is turned off and shows the application password steps for each client.
- It also runs when the AI agents settings screen loads, even with the agent surface off.
happenboard_mcp_oauth_transport_is_secure
Filters whether the site counts as served securely enough for OAuth sign-in. By default HTTPS passes, and so do a loopback host (localhost, 127.0.0.1, ::1, any *.localhost) and a local environment type.
Signature
apply_filters( 'happenboard_mcp_oauth_transport_is_secure', bool $secure, string $url )
Parameters
-
$securebool - Result of the built-in check on the MCP endpoint URL.
-
$urlstring - The MCP endpoint URL, for example
https://example.com/wp-json/happenboard/mcp/v1.
Return value
bool
False refuses OAuth on this site. True cannot make plain HTTP work: see the notes.
Example
add_filter(
'happenboard_mcp_oauth_transport_is_secure',
function ( $secure, $url ) {
// Require HTTPS for OAuth, even on a local site.
return $secure && str_starts_with( $url, 'https://' );
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - Use it to take OAuth away. Each OAuth request also needs
is_ssl(), a loopback host or alocalenvironment, so returning true on a plain HTTP site changes the setup card but every sign-in is still refused. - Behind a proxy that ends HTTPS, make
is_ssl()true inwp-config.php(fromX-Forwarded-Proto, for example) rather than using this filter.
happenboard_mcp_protected_resource_metadata
Filters the RFC 9728 protected resource metadata, the document that tells a client where to sign in. It is served at /.well-known/oauth-protected-resource/wp-json/happenboard/mcp/v1 and at the REST alias /wp-json/happenboard/mcp/v1/oauth-protected-resource.
Signature
apply_filters( 'happenboard_mcp_protected_resource_metadata', array $document )
Parameters
-
$documentarray<string,mixed> - Keys
resource(the MCP endpoint URL),authorization_servers,scopes_supported(mcpandmcp.read),bearer_methods_supportedandresource_name(“HappenBoard on” and the site name).
Return value
array<string,mixed>
The document, sent as JSON.
Example
add_filter(
'happenboard_mcp_protected_resource_metadata',
function ( $document ) {
// A clearer name, and a page about your rules for AI access.
$document['resource_name'] = 'Acme Theatre events';
$document['resource_documentation'] = 'https://example.com/ai-access/';
return $document;
}
);
Notes
- Add or reword informative fields only. Tokens are still issued for, and checked against, the real endpoint URL, so a changed
resourceorauthorization_serversbreaks sign-in. - The document is public, and the well-known URL sends it with
Access-Control-Allow-Origin: *. Put nothing private in it. - It never runs while OAuth is off: the REST alias then answers 404, and the well-known URL is left to WordPress.
happenboard_mcp_authorization_server_metadata
Filters the RFC 8414 authorization server metadata: the sign-in, token, registration and revocation endpoints a client discovers. It is served at /.well-known/oauth-authorization-server/wp-json/happenboard/mcp and at /wp-json/happenboard/mcp/v1/oauth-authorization-server.
Signature
apply_filters( 'happenboard_mcp_authorization_server_metadata', array $document )
Parameters
-
$documentarray<string,mixed> - Keys
issuer, the four endpoints, and what is supported: scopesmcpandmcp.read, theauthorization_codeandrefresh_tokengrants, PKCE withS256and the client authentication methods.
Return value
array<string,mixed>
The document, sent as JSON.
Example
add_filter(
'happenboard_mcp_authorization_server_metadata',
function ( $document ) {
// Point clients at your documentation and your AI access policy.
$document['service_documentation'] = 'https://example.com/ai-access/';
$document['op_policy_uri'] = 'https://example.com/ai-access-policy/';
return $document;
}
);
Notes
- Add informative fields only. Clients check that
issuermatches the URL they fetched the document from, and HappenBoard does not follow changed endpoints, so editing either breaks sign-in. - The issuer has its own path,
/wp-json/happenboard/mcp, rather than the site root. Another plugin’s OAuth server on the same site, such as the SEOPress one, keeps its own document. - Like the protected resource document, it is public and never runs while OAuth is off.
Next steps
- AI agents and the Abilities API: how a site owner turns the surface on and connects an agent.
- Admin, roles and capability hooks: the capabilities each ability checks, and how roles get them.
- Pro roles and permissions: the Pro capabilities behind the ticket and check-in abilities.