Agent and ability hooks

Available in:Free

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": true in their input.
  • Every write is announced on happenboard_ability_executed and 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' => false to 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

Action Free src/Service/Abilities/Bootstrap.php

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’s Registry are available here.
  • Registry::ability( $name, $args ) takes label, description, category, cap, execute, input_schema and output_schema, plus the optional namespace (default happenboard), write, destructive and mcp_public.
  • Use your own namespace, as the example does, and one of the free plugin’s categories: happenboard-events, happenboard-booking, happenboard-attendees or happenboard-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

Action Pro src/Service/Abilities/Bootstrap.php

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-commerce category exists only when Pro is active, so abilities in it belong on this hook.
  • Set write to true for anything that changes data: the ability then uses the write budget, is announced on happenboard_ability_executed and is called with POST. Add destructive to require confirm: true.
  • Pro’s own abilities use the happenboard-pro namespace. Keep yours in your own.

happenboard_registered_abilities

Filter Free src/Service/Abilities/Registry.php

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

$names array<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 with wp_register_ability().
  • A removed ability is never registered, so it is gone for MCP clients too.

happenboard_ability_executed

Action Free src/Service/Abilities/Registry.php

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

$name string
Full ability name, for example happenboard/create-event.
$input array<string,mixed>
The input, validated against the ability’s schema. It may contain credentials: redact it before storing it, as the agent log does.
$result mixed
What the ability returned, or the WP_Error the call was refused with.
$status string
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 without confirm: true) or forbidden (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 write through HappenBoard’s Registry do.
  • A forbidden call fires it from the permission check, before the ability runs, once per ability per request and only for a signed-in user. $result is then a happenboard_forbidden error (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

Filter Free src/Service/Security/AbuseGuard.php

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

$limits array<string,int>
Keys read_per_minute (300), write_per_minute (60) and write_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_limit and a retry_after of 60 or 3600 seconds. A write over budget is recorded as rate_limited.
  • Only abilities declared through HappenBoard’s Registry are budgeted. The public RSVP and booking forms have their own limits and are not affected.

happenboard_abuse_guard_denial_threshold

Action Free src/Service/Security/AbuseGuard.php

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_id int
ID of the signed-in user. Logged-out callers are refused before anything is counted.
$count int
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

Filter Free src/Service/Abilities/AuditLog.php

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

$days int
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

Filter Free src/Service/Abilities/Mcp/Exposure.php

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_only bool
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/v1 route. To take a write away everywhere, use happenboard_registered_abilities or the user’s capabilities.

happenboard_mcp_served_abilities

Filter Free src/Service/Abilities/Mcp/Exposure.php

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

$served array<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/v1 route and your own code can still run it.
  • You may add an ability from another plugin, a WP_Ability from wp_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

Filter Free src/Service/Abilities/Mcp/Server.php

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

$allowed array<int,string>
Absolute URLs. Default home_url() and site_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 Origin header 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

Filter Free src/Service/Abilities/Mcp/OAuth.php

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

$enabled bool
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

Filter Free src/Service/Abilities/Mcp/OAuth.php

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

$secure bool
Result of the built-in check on the MCP endpoint URL.
$url string
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 a local environment, 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 in wp-config.php (from X-Forwarded-Proto, for example) rather than using this filter.

happenboard_mcp_protected_resource_metadata

Filter Free src/Service/Abilities/Mcp/OAuth.php

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

$document array<string,mixed>
Keys resource (the MCP endpoint URL), authorization_servers, scopes_supported (mcp and mcp.read), bearer_methods_supported and resource_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 resource or authorization_servers breaks 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

Filter Free src/Service/Abilities/Mcp/OAuth.php

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

$document array<string,mixed>
Keys issuer, the four endpoints, and what is supported: scopes mcp and mcp.read, the authorization_code and refresh_token grants, PKCE with S256 and 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 issuer matches 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