@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,485 @@
|
|
|
1
|
+
import { missingScopes, rolesFromScopes } from "nodefony";
|
|
2
|
+
// nodefony/mcp/tools.ts
|
|
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
|
+
const MCP_TEXT_MAX_CHARS = 32e3;
|
|
37
|
+
/** Ce qu'on garde d'une valeur dans une vue d'ensemble : la surface. */
|
|
38
|
+
const SCALAR_MAX_CHARS = 120;
|
|
39
|
+
/**
|
|
40
|
+
* La SURFACE d'un objet — ses champs scalaires courts, sans sa profondeur.
|
|
41
|
+
*
|
|
42
|
+
* Ce qui pèse dans un inventaire n'est presque jamais ce qu'on y cherche : sur
|
|
43
|
+
* une route, `name`/`path`/`controller` tiennent en cent caractères, et c'est la
|
|
44
|
+
* description, les tableaux de tags et les objets imbriqués qui font les 47 ko.
|
|
45
|
+
* On jette donc la profondeur et on garde la surface — TOUTES les entrées
|
|
46
|
+
* restent visibles, ce qu'un échantillon des N premières ne permet pas : une
|
|
47
|
+
* question portant sur la 100ᵉ recevrait une réponse fausse, et l'agent n'aurait
|
|
48
|
+
* aucun moyen de le savoir.
|
|
49
|
+
*/
|
|
50
|
+
function surfaceOf(value) {
|
|
51
|
+
if (value === null || typeof value !== "object") return value;
|
|
52
|
+
if (Array.isArray(value)) return `[${value.length}]`;
|
|
53
|
+
const kept = {};
|
|
54
|
+
for (const [key, v] of Object.entries(value)) if (v === null || typeof v === "number" || typeof v === "boolean") kept[key] = v;
|
|
55
|
+
else if (typeof v === "string" && v.length <= SCALAR_MAX_CHARS) kept[key] = v;
|
|
56
|
+
else if (Array.isArray(v) && v.every((e) => typeof e === "string")) {
|
|
57
|
+
if (v.join(",").length <= SCALAR_MAX_CHARS) kept[key] = v;
|
|
58
|
+
}
|
|
59
|
+
return kept;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Une donnée trop volumineuse, ramenée à une RÉPONSE.
|
|
63
|
+
*
|
|
64
|
+
* Deux formes, selon ce qu'on a sous la main :
|
|
65
|
+
* - **un tableau** rend son `count` — la première chose qu'on veut d'une liste,
|
|
66
|
+
* et celle qu'un modèle calcule mal et cher — puis ses entrées en surface ;
|
|
67
|
+
* - **un objet** rend ses clés de premier niveau : c'est la carte qui permet de
|
|
68
|
+
* demander ensuite la bonne branche (pour `config`, `inspect module <nom>`).
|
|
69
|
+
*
|
|
70
|
+
* Dans les deux cas la réponse DIT ce qu'elle a fait, et elle le dit EN TÊTE.
|
|
71
|
+
* Une troncature muette est pire que pas de troncature — l'agent croit tout
|
|
72
|
+
* tenir ; une troncature annoncée APRÈS vingt mille caractères d'entrées ne vaut
|
|
73
|
+
* guère mieux, il a déjà décidé quoi faire de ce qu'il a lu. Mesuré : sur la
|
|
74
|
+
* tâche « annonce le nombre de routes », l'agent recevait `count` puis 88
|
|
75
|
+
* entrées puis, tout en bas, l'avertissement — et repartait recompter les
|
|
76
|
+
* entrées visibles au lieu de lire le compte exact qu'on venait de lui donner.
|
|
77
|
+
*
|
|
78
|
+
* Et la note ne renvoie PAS vers un affinement : `routes`, `services` ou
|
|
79
|
+
* `modules` n'acceptent aucun filtre, si bien que « affine la demande » nommait
|
|
80
|
+
* un geste qui n'existe pas. Elle désigne ce qui EST utilisable — `count`, exact
|
|
81
|
+
* et portant sur la liste entière.
|
|
82
|
+
*/
|
|
83
|
+
function summarize(value, size) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
const items = value.map(surfaceOf);
|
|
86
|
+
let rendered = JSON.stringify({
|
|
87
|
+
count: value.length,
|
|
88
|
+
note: `count = ${value.length} : c'est le nombre EXACT, il porte sur la liste entière. Les entrées ci-dessous sont ramenées à leur surface (${size} caractères au complet) — demande le détail d'une entrée précise plutôt que la liste entière`,
|
|
89
|
+
items
|
|
90
|
+
}, null, 2);
|
|
91
|
+
if (rendered.length <= 32e3) return rendered;
|
|
92
|
+
let keptCount = items.length;
|
|
93
|
+
while (keptCount > 1 && rendered.length > 32e3) {
|
|
94
|
+
keptCount = Math.floor(keptCount / 2);
|
|
95
|
+
rendered = JSON.stringify({
|
|
96
|
+
count: value.length,
|
|
97
|
+
note: `count = ${value.length} : c'est le nombre EXACT, il porte sur la liste entière. Seules ${keptCount} entrées sont montrées (la liste pèse ${size} caractères) — toute affirmation sur le NOMBRE se fonde sur count, jamais sur les entrées visibles.`,
|
|
98
|
+
items: items.slice(0, keptCount)
|
|
99
|
+
}, null, 2);
|
|
100
|
+
}
|
|
101
|
+
return rendered;
|
|
102
|
+
}
|
|
103
|
+
const keys = value && typeof value === "object" ? Object.keys(value) : void 0;
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
keys,
|
|
106
|
+
note: `réponse de ${size} caractères, trop volumineuse pour être rendue entière — demande une branche précise (par exemple le module qui t'intéresse)`
|
|
107
|
+
}, null, 2);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Rend un résultat d'outil — les données partent en JSON COMPACT.
|
|
111
|
+
*
|
|
112
|
+
* Le lecteur est un agent : il relit le JSON compact sans peine, et chaque
|
|
113
|
+
* caractère rendu se repaie à chaque tour suivant de sa conversation.
|
|
114
|
+
* L'indentation représentait ~25 % d'un rapport `nodefony_check`.
|
|
115
|
+
*
|
|
116
|
+
* Exporté parce qu'une application qui déclare un outil en a besoin : sans lui,
|
|
117
|
+
* chacune réinventerait l'enveloppe `content[]`, et une enveloppe mal formée
|
|
118
|
+
* n'échoue pas — elle rend un résultat vide que l'agent prend pour une réponse.
|
|
119
|
+
*
|
|
120
|
+
* **Un résultat est BORNÉ** ({@link MCP_TEXT_MAX_CHARS}) : au-delà, il rend un
|
|
121
|
+
* résumé plutôt qu'un déversement (voir {@link summarize}). La garde vit ICI, et
|
|
122
|
+
* non dans chaque outil, pour qu'un outil déclaré par une
|
|
123
|
+
* application en hérite sans le savoir — c'est le genre de règle qu'on
|
|
124
|
+
* n'applique jamais deux fois de la même façon si on la laisse à l'appelant.
|
|
125
|
+
* En deçà de la borne — le cas courant — la donnée part TELLE QUELLE : un
|
|
126
|
+
* consommateur qui la parse n'a rien à changer, et on ne paie pas la garde sur
|
|
127
|
+
* 100 % des appels pour 1 % de gros sujets.
|
|
128
|
+
*
|
|
129
|
+
* @param value - donnée à rendre (sérialisée si ce n'est pas déjà du texte)
|
|
130
|
+
* @param isError - marque un échec MÉTIER (pas une faute de protocole)
|
|
131
|
+
*/
|
|
132
|
+
function mcpText(value, isError = false) {
|
|
133
|
+
const rendered = typeof value === "string" ? value : JSON.stringify(value);
|
|
134
|
+
const text = isError || rendered.length <= 32e3 ? rendered : summarize(value, rendered.length);
|
|
135
|
+
return isError ? {
|
|
136
|
+
content: [{
|
|
137
|
+
type: "text",
|
|
138
|
+
text
|
|
139
|
+
}],
|
|
140
|
+
isError: true
|
|
141
|
+
} : { content: [{
|
|
142
|
+
type: "text",
|
|
143
|
+
text
|
|
144
|
+
}] };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Forme admise pour un nom d'outil.
|
|
148
|
+
*
|
|
149
|
+
* Ce nom ne reste pas dans le serveur : il voyage jusque dans le contexte du
|
|
150
|
+
* modèle, et sert d'identifiant d'appel. Un nom porteur d'espace, de guillemet
|
|
151
|
+
* ou de saut de ligne ne provoque pas d'erreur — il produit des appels que rien
|
|
152
|
+
* ne résout, ce qui est bien pire à diagnostiquer. La borne haute est du même
|
|
153
|
+
* ordre : un nom de 4 000 caractères passerait, et mangerait le contexte.
|
|
154
|
+
*/
|
|
155
|
+
const NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
156
|
+
/** Appelant anonyme — le défaut, et le seul défaut sûr. */
|
|
157
|
+
const ANONYMOUS = {
|
|
158
|
+
authenticated: false,
|
|
159
|
+
scopes: [],
|
|
160
|
+
roles: []
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Préfixe RÉSERVÉ aux modules du framework (`@nodefony/*`) — un agent qui lit
|
|
164
|
+
* `nodefony_inspect` doit pouvoir croire que c'est le framework qui répond.
|
|
165
|
+
*
|
|
166
|
+
* Sans la réserve, un module d'application pourrait déclarer `nodefony_doc` et
|
|
167
|
+
* répondre à la place de l'outil que l'agent a lu dans la documentation du
|
|
168
|
+
* framework : l'ordre de chargement des modules ne protège rien, il change
|
|
169
|
+
* avec le manifeste.
|
|
170
|
+
*/
|
|
171
|
+
const MCP_RESERVED_PREFIX = "nodefony_";
|
|
172
|
+
/** Portée npm des modules du framework, seuls détenteurs du préfixe réservé. */
|
|
173
|
+
const FRAMEWORK_SCOPE = "@nodefony/";
|
|
174
|
+
/**
|
|
175
|
+
* Le module a-t-il droit au préfixe {@link MCP_RESERVED_PREFIX} ?
|
|
176
|
+
*
|
|
177
|
+
* Lit `getModuleName()` (le nom npm du paquet), défensivement : un module écrit
|
|
178
|
+
* en JavaScript peut ne pas l'avoir, et une absence vaut refus.
|
|
179
|
+
*
|
|
180
|
+
* @param module - module chargé, de forme non garantie
|
|
181
|
+
* @returns `true` si le module appartient à la portée `@nodefony/`
|
|
182
|
+
*/
|
|
183
|
+
function ownsReservedPrefix(module) {
|
|
184
|
+
if (typeof module !== "object" || module === null) return false;
|
|
185
|
+
if (!("getModuleName" in module)) return false;
|
|
186
|
+
const getName = module.getModuleName;
|
|
187
|
+
if (typeof getName !== "function") return false;
|
|
188
|
+
const name = getName.call(module);
|
|
189
|
+
return typeof name === "string" && name.startsWith(FRAMEWORK_SCOPE);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* L'appelant satisfait-il ce que l'outil exige ?
|
|
193
|
+
*
|
|
194
|
+
* @returns `null` s'il passe, sinon le motif de la rétention
|
|
195
|
+
*/
|
|
196
|
+
function withholdReason(tool, caller) {
|
|
197
|
+
const needsScopes = tool.scopes !== void 0 && tool.scopes.length > 0;
|
|
198
|
+
if (!needsScopes && tool.requiresAuth !== true) return null;
|
|
199
|
+
if (!caller.authenticated) {
|
|
200
|
+
if (tool.actsForUser === true) return "agit au nom d'un compte, l'appelant est anonyme";
|
|
201
|
+
const opened = needsScopes ? rolesFromScopes(tool.scopes) : [];
|
|
202
|
+
if (opened.length > 0 && opened.every((role) => caller.roles.includes(role))) return null;
|
|
203
|
+
return "exige une identité prouvée, l'appelant est anonyme";
|
|
204
|
+
}
|
|
205
|
+
if (!needsScopes) return null;
|
|
206
|
+
const missing = tool.scopes.filter((scope) => !caller.scopes.includes(scope));
|
|
207
|
+
return missing.length === 0 ? null : `scopes manquants : ${missing.join(", ")}`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Motif pour lequel une valeur rendue par `getMcpTools()` n'est pas un outil
|
|
211
|
+
* servable, ou `null` si elle en est un.
|
|
212
|
+
*
|
|
213
|
+
* Un module peut être écrit en JavaScript : le contrat `IMcpTool` n'y est
|
|
214
|
+
* vérifié par personne. `scopes` et `requiresAuth` portent l'autorisation —
|
|
215
|
+
* `requiresAuth: "oui"` ne vaut pas `true` et servirait l'outil à un anonyme.
|
|
216
|
+
* Une forme fausse se REFUSE à la déclaration, elle ne s'interprète jamais.
|
|
217
|
+
*
|
|
218
|
+
* @param tool - valeur déclarée par un module
|
|
219
|
+
* @returns le motif du refus, ou `null`
|
|
220
|
+
*/
|
|
221
|
+
function mcpToolFault(tool) {
|
|
222
|
+
if (typeof tool !== "object" || tool === null) return "pas un objet";
|
|
223
|
+
if (!("name" in tool) || typeof tool.name !== "string" || !NAME_PATTERN.test(tool.name)) return `nom absent ou hors forme — attendu ${NAME_PATTERN.source}`;
|
|
224
|
+
if (!("handler" in tool) || typeof tool.handler !== "function") return "sans handler";
|
|
225
|
+
if (!("description" in tool) || typeof tool.description !== "string") return "`description` n'est pas une chaîne";
|
|
226
|
+
if (!("inputSchema" in tool) || typeof tool.inputSchema !== "object" || tool.inputSchema === null || Array.isArray(tool.inputSchema)) return "`inputSchema` n'est pas un objet";
|
|
227
|
+
if ("annotations" in tool && tool.annotations !== void 0 && (typeof tool.annotations !== "object" || tool.annotations === null)) return "`annotations` n'est pas un objet";
|
|
228
|
+
if ("scopes" in tool && tool.scopes !== void 0 && !(Array.isArray(tool.scopes) && tool.scopes.every((scope) => typeof scope === "string"))) return "`scopes` n'est pas un tableau de chaînes";
|
|
229
|
+
if ("scopesFor" in tool && tool.scopesFor !== void 0 && typeof tool.scopesFor !== "function") return "`scopesFor` n'est pas une fonction";
|
|
230
|
+
if ("requiresAuth" in tool && tool.requiresAuth !== void 0 && typeof tool.requiresAuth !== "boolean") return "`requiresAuth` n'est pas un booléen";
|
|
231
|
+
if ("actsForUser" in tool && tool.actsForUser !== void 0) {
|
|
232
|
+
if (typeof tool.actsForUser !== "boolean") return "`actsForUser` n'est pas un booléen";
|
|
233
|
+
if (tool.actsForUser && !("scopes" in tool && Array.isArray(tool.scopes) && tool.scopes.length > 0)) return "`actsForUser` exige des `scopes` : les rôles du compte ne sont pas bornés par le jeton";
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/** La valeur déclarée a la forme d'un outil servable (cf {@link mcpToolFault}). */
|
|
238
|
+
function isMcpTool(tool) {
|
|
239
|
+
return mcpToolFault(tool) === null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Énumère ce que cette application DÉCLARE — les outils de ses modules, lus
|
|
243
|
+
* par `getMcpTools()` — sans rien retenir sur l'identité de qui que ce soit.
|
|
244
|
+
*
|
|
245
|
+
* Cinq refus, tous énoncés par `onSkip` plutôt que silencieux :
|
|
246
|
+
* - **nom hors forme** ({@link NAME_PATTERN}) — il ne serait pas appelable ;
|
|
247
|
+
* - **forme fausse** ({@link mcpToolFault}) — handler, description, schéma ou
|
|
248
|
+
* autorisation qui ne respectent pas `IMcpTool` ;
|
|
249
|
+
* - **préfixe réservé** ({@link MCP_RESERVED_PREFIX}) pris par un module hors de
|
|
250
|
+
* `@nodefony/` — un module d'application ne peut donc pas se substituer à
|
|
251
|
+
* `nodefony_inspect` et répondre à sa place ;
|
|
252
|
+
* - **collision** — le premier inscrit garde le nom ;
|
|
253
|
+
* - **déclaration en échec** — un `getMcpTools()` qui lève ne doit pas priver
|
|
254
|
+
* l'agent des outils de tous les autres modules.
|
|
255
|
+
*
|
|
256
|
+
* 🔴 **Ce que rend cette fonction n'est PAS servable tel quel** : elle contient
|
|
257
|
+
* les outils réservés. Toute porte passe par {@link collectMcpTools}, qui filtre
|
|
258
|
+
* sur l'appelant. Le seul usage légitime de la liste brute est de raisonner sur
|
|
259
|
+
* le CODE — ce que la porte EXIGE ({@link mcpDeclaredScopes}) — jamais de
|
|
260
|
+
* répondre à une requête.
|
|
261
|
+
*
|
|
262
|
+
* @param options - modules et journal des écarts
|
|
263
|
+
* @returns les outils déclarés, dans l'ordre où ils seraient publiés
|
|
264
|
+
*/
|
|
265
|
+
function declareMcpTools(options) {
|
|
266
|
+
const declared = [];
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
const add = (tool, origin, ownsPrefix) => {
|
|
269
|
+
const name = typeof tool === "object" && tool !== null && "name" in tool ? tool.name : void 0;
|
|
270
|
+
if (typeof name !== "string" || !NAME_PATTERN.test(name)) {
|
|
271
|
+
options.onSkip?.(`outil écarté (${origin}) : nom absent ou hors forme — attendu ${NAME_PATTERN.source}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!ownsPrefix && name.startsWith("nodefony_")) {
|
|
275
|
+
options.onSkip?.(`outil « ${name} » écarté (${origin}) : le préfixe « ${MCP_RESERVED_PREFIX} » est réservé aux modules ${FRAMEWORK_SCOPE}*`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!isMcpTool(tool)) {
|
|
279
|
+
options.onSkip?.(`outil « ${name} » écarté (${origin}) : ${mcpToolFault(tool)}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (seen.has(tool.name)) {
|
|
283
|
+
options.onSkip?.(`outil « ${tool.name} » écarté (${origin}) : ce nom est déjà pris`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
seen.add(tool.name);
|
|
287
|
+
declared.push(tool);
|
|
288
|
+
};
|
|
289
|
+
for (const [name, module] of Object.entries(options.modules ?? {})) {
|
|
290
|
+
if (typeof module !== "object" || module === null || !("getMcpTools" in module)) continue;
|
|
291
|
+
const declare = module.getMcpTools;
|
|
292
|
+
if (typeof declare !== "function") continue;
|
|
293
|
+
let tools;
|
|
294
|
+
try {
|
|
295
|
+
tools = declare.call(module);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
options.onSkip?.(`module « ${name} » : getMcpTools() a échoué — ${error instanceof Error ? error.message : String(error)}`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (!Array.isArray(tools)) {
|
|
301
|
+
options.onSkip?.(`module « ${name} » : getMcpTools() doit rendre un tableau`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const ownsPrefix = ownsReservedPrefix(module);
|
|
305
|
+
for (const tool of tools) add(tool, `module ${name}`, ownsPrefix);
|
|
306
|
+
}
|
|
307
|
+
return declared;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Ramasse les outils SERVIS à cet appelant.
|
|
311
|
+
*
|
|
312
|
+
* Aux trois refus de {@link declareMcpTools} s'ajoute une RÉTENTION, qui n'est
|
|
313
|
+
* pas une faute : un outil dont l'appelant ne satisfait pas les exigences
|
|
314
|
+
* ({@link IMcpTool.scopes}, {@link IMcpTool.requiresAuth}) ne sort pas d'ici.
|
|
315
|
+
* ⭐ **Filtrer À LA COLLECTE protège `tools/list` ET `tools/call` d'un seul
|
|
316
|
+
* geste** : le protocole ne reçoit que les outils servis, donc un outil retenu
|
|
317
|
+
* est « inconnu » pour lui — indistinguable d'un outil inexistant, ce qui ne
|
|
318
|
+
* révèle même pas son existence. Filtrer la liste sans filtrer l'appel n'aurait
|
|
319
|
+
* été qu'un rideau, et c'est l'erreur classique : deux points de décision, dont
|
|
320
|
+
* un qu'on oublie.
|
|
321
|
+
*
|
|
322
|
+
* @param options - modules, appelant et journaux
|
|
323
|
+
* @returns les outils exécutables, dans l'ordre où ils seront publiés
|
|
324
|
+
*/
|
|
325
|
+
function collectMcpTools(options) {
|
|
326
|
+
const caller = options.caller ?? ANONYMOUS;
|
|
327
|
+
const served = [];
|
|
328
|
+
for (const tool of declareMcpTools(options)) {
|
|
329
|
+
const withheld = withholdReason(tool, caller);
|
|
330
|
+
if (withheld !== null) {
|
|
331
|
+
options.onWithheld?.(tool.name, withheld);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
served.push(tool);
|
|
335
|
+
}
|
|
336
|
+
return served;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Le vocabulaire de scopes que cette porte EXIGE — l'union de ce que ses outils
|
|
340
|
+
* déclarent, jamais une liste écrite à côté.
|
|
341
|
+
*
|
|
342
|
+
* 🔴 **C'est ce qu'un client doit demander**, et c'est à ce titre qu'il est
|
|
343
|
+
* publié (RFC 9728 `scopes_supported`) et nommé dans le défi (RFC 6750
|
|
344
|
+
* `scope`). Une liste de configuration disait autre chose que le code par
|
|
345
|
+
* simple oubli, dans les deux sens et sans qu'aucun contrôle ne l'aperçoive :
|
|
346
|
+
* un scope publié que rien n'exige envoie demander un droit qui n'ouvre rien ;
|
|
347
|
+
* un scope exigé que rien ne publie laisse le client deviner ce qu'on attend de
|
|
348
|
+
* lui — et un outil de module en était systématiquement là.
|
|
349
|
+
*
|
|
350
|
+
* L'énumération est délibérément celle des outils DÉCLARÉS, pas de ceux servis
|
|
351
|
+
* à un appelant : le document se lit avant d'avoir un jeton, et un catalogue
|
|
352
|
+
* filtré n'annoncerait à l'anonyme que ce dont il n'a pas besoin.
|
|
353
|
+
*
|
|
354
|
+
* @param options - modules et journal des écarts
|
|
355
|
+
* @returns les scopes, dédupliqués et triés ; vide si la porte n'exige rien
|
|
356
|
+
*/
|
|
357
|
+
function mcpDeclaredScopes(options) {
|
|
358
|
+
const vocabulaire = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const tool of declareMcpTools(options)) for (const scope of tool.scopes ?? []) vocabulaire.add(scope);
|
|
360
|
+
return [...vocabulaire].sort();
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Projette les outils pour `tools/list` — sans leur implémentation.
|
|
364
|
+
*
|
|
365
|
+
* Le `handler` est une fermeture sur l'état du serveur ; `JSON.stringify` le
|
|
366
|
+
* laisserait tomber en silence, mais compter là-dessus reviendrait à publier
|
|
367
|
+
* par accident ce qu'on ne publie pas exprès.
|
|
368
|
+
*/
|
|
369
|
+
function publishMcpTools(tools) {
|
|
370
|
+
return tools.map(({ name, description, inputSchema, annotations }) => annotations === void 0 ? {
|
|
371
|
+
name,
|
|
372
|
+
description,
|
|
373
|
+
inputSchema
|
|
374
|
+
} : {
|
|
375
|
+
name,
|
|
376
|
+
description,
|
|
377
|
+
inputSchema,
|
|
378
|
+
annotations
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Exécute un outil par son nom public.
|
|
383
|
+
*
|
|
384
|
+
* Un échec métier (sujet inconnu, module absent) rend un résultat `isError`,
|
|
385
|
+
* **pas** une erreur JSON-RPC : le protocole réserve celle-ci aux fautes de
|
|
386
|
+
* protocole. La distinction compte pour l'agent — une erreur de protocole
|
|
387
|
+
* signifie « tu t'y prends mal », un `isError` signifie « ta demande est
|
|
388
|
+
* recevable, voici pourquoi elle n'aboutit pas » ; c'est la seconde qu'il peut
|
|
389
|
+
* corriger seul.
|
|
390
|
+
*
|
|
391
|
+
* ⭐ **Aucun contrôle d'autorisation ici, et c'est voulu** : `tools` ne contient
|
|
392
|
+
* que ce que {@link collectMcpTools} a servi à CET appelant. Refaire la
|
|
393
|
+
* vérification à ce niveau créerait un second point de décision — deux copies
|
|
394
|
+
* d'une même règle, qui divergent en silence. Le corollaire tient en une
|
|
395
|
+
* phrase : **ne jamais passer ici une liste non filtrée.**
|
|
396
|
+
*
|
|
397
|
+
* @param name - nom public de l'outil (`nodefony_inspect`…)
|
|
398
|
+
* @param args - arguments fournis par l'agent
|
|
399
|
+
* @param tools - outils servis, tels que {@link collectMcpTools} les a ramassés
|
|
400
|
+
* @param caller - appelant établi, transmis au handler (anonyme par défaut)
|
|
401
|
+
* @param run - annulation et progression de CET appel (détaché par défaut)
|
|
402
|
+
* @returns le résultat de l'outil, ou `null` si le nom n'est pas exposé
|
|
403
|
+
*/
|
|
404
|
+
async function callMcpTool(name, args, tools, caller = ANONYMOUS, run = DETACHED_RUN) {
|
|
405
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
406
|
+
if (!tool) return null;
|
|
407
|
+
const refusal = unknownArguments(tool, args);
|
|
408
|
+
if (refusal !== null) return mcpText(refusal, true);
|
|
409
|
+
const scopes = mcpCallScopes(tool, args);
|
|
410
|
+
if (scopes === null) return mcpText(`« ${tool.name} » : les scopes de cet appel sont indéterminables (\`scopesFor\` a levé ou rendu une forme fausse) — appel refusé`, true);
|
|
411
|
+
const missing = missingScopes(caller.scopes, scopes);
|
|
412
|
+
if (missing.length > 0) return mcpText(insufficientScopeMessage(tool.name, missing), true);
|
|
413
|
+
return tool.handler(args, caller, run);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Tous les scopes qu'exige UN appel : ceux de l'outil, puis ceux que ses
|
|
417
|
+
* arguments ajoutent ({@link IMcpTool.scopesFor}). Seule source de la règle,
|
|
418
|
+
* lue par l'appel en process ET par le défi `insufficient_scope` d'une porte.
|
|
419
|
+
*
|
|
420
|
+
* @param tool - outil servi
|
|
421
|
+
* @param args - arguments de l'appel
|
|
422
|
+
* @returns les scopes exigés, sans doublon ; `null` si `scopesFor` lève ou rend
|
|
423
|
+
* une forme fausse (l'appel est alors refusé, jamais servi sans borne)
|
|
424
|
+
*/
|
|
425
|
+
function mcpCallScopes(tool, args) {
|
|
426
|
+
const base = tool.scopes ?? [];
|
|
427
|
+
if (tool.scopesFor === void 0) return base;
|
|
428
|
+
let extra;
|
|
429
|
+
try {
|
|
430
|
+
extra = tool.scopesFor(args);
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
if (!Array.isArray(extra) || !extra.every((scope) => typeof scope === "string")) return null;
|
|
435
|
+
return extra.length === 0 ? base : [.../* @__PURE__ */ new Set([...base, ...extra])];
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Le message d'un appel refusé faute de scope — le même pour le résultat
|
|
439
|
+
* `isError` et la description du défi `403`.
|
|
440
|
+
*
|
|
441
|
+
* @param name - nom de l'outil
|
|
442
|
+
* @param missing - scopes absents du jeton
|
|
443
|
+
* @returns l'explication, qui nomme les scopes à demander
|
|
444
|
+
*/
|
|
445
|
+
function insufficientScopeMessage(name, missing) {
|
|
446
|
+
return `« ${name} » avec ces arguments exige le(s) scope(s) ${missing.join(" ")}, absent(s) du jeton présenté — redemander un jeton qui les porte`;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* L'exécution d'un appel SANS porte qui l'observe (appel direct, test) : jamais
|
|
450
|
+
* annulé, progression muette. Partagé — un signal jamais abattu ne retient rien.
|
|
451
|
+
*/
|
|
452
|
+
const DETACHED_RUN = {
|
|
453
|
+
signal: new AbortController().signal,
|
|
454
|
+
progress: () => {}
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* Arguments que l'outil ne déclare pas — refusés, jamais ignorés.
|
|
458
|
+
*
|
|
459
|
+
* ⚠️ Un paramètre accepté puis jeté est pire qu'un refus : la réponse a l'air
|
|
460
|
+
* d'avoir honoré la demande. Vécu sur cette porte — un `limit` passé à la
|
|
461
|
+
* recherche documentaire revenait avec vingt résultats, sans un mot, et
|
|
462
|
+
* l'appelant en concluait que la borne ne marchait pas plutôt que de voir
|
|
463
|
+
* qu'elle n'avait jamais été transmise. Le même silence accueillait une clé
|
|
464
|
+
* mal orthographiée : l'outil répondait comme si de rien n'était.
|
|
465
|
+
*
|
|
466
|
+
* La garde vit ICI, une fois, plutôt que dans chaque handler — et elle
|
|
467
|
+
* s'applique donc aussi aux outils qu'une application déclare. Un outil qui
|
|
468
|
+
* veut réellement des arguments libres l'écrit dans son schéma
|
|
469
|
+
* (`additionalProperties: true`), ce qui en fait un choix relu, pas un défaut.
|
|
470
|
+
*
|
|
471
|
+
* @param tool - l'outil visé, avec son schéma d'entrée.
|
|
472
|
+
* @param args - ce que l'agent a passé.
|
|
473
|
+
* @returns le message de refus, ou `null` si tout est déclaré.
|
|
474
|
+
*/
|
|
475
|
+
function unknownArguments(tool, args) {
|
|
476
|
+
const schema = tool.inputSchema;
|
|
477
|
+
if (schema?.additionalProperties === true) return null;
|
|
478
|
+
const declared = schema?.properties;
|
|
479
|
+
if (!declared || typeof declared !== "object") return null;
|
|
480
|
+
const intruders = Object.keys(args).filter((key) => !Object.hasOwn(declared, key));
|
|
481
|
+
if (intruders.length === 0) return null;
|
|
482
|
+
const known = Object.keys(declared);
|
|
483
|
+
return `« ${tool.name} » ne connaît pas ${intruders.map((c) => `« ${c} »`).join(", ")}. ` + (known.length > 0 ? `Arguments acceptés : ${known.join(", ")}.` : "Cet outil ne prend aucun argument.");
|
|
484
|
+
}
|
|
485
|
+
export { MCP_RESERVED_PREFIX, MCP_TEXT_MAX_CHARS, callMcpTool, collectMcpTools, declareMcpTools, insufficientScopeMessage, mcpCallScopes, mcpDeclaredScopes, mcpText, publishMcpTools };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// nodefony/src/errors/AiError.ts
|
|
2
|
+
/**
|
|
3
|
+
* Erreur du module ai.
|
|
4
|
+
*
|
|
5
|
+
* Deux champs qui font la différence en production :
|
|
6
|
+
* - `code` : identifiant MACHINE (stable, grep-able, consommé par Studio et
|
|
7
|
+
* l'audit) — le message, lui, peut changer sans rien casser ;
|
|
8
|
+
* - `context` : payload structuré joint au log (jamais de secret ici).
|
|
9
|
+
*/
|
|
10
|
+
var AiError = class extends Error {
|
|
11
|
+
code;
|
|
12
|
+
context;
|
|
13
|
+
constructor(message, code = "AI_ERROR", context) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.context = context;
|
|
17
|
+
this.name = "AiError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export { AiError, AiError as default };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Kernel, Module } from "nodefony";
|
|
2
|
+
import { type IAiConfigInput } from "./nodefony/config/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* @nodefony/ai — La boîte à outils IA de Nodefony : serveur MCP, puis les services de la couche agentique.
|
|
5
|
+
*
|
|
6
|
+
* Module applicatif : un workspace npm à part entière
|
|
7
|
+
* (`src/packages/@nodefony/ai/`), chargé par le manifeste `modules` de
|
|
8
|
+
* `nodefony.config.ts`. Le Kernel l'importe PAR SON NOM (`@nodefony/ai`) —
|
|
9
|
+
* d'où le workspace, qui le rend résolvable.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Rend la config du module typée à l'appel : `use("@nodefony/ai", { … })`
|
|
13
|
+
* dans `nodefony.config.ts` propose les clés du schéma et refuse les fautes de
|
|
14
|
+
* frappe, au lieu d'avaler un `Record<string, unknown>`.
|
|
15
|
+
*/
|
|
16
|
+
declare module "nodefony" {
|
|
17
|
+
interface NodefonyModuleConfig {
|
|
18
|
+
"@nodefony/ai": IAiConfigInput;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
declare class AiModule extends Module {
|
|
22
|
+
constructor(kernel: Kernel);
|
|
23
|
+
/**
|
|
24
|
+
* Valide la config au boot — défauts du schéma fusionnés avec ce que l'app
|
|
25
|
+
* passe dans `use()`. Une clé inconnue ou mal typée plante ICI, avec le champ
|
|
26
|
+
* fautif nommé, plutôt qu'en `undefined.x` au premier appel en production.
|
|
27
|
+
*/
|
|
28
|
+
onKernelRegister(): Promise<this>;
|
|
29
|
+
}
|
|
30
|
+
export default AiModule;
|
|
31
|
+
export type { IAiConfigInput } from "./nodefony/config/config.js";
|
|
32
|
+
export { aiConfigSchema, mcpConfigSchema, type IAiConfig, type IMcpConfig, } from "./nodefony/config/config.js";
|
|
33
|
+
export { defineAiConfig, aiConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
|
|
34
|
+
export { MCP_PROTOCOL_VERSION, MCP_SUPPORTED_VERSIONS, MCP_DEFAULT_NEGOTIATED_VERSION, META_PROTOCOL_VERSION, META_SERVER_INFO, McpProtocolError, JsonRpcError, jsonRpcSuccess, jsonRpcFailure, isNotification, } from "./nodefony/mcp/protocol.js";
|
|
35
|
+
export type { IMcpHttpReply, JsonRpcId, IJsonRpcMessage, IJsonRpcSuccess, IJsonRpcFailure, McpProtocolVersion, } from "./nodefony/mcp/protocol.js";
|
|
36
|
+
export { handleMcpMessage } from "./nodefony/mcp/server.js";
|
|
37
|
+
export type { IMcpServerContext, IMcpHeaders, IMcpTransport, IServerInfo, IMcpCacheHints, } from "./nodefony/mcp/server.js";
|
|
38
|
+
export { defineMcpGate, mcpGateSchema, MCP_STREAM_MAX_EVENT_BYTES, } from "./nodefony/mcp/gate.js";
|
|
39
|
+
export type { IMcpGate, IMcpGateDefinition, IMcpGateSettings, IMcpGateRequest, IMcpGateRuntime, IMcpGateResponse, IMcpStdioRequest, McpGateSeverity, } from "./nodefony/mcp/gate.js";
|
|
40
|
+
export { serveMcpStdio } from "./nodefony/mcp/stdio.js";
|
|
41
|
+
export type { IMcpStdioOptions } from "./nodefony/mcp/stdio.js";
|
|
42
|
+
export { createProgressReporter, readProgressToken, MCP_PROGRESS_MIN_INTERVAL_MS, MCP_PROGRESS_MESSAGE_MAX, } from "./nodefony/mcp/progress.js";
|
|
43
|
+
export type { IMcpProgressReporter, McpProgressToken, } from "./nodefony/mcp/progress.js";
|
|
44
|
+
export { createMcpResponseStream, MCP_STREAM_GRACE_MS, } from "./nodefony/mcp/stream.js";
|
|
45
|
+
export type { IMcpEventSink, IMcpResponseStream, IMcpResponseStreamOptions, } from "./nodefony/mcp/stream.js";
|
|
46
|
+
export { checkMcpAccess, isLocalAddress, isRelayedRequest, RELAY_HEADERS, } from "./nodefony/mcp/guard.js";
|
|
47
|
+
export type { GuardVerdict, IGuardInput, IGuardPolicy, } from "./nodefony/mcp/guard.js";
|
|
48
|
+
export { SESSION_TICKET_HEADER, SESSION_TICKET_LENGTH, SESSION_TICKET_MAX_PENDING, SESSION_TICKET_TTL_MS, SessionTicketStore, fingerprintSessionTicket, } from "./nodefony/mcp/sessionTicket.js";
|
|
49
|
+
export type { ISessionTicketStoreOptions } from "./nodefony/mcp/sessionTicket.js";
|
|
50
|
+
export { TICKET_SESSION_REFUSED, defineTicketSessionGate, isSessionFlowView, isSessionServiceView, openTicketSession, readTicketSessionVerdict, ticketSessionGateSchema, toTicketSessionUser, } from "./nodefony/mcp/ticketSession.js";
|
|
51
|
+
export type { ISessionFlowView, ISessionServiceView, ITicketSessionDeps, ITicketSessionGate, ITicketSessionGateDefinition, ITicketSessionGateSettings, ITicketSessionRequest, ITicketSessionUser, TicketSessionVerdict, } from "./nodefony/mcp/ticketSession.js";
|
|
52
|
+
export { mcpCallerRoles } from "./nodefony/mcp/caller.js";
|
|
53
|
+
export type { IMcpGatePosture } from "./nodefony/mcp/caller.js";
|
|
54
|
+
export { mcpText, declareMcpTools, collectMcpTools, mcpDeclaredScopes, mcpCallScopes, insufficientScopeMessage, publishMcpTools, callMcpTool, MCP_TEXT_MAX_CHARS, MCP_RESERVED_PREFIX, } from "./nodefony/mcp/tools.js";
|
|
55
|
+
export type { IMcpTool, IMcpToolAnnotations, IMcpToolDefinition, IMcpToolResult, IMcpCaller, IMcpToolRun, IMcpDeclareOptions, IMcpCollectOptions, } from "./nodefony/mcp/tools.js";
|
|
56
|
+
export { AiError } from "./nodefony/src/errors/AiError.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command, CliKernel } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Sert le MCP de l'application sur l'entrée et la sortie standard —
|
|
4
|
+
* `nodefony ai:stdio`.
|
|
5
|
+
*
|
|
6
|
+
* Pour le client qui lance lui-même son serveur MCP et lui parle par stdio
|
|
7
|
+
* (agent de CI, interface en ligne de commande, agent cloud `"type": "local"`) :
|
|
8
|
+
* rien n'y écoute sur `localhost`, la route `/nodefony/mcp` lui est hors
|
|
9
|
+
* d'atteinte.
|
|
10
|
+
*
|
|
11
|
+
* Servi dans TOUS les environnements : les outils des modules chargés, avec la
|
|
12
|
+
* posture de l'OPÉRATEUR LOCAL — celle de toute commande du CLI
|
|
13
|
+
* (`localOperatorCaller`, qui dit pourquoi). Qui lance la commande a déjà un
|
|
14
|
+
* terminal dans l'application : `nodefony inspect` lui rend les mêmes données.
|
|
15
|
+
* Ce rôle n'ouvre que l'administration du framework (`admin:read`/`admin:write`
|
|
16
|
+
* se traduisent en lui, `rolesFromScopes`) ; un outil à scope APPLICATIF, un
|
|
17
|
+
* outil `requiresAuth` ou `actsForUser` reste retenu — il exige un jeton, donc
|
|
18
|
+
* une porte HTTP protégée. Les outils `nodefony_*` n'y paraissent que lorsque
|
|
19
|
+
* `@nodefony/devkit` est chargé (développement).
|
|
20
|
+
*
|
|
21
|
+
* Limite : le code servi est celui du boot, sans rechargement à chaud — la
|
|
22
|
+
* porte HTTP de développement reste celle qui suit les sources.
|
|
23
|
+
*/
|
|
24
|
+
declare class StdioCommand extends Command {
|
|
25
|
+
constructor(cli: CliKernel);
|
|
26
|
+
generate(): Promise<this>;
|
|
27
|
+
}
|
|
28
|
+
export default StdioCommand;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Réglages de la porte MCP (`POST /nodefony/mcp`) — la SOURCE UNIQUE du schéma
|
|
4
|
+
* `mcp.*`, que tout module qui sert ou configure la porte importe d'ici.
|
|
5
|
+
*
|
|
6
|
+
* Les outils exposés ne se règlent PAS ici : chaque module déclare les siens
|
|
7
|
+
* (`getMcpTools()`), et un module qui veut filtrer son propre catalogue porte ce
|
|
8
|
+
* réglage dans SA config.
|
|
9
|
+
*
|
|
10
|
+
* Tant qu'aucun serveur d'autorisation n'est déclaré, la porte n'est protégée
|
|
11
|
+
* que par les deux gardes que la spec impose au transport ({@link allowedOrigins},
|
|
12
|
+
* {@link allowRemote}) — qui visent le risque réel d'une porte locale : une page
|
|
13
|
+
* web ouverte dans le navigateur du développeur.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ **Piège Zod 4** : un `.default({})` posé à plat sur un sous-objet NE
|
|
16
|
+
* ré-applique PAS les défauts de ses champs. Le pattern du dépôt est donc
|
|
17
|
+
* `sous.default(() => sous.parse({}))` — le callback force la ré-évaluation, et
|
|
18
|
+
* une application qui n'écrit rien obtient bien les valeurs d'usine.
|
|
19
|
+
*/
|
|
20
|
+
export declare const mcpConfigSchema: z.ZodObject<{
|
|
21
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
22
|
+
allowedOrigins: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
23
|
+
allowRemote: z.ZodDefault<z.ZodBoolean>;
|
|
24
|
+
authorization: z.ZodDefault<z.ZodObject<{
|
|
25
|
+
authorizationServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
26
|
+
resource: z.ZodDefault<z.ZodString>;
|
|
27
|
+
additionalResources: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
28
|
+
resourceName: z.ZodOptional<z.ZodString>;
|
|
29
|
+
resourceDocumentation: z.ZodOptional<z.ZodString>;
|
|
30
|
+
anonymous: z.ZodDefault<z.ZodBoolean>;
|
|
31
|
+
}, z.core.$strict>>;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
export declare const aiConfigSchema: z.ZodObject<{
|
|
34
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
/** Réglages `mcp.*` tels que le CODE les lit — défauts appliqués. */
|
|
37
|
+
export type IMcpConfig = z.output<typeof mcpConfigSchema>;
|
|
38
|
+
/** Config telle que l'APP l'écrit dans `use()` — tous les champs optionnels. */
|
|
39
|
+
export type IAiConfigInput = z.input<typeof aiConfigSchema>;
|
|
40
|
+
/** Config telle que le CODE la lit — défauts appliqués, rien d'optionnel. */
|
|
41
|
+
export type IAiConfig = z.output<typeof aiConfigSchema>;
|
|
42
|
+
/** Défauts matérialisés (passés au `super()` du Module). */
|
|
43
|
+
declare const defaults: IAiConfig;
|
|
44
|
+
export default defaults;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema de la configuration de @nodefony/ai — lu SANS démarrer
|
|
3
|
+
* l'application, par `nodefony env` et la notice `.env.example`, qui y
|
|
4
|
+
* trouvent les noms courts (`.meta({ env })`).
|
|
5
|
+
*
|
|
6
|
+
* Déclaré dans `package.json` → `nodefony.configSchema` ; le nom de l'export
|
|
7
|
+
* est FIXE. Fichier FEUILLE : il n'importe que zod et le schéma, et n'ouvre
|
|
8
|
+
* rien à l'import — une commande qui ne démarre pas l'application l'exécute.
|
|
9
|
+
*
|
|
10
|
+
* @returns le JSON Schema, métadonnées Nodefony comprises.
|
|
11
|
+
*/
|
|
12
|
+
export declare function configJsonSchema(): unknown;
|