@nodefony/ai 0.0.0-stage → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +123 -2
- package/dist/_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js +8 -0
- package/dist/index.js +37 -0
- package/dist/nodefony/command/StdioCommand.js +75 -0
- package/dist/nodefony/config/config.js +181 -0
- package/dist/nodefony/config/configSchema.js +18 -0
- package/dist/nodefony/config/defineModuleConfig.js +51 -0
- package/dist/nodefony/mcp/caller.js +38 -0
- package/dist/nodefony/mcp/gate.js +628 -0
- package/dist/nodefony/mcp/guard.js +82 -0
- package/dist/nodefony/mcp/progress.js +74 -0
- package/dist/nodefony/mcp/protocol.js +76 -0
- package/dist/nodefony/mcp/server.js +302 -0
- package/dist/nodefony/mcp/sessionTicket.js +89 -0
- package/dist/nodefony/mcp/stdio.js +64 -0
- package/dist/nodefony/mcp/stream.js +80 -0
- package/dist/nodefony/mcp/ticketSession.js +177 -0
- package/dist/nodefony/mcp/tools.js +485 -0
- package/dist/nodefony/src/errors/AiError.js +20 -0
- package/dist/types/index.d.ts +56 -0
- package/dist/types/nodefony/command/StdioCommand.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +44 -0
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +43 -0
- package/dist/types/nodefony/mcp/caller.d.ts +54 -0
- package/dist/types/nodefony/mcp/gate.d.ts +235 -0
- package/dist/types/nodefony/mcp/guard.d.ts +89 -0
- package/dist/types/nodefony/mcp/progress.d.ts +48 -0
- package/dist/types/nodefony/mcp/protocol.d.ts +107 -0
- package/dist/types/nodefony/mcp/server.d.ts +156 -0
- package/dist/types/nodefony/mcp/sessionTicket.d.ts +60 -0
- package/dist/types/nodefony/mcp/stdio.d.ts +38 -0
- package/dist/types/nodefony/mcp/stream.d.ts +76 -0
- package/dist/types/nodefony/mcp/ticketSession.d.ts +174 -0
- package/dist/types/nodefony/mcp/tools.d.ts +232 -0
- package/dist/types/nodefony/src/errors/AiError.d.ts +14 -0
- package/docs/index.md +56 -0
- package/docs/mcp.md +754 -0
- package/package.json +83 -4
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { IMcpTool, IMcpToolAnnotations, IMcpToolDefinition, IMcpToolResult, IMcpCaller, IMcpToolRun } from "nodefony";
|
|
2
|
+
export type { IMcpTool, IMcpToolAnnotations, IMcpToolDefinition, IMcpToolResult, IMcpCaller, IMcpToolRun, };
|
|
3
|
+
/**
|
|
4
|
+
* Outils MCP : la **collecte** de ceux qu'une application déclare, leur
|
|
5
|
+
* publication et leur exécution — la part générique de toute porte MCP.
|
|
6
|
+
*
|
|
7
|
+
* Les CONTRATS (`IMcpTool` et ses voisins) vivent au cœur, parce qu'un module
|
|
8
|
+
* les implémente par {@link IModule.getMcpTools} sans dépendre d'aucune porte ;
|
|
9
|
+
* la MÉCANIQUE vit ici. Le catalogue `nodefony_*` n'est pas ici non plus : il
|
|
10
|
+
* appartient à `@nodefony/devkit`, qui le déclare comme n'importe quel module.
|
|
11
|
+
*
|
|
12
|
+
* ## Pourquoi la collecte, et pas un registre
|
|
13
|
+
*
|
|
14
|
+
* Un module déclare ses outils par {@link IModule.getMcpTools} ; ils sont
|
|
15
|
+
* ramassés **au moment de servir la requête**, jamais stockés. C'est
|
|
16
|
+
* délibérément l'inverse du data plane admin ({@link IAdminRegistry}), qui a
|
|
17
|
+
* besoin d'un registre parce qu'il monte des ROUTES au boot et qu'un module
|
|
18
|
+
* arrivé trop tard n'aurait plus rien à monter. Un outil MCP n'a rien à monter :
|
|
19
|
+
* il est lu à la demande. D'où trois propriétés gagnées gratuitement — aucune
|
|
20
|
+
* structure allouée au démarrage (donc coût nul en production, où aucune porte
|
|
21
|
+
* MCP n'existe), aucun ordre de `register()` à respecter, et une fraîcheur qui
|
|
22
|
+
* est une propriété du mécanisme plutôt qu'une discipline à tenir.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Au-delà de quoi une réponse cesse d'être une réponse.
|
|
26
|
+
*
|
|
27
|
+
* Mesuré, pas choisi en l'air : `inspect routes` rendait 47 000 caractères et
|
|
28
|
+
* `inspect config` 190 730 — le second dépassait ce que le client accepte, il
|
|
29
|
+
* partait sur disque, et l'agent brûlait vingt tours à le relire au `jq`. Le
|
|
30
|
+
* premier passait, et ne servait à rien pour autant : à qui demandait le NOMBRE
|
|
31
|
+
* de routes, il fallait compter 119 objets dans un mur de JSON.
|
|
32
|
+
*
|
|
33
|
+
* La borne est donc largement SOUS la limite des clients : ce qu'on protège
|
|
34
|
+
* n'est pas le transport, c'est la capacité de l'agent à répondre sans relire.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MCP_TEXT_MAX_CHARS = 32000;
|
|
37
|
+
/**
|
|
38
|
+
* Rend un résultat d'outil — les données partent en JSON COMPACT.
|
|
39
|
+
*
|
|
40
|
+
* Le lecteur est un agent : il relit le JSON compact sans peine, et chaque
|
|
41
|
+
* caractère rendu se repaie à chaque tour suivant de sa conversation.
|
|
42
|
+
* L'indentation représentait ~25 % d'un rapport `nodefony_check`.
|
|
43
|
+
*
|
|
44
|
+
* Exporté parce qu'une application qui déclare un outil en a besoin : sans lui,
|
|
45
|
+
* chacune réinventerait l'enveloppe `content[]`, et une enveloppe mal formée
|
|
46
|
+
* n'échoue pas — elle rend un résultat vide que l'agent prend pour une réponse.
|
|
47
|
+
*
|
|
48
|
+
* **Un résultat est BORNÉ** ({@link MCP_TEXT_MAX_CHARS}) : au-delà, il rend un
|
|
49
|
+
* résumé plutôt qu'un déversement (voir {@link summarize}). La garde vit ICI, et
|
|
50
|
+
* non dans chaque outil, pour qu'un outil déclaré par une
|
|
51
|
+
* application en hérite sans le savoir — c'est le genre de règle qu'on
|
|
52
|
+
* n'applique jamais deux fois de la même façon si on la laisse à l'appelant.
|
|
53
|
+
* En deçà de la borne — le cas courant — la donnée part TELLE QUELLE : un
|
|
54
|
+
* consommateur qui la parse n'a rien à changer, et on ne paie pas la garde sur
|
|
55
|
+
* 100 % des appels pour 1 % de gros sujets.
|
|
56
|
+
*
|
|
57
|
+
* @param value - donnée à rendre (sérialisée si ce n'est pas déjà du texte)
|
|
58
|
+
* @param isError - marque un échec MÉTIER (pas une faute de protocole)
|
|
59
|
+
*/
|
|
60
|
+
export declare function mcpText(value: unknown, isError?: boolean): IMcpToolResult;
|
|
61
|
+
/**
|
|
62
|
+
* Ce qu'il faut pour ÉNUMÉRER les déclarations d'outils d'une application.
|
|
63
|
+
*
|
|
64
|
+
* Distinct de {@link IMcpCollectOptions}, qui ajoute l'appelant : énumérer est
|
|
65
|
+
* une question sur le CODE (« que déclare cette application ? »), collecter est
|
|
66
|
+
* une question sur une REQUÊTE (« que sert-on à celui-ci ? »). Le document de
|
|
67
|
+
* ressource protégée, servi sans contexte d'appelant, ne peut poser que la
|
|
68
|
+
* première.
|
|
69
|
+
*/
|
|
70
|
+
export interface IMcpDeclareOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Modules chargés (`kernel.modules`), dont on lit `getMcpTools()`.
|
|
73
|
+
*
|
|
74
|
+
* Typé large à dessein : le cœur ne peut pas dépendre de la forme concrète
|
|
75
|
+
* d'un module ici, et la lecture est de toute façon défensive.
|
|
76
|
+
*/
|
|
77
|
+
modules?: Readonly<Record<string, unknown>> | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Appelé pour chaque déclaration ÉCARTÉE, avec le motif.
|
|
80
|
+
*
|
|
81
|
+
* Sans ce rappel, un outil mal nommé ou en collision disparaîtrait en
|
|
82
|
+
* silence, et son auteur chercherait la faute dans son handler — qui n'a
|
|
83
|
+
* jamais été appelé.
|
|
84
|
+
*/
|
|
85
|
+
onSkip?: ((why: string) => void) | undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Ce qu'il faut pour ramasser les outils SERVIS à un appelant. */
|
|
88
|
+
export interface IMcpCollectOptions extends IMcpDeclareOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Ce que la porte a ÉTABLI de l'appelant. Absent = anonyme.
|
|
91
|
+
*
|
|
92
|
+
* Une porte sans authentification n'a rien à passer, et c'est le cas sûr :
|
|
93
|
+
* l'anonyme par défaut RETIENT tout outil qui exige quoi que ce soit.
|
|
94
|
+
*/
|
|
95
|
+
caller?: IMcpCaller | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Appelé pour chaque outil RETENU faute d'autorisation.
|
|
98
|
+
*
|
|
99
|
+
* Distinct d'{@link IMcpCollectOptions.onSkip}, et la distinction n'est pas
|
|
100
|
+
* cosmétique : un `onSkip` dénonce une FAUTE de l'auteur (nom hors forme,
|
|
101
|
+
* handler absent) et mérite un avertissement ; une rétention est le
|
|
102
|
+
* fonctionnement NORMAL d'un catalogue filtré, et crier à chaque requête pour
|
|
103
|
+
* chaque outil protégé noierait le journal. Le rappel existe quand même,
|
|
104
|
+
* parce que sans lui un développeur qui déclare des scopes sur une porte non
|
|
105
|
+
* authentifiée chercherait son outil sans jamais comprendre.
|
|
106
|
+
*/
|
|
107
|
+
onWithheld?: (name: string, why: string) => void;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Préfixe RÉSERVÉ aux modules du framework (`@nodefony/*`) — un agent qui lit
|
|
111
|
+
* `nodefony_inspect` doit pouvoir croire que c'est le framework qui répond.
|
|
112
|
+
*
|
|
113
|
+
* Sans la réserve, un module d'application pourrait déclarer `nodefony_doc` et
|
|
114
|
+
* répondre à la place de l'outil que l'agent a lu dans la documentation du
|
|
115
|
+
* framework : l'ordre de chargement des modules ne protège rien, il change
|
|
116
|
+
* avec le manifeste.
|
|
117
|
+
*/
|
|
118
|
+
export declare const MCP_RESERVED_PREFIX = "nodefony_";
|
|
119
|
+
/**
|
|
120
|
+
* Énumère ce que cette application DÉCLARE — les outils de ses modules, lus
|
|
121
|
+
* par `getMcpTools()` — sans rien retenir sur l'identité de qui que ce soit.
|
|
122
|
+
*
|
|
123
|
+
* Cinq refus, tous énoncés par `onSkip` plutôt que silencieux :
|
|
124
|
+
* - **nom hors forme** ({@link NAME_PATTERN}) — il ne serait pas appelable ;
|
|
125
|
+
* - **forme fausse** ({@link mcpToolFault}) — handler, description, schéma ou
|
|
126
|
+
* autorisation qui ne respectent pas `IMcpTool` ;
|
|
127
|
+
* - **préfixe réservé** ({@link MCP_RESERVED_PREFIX}) pris par un module hors de
|
|
128
|
+
* `@nodefony/` — un module d'application ne peut donc pas se substituer à
|
|
129
|
+
* `nodefony_inspect` et répondre à sa place ;
|
|
130
|
+
* - **collision** — le premier inscrit garde le nom ;
|
|
131
|
+
* - **déclaration en échec** — un `getMcpTools()` qui lève ne doit pas priver
|
|
132
|
+
* l'agent des outils de tous les autres modules.
|
|
133
|
+
*
|
|
134
|
+
* 🔴 **Ce que rend cette fonction n'est PAS servable tel quel** : elle contient
|
|
135
|
+
* les outils réservés. Toute porte passe par {@link collectMcpTools}, qui filtre
|
|
136
|
+
* sur l'appelant. Le seul usage légitime de la liste brute est de raisonner sur
|
|
137
|
+
* le CODE — ce que la porte EXIGE ({@link mcpDeclaredScopes}) — jamais de
|
|
138
|
+
* répondre à une requête.
|
|
139
|
+
*
|
|
140
|
+
* @param options - modules et journal des écarts
|
|
141
|
+
* @returns les outils déclarés, dans l'ordre où ils seraient publiés
|
|
142
|
+
*/
|
|
143
|
+
export declare function declareMcpTools(options: IMcpDeclareOptions): IMcpTool[];
|
|
144
|
+
/**
|
|
145
|
+
* Ramasse les outils SERVIS à cet appelant.
|
|
146
|
+
*
|
|
147
|
+
* Aux trois refus de {@link declareMcpTools} s'ajoute une RÉTENTION, qui n'est
|
|
148
|
+
* pas une faute : un outil dont l'appelant ne satisfait pas les exigences
|
|
149
|
+
* ({@link IMcpTool.scopes}, {@link IMcpTool.requiresAuth}) ne sort pas d'ici.
|
|
150
|
+
* ⭐ **Filtrer À LA COLLECTE protège `tools/list` ET `tools/call` d'un seul
|
|
151
|
+
* geste** : le protocole ne reçoit que les outils servis, donc un outil retenu
|
|
152
|
+
* est « inconnu » pour lui — indistinguable d'un outil inexistant, ce qui ne
|
|
153
|
+
* révèle même pas son existence. Filtrer la liste sans filtrer l'appel n'aurait
|
|
154
|
+
* été qu'un rideau, et c'est l'erreur classique : deux points de décision, dont
|
|
155
|
+
* un qu'on oublie.
|
|
156
|
+
*
|
|
157
|
+
* @param options - modules, appelant et journaux
|
|
158
|
+
* @returns les outils exécutables, dans l'ordre où ils seront publiés
|
|
159
|
+
*/
|
|
160
|
+
export declare function collectMcpTools(options: IMcpCollectOptions): IMcpTool[];
|
|
161
|
+
/**
|
|
162
|
+
* Le vocabulaire de scopes que cette porte EXIGE — l'union de ce que ses outils
|
|
163
|
+
* déclarent, jamais une liste écrite à côté.
|
|
164
|
+
*
|
|
165
|
+
* 🔴 **C'est ce qu'un client doit demander**, et c'est à ce titre qu'il est
|
|
166
|
+
* publié (RFC 9728 `scopes_supported`) et nommé dans le défi (RFC 6750
|
|
167
|
+
* `scope`). Une liste de configuration disait autre chose que le code par
|
|
168
|
+
* simple oubli, dans les deux sens et sans qu'aucun contrôle ne l'aperçoive :
|
|
169
|
+
* un scope publié que rien n'exige envoie demander un droit qui n'ouvre rien ;
|
|
170
|
+
* un scope exigé que rien ne publie laisse le client deviner ce qu'on attend de
|
|
171
|
+
* lui — et un outil de module en était systématiquement là.
|
|
172
|
+
*
|
|
173
|
+
* L'énumération est délibérément celle des outils DÉCLARÉS, pas de ceux servis
|
|
174
|
+
* à un appelant : le document se lit avant d'avoir un jeton, et un catalogue
|
|
175
|
+
* filtré n'annoncerait à l'anonyme que ce dont il n'a pas besoin.
|
|
176
|
+
*
|
|
177
|
+
* @param options - modules et journal des écarts
|
|
178
|
+
* @returns les scopes, dédupliqués et triés ; vide si la porte n'exige rien
|
|
179
|
+
*/
|
|
180
|
+
export declare function mcpDeclaredScopes(options: IMcpDeclareOptions): string[];
|
|
181
|
+
/**
|
|
182
|
+
* Projette les outils pour `tools/list` — sans leur implémentation.
|
|
183
|
+
*
|
|
184
|
+
* Le `handler` est une fermeture sur l'état du serveur ; `JSON.stringify` le
|
|
185
|
+
* laisserait tomber en silence, mais compter là-dessus reviendrait à publier
|
|
186
|
+
* par accident ce qu'on ne publie pas exprès.
|
|
187
|
+
*/
|
|
188
|
+
export declare function publishMcpTools(tools: readonly IMcpTool[]): IMcpToolDefinition[];
|
|
189
|
+
/**
|
|
190
|
+
* Exécute un outil par son nom public.
|
|
191
|
+
*
|
|
192
|
+
* Un échec métier (sujet inconnu, module absent) rend un résultat `isError`,
|
|
193
|
+
* **pas** une erreur JSON-RPC : le protocole réserve celle-ci aux fautes de
|
|
194
|
+
* protocole. La distinction compte pour l'agent — une erreur de protocole
|
|
195
|
+
* signifie « tu t'y prends mal », un `isError` signifie « ta demande est
|
|
196
|
+
* recevable, voici pourquoi elle n'aboutit pas » ; c'est la seconde qu'il peut
|
|
197
|
+
* corriger seul.
|
|
198
|
+
*
|
|
199
|
+
* ⭐ **Aucun contrôle d'autorisation ici, et c'est voulu** : `tools` ne contient
|
|
200
|
+
* que ce que {@link collectMcpTools} a servi à CET appelant. Refaire la
|
|
201
|
+
* vérification à ce niveau créerait un second point de décision — deux copies
|
|
202
|
+
* d'une même règle, qui divergent en silence. Le corollaire tient en une
|
|
203
|
+
* phrase : **ne jamais passer ici une liste non filtrée.**
|
|
204
|
+
*
|
|
205
|
+
* @param name - nom public de l'outil (`nodefony_inspect`…)
|
|
206
|
+
* @param args - arguments fournis par l'agent
|
|
207
|
+
* @param tools - outils servis, tels que {@link collectMcpTools} les a ramassés
|
|
208
|
+
* @param caller - appelant établi, transmis au handler (anonyme par défaut)
|
|
209
|
+
* @param run - annulation et progression de CET appel (détaché par défaut)
|
|
210
|
+
* @returns le résultat de l'outil, ou `null` si le nom n'est pas exposé
|
|
211
|
+
*/
|
|
212
|
+
export declare function callMcpTool(name: string, args: Record<string, unknown>, tools: readonly IMcpTool[], caller?: IMcpCaller, run?: IMcpToolRun): Promise<IMcpToolResult | null>;
|
|
213
|
+
/**
|
|
214
|
+
* Tous les scopes qu'exige UN appel : ceux de l'outil, puis ceux que ses
|
|
215
|
+
* arguments ajoutent ({@link IMcpTool.scopesFor}). Seule source de la règle,
|
|
216
|
+
* lue par l'appel en process ET par le défi `insufficient_scope` d'une porte.
|
|
217
|
+
*
|
|
218
|
+
* @param tool - outil servi
|
|
219
|
+
* @param args - arguments de l'appel
|
|
220
|
+
* @returns les scopes exigés, sans doublon ; `null` si `scopesFor` lève ou rend
|
|
221
|
+
* une forme fausse (l'appel est alors refusé, jamais servi sans borne)
|
|
222
|
+
*/
|
|
223
|
+
export declare function mcpCallScopes(tool: IMcpTool, args: Readonly<Record<string, unknown>>): readonly string[] | null;
|
|
224
|
+
/**
|
|
225
|
+
* Le message d'un appel refusé faute de scope — le même pour le résultat
|
|
226
|
+
* `isError` et la description du défi `403`.
|
|
227
|
+
*
|
|
228
|
+
* @param name - nom de l'outil
|
|
229
|
+
* @param missing - scopes absents du jeton
|
|
230
|
+
* @returns l'explication, qui nomme les scopes à demander
|
|
231
|
+
*/
|
|
232
|
+
export declare function insufficientScopeMessage(name: string, missing: readonly string[]): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erreur du module ai.
|
|
3
|
+
*
|
|
4
|
+
* Deux champs qui font la différence en production :
|
|
5
|
+
* - `code` : identifiant MACHINE (stable, grep-able, consommé par Studio et
|
|
6
|
+
* l'audit) — le message, lui, peut changer sans rien casser ;
|
|
7
|
+
* - `context` : payload structuré joint au log (jamais de secret ici).
|
|
8
|
+
*/
|
|
9
|
+
export declare class AiError extends Error {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
12
|
+
constructor(message: string, code?: string, context?: Record<string, unknown> | undefined);
|
|
13
|
+
}
|
|
14
|
+
export default AiError;
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@nodefony/ai — la boîte à outils de l'IA dans une application"
|
|
3
|
+
navTitle: ai
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/ai"
|
|
6
|
+
topic: overview
|
|
7
|
+
audience: [developer]
|
|
8
|
+
tags: [module, ia, mcp, agent]
|
|
9
|
+
status: draft
|
|
10
|
+
updated: 2026-10-16
|
|
11
|
+
source: "src/packages/@nodefony/ai/docs/index.md"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# ai
|
|
15
|
+
|
|
16
|
+
> La boîte à outils de l'IA DANS une application Nodefony : exposer ses outils métier à un
|
|
17
|
+
> agent par le _Model Context Protocol_, et ouvrir sa propre porte MCP.
|
|
18
|
+
|
|
19
|
+
📍 [Documentation](../../../../../docs/index.md) › **@nodefony/ai**
|
|
20
|
+
|
|
21
|
+
Cette page est **surfacée dans Studio** (onglet Docs du module).
|
|
22
|
+
|
|
23
|
+
## Ce que fait le module
|
|
24
|
+
|
|
25
|
+
- **Le protocole MCP, tenu pour toi** : versions, erreurs, progression, annulation, flux SSE.
|
|
26
|
+
Un outil n'écrit que sa logique.
|
|
27
|
+
- **La mécanique de porte** : `defineMcpGate()` définit une porte, validée au démarrage ;
|
|
28
|
+
`gate.serve(this, body)` la sert depuis une action de controller.
|
|
29
|
+
- **Aucune route montée** : une porte se déclare, par le controller de qui en a besoin — la
|
|
30
|
+
porte de développement de `@nodefony/devkit`, ou celle de ton application.
|
|
31
|
+
|
|
32
|
+
C'est un module **socle** (`policy: "mandatory"`), chargé dans tous les environnements. Il ne
|
|
33
|
+
coûte rien par requête tant qu'aucune porte n'est déclarée.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
use("@nodefony/ai", undefined, { policy: "mandatory" }),
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Les réglages d'une porte se passent à `defineMcpGate()`, pas au module.
|
|
42
|
+
|
|
43
|
+
## Par où commencer
|
|
44
|
+
|
|
45
|
+
```nodefony-cards
|
|
46
|
+
[
|
|
47
|
+
{ "icon": "🤖", "title": "Boîte à outils MCP", "href": "mcp.md",
|
|
48
|
+
"desc": "Écrire un outil, le réserver à un appelant authentifié, signaler la progression d'un outil long, et monter sa propre porte avec defineMcpGate.",
|
|
49
|
+
"meta": "un agent doit interroger ou piloter ton application" }
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Pour aller plus loin
|
|
54
|
+
|
|
55
|
+
- La porte de développement et ses outils intégrés → [devkit](../../devkit/docs/index.md)
|
|
56
|
+
- [Toute la documentation](../../../../../docs/index.md)
|