---
title: "@nodefony/devkit — l'outillage de développement d'une application"
source: src/packages/@nodefony/devkit/docs/index.md
url: https://nodefony.github.io/nodefony-core/docs/modules/devkit/
---


> L'outillage de développement d'une application : sa carte de visite, les
> skills qui disent à un agent comment faire les tâches courantes, et les portes
> qui mènent au reste.

Cette page est **surfacée dans Studio** (onglet Docs du module).

📍 [Documentation](../../) › **@nodefony/devkit**

## Par où commencer

```nodefony-cards
[
  { "icon": "🪪", "title": "La carte de visite", "href": "#le-problème-quil-résout",
    "desc": "`npx nodefony card` : qui répond ici, quels modules, où lire, quoi lancer. Servie par le cœur — elle marche même sans application construite." },
  { "icon": "🔌", "title": "Le serveur MCP", "href": "#le-serveur-mcp--les-mêmes-réponses-en-outils",
    "desc": "Les mêmes réponses, mais en outils qu'un agent appelle — dont l'autorisation OAuth 2.1 et vos propres outils." },
  { "icon": "🎓", "title": "Les skills d'agent", "href": "#les-skills-dagent--répondre-à--comment-fait-on-ça-ici--",
    "desc": "Ce qui répond à « comment fait-on ça, ici ? » sans que l'agent invente une convention." },
  { "icon": "🚫", "title": "Ce qu'il ne fait pas", "href": "#ce-quil-ne-fait-pas",
    "desc": "La frontière du module — et pourquoi il n'existe pas en production." }
]
```

## Le problème qu'il résout

Une application Nodefony sait beaucoup de choses sur elle-même — ses modules, ses
routes, sa configuration, la documentation installée avec chaque paquet. Mais
elle ne le **dit** à personne. Celui qui arrive — un développeur qui reprend le
projet, un agent qui code — n'a d'autre choix que de deviner : lire les sources,
supposer une convention, inventer une route.

Le devkit répond à la question d'ouverture, et à elle seule : **qui répond ici,
et où faut-il aller ensuite ?**

```bash
npx nodefony card               # -j pour du JSON (| jq)
```

La réponse tient en trois blocs : l'identité (nom, version, environnement, cœur),
les modules, puis **où lire** et **quoi lancer**.

Cette commande-là est servie par le **cœur**, pas par ce module : une carte de
visite qui exigerait une application déjà construite, ou une variable
d'environnement posée, serait fermée au moment exact où l'on en a besoin. Elle ne
lit que des fichiers — et quand rien n'a démarré, elle annonce des modules
**installés** plutôt que chargés, en renvoyant à `npx nodefony inspect modules`.

## Pourquoi il n'existe pas en production

Ce que la carte expose — modules chargés, chemins de documentation, commandes —
aide pendant le développement. En production, c'est une description de votre
architecture offerte à qui la demande : une divulgation, pas une fonctionnalité.

D'où la double protection, et les deux moitiés comptent :

```ts
// nodefony.config.ts — posé par `nodefony create app`
use("@nodefony/devkit", {}, { policy: "dev" }),
```

- **`devDependencies`** : `npm ci --omit=dev` ne l'installe pas ;
- **`policy: "dev"`** : un déploiement qui installerait tout ne le charge pas
  quand même. Un module non chargé n'est **même pas importé** — le coût en
  production est nul, pas « faible ».

Corollaire à connaître : hors développement, c'est **la route** qui n'existe pas.
La commande, elle, répond — elle ne passe pas par ce module.

## Quatre portes, une seule source

| Porte                           | Pour qui                                                     |
| ------------------------------- | ------------------------------------------------------------ |
| `npx nodefony card`             | un agent, un humain au terminal — servie par le cœur         |
| `GET /nodefony/devkit/api/card` | Studio, un script authentifié — modules réellement CHARGÉS   |
| `POST /nodefony/mcp`            | un agent qui appelle des **outils** (Model Context Protocol) |
| `buildCard()` (export du cœur)  | une porte de plus, à écrire — rien à réimplémenter           |

**Ajouter une porte n'ajoute jamais une vérité** : toutes lisent le même
service, qui dérive le même Kernel. La construction elle-même vit dans une
fonction pure (`buildCard`) qui reçoit son état au lieu de le lire — c'est ce qui
la rend éprouvable sans serveur, et ce qui l'empêche d'inventer quoi que ce soit.

