WPFormationWordPress, rien que du WordPress
Formation WordPress + IA
IA & Claude Code · Qualiopi · OPCO
Je me forme
Trois encadrés : quatre capacités déjà présentes, huit points d'accroche, quatre refus silencieux.
WordPress & IA

Abilities API WordPress 7.1 : le guide complet du développeur

Par Fabrice Ducarme··Mis à jour le ·20 min de lecture
Résumé de l'article

L’Abilities API est dans le cœur de WordPress depuis la version 6.9, et 7.1 y ajoute la validation personnalisée et le suivi des appels. Pour enregistrer votre première ability, il faut passer quatre obstacles que les exemples qui circulent ne signalent pas : l’enregistrement doit se faire dans un hook précis, la propriété category est obligatoire (elle deviendra facultative avec WordPress 7.2), la catégorie doit exister avant, et elle se déclare sur une action différente. Le code complet, testé sur un WordPress 7.1 en production, est dans cet article.

Pas le temps ? Faites-le analyser par l'IA

Vous me lisez souvent ? Dites-le à Google

J'ajoute WPFormation à mes sources

On lit partout que l’Abilities API sert à donner des capacités aux agents IA plutôt que de leur ouvrir votre base en grand. C’est vrai, et c’est une bonne idée.

Mais entre le premier wp_register_ability() et une ability qui répond vraiment, il y a quatre portes fermées. Je les ai toutes prises dans la figure, dans l’ordre, sur un WordPress 7.1 en production. Aucune n’est mise en avant dans les tutoriels que j’ai lus, alors que deux d’entre elles figurent noir sur blanc dans la référence officielle de la fonction.

Alors voici le tutoriel que j’aurais voulu trouver : le code qui marche, les quatre messages d’erreur que vous allez croiser, et ce qu’ils veulent dire.

Une ability, c’est quoi au juste ?

Une ability est une capacité déclarée : un nom, une description, un schéma d’entrée, un schéma de sortie, une fonction qui fait le travail, et une règle de permission. Un agent qui interroge votre site voit la liste de ce qu’il peut demander, avec le format exact attendu.

En quoi c’est différent de la REST API, qui existe depuis des années ? La REST API de WordPress expose des ressources : des articles, des utilisateurs, des médias. L’agent doit deviner comment les combiner pour arriver à ses fins. Une ability expose une intention : "compter les articles", "vérifier le stock", "planifier une publication". Le schéma dit ce qu’elle attend, la permission dit qui a le droit, et vous gardez la main sur ce que la machine peut faire chez vous.

L’API PHP est arrivée dans le cœur avec WordPress 6.9, en décembre 2025. Son pendant JavaScript a suivi en 7.0, la version WordPress 7 "Armstrong". Le dépôt séparé qui servait à la développer a été archivé en février 2026 : tout vit désormais dans le cœur, et il n’y a plus rien à installer.

4 classes et 15 points d’accroche dans le cœur de WordPress

Avant d’écrire quoi que ce soit, j’ai voulu savoir à quoi je touchais. L’Abilities API pèse 2 506 lignes dans le cœur de WordPress 7.1, réparties en cinq fichiers. Plus d’un tiers tient dans une seule classe :

class-wp-ability.php                       901 lignes
abilities-api.php                          792 lignes
class-wp-abilities-registry.php            340 lignes
class-wp-ability-categories-registry.php   256 lignes
class-wp-ability-category.php              217 lignes

Côté objets, quatre classes seulement. Voici leurs méthodes publiques, telles que je les ai relevées :

WP_Ability          get_name() get_label() get_description() get_category()
                    get_input_schema() get_output_schema() get_meta() get_meta_item()
                    normalize_input() validate_input() check_permissions() execute()

WP_Ability_Category get_slug() get_label() get_description() get_meta()

WP_Abilities_Registry            register() unregister() get_all_registered()
                                 is_registered() get_registered() get_instance()
WP_Ability_Categories_Registry   les mêmes six méthodes

Une asymétrie mérite d’être notée, parce qu’elle m’a valu une erreur fatale de plus : WP_Ability possède un get_name(), mais WP_Ability_Category n’en a pas. Pour une catégorie, c’est get_slug(). Les deux objets se ressemblent assez pour qu’on suppose la même interface, et ils ne l’ont pas.

