@nodefony/devkit 10.0.0-alpha.1

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 (67) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +318 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
  9. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
  10. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
  11. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
  12. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
  15. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  16. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  17. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  18. package/dist/index.js +47 -0
  19. package/dist/nodefony/command/CardCommand.js +70 -0
  20. package/dist/nodefony/config/config.js +200 -0
  21. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  22. package/dist/nodefony/controllers/DevkitController.js +60 -0
  23. package/dist/nodefony/controllers/McpController.js +223 -0
  24. package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
  25. package/dist/nodefony/interfaces/IDevkitService.js +1 -0
  26. package/dist/nodefony/interfaces/index.js +1 -0
  27. package/dist/nodefony/service/DevkitService.js +198 -0
  28. package/dist/nodefony/src/card.js +2 -0
  29. package/dist/nodefony/src/errors/DevkitError.js +21 -0
  30. package/dist/nodefony/src/mcp/guard.js +51 -0
  31. package/dist/nodefony/src/mcp/protocol.js +127 -0
  32. package/dist/nodefony/src/mcp/server.js +133 -0
  33. package/dist/nodefony/src/mcp/tools.js +163 -0
  34. package/dist/types/index.d.ts +52 -0
  35. package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
  36. package/dist/types/nodefony/config/config.d.ts +25 -0
  37. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  38. package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
  39. package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
  40. package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
  41. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  43. package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
  44. package/dist/types/nodefony/src/card.d.ts +18 -0
  45. package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
  46. package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
  47. package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
  48. package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
  49. package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
  50. package/docs/index.md +358 -0
  51. package/package.json +77 -0
  52. package/skills/nodefony-add-crud/SKILL.md +199 -0
  53. package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
  54. package/skills/nodefony-add-service/SKILL.md +90 -0
  55. package/skills/nodefony-browser/SKILL.md +416 -0
  56. package/skills/nodefony-browser/references/socket.md +115 -0
  57. package/skills/nodefony-browser/references/sondes.md +175 -0
  58. package/skills/nodefony-browser/scripts/audit.mjs +169 -0
  59. package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
  60. package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
  61. package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
  62. package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
  63. package/skills/nodefony-browser/scripts/socket.mjs +354 -0
  64. package/skills/nodefony-browser/scripts/watch.mjs +125 -0
  65. package/skills/nodefony-migrate-schema/SKILL.md +359 -0
  66. package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
  67. package/skills/nodefony-protect-route/SKILL.md +195 -0