> La route de la carte vit sous `/nodefony/<module>/api`, que le pare-feu d'une
> application réelle couvre : un agent qui code ne s'authentifie pas et n'a pas
> de navigateur. La porte qui compte pour lui est la commande — ou le serveur
> MCP ci-dessous.

## Le serveur MCP — les mêmes réponses, en outils

```bash
npx nodefony ai:mcp        # écrit .mcp.json ; --dry-run pour voir sans écrire
```

Neuf outils. Quatre sont les commandes que vous connaissez déjà :
`nodefony_inspect` (ce qui est monté), `nodefony_check` (ce qui manque),
`nodefony_symbols` (ce qu'une API signifie), `nodefony_card` (par où commencer).
`nodefony_docs` sert la documentation des modules. `nodefony_admin_list` et
`nodefony_admin_call` ouvrent les lectures du data plane d'administration — le
catalogue, puis l'appel — et exigent un jeton porteur du scope `admin:read` :
sans lui, ils n'apparaissent pas dans la liste. `nodefony_observe` mesure ce que l'application fait réellement de ses requêtes, pendant qu'elle tourne ; `nodefony_profile_route` mesure une route en lui envoyant lui-même ses requêtes (sections suivantes).

**Il n'y a pas de process à lancer.** Depuis la révision `2026-07-28` du
transport, un serveur MCP est un endpoint `POST` sans session : c'est donc une
route de votre application. Elle vit tant qu'elle tourne, suit chaque
rechargement du serveur de développement, et n'a aucun cache à invalider.

**La déclaration reste dans VOTRE projet.** `ai:mcp` écrit `.mcp.json` à la
racine du projet — jamais dans une configuration globale. Ce n'est pas un détail
de rangement : l'URL porte un **port**, et deux applications Nodefony ouvertes en
même temps n'écoutent pas sur le même. Une déclaration globale en désignerait
forcément une, au hasard. (Pour Mistral Vibe, l'équivalent par projet est
`.vibe/config.toml`, section `[[mcp_servers]]`.)

**Cinq révisions servies** — `2026-07-28`, `2025-11-25`, `2025-06-18`,
`2025-03-26`, `2024-11-05` — et le serveur **répond celle que le client
demande**. Annoncer la plus récente à tout le monde rend la porte injoignable :
un SDK qui ne connaît pas encore cette révision raccroche, et le serveur le plus
conforme du monde ne parle alors à personne.

Trois choses à savoir avant de s'étonner d'un refus :

- une **adresse non locale** reçoit `403` (`mcp.allowRemote`) ;
- une **origine de navigateur** non déclarée reçoit `403` (`mcp.allowedOrigins`).
  Un client MCP natif n'envoie pas d'`Origin` ; une page web en envoie toujours
  un — c'est ce qui ferme le détournement DNS, seul vecteur réel contre un
  serveur local ;
- le module étant `policy: "dev"`, **la route n'existe pas en production**.

Les outils intégrés sont en lecture seule, et `mcp.tools` est une allowlist.

### Mesurer l'application qui tourne — `nodefony_observe`

Un agent de code qui vient de modifier un contrôleur ne voit pas l'effet de son
changement : la barre de debug et la console d'administration affichent les
mesures du profileur, mais il ne les lit pas. `nodefony_observe` les lui rend.
Pendant une fenêtre de 1 à 50 secondes (10 par défaut), l'outil relève le
profileur de développement toutes les deux secondes et rend un rapport :

1. un **verdict** — `ok`, `anomalies (N)`, et `INCOMPLÈTE` quand les chiffres ne
   portent pas sur tout (profils perdus, aucun trafic, appel interrompu), avec
   la raison ;
2. les **anomalies**, nommées par route et par contrôleur : une requête SQL
   répétée dans une boucle (« N+1 »), les erreurs `5xx` avec leur `requestId` et
   leurs lignes de journal, les routes dont le p95 dépasse 500 ms avec ce qui
   coûte par requête, un blocage de la boucle d'événements de 100 ms ou plus ;
3. un tableau par route, les requêtes SQL les plus coûteuses, et un bloc JSON
   pour comparer deux appels — avant et après une correction.

```jsonc
{ "name": "nodefony_observe", "arguments": { "durationSec": 10 } }
// "route": "<nom de route>" pour n'en observer qu'une ; "includeTooling": true
// pour compter aussi les routes /nodefony/… de l'outillage
```

**L'outil ne génère aucun trafic.** On ouvre la page, on joue les tests ou on
envoie des requêtes pendant que la fenêtre court ; sans trafic, il n'a rien à
juger, et il le dit.

Ce que les chiffres veulent dire :

- **durées et temps « par requête »** portent sur les requêtes **servies**
  (statut inférieur à 400, sans exception). Un refus `401`/`403` rendu en
  quelques millisecondes ne mesure pas la route : il est compté à part ;
- **`action.self`** est le temps de l'action et des services qu'elle appelle,
  une fois retirés les phases imbriquées (initialisation, rendu, envoi) et le
  SQL — les intervalles sont fusionnés, une requête lancée pendant le rendu
  n'est pas comptée deux fois. **« SQL (cumul) »** additionne les durées des
  requêtes, même lancées en parallèle ;
- **la boucle d'événements** est rapportée en retard moyen et en **pire
  blocage** : un gel unique de 200 ms ne déplace la moyenne d'un intervalle de
  deux secondes que de quelques millisecondes ;
- **la forme d'une requête SQL** remplace chaque valeur par `?` : le rapport ne
  contient jamais une donnée. Le journal d'une erreur n'en garde que les lignes
  WARNING et plus graves ;
- la **variation du tas** est mesurée sans GC forcé : elle est indicative,
  jamais une anomalie.

Ce qui encadre l'outil :

- c'est un **outil long** : chaque relevé part en progression, et un client qui l'annule l'arrête au relevé suivant, qu'il ferme le flux de la réponse (révision courante de MCP) ou envoie une notification d'annulation (révision précédente, celle du SDK officiel) ;
- un **rechargement du serveur** pendant la fenêtre (un fichier modifié) ne fait
  pas perdre l'observation : l'outil rend un rapport partiel qui le dit, avant
  que le serveur ne ferme ses connexions ;
- le **journal des erreurs** est lu au relevé qui voit la requête — le syslog de
  développement ne garde que ses dernières lignes — et seulement pour un appel
  **authentifié** (scope `admin:read`), par la même voie que
  `nodefony_admin_call` ;
- le profileur garde les 500 dernières requêtes : au-delà de ce débit entre deux
  relevés, les profils perdus sont comptés, et le verdict le dit.

La lecture de chaque anomalie et le geste qui la corrige sont dans le skill
d'agent `nodefony-troubleshoot`, section « Mesurer l'application qui tourne ».

### Mesurer une route — `nodefony_profile_route`

Là où `nodefony_observe` attend le trafic, `nodefony_profile_route` le **produit** :
il envoie lui-même N requêtes `GET` (ou `HEAD`) sur un chemin de l'application,
retrouve leurs profils par l'identifiant qu'il pose (`x-request-id`) et rend le
même rapport. C'est la boucle « je modifie → je mesure » en un appel.

```jsonc
// tools/call
{
  "name": "nodefony_profile_route",
  "arguments": { "path": "/orders?page=2", "count": 50 },
}
// optionnel : "concurrency" (défaut 4, max 8), "warmup" (défaut 3, hors mesures),
//             "method": "HEAD", "as": "<identifiant>", "anonymous": true
```

`path` est un **chemin**, jamais une URL : hôte, schéma et `//` sont refusés,
l'outil ne vise que l'application elle-même. Au plus 200 requêtes par appel —
sous les 500 profils que garde le profileur — et 45 secondes de tir.

**Au nom de qui.** Une page protégée vue sans compte ne rend que des refus.
Quand le jeton MCP porte le scope `admin:write`, l'outil joue son
**propriétaire** : il ouvre une vraie session à son nom par le flux de connexion
de l'application (un ticket à usage unique, présenté en boucle locale à
`POST /nodefony/devkit/profile-session`), tire avec, puis la détruit — l'audit
l'écrit sous la raison `devkit`. `as` joue un autre compte, à condition que ses
rôles soient inclus dans ceux du propriétaire, hiérarchie comprise : jouer un
compte ne monte jamais en droits. Sans ce scope, l'outil joue l'anonyme et le
rapport donne la commande :