Combien de points d’accroche, au juste ? La note de version en cite deux. En cherchant les do_action et apply_filters dans les fichiers du cœur, j’en ai compté quinze :

Tableau des quinze points d'accroche de l'Abilities API, classés par usage.
Quinze points d’accroche relevés dans le cœur, quand la note de version n’en cite que deux.

Deux d’entre eux sont particulièrement utiles. wp_register_ability_args intercepte les propriétés de toute ability au moment de son enregistrement, y compris celles du cœur et celles des extensions : de quoi forcer une permission plus stricte sur une capacité que vous n’avez pas écrite. Et wp_get_abilities_item_include décide si une ability apparaît dans la liste, ce qui suffit à masquer une capacité sans la désenregistrer.

Les 7 nouveautés de l’API dans WordPress 7.1

La version 7.1, sortie le 19 août 2026, enrichit l’API sur sept points, et ses versions de maintenance, jusqu’à la 7.1.4 du 28 octobre, n’y ont rien changé. Deux comptent vraiment pour du code de production.

  • wp_ability_validate_input et wp_ability_validate_output : deux filtres pour ajouter vos propres règles de validation, au-delà de ce que le schéma vérifie.
  • wp_ability_invoked : une action déclenchée au début de chaque exécution, faite pour l’audit et la télémétrie.
  • Cinq champs de profil supplémentaires sur core/get-user-info, plus une propriété fields pour ne demander qu’un sous-ensemble, et cette capacité devient joignable par REST.
  • Des schémas cohérents entre les capacités du cœur, avec titres et descriptions traduisibles.
  • La conversion, avant exécution, de l’entrée d’un appel REST aux types déclarés dans le schéma : les chaînes deviennent entiers, booléens ou tableaux.
  • Un drapeau public dans meta, qui déclare en une fois qu’une capacité est destinée aux clients extérieurs (REST, adaptateurs MCP, agents) et sert de valeur par défaut à show_in_rest.
  • Des critères de filtrage pour wp_get_abilities() : par catégorie, par espace de noms ou par valeur de meta.

Sans les deux premiers, votre validation devait vivre dans la fonction d’exécution, et vous n’aviez aucun moyen propre de savoir quel agent appelait quoi.

La conversion des types suit le schéma d’entrée. Avec un champ déclaré entier, le 10 reçu dans l’adresse devient l’entier 10 ; avec un champ déclaré chaîne, il reste une chaîne. Cependant, WordPress ne convertit que ce qui passe déjà la validation : une entrée invalide reste telle quelle et part en erreur, et une ability sans schéma d’entrée reçoit la valeur brute. C’est cette valeur convertie qui arrive dans votre permission_callback, puis dans votre fonction d’exécution.

Les 4 refus que vous allez rencontrer au premier essai

J’ai lancé mon premier essai comme n’importe qui : une fonction, un tableau de propriétés, un appel. Voici ce qui s’est passé, étape par étape. Chaque message est copié de la sortie réelle.

Les quatre refus rencontrés avant qu'une ability s'enregistre, du mauvais moment à la mauvaise action.
Les quatre refus, dans l’ordre où on les rencontre. Les trois premiers ne disent rien sans WP_DEBUG.

Porte 1 : appeler la fonction au bon moment

Appelée directement, wp_register_ability() ne fait rien. Pas d’erreur fatale, pas d’avertissement visible… la fonction rend la main, et l’ability n’existe pas. Mon test affichait simplement "enregistrée : NON".

L’enregistrement doit se faire dans un callback accroché à l’action wp_abilities_api_init. Attention : en dehors de cette fenêtre, le registre refuse.

Porte 2 : la propriété category est obligatoire

Une fois dans le bon hook, deuxième refus, et cette fois avec un message :

WP_Abilities_Registry::register :
Les propriétés de l'"ability" doivent contenir une chaîne pour `category`.

La plupart des exemples que j’ai croisés donnent un tableau avec label, description, execute_callback et permission_callback. Il manque category, et sans elle rien ne s’enregistre.

