@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.
Files changed (40) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +123 -2
  3. package/dist/_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js +8 -0
  4. package/dist/index.js +37 -0
  5. package/dist/nodefony/command/StdioCommand.js +75 -0
  6. package/dist/nodefony/config/config.js +181 -0
  7. package/dist/nodefony/config/configSchema.js +18 -0
  8. package/dist/nodefony/config/defineModuleConfig.js +51 -0
  9. package/dist/nodefony/mcp/caller.js +38 -0
  10. package/dist/nodefony/mcp/gate.js +628 -0
  11. package/dist/nodefony/mcp/guard.js +82 -0
  12. package/dist/nodefony/mcp/progress.js +74 -0
  13. package/dist/nodefony/mcp/protocol.js +76 -0
  14. package/dist/nodefony/mcp/server.js +302 -0
  15. package/dist/nodefony/mcp/sessionTicket.js +89 -0
  16. package/dist/nodefony/mcp/stdio.js +64 -0
  17. package/dist/nodefony/mcp/stream.js +80 -0
  18. package/dist/nodefony/mcp/ticketSession.js +177 -0
  19. package/dist/nodefony/mcp/tools.js +485 -0
  20. package/dist/nodefony/src/errors/AiError.js +20 -0
  21. package/dist/types/index.d.ts +56 -0
  22. package/dist/types/nodefony/command/StdioCommand.d.ts +28 -0
  23. package/dist/types/nodefony/config/config.d.ts +44 -0
  24. package/dist/types/nodefony/config/configSchema.d.ts +12 -0
  25. package/dist/types/nodefony/config/defineModuleConfig.d.ts +43 -0
  26. package/dist/types/nodefony/mcp/caller.d.ts +54 -0
  27. package/dist/types/nodefony/mcp/gate.d.ts +235 -0
  28. package/dist/types/nodefony/mcp/guard.d.ts +89 -0
  29. package/dist/types/nodefony/mcp/progress.d.ts +48 -0
  30. package/dist/types/nodefony/mcp/protocol.d.ts +107 -0
  31. package/dist/types/nodefony/mcp/server.d.ts +156 -0
  32. package/dist/types/nodefony/mcp/sessionTicket.d.ts +60 -0
  33. package/dist/types/nodefony/mcp/stdio.d.ts +38 -0
  34. package/dist/types/nodefony/mcp/stream.d.ts +76 -0
  35. package/dist/types/nodefony/mcp/ticketSession.d.ts +174 -0
  36. package/dist/types/nodefony/mcp/tools.d.ts +232 -0
  37. package/dist/types/nodefony/src/errors/AiError.d.ts +14 -0
  38. package/docs/index.md +56 -0
  39. package/docs/mcp.md +754 -0
  40. package/package.json +83 -4