```bash
npx nodefony security:mcp:token --scope "admin:read admin:write" --write
```

Extrait de deux appels réels, avant puis après la correction d'un N+1 (la même
lecture, une requête par compte, puis une seule requête `$in`) :

```text
Verdict : anomalies (1) — observation complète
Tir : 30/30 requête(s) GET /nodefony/test/db/n-plus-one · concurrence 4 · 106 ms · chauffe de 3 requête(s) hors mesures (p50 1.5 ms, max 2.5 ms)
Joué en tant que : `admin` (ROLE_ADMIN, ROLE_NODEFONY_ADMIN, ROLE_DEV, ROLE_SUPERVISOR)
Reçu par le client : 2xx 30 · 3xx 0 · 401 0 · 403 0 · autres 4xx 0 · 5xx 0 · sans réponse 0

Anomalies (les plus coûteuses d'abord) :
1. N+1 — route `DbController::nPlusOne` (DbController.nPlusOne) : `select … from "User" where "User"."id" = ? …` répétée ×16 dans une requête (30 requête(s) touchée(s), 14 ms cumulés). […] Remède habituel : charger la relation en une requête (`relations`, `$in`).

route | type | n | p50 | p95 | max | 2xx/3xx/4xx/5xx | SQL par requête moy/max | temps par requête
`DbController::nPlusOne` (DbController.nPlusOne) | http | 30 | 1.2 ms | 1.7 ms | 1.7 ms | 30/0/0/0 | 18.0/18 | SQL (cumul) 0.7 ms · action.self 0.2 ms · send 0.1 ms

— après correction —
Verdict : ok — observation complète
`DbController::nPlusOneFixed` (DbController.nPlusOneFixed) | http | 30 | 0.7 ms | 0.9 ms | 2.1 ms | 30/0/0/0 | 3.0/3 | SQL (cumul) 0.4 ms · send 0.1 ms · action.self 0.1 ms
```