@@ -0,0 +1,143 @@
1
+ import { Service, Module } from "nodefony";
2
+ import type { IMcpToolDeps, IProtectedResourceInput } from "nodefony";
3
+ import type { IDevkitCard, IDevkitService } from "../interfaces/IDevkitService.js";
4
+ import { type DevkitConfig } from "../config/config.js";
5
+ /**
6
+ * Service principal du module — la logique vit ici, pas dans les controllers
7
+ * (un controller traduit du HTTP/WS ; un service, lui, est réutilisable par la
8
+ * CLI, un job, un autre module).
9
+ *
10
+ * Cycle : `constructor` (fusion défauts + config de l'app) → `init`
11
+ * (branchements kernel) → méthodes métier.
12
+ *
13
+ * Un service porte DEUX noms, et c'est normal :
14
+ * `@injectable()` → nomme la CLASSE (`DevkitService`),
15
+ * c'est ce qu'on écrit dans `@inject("…")`
16
+ * `super("devkit", …)` → nomme l'INSTANCE, sa clé dans le conteneur,
17
+ * c'est ce qu'on écrit dans `kernel.get("…")`
18
+ * Les deux mènent à la MÊME instance : le conteneur les réconcilie via la classe.
19
+ * (Le décorateur ne peut pas deviner la clé — il s'exécute au chargement de la
20
+ * classe, le `super()` seulement à la construction.)
21
+ *
22
+ * ⚠️ Ne JAMAIS redéclarer `options` comme propriété : la classe `Service` parente
23
+ * l'assigne déjà via le 4ᵉ argument du `super()`. On garde une référence typée
24
+ * `cfg` pour lire la config sans se battre avec TypeScript.
25
+ *
26
+ * POUR L'UTILISER AILLEURS, deux voies, toutes deux légales :
27
+ *
28
+ * ```ts
29
+ * // 1. INJECTION par le constructeur — la dépendance est DÉCLARÉE, donc le
30
+ * // conteneur l'ordonnance et elle se voit dans la signature (nom de CLASSE).
31
+ * import { inject, injectable, Service, Module } from "nodefony";
32
+ *
33
+ * @injectable()
34
+ * class ReportService extends Service {
35
+ * constructor(
36
+ * module: Module,
37
+ * @inject("DevkitService") private devkit: DevkitService,
38
+ * ) {
39
+ * super("report", module.container, module.notificationsCenter);
40
+ * }
41
+ * }
42
+ *
43
+ * // 2. RÉSOLUTION par le conteneur, pour une dépendance tardive ou optionnelle
44
+ * // (nom d'INSTANCE).
45
+ * const devkit = this.container.get("devkit");
46
+ * ```
47
+ */
48
+ declare class DevkitService extends Service implements IDevkitService {
49
+ module: Module;
50
+ private readonly cfg;
51
+ constructor(module: Module);
52
+ /**
53
+ * Hook de démarrage d'un service : appelé UNE fois par le kernel, après la
54
+ * construction. C'est ici qu'on s'abonne aux événements du kernel — jamais
55
+ * dans le constructeur, où le kernel n'est pas encore prêt.
56
+ *
57
+ * ⚠️ Il s'appelle `init`, pas `initialize`. Le kernel ne cherche que `init`
58
+ * (`guardServiceInitialize`) : une méthode nommée `initialize` sur un service
59
+ * n'est JAMAIS appelée, et rien ne le signale — le code y dort en silence.
60
+ * (`initialize` existe bien, mais sur un CONTROLLER, où il tourne à CHAQUE
61
+ * requête : deux cycles de vie distincts, d'où deux noms.)
62
+ */
63
+ init(): Promise<this>;
64
+ /**
65
+ * Carte de visite de l'application — recalculée à CHAQUE lecture.
66
+ *
67
+ * Tout est DÉRIVÉ de l'état du Kernel : le module ne stocke rien en propre, et
68
+ * ne peut donc pas décrire une application qui n'est plus celle-là. Un cache
69
+ * mentirait au premier module ajouté ; le coût ne le justifie pas (quelques
70
+ * lectures de champs, sur une route de développement appelée à la main).
71
+ *
72
+ * `buildCard` reste PURE et reçoit cet état : c'est la frontière qui rend la
73
+ * composition de la carte éprouvable sans Kernel ni serveur.
74
+ */
75
+ getCard(): IDevkitCard;
76
+ /**
77
+ * Réglages du serveur MCP, tels que l'application les a effectivement.
78
+ *
79
+ * La porte HTTP les LIT ici plutôt que de relire la configuration de son
80
+ * côté : les défauts du schéma sont déjà fusionnés avec ce que l'app a passé
81
+ * dans `use()`, une seconde lecture finirait par diverger de celle-ci.
82
+ */
83
+ mcpSettings(): DevkitConfig["mcp"];
84
+ /**
85
+ * Ce dont les outils MCP intégrés ont besoin pour répondre.
86
+ *
87
+ * Composé ICI, et non dans la porte HTTP, parce que DEUX questions les
88
+ * réclament — « que sert-on à cet appelant ? » (la porte) et « qu'exige cette
89
+ * porte ? » ({@link DevkitService.declaredMcpScopes}, lue sans requête, au
90
+ * moment de publier le document RFC 9728). Deux compositions auraient fini
91
+ * par diverger, et c'est le document publié qui aurait eu tort.
92
+ *
93
+ * @returns broker d'administration (absent si `@nodefony/framework` n'est pas
94
+ * monté — les outils le DISENT alors, ils ne plantent pas), carte de
95
+ * visite et racine de l'APPLICATION (jamais `process.cwd()` : le
96
+ * serveur répond dans le process de l'app, dont le dossier courant
97
+ * n'est pas garanti être celui du projet).
98
+ */
99
+ mcpToolDeps(): IMcpToolDeps;
100
+ /**
101
+ * Les scopes que la porte MCP EXIGE — dérivés des outils qu'elle déclare.
102
+ *
103
+ * 🔴 **Aucune liste de configuration ne double celle-ci**, et c'est la
104
+ * correction d'un mensonge normatif : la liste écrite publiait `admin:write`
105
+ * qu'aucun outil n'exige, et taisait le scope de tout outil déclaré par un
106
+ * module — deux écarts qu'aucun contrôle ne pouvait voir, puisque rien ne
107
+ * reliait les deux. Une application qui veut voir un scope publié le pose sur
108
+ * son outil (`IMcpTool.scopes`), seul endroit où un scope a un EFFET.
109
+ *
110
+ * Vide quand la porte n'exige rien : `scopes_supported` est alors OMIS du
111
+ * document (RFC 9728 §2, champ optionnel) plutôt que publié vide.
112
+ *
113
+ * @returns les scopes dédupliqués et triés
114
+ */
115
+ declaredMcpScopes(): readonly string[];
116
+ /**
117
+ * Ce que ce module protège, à publier en RFC 9728 — la porte MCP, ou rien.
118
+ *
119
+ * ⭐ **Le document n'est plus monté ici.** Il l'était, par un controller
120
+ * dédié, et cela faisait deux implémentations d'une même règle : celle du
121
+ * pare-feu (les zones qui déclarent leur ressource) et celle-ci. Deux copies
122
+ * divergent — chacune passe ses propres tests — et elles pouvaient en plus se
123
+ * disputer un chemin, que `Router.createRoute` attribue au premier arrivé sans
124
+ * un mot. Le module DÉCLARE désormais, `@nodefony/framework` monte.
125
+ *
126
+ * 🔴 **Effet voulu du changement** : le document n'est plus servi que sur
127
+ * l'autorité de `resource`. L'ancien controller répondait sur n'importe
128
+ * laquelle — exactement le défaut corrigé sur le document d'émetteur, qu'un
129
+ * vrai client MCP avait trouvé : recevoir le document d'une autre autorité le
130
+ * fait ARRÊTER, là où un `404` l'aurait laissé continuer.
131
+ *
132
+ * Rôle éteint (aucun serveur d'autorisation déclaré) ⇒ rien : un document sans
133
+ * `authorization_servers` apprendrait au client qu'un jeton est nécessaire
134
+ * sans lui dire où l'obtenir, ce que la spécification MCP interdit.
135
+ *
136
+ * @returns une entrée pour la porte MCP, ou aucune
137
+ */
138
+ publishedProtectedResources(): readonly IProtectedResourceInput[];
139
+ status(): {
140
+ ready: boolean;
141
+ };
142
+ }
143
+ export default DevkitService;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * La composition de la carte vit désormais dans le CŒUR
3
+ * (`nodefony` → `src/cli/cardReport.ts`) ; ce fichier n'en est plus que le point
4
+ * d'entrée historique du module.
5
+ *
6
+ * Pourquoi elle a déménagé : la carte doit répondre sur une application **non
7
+ * construite** et dans un terminal **sans `NODE_ENV`** — deux situations où
8
+ * aucun module n'est chargé, celui-ci compris. Une capacité qui doit tenir sans
9
+ * installation ne peut pas dépendre d'un module : c'est la règle que `check`,
10
+ * `env` et `inspect` suivent déjà, et elle est inscrite au `CLAUDE.md` de ce
11
+ * paquet (« y déplacer une capacité qui doit marcher sans installation ou
12
+ * application cassée » est justement ce qu'il interdit).
13
+ *
14
+ * Ce qui reste ici : la porte **HTTP**, servie par `DevkitService` quand le
15
+ * Kernel tourne — elle seule connaît les modules réellement CHARGÉS. Une
16
+ * composition, deux portes, aucune divergence possible.
17
+ */
18
+ export { buildCard, renderCard } from "nodefony";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Erreur du module devkit.
3
+ *
4
+ * Deux champs qui font la différence en production :
5
+ * - `code` : identifiant MACHINE (stable, grep-able, consommé par Studio et
6
+ * l'audit) — le message, lui, peut changer sans rien casser ;
7
+ * - `context` : payload structuré joint au log (jamais de secret ici).
8
+ */
9
+ export declare class DevkitError extends Error {
10
+ readonly code: string;
11
+ readonly context?: Record<string, unknown> | undefined;
12
+ constructor(message: string, code?: string, context?: Record<string, unknown> | undefined);
13
+ }
14
+ export default DevkitError;
@@ -0,0 +1,66 @@
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;
26
+ /** Adresse distante de la connexion (`socket.remoteAddress`). */
27
+ remoteAddress?: string;
28
+ }
29
+ /** Réglages qui gouvernent les gardes (viennent de la config du module). */
30
+ export interface IGuardPolicy {
31
+ /** Origines de navigateur admises ; vide = aucune. */
32
+ allowedOrigins: readonly string[];
33
+ /** Accepter une adresse distante non locale. */
34
+ allowRemote: boolean;
35
+ }
36
+ /**
37
+ * Une adresse est-elle celle de la machine locale ?
38
+ *
39
+ * Couvre les trois formes que Node rend selon la pile réseau : IPv4
40
+ * (`127.x.x.x`), IPv6 (`::1`), et l'IPv4 encapsulée en IPv6
41
+ * (`::ffff:127.0.0.1`) — la plus fréquente sur un serveur à double pile, et
42
+ * celle qu'une comparaison naïve à `"127.0.0.1"` rate.
43
+ *
44
+ * @param address - `socket.remoteAddress`, éventuellement absent
45
+ * @returns `true` si l'appel vient de cette machine
46
+ */
47
+ export declare function isLocalAddress(address?: string): boolean;
48
+ /**
49
+ * Décide si un appel MCP peut être servi.
50
+ *
51
+ * ⭐ **La règle sur `Origin` est contre-intuitive, et c'est elle qui protège.**
52
+ * Un client MCP légitime est un *process* (Cursor, Claude Code, un agent tiers)
53
+ * : il n'envoie **aucun** `Origin`, cet en-tête étant posé par les navigateurs.
54
+ * Une page web malveillante, elle, en pose **toujours** un lorsqu'elle vise
55
+ * `https://localhost:5152`. Donc : *absent* → on passe ; *présent et hors
56
+ * allowlist* → `403`. C'est ce qui referme le DNS rebinding, le seul vecteur
57
+ * réel contre un serveur MCP local.
58
+ *
59
+ * L'ordre des contrôles est délibéré : la localité d'abord, parce qu'un appel
60
+ * distant ne doit même pas apprendre quelles origines sont admises.
61
+ *
62
+ * @param input - ce que la requête présente
63
+ * @param policy - les réglages du module
64
+ * @returns le verdict, avec le motif quand il refuse
65
+ */
66
+ export declare function checkMcpAccess(input: IGuardInput, policy: IGuardPolicy): GuardVerdict;
@@ -0,0 +1,139 @@
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
+ /** Révision du protocole que ce serveur annonce. */
13
+ export declare const MCP_PROTOCOL_VERSION = "2026-07-28";
14
+ /**
15
+ * Versions que ce serveur sait servir — publiées par `server/discover` et
16
+ * listées dans l'erreur `UnsupportedProtocolVersion`.
17
+ *
18
+ * Une seule pour l'instant, et c'est délibéré : annoncer une révision qu'on
19
+ * n'a pas éprouvée reviendrait à promettre une sémantique qu'on ne tient pas.
20
+ */
21
+ export declare const MCP_SUPPORTED_VERSIONS: readonly ["2026-07-28"];
22
+ /**
23
+ * Clé de métadonnée par laquelle un client MODERNE déclare sa révision.
24
+ *
25
+ * ⭐ **C'est la différence d'ÈRE, et elle commande tout le reste.** Jusqu'à
26
+ * `2025-11-25` (ère « legacy »), un client ouvrait une session par un handshake
27
+ * `initialize`. Depuis `2026-07-28` (ère « modern »), il n'y a plus de session :
28
+ * chaque requête porte elle-même sa version et les capacités du client, dans
29
+ * `params._meta`. Un serveur qui n'écouterait que `initialize` serait un
30
+ * serveur *legacy* — quelle que soit la version qu'il prétend annoncer.
31
+ */
32
+ export declare const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
33
+ /** Clé de métadonnée portant l'identité du serveur dans `server/discover`. */
34
+ export declare const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
35
+ /**
36
+ * Chemin de l'endpoint MCP.
37
+ *
38
+ * ## Pourquoi `/nodefony/mcp`, et pas `/nodefony/devkit/api/mcp`
39
+ *
40
+ * Cette URL est un **contrat public** : elle est écrite dans le `.mcp.json` de
41
+ * chaque utilisateur. Y faire figurer le module qui l'implémente la rendrait
42
+ * caduque au premier déménagement — or ce serveur a vocation à bouger le jour
43
+ * où une application voudra s'exposer en production (le devkit, lui, est
44
+ * `policy: "dev"`). Le nom d'un module est un détail d'implémentation ; une URL
45
+ * ne l'est pas.
46
+ *
47
+ * Le segment `api` est écarté pour une autre raison : il désigne le plan
48
+ * d'administration JSON de Studio, avec son contrôle d'accès par rôle. Le MCP
49
+ * n'est ni REST ni destiné à Studio — ranger deux protocoles sous le même
50
+ * segment promettrait une parenté qui n'existe pas.
51
+ *
52
+ * Et `/mcp` à la racine, qui est la convention de fait ailleurs, prendrait un
53
+ * chemin qui **appartient à l'application** : `/nodefony` est le préfixe
54
+ * réservé du framework, donc sans collision possible.
55
+ *
56
+ * Constante et **non configurable** : une route décorée est statique, et un
57
+ * réglage qui n'agirait pas serait pire qu'aucun réglage.
58
+ */
59
+ export declare const MCP_ENDPOINT_PATH = "/nodefony/mcp";
60
+ /** Codes d'erreur JSON-RPC 2.0 employés par ce serveur. */
61
+ export declare const JsonRpcError: {
62
+ /** Corps illisible. */
63
+ readonly PARSE_ERROR: -32700;
64
+ /** Message qui n'est pas une requête JSON-RPC valide. */
65
+ readonly INVALID_REQUEST: -32600;
66
+ /** Méthode inconnue — la spec exige alors un `404` HTTP. */
67
+ readonly METHOD_NOT_FOUND: -32601;
68
+ /** Paramètres absents ou mal typés. */
69
+ readonly INVALID_PARAMS: -32602;
70
+ /** Échec côté serveur. */
71
+ readonly INTERNAL_ERROR: -32603;
72
+ };
73
+ /**
74
+ * Codes réservés par la spec MCP, hors plage JSON-RPC standard.
75
+ *
76
+ * Ils ne sont pas décoratifs : un client s'en sert pour se rattraper seul —
77
+ * renégocier une version sur `-32022`, relire `tools/list` puis réessayer sur
78
+ * `-32020`. Rendre un `-32600` générique à leur place le priverait de cette
79
+ * reprise et transformerait un désaccord réparable en échec définitif.
80
+ */
81
+ export declare const McpProtocolError: {
82
+ /**
83
+ * Les en-têtes HTTP contredisent le corps, ou un en-tête requis manque.
84
+ * La spec impose `400` **et** ce code (`streamable-http` §Server Validation).
85
+ */
86
+ readonly HEADER_MISMATCH: -32020;
87
+ /**
88
+ * La révision demandée n'est pas servie. La réponse **doit** lister celles
89
+ * qu'on sert, sans quoi le client n'a rien pour choisir.
90
+ */
91
+ readonly UNSUPPORTED_PROTOCOL_VERSION: -32022;
92
+ };
93
+ /** Identifiant d'une requête JSON-RPC (jamais `null` pour une requête). */
94
+ export type JsonRpcId = string | number;
95
+ /** Message entrant : requête (avec `id`) ou notification (sans `id`). */
96
+ export interface IJsonRpcMessage {
97
+ jsonrpc?: unknown;
98
+ id?: unknown;
99
+ method?: unknown;
100
+ params?: unknown;
101
+ }
102
+ /** Réponse JSON-RPC de succès. */
103
+ export interface IJsonRpcSuccess {
104
+ jsonrpc: "2.0";
105
+ id: JsonRpcId;
106
+ result: unknown;
107
+ }
108
+ /** Réponse JSON-RPC d'erreur. */
109
+ export interface IJsonRpcFailure {
110
+ jsonrpc: "2.0";
111
+ /** `null` quand l'erreur survient avant d'avoir pu lire un `id`. */
112
+ id: JsonRpcId | null;
113
+ error: {
114
+ code: number;
115
+ message: string;
116
+ data?: unknown;
117
+ };
118
+ }
119
+ /** Ce que le serveur MCP rend, avant traduction en réponse HTTP. */
120
+ export interface IMcpHttpReply {
121
+ /** Statut HTTP à poser. */
122
+ status: number;
123
+ /**
124
+ * Corps JSON, ou `null` pour un `202 Accepted` sans corps — la spec l'exige
125
+ * pour une notification acceptée.
126
+ */
127
+ body: IJsonRpcSuccess | IJsonRpcFailure | null;
128
+ }
129
+ /** Fabrique une réponse de succès. */
130
+ export declare function jsonRpcSuccess(id: JsonRpcId, result: unknown): IJsonRpcSuccess;
131
+ /** Fabrique une réponse d'erreur. */
132
+ export declare function jsonRpcFailure(id: JsonRpcId | null, code: number, message: string, data?: unknown): IJsonRpcFailure;
133
+ /**
134
+ * Un message est-il une NOTIFICATION (pas d'`id`) plutôt qu'une requête ?
135
+ *
136
+ * La distinction commande le statut HTTP : une notification acceptée rend
137
+ * `202` **sans corps**, une requête rend son objet JSON.
138
+ */
139
+ export declare function isNotification(message: IJsonRpcMessage): boolean;
@@ -0,0 +1,48 @@
1
+ import { type IJsonRpcMessage, type IMcpHttpReply } from "./protocol.js";
2
+ import { type IMcpToolDeps } from "./tools.js";
3
+ /**
4
+ * Cœur du serveur MCP : un message JSON-RPC entre, une réponse HTTP sort.
5
+ *
6
+ * **Fonction pure** — elle ne touche ni au socket, ni au conteneur, ni à
7
+ * l'horloge. C'est ce qui permet d'éprouver le protocole entier (statuts
8
+ * compris) sans démarrer de serveur, et c'est aussi ce qui rend le transport
9
+ * interchangeable : le jour où un transport `stdio` serait nécessaire pour
10
+ * répondre application éteinte, il appellerait cette même fonction.
11
+ *
12
+ * ## Ce que la révision 2026-07-28 change, et pourquoi c'est ce qui rend cette
13
+ * porte viable
14
+ *
15
+ * Les sessions de niveau protocole ont disparu. Rien n'est retenu entre deux
16
+ * appels : chaque `POST` porte tout ce qu'il faut pour être servi. Un
17
+ * redémarrage du serveur de développement — celui que le superviseur déclenche
18
+ * à chaque fichier sauvegardé — ne casse donc aucun état, et la réponse
19
+ * suivante vient du code qui vient d'être rechargé. Aucun cache à invalider,
20
+ * jamais : la fraîcheur est une propriété du protocole, pas une discipline.
21
+ */
22
+ /** Ce que ce serveur sait faire, annoncé à l'initialisation. */
23
+ interface IServerInfo {
24
+ name: string;
25
+ version: string;
26
+ }
27
+ /** Tout ce dont le traitement d'un message a besoin. */
28
+ export interface IMcpServerContext extends IMcpToolDeps {
29
+ /** Allowlist des outils (`devkit.mcp.tools`). */
30
+ tools: readonly string[];
31
+ /** Identité annoncée au client. */
32
+ serverInfo: IServerInfo;
33
+ }
34
+ /** En-têtes HTTP dont le protocole se sert. */
35
+ export interface IMcpHeaders {
36
+ /** `MCP-Protocol-Version`, absent chez un client de l'ère legacy. */
37
+ protocolVersion?: string;
38
+ }
39
+ /**
40
+ * Traite UN message JSON-RPC.
41
+ *
42
+ * @param message - corps du `POST`, déjà parsé
43
+ * @param context - outils autorisés et briques qui répondent
44
+ * @param headers - en-têtes du transport (`MCP-Protocol-Version`)
45
+ * @returns statut HTTP et corps à écrire (corps `null` = `202` sans contenu)
46
+ */
47
+ export declare function handleMcpMessage(message: IJsonRpcMessage, context: IMcpServerContext, headers?: IMcpHeaders): Promise<IMcpHttpReply>;
48
+ export {};
@@ -0,0 +1,81 @@
1
+ import { type IAdminBrokerLike } from "nodefony";
2
+ /**
3
+ * Catalogue des outils MCP, et leur exécution.
4
+ *
5
+ * ⚠️ **Rien n'est calculé ici.** Chaque outil traduit un appel JSON-RPC vers une
6
+ * brique qui répond DÉJÀ à une autre porte : `inspect` lit le plan
7
+ * d'administration par `readAdminSubject` (la même fonction que la commande
8
+ * `nodefony inspect`), `card` appelle le service du module (la même que la route
9
+ * HTTP). Une source, plusieurs portes — un outil qui recalculerait sa réponse
10
+ * finirait par contredire la commande, et c'est lui qu'on croirait sur parole.
11
+ *
12
+ * ⭐ **La description d'un outil est le premier critère de son déclenchement.**
13
+ * Un modèle n'appelle pas ce qu'il ne comprend pas : le POC de 2026-05 l'a payé
14
+ * cash — un outil à description neutre n'a jamais été appelé, un skill
15
+ * auto-déclenché prenait la main à chaque fois. Ces descriptions disent donc ce
16
+ * que l'outil rend ET quand s'en servir, pas seulement son nom.
17
+ */
18
+ /** Ce dont les outils ont besoin pour répondre — injecté, jamais lu ici. */
19
+ export interface IMcpToolDeps {
20
+ /** Le service `adminBroker` du conteneur, ou `undefined` s'il manque. */
21
+ broker: IAdminBrokerLike | undefined;
22
+ /** Compose la carte de visite de l'application. */
23
+ getCard: () => unknown;
24
+ /**
25
+ * Racine depuis laquelle lire le disque (graphe symbolique, diagnostic).
26
+ *
27
+ * Injectée plutôt que lue par `process.cwd()` : le serveur répond dans le
28
+ * process de l'application, dont le dossier courant n'est pas garanti être
29
+ * celui du projet — et un outil qui diagnostiquerait le mauvais dossier
30
+ * conclurait « rien à signaler » avec aplomb.
31
+ */
32
+ projectRoot: string;
33
+ }
34
+ /** Un outil tel que `tools/list` le publie. */
35
+ export interface IMcpToolDefinition {
36
+ name: string;
37
+ description: string;
38
+ inputSchema: Record<string, unknown>;
39
+ }
40
+ /** Ce que rend `tools/call` — du contenu, et l'aveu d'un échec métier. */
41
+ export interface IMcpToolResult {
42
+ content: {
43
+ type: "text";
44
+ text: string;
45
+ }[];
46
+ isError?: boolean;
47
+ }
48
+ /**
49
+ * Outils publiés, filtrés par l'allowlist de configuration.
50
+ *
51
+ * Une clé inconnue dans la configuration est simplement ignorée : elle ne peut
52
+ * rien ouvrir. C'est le sens d'une allowlist — ce qui n'est pas nommé ICI
53
+ * n'existe pas, et la faute de frappe d'un utilisateur ne peut pas activer
54
+ * autre chose que ce qu'il voulait.
55
+ *
56
+ * ⚠️ **`Object.hasOwn` n'est pas une précaution de style.** Sans lui,
57
+ * `tools: ["toString"]` résolvait une méthode héritée d'`Object.prototype` :
58
+ * la valeur n'étant pas `undefined`, elle franchissait le filtre et un outil
59
+ * fantôme entrait dans le catalogue publié. Trouvé par le test, pas à la
60
+ * relecture.
61
+ *
62
+ * @param enabled - clés d'outils autorisées (`devkit.mcp.tools`)
63
+ */
64
+ export declare function listMcpTools(enabled: readonly string[]): IMcpToolDefinition[];
65
+ /**
66
+ * Exécute un outil par son nom public.
67
+ *
68
+ * Un échec métier (sujet inconnu, module absent) rend un résultat `isError`,
69
+ * **pas** une erreur JSON-RPC : le protocole réserve celle-ci aux fautes de
70
+ * protocole. La distinction compte pour l'agent — une erreur de protocole
71
+ * signifie « tu t'y prends mal », un `isError` signifie « ta demande est
72
+ * recevable, voici pourquoi elle n'aboutit pas » ; c'est la seconde qu'il peut
73
+ * corriger seul.
74
+ *
75
+ * @param name - nom public de l'outil (`nodefony_inspect`…)
76
+ * @param args - arguments fournis par l'agent
77
+ * @param enabled - allowlist de configuration
78
+ * @param deps - briques qui répondent réellement
79
+ * @returns le résultat de l'outil, ou `null` si le nom n'est pas exposé
80
+ */
81
+ export declare function callMcpTool(name: string, args: Record<string, unknown>, enabled: readonly string[], deps: IMcpToolDeps): Promise<IMcpToolResult | null>;