@@ -0,0 +1,43 @@
1
+ import type { IAiConfig } from "./config.js";
2
+ /**
3
+ * @nodefony/ai — LE COMMENT : builder PUR de la config.
4
+ *
5
+ * ⭐ TL;DR : ce fichier VALIDE et GÈLE. Il ne porte AUCUNE valeur — les défauts
6
+ * vivent dans le schéma de `./config.ts` (règle d'or ADR-0006). Pour changer un
7
+ * défaut, c'est là-bas ; ici, on ne fait que le faire respecter.
8
+ *
9
+ * Le nom du FICHIER est le même dans tous les modules Nodefony
10
+ * (`defineModuleConfig.ts`) ; la FONCTION, elle, est préfixée par le module —
11
+ * deux modules importés côte à côte ne se marchent pas dessus.
12
+ *
13
+ * L'environnement ne se lit JAMAIS ici. La surcharge générique
14
+ * (`NF__AI__<CHEMIN>`) comme un nom court (`NF_AI_<RÉGLAGE>`) sont
15
+ * posés par le Kernel avant cette validation. Un nom court se DÉCLARE sur son
16
+ * champ, dans `./config.ts` : `.meta({ env: "NF_AI_<RÉGLAGE>" })`.
17
+ *
18
+ * 🔴 **La validation passe par `parseModuleConfig`, du cœur** — un seul endroit
19
+ * pour une seule règle. Il lève une `BootConfigurationError`, et pas une `Error`
20
+ * ordinaire : le kernel distingue les deux. Un hook de cycle de vie qui lève une
21
+ * erreur QUELCONQUE est absorbé en développement (fail-soft — il protège la DX
22
+ * d'un module optionnel cassé), tandis qu'une erreur de CONFIGURATION interrompt
23
+ * le boot dans TOUS les environnements. Mesuré sur un module généré : avec une
24
+ * `Error`, `use("@nodefony/ai", { gretting: "…" })` laissait l'application
25
+ * démarrer, servir, et rendre 0 — la faute de frappe n'atteignait personne. Une
26
+ * configuration explicite qu'on ne peut pas honorer ne se répare pas en
27
+ * continuant.
28
+ *
29
+ * ⚠️ Ne PAS réécrire ce bloc à la main : les dix modules du framework portaient
30
+ * chacun leur copie du `try`/`catch`, et elles avaient divergé sur les deux
31
+ * points qui comptent — le type de l'erreur levée et la langue du message.
32
+ *
33
+ * @param config - config brute venue de `use("@nodefony/ai", { … })`.
34
+ * @returns config validée et gelée.
35
+ * @throws BootConfigurationError si un champ est invalide ou inconnu (toutes les
36
+ * erreurs Zod agrégées, par champ) — le boot s'interrompt, en dev comme en prod.
37
+ */
38
+ export declare function defineAiConfig(config?: unknown): IAiConfig;
39
+ /**
40
+ * JSON Schema de la config — Studio en dérive son formulaire d'édition
41
+ * (libellés, types, défauts, descriptions), sans une ligne d'UI écrite à la main.
42
+ */
43
+ export declare function aiConfigJsonSchema(): unknown;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Ce qu'une porte MCP sait d'elle-même et de son appelant, au moment de dire
3
+ * quels rôles celui-ci porte.
4
+ *
5
+ * Les trois champs sont séparés parce qu'ils répondent à trois questions
6
+ * distinctes que confondre a déjà coûté : « cette porte exige-t-elle une
7
+ * autorisation ? », « une identité a-t-elle été prouvée ? », « qu'ouvre-t-elle ? ».
8
+ */
9
+ export interface IMcpGatePosture {
10
+ /**
11
+ * La porte déclare-t-elle un serveur d'autorisation ?
12
+ *
13
+ * ⚠️ À ne PAS confondre avec `authenticated`. Une porte protégée peut tolérer
14
+ * l'anonyme ; une porte non protégée n'a personne à authentifier. Ce sont ces
15
+ * deux cas — tous deux « non authentifiés » — qui doivent recevoir des
16
+ * réponses opposées.
17
+ */
18
+ protected: boolean;
19
+ /** Un jeton a-t-il été vérifié pour cet appel ? */
20
+ authenticated: boolean;
21
+ /** Scopes que le serveur d'autorisation a réellement accordés. */
22
+ scopes: readonly string[];
23
+ }
24
+ /**
25
+ * Quels rôles porte l'appelant d'une porte MCP — **la règle, à un seul endroit**.
26
+ *
27
+ * ⭐ Toute porte MCP passe par ici, y compris celles qu'un module créera plus
28
+ * tard (gestion d'agents, passerelles métier). C'est délibéré : la question
29
+ * « que vaut un appelant sur ma porte » a exactement une bonne réponse, et
30
+ * laisser chaque porte la réécrire garantit qu'une copie finira plus permissive
31
+ * que les autres sans que rien ne le signale — c'est déjà ce qui s'est produit
32
+ * quand la lecture d'administration fabriquait un administrateur.
33
+ *
34
+ * Les trois postures :
35
+ *
36
+ * | La porte | L'appelant | Ce qu'il porte |
37
+ * | --- | --- | --- |
38
+ * | non protégée | quiconque l'atteint | `operatorRoles` — AUCUN par défaut ; une porte dont le PÉRIMÈTRE protège l'élargit |
39
+ * | protégée | jeton vérifié | ce que ses SCOPES ouvrent, rien de plus |
40
+ * | protégée | anonyme toléré | **rien** — la déclaration d'autorisation serait vidée de son sens |
41
+ *
42
+ * Le premier cas n'accorde RIEN par défaut : une porte non protégée peut être
43
+ * celle d'une application, ouverte en production. Seule une porte que son
44
+ * PÉRIMÈTRE protège — la porte de développement de devkit, module
45
+ * `policy: "dev"`, dont l'appelant lit déjà les sources — passe le rôle
46
+ * d'opérateur, et l'écrit. Ce qui compte, c'est que ce soit un CHOIX écrit par
47
+ * la porte — pas un administrateur fabriqué au fond d'une fonction de lecture.
48
+ *
49
+ * @param posture - ce que la porte sait d'elle-même et de cet appel.
50
+ * @param operatorRoles - ce qu'accorde une porte NON protégée ; aucun rôle par
51
+ * défaut.
52
+ * @returns les rôles à poser sur `IMcpCaller.roles`.
53
+ */
54
+ export declare function mcpCallerRoles(posture: IMcpGatePosture, operatorRoles?: readonly string[]): readonly string[];
@@ -0,0 +1,235 @@
1
+ import { z } from "zod";
2
+ import type { IAccessAccountResolver, IAccessTokenVerifier, IProtectedResourceInput, IMcpTool } from "nodefony";
3
+ import type { Controller } from "@nodefony/framework";
4
+ import type { IServerInfo } from "./server.js";
5
+ import { type IMcpEventSink } from "./stream.js";
6
+ /**
7
+ * Une porte MCP sur HTTP — la mécanique, sans la route.
8
+ *
9
+ * `@nodefony/ai` ne monte AUCUNE route : une porte se DÉCLARE, par qui en a
10
+ * besoin — devkit pour l'outillage de développement, une application pour ses
11
+ * propres outils, en développement ou en production selon son choix. On la
12
+ * définit une fois ({@link defineMcpGate}), on la sert d'une ligne
13
+ * (`gate.serve(this, body)`). Gardes d'origine et de localité, validation du
14
+ * jeton, défi `401` avec `resource_metadata`, traduction HTTP ↔ JSON-RPC, flux
15
+ * SSE, refus `405` des verbes retirés : une porte qui recoderait l'une de ces
16
+ * règles finirait plus permissive que les autres sans que rien ne le dise.
17
+ */
18
+ /**
19
+ * Plafond d'un événement sur le flux de réponse MCP, octets UTF-8 — celui de la
20
+ * réponse finale, qui porte le résultat entier de l'outil (16 Mio).
21
+ */
22
+ export declare const MCP_STREAM_MAX_EVENT_BYTES: number;
23
+ /** Sévérités qu'une porte MCP écrit au journal. */
24
+ export type McpGateSeverity = "DEBUG" | "WARNING" | "ERROR" | "CRITIC";
25
+ /**
26
+ * Le schéma d'une porte — les réglages de transport et d'autorisation de
27
+ * {@link mcpConfigSchema} (les MÊMES clés que `devkit.mcp.*`), plus ce que la
28
+ * porte publie et comment. Strict : une clé inconnue arrête le démarrage en la
29
+ * nommant, au lieu d'être ignorée.
30
+ */
31
+ export declare const mcpGateSchema: z.ZodObject<{
32
+ enabled: z.ZodDefault<z.ZodBoolean>;
33
+ allowedOrigins: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
+ allowRemote: z.ZodDefault<z.ZodBoolean>;
35
+ authorization: z.ZodDefault<z.ZodObject<{
36
+ authorizationServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
+ resource: z.ZodDefault<z.ZodString>;
38
+ additionalResources: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
+ resourceName: z.ZodOptional<z.ZodString>;
40
+ resourceDocumentation: z.ZodOptional<z.ZodString>;
41
+ anonymous: z.ZodDefault<z.ZodBoolean>;
42
+ }, z.core.$strict>>;
43
+ name: z.ZodOptional<z.ZodString>;
44
+ version: z.ZodOptional<z.ZodString>;
45
+ instructions: z.ZodOptional<z.ZodString>;
46
+ tools: z.ZodUnion<readonly [z.ZodLiteral<"modules">, z.ZodArray<z.ZodCustom<IMcpTool, IMcpTool>>]>;
47
+ unprotectedRoles: z.ZodDefault<z.ZodArray<z.ZodString>>;
48
+ protocol: z.ZodDefault<z.ZodObject<{
49
+ versions: z.ZodOptional<z.ZodArray<z.ZodEnum<{
50
+ "2024-11-05": "2024-11-05";
51
+ "2025-03-26": "2025-03-26";
52
+ "2025-06-18": "2025-06-18";
53
+ "2025-11-25": "2025-11-25";
54
+ "2026-07-28": "2026-07-28";
55
+ }>>>;
56
+ }, z.core.$strict>>;
57
+ cache: z.ZodDefault<z.ZodObject<{
58
+ ttlMs: z.ZodDefault<z.ZodNumber>;
59
+ scope: z.ZodDefault<z.ZodEnum<{
60
+ private: "private";
61
+ public: "public";
62
+ }>>;
63
+ }, z.core.$strict>>;
64
+ }, z.core.$strict>;
65
+ /** Ce qu'on écrit pour définir une porte (défauts compris). */
66
+ export type IMcpGateDefinition = z.input<typeof mcpGateSchema>;
67
+ /** Les réglages EFFECTIFS d'une porte, validés et figés. */
68
+ export type IMcpGateSettings = z.output<typeof mcpGateSchema>;
69
+ /** Ce que la porte a reçu — lu par elle, jamais par la mécanique. */
70
+ export interface IMcpGateRequest<S extends IMcpEventSink = IMcpEventSink> {
71
+ /** Corps tel que le parseur HTTP l'a rendu (`{}` pour un JSON illisible). */
72
+ body: unknown;
73
+ /**
74
+ * Le corps BRUT, lu à la demande : seul moyen de distinguer un `{}` envoyé
75
+ * d'un texte qui ne se parse pas (JSON-RPC `-32700` contre `-32600`). Appelé
76
+ * uniquement quand le corps n'a aucune clé.
77
+ */
78
+ rawBody?: (() => string | undefined) | undefined;
79
+ /** En-tête `Origin`. */
80
+ origin?: string | undefined;
81
+ /** En-tête `MCP-Protocol-Version`. */
82
+ protocolVersion?: string | undefined;
83
+ /** En-tête `Authorization`. */
84
+ authorization?: string | undefined;
85
+ /** Adresse de l'appelant (après `trustProxy`). */
86
+ remoteAddress?: string | undefined;
87
+ /**
88
+ * La requête a traversé un relais (en-tête `Forwarded`, `X-Forwarded-For`,
89
+ * `X-Real-IP` ou `Via`) — refusée par une porte locale.
90
+ */
91
+ relayed?: boolean | undefined;
92
+ /** Abattu quand le client abandonne la requête. */
93
+ signal?: AbortSignal | undefined;
94
+ /**
95
+ * Ouvre un flux SSE — absent quand le client n'en accepte pas : la
96
+ * progression devient alors muette, jamais refusée.
97
+ */
98
+ openStream?: (() => S | Promise<S>) | undefined;
99
+ /**
100
+ * Prévenu de l'identifiant du compte au nom duquel un outil agit — pour le
101
+ * journal de la requête, qui le lit au démontage, hors de la bulle.
102
+ */
103
+ onAccount?: ((identifier: string) => void) | undefined;
104
+ }
105
+ /**
106
+ * Ce qu'un transport de process (stdio) a reçu : une ligne, sans en-tête ni
107
+ * adresse.
108
+ */
109
+ export interface IMcpStdioRequest {
110
+ /** La ligne reçue, telle quelle — un message JSON-RPC, en principe. */
111
+ line: string;
112
+ /** Abattu quand le client s'en va (entrée standard fermée). */
113
+ signal?: AbortSignal | undefined;
114
+ /**
115
+ * Ouvre le puits où écrire les notifications de l'appel, puis sa réponse
116
+ * finale — absent, la progression devient muette.
117
+ */
118
+ openStream?: (() => IMcpEventSink | Promise<IMcpEventSink>) | undefined;
119
+ }
120
+ /** Ce que l'environnement fournit à une réponse — `serve()` le lit seul. */
121
+ export interface IMcpGateRuntime {
122
+ /** Les modules chargés, pour une porte `tools: "modules"`. */
123
+ modules?: Readonly<Record<string, unknown>> | undefined;
124
+ /** Qui sait valider un jeton — requis dès qu'un serveur d'autorisation est déclaré. */
125
+ verifier?: IAccessTokenVerifier | undefined;
126
+ /**
127
+ * Qui retrouve le compte qu'un jeton désigne — requis par un outil
128
+ * `actsForUser` ; absent, un tel outil refuse de s'exécuter.
129
+ */
130
+ accountResolver?: IAccessAccountResolver | undefined;
131
+ /** Identité annoncée ; défaut : `name`/`version` de la porte, sinon le projet. */
132
+ serverInfo?: IServerInfo | undefined;
133
+ /** Journal de la porte ; muet si absent. */
134
+ log?: ((message: string, severity: McpGateSeverity) => void) | undefined;
135
+ }
136
+ /** Réponse DÉCRITE par la mécanique — la porte l'écrit. */
137
+ export type IMcpGateResponse<S extends IMcpEventSink = IMcpEventSink> = {
138
+ kind: "json";
139
+ status: number;
140
+ body: unknown;
141
+ headers?: Readonly<Record<string, string>>;
142
+ } | {
143
+ kind: "empty";
144
+ status: number;
145
+ headers?: Readonly<Record<string, string>>;
146
+ } | {
147
+ kind: "stream";
148
+ stream: S;
149
+ };
150
+ /** Une porte MCP définie — à servir depuis une action de controller. */
151
+ export interface IMcpGate {
152
+ /** Les réglages effectifs, validés et figés. */
153
+ readonly settings: IMcpGateSettings;
154
+ /**
155
+ * Sert la requête courante d'un controller : `POST` sert le message, `GET` et
156
+ * `DELETE` rendent `405` (verbes retirés par la révision `2026-07-28`).
157
+ *
158
+ * @param controller - le controller de la porte.
159
+ * @param body - le corps reçu (`@Body()`).
160
+ * @returns ce que l'action doit rendre.
161
+ */
162
+ serve(controller: Controller, body: unknown): Promise<unknown>;
163
+ /**
164
+ * Répond à UN `POST` décrit — sans controller, éprouvable sans serveur (et
165
+ * réutilisable par un autre transport).
166
+ *
167
+ * @param request - ce que la porte a reçu.
168
+ * @param runtime - modules, vérificateur, identité, journal.
169
+ * @returns la réponse à écrire ; `stream` quand un flux SSE a été ouvert.
170
+ */
171
+ respond<S extends IMcpEventSink>(request: IMcpGateRequest<S>, runtime?: IMcpGateRuntime): Promise<IMcpGateResponse<S>>;
172
+ /**
173
+ * Répond à UNE ligne reçue sur l'entrée standard — le pendant de
174
+ * {@link IMcpGate.respond} pour un client qui lance lui-même son serveur.
175
+ *
176
+ * Ni origine, ni adresse, ni jeton : la frontière est celle du process.
177
+ * L'appelant est anonyme et porte les rôles d'une porte non protégée
178
+ * (`unprotectedRoles`, aucun par défaut) ; les outils réservés restent
179
+ * retenus. Les annulations (`notifications/cancelled`) visent l'unique
180
+ * appelant du process ; un appel annulé ne produit PLUS RIEN — ni
181
+ * progression, ni réponse (`empty`), comme la spec l'exige en stdio. Le
182
+ * statut de la réponse décrite n'a pas de sens hors HTTP : seul son corps
183
+ * s'écrit.
184
+ *
185
+ * @param request - la ligne reçue, et le puits où écrire.
186
+ * @param runtime - modules, identité, journal.
187
+ * @returns la réponse à écrire ; `stream` quand le puits a déjà tout reçu.
188
+ */
189
+ respondStdio(request: IMcpStdioRequest, runtime?: IMcpGateRuntime): Promise<IMcpGateResponse>;
190
+ /**
191
+ * Ce que la porte protège, à publier en RFC 9728 — une entrée, ou rien.
192
+ *
193
+ * Un service qui expose `publishedProtectedResources()` voit
194
+ * `@nodefony/framework` monter le document ; les scopes publiés sont ceux
195
+ * que le défi exige. Rôle éteint (aucun serveur d'autorisation) ⇒ rien : un
196
+ * document sans `authorization_servers` apprendrait au client qu'un jeton est
197
+ * nécessaire sans lui dire où l'obtenir.
198
+ *
199
+ * @param modules - les modules chargés, pour une porte `tools: "modules"`.
200
+ * @returns zéro ou une entrée.
201
+ */
202
+ protectedResources(modules?: Readonly<Record<string, unknown>>): readonly IProtectedResourceInput[];
203
+ }
204
+ /**
205
+ * Définit une porte MCP — UNE fois, à côté de son controller.
206
+ *
207
+ * La définition est validée et figée ici : une clé inconnue, une révision du
208
+ * protocole que le paquet installé ne sait pas servir, un outil mal formé, un
209
+ * nom en double ou réservé (`nodefony_`) lèvent une `BootConfigurationError`
210
+ * qui nomme la faute — au démarrage, jamais à la première requête. Chaque
211
+ * requête ne paie ensuite aucune validation.
212
+ *
213
+ * ```ts
214
+ * const shopGate = defineMcpGate({
215
+ * name: "shop",
216
+ * instructions: "Stock et commandes du magasin.",
217
+ * tools: [stockTool, orderTool],
218
+ * });
219
+ *
220
+ * @route("shop-mcp", { path: "/mcp", requirements: { methods: ["POST", "GET", "DELETE"] } })
221
+ * mcp(@Body() body: unknown) {
222
+ * return shopGate.serve(this, body);
223
+ * }
224
+ * ```
225
+ *
226
+ * Défauts : appels locaux seulement (`allowRemote: false`), aucune origine
227
+ * navigateur, aucun jeton exigé, AUCUN rôle accordé — les outils réservés
228
+ * (`scopes`, `requiresAuth`) restent retenus tant qu'une `authorization` ne
229
+ * les ouvre pas.
230
+ *
231
+ * @param definition - ce que la porte publie, à qui, et comment.
232
+ * @returns la porte, prête à servir.
233
+ * @throws BootConfigurationError si la définition est invalide.
234
+ */
235
+ export declare function defineMcpGate(definition: IMcpGateDefinition): IMcpGate;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Gardes du transport MCP — ce qui protège une porte locale SANS authentification.
3
+ *
4
+ * Ces deux contrôles ne sont pas un pis-aller : ce sont exactement les
5
+ * exigences que la spec pose au transport lui-même, indépendamment de toute
6
+ * autorisation (`transports/streamable-http` §Security & Endpoint) —
7
+ * « Servers **MUST** validate the `Origin` header on all incoming connections
8
+ * to prevent DNS rebinding attacks » et « When running locally, servers
9
+ * **SHOULD** bind only to localhost ».
10
+ *
11
+ * Fonctions **pures** : elles reçoivent ce qu'elles jugent, elles ne le lisent
12
+ * nulle part. C'est ce qui les rend éprouvables sans serveur ni requête réelle
13
+ * — et ce qui permet de vérifier le refus, qui est le comportement qui compte.
14
+ */
15
+ /** Verdict d'une garde : passer, ou refuser en disant pourquoi. */
16
+ export type GuardVerdict = {
17
+ allowed: true;
18
+ } | {
19
+ allowed: false;
20
+ why: string;
21
+ };
22
+ /** Ce que la garde a besoin de savoir d'une requête. */
23
+ export interface IGuardInput {
24
+ /** En-tête `Origin`, `undefined` s'il est absent. */
25
+ origin?: string | undefined;
26
+ /** Adresse distante de la connexion (`socket.remoteAddress`). */
27
+ remoteAddress?: string | undefined;
28
+ /**
29
+ * La requête porte un en-tête de relais (`Forwarded`, `X-Forwarded-For`,
30
+ * `X-Real-IP`, `Via`) : elle a traversé un proxy, quelle que soit
31
+ * l'adresse de la connexion.
32
+ */
33
+ relayed?: boolean | undefined;
34
+ }
35
+ /** Réglages qui gouvernent les gardes (viennent de la config du module). */
36
+ export interface IGuardPolicy {
37
+ /** Origines de navigateur admises ; vide = aucune. */
38
+ allowedOrigins: readonly string[];
39
+ /** Accepter une adresse distante non locale. */
40
+ allowRemote: boolean;
41
+ }
42
+ /**
43
+ * Une adresse est-elle celle de la machine locale ?
44
+ *
45
+ * Couvre les trois formes que Node rend selon la pile réseau : IPv4
46
+ * (`127.x.x.x`), IPv6 (`::1`), et l'IPv4 encapsulée en IPv6
47
+ * (`::ffff:127.0.0.1`) — la plus fréquente sur un serveur à double pile, et
48
+ * celle qu'une comparaison naïve à `"127.0.0.1"` rate.
49
+ *
50
+ * @param address - `socket.remoteAddress`, éventuellement absent
51
+ * @returns `true` si l'appel vient de cette machine
52
+ */
53
+ /**
54
+ * Les en-têtes qu'un relais pose — leur présence suffit à dire que la requête
55
+ * n'est pas arrivée directement, quelle que soit l'adresse de la connexion.
56
+ */
57
+ export declare const RELAY_HEADERS: readonly ["forwarded", "x-forwarded-for", "x-real-ip", "via"];
58
+ /**
59
+ * La requête a-t-elle traversé un relais (proxy inverse, répartiteur) ?
60
+ *
61
+ * Un relais posé sur la MÊME machine fait arriver un client distant depuis la
62
+ * boucle locale : l'adresse de la connexion ne prouve alors plus rien. Toute
63
+ * porte qui exige la localité refuse donc aussi l'appel relayé — la porte MCP
64
+ * comme la porte de session par ticket, une seule règle.
65
+ *
66
+ * @param headers - en-têtes reçus (`request.headers` de Node)
67
+ * @returns `true` si un en-tête de relais non vide est présent
68
+ */
69
+ export declare function isRelayedRequest(headers: Readonly<Record<string, string | string[] | undefined>>): boolean;
70
+ export declare function isLocalAddress(address?: string): boolean;
71
+ /**
72
+ * Décide si un appel MCP peut être servi.
73
+ *
74
+ * ⭐ **La règle sur `Origin` est contre-intuitive, et c'est elle qui protège.**
75
+ * Un client MCP légitime est un *process* (Cursor, Claude Code, un agent tiers)
76
+ * : il n'envoie **aucun** `Origin`, cet en-tête étant posé par les navigateurs.
77
+ * Une page web malveillante, elle, en pose **toujours** un lorsqu'elle vise
78
+ * `https://localhost:5152`. Donc : *absent* → on passe ; *présent et hors
79
+ * allowlist* → `403`. C'est ce qui referme le DNS rebinding, le seul vecteur
80
+ * réel contre un serveur MCP local.
81
+ *
82
+ * L'ordre des contrôles est délibéré : la localité d'abord, parce qu'un appel
83
+ * distant ne doit même pas apprendre quelles origines sont admises.
84
+ *
85
+ * @param input - ce que la requête présente
86
+ * @param policy - les réglages du module
87
+ * @returns le verdict, avec le motif quand il refuse
88
+ */
89
+ export declare function checkMcpAccess(input: IGuardInput, policy: IGuardPolicy): GuardVerdict;
@@ -0,0 +1,48 @@
1
+ import { type IJsonRpcNotification } from "nodefony";
2
+ /**
3
+ * La progression d'un appel d'outil (`notifications/progress`) — règle PURE,
4
+ * horloge injectée.
5
+ *
6
+ * La norme (`basic/patterns/progress`) pose quatre exigences, toutes tenues
7
+ * ICI plutôt que dans chaque outil :
8
+ * - une notification ne cite qu'un jeton fourni par une requête EN COURS — le
9
+ * rapporteur se tait dès que l'outil a rendu sa réponse ({@link IMcpProgressReporter.end}) ;
10
+ * - `progress` CROÎT à chaque notification — une valeur qui stagne ou recule
11
+ * est ignorée, pas transmise ;
12
+ * - la cadence est bornée (« SHOULD implement rate limiting ») ;
13
+ * - sans jeton, rien n'est émis : l'agent n'a rien demandé.
14
+ */
15
+ /** Intervalle minimal entre deux notifications de progression, en millisecondes. */
16
+ export declare const MCP_PROGRESS_MIN_INTERVAL_MS = 100;
17
+ /** Longueur maximale du `message` d'une progression, en caractères. */
18
+ export declare const MCP_PROGRESS_MESSAGE_MAX = 1000;
19
+ /** Un jeton de progression : chaîne ou entier (`progress` §Progress Flow). */
20
+ export type McpProgressToken = string | number;
21
+ /** Le rapporteur d'UN appel : ce que l'outil appelle, ce que la porte clôt. */
22
+ export interface IMcpProgressReporter {
23
+ /**
24
+ * Signale un avancement ; ignoré s'il ne croît pas, s'il arrive trop tôt
25
+ * après le précédent, ou après {@link end}.
26
+ */
27
+ progress(progress: number, total?: number, message?: string): void;
28
+ /** L'outil a rendu sa réponse : plus aucune notification ne part. */
29
+ end(): void;
30
+ }
31
+ /**
32
+ * Lit le jeton de progression d'une requête (`params._meta.progressToken`).
33
+ *
34
+ * @param params - les `params` de la requête.
35
+ * @returns le jeton, ou `null` s'il manque ou n'est ni une chaîne ni un entier.
36
+ */
37
+ export declare function readProgressToken(params: Record<string, unknown>): McpProgressToken | null;
38
+ /**
39
+ * Fabrique le rapporteur de progression d'un appel.
40
+ *
41
+ * @param token - le jeton de la requête ({@link readProgressToken}), ou `null`.
42
+ * @param notify - émet une notification sur le flux de la réponse ; absent
43
+ * quand la porte ne sait pas tenir de flux.
44
+ * @param now - horloge (millisecondes), injectée pour l'épreuve.
45
+ * @param minIntervalMs - intervalle minimal entre deux notifications.
46
+ * @returns un rapporteur, muet quand il n'y a ni jeton ni flux.
47
+ */
48
+ export declare function createProgressReporter(token: McpProgressToken | null, notify: ((notification: IJsonRpcNotification) => void) | undefined, now?: () => number, minIntervalMs?: number): IMcpProgressReporter;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Model Context Protocol — types et constantes du transport « Streamable HTTP ».
3
+ *
4
+ * Révision visée : **2026-07-28**, celle qui a supprimé les sessions de niveau
5
+ * protocole et le flux `GET`. C'est ce qui rend cette porte possible sans
6
+ * process dédié : chaque message est un `POST` autonome, donc un redémarrage du
7
+ * serveur de développement ne casse rien — le client rejoue simplement sa
8
+ * requête, et la réponse vient du code qui vient d'être rechargé.
9
+ *
10
+ * @see https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
11
+ */
12
+ import type { IJsonRpcSuccess, IJsonRpcFailure } from "nodefony";
13
+ /** Révision PRÉFÉRÉE de ce serveur — celle qu'il met en tête de ce qu'il sait faire. */
14
+ export declare const MCP_PROTOCOL_VERSION = "2026-07-28";
15
+ /**
16
+ * Révision supposée quand un client n'en déclare AUCUNE.
17
+ *
18
+ * Ce n'est pas un choix : la spec impose de traiter une requête sans
19
+ * déclaration comme `2025-03-26`, et le SDK de référence en fait sa
20
+ * `DEFAULT_NEGOTIATED_PROTOCOL_VERSION`. Répondre notre préférée à un client
21
+ * muet reviendrait à lui imposer une révision qu'il n'a pas demandée.
22
+ */
23
+ export declare const MCP_DEFAULT_NEGOTIATED_VERSION = "2025-03-26";
24
+ /**
25
+ * Versions que ce serveur sait servir — publiées par `server/discover`, listées
26
+ * dans l'erreur `UnsupportedProtocolVersion`, et candidates à l'écho
27
+ * d'`initialize`. De la plus récente à la plus ancienne.
28
+ *
29
+ * 🔴 **N'en servir qu'une était un défaut, et il a coûté la connexion.** Le
30
+ * serveur annonçait `2026-07-28` à TOUT client, y compris à ceux qui
31
+ * demandaient autre chose — or aucun client déployé ne connaît cette révision :
32
+ * le SDK de référence (`@modelcontextprotocol/sdk@1.30.0`) porte
33
+ * `LATEST = 2025-11-25` et refuse net toute réponse hors de sa liste
34
+ * (« Server's protocol version is not supported »). Une porte parfaitement
35
+ * conforme à la dernière spec, et injoignable par tout le monde.
36
+ *
37
+ * Ces cinq révisions sont celles que la spec a publiées. Les servir toutes est
38
+ * honnête ici, et vérifiable : les quatre méthodes de ce serveur — `initialize`,
39
+ * `ping`, `tools/list`, `tools/call` — ont la même forme de REQUÊTE dans
40
+ * chacune. La forme du RÉSULTAT, elle, dépend de l'ÈRE du client : depuis
41
+ * `2026-07-28`, tout résultat porte `resultType` (MUST du schéma) et `_meta`
42
+ * serverInfo — servis par `eraResult` (server.ts) à un client moderne, jamais
43
+ * à un client legacy qui ne définit pas ces champs. Un test EXERCE chacune,
44
+ * plutôt que de se fier à cette phrase.
45
+ */
46
+ export declare const MCP_SUPPORTED_VERSIONS: readonly ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
47
+ /** Une révision du protocole que ce paquet sait servir. */
48
+ export type McpProtocolVersion = (typeof MCP_SUPPORTED_VERSIONS)[number];
49
+ /**
50
+ * Clé de métadonnée par laquelle un client MODERNE déclare sa révision.
51
+ *
52
+ * ⭐ **C'est la différence d'ÈRE, et elle commande tout le reste.** Jusqu'à
53
+ * `2025-11-25` (ère « legacy »), un client ouvrait une session par un handshake
54
+ * `initialize`. Depuis `2026-07-28` (ère « modern »), il n'y a plus de session :
55
+ * chaque requête porte elle-même sa version et les capacités du client, dans
56
+ * `params._meta`. Un serveur qui n'écouterait que `initialize` serait un
57
+ * serveur *legacy* — quelle que soit la version qu'il prétend annoncer.
58
+ */
59
+ export declare const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
60
+ /** Clé de métadonnée portant l'identité du serveur dans `server/discover`. */
61
+ export declare const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
62
+ export { JsonRpcError, jsonRpcSuccess, jsonRpcFailure, isNotification, } from "nodefony";
63
+ export type { JsonRpcId, IJsonRpcMessage, IJsonRpcSuccess, IJsonRpcFailure, } from "nodefony";
64
+ /**
65
+ * Codes réservés par la spec MCP, hors plage JSON-RPC standard.
66
+ *
67
+ * Ils ne sont pas décoratifs : un client s'en sert pour se rattraper seul —
68
+ * renégocier une version sur `-32022`, relire `tools/list` puis réessayer sur
69
+ * `-32020`. Rendre un `-32600` générique à leur place le priverait de cette
70
+ * reprise et transformerait un désaccord réparable en échec définitif.
71
+ */
72
+ export declare const McpProtocolError: {
73
+ /**
74
+ * Les en-têtes HTTP contredisent le corps, ou un en-tête requis manque.
75
+ * La spec impose `400` **et** ce code (`streamable-http` §Server Validation).
76
+ */
77
+ readonly HEADER_MISMATCH: -32020;
78
+ /**
79
+ * La révision demandée n'est pas servie. La réponse **doit** lister celles
80
+ * qu'on sert, sans quoi le client n'a rien pour choisir.
81
+ */
82
+ readonly UNSUPPORTED_PROTOCOL_VERSION: -32022;
83
+ };
84
+ /** Ce que le serveur MCP rend, avant traduction en réponse HTTP. */
85
+ export interface IMcpHttpReply {
86
+ /** Statut HTTP à poser. */
87
+ status: number;
88
+ /**
89
+ * Corps JSON, ou `null` pour un `202 Accepted` sans corps — la spec l'exige
90
+ * pour une notification acceptée.
91
+ */
92
+ body: IJsonRpcSuccess | IJsonRpcFailure | null;
93
+ /**
94
+ * L'exception levée par un outil, rendue À CÔTÉ de la réponse — jamais
95
+ * sérialisée.
96
+ *
97
+ * Le client ne reçoit qu'un message générique : le message d'une exception
98
+ * peut porter un chemin de disque, une chaîne de connexion ou un secret, et
99
+ * l'outil qui la lève est du code d'application arbitraire. Le détail reste
100
+ * donc au serveur, et c'est au TRANSPORT de le journaliser : le protocole est
101
+ * une fonction pure, qui n'a pas de journal.
102
+ */
103
+ failure?: {
104
+ tool: string;
105
+ error: unknown;
106
+ } | undefined;
107
+ }