Ce que le rapport dit **en tête**, avant tout chiffre : les requêtes refusées
(`401`/`403` — verdict « rien à juger »), redirigées (vers la page de
connexion), restées sans réponse, et les profils non retrouvés. Une mesure qui ne
porte pas sur ce qui était demandé le dit d'abord.

À savoir :

- ce sont de **vraies requêtes** : une page qui écrit en `GET` écrit N fois — un
  compteur, ou une session créée pour chaque visiteur anonyme ;
- les zones à jeton (`jwt`, `external-jwt`) ne sont pas jouées : leurs requêtes
  rendent `401`, et le rapport le dit ;
- un tir plus court que cinq périodes de la sonde laisse la boucle d'événements
  « non mesurée » plutôt que d'afficher un zéro trompeur.

### Obtenir le porteur — `security:mcp:token`

Dès que la porte demande un jeton, il faut en produire un. L'application **le signe elle-même**, en
ligne de commande, sans qu'aucun serveur n'écoute :

```bash
npx nodefony security:mcp:token --write   # pose NF_MCP_TOKEN chez les agents présents
npx nodefony ai:mcp --auth            # l'en-tête porte ${NF_MCP_TOKEN}, jamais le jeton
```

`.mcp.json` ne contient donc **que le nom de la variable**. Le flux complet — audience à déclarer,
durée, rotation, et le refus qu'on rencontre en premier — vit dans
[obtenir un jeton](../security/obtenir-un-jeton/).

### Passer la porte sous autorisation OAuth 2.1

Le périmètre ci-dessus suffit à un poste de développement. Dès que la porte doit
répondre à quelqu'un d'autre, déclarez un serveur d'autorisation : elle devient
alors un **resource server** au sens de la spécification — elle publie ses
métadonnées ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)), valide
le porteur, refuse en `401`/`WWW-Authenticate`
([RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)) et vérifie
l'audience ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html)).

```ts
use("@nodefony/devkit", {
  mcp: {
    authorization: {
      authorizationServers: ["https://auth.example"],
      resource: "https://mon-app.example/nodefony/mcp",
    },
  },
});
```

| Ce qui se passe alors                                        | Où                                                       |
| ------------------------------------------------------------ | -------------------------------------------------------- |
| Le document de métadonnées est publié                        | `GET /.well-known/oauth-protected-resource/nodefony/mcp` |
| Une requête sans jeton — ou sans jeton LISIBLE — est refusée | `401` + `WWW-Authenticate: Bearer resource_metadata="…"` |
| Un jeton d'une autre audience n'ouvre rien                   | `401` — ou servi en **anonyme** si `anonymous: true`     |
| Un outil déclarant des `scopes` devient atteignable          | pour qui présente **tous** ces scopes                    |
| Les scopes publiés sont l'**union** de ceux des outils       | `scopes_supported` du document, et `scope` du défi       |