Ce qui change avec WordPress 7.2 : cette porte-là va disparaître. Dans la version en développement, une ability déclarée sans category est rangée d’office dans une catégorie uncategorized fournie par le cœur. Une catégorie inexistante reste refusée, la porte suivante tient donc toujours. La 7.2 est annoncée pour début décembre 2026 ; d’ici là, sur toute la série 7.1, la propriété reste obligatoire.

Porte 3 : la catégorie doit exister avant

J’ai donc ajouté 'category' => 'site-information', un nom qui me semblait raisonnable. Troisième refus :

La catégorie de l'"ability" "site-information" n'est pas enregistrée.
Veuillez enregistrer la catégorie avant de l'assigner à l'"ability".

La catégorie n’est pas une étiquette libre mais un objet déclaré. Sur mon site, trois seulement existaient : site, user, et yoast-seo. Vous pouvez réutiliser une catégorie du cœur, ou déclarer la vôtre.

Porte 4 : la catégorie se déclare ailleurs

J’ai donc appelé wp_register_ability_category() juste avant mon ability, dans le même hook. Quatrième refus, et celui-là je ne l’avais pas vu venir :

Toutes les catégories des "abilities" doivent être inscrites
sur l'action wp_abilities_api_categories_init.

Il y a donc deux actions distinctes, et l’ordre compte : wp_abilities_api_categories_init pour les catégories, puis wp_abilities_api_init pour les abilities. Mettre les deux au même endroit ne marche pas… et c’est exactement là que j’ai perdu le plus de temps.

Les quatre refus sont silencieux côté navigateur : ils passent par le mécanisme interne de WordPress qui signale les mauvaises pratiques. Sans WP_DEBUG actif, vous ne voyez rien du tout, juste une ability qui n’existe pas. C’est la première chose à activer avant de chercher pourquoi votre code ne fait rien.

Le code complet d’une ability qui compte vos articles

Voici l’ability que j’ai fait tourner : elle compte les articles publiés, avec un filtre facultatif par catégorie. Simple… mais elle utilise tout ce qui compte, y compris les annotations que lira un agent. Placez ce fichier dans wp-content/mu-plugins/ ou dans votre extension.

<?php
/**
 * Plugin Name: Ability compter les articles
 */

// 1. La catégorie, sur SON action.
add_action( 'wp_abilities_api_categories_init', function () {
    wp_register_ability_category( 'wpformation', array(
        'label'       => 'WPFormation',
        'description' => 'Capacités maison.',
    ) );
} );

// 2. L'ability, sur la sienne.
add_action( 'wp_abilities_api_init', function () {
    wp_register_ability( 'wpformation/compter-articles', array(
        'label'         => 'Compter les articles publiés',
        'description'   => "Renvoie le nombre d'articles publiés, filtré par catégorie si besoin.",
        'category'      => 'wpformation',
        'input_schema'  => array(
            'type'       => 'object',
            'properties' => array(
                'categorie' => array( 'type' => 'string', 'description' => 'Slug de catégorie.' ),
            ),
            'additionalProperties' => false,
        ),
        'output_schema' => array(
            'type'       => 'object',
            'properties' => array(
                'total'     => array( 'type' => 'integer' ),
                'categorie' => array( 'type' => 'string' ),
            ),
            'required'   => array( 'total' ),
        ),
        'execute_callback' => function ( $input ) {
            $args = array( 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => 1 );
            if ( ! empty( $input['categorie'] ) ) {
                $args['category_name'] = $input['categorie'];
            }
            $q = new WP_Query( $args );
            return array(
                'total'     => (int) $q->found_posts,
                'categorie' => isset( $input['categorie'] ) ? $input['categorie'] : 'toutes',
            );
        },
        'permission_callback' => function () {
            return current_user_can( 'read' );
        },
        'meta' => array(
            'annotations'  => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
            'public'       => true,
            'show_in_rest' => true,
        ),
    ) );
} );

Et ce 'public' => true, à quoi sert-il ? C’est le drapeau de la 7.1 vu plus haut : il rend show_in_rest vrai par défaut. Je laisse quand même show_in_rest explicite, pour que l’exposition HTTP se lise d’un coup d’œil.

