@nodefony/ai 10.0.0-beta.3 → 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.
Files changed (43) hide show
  1. package/README.md +14 -6
  2. package/dist/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +21 -14
  4. package/dist/nodefony/command/StdioCommand.js +75 -0
  5. package/dist/nodefony/config/config.js +141 -8
  6. package/dist/nodefony/config/configSchema.js +1 -2
  7. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  8. package/dist/nodefony/mcp/caller.js +38 -0
  9. package/dist/nodefony/mcp/gate.js +628 -0
  10. package/dist/nodefony/mcp/guard.js +82 -0
  11. package/dist/nodefony/mcp/progress.js +74 -0
  12. package/dist/nodefony/mcp/protocol.js +76 -0
  13. package/dist/nodefony/mcp/server.js +302 -0
  14. package/dist/nodefony/mcp/sessionTicket.js +89 -0
  15. package/dist/nodefony/mcp/stdio.js +64 -0
  16. package/dist/nodefony/mcp/stream.js +80 -0
  17. package/dist/nodefony/mcp/ticketSession.js +177 -0
  18. package/dist/nodefony/mcp/tools.js +485 -0
  19. package/dist/nodefony/src/errors/AiError.js +1 -2
  20. package/dist/types/index.d.ts +25 -0
  21. package/dist/types/nodefony/command/StdioCommand.d.ts +28 -0
  22. package/dist/types/nodefony/config/config.d.ts +28 -32
  23. package/dist/types/nodefony/mcp/caller.d.ts +54 -0
  24. package/dist/types/nodefony/mcp/gate.d.ts +235 -0
  25. package/dist/types/nodefony/mcp/guard.d.ts +89 -0
  26. package/dist/types/nodefony/mcp/progress.d.ts +48 -0
  27. package/dist/types/nodefony/mcp/protocol.d.ts +107 -0
  28. package/dist/types/nodefony/mcp/server.d.ts +156 -0
  29. package/dist/types/nodefony/mcp/sessionTicket.d.ts +60 -0
  30. package/dist/types/nodefony/mcp/stdio.d.ts +38 -0
  31. package/dist/types/nodefony/mcp/stream.d.ts +76 -0
  32. package/dist/types/nodefony/mcp/ticketSession.d.ts +174 -0
  33. package/dist/types/nodefony/mcp/tools.d.ts +232 -0
  34. package/docs/index.md +56 -0
  35. package/docs/mcp.md +754 -0
  36. package/package.json +13 -11
  37. package/dist/_virtual/_@oxc-project_runtime@0.154.0/helpers/esm/decorateMetadata.js +0 -6
  38. package/dist/nodefony/interfaces/IAiService.js +0 -1
  39. package/dist/nodefony/interfaces/index.js +0 -1
  40. package/dist/nodefony/service/AiService.js +0 -96
  41. package/dist/types/nodefony/interfaces/IAiService.d.ts +0 -15
  42. package/dist/types/nodefony/interfaces/index.d.ts +0 -1
  43. package/dist/types/nodefony/service/AiService.d.ts +0 -80
@@ -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;
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)