Le serveur d'**autorisation** n'est jamais à écrire : la spécification le place
« beyond the scope […] or a separate entity ». N'importe quel émetteur OAuth 2.1
convient.

> 🔴 **Ce module ne valide pas les jetons lui-même** — il est `policy: "dev"` et
> ne porte aucune cryptographie. Il cherche un service `accessTokenVerifier` dans le
> conteneur (contrat `IAccessTokenVerifier`, exporté par `nodefony`). Sans lui,
> une porte déclarée protégée répond `503` et le journal le dit en `CRITIC` :
> accepter des porteurs sans les lire serait pire que rester anonyme.

### Ajouter VOS outils — ce que l'agent ne peut pas deviner

Les outils intégrés ci-dessus décrivent le framework. Ils ne savent rien de votre
métier — et c'est précisément ce qu'un agent invente le plus mal. N'importe quel
module de votre application peut donc en publier :

```ts
import { Module, type IMcpTool } from "nodefony";
import { mcpText } from "@nodefony/ai";

class Shop extends Module {
  getMcpTools(): IMcpTool[] {
    return [
      {
        name: "shop_stock",
        // ⭐ La description est ce qui DÉCLENCHE l'outil. Dire ce qu'il rend
        // ET quand s'en servir — un modèle n'appelle pas ce qu'il ne
        // comprend pas.
        description:
          "Stock réel d'une référence produit. À utiliser avant de proposer " +
          "une commande — la réponse vient de la base, pas d'un cache.",
        inputSchema: {
          type: "object",
          properties: { sku: { type: "string", description: "Référence" } },
          required: ["sku"],
        },
        // Il LIT la base et ne sort pas de l'application : il le dit, sinon la
        // spec le tient pour destructif et l'agent demande confirmation à
        // chaque lecture.
        annotations: { readOnlyHint: true, openWorldHint: false },
        handler: async (args) => mcpText(await this.stock(String(args.sku))),
      },
    ];
  }
}
```

Quatre choses à savoir :