Si les deux drapeaux se contredisent, c’est show_in_rest qui l’emporte dès que vous l’écrivez. Ainsi, une ability déclarée public avec show_in_rest à false reste hors de l’API REST. De plus, aucun des deux ne remplace la permission : ils règlent la visibilité, et le permission_callback reste le seul contrôle d’accès.

Et la permission, on la met où ? Un mot sur permission_callback. Dans mon test je l’ai ouvert pour aller vite, mais en production c’est votre garde-barrière : il décide qui peut déclencher la capacité. Une ability sans permission sérieuse revient à laisser une porte ouverte, avec une étiquette dessus.

Voir une ability dans la liste ne veut pourtant pas dire pouvoir l’exécuter. En effet, pour la liste REST, WordPress 7.1 vérifie seulement show_in_rest et le droit de lecture général du compte connecté. Il n’appelle jamais le permission_callback de chaque ability. Un simple abonné voit donc aussi les capacités dont la permission est réservée aux administrateurs, et le refus n’arrive qu’à l’appel de /run. Pour retirer une ability de toutes les listes, liste REST comprise, j’utilise le filtre wp_get_abilities_item_include. Les options item_include_callback et result_callback, elles, ne trient qu’un appel PHP précis à wp_get_abilities(). La 7.2, encore en bêta, ajoute une propriété required_capabilities : un client pourra alors demander la liste des seules capacités que son compte peut lancer (ticket Trac #66325).

Brancher la validation et la télémétrie de 7.1

Le schéma ne suffit-il pas à valider les entrées ? Non, il vérifie les types sans rien connaître du site. Mon schéma accepte n’importe quelle chaîne comme catégorie, y compris une qui n’existe pas sur le site : l’ability répond alors par un total de 0, sans signaler l’erreur. Le filtre wp_ability_validate_input va plus loin : il peut refuser une catégorie inconnue.

Je garde dans le schéma tout ce qu’il sait décrire : le type, les champs obligatoires, les valeurs permises et les limites. Le filtre ne sert qu’aux règles qui dépendent du site, comme l’existence réelle d’une catégorie. Techniquement, il peut aussi accepter une entrée que le schéma a refusée, mais cela masquerait un schéma incomplet au lieu de le corriger.

// Validation métier : au-delà du type, la réalité du site.
add_filter( 'wp_ability_validate_input', function ( $valide, $input, $ability ) {
    $nom = is_object( $ability ) ? $ability->get_name() : (string) $ability;
    if ( is_wp_error( $valide ) || 'wpformation/compter-articles' !== $nom || empty( $input['categorie'] ) ) {
        return $valide;
    }
    if ( ! term_exists( $input['categorie'], 'category' ) ) {
        return new WP_Error( 'categorie_inconnue',
            'Catégorie inexistante : ' . $input['categorie'] );
    }
    return $valide;
}, 10, 3 );

// Télémétrie : une ligne de journal par appel.
add_action( 'wp_ability_invoked', function ( $ability ) {
    $nom = is_object( $ability ) ? $ability->get_name() : (string) $ability;
    error_log( 'ability appelée : ' . $nom );
}, 10, 1 );

Le piège des arguments : là où vous attendez l’ability, ces hooks vous passent son nom, une chaîne, pas l’objet WP_Ability. Le nom arrive en premier argument sur wp_ability_invoked, en troisième sur wp_ability_validate_input, dont le premier est le résultat de la validation en cours. J’ai écrit $ability->get_name() en lisant trop vite la description officielle, et j’ai récolté une erreur fatale, deux fois de suite. L’objet existe bien, mais plus loin dans la liste : en troisième position sur wp_ability_invoked, et pas du tout sur wp_ability_validate_input, qui ne transmet que le nom. D’où le is_object() dans le code ci-dessus : il fonctionne dans les deux cas.

Dans quel ordre se déclenchent les 8 points d’accroche ?

Reste à savoir quand ces filtres se déclenchent. J’ai accroché les huit points d’accroche du cycle d’exécution à un journal, puis appelé core/get-site-info. Le résultat n’est pas celui que j’attendais.

Les huit points d'accroche d'une ability dans leur ordre réel, relevé sur WordPress 7.1.
L’ordre réel des huit points d’accroche. Le premier se déclenche avant tout contrôle.

Regardez la position de wp_ability_invoked. Il arrive en premier, avant la normalisation, avant la validation du schéma, et avant le contrôle des droits. Autrement dit : il se déclenche même quand l’appel va être refusé pour permission insuffisante, et même quand l’entrée est invalide.

La conséquence est directe pour quiconque veut compter les appels. Ce hook mesure toutes les tentatives, réussies ou non. Si vous branchez votre journal dessus en pensant suivre l’usage réel de vos capacités, vos chiffres compteront aussi les agents qui se sont fait refouler. Pour compter les appels aboutis, c’est wp_after_execute_ability qu’il faut prendre, en huitième position.

Le couple à brancher pour un vrai journal : wp_ability_invoked pour la tentative, wp_after_execute_ability pour l’aboutissement. L’écart entre les deux compteurs vous donne exactement le nombre d’appels rejetés, et c’est souvent la ligne la plus intéressante du journal.

Pourquoi votre test échoue alors que le code est bon

Voilà le piège qui m’a coûté le plus de temps après les quatre refus, et il n’a rien à voir avec l’API.

J’ai enregistré ma capacité, elle apparaissait bien dans la liste, et l’appel me renvoyait ceci :

check_permissions() : false
execute() : WP_Error ability_invalid_permissions
  L'"ability" "core/get-site-info" n'a pas les droits suffisants.

Le code n’y était pour rien. En ligne de commande, WordPress n’a aucun utilisateur connecté, donc current_user_can() répond faux quelle que soit la capacité demandée. Le même appel, précédé d’une ligne, passe sans broncher :

wp_set_current_user( 1 );    // l'identifiant d'un compte administrateur
$res = $ability->execute( array( 'fields' => array( 'name', 'version' ) ) );

// {"name":"WPFormation","version":"7.1"}

Le symptôme est trompeur parce qu’il ressemble à une erreur de déclaration : une capacité qui existe, qui refuse, et un message qui parle de droits alors que vous êtes root sur le serveur. Avant de suspecter votre permission_callback, vérifiez donc qui WordPress croit être.

Vérifiez que votre code marche vraiment

Le plus rapide est de passer par WP-CLI, sans rien écrire de définitif. Voici la sortie réelle de mon test sur le serveur, telle quelle :

enregistree      : OUI
execute([])                            : {"total":340,"categorie":"toutes"}
execute({"categorie":"ecosysteme-wordpress"}) : {"total":18,"categorie":"ecosysteme-wordpress"}
execute({"categorie":"nimportequoi"})  : WP_Error [categorie_inconnue]
                                         Categorie inexistante : nimportequoi
execute({"inconnu":1})                 : WP_Error [ability_invalid_input]
                                         inconnu n'est pas une propriete valide de l'objet.
telemetrie       : 4 appel(s) -> wpformation/compter-articles
total abilities  : 5

Quatre lignes, quatre enseignements. L’ability répond et renvoie de vrais chiffres, ceux de mon site : 340 articles publiés, 18 dans la catégorie interrogée. Les deux refus suivants ne viennent pas du même endroit, et c’est tout l’intérêt : categorie_inconnue est ma règle métier, branchée sur le filtre de 7.1, tandis que ability_invalid_input vient du schéma lui-même, sans que j’aie rien écrit. Et la télémétrie compte bien les quatre appels, y compris les deux qui ont échoué.

Nota : sous WP-CLI, les deux actions d’initialisation sont déjà passées quand votre code s’exécute. Pour un test à la volée, il faut les rejouer avec do_action(). Dans une extension normale, vous n’avez pas ce souci.

Autre vérification utile, par exemple pour savoir ce qui tourne déjà chez vous : wp_get_abilities() retourne le registre complet, ou une partie seulement avec les critères de filtrage de la 7.1, et wp_get_ability( 'nom' ) une capacité précise.

Les 4 capacités déjà présentes sur mon site

Tableau des quatre capacités déclarées sur le site, avec leurs annotations et leur exposition.
Les quatre capacités présentes sans installation, et les annotations qu’un agent lit avant d’appeler.

Avant d’écrire la moindre ligne, j’ai regardé ce qui était déjà enregistré chez moi. Quatre capacités, dont une qui ne vient pas du cœur :

  • core/get-site-info
  • core/get-user-info
  • core/get-environment-info
  • yoast-seo/get-seo-scores

Yoast SEO déclare donc déjà sa propre ability, avec sa propre catégorie yoast-seo. Si vous avez Yoast, un agent qui interroge correctement votre site peut lui demander vos scores de référencement, et vous ne l’avez jamais décidé. Ce n’est pas un reproche : l’usage est exactement celui-là… mais autant le savoir.

Deuxième surprise, plus technique, et elle ne venait pas de WordPress. Sur mon propre site, l’espace de noms REST wp-abilities/v1 est bien déclaré, mais aucune route n’y répond : chaque appel renvoie une erreur "aucune route correspondante". J’ai d’abord cru que la couche PHP était là et l’exposition HTTP non. Je détaille plus bas d’où venait vraiment ce 404, avec les deux réponses côte à côte : c’est le piège dans lequel je suis tombé le premier.

Vos capacités répondent-elles en HTTP ? Oui, sauf si vous les avez bloquées vous-même

C’est le point que je cherchais depuis le début, parce qu’il décide de ce que vous pouvez faire aujourd’hui avec tout ça. Et il m’a fait tomber dans un piège que je vous raconte en entier, parce que vous risquez le même.

Les quatre capacités présentes sur mon site portent toutes show_in_rest => true. L’espace de noms wp-abilities/v1 est bien déclaré par le serveur REST. Tout indique une exposition par HTTP. J’ai donc envoyé la requête :

GET /wp-json/wp-abilities/v1

statut : 404
{"code":"rest_no_route",
 "message":"Aucune route correspondante à l'URL et à la méthode de requête n'a été trouvée."}

Un espace de noms déclaré et vide. J’ai d’abord cru que le transport n’existait pas encore, puis j’ai mis ce 404 sur le compte de ma version de WordPress. C’était faux deux fois. Le cœur enregistre ces routes depuis la 6.9, et la 7.0 comme la 7.1 les ont. Ce qui les masquait, c’est un durcissement que j’avais posé moi-même : un petit fichier dans mu-plugins qui retire de l’API REST les espaces de noms que je n’utilise pas, et wp-abilities figurait dans sa liste. Je l’avais oublié. Sur une installation sans ce filtre, la réponse change du tout au tout.

GET /wp-json/wp-abilities/v1          → 200

/wp-abilities/v1                                GET
/wp-abilities/v1/abilities                      GET
/wp-abilities/v1/abilities/<name>               GET
/wp-abilities/v1/abilities/<name>/run           GET, POST, PUT, PATCH, DELETE
/wp-abilities/v1/categories                     GET
/wp-abilities/v1/categories/<slug>              GET

Six routes, dont une /run que l’index annonce avec cinq verbes. À l’exécution, une capacité n’en accepte qu’un, selon ses annotations : GET si elle est en lecture seule, DELETE si elle est destructive et idempotente, POST dans tous les autres cas. Les autres verbes reçoivent une 405 (rest_ability_invalid_method), d’après le contrôle de méthode du cœur. Les routes ne sortent pas d’un plugin, et elles ne datent pas de la 7.1 : le cœur les instancie lui-même depuis la 6.9, dans wp-includes/rest-api.php, au milieu des contrôleurs historiques, entre les collections de polices et les icônes. Trois classes tiennent la boutique : la liste, le détail par catégorie, et l’exécution.

Reste la question qui compte vraiment : qui peut appeler ces routes ? Sans authentification, la réponse est nette.

GET /wp-json/wp-abilities/v1/abilities

statut : 401
{"code":"rest_forbidden",
 "message":"Désolé, vous n'avez pas l'autorisation de faire cela."}

La route existe, elle refuse simplement les anonymes. Un agent extérieur authentifié peut donc bel et bien lister vos capacités et les exécuter à distance. Il lui faut un mot de passe d’application et les droits correspondant à la capacité visée. C’est possible depuis la 6.9, sur tout site qui n’a pas filtré l’espace de noms. Ce que la 7.1 ajoute côté HTTP est plus modeste : core/get-user-info devient joignable par REST, et l’entrée d’un appel est convertie aux types du schéma avant l’exécution.

Deux conséquences pratiques. La première : un 404 sur ces routes ne vient pas de WordPress, il vient de chez vous. Cherchez une extension de sécurité ou un fichier maison qui filtre rest_endpoints, et souvenez-vous que l’espace de noms reste annoncé dans l’index même quand ses routes ont été retirées. La seconde, plus sérieuse : dès qu’une capacité est exposée en REST, par show_in_rest ou par le drapeau public de la 7.1, la permission que vous écrivez dans permission_callback n’est plus une précaution théorique. C’est elle qui décide si un appel venu d’Internet exécute votre code. Traitez-la comme une règle de sécurité, pas comme une formalité.

Faut-il pour autant tout ouvrir ? Non, et le passage par un serveur MCP garde tout son intérêt : il vous laisse choisir quelles capacités sont relayées, sous quelle identité et avec quelle journalisation, au lieu d’exposer l’ensemble du registre à quiconque détient un jeton valide. Déclarer une ability reste gagnant dans les deux cas : vous écrivez une fois le contrat de votre capacité, schémas et permissions compris, vous héritez d’une validation d’entrée de bonne qualité, et vous êtes désormais joignable par HTTP sans une ligne de code de transport.

Une capacité ou un point d’entrée REST ?

La question revient à chaque fois, alors posons les deux côte à côte. Ce sont deux outils différents, pas deux générations du même.

Point d’entrée RESTAbility
Ce qui est exposéUne ressourceUne intention
Contrat d’entréeÀ écrire soi-mêmeSchéma JSON, validé par le cœur
Contrat de sortieAucunSchéma JSON, vérifié après exécution
DécouverteIndex du serveur RESTwp_get_abilities(), en PHP
Message d’erreurCelui que vous écrivezGénéré, traduit, avec le nom du champ fautif
Sécurité par défautpermission_callback obligatoirepermission_callback et annotations
Appelable en HTTPOuiOui depuis la 6.9, authentification requise

La ligne qui compte est l’avant-dernière. Les annotations n’ont pas d’équivalent côté REST : une route ne dit pas d’elle-même si elle est destructive ou si on peut la rejouer sans dommage. C’est une information écrite pour des machines qui décident seules, et c’est bien ce qui change avec cette API.

Le bon moment pour s’y mettre

Mon avis, après ces essais : l’idée est bonne et la mise en œuvre est rugueuse.

Bonne, parce que déclarer des capacités vaut mieux que distribuer des clés d’API et espérer. Vous décidez de ce qu’un agent peut demander, dans quel format, et sous quelle permission. C’est une façon saine de laisser une machine travailler sur un site sans lui ouvrir la maison.

Rugueuse, parce que quatre refus successifs pour une capacité de dix lignes, c’est trois de trop. Aucun n’est difficile à corriger une fois qu’on sait. Le problème, c’est qu’on ne sait pas : les messages n’arrivent que si le mode débogage est actif. J’ai publié dix extensions WordPress, et j’ai quand même passé une bonne demi-heure sur une histoire d’action mal choisie.

Alors, pour qui c’est utile aujourd’hui ? Si vous développez une extension et que vos utilisateurs commencent à brancher des assistants sur leur site, comme je le fais moi-même avec Claude Code au quotidien, oui, déclarez vos capacités maintenant : c’est peu de code et ça vous place au bon endroit. Si vous gérez des sites clients sans développer, il n’y a rien à faire pour l’instant. Le seul geste utile : regarder qui utilise déjà cette porte chez vous, ce qu’une heure d’expertise WordPress suffit à établir.

La couche est dans le cœur, elle grossit à chaque version, et elle ne partira pas. Autant l’apprendre pendant qu’elle tient encore en cinq fichiers…

Questions fréquentes

Un agent extérieur peut-il appeler mes abilities ?

Oui, à condition qu’il soit authentifié et que la capacité soit déclarée avec show_in_rest, ou avec le drapeau public de la 7.1, qui l’active par défaut. Le cœur enregistre six routes sous wp-abilities/v1 depuis WordPress 6.9, dont une /run pour exécuter une capacité à distance. Sans authentification, la réponse est une 401 : la route existe, elle refuse les anonymes. Si vous obtenez une 404, c’est qu’un réglage de sécurité de votre site a retiré l’espace de noms : c’était mon cas, et je l’avais oublié.

Pourquoi mon test en ligne de commande refuse les droits ?

Parce qu’en ligne de commande, aucun utilisateur n’est connecté : current_user_can() répond faux quoi qu’il arrive, et l’exécution renvoie ability_invalid_permissions. Ajoutez wp_set_current_user() avec l’identifiant d’un compte administrateur avant votre appel, et le même code passe.

Quelle version de WordPress faut-il pour utiliser les abilities ?

L’API PHP est disponible depuis WordPress 6.9, avec un pendant JavaScript ajouté en 7.0. Les filtres de validation et l’action de suivi des appels présentés ici sont en revanche des nouveautés de la 7.1, sortie le 19 août 2026. Sur une version antérieure, le code s’enregistre mais la validation personnalisée ne se déclenche pas.

Mon ability ne s’enregistre pas et je n’ai aucune erreur, pourquoi ?

C’est le cas le plus fréquent, et il a quatre causes possibles : l’appel se fait hors de l’action wp_abilities_api_init, la propriété category manque (elle ne deviendra facultative qu’avec la 7.2), la catégorie indiquée n’existe pas, ou elle a été déclarée ailleurs que sur wp_abilities_api_categories_init. Activez WP_DEBUG : les quatre cas produisent un message explicite qui reste invisible sans lui.

Peut-on appeler une ability depuis l’extérieur en HTTP ?

Oui, depuis WordPress 6.9, avec une authentification : le cœur enregistre six routes sous wp-abilities/v1, dont une /run qui exécute la capacité. Vérifiez tout de même sur votre propre site avant de bâtir une intégration dessus : interrogez /wp-json/wp-abilities/v1 et regardez si des routes apparaissent. Une 401 signifie que la route existe et réclame des identifiants, une 404 qu’un filtre de votre site l’a retirée.

Faut-il déclarer sa propre catégorie ou réutiliser celles du cœur ?

Les deux fonctionnent. Le cœur fournit site et user, plus uncategorized à partir de la 7.2, et un plugin peut ajouter la sienne, comme le fait Yoast avec yoast-seo. Déclarer la vôtre, nommée d’après votre extension, évite les collisions et regroupe vos capacités. C’est quatre lignes de code sur l’action dédiée.

Quelle différence avec un serveur MCP branché sur WordPress ?

Les deux servent à donner des capacités à un agent, mais à des étages différents. Une ability est une capacité déclarée dans WordPress, réutilisable par n’importe quel consommateur. Un serveur MCP est un protocole de dialogue entre un assistant et un service. Les deux se combinent très bien : les abilities décrivent ce que le site sait faire, le serveur MCP l’expose à l’assistant. Sur ce second volet, je vous invite à lire mon guide pour connecter Claude à un site WordPress.

Guide PDF gratuit

La chaîne WordPress + IA de ce site, en 12 étapes. Le guide vous donne la méthode pour la refaire à la main.

De la veille à la mesure, avec Claude Code, sur un WordPress classique.

  • La carte des 12 étapes du workflow
  • Une checklist et un prompt par étape
  • 3 skills de départ à copier
  • Les garde-fous nés de vrais incidents

Double opt-in : un email de confirmation à valider pour recevoir le guide. Newsletter max 2/mois. WPFormation utilise les données saisies, via Brevo, pour votre inscription et les emails annoncés, avec votre consentement. Données jamais revendues. Désabonnement en 1 clic. Vos droits et la conservation de vos données

Voir le détail du guide et de la version complète
Fabrice Ducarme, formateur WordPress
Fabrice Ducarme

Référence francophone WordPress depuis 2012. Expert en IA (Claude, Gemini) et développement Headless (Next.js), je forme les professionnels à maîtriser l'écosystème web d'aujourd'hui et de demain.

Comment j'utilise l'IA pour écrire ces articles