- **un outil dit ce qu'il fait** (`annotations`, indices `ToolAnnotations` de la
  spec MCP) : sans annotation, la spec le tient pour un outil qui écrit, détruit
  et sort de l'application. Un outil qui lit se déclare `readOnlyHint: true` ;
  un outil qui modifie, `readOnlyHint: false`. Un indice n'est jamais une
  autorisation — le détail des cinq indices et de leurs défauts :
  [écrire un outil](../ai/mcp/#dire-ce-que-fait-loutil--les-annotations) ;
- **rien ne s'enregistre au démarrage** : la liste est relue à chaque requête,
  donc un module ajouté apparaît sans redémarrer quoi que ce soit ;
- **`mcp.tools` ne filtre que les outils intégrés** — le vôtre est publié dès
  qu'il est déclaré, sans ligne de configuration supplémentaire ;
- un outil **écarté** (nom hors `[a-zA-Z0-9_-]{1,64}`, nom déjà pris, handler
  absent, déclaration qui lève) le dit en `WARNING` dans les journaux du
  serveur — il ne disparaît jamais en silence. Les outils intégrés gagnent
  toute collision : personne ne peut répondre à la place de `nodefony_inspect`.

### Réserver un outil à qui est autorisé

Un outil peut exiger des **scopes** (tous, pas au moins un) et/ou une identité
prouvée. Son handler reçoit alors l'appelant en second paramètre, pour borner ce
qu'il **rend** et pas seulement décider s'il répond :

```ts
{
  name: "shop_invoice",
  description: "Facture d'une commande.",
  inputSchema: { type: "object", properties: { id: { type: "string" } } },
  scopes: ["shop:read", "shop:billing"],   // ou : requiresAuth: true
  handler: async (args, caller) =>
    mcpText(await this.invoice(String(args.id), caller.subject)),
}
```

La spec le prévoit explicitement : le jeu d'outils « MAY vary by the
authorization presented on the request — for example, returning only the tools
the caller's granted scopes permit », précisément parce que les identifiants
sont une **entrée de requête, pas un état de connexion**. C'est pourquoi la
liste est recollectée à chaque appel.

Le filtre s'applique **à la collecte**, donc en un seul point : un outil retenu
est absent de `tools/list` **et** inappelable en le nommant, et le refus dit
« outil inconnu » plutôt qu'« interdit » — son existence même n'est pas révélée.

> 🔴 **Tant que la porte n'authentifie personne, un outil à scopes ne sortira
> jamais.** C'est le comportement voulu — fermé par défaut — mais il faut le
> savoir avant de chercher une panne : `caller` vaut
> `{ authenticated: false, scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
> branché. Le jour où il le sera, ces déclarations prendront effet sans qu'une
> ligne d'outil change.

> ⚠️ Cette porte n'est pas authentifiée (voir l'écart ci-dessus) et le module est
> `policy: "dev"`. Avant d'exposer une donnée par un outil, se demander si elle
> supporterait d'être lue **sans identification**, par qui a accès à la machine.

### Un outil LONG — progression et annulation

Le troisième paramètre du handler sert l'outil qui dure : `run.signal` est
abattu quand le client annule — qu'il ferme le flux de la réponse (révision courante
de MCP) ou envoie `notifications/cancelled` (révision précédente) —,
`run.progress()` lui donne signe de vie.
Un outil court l'ignore ; un appel direct (un test) ne le fournit pas, d'où le
`run?.` :

```ts
{
  name: "shop_reindex",
  description: "Réindexe le catalogue, lot par lot. Long : signale chaque lot.",
  inputSchema: { type: "object", properties: {} },
  handler: async (_args, _caller, run) => {
    const lots = await this.lots();
    for (const [i, lot] of lots.entries()) {
      if (run?.signal.aborted) return mcpText("interrompu", true);
      await this.reindex(lot, run?.signal);
      run?.progress(i + 1, lots.length, `lot ${i + 1}/${lots.length}`);
    }
    return mcpText(`${lots.length} lots réindexés`);
  },
}
```

Ce que la porte fait du reste :

- **le flux s'ouvre à la première progression, ou au bout de cinq secondes.**
  Un agent qui envoie un `progressToken` et accepte `text/event-stream` reçoit
  des `notifications/progress`, puis la réponse, qui clôt le flux. Un outil
  encore en cours après le délai de grâce (`MCP_STREAM_GRACE_MS`, 5 s) ouvre le
  flux même sans jeton : la norme oblige le client à accepter une réponse en
  flux, et c'est ce qui désarme le délai d'inactivité de la requête (30 s par
  défaut) — sans cela, un outil long répondant en JSON recevrait un `408`. Un
  outil qui répond avant reste en JSON ;
- **la norme est tenue pour vous** : une valeur qui ne croît pas est ignorée, la
  cadence est bornée (une notification par 100 ms au plus), et plus rien ne
  part une fois l'outil terminé ;
- **fermer le flux, c'est annuler** (transport HTTP, révision 2026-07-28) :
  `run.signal` est abattu, à l'outil de s'arrêter.

Le journal `notifications/message` n'est **pas** proposé : la révision
2026-07-28 le déprécie (« New implementations SHOULD NOT adopt it »). Ce qu'un
outil veut dire en route passe par le `message` de la progression ; ce qui doit
atteindre le modèle va dans la réponse finale. `subscriptions/listen` est refusé
par une erreur explicite (`-32601`) : le catalogue ne change qu'au rechargement
de l'application, qui redémarre son process, si bien qu'un flux d'abonnement
casserait au lieu de notifier. Le `tools/list` suivant rend l'état à jour.

## Les skills d'agent — répondre à « comment fait-on ça, ici ? »

La carte dit **où aller**. Elle ne dit pas **comment faire**. Or c'est là qu'un
agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
dans le corps de l'action.

Le paquet livre donc onze **skills** au format [Agent Skills](https://agentskills.io) :
un qui porte la conduite commune de toute tâche, un qui part du symptôme d'une panne,
et un par geste où l'invention coûte cher :

| Skill                           | Le besoin qu'il couvre                                           |
| ------------------------------- | ---------------------------------------------------------------- |
| `nodefony-dev`                  | conduire une tâche de bout en bout, avant toute modification     |
| `nodefony-add-crud`             | exposer une ressource REST complète, entité comprise             |
| `nodefony-add-service`          | ajouter de la logique métier réutilisable, vue du conteneur      |
| `nodefony-authentication`       | connecter un utilisateur ou un programme, sans rien réécrire     |
| `nodefony-protect-route`        | réserver une route à qui est habilité                            |
| `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse                  |
| `nodefony-browser`              | voir **et mesurer** un écran, sans navigateur sur le poste       |
| `nodefony-migrate-schema`       | faire évoluer le schéma d'une base et le porter en production    |
| `nodefony-devops`               | exploiter l'application en conteneur, sans réécrire son contrat  |
| `nodefony-troubleshoot`         | partir d'un symptôme de panne pour trouver le geste qui la règle |
| `nodefony-security-review`      | relire la sécurité d'un diff avant de le commiter                |
| `nodefony-upgrade`              | monter le framework de version sans rien laisser derrière        |

Tous portent le préfixe `nodefony-` : leurs pointeurs arrivent dans le dossier où
vous écrivez aussi les vôtres, et sans namespace un skill maison du même nom
serait écrasé à la synchronisation suivante.

`nodefony create app` les met à disposition à la création ; après une montée de
version, `npx nodefony ai:sync` les remet à jour (`--dry-run` montre sans
écrire).

**Ce qui est écrit dans le projet est un pointeur, jamais une copie.** Le contenu
reste dans le paquet installé et suit `npm update` : une recette recopiée dans un
projet décrit, six mois plus tard, un framework qui a changé — et comme rien ne
casse, personne ne s'en aperçoit. C'est le même principe que le reste du devkit :
**rien de figé n'est copié chez l'utilisateur.**

Le dossier visé — `.agents/skills/` — est celui que tous les clients conformes
lisent, plutôt que le dossier propriétaire d'un seul d'entre eux. Ces fichiers
sont faits pour être commités : l'équipe entière et l'intégration continue
travaillent alors avec les mêmes recettes.

> Aucun `postinstall` ne les pose : `--ignore-scripts` est courant, les scripts
> d'installation sont un vecteur d'attaque connu de l'écosystème npm, et écrire
> dans un dossier versionné à chaque installation produirait des différences
> surprises.

## Ce qu'il ne fait pas

Le scaffold (`nodefony create …`), le diagnostic (`nodefony doctor`) et
l'introspection (`nodefony inspect`) **ne sont pas ici** : ils vivent dans le
cœur, parce qu'ils doivent répondre sans qu'aucun module soit installé — et
surtout quand l'application est cassée. Un outil de diagnostic qui exige que
l'application démarre ne sert pas au moment où on en a besoin.

Même partage pour les skills, et il se retient en une phrase : **le VERBE vit
dans le cœur, le CONTENU dans ce paquet.** `ai:sync` doit répondre dans un
terminal qui n'a rien posé (portée par un module `policy: "dev"`, elle serait
absente sans `NODE_ENV`) ; les skills, eux, doivent se mettre à jour par npm.

## Configuration

| Clé                  | Type       | Défaut                                             | Rôle                                           |
| -------------------- | ---------- | -------------------------------------------------- | ---------------------------------------------- |
| `enabled`            | `boolean`  | `true`                                             | Interrupteur du module                         |
| `mcp.enabled`        | `boolean`  | `true`                                             | Répond-on aux requêtes MCP ? Coupé → `404`     |
| `mcp.allowedOrigins` | `string[]` | `[]`                                               | Origines de navigateur admises ; vide = aucune |
| `mcp.allowRemote`    | `boolean`  | `false`                                            | Accepter un appel d'une adresse non locale     |
| `mcp.tools`          | `string[]` | les sept outils intégrés (`BUILTIN_MCP_TOOL_KEYS`) | Allowlist des outils — lecture seule           |

Les clés, leurs types et leurs défauts viennent du schéma Zod
(`nodefony/config/config.ts`) — **source unique** dont dérivent la
documentation, la validation au boot et le formulaire d'édition de Studio.

## Pour aller plus loin

- ⬆️ **Retour au hub** : [documentation Nodefony](../../) — et
  [par où commencer](../../demarrer/) si vous arrivez sur le framework.
- 🏗️ **Générer du code plutôt que l'écrire** :
  [`nodefony create`](../../guides/generer-du-code/) — voir ce qui va changer
  avant que ça change, et l'appeler depuis un agent.
- 🧪 **Ce que vaut cet outillage, mesuré** :
  [éprouver un framework avec un agent](https://github.com/nodefony/nodefony-core/blob/main/docs/guides/eprouver-loutillage-agent.md).
- 🖥️ **Les commandes du cœur** : [la CLI](../core/cli/) — `card` et `inspect`
  y sont servies, pas ici.
- 📚 **La documentation installée avec les paquets** :
  [`@nodefony/documentation`](../documentation/).
- 📖 [Lexique général](../../lexique/) du